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