xto-fronted 0.4.7 → 0.4.9

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 (82) hide show
  1. package/.env.development +7 -7
  2. package/.env.production +7 -7
  3. package/dist/assets/403-AFBQifUI.js +1 -0
  4. package/dist/assets/403-BHEXXbt2.css +1 -0
  5. package/dist/assets/404-Ct_A1n7S.css +1 -0
  6. package/dist/assets/404-WFvpcD2_.js +1 -0
  7. package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
  8. package/dist/assets/index-1juADvYN.js +2 -0
  9. package/dist/assets/index-4-QoJAgA.css +1 -0
  10. package/dist/assets/index-B-sX4Ru0.js +1 -0
  11. package/dist/assets/index-BHwEwbkp.js +1 -0
  12. package/dist/assets/index-BMcziU5a.css +1 -0
  13. package/dist/assets/index-BRR97dc6.js +1 -0
  14. package/dist/assets/index-BTsRosKu.js +1 -0
  15. package/dist/assets/index-BZA0ksjx.css +1 -0
  16. package/dist/assets/index-BpV_8nl0.js +1 -0
  17. package/dist/assets/index-BvzhR4zp.js +1 -0
  18. package/dist/assets/index-CUh_s55Z.css +1 -0
  19. package/dist/assets/index-CVjdnIgR.css +1 -0
  20. package/dist/assets/index-CYq57-zj.js +1 -0
  21. package/dist/assets/index-CZAlkDIC.css +1 -0
  22. package/dist/assets/index-CkL3sVAQ.js +2 -0
  23. package/dist/assets/index-CtrKVYJb.css +1 -0
  24. package/dist/assets/index-Cz2P_bsS.js +1 -0
  25. package/dist/assets/index-D9wlAuR_.js +1 -0
  26. package/dist/assets/index-DawJb02s.css +1 -0
  27. package/dist/assets/index-DfFR6NLf.js +1 -0
  28. package/dist/assets/index-Do3gMkWw.js +2 -0
  29. package/dist/assets/index-DwVgMO8e.js +1 -0
  30. package/dist/assets/index-GDP-IkXE.css +1 -0
  31. package/dist/assets/index-Iaz1ZzPC.js +2 -0
  32. package/dist/assets/index-PfV8pzQz.css +1 -0
  33. package/dist/assets/index-Swfu6yvD.css +1 -0
  34. package/dist/assets/index-Te8_PRgJ.js +1 -0
  35. package/dist/assets/index-WyZ91RLx.css +1 -0
  36. package/dist/assets/index-tFYRoFdE.js +1 -0
  37. package/dist/assets/vendor-42ANG6Sg.js +6 -0
  38. package/dist/assets/vite-Dw-pgLOX.js +1 -0
  39. package/dist/assets/vue-vendor-Br-l7wbK.js +29 -0
  40. package/dist/assets/xto-base-C-IBqjVs.js +1 -0
  41. package/dist/assets/xto-base-C6eqMPdO.css +1 -0
  42. package/dist/assets/xto-business--V1F5Gwb.css +1 -0
  43. package/dist/assets/xto-core-DZK7Cyg0.js +1 -0
  44. package/dist/assets/xto-data-BFpiDgJi.js +1 -0
  45. package/dist/assets/xto-data-CnAQAQH2.css +1 -0
  46. package/dist/assets/xto-feedback-B7ipsTfz.js +1 -0
  47. package/dist/assets/xto-feedback-DBwJzoTj.css +1 -0
  48. package/dist/assets/xto-form-CrsyAjyr.css +1 -0
  49. package/dist/assets/xto-form-NRjKKNcY.js +1 -0
  50. package/dist/assets/xto-layout-BqU8RuWL.css +1 -0
  51. package/dist/assets/xto-navigation-BiSaXPfr.js +1 -0
  52. package/dist/assets/xto-navigation-C1cnSL2E.css +1 -0
  53. package/dist/assets/xto-navigation-CBPg4dCc.css +1 -0
  54. package/dist/assets/xto-navigation-CKabFu9d.js +1 -0
  55. package/dist/index.html +28 -0
  56. package/package.json +85 -85
  57. package/src/api/auth.ts +25 -25
  58. package/src/api/system.ts +66 -66
  59. package/src/assets/styles/_dark.scss +524 -406
  60. package/src/assets/styles/index.scss +8 -0
  61. package/src/components/Layout/Header.vue +968 -973
  62. package/src/components/Layout/Sidebar.vue +283 -273
  63. package/src/components/Layout/TopMenu.vue +186 -0
  64. package/src/components/Layout/index.vue +60 -3
  65. package/src/composables/useApp.ts +61 -61
  66. package/src/composables/useAuth.ts +16 -16
  67. package/src/directives/permission.ts +27 -27
  68. package/src/env.d.ts +18 -18
  69. package/src/index.ts +47 -47
  70. package/src/router/dynamicRoutes.ts +162 -162
  71. package/src/router/guards.ts +128 -128
  72. package/src/router/index.ts +79 -79
  73. package/src/stores/auth.ts +65 -65
  74. package/src/stores/menu.ts +48 -48
  75. package/src/stores/user.ts +50 -50
  76. package/src/types/api.d.ts +80 -80
  77. package/src/utils/auth.ts +99 -99
  78. package/src/utils/config.ts +80 -80
  79. package/src/utils/permission.ts +32 -32
  80. package/src/utils/request.ts +124 -124
  81. package/src/views/login/index.vue +194 -194
  82. package/vite.config.ts +135 -135
@@ -1,974 +1,969 @@
1
- <script setup lang="ts">
2
- import { ref, computed, onMounted, onUnmounted } from 'vue'
3
- import { useRoute, useRouter } from 'vue-router'
4
- import { useAppStore } from '@/stores/app'
5
- import { useUserStore } from '@/stores/user'
6
- import { useAuthStore } from '@/stores/auth'
7
- import { useMenuStore } from '@/stores/menu'
8
- import { Icon } from '@xto/base'
9
- import { Drawer } from '@xto/feedback'
10
-
11
- type LayoutMode = 'sidebar' | 'top' | 'mix'
12
-
13
- const route = useRoute()
14
- const router = useRouter()
15
- const appStore = useAppStore()
16
- const userStore = useUserStore()
17
- const authStore = useAuthStore()
18
- const menuStore = useMenuStore()
19
-
20
- const dropdownVisible = ref(false)
21
- const drawerVisible = ref(false)
22
- const dropdownRef = ref<HTMLElement | null>(null)
23
- const isFullscreen = ref(false)
24
- const searchVisible = ref(false)
25
- const searchKeyword = ref('')
26
- const searchRef = ref<HTMLElement | null>(null)
27
-
28
- // 灰色模式状态
29
- const greyMode = ref(false)
30
-
31
- // 布局模式选项
32
- const layoutOptions: { value: LayoutMode; label: string; icon: string }[] = [
33
- { value: 'sidebar', label: '左侧菜单', icon: 'sidebar-left' },
34
- { value: 'top', label: '顶部菜单', icon: 'menu' },
35
- { value: 'mix', label: '混合菜单', icon: 'grid' }
36
- ]
37
-
38
- // 主题色选项
39
- const colorOptions = [
40
- { value: '#409eff', label: '默认蓝' },
41
- { value: '#1890ff', label: '科技蓝' },
42
- { value: '#52c41a', label: '极光绿' },
43
- { value: '#faad14', label: '日落橙' },
44
- { value: '#f5222d', label: '薄暮红' },
45
- { value: '#722ed1', label: '酱紫' }
46
- ]
47
-
48
- // 面包屑
49
- const breadcrumbs = computed(() => {
50
- const matched = route.matched.filter(item => item.meta && item.meta.title)
51
- return matched.map(item => ({
52
- title: item.meta.title as string,
53
- path: item.path
54
- }))
55
- })
56
-
57
- // 扁平化菜单用于搜索
58
- const flattenMenus = (menus: any[], parentTitle = ''): any[] => {
59
- const result: any[] = []
60
- menus.forEach(menu => {
61
- if (menu.children && menu.children.length > 0) {
62
- result.push(...flattenMenus(menu.children, menu.title))
63
- } else {
64
- result.push({ ...menu, parentTitle })
65
- }
66
- })
67
- return result
68
- }
69
-
70
- // 搜索结果
71
- const searchResults = computed(() => {
72
- if (!searchKeyword.value.trim()) return []
73
- const flatMenus = flattenMenus(menuStore.menuList)
74
- return flatMenus.filter(menu =>
75
- menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
76
- )
77
- })
78
-
79
- // 当前布局模式
80
- const currentLayout = computed(() => appStore.layout)
81
-
82
- // 切换折叠
83
- const toggleCollapse = () => {
84
- appStore.toggleCollapse()
85
- }
86
-
87
- // 切换主题
88
- const toggleTheme = () => {
89
- appStore.toggleTheme()
90
- }
91
-
92
- // 打开设置抽屉
93
- const openSettingsDrawer = () => {
94
- drawerVisible.value = true
95
- }
96
-
97
- // 切换布局模式
98
- const handleLayoutChange = (mode: LayoutMode) => {
99
- appStore.setLayout(mode)
100
- }
101
-
102
- // 切换灰色模式
103
- const handleGreyModeChange = (value: boolean) => {
104
- greyMode.value = value
105
- const html = document.documentElement
106
- if (value) {
107
- html.classList.add('grey-mode')
108
- } else {
109
- html.classList.remove('grey-mode')
110
- }
111
- }
112
-
113
- // 切换全屏
114
- const toggleFullscreen = () => {
115
- if (!document.fullscreenElement) {
116
- document.documentElement.requestFullscreen()
117
- } else {
118
- document.exitFullscreen()
119
- }
120
- }
121
-
122
- // 监听全屏变化
123
- const handleFullscreenChange = () => {
124
- isFullscreen.value = !!document.fullscreenElement
125
- }
126
-
127
- // 切换下拉菜单
128
- const toggleDropdown = () => {
129
- dropdownVisible.value = !dropdownVisible.value
130
- }
131
-
132
- // 关闭下拉菜单
133
- const closeDropdowns = () => {
134
- dropdownVisible.value = false
135
- }
136
-
137
- // 显示搜索
138
- const showSearch = () => {
139
- searchVisible.value = true
140
- }
141
-
142
- // 隐藏搜索
143
- const hideSearch = () => {
144
- searchVisible.value = false
145
- searchKeyword.value = ''
146
- }
147
-
148
- // 搜索结果点击
149
- const handleSearchItemClick = (path: string) => {
150
- router.push(path)
151
- hideSearch()
152
- }
153
-
154
- // 设置主题色
155
- const handleColorChange = (color: string) => {
156
- appStore.setPrimaryColor(color)
157
- }
158
-
159
- // 个人信息
160
- const handleProfile = () => {
161
- closeDropdowns()
162
- router.push('/profile')
163
- }
164
-
165
- // 修改密码
166
- const handleChangePassword = () => {
167
- closeDropdowns()
168
- router.push('/change-password')
169
- }
170
-
171
- // 退出登录
172
- const handleLogout = () => {
173
- closeDropdowns()
174
- authStore.logout()
175
- userStore.clearUserInfo()
176
- menuStore.clearMenu()
177
- router.push('/login')
178
- }
179
-
180
- // 点击外部关闭下拉菜单
181
- const handleClickOutside = (event: MouseEvent) => {
182
- if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
183
- closeDropdowns()
184
- }
185
- if (searchRef.value && !searchRef.value.contains(event.target as Node)) {
186
- hideSearch()
187
- }
188
- }
189
-
190
- // 键盘快捷键
191
- const handleKeydown = (event: KeyboardEvent) => {
192
- if (event.key === 'Escape') {
193
- hideSearch()
194
- closeDropdowns()
195
- }
196
- // Ctrl+K 打开搜索
197
- if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
198
- event.preventDefault()
199
- showSearch()
200
- }
201
- }
202
-
203
- onMounted(() => {
204
- document.addEventListener('click', handleClickOutside)
205
- document.addEventListener('fullscreenchange', handleFullscreenChange)
206
- document.addEventListener('keydown', handleKeydown)
207
- appStore.initTheme()
208
- // 初始化灰色模式状态
209
- greyMode.value = document.documentElement.classList.contains('grey-mode')
210
- })
211
-
212
- onUnmounted(() => {
213
- document.removeEventListener('click', handleClickOutside)
214
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
215
- document.removeEventListener('keydown', handleKeydown)
216
- })
217
- </script>
218
-
219
- <template>
220
- <div class="header">
221
- <!-- 左侧 -->
222
- <div class="header__left">
223
- <!-- 折叠按钮 -->
224
- <div class="header__collapse" @click="toggleCollapse">
225
- <Icon :name="appStore.isCollapsed ? 'menu-unfold' : 'menu-fold'" :size="18" />
226
- </div>
227
-
228
- <!-- 面包屑 -->
229
- <div v-if="appStore.showBreadcrumb" class="header__breadcrumb">
230
- <span v-for="(item, index) in breadcrumbs" :key="item.path">
231
- <span v-if="index > 0" class="breadcrumb-separator">/</span>
232
- <span :class="{ 'is-current': index === breadcrumbs.length - 1 }">
233
- {{ item.title }}
234
- </span>
235
- </span>
236
- </div>
237
- </div>
238
-
239
- <!-- 右侧 -->
240
- <div class="header__right">
241
- <!-- 搜索按钮 -->
242
- <div class="header__action" @click="showSearch" title="搜索 (Ctrl+K)">
243
- <Icon name="search" :size="16" />
244
- </div>
245
-
246
- <!-- 全屏切换 -->
247
- <div class="header__action" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
248
- <Icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" :size="16" />
249
- </div>
250
-
251
- <!-- 换肤设置 -->
252
- <div class="header__action" @click="openSettingsDrawer" title="换肤设置">
253
- <Icon name="setting" :size="16" />
254
- </div>
255
-
256
- <!-- 主题切换 -->
257
- <div class="header__action" @click="toggleTheme" title="切换主题">
258
- <Icon :name="appStore.isDark ? 'sun' : 'moon'" :size="16" />
259
- </div>
260
-
261
- <!-- 用户头像 -->
262
- <div class="header__user" ref="dropdownRef">
263
- <div class="header__user-trigger" @click.stop="toggleDropdown">
264
- <div class="header__avatar">
265
- <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
266
- </div>
267
- <span class="header__user-name">{{ userStore.userName }}</span>
268
- <span class="header__user-arrow" :class="{ 'is-active': dropdownVisible }">▼</span>
269
- </div>
270
-
271
- <!-- 下拉菜单 -->
272
- <Transition name="dropdown">
273
- <div v-if="dropdownVisible" class="header__dropdown">
274
- <div class="header__dropdown-header">
275
- <div class="header__dropdown-avatar">
276
- <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
277
- </div>
278
- <div class="header__dropdown-info">
279
- <div class="header__dropdown-name">{{ userStore.userName }}</div>
280
- <div class="header__dropdown-role">{{ userStore.departmentName }}</div>
281
- </div>
282
- </div>
283
- <div class="header__dropdown-divider"></div>
284
- <div class="header__dropdown-menu">
285
- <div class="header__dropdown-item" @click="handleProfile">
286
- <Icon name="user" :size="16" />
287
- <span>个人信息</span>
288
- </div>
289
- <div class="header__dropdown-item" @click="handleChangePassword">
290
- <Icon name="lock" :size="16" />
291
- <span>修改密码</span>
292
- </div>
293
- <div class="header__dropdown-divider"></div>
294
- <div class="header__dropdown-item header__dropdown-item--danger" @click="handleLogout">
295
- <Icon name="logout" :size="16" />
296
- <span>退出登录</span>
297
- </div>
298
- </div>
299
- </div>
300
- </Transition>
301
- </div>
302
- </div>
303
-
304
- <!-- 全局搜索弹窗 -->
305
- <Transition name="search">
306
- <div v-if="searchVisible" class="header__search-modal" ref="searchRef">
307
- <div class="search-container">
308
- <div class="search-input-wrapper">
309
- <Icon class="search-icon" name="search" :size="20" />
310
- <input
311
- v-model="searchKeyword"
312
- type="text"
313
- class="search-input"
314
- placeholder="搜索菜单..."
315
- autofocus
316
- @keyup.enter="searchResults[0] && handleSearchItemClick(searchResults[0].path)"
317
- />
318
- <span class="search-shortcut">ESC 关闭</span>
319
- </div>
320
- <div v-if="searchResults.length > 0" class="search-results">
321
- <div
322
- v-for="(item, index) in searchResults"
323
- :key="item.path"
324
- :class="['search-result-item', { 'is-first': index === 0 }]"
325
- @click="handleSearchItemClick(item.path)"
326
- >
327
- <Icon class="search-result-icon" :name="item.icon || 'file'" :size="20" />
328
- <div class="search-result-info">
329
- <span class="search-result-title">{{ item.title }}</span>
330
- <span v-if="item.parentTitle" class="search-result-parent">{{ item.parentTitle }}</span>
331
- </div>
332
- </div>
333
- </div>
334
- <div v-else-if="searchKeyword" class="search-empty">
335
- 未找到匹配的菜单
336
- </div>
337
- </div>
338
- </div>
339
- </Transition>
340
-
341
- <!-- 换肤设置抽屉 -->
342
- <Drawer
343
- v-model="drawerVisible"
344
- title="换肤设置"
345
- direction="rtl"
346
- size="320px"
347
- >
348
- <div class="settings-drawer">
349
- <!-- 布局模式 -->
350
- <div class="settings-section">
351
- <div class="settings-title">布局模式</div>
352
- <div class="settings-layout-options">
353
- <div
354
- v-for="option in layoutOptions"
355
- :key="option.value"
356
- :class="['layout-option', { 'is-active': currentLayout === option.value }]"
357
- @click="handleLayoutChange(option.value)"
358
- >
359
- <div class="layout-option__preview">
360
- <div v-if="option.value === 'sidebar'" class="layout-preview-sidebar">
361
- <div class="preview-aside"></div>
362
- <div class="preview-main">
363
- <div class="preview-header"></div>
364
- <div class="preview-content"></div>
365
- </div>
366
- </div>
367
- <div v-else-if="option.value === 'top'" class="layout-preview-top">
368
- <div class="preview-header-full"></div>
369
- <div class="preview-content-full"></div>
370
- </div>
371
- <div v-else class="layout-preview-mix">
372
- <div class="preview-header-mix">
373
- <div class="preview-mix-left"></div>
374
- </div>
375
- <div class="preview-mix-body">
376
- <div class="preview-mix-aside"></div>
377
- <div class="preview-mix-content"></div>
378
- </div>
379
- </div>
380
- </div>
381
- <span class="layout-option__label">{{ option.label }}</span>
382
- </div>
383
- </div>
384
- </div>
385
-
386
- <!-- 主题色 -->
387
- <div class="settings-section">
388
- <div class="settings-title">主题色</div>
389
- <div class="settings-color-options">
390
- <div
391
- v-for="color in colorOptions"
392
- :key="color.value"
393
- :class="['color-option', { 'is-active': appStore.primaryColor === color.value }]"
394
- :style="{ backgroundColor: color.value }"
395
- :title="color.label"
396
- @click="handleColorChange(color.value)"
397
- >
398
- <Icon v-if="appStore.primaryColor === color.value" name="check" :size="12" color="#fff" />
399
- </div>
400
- </div>
401
- </div>
402
-
403
- <!-- 功能开关 -->
404
- <div class="settings-section">
405
- <div class="settings-title">功能设置</div>
406
- <div class="settings-switch-list">
407
- <div class="settings-switch-item">
408
- <span>灰色模式</span>
409
- <div class="switch-wrapper" :class="{ 'is-checked': greyMode }" @click="handleGreyModeChange(!greyMode)">
410
- <span class="switch-core"></span>
411
- </div>
412
- </div>
413
- <div class="settings-switch-item">
414
- <span>暗黑模式</span>
415
- <div class="switch-wrapper" :class="{ 'is-checked': appStore.isDark }" @click="toggleTheme">
416
- <span class="switch-core"></span>
417
- </div>
418
- </div>
419
- </div>
420
- </div>
421
- </div>
422
- </Drawer>
423
- </div>
424
- </template>
425
-
426
- <style lang="scss" scoped>
427
- .header {
428
- display: flex;
429
- align-items: center;
430
- justify-content: space-between;
431
- padding: 0 20px;
432
- height: 100%;
433
- background-color: var(--bg-color);
434
- border-bottom: 1px solid var(--color-border-lighter);
435
-
436
- &__left {
437
- display: flex;
438
- align-items: center;
439
- gap: 15px;
440
- }
441
-
442
- &__collapse {
443
- width: 24px;
444
- height: 24px;
445
- display: flex;
446
- align-items: center;
447
- justify-content: center;
448
- cursor: pointer;
449
- color: var(--color-text-regular);
450
- transition: color 0.2s;
451
-
452
- &:hover {
453
- color: var(--color-primary);
454
- }
455
- }
456
-
457
- &__breadcrumb {
458
- font-size: 14px;
459
- color: var(--color-text-secondary);
460
-
461
- .breadcrumb-separator {
462
- margin: 0 8px;
463
- color: var(--color-text-placeholder);
464
- }
465
-
466
- .is-current {
467
- color: var(--color-text-primary);
468
- font-weight: 500;
469
- }
470
- }
471
-
472
- &__right {
473
- display: flex;
474
- align-items: center;
475
- gap: 8px;
476
- }
477
-
478
- &__action {
479
- width: 32px;
480
- height: 32px;
481
- display: flex;
482
- align-items: center;
483
- justify-content: center;
484
- cursor: pointer;
485
- border-radius: var(--border-radius-base);
486
- color: var(--color-text-regular);
487
- transition: all 0.2s;
488
-
489
- &:hover {
490
- background-color: var(--color-fill);
491
- color: var(--color-primary);
492
- }
493
- }
494
-
495
- &__user {
496
- position: relative;
497
- margin-left: 8px;
498
-
499
- &-trigger {
500
- display: flex;
501
- align-items: center;
502
- gap: 8px;
503
- cursor: pointer;
504
- padding: 4px 8px;
505
- border-radius: var(--border-radius-base);
506
- transition: background-color 0.2s;
507
-
508
- &:hover {
509
- background-color: var(--color-fill);
510
- }
511
- }
512
-
513
- &-name {
514
- font-size: 14px;
515
- color: var(--color-text-primary);
516
- max-width: 100px;
517
- overflow: hidden;
518
- text-overflow: ellipsis;
519
- white-space: nowrap;
520
- }
521
-
522
- &-arrow {
523
- font-size: 10px;
524
- color: var(--color-text-secondary);
525
- transition: transform 0.2s;
526
-
527
- &.is-active {
528
- transform: rotate(180deg);
529
- }
530
- }
531
- }
532
-
533
- &__avatar {
534
- width: 32px;
535
- height: 32px;
536
- border-radius: 50%;
537
- background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
538
- display: flex;
539
- align-items: center;
540
- justify-content: center;
541
- color: #fff;
542
- font-size: 14px;
543
- font-weight: 500;
544
- }
545
-
546
- &__dropdown {
547
- position: absolute;
548
- top: calc(100% + 8px);
549
- right: 0;
550
- min-width: 200px;
551
- background-color: var(--bg-color);
552
- border-radius: var(--border-radius-base);
553
- box-shadow: var(--box-shadow);
554
- overflow: hidden;
555
- z-index: 100;
556
-
557
- &-header {
558
- display: flex;
559
- align-items: center;
560
- gap: 12px;
561
- padding: 16px;
562
- }
563
-
564
- &-avatar {
565
- width: 40px;
566
- height: 40px;
567
- border-radius: 50%;
568
- background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
569
- display: flex;
570
- align-items: center;
571
- justify-content: center;
572
- color: #fff;
573
- font-size: 16px;
574
- font-weight: 500;
575
- }
576
-
577
- &-info {
578
- flex: 1;
579
- }
580
-
581
- &-name {
582
- font-size: 14px;
583
- font-weight: 500;
584
- color: var(--color-text-primary);
585
- }
586
-
587
- &-role {
588
- font-size: 12px;
589
- color: var(--color-text-secondary);
590
- margin-top: 2px;
591
- }
592
-
593
- &-divider {
594
- height: 1px;
595
- background-color: var(--color-border-lighter);
596
- }
597
-
598
- &-menu {
599
- padding: 8px 0;
600
- }
601
-
602
- &-item {
603
- display: flex;
604
- align-items: center;
605
- gap: 10px;
606
- padding: 10px 16px;
607
- cursor: pointer;
608
- font-size: 14px;
609
- color: var(--color-text-regular);
610
- transition: all 0.2s;
611
-
612
- &:hover {
613
- background-color: var(--color-fill);
614
- color: var(--color-text-primary);
615
- }
616
-
617
- &--danger {
618
- color: var(--color-danger);
619
-
620
- &:hover {
621
- background-color: var(--color-danger-light);
622
- color: var(--color-danger);
623
- }
624
- }
625
- }
626
- }
627
-
628
- &__search-modal {
629
- position: fixed;
630
- top: 0;
631
- left: 0;
632
- right: 0;
633
- bottom: 0;
634
- background-color: rgba(0, 0, 0, 0.5);
635
- display: flex;
636
- align-items: flex-start;
637
- justify-content: center;
638
- padding-top: 100px;
639
- z-index: 200;
640
- }
641
- }
642
-
643
- // 搜索容器
644
- .search-container {
645
- width: 600px;
646
- max-width: 90vw;
647
- background-color: var(--bg-color);
648
- border-radius: var(--border-radius-large);
649
- box-shadow: var(--box-shadow-dark);
650
- overflow: hidden;
651
- }
652
-
653
- .search-input-wrapper {
654
- display: flex;
655
- align-items: center;
656
- padding: 16px 20px;
657
- border-bottom: 1px solid var(--color-border-lighter);
658
-
659
- .search-icon {
660
- color: var(--color-text-secondary);
661
- margin-right: 12px;
662
- }
663
-
664
- .search-input {
665
- flex: 1;
666
- font-size: 16px;
667
- color: var(--color-text-primary);
668
- background: transparent;
669
- border: none;
670
- outline: none;
671
-
672
- &::placeholder {
673
- color: var(--color-text-placeholder);
674
- }
675
- }
676
-
677
- .search-shortcut {
678
- font-size: 12px;
679
- color: var(--color-text-secondary);
680
- padding: 4px 8px;
681
- background-color: var(--color-fill);
682
- border-radius: var(--border-radius-base);
683
- }
684
- }
685
-
686
- .search-results {
687
- max-height: 400px;
688
- overflow-y: auto;
689
- padding: 8px 0;
690
- }
691
-
692
- .search-result-item {
693
- display: flex;
694
- align-items: center;
695
- gap: 12px;
696
- padding: 12px 20px;
697
- cursor: pointer;
698
- transition: background-color 0.2s;
699
-
700
- &:hover {
701
- background-color: var(--color-fill);
702
- }
703
-
704
- &.is-first {
705
- background-color: var(--color-primary-light-9);
706
- }
707
-
708
- .search-result-icon {
709
- color: var(--color-text-secondary);
710
- }
711
-
712
- .search-result-info {
713
- flex: 1;
714
- display: flex;
715
- flex-direction: column;
716
- gap: 2px;
717
- }
718
-
719
- .search-result-title {
720
- font-size: 14px;
721
- color: var(--color-text-primary);
722
- }
723
-
724
- .search-result-parent {
725
- font-size: 12px;
726
- color: var(--color-text-secondary);
727
- }
728
- }
729
-
730
- .search-empty {
731
- padding: 40px 20px;
732
- text-align: center;
733
- color: var(--color-text-secondary);
734
- font-size: 14px;
735
- }
736
-
737
- // 设置抽屉内容
738
- .settings-drawer {
739
- .settings-section {
740
- margin-bottom: 24px;
741
- }
742
-
743
- .settings-title {
744
- font-size: 14px;
745
- font-weight: 500;
746
- color: var(--color-text-primary);
747
- margin-bottom: 12px;
748
- }
749
-
750
- .settings-layout-options {
751
- display: flex;
752
- gap: 12px;
753
- }
754
-
755
- .layout-option {
756
- flex: 1;
757
- display: flex;
758
- flex-direction: column;
759
- align-items: center;
760
- gap: 8px;
761
- padding: 12px;
762
- border: 1px solid var(--color-border);
763
- border-radius: var(--border-radius-base);
764
- cursor: pointer;
765
- transition: all 0.2s;
766
-
767
- &:hover {
768
- border-color: var(--color-primary-light-5);
769
- }
770
-
771
- &.is-active {
772
- border-color: var(--color-primary);
773
- background-color: var(--color-primary-light-9);
774
- }
775
-
776
- &__preview {
777
- width: 48px;
778
- height: 36px;
779
- border: 1px solid var(--color-border-light);
780
- border-radius: 2px;
781
- overflow: hidden;
782
- }
783
-
784
- &__label {
785
- font-size: 12px;
786
- color: var(--color-text-regular);
787
- }
788
- }
789
-
790
- // 布局预览样式
791
- .layout-preview-sidebar {
792
- display: flex;
793
- height: 100%;
794
-
795
- .preview-aside {
796
- width: 25%;
797
- height: 100%;
798
- background-color: var(--color-primary-light-7);
799
- }
800
-
801
- .preview-main {
802
- flex: 1;
803
- display: flex;
804
- flex-direction: column;
805
-
806
- .preview-header {
807
- height: 20%;
808
- background-color: var(--color-border-light);
809
- }
810
-
811
- .preview-content {
812
- flex: 1;
813
- background-color: var(--bg-color-page);
814
- }
815
- }
816
- }
817
-
818
- .layout-preview-top {
819
- display: flex;
820
- flex-direction: column;
821
- height: 100%;
822
-
823
- .preview-header-full {
824
- height: 25%;
825
- background-color: var(--color-primary-light-7);
826
- }
827
-
828
- .preview-content-full {
829
- flex: 1;
830
- background-color: var(--bg-color-page);
831
- }
832
- }
833
-
834
- .layout-preview-mix {
835
- display: flex;
836
- flex-direction: column;
837
- height: 100%;
838
-
839
- .preview-header-mix {
840
- height: 25%;
841
- background-color: var(--color-primary-light-7);
842
- display: flex;
843
-
844
- .preview-mix-left {
845
- width: 30%;
846
- background-color: var(--color-primary);
847
- }
848
- }
849
-
850
- .preview-mix-body {
851
- flex: 1;
852
- display: flex;
853
-
854
- .preview-mix-aside {
855
- width: 25%;
856
- background-color: var(--color-primary-light-8);
857
- }
858
-
859
- .preview-mix-content {
860
- flex: 1;
861
- background-color: var(--bg-color-page);
862
- }
863
- }
864
- }
865
-
866
- .settings-color-options {
867
- display: flex;
868
- gap: 12px;
869
- }
870
-
871
- .color-option {
872
- width: 24px;
873
- height: 24px;
874
- border-radius: 4px;
875
- cursor: pointer;
876
- display: flex;
877
- align-items: center;
878
- justify-content: center;
879
- transition: transform 0.2s;
880
-
881
- &:hover {
882
- transform: scale(1.1);
883
- }
884
-
885
- &.is-active {
886
- box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--color-primary);
887
- }
888
- }
889
-
890
- .settings-switch-list {
891
- display: flex;
892
- flex-direction: column;
893
- gap: 12px;
894
- }
895
-
896
- .settings-switch-item {
897
- display: flex;
898
- align-items: center;
899
- justify-content: space-between;
900
-
901
- span {
902
- font-size: 14px;
903
- color: var(--color-text-regular);
904
- }
905
- }
906
-
907
- .switch-wrapper {
908
- width: 44px;
909
- height: 22px;
910
- display: flex;
911
- align-items: center;
912
- cursor: pointer;
913
-
914
- .switch-core {
915
- width: 100%;
916
- height: 100%;
917
- border-radius: 11px;
918
- background-color: var(--color-border);
919
- position: relative;
920
- transition: background-color 0.2s;
921
-
922
- &::after {
923
- content: '';
924
- position: absolute;
925
- top: 2px;
926
- left: 2px;
927
- width: 18px;
928
- height: 18px;
929
- background-color: #fff;
930
- border-radius: 50%;
931
- transition: left 0.2s;
932
- }
933
- }
934
-
935
- &.is-checked {
936
- .switch-core {
937
- background-color: var(--color-primary);
938
-
939
- &::after {
940
- left: 24px;
941
- }
942
- }
943
- }
944
- }
945
- }
946
-
947
- // 下拉动画
948
- .dropdown-enter-active,
949
- .dropdown-leave-active {
950
- transition: all 0.2s ease;
951
- }
952
-
953
- .dropdown-enter-from,
954
- .dropdown-leave-to {
955
- opacity: 0;
956
- transform: translateY(-10px);
957
- }
958
-
959
- // 搜索弹窗动画
960
- .search-enter-active,
961
- .search-leave-active {
962
- transition: all 0.2s ease;
963
- }
964
-
965
- .search-enter-from,
966
- .search-leave-to {
967
- opacity: 0;
968
- }
969
-
970
- // 灰色模式
971
- :root.grey-mode {
972
- filter: grayscale(100%);
973
- }
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { useAppStore } from '@/stores/app'
5
+ import { useUserStore } from '@/stores/user'
6
+ import { useAuthStore } from '@/stores/auth'
7
+ import { useMenuStore } from '@/stores/menu'
8
+ import { Icon } from '@xto/base'
9
+ import { Drawer } from '@xto/feedback'
10
+
11
+ type LayoutMode = 'sidebar' | 'top' | 'mix'
12
+
13
+ const route = useRoute()
14
+ const router = useRouter()
15
+ const appStore = useAppStore()
16
+ const userStore = useUserStore()
17
+ const authStore = useAuthStore()
18
+ const menuStore = useMenuStore()
19
+
20
+ const dropdownVisible = ref(false)
21
+ const drawerVisible = ref(false)
22
+ const dropdownRef = ref<HTMLElement | null>(null)
23
+ const isFullscreen = ref(false)
24
+ const searchVisible = ref(false)
25
+ const searchKeyword = ref('')
26
+ const searchRef = ref<HTMLElement | null>(null)
27
+
28
+ // 灰色模式状态
29
+ const greyMode = ref(false)
30
+
31
+ // 布局模式选项
32
+ const layoutOptions: { value: LayoutMode; label: string; icon: string }[] = [
33
+ { value: 'sidebar', label: '左侧菜单', icon: 'sidebar-left' },
34
+ { value: 'top', label: '顶部菜单', icon: 'menu' },
35
+ { value: 'mix', label: '混合菜单', icon: 'grid' }
36
+ ]
37
+
38
+ // 主题色选项
39
+ const colorOptions = [
40
+ { value: '#409eff', label: '默认蓝' },
41
+ { value: '#1890ff', label: '科技蓝' },
42
+ { value: '#52c41a', label: '极光绿' },
43
+ { value: '#faad14', label: '日落橙' },
44
+ { value: '#f5222d', label: '薄暮红' },
45
+ { value: '#722ed1', label: '酱紫' }
46
+ ]
47
+
48
+ // 面包屑
49
+ const breadcrumbs = computed(() => {
50
+ const matched = route.matched.filter(item => item.meta && item.meta.title)
51
+ return matched.map(item => ({
52
+ title: item.meta.title as string,
53
+ path: item.path
54
+ }))
55
+ })
56
+
57
+ // 扁平化菜单用于搜索
58
+ const flattenMenus = (menus: any[], parentTitle = ''): any[] => {
59
+ const result: any[] = []
60
+ menus.forEach(menu => {
61
+ if (menu.children && menu.children.length > 0) {
62
+ result.push(...flattenMenus(menu.children, menu.title))
63
+ } else {
64
+ result.push({ ...menu, parentTitle })
65
+ }
66
+ })
67
+ return result
68
+ }
69
+
70
+ // 搜索结果
71
+ const searchResults = computed(() => {
72
+ if (!searchKeyword.value.trim()) return []
73
+ const flatMenus = flattenMenus(menuStore.menuList)
74
+ return flatMenus.filter(menu =>
75
+ menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
76
+ )
77
+ })
78
+
79
+ // 当前布局模式
80
+ const currentLayout = computed(() => appStore.layout)
81
+
82
+ // 切换折叠
83
+ const toggleCollapse = () => {
84
+ appStore.toggleCollapse()
85
+ }
86
+
87
+ // 切换主题
88
+ const toggleTheme = () => {
89
+ appStore.toggleTheme()
90
+ }
91
+
92
+ // 打开设置抽屉
93
+ const openSettingsDrawer = () => {
94
+ drawerVisible.value = true
95
+ }
96
+
97
+ // 切换布局模式
98
+ const handleLayoutChange = (mode: LayoutMode) => {
99
+ appStore.setLayout(mode)
100
+ }
101
+
102
+ // 切换灰色模式
103
+ const handleGreyModeChange = (value: boolean) => {
104
+ greyMode.value = value
105
+ const html = document.documentElement
106
+ if (value) {
107
+ html.classList.add('grey-mode')
108
+ } else {
109
+ html.classList.remove('grey-mode')
110
+ }
111
+ }
112
+
113
+ // 切换全屏
114
+ const toggleFullscreen = () => {
115
+ if (!document.fullscreenElement) {
116
+ document.documentElement.requestFullscreen()
117
+ } else {
118
+ document.exitFullscreen()
119
+ }
120
+ }
121
+
122
+ // 监听全屏变化
123
+ const handleFullscreenChange = () => {
124
+ isFullscreen.value = !!document.fullscreenElement
125
+ }
126
+
127
+ // 切换下拉菜单
128
+ const toggleDropdown = () => {
129
+ dropdownVisible.value = !dropdownVisible.value
130
+ }
131
+
132
+ // 关闭下拉菜单
133
+ const closeDropdowns = () => {
134
+ dropdownVisible.value = false
135
+ }
136
+
137
+ // 显示搜索
138
+ const showSearch = () => {
139
+ searchVisible.value = true
140
+ }
141
+
142
+ // 隐藏搜索
143
+ const hideSearch = () => {
144
+ searchVisible.value = false
145
+ searchKeyword.value = ''
146
+ }
147
+
148
+ // 搜索结果点击
149
+ const handleSearchItemClick = (path: string) => {
150
+ router.push(path)
151
+ hideSearch()
152
+ }
153
+
154
+ // 设置主题色
155
+ const handleColorChange = (color: string) => {
156
+ appStore.setPrimaryColor(color)
157
+ }
158
+
159
+ // 个人信息
160
+ const handleProfile = () => {
161
+ closeDropdowns()
162
+ router.push('/profile')
163
+ }
164
+
165
+ // 修改密码
166
+ const handleChangePassword = () => {
167
+ closeDropdowns()
168
+ router.push('/change-password')
169
+ }
170
+
171
+ // 退出登录
172
+ const handleLogout = () => {
173
+ closeDropdowns()
174
+ authStore.logout()
175
+ userStore.clearUserInfo()
176
+ menuStore.clearMenu()
177
+ router.push('/login')
178
+ }
179
+
180
+ // 点击外部关闭下拉菜单
181
+ const handleClickOutside = (event: MouseEvent) => {
182
+ if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
183
+ closeDropdowns()
184
+ }
185
+ if (searchRef.value && !searchRef.value.contains(event.target as Node)) {
186
+ hideSearch()
187
+ }
188
+ }
189
+
190
+ // 键盘快捷键
191
+ const handleKeydown = (event: KeyboardEvent) => {
192
+ if (event.key === 'Escape') {
193
+ hideSearch()
194
+ closeDropdowns()
195
+ }
196
+ // Ctrl+K 打开搜索
197
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
198
+ event.preventDefault()
199
+ showSearch()
200
+ }
201
+ }
202
+
203
+ onMounted(() => {
204
+ document.addEventListener('click', handleClickOutside)
205
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
206
+ document.addEventListener('keydown', handleKeydown)
207
+ appStore.initTheme()
208
+ // 初始化灰色模式状态
209
+ greyMode.value = document.documentElement.classList.contains('grey-mode')
210
+ })
211
+
212
+ onUnmounted(() => {
213
+ document.removeEventListener('click', handleClickOutside)
214
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
215
+ document.removeEventListener('keydown', handleKeydown)
216
+ })
217
+ </script>
218
+
219
+ <template>
220
+ <div class="header">
221
+ <!-- 左侧 -->
222
+ <div class="header__left">
223
+ <!-- 折叠按钮 -->
224
+ <div class="header__collapse" @click="toggleCollapse">
225
+ <Icon :name="appStore.isCollapsed ? 'menu-unfold' : 'menu-fold'" :size="18" />
226
+ </div>
227
+
228
+ <!-- 面包屑 -->
229
+ <div v-if="appStore.showBreadcrumb" class="header__breadcrumb">
230
+ <span v-for="(item, index) in breadcrumbs" :key="item.path">
231
+ <span v-if="index > 0" class="breadcrumb-separator">/</span>
232
+ <span :class="{ 'is-current': index === breadcrumbs.length - 1 }">
233
+ {{ item.title }}
234
+ </span>
235
+ </span>
236
+ </div>
237
+ </div>
238
+
239
+ <!-- 右侧 -->
240
+ <div class="header__right">
241
+ <!-- 搜索按钮 -->
242
+ <div class="header__action" @click="showSearch" title="搜索 (Ctrl+K)">
243
+ <Icon name="search" :size="16" />
244
+ </div>
245
+
246
+ <!-- 全屏切换 -->
247
+ <div class="header__action" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
248
+ <Icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" :size="16" />
249
+ </div>
250
+
251
+ <!-- 换肤设置 -->
252
+ <div class="header__action" @click="openSettingsDrawer" title="换肤设置">
253
+ <Icon name="skin" :size="16" />
254
+ </div>
255
+
256
+ <!-- 主题切换 -->
257
+ <div class="header__action" @click="toggleTheme" title="切换主题">
258
+ <Icon :name="appStore.isDark ? 'sun' : 'moon'" :size="16" />
259
+ </div>
260
+
261
+ <!-- 用户头像 -->
262
+ <div class="header__user" ref="dropdownRef">
263
+ <div class="header__user-trigger" @click.stop="toggleDropdown">
264
+ <div class="header__avatar">
265
+ <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
266
+ </div>
267
+ <span class="header__user-name">{{ userStore.userName }}</span>
268
+ <span class="header__user-arrow" :class="{ 'is-active': dropdownVisible }">▼</span>
269
+ </div>
270
+
271
+ <!-- 下拉菜单 -->
272
+ <Transition name="dropdown">
273
+ <div v-if="dropdownVisible" class="header__dropdown">
274
+ <div class="header__dropdown-header">
275
+ <div class="header__dropdown-avatar">
276
+ <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
277
+ </div>
278
+ <div class="header__dropdown-info">
279
+ <div class="header__dropdown-name">{{ userStore.userName }}</div>
280
+ <div class="header__dropdown-role">{{ userStore.departmentName }}</div>
281
+ </div>
282
+ </div>
283
+ <div class="header__dropdown-divider"></div>
284
+ <div class="header__dropdown-menu">
285
+ <div class="header__dropdown-item" @click="handleProfile">
286
+ <Icon name="user" :size="16" />
287
+ <span>个人信息</span>
288
+ </div>
289
+ <div class="header__dropdown-item" @click="handleChangePassword">
290
+ <Icon name="lock" :size="16" />
291
+ <span>修改密码</span>
292
+ </div>
293
+ <div class="header__dropdown-divider"></div>
294
+ <div class="header__dropdown-item header__dropdown-item--danger" @click="handleLogout">
295
+ <Icon name="logout" :size="16" />
296
+ <span>退出登录</span>
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </Transition>
301
+ </div>
302
+ </div>
303
+
304
+ <!-- 全局搜索弹窗 -->
305
+ <Transition name="search">
306
+ <div v-if="searchVisible" class="header__search-modal" ref="searchRef">
307
+ <div class="search-container">
308
+ <div class="search-input-wrapper">
309
+ <Icon class="search-icon" name="search" :size="20" />
310
+ <input
311
+ v-model="searchKeyword"
312
+ type="text"
313
+ class="search-input"
314
+ placeholder="搜索菜单..."
315
+ autofocus
316
+ @keyup.enter="searchResults[0] && handleSearchItemClick(searchResults[0].path)"
317
+ />
318
+ <span class="search-shortcut">ESC 关闭</span>
319
+ </div>
320
+ <div v-if="searchResults.length > 0" class="search-results">
321
+ <div
322
+ v-for="(item, index) in searchResults"
323
+ :key="item.path"
324
+ :class="['search-result-item', { 'is-first': index === 0 }]"
325
+ @click="handleSearchItemClick(item.path)"
326
+ >
327
+ <Icon class="search-result-icon" :name="item.icon || 'file'" :size="20" />
328
+ <div class="search-result-info">
329
+ <span class="search-result-title">{{ item.title }}</span>
330
+ <span v-if="item.parentTitle" class="search-result-parent">{{ item.parentTitle }}</span>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ <div v-else-if="searchKeyword" class="search-empty">
335
+ 未找到匹配的菜单
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </Transition>
340
+
341
+ <!-- 换肤设置抽屉 -->
342
+ <Drawer
343
+ v-model="drawerVisible"
344
+ title="换肤设置"
345
+ direction="rtl"
346
+ size="320px"
347
+ >
348
+ <div class="settings-drawer">
349
+ <!-- 布局模式 -->
350
+ <div class="settings-section">
351
+ <div class="settings-title">布局模式</div>
352
+ <div class="settings-layout-options">
353
+ <div
354
+ v-for="option in layoutOptions"
355
+ :key="option.value"
356
+ :class="['layout-option', { 'is-active': currentLayout === option.value }]"
357
+ @click="handleLayoutChange(option.value)"
358
+ >
359
+ <div class="layout-option__preview">
360
+ <div v-if="option.value === 'sidebar'" class="layout-preview-sidebar">
361
+ <div class="preview-aside"></div>
362
+ <div class="preview-main">
363
+ <div class="preview-header"></div>
364
+ <div class="preview-content"></div>
365
+ </div>
366
+ </div>
367
+ <div v-else-if="option.value === 'top'" class="layout-preview-top">
368
+ <div class="preview-header-full"></div>
369
+ <div class="preview-content-full"></div>
370
+ </div>
371
+ <div v-else class="layout-preview-mix">
372
+ <div class="preview-header-mix">
373
+ <div class="preview-mix-left"></div>
374
+ </div>
375
+ <div class="preview-mix-body">
376
+ <div class="preview-mix-aside"></div>
377
+ <div class="preview-mix-content"></div>
378
+ </div>
379
+ </div>
380
+ </div>
381
+ <span class="layout-option__label">{{ option.label }}</span>
382
+ </div>
383
+ </div>
384
+ </div>
385
+
386
+ <!-- 主题色 -->
387
+ <div class="settings-section">
388
+ <div class="settings-title">主题色</div>
389
+ <div class="settings-color-options">
390
+ <div
391
+ v-for="color in colorOptions"
392
+ :key="color.value"
393
+ :class="['color-option', { 'is-active': appStore.primaryColor === color.value }]"
394
+ :style="{ backgroundColor: color.value }"
395
+ :title="color.label"
396
+ @click="handleColorChange(color.value)"
397
+ >
398
+ <Icon v-if="appStore.primaryColor === color.value" name="check" :size="12" color="#fff" />
399
+ </div>
400
+ </div>
401
+ </div>
402
+
403
+ <!-- 功能开关 -->
404
+ <div class="settings-section">
405
+ <div class="settings-title">功能设置</div>
406
+ <div class="settings-switch-list">
407
+ <div class="settings-switch-item">
408
+ <span>灰色模式</span>
409
+ <div class="switch-wrapper" :class="{ 'is-checked': greyMode }" @click="handleGreyModeChange(!greyMode)">
410
+ <span class="switch-core"></span>
411
+ </div>
412
+ </div>
413
+ <div class="settings-switch-item">
414
+ <span>暗黑模式</span>
415
+ <div class="switch-wrapper" :class="{ 'is-checked': appStore.isDark }" @click="toggleTheme">
416
+ <span class="switch-core"></span>
417
+ </div>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </Drawer>
423
+ </div>
424
+ </template>
425
+
426
+ <style lang="scss" scoped>
427
+ .header {
428
+ display: flex;
429
+ align-items: center;
430
+ justify-content: space-between;
431
+ padding: 0 20px;
432
+ height: 100%;
433
+ background-color: var(--bg-color);
434
+ border-bottom: 1px solid var(--color-border-lighter);
435
+
436
+ &__left {
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 15px;
440
+ }
441
+
442
+ &__collapse {
443
+ width: 24px;
444
+ height: 24px;
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ cursor: pointer;
449
+ color: var(--color-text-regular);
450
+ transition: color 0.2s;
451
+
452
+ &:hover {
453
+ color: var(--color-primary);
454
+ }
455
+ }
456
+
457
+ &__breadcrumb {
458
+ font-size: 14px;
459
+ color: var(--color-text-secondary);
460
+
461
+ .breadcrumb-separator {
462
+ margin: 0 8px;
463
+ color: var(--color-text-placeholder);
464
+ }
465
+
466
+ .is-current {
467
+ color: var(--color-text-primary);
468
+ font-weight: 500;
469
+ }
470
+ }
471
+
472
+ &__right {
473
+ display: flex;
474
+ align-items: center;
475
+ gap: 8px;
476
+ }
477
+
478
+ &__action {
479
+ width: 32px;
480
+ height: 32px;
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: center;
484
+ cursor: pointer;
485
+ border-radius: var(--border-radius-base);
486
+ color: var(--color-text-regular);
487
+ transition: all 0.2s;
488
+
489
+ &:hover {
490
+ background-color: var(--color-fill);
491
+ color: var(--color-primary);
492
+ }
493
+ }
494
+
495
+ &__user {
496
+ position: relative;
497
+ margin-left: 8px;
498
+
499
+ &-trigger {
500
+ display: flex;
501
+ align-items: center;
502
+ gap: 8px;
503
+ cursor: pointer;
504
+ padding: 4px 8px;
505
+ border-radius: var(--border-radius-base);
506
+ transition: background-color 0.2s;
507
+
508
+ &:hover {
509
+ background-color: var(--color-fill);
510
+ }
511
+ }
512
+
513
+ &-name {
514
+ font-size: 14px;
515
+ color: var(--color-text-primary);
516
+ max-width: 100px;
517
+ overflow: hidden;
518
+ text-overflow: ellipsis;
519
+ white-space: nowrap;
520
+ }
521
+
522
+ &-arrow {
523
+ font-size: 10px;
524
+ color: var(--color-text-secondary);
525
+ transition: transform 0.2s;
526
+
527
+ &.is-active {
528
+ transform: rotate(180deg);
529
+ }
530
+ }
531
+ }
532
+
533
+ &__avatar {
534
+ width: 32px;
535
+ height: 32px;
536
+ border-radius: 50%;
537
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
538
+ display: flex;
539
+ align-items: center;
540
+ justify-content: center;
541
+ color: #fff;
542
+ font-size: 14px;
543
+ font-weight: 500;
544
+ }
545
+
546
+ &__dropdown {
547
+ position: absolute;
548
+ top: calc(100% + 8px);
549
+ right: 0;
550
+ min-width: 200px;
551
+ background-color: var(--bg-color);
552
+ border-radius: var(--border-radius-base);
553
+ box-shadow: var(--box-shadow);
554
+ overflow: hidden;
555
+ z-index: 100;
556
+
557
+ &-header {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 12px;
561
+ padding: 16px;
562
+ }
563
+
564
+ &-avatar {
565
+ width: 40px;
566
+ height: 40px;
567
+ border-radius: 50%;
568
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
569
+ display: flex;
570
+ align-items: center;
571
+ justify-content: center;
572
+ color: #fff;
573
+ font-size: 16px;
574
+ font-weight: 500;
575
+ }
576
+
577
+ &-info {
578
+ flex: 1;
579
+ }
580
+
581
+ &-name {
582
+ font-size: 14px;
583
+ font-weight: 500;
584
+ color: var(--color-text-primary);
585
+ }
586
+
587
+ &-role {
588
+ font-size: 12px;
589
+ color: var(--color-text-secondary);
590
+ margin-top: 2px;
591
+ }
592
+
593
+ &-divider {
594
+ height: 1px;
595
+ background-color: var(--color-border-lighter);
596
+ }
597
+
598
+ &-menu {
599
+ padding: 8px 0;
600
+ }
601
+
602
+ &-item {
603
+ display: flex;
604
+ align-items: center;
605
+ gap: 10px;
606
+ padding: 10px 16px;
607
+ cursor: pointer;
608
+ font-size: 14px;
609
+ color: var(--color-text-regular);
610
+ transition: all 0.2s;
611
+
612
+ &:hover {
613
+ background-color: var(--color-fill);
614
+ color: var(--color-text-primary);
615
+ }
616
+
617
+ &--danger {
618
+ color: var(--color-danger);
619
+
620
+ &:hover {
621
+ background-color: var(--color-danger-light);
622
+ color: var(--color-danger);
623
+ }
624
+ }
625
+ }
626
+ }
627
+
628
+ &__search-modal {
629
+ position: fixed;
630
+ top: 0;
631
+ left: 0;
632
+ right: 0;
633
+ bottom: 0;
634
+ background-color: rgba(0, 0, 0, 0.5);
635
+ display: flex;
636
+ align-items: flex-start;
637
+ justify-content: center;
638
+ padding-top: 100px;
639
+ z-index: 200;
640
+ }
641
+ }
642
+
643
+ // 搜索容器
644
+ .search-container {
645
+ width: 600px;
646
+ max-width: 90vw;
647
+ background-color: var(--bg-color);
648
+ border-radius: var(--border-radius-large);
649
+ box-shadow: var(--box-shadow-dark);
650
+ overflow: hidden;
651
+ }
652
+
653
+ .search-input-wrapper {
654
+ display: flex;
655
+ align-items: center;
656
+ padding: 16px 20px;
657
+ border-bottom: 1px solid var(--color-border-lighter);
658
+
659
+ .search-icon {
660
+ color: var(--color-text-secondary);
661
+ margin-right: 12px;
662
+ }
663
+
664
+ .search-input {
665
+ flex: 1;
666
+ font-size: 16px;
667
+ color: var(--color-text-primary);
668
+ background: transparent;
669
+ border: none;
670
+ outline: none;
671
+
672
+ &::placeholder {
673
+ color: var(--color-text-placeholder);
674
+ }
675
+ }
676
+
677
+ .search-shortcut {
678
+ font-size: 12px;
679
+ color: var(--color-text-secondary);
680
+ padding: 4px 8px;
681
+ background-color: var(--color-fill);
682
+ border-radius: var(--border-radius-base);
683
+ }
684
+ }
685
+
686
+ .search-results {
687
+ max-height: 400px;
688
+ overflow-y: auto;
689
+ padding: 8px 0;
690
+ }
691
+
692
+ .search-result-item {
693
+ display: flex;
694
+ align-items: center;
695
+ gap: 12px;
696
+ padding: 12px 20px;
697
+ cursor: pointer;
698
+ transition: background-color 0.2s;
699
+
700
+ &:hover {
701
+ background-color: var(--color-fill);
702
+ }
703
+
704
+ &.is-first {
705
+ background-color: var(--color-primary-light-9);
706
+ }
707
+
708
+ .search-result-icon {
709
+ color: var(--color-text-secondary);
710
+ }
711
+
712
+ .search-result-info {
713
+ flex: 1;
714
+ display: flex;
715
+ flex-direction: column;
716
+ gap: 2px;
717
+ }
718
+
719
+ .search-result-title {
720
+ font-size: 14px;
721
+ color: var(--color-text-primary);
722
+ }
723
+
724
+ .search-result-parent {
725
+ font-size: 12px;
726
+ color: var(--color-text-secondary);
727
+ }
728
+ }
729
+
730
+ .search-empty {
731
+ padding: 40px 20px;
732
+ text-align: center;
733
+ color: var(--color-text-secondary);
734
+ font-size: 14px;
735
+ }
736
+
737
+ // 设置抽屉内容
738
+ .settings-drawer {
739
+ .settings-section {
740
+ margin-bottom: 24px;
741
+ }
742
+
743
+ .settings-title {
744
+ font-size: 14px;
745
+ font-weight: 500;
746
+ color: var(--color-text-primary);
747
+ margin-bottom: 12px;
748
+ }
749
+
750
+ .settings-layout-options {
751
+ display: flex;
752
+ gap: 12px;
753
+ }
754
+
755
+ .layout-option {
756
+ flex: 1;
757
+ display: flex;
758
+ flex-direction: column;
759
+ align-items: center;
760
+ gap: 8px;
761
+ padding: 12px;
762
+ border: 1px solid var(--color-border);
763
+ border-radius: var(--border-radius-base);
764
+ cursor: pointer;
765
+ transition: all 0.2s;
766
+
767
+ &:hover {
768
+ border-color: var(--color-primary-light-5);
769
+ }
770
+
771
+ &.is-active {
772
+ border-color: var(--color-primary);
773
+ background-color: var(--color-primary-light-9);
774
+ }
775
+
776
+ &__preview {
777
+ width: 48px;
778
+ height: 36px;
779
+ border: 1px solid var(--color-border-light);
780
+ border-radius: 2px;
781
+ overflow: hidden;
782
+ }
783
+
784
+ &__label {
785
+ font-size: 12px;
786
+ color: var(--color-text-regular);
787
+ }
788
+ }
789
+
790
+ // 布局预览样式
791
+ .layout-preview-sidebar {
792
+ display: flex;
793
+ height: 100%;
794
+
795
+ .preview-aside {
796
+ width: 25%;
797
+ height: 100%;
798
+ background-color: var(--color-primary-light-7);
799
+ }
800
+
801
+ .preview-main {
802
+ flex: 1;
803
+ display: flex;
804
+ flex-direction: column;
805
+
806
+ .preview-header {
807
+ height: 20%;
808
+ background-color: var(--color-border-light);
809
+ }
810
+
811
+ .preview-content {
812
+ flex: 1;
813
+ background-color: var(--bg-color-page);
814
+ }
815
+ }
816
+ }
817
+
818
+ .layout-preview-top {
819
+ display: flex;
820
+ flex-direction: column;
821
+ height: 100%;
822
+
823
+ .preview-header-full {
824
+ height: 25%;
825
+ background-color: var(--color-primary-light-7);
826
+ }
827
+
828
+ .preview-content-full {
829
+ flex: 1;
830
+ background-color: var(--bg-color-page);
831
+ }
832
+ }
833
+
834
+ .layout-preview-mix {
835
+ display: flex;
836
+ flex-direction: column;
837
+ height: 100%;
838
+
839
+ .preview-header-mix {
840
+ height: 25%;
841
+ background-color: var(--color-primary-light-7);
842
+ display: flex;
843
+
844
+ .preview-mix-left {
845
+ width: 30%;
846
+ background-color: var(--color-primary);
847
+ }
848
+ }
849
+
850
+ .preview-mix-body {
851
+ flex: 1;
852
+ display: flex;
853
+
854
+ .preview-mix-aside {
855
+ width: 25%;
856
+ background-color: var(--color-primary-light-8);
857
+ }
858
+
859
+ .preview-mix-content {
860
+ flex: 1;
861
+ background-color: var(--bg-color-page);
862
+ }
863
+ }
864
+ }
865
+
866
+ .settings-color-options {
867
+ display: flex;
868
+ gap: 12px;
869
+ }
870
+
871
+ .color-option {
872
+ width: 24px;
873
+ height: 24px;
874
+ border-radius: 4px;
875
+ cursor: pointer;
876
+ display: flex;
877
+ align-items: center;
878
+ justify-content: center;
879
+ transition: transform 0.2s;
880
+
881
+ &:hover {
882
+ transform: scale(1.1);
883
+ }
884
+
885
+ &.is-active {
886
+ box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--color-primary);
887
+ }
888
+ }
889
+
890
+ .settings-switch-list {
891
+ display: flex;
892
+ flex-direction: column;
893
+ gap: 12px;
894
+ }
895
+
896
+ .settings-switch-item {
897
+ display: flex;
898
+ align-items: center;
899
+ justify-content: space-between;
900
+
901
+ span {
902
+ font-size: 14px;
903
+ color: var(--color-text-regular);
904
+ }
905
+ }
906
+
907
+ .switch-wrapper {
908
+ width: 44px;
909
+ height: 22px;
910
+ display: flex;
911
+ align-items: center;
912
+ cursor: pointer;
913
+
914
+ .switch-core {
915
+ width: 100%;
916
+ height: 100%;
917
+ border-radius: 11px;
918
+ background-color: var(--color-border);
919
+ position: relative;
920
+ transition: background-color 0.2s;
921
+
922
+ &::after {
923
+ content: '';
924
+ position: absolute;
925
+ top: 2px;
926
+ left: 2px;
927
+ width: 18px;
928
+ height: 18px;
929
+ background-color: #fff;
930
+ border-radius: 50%;
931
+ transition: left 0.2s;
932
+ }
933
+ }
934
+
935
+ &.is-checked {
936
+ .switch-core {
937
+ background-color: var(--color-primary);
938
+
939
+ &::after {
940
+ left: 24px;
941
+ }
942
+ }
943
+ }
944
+ }
945
+ }
946
+
947
+ // 下拉动画
948
+ .dropdown-enter-active,
949
+ .dropdown-leave-active {
950
+ transition: all 0.2s ease;
951
+ }
952
+
953
+ .dropdown-enter-from,
954
+ .dropdown-leave-to {
955
+ opacity: 0;
956
+ transform: translateY(-10px);
957
+ }
958
+
959
+ // 搜索弹窗动画
960
+ .search-enter-active,
961
+ .search-leave-active {
962
+ transition: all 0.2s ease;
963
+ }
964
+
965
+ .search-enter-from,
966
+ .search-leave-to {
967
+ opacity: 0;
968
+ }
974
969
  </style>