feat: search field width + collapse actions into dropdown

- ty-form 字段支持 width(行内默认 200px、弹窗默认 100%)
- ty-search-form 展开为多行高级搜索
- ty-crud 操作列数据驱动(actions 内置/自定义),超过 collapseCount 折叠为「更多」下拉

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 09:34:39 +08:00
parent 104b7678d3
commit 1ee3ed28a5
5 changed files with 89 additions and 13 deletions

View File

@ -20,9 +20,28 @@
:actions-width="actionsWidth"
>
<template #actions="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
<slot name="row-actions" :row="row" />
<el-button
v-for="a in displayActions(row)"
:key="a.label"
link
:type="a.type"
@click="a.onClick(row)"
>
{{ a.label }}
</el-button>
<el-dropdown v-if="overflowActions(row).length > 0" trigger="click" @command="handleCommand($event, row)">
<el-button link type="primary">
更多
<icon type="iconify" icon="mdi:chevron-down" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="a in overflowActions(row)" :key="a.label" :command="a">
{{ a.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</ty-table>
</el-card>
@ -41,9 +60,9 @@
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
import type { CrudAction, CrudActionItem, JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
defineOptions({ name: 'TyCrud' })
@ -71,6 +90,10 @@ const props = withDefaults(
nameKey?: string
/** 编辑模式下强制只读的字段。 */
editReadonlyKeys?: string[]
/** 操作列动作(内置 'edit'/'delete' 或自定义动作)。 */
actions?: CrudAction[]
/** 操作列按钮超过该数量时折叠为「更多」。 */
collapseCount?: number
actionsWidth?: number | string
}>(),
{
@ -78,6 +101,8 @@ const props = withDefaults(
idKey: 'id',
nameKey: 'name',
editReadonlyKeys: () => [],
actions: () => ['edit', 'delete'],
collapseCount: 2,
actionsWidth: 220,
},
)
@ -172,6 +197,32 @@ async function handleDelete(row: Record<string, any>): Promise<void> {
fetchList()
}
const resolvedActions = computed<CrudActionItem[]>(() =>
props.actions.map((a): CrudActionItem => {
if (a === 'edit') return { label: '编辑', type: 'primary', onClick: (row) => openEdit(row) }
if (a === 'delete') return { label: '删除', type: 'danger', onClick: (row) => handleDelete(row) }
return a
}),
)
function visibleActions(row: Record<string, any>): CrudActionItem[] {
return resolvedActions.value.filter((a) => !a.show || a.show(row))
}
function displayActions(row: Record<string, any>): CrudActionItem[] {
const visible = visibleActions(row)
return visible.length > props.collapseCount ? visible.slice(0, 1) : visible
}
function overflowActions(row: Record<string, any>): CrudActionItem[] {
const visible = visibleActions(row)
return visible.length > props.collapseCount ? visible.slice(1) : []
}
function handleCommand(action: CrudActionItem, row: Record<string, any>): void {
action.onClick(row)
}
onMounted(fetchList)
</script>

View File

@ -12,7 +12,7 @@
:placeholder="prop.placeholder ?? '请选择'"
:clearable="!isRequired(key)"
:disabled="isReadonly(key, prop)"
style="width: 100%"
:style="widgetStyle(prop)"
>
<el-option
v-for="(v, i) in prop.enum"
@ -30,6 +30,7 @@
:min="prop.minimum"
:max="prop.maximum"
:disabled="isReadonly(key, prop)"
:style="widgetStyle(prop)"
/>
<el-input
@ -39,6 +40,7 @@
show-password
:placeholder="prop.placeholder"
:disabled="isReadonly(key, prop)"
:style="widgetStyle(prop)"
/>
<el-input
@ -47,6 +49,7 @@
type="textarea"
:placeholder="prop.placeholder"
:disabled="isReadonly(key, prop)"
:style="widgetStyle(prop)"
/>
<el-input
@ -54,6 +57,7 @@
v-model="modelValue[key]"
:placeholder="prop.placeholder"
:disabled="isReadonly(key, prop)"
:style="widgetStyle(prop)"
/>
</el-form-item>
@ -127,6 +131,10 @@ function isReadonly(key: string, prop: { readOnly?: boolean }): boolean {
return prop.readOnly === true || props.readonlyKeys.includes(key)
}
function widgetStyle(prop: JsonSchemaProperty): Record<string, string> {
return { width: prop.width ?? (props.inline ? '200px' : '100%') }
}
defineExpose({
validate: () => formRef.value?.validate(),
resetFields: () => formRef.value?.resetFields(),

View File

@ -8,14 +8,11 @@
:create="create"
:update="update"
:remove="remove"
:actions="actions"
id-key="id"
name-key="username"
:edit-readonly-keys="['username']"
>
<template #row-actions="{ row }">
<el-button link type="warning" @click="openResetPwd(row)">重置密码</el-button>
</template>
</ty-crud>
/>
<el-dialog v-model="resetPwd.visible" title="重置密码" width="420px" destroy-on-close>
<el-form label-width="80px">
@ -34,7 +31,7 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { ElMessage } from 'element-plus'
import type { JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
import type { CrudAction, JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
const STATUS_MAP = {
enabled: { label: '启用', type: 'success' },
@ -110,6 +107,12 @@ const formSchema: JsonSchema = {
required: ['username', 'password'],
}
const actions: CrudAction[] = [
'edit',
'delete',
{ label: '重置密码', type: 'warning', onClick: (row) => openResetPwd(row) },
]
// ---- 占位数据(后续对接后端时替换为 http 调用)----
interface UserRow {
id: string

View File

@ -24,6 +24,8 @@ export interface JsonSchemaProperty {
minimum?: number
maximum?: number
placeholder?: string
/** 控件宽度(如 '160px');缺省时搜索行内默认 200px、弹窗表单默认 100%。 */
width?: string
}
/** JSON Schema 对象(表单 schema)。 */
@ -64,3 +66,15 @@ export interface PageQuery {
pageSize: number
[key: string]: unknown
}
/** 操作列自定义动作。 */
export interface CrudActionItem {
label: string
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
onClick: (row: Record<string, any>) => void | Promise<void>
/** 是否对当前行显示(缺省显示)。 */
show?: (row: Record<string, any>) => boolean
}
/** 操作列动作:内置 'edit'/'delete' 或自定义动作。 */
export type CrudAction = 'edit' | 'delete' | CrudActionItem

File diff suppressed because one or more lines are too long