xto-fronted 0.4.90 → 0.4.91

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 (120) hide show
  1. package/dist/assets/404-Bq0LY5Cd.js +1 -0
  2. package/dist/assets/404-Cw_4ZCL6.css +1 -0
  3. package/dist/assets/{index-BDgOY6Rp.js → index-7ZZxpSfk.js} +1 -1
  4. package/dist/assets/index-BJUe8VUp.js +1 -0
  5. package/dist/assets/{index-Bz0BgZQ1.js → index-BlOR_ICg.js} +1 -1
  6. package/dist/assets/index-BlRslYYI.css +1 -0
  7. package/dist/assets/index-BudArKxR.css +1 -0
  8. package/dist/assets/{index-CwRA10ac.js → index-BzbOWBCV.js} +1 -1
  9. package/dist/assets/index-CFhWBbxk.css +1 -0
  10. package/dist/assets/{index-CfpZmcpk.css → index-CH6aTfYg.css} +1 -1
  11. package/dist/assets/{index-BIoRANs0.js → index-CT5f37nN.js} +1 -1
  12. package/dist/assets/index-Ce-kjtEM.js +2 -0
  13. package/dist/assets/{index-t-2Y0KhA.css → index-Cpew6d-v.css} +1 -1
  14. package/dist/assets/index-DkkuYBgT.css +1 -0
  15. package/dist/assets/index-vfvEFrCH.css +1 -0
  16. package/dist/assets/{index-CwJSA85U.js → index-wVLLAoVp.js} +1 -1
  17. package/dist/assets/vendor-DZmPBJ9d.js +16 -0
  18. package/dist/assets/vue-vendor-DjmFuEnG.js +29 -0
  19. package/dist/assets/{xto-base-PwLGsxxb.js → xto-base-B5HYOo6i.js} +1 -1
  20. package/dist/assets/{xto-core-CtL4zKiV.js → xto-core-DZYp_YAR.js} +1 -1
  21. package/dist/assets/{xto-data-bCXQa7fT.js → xto-data-ogck6x_i.js} +1 -1
  22. package/dist/assets/{xto-feedback-CPydp0kn.js → xto-feedback-C0-6cAL6.js} +1 -1
  23. package/dist/assets/{xto-form-bywohdAf.js → xto-form-IDg_78Vf.js} +1 -1
  24. package/dist/assets/{xto-navigation-Bbdpine9.js → xto-navigation-CPYLzfu7.js} +1 -1
  25. package/dist/index.html +9 -9
  26. package/package.json +91 -91
  27. package/src/App.vue +48 -48
  28. package/src/assets/styles/_dark.scss +639 -572
  29. package/src/assets/styles/_root.scss +183 -183
  30. package/src/assets/styles/_variables.scss +69 -69
  31. package/src/assets/styles/index.scss +460 -460
  32. package/src/components/Layout/Sidebar.vue +198 -198
  33. package/src/components/Layout/TopMenu.vue +1170 -1170
  34. package/src/components/Layout/index.vue +192 -192
  35. package/src/directives/permission.ts +12 -3
  36. package/src/index.ts +100 -100
  37. package/src/router/layoutRoute.ts +59 -59
  38. package/src/stores/menu.ts +64 -3
  39. package/src/types/json-bigint.d.ts +18 -18
  40. package/src/utils/permission.ts +12 -5
  41. package/src/views/dashboard/index.vue +545 -545
  42. package/src/views/error/403.vue +251 -251
  43. package/src/views/error/404.vue +253 -253
  44. package/src/views/login/index.vue +586 -586
  45. package/src/views/system/menu/index.vue +690 -690
  46. package/src/views/system/role/index.vue +583 -583
  47. package/src/views/system/user/index.vue +655 -655
  48. package/dist/App.vue.d.ts +0 -2
  49. package/dist/api/auth.d.ts +0 -8
  50. package/dist/api/system.d.ts +0 -16
  51. package/dist/api/user.d.ts +0 -13
  52. package/dist/assets/404-C9Uh6Uu-.css +0 -1
  53. package/dist/assets/404-zjGLLssH.js +0 -1
  54. package/dist/assets/index-B5xc4gQB.css +0 -1
  55. package/dist/assets/index-CAdztNsv.css +0 -1
  56. package/dist/assets/index-CCXrcISf.css +0 -1
  57. package/dist/assets/index-D8NDxq9d.js +0 -1
  58. package/dist/assets/index-DEB6-Iv_.js +0 -2
  59. package/dist/assets/index-DM4Ezclc.css +0 -1
  60. package/dist/assets/index-DYv7nImj.css +0 -1
  61. package/dist/assets/vendor-CUVPinTg.js +0 -13
  62. package/dist/assets/vue-vendor-DeJXJVbN.js +0 -29
  63. package/dist/components/Layout/Footer.vue.d.ts +0 -2
  64. package/dist/components/Layout/Header.vue.d.ts +0 -5
  65. package/dist/components/Layout/MixTopMenu.vue.d.ts +0 -5
  66. package/dist/components/Layout/Sidebar.vue.d.ts +0 -11
  67. package/dist/components/Layout/SidebarMenuItem.vue.d.ts +0 -5
  68. package/dist/components/Layout/Tabs.vue.d.ts +0 -2
  69. package/dist/components/Layout/TopMenu.vue.d.ts +0 -5
  70. package/dist/components/Layout/index.vue.d.ts +0 -2
  71. package/dist/composables/useApp.d.ts +0 -29
  72. package/dist/composables/useAuth.d.ts +0 -6
  73. package/dist/composables/useForm.d.ts +0 -20
  74. package/dist/composables/useI18n.d.ts +0 -30
  75. package/dist/composables/useTable.d.ts +0 -29
  76. package/dist/directives/permission.d.ts +0 -4
  77. package/dist/enums/index.d.ts +0 -32
  78. package/dist/index-58aI1w0v.js +0 -515
  79. package/dist/index-A_B_Ap_A.js +0 -4240
  80. package/dist/index-BAmYUT0G.js +0 -189
  81. package/dist/index-BRvi9qW-.js +0 -515
  82. package/dist/index-BVGW4DDQ.js +0 -189
  83. package/dist/index-Bmf0YbVq.js +0 -189
  84. package/dist/index-C2-a5KSQ.js +0 -4233
  85. package/dist/index-CeZ0CSSs.js +0 -641
  86. package/dist/index-D25KzR0I.js +0 -479
  87. package/dist/index-DEYOivza.js +0 -641
  88. package/dist/index-DReodgBw.js +0 -4233
  89. package/dist/index-DjERNRXX.js +0 -515
  90. package/dist/index-LSdsO2Ox.js +0 -479
  91. package/dist/index-gBlRG4kk.js +0 -479
  92. package/dist/index-xWU3J3OH.js +0 -641
  93. package/dist/index.d.ts +0 -59
  94. package/dist/index.es.js +0 -95
  95. package/dist/index.umd.js +0 -8
  96. package/dist/main.d.ts +0 -0
  97. package/dist/router/dynamicRoutes.d.ts +0 -30
  98. package/dist/router/guards.d.ts +0 -17
  99. package/dist/router/index.d.ts +0 -6
  100. package/dist/router/layoutRoute.d.ts +0 -22
  101. package/dist/router/staticRoutes.d.ts +0 -2
  102. package/dist/stores/app.d.ts +0 -93
  103. package/dist/stores/auth.d.ts +0 -41
  104. package/dist/stores/index.d.ts +0 -10
  105. package/dist/stores/locale.d.ts +0 -42
  106. package/dist/stores/menu.d.ts +0 -77
  107. package/dist/stores/user.d.ts +0 -92
  108. package/dist/style.css +0 -1
  109. package/dist/utils/auth.d.ts +0 -27
  110. package/dist/utils/config.d.ts +0 -30
  111. package/dist/utils/permission.d.ts +0 -18
  112. package/dist/utils/request.d.ts +0 -29
  113. package/dist/utils/storage.d.ts +0 -24
  114. package/dist/views/dashboard/index.vue.d.ts +0 -2
  115. package/dist/views/error/403.vue.d.ts +0 -2
  116. package/dist/views/error/404.vue.d.ts +0 -2
  117. package/dist/views/login/index.vue.d.ts +0 -4
  118. package/dist/views/system/menu/index.vue.d.ts +0 -4
  119. package/dist/views/system/role/index.vue.d.ts +0 -4
  120. package/dist/views/system/user/index.vue.d.ts +0 -4
@@ -1,690 +1,690 @@
1
- <script setup lang="ts">
2
- import { ref, reactive, computed, onMounted } from 'vue'
3
- import { Form, FormItem, Input, Select, Switch, InputNumber } from '@xto/form'
4
- import { Tag } from '@xto/data'
5
- import { Modal, Message } from '@xto/feedback'
6
- import { Space, Button } from '@xto/base'
7
- import { MenuType, MenuTypeOptions, Status } from '@/enums'
8
-
9
- // 菜单数据类型
10
- interface Menu {
11
- id: number
12
- parentId: number | null
13
- name: string
14
- path: string
15
- component: string
16
- redirect: string
17
- icon: string
18
- title: string
19
- type: MenuType
20
- sort: number
21
- status: Status
22
- hidden: boolean
23
- keepAlive: boolean
24
- children?: Menu[]
25
- }
26
-
27
- // Mock 数据
28
- const mockMenus: Menu[] = [
29
- {
30
- id: 1,
31
- parentId: null,
32
- name: 'Dashboard',
33
- path: '/dashboard',
34
- component: 'dashboard/index',
35
- redirect: '',
36
- icon: 'dashboard',
37
- title: '仪表盘',
38
- type: MenuType.MENU,
39
- sort: 1,
40
- status: Status.ENABLED,
41
- hidden: false,
42
- keepAlive: true
43
- },
44
- {
45
- id: 2,
46
- parentId: null,
47
- name: 'System',
48
- path: '/system',
49
- component: '',
50
- redirect: '/system/user',
51
- icon: 'setting',
52
- title: '系统管理',
53
- type: MenuType.DIRECTORY,
54
- sort: 2,
55
- status: Status.ENABLED,
56
- hidden: false,
57
- keepAlive: false,
58
- children: [
59
- {
60
- id: 21,
61
- parentId: 2,
62
- name: 'SystemUser',
63
- path: '/system/user',
64
- component: 'system/user/index',
65
- redirect: '',
66
- icon: 'user',
67
- title: '用户管理',
68
- type: MenuType.MENU,
69
- sort: 1,
70
- status: Status.ENABLED,
71
- hidden: false,
72
- keepAlive: true
73
- },
74
- {
75
- id: 22,
76
- parentId: 2,
77
- name: 'SystemRole',
78
- path: '/system/role',
79
- component: 'system/role/index',
80
- redirect: '',
81
- icon: 'role',
82
- title: '角色管理',
83
- type: MenuType.MENU,
84
- sort: 2,
85
- status: Status.ENABLED,
86
- hidden: false,
87
- keepAlive: true
88
- },
89
- {
90
- id: 23,
91
- parentId: 2,
92
- name: 'SystemMenu',
93
- path: '/system/menu',
94
- component: 'system/menu/index',
95
- redirect: '',
96
- icon: 'menu',
97
- title: '菜单管理',
98
- type: MenuType.MENU,
99
- sort: 3,
100
- status: Status.ENABLED,
101
- hidden: false,
102
- keepAlive: true
103
- }
104
- ]
105
- }
106
- ]
107
-
108
- const loading = ref(false)
109
- const menuList = ref<Menu[]>([])
110
-
111
- // 弹窗
112
- const modalVisible = ref(false)
113
- const modalTitle = computed(() => formData.id ? '编辑菜单' : '新增菜单')
114
- const formData = reactive({
115
- id: 0,
116
- parentId: null as number | null,
117
- name: '',
118
- path: '',
119
- component: '',
120
- redirect: '',
121
- icon: '',
122
- title: '',
123
- type: MenuType.MENU,
124
- sort: 0,
125
- status: Status.ENABLED,
126
- hidden: false,
127
- keepAlive: true
128
- })
129
-
130
- const rules: Record<string, any[]> = {
131
- name: [
132
- { required: true, message: '请输入菜单名称', trigger: 'blur' }
133
- ],
134
- path: [
135
- { required: true, message: '请输入路由路径', trigger: 'blur' }
136
- ],
137
- title: [
138
- { required: true, message: '请输入菜单标题', trigger: 'blur' }
139
- ]
140
- }
141
-
142
- const formRef = ref()
143
-
144
- // 获取菜单列表
145
- const getMenuList = () => {
146
- loading.value = true
147
- setTimeout(() => {
148
- menuList.value = mockMenus
149
- loading.value = false
150
- }, 300)
151
- }
152
-
153
- // 新增
154
- const handleAdd = (parentId: number | null = null) => {
155
- Object.assign(formData, {
156
- id: 0,
157
- parentId,
158
- name: '',
159
- path: '',
160
- component: '',
161
- redirect: '',
162
- icon: '',
163
- title: '',
164
- type: MenuType.MENU,
165
- sort: 0,
166
- status: Status.ENABLED,
167
- hidden: false,
168
- keepAlive: true
169
- })
170
- modalVisible.value = true
171
- }
172
-
173
- // 编辑
174
- const handleEdit = (node: Menu) => {
175
- Object.assign(formData, node)
176
- modalVisible.value = true
177
- }
178
-
179
- // 提交
180
- const handleSubmit = async () => {
181
- try {
182
- await formRef.value?.validate()
183
- Message.success(formData.id ? '编辑成功' : '新增成功')
184
- modalVisible.value = false
185
- getMenuList()
186
- } catch (error) {
187
- console.error(error)
188
- }
189
- }
190
-
191
- // 菜单图标
192
- const getMenuIcon = (icon?: string) => {
193
- const iconMap: Record<string, string> = {
194
- dashboard: '📊',
195
- system: '⚙️',
196
- user: '👤',
197
- role: '👥',
198
- menu: '📋',
199
- setting: '🔧'
200
- }
201
- return iconMap[icon || ''] || '📄'
202
- }
203
-
204
- // 获取类型标签类型
205
- const getTypeTagType = (type: MenuType) => {
206
- switch (type) {
207
- case MenuType.DIRECTORY: return 'primary'
208
- case MenuType.MENU: return 'success'
209
- default: return 'warning'
210
- }
211
- }
212
-
213
- // 获取类型标签文本
214
- const getTypeTagText = (type: MenuType) => {
215
- switch (type) {
216
- case MenuType.DIRECTORY: return '目录'
217
- case MenuType.MENU: return '菜单'
218
- default: return '按钮'
219
- }
220
- }
221
-
222
- onMounted(() => {
223
- getMenuList()
224
- })
225
- </script>
226
-
227
- <template>
228
- <div class="menu-page">
229
- <!-- 表格 -->
230
- <div class="table-section">
231
- <!-- 工具栏 -->
232
- <div class="table-toolbar">
233
- <div class="toolbar-left">
234
- <Button type="primary" @click="handleAdd()">
235
- <template #icon>
236
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
237
- <line x1="12" y1="5" x2="12" y2="19"/>
238
- <line x1="5" y1="12" x2="19" y2="12"/>
239
- </svg>
240
- </template>
241
- 新增菜单
242
- </Button>
243
- </div>
244
- <div class="toolbar-right">
245
- <span class="table-count">共 {{ menuList.length }} 个菜单</span>
246
- </div>
247
- </div>
248
-
249
- <!-- 菜单树表格 -->
250
- <div class="table-wrapper">
251
- <table class="data-table">
252
- <thead>
253
- <tr>
254
- <th class="col-name">菜单名称</th>
255
- <th class="col-icon">图标</th>
256
- <th class="col-path">路由路径</th>
257
- <th class="col-type">类型</th>
258
- <th class="col-sort">排序</th>
259
- <th class="col-status">状态</th>
260
- <th class="col-actions">操作</th>
261
- </tr>
262
- </thead>
263
- <tbody>
264
- <tr v-if="loading">
265
- <td colspan="7" class="loading-cell">
266
- <div class="loading-content">
267
- <div class="loading-spinner"></div>
268
- <span>加载中...</span>
269
- </div>
270
- </td>
271
- </tr>
272
- <tr v-else-if="menuList.length === 0">
273
- <td colspan="7" class="empty-cell">
274
- <div class="empty-content">
275
- <svg viewBox="0 0 64 41" fill="none">
276
- <g transform="translate(0 1)">
277
- <ellipse fill="#f5f5f5" cx="32" cy="33" rx="32" ry="7"/>
278
- <g stroke="var(--color-text-placeholder)" stroke-width="2">
279
- <path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"/>
280
- <path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35H11.95C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" fill="var(--color-fill)"/>
281
- </g>
282
- </g>
283
- </svg>
284
- <span>暂无数据</span>
285
- </div>
286
- </td>
287
- </tr>
288
- <template v-else v-for="menu in menuList" :key="menu.id">
289
- <tr class="data-row tree-row--level-0">
290
- <td class="col-name">
291
- <div class="menu-info">
292
- <span class="menu-icon-wrapper">{{ getMenuIcon(menu.icon) }}</span>
293
- <span class="menu-title" @click="handleEdit(menu)">{{ menu.title }}</span>
294
- </div>
295
- </td>
296
- <td class="col-icon">
297
- <span class="icon-text">{{ getMenuIcon(menu.icon) }}</span>
298
- </td>
299
- <td class="col-path">
300
- <code class="path-code">{{ menu.path }}</code>
301
- </td>
302
- <td class="col-type">
303
- <Tag :type="getTypeTagType(menu.type)" size="small">
304
- {{ getTypeTagText(menu.type) }}
305
- </Tag>
306
- </td>
307
- <td class="col-sort">
308
- <span class="sort-badge">{{ menu.sort }}</span>
309
- </td>
310
- <td class="col-status">
311
- <Tag :type="menu.status === Status.ENABLED ? 'success' : 'danger'" size="small">
312
- {{ menu.status === Status.ENABLED ? '启用' : '禁用' }}
313
- </Tag>
314
- </td>
315
- <td class="col-actions">
316
- <Space class="action-buttons">
317
- <Button type="primary" link size="small" @click="handleAdd(menu.id)">
318
- <template #icon>
319
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
320
- <line x1="12" y1="5" x2="12" y2="19"/>
321
- <line x1="5" y1="12" x2="19" y2="12"/>
322
- </svg>
323
- </template>
324
- 新增
325
- </Button>
326
- <Button type="primary" link size="small" @click="handleEdit(menu)">
327
- <template #icon>
328
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
329
- <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
330
- <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
331
- </svg>
332
- </template>
333
- 编辑
334
- </Button>
335
- </Space>
336
- </td>
337
- </tr>
338
- <template v-if="menu.children" v-for="child in menu.children" :key="child.id">
339
- <tr class="data-row tree-row--level-1">
340
- <td class="col-name">
341
- <div class="menu-info">
342
- <span class="tree-indent"></span>
343
- <span class="tree-line"></span>
344
- <span class="menu-icon-wrapper">{{ getMenuIcon(child.icon) }}</span>
345
- <span class="menu-title" @click="handleEdit(child)">{{ child.title }}</span>
346
- </div>
347
- </td>
348
- <td class="col-icon">
349
- <span class="icon-text">{{ getMenuIcon(child.icon) }}</span>
350
- </td>
351
- <td class="col-path">
352
- <code class="path-code">{{ child.path }}</code>
353
- </td>
354
- <td class="col-type">
355
- <Tag :type="getTypeTagType(child.type)" size="small">
356
- {{ getTypeTagText(child.type) }}
357
- </Tag>
358
- </td>
359
- <td class="col-sort">
360
- <span class="sort-badge">{{ child.sort }}</span>
361
- </td>
362
- <td class="col-status">
363
- <Tag :type="child.status === Status.ENABLED ? 'success' : 'danger'" size="small">
364
- {{ child.status === Status.ENABLED ? '启用' : '禁用' }}
365
- </Tag>
366
- </td>
367
- <td class="col-actions">
368
- <Space class="action-buttons">
369
- <Button type="primary" link size="small" @click="handleEdit(child)">
370
- <template #icon>
371
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
372
- <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
373
- <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
374
- </svg>
375
- </template>
376
- 编辑
377
- </Button>
378
- </Space>
379
- </td>
380
- </tr>
381
- </template>
382
- </template>
383
- </tbody>
384
- </table>
385
- </div>
386
- </div>
387
-
388
- <!-- 编辑弹窗 -->
389
- <Modal v-model="modalVisible" :title="modalTitle" width="600px" class="menu-modal">
390
- <Form ref="formRef" :model="formData" :rules="rules" label-width="80px" class="menu-form">
391
- <div class="form-grid">
392
- <FormItem label="上级菜单" class="form-item-full">
393
- <Input v-model="formData.parentId" placeholder="上级菜单ID" disabled />
394
- </FormItem>
395
- <FormItem label="菜单类型">
396
- <Select v-model="formData.type" :options="MenuTypeOptions" />
397
- </FormItem>
398
- <FormItem label="排序">
399
- <InputNumber v-model="formData.sort" :min="0" />
400
- </FormItem>
401
- <FormItem label="菜单名称" prop="name">
402
- <Input v-model="formData.name" placeholder="请输入菜单名称(路由name)" />
403
- </FormItem>
404
- <FormItem label="菜单标题" prop="title">
405
- <Input v-model="formData.title" placeholder="请输入菜单标题" />
406
- </FormItem>
407
- <FormItem label="路由路径" prop="path" class="form-item-full">
408
- <Input v-model="formData.path" placeholder="请输入路由路径" />
409
- </FormItem>
410
- <FormItem label="组件路径" class="form-item-full">
411
- <Input v-model="formData.component" placeholder="请输入组件路径" />
412
- </FormItem>
413
- <FormItem label="图标">
414
- <Input v-model="formData.icon" placeholder="请输入图标名称" />
415
- </FormItem>
416
- <FormItem label="状态">
417
- <div class="status-field">
418
- <Switch v-model="formData.status" :active-value="Status.ENABLED" :inactive-value="Status.DISABLED" />
419
- <span class="status-label">{{ formData.status === Status.ENABLED ? '启用' : '禁用' }}</span>
420
- </div>
421
- </FormItem>
422
- <FormItem label="隐藏">
423
- <div class="status-field">
424
- <Switch v-model="formData.hidden" />
425
- <span class="status-label">{{ formData.hidden ? '隐藏' : '显示' }}</span>
426
- </div>
427
- </FormItem>
428
- <FormItem label="缓存">
429
- <div class="status-field">
430
- <Switch v-model="formData.keepAlive" />
431
- <span class="status-label">{{ formData.keepAlive ? '开启' : '关闭' }}</span>
432
- </div>
433
- </FormItem>
434
- </div>
435
- </Form>
436
- <template #footer>
437
- <Space>
438
- <Button @click="modalVisible = false">取消</Button>
439
- <Button type="primary" @click="handleSubmit">确定</Button>
440
- </Space>
441
- </template>
442
- </Modal>
443
- </div>
444
- </template>
445
-
446
- <style lang="scss" scoped>
447
- .menu-page {
448
- padding: 24px;
449
- background: var(--bg-color-page);
450
- min-height: 100%;
451
- }
452
-
453
- // 表格区域
454
- .table-section {
455
- background: var(--bg-color);
456
- border-radius: var(--border-radius-large);
457
- box-shadow: var(--box-shadow-card);
458
- overflow: hidden;
459
- }
460
-
461
- .table-toolbar {
462
- display: flex;
463
- justify-content: space-between;
464
- align-items: center;
465
- padding: 16px 24px;
466
- border-bottom: 1px solid var(--color-border-lighter);
467
-
468
- .toolbar-left {
469
- display: flex;
470
- gap: 12px;
471
- }
472
-
473
- .table-count {
474
- font-size: 14px;
475
- color: var(--color-text-secondary);
476
- }
477
- }
478
-
479
- .table-wrapper {
480
- overflow-x: auto;
481
- }
482
-
483
- // 表格样式
484
- .data-table {
485
- width: 100%;
486
- border-collapse: collapse;
487
-
488
- th, td {
489
- padding: 14px 16px;
490
- text-align: left;
491
- border-bottom: 1px solid var(--color-border-lighter);
492
- }
493
-
494
- th {
495
- font-size: 14px;
496
- font-weight: 500;
497
- color: var(--color-text-secondary);
498
- background: var(--color-fill-light);
499
- white-space: nowrap;
500
- }
501
-
502
- .data-row {
503
- transition: background-color 0.2s;
504
-
505
- &:hover {
506
- background: var(--color-primary-light-6);
507
- }
508
- }
509
-
510
- td {
511
- vertical-align: middle;
512
- }
513
-
514
- .loading-cell,
515
- .empty-cell {
516
- padding: 60px 20px;
517
- }
518
-
519
- .loading-content,
520
- .empty-content {
521
- display: flex;
522
- flex-direction: column;
523
- align-items: center;
524
- gap: 16px;
525
- color: var(--color-text-placeholder);
526
-
527
- svg {
528
- width: 64px;
529
- height: 41px;
530
- }
531
- }
532
-
533
- .loading-spinner {
534
- width: 32px;
535
- height: 32px;
536
- border: 3px solid var(--color-border-lighter);
537
- border-top-color: var(--color-primary);
538
- border-radius: 50%;
539
- animation: spin 0.8s linear infinite;
540
- }
541
- }
542
-
543
- @keyframes spin {
544
- to { transform: rotate(360deg); }
545
- }
546
-
547
- // 树形结构
548
- .tree-row--level-1 {
549
- .col-name {
550
- padding-left: 40px;
551
- }
552
- }
553
-
554
- .tree-indent {
555
- display: inline-block;
556
- width: 24px;
557
- }
558
-
559
- .tree-line {
560
- position: relative;
561
- display: inline-block;
562
- width: 16px;
563
- height: 1px;
564
- background: var(--color-border-light);
565
- margin-right: 8px;
566
- vertical-align: middle;
567
- }
568
-
569
- // 列样式
570
- .col-name {
571
- min-width: 200px;
572
- }
573
-
574
- .menu-info {
575
- display: flex;
576
- align-items: center;
577
- gap: 8px;
578
- }
579
-
580
- .menu-icon-wrapper {
581
- width: 28px;
582
- height: 28px;
583
- display: flex;
584
- align-items: center;
585
- justify-content: center;
586
- background: var(--color-fill-light);
587
- border-radius: 6px;
588
- font-size: 14px;
589
- }
590
-
591
- .menu-title {
592
- font-size: 14px;
593
- font-weight: 500;
594
- color: var(--color-text-primary);
595
- cursor: pointer;
596
- transition: color 0.2s;
597
-
598
- &:hover {
599
- color: var(--color-primary);
600
- }
601
- }
602
-
603
- .col-icon {
604
- width: 80px;
605
- }
606
-
607
- .icon-text {
608
- font-size: 16px;
609
- }
610
-
611
- .col-path {
612
- min-width: 140px;
613
- }
614
-
615
- .path-code {
616
- font-family: 'SF Mono', Monaco, Consolas, monospace;
617
- font-size: 12px;
618
- padding: 4px 8px;
619
- background: var(--color-fill-light);
620
- border-radius: 4px;
621
- color: var(--color-text-secondary);
622
- }
623
-
624
- .col-type {
625
- width: 80px;
626
- }
627
-
628
- .col-sort {
629
- width: 60px;
630
- }
631
-
632
- .sort-badge {
633
- display: inline-flex;
634
- align-items: center;
635
- justify-content: center;
636
- min-width: 24px;
637
- height: 24px;
638
- padding: 0 8px;
639
- background: var(--color-fill);
640
- border-radius: 12px;
641
- font-size: 12px;
642
- color: var(--color-text-secondary);
643
- }
644
-
645
- .col-status {
646
- width: 80px;
647
- }
648
-
649
- .col-actions {
650
- min-width: 140px;
651
- }
652
-
653
- .action-buttons {
654
- :deep(.x-button) {
655
- padding: 4px 8px;
656
-
657
- svg {
658
- width: 14px;
659
- height: 14px;
660
- margin-right: 4px;
661
- }
662
- }
663
- }
664
-
665
- // 弹窗表单
666
- .menu-form {
667
- padding: 16px 0;
668
-
669
- .form-grid {
670
- display: grid;
671
- grid-template-columns: repeat(2, 1fr);
672
- gap: 0 24px;
673
- }
674
-
675
- .form-item-full {
676
- grid-column: span 2;
677
- }
678
-
679
- .status-field {
680
- display: flex;
681
- align-items: center;
682
- gap: 12px;
683
- }
684
-
685
- .status-label {
686
- font-size: 14px;
687
- color: var(--color-text-secondary);
688
- }
689
- }
690
- </style>
1
+ <script setup lang="ts">
2
+ import { ref, reactive, computed, onMounted } from 'vue'
3
+ import { Form, FormItem, Input, Select, Switch, InputNumber } from '@xto/form'
4
+ import { Tag } from '@xto/data'
5
+ import { Modal, Message } from '@xto/feedback'
6
+ import { Space, Button } from '@xto/base'
7
+ import { MenuType, MenuTypeOptions, Status } from '@/enums'
8
+
9
+ // 菜单数据类型
10
+ interface Menu {
11
+ id: number
12
+ parentId: number | null
13
+ name: string
14
+ path: string
15
+ component: string
16
+ redirect: string
17
+ icon: string
18
+ title: string
19
+ type: MenuType
20
+ sort: number
21
+ status: Status
22
+ hidden: boolean
23
+ keepAlive: boolean
24
+ children?: Menu[]
25
+ }
26
+
27
+ // Mock 数据
28
+ const mockMenus: Menu[] = [
29
+ {
30
+ id: 1,
31
+ parentId: null,
32
+ name: 'Dashboard',
33
+ path: '/dashboard',
34
+ component: 'dashboard/index',
35
+ redirect: '',
36
+ icon: 'dashboard',
37
+ title: '仪表盘',
38
+ type: MenuType.MENU,
39
+ sort: 1,
40
+ status: Status.ENABLED,
41
+ hidden: false,
42
+ keepAlive: true
43
+ },
44
+ {
45
+ id: 2,
46
+ parentId: null,
47
+ name: 'System',
48
+ path: '/system',
49
+ component: '',
50
+ redirect: '/system/user',
51
+ icon: 'setting',
52
+ title: '系统管理',
53
+ type: MenuType.DIRECTORY,
54
+ sort: 2,
55
+ status: Status.ENABLED,
56
+ hidden: false,
57
+ keepAlive: false,
58
+ children: [
59
+ {
60
+ id: 21,
61
+ parentId: 2,
62
+ name: 'SystemUser',
63
+ path: '/system/user',
64
+ component: 'system/user/index',
65
+ redirect: '',
66
+ icon: 'user',
67
+ title: '用户管理',
68
+ type: MenuType.MENU,
69
+ sort: 1,
70
+ status: Status.ENABLED,
71
+ hidden: false,
72
+ keepAlive: true
73
+ },
74
+ {
75
+ id: 22,
76
+ parentId: 2,
77
+ name: 'SystemRole',
78
+ path: '/system/role',
79
+ component: 'system/role/index',
80
+ redirect: '',
81
+ icon: 'role',
82
+ title: '角色管理',
83
+ type: MenuType.MENU,
84
+ sort: 2,
85
+ status: Status.ENABLED,
86
+ hidden: false,
87
+ keepAlive: true
88
+ },
89
+ {
90
+ id: 23,
91
+ parentId: 2,
92
+ name: 'SystemMenu',
93
+ path: '/system/menu',
94
+ component: 'system/menu/index',
95
+ redirect: '',
96
+ icon: 'menu',
97
+ title: '菜单管理',
98
+ type: MenuType.MENU,
99
+ sort: 3,
100
+ status: Status.ENABLED,
101
+ hidden: false,
102
+ keepAlive: true
103
+ }
104
+ ]
105
+ }
106
+ ]
107
+
108
+ const loading = ref(false)
109
+ const menuList = ref<Menu[]>([])
110
+
111
+ // 弹窗
112
+ const modalVisible = ref(false)
113
+ const modalTitle = computed(() => formData.id ? '编辑菜单' : '新增菜单')
114
+ const formData = reactive({
115
+ id: 0,
116
+ parentId: null as number | null,
117
+ name: '',
118
+ path: '',
119
+ component: '',
120
+ redirect: '',
121
+ icon: '',
122
+ title: '',
123
+ type: MenuType.MENU,
124
+ sort: 0,
125
+ status: Status.ENABLED,
126
+ hidden: false,
127
+ keepAlive: true
128
+ })
129
+
130
+ const rules: Record<string, any[]> = {
131
+ name: [
132
+ { required: true, message: '请输入菜单名称', trigger: 'blur' }
133
+ ],
134
+ path: [
135
+ { required: true, message: '请输入路由路径', trigger: 'blur' }
136
+ ],
137
+ title: [
138
+ { required: true, message: '请输入菜单标题', trigger: 'blur' }
139
+ ]
140
+ }
141
+
142
+ const formRef = ref()
143
+
144
+ // 获取菜单列表
145
+ const getMenuList = () => {
146
+ loading.value = true
147
+ setTimeout(() => {
148
+ menuList.value = mockMenus
149
+ loading.value = false
150
+ }, 300)
151
+ }
152
+
153
+ // 新增
154
+ const handleAdd = (parentId: number | null = null) => {
155
+ Object.assign(formData, {
156
+ id: 0,
157
+ parentId,
158
+ name: '',
159
+ path: '',
160
+ component: '',
161
+ redirect: '',
162
+ icon: '',
163
+ title: '',
164
+ type: MenuType.MENU,
165
+ sort: 0,
166
+ status: Status.ENABLED,
167
+ hidden: false,
168
+ keepAlive: true
169
+ })
170
+ modalVisible.value = true
171
+ }
172
+
173
+ // 编辑
174
+ const handleEdit = (node: Menu) => {
175
+ Object.assign(formData, node)
176
+ modalVisible.value = true
177
+ }
178
+
179
+ // 提交
180
+ const handleSubmit = async () => {
181
+ try {
182
+ await formRef.value?.validate()
183
+ Message.success(formData.id ? '编辑成功' : '新增成功')
184
+ modalVisible.value = false
185
+ getMenuList()
186
+ } catch (error) {
187
+ console.error(error)
188
+ }
189
+ }
190
+
191
+ // 菜单图标
192
+ const getMenuIcon = (icon?: string) => {
193
+ const iconMap: Record<string, string> = {
194
+ dashboard: '📊',
195
+ system: '⚙️',
196
+ user: '👤',
197
+ role: '👥',
198
+ menu: '📋',
199
+ setting: '🔧'
200
+ }
201
+ return iconMap[icon || ''] || '📄'
202
+ }
203
+
204
+ // 获取类型标签类型
205
+ const getTypeTagType = (type: MenuType) => {
206
+ switch (type) {
207
+ case MenuType.DIRECTORY: return 'primary'
208
+ case MenuType.MENU: return 'success'
209
+ default: return 'warning'
210
+ }
211
+ }
212
+
213
+ // 获取类型标签文本
214
+ const getTypeTagText = (type: MenuType) => {
215
+ switch (type) {
216
+ case MenuType.DIRECTORY: return '目录'
217
+ case MenuType.MENU: return '菜单'
218
+ default: return '按钮'
219
+ }
220
+ }
221
+
222
+ onMounted(() => {
223
+ getMenuList()
224
+ })
225
+ </script>
226
+
227
+ <template>
228
+ <div class="menu-page">
229
+ <!-- 表格 -->
230
+ <div class="table-section">
231
+ <!-- 工具栏 -->
232
+ <div class="table-toolbar">
233
+ <div class="toolbar-left">
234
+ <Button type="primary" @click="handleAdd()">
235
+ <template #icon>
236
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
237
+ <line x1="12" y1="5" x2="12" y2="19"/>
238
+ <line x1="5" y1="12" x2="19" y2="12"/>
239
+ </svg>
240
+ </template>
241
+ 新增菜单
242
+ </Button>
243
+ </div>
244
+ <div class="toolbar-right">
245
+ <span class="table-count">共 {{ menuList.length }} 个菜单</span>
246
+ </div>
247
+ </div>
248
+
249
+ <!-- 菜单树表格 -->
250
+ <div class="table-wrapper">
251
+ <table class="data-table">
252
+ <thead>
253
+ <tr>
254
+ <th class="col-name">菜单名称</th>
255
+ <th class="col-icon">图标</th>
256
+ <th class="col-path">路由路径</th>
257
+ <th class="col-type">类型</th>
258
+ <th class="col-sort">排序</th>
259
+ <th class="col-status">状态</th>
260
+ <th class="col-actions">操作</th>
261
+ </tr>
262
+ </thead>
263
+ <tbody>
264
+ <tr v-if="loading">
265
+ <td colspan="7" class="loading-cell">
266
+ <div class="loading-content">
267
+ <div class="loading-spinner"></div>
268
+ <span>加载中...</span>
269
+ </div>
270
+ </td>
271
+ </tr>
272
+ <tr v-else-if="menuList.length === 0">
273
+ <td colspan="7" class="empty-cell">
274
+ <div class="empty-content">
275
+ <svg viewBox="0 0 64 41" fill="none">
276
+ <g transform="translate(0 1)">
277
+ <ellipse fill="#f5f5f5" cx="32" cy="33" rx="32" ry="7"/>
278
+ <g stroke="var(--color-text-placeholder)" stroke-width="2">
279
+ <path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"/>
280
+ <path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35H11.95C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" fill="var(--color-fill)"/>
281
+ </g>
282
+ </g>
283
+ </svg>
284
+ <span>暂无数据</span>
285
+ </div>
286
+ </td>
287
+ </tr>
288
+ <template v-else v-for="menu in menuList" :key="menu.id">
289
+ <tr class="data-row tree-row--level-0">
290
+ <td class="col-name">
291
+ <div class="menu-info">
292
+ <span class="menu-icon-wrapper">{{ getMenuIcon(menu.icon) }}</span>
293
+ <span class="menu-title" @click="handleEdit(menu)">{{ menu.title }}</span>
294
+ </div>
295
+ </td>
296
+ <td class="col-icon">
297
+ <span class="icon-text">{{ getMenuIcon(menu.icon) }}</span>
298
+ </td>
299
+ <td class="col-path">
300
+ <code class="path-code">{{ menu.path }}</code>
301
+ </td>
302
+ <td class="col-type">
303
+ <Tag :type="getTypeTagType(menu.type)" size="small">
304
+ {{ getTypeTagText(menu.type) }}
305
+ </Tag>
306
+ </td>
307
+ <td class="col-sort">
308
+ <span class="sort-badge">{{ menu.sort }}</span>
309
+ </td>
310
+ <td class="col-status">
311
+ <Tag :type="menu.status === Status.ENABLED ? 'success' : 'danger'" size="small">
312
+ {{ menu.status === Status.ENABLED ? '启用' : '禁用' }}
313
+ </Tag>
314
+ </td>
315
+ <td class="col-actions">
316
+ <Space class="action-buttons">
317
+ <Button type="primary" link size="small" @click="handleAdd(menu.id)">
318
+ <template #icon>
319
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
320
+ <line x1="12" y1="5" x2="12" y2="19"/>
321
+ <line x1="5" y1="12" x2="19" y2="12"/>
322
+ </svg>
323
+ </template>
324
+ 新增
325
+ </Button>
326
+ <Button type="primary" link size="small" @click="handleEdit(menu)">
327
+ <template #icon>
328
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
329
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
330
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
331
+ </svg>
332
+ </template>
333
+ 编辑
334
+ </Button>
335
+ </Space>
336
+ </td>
337
+ </tr>
338
+ <template v-if="menu.children" v-for="child in menu.children" :key="child.id">
339
+ <tr class="data-row tree-row--level-1">
340
+ <td class="col-name">
341
+ <div class="menu-info">
342
+ <span class="tree-indent"></span>
343
+ <span class="tree-line"></span>
344
+ <span class="menu-icon-wrapper">{{ getMenuIcon(child.icon) }}</span>
345
+ <span class="menu-title" @click="handleEdit(child)">{{ child.title }}</span>
346
+ </div>
347
+ </td>
348
+ <td class="col-icon">
349
+ <span class="icon-text">{{ getMenuIcon(child.icon) }}</span>
350
+ </td>
351
+ <td class="col-path">
352
+ <code class="path-code">{{ child.path }}</code>
353
+ </td>
354
+ <td class="col-type">
355
+ <Tag :type="getTypeTagType(child.type)" size="small">
356
+ {{ getTypeTagText(child.type) }}
357
+ </Tag>
358
+ </td>
359
+ <td class="col-sort">
360
+ <span class="sort-badge">{{ child.sort }}</span>
361
+ </td>
362
+ <td class="col-status">
363
+ <Tag :type="child.status === Status.ENABLED ? 'success' : 'danger'" size="small">
364
+ {{ child.status === Status.ENABLED ? '启用' : '禁用' }}
365
+ </Tag>
366
+ </td>
367
+ <td class="col-actions">
368
+ <Space class="action-buttons">
369
+ <Button type="primary" link size="small" @click="handleEdit(child)">
370
+ <template #icon>
371
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
372
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
373
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
374
+ </svg>
375
+ </template>
376
+ 编辑
377
+ </Button>
378
+ </Space>
379
+ </td>
380
+ </tr>
381
+ </template>
382
+ </template>
383
+ </tbody>
384
+ </table>
385
+ </div>
386
+ </div>
387
+
388
+ <!-- 编辑弹窗 -->
389
+ <Modal v-model="modalVisible" :title="modalTitle" width="600px" class="menu-modal">
390
+ <Form ref="formRef" :model="formData" :rules="rules" label-width="80px" class="menu-form">
391
+ <div class="form-grid">
392
+ <FormItem label="上级菜单" class="form-item-full">
393
+ <Input v-model="formData.parentId" placeholder="上级菜单ID" disabled />
394
+ </FormItem>
395
+ <FormItem label="菜单类型">
396
+ <Select v-model="formData.type" :options="MenuTypeOptions" />
397
+ </FormItem>
398
+ <FormItem label="排序">
399
+ <InputNumber v-model="formData.sort" :min="0" />
400
+ </FormItem>
401
+ <FormItem label="菜单名称" prop="name">
402
+ <Input v-model="formData.name" placeholder="请输入菜单名称(路由name)" />
403
+ </FormItem>
404
+ <FormItem label="菜单标题" prop="title">
405
+ <Input v-model="formData.title" placeholder="请输入菜单标题" />
406
+ </FormItem>
407
+ <FormItem label="路由路径" prop="path" class="form-item-full">
408
+ <Input v-model="formData.path" placeholder="请输入路由路径" />
409
+ </FormItem>
410
+ <FormItem label="组件路径" class="form-item-full">
411
+ <Input v-model="formData.component" placeholder="请输入组件路径" />
412
+ </FormItem>
413
+ <FormItem label="图标">
414
+ <Input v-model="formData.icon" placeholder="请输入图标名称" />
415
+ </FormItem>
416
+ <FormItem label="状态">
417
+ <div class="status-field">
418
+ <Switch v-model="formData.status" :active-value="Status.ENABLED" :inactive-value="Status.DISABLED" />
419
+ <span class="status-label">{{ formData.status === Status.ENABLED ? '启用' : '禁用' }}</span>
420
+ </div>
421
+ </FormItem>
422
+ <FormItem label="隐藏">
423
+ <div class="status-field">
424
+ <Switch v-model="formData.hidden" />
425
+ <span class="status-label">{{ formData.hidden ? '隐藏' : '显示' }}</span>
426
+ </div>
427
+ </FormItem>
428
+ <FormItem label="缓存">
429
+ <div class="status-field">
430
+ <Switch v-model="formData.keepAlive" />
431
+ <span class="status-label">{{ formData.keepAlive ? '开启' : '关闭' }}</span>
432
+ </div>
433
+ </FormItem>
434
+ </div>
435
+ </Form>
436
+ <template #footer>
437
+ <Space>
438
+ <Button @click="modalVisible = false">取消</Button>
439
+ <Button type="primary" @click="handleSubmit">确定</Button>
440
+ </Space>
441
+ </template>
442
+ </Modal>
443
+ </div>
444
+ </template>
445
+
446
+ <style lang="scss" scoped>
447
+ .menu-page {
448
+ padding: 24px;
449
+ background: var(--bg-color-page);
450
+ min-height: 100%;
451
+ }
452
+
453
+ // 表格区域
454
+ .table-section {
455
+ background: var(--bg-color);
456
+ border-radius: var(--border-radius-large);
457
+ box-shadow: var(--box-shadow-card);
458
+ overflow: hidden;
459
+ }
460
+
461
+ .table-toolbar {
462
+ display: flex;
463
+ justify-content: space-between;
464
+ align-items: center;
465
+ padding: 16px 24px;
466
+ border-bottom: 1px solid var(--color-border-lighter);
467
+
468
+ .toolbar-left {
469
+ display: flex;
470
+ gap: 12px;
471
+ }
472
+
473
+ .table-count {
474
+ font-size: 14px;
475
+ color: var(--color-text-secondary);
476
+ }
477
+ }
478
+
479
+ .table-wrapper {
480
+ overflow-x: auto;
481
+ }
482
+
483
+ // 表格样式
484
+ .data-table {
485
+ width: 100%;
486
+ border-collapse: collapse;
487
+
488
+ th, td {
489
+ padding: 14px 16px;
490
+ text-align: left;
491
+ border-bottom: 1px solid var(--color-border-lighter);
492
+ }
493
+
494
+ th {
495
+ font-size: 14px;
496
+ font-weight: 500;
497
+ color: var(--color-text-secondary);
498
+ background: var(--color-fill-light);
499
+ white-space: nowrap;
500
+ }
501
+
502
+ .data-row {
503
+ transition: background-color 0.2s;
504
+
505
+ &:hover {
506
+ background: var(--color-primary-light-6);
507
+ }
508
+ }
509
+
510
+ td {
511
+ vertical-align: middle;
512
+ }
513
+
514
+ .loading-cell,
515
+ .empty-cell {
516
+ padding: 60px 20px;
517
+ }
518
+
519
+ .loading-content,
520
+ .empty-content {
521
+ display: flex;
522
+ flex-direction: column;
523
+ align-items: center;
524
+ gap: 16px;
525
+ color: var(--color-text-placeholder);
526
+
527
+ svg {
528
+ width: 64px;
529
+ height: 41px;
530
+ }
531
+ }
532
+
533
+ .loading-spinner {
534
+ width: 32px;
535
+ height: 32px;
536
+ border: 3px solid var(--color-border-lighter);
537
+ border-top-color: var(--color-primary);
538
+ border-radius: 50%;
539
+ animation: spin 0.8s linear infinite;
540
+ }
541
+ }
542
+
543
+ @keyframes spin {
544
+ to { transform: rotate(360deg); }
545
+ }
546
+
547
+ // 树形结构
548
+ .tree-row--level-1 {
549
+ .col-name {
550
+ padding-left: 40px;
551
+ }
552
+ }
553
+
554
+ .tree-indent {
555
+ display: inline-block;
556
+ width: 24px;
557
+ }
558
+
559
+ .tree-line {
560
+ position: relative;
561
+ display: inline-block;
562
+ width: 16px;
563
+ height: 1px;
564
+ background: var(--color-border-light);
565
+ margin-right: 8px;
566
+ vertical-align: middle;
567
+ }
568
+
569
+ // 列样式
570
+ .col-name {
571
+ min-width: 200px;
572
+ }
573
+
574
+ .menu-info {
575
+ display: flex;
576
+ align-items: center;
577
+ gap: 8px;
578
+ }
579
+
580
+ .menu-icon-wrapper {
581
+ width: 28px;
582
+ height: 28px;
583
+ display: flex;
584
+ align-items: center;
585
+ justify-content: center;
586
+ background: var(--color-fill-light);
587
+ border-radius: 6px;
588
+ font-size: 14px;
589
+ }
590
+
591
+ .menu-title {
592
+ font-size: 14px;
593
+ font-weight: 500;
594
+ color: var(--color-text-primary);
595
+ cursor: pointer;
596
+ transition: color 0.2s;
597
+
598
+ &:hover {
599
+ color: var(--color-primary);
600
+ }
601
+ }
602
+
603
+ .col-icon {
604
+ width: 80px;
605
+ }
606
+
607
+ .icon-text {
608
+ font-size: 16px;
609
+ }
610
+
611
+ .col-path {
612
+ min-width: 140px;
613
+ }
614
+
615
+ .path-code {
616
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
617
+ font-size: 12px;
618
+ padding: 4px 8px;
619
+ background: var(--color-fill-light);
620
+ border-radius: 4px;
621
+ color: var(--color-text-secondary);
622
+ }
623
+
624
+ .col-type {
625
+ width: 80px;
626
+ }
627
+
628
+ .col-sort {
629
+ width: 60px;
630
+ }
631
+
632
+ .sort-badge {
633
+ display: inline-flex;
634
+ align-items: center;
635
+ justify-content: center;
636
+ min-width: 24px;
637
+ height: 24px;
638
+ padding: 0 8px;
639
+ background: var(--color-fill);
640
+ border-radius: 12px;
641
+ font-size: 12px;
642
+ color: var(--color-text-secondary);
643
+ }
644
+
645
+ .col-status {
646
+ width: 80px;
647
+ }
648
+
649
+ .col-actions {
650
+ min-width: 140px;
651
+ }
652
+
653
+ .action-buttons {
654
+ :deep(.x-button) {
655
+ padding: 4px 8px;
656
+
657
+ svg {
658
+ width: 14px;
659
+ height: 14px;
660
+ margin-right: 4px;
661
+ }
662
+ }
663
+ }
664
+
665
+ // 弹窗表单
666
+ .menu-form {
667
+ padding: 16px 0;
668
+
669
+ .form-grid {
670
+ display: grid;
671
+ grid-template-columns: repeat(2, 1fr);
672
+ gap: 0 24px;
673
+ }
674
+
675
+ .form-item-full {
676
+ grid-column: span 2;
677
+ }
678
+
679
+ .status-field {
680
+ display: flex;
681
+ align-items: center;
682
+ gap: 12px;
683
+ }
684
+
685
+ .status-label {
686
+ font-size: 14px;
687
+ color: var(--color-text-secondary);
688
+ }
689
+ }
690
+ </style>