@liujitcn/kratos-admin-system 0.0.1

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 (208) hide show
  1. package/README.md +137 -0
  2. package/dist/package/declarations/index.d.ts +1 -0
  3. package/dist/package/declarations/src/ai.d.ts +10 -0
  4. package/dist/package/declarations/src/api/base/ai_message.d.ts +27 -0
  5. package/dist/package/declarations/src/api/base/ai_session.d.ts +17 -0
  6. package/dist/package/declarations/src/api/base/ai_tool.d.ts +7 -0
  7. package/dist/package/declarations/src/api/base/oauth.d.ts +13 -0
  8. package/dist/package/declarations/src/api/base/sse.d.ts +31 -0
  9. package/dist/package/declarations/src/api/system/auth.d.ts +17 -0
  10. package/dist/package/declarations/src/api/system/base_api.d.ts +22 -0
  11. package/dist/package/declarations/src/api/system/base_area.d.ts +19 -0
  12. package/dist/package/declarations/src/api/system/base_config.d.ts +20 -0
  13. package/dist/package/declarations/src/api/system/base_dept.d.ts +21 -0
  14. package/dist/package/declarations/src/api/system/base_dict.d.ts +32 -0
  15. package/dist/package/declarations/src/api/system/base_job.d.ts +28 -0
  16. package/dist/package/declarations/src/api/system/base_log.d.ts +9 -0
  17. package/dist/package/declarations/src/api/system/base_menu.d.ts +21 -0
  18. package/dist/package/declarations/src/api/system/base_migration.d.ts +10 -0
  19. package/dist/package/declarations/src/api/system/base_post.d.ts +21 -0
  20. package/dist/package/declarations/src/api/system/base_role.d.ts +23 -0
  21. package/dist/package/declarations/src/api/system/base_tenant.d.ts +21 -0
  22. package/dist/package/declarations/src/api/system/base_user.d.ts +23 -0
  23. package/dist/package/declarations/src/api/system/code_gen.d.ts +14 -0
  24. package/dist/package/declarations/src/api/system/code_gen_column.d.ts +12 -0
  25. package/dist/package/declarations/src/api/system/code_gen_proto.d.ts +10 -0
  26. package/dist/package/declarations/src/api/system/code_gen_sse.d.ts +8 -0
  27. package/dist/package/declarations/src/api/system/code_gen_table.d.ts +18 -0
  28. package/dist/package/declarations/src/api/system/project_document.d.ts +9 -0
  29. package/dist/package/declarations/src/components/Ai.vue.d.ts +3 -0
  30. package/dist/package/declarations/src/index.d.ts +3 -0
  31. package/dist/package/declarations/src/module.d.ts +2 -0
  32. package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +82 -0
  33. package/dist/package/declarations/src/rpc/base/v1/ai_session.d.ts +176 -0
  34. package/dist/package/declarations/src/rpc/base/v1/ai_tool.d.ts +59 -0
  35. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +167 -0
  36. package/dist/package/declarations/src/rpc/base/v1/sse.d.ts +13 -0
  37. package/dist/package/declarations/src/rpc/common/v1/common.d.ts +57 -0
  38. package/dist/package/declarations/src/rpc/common/v1/enum.d.ts +58 -0
  39. package/dist/package/declarations/src/rpc/common/v1/types.d.ts +60 -0
  40. package/dist/package/declarations/src/rpc/google/protobuf/empty.d.ts +11 -0
  41. package/dist/package/declarations/src/rpc/google/protobuf/timestamp.d.ts +107 -0
  42. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +156 -0
  43. package/dist/package/declarations/src/rpc/system/admin/v1/base_api.d.ts +193 -0
  44. package/dist/package/declarations/src/rpc/system/admin/v1/base_area.d.ts +82 -0
  45. package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +112 -0
  46. package/dist/package/declarations/src/rpc/system/admin/v1/base_dept.d.ts +112 -0
  47. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +215 -0
  48. package/dist/package/declarations/src/rpc/system/admin/v1/base_job.d.ts +177 -0
  49. package/dist/package/declarations/src/rpc/system/admin/v1/base_log.d.ts +87 -0
  50. package/dist/package/declarations/src/rpc/system/admin/v1/base_menu.d.ts +151 -0
  51. package/dist/package/declarations/src/rpc/system/admin/v1/base_migration.d.ts +64 -0
  52. package/dist/package/declarations/src/rpc/system/admin/v1/base_post.d.ts +112 -0
  53. package/dist/package/declarations/src/rpc/system/admin/v1/base_role.d.ts +128 -0
  54. package/dist/package/declarations/src/rpc/system/admin/v1/base_tenant.d.ts +112 -0
  55. package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +161 -0
  56. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen.d.ts +155 -0
  57. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_column.d.ts +143 -0
  58. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_proto.d.ts +86 -0
  59. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_table.d.ts +149 -0
  60. package/dist/package/declarations/src/rpc/system/admin/v1/project_document.d.ts +64 -0
  61. package/dist/package/declarations/src/rpc/system/common/v1/enum.d.ts +67 -0
  62. package/dist/package/declarations/src/views/ai/chat/attachment.d.ts +14 -0
  63. package/dist/package/declarations/src/views/ai/chat/components/AiMarkdown.vue.d.ts +13 -0
  64. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +45 -0
  65. package/dist/package/declarations/src/views/ai/chat/components/SessionPanel.vue.d.ts +34 -0
  66. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +12 -0
  67. package/dist/package/declarations/src/views/ai/chat/index.vue.d.ts +4 -0
  68. package/dist/package/declarations/src/views/ai/chat/message.d.ts +59 -0
  69. package/dist/package/declarations/src/views/ai/chat/stream.d.ts +10 -0
  70. package/dist/package/declarations/src/views/ai/chat/types.d.ts +115 -0
  71. package/dist/package/declarations/src/views/base/api/index.vue.d.ts +3 -0
  72. package/dist/package/declarations/src/views/base/area/index.vue.d.ts +3 -0
  73. package/dist/package/declarations/src/views/base/config/index.vue.d.ts +3 -0
  74. package/dist/package/declarations/src/views/base/dept/index.vue.d.ts +3 -0
  75. package/dist/package/declarations/src/views/base/dict/index.vue.d.ts +3 -0
  76. package/dist/package/declarations/src/views/base/dict/item.vue.d.ts +3 -0
  77. package/dist/package/declarations/src/views/base/job/index.vue.d.ts +3 -0
  78. package/dist/package/declarations/src/views/base/job/log.vue.d.ts +3 -0
  79. package/dist/package/declarations/src/views/base/log/index.vue.d.ts +3 -0
  80. package/dist/package/declarations/src/views/base/menu/index.vue.d.ts +3 -0
  81. package/dist/package/declarations/src/views/base/migration/index.vue.d.ts +4 -0
  82. package/dist/package/declarations/src/views/base/post/index.vue.d.ts +3 -0
  83. package/dist/package/declarations/src/views/base/role/index.vue.d.ts +3 -0
  84. package/dist/package/declarations/src/views/base/tenant/index.vue.d.ts +3 -0
  85. package/dist/package/declarations/src/views/base/user/components/dept-tree.vue.d.ts +19 -0
  86. package/dist/package/declarations/src/views/base/user/index.vue.d.ts +3 -0
  87. package/dist/package/declarations/src/views/profile/components/base.vue.d.ts +13 -0
  88. package/dist/package/declarations/src/views/profile/components/password.vue.d.ts +3 -0
  89. package/dist/package/declarations/src/views/profile/components/security.vue.d.ts +15 -0
  90. package/dist/package/declarations/src/views/profile/index.vue.d.ts +3 -0
  91. package/dist/package/declarations/src/views/tool/api-doc/index.vue.d.ts +4 -0
  92. package/dist/package/declarations/src/views/tool/code-gen/code-preview/index.vue.d.ts +3 -0
  93. package/dist/package/declarations/src/views/tool/code-gen/columns/index.vue.d.ts +3 -0
  94. package/dist/package/declarations/src/views/tool/code-gen/columns/option-copy.d.ts +25 -0
  95. package/dist/package/declarations/src/views/tool/code-gen/components/CodeGenProgressDialog.vue.d.ts +16 -0
  96. package/dist/package/declarations/src/views/tool/code-gen/components/CodePreviewPane.vue.d.ts +9 -0
  97. package/dist/package/declarations/src/views/tool/code-gen/config.d.ts +32 -0
  98. package/dist/package/declarations/src/views/tool/code-gen/preview/capabilities.d.ts +20 -0
  99. package/dist/package/declarations/src/views/tool/code-gen/preview/data.d.ts +42 -0
  100. package/dist/package/declarations/src/views/tool/code-gen/preview/index.vue.d.ts +3 -0
  101. package/dist/package/declarations/src/views/tool/code-gen/proto/index.vue.d.ts +3 -0
  102. package/dist/package/declarations/src/views/tool/code-gen/table/index.vue.d.ts +3 -0
  103. package/dist/package/declarations/src/views/tool/project-doc/index.vue.d.ts +4 -0
  104. package/dist/package/src/ai.ts +16 -0
  105. package/dist/package/src/api/base/ai_message.ts +139 -0
  106. package/dist/package/src/api/base/ai_session.ts +77 -0
  107. package/dist/package/src/api/base/ai_tool.ts +22 -0
  108. package/dist/package/src/api/base/oauth.ts +47 -0
  109. package/dist/package/src/api/base/sse.ts +192 -0
  110. package/dist/package/src/api/system/auth.ts +65 -0
  111. package/dist/package/src/api/system/base_api.ts +95 -0
  112. package/dist/package/src/api/system/base_area.ts +73 -0
  113. package/dist/package/src/api/system/base_config.ts +82 -0
  114. package/dist/package/src/api/system/base_dept.ts +83 -0
  115. package/dist/package/src/api/system/base_dict.ts +144 -0
  116. package/dist/package/src/api/system/base_job.ts +125 -0
  117. package/dist/package/src/api/system/base_log.ts +32 -0
  118. package/dist/package/src/api/system/base_menu.ts +83 -0
  119. package/dist/package/src/api/system/base_migration.ts +35 -0
  120. package/dist/package/src/api/system/base_post.ts +83 -0
  121. package/dist/package/src/api/system/base_role.ts +93 -0
  122. package/dist/package/src/api/system/base_tenant.ts +83 -0
  123. package/dist/package/src/api/system/base_user.ts +93 -0
  124. package/dist/package/src/api/system/code_gen.ts +56 -0
  125. package/dist/package/src/api/system/code_gen_column.ts +43 -0
  126. package/dist/package/src/api/system/code_gen_proto.ts +32 -0
  127. package/dist/package/src/api/system/code_gen_sse.ts +33 -0
  128. package/dist/package/src/api/system/code_gen_table.ts +74 -0
  129. package/dist/package/src/api/system/project_document.ts +33 -0
  130. package/dist/package/src/components/Ai.vue +57 -0
  131. package/dist/package/src/index.ts +3 -0
  132. package/dist/package/src/module.ts +17 -0
  133. package/dist/package/src/rpc/base/v1/ai_message.ts +98 -0
  134. package/dist/package/src/rpc/base/v1/ai_session.ts +214 -0
  135. package/dist/package/src/rpc/base/v1/ai_tool.ts +74 -0
  136. package/dist/package/src/rpc/base/v1/oauth.ts +199 -0
  137. package/dist/package/src/rpc/base/v1/sse.ts +22 -0
  138. package/dist/package/src/rpc/common/v1/common.ts +71 -0
  139. package/dist/package/src/rpc/common/v1/enum.ts +71 -0
  140. package/dist/package/src/rpc/common/v1/types.ts +77 -0
  141. package/dist/package/src/rpc/google/protobuf/empty.ts +19 -0
  142. package/dist/package/src/rpc/google/protobuf/timestamp.ts +115 -0
  143. package/dist/package/src/rpc/system/admin/v1/auth.ts +208 -0
  144. package/dist/package/src/rpc/system/admin/v1/base_api.ts +241 -0
  145. package/dist/package/src/rpc/system/admin/v1/base_area.ts +105 -0
  146. package/dist/package/src/rpc/system/admin/v1/base_config.ts +140 -0
  147. package/dist/package/src/rpc/system/admin/v1/base_dept.ts +140 -0
  148. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +249 -0
  149. package/dist/package/src/rpc/system/admin/v1/base_job.ts +206 -0
  150. package/dist/package/src/rpc/system/admin/v1/base_log.ts +101 -0
  151. package/dist/package/src/rpc/system/admin/v1/base_menu.ts +195 -0
  152. package/dist/package/src/rpc/system/admin/v1/base_migration.ts +81 -0
  153. package/dist/package/src/rpc/system/admin/v1/base_post.ts +134 -0
  154. package/dist/package/src/rpc/system/admin/v1/base_role.ts +151 -0
  155. package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +134 -0
  156. package/dist/package/src/rpc/system/admin/v1/base_user.ts +190 -0
  157. package/dist/package/src/rpc/system/admin/v1/code_gen.ts +176 -0
  158. package/dist/package/src/rpc/system/admin/v1/code_gen_column.ts +170 -0
  159. package/dist/package/src/rpc/system/admin/v1/code_gen_proto.ts +104 -0
  160. package/dist/package/src/rpc/system/admin/v1/code_gen_table.ts +179 -0
  161. package/dist/package/src/rpc/system/admin/v1/project_document.ts +79 -0
  162. package/dist/package/src/rpc/system/common/v1/enum.ts +80 -0
  163. package/dist/package/src/typings/swagger-ui-dist.d.ts +28 -0
  164. package/dist/package/src/typings/x-markdown-vue.d.ts +1 -0
  165. package/dist/package/src/views/ai/chat/attachment.ts +65 -0
  166. package/dist/package/src/views/ai/chat/components/AiMarkdown.vue +162 -0
  167. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +1861 -0
  168. package/dist/package/src/views/ai/chat/components/SessionPanel.vue +316 -0
  169. package/dist/package/src/views/ai/chat/components/XSender.vue +554 -0
  170. package/dist/package/src/views/ai/chat/index.vue +848 -0
  171. package/dist/package/src/views/ai/chat/message.ts +416 -0
  172. package/dist/package/src/views/ai/chat/stream.ts +115 -0
  173. package/dist/package/src/views/ai/chat/types.ts +126 -0
  174. package/dist/package/src/views/base/api/index.vue +455 -0
  175. package/dist/package/src/views/base/area/index.vue +269 -0
  176. package/dist/package/src/views/base/config/index.vue +530 -0
  177. package/dist/package/src/views/base/dept/index.vue +461 -0
  178. package/dist/package/src/views/base/dict/index.vue +304 -0
  179. package/dist/package/src/views/base/dict/item.vue +373 -0
  180. package/dist/package/src/views/base/job/index.vue +535 -0
  181. package/dist/package/src/views/base/job/log.vue +202 -0
  182. package/dist/package/src/views/base/log/index.vue +317 -0
  183. package/dist/package/src/views/base/menu/index.vue +944 -0
  184. package/dist/package/src/views/base/migration/index.vue +791 -0
  185. package/dist/package/src/views/base/post/index.vue +324 -0
  186. package/dist/package/src/views/base/role/index.vue +684 -0
  187. package/dist/package/src/views/base/tenant/index.vue +328 -0
  188. package/dist/package/src/views/base/user/components/dept-tree.vue +75 -0
  189. package/dist/package/src/views/base/user/index.vue +783 -0
  190. package/dist/package/src/views/profile/components/base.vue +320 -0
  191. package/dist/package/src/views/profile/components/password.vue +269 -0
  192. package/dist/package/src/views/profile/components/security.vue +443 -0
  193. package/dist/package/src/views/profile/index.vue +199 -0
  194. package/dist/package/src/views/tool/api-doc/index.vue +275 -0
  195. package/dist/package/src/views/tool/code-gen/code-preview/index.vue +233 -0
  196. package/dist/package/src/views/tool/code-gen/columns/index.vue +1456 -0
  197. package/dist/package/src/views/tool/code-gen/columns/option-copy.ts +125 -0
  198. package/dist/package/src/views/tool/code-gen/components/CodeGenProgressDialog.vue +328 -0
  199. package/dist/package/src/views/tool/code-gen/components/CodePreviewPane.vue +42 -0
  200. package/dist/package/src/views/tool/code-gen/config.ts +175 -0
  201. package/dist/package/src/views/tool/code-gen/preview/capabilities.ts +34 -0
  202. package/dist/package/src/views/tool/code-gen/preview/data.ts +331 -0
  203. package/dist/package/src/views/tool/code-gen/preview/index.vue +831 -0
  204. package/dist/package/src/views/tool/code-gen/proto/index.vue +703 -0
  205. package/dist/package/src/views/tool/code-gen/table/index.vue +864 -0
  206. package/dist/package/src/views/tool/project-doc/index.vue +561 -0
  207. package/package.json +60 -0
  208. package/tsconfig.json +13 -0
@@ -0,0 +1,944 @@
1
+ <template>
2
+ <div class="table-box">
3
+ <ProTable
4
+ ref="proTable"
5
+ title="菜单列表"
6
+ row-key="id"
7
+ :indent="20"
8
+ :columns="columns"
9
+ :header-actions="headerActions"
10
+ :request-api="requestMenuTable"
11
+ :pagination="false"
12
+ :default-expand-all="false"
13
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
14
+ />
15
+
16
+ <FormDialog
17
+ v-model="dialog.visible"
18
+ ref="formDialogRef"
19
+ :title="dialog.title"
20
+ width="1180px"
21
+ :model="formData"
22
+ :fields="formFields"
23
+ :rules="rules"
24
+ label-width="120px"
25
+ :col-span="12"
26
+ @confirm="handleSubmit"
27
+ @close="handleCloseDialog"
28
+ >
29
+ <template #menuIcon>
30
+ <SelectIcon v-model:icon-value="menuIconValue" placeholder="请选择菜单图标" />
31
+ </template>
32
+
33
+ <template #apiTransferItem="slotScope">
34
+ <el-popover effect="light" trigger="hover" placement="top" width="auto">
35
+ <template #default>
36
+ <div>操作名称:{{ slotScope.option.operation }}</div>
37
+ <div>请求方式:{{ slotScope.option.method }}</div>
38
+ <div>请求地址:{{ slotScope.option.path }}</div>
39
+ </template>
40
+ <template #reference>{{ slotScope.option.label }}</template>
41
+ </el-popover>
42
+ </template>
43
+ </FormDialog>
44
+ </div>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import { computed, h, reactive, ref, resolveComponent, resolveDynamicComponent, watch } from "vue";
49
+ import { ElIcon, ElTag, type FormRules } from "element-plus";
50
+ import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
51
+ 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";
53
+ import type { ProFormField, ProFormOption } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
54
+ import SelectIcon from "@liujitcn/kratos-admin-core/components/SelectIcon/index.vue";
55
+ import { defBaseMenuService } from "@liujitcn/kratos-admin-system/api/system/base_menu";
56
+ import { defBaseApiService } from "@liujitcn/kratos-admin-system/api/system/base_api";
57
+ import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
58
+ import type { BaseApi } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_api";
59
+ import type { BaseMenu, BaseMenuForm, BaseMenuMeta } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_menu";
60
+ import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
61
+ import { BaseMenuType } from "@liujitcn/kratos-admin-system/rpc/system/common/v1/enum";
62
+ import { normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
63
+
64
+ defineOptions({
65
+ name: "BaseMenu",
66
+ inheritAttrs: false
67
+ });
68
+
69
+ /**
70
+ * 菜单表单状态,统一补齐 meta 字段,便于 ProForm 直接双向绑定。
71
+ */
72
+ type MenuFormState = Omit<BaseMenuForm, "meta"> & {
73
+ /** 菜单元信息。 */
74
+ meta: BaseMenuMeta;
75
+ };
76
+
77
+ const { BUTTONS } = useAuthButtons();
78
+ const proTable = ref<ProTableInstance>();
79
+ const formDialogRef = ref<InstanceType<typeof FormDialog>>();
80
+ const menuOptions = ref<ProFormOption[]>([]);
81
+ const parentMenuTypeMap = ref(new Map<number, BaseMenuType>());
82
+ const apiList = ref<BaseApi[]>([]);
83
+
84
+ const dialog = reactive({
85
+ title: "",
86
+ visible: false,
87
+ parentType: BaseMenuType.UNKNOWN_MT,
88
+ parentLocked: true
89
+ });
90
+
91
+ /** 创建默认菜单元信息。 */
92
+ function createDefaultMenuMeta(): BaseMenuMeta {
93
+ return {
94
+ title: "",
95
+ icon: "",
96
+ always_show: false,
97
+ hidden: false,
98
+ keep_alive: false,
99
+ full: false,
100
+ affix: false,
101
+ params: []
102
+ };
103
+ }
104
+
105
+ /** 创建默认菜单表单。 */
106
+ function createDefaultMenuForm(): MenuFormState {
107
+ return {
108
+ id: 0,
109
+ parent_id: undefined,
110
+ type: BaseMenuType.FOLDER,
111
+ path: "",
112
+ name: "",
113
+ component: "",
114
+ redirect: "",
115
+ meta: createDefaultMenuMeta(),
116
+ api: [],
117
+ sort: 1,
118
+ status: Status.ENABLE
119
+ };
120
+ }
121
+
122
+ const formData = reactive<MenuFormState>(createDefaultMenuForm());
123
+
124
+ /** 统一接管菜单图标字段,规避可选字段类型带来的模板告警。 */
125
+ const menuIconValue = computed({
126
+ get: () => formData.meta.icon ?? "",
127
+ set: value => {
128
+ formData.meta.icon = value;
129
+ }
130
+ });
131
+
132
+ const menuTypeOptions: ProFormOption[] = [
133
+ { label: "目录", value: BaseMenuType.FOLDER },
134
+ { label: "菜单", value: BaseMenuType.MENU },
135
+ { label: "按钮", value: BaseMenuType.BUTTON },
136
+ { label: "外链", value: BaseMenuType.EXT_LINK }
137
+ ];
138
+
139
+ /** 根据三、五、七、九位编号识别菜单层级。 */
140
+ function getMenuLevel(menuId: number) {
141
+ if (menuId >= 100 && menuId <= 999) return 1;
142
+ if (menuId >= 10000 && menuId <= 99999) return 2;
143
+ if (menuId >= 1000000 && menuId <= 9999999) return 3;
144
+ if (menuId >= 100000000 && menuId <= 999999999) return 4;
145
+ return 0;
146
+ }
147
+
148
+ /** 判断当前菜单是否还能继续新增下级节点。 */
149
+ function canCreateChild(menu: BaseMenu) {
150
+ const level = getMenuLevel(menu.id);
151
+ if (menu.type === BaseMenuType.FOLDER) return level === 1 || level === 2;
152
+ if (menu.type === BaseMenuType.MENU) return level === 2 || level === 3;
153
+ return false;
154
+ }
155
+
156
+ /** 根据父级层级限制可创建的菜单类型。 */
157
+ const availableMenuTypeOptions = computed(() => {
158
+ const parentLevel = getMenuLevel(formData.parent_id ?? 0);
159
+
160
+ if (formData.id > 0 && parentLevel === 0) return menuTypeOptions.filter(item => item.value === BaseMenuType.FOLDER);
161
+ if (dialog.parentType === BaseMenuType.FOLDER && parentLevel === 1)
162
+ return menuTypeOptions.filter(
163
+ item => item.value === BaseMenuType.FOLDER || item.value === BaseMenuType.MENU || item.value === BaseMenuType.EXT_LINK
164
+ );
165
+ if (dialog.parentType === BaseMenuType.FOLDER && parentLevel === 2)
166
+ return menuTypeOptions.filter(item => item.value === BaseMenuType.MENU || item.value === BaseMenuType.EXT_LINK);
167
+ if (dialog.parentType === BaseMenuType.MENU && (parentLevel === 2 || parentLevel === 3))
168
+ return menuTypeOptions.filter(item => item.value === BaseMenuType.BUTTON);
169
+ return [];
170
+ });
171
+
172
+ watch(
173
+ () => formData.parent_id,
174
+ () => {
175
+ dialog.parentType = parentMenuTypeMap.value.get(formData.parent_id ?? 0) ?? BaseMenuType.UNKNOWN_MT;
176
+ if (formData.id > 0 || availableMenuTypeOptions.value.some(item => item.value === formData.type)) return;
177
+ formData.type = (availableMenuTypeOptions.value[0]?.value as BaseMenuType) ?? BaseMenuType.UNKNOWN_MT;
178
+ }
179
+ );
180
+
181
+ const statusOptions: ProFormOption[] = [
182
+ { label: "启用", value: Status.ENABLE },
183
+ { label: "禁用", value: Status.DISABLE }
184
+ ];
185
+
186
+ /**
187
+ * 渲染菜单图标单元格,统一兼容 Element Plus 图标和本地 svg 图标。
188
+ */
189
+ function renderMenuIconCell(scope: RenderScope<BaseMenu>) {
190
+ const icon = scope.row.meta?.icon;
191
+ const iconName = resolveElementIcon(icon);
192
+ if (iconName) {
193
+ return h(
194
+ ElIcon,
195
+ { size: 18 },
196
+ {
197
+ default: () => [h(resolveDynamicComponent(iconName) as any)]
198
+ }
199
+ );
200
+ }
201
+ if (icon) return h(resolveComponent("svg-icon"), { iconClass: icon });
202
+ return "--";
203
+ }
204
+
205
+ /**
206
+ * 渲染菜单显示状态标签,减少页面模板中的重复判断。
207
+ */
208
+ function renderHiddenCell(scope: RenderScope<BaseMenu>) {
209
+ const isHidden = Boolean(scope.row.meta?.hidden);
210
+ return h(ElTag, { type: isHidden ? "info" : "success" }, () => (isHidden ? "隐藏" : "显示"));
211
+ }
212
+
213
+ /** 菜单表格列配置。 */
214
+ const columns = computed<ColumnProps[]>(() => [
215
+ { type: "selection", width: 55, selectable: row => (row as BaseMenu).parent_id !== 0 },
216
+ {
217
+ prop: "meta.title",
218
+ label: "菜单名称",
219
+ minWidth: 220,
220
+ align: "left",
221
+ search: { el: "input", key: "title" }
222
+ },
223
+ { prop: "type", label: "菜单类型", minWidth: 120, dictCode: "base_menu_type", search: { el: "select" } },
224
+ {
225
+ prop: "meta.icon",
226
+ label: "菜单图标",
227
+ width: 90,
228
+ render: scope => renderMenuIconCell(scope as unknown as RenderScope<BaseMenu>)
229
+ },
230
+ { prop: "path", label: "路由路径/权限标识", minWidth: 260, search: { el: "input" } },
231
+ { prop: "name", label: "路由名称", minWidth: 180, search: { el: "input" } },
232
+ { prop: "component", label: "组件路径", minWidth: 260 },
233
+ { prop: "redirect", label: "重定向地址", minWidth: 220 },
234
+ { prop: "sort", label: "排序", minWidth: 80, align: "right" },
235
+ {
236
+ prop: "status",
237
+ label: "状态",
238
+ width: 100,
239
+ dictCode: "status",
240
+ search: { el: "select" },
241
+ cellType: "status",
242
+ statusProps: {
243
+ activeValue: Status.ENABLE,
244
+ inactiveValue: Status.DISABLE,
245
+ activeText: "启用",
246
+ inactiveText: "禁用",
247
+ disabled: () => !BUTTONS.value["base:menu:status"],
248
+ beforeChange: scope => handleBeforeSetStatus(scope.row as BaseMenu)
249
+ }
250
+ },
251
+ {
252
+ prop: "meta.hidden",
253
+ label: "显示状态",
254
+ width: 100,
255
+ render: scope => renderHiddenCell(scope as unknown as RenderScope<BaseMenu>)
256
+ },
257
+ { prop: "created_at", label: "创建时间", minWidth: 180 },
258
+ { prop: "updated_at", label: "更新时间", minWidth: 180 },
259
+ {
260
+ prop: "operation",
261
+ label: "操作",
262
+ width: 220,
263
+ fixed: "right",
264
+ cellType: "actions",
265
+ actions: [
266
+ {
267
+ label: "新增",
268
+ type: "primary",
269
+ link: true,
270
+ icon: CirclePlus,
271
+ hidden: scope => !BUTTONS.value["base:menu:create"] || !canCreateChild(scope.row as BaseMenu),
272
+ onClick: scope => handleOpenDialog(scope.row as BaseMenu)
273
+ },
274
+ {
275
+ label: "编辑",
276
+ type: "primary",
277
+ link: true,
278
+ icon: EditPen,
279
+ hidden: () => !BUTTONS.value["base:menu:update"],
280
+ onClick: scope => handleOpenDialog(undefined, (scope.row as BaseMenu).id)
281
+ },
282
+ {
283
+ label: "删除",
284
+ type: "danger",
285
+ link: true,
286
+ icon: Delete,
287
+ hidden: scope => !BUTTONS.value["base:menu:delete"] || (scope.row as BaseMenu).parent_id === 0,
288
+ onClick: scope => handleDeleteMenu(scope.row as BaseMenu)
289
+ }
290
+ ]
291
+ }
292
+ ]);
293
+
294
+ /** 菜单表格顶部按钮配置。 */
295
+ const headerActions = computed<HeaderActionProps[]>(() => [
296
+ {
297
+ label: "新增",
298
+ type: "success",
299
+ icon: CirclePlus,
300
+ hidden: () => !BUTTONS.value["base:menu:create"],
301
+ onClick: () => handleOpenDialog()
302
+ },
303
+ {
304
+ label: "删除",
305
+ type: "danger",
306
+ icon: Delete,
307
+ hidden: () => !BUTTONS.value["base:menu:delete"],
308
+ disabled: scope => !scope.selectedList.length,
309
+ onClick: scope => handleDeleteMenu(scope.selectedList as BaseMenu[])
310
+ }
311
+ ]);
312
+
313
+ /** API 穿梭框可选数据。 */
314
+ const transferData = computed<ProFormOption[]>(() => {
315
+ return apiList.value.map(item => ({
316
+ ...item,
317
+ value: item.operation,
318
+ label: `${item.service_desc}/${item.desc}`
319
+ }));
320
+ });
321
+
322
+ /** 菜单表单字段配置。 */
323
+ const formFields = computed<ProFormField[]>(() => [
324
+ {
325
+ prop: "parent_id",
326
+ label: "上级菜单",
327
+ component: "tree-select",
328
+ options: menuOptions.value,
329
+ props: () => ({
330
+ nodeKey: "value",
331
+ props: { label: "label", children: "children" },
332
+ checkStrictly: true,
333
+ clearable: false,
334
+ filterable: true,
335
+ placeholder: "请选择上级菜单",
336
+ disabled: dialog.parentLocked,
337
+ style: { width: "100%" }
338
+ })
339
+ },
340
+ {
341
+ prop: "type",
342
+ label: "菜单类型",
343
+ component: "radio-group",
344
+ options: availableMenuTypeOptions.value,
345
+ props: model => ({
346
+ disabled: model.id > 0 && model.parent_id === 0
347
+ })
348
+ },
349
+ {
350
+ prop: "meta.title",
351
+ label: formData.type === BaseMenuType.BUTTON ? "按钮名称" : "菜单标题",
352
+ component: "input",
353
+ itemProps: {
354
+ required: true
355
+ },
356
+ props: () => ({
357
+ placeholder: formData.type === BaseMenuType.BUTTON ? "请输入按钮名称" : "请输入菜单标题"
358
+ })
359
+ },
360
+ {
361
+ prop: "path",
362
+ label: getPathFieldLabel(),
363
+ component: "input",
364
+ labelTooltip: getPathFieldTooltip(),
365
+ itemProps: model => ({
366
+ required: model.type !== BaseMenuType.FOLDER
367
+ }),
368
+ props: () => ({
369
+ placeholder: getPathFieldPlaceholder()
370
+ }),
371
+ visible: model => model.type !== BaseMenuType.FOLDER
372
+ },
373
+ {
374
+ prop: "redirect",
375
+ label: getRedirectFieldLabel(),
376
+ component: "input",
377
+ props: () => ({ placeholder: getRedirectFieldPlaceholder() }),
378
+ itemProps: model => ({
379
+ required: model.type === BaseMenuType.EXT_LINK
380
+ }),
381
+ visible: model => model.type === BaseMenuType.FOLDER || model.type === BaseMenuType.EXT_LINK
382
+ },
383
+ {
384
+ prop: "meta.icon",
385
+ label: "菜单图标",
386
+ component: "slot",
387
+ slotName: "menuIcon",
388
+ itemProps: model => ({
389
+ required: model.type !== BaseMenuType.BUTTON
390
+ }),
391
+ visible: model => model.type !== BaseMenuType.BUTTON
392
+ },
393
+ {
394
+ prop: "name",
395
+ label: "路由名称",
396
+ component: "input",
397
+ labelTooltip: "如果需要开启缓存,需保证页面 defineOptions 中的 name 与此处一致,建议使用驼峰。",
398
+ itemProps: model => ({
399
+ required: model.type === BaseMenuType.MENU
400
+ }),
401
+ props: { placeholder: "请输入路由名称" },
402
+ visible: model => model.type === BaseMenuType.MENU
403
+ },
404
+ {
405
+ prop: "component",
406
+ label: "组件路径",
407
+ component: "input",
408
+ labelTooltip: "组件页面完整路径,格式为模块名/模块内 views 路径,例如 system/base/user/index,缺省后缀 .vue。",
409
+ itemProps: model => ({
410
+ required: model.type === BaseMenuType.MENU
411
+ }),
412
+ props: { placeholder: "system/base/user/index" },
413
+ visible: model => model.type === BaseMenuType.MENU
414
+ },
415
+ {
416
+ prop: "meta.hidden",
417
+ label: "是否隐藏",
418
+ component: "switch",
419
+ props: {
420
+ inlinePrompt: true,
421
+ activeText: "是",
422
+ inactiveText: "否",
423
+ activeValue: true,
424
+ inactiveValue: false
425
+ },
426
+ visible: model => model.type !== BaseMenuType.BUTTON
427
+ },
428
+ {
429
+ prop: "meta.always_show",
430
+ label: "始终显示",
431
+ component: "switch",
432
+ labelTooltip: "选择“是”,即使目录或菜单下只有一个子节点,也会显示父节点;选择“否”,若只有一个子节点则只显示子节点。",
433
+ props: {
434
+ inlinePrompt: true,
435
+ activeText: "是",
436
+ inactiveText: "否",
437
+ activeValue: true,
438
+ inactiveValue: false
439
+ },
440
+ visible: model => model.type === BaseMenuType.FOLDER || model.type === BaseMenuType.MENU
441
+ },
442
+ {
443
+ prop: "meta.keep_alive",
444
+ label: "缓存页面",
445
+ component: "switch",
446
+ props: {
447
+ inlinePrompt: true,
448
+ activeText: "开",
449
+ inactiveText: "关",
450
+ activeValue: true,
451
+ inactiveValue: false
452
+ },
453
+ visible: model => model.type === BaseMenuType.MENU
454
+ },
455
+ {
456
+ prop: "meta.full",
457
+ label: "全屏模式",
458
+ component: "switch",
459
+ props: {
460
+ inlinePrompt: true,
461
+ activeText: "开",
462
+ inactiveText: "关",
463
+ activeValue: true,
464
+ inactiveValue: false
465
+ },
466
+ visible: model => model.type === BaseMenuType.MENU
467
+ },
468
+ {
469
+ prop: "meta.affix",
470
+ label: "固定标签",
471
+ component: "switch",
472
+ props: {
473
+ inlinePrompt: true,
474
+ activeText: "开",
475
+ inactiveText: "关",
476
+ activeValue: true,
477
+ inactiveValue: false
478
+ },
479
+ visible: model => model.type === BaseMenuType.MENU
480
+ },
481
+ {
482
+ prop: "meta.params",
483
+ label: "路由参数",
484
+ component: "kv-list",
485
+ labelTooltip: "组件页面使用 useRoute().query.参数名 获取路由参数值。",
486
+ props: {
487
+ addText: "添加路由参数",
488
+ keyInputProps: { placeholder: "参数名" },
489
+ valueInputProps: { placeholder: "参数值" }
490
+ },
491
+ itemProps: {
492
+ class: "menu-form__params"
493
+ },
494
+ visible: model => model.type === BaseMenuType.MENU
495
+ },
496
+ {
497
+ prop: "api",
498
+ label: "API 列表",
499
+ component: "transfer",
500
+ slotName: "apiTransferItem",
501
+ options: transferData.value,
502
+ props: {
503
+ filterable: true,
504
+ titles: ["可选 API", "已选 API"]
505
+ },
506
+ visible: model => model.type === BaseMenuType.MENU || model.type === BaseMenuType.BUTTON,
507
+ colSpan: 24
508
+ },
509
+ {
510
+ prop: "sort",
511
+ label: "排序",
512
+ component: "input-number",
513
+ itemProps: {
514
+ required: true
515
+ },
516
+ props: {
517
+ min: 1,
518
+ controlsPosition: "right",
519
+ precision: 0,
520
+ step: 1,
521
+ style: { width: "100%" }
522
+ }
523
+ },
524
+ {
525
+ prop: "status",
526
+ label: "状态",
527
+ component: "radio-group",
528
+ itemProps: {
529
+ required: true
530
+ },
531
+ options: statusOptions
532
+ }
533
+ ]);
534
+
535
+ const rules = computed<FormRules>(() => ({
536
+ parent_id: formData.id ? [] : [{ required: true, type: "number", min: 1, message: "请选择上级菜单", trigger: "change" }],
537
+ type: [{ required: true, message: "请选择菜单类型", trigger: "change" }],
538
+ "meta.title": [
539
+ {
540
+ validator: (_rule, value, callback) => {
541
+ if (value) return callback();
542
+ callback(new Error(formData.type === BaseMenuType.BUTTON ? "请输入按钮名称" : "请输入菜单标题"));
543
+ },
544
+ trigger: "blur"
545
+ }
546
+ ],
547
+ "meta.icon": [
548
+ {
549
+ validator: (_rule, value, callback) => {
550
+ if (formData.type === BaseMenuType.BUTTON) return callback();
551
+ if (value) return callback();
552
+ callback(new Error("请选择菜单图标"));
553
+ },
554
+ trigger: "change"
555
+ }
556
+ ],
557
+ path: [
558
+ { max: 1024, message: "路径不能超过 1024 个字符", trigger: "blur" },
559
+ {
560
+ validator: (_rule, value, callback) => {
561
+ if (formData.type === BaseMenuType.FOLDER) return callback();
562
+ if (value) return callback();
563
+ if (formData.type === BaseMenuType.BUTTON) return callback(new Error("请输入权限标识"));
564
+ if (formData.type === BaseMenuType.EXT_LINK) return callback(new Error("请输入完整外链地址"));
565
+ callback(new Error("请输入路由路径"));
566
+ },
567
+ trigger: "blur"
568
+ }
569
+ ],
570
+ redirect: [
571
+ { max: 1024, message: "重定向地址不能超过 1024 个字符", trigger: "blur" },
572
+ {
573
+ validator: (_rule, value, callback) => {
574
+ if (formData.type !== BaseMenuType.EXT_LINK) return callback();
575
+ if (/^https?:\/\/\S+$/.test(value)) return callback();
576
+ callback(new Error("请输入完整的 HTTP 外链地址"));
577
+ },
578
+ trigger: "blur"
579
+ }
580
+ ],
581
+ name: [
582
+ { max: 255, message: "路由名称不能超过 255 个字符", trigger: "blur" },
583
+ {
584
+ validator: (_rule, value, callback) => {
585
+ if (formData.type !== BaseMenuType.MENU) return callback();
586
+ if (value) return callback();
587
+ callback(new Error("请输入路由名称"));
588
+ },
589
+ trigger: "blur"
590
+ }
591
+ ],
592
+ component: [
593
+ { max: 255, message: "组件路径不能超过 255 个字符", trigger: "blur" },
594
+ {
595
+ validator: (_rule, value, callback) => {
596
+ if (formData.type !== BaseMenuType.MENU) return callback();
597
+ if (value) return callback();
598
+ callback(new Error("请输入组件路径"));
599
+ },
600
+ trigger: "blur"
601
+ }
602
+ ],
603
+ sort: [{ required: true, type: "number", min: 1, message: "排序必须大于 0", trigger: "blur" }],
604
+ status: [{ required: true, message: "请选择状态", trigger: "change" }]
605
+ }));
606
+
607
+ /** 计算当前路径字段文案。 */
608
+ function getPathFieldLabel() {
609
+ if (formData.type === BaseMenuType.BUTTON) return "权限标识";
610
+ if (formData.type === BaseMenuType.EXT_LINK) return "内部路径";
611
+ return "路由路径";
612
+ }
613
+
614
+ /** 计算当前路径字段占位文案。 */
615
+ function getPathFieldPlaceholder() {
616
+ if (formData.type === BaseMenuType.BUTTON) return "请输入按钮权限标识";
617
+ if (formData.type === BaseMenuType.EXT_LINK) return "external/baidu";
618
+ if (formData.type === BaseMenuType.FOLDER) return "base";
619
+ return "user";
620
+ }
621
+
622
+ /** 计算当前路径字段提示文案。 */
623
+ function getPathFieldTooltip() {
624
+ if (formData.type === BaseMenuType.BUTTON) return "按钮类型菜单使用权限标识,例如 base:user:create。";
625
+ if (formData.type === BaseMenuType.EXT_LINK) return "外链使用内部唯一路径,例如 external/baidu;实际地址填写在外链地址中。";
626
+ if (formData.type === BaseMenuType.FOLDER) return "目录不填写路径,仅用于组织下级菜单。";
627
+ return "菜单路径通常不带 /,例如 user。";
628
+ }
629
+
630
+ /** 计算重定向字段文案。 */
631
+ function getRedirectFieldLabel() {
632
+ return formData.type === BaseMenuType.EXT_LINK ? "外链地址" : "跳转路由";
633
+ }
634
+
635
+ /** 计算重定向字段占位文案。 */
636
+ function getRedirectFieldPlaceholder() {
637
+ return formData.type === BaseMenuType.EXT_LINK ? "https://www.baidu.com/" : "请输入跳转路由";
638
+ }
639
+
640
+ /** 判断当前图标是否为 Element Plus 图标。 */
641
+ function resolveElementIcon(icon?: string) {
642
+ if (!icon) return "";
643
+ if (icon.startsWith("el-icon-")) return icon.replace("el-icon-", "");
644
+ if (/^[A-Z]/.test(icon)) return icon;
645
+ return "";
646
+ }
647
+
648
+ /**
649
+ * 将菜单接口返回的 API 字段统一转换为穿梭框可识别的 operation 列表。
650
+ */
651
+ function normalizeMenuApiSelection(api?: unknown[]) {
652
+ if (!Array.isArray(api)) return [];
653
+
654
+ const apiOperationSet = new Set(apiList.value.map(item => item.operation));
655
+ const apiIdMap = new Map(apiList.value.map(item => [String(item.id), item.operation]));
656
+
657
+ return api
658
+ .map(item => {
659
+ if (typeof item === "string") {
660
+ if (apiOperationSet.has(item)) return item;
661
+ return apiIdMap.get(item) ?? "";
662
+ }
663
+
664
+ if (typeof item === "number") {
665
+ return apiIdMap.get(String(item)) ?? "";
666
+ }
667
+
668
+ if (item && typeof item === "object") {
669
+ const currentItem = item as Record<string, unknown>;
670
+ if (typeof currentItem.operation === "string") return currentItem.operation;
671
+ if (currentItem.id !== undefined) return apiIdMap.get(String(currentItem.id)) ?? "";
672
+ }
673
+
674
+ return "";
675
+ })
676
+ .filter((item, index, currentList) => item && currentList.indexOf(item) === index);
677
+ }
678
+
679
+ /** 将服务端菜单表单补齐为前端可编辑结构。 */
680
+ function normalizeMenuForm(data?: Partial<BaseMenuForm>): MenuFormState {
681
+ const defaultForm = createDefaultMenuForm();
682
+ const normalizedMeta = {
683
+ ...createDefaultMenuMeta(),
684
+ ...(data?.meta ?? {}),
685
+ params: data?.meta?.params ?? []
686
+ };
687
+
688
+ return {
689
+ ...defaultForm,
690
+ ...data,
691
+ parent_id: data?.parent_id === 0 ? undefined : data?.parent_id,
692
+ type: data?.type ?? BaseMenuType.FOLDER,
693
+ status: data?.status ?? Status.ENABLE,
694
+ api: normalizeMenuApiSelection(data?.api),
695
+ sort: data?.sort ?? 1,
696
+ meta: normalizedMeta
697
+ };
698
+ }
699
+
700
+ /** 构建可选父级菜单树,并记录父节点类型用于约束下级菜单类型。 */
701
+ function buildMenuOptions(menuList: BaseMenu[] = []) {
702
+ const typeMap = new Map<number, BaseMenuType>();
703
+ const convert = (list: BaseMenu[]): ProFormOption[] =>
704
+ list
705
+ .filter(item => item.type === BaseMenuType.FOLDER || item.type === BaseMenuType.MENU)
706
+ .map(item => {
707
+ typeMap.set(item.id, item.type);
708
+ return {
709
+ label: item.meta?.title || item.name || item.path,
710
+ value: item.id,
711
+ children: convert(item.children ?? [])
712
+ };
713
+ });
714
+ const options = convert(menuList);
715
+ parentMenuTypeMap.value = typeMap;
716
+ return options;
717
+ }
718
+
719
+ /** 根据菜单类型清理无效字段,避免提交脏数据。 */
720
+ function buildSubmitPayload(): BaseMenuForm {
721
+ const payload = normalizeMenuForm(formData);
722
+ // 一级菜单在表单中保持空白,提交时仍按接口约定传回根节点标识。
723
+ if (payload.id > 0 && payload.parent_id === undefined) payload.parent_id = 0;
724
+ payload.meta.params = (payload.meta.params ?? []).filter(item => item.key || item.value);
725
+
726
+ if (payload.type === BaseMenuType.BUTTON) {
727
+ payload.name = "";
728
+ payload.component = "";
729
+ payload.redirect = "";
730
+ payload.meta.icon = "";
731
+ payload.meta.always_show = false;
732
+ payload.meta.hidden = true;
733
+ payload.meta.keep_alive = false;
734
+ payload.meta.full = false;
735
+ payload.meta.affix = false;
736
+ payload.meta.params = [];
737
+ }
738
+
739
+ if (payload.type === BaseMenuType.FOLDER) {
740
+ payload.path = "";
741
+ payload.name = "";
742
+ payload.component = "Layout";
743
+ payload.api = [];
744
+ payload.meta.keep_alive = false;
745
+ payload.meta.full = false;
746
+ payload.meta.affix = false;
747
+ payload.meta.params = [];
748
+ }
749
+
750
+ if (payload.type === BaseMenuType.EXT_LINK) {
751
+ payload.name = "";
752
+ payload.component = "";
753
+ payload.api = [];
754
+ payload.meta.keep_alive = false;
755
+ payload.meta.full = false;
756
+ payload.meta.affix = false;
757
+ payload.meta.params = [];
758
+ }
759
+
760
+ return payload;
761
+ }
762
+
763
+ /** 加载菜单树选项和 API 列表,确保弹窗打开时相关数据已可用。 */
764
+ async function loadDialogResources() {
765
+ const [menuData, apiData] = await Promise.all([defBaseMenuService.TreeBaseMenu({}), defBaseApiService.OptionBaseApi({})]);
766
+ menuOptions.value = buildMenuOptions(menuData.base_menus ?? []);
767
+ apiList.value = apiData.base_apis ?? [];
768
+ }
769
+
770
+ /** 根据关键字递归过滤菜单树,保留匹配节点及其父级。 */
771
+ function filterMenuTree(menuList: BaseMenu[], keywordMap: Record<string, string>) {
772
+ const titleKeyword = (keywordMap.title ?? "").trim().toLowerCase();
773
+ const nameKeyword = (keywordMap.name ?? "").trim().toLowerCase();
774
+ const pathKeyword = (keywordMap.path ?? "").trim().toLowerCase();
775
+
776
+ return menuList.reduce<BaseMenu[]>((result, item) => {
777
+ const children = filterMenuTree(item.children ?? [], keywordMap);
778
+ const title = item.meta?.title?.toLowerCase() ?? "";
779
+ const name = item.name?.toLowerCase() ?? "";
780
+ const path = item.path?.toLowerCase() ?? "";
781
+ const matched =
782
+ (!titleKeyword || title.includes(titleKeyword)) &&
783
+ (!nameKeyword || name.includes(nameKeyword)) &&
784
+ (!pathKeyword || path.includes(pathKeyword));
785
+
786
+ if (!matched && !children.length) return result;
787
+
788
+ result.push({
789
+ ...item,
790
+ children
791
+ });
792
+ return result;
793
+ }, []);
794
+ }
795
+
796
+ /** 请求菜单表格数据,并按搜索条件过滤树形结构。 */
797
+ async function requestMenuTable(params: Record<string, string>) {
798
+ const data = await defBaseMenuService.TreeBaseMenu({});
799
+ const keywordMap = {
800
+ title: params.title ?? "",
801
+ name: params.name ?? "",
802
+ path: params.path ?? ""
803
+ };
804
+
805
+ return {
806
+ data: filterMenuTree(data.base_menus ?? [], keywordMap)
807
+ };
808
+ }
809
+
810
+ /** 刷新菜单表格。 */
811
+ function refreshTable() {
812
+ proTable.value?.getTableList();
813
+ }
814
+
815
+ /**
816
+ * 打开菜单弹窗。
817
+ * parentMenu 为新增时的固定父节点,menuId 为编辑时的菜单 ID。
818
+ */
819
+ async function handleOpenDialog(parentMenu?: BaseMenu, menuId?: number) {
820
+ await loadDialogResources();
821
+ dialog.parentLocked = Boolean(parentMenu || menuId);
822
+ dialog.parentType = parentMenu?.type ?? BaseMenuType.UNKNOWN_MT;
823
+ resetForm(menuId ? undefined : { parent_id: parentMenu?.id });
824
+ dialog.visible = true;
825
+
826
+ if (menuId) {
827
+ dialog.title = "修改菜单";
828
+ const data = await defBaseMenuService.GetBaseMenu({ id: menuId });
829
+ resetForm(data);
830
+ return;
831
+ }
832
+
833
+ dialog.title = "新增菜单";
834
+ }
835
+
836
+ /** 关闭菜单弹窗并显式重置表单与校验状态。 */
837
+ function handleCloseDialog() {
838
+ dialog.visible = false;
839
+ dialog.parentType = BaseMenuType.UNKNOWN_MT;
840
+ dialog.parentLocked = true;
841
+ resetForm();
842
+ }
843
+
844
+ /** 重置当前表单数据和校验状态,避免新增与编辑切换时残留旧值。 */
845
+ function resetForm(data?: Partial<BaseMenuForm>) {
846
+ formDialogRef.value?.resetFields();
847
+ formDialogRef.value?.clearValidate();
848
+ Object.assign(formData, normalizeMenuForm(data));
849
+ }
850
+
851
+ /** 提交菜单表单,并在成功后关闭弹窗、刷新表格。 */
852
+ async function handleSubmit() {
853
+ const valid = await formDialogRef.value?.validate();
854
+ if (!valid) return;
855
+
856
+ const payload = buildSubmitPayload();
857
+ if (payload.id > 0) {
858
+ await defBaseMenuService.UpdateBaseMenu({ base_menu: payload });
859
+ ElMessage.success("菜单更新成功");
860
+ } else {
861
+ await defBaseMenuService.CreateBaseMenu({ base_menu: payload });
862
+ ElMessage.success("菜单创建成功");
863
+ }
864
+
865
+ handleCloseDialog();
866
+ refreshTable();
867
+ }
868
+
869
+ /**
870
+ * 在菜单状态切换前先完成确认与接口调用,避免首屏渲染触发误操作。
871
+ */
872
+ async function handleBeforeSetStatus(row: BaseMenu) {
873
+ const nextStatus = row.status === Status.ENABLE ? Status.DISABLE : Status.ENABLE;
874
+ const text = nextStatus === Status.ENABLE ? "启用" : "禁用";
875
+ const menuName = row.meta?.title || row.name || row.path || `ID:${row.id}`;
876
+ try {
877
+ await ElMessageBox.confirm(`是否确定${text}菜单?\n菜单名称:${menuName}`, "提示", {
878
+ confirmButtonText: "确认",
879
+ cancelButtonText: "取消",
880
+ type: "warning"
881
+ });
882
+ await defBaseMenuService.SetBaseMenuStatus({ id: row.id, status: nextStatus });
883
+ ElMessage.success(`${text}成功`);
884
+ refreshTable();
885
+ return true;
886
+ } catch {
887
+ return false;
888
+ }
889
+ }
890
+
891
+ /**
892
+ * 删除菜单,兼容单条删除与批量删除。
893
+ */
894
+ function handleDeleteMenu(selected?: number | string | Array<number | string> | BaseMenu | BaseMenu[]) {
895
+ const menuList = Array.isArray(selected)
896
+ ? (selected.filter(item => typeof item === "object") as BaseMenu[])
897
+ : selected && typeof selected === "object"
898
+ ? [selected as BaseMenu]
899
+ : [];
900
+ if (menuList.some(item => item.parent_id === 0)) {
901
+ ElMessage.warning("一级菜单不允许删除");
902
+ return;
903
+ }
904
+ const menuIds = (
905
+ menuList.length ? menuList.map(item => item.id) : normalizeSelectedIds(selected as number | string | Array<number | string>)
906
+ ).join(",");
907
+ if (!menuIds) {
908
+ ElMessage.warning("请勾选删除项");
909
+ return;
910
+ }
911
+
912
+ const singleMenuName = menuList[0]?.meta?.title || menuList[0]?.name || menuList[0]?.path || `ID:${menuList[0]?.id ?? ""}`;
913
+ const confirmMessage = menuList.length
914
+ ? menuList.length === 1
915
+ ? `是否确定删除菜单?\n菜单名称:${singleMenuName}`
916
+ : `确认删除已选中的 ${menuList.length} 个菜单项吗?`
917
+ : "确认删除已选中的菜单项吗?";
918
+
919
+ ElMessageBox.confirm(confirmMessage, "警告", {
920
+ confirmButtonText: "确定",
921
+ cancelButtonText: "取消",
922
+ type: "warning"
923
+ }).then(
924
+ () => {
925
+ defBaseMenuService.DeleteBaseMenu({ id: menuIds }).then(() => {
926
+ ElMessage.success("删除菜单成功");
927
+ refreshTable();
928
+ });
929
+ },
930
+ () => {
931
+ ElMessage.info("已取消删除菜单");
932
+ }
933
+ );
934
+ }
935
+ </script>
936
+
937
+ <style scoped lang="scss">
938
+ .table-box {
939
+ height: 100%;
940
+ }
941
+ :deep(.menu-form__params .el-form-item__content) {
942
+ align-items: flex-start;
943
+ }
944
+ </style>