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,1171 +1,1171 @@
1
- <script setup lang="ts">
2
- import { computed, ref, onMounted, onUnmounted } from 'vue'
3
- import { useRoute, useRouter } from 'vue-router'
4
- import { useMenuStore } from '@/stores/menu'
5
- import { useAppStore } from '@/stores/app'
6
- import { useUserStore } from '@/stores/user'
7
- import { useAuthStore } from '@/stores/auth'
8
- import { Menu, MenuItem, SubMenu } from '@xto/navigation'
9
- import { Icon } from '@xto/base'
10
- import { Drawer } from '@xto/feedback'
11
-
12
- const route = useRoute()
13
- const router = useRouter()
14
- const menuStore = useMenuStore()
15
- const appStore = useAppStore()
16
- const userStore = useUserStore()
17
- const authStore = useAuthStore()
18
-
19
- const activeMenu = computed(() => route.path)
20
-
21
- // 扁平化菜单用于搜索
22
- const flattenMenus = (menus: any[], parentTitle = ''): any[] => {
23
- const result: any[] = []
24
- menus.forEach(menu => {
25
- if (menu.children && menu.children.length > 0) {
26
- result.push(...flattenMenus(menu.children, menu.menuName))
27
- } else {
28
- result.push({ ...menu, parentTitle, title: menu.menuName, path: menu.menuUrl })
29
- }
30
- })
31
- return result
32
- }
33
-
34
- // 搜索结果
35
- const searchResults = computed(() => {
36
- if (!searchKeyword.value.trim()) return []
37
- const flatMenus = flattenMenus(menuStore.menuList)
38
- return flatMenus.filter(menu =>
39
- menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
40
- )
41
- })
42
-
43
- // 菜单主题相关
44
- const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
45
- const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
46
- const menuActiveTextColor = computed(() => '#409eff')
47
-
48
- // 下拉菜单
49
- const dropdownVisible = ref(false)
50
- const dropdownRef = ref<HTMLElement | null>(null)
51
- const isFullscreen = ref(false)
52
- const drawerVisible = ref(false)
53
- const greyMode = ref(false)
54
- const searchVisible = ref(false)
55
- const searchKeyword = ref('')
56
- const searchRef = ref<HTMLElement | null>(null)
57
-
58
- // 菜单选择
59
- const handleMenuSelect = (index: string) => {
60
- if (index && index !== route.path) {
61
- router.push(index)
62
- }
63
- }
64
-
65
- // 已知的图标名称列表
66
- const knownIcons = new Set([
67
- 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right',
68
- 'caret-down', 'caret-right', 'plus', 'minus', 'close', 'check',
69
- 'edit', 'delete', 'copy', 'download', 'upload', 'refresh', 'search',
70
- 'filter', 'more', 'setting', 'share', 'loading', 'info', 'success',
71
- 'warning', 'error', 'question', 'user', 'user-add', 'user-group',
72
- 'logout', 'login', 'file', 'folder', 'folder-open', 'document',
73
- 'image', 'video', 'music', 'camera', 'mail', 'phone', 'chat',
74
- 'bell', 'message', 'eye', 'eye-off', 'calendar', 'clock', 'history',
75
- 'timer', 'location', 'map', 'globe', 'star', 'heart', 'thumb-up',
76
- 'link', 'external-link', 'lock', 'unlock', 'key', 'home', 'menu',
77
- 'menu-fold', 'menu-unfold', 'sidebar-fold', 'sidebar-expand',
78
- 'sidebar-left', 'dashboard', 'chart', 'chart-pie', 'chart-line',
79
- 'report', 'analytics', 'system', 'permission', 'role', 'user-manage',
80
- 'log', 'notification', 'app', 'list', 'grid', 'fullscreen',
81
- 'fullscreen-exit', 'zoom-in', 'zoom-out', 'print', 'bookmark',
82
- 'tag', 'code', 'terminal', 'database', 'server', 'cloud', 'gift',
83
- 'moon', 'sun', 'theme', 'skin'
84
- ])
85
-
86
- // 获取菜单图标名称
87
- const getMenuIcon = (icon?: string): string => {
88
- if (!icon || icon === '') return ''
89
-
90
- if (icon.startsWith('tineco-icon-')) {
91
- const iconName = icon.replace('tineco-icon-', '')
92
- const tinecoIconMap: Record<string, string> = {
93
- home: 'home',
94
- dashboard: 'dashboard',
95
- system: 'system',
96
- user: 'user',
97
- role: 'role',
98
- menu: 'list',
99
- setting: 'setting',
100
- file: 'file',
101
- folder: 'folder',
102
- chart: 'chart',
103
- report: 'report',
104
- analytics: 'analytics'
105
- }
106
- return tinecoIconMap[iconName] || iconName
107
- }
108
-
109
- const iconMap: Record<string, string> = {
110
- dashboard: 'dashboard',
111
- system: 'system',
112
- user: 'user',
113
- role: 'role',
114
- menu: 'list',
115
- setting: 'setting',
116
- home: 'home',
117
- chart: 'chart',
118
- report: 'report',
119
- analytics: 'analytics',
120
- permission: 'permission',
121
- log: 'log',
122
- notification: 'notification',
123
- app: 'app',
124
- list: 'list',
125
- grid: 'grid'
126
- }
127
-
128
- return iconMap[icon] || icon
129
- }
130
-
131
- const getFirstChar = (name?: string): string => {
132
- if (!name) return ''
133
- return name.charAt(0)
134
- }
135
-
136
- const iconExists = (iconName: string): boolean => {
137
- return knownIcons.has(iconName)
138
- }
139
-
140
- // 切换主题
141
- const toggleTheme = () => {
142
- appStore.toggleTheme()
143
- drawerVisible.value = false
144
- }
145
-
146
- // 切换全屏
147
- const toggleFullscreen = () => {
148
- if (!document.fullscreenElement) {
149
- document.documentElement.requestFullscreen()
150
- } else {
151
- document.exitFullscreen()
152
- }
153
- }
154
-
155
- // 监听全屏变化
156
- const handleFullscreenChange = () => {
157
- isFullscreen.value = !!document.fullscreenElement
158
- }
159
-
160
- // 切换下拉菜单
161
- const toggleDropdown = () => {
162
- dropdownVisible.value = !dropdownVisible.value
163
- }
164
-
165
- // 关闭下拉菜单
166
- const closeDropdowns = () => {
167
- dropdownVisible.value = false
168
- }
169
-
170
- // 隐藏搜索
171
- const hideSearch = () => {
172
- searchVisible.value = false
173
- searchKeyword.value = ''
174
- }
175
-
176
- // 搜索结果点击
177
- const handleSearchItemClick = (path: string) => {
178
- router.push(path)
179
- hideSearch()
180
- }
181
-
182
- // 打开设置抽屉
183
- const openSettingsDrawer = () => {
184
- drawerVisible.value = true
185
- }
186
-
187
- // 切换灰色模式
188
- const handleGreyModeChange = (value: boolean) => {
189
- greyMode.value = value
190
- const html = document.documentElement
191
- if (value) {
192
- html.classList.add('grey-mode')
193
- } else {
194
- html.classList.remove('grey-mode')
195
- }
196
- }
197
-
198
- // 抽屉内灰色模式开关
199
- const handleGreyModeToggle = () => {
200
- handleGreyModeChange(!greyMode.value)
201
- drawerVisible.value = false
202
- }
203
-
204
- // 抽屉内暗黑模式开关
205
- const handleDarkModeToggle = () => {
206
- appStore.toggleTheme()
207
- drawerVisible.value = false
208
- }
209
-
210
- // 布局模式选项
211
- type LayoutMode = 'sidebar' | 'top' | 'mix'
212
- const layoutOptions: { value: LayoutMode; label: string; icon: string }[] = [
213
- { value: 'sidebar', label: '左侧菜单', icon: 'sidebar-left' },
214
- { value: 'top', label: '顶部菜单', icon: 'menu' },
215
- { value: 'mix', label: '混合菜单', icon: 'grid' }
216
- ]
217
-
218
- const currentLayout = computed(() => appStore.layout)
219
-
220
- // 切换布局模式
221
- const handleLayoutChange = (mode: LayoutMode) => {
222
- appStore.setLayout(mode)
223
- drawerVisible.value = false
224
- }
225
-
226
- // 主题色选项
227
- const colorOptions = [
228
- { value: '#409eff', label: '默认蓝' },
229
- { value: '#1890ff', label: '科技蓝' },
230
- { value: '#52c41a', label: '极光绿' },
231
- { value: '#faad14', label: '日落橙' },
232
- { value: '#f5222d', label: '薄暮红' },
233
- { value: '#722ed1', label: '酱紫' }
234
- ]
235
-
236
- // 设置主题色
237
- const handleColorChange = (color: string) => {
238
- appStore.setPrimaryColor(color)
239
- drawerVisible.value = false
240
- }
241
-
242
- // 个人信息
243
- const handleProfile = () => {
244
- closeDropdowns()
245
- router.push('/profile')
246
- }
247
-
248
- // 修改密码
249
- const handleChangePassword = () => {
250
- closeDropdowns()
251
- router.push('/change-password')
252
- }
253
-
254
- // 退出登录
255
- const handleLogout = () => {
256
- closeDropdowns()
257
- authStore.logout()
258
- userStore.clearUserInfo()
259
- menuStore.clearMenu()
260
- router.push('/login')
261
- }
262
-
263
- // 点击外部关闭下拉菜单
264
- const handleClickOutside = (event: MouseEvent) => {
265
- if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
266
- closeDropdowns()
267
- }
268
- if (searchRef.value && !searchRef.value.contains(event.target as Node)) {
269
- hideSearch()
270
- }
271
- }
272
-
273
- // 键盘快捷键
274
- const handleKeydown = (event: KeyboardEvent) => {
275
- if (event.key === 'Escape') {
276
- hideSearch()
277
- closeDropdowns()
278
- }
279
- }
280
-
281
- onMounted(() => {
282
- document.addEventListener('click', handleClickOutside)
283
- document.addEventListener('fullscreenchange', handleFullscreenChange)
284
- document.addEventListener('keydown', handleKeydown)
285
- greyMode.value = document.documentElement.classList.contains('grey-mode')
286
- })
287
-
288
- onUnmounted(() => {
289
- document.removeEventListener('click', handleClickOutside)
290
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
291
- document.removeEventListener('keydown', handleKeydown)
292
- })
293
- </script>
294
-
295
- <template>
296
- <div class="top-menu">
297
- <!-- 左侧 Logo -->
298
- <div class="top-menu__logo">
299
- <img src="/vite.svg" alt="Logo" class="top-menu__logo-img" />
300
- <span class="top-menu__logo-text">{{ appStore.appName }}</span>
301
- </div>
302
-
303
- <!-- 左侧菜单 -->
304
- <Menu
305
- :model-value="activeMenu"
306
- mode="horizontal"
307
- :background-color="menuBgColor"
308
- :text-color="menuTextColor"
309
- :active-text-color="menuActiveTextColor"
310
- class="top-menu__menu"
311
- @select="handleMenuSelect"
312
- >
313
- <template v-for="menu in menuStore.menuList" :key="menu.menuUrl">
314
- <!-- 有子菜单 -->
315
- <SubMenu v-if="menu.children && menu.children.length > 0" :index="menu.menuUrl">
316
- <template #title>
317
- <span class="top-menu__menu-content">
318
- <span v-if="menu.menuName !== '首页'" class="top-menu__menu-icon">
319
- <Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
320
- <span v-else class="top-menu__menu-char">{{ getFirstChar(menu.menuName) }}</span>
321
- </span>
322
- <span class="top-menu__menu-text">{{ menu.menuName }}</span>
323
- </span>
324
- </template>
325
- <!-- 递归渲染子菜单 -->
326
- <template v-for="child in menu.children" :key="child.menuUrl">
327
- <SubMenu v-if="child.children && child.children.length > 0" :index="child.menuUrl">
328
- <template #title>
329
- <span class="top-menu__menu-content">
330
- <span class="top-menu__menu-icon">
331
- <Icon v-if="iconExists(getMenuIcon(child.icon))" :name="getMenuIcon(child.icon)" :size="16" />
332
- <span v-else class="top-menu__menu-char">{{ getFirstChar(child.menuName) }}</span>
333
- </span>
334
- <span class="top-menu__menu-text">{{ child.menuName }}</span>
335
- </span>
336
- </template>
337
- <!-- 三级菜单项 -->
338
- <MenuItem
339
- v-for="grandChild in child.children"
340
- :key="grandChild.menuUrl"
341
- :index="grandChild.menuUrl"
342
- >
343
- <span class="top-menu__menu-content">
344
- <span class="top-menu__menu-icon">
345
- <Icon v-if="iconExists(getMenuIcon(grandChild.icon))" :name="getMenuIcon(grandChild.icon)" :size="16" />
346
- <span v-else class="top-menu__menu-char">{{ getFirstChar(grandChild.menuName) }}</span>
347
- </span>
348
- <span class="top-menu__menu-text">{{ grandChild.menuName }}</span>
349
- </span>
350
- </MenuItem>
351
- </SubMenu>
352
- <!-- 二级菜单项 -->
353
- <MenuItem v-else :index="child.menuUrl">
354
- <span class="top-menu__menu-content">
355
- <span class="top-menu__menu-icon">
356
- <Icon v-if="iconExists(getMenuIcon(child.icon))" :name="getMenuIcon(child.icon)" :size="16" />
357
- <span v-else class="top-menu__menu-char">{{ getFirstChar(child.menuName) }}</span>
358
- </span>
359
- <span class="top-menu__menu-text">{{ child.menuName }}</span>
360
- </span>
361
- </MenuItem>
362
- </template>
363
- </SubMenu>
364
- <!-- 无子菜单 -->
365
- <MenuItem v-else :index="menu.menuUrl">
366
- <span class="top-menu__menu-content">
367
- <span v-if="menu.menuName !== '首页'" class="top-menu__menu-icon">
368
- <Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
369
- <span v-else class="top-menu__menu-char">{{ getFirstChar(menu.menuName) }}</span>
370
- </span>
371
- <span class="top-menu__menu-text">{{ menu.menuName }}</span>
372
- </span>
373
- </MenuItem>
374
- </template>
375
- </Menu>
376
-
377
- <!-- 右侧操作区域 -->
378
- <div class="top-menu__actions">
379
- <!-- 搜索输入框 -->
380
- <div class="top-menu__search" ref="searchRef">
381
- <Icon name="search" :size="14" class="top-menu__search-icon" />
382
- <input
383
- v-model="searchKeyword"
384
- type="text"
385
- class="top-menu__search-input"
386
- placeholder="搜索菜单..."
387
- @focus="searchVisible = true"
388
- />
389
- <!-- 搜索结果下拉 -->
390
- <Transition name="search-dropdown">
391
- <div v-if="searchVisible && (searchResults.length > 0 || searchKeyword)" class="top-menu__search-dropdown">
392
- <div v-if="searchResults.length > 0" class="top-menu__search-results">
393
- <div
394
- v-for="item in searchResults"
395
- :key="item.path"
396
- class="top-menu__search-item"
397
- @click="handleSearchItemClick(item.path)"
398
- >
399
- <span v-if="item.title !== '首页'" class="top-menu__search-icon-item">
400
- <Icon v-if="iconExists(getMenuIcon(item.icon))" :name="getMenuIcon(item.icon)" :size="16" />
401
- <span v-else class="top-menu__search-char">{{ getFirstChar(item.title) }}</span>
402
- </span>
403
- <span class="top-menu__search-item-title">{{ item.title }}</span>
404
- <span v-if="item.parentTitle" class="top-menu__search-item-parent">{{ item.parentTitle }}</span>
405
- </div>
406
- </div>
407
- <div v-else class="top-menu__search-empty">
408
- 未找到匹配的菜单
409
- </div>
410
- </div>
411
- </Transition>
412
- </div>
413
-
414
- <!-- 全屏切换 -->
415
- <div class="top-menu__action" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
416
- <Icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" :size="16" />
417
- </div>
418
-
419
- <!-- 换肤设置 -->
420
- <div class="top-menu__action" @click="openSettingsDrawer" title="换肤设置">
421
- <Icon name="skin" :size="16" />
422
- </div>
423
-
424
- <!-- 主题切换 -->
425
- <div class="top-menu__action" @click="toggleTheme" title="切换主题">
426
- <Icon :name="appStore.isDark ? 'sun' : 'moon'" :size="16" />
427
- </div>
428
-
429
- <!-- 用户头像 -->
430
- <div class="top-menu__user" ref="dropdownRef">
431
- <div class="top-menu__user-trigger" @click.stop="toggleDropdown">
432
- <div class="top-menu__avatar">
433
- <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
434
- </div>
435
- <span class="top-menu__user-name">{{ userStore.userName }}</span>
436
- <span class="top-menu__user-arrow" :class="{ 'is-active': dropdownVisible }">▼</span>
437
- </div>
438
-
439
- <!-- 下拉菜单 -->
440
- <Transition name="dropdown">
441
- <div v-if="dropdownVisible" class="top-menu__dropdown">
442
- <div class="top-menu__dropdown-header">
443
- <div class="top-menu__dropdown-avatar">
444
- <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
445
- </div>
446
- <div class="top-menu__dropdown-info">
447
- <div class="top-menu__dropdown-name">{{ userStore.userName }}</div>
448
- <div class="top-menu__dropdown-role">{{ userStore.departmentName }}</div>
449
- </div>
450
- </div>
451
- <div class="top-menu__dropdown-divider"></div>
452
- <div class="top-menu__dropdown-menu">
453
- <div class="top-menu__dropdown-item" @click="handleProfile">
454
- <Icon name="user" :size="16" />
455
- <span>个人信息</span>
456
- </div>
457
- <div class="top-menu__dropdown-item" @click="handleChangePassword">
458
- <Icon name="lock" :size="16" />
459
- <span>修改密码</span>
460
- </div>
461
- <div class="top-menu__dropdown-divider"></div>
462
- <div class="top-menu__dropdown-item top-menu__dropdown-item--danger" @click="handleLogout">
463
- <Icon name="logout" :size="16" />
464
- <span>退出登录</span>
465
- </div>
466
- </div>
467
- </div>
468
- </Transition>
469
- </div>
470
- </div>
471
-
472
- <!-- 换肤设置抽屉 -->
473
- <Drawer
474
- v-model="drawerVisible"
475
- title="换肤设置"
476
- direction="rtl"
477
- size="320px"
478
- >
479
- <div class="settings-drawer">
480
- <!-- 布局模式 -->
481
- <div class="settings-section">
482
- <div class="settings-title">布局模式</div>
483
- <div class="settings-layout-options">
484
- <div
485
- v-for="option in layoutOptions"
486
- :key="option.value"
487
- :class="['layout-option', { 'is-active': currentLayout === option.value }]"
488
- @click="handleLayoutChange(option.value)"
489
- >
490
- <div class="layout-option__preview">
491
- <div v-if="option.value === 'sidebar'" class="layout-preview-sidebar">
492
- <div class="preview-aside"></div>
493
- <div class="preview-main">
494
- <div class="preview-header"></div>
495
- <div class="preview-content"></div>
496
- </div>
497
- </div>
498
- <div v-else-if="option.value === 'top'" class="layout-preview-top">
499
- <div class="preview-header-full"></div>
500
- <div class="preview-content-full"></div>
501
- </div>
502
- <div v-else class="layout-preview-mix">
503
- <div class="preview-header-mix">
504
- <div class="preview-mix-left"></div>
505
- </div>
506
- <div class="preview-mix-body">
507
- <div class="preview-mix-aside"></div>
508
- <div class="preview-mix-content"></div>
509
- </div>
510
- </div>
511
- </div>
512
- <span class="layout-option__label">{{ option.label }}</span>
513
- </div>
514
- </div>
515
- </div>
516
-
517
- <!-- 主题色 -->
518
- <div class="settings-section">
519
- <div class="settings-title">主题色</div>
520
- <div class="settings-color-options">
521
- <div
522
- v-for="color in colorOptions"
523
- :key="color.value"
524
- :class="['color-option', { 'is-active': appStore.primaryColor === color.value }]"
525
- :style="{ backgroundColor: color.value }"
526
- :title="color.label"
527
- @click="handleColorChange(color.value)"
528
- >
529
- <Icon v-if="appStore.primaryColor === color.value" name="check" :size="12" color="#fff" />
530
- </div>
531
- </div>
532
- </div>
533
-
534
- <!-- 功能开关 -->
535
- <div class="settings-section">
536
- <div class="settings-title">功能设置</div>
537
- <div class="settings-switch-list">
538
- <div class="settings-switch-item">
539
- <span>灰色模式</span>
540
- <div class="switch-wrapper" :class="{ 'is-checked': greyMode }" @click="handleGreyModeToggle">
541
- <span class="switch-core"></span>
542
- </div>
543
- </div>
544
- <div class="settings-switch-item">
545
- <span>暗黑模式</span>
546
- <div class="switch-wrapper" :class="{ 'is-checked': appStore.isDark }" @click="handleDarkModeToggle">
547
- <span class="switch-core"></span>
548
- </div>
549
- </div>
550
- </div>
551
- </div>
552
- </div>
553
- </Drawer>
554
- </div>
555
- </template>
556
-
557
- <style lang="scss" scoped>
558
- .top-menu {
559
- width: 100%;
560
- height: 100%;
561
- display: flex;
562
- align-items: center;
563
- padding: 0 20px;
564
-
565
- &__logo {
566
- display: flex;
567
- align-items: center;
568
- gap: 10px;
569
- margin-right: 20px;
570
- }
571
-
572
- &__logo-img {
573
- width: 32px;
574
- height: 32px;
575
- }
576
-
577
- &__logo-text {
578
- font-size: 16px;
579
- font-weight: 600;
580
- color: var(--color-primary);
581
- }
582
-
583
- &__menu {
584
- flex: 1;
585
- border-bottom: none;
586
- height: 49px;
587
- display: flex;
588
- align-items: center;
589
- line-height: 49px;
590
-
591
- // 深度选择器:使用正确的类名 x-menu(不是 xto-menu)
592
- :deep(.x-menu) {
593
- height: 49px;
594
- line-height: 49px;
595
- border-bottom: none;
596
- }
597
-
598
- :deep(.x-menu--horizontal) {
599
- height: 49px;
600
- display: flex;
601
- align-items: center;
602
- border-bottom: none;
603
- line-height: 49px;
604
- }
605
-
606
- :deep(.x-menu-item) {
607
- height: 49px;
608
- line-height: 49px;
609
- box-sizing: border-box;
610
- border-bottom: none;
611
- }
612
-
613
- :deep(.x-menu-item.is-horizontal) {
614
- height: 49px;
615
- line-height: 49px;
616
- }
617
-
618
- :deep(.x-menu-item__content) {
619
- height: 49px;
620
- }
621
-
622
- :deep(.x-sub-menu) {
623
- height: 49px;
624
- line-height: 49px;
625
- box-sizing: border-box;
626
- border-bottom: none;
627
- }
628
-
629
- :deep(.x-sub-menu--horizontal) {
630
- height: 49px;
631
- }
632
-
633
- :deep(.x-sub-menu__title) {
634
- height: 49px;
635
- line-height: 49px;
636
- box-sizing: border-box;
637
- border-bottom: none;
638
- }
639
-
640
- :deep(.x-sub-menu__content) {
641
- height: 49px;
642
- }
643
- }
644
-
645
- &__menu-content {
646
- align-items: center;
647
- line-height: 1;
648
- }
649
-
650
- &__menu-text {
651
- line-height: 1;
652
- }
653
-
654
- &__menu-icon {
655
- display: inline-flex;
656
- align-items: center;
657
- justify-content: center;
658
- width: 16px;
659
- height: 16px;
660
- margin-right: 8px;
661
- flex-shrink: 0;
662
- }
663
-
664
- &__menu-char {
665
- display: inline-flex;
666
- align-items: center;
667
- justify-content: center;
668
- width: 16px;
669
- height: 16px;
670
- font-size: 12px;
671
- font-weight: 600;
672
- color: var(--color-primary);
673
- background-color: var(--color-primary-light-8);
674
- border-radius: 4px;
675
- }
676
-
677
- // 右侧操作区域
678
- &__actions {
679
- display: flex;
680
- align-items: center;
681
- gap: 8px;
682
- }
683
-
684
- &__search {
685
- position: relative;
686
- display: flex;
687
- align-items: center;
688
- background-color: var(--color-fill-light);
689
- border-radius: var(--border-radius-base);
690
- padding: 0 12px;
691
- height: 32px;
692
- width: 200px;
693
-
694
- &-icon {
695
- color: var(--color-text-secondary);
696
- }
697
-
698
- &-input {
699
- flex: 1;
700
- height: 100%;
701
- font-size: 14px;
702
- color: var(--color-text-primary);
703
- background: transparent;
704
- border: none;
705
- outline: none;
706
- padding-left: 8px;
707
-
708
- &::placeholder {
709
- color: var(--color-text-placeholder);
710
- }
711
- }
712
-
713
- &-dropdown {
714
- position: absolute;
715
- top: calc(100% + 4px);
716
- left: 0;
717
- right: 0;
718
- min-width: 200px;
719
- max-height: 300px;
720
- overflow-y: auto;
721
- background-color: var(--bg-color);
722
- border-radius: var(--border-radius-base);
723
- box-shadow: var(--box-shadow);
724
- z-index: 100;
725
- }
726
-
727
- &-results {
728
- padding: 8px 0;
729
- }
730
-
731
- &-item {
732
- display: flex;
733
- align-items: center;
734
- gap: 8px;
735
- padding: 8px 12px;
736
- cursor: pointer;
737
- transition: background-color 0.2s;
738
-
739
- &:hover {
740
- background-color: var(--color-fill);
741
- }
742
-
743
- &-title {
744
- font-size: 14px;
745
- color: var(--color-text-primary);
746
- }
747
-
748
- &-parent {
749
- font-size: 12px;
750
- color: var(--color-text-secondary);
751
- margin-left: auto;
752
- }
753
- }
754
-
755
- &-icon-item {
756
- display: inline-flex;
757
- align-items: center;
758
- justify-content: center;
759
- width: 16px;
760
- height: 16px;
761
- flex-shrink: 0;
762
- }
763
-
764
- &-char {
765
- display: inline-flex;
766
- align-items: center;
767
- justify-content: center;
768
- width: 16px;
769
- height: 16px;
770
- font-size: 12px;
771
- font-weight: 600;
772
- color: var(--color-primary);
773
- background-color: var(--color-primary-light-8);
774
- border-radius: 4px;
775
- }
776
-
777
- &-empty {
778
- padding: 16px 12px;
779
- text-align: center;
780
- color: var(--color-text-secondary);
781
- font-size: 14px;
782
- }
783
- }
784
-
785
- &__action {
786
- width: 32px;
787
- height: 32px;
788
- display: flex;
789
- align-items: center;
790
- justify-content: center;
791
- cursor: pointer;
792
- border-radius: var(--border-radius-base);
793
- color: var(--color-text-regular);
794
- transition: all 0.2s;
795
-
796
- &:hover {
797
- background-color: var(--color-fill);
798
- color: var(--color-primary);
799
- }
800
- }
801
-
802
- // 用户头像
803
- &__user {
804
- position: relative;
805
- margin-left: 8px;
806
-
807
- &-trigger {
808
- display: flex;
809
- align-items: center;
810
- gap: 8px;
811
- cursor: pointer;
812
- padding: 4px 8px;
813
- border-radius: var(--border-radius-base);
814
- transition: background-color 0.2s;
815
-
816
- &:hover {
817
- background-color: var(--color-fill);
818
- }
819
- }
820
-
821
- &-name {
822
- font-size: 14px;
823
- color: var(--color-text-primary);
824
- max-width: 100px;
825
- overflow: hidden;
826
- text-overflow: ellipsis;
827
- white-space: nowrap;
828
- }
829
-
830
- &-arrow {
831
- font-size: 10px;
832
- color: var(--color-text-secondary);
833
- transition: transform 0.2s;
834
-
835
- &.is-active {
836
- transform: rotate(180deg);
837
- }
838
- }
839
- }
840
-
841
- &__avatar {
842
- width: 32px;
843
- height: 32px;
844
- border-radius: 50%;
845
- background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
846
- display: flex;
847
- align-items: center;
848
- justify-content: center;
849
- color: #fff;
850
- font-size: 14px;
851
- font-weight: 500;
852
- }
853
-
854
- // 下拉菜单
855
- &__dropdown {
856
- position: absolute;
857
- top: calc(100% + 8px);
858
- right: 0;
859
- min-width: 200px;
860
- background-color: var(--bg-color);
861
- border-radius: var(--border-radius-base);
862
- box-shadow: var(--box-shadow);
863
- overflow: hidden;
864
- z-index: 100;
865
-
866
- &-header {
867
- display: flex;
868
- align-items: center;
869
- gap: 12px;
870
- padding: 16px;
871
- }
872
-
873
- &-avatar {
874
- width: 40px;
875
- height: 40px;
876
- border-radius: 50%;
877
- background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
878
- display: flex;
879
- align-items: center;
880
- justify-content: center;
881
- color: #fff;
882
- font-size: 16px;
883
- font-weight: 500;
884
- }
885
-
886
- &-info {
887
- flex: 1;
888
- }
889
-
890
- &-name {
891
- font-size: 14px;
892
- font-weight: 500;
893
- color: var(--color-text-primary);
894
- }
895
-
896
- &-role {
897
- font-size: 12px;
898
- color: var(--color-text-secondary);
899
- margin-top: 2px;
900
- }
901
-
902
- &-divider {
903
- height: 1px;
904
- background-color: var(--color-border-lighter);
905
- }
906
-
907
- &-menu {
908
- padding: 8px 0;
909
- }
910
-
911
- &-item {
912
- display: flex;
913
- align-items: center;
914
- gap: 10px;
915
- padding: 10px 16px;
916
- cursor: pointer;
917
- font-size: 14px;
918
- color: var(--color-text-regular);
919
- transition: all 0.2s;
920
-
921
- &:hover {
922
- background-color: var(--color-fill);
923
- color: var(--color-text-primary);
924
- }
925
-
926
- &--danger {
927
- color: var(--color-danger);
928
-
929
- &:hover {
930
- background-color: var(--color-danger-light);
931
- color: var(--color-danger);
932
- }
933
- }
934
- }
935
- }
936
- }
937
-
938
- // 设置抽屉内容
939
- .settings-drawer {
940
- .settings-section {
941
- margin-bottom: 24px;
942
- }
943
-
944
- .settings-title {
945
- font-size: 14px;
946
- font-weight: 500;
947
- color: var(--color-text-primary);
948
- margin-bottom: 12px;
949
- }
950
-
951
- .settings-layout-options {
952
- display: flex;
953
- gap: 12px;
954
- }
955
-
956
- .layout-option {
957
- flex: 1;
958
- display: flex;
959
- flex-direction: column;
960
- align-items: center;
961
- gap: 8px;
962
- padding: 12px;
963
- border: 1px solid var(--color-border);
964
- border-radius: var(--border-radius-base);
965
- cursor: pointer;
966
- transition: all 0.2s;
967
-
968
- &:hover {
969
- border-color: var(--color-primary-light-5);
970
- }
971
-
972
- &.is-active {
973
- border-color: var(--color-primary);
974
- background-color: var(--color-primary-light-9);
975
- }
976
-
977
- &__preview {
978
- width: 48px;
979
- height: 36px;
980
- border: 1px solid var(--color-border-light);
981
- border-radius: 2px;
982
- overflow: hidden;
983
- }
984
-
985
- &__label {
986
- font-size: 12px;
987
- color: var(--color-text-regular);
988
- }
989
- }
990
-
991
- // 布局预览样式
992
- .layout-preview-sidebar {
993
- display: flex;
994
- height: 100%;
995
-
996
- .preview-aside {
997
- width: 25%;
998
- height: 100%;
999
- background-color: var(--color-primary-light-7);
1000
- }
1001
-
1002
- .preview-main {
1003
- flex: 1;
1004
- display: flex;
1005
- flex-direction: column;
1006
-
1007
- .preview-header {
1008
- height: 20%;
1009
- background-color: var(--color-border-light);
1010
- }
1011
-
1012
- .preview-content {
1013
- flex: 1;
1014
- background-color: var(--bg-color-page);
1015
- }
1016
- }
1017
- }
1018
-
1019
- .layout-preview-top {
1020
- display: flex;
1021
- flex-direction: column;
1022
- height: 100%;
1023
-
1024
- .preview-header-full {
1025
- height: 25%;
1026
- background-color: var(--color-primary-light-7);
1027
- }
1028
-
1029
- .preview-content-full {
1030
- flex: 1;
1031
- background-color: var(--bg-color-page);
1032
- }
1033
- }
1034
-
1035
- .layout-preview-mix {
1036
- display: flex;
1037
- flex-direction: column;
1038
- height: 100%;
1039
-
1040
- .preview-header-mix {
1041
- height: 25%;
1042
- background-color: var(--color-primary-light-7);
1043
- display: flex;
1044
-
1045
- .preview-mix-left {
1046
- width: 30%;
1047
- background-color: var(--color-primary);
1048
- }
1049
- }
1050
-
1051
- .preview-mix-body {
1052
- flex: 1;
1053
- display: flex;
1054
-
1055
- .preview-mix-aside {
1056
- width: 25%;
1057
- background-color: var(--color-primary-light-8);
1058
- }
1059
-
1060
- .preview-mix-content {
1061
- flex: 1;
1062
- background-color: var(--bg-color-page);
1063
- }
1064
- }
1065
- }
1066
-
1067
- .settings-color-options {
1068
- display: flex;
1069
- gap: 12px;
1070
- }
1071
-
1072
- .color-option {
1073
- width: 24px;
1074
- height: 24px;
1075
- border-radius: 4px;
1076
- cursor: pointer;
1077
- display: flex;
1078
- align-items: center;
1079
- justify-content: center;
1080
- transition: transform 0.2s;
1081
-
1082
- &:hover {
1083
- transform: scale(1.1);
1084
- }
1085
-
1086
- &.is-active {
1087
- box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--color-primary);
1088
- }
1089
- }
1090
-
1091
- .settings-switch-list {
1092
- display: flex;
1093
- flex-direction: column;
1094
- gap: 12px;
1095
- }
1096
-
1097
- .settings-switch-item {
1098
- display: flex;
1099
- align-items: center;
1100
- justify-content: space-between;
1101
-
1102
- span {
1103
- font-size: 14px;
1104
- color: var(--color-text-regular);
1105
- }
1106
- }
1107
-
1108
- .switch-wrapper {
1109
- width: 44px;
1110
- height: 22px;
1111
- display: flex;
1112
- align-items: center;
1113
- cursor: pointer;
1114
-
1115
- .switch-core {
1116
- width: 100%;
1117
- height: 100%;
1118
- border-radius: 11px;
1119
- background-color: var(--color-border);
1120
- position: relative;
1121
- transition: background-color 0.2s;
1122
-
1123
- &::after {
1124
- content: '';
1125
- position: absolute;
1126
- top: 2px;
1127
- left: 2px;
1128
- width: 18px;
1129
- height: 18px;
1130
- background-color: #fff;
1131
- border-radius: 50%;
1132
- transition: left 0.2s;
1133
- }
1134
- }
1135
-
1136
- &.is-checked {
1137
- .switch-core {
1138
- background-color: var(--color-primary);
1139
-
1140
- &::after {
1141
- left: 24px;
1142
- }
1143
- }
1144
- }
1145
- }
1146
- }
1147
-
1148
- // 下拉动画
1149
- .dropdown-enter-active,
1150
- .dropdown-leave-active {
1151
- transition: all 0.2s ease;
1152
- }
1153
-
1154
- .dropdown-enter-from,
1155
- .dropdown-leave-to {
1156
- opacity: 0;
1157
- transform: translateY(-10px);
1158
- }
1159
-
1160
- // 搜索下拉动画
1161
- .search-dropdown-enter-active,
1162
- .search-dropdown-leave-active {
1163
- transition: all 0.2s ease;
1164
- }
1165
-
1166
- .search-dropdown-enter-from,
1167
- .search-dropdown-leave-to {
1168
- opacity: 0;
1169
- transform: translateY(-4px);
1170
- }
1
+ <script setup lang="ts">
2
+ import { computed, ref, onMounted, onUnmounted } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { useMenuStore } from '@/stores/menu'
5
+ import { useAppStore } from '@/stores/app'
6
+ import { useUserStore } from '@/stores/user'
7
+ import { useAuthStore } from '@/stores/auth'
8
+ import { Menu, MenuItem, SubMenu } from '@xto/navigation'
9
+ import { Icon } from '@xto/base'
10
+ import { Drawer } from '@xto/feedback'
11
+
12
+ const route = useRoute()
13
+ const router = useRouter()
14
+ const menuStore = useMenuStore()
15
+ const appStore = useAppStore()
16
+ const userStore = useUserStore()
17
+ const authStore = useAuthStore()
18
+
19
+ const activeMenu = computed(() => route.path)
20
+
21
+ // 扁平化菜单用于搜索
22
+ const flattenMenus = (menus: any[], parentTitle = ''): any[] => {
23
+ const result: any[] = []
24
+ menus.forEach(menu => {
25
+ if (menu.children && menu.children.length > 0) {
26
+ result.push(...flattenMenus(menu.children, menu.menuName))
27
+ } else {
28
+ result.push({ ...menu, parentTitle, title: menu.menuName, path: menu.menuUrl })
29
+ }
30
+ })
31
+ return result
32
+ }
33
+
34
+ // 搜索结果
35
+ const searchResults = computed(() => {
36
+ if (!searchKeyword.value.trim()) return []
37
+ const flatMenus = flattenMenus(menuStore.menuList)
38
+ return flatMenus.filter(menu =>
39
+ menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
40
+ )
41
+ })
42
+
43
+ // 菜单主题相关
44
+ const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
45
+ const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
46
+ const menuActiveTextColor = computed(() => '#409eff')
47
+
48
+ // 下拉菜单
49
+ const dropdownVisible = ref(false)
50
+ const dropdownRef = ref<HTMLElement | null>(null)
51
+ const isFullscreen = ref(false)
52
+ const drawerVisible = ref(false)
53
+ const greyMode = ref(false)
54
+ const searchVisible = ref(false)
55
+ const searchKeyword = ref('')
56
+ const searchRef = ref<HTMLElement | null>(null)
57
+
58
+ // 菜单选择
59
+ const handleMenuSelect = (index: string) => {
60
+ if (index && index !== route.path) {
61
+ router.push(index)
62
+ }
63
+ }
64
+
65
+ // 已知的图标名称列表
66
+ const knownIcons = new Set([
67
+ 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right',
68
+ 'caret-down', 'caret-right', 'plus', 'minus', 'close', 'check',
69
+ 'edit', 'delete', 'copy', 'download', 'upload', 'refresh', 'search',
70
+ 'filter', 'more', 'setting', 'share', 'loading', 'info', 'success',
71
+ 'warning', 'error', 'question', 'user', 'user-add', 'user-group',
72
+ 'logout', 'login', 'file', 'folder', 'folder-open', 'document',
73
+ 'image', 'video', 'music', 'camera', 'mail', 'phone', 'chat',
74
+ 'bell', 'message', 'eye', 'eye-off', 'calendar', 'clock', 'history',
75
+ 'timer', 'location', 'map', 'globe', 'star', 'heart', 'thumb-up',
76
+ 'link', 'external-link', 'lock', 'unlock', 'key', 'home', 'menu',
77
+ 'menu-fold', 'menu-unfold', 'sidebar-fold', 'sidebar-expand',
78
+ 'sidebar-left', 'dashboard', 'chart', 'chart-pie', 'chart-line',
79
+ 'report', 'analytics', 'system', 'permission', 'role', 'user-manage',
80
+ 'log', 'notification', 'app', 'list', 'grid', 'fullscreen',
81
+ 'fullscreen-exit', 'zoom-in', 'zoom-out', 'print', 'bookmark',
82
+ 'tag', 'code', 'terminal', 'database', 'server', 'cloud', 'gift',
83
+ 'moon', 'sun', 'theme', 'skin'
84
+ ])
85
+
86
+ // 获取菜单图标名称
87
+ const getMenuIcon = (icon?: string): string => {
88
+ if (!icon || icon === '') return ''
89
+
90
+ if (icon.startsWith('tineco-icon-')) {
91
+ const iconName = icon.replace('tineco-icon-', '')
92
+ const tinecoIconMap: Record<string, string> = {
93
+ home: 'home',
94
+ dashboard: 'dashboard',
95
+ system: 'system',
96
+ user: 'user',
97
+ role: 'role',
98
+ menu: 'list',
99
+ setting: 'setting',
100
+ file: 'file',
101
+ folder: 'folder',
102
+ chart: 'chart',
103
+ report: 'report',
104
+ analytics: 'analytics'
105
+ }
106
+ return tinecoIconMap[iconName] || iconName
107
+ }
108
+
109
+ const iconMap: Record<string, string> = {
110
+ dashboard: 'dashboard',
111
+ system: 'system',
112
+ user: 'user',
113
+ role: 'role',
114
+ menu: 'list',
115
+ setting: 'setting',
116
+ home: 'home',
117
+ chart: 'chart',
118
+ report: 'report',
119
+ analytics: 'analytics',
120
+ permission: 'permission',
121
+ log: 'log',
122
+ notification: 'notification',
123
+ app: 'app',
124
+ list: 'list',
125
+ grid: 'grid'
126
+ }
127
+
128
+ return iconMap[icon] || icon
129
+ }
130
+
131
+ const getFirstChar = (name?: string): string => {
132
+ if (!name) return ''
133
+ return name.charAt(0)
134
+ }
135
+
136
+ const iconExists = (iconName: string): boolean => {
137
+ return knownIcons.has(iconName)
138
+ }
139
+
140
+ // 切换主题
141
+ const toggleTheme = () => {
142
+ appStore.toggleTheme()
143
+ drawerVisible.value = false
144
+ }
145
+
146
+ // 切换全屏
147
+ const toggleFullscreen = () => {
148
+ if (!document.fullscreenElement) {
149
+ document.documentElement.requestFullscreen()
150
+ } else {
151
+ document.exitFullscreen()
152
+ }
153
+ }
154
+
155
+ // 监听全屏变化
156
+ const handleFullscreenChange = () => {
157
+ isFullscreen.value = !!document.fullscreenElement
158
+ }
159
+
160
+ // 切换下拉菜单
161
+ const toggleDropdown = () => {
162
+ dropdownVisible.value = !dropdownVisible.value
163
+ }
164
+
165
+ // 关闭下拉菜单
166
+ const closeDropdowns = () => {
167
+ dropdownVisible.value = false
168
+ }
169
+
170
+ // 隐藏搜索
171
+ const hideSearch = () => {
172
+ searchVisible.value = false
173
+ searchKeyword.value = ''
174
+ }
175
+
176
+ // 搜索结果点击
177
+ const handleSearchItemClick = (path: string) => {
178
+ router.push(path)
179
+ hideSearch()
180
+ }
181
+
182
+ // 打开设置抽屉
183
+ const openSettingsDrawer = () => {
184
+ drawerVisible.value = true
185
+ }
186
+
187
+ // 切换灰色模式
188
+ const handleGreyModeChange = (value: boolean) => {
189
+ greyMode.value = value
190
+ const html = document.documentElement
191
+ if (value) {
192
+ html.classList.add('grey-mode')
193
+ } else {
194
+ html.classList.remove('grey-mode')
195
+ }
196
+ }
197
+
198
+ // 抽屉内灰色模式开关
199
+ const handleGreyModeToggle = () => {
200
+ handleGreyModeChange(!greyMode.value)
201
+ drawerVisible.value = false
202
+ }
203
+
204
+ // 抽屉内暗黑模式开关
205
+ const handleDarkModeToggle = () => {
206
+ appStore.toggleTheme()
207
+ drawerVisible.value = false
208
+ }
209
+
210
+ // 布局模式选项
211
+ type LayoutMode = 'sidebar' | 'top' | 'mix'
212
+ const layoutOptions: { value: LayoutMode; label: string; icon: string }[] = [
213
+ { value: 'sidebar', label: '左侧菜单', icon: 'sidebar-left' },
214
+ { value: 'top', label: '顶部菜单', icon: 'menu' },
215
+ { value: 'mix', label: '混合菜单', icon: 'grid' }
216
+ ]
217
+
218
+ const currentLayout = computed(() => appStore.layout)
219
+
220
+ // 切换布局模式
221
+ const handleLayoutChange = (mode: LayoutMode) => {
222
+ appStore.setLayout(mode)
223
+ drawerVisible.value = false
224
+ }
225
+
226
+ // 主题色选项
227
+ const colorOptions = [
228
+ { value: '#409eff', label: '默认蓝' },
229
+ { value: '#1890ff', label: '科技蓝' },
230
+ { value: '#52c41a', label: '极光绿' },
231
+ { value: '#faad14', label: '日落橙' },
232
+ { value: '#f5222d', label: '薄暮红' },
233
+ { value: '#722ed1', label: '酱紫' }
234
+ ]
235
+
236
+ // 设置主题色
237
+ const handleColorChange = (color: string) => {
238
+ appStore.setPrimaryColor(color)
239
+ drawerVisible.value = false
240
+ }
241
+
242
+ // 个人信息
243
+ const handleProfile = () => {
244
+ closeDropdowns()
245
+ router.push('/profile')
246
+ }
247
+
248
+ // 修改密码
249
+ const handleChangePassword = () => {
250
+ closeDropdowns()
251
+ router.push('/change-password')
252
+ }
253
+
254
+ // 退出登录
255
+ const handleLogout = () => {
256
+ closeDropdowns()
257
+ authStore.logout()
258
+ userStore.clearUserInfo()
259
+ menuStore.clearMenu()
260
+ router.push('/login')
261
+ }
262
+
263
+ // 点击外部关闭下拉菜单
264
+ const handleClickOutside = (event: MouseEvent) => {
265
+ if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
266
+ closeDropdowns()
267
+ }
268
+ if (searchRef.value && !searchRef.value.contains(event.target as Node)) {
269
+ hideSearch()
270
+ }
271
+ }
272
+
273
+ // 键盘快捷键
274
+ const handleKeydown = (event: KeyboardEvent) => {
275
+ if (event.key === 'Escape') {
276
+ hideSearch()
277
+ closeDropdowns()
278
+ }
279
+ }
280
+
281
+ onMounted(() => {
282
+ document.addEventListener('click', handleClickOutside)
283
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
284
+ document.addEventListener('keydown', handleKeydown)
285
+ greyMode.value = document.documentElement.classList.contains('grey-mode')
286
+ })
287
+
288
+ onUnmounted(() => {
289
+ document.removeEventListener('click', handleClickOutside)
290
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
291
+ document.removeEventListener('keydown', handleKeydown)
292
+ })
293
+ </script>
294
+
295
+ <template>
296
+ <div class="top-menu">
297
+ <!-- 左侧 Logo -->
298
+ <div class="top-menu__logo">
299
+ <img src="/vite.svg" alt="Logo" class="top-menu__logo-img" />
300
+ <span class="top-menu__logo-text">{{ appStore.appName }}</span>
301
+ </div>
302
+
303
+ <!-- 左侧菜单 -->
304
+ <Menu
305
+ :model-value="activeMenu"
306
+ mode="horizontal"
307
+ :background-color="menuBgColor"
308
+ :text-color="menuTextColor"
309
+ :active-text-color="menuActiveTextColor"
310
+ class="top-menu__menu"
311
+ @select="handleMenuSelect"
312
+ >
313
+ <template v-for="menu in menuStore.menuList" :key="menu.menuUrl">
314
+ <!-- 有子菜单 -->
315
+ <SubMenu v-if="menu.children && menu.children.length > 0" :index="menu.menuUrl">
316
+ <template #title>
317
+ <span class="top-menu__menu-content">
318
+ <span v-if="menu.menuName !== '首页'" class="top-menu__menu-icon">
319
+ <Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
320
+ <span v-else class="top-menu__menu-char">{{ getFirstChar(menu.menuName) }}</span>
321
+ </span>
322
+ <span class="top-menu__menu-text">{{ menu.menuName }}</span>
323
+ </span>
324
+ </template>
325
+ <!-- 递归渲染子菜单 -->
326
+ <template v-for="child in menu.children" :key="child.menuUrl">
327
+ <SubMenu v-if="child.children && child.children.length > 0" :index="child.menuUrl">
328
+ <template #title>
329
+ <span class="top-menu__menu-content">
330
+ <span class="top-menu__menu-icon">
331
+ <Icon v-if="iconExists(getMenuIcon(child.icon))" :name="getMenuIcon(child.icon)" :size="16" />
332
+ <span v-else class="top-menu__menu-char">{{ getFirstChar(child.menuName) }}</span>
333
+ </span>
334
+ <span class="top-menu__menu-text">{{ child.menuName }}</span>
335
+ </span>
336
+ </template>
337
+ <!-- 三级菜单项 -->
338
+ <MenuItem
339
+ v-for="grandChild in child.children"
340
+ :key="grandChild.menuUrl"
341
+ :index="grandChild.menuUrl"
342
+ >
343
+ <span class="top-menu__menu-content">
344
+ <span class="top-menu__menu-icon">
345
+ <Icon v-if="iconExists(getMenuIcon(grandChild.icon))" :name="getMenuIcon(grandChild.icon)" :size="16" />
346
+ <span v-else class="top-menu__menu-char">{{ getFirstChar(grandChild.menuName) }}</span>
347
+ </span>
348
+ <span class="top-menu__menu-text">{{ grandChild.menuName }}</span>
349
+ </span>
350
+ </MenuItem>
351
+ </SubMenu>
352
+ <!-- 二级菜单项 -->
353
+ <MenuItem v-else :index="child.menuUrl">
354
+ <span class="top-menu__menu-content">
355
+ <span class="top-menu__menu-icon">
356
+ <Icon v-if="iconExists(getMenuIcon(child.icon))" :name="getMenuIcon(child.icon)" :size="16" />
357
+ <span v-else class="top-menu__menu-char">{{ getFirstChar(child.menuName) }}</span>
358
+ </span>
359
+ <span class="top-menu__menu-text">{{ child.menuName }}</span>
360
+ </span>
361
+ </MenuItem>
362
+ </template>
363
+ </SubMenu>
364
+ <!-- 无子菜单 -->
365
+ <MenuItem v-else :index="menu.menuUrl">
366
+ <span class="top-menu__menu-content">
367
+ <span v-if="menu.menuName !== '首页'" class="top-menu__menu-icon">
368
+ <Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
369
+ <span v-else class="top-menu__menu-char">{{ getFirstChar(menu.menuName) }}</span>
370
+ </span>
371
+ <span class="top-menu__menu-text">{{ menu.menuName }}</span>
372
+ </span>
373
+ </MenuItem>
374
+ </template>
375
+ </Menu>
376
+
377
+ <!-- 右侧操作区域 -->
378
+ <div class="top-menu__actions">
379
+ <!-- 搜索输入框 -->
380
+ <div class="top-menu__search" ref="searchRef">
381
+ <Icon name="search" :size="14" class="top-menu__search-icon" />
382
+ <input
383
+ v-model="searchKeyword"
384
+ type="text"
385
+ class="top-menu__search-input"
386
+ placeholder="搜索菜单..."
387
+ @focus="searchVisible = true"
388
+ />
389
+ <!-- 搜索结果下拉 -->
390
+ <Transition name="search-dropdown">
391
+ <div v-if="searchVisible && (searchResults.length > 0 || searchKeyword)" class="top-menu__search-dropdown">
392
+ <div v-if="searchResults.length > 0" class="top-menu__search-results">
393
+ <div
394
+ v-for="item in searchResults"
395
+ :key="item.path"
396
+ class="top-menu__search-item"
397
+ @click="handleSearchItemClick(item.path)"
398
+ >
399
+ <span v-if="item.title !== '首页'" class="top-menu__search-icon-item">
400
+ <Icon v-if="iconExists(getMenuIcon(item.icon))" :name="getMenuIcon(item.icon)" :size="16" />
401
+ <span v-else class="top-menu__search-char">{{ getFirstChar(item.title) }}</span>
402
+ </span>
403
+ <span class="top-menu__search-item-title">{{ item.title }}</span>
404
+ <span v-if="item.parentTitle" class="top-menu__search-item-parent">{{ item.parentTitle }}</span>
405
+ </div>
406
+ </div>
407
+ <div v-else class="top-menu__search-empty">
408
+ 未找到匹配的菜单
409
+ </div>
410
+ </div>
411
+ </Transition>
412
+ </div>
413
+
414
+ <!-- 全屏切换 -->
415
+ <div class="top-menu__action" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
416
+ <Icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" :size="16" />
417
+ </div>
418
+
419
+ <!-- 换肤设置 -->
420
+ <div class="top-menu__action" @click="openSettingsDrawer" title="换肤设置">
421
+ <Icon name="skin" :size="16" />
422
+ </div>
423
+
424
+ <!-- 主题切换 -->
425
+ <div class="top-menu__action" @click="toggleTheme" title="切换主题">
426
+ <Icon :name="appStore.isDark ? 'sun' : 'moon'" :size="16" />
427
+ </div>
428
+
429
+ <!-- 用户头像 -->
430
+ <div class="top-menu__user" ref="dropdownRef">
431
+ <div class="top-menu__user-trigger" @click.stop="toggleDropdown">
432
+ <div class="top-menu__avatar">
433
+ <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
434
+ </div>
435
+ <span class="top-menu__user-name">{{ userStore.userName }}</span>
436
+ <span class="top-menu__user-arrow" :class="{ 'is-active': dropdownVisible }">▼</span>
437
+ </div>
438
+
439
+ <!-- 下拉菜单 -->
440
+ <Transition name="dropdown">
441
+ <div v-if="dropdownVisible" class="top-menu__dropdown">
442
+ <div class="top-menu__dropdown-header">
443
+ <div class="top-menu__dropdown-avatar">
444
+ <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
445
+ </div>
446
+ <div class="top-menu__dropdown-info">
447
+ <div class="top-menu__dropdown-name">{{ userStore.userName }}</div>
448
+ <div class="top-menu__dropdown-role">{{ userStore.departmentName }}</div>
449
+ </div>
450
+ </div>
451
+ <div class="top-menu__dropdown-divider"></div>
452
+ <div class="top-menu__dropdown-menu">
453
+ <div class="top-menu__dropdown-item" @click="handleProfile">
454
+ <Icon name="user" :size="16" />
455
+ <span>个人信息</span>
456
+ </div>
457
+ <div class="top-menu__dropdown-item" @click="handleChangePassword">
458
+ <Icon name="lock" :size="16" />
459
+ <span>修改密码</span>
460
+ </div>
461
+ <div class="top-menu__dropdown-divider"></div>
462
+ <div class="top-menu__dropdown-item top-menu__dropdown-item--danger" @click="handleLogout">
463
+ <Icon name="logout" :size="16" />
464
+ <span>退出登录</span>
465
+ </div>
466
+ </div>
467
+ </div>
468
+ </Transition>
469
+ </div>
470
+ </div>
471
+
472
+ <!-- 换肤设置抽屉 -->
473
+ <Drawer
474
+ v-model="drawerVisible"
475
+ title="换肤设置"
476
+ direction="rtl"
477
+ size="320px"
478
+ >
479
+ <div class="settings-drawer">
480
+ <!-- 布局模式 -->
481
+ <div class="settings-section">
482
+ <div class="settings-title">布局模式</div>
483
+ <div class="settings-layout-options">
484
+ <div
485
+ v-for="option in layoutOptions"
486
+ :key="option.value"
487
+ :class="['layout-option', { 'is-active': currentLayout === option.value }]"
488
+ @click="handleLayoutChange(option.value)"
489
+ >
490
+ <div class="layout-option__preview">
491
+ <div v-if="option.value === 'sidebar'" class="layout-preview-sidebar">
492
+ <div class="preview-aside"></div>
493
+ <div class="preview-main">
494
+ <div class="preview-header"></div>
495
+ <div class="preview-content"></div>
496
+ </div>
497
+ </div>
498
+ <div v-else-if="option.value === 'top'" class="layout-preview-top">
499
+ <div class="preview-header-full"></div>
500
+ <div class="preview-content-full"></div>
501
+ </div>
502
+ <div v-else class="layout-preview-mix">
503
+ <div class="preview-header-mix">
504
+ <div class="preview-mix-left"></div>
505
+ </div>
506
+ <div class="preview-mix-body">
507
+ <div class="preview-mix-aside"></div>
508
+ <div class="preview-mix-content"></div>
509
+ </div>
510
+ </div>
511
+ </div>
512
+ <span class="layout-option__label">{{ option.label }}</span>
513
+ </div>
514
+ </div>
515
+ </div>
516
+
517
+ <!-- 主题色 -->
518
+ <div class="settings-section">
519
+ <div class="settings-title">主题色</div>
520
+ <div class="settings-color-options">
521
+ <div
522
+ v-for="color in colorOptions"
523
+ :key="color.value"
524
+ :class="['color-option', { 'is-active': appStore.primaryColor === color.value }]"
525
+ :style="{ backgroundColor: color.value }"
526
+ :title="color.label"
527
+ @click="handleColorChange(color.value)"
528
+ >
529
+ <Icon v-if="appStore.primaryColor === color.value" name="check" :size="12" color="#fff" />
530
+ </div>
531
+ </div>
532
+ </div>
533
+
534
+ <!-- 功能开关 -->
535
+ <div class="settings-section">
536
+ <div class="settings-title">功能设置</div>
537
+ <div class="settings-switch-list">
538
+ <div class="settings-switch-item">
539
+ <span>灰色模式</span>
540
+ <div class="switch-wrapper" :class="{ 'is-checked': greyMode }" @click="handleGreyModeToggle">
541
+ <span class="switch-core"></span>
542
+ </div>
543
+ </div>
544
+ <div class="settings-switch-item">
545
+ <span>暗黑模式</span>
546
+ <div class="switch-wrapper" :class="{ 'is-checked': appStore.isDark }" @click="handleDarkModeToggle">
547
+ <span class="switch-core"></span>
548
+ </div>
549
+ </div>
550
+ </div>
551
+ </div>
552
+ </div>
553
+ </Drawer>
554
+ </div>
555
+ </template>
556
+
557
+ <style lang="scss" scoped>
558
+ .top-menu {
559
+ width: 100%;
560
+ height: 100%;
561
+ display: flex;
562
+ align-items: center;
563
+ padding: 0 20px;
564
+
565
+ &__logo {
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 10px;
569
+ margin-right: 20px;
570
+ }
571
+
572
+ &__logo-img {
573
+ width: 32px;
574
+ height: 32px;
575
+ }
576
+
577
+ &__logo-text {
578
+ font-size: 16px;
579
+ font-weight: 600;
580
+ color: var(--color-primary);
581
+ }
582
+
583
+ &__menu {
584
+ flex: 1;
585
+ border-bottom: none;
586
+ height: 49px;
587
+ display: flex;
588
+ align-items: center;
589
+ line-height: 49px;
590
+
591
+ // 深度选择器:使用正确的类名 x-menu(不是 xto-menu)
592
+ :deep(.x-menu) {
593
+ height: 49px;
594
+ line-height: 49px;
595
+ border-bottom: none;
596
+ }
597
+
598
+ :deep(.x-menu--horizontal) {
599
+ height: 49px;
600
+ display: flex;
601
+ align-items: center;
602
+ border-bottom: none;
603
+ line-height: 49px;
604
+ }
605
+
606
+ :deep(.x-menu-item) {
607
+ height: 49px;
608
+ line-height: 49px;
609
+ box-sizing: border-box;
610
+ border-bottom: none;
611
+ }
612
+
613
+ :deep(.x-menu-item.is-horizontal) {
614
+ height: 49px;
615
+ line-height: 49px;
616
+ }
617
+
618
+ :deep(.x-menu-item__content) {
619
+ height: 49px;
620
+ }
621
+
622
+ :deep(.x-sub-menu) {
623
+ height: 49px;
624
+ line-height: 49px;
625
+ box-sizing: border-box;
626
+ border-bottom: none;
627
+ }
628
+
629
+ :deep(.x-sub-menu--horizontal) {
630
+ height: 49px;
631
+ }
632
+
633
+ :deep(.x-sub-menu__title) {
634
+ height: 49px;
635
+ line-height: 49px;
636
+ box-sizing: border-box;
637
+ border-bottom: none;
638
+ }
639
+
640
+ :deep(.x-sub-menu__content) {
641
+ height: 49px;
642
+ }
643
+ }
644
+
645
+ &__menu-content {
646
+ align-items: center;
647
+ line-height: 1;
648
+ }
649
+
650
+ &__menu-text {
651
+ line-height: 1;
652
+ }
653
+
654
+ &__menu-icon {
655
+ display: inline-flex;
656
+ align-items: center;
657
+ justify-content: center;
658
+ width: 16px;
659
+ height: 16px;
660
+ margin-right: 8px;
661
+ flex-shrink: 0;
662
+ }
663
+
664
+ &__menu-char {
665
+ display: inline-flex;
666
+ align-items: center;
667
+ justify-content: center;
668
+ width: 16px;
669
+ height: 16px;
670
+ font-size: 12px;
671
+ font-weight: 600;
672
+ color: var(--color-primary);
673
+ background-color: var(--color-primary-light-8);
674
+ border-radius: 4px;
675
+ }
676
+
677
+ // 右侧操作区域
678
+ &__actions {
679
+ display: flex;
680
+ align-items: center;
681
+ gap: 8px;
682
+ }
683
+
684
+ &__search {
685
+ position: relative;
686
+ display: flex;
687
+ align-items: center;
688
+ background-color: var(--color-fill-light);
689
+ border-radius: var(--border-radius-base);
690
+ padding: 0 12px;
691
+ height: 32px;
692
+ width: 200px;
693
+
694
+ &-icon {
695
+ color: var(--color-text-secondary);
696
+ }
697
+
698
+ &-input {
699
+ flex: 1;
700
+ height: 100%;
701
+ font-size: 14px;
702
+ color: var(--color-text-primary);
703
+ background: transparent;
704
+ border: none;
705
+ outline: none;
706
+ padding-left: 8px;
707
+
708
+ &::placeholder {
709
+ color: var(--color-text-placeholder);
710
+ }
711
+ }
712
+
713
+ &-dropdown {
714
+ position: absolute;
715
+ top: calc(100% + 4px);
716
+ left: 0;
717
+ right: 0;
718
+ min-width: 200px;
719
+ max-height: 300px;
720
+ overflow-y: auto;
721
+ background-color: var(--bg-color);
722
+ border-radius: var(--border-radius-base);
723
+ box-shadow: var(--box-shadow);
724
+ z-index: 100;
725
+ }
726
+
727
+ &-results {
728
+ padding: 8px 0;
729
+ }
730
+
731
+ &-item {
732
+ display: flex;
733
+ align-items: center;
734
+ gap: 8px;
735
+ padding: 8px 12px;
736
+ cursor: pointer;
737
+ transition: background-color 0.2s;
738
+
739
+ &:hover {
740
+ background-color: var(--color-fill);
741
+ }
742
+
743
+ &-title {
744
+ font-size: 14px;
745
+ color: var(--color-text-primary);
746
+ }
747
+
748
+ &-parent {
749
+ font-size: 12px;
750
+ color: var(--color-text-secondary);
751
+ margin-left: auto;
752
+ }
753
+ }
754
+
755
+ &-icon-item {
756
+ display: inline-flex;
757
+ align-items: center;
758
+ justify-content: center;
759
+ width: 16px;
760
+ height: 16px;
761
+ flex-shrink: 0;
762
+ }
763
+
764
+ &-char {
765
+ display: inline-flex;
766
+ align-items: center;
767
+ justify-content: center;
768
+ width: 16px;
769
+ height: 16px;
770
+ font-size: 12px;
771
+ font-weight: 600;
772
+ color: var(--color-primary);
773
+ background-color: var(--color-primary-light-8);
774
+ border-radius: 4px;
775
+ }
776
+
777
+ &-empty {
778
+ padding: 16px 12px;
779
+ text-align: center;
780
+ color: var(--color-text-secondary);
781
+ font-size: 14px;
782
+ }
783
+ }
784
+
785
+ &__action {
786
+ width: 32px;
787
+ height: 32px;
788
+ display: flex;
789
+ align-items: center;
790
+ justify-content: center;
791
+ cursor: pointer;
792
+ border-radius: var(--border-radius-base);
793
+ color: var(--color-text-regular);
794
+ transition: all 0.2s;
795
+
796
+ &:hover {
797
+ background-color: var(--color-fill);
798
+ color: var(--color-primary);
799
+ }
800
+ }
801
+
802
+ // 用户头像
803
+ &__user {
804
+ position: relative;
805
+ margin-left: 8px;
806
+
807
+ &-trigger {
808
+ display: flex;
809
+ align-items: center;
810
+ gap: 8px;
811
+ cursor: pointer;
812
+ padding: 4px 8px;
813
+ border-radius: var(--border-radius-base);
814
+ transition: background-color 0.2s;
815
+
816
+ &:hover {
817
+ background-color: var(--color-fill);
818
+ }
819
+ }
820
+
821
+ &-name {
822
+ font-size: 14px;
823
+ color: var(--color-text-primary);
824
+ max-width: 100px;
825
+ overflow: hidden;
826
+ text-overflow: ellipsis;
827
+ white-space: nowrap;
828
+ }
829
+
830
+ &-arrow {
831
+ font-size: 10px;
832
+ color: var(--color-text-secondary);
833
+ transition: transform 0.2s;
834
+
835
+ &.is-active {
836
+ transform: rotate(180deg);
837
+ }
838
+ }
839
+ }
840
+
841
+ &__avatar {
842
+ width: 32px;
843
+ height: 32px;
844
+ border-radius: 50%;
845
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
846
+ display: flex;
847
+ align-items: center;
848
+ justify-content: center;
849
+ color: #fff;
850
+ font-size: 14px;
851
+ font-weight: 500;
852
+ }
853
+
854
+ // 下拉菜单
855
+ &__dropdown {
856
+ position: absolute;
857
+ top: calc(100% + 8px);
858
+ right: 0;
859
+ min-width: 200px;
860
+ background-color: var(--bg-color);
861
+ border-radius: var(--border-radius-base);
862
+ box-shadow: var(--box-shadow);
863
+ overflow: hidden;
864
+ z-index: 100;
865
+
866
+ &-header {
867
+ display: flex;
868
+ align-items: center;
869
+ gap: 12px;
870
+ padding: 16px;
871
+ }
872
+
873
+ &-avatar {
874
+ width: 40px;
875
+ height: 40px;
876
+ border-radius: 50%;
877
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
878
+ display: flex;
879
+ align-items: center;
880
+ justify-content: center;
881
+ color: #fff;
882
+ font-size: 16px;
883
+ font-weight: 500;
884
+ }
885
+
886
+ &-info {
887
+ flex: 1;
888
+ }
889
+
890
+ &-name {
891
+ font-size: 14px;
892
+ font-weight: 500;
893
+ color: var(--color-text-primary);
894
+ }
895
+
896
+ &-role {
897
+ font-size: 12px;
898
+ color: var(--color-text-secondary);
899
+ margin-top: 2px;
900
+ }
901
+
902
+ &-divider {
903
+ height: 1px;
904
+ background-color: var(--color-border-lighter);
905
+ }
906
+
907
+ &-menu {
908
+ padding: 8px 0;
909
+ }
910
+
911
+ &-item {
912
+ display: flex;
913
+ align-items: center;
914
+ gap: 10px;
915
+ padding: 10px 16px;
916
+ cursor: pointer;
917
+ font-size: 14px;
918
+ color: var(--color-text-regular);
919
+ transition: all 0.2s;
920
+
921
+ &:hover {
922
+ background-color: var(--color-fill);
923
+ color: var(--color-text-primary);
924
+ }
925
+
926
+ &--danger {
927
+ color: var(--color-danger);
928
+
929
+ &:hover {
930
+ background-color: var(--color-danger-light);
931
+ color: var(--color-danger);
932
+ }
933
+ }
934
+ }
935
+ }
936
+ }
937
+
938
+ // 设置抽屉内容
939
+ .settings-drawer {
940
+ .settings-section {
941
+ margin-bottom: 24px;
942
+ }
943
+
944
+ .settings-title {
945
+ font-size: 14px;
946
+ font-weight: 500;
947
+ color: var(--color-text-primary);
948
+ margin-bottom: 12px;
949
+ }
950
+
951
+ .settings-layout-options {
952
+ display: flex;
953
+ gap: 12px;
954
+ }
955
+
956
+ .layout-option {
957
+ flex: 1;
958
+ display: flex;
959
+ flex-direction: column;
960
+ align-items: center;
961
+ gap: 8px;
962
+ padding: 12px;
963
+ border: 1px solid var(--color-border);
964
+ border-radius: var(--border-radius-base);
965
+ cursor: pointer;
966
+ transition: all 0.2s;
967
+
968
+ &:hover {
969
+ border-color: var(--color-primary-light-5);
970
+ }
971
+
972
+ &.is-active {
973
+ border-color: var(--color-primary);
974
+ background-color: var(--color-primary-light-9);
975
+ }
976
+
977
+ &__preview {
978
+ width: 48px;
979
+ height: 36px;
980
+ border: 1px solid var(--color-border-light);
981
+ border-radius: 2px;
982
+ overflow: hidden;
983
+ }
984
+
985
+ &__label {
986
+ font-size: 12px;
987
+ color: var(--color-text-regular);
988
+ }
989
+ }
990
+
991
+ // 布局预览样式
992
+ .layout-preview-sidebar {
993
+ display: flex;
994
+ height: 100%;
995
+
996
+ .preview-aside {
997
+ width: 25%;
998
+ height: 100%;
999
+ background-color: var(--color-primary-light-7);
1000
+ }
1001
+
1002
+ .preview-main {
1003
+ flex: 1;
1004
+ display: flex;
1005
+ flex-direction: column;
1006
+
1007
+ .preview-header {
1008
+ height: 20%;
1009
+ background-color: var(--color-border-light);
1010
+ }
1011
+
1012
+ .preview-content {
1013
+ flex: 1;
1014
+ background-color: var(--bg-color-page);
1015
+ }
1016
+ }
1017
+ }
1018
+
1019
+ .layout-preview-top {
1020
+ display: flex;
1021
+ flex-direction: column;
1022
+ height: 100%;
1023
+
1024
+ .preview-header-full {
1025
+ height: 25%;
1026
+ background-color: var(--color-primary-light-7);
1027
+ }
1028
+
1029
+ .preview-content-full {
1030
+ flex: 1;
1031
+ background-color: var(--bg-color-page);
1032
+ }
1033
+ }
1034
+
1035
+ .layout-preview-mix {
1036
+ display: flex;
1037
+ flex-direction: column;
1038
+ height: 100%;
1039
+
1040
+ .preview-header-mix {
1041
+ height: 25%;
1042
+ background-color: var(--color-primary-light-7);
1043
+ display: flex;
1044
+
1045
+ .preview-mix-left {
1046
+ width: 30%;
1047
+ background-color: var(--color-primary);
1048
+ }
1049
+ }
1050
+
1051
+ .preview-mix-body {
1052
+ flex: 1;
1053
+ display: flex;
1054
+
1055
+ .preview-mix-aside {
1056
+ width: 25%;
1057
+ background-color: var(--color-primary-light-8);
1058
+ }
1059
+
1060
+ .preview-mix-content {
1061
+ flex: 1;
1062
+ background-color: var(--bg-color-page);
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ .settings-color-options {
1068
+ display: flex;
1069
+ gap: 12px;
1070
+ }
1071
+
1072
+ .color-option {
1073
+ width: 24px;
1074
+ height: 24px;
1075
+ border-radius: 4px;
1076
+ cursor: pointer;
1077
+ display: flex;
1078
+ align-items: center;
1079
+ justify-content: center;
1080
+ transition: transform 0.2s;
1081
+
1082
+ &:hover {
1083
+ transform: scale(1.1);
1084
+ }
1085
+
1086
+ &.is-active {
1087
+ box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--color-primary);
1088
+ }
1089
+ }
1090
+
1091
+ .settings-switch-list {
1092
+ display: flex;
1093
+ flex-direction: column;
1094
+ gap: 12px;
1095
+ }
1096
+
1097
+ .settings-switch-item {
1098
+ display: flex;
1099
+ align-items: center;
1100
+ justify-content: space-between;
1101
+
1102
+ span {
1103
+ font-size: 14px;
1104
+ color: var(--color-text-regular);
1105
+ }
1106
+ }
1107
+
1108
+ .switch-wrapper {
1109
+ width: 44px;
1110
+ height: 22px;
1111
+ display: flex;
1112
+ align-items: center;
1113
+ cursor: pointer;
1114
+
1115
+ .switch-core {
1116
+ width: 100%;
1117
+ height: 100%;
1118
+ border-radius: 11px;
1119
+ background-color: var(--color-border);
1120
+ position: relative;
1121
+ transition: background-color 0.2s;
1122
+
1123
+ &::after {
1124
+ content: '';
1125
+ position: absolute;
1126
+ top: 2px;
1127
+ left: 2px;
1128
+ width: 18px;
1129
+ height: 18px;
1130
+ background-color: #fff;
1131
+ border-radius: 50%;
1132
+ transition: left 0.2s;
1133
+ }
1134
+ }
1135
+
1136
+ &.is-checked {
1137
+ .switch-core {
1138
+ background-color: var(--color-primary);
1139
+
1140
+ &::after {
1141
+ left: 24px;
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ }
1147
+
1148
+ // 下拉动画
1149
+ .dropdown-enter-active,
1150
+ .dropdown-leave-active {
1151
+ transition: all 0.2s ease;
1152
+ }
1153
+
1154
+ .dropdown-enter-from,
1155
+ .dropdown-leave-to {
1156
+ opacity: 0;
1157
+ transform: translateY(-10px);
1158
+ }
1159
+
1160
+ // 搜索下拉动画
1161
+ .search-dropdown-enter-active,
1162
+ .search-dropdown-leave-active {
1163
+ transition: all 0.2s ease;
1164
+ }
1165
+
1166
+ .search-dropdown-enter-from,
1167
+ .search-dropdown-leave-to {
1168
+ opacity: 0;
1169
+ transform: translateY(-4px);
1170
+ }
1171
1171
  </style>