@liujitcn/kratos-admin-system 0.0.20 → 0.0.23

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