- 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>
187 lines
5.2 KiB
Vue
187 lines
5.2 KiB
Vue
<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>
|