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:
parent
104b7678d3
commit
1ee3ed28a5
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
Loading…
x
Reference in New Issue
Block a user