xto-fronted 0.4.92 → 0.4.94

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