diff --git a/src/components/HeaderSearch/index.vue b/src/components/HeaderSearch/index.vue index 6837a92..905b48b 100644 --- a/src/components/HeaderSearch/index.vue +++ b/src/components/HeaderSearch/index.vue @@ -5,6 +5,7 @@ v-model="show" width="600" @close="close" + @opened="onDialogOpened" :show-close="false" append-to-body > @@ -22,24 +23,55 @@ > +
+ 找到 {{ options.length }} 个结果 +
+
-
-
- -
-
- + +
@@ -58,13 +90,13 @@ interface SearchItem { query?: string } -const search = ref('') +const search = ref('') const options = ref([]) const searchPool = ref([]) -const activeIndex = ref(-1) -const show = ref(false) +const activeIndex = ref(-1) +const show = ref(false) const fuse = ref | undefined>(undefined) -const headerSearchSelectRef = ref(null) +const headerSearchSelectRef = ref(null) const router = useRouter() const theme = computed(() => useSettingsStore().theme) const routes = computed(() => usePermissionStore().defaultRoutes) @@ -72,36 +104,40 @@ const routes = computed(() => usePermissionStore().defaultRoutes) function click(): void { show.value = !show.value if (show.value) { - headerSearchSelectRef.value && headerSearchSelectRef.value.focus() options.value = searchPool.value } } +function onDialogOpened(): void { + nextTick(() => { + headerSearchSelectRef.value && headerSearchSelectRef.value.focus() + }) +} + function close(): void { headerSearchSelectRef.value && headerSearchSelectRef.value.blur() search.value = '' - options.value = [] + options.value = searchPool.value show.value = false activeIndex.value = -1 } function change(val: SearchItem): void { - const path = val.path + const p = val.path const query = val.query - if (isHttp(path)) { + if (isHttp(p)) { // http(s):// 路径新窗口打开 - const pindex = path.indexOf("http") - window.open(path.substr(pindex, path.length), "_blank") + const pindex = p.indexOf("http") + window.open(p.substr(pindex, p.length), "_blank") } else { if (query) { - router.push({ path: path, query: JSON.parse(query) }) + router.push({ path: p, query: JSON.parse(query) }) } else { - router.push(path) + router.push(p) } } - search.value = '' - options.value = [] + options.value = searchPool.value nextTick(() => { show.value = false }) @@ -110,19 +146,15 @@ function change(val: SearchItem): void { function initFuse(list: SearchItem[]): void { fuse.value = new Fuse(list, { shouldSort: true, - threshold: 0.4, + threshold: 0.2, minMatchCharLength: 1, keys: ['title', 'path'] }) } -// Filter out the routes that can be displayed in the sidebar -// And generate the internationalized title -function generateRoutes(routes :any, basePath = '', prefixTitle: string[] = []): SearchItem[] { +function generateRoutes(routes: any, basePath = '', prefixTitle: string[] = []): SearchItem[] { let res: SearchItem[] = [] - for (const r of routes) { - // skip hidden router if (r.hidden) { continue } const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path const data: SearchItem = { @@ -130,21 +162,16 @@ function generateRoutes(routes :any, basePath = '', prefixTitle: string[] = []): title: [...prefixTitle], icon: '' } - if (r.meta && r.meta.title) { data.title = [...data.title, r.meta.title as string] data.icon = (r.meta.icon as string) || '' if (r.redirect !== "noRedirect") { - // only push the routes with title - // special case: need to exclude parent router without redirect res.push(data) } } if (r.query) { data.query = r.query } - - // recursive child routes if (r.children) { const tempRoutes = generateRoutes(r.children, data.path, data.title) if (tempRoutes.length >= 1) { @@ -158,8 +185,18 @@ function generateRoutes(routes :any, basePath = '', prefixTitle: string[] = []): function querySearch(query: string): void { activeIndex.value = -1 if (query !== '') { - const results = fuse.value.search(query) as any[] - options.value = results.map((item: any) => item.item) ?? searchPool.value + const q = query.toLowerCase() + const pathMatches = searchPool.value.filter((item: SearchItem) => + item.path.toLowerCase().includes(q) + ) + const fuseMatches = (fuse.value?.search(query) ?? []).map((item: any) => item.item as SearchItem) + const merged: SearchItem[] = [...pathMatches] + fuseMatches.forEach((item: SearchItem) => { + if (!merged.find((m: SearchItem) => m.path === item.path)) { + merged.push(item) + } + }) + options.value = merged } else { options.value = searchPool.value } @@ -187,6 +224,18 @@ function selectActiveResult(): void { } } +function highlightText(text: string): string { + if (!text) return '' + if (!search.value) return text + const keyword = escapeRegExp(search.value) + const reg = new RegExp(`(${keyword})`, 'gi') + return text.replace(reg, '$1') +} + +function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + onMounted(() => { searchPool.value = generateRoutes(routes.value) }) @@ -197,6 +246,20 @@ watch(searchPool, (list: SearchItem[]) => { + +.search-footer { + display: flex; + align-items: center; + gap: 28px; + padding: 10px 20px; + border-top: 1px solid #f0f0f0; + color: #999; + font-size: 12px; + + .shortcut-item { + display: flex; + align-items: center; + gap: 5px; + } + + kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 5px; + border: 1px solid #ddd; + border-radius: 4px; + background: #f7f7f7; + color: #555; + font-size: 11px; + font-family: inherit; + line-height: 1; + box-shadow: 0 1px 0 #ccc; + } +} + \ No newline at end of file