| | |
| | | import { type Menu } from '/@/router/types'; |
| | | import { type AnyFunction } from '@vben/types'; |
| | | import { ref, onBeforeMount, unref, Ref, nextTick } from 'vue'; |
| | | import { getMenus } from '/@/router/menus'; |
| | | import { cloneDeep } from 'lodash-es'; |
| | | import { filter, forEach } from '/@/utils/helper/treeHelper'; |
| | | import { useGo } from '/@/hooks/web/usePage'; |
| | | import { useI18n } from '@/hooks/web/useI18n'; |
| | | import { useGo } from '@/hooks/web/usePage'; |
| | | import { getMenus } from '@/router/menus'; |
| | | import { type Menu } from '@/router/types'; |
| | | import { filter, forEach } from '@/utils/helper/treeHelper'; |
| | | import { useScrollTo } from '@vben/hooks'; |
| | | import { type AnyFunction } from '@vben/types'; |
| | | import { onKeyStroke, useDebounceFn } from '@vueuse/core'; |
| | | import { useI18n } from '/@/hooks/web/useI18n'; |
| | | import { cloneDeep } from 'lodash-es'; |
| | | import { Ref, nextTick, onBeforeMount, ref, unref } from 'vue'; |
| | | |
| | | export interface SearchResult { |
| | | name: string; |
| | | path: string; |
| | | icon?: string; |
| | | // 搜索结果包含的字符着色 |
| | | chars: { char: string; highlight: boolean }[]; |
| | | } |
| | | |
| | | // Translate special characters |
| | |
| | | const list = await getMenus(); |
| | | menuList = cloneDeep(list); |
| | | forEach(menuList, (item) => { |
| | | item.name = t(item.name); |
| | | item.name = t(item.meta?.title || item.name); |
| | | }); |
| | | }); |
| | | |
| | | function search(e: ChangeEvent) { |
| | | e?.stopPropagation(); |
| | | const key = e.target.value; |
| | | keyword.value = key.trim(); |
| | | keyword.value = key.trim().toLowerCase(); |
| | | if (!key) { |
| | | searchResult.value = []; |
| | | return; |
| | | } |
| | | const reg = createSearchReg(unref(keyword)); |
| | | const filterMenu = filter(menuList, (item) => { |
| | | return reg.test(item.name) && !item.hideMenu; |
| | | return reg.test(item.name?.toLowerCase()) && !item.hideMenu; |
| | | }); |
| | | searchResult.value = handlerSearchResult(filterMenu, reg); |
| | | activeIndex.value = 0; |
| | |
| | | const ret: SearchResult[] = []; |
| | | filterMenu.forEach((item) => { |
| | | const { name, path, icon, children, hideMenu, meta } = item; |
| | | if (!hideMenu && reg.test(name) && (!children?.length || meta?.hideChildrenInMenu)) { |
| | | if ( |
| | | !hideMenu && |
| | | reg.test(name?.toLowerCase() ?? '') && |
| | | (!children?.length || meta?.hideChildrenInMenu) |
| | | ) { |
| | | const chars: { char: string; highlight: boolean }[] = []; |
| | | |
| | | // 显示字符串 |
| | | const label = (parent?.name ? `${parent.name} > ${name}` : name) ?? ''; |
| | | const labelChars = label.split(''); |
| | | let labelPointer = 0; |
| | | |
| | | const keywordChars = keyword.value.split(''); |
| | | const keywordLength = keywordChars.length; |
| | | let keywordPointer = 0; |
| | | |
| | | // 用于查找完整关键词的匹配 |
| | | let includePointer = 0; |
| | | |
| | | // 优先查找完整关键词的匹配 |
| | | if (label.toLowerCase().includes(keyword.value.toLowerCase())) { |
| | | while (includePointer < labelChars.length) { |
| | | if ( |
| | | label.toLowerCase().slice(includePointer, includePointer + keywordLength) === |
| | | keyword.value.toLowerCase() |
| | | ) { |
| | | chars.push( |
| | | ...label |
| | | .substring(labelPointer, includePointer) |
| | | .split('') |
| | | .map((v) => ({ |
| | | char: v, |
| | | highlight: false, |
| | | })), |
| | | ); |
| | | chars.push( |
| | | ...label |
| | | .slice(includePointer, includePointer + keywordLength) |
| | | .split('') |
| | | .map((v) => ({ |
| | | char: v, |
| | | highlight: true, |
| | | })), |
| | | ); |
| | | includePointer += keywordLength; |
| | | labelPointer = includePointer; |
| | | } else { |
| | | includePointer++; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 查找满足关键词顺序的匹配 |
| | | while (labelPointer < labelChars.length) { |
| | | keywordPointer = 0; |
| | | while (keywordPointer < keywordChars.length) { |
| | | if (keywordChars[keywordPointer] !== void 0 && labelChars[labelPointer] !== void 0) { |
| | | if ( |
| | | keywordChars[keywordPointer].toLowerCase() === |
| | | labelChars[labelPointer].toLowerCase() |
| | | ) { |
| | | chars.push({ |
| | | char: labelChars[labelPointer], |
| | | highlight: true, |
| | | }); |
| | | keywordPointer++; |
| | | } else { |
| | | chars.push({ |
| | | char: labelChars[labelPointer], |
| | | highlight: false, |
| | | }); |
| | | } |
| | | } else { |
| | | keywordPointer++; |
| | | } |
| | | labelPointer++; |
| | | } |
| | | } |
| | | ret.push({ |
| | | name: parent?.name ? `${parent.name} > ${name}` : name, |
| | | name: label, |
| | | chars, |
| | | path, |
| | | icon, |
| | | }); |
| | |
| | | ret.push(...handlerSearchResult(children, reg, item)); |
| | | } |
| | | }); |
| | | return ret; |
| | | |
| | | // 排序 |
| | | return ret.sort((a, b) => { |
| | | if ( |
| | | a.name.toLowerCase().includes(keyword.value.toLowerCase()) && |
| | | b.name.toLowerCase().includes(keyword.value.toLowerCase()) |
| | | ) { |
| | | // 两者都存在完整关键词的匹配 |
| | | |
| | | // 匹配数量 |
| | | const ca = |
| | | a.name.toLowerCase().match(new RegExp(keyword.value.toLowerCase(), 'g'))?.length ?? 0; |
| | | const cb = |
| | | b.name.toLowerCase().match(new RegExp(keyword.value.toLowerCase(), 'g'))?.length ?? 0; |
| | | |
| | | // 匹配数量越多的优先显示,数量相同的按字符串排序 |
| | | return ca === cb ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) : cb - ca; |
| | | } else { |
| | | if (a.name.toLowerCase().includes(keyword.value.toLowerCase())) { |
| | | // 完整关键词的匹配优先 |
| | | return -1; |
| | | } else if (b.name.toLowerCase().includes(keyword.value.toLowerCase())) { |
| | | // 完整关键词的匹配优先 |
| | | return 1; |
| | | } else { |
| | | // 按字符串排序 |
| | | return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // Activate when the mouse moves to a certain line |