xto-fronted 0.3.3 → 0.3.5

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.
@@ -5,6 +5,10 @@ import { useAppStore } from '@/stores/app'
5
5
  import { useUserStore } from '@/stores/user'
6
6
  import { useAuthStore } from '@/stores/auth'
7
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'
8
12
 
9
13
  const route = useRoute()
10
14
  const router = useRouter()
@@ -13,10 +17,33 @@ const userStore = useUserStore()
13
17
  const authStore = useAuthStore()
14
18
  const menuStore = useMenuStore()
15
19
 
16
- const isCollapsed = computed(() => appStore.isCollapsed)
17
20
  const dropdownVisible = ref(false)
21
+ const drawerVisible = ref(false)
18
22
  const dropdownRef = ref<HTMLElement | null>(null)
19
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
+ ]
20
47
 
21
48
  // 面包屑
22
49
  const breadcrumbs = computed(() => {
@@ -27,6 +54,31 @@ const breadcrumbs = computed(() => {
27
54
  }))
28
55
  })
29
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
+
30
82
  // 切换折叠
31
83
  const toggleCollapse = () => {
32
84
  appStore.toggleCollapse()
@@ -37,6 +89,27 @@ const toggleTheme = () => {
37
89
  appStore.toggleTheme()
38
90
  }
39
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
+
40
113
  // 切换全屏
41
114
  const toggleFullscreen = () => {
42
115
  if (!document.fullscreenElement) {
@@ -57,27 +130,47 @@ const toggleDropdown = () => {
57
130
  }
58
131
 
59
132
  // 关闭下拉菜单
60
- const closeDropdown = () => {
133
+ const closeDropdowns = () => {
61
134
  dropdownVisible.value = false
62
135
  }
63
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
+
64
159
  // 个人信息
65
160
  const handleProfile = () => {
66
- closeDropdown()
67
- // TODO: 跳转到个人信息页面
68
- alert('个人信息功能开发中...')
161
+ closeDropdowns()
162
+ router.push('/profile')
69
163
  }
70
164
 
71
165
  // 修改密码
72
166
  const handleChangePassword = () => {
73
- closeDropdown()
74
- // TODO: 打开修改密码弹窗
75
- alert('修改密码功能开发中...')
167
+ closeDropdowns()
168
+ router.push('/change-password')
76
169
  }
77
170
 
78
171
  // 退出登录
79
172
  const handleLogout = () => {
80
- closeDropdown()
173
+ closeDropdowns()
81
174
  authStore.logout()
82
175
  userStore.clearUserInfo()
83
176
  menuStore.clearMenu()
@@ -87,18 +180,39 @@ const handleLogout = () => {
87
180
  // 点击外部关闭下拉菜单
88
181
  const handleClickOutside = (event: MouseEvent) => {
89
182
  if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
90
- closeDropdown()
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()
91
200
  }
92
201
  }
93
202
 
94
203
  onMounted(() => {
95
204
  document.addEventListener('click', handleClickOutside)
96
205
  document.addEventListener('fullscreenchange', handleFullscreenChange)
206
+ document.addEventListener('keydown', handleKeydown)
207
+ appStore.initTheme()
208
+ // 初始化灰色模式状态
209
+ greyMode.value = document.documentElement.classList.contains('grey-mode')
97
210
  })
98
211
 
99
212
  onUnmounted(() => {
100
213
  document.removeEventListener('click', handleClickOutside)
101
214
  document.removeEventListener('fullscreenchange', handleFullscreenChange)
215
+ document.removeEventListener('keydown', handleKeydown)
102
216
  })
103
217
  </script>
104
218
 
@@ -106,13 +220,15 @@ onUnmounted(() => {
106
220
  <div class="header">
107
221
  <!-- 左侧 -->
108
222
  <div class="header__left">
223
+ <!-- 折叠按钮 -->
109
224
  <div class="header__collapse" @click="toggleCollapse">
110
- <span v-if="isCollapsed">☰</span>
111
- <span v-else>☰</span>
225
+ <Icon :name="appStore.isCollapsed ? 'menu-unfold' : 'menu-fold'" :size="18" />
112
226
  </div>
113
- <div class="header__breadcrumb">
227
+
228
+ <!-- 面包屑 -->
229
+ <div v-if="appStore.showBreadcrumb" class="header__breadcrumb">
114
230
  <span v-for="(item, index) in breadcrumbs" :key="item.path">
115
- <span v-if="index > 0"> / </span>
231
+ <span v-if="index > 0" class="breadcrumb-separator">/</span>
116
232
  <span :class="{ 'is-current': index === breadcrumbs.length - 1 }">
117
233
  {{ item.title }}
118
234
  </span>
@@ -122,20 +238,24 @@ onUnmounted(() => {
122
238
 
123
239
  <!-- 右侧 -->
124
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
+
125
246
  <!-- 全屏切换 -->
126
- <div class="header__action" @click="toggleFullscreen" :title="isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'">
127
- <svg v-if="isFullscreen" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
128
- <path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
129
- </svg>
130
- <svg v-else viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
131
- <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
132
- </svg>
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" />
133
254
  </div>
134
255
 
135
256
  <!-- 主题切换 -->
136
257
  <div class="header__action" @click="toggleTheme" title="切换主题">
137
- <span v-if="appStore.isDark">🌙</span>
138
- <span v-else>☀️</span>
258
+ <Icon :name="appStore.isDark ? 'sun' : 'moon'" :size="16" />
139
259
  </div>
140
260
 
141
261
  <!-- 用户头像 -->
@@ -157,22 +277,22 @@ onUnmounted(() => {
157
277
  </div>
158
278
  <div class="header__dropdown-info">
159
279
  <div class="header__dropdown-name">{{ userStore.nickname }}</div>
160
- <div class="header__dropdown-role">{{ userStore.roles.join(', ') }}</div>
280
+ <div class="header__dropdown-role">{{ userStore.roles?.join(', ') }}</div>
161
281
  </div>
162
282
  </div>
163
283
  <div class="header__dropdown-divider"></div>
164
284
  <div class="header__dropdown-menu">
165
285
  <div class="header__dropdown-item" @click="handleProfile">
166
- <span class="header__dropdown-icon">👤</span>
286
+ <Icon name="user" :size="16" />
167
287
  <span>个人信息</span>
168
288
  </div>
169
289
  <div class="header__dropdown-item" @click="handleChangePassword">
170
- <span class="header__dropdown-icon">🔐</span>
290
+ <Icon name="lock" :size="16" />
171
291
  <span>修改密码</span>
172
292
  </div>
173
293
  <div class="header__dropdown-divider"></div>
174
294
  <div class="header__dropdown-item header__dropdown-item--danger" @click="handleLogout">
175
- <span class="header__dropdown-icon">🚪</span>
295
+ <Icon name="logout" :size="16" />
176
296
  <span>退出登录</span>
177
297
  </div>
178
298
  </div>
@@ -180,6 +300,126 @@ onUnmounted(() => {
180
300
  </Transition>
181
301
  </div>
182
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>
183
423
  </div>
184
424
  </template>
185
425
 
@@ -190,6 +430,8 @@ onUnmounted(() => {
190
430
  justify-content: space-between;
191
431
  padding: 0 20px;
192
432
  height: 100%;
433
+ background-color: var(--bg-color);
434
+ border-bottom: 1px solid var(--color-border-lighter);
193
435
 
194
436
  &__left {
195
437
  display: flex;
@@ -204,8 +446,8 @@ onUnmounted(() => {
204
446
  align-items: center;
205
447
  justify-content: center;
206
448
  cursor: pointer;
207
- font-size: 18px;
208
449
  color: var(--color-text-regular);
450
+ transition: color 0.2s;
209
451
 
210
452
  &:hover {
211
453
  color: var(--color-primary);
@@ -216,6 +458,11 @@ onUnmounted(() => {
216
458
  font-size: 14px;
217
459
  color: var(--color-text-secondary);
218
460
 
461
+ .breadcrumb-separator {
462
+ margin: 0 8px;
463
+ color: var(--color-text-placeholder);
464
+ }
465
+
219
466
  .is-current {
220
467
  color: var(--color-text-primary);
221
468
  font-weight: 500;
@@ -225,7 +472,7 @@ onUnmounted(() => {
225
472
  &__right {
226
473
  display: flex;
227
474
  align-items: center;
228
- gap: 15px;
475
+ gap: 8px;
229
476
  }
230
477
 
231
478
  &__action {
@@ -236,15 +483,18 @@ onUnmounted(() => {
236
483
  justify-content: center;
237
484
  cursor: pointer;
238
485
  border-radius: var(--border-radius-base);
239
- font-size: 16px;
486
+ color: var(--color-text-regular);
487
+ transition: all 0.2s;
240
488
 
241
489
  &:hover {
242
490
  background-color: var(--color-fill);
491
+ color: var(--color-primary);
243
492
  }
244
493
  }
245
494
 
246
495
  &__user {
247
496
  position: relative;
497
+ margin-left: 8px;
248
498
 
249
499
  &-trigger {
250
500
  display: flex;
@@ -263,6 +513,10 @@ onUnmounted(() => {
263
513
  &-name {
264
514
  font-size: 14px;
265
515
  color: var(--color-text-primary);
516
+ max-width: 100px;
517
+ overflow: hidden;
518
+ text-overflow: ellipsis;
519
+ white-space: nowrap;
266
520
  }
267
521
 
268
522
  &-arrow {
@@ -369,9 +623,323 @@ onUnmounted(() => {
369
623
  }
370
624
  }
371
625
  }
626
+ }
372
627
 
373
- &-icon {
374
- font-size: 16px;
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
+ }
375
943
  }
376
944
  }
377
945
  }
@@ -387,4 +955,20 @@ onUnmounted(() => {
387
955
  opacity: 0;
388
956
  transform: translateY(-10px);
389
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
+ }
390
974
  </style>