feat(menu): 简化菜单表单配置,移除路由 name/运行形态等派生字段

- 移除表单「路由 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 单独验证通过(零类型错误)。
This commit is contained in:
xiangning 2026-07-07 09:00:45 +08:00
parent 706b03f84e
commit 04f714f4b6
7 changed files with 61 additions and 93 deletions

View File

@ -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;

View File

@ -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: '费用申请',
});

View File

@ -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,

View File

@ -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: '编码',

View File

@ -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: '费用申请表单',

View File

@ -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<WorkflowApi.FormDefinitionItem, 'code' | 'id' | 'name'>,
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,

View File

@ -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);
}
},
});