xto-fronted 0.4.102 → 0.4.104

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