@liujitcn/kratos-admin-system 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +51 -50
  2. package/dist/package/declarations/src/api/system/base_language.d.ts +51 -0
  3. package/dist/package/declarations/src/api/system/base_translation.d.ts +7 -0
  4. package/dist/package/declarations/src/api/system/code_gen_i18n.d.ts +8 -0
  5. package/dist/package/declarations/src/components/DynamicTranslationCell.vue.d.ts +39 -0
  6. package/dist/package/declarations/src/components/DynamicTranslationEditor.vue.d.ts +23 -0
  7. package/dist/package/declarations/src/components/dynamicTranslation.d.ts +44 -0
  8. package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +5 -0
  9. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +79 -0
  10. package/dist/package/declarations/src/rpc/system/admin/v1/base_language.d.ts +114 -0
  11. package/dist/package/declarations/src/rpc/system/admin/v1/base_menu.d.ts +40 -0
  12. package/dist/package/declarations/src/rpc/system/admin/v1/base_translation.d.ts +102 -0
  13. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen.d.ts +2 -0
  14. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_column.d.ts +7 -0
  15. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_table.d.ts +13 -0
  16. package/dist/package/declarations/src/views/tool/code-gen/components/CodeGenLocaleEditor.vue.d.ts +23 -0
  17. package/dist/package/declarations/src/views/tool/code-gen/config.d.ts +7 -7
  18. package/dist/package/declarations/src/views/tool/language/index.vue.d.ts +3 -0
  19. package/dist/package/src/api/base/ai_message.ts +7 -12
  20. package/dist/package/src/api/base/sse.ts +3 -1
  21. package/dist/package/src/api/system/base_language.ts +137 -0
  22. package/dist/package/src/api/system/base_translation.ts +22 -0
  23. package/dist/package/src/api/system/code_gen_column.ts +17 -3
  24. package/dist/package/src/api/system/code_gen_i18n.ts +18 -0
  25. package/dist/package/src/api/system/code_gen_table.ts +12 -4
  26. package/dist/package/src/components/DynamicTranslationCell.vue +176 -0
  27. package/dist/package/src/components/DynamicTranslationEditor.vue +166 -0
  28. package/dist/package/src/components/dynamicTranslation.ts +86 -0
  29. package/dist/package/src/locales/README.md +28 -0
  30. package/dist/package/src/locales/en-US.json +929 -0
  31. package/dist/package/src/locales/es-ES.json +928 -0
  32. package/dist/package/src/locales/fr-FR.json +928 -0
  33. package/dist/package/src/locales/ja-JP.json +929 -0
  34. package/dist/package/src/locales/ko-KR.json +928 -0
  35. package/dist/package/src/locales/zh-CN.json +929 -0
  36. package/dist/package/src/locales/zh-TW.json +928 -0
  37. package/dist/package/src/module.ts +19 -1
  38. package/dist/package/src/rpc/system/admin/v1/base_config.ts +5 -0
  39. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +81 -0
  40. package/dist/package/src/rpc/system/admin/v1/base_language.ts +135 -0
  41. package/dist/package/src/rpc/system/admin/v1/base_menu.ts +41 -0
  42. package/dist/package/src/rpc/system/admin/v1/base_translation.ts +117 -0
  43. package/dist/package/src/rpc/system/admin/v1/code_gen.ts +5 -1
  44. package/dist/package/src/rpc/system/admin/v1/code_gen_column.ts +8 -0
  45. package/dist/package/src/rpc/system/admin/v1/code_gen_table.ts +15 -0
  46. package/dist/package/src/views/ai/chat/components/AiMarkdown.vue +1 -6
  47. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +70 -53
  48. package/dist/package/src/views/ai/chat/components/SessionPanel.vue +20 -9
  49. package/dist/package/src/views/ai/chat/components/XSender.vue +36 -23
  50. package/dist/package/src/views/ai/chat/index.vue +55 -39
  51. package/dist/package/src/views/ai/chat/message.ts +18 -17
  52. package/dist/package/src/views/base/api/index.vue +121 -83
  53. package/dist/package/src/views/base/area/index.vue +81 -31
  54. package/dist/package/src/views/base/config/index.vue +272 -78
  55. package/dist/package/src/views/base/dept/index.vue +110 -61
  56. package/dist/package/src/views/base/dict/index.vue +137 -58
  57. package/dist/package/src/views/base/dict/item.vue +147 -61
  58. package/dist/package/src/views/base/job/index.vue +134 -81
  59. package/dist/package/src/views/base/job/log.vue +21 -22
  60. package/dist/package/src/views/base/log/index.vue +68 -46
  61. package/dist/package/src/views/base/menu/index.vue +257 -156
  62. package/dist/package/src/views/base/migration/index.vue +49 -29
  63. package/dist/package/src/views/base/post/index.vue +128 -59
  64. package/dist/package/src/views/base/role/index.vue +145 -78
  65. package/dist/package/src/views/base/tenant/index.vue +132 -63
  66. package/dist/package/src/views/base/user/components/dept-tree.vue +2 -1
  67. package/dist/package/src/views/base/user/index.vue +192 -112
  68. package/dist/package/src/views/profile/components/base.vue +44 -27
  69. package/dist/package/src/views/profile/components/password.vue +43 -27
  70. package/dist/package/src/views/profile/components/security.vue +89 -53
  71. package/dist/package/src/views/profile/index.vue +10 -9
  72. package/dist/package/src/views/tool/api-doc/index.vue +6 -2
  73. package/dist/package/src/views/tool/code-gen/code-preview/index.vue +30 -14
  74. package/dist/package/src/views/tool/code-gen/columns/index.vue +382 -273
  75. package/dist/package/src/views/tool/code-gen/columns/option-copy.ts +1 -6
  76. package/dist/package/src/views/tool/code-gen/components/CodeGenLocaleEditor.vue +118 -0
  77. package/dist/package/src/views/tool/code-gen/components/CodeGenProgressDialog.vue +65 -36
  78. package/dist/package/src/views/tool/code-gen/components/CodePreviewPane.vue +8 -5
  79. package/dist/package/src/views/tool/code-gen/config.ts +114 -77
  80. package/dist/package/src/views/tool/code-gen/preview/data.ts +19 -17
  81. package/dist/package/src/views/tool/code-gen/preview/index.vue +96 -58
  82. package/dist/package/src/views/tool/code-gen/proto/index.vue +110 -62
  83. package/dist/package/src/views/tool/code-gen/table/index.vue +197 -117
  84. package/dist/package/src/views/tool/language/index.vue +256 -0
  85. package/dist/package/src/views/tool/project-doc/index.vue +17 -15
  86. package/package.json +6 -2
@@ -2,7 +2,7 @@
2
2
  <div class="table-box">
3
3
  <ProTable
4
4
  ref="proTable"
5
- title="菜单列表"
5
+ :title="t('system.menu.title.list')"
6
6
  row-key="id"
7
7
  :indent="20"
8
8
  :columns="columns"
@@ -16,7 +16,7 @@
16
16
  <FormDialog
17
17
  v-model="dialog.visible"
18
18
  ref="formDialogRef"
19
- :title="dialog.title"
19
+ :title="t(dialog.editing ? 'system.menu.action.edit' : 'system.menu.action.create')"
20
20
  width="1180px"
21
21
  :model="formData"
22
22
  :fields="formFields"
@@ -27,15 +27,25 @@
27
27
  @close="handleCloseDialog"
28
28
  >
29
29
  <template #menuIcon>
30
- <SelectIcon v-model:icon-value="menuIconValue" placeholder="请选择菜单图标" />
30
+ <SelectIcon v-model:icon-value="menuIconValue" :placeholder="t('system.menu.placeholder.icon')" />
31
+ </template>
32
+
33
+ <template #translations>
34
+ <DynamicTranslationEditor
35
+ v-model="translationValues"
36
+ :resource-id="formData.id"
37
+ :resource-type="TranslationResourceType.TRANSLATION_RESOURCE_TYPE_MENU"
38
+ :draft-enabled="configStore.translationDraftEnabled"
39
+ :maxlength="100"
40
+ />
31
41
  </template>
32
42
 
33
43
  <template #apiTransferItem="slotScope">
34
44
  <el-popover effect="light" trigger="hover" placement="top" width="auto">
35
45
  <template #default>
36
- <div>操作名称:{{ slotScope.option.operation }}</div>
37
- <div>请求方式:{{ slotScope.option.method }}</div>
38
- <div>请求地址:{{ slotScope.option.path }}</div>
46
+ <div>{{ t("system.api.field.operation") }}:{{ slotScope.option.operation }}</div>
47
+ <div>{{ t("system.api.field.method") }}:{{ slotScope.option.method }}</div>
48
+ <div>{{ t("system.api.field.path") }}:{{ slotScope.option.path }}</div>
39
49
  </template>
40
50
  <template #reference>{{ slotScope.option.label }}</template>
41
51
  </el-popover>
@@ -49,10 +59,16 @@ import { computed, h, reactive, ref, resolveComponent, resolveDynamicComponent,
49
59
  import { ElIcon, ElTag, type FormRules } from "element-plus";
50
60
  import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
51
61
  import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
52
- import type { ColumnProps, HeaderActionProps, ProTableInstance, RenderScope } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
62
+ import type {
63
+ ColumnProps,
64
+ HeaderActionProps,
65
+ ProTableInstance,
66
+ RenderScope
67
+ } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
53
68
  import type { ProFormField, ProFormOption } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
54
69
  import SelectIcon from "@liujitcn/kratos-admin-core/components/SelectIcon/index.vue";
55
70
  import { defBaseMenuService } from "@liujitcn/kratos-admin-system/api/system/base_menu";
71
+ import { loadEnabledBaseLanguages } from "@liujitcn/kratos-admin-system/api/system/base_language";
56
72
  import { defBaseApiService } from "@liujitcn/kratos-admin-system/api/system/base_api";
57
73
  import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
58
74
  import type { BaseApi } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_api";
@@ -65,6 +81,18 @@ import type {
65
81
  import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
66
82
  import { BaseMenuType } from "@liujitcn/kratos-admin-system/rpc/system/common/v1/enum";
67
83
  import { normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
84
+ import { t } from "@liujitcn/kratos-admin-core";
85
+ import { useConfigStore } from "@liujitcn/kratos-admin-core/stores/runtime";
86
+ import DynamicTranslationEditor from "@liujitcn/kratos-admin-system/components/DynamicTranslationEditor.vue";
87
+ import DynamicTranslationCell from "@liujitcn/kratos-admin-system/components/DynamicTranslationCell.vue";
88
+ import {
89
+ normalizeDynamicTranslations,
90
+ serializeDynamicTranslations,
91
+ type DynamicTranslationRecord,
92
+ type DynamicTranslationValue
93
+ } from "@liujitcn/kratos-admin-system/components/dynamicTranslation";
94
+ import { TranslationResourceType } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_translation";
95
+ import type { BaseMenuTranslation } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_menu";
68
96
 
69
97
  defineOptions({
70
98
  name: "BaseMenu",
@@ -82,15 +110,17 @@ type MenuFormState = Omit<BaseMenuForm, "meta"> & {
82
110
  };
83
111
 
84
112
  const { BUTTONS } = useAuthButtons();
113
+ const configStore = useConfigStore();
85
114
  const proTable = ref<ProTableInstance>();
86
115
  const formDialogRef = ref<InstanceType<typeof FormDialog>>();
87
116
  const menuOptions = ref<ProFormOption[]>([]);
88
117
  const parentMenuTypeMap = ref(new Map<number, BaseMenuType>());
89
118
  const appMenuIds = ref(new Set<number>([APP_MENU_ROOT_ID]));
90
119
  const apiList = ref<BaseApi[]>([]);
120
+ const translationValues = ref<DynamicTranslationValue[]>(normalizeDynamicTranslations(undefined, "title"));
91
121
 
92
122
  const dialog = reactive({
93
- title: "",
123
+ editing: false,
94
124
  visible: false,
95
125
  parentType: BaseMenuType.UNKNOWN_MT,
96
126
  parentLocked: true
@@ -133,6 +163,7 @@ function createDefaultMenuForm(): MenuFormState {
133
163
  redirect: "",
134
164
  meta: createDefaultMenuMeta(),
135
165
  api: [],
166
+ translations: [],
136
167
  sort: 1,
137
168
  status: Status.ENABLE
138
169
  };
@@ -148,12 +179,12 @@ const menuIconValue = computed({
148
179
  }
149
180
  });
150
181
 
151
- const menuTypeOptions: ProFormOption[] = [
152
- { label: "目录", value: BaseMenuType.FOLDER },
153
- { label: "菜单", value: BaseMenuType.MENU },
154
- { label: "按钮", value: BaseMenuType.BUTTON },
155
- { label: "外链", value: BaseMenuType.EXT_LINK }
156
- ];
182
+ const menuTypeOptions = computed<ProFormOption[]>(() => [
183
+ { label: t("system.menu.type.folder"), value: BaseMenuType.FOLDER },
184
+ { label: t("system.menu.type.menu"), value: BaseMenuType.MENU },
185
+ { label: t("system.menu.type.button"), value: BaseMenuType.BUTTON },
186
+ { label: t("system.menu.type.external"), value: BaseMenuType.EXT_LINK }
187
+ ]);
157
188
 
158
189
  /** 判断父节点是否属于固定移动端菜单树。 */
159
190
  function isAppMenu(parentId?: number) {
@@ -187,17 +218,17 @@ function canCreateChild(menu: BaseMenu) {
187
218
  const availableMenuTypeOptions = computed(() => {
188
219
  const parentLevel = getMenuLevel(formData.parent_id ?? 0);
189
220
 
190
- if (formData.id === APP_MENU_ROOT_ID) return menuTypeOptions.filter(item => item.value === BaseMenuType.FOLDER);
191
- if (isAppMenu(formData.parent_id)) return menuTypeOptions.filter(item => item.value === BaseMenuType.MENU);
192
- if (formData.id > 0 && parentLevel === 0) return menuTypeOptions.filter(item => item.value === BaseMenuType.FOLDER);
221
+ if (formData.id === APP_MENU_ROOT_ID) return menuTypeOptions.value.filter(item => item.value === BaseMenuType.FOLDER);
222
+ if (isAppMenu(formData.parent_id)) return menuTypeOptions.value.filter(item => item.value === BaseMenuType.MENU);
223
+ if (formData.id > 0 && parentLevel === 0) return menuTypeOptions.value.filter(item => item.value === BaseMenuType.FOLDER);
193
224
  if (dialog.parentType === BaseMenuType.FOLDER && parentLevel === 1)
194
- return menuTypeOptions.filter(
225
+ return menuTypeOptions.value.filter(
195
226
  item => item.value === BaseMenuType.FOLDER || item.value === BaseMenuType.MENU || item.value === BaseMenuType.EXT_LINK
196
227
  );
197
228
  if (dialog.parentType === BaseMenuType.FOLDER && parentLevel === 2)
198
- return menuTypeOptions.filter(item => item.value === BaseMenuType.MENU || item.value === BaseMenuType.EXT_LINK);
229
+ return menuTypeOptions.value.filter(item => item.value === BaseMenuType.MENU || item.value === BaseMenuType.EXT_LINK);
199
230
  if (dialog.parentType === BaseMenuType.MENU && (parentLevel === 2 || parentLevel === 3))
200
- return menuTypeOptions.filter(item => item.value === BaseMenuType.BUTTON);
231
+ return menuTypeOptions.value.filter(item => item.value === BaseMenuType.BUTTON);
201
232
  return [];
202
233
  });
203
234
 
@@ -216,16 +247,16 @@ watch(
216
247
  }
217
248
  );
218
249
 
219
- const statusOptions: ProFormOption[] = [
220
- { label: "启用", value: Status.ENABLE },
221
- { label: "禁用", value: Status.DISABLE }
222
- ];
250
+ const statusOptions = computed<ProFormOption[]>(() => [
251
+ { label: t("common.status.enabled"), value: Status.ENABLE },
252
+ { label: t("common.status.disabled"), value: Status.DISABLE }
253
+ ]);
223
254
 
224
- const appAccessOptions: ProFormOption[] = [
225
- { label: "公开访问", value: "PUBLIC" },
226
- { label: "仅游客", value: "GUEST_ONLY" },
227
- { label: "登录访问", value: "AUTHENTICATED" }
228
- ];
255
+ const appAccessOptions = computed<ProFormOption[]>(() => [
256
+ { label: t("system.menu.access.public"), value: "PUBLIC" },
257
+ { label: t("system.menu.access.guest"), value: "GUEST_ONLY" },
258
+ { label: t("system.menu.access.authenticated"), value: "AUTHENTICATED" }
259
+ ]);
229
260
 
230
261
  /**
231
262
  * 渲染菜单图标单元格,统一兼容 Element Plus 图标和本地 svg 图标。
@@ -252,7 +283,19 @@ function renderMenuIconCell(scope: RenderScope<BaseMenu>) {
252
283
  */
253
284
  function renderHiddenCell(scope: RenderScope<BaseMenu>) {
254
285
  const isHidden = Boolean(scope.row.meta?.hidden);
255
- return h(ElTag, { type: isHidden ? "info" : "success" }, () => (isHidden ? "隐藏" : "显示"));
286
+ return h(ElTag, { type: isHidden ? "info" : "success" }, () =>
287
+ t(isHidden ? "system.menu.status.hidden" : "system.menu.status.visible")
288
+ );
289
+ }
290
+
291
+ /** 渲染菜单标题翻译预览,并复用当前页面的编辑弹窗。 */
292
+ function renderMenuTitleCell(scope: RenderScope<BaseMenu>) {
293
+ const row = scope.row;
294
+ return h(DynamicTranslationCell, {
295
+ source: row.meta?.title ?? "",
296
+ translations: row.translations,
297
+ textField: "title"
298
+ });
256
299
  }
257
300
 
258
301
  /** 菜单表格列配置。 */
@@ -267,26 +310,28 @@ const columns = computed<ColumnProps[]>(() => [
267
310
  },
268
311
  {
269
312
  prop: "meta.title",
270
- label: "菜单名称",
313
+ label: t("system.menu.field.name"),
271
314
  minWidth: 220,
272
315
  align: "left",
273
- search: { el: "input", key: "title" }
316
+ search: { el: "input", key: "title" },
317
+ showOverflowTooltip: false,
318
+ render: scope => renderMenuTitleCell(scope as unknown as RenderScope<BaseMenu>)
274
319
  },
275
- { prop: "type", label: "菜单类型", minWidth: 120, dictCode: "base_menu_type", search: { el: "select" } },
320
+ { prop: "type", label: t("system.menu.field.type"), minWidth: 120, dictCode: "base_menu_type", search: { el: "select" } },
276
321
  {
277
322
  prop: "meta.icon",
278
- label: "菜单图标",
323
+ label: t("system.menu.field.icon"),
279
324
  width: 90,
280
325
  render: scope => renderMenuIconCell(scope as unknown as RenderScope<BaseMenu>)
281
326
  },
282
- { prop: "path", label: "路由路径/权限标识", minWidth: 260, search: { el: "input" } },
283
- { prop: "name", label: "路由名称", minWidth: 180, search: { el: "input" } },
284
- { prop: "component", label: "组件路径", minWidth: 260 },
285
- { prop: "redirect", label: "重定向地址", minWidth: 220 },
286
- { prop: "sort", label: "排序", minWidth: 80, align: "right" },
327
+ { prop: "path", label: t("system.menu.field.pathOrPermission"), minWidth: 260, search: { el: "input" } },
328
+ { prop: "name", label: t("system.menu.field.routeName"), minWidth: 180, search: { el: "input" } },
329
+ { prop: "component", label: t("system.menu.field.component"), minWidth: 260 },
330
+ { prop: "redirect", label: t("system.menu.field.redirect"), minWidth: 220 },
331
+ { prop: "sort", label: t("system.common.field.sort"), minWidth: 80, align: "right" },
287
332
  {
288
333
  prop: "status",
289
- label: "状态",
334
+ label: t("system.common.field.status"),
290
335
  width: 100,
291
336
  dictCode: "status",
292
337
  search: { el: "select" },
@@ -294,29 +339,29 @@ const columns = computed<ColumnProps[]>(() => [
294
339
  statusProps: {
295
340
  activeValue: Status.ENABLE,
296
341
  inactiveValue: Status.DISABLE,
297
- activeText: "启用",
298
- inactiveText: "禁用",
342
+ activeText: t("common.status.enabled"),
343
+ inactiveText: t("common.status.disabled"),
299
344
  disabled: () => !BUTTONS.value["base:menu:status"],
300
345
  beforeChange: scope => handleBeforeSetStatus(scope.row as BaseMenu)
301
346
  }
302
347
  },
303
348
  {
304
349
  prop: "meta.hidden",
305
- label: "显示状态",
350
+ label: t("system.menu.field.displayStatus"),
306
351
  width: 100,
307
352
  render: scope => renderHiddenCell(scope as unknown as RenderScope<BaseMenu>)
308
353
  },
309
- { prop: "created_at", label: "创建时间", minWidth: 180 },
310
- { prop: "updated_at", label: "更新时间", minWidth: 180 },
354
+ { prop: "created_at", label: t("system.common.field.createdAt"), minWidth: 180 },
355
+ { prop: "updated_at", label: t("system.common.field.updatedAt"), minWidth: 180 },
311
356
  {
312
357
  prop: "operation",
313
- label: "操作",
358
+ label: t("system.common.field.action"),
314
359
  width: 220,
315
360
  fixed: "right",
316
361
  cellType: "actions",
317
362
  actions: [
318
363
  {
319
- label: "新增",
364
+ label: t("common.action.create"),
320
365
  type: "primary",
321
366
  link: true,
322
367
  icon: CirclePlus,
@@ -324,7 +369,7 @@ const columns = computed<ColumnProps[]>(() => [
324
369
  onClick: scope => handleOpenDialog(scope.row as BaseMenu)
325
370
  },
326
371
  {
327
- label: "编辑",
372
+ label: t("common.action.edit"),
328
373
  type: "primary",
329
374
  link: true,
330
375
  icon: EditPen,
@@ -332,7 +377,7 @@ const columns = computed<ColumnProps[]>(() => [
332
377
  onClick: scope => handleOpenDialog(undefined, (scope.row as BaseMenu).id)
333
378
  },
334
379
  {
335
- label: "删除",
380
+ label: t("common.action.delete"),
336
381
  type: "danger",
337
382
  link: true,
338
383
  icon: Delete,
@@ -349,14 +394,14 @@ const columns = computed<ColumnProps[]>(() => [
349
394
  /** 菜单表格顶部按钮配置。 */
350
395
  const headerActions = computed<HeaderActionProps[]>(() => [
351
396
  {
352
- label: "新增",
397
+ label: t("common.action.create"),
353
398
  type: "success",
354
399
  icon: CirclePlus,
355
400
  hidden: () => !BUTTONS.value["base:menu:create"],
356
401
  onClick: () => handleOpenDialog()
357
402
  },
358
403
  {
359
- label: "删除",
404
+ label: t("common.action.delete"),
360
405
  type: "danger",
361
406
  icon: Delete,
362
407
  hidden: () => !BUTTONS.value["base:menu:delete"],
@@ -378,7 +423,7 @@ const transferData = computed<ProFormOption[]>(() => {
378
423
  const formFields = computed<ProFormField[]>(() => [
379
424
  {
380
425
  prop: "parent_id",
381
- label: "上级菜单",
426
+ label: t("system.menu.field.parent"),
382
427
  component: "tree-select",
383
428
  options: menuOptions.value,
384
429
  props: () => ({
@@ -387,14 +432,14 @@ const formFields = computed<ProFormField[]>(() => [
387
432
  checkStrictly: true,
388
433
  clearable: false,
389
434
  filterable: true,
390
- placeholder: "请选择上级菜单",
435
+ placeholder: t("system.menu.placeholder.parent"),
391
436
  disabled: dialog.parentLocked,
392
437
  style: { width: "100%" }
393
438
  })
394
439
  },
395
440
  {
396
441
  prop: "type",
397
- label: "菜单类型",
442
+ label: t("system.menu.field.type"),
398
443
  component: "radio-group",
399
444
  options: availableMenuTypeOptions.value,
400
445
  props: model => ({
@@ -403,15 +448,24 @@ const formFields = computed<ProFormField[]>(() => [
403
448
  },
404
449
  {
405
450
  prop: "meta.title",
406
- label: formData.type === BaseMenuType.BUTTON ? "按钮名称" : "菜单标题",
451
+ label: t(formData.type === BaseMenuType.BUTTON ? "system.menu.field.buttonName" : "system.menu.field.title"),
407
452
  component: "input",
408
453
  itemProps: {
409
454
  required: true
410
455
  },
411
456
  props: () => ({
412
- placeholder: formData.type === BaseMenuType.BUTTON ? "请输入按钮名称" : "请输入菜单标题"
457
+ placeholder: t(
458
+ formData.type === BaseMenuType.BUTTON ? "system.menu.placeholder.buttonName" : "system.menu.placeholder.title"
459
+ )
413
460
  })
414
461
  },
462
+ {
463
+ prop: "translations",
464
+ label: t("system.translation.field.translations"),
465
+ component: "slot",
466
+ slotName: "translations",
467
+ colSpan: 24
468
+ },
415
469
  {
416
470
  prop: "path",
417
471
  label: getPathFieldLabel(),
@@ -437,7 +491,7 @@ const formFields = computed<ProFormField[]>(() => [
437
491
  },
438
492
  {
439
493
  prop: "meta.icon",
440
- label: "菜单图标",
494
+ label: t("system.menu.field.icon"),
441
495
  component: "slot",
442
496
  slotName: "menuIcon",
443
497
  itemProps: model => ({
@@ -447,65 +501,65 @@ const formFields = computed<ProFormField[]>(() => [
447
501
  },
448
502
  {
449
503
  prop: "meta.icon",
450
- label: "默认图标",
504
+ label: t("system.menu.field.defaultIcon"),
451
505
  component: "input",
452
- labelTooltip: "填写移动端模块注册的图标键或 HTTPS 图片地址。",
506
+ labelTooltip: t("system.menu.tooltip.mobileIcon"),
453
507
  itemProps: model => ({
454
508
  required: Boolean(model.meta?.app?.in_tab_bar)
455
509
  }),
456
- props: { placeholder: "例如 HOME_DEFAULT" },
510
+ props: { placeholder: t("system.menu.placeholder.defaultIcon") },
457
511
  visible: model => isAppMenu(model.parent_id)
458
512
  },
459
513
  {
460
514
  prop: "name",
461
- label: isAppMenu(formData.parent_id) ? "逻辑名称" : "路由名称",
515
+ label: t(isAppMenu(formData.parent_id) ? "system.menu.field.logicalName" : "system.menu.field.routeName"),
462
516
  component: "input",
463
- labelTooltip: isAppMenu(formData.parent_id)
464
- ? "移动端页面使用 App 前缀的唯一逻辑名称。"
465
- : "如果需要开启缓存,需保证页面 defineOptions 中的 name 与此处一致,建议使用驼峰。",
517
+ labelTooltip: isAppMenu(formData.parent_id) ? t("system.menu.tooltip.logicalName") : t("system.menu.tooltip.routeName"),
466
518
  itemProps: model => ({
467
519
  required: model.type === BaseMenuType.MENU
468
520
  }),
469
521
  props: () => ({
470
- placeholder: isAppMenu(formData.parent_id) ? "例如 AppHome" : "请输入路由名称"
522
+ placeholder: isAppMenu(formData.parent_id)
523
+ ? t("system.menu.placeholder.logicalName")
524
+ : t("system.menu.placeholder.routeName")
471
525
  }),
472
526
  visible: model => model.type === BaseMenuType.MENU
473
527
  },
474
528
  {
475
529
  prop: "meta.app.view_key",
476
- label: "视图键",
530
+ label: t("system.menu.field.viewKey"),
477
531
  component: "input",
478
- labelTooltip: "必须与移动端模块注册的稳定 viewKey 一致,不填写组件文件路径。",
532
+ labelTooltip: t("system.menu.tooltip.viewKey"),
479
533
  itemProps: { required: true },
480
- props: { placeholder: "例如 HOME" },
534
+ props: { placeholder: t("system.menu.placeholder.viewKey") },
481
535
  visible: model => isAppMenu(model.parent_id)
482
536
  },
483
537
  {
484
538
  prop: "meta.app.access",
485
- label: "访问模式",
539
+ label: t("system.menu.field.access"),
486
540
  component: "select",
487
- options: appAccessOptions,
541
+ options: appAccessOptions.value,
488
542
  itemProps: { required: true },
489
543
  props: {
490
544
  clearable: false,
491
- placeholder: "请选择访问模式"
545
+ placeholder: t("system.menu.placeholder.access")
492
546
  },
493
547
  visible: model => isAppMenu(model.parent_id)
494
548
  },
495
549
  {
496
550
  prop: "meta.app.selected_icon",
497
- label: "选中图标",
551
+ label: t("system.menu.field.selectedIcon"),
498
552
  component: "input",
499
- labelTooltip: "填写移动端模块注册的图标键或 HTTPS 图片地址。",
553
+ labelTooltip: t("system.menu.tooltip.mobileIcon"),
500
554
  itemProps: { required: true },
501
- props: { placeholder: "例如 HOME_SELECTED" },
555
+ props: { placeholder: t("system.menu.placeholder.selectedIcon") },
502
556
  visible: model => isAppTab(model.parent_id)
503
557
  },
504
558
  {
505
559
  prop: "component",
506
- label: "组件路径",
560
+ label: t("system.menu.field.component"),
507
561
  component: "input",
508
- labelTooltip: "组件页面完整路径,格式为模块名/模块内 views 路径,例如 system/base/user/index,缺省后缀 .vue。",
562
+ labelTooltip: t("system.menu.tooltip.component"),
509
563
  itemProps: model => ({
510
564
  required: model.type === BaseMenuType.MENU && !isAppMenu(model.parent_id)
511
565
  }),
@@ -514,12 +568,12 @@ const formFields = computed<ProFormField[]>(() => [
514
568
  },
515
569
  {
516
570
  prop: "meta.hidden",
517
- label: "是否隐藏",
571
+ label: t("system.menu.field.hidden"),
518
572
  component: "switch",
519
573
  props: {
520
574
  inlinePrompt: true,
521
- activeText: "是",
522
- inactiveText: "否",
575
+ activeText: t("system.common.value.yes"),
576
+ inactiveText: t("system.common.value.no"),
523
577
  activeValue: true,
524
578
  inactiveValue: false
525
579
  },
@@ -527,13 +581,13 @@ const formFields = computed<ProFormField[]>(() => [
527
581
  },
528
582
  {
529
583
  prop: "meta.always_show",
530
- label: "始终显示",
584
+ label: t("system.menu.field.alwaysShow"),
531
585
  component: "switch",
532
- labelTooltip: "选择“是”,即使目录或菜单下只有一个子节点,也会显示父节点;选择“否”,若只有一个子节点则只显示子节点。",
586
+ labelTooltip: t("system.menu.tooltip.alwaysShow"),
533
587
  props: {
534
588
  inlinePrompt: true,
535
- activeText: "是",
536
- inactiveText: "否",
589
+ activeText: t("system.common.value.yes"),
590
+ inactiveText: t("system.common.value.no"),
537
591
  activeValue: true,
538
592
  inactiveValue: false
539
593
  },
@@ -541,12 +595,12 @@ const formFields = computed<ProFormField[]>(() => [
541
595
  },
542
596
  {
543
597
  prop: "meta.keep_alive",
544
- label: "缓存页面",
598
+ label: t("system.menu.field.keepAlive"),
545
599
  component: "switch",
546
600
  props: {
547
601
  inlinePrompt: true,
548
- activeText: "开",
549
- inactiveText: "关",
602
+ activeText: t("common.status.enabled"),
603
+ inactiveText: t("common.status.disabled"),
550
604
  activeValue: true,
551
605
  inactiveValue: false
552
606
  },
@@ -554,12 +608,12 @@ const formFields = computed<ProFormField[]>(() => [
554
608
  },
555
609
  {
556
610
  prop: "meta.full",
557
- label: "全屏模式",
611
+ label: t("system.menu.field.fullscreen"),
558
612
  component: "switch",
559
613
  props: {
560
614
  inlinePrompt: true,
561
- activeText: "开",
562
- inactiveText: "关",
615
+ activeText: t("common.status.enabled"),
616
+ inactiveText: t("common.status.disabled"),
563
617
  activeValue: true,
564
618
  inactiveValue: false
565
619
  },
@@ -567,12 +621,12 @@ const formFields = computed<ProFormField[]>(() => [
567
621
  },
568
622
  {
569
623
  prop: "meta.affix",
570
- label: "固定标签",
624
+ label: t("system.menu.field.affix"),
571
625
  component: "switch",
572
626
  props: {
573
627
  inlinePrompt: true,
574
- activeText: "开",
575
- inactiveText: "关",
628
+ activeText: t("common.status.enabled"),
629
+ inactiveText: t("common.status.disabled"),
576
630
  activeValue: true,
577
631
  inactiveValue: false
578
632
  },
@@ -580,13 +634,13 @@ const formFields = computed<ProFormField[]>(() => [
580
634
  },
581
635
  {
582
636
  prop: "meta.params",
583
- label: "路由参数",
637
+ label: t("system.menu.field.routeParams"),
584
638
  component: "kv-list",
585
- labelTooltip: "组件页面使用 useRoute().query.参数名 获取路由参数值。",
639
+ labelTooltip: t("system.menu.tooltip.routeParams"),
586
640
  props: {
587
- addText: "添加路由参数",
588
- keyInputProps: { placeholder: "参数名" },
589
- valueInputProps: { placeholder: "参数值" }
641
+ addText: t("system.menu.action.addRouteParam"),
642
+ keyInputProps: { placeholder: t("common.field.parameterName") },
643
+ valueInputProps: { placeholder: t("common.field.parameterValue") }
590
644
  },
591
645
  itemProps: {
592
646
  class: "menu-form__params"
@@ -595,20 +649,20 @@ const formFields = computed<ProFormField[]>(() => [
595
649
  },
596
650
  {
597
651
  prop: "api",
598
- label: "API 列表",
652
+ label: t("system.menu.field.apiList"),
599
653
  component: "transfer",
600
654
  slotName: "apiTransferItem",
601
655
  options: transferData.value,
602
656
  props: {
603
657
  filterable: true,
604
- titles: ["可选 API", "已选 API"]
658
+ titles: [t("system.menu.value.availableApi"), t("system.menu.value.selectedApi")]
605
659
  },
606
660
  visible: model => model.id === APP_MENU_ROOT_ID || model.type === BaseMenuType.MENU || model.type === BaseMenuType.BUTTON,
607
661
  colSpan: 24
608
662
  },
609
663
  {
610
664
  prop: "sort",
611
- label: "排序",
665
+ label: t("system.common.field.sort"),
612
666
  component: "input-number",
613
667
  itemProps: {
614
668
  required: true
@@ -623,23 +677,35 @@ const formFields = computed<ProFormField[]>(() => [
623
677
  },
624
678
  {
625
679
  prop: "status",
626
- label: "状态",
680
+ label: t("system.common.field.status"),
627
681
  component: "radio-group",
628
682
  itemProps: {
629
683
  required: true
630
684
  },
631
- options: statusOptions
685
+ options: statusOptions.value
632
686
  }
633
687
  ]);
634
688
 
635
689
  const rules = computed<FormRules>(() => ({
636
- parent_id: formData.id ? [] : [{ required: true, type: "number", min: 1, message: "请选择上级菜单", trigger: "change" }],
637
- type: [{ required: true, message: "请选择菜单类型", trigger: "change" }],
690
+ parent_id: formData.id
691
+ ? []
692
+ : [{ required: true, type: "number", min: 1, message: t("system.menu.placeholder.parent"), trigger: "change" }],
693
+ type: [
694
+ {
695
+ required: true,
696
+ message: t("system.common.validation.requiredSelect", { field: t("system.menu.field.type") }),
697
+ trigger: "change"
698
+ }
699
+ ],
638
700
  "meta.title": [
639
701
  {
640
702
  validator: (_rule, value, callback) => {
641
703
  if (value) return callback();
642
- callback(new Error(formData.type === BaseMenuType.BUTTON ? "请输入按钮名称" : "请输入菜单标题"));
704
+ callback(
705
+ new Error(
706
+ t(formData.type === BaseMenuType.BUTTON ? "system.menu.placeholder.buttonName" : "system.menu.placeholder.title")
707
+ )
708
+ );
643
709
  },
644
710
  trigger: "blur"
645
711
  }
@@ -650,7 +716,9 @@ const rules = computed<FormRules>(() => ({
650
716
  if (formData.type === BaseMenuType.BUTTON) return callback();
651
717
  if (isAppMenu(formData.parent_id) && !isAppTab(formData.parent_id)) return callback();
652
718
  if (value) return callback();
653
- callback(new Error(isAppMenu(formData.parent_id) ? "请输入默认图标" : "请选择菜单图标"));
719
+ callback(
720
+ new Error(t(isAppMenu(formData.parent_id) ? "system.menu.validation.defaultIcon" : "system.menu.placeholder.icon"))
721
+ );
654
722
  },
655
723
  trigger: "change"
656
724
  }
@@ -659,7 +727,7 @@ const rules = computed<FormRules>(() => ({
659
727
  {
660
728
  validator: (_rule, value, callback) => {
661
729
  if (!isAppMenu(formData.parent_id) || value) return callback();
662
- callback(new Error("请输入视图键"));
730
+ callback(new Error(t("system.menu.validation.viewKey")));
663
731
  },
664
732
  trigger: "blur"
665
733
  }
@@ -668,7 +736,7 @@ const rules = computed<FormRules>(() => ({
668
736
  {
669
737
  validator: (_rule, value, callback) => {
670
738
  if (!isAppMenu(formData.parent_id) || value) return callback();
671
- callback(new Error("请选择访问模式"));
739
+ callback(new Error(t("system.menu.placeholder.access")));
672
740
  },
673
741
  trigger: "change"
674
742
  }
@@ -677,23 +745,31 @@ const rules = computed<FormRules>(() => ({
677
745
  {
678
746
  validator: (_rule, value, callback) => {
679
747
  if (!isAppTab(formData.parent_id) || value) return callback();
680
- callback(new Error("请输入选中图标"));
748
+ callback(new Error(t("system.menu.validation.selectedIcon")));
681
749
  },
682
750
  trigger: "blur"
683
751
  }
684
752
  ],
685
753
  path: [
686
- { max: 1024, message: "路径不能超过 1024 个字符", trigger: "blur" },
754
+ {
755
+ max: 1024,
756
+ message: t("system.common.validation.maxLength", { field: t("system.menu.field.path"), max: 1024 }),
757
+ trigger: "blur"
758
+ },
687
759
  {
688
760
  validator: (_rule, value, callback) => {
689
761
  if (formData.type === BaseMenuType.FOLDER) return callback();
690
762
  if (!value) {
691
- if (formData.type === BaseMenuType.BUTTON) return callback(new Error("请输入权限标识"));
692
- if (formData.type === BaseMenuType.EXT_LINK) return callback(new Error("请输入完整外链地址"));
693
- return callback(new Error(isAppMenu(formData.parent_id) ? "请输入逻辑路径" : "请输入路由路径"));
763
+ if (formData.type === BaseMenuType.BUTTON) return callback(new Error(t("system.menu.validation.permission")));
764
+ if (formData.type === BaseMenuType.EXT_LINK) return callback(new Error(t("system.menu.validation.externalUrl")));
765
+ return callback(
766
+ new Error(
767
+ t(isAppMenu(formData.parent_id) ? "system.menu.validation.logicalPath" : "system.menu.validation.routePath")
768
+ )
769
+ );
694
770
  }
695
771
  if (isAppMenu(formData.parent_id) && !String(value).startsWith("app/")) {
696
- return callback(new Error("移动端逻辑路径必须使用 app/ 前缀"));
772
+ return callback(new Error(t("system.menu.validation.appPathPrefix")));
697
773
  }
698
774
  callback();
699
775
  },
@@ -701,24 +777,37 @@ const rules = computed<FormRules>(() => ({
701
777
  }
702
778
  ],
703
779
  redirect: [
704
- { max: 1024, message: "重定向地址不能超过 1024 个字符", trigger: "blur" },
780
+ {
781
+ max: 1024,
782
+ message: t("system.common.validation.maxLength", { field: t("system.menu.field.redirect"), max: 1024 }),
783
+ trigger: "blur"
784
+ },
705
785
  {
706
786
  validator: (_rule, value, callback) => {
707
787
  if (formData.type !== BaseMenuType.EXT_LINK) return callback();
708
788
  if (/^https?:\/\/\S+$/.test(value)) return callback();
709
- callback(new Error("请输入完整的 HTTP 外链地址"));
789
+ callback(new Error(t("system.menu.validation.httpUrl")));
710
790
  },
711
791
  trigger: "blur"
712
792
  }
713
793
  ],
714
794
  name: [
715
- { max: 255, message: "路由名称不能超过 255 个字符", trigger: "blur" },
795
+ {
796
+ max: 255,
797
+ message: t("system.common.validation.maxLength", { field: t("system.menu.field.routeName"), max: 255 }),
798
+ trigger: "blur"
799
+ },
716
800
  {
717
801
  validator: (_rule, value, callback) => {
718
802
  if (formData.type !== BaseMenuType.MENU) return callback();
719
- if (!value) return callback(new Error(isAppMenu(formData.parent_id) ? "请输入逻辑名称" : "请输入路由名称"));
803
+ if (!value)
804
+ return callback(
805
+ new Error(
806
+ t(isAppMenu(formData.parent_id) ? "system.menu.validation.logicalName" : "system.menu.placeholder.routeName")
807
+ )
808
+ );
720
809
  if (isAppMenu(formData.parent_id) && !String(value).startsWith("App")) {
721
- return callback(new Error("移动端逻辑名称必须使用 App 前缀"));
810
+ return callback(new Error(t("system.menu.validation.appNamePrefix")));
722
811
  }
723
812
  callback();
724
813
  },
@@ -726,32 +815,42 @@ const rules = computed<FormRules>(() => ({
726
815
  }
727
816
  ],
728
817
  component: [
729
- { max: 255, message: "组件路径不能超过 255 个字符", trigger: "blur" },
818
+ {
819
+ max: 255,
820
+ message: t("system.common.validation.maxLength", { field: t("system.menu.field.component"), max: 255 }),
821
+ trigger: "blur"
822
+ },
730
823
  {
731
824
  validator: (_rule, value, callback) => {
732
825
  if (formData.type !== BaseMenuType.MENU || isAppMenu(formData.parent_id)) return callback();
733
826
  if (value) return callback();
734
- callback(new Error("请输入组件路径"));
827
+ callback(new Error(t("system.menu.validation.component")));
735
828
  },
736
829
  trigger: "blur"
737
830
  }
738
831
  ],
739
- sort: [{ required: true, type: "number", min: 1, message: "排序必须大于 0", trigger: "blur" }],
740
- status: [{ required: true, message: "请选择状态", trigger: "change" }]
832
+ sort: [{ required: true, type: "number", min: 1, message: t("system.common.validation.sortPositive"), trigger: "blur" }],
833
+ status: [
834
+ {
835
+ required: true,
836
+ message: t("system.common.validation.requiredSelect", { field: t("system.common.field.status") }),
837
+ trigger: "change"
838
+ }
839
+ ]
741
840
  }));
742
841
 
743
842
  /** 计算当前路径字段文案。 */
744
843
  function getPathFieldLabel() {
745
- if (isAppMenu(formData.parent_id)) return "逻辑路径";
746
- if (formData.type === BaseMenuType.BUTTON) return "权限标识";
747
- if (formData.type === BaseMenuType.EXT_LINK) return "内部路径";
748
- return "路由路径";
844
+ if (isAppMenu(formData.parent_id)) return t("system.menu.field.logicalPath");
845
+ if (formData.type === BaseMenuType.BUTTON) return t("system.menu.field.permission");
846
+ if (formData.type === BaseMenuType.EXT_LINK) return t("system.menu.field.internalPath");
847
+ return t("system.menu.field.path");
749
848
  }
750
849
 
751
850
  /** 计算当前路径字段占位文案。 */
752
851
  function getPathFieldPlaceholder() {
753
- if (isAppMenu(formData.parent_id)) return "例如 app/home";
754
- if (formData.type === BaseMenuType.BUTTON) return "请输入按钮权限标识";
852
+ if (isAppMenu(formData.parent_id)) return t("system.menu.placeholder.logicalPath");
853
+ if (formData.type === BaseMenuType.BUTTON) return t("system.menu.placeholder.permission");
755
854
  if (formData.type === BaseMenuType.EXT_LINK) return "external/baidu";
756
855
  if (formData.type === BaseMenuType.FOLDER) return "base";
757
856
  return "user";
@@ -759,21 +858,21 @@ function getPathFieldPlaceholder() {
759
858
 
760
859
  /** 计算当前路径字段提示文案。 */
761
860
  function getPathFieldTooltip() {
762
- if (isAppMenu(formData.parent_id)) return "使用 app/ 前缀的稳定逻辑路径,不填写 uni-app 物理页面路径。";
763
- if (formData.type === BaseMenuType.BUTTON) return "按钮类型菜单使用权限标识,例如 base:user:create。";
764
- if (formData.type === BaseMenuType.EXT_LINK) return "外链使用内部唯一路径,例如 external/baidu;实际地址填写在外链地址中。";
765
- if (formData.type === BaseMenuType.FOLDER) return "目录不填写路径,仅用于组织下级菜单。";
766
- return "菜单路径通常不带 /,例如 user。";
861
+ if (isAppMenu(formData.parent_id)) return t("system.menu.tooltip.logicalPath");
862
+ if (formData.type === BaseMenuType.BUTTON) return t("system.menu.tooltip.permission");
863
+ if (formData.type === BaseMenuType.EXT_LINK) return t("system.menu.tooltip.internalPath");
864
+ if (formData.type === BaseMenuType.FOLDER) return t("system.menu.tooltip.folderPath");
865
+ return t("system.menu.tooltip.routePath");
767
866
  }
768
867
 
769
868
  /** 计算重定向字段文案。 */
770
869
  function getRedirectFieldLabel() {
771
- return formData.type === BaseMenuType.EXT_LINK ? "外链地址" : "跳转路由";
870
+ return t(formData.type === BaseMenuType.EXT_LINK ? "system.menu.field.externalUrl" : "system.menu.field.redirectRoute");
772
871
  }
773
872
 
774
873
  /** 计算重定向字段占位文案。 */
775
874
  function getRedirectFieldPlaceholder() {
776
- return formData.type === BaseMenuType.EXT_LINK ? "https://www.baidu.com/" : "请输入跳转路由";
875
+ return formData.type === BaseMenuType.EXT_LINK ? "https://www.example.com/" : t("system.menu.placeholder.redirectRoute");
777
876
  }
778
877
 
779
878
  /** 判断当前图标是否为 Element Plus 图标。 */
@@ -863,6 +962,7 @@ function buildMenuOptions(menuList: BaseMenu[] = []) {
863
962
  /** 根据菜单类型清理无效字段,避免提交脏数据。 */
864
963
  function buildSubmitPayload(): BaseMenuForm {
865
964
  const payload = normalizeMenuForm(formData);
965
+ payload.translations = serializeDynamicTranslations(translationValues.value, "title") as unknown as BaseMenuTranslation[];
866
966
  // 一级菜单在表单中保持空白,提交时仍按接口约定传回根节点标识。
867
967
  if (payload.id > 0 && payload.parent_id === undefined) payload.parent_id = 0;
868
968
  payload.meta.params = (payload.meta.params ?? []).filter(item => item.key || item.value);
@@ -953,6 +1053,7 @@ function filterMenuTree(menuList: BaseMenu[], keywordMap: Record<string, string>
953
1053
 
954
1054
  /** 请求菜单表格数据,并按搜索条件过滤树形结构。 */
955
1055
  async function requestMenuTable(params: Record<string, string>) {
1056
+ await loadEnabledBaseLanguages();
956
1057
  const data = await defBaseMenuService.TreeBaseMenu({});
957
1058
  const keywordMap = {
958
1059
  title: params.title ?? "",
@@ -975,20 +1076,19 @@ function refreshTable() {
975
1076
  * parentMenu 为新增时的固定父节点,menuId 为编辑时的菜单 ID。
976
1077
  */
977
1078
  async function handleOpenDialog(parentMenu?: BaseMenu, menuId?: number) {
1079
+ await loadEnabledBaseLanguages();
978
1080
  await loadDialogResources();
979
1081
  dialog.parentLocked = Boolean(parentMenu || menuId);
1082
+ dialog.editing = Boolean(menuId);
980
1083
  dialog.parentType = parentMenu?.type ?? BaseMenuType.UNKNOWN_MT;
981
1084
  resetForm(menuId ? undefined : { parent_id: parentMenu?.id });
982
1085
  dialog.visible = true;
983
1086
 
984
1087
  if (menuId) {
985
- dialog.title = "修改菜单";
986
1088
  const data = await defBaseMenuService.GetBaseMenu({ id: menuId });
987
1089
  resetForm(data);
988
1090
  return;
989
1091
  }
990
-
991
- dialog.title = "新增菜单";
992
1092
  }
993
1093
 
994
1094
  /** 关闭菜单弹窗并显式重置表单与校验状态。 */
@@ -1004,6 +1104,7 @@ function resetForm(data?: Partial<BaseMenuForm>) {
1004
1104
  formDialogRef.value?.resetFields();
1005
1105
  formDialogRef.value?.clearValidate();
1006
1106
  Object.assign(formData, normalizeMenuForm(data));
1107
+ translationValues.value = normalizeDynamicTranslations(data?.translations as DynamicTranslationRecord[] | undefined, "title");
1007
1108
  }
1008
1109
 
1009
1110
  /** 提交菜单表单,并在成功后关闭弹窗、刷新表格。 */
@@ -1014,10 +1115,10 @@ async function handleSubmit() {
1014
1115
  const payload = buildSubmitPayload();
1015
1116
  if (payload.id > 0) {
1016
1117
  await defBaseMenuService.UpdateBaseMenu({ base_menu: payload });
1017
- ElMessage.success("菜单更新成功");
1118
+ ElMessage.success(t("system.menu.message.updateSuccess"));
1018
1119
  } else {
1019
1120
  await defBaseMenuService.CreateBaseMenu({ base_menu: payload });
1020
- ElMessage.success("菜单创建成功");
1121
+ ElMessage.success(t("system.menu.message.createSuccess"));
1021
1122
  }
1022
1123
 
1023
1124
  handleCloseDialog();
@@ -1029,16 +1130,16 @@ async function handleSubmit() {
1029
1130
  */
1030
1131
  async function handleBeforeSetStatus(row: BaseMenu) {
1031
1132
  const nextStatus = row.status === Status.ENABLE ? Status.DISABLE : Status.ENABLE;
1032
- const text = nextStatus === Status.ENABLE ? "启用" : "禁用";
1133
+ const action = t(nextStatus === Status.ENABLE ? "common.status.enabled" : "common.status.disabled");
1033
1134
  const menuName = row.meta?.title || row.name || row.path || `ID:${row.id}`;
1034
1135
  try {
1035
- await ElMessageBox.confirm(`是否确定${text}菜单?\n菜单名称:${menuName}`, "提示", {
1036
- confirmButtonText: "确认",
1037
- cancelButtonText: "取消",
1136
+ await ElMessageBox.confirm(t("system.menu.message.confirmStatus", { action, name: menuName }), t("common.title.notice"), {
1137
+ confirmButtonText: t("common.action.confirm"),
1138
+ cancelButtonText: t("common.action.cancel"),
1038
1139
  type: "warning"
1039
1140
  });
1040
1141
  await defBaseMenuService.SetBaseMenuStatus({ id: row.id, status: nextStatus });
1041
- ElMessage.success(`${text}成功`);
1142
+ ElMessage.success(t("system.common.message.statusSuccess", { action }));
1042
1143
  refreshTable();
1043
1144
  return true;
1044
1145
  } catch {
@@ -1056,37 +1157,37 @@ function handleDeleteMenu(selected?: number | string | Array<number | string> |
1056
1157
  ? [selected as BaseMenu]
1057
1158
  : [];
1058
1159
  if (menuList.some(item => item.parent_id === 0 || item.id === APP_MENU_ROOT_ID)) {
1059
- ElMessage.warning("固定菜单不允许删除");
1160
+ ElMessage.warning(t("system.menu.message.protectedDelete"));
1060
1161
  return;
1061
1162
  }
1062
1163
  const menuIds = (
1063
1164
  menuList.length ? menuList.map(item => item.id) : normalizeSelectedIds(selected as number | string | Array<number | string>)
1064
1165
  ).join(",");
1065
1166
  if (!menuIds) {
1066
- ElMessage.warning("请勾选删除项");
1167
+ ElMessage.warning(t("system.common.message.selectDeleteItem"));
1067
1168
  return;
1068
1169
  }
1069
1170
 
1070
1171
  const singleMenuName = menuList[0]?.meta?.title || menuList[0]?.name || menuList[0]?.path || `ID:${menuList[0]?.id ?? ""}`;
1071
1172
  const confirmMessage = menuList.length
1072
1173
  ? menuList.length === 1
1073
- ? `是否确定删除菜单?\n菜单名称:${singleMenuName}`
1074
- : `确认删除已选中的 ${menuList.length} 个菜单项吗?`
1075
- : "确认删除已选中的菜单项吗?";
1174
+ ? t("system.menu.message.confirmDeleteSingle", { name: singleMenuName })
1175
+ : t("system.menu.message.confirmDeleteBatch", { count: menuList.length })
1176
+ : t("system.menu.message.confirmDeleteSelected");
1076
1177
 
1077
- ElMessageBox.confirm(confirmMessage, "警告", {
1078
- confirmButtonText: "确定",
1079
- cancelButtonText: "取消",
1178
+ ElMessageBox.confirm(confirmMessage, t("common.title.warning"), {
1179
+ confirmButtonText: t("common.action.confirm"),
1180
+ cancelButtonText: t("common.action.cancel"),
1080
1181
  type: "warning"
1081
1182
  }).then(
1082
1183
  () => {
1083
1184
  defBaseMenuService.DeleteBaseMenu({ id: menuIds }).then(() => {
1084
- ElMessage.success("删除菜单成功");
1185
+ ElMessage.success(t("system.menu.message.deleteSuccess"));
1085
1186
  refreshTable();
1086
1187
  });
1087
1188
  },
1088
1189
  () => {
1089
- ElMessage.info("已取消删除菜单");
1190
+ ElMessage.info(t("system.menu.message.deleteCanceled"));
1090
1191
  }
1091
1192
  );
1092
1193
  }