xto-fronted 0.4.102 → 0.4.103

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