xto-fronted 0.4.39 → 0.4.40

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.
@@ -0,0 +1,930 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { useMenuStore } from '@/stores/menu'
5
+ import { useAppStore } from '@/stores/app'
6
+ import { useUserStore } from '@/stores/user'
7
+ import { useAuthStore } from '@/stores/auth'
8
+ import { Menu, MenuItem } from '@xto/navigation'
9
+ import { Icon } from '@xto/base'
10
+ import { Drawer } from '@xto/feedback'
11
+
12
+ const route = useRoute()
13
+ const router = useRouter()
14
+ const menuStore = useMenuStore()
15
+ const appStore = useAppStore()
16
+ const userStore = useUserStore()
17
+ const authStore = useAuthStore()
18
+
19
+ // 当前激活的一级菜单路径
20
+ const activeTopMenuPath = ref<string>('')
21
+
22
+ // 计算当前激活的一级菜单
23
+ const currentTopMenu = computed(() => {
24
+ return menuStore.menuList.find(menu => {
25
+ // 检查当前路由是否属于该一级菜单
26
+ if (route.path.startsWith(menu.menuUrl)) return true
27
+ if (menu.children && menu.children.some(child => route.path.startsWith(child.menuUrl) || route.path === child.menuUrl)) return true
28
+ return false
29
+ })
30
+ })
31
+
32
+ // 初始化激活的一级菜单
33
+ const initActiveTopMenu = () => {
34
+ if (currentTopMenu.value) {
35
+ activeTopMenuPath.value = currentTopMenu.value.menuUrl
36
+ } else if (menuStore.menuList.length > 0) {
37
+ activeTopMenuPath.value = menuStore.menuList[0].menuUrl
38
+ }
39
+ }
40
+
41
+ // 监听路由变化,更新激活的一级菜单
42
+ watch(() => route.path, () => {
43
+ initActiveTopMenu()
44
+ }, { immediate: true })
45
+
46
+ // 监听菜单列表变化
47
+ watch(() => menuStore.menuList, () => {
48
+ initActiveTopMenu()
49
+ }, { immediate: true })
50
+
51
+ // 获取当前选中一级菜单的子菜单
52
+ const currentSubMenuList = computed(() => {
53
+ const topMenu = menuStore.menuList.find(menu => menu.menuUrl === activeTopMenuPath.value)
54
+ return topMenu?.children || []
55
+ })
56
+
57
+ // 更新 appStore 中的当前子菜单列表
58
+ watch(currentSubMenuList, (list) => {
59
+ appStore.setMixSubMenus(list)
60
+ }, { immediate: true })
61
+
62
+ // 菜单主题相关
63
+ const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
64
+ const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
65
+ const menuActiveTextColor = computed(() => '#409eff')
66
+
67
+ // 下拉菜单
68
+ const dropdownVisible = ref(false)
69
+ const dropdownRef = ref<HTMLElement | null>(null)
70
+ const isFullscreen = ref(false)
71
+ const drawerVisible = ref(false)
72
+ const greyMode = ref(false)
73
+
74
+ // 一级菜单选择处理
75
+ const handleTopMenuSelect = (index: string) => {
76
+ activeTopMenuPath.value = index
77
+ const topMenu = menuStore.menuList.find(menu => menu.menuUrl === index)
78
+
79
+ if (topMenu) {
80
+ // 如果有子菜单,导航到第一个子菜单或保持当前位置
81
+ if (topMenu.children && topMenu.children.length > 0) {
82
+ // 检查当前路由是否已经在该一级菜单的子菜单下
83
+ const isInSubMenus = topMenu.children.some(child =>
84
+ route.path.startsWith(child.menuUrl) || route.path === child.menuUrl
85
+ )
86
+ if (!isInSubMenus) {
87
+ // 导航到第一个子菜单
88
+ router.push(topMenu.children[0].menuUrl)
89
+ }
90
+ } else {
91
+ // 如果没有子菜单,直接导航到该路由
92
+ if (route.path !== index) {
93
+ router.push(index)
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // 已知的图标名称列表
100
+ const knownIcons = new Set([
101
+ 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right',
102
+ 'caret-down', 'caret-right', 'plus', 'minus', 'close', 'check',
103
+ 'edit', 'delete', 'copy', 'download', 'upload', 'refresh', 'search',
104
+ 'filter', 'more', 'setting', 'share', 'loading', 'info', 'success',
105
+ 'warning', 'error', 'question', 'user', 'user-add', 'user-group',
106
+ 'logout', 'login', 'file', 'folder', 'folder-open', 'document',
107
+ 'image', 'video', 'music', 'camera', 'mail', 'phone', 'chat',
108
+ 'bell', 'message', 'eye', 'eye-off', 'calendar', 'clock', 'history',
109
+ 'timer', 'location', 'map', 'globe', 'star', 'heart', 'thumb-up',
110
+ 'link', 'external-link', 'lock', 'unlock', 'key', 'home', 'menu',
111
+ 'menu-fold', 'menu-unfold', 'sidebar-fold', 'sidebar-expand',
112
+ 'sidebar-left', 'dashboard', 'chart', 'chart-pie', 'chart-line',
113
+ 'report', 'analytics', 'system', 'permission', 'role', 'user-manage',
114
+ 'log', 'notification', 'app', 'list', 'grid', 'fullscreen',
115
+ 'fullscreen-exit', 'zoom-in', 'zoom-out', 'print', 'bookmark',
116
+ 'tag', 'code', 'terminal', 'database', 'server', 'cloud', 'gift',
117
+ 'moon', 'sun', 'theme', 'skin'
118
+ ])
119
+
120
+ // 获取菜单图标名称
121
+ const getMenuIcon = (icon?: string): string => {
122
+ if (!icon || icon === '') return ''
123
+
124
+ if (icon.startsWith('tineco-icon-')) {
125
+ const iconName = icon.replace('tineco-icon-', '')
126
+ const tinecoIconMap: Record<string, string> = {
127
+ home: 'home',
128
+ dashboard: 'dashboard',
129
+ system: 'system',
130
+ user: 'user',
131
+ role: 'role',
132
+ menu: 'list',
133
+ setting: 'setting',
134
+ file: 'file',
135
+ folder: 'folder',
136
+ chart: 'chart',
137
+ report: 'report',
138
+ analytics: 'analytics'
139
+ }
140
+ return tinecoIconMap[iconName] || iconName
141
+ }
142
+
143
+ const iconMap: Record<string, string> = {
144
+ dashboard: 'dashboard',
145
+ system: 'system',
146
+ user: 'user',
147
+ role: 'role',
148
+ menu: 'list',
149
+ setting: 'setting',
150
+ home: 'home',
151
+ chart: 'chart',
152
+ report: 'report',
153
+ analytics: 'analytics',
154
+ permission: 'permission',
155
+ log: 'log',
156
+ notification: 'notification',
157
+ app: 'app',
158
+ list: 'list',
159
+ grid: 'grid'
160
+ }
161
+
162
+ return iconMap[icon] || icon
163
+ }
164
+
165
+ const getFirstChar = (name?: string): string => {
166
+ if (!name) return ''
167
+ return name.charAt(0)
168
+ }
169
+
170
+ const iconExists = (iconName: string): boolean => {
171
+ return knownIcons.has(iconName)
172
+ }
173
+
174
+ // 切换主题
175
+ const toggleTheme = () => {
176
+ appStore.toggleTheme()
177
+ drawerVisible.value = false
178
+ }
179
+
180
+ // 切换全屏
181
+ const toggleFullscreen = () => {
182
+ if (!document.fullscreenElement) {
183
+ document.documentElement.requestFullscreen()
184
+ } else {
185
+ document.exitFullscreen()
186
+ }
187
+ }
188
+
189
+ // 监听全屏变化
190
+ const handleFullscreenChange = () => {
191
+ isFullscreen.value = !!document.fullscreenElement
192
+ }
193
+
194
+ // 切换下拉菜单
195
+ const toggleDropdown = () => {
196
+ dropdownVisible.value = !dropdownVisible.value
197
+ }
198
+
199
+ // 关闭下拉菜单
200
+ const closeDropdowns = () => {
201
+ dropdownVisible.value = false
202
+ }
203
+
204
+ // 打开设置抽屉
205
+ const openSettingsDrawer = () => {
206
+ drawerVisible.value = true
207
+ }
208
+
209
+ // 切换灰色模式
210
+ const handleGreyModeChange = (value: boolean) => {
211
+ greyMode.value = value
212
+ const html = document.documentElement
213
+ if (value) {
214
+ html.classList.add('grey-mode')
215
+ } else {
216
+ html.classList.remove('grey-mode')
217
+ }
218
+ }
219
+
220
+ // 抽屉内灰色模式开关
221
+ const handleGreyModeToggle = () => {
222
+ handleGreyModeChange(!greyMode.value)
223
+ drawerVisible.value = false
224
+ }
225
+
226
+ // 抽屉内暗黑模式开关
227
+ const handleDarkModeToggle = () => {
228
+ appStore.toggleTheme()
229
+ drawerVisible.value = false
230
+ }
231
+
232
+ // 布局模式选项
233
+ type LayoutMode = 'sidebar' | 'top' | 'mix'
234
+ const layoutOptions: { value: LayoutMode; label: string; icon: string }[] = [
235
+ { value: 'sidebar', label: '左侧菜单', icon: 'sidebar-left' },
236
+ { value: 'top', label: '顶部菜单', icon: 'menu' },
237
+ { value: 'mix', label: '混合菜单', icon: 'grid' }
238
+ ]
239
+
240
+ const currentLayout = computed(() => appStore.layout)
241
+
242
+ // 切换布局模式
243
+ const handleLayoutChange = (mode: LayoutMode) => {
244
+ appStore.setLayout(mode)
245
+ drawerVisible.value = false
246
+ }
247
+
248
+ // 主题色选项
249
+ const colorOptions = [
250
+ { value: '#409eff', label: '默认蓝' },
251
+ { value: '#1890ff', label: '科技蓝' },
252
+ { value: '#52c41a', label: '极光绿' },
253
+ { value: '#faad14', label: '日落橙' },
254
+ { value: '#f5222d', label: '薄暮红' },
255
+ { value: '#722ed1', label: '酱紫' }
256
+ ]
257
+
258
+ // 设置主题色
259
+ const handleColorChange = (color: string) => {
260
+ appStore.setPrimaryColor(color)
261
+ drawerVisible.value = false
262
+ }
263
+
264
+ // 个人信息
265
+ const handleProfile = () => {
266
+ closeDropdowns()
267
+ router.push('/profile')
268
+ }
269
+
270
+ // 修改密码
271
+ const handleChangePassword = () => {
272
+ closeDropdowns()
273
+ router.push('/change-password')
274
+ }
275
+
276
+ // 退出登录
277
+ const handleLogout = () => {
278
+ closeDropdowns()
279
+ authStore.logout()
280
+ userStore.clearUserInfo()
281
+ menuStore.clearMenu()
282
+ router.push('/login')
283
+ }
284
+
285
+ // 点击外部关闭下拉菜单
286
+ const handleClickOutside = (event: MouseEvent) => {
287
+ if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
288
+ closeDropdowns()
289
+ }
290
+ }
291
+
292
+ onMounted(() => {
293
+ document.addEventListener('click', handleClickOutside)
294
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
295
+ greyMode.value = document.documentElement.classList.contains('grey-mode')
296
+ initActiveTopMenu()
297
+ })
298
+
299
+ onUnmounted(() => {
300
+ document.removeEventListener('click', handleClickOutside)
301
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
302
+ })
303
+ </script>
304
+
305
+ <template>
306
+ <div class="mix-top-menu">
307
+ <!-- 左侧 Logo -->
308
+ <div class="mix-top-menu__logo">
309
+ <img src="/vite.svg" alt="Logo" class="mix-top-menu__logo-img" />
310
+ <span class="mix-top-menu__logo-text">{{ appStore.appName }}</span>
311
+ </div>
312
+
313
+ <!-- 一级菜单(只显示一级菜单,不显示下拉子菜单) -->
314
+ <Menu
315
+ :default-active="activeTopMenuPath"
316
+ mode="horizontal"
317
+ :background-color="menuBgColor"
318
+ :text-color="menuTextColor"
319
+ :active-text-color="menuActiveTextColor"
320
+ class="mix-top-menu__menu"
321
+ @select="handleTopMenuSelect"
322
+ >
323
+ <MenuItem
324
+ v-for="menu in menuStore.menuList"
325
+ :key="menu.menuUrl"
326
+ :index="menu.menuUrl"
327
+ >
328
+ <span class="mix-top-menu__menu-content">
329
+ <span class="mix-top-menu__menu-icon">
330
+ <Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
331
+ <span v-else class="mix-top-menu__menu-char">{{ getFirstChar(menu.menuName) }}</span>
332
+ </span>
333
+ <span class="mix-top-menu__menu-text">{{ menu.menuName }}</span>
334
+ </span>
335
+ </MenuItem>
336
+ </Menu>
337
+
338
+ <!-- 右侧操作区域 -->
339
+ <div class="mix-top-menu__actions">
340
+ <!-- 全屏切换 -->
341
+ <div class="mix-top-menu__action" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
342
+ <Icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" :size="16" />
343
+ </div>
344
+
345
+ <!-- 换肤设置 -->
346
+ <div class="mix-top-menu__action" @click="openSettingsDrawer" title="换肤设置">
347
+ <Icon name="skin" :size="16" />
348
+ </div>
349
+
350
+ <!-- 主题切换 -->
351
+ <div class="mix-top-menu__action" @click="toggleTheme" title="切换主题">
352
+ <Icon :name="appStore.isDark ? 'sun' : 'moon'" :size="16" />
353
+ </div>
354
+
355
+ <!-- 用户头像 -->
356
+ <div class="mix-top-menu__user" ref="dropdownRef">
357
+ <div class="mix-top-menu__user-trigger" @click.stop="toggleDropdown">
358
+ <div class="mix-top-menu__avatar">
359
+ <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
360
+ </div>
361
+ <span class="mix-top-menu__user-name">{{ userStore.userName }}</span>
362
+ <span class="mix-top-menu__user-arrow" :class="{ 'is-active': dropdownVisible }">▼</span>
363
+ </div>
364
+
365
+ <!-- 下拉菜单 -->
366
+ <Transition name="dropdown">
367
+ <div v-if="dropdownVisible" class="mix-top-menu__dropdown">
368
+ <div class="mix-top-menu__dropdown-header">
369
+ <div class="mix-top-menu__dropdown-avatar">
370
+ <span>{{ userStore.userName?.charAt(0) || 'U' }}</span>
371
+ </div>
372
+ <div class="mix-top-menu__dropdown-info">
373
+ <div class="mix-top-menu__dropdown-name">{{ userStore.userName }}</div>
374
+ <div class="mix-top-menu__dropdown-role">{{ userStore.departmentName }}</div>
375
+ </div>
376
+ </div>
377
+ <div class="mix-top-menu__dropdown-divider"></div>
378
+ <div class="mix-top-menu__dropdown-menu">
379
+ <div class="mix-top-menu__dropdown-item" @click="handleProfile">
380
+ <Icon name="user" :size="16" />
381
+ <span>个人信息</span>
382
+ </div>
383
+ <div class="mix-top-menu__dropdown-item" @click="handleChangePassword">
384
+ <Icon name="lock" :size="16" />
385
+ <span>修改密码</span>
386
+ </div>
387
+ <div class="mix-top-menu__dropdown-divider"></div>
388
+ <div class="mix-top-menu__dropdown-item mix-top-menu__dropdown-item--danger" @click="handleLogout">
389
+ <Icon name="logout" :size="16" />
390
+ <span>退出登录</span>
391
+ </div>
392
+ </div>
393
+ </div>
394
+ </Transition>
395
+ </div>
396
+ </div>
397
+
398
+ <!-- 换肤设置抽屉 -->
399
+ <Drawer
400
+ v-model="drawerVisible"
401
+ title="换肤设置"
402
+ direction="rtl"
403
+ size="320px"
404
+ >
405
+ <div class="settings-drawer">
406
+ <!-- 布局模式 -->
407
+ <div class="settings-section">
408
+ <div class="settings-title">布局模式</div>
409
+ <div class="settings-layout-options">
410
+ <div
411
+ v-for="option in layoutOptions"
412
+ :key="option.value"
413
+ :class="['layout-option', { 'is-active': currentLayout === option.value }]"
414
+ @click="handleLayoutChange(option.value)"
415
+ >
416
+ <div class="layout-option__preview">
417
+ <div v-if="option.value === 'sidebar'" class="layout-preview-sidebar">
418
+ <div class="preview-aside"></div>
419
+ <div class="preview-main">
420
+ <div class="preview-header"></div>
421
+ <div class="preview-content"></div>
422
+ </div>
423
+ </div>
424
+ <div v-else-if="option.value === 'top'" class="layout-preview-top">
425
+ <div class="preview-header-full"></div>
426
+ <div class="preview-content-full"></div>
427
+ </div>
428
+ <div v-else class="layout-preview-mix">
429
+ <div class="preview-header-mix">
430
+ <div class="preview-mix-left"></div>
431
+ </div>
432
+ <div class="preview-mix-body">
433
+ <div class="preview-mix-aside"></div>
434
+ <div class="preview-mix-content"></div>
435
+ </div>
436
+ </div>
437
+ </div>
438
+ <span class="layout-option__label">{{ option.label }}</span>
439
+ </div>
440
+ </div>
441
+ </div>
442
+
443
+ <!-- 主题色 -->
444
+ <div class="settings-section">
445
+ <div class="settings-title">主题色</div>
446
+ <div class="settings-color-options">
447
+ <div
448
+ v-for="color in colorOptions"
449
+ :key="color.value"
450
+ :class="['color-option', { 'is-active': appStore.primaryColor === color.value }]"
451
+ :style="{ backgroundColor: color.value }"
452
+ :title="color.label"
453
+ @click="handleColorChange(color.value)"
454
+ >
455
+ <Icon v-if="appStore.primaryColor === color.value" name="check" :size="12" color="#fff" />
456
+ </div>
457
+ </div>
458
+ </div>
459
+
460
+ <!-- 功能开关 -->
461
+ <div class="settings-section">
462
+ <div class="settings-title">功能设置</div>
463
+ <div class="settings-switch-list">
464
+ <div class="settings-switch-item">
465
+ <span>灰色模式</span>
466
+ <div class="switch-wrapper" :class="{ 'is-checked': greyMode }" @click="handleGreyModeToggle">
467
+ <span class="switch-core"></span>
468
+ </div>
469
+ </div>
470
+ <div class="settings-switch-item">
471
+ <span>暗黑模式</span>
472
+ <div class="switch-wrapper" :class="{ 'is-checked': appStore.isDark }" @click="handleDarkModeToggle">
473
+ <span class="switch-core"></span>
474
+ </div>
475
+ </div>
476
+ </div>
477
+ </div>
478
+ </div>
479
+ </Drawer>
480
+ </div>
481
+ </template>
482
+
483
+ <style lang="scss" scoped>
484
+ .mix-top-menu {
485
+ width: 100%;
486
+ height: 100%;
487
+ display: flex;
488
+ align-items: center;
489
+ padding: 0 20px;
490
+
491
+ &__logo {
492
+ display: flex;
493
+ align-items: center;
494
+ gap: 10px;
495
+ margin-right: 20px;
496
+ }
497
+
498
+ &__logo-img {
499
+ width: 32px;
500
+ height: 32px;
501
+ }
502
+
503
+ &__logo-text {
504
+ font-size: 16px;
505
+ font-weight: 600;
506
+ color: var(--color-primary);
507
+ }
508
+
509
+ &__menu {
510
+ flex: 1;
511
+ border-bottom: none;
512
+ height: 100%;
513
+ }
514
+
515
+ &__menu-content {
516
+ display: inline-flex;
517
+ align-items: center;
518
+ line-height: 1;
519
+ }
520
+
521
+ &__menu-text {
522
+ flex: 1;
523
+ line-height: 1;
524
+ }
525
+
526
+ &__menu-icon {
527
+ display: inline-flex;
528
+ align-items: center;
529
+ justify-content: center;
530
+ width: 16px;
531
+ height: 16px;
532
+ margin-right: 8px;
533
+ flex-shrink: 0;
534
+ }
535
+
536
+ &__menu-char {
537
+ display: inline-flex;
538
+ align-items: center;
539
+ justify-content: center;
540
+ width: 16px;
541
+ height: 16px;
542
+ font-size: 12px;
543
+ font-weight: 600;
544
+ color: var(--color-primary);
545
+ background-color: var(--color-primary-light-8);
546
+ border-radius: 4px;
547
+ }
548
+
549
+ // 右侧操作区域
550
+ &__actions {
551
+ display: flex;
552
+ align-items: center;
553
+ gap: 8px;
554
+ }
555
+
556
+ &__action {
557
+ width: 32px;
558
+ height: 32px;
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ cursor: pointer;
563
+ border-radius: var(--border-radius-base);
564
+ color: var(--color-text-regular);
565
+ transition: all 0.2s;
566
+
567
+ &:hover {
568
+ background-color: var(--color-fill);
569
+ color: var(--color-primary);
570
+ }
571
+ }
572
+
573
+ // 用户头像
574
+ &__user {
575
+ position: relative;
576
+ margin-left: 8px;
577
+
578
+ &-trigger {
579
+ display: flex;
580
+ align-items: center;
581
+ gap: 8px;
582
+ cursor: pointer;
583
+ padding: 4px 8px;
584
+ border-radius: var(--border-radius-base);
585
+ transition: background-color 0.2s;
586
+
587
+ &:hover {
588
+ background-color: var(--color-fill);
589
+ }
590
+ }
591
+
592
+ &-name {
593
+ font-size: 14px;
594
+ color: var(--color-text-primary);
595
+ max-width: 100px;
596
+ overflow: hidden;
597
+ text-overflow: ellipsis;
598
+ white-space: nowrap;
599
+ }
600
+
601
+ &-arrow {
602
+ font-size: 10px;
603
+ color: var(--color-text-secondary);
604
+ transition: transform 0.2s;
605
+
606
+ &.is-active {
607
+ transform: rotate(180deg);
608
+ }
609
+ }
610
+ }
611
+
612
+ &__avatar {
613
+ width: 32px;
614
+ height: 32px;
615
+ border-radius: 50%;
616
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
617
+ display: flex;
618
+ align-items: center;
619
+ justify-content: center;
620
+ color: #fff;
621
+ font-size: 14px;
622
+ font-weight: 500;
623
+ }
624
+
625
+ // 下拉菜单
626
+ &__dropdown {
627
+ position: absolute;
628
+ top: calc(100% + 8px);
629
+ right: 0;
630
+ min-width: 200px;
631
+ background-color: var(--bg-color);
632
+ border-radius: var(--border-radius-base);
633
+ box-shadow: var(--box-shadow);
634
+ overflow: hidden;
635
+ z-index: 100;
636
+
637
+ &-header {
638
+ display: flex;
639
+ align-items: center;
640
+ gap: 12px;
641
+ padding: 16px;
642
+ }
643
+
644
+ &-avatar {
645
+ width: 40px;
646
+ height: 40px;
647
+ border-radius: 50%;
648
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light-3));
649
+ display: flex;
650
+ align-items: center;
651
+ justify-content: center;
652
+ color: #fff;
653
+ font-size: 16px;
654
+ font-weight: 500;
655
+ }
656
+
657
+ &-info {
658
+ flex: 1;
659
+ }
660
+
661
+ &-name {
662
+ font-size: 14px;
663
+ font-weight: 500;
664
+ color: var(--color-text-primary);
665
+ }
666
+
667
+ &-role {
668
+ font-size: 12px;
669
+ color: var(--color-text-secondary);
670
+ margin-top: 2px;
671
+ }
672
+
673
+ &-divider {
674
+ height: 1px;
675
+ background-color: var(--color-border-lighter);
676
+ }
677
+
678
+ &-menu {
679
+ padding: 8px 0;
680
+ }
681
+
682
+ &-item {
683
+ display: flex;
684
+ align-items: center;
685
+ gap: 10px;
686
+ padding: 10px 16px;
687
+ cursor: pointer;
688
+ font-size: 14px;
689
+ color: var(--color-text-regular);
690
+ transition: all 0.2s;
691
+
692
+ &:hover {
693
+ background-color: var(--color-fill);
694
+ color: var(--color-text-primary);
695
+ }
696
+
697
+ &--danger {
698
+ color: var(--color-danger);
699
+
700
+ &:hover {
701
+ background-color: var(--color-danger-light);
702
+ color: var(--color-danger);
703
+ }
704
+ }
705
+ }
706
+ }
707
+ }
708
+
709
+ // 设置抽屉内容
710
+ .settings-drawer {
711
+ .settings-section {
712
+ margin-bottom: 24px;
713
+ }
714
+
715
+ .settings-title {
716
+ font-size: 14px;
717
+ font-weight: 500;
718
+ color: var(--color-text-primary);
719
+ margin-bottom: 12px;
720
+ }
721
+
722
+ .settings-layout-options {
723
+ display: flex;
724
+ gap: 12px;
725
+ }
726
+
727
+ .layout-option {
728
+ flex: 1;
729
+ display: flex;
730
+ flex-direction: column;
731
+ align-items: center;
732
+ gap: 8px;
733
+ padding: 12px;
734
+ border: 1px solid var(--color-border);
735
+ border-radius: var(--border-radius-base);
736
+ cursor: pointer;
737
+ transition: all 0.2s;
738
+
739
+ &:hover {
740
+ border-color: var(--color-primary-light-5);
741
+ }
742
+
743
+ &.is-active {
744
+ border-color: var(--color-primary);
745
+ background-color: var(--color-primary-light-9);
746
+ }
747
+
748
+ &__preview {
749
+ width: 48px;
750
+ height: 36px;
751
+ border: 1px solid var(--color-border-light);
752
+ border-radius: 2px;
753
+ overflow: hidden;
754
+ }
755
+
756
+ &__label {
757
+ font-size: 12px;
758
+ color: var(--color-text-regular);
759
+ }
760
+ }
761
+
762
+ // 布局预览样式
763
+ .layout-preview-sidebar {
764
+ display: flex;
765
+ height: 100%;
766
+
767
+ .preview-aside {
768
+ width: 25%;
769
+ height: 100%;
770
+ background-color: var(--color-primary-light-7);
771
+ }
772
+
773
+ .preview-main {
774
+ flex: 1;
775
+ display: flex;
776
+ flex-direction: column;
777
+
778
+ .preview-header {
779
+ height: 20%;
780
+ background-color: var(--color-border-light);
781
+ }
782
+
783
+ .preview-content {
784
+ flex: 1;
785
+ background-color: var(--bg-color-page);
786
+ }
787
+ }
788
+ }
789
+
790
+ .layout-preview-top {
791
+ display: flex;
792
+ flex-direction: column;
793
+ height: 100%;
794
+
795
+ .preview-header-full {
796
+ height: 25%;
797
+ background-color: var(--color-primary-light-7);
798
+ }
799
+
800
+ .preview-content-full {
801
+ flex: 1;
802
+ background-color: var(--bg-color-page);
803
+ }
804
+ }
805
+
806
+ .layout-preview-mix {
807
+ display: flex;
808
+ flex-direction: column;
809
+ height: 100%;
810
+
811
+ .preview-header-mix {
812
+ height: 25%;
813
+ background-color: var(--color-primary-light-7);
814
+ display: flex;
815
+
816
+ .preview-mix-left {
817
+ width: 30%;
818
+ background-color: var(--color-primary);
819
+ }
820
+ }
821
+
822
+ .preview-mix-body {
823
+ flex: 1;
824
+ display: flex;
825
+
826
+ .preview-mix-aside {
827
+ width: 25%;
828
+ background-color: var(--color-primary-light-8);
829
+ }
830
+
831
+ .preview-mix-content {
832
+ flex: 1;
833
+ background-color: var(--bg-color-page);
834
+ }
835
+ }
836
+ }
837
+
838
+ .settings-color-options {
839
+ display: flex;
840
+ gap: 12px;
841
+ }
842
+
843
+ .color-option {
844
+ width: 24px;
845
+ height: 24px;
846
+ border-radius: 4px;
847
+ cursor: pointer;
848
+ display: flex;
849
+ align-items: center;
850
+ justify-content: center;
851
+ transition: transform 0.2s;
852
+
853
+ &:hover {
854
+ transform: scale(1.1);
855
+ }
856
+
857
+ &.is-active {
858
+ box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--color-primary);
859
+ }
860
+ }
861
+
862
+ .settings-switch-list {
863
+ display: flex;
864
+ flex-direction: column;
865
+ gap: 12px;
866
+ }
867
+
868
+ .settings-switch-item {
869
+ display: flex;
870
+ align-items: center;
871
+ justify-content: space-between;
872
+
873
+ span {
874
+ font-size: 14px;
875
+ color: var(--color-text-regular);
876
+ }
877
+ }
878
+
879
+ .switch-wrapper {
880
+ width: 44px;
881
+ height: 22px;
882
+ display: flex;
883
+ align-items: center;
884
+ cursor: pointer;
885
+
886
+ .switch-core {
887
+ width: 100%;
888
+ height: 100%;
889
+ border-radius: 11px;
890
+ background-color: var(--color-border);
891
+ position: relative;
892
+ transition: background-color 0.2s;
893
+
894
+ &::after {
895
+ content: '';
896
+ position: absolute;
897
+ top: 2px;
898
+ left: 2px;
899
+ width: 18px;
900
+ height: 18px;
901
+ background-color: #fff;
902
+ border-radius: 50%;
903
+ transition: left 0.2s;
904
+ }
905
+ }
906
+
907
+ &.is-checked {
908
+ .switch-core {
909
+ background-color: var(--color-primary);
910
+
911
+ &::after {
912
+ left: 24px;
913
+ }
914
+ }
915
+ }
916
+ }
917
+ }
918
+
919
+ // 下拉动画
920
+ .dropdown-enter-active,
921
+ .dropdown-leave-active {
922
+ transition: all 0.2s ease;
923
+ }
924
+
925
+ .dropdown-enter-from,
926
+ .dropdown-leave-to {
927
+ opacity: 0;
928
+ transform: translateY(-10px);
929
+ }
930
+ </style>