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" :actions-width="actionsWidth"
> >
<template #actions="{ row }"> <template #actions="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button> <el-button
<el-button link type="danger" @click="handleDelete(row)">删除</el-button> v-for="a in displayActions(row)"
<slot name="row-actions" :row="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> </template>
</ty-table> </ty-table>
</el-card> </el-card>
@ -41,9 +60,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' 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' }) defineOptions({ name: 'TyCrud' })
@ -71,6 +90,10 @@ const props = withDefaults(
nameKey?: string nameKey?: string
/** 编辑模式下强制只读的字段。 */ /** 编辑模式下强制只读的字段。 */
editReadonlyKeys?: string[] editReadonlyKeys?: string[]
/** 操作列动作(内置 'edit'/'delete' 或自定义动作)。 */
actions?: CrudAction[]
/** 操作列按钮超过该数量时折叠为「更多」。 */
collapseCount?: number
actionsWidth?: number | string actionsWidth?: number | string
}>(), }>(),
{ {
@ -78,6 +101,8 @@ const props = withDefaults(
idKey: 'id', idKey: 'id',
nameKey: 'name', nameKey: 'name',
editReadonlyKeys: () => [], editReadonlyKeys: () => [],
actions: () => ['edit', 'delete'],
collapseCount: 2,
actionsWidth: 220, actionsWidth: 220,
}, },
) )
@ -172,6 +197,32 @@ async function handleDelete(row: Record<string, any>): Promise<void> {
fetchList() 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) onMounted(fetchList)
</script> </script>

View File

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

View File

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

View File

@ -24,6 +24,8 @@ export interface JsonSchemaProperty {
minimum?: number minimum?: number
maximum?: number maximum?: number
placeholder?: string placeholder?: string
/** 控件宽度(如 '160px');缺省时搜索行内默认 200px、弹窗表单默认 100%。 */
width?: string
} }
/** JSON Schema 对象(表单 schema)。 */ /** JSON Schema 对象(表单 schema)。 */
@ -64,3 +66,15 @@ export interface PageQuery {
pageSize: number pageSize: number
[key: string]: unknown [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