xto-fronted 0.4.93 → 0.4.95

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