Skip to content

Commit

Permalink
feat(projects): 增加全局搜索菜单功能
Browse files Browse the repository at this point in the history
  • Loading branch information
yanbowe committed Dec 29, 2021
1 parent 90ddf98 commit b9ce691
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/layouts/common/GlobalHeader/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<header-menu />
</div>
<div class="flex justify-end h-full">
<global-search />
<github-site />
<full-screen />
<theme-mode />
Expand All @@ -34,6 +35,7 @@ import {
GithubSite
} from './components';
import GlobalLogo from '../GlobalLogo/index.vue';
import GlobalSearch from '../GlobalSearch/index.vue';
interface Props {
/** 显示logo */
Expand Down
24 changes: 24 additions & 0 deletions src/layouts/common/GlobalSearch/components/SearchFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<template>
<div class="px-24px h-44px flex-y-center">
<span class="mr-14px">
<icon-ant-design:enter-outlined class="icon text-20px p-2px mr-3px" />
确认
</span>
<span class="mr-14px">
<icon-mdi:arrow-up-thin class="icon text-20px p-2px mr-5px" />
<icon-mdi:arrow-down-thin class="icon text-20px p-2px mr-3px" />
切换
</span>
<span>
<icon-mdi:close class="icon text-20px p-2px mr-3px" />
关闭
</span>
</div>
</template>

<script lang="ts" setup></script>
<style lang="scss" scoped>
.icon {
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66;
}
</style>
127 changes: 127 additions & 0 deletions src/layouts/common/GlobalSearch/components/SearchModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<template>
<n-modal
v-model:show="show"
:segmented="{ footer: 'soft' }"
:closable="false"
preset="card"
footer-style="padding: 0; margin: 0"
class="w-630px fixed top-50px left-1/2 transform -translate-x-1/2"
>
<n-input ref="inputRef" v-model:value="keyword" clearable placeholder="请输入关键词搜索" @input="handleSearch">
<template #prefix>
<icon-uil:search class="text-15px text-[#c2c2c2]" />
</template>
</n-input>
<div class="mt-20px">
<n-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
<search-result v-else v-model:value="activePath" :options="resultOptions" @enter="handleEnter" />
</div>
<template #footer>
<search-footer />
</template>
</n-modal>
</template>

<script lang="ts" setup>
import { ref, shallowRef, computed, watch, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
import { NModal, NInput, NEmpty } from 'naive-ui';
import { useDebounceFn, onKeyStroke } from '@vueuse/core';
import { menusList } from '@/router';
import { isUrl } from '@/utils';
import SearchResult from './SearchResult.vue';
import SearchFooter from './SearchFooter.vue';
interface Props {
/** 弹窗显隐 */
value: boolean;
}
interface Emits {
(e: 'update:value', val: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>();
const router = useRouter();
const keyword = ref('');
const activePath = ref('');
const resultOptions = shallowRef<RouteRecordRaw[]>([]);
const inputRef = ref<HTMLInputElement | null>(null);
const handleSearch = useDebounceFn(search, 300);
const show = computed({
get() {
return props.value;
},
set(val: boolean) {
emit('update:value', val);
}
});
watch(show, async val => {
if (val) {
/** 自动聚焦 */
await nextTick();
inputRef.value?.focus();
}
});
/** 查询 */
function search() {
resultOptions.value = menusList.filter(menu => keyword.value && menu.meta?.title.includes(keyword.value.trim()));
if (resultOptions.value?.length > 0) {
activePath.value = resultOptions.value[0].path;
} else {
activePath.value = '';
}
}
function handleClose() {
resultOptions.value = [];
keyword.value = '';
show.value = false;
}
/** key up */
function handleUp() {
const { length } = resultOptions.value;
if (length === 0) return;
const index = resultOptions.value.findIndex(item => item.path === activePath.value);
if (index === 0) {
activePath.value = resultOptions.value[length - 1].path;
} else {
activePath.value = resultOptions.value[index - 1].path;
}
}
/** key down */
function handleDown() {
const { length } = resultOptions.value;
if (length === 0) return;
const index = resultOptions.value.findIndex(item => item.path === activePath.value);
if (index + 1 === length) {
activePath.value = resultOptions.value[0].path;
} else {
activePath.value = resultOptions.value[index + 1].path;
}
}
/** key enter */
function handleEnter() {
if (isUrl(activePath.value)) {
window.open(activePath.value, '__blank');
} else {
router.push(activePath.value);
handleClose();
}
}
onKeyStroke('Escape', handleClose);
onKeyStroke('Enter', handleEnter);
onKeyStroke('ArrowUp', handleUp);
onKeyStroke('ArrowDown', handleDown);
</script>
<style lang="scss" scoped></style>
62 changes: 62 additions & 0 deletions src/layouts/common/GlobalSearch/components/SearchResult.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<template>
<n-scrollbar>
<div class="pb-12px">
<template v-for="item in options" :key="item.path">
<div
class="bg-[#e5e7eb] dark:bg-dark h-56px mt-8px px-14px rounded-4px cursor-pointer flex-y-center justify-between"
:style="{
background: item.path === active ? theme.themeColor : '',
color: item.path === active ? '#fff' : ''
}"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<Icon :icon="item.meta?.icon ?? 'mdi:bookmark-minus-outline'" />
<span class="flex-1 ml-5px">{{ item.meta?.title }}</span>
<icon-ant-design:enter-outlined class="icon text-20px p-2px mr-3px" />
</div>
</template>
</div>
</n-scrollbar>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
import { NScrollbar } from 'naive-ui';
import { Icon } from '@iconify/vue';
import { useThemeStore } from '@/store';
interface Props {
value: string;
options: RouteRecordRaw[];
}
interface Emits {
(e: 'update:value', val: string): void;
(e: 'enter'): void;
}
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>();
const active = computed({
get() {
return props.value;
},
set(val: string) {
emit('update:value', val);
}
});
const theme = useThemeStore();
/** 鼠标移入 */
async function handleMouse(item: RouteRecordRaw) {
active.value = item.path;
}
function handleTo() {
emit('enter');
}
</script>
<style lang="scss" scoped></style>
3 changes: 3 additions & 0 deletions src/layouts/common/GlobalSearch/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import SearchModal from './SearchModal.vue';

export { SearchModal };
20 changes: 20 additions & 0 deletions src/layouts/common/GlobalSearch/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<div>
<hover-container tooltip-content="搜索" class="w-40px h-full" @click="handleSearch">
<icon-uil:search class="text-20px text-[#666]" />
</hover-container>
<search-modal v-model:value="show" />
</div>
</template>

<script lang="ts" setup>
import { useBoolean } from '@/hooks';
import { HoverContainer } from '@/components';
import { SearchModal } from './components';
const { bool: show, toggle } = useBoolean();
function handleSearch() {
toggle();
}
</script>
<style lang="scss" scoped></style>
2 changes: 1 addition & 1 deletion src/router/setup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ export async function setupRouter(app: App) {
}

export { default as cacheRoutes } from './cache';
export { default as menus } from './menus';
export { menusList, menus } from './menus';
6 changes: 4 additions & 2 deletions src/router/setup/menus.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { transformRouteToMenu } from '@/utils';
import { transformRouteToMenu, transformRouteToList } from '@/utils';
import customRoutes from '../modules';

/** 菜单 */
const menus = transformRouteToMenu(customRoutes);
/** 菜单搜索列表 */
const menusList = transformRouteToList(customRoutes);

export default menus;
export { menus, menusList };
14 changes: 14 additions & 0 deletions src/utils/router/menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ export function transformRouteToMenu(routes: RouteRecordRaw[]) {
return globalMenu;
}

/** 将路由转换成菜单列表 */
export function transformRouteToList(routes: RouteRecordRaw[], treeMap: RouteRecordRaw[] = []) {
if (routes && routes.length === 0) return [];
return routes.reduce((acc, cur) => {
if (!cur.meta?.notAsMenu) {
acc.push(cur);
}
if (cur.children && cur.children.length > 0) {
transformRouteToList(cur.children, treeMap);
}
return acc;
}, treeMap);
}

/** 判断路由是否为Url链接 */
export function isUrl(path: string): boolean {
const reg =
Expand Down

0 comments on commit b9ce691

Please sign in to comment.