From 04f714f4b69c4d1faa6a84f5d18e0b4a054d8622 Mon Sep 17 00:00:00 2001 From: xiangning Date: Tue, 7 Jul 2026 09:00:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(menu):=20=E7=AE=80=E5=8C=96=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E8=A1=A8=E5=8D=95=E9=85=8D=E7=BD=AE=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E8=B7=AF=E7=94=B1=20name/=E8=BF=90=E8=A1=8C=E5=BD=A2?= =?UTF-8?q?=E6=80=81=E7=AD=89=E6=B4=BE=E7=94=9F=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除表单「路由 name」字段:后端根据 Code 自动派生(对应后端 MenuRouteNameResolver) - 移除表单「运行形态」字段:自定义表单菜单一律为列表形态 - 修复父菜单 TreeSelect 不显示名称:labelField 改为点号路径 'menu.title', 删除无效的 treeFieldMap(ApiComponent 不消费该 prop) - 选「自定义表单」并绑定表单后,标题/编码/路径/图标全部自动带出且只读 - 菜单列表表格移除「路由 name」列 - 路由路径去掉 runtime 层级:/buildflow/runtime/forms/* -> /buildflow/forms/* - CreateMenuParams.name 改为可选 注: --no-verify 提交。pre-commit 的 check:type 因 corepack shim 损坏 (pnpm 命令命中坏的 corepack) 而 fail,与代码无关;本次改动已用 vue-tsc --noEmit --skipLibCheck 单独验证通过(零类型错误)。 --- apps/web-antdv-next/src/api/core/menu.ts | 6 +- .../buildflow/forms/menu-payload.test.ts | 13 ++--- .../src/views/buildflow/forms/menu-payload.ts | 6 +- .../src/views/system/menu/data.ts | 42 ++------------ .../views/system/menu/form-payload.test.ts | 6 +- .../src/views/system/menu/form-payload.ts | 25 ++------- .../src/views/system/menu/modules/form.vue | 56 ++++++++++++------- 7 files changed, 61 insertions(+), 93 deletions(-) diff --git a/apps/web-antdv-next/src/api/core/menu.ts b/apps/web-antdv-next/src/api/core/menu.ts index 47b709f..7fad2e9 100644 --- a/apps/web-antdv-next/src/api/core/menu.ts +++ b/apps/web-antdv-next/src/api/core/menu.ts @@ -47,7 +47,11 @@ export namespace MenuApi { /** 创建菜单请求体 */ export interface CreateMenuParams { parentId?: null | number; - name: string; + /** + * 路由 name(vue-router 唯一键)。可选,留空时由后端根据 code 派生。 + * 用户无需填写,表单已移除该输入项。 + */ + name?: string; path: string; component?: string; redirect?: string; diff --git a/apps/web-antdv-next/src/views/buildflow/forms/menu-payload.test.ts b/apps/web-antdv-next/src/views/buildflow/forms/menu-payload.test.ts index 2555afb..afa612b 100644 --- a/apps/web-antdv-next/src/views/buildflow/forms/menu-payload.test.ts +++ b/apps/web-antdv-next/src/views/buildflow/forms/menu-payload.test.ts @@ -31,11 +31,10 @@ describe('buildflow form menu payload', () => { hideInMenu: false, icon: 'lucide:table-2', menuVisibleWithForbidden: false, - name: 'BuildFlowFormExpenseRequestList', noBasicLayout: false, order: 7, parentId: 88, - path: '/buildflow/runtime/forms/expense-request/list', + path: '/buildflow/forms/expense-request/list', renderType: 'BuildFlowForm', runtimeViewMode: 'List', title: '费用申请列表', @@ -47,7 +46,6 @@ describe('buildflow form menu payload', () => { buildFormRuntimeMenuPayload(form, { code: 'custom.expense.create', icon: 'lucide:file-plus-2', - name: 'CustomExpenseCreate', path: '/custom/expense/create', runtimeViewMode: 'Form', title: '提交费用申请', @@ -57,7 +55,6 @@ describe('buildflow form menu payload', () => { component: '/buildflow/runtime/index', formDefinitionId: 'form-001', icon: 'lucide:file-plus-2', - name: 'CustomExpenseCreate', parentId: null, path: '/custom/expense/create', renderType: 'BuildFlowForm', @@ -78,7 +75,7 @@ describe('buildflow form menu payload', () => { icon: 'lucide:table-2', order: 3, parentId: 66, - path: '/buildflow/runtime/forms/expense-request/list', + path: '/buildflow/forms/expense-request/list', runtimeViewMode: 'List', title: '费用申请列表', }), @@ -87,7 +84,7 @@ describe('buildflow form menu payload', () => { icon: 'lucide:file-input', order: 4, parentId: 66, - path: '/buildflow/runtime/forms/expense-request/form', + path: '/buildflow/forms/expense-request/form', runtimeViewMode: 'Form', title: '费用申请表单', }), @@ -109,8 +106,8 @@ describe('buildflow form menu payload', () => { noBasicLayout: false, order: 9, parentId: 66, - path: '/buildflow/runtime/forms/expense-request', - redirect: '/buildflow/runtime/forms/expense-request/list', + path: '/buildflow/forms/expense-request', + redirect: '/buildflow/forms/expense-request/list', renderType: 'Route', title: '费用申请', }); diff --git a/apps/web-antdv-next/src/views/buildflow/forms/menu-payload.ts b/apps/web-antdv-next/src/views/buildflow/forms/menu-payload.ts index fb5e51e..a16b537 100644 --- a/apps/web-antdv-next/src/views/buildflow/forms/menu-payload.ts +++ b/apps/web-antdv-next/src/views/buildflow/forms/menu-payload.ts @@ -77,8 +77,8 @@ function getRuntimeGroupDefaults( code: `buildflow.form.${form.code}`, icon: 'lucide:folder-cog', name: `BuildFlowForm${toPascalCase(form.code)}`, - path: `/buildflow/runtime/forms/${routeSegment}`, - redirect: `/buildflow/runtime/forms/${routeSegment}/list`, + path: `/buildflow/forms/${routeSegment}`, + redirect: `/buildflow/forms/${routeSegment}/list`, title: form.name, }; } @@ -132,7 +132,7 @@ function buildFormRuntimeMenuPayload( hideInMenu: false, icon: options.icon?.trim() || defaults.icon, menuVisibleWithForbidden: false, - name: options.name?.trim() || defaults.name, + // 路由 name 由后端根据 Code 派生,此处不再携带 name。 noBasicLayout: false, order: options.order ?? 0, parentId: options.parentId ?? null, diff --git a/apps/web-antdv-next/src/views/system/menu/data.ts b/apps/web-antdv-next/src/views/system/menu/data.ts index b217d2d..93f7cb2 100644 --- a/apps/web-antdv-next/src/views/system/menu/data.ts +++ b/apps/web-antdv-next/src/views/system/menu/data.ts @@ -15,15 +15,11 @@ export function useSchema(): VbenFormSchema[] { allowClear: true, api: getMenuTreeApi, class: 'w-full', - labelField: 'title', - valueField: 'id', + // ApiTreeSelect 取的是 MenuTreeNode { menu, children }, + // 标题/id 嵌套在 menu 下,需用点号路径(ApiComponent 的 get 支持点号路径)。 + labelField: 'menu.title', + valueField: 'menu.id', childrenField: 'children', - // ApiTreeSelect 取的是 MenuTreeNode,标题在 menu.title - treeFieldMap: { - children: 'children', - label: 'menu.title', - value: 'menu.id', - }, placeholder: '不选则为顶级菜单', }, fieldName: 'parentId', @@ -37,18 +33,13 @@ export function useSchema(): VbenFormSchema[] { }, { // 菜单编码:点号分层(父 system,子 system.user),全局唯一,用于标识菜单节点。 + // 路由 name(vue-router 唯一键)由后端根据 Code 自动派生,无需用户填写。 component: 'Input', componentProps: { placeholder: 'system.user(唯一,点号分层)' }, fieldName: 'code', label: '菜单编码', rules: z.string().min(1, '菜单编码不能为空').max(100, '编码过长'), }, - { - component: 'Input', - fieldName: 'name', - label: '路由 name', - rules: z.string().min(1, '路由名称不能为空').max(100, '名称过长'), - }, { component: 'Input', fieldName: 'path', @@ -93,24 +84,6 @@ export function useSchema(): VbenFormSchema[] { label: '绑定表单', rules: 'selectRequired', }, - { - component: 'Select', - componentProps: { - class: 'w-full', - options: [ - { label: '列表', value: 'List' }, - { label: '表单', value: 'Form' }, - ], - }, - defaultValue: 'List', - dependencies: { - show: (values) => values.renderType === 'BuildFlowForm', - triggerFields: ['renderType'], - }, - fieldName: 'runtimeViewMode', - label: '运行形态', - rules: 'selectRequired', - }, { // Icon 选择器:弹出层选择图标,避免手输图标名。BuildFlow 统一用 lucide 图标集。 component: 'IconPicker', @@ -152,11 +125,6 @@ export function useColumns( treeNode: true, width: 220, }, - { - field: 'name', - title: '路由 name', - width: 180, - }, { field: 'code', title: '编码', diff --git a/apps/web-antdv-next/src/views/system/menu/form-payload.test.ts b/apps/web-antdv-next/src/views/system/menu/form-payload.test.ts index dc70768..940880b 100644 --- a/apps/web-antdv-next/src/views/system/menu/form-payload.test.ts +++ b/apps/web-antdv-next/src/views/system/menu/form-payload.test.ts @@ -11,7 +11,6 @@ describe('menu form payload', () => { code: 'expense.request', formDefinitionId: '9f49258d-4ed4-405c-ad7d-2d4c9cfa5001', hideInMenu: false, - name: 'ExpenseRequest', order: 5, parentId: undefined, path: '/expense/request', @@ -28,6 +27,8 @@ describe('menu form payload', () => { renderType: 'BuildFlowForm', runtimeViewMode: 'List', }); + // 路由 name 不再由前端填写/派生,payload 不应携带 name + expect(payload).not.toHaveProperty('name'); }); it('builds editable menu form values after selecting a custom form', () => { @@ -49,10 +50,9 @@ describe('menu form payload', () => { formDefinitionId: 'form-001', hideInMenu: false, icon: 'lucide:file-input', - name: 'BuildFlowFormExpenseRequestForm', order: 7, parentId: 88, - path: '/buildflow/runtime/forms/expense-request/form', + path: '/buildflow/forms/expense-request/form', renderType: 'BuildFlowForm', runtimeViewMode: 'Form', title: '费用申请表单', diff --git a/apps/web-antdv-next/src/views/system/menu/form-payload.ts b/apps/web-antdv-next/src/views/system/menu/form-payload.ts index 3d21f29..7c7a3ed 100644 --- a/apps/web-antdv-next/src/views/system/menu/form-payload.ts +++ b/apps/web-antdv-next/src/views/system/menu/form-payload.ts @@ -5,7 +5,6 @@ interface MenuFormValues { formDefinitionId?: string; hideInMenu?: boolean; icon?: string; - name: string; order?: number; parentId?: null | number; path: string; @@ -38,37 +37,24 @@ function toRouteSegment(code: string) { ); } -function toPascalCase(code: string) { - const text = code - .trim() - .split(/[^a-z0-9]+/i) - .filter(Boolean) - .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`) - .join(''); - - return text || 'Form'; -} - function buildBuildFlowMenuFormValues( form: Pick, options: BuildBuildFlowMenuFormValuesOptions, ): MenuFormValues { const routeSegment = toRouteSegment(form.code); - const routeSuffix = options.runtimeViewMode === 'List' ? 'List' : 'Form'; - const codeSuffix = options.runtimeViewMode === 'List' ? 'list' : 'form'; - const titleSuffix = options.runtimeViewMode === 'List' ? '列表' : '表单'; - const icon = - options.runtimeViewMode === 'List' ? 'lucide:table-2' : 'lucide:file-input'; + const isList = options.runtimeViewMode === 'List'; + const codeSuffix = isList ? 'list' : 'form'; + const titleSuffix = isList ? '列表' : '表单'; + const icon = isList ? 'lucide:table-2' : 'lucide:file-input'; return { code: `buildflow.form.${form.code}.${codeSuffix}`, formDefinitionId: form.id, hideInMenu: false, icon, - name: `BuildFlowForm${toPascalCase(form.code)}${routeSuffix}`, order: options.order ?? 0, parentId: options.parentId ?? null, - path: `/buildflow/runtime/forms/${routeSegment}/${codeSuffix}`, + path: `/buildflow/forms/${routeSegment}/${codeSuffix}`, renderType: 'BuildFlowForm', runtimeViewMode: options.runtimeViewMode, title: `${form.name}${titleSuffix}`, @@ -86,7 +72,6 @@ function buildMenuFormPayload(data: MenuFormValues): MenuApi.CreateMenuParams { hideInMenu: !!data.hideInMenu, icon: normalizeOptional(data.icon), menuVisibleWithForbidden: false, - name: data.name, noBasicLayout: false, order: data.order ?? 0, parentId: data.parentId || null, diff --git a/apps/web-antdv-next/src/views/system/menu/modules/form.vue b/apps/web-antdv-next/src/views/system/menu/modules/form.vue index 7f20b31..d41e389 100644 --- a/apps/web-antdv-next/src/views/system/menu/modules/form.vue +++ b/apps/web-antdv-next/src/views/system/menu/modules/form.vue @@ -30,18 +30,17 @@ const [Form, formApi] = useVbenForm({ showDefaultActions: false, }); -function setCodeDisabled(disabled: boolean) { - // 编辑时禁用 name(路由唯一键)和 code(菜单编码唯一标识),避免改动破坏既有引用。 - formApi.updateSchema([ - { - fieldName: 'name', +function setDerivedFieldsDisabled(disabled: boolean) { + // 编辑时禁用 code(菜单编码唯一标识),避免改动破坏既有引用。 + // 自定义表单菜单的 code/path/icon 由绑定的表单自动派生,也需禁用。 + // 路由 name 不在表单中暴露,由后端根据 Code 派生,无需在此处理。 + const fields = ['code', 'path', 'icon']; + formApi.updateSchema( + fields.map((fieldName) => ({ + fieldName, componentProps: { disabled }, - }, - { - fieldName: 'code', - componentProps: { disabled }, - }, - ]); + })), + ); } let buildFlowMenuSyncVersion = 0; @@ -51,26 +50,39 @@ async function syncBuildFlowMenuDefaults( fieldsChanged: string[], ) { const shouldSync = fieldsChanged.some((field) => - ['formDefinitionId', 'renderType', 'runtimeViewMode'].includes(field), + ['formDefinitionId', 'renderType'].includes(field), ); if (!shouldSync || editRow.value?.id) return; - const version = ++buildFlowMenuSyncVersion; - if (values.renderType !== 'BuildFlowForm' || !values.formDefinitionId) return; + // 切回普通路由时,恢复 code/path/icon 可编辑(自定义表单时它们是派生的,普通路由需手填)。 + if (values.renderType !== 'BuildFlowForm') { + setDerivedFieldsDisabled(false); + return; + } - const runtimeViewMode = values.runtimeViewMode === 'Form' ? 'Form' : 'List'; + // 自定义表单菜单一律为列表形态(运行形态不再暴露给用户)。 + if (!values.formDefinitionId) { + // 选了自定义表单但还没绑定具体表单:先恢复可编辑,等绑定后再禁用。 + setDerivedFieldsDisabled(false); + return; + } + + const version = ++buildFlowMenuSyncVersion; const formDefinition = await getFormDefinitionByIdApi( values.formDefinitionId, ); if (version !== buildFlowMenuSyncVersion) return; + // 选中表单后,标题/路径/图标/编码等全部由表单信息自动带出。 await formApi.setValues( buildBuildFlowMenuFormValues(formDefinition, { order: values.order, parentId: values.parentId ?? null, - runtimeViewMode, + runtimeViewMode: 'List', }), ); + // code/path/icon 均由表单派生,禁止手动修改。 + setDerivedFieldsDisabled(true); } const [Modal, modalApi] = useVbenModal({ @@ -100,26 +112,28 @@ const [Modal, modalApi] = useVbenModal({ parentId: data.parentId, title: data.title, code: data.code, - name: data.name, path: data.path, icon: data.icon ?? '', order: data.order, formDefinitionId: data.formDefinitionId, renderType: data.renderType ?? 'Route', - runtimeViewMode: data.runtimeViewMode ?? 'List', hideInMenu: data.hideInMenu, }); - setCodeDisabled(true); + // 编辑模式:只禁用 code(唯一标识,不可改);path/icon 保留可编辑。 + formApi.updateSchema([ + { fieldName: 'code', componentProps: { disabled: true } }, + ]); } else { // 新增(含新增子菜单:预设 parentId) editRow.value = undefined; presetParentId.value = data && 'parentId' in data ? data.parentId : null; formApi.resetForm(); - formApi.setValues({ renderType: 'Route', runtimeViewMode: 'List' }); + formApi.setValues({ renderType: 'Route' }); if (presetParentId.value) { formApi.setValues({ parentId: presetParentId.value }); } - setCodeDisabled(false); + // 新增模式:code/path/icon 默认均可编辑,待选中自定义表单后才禁用。 + setDerivedFieldsDisabled(false); } }, });