xto-fronted 0.4.55 → 0.4.56

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.
@@ -18,6 +18,28 @@ const authStore = useAuthStore()
18
18
 
19
19
  const activeMenu = computed(() => route.path)
20
20
 
21
+ // 扁平化菜单用于搜索
22
+ const flattenMenus = (menus: any[], parentTitle = ''): any[] => {
23
+ const result: any[] = []
24
+ menus.forEach(menu => {
25
+ if (menu.children && menu.children.length > 0) {
26
+ result.push(...flattenMenus(menu.children, menu.menuName))
27
+ } else {
28
+ result.push({ ...menu, parentTitle, title: menu.menuName, path: menu.menuUrl })
29
+ }
30
+ })
31
+ return result
32
+ }
33
+
34
+ // 搜索结果
35
+ const searchResults = computed(() => {
36
+ if (!searchKeyword.value.trim()) return []
37
+ const flatMenus = flattenMenus(menuStore.menuList)
38
+ return flatMenus.filter(menu =>
39
+ menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
40
+ )
41
+ })
42
+
21
43
  // 菜单主题相关
22
44
  const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
23
45
  const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
@@ -29,6 +51,9 @@ const dropdownRef = ref<HTMLElement | null>(null)
29
51
  const isFullscreen = ref(false)
30
52
  const drawerVisible = ref(false)
31
53
  const greyMode = ref(false)
54
+ const searchVisible = ref(false)
55
+ const searchKeyword = ref('')
56
+ const searchRef = ref<HTMLElement | null>(null)
32
57
 
33
58
  // 菜单选择
34
59
  const handleMenuSelect = (index: string) => {
@@ -142,6 +167,18 @@ const closeDropdowns = () => {
142
167
  dropdownVisible.value = false
143
168
  }
144
169
 
170
+ // 隐藏搜索
171
+ const hideSearch = () => {
172
+ searchVisible.value = false
173
+ searchKeyword.value = ''
174
+ }
175
+
176
+ // 搜索结果点击
177
+ const handleSearchItemClick = (path: string) => {
178
+ router.push(path)
179
+ hideSearch()
180
+ }
181
+
145
182
  // 打开设置抽屉
146
183
  const openSettingsDrawer = () => {
147
184
  drawerVisible.value = true
@@ -228,17 +265,30 @@ const handleClickOutside = (event: MouseEvent) => {
228
265
  if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
229
266
  closeDropdowns()
230
267
  }
268
+ if (searchRef.value && !searchRef.value.contains(event.target as Node)) {
269
+ hideSearch()
270
+ }
271
+ }
272
+
273
+ // 键盘快捷键
274
+ const handleKeydown = (event: KeyboardEvent) => {
275
+ if (event.key === 'Escape') {
276
+ hideSearch()
277
+ closeDropdowns()
278
+ }
231
279
  }
232
280
 
233
281
  onMounted(() => {
234
282
  document.addEventListener('click', handleClickOutside)
235
283
  document.addEventListener('fullscreenchange', handleFullscreenChange)
284
+ document.addEventListener('keydown', handleKeydown)
236
285
  greyMode.value = document.documentElement.classList.contains('grey-mode')
237
286
  })
238
287
 
239
288
  onUnmounted(() => {
240
289
  document.removeEventListener('click', handleClickOutside)
241
290
  document.removeEventListener('fullscreenchange', handleFullscreenChange)
291
+ document.removeEventListener('keydown', handleKeydown)
242
292
  })
243
293
  </script>
244
294
 
@@ -301,6 +351,41 @@ onUnmounted(() => {
301
351
 
302
352
  <!-- 右侧操作区域 -->
303
353
  <div class="top-menu__actions">
354
+ <!-- 搜索输入框 -->
355
+ <div class="top-menu__search" ref="searchRef">
356
+ <Icon name="search" :size="14" class="top-menu__search-icon" />
357
+ <input
358
+ v-model="searchKeyword"
359
+ type="text"
360
+ class="top-menu__search-input"
361
+ placeholder="搜索菜单..."
362
+ @focus="searchVisible = true"
363
+ />
364
+ <!-- 搜索结果下拉 -->
365
+ <Transition name="search-dropdown">
366
+ <div v-if="searchVisible && (searchResults.length > 0 || searchKeyword)" class="top-menu__search-dropdown">
367
+ <div v-if="searchResults.length > 0" class="top-menu__search-results">
368
+ <div
369
+ v-for="item in searchResults"
370
+ :key="item.path"
371
+ class="top-menu__search-item"
372
+ @click="handleSearchItemClick(item.path)"
373
+ >
374
+ <span v-if="item.title !== '首页'" class="top-menu__search-icon-item">
375
+ <Icon v-if="iconExists(getMenuIcon(item.icon))" :name="getMenuIcon(item.icon)" :size="16" />
376
+ <span v-else class="top-menu__search-char">{{ getFirstChar(item.title) }}</span>
377
+ </span>
378
+ <span class="top-menu__search-item-title">{{ item.title }}</span>
379
+ <span v-if="item.parentTitle" class="top-menu__search-item-parent">{{ item.parentTitle }}</span>
380
+ </div>
381
+ </div>
382
+ <div v-else class="top-menu__search-empty">
383
+ 未找到匹配的菜单
384
+ </div>
385
+ </div>
386
+ </Transition>
387
+ </div>
388
+
304
389
  <!-- 全屏切换 -->
305
390
  <div class="top-menu__action" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
306
391
  <Icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" :size="16" />
@@ -571,6 +656,107 @@ onUnmounted(() => {
571
656
  gap: 8px;
572
657
  }
573
658
 
659
+ &__search {
660
+ position: relative;
661
+ display: flex;
662
+ align-items: center;
663
+ background-color: var(--color-fill-light);
664
+ border-radius: var(--border-radius-base);
665
+ padding: 0 12px;
666
+ height: 32px;
667
+ width: 200px;
668
+
669
+ &-icon {
670
+ color: var(--color-text-secondary);
671
+ }
672
+
673
+ &-input {
674
+ flex: 1;
675
+ height: 100%;
676
+ font-size: 14px;
677
+ color: var(--color-text-primary);
678
+ background: transparent;
679
+ border: none;
680
+ outline: none;
681
+ padding-left: 8px;
682
+
683
+ &::placeholder {
684
+ color: var(--color-text-placeholder);
685
+ }
686
+ }
687
+
688
+ &-dropdown {
689
+ position: absolute;
690
+ top: calc(100% + 4px);
691
+ left: 0;
692
+ right: 0;
693
+ min-width: 200px;
694
+ max-height: 300px;
695
+ overflow-y: auto;
696
+ background-color: var(--bg-color);
697
+ border-radius: var(--border-radius-base);
698
+ box-shadow: var(--box-shadow);
699
+ z-index: 100;
700
+ }
701
+
702
+ &-results {
703
+ padding: 8px 0;
704
+ }
705
+
706
+ &-item {
707
+ display: flex;
708
+ align-items: center;
709
+ gap: 8px;
710
+ padding: 8px 12px;
711
+ cursor: pointer;
712
+ transition: background-color 0.2s;
713
+
714
+ &:hover {
715
+ background-color: var(--color-fill);
716
+ }
717
+
718
+ &-title {
719
+ font-size: 14px;
720
+ color: var(--color-text-primary);
721
+ }
722
+
723
+ &-parent {
724
+ font-size: 12px;
725
+ color: var(--color-text-secondary);
726
+ margin-left: auto;
727
+ }
728
+ }
729
+
730
+ &-icon-item {
731
+ display: inline-flex;
732
+ align-items: center;
733
+ justify-content: center;
734
+ width: 16px;
735
+ height: 16px;
736
+ flex-shrink: 0;
737
+ }
738
+
739
+ &-char {
740
+ display: inline-flex;
741
+ align-items: center;
742
+ justify-content: center;
743
+ width: 16px;
744
+ height: 16px;
745
+ font-size: 12px;
746
+ font-weight: 600;
747
+ color: var(--color-primary);
748
+ background-color: var(--color-primary-light-8);
749
+ border-radius: 4px;
750
+ }
751
+
752
+ &-empty {
753
+ padding: 16px 12px;
754
+ text-align: center;
755
+ color: var(--color-text-secondary);
756
+ font-size: 14px;
757
+ }
758
+ }
759
+
574
760
  &__action {
575
761
  width: 32px;
576
762
  height: 32px;
@@ -945,4 +1131,16 @@ onUnmounted(() => {
945
1131
  opacity: 0;
946
1132
  transform: translateY(-10px);
947
1133
  }
1134
+
1135
+ // 搜索下拉动画
1136
+ .search-dropdown-enter-active,
1137
+ .search-dropdown-leave-active {
1138
+ transition: all 0.2s ease;
1139
+ }
1140
+
1141
+ .search-dropdown-enter-from,
1142
+ .search-dropdown-leave-to {
1143
+ opacity: 0;
1144
+ transform: translateY(-4px);
1145
+ }
948
1146
  </style>