Skip to content

Commit

Permalink
feat(projects): 全局搜索菜单功能
Browse files Browse the repository at this point in the history
  • Loading branch information
yanbowe authored and buqiyuan committed Dec 31, 2021
1 parent f4a24ef commit b4c9ba9
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/layout/header/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import SearchModal from './search/index.vue';

export { SearchModal };
31 changes: 31 additions & 0 deletions src/layout/header/components/search/components/SearchFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div class="flex items-center">
<span class="mr-14px">
<EnterOutlined class="icon text-15px p-2px mr-3px" />
确认
</span>
<span class="mr-14px">
<ArrowUpOutlined class="icon text-15px p-2px mr-5px" />
<ArrowDownOutlined class="icon text-15px p-2px mr-3px" />
切换
</span>
<span>
<CloseOutlined class="icon text-15px p-2px mr-3px" />
关闭
</span>
</div>
</template>

<script lang="ts" setup>
import {
EnterOutlined,
ArrowDownOutlined,
ArrowUpOutlined,
CloseOutlined,
} from '@ant-design/icons-vue';
</script>
<style lang="less" scoped>
.icon {
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66;
}
</style>
58 changes: 58 additions & 0 deletions src/layout/header/components/search/components/SearchResult.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<div>
<div class="pb-12px">
<template v-for="item in options" :key="item.name">
<div
class="bg-[#e5e7eb] h-56px mt-8px px-14px rounded-4px flex items-center justify-justify-between"
style="cursor: pointer"
:style="{
background: item.name === active ? '#1890ff' : '',
color: item.name === active ? '#fff' : '',
}"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<BookOutlined />
<span class="flex-1 ml-5px">{{ item.meta?.title }}</span>
<EnterOutlined class="icon text-20px p-2px mr-3px" />
</div>
</template>
</div>
</div>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
import { EnterOutlined, BookOutlined } from '@ant-design/icons-vue';
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);
},
});
/** 鼠标移入 */
async function handleMouse(item: RouteRecordRaw) {
active.value = item.name as string;
}
function handleTo() {
emit('enter');
}
</script>
<style lang="less" scoped></style>
150 changes: 150 additions & 0 deletions src/layout/header/components/search/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<template>
<CustomAModal title="搜索菜单" v-model:visible="show" :keyboard="false">
<a-input
ref="inputRef"
v-model:value="keyword"
clearable
placeholder="请输入关键词搜索"
@change="handleSearch"
>
<template #prefix>
<SearchOutlined class="text-15px text-[#c2c2c2]" />
</template>
</a-input>
<div class="mt-20px">
<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>
</CustomAModal>
</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 { Empty } from 'ant-design-vue';
import { CustomAModal } from '@/components/a-custom-modal';
import { useDebounceFn, onKeyStroke } from '@vueuse/core';
import { useUserStore } from '@/store/modules/user';
import { SearchOutlined } from '@ant-design/icons-vue';
import SearchResult from './components/SearchResult.vue';
import SearchFooter from './components/SearchFooter.vue';
interface Props {
/** 弹窗显隐 */
value: boolean;
}
interface Emits {
(e: 'update:value', val: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>();
const userStore = useUserStore();
const router = useRouter();
const keyword = ref('');
const activePath = ref('');
const menusList = computed(() => transformRouteToList(userStore.menus));
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.value.filter((menu) => {
const title = menu.meta?.title as string;
return keyword.value && title.includes(keyword.value.trim());
});
if (resultOptions.value?.length > 0) {
activePath.value = resultOptions.value[0].name as string;
} else {
activePath.value = '';
}
}
/** 将路由转换成菜单列表 */
function transformRouteToList(routes: RouteRecordRaw[], treeMap: RouteRecordRaw[] = []) {
if (routes && routes.length === 0) return [];
return routes.reduce((acc, cur) => {
/** 允许在菜单内显示并且无子路由 */
if (!cur.meta?.hideInMenu && !cur.children) {
acc.push(cur);
}
if (cur.children && cur.children.length > 0) {
transformRouteToList(cur.children, treeMap);
}
return acc;
}, treeMap);
}
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.name === activePath.value);
if (index === 0) {
activePath.value = resultOptions.value[length - 1].name as string;
} else {
activePath.value = resultOptions.value[index - 1].name as string;
}
}
/** key down */
function handleDown() {
const { length } = resultOptions.value;
if (length === 0) return;
const index = resultOptions.value.findIndex((item) => item.name === activePath.value);
if (index + 1 === length) {
activePath.value = resultOptions.value[0].name as string;
} else {
activePath.value = resultOptions.value[index + 1].name as string;
}
}
/** key enter */
function handleEnter() {
if (/http(s)?:/.test(activePath.value)) {
window.open(activePath.value);
} else {
router.push({ name: activePath.value });
handleClose();
}
}
onKeyStroke('Escape', handleClose);
onKeyStroke('Enter', handleEnter);
onKeyStroke('ArrowUp', handleUp);
onKeyStroke('ArrowDown', handleDown);
</script>
<style lang="less" scoped></style>
6 changes: 6 additions & 0 deletions src/layout/header/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
</template>
</Dropdown>
</div>
<SearchModal v-model:value="searchVisible" />
</Layout.Header>
</template>

Expand Down Expand Up @@ -79,6 +80,7 @@
GithubOutlined,
LockOutlined,
} from '@ant-design/icons-vue';
import { SearchModal } from './components';
import { useUserStore } from '@/store/modules/user';
import { useLockscreenStore } from '@/store/modules/lockscreen';
Expand All @@ -96,6 +98,7 @@
const router = useRouter();
const route = useRoute();
const userInfo = computed(() => userStore.userInfo);
const searchVisible = ref(false);
const menus = computed(() => {
console.log('route', route, userStore.menus);
if (route.meta?.namePath) {
Expand Down Expand Up @@ -204,6 +207,9 @@
{
icon: SearchOutlined,
tips: '搜索',
eventObject: {
click: () => (searchVisible.value = true),
},
},
{
icon: GithubOutlined,
Expand Down

0 comments on commit b4c9ba9

Please sign in to comment.