From 35892486e8912e10b196ce63d5a3be33d8b50866 Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 10:17:09 +0800 Subject: [PATCH] feat: unified dialog manager (showDialog service + ty-dialog host) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 showDialog({component, props, title,...}) 服务,返回 Promise - 内容组件 defineExpose({submit}) 校验并返回数据,校验失败对话框不关闭 - 宿主挂 App.vue,统一渲染对话框栈(支持嵌套) - ty-crud 改用 showDialog 打开表单(移除 ty-form-dialog) - ty-form 增加 submit();用户管理页重置密码抽成独立组件 + showDialog Co-Authored-By: Claude Opus 4.8 --- components.d.ts | 4 +- src/App.vue | 1 + src/components/ty-crud.vue | 72 ++++++++----------- src/components/ty-dialog.vue | 61 ++++++++++++++++ src/components/ty-form-dialog.vue | 65 ----------------- src/components/ty-form.vue | 6 ++ src/pages/system/user/index.vue | 46 ++++-------- src/pages/system/user/reset-password-form.vue | 27 +++++++ src/utils/dialog.ts | 50 +++++++++++++ stats.html | 2 +- typed-router.d.ts | 13 ++++ 11 files changed, 201 insertions(+), 146 deletions(-) create mode 100644 src/components/ty-dialog.vue delete mode 100644 src/components/ty-form-dialog.vue create mode 100644 src/pages/system/user/reset-password-form.vue create mode 100644 src/utils/dialog.ts diff --git a/components.d.ts b/components.d.ts index 820e77e..33b3ec1 100644 --- a/components.d.ts +++ b/components.d.ts @@ -46,9 +46,9 @@ declare module 'vue' { RouterView: typeof import('vue-router')['RouterView'] TyBreadcrumb: typeof import('./src/components/ty-breadcrumb.vue')['default'] TyCrud: typeof import('./src/components/ty-crud.vue')['default'] + TyDialog: typeof import('./src/components/ty-dialog.vue')['default'] TyFooter: typeof import('./src/components/ty-footer.vue')['default'] TyForm: typeof import('./src/components/ty-form.vue')['default'] - TyFormDialog: typeof import('./src/components/ty-form-dialog.vue')['default'] TyHeader: typeof import('./src/components/ty-header.vue')['default'] TyMixbar: typeof import('./src/components/ty-mixbar.vue')['default'] TySearchForm: typeof import('./src/components/ty-search-form.vue')['default'] @@ -99,9 +99,9 @@ declare global { const RouterView: typeof import('vue-router')['RouterView'] const TyBreadcrumb: typeof import('./src/components/ty-breadcrumb.vue')['default'] const TyCrud: typeof import('./src/components/ty-crud.vue')['default'] + const TyDialog: typeof import('./src/components/ty-dialog.vue')['default'] const TyFooter: typeof import('./src/components/ty-footer.vue')['default'] const TyForm: typeof import('./src/components/ty-form.vue')['default'] - const TyFormDialog: typeof import('./src/components/ty-form-dialog.vue')['default'] const TyHeader: typeof import('./src/components/ty-header.vue')['default'] const TyMixbar: typeof import('./src/components/ty-mixbar.vue')['default'] const TySearchForm: typeof import('./src/components/ty-search-form.vue')['default'] diff --git a/src/App.vue b/src/App.vue index 02c5506..4383df7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,7 @@ diff --git a/src/components/ty-crud.vue b/src/components/ty-crud.vue index 9fbd663..66d5a04 100644 --- a/src/components/ty-crud.vue +++ b/src/components/ty-crud.vue @@ -63,17 +63,6 @@ - - @@ -88,6 +77,8 @@ import type { PageResult, TableColumn, } from '@/types/crud' +import TyForm from '@/components/ty-form.vue' +import { showDialog } from '@/utils/dialog' defineOptions({ name: 'TyCrud' }) @@ -144,12 +135,6 @@ const selectedRows = ref[]>([]) const query = reactive>(buildModel(props.searchSchema)) -const dialogVisible = ref(false) -const dialogMode = ref<'create' | 'edit'>('create') -const editingId = ref(null) -const submitting = ref(false) -const form = reactive>({}) - function buildModel(schema?: JsonSchema): Record { const model: Record = {} if (!schema) return model @@ -183,39 +168,38 @@ function handleReset(): void { fetchList() } -function openCreate(): void { - dialogMode.value = 'create' - editingId.value = null - Object.assign(form, buildModel(props.formSchema)) - dialogVisible.value = true +async function openCreate(): Promise { + const model = buildModel(props.formSchema) + const data = await showDialog({ + component: TyForm, + props: { schema: props.formSchema, modelValue: model, mode: 'create' }, + title: `新增${props.entityName}`, + }) + if (data == null) return + await props.create(data) + ElMessage.success('新增成功') + fetchList() } -function openEdit(row: Record): void { - dialogMode.value = 'edit' - editingId.value = String(row[props.idKey]) +async function openEdit(row: Record): Promise { 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 { - 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 - } + const data = await showDialog({ + component: TyForm, + props: { + schema: props.formSchema, + modelValue: model, + mode: 'edit', + readonlyKeys: props.editReadonlyKeys, + }, + title: `编辑${props.entityName}`, + }) + if (data == null) return + await props.update(String(row[props.idKey]), data) + ElMessage.success('更新成功') + fetchList() } async function handleDelete(row: Record): Promise { diff --git a/src/components/ty-dialog.vue b/src/components/ty-dialog.vue new file mode 100644 index 0000000..1cb432f --- /dev/null +++ b/src/components/ty-dialog.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/components/ty-form-dialog.vue b/src/components/ty-form-dialog.vue deleted file mode 100644 index 77a97a3..0000000 --- a/src/components/ty-form-dialog.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/src/components/ty-form.vue b/src/components/ty-form.vue index 0addad4..8ce5b34 100644 --- a/src/components/ty-form.vue +++ b/src/components/ty-form.vue @@ -147,7 +147,13 @@ function widgetStyle(prop: JsonSchemaProperty): Record { return { width: prop.width ?? (props.inline ? '200px' : '100%') } } +async function submit(): Promise> { + await formRef.value?.validate() + return { ...props.modelValue } +} + defineExpose({ + submit, validate: () => formRef.value?.validate(), resetFields: () => formRef.value?.resetFields(), clearValidate: () => formRef.value?.clearValidate(), diff --git a/src/pages/system/user/index.vue b/src/pages/system/user/index.vue index 446d157..ca881ad 100644 --- a/src/pages/system/user/index.vue +++ b/src/pages/system/user/index.vue @@ -25,32 +25,14 @@ - - - - {{ resetPwd.username }} - - - - - - diff --git a/src/utils/dialog.ts b/src/utils/dialog.ts new file mode 100644 index 0000000..6b7c5e5 --- /dev/null +++ b/src/utils/dialog.ts @@ -0,0 +1,50 @@ +import { markRaw, reactive, type Component } from 'vue' + +export interface DialogOptions { + /** 对话框内容组件。 */ + component: Component + title?: string + width?: string + /** 传给内容组件的 props。 */ + props?: Record + confirmText?: string + cancelText?: string + /** 是否显示默认 取消/确定 底部(false 时内容组件自带底部)。 */ + showFooter?: boolean + /** 确认回调,参数为内容组件 submit() 返回的数据。 */ + onConfirm?: (data: any) => void | Promise +} + +export interface DialogInstance { + id: number + options: DialogOptions + resolve: (value: any) => void +} + +/** 当前打开的对话框栈(由 宿主渲染)。 */ +export const dialogs = reactive([]) + +let seq = 0 + +/** + * 打开一个对话框。返回 Promise:确认时 resolve 内容组件 submit() 返回的数据, + * 取消/关闭时 resolve null。 + */ +export function showDialog(options: DialogOptions): Promise { + return new Promise((resolve) => { + dialogs.push({ + id: ++seq, + options: { ...options, component: markRaw(options.component) }, + resolve, + }) + }) +} + +/** 关闭指定对话框并 resolve。 */ +export function closeDialog(id: number, result?: any): void { + const idx = dialogs.findIndex((d) => d.id === id) + if (idx >= 0) { + const [dialog] = dialogs.splice(idx, 1) + dialog.resolve(result) + } +} diff --git a/stats.html b/stats.html index 37fb7b8..9591460 100644 --- a/stats.html +++ b/stats.html @@ -4930,7 +4930,7 @@ var drawChart = (function (exports) {