xto-fronted 0.4.101 → 0.4.103

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 (123) hide show
  1. package/dist/{index-C1j4f3mM.js → index-BlN5yXBT.js} +2 -2
  2. package/dist/{index-BKj-34y6.js → index-BsPaXQak.js} +2 -2
  3. package/dist/{index-CJSTBnGF.js → index-CNx4xpUG.js} +981 -971
  4. package/dist/{index-BK4Mut6H.js → index-DEChud2_.js} +2 -2
  5. package/dist/{index-B6s_uLJE.js → index-DdACCkye.js} +23 -23
  6. package/dist/index.js +1 -1
  7. package/dist/stores/app.d.ts +8 -2
  8. package/dist/stores/auth.d.ts +2 -2
  9. package/dist/style.css +1 -1
  10. package/package.json +94 -94
  11. package/src/App.vue +48 -48
  12. package/src/assets/styles/_dark.scss +639 -639
  13. package/src/assets/styles/_root.scss +183 -183
  14. package/src/assets/styles/_variables.scss +69 -69
  15. package/src/assets/styles/index.scss +460 -460
  16. package/src/components/Layout/Header.vue +1 -1
  17. package/src/components/Layout/MixTopMenu.vue +1185 -1185
  18. package/src/components/Layout/Sidebar.vue +229 -229
  19. package/src/components/Layout/SidebarMenuItem.vue +163 -163
  20. package/src/components/Layout/TopMenu.vue +1177 -1177
  21. package/src/components/Layout/index.vue +199 -199
  22. package/src/composables/useI18n.ts +43 -43
  23. package/src/index.ts +114 -100
  24. package/src/router/layoutRoute.ts +70 -70
  25. package/src/stores/app.ts +9 -0
  26. package/src/stores/index.ts +15 -15
  27. package/src/stores/locale.ts +66 -66
  28. package/src/types/json-bigint.d.ts +18 -18
  29. package/src/types/xto.d.ts +172 -172
  30. package/src/utils/request.ts +184 -184
  31. package/src/views/dashboard/index.vue +545 -545
  32. package/src/views/error/403.vue +251 -251
  33. package/src/views/error/404.vue +253 -253
  34. package/src/views/login/index.vue +586 -586
  35. package/src/views/system/menu/index.vue +690 -690
  36. package/src/views/system/role/index.vue +583 -583
  37. package/src/views/system/user/index.vue +655 -655
  38. package/dist/assets/404-C9Uh6Uu-.css +0 -1
  39. package/dist/assets/404-zjGLLssH.js +0 -1
  40. package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
  41. package/dist/assets/index-B5xc4gQB.css +0 -1
  42. package/dist/assets/index-BDgOY6Rp.js +0 -1
  43. package/dist/assets/index-BIoRANs0.js +0 -1
  44. package/dist/assets/index-BRR97dc6.js +0 -1
  45. package/dist/assets/index-Bz0BgZQ1.js +0 -1
  46. package/dist/assets/index-CAdztNsv.css +0 -1
  47. package/dist/assets/index-CCXrcISf.css +0 -1
  48. package/dist/assets/index-CfpZmcpk.css +0 -1
  49. package/dist/assets/index-CwJSA85U.js +0 -1
  50. package/dist/assets/index-CwRA10ac.js +0 -1
  51. package/dist/assets/index-D8NDxq9d.js +0 -1
  52. package/dist/assets/index-DEB6-Iv_.js +0 -2
  53. package/dist/assets/index-DM4Ezclc.css +0 -1
  54. package/dist/assets/index-DYv7nImj.css +0 -1
  55. package/dist/assets/index-t-2Y0KhA.css +0 -1
  56. package/dist/assets/vendor-CUVPinTg.js +0 -13
  57. package/dist/assets/vue-vendor-DeJXJVbN.js +0 -29
  58. package/dist/assets/xto-base-CL2NKZJJ.css +0 -1
  59. package/dist/assets/xto-base-PwLGsxxb.js +0 -1
  60. package/dist/assets/xto-business--V1F5Gwb.css +0 -1
  61. package/dist/assets/xto-core-CtL4zKiV.js +0 -1
  62. package/dist/assets/xto-data-MxZsiJgi.css +0 -1
  63. package/dist/assets/xto-data-bCXQa7fT.js +0 -1
  64. package/dist/assets/xto-feedback-Bxx38c3P.css +0 -1
  65. package/dist/assets/xto-feedback-CPydp0kn.js +0 -1
  66. package/dist/assets/xto-form-Cu6q3VLG.css +0 -1
  67. package/dist/assets/xto-form-bywohdAf.js +0 -1
  68. package/dist/assets/xto-layout-BDD6sSlM.css +0 -1
  69. package/dist/assets/xto-navigation-Bbdpine9.js +0 -1
  70. package/dist/assets/xto-navigation-XfpyMpEo.css +0 -1
  71. package/dist/index-3ekBp4iW.js +0 -479
  72. package/dist/index-58aI1w0v.js +0 -515
  73. package/dist/index-A_B_Ap_A.js +0 -4240
  74. package/dist/index-B-lMqzxZ.js +0 -479
  75. package/dist/index-B5DLfOYb.js +0 -189
  76. package/dist/index-BAmYUT0G.js +0 -189
  77. package/dist/index-BJlOXgu5.js +0 -515
  78. package/dist/index-BMQao91y.js +0 -189
  79. package/dist/index-BRvi9qW-.js +0 -515
  80. package/dist/index-BVGW4DDQ.js +0 -189
  81. package/dist/index-BXg94yA2.js +0 -515
  82. package/dist/index-BYAkZ2gD.js +0 -641
  83. package/dist/index-BfXnrw05.js +0 -515
  84. package/dist/index-Bmb0rt9C.js +0 -641
  85. package/dist/index-Bmf0YbVq.js +0 -189
  86. package/dist/index-C1BnOFy7.js +0 -3145
  87. package/dist/index-C2-a5KSQ.js +0 -4233
  88. package/dist/index-C3K89jzC.js +0 -515
  89. package/dist/index-C92NkXAn.js +0 -479
  90. package/dist/index-CAHSv7LK.js +0 -4285
  91. package/dist/index-CVH7bDsl.js +0 -4285
  92. package/dist/index-Ccp6zfq-.js +0 -4290
  93. package/dist/index-CeZ0CSSs.js +0 -641
  94. package/dist/index-Cf8E7FM1.js +0 -4270
  95. package/dist/index-CgyQqbdx.js +0 -189
  96. package/dist/index-ChowNrlU.js +0 -641
  97. package/dist/index-CvQgEgUM.js +0 -641
  98. package/dist/index-D25KzR0I.js +0 -479
  99. package/dist/index-D4LWXVnG.js +0 -515
  100. package/dist/index-DCApv1oX.js +0 -641
  101. package/dist/index-DCBIjLHy.js +0 -515
  102. package/dist/index-DEYOivza.js +0 -641
  103. package/dist/index-DHH8Os_2.js +0 -189
  104. package/dist/index-DReodgBw.js +0 -4233
  105. package/dist/index-DTRJONCd.js +0 -515
  106. package/dist/index-DgffG7KK.js +0 -641
  107. package/dist/index-DjERNRXX.js +0 -515
  108. package/dist/index-DjXyzwL0.js +0 -479
  109. package/dist/index-DkOqM4e2.js +0 -3147
  110. package/dist/index-Ds8IV04t.js +0 -189
  111. package/dist/index-LSdsO2Ox.js +0 -479
  112. package/dist/index-UJixTdep.js +0 -479
  113. package/dist/index-WPRGF_GX.js +0 -189
  114. package/dist/index-WPWzllES.js +0 -641
  115. package/dist/index-Wl2Qg26t.js +0 -3147
  116. package/dist/index-dk0diNwi.js +0 -479
  117. package/dist/index-gBlRG4kk.js +0 -479
  118. package/dist/index-mVol7F2K.js +0 -479
  119. package/dist/index-xWU3J3OH.js +0 -641
  120. package/dist/index-zKJLxthI.js +0 -189
  121. package/dist/index.es.js +0 -95
  122. package/dist/index.html +0 -28
  123. package/dist/index.umd.js +0 -8
@@ -1,1178 +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
- // 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
- }
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(appStore.loginPath)
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
+ }
1178
1178
  </style>