Skip to content

Commit

Permalink
feat(projects): 迁移全局搜索菜单功能
Browse files Browse the repository at this point in the history
  • Loading branch information
yanbowe committed Jan 24, 2022
1 parent b16721b commit 554d7fd
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/layouts/common/GlobalHeader/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<header-menu />
</div>
<div class="flex justify-end h-full">
<global-search />
<github-site />
<full-screen />
<theme-mode />
Expand All @@ -22,6 +23,7 @@ import { DarkModeContainer } from '@/components';
import { useThemeStore } from '@/store';
import type { GlobalHeaderProps } from '@/interface';
import GlobalLogo from '../GlobalLogo/index.vue';
import GlobalSearch from '../GlobalSearch/index.vue';
import {
MenuCollapse,
GlobalBreadcrumb,
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>
136 changes: 136 additions & 0 deletions src/layouts/common/GlobalSearch/components/SearchModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<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"
@after-leave="handleClose"
>
<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 { NModal, NInput, NEmpty } from 'naive-ui';
import { useDebounceFn, onKeyStroke } from '@vueuse/core';
import { useRouteStore } from '@/store';
import type { RouteList } from './types';
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 routeStore = useRouteStore();
const keyword = ref('');
const activePath = ref('');
const resultOptions = shallowRef<RouteList[]>([]);
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 = routeStore.menusList.filter(
menu => keyword.value && menu.meta?.title.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim())
);
if (resultOptions.value?.length > 0) {
activePath.value = resultOptions.value[0].path;
} else {
activePath.value = '';
}
}
function handleClose() {
show.value = false;
/** 延时处理防止用户看到某些操作 */
setTimeout(() => {
resultOptions.value = [];
keyword.value = '';
}, 200);
}
/** 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() {
const { length } = resultOptions.value;
if (length === 0 || activePath.value === '') return;
const item = resultOptions.value.find(item => item.path === activePath.value);
if (item?.meta?.href) {
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 { NScrollbar } from 'naive-ui';
import { Icon } from '@iconify/vue';
import { useThemeStore } from '@/store';
import type { RouteList } from './types';
interface Props {
value: string;
options: RouteList[];
}
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: RouteList) {
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 };
1 change: 1 addition & 0 deletions src/layouts/common/GlobalSearch/components/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type RouteList = AuthRoute.Route;
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 { HoverContainer } from '@/components';
import { useBoolean } from '@/hooks';
import { SearchModal } from './components';
const { bool: show, toggle } = useBoolean();
function handleSearch() {
toggle();
}
</script>
<style lang="scss" scoped></style>
11 changes: 10 additions & 1 deletion src/store/modules/route/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { Router } from 'vue-router';
import { defineStore } from 'pinia';
import { fetchUserRoutes } from '@/service';
import { getUserInfo, transformAuthRouteToMenu, transformAuthRoutesToVueRoutes, getCacheRoutes } from '@/utils';
import {
getUserInfo,
transformAuthRouteToMenu,
transformAuthRoutesToVueRoutes,
transformRouteToList,
getCacheRoutes
} from '@/utils';
import type { GlobalMenuOption } from '@/interface';
import { useTabStore } from '../tab';

Expand All @@ -12,6 +18,7 @@ interface RouteState {
routeHomeName: AuthRoute.RouteKey;
/** 菜单 */
menus: GlobalMenuOption[];
menusList: AuthRoute.Route[];
/** 缓存的路由名称 */
cacheRoutes: string[];
}
Expand All @@ -21,6 +28,7 @@ export const useRouteStore = defineStore('route-store', {
isAddedDynamicRoute: false,
routeHomeName: 'dashboard_analysis',
menus: [],
menusList: [],
cacheRoutes: []
}),
actions: {
Expand All @@ -37,6 +45,7 @@ export const useRouteStore = defineStore('route-store', {
if (data) {
this.routeHomeName = data.home;
this.menus = transformAuthRouteToMenu(data.routes);
this.menusList = transformRouteToList(data.routes);

const vueRoutes = transformAuthRoutesToVueRoutes(data.routes);
vueRoutes.forEach(route => {
Expand Down
14 changes: 14 additions & 0 deletions src/utils/router/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ export function transformAuthRoutesToVueRoutes(routes: AuthRoute.Route[]) {
return routes.map(route => transformAuthRouteToVueRoute(route)).flat(1);
}

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

/**
* 将单个权限路由转换成vue路由
* @param route - 权限路由
Expand Down

0 comments on commit 554d7fd

Please sign in to comment.