ec-admin/src/components/ty-crud.vue
liuyun 104b7678d3 feat: rename ty-form, add search form collapse, use UnoCSS @apply
- ty-schema-form 改名 ty-form
- ty-search-form 支持折叠/展开(collapseCount 默认 2,查询/重置置右)
- ty-table/ty-crud/ty-search-form 样式改用 @apply 工具类

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:27:55 +08:00

187 lines
5.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="ty-crud">
<el-card v-if="searchSchema" class="ty-crud__search" shadow="never">
<ty-search-form :schema="searchSchema" :model-value="query" @search="handleSearch" @reset="handleReset" />
</el-card>
<el-card shadow="never">
<div class="ty-crud__toolbar">
<el-button type="primary" @click="openCreate">新增{{ entityName }}</el-button>
<slot name="toolbar" />
</div>
<ty-table
:columns="columns"
:data="list"
:loading="loading"
:total="total"
v-model:page="page"
v-model:page-size="pageSize"
: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" />
</template>
</ty-table>
</el-card>
<ty-form-dialog
v-model:visible="dialogVisible"
:title="dialogMode === 'edit' ? `编辑${entityName}` : `新增${entityName}`"
:schema="formSchema"
:model-value="form"
:mode="dialogMode"
:readonly-keys="dialogMode === 'edit' ? editReadonlyKeys : []"
:submitting="submitting"
@submit="handleSubmit"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
defineOptions({ name: 'TyCrud' })
const props = withDefaults(
defineProps<{
/** 实体名(用于标题与删除确认,如「用户」)。 */
entityName: string
/** 表格列配置。 */
columns: TableColumn[]
/** 表单 JSON Schema。 */
formSchema: JsonSchema
/** 搜索 JSON Schema(可选)。 */
searchSchema?: JsonSchema
/** 数据加载:分页查询。 */
list: (query: PageQuery) => Promise<PageResult<Record<string, any>>>
/** 新增。 */
create: (data: Record<string, any>) => Promise<unknown>
/** 更新。 */
update: (id: string, data: Record<string, any>) => Promise<unknown>
/** 删除。 */
remove: (id: string) => Promise<unknown>
/** 主键字段名。 */
idKey?: string
/** 删除确认展示的字段名。 */
nameKey?: string
/** 编辑模式下强制只读的字段。 */
editReadonlyKeys?: string[]
actionsWidth?: number | string
}>(),
{
searchSchema: undefined,
idKey: 'id',
nameKey: 'name',
editReadonlyKeys: () => [],
actionsWidth: 220,
},
)
const loading = ref(false)
const list = ref<Record<string, any>[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const query = reactive<Record<string, any>>(buildModel(props.searchSchema))
const dialogVisible = ref(false)
const dialogMode = ref<'create' | 'edit'>('create')
const editingId = ref<string | null>(null)
const submitting = ref(false)
const form = reactive<Record<string, any>>({})
function buildModel(schema?: JsonSchema): Record<string, any> {
const model: Record<string, any> = {}
if (!schema) return model
for (const [key, prop] of Object.entries(schema.properties)) {
if (prop.default !== undefined) model[key] = prop.default
else if (prop.enum?.length) model[key] = prop.enum[0]
else if (prop.type === 'boolean') model[key] = false
else model[key] = ''
}
return model
}
async function fetchList(): Promise<void> {
loading.value = true
try {
const result = await props.list({ page: page.value, pageSize: pageSize.value, ...query })
list.value = result.list
total.value = result.total
} finally {
loading.value = false
}
}
function handleSearch(): void {
page.value = 1
fetchList()
}
function handleReset(): void {
page.value = 1
fetchList()
}
function openCreate(): void {
dialogMode.value = 'create'
editingId.value = null
Object.assign(form, buildModel(props.formSchema))
dialogVisible.value = true
}
function openEdit(row: Record<string, any>): void {
dialogMode.value = 'edit'
editingId.value = String(row[props.idKey])
const model = buildModel(props.formSchema)
for (const key of Object.keys(props.formSchema.properties)) {
model[key] = row[key] ?? model[key]
}
Object.assign(form, model)
dialogVisible.value = true
}
async function handleSubmit(): Promise<void> {
submitting.value = true
try {
if (dialogMode.value === 'edit') {
await props.update(editingId.value!, { ...form })
ElMessage.success('更新成功')
} else {
await props.create({ ...form })
ElMessage.success('新增成功')
}
dialogVisible.value = false
fetchList()
} finally {
submitting.value = false
}
}
async function handleDelete(row: Record<string, any>): Promise<void> {
const name = row[props.nameKey] ?? ''
await ElMessageBox.confirm(`确认删除${props.entityName}「${name}」?`, '提示', { type: 'warning' })
await props.remove(String(row[props.idKey]))
ElMessage.success('删除成功')
fetchList()
}
onMounted(fetchList)
</script>
<style scoped lang="scss">
.ty-crud__search {
@apply mb-md;
}
.ty-crud__toolbar {
@apply mb-md;
}
</style>