refactor: ♻️ 菜单搜索添加快捷操作

This commit is contained in:
ray
2024-10-29 23:59:37 +08:00
parent dbcd0f209b
commit 3900c14024

View File

@@ -1,39 +1,41 @@
<template> <template>
<div class="nav-action-item" @click="showSearchModal"> <div @click="openSearchModal">
<svg-icon icon-class="search" /> <svg-icon icon-class="search" />
<el-dialog <el-dialog
v-model="visible" v-model="isModalVisible"
width="30%" width="30%"
:append-to-body="true" :append-to-body="true"
:show-close="false" :show-close="false"
@close="hideSearchModal" @close="closeSearchModal"
> >
<template #header> <template #header>
<el-input <el-input
ref="menuSearchInput" ref="searchInputRef"
v-model="searchKey" v-model="searchKeyword"
size="large" size="large"
placeholder="搜索" placeholder="输入菜单名称关键字搜索"
clearable clearable
@keyup.enter="inputSearch" @keyup.enter="selectActiveResult"
@input="inputSearch" @input="updateSearchResults"
@keydown.up.prevent="navigateResults('up')"
@keydown.down.prevent="navigateResults('down')"
@keydown.esc="closeSearchModal"
> >
<template #prepend> <template #prepend>
<el-button icon="Search" /> <el-button icon="Search" />
</template> </template>
</el-input> </el-input>
</template> </template>
<div class="search-content">
<ul v-if="searchResult.length > 0"> <div class="search-result">
<ul v-if="displayResults.length > 0">
<li <li
v-for="(item, index) in searchResult" v-for="(item, index) in displayResults"
:key="index" :key="item.path"
@click="toMenu(item)" :class="{ active: index === activeIndex }"
> @click="navigateToRoute(item)"
<el-icon
v-if="item.icon && item.icon.startsWith('el-icon')"
class="sub-el-icon"
> >
<el-icon v-if="item.icon && item.icon.startsWith('el-icon')">
<component :is="item.icon.replace('el-icon-', '')" /> <component :is="item.icon.replace('el-icon-', '')" />
</el-icon> </el-icon>
<svg-icon v-else-if="item.icon" :icon-class="item.icon" /> <svg-icon v-else-if="item.icon" :icon-class="item.icon" />
@@ -41,84 +43,119 @@
{{ item.title }} {{ item.title }}
</li> </li>
</ul> </ul>
<div v-else class="search-space">暂无数据</div> <div v-else class="text-center py-5">暂无历史记录</div>
</div> </div>
<template #footer />
<template #footer>
<div class="dialog-footer">
<svg-icon icon-class="enter" size="20px" />
<span>选择</span>
<svg-icon icon-class="down" size="20px" class="ml-5" />
<svg-icon icon-class="up" size="20px" class="ml-1" />
<span>切换</span>
<svg-icon icon-class="esc" size="20px" class="ml-5" />
<span>退出</span>
</div>
</template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import router from "@/router"; import router from "@/router";
const visible = ref(false);
const searchKey = ref("");
import { usePermissionStore } from "@/store"; import { usePermissionStore } from "@/store";
import { isExternal } from "@/utils"; import { isExternal } from "@/utils";
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from "vue-router";
const permissionStore = usePermissionStore();
//获取当前拥有的路由
const menuSearchInput = ref();
const noSearchRoutePath = ref(["/redirect", "/login", "/401", "/404"]);
const searchData = ref<SearchItem[]>([]);
const searchResult = ref<SearchItem[]>([]); const permissionStore = usePermissionStore();
function showSearchModal() { const isModalVisible = ref(false);
searchKey.value = ""; const searchKeyword = ref("");
searchResult.value = []; const searchInputRef = ref();
visible.value = true; const excludedRoutes = ref(["/redirect", "/login", "/401", "/404"]);
const menuItems = ref<SearchItem[]>([]);
const searchResults = ref<SearchItem[]>([]);
const searchHistory = ref<SearchItem[]>([]);
const activeIndex = ref(-1);
interface SearchItem {
title: string;
path: string;
name?: string;
icon?: string;
redirect?: string;
}
// 打开搜索模态框
function openSearchModal() {
searchKeyword.value = "";
activeIndex.value = -1;
isModalVisible.value = true;
setTimeout(() => { setTimeout(() => {
menuSearchInput.value.focus(); searchInputRef.value.focus();
}, 100); }, 100);
} }
function hideSearchModal() {
visible.value = false; // 关闭搜索模态框
function closeSearchModal() {
isModalVisible.value = false;
} }
function inputSearch() { // 更新搜索结果
searchResult.value = []; function updateSearchResults() {
activeIndex.value = -1;
if (searchKey.value) { if (searchKeyword.value) {
let toLowerCaseKey = searchKey.value.toLowerCase(); const keyword = searchKeyword.value.toLowerCase();
searchResult.value = searchData.value.filter((item) => searchResults.value = menuItems.value.filter((item) =>
item.toLowerCaseTitle.includes(toLowerCaseKey) item.title.toLowerCase().includes(keyword)
); );
} else {
searchResults.value = [];
} }
} }
onMounted(() => { // 显示搜索结果或历史记录
searchResult.value = []; const displayResults = computed(() => {
initSearchData(permissionStore.routes); return searchResults.value.length > 0
? searchResults.value
: searchHistory.value.slice(0, 3);
}); });
function initSearchData(routes: RouteRecordRaw[], parentPath: string = "") { // 执行搜索
parentPath = parentPath === "/" ? "" : parentPath; function selectActiveResult() {
routes.forEach((item: any) => { if (displayResults.value.length > 0 && activeIndex.value >= 0) {
if (noSearchRoutePath.value.includes(item.path)) { navigateToRoute(displayResults.value[activeIndex.value]);
return;
} }
let path = item.path.startsWith("/")
? item.path
: parentPath + "/" + item.path;
path = isExternal(item.path) ? item.path : path;
if (item.children) {
initSearchData(item.children, path);
} else if (item.meta && item.meta.title && item.path) {
let title = item.meta.title == "dashboard" ? "首页" : item.meta.title;
let toLowerCaseTitle = title.toLowerCase();
searchData.value.push({
title: title,
toLowerCaseTitle: toLowerCaseTitle,
path: path,
name: item.name,
icon: item.meta.icon,
redirect: item.redirect ? item.redirect : undefined,
});
}
});
} }
function toMenu(item: SearchItem) { // 导航搜索结果
hideSearchModal(); function navigateResults(direction: string) {
if (displayResults.value.length === 0) return;
if (direction === "up") {
activeIndex.value =
activeIndex.value <= 0
? displayResults.value.length - 1
: activeIndex.value - 1;
} else if (direction === "down") {
activeIndex.value =
activeIndex.value >= displayResults.value.length - 1
? 0
: activeIndex.value + 1;
}
}
// 跳转
function navigateToRoute(item: SearchItem) {
closeSearchModal();
if (!searchHistory.value.some((history) => history.path === item.path)) {
searchHistory.value.unshift(item);
if (searchHistory.value.length > 3) {
searchHistory.value.pop();
}
}
if (isExternal(item.path)) { if (isExternal(item.path)) {
window.open(item.path, "_blank"); window.open(item.path, "_blank");
} else { } else {
@@ -126,35 +163,82 @@ function toMenu(item: SearchItem) {
} }
} }
interface SearchItem { function loadRoutes(routes: RouteRecordRaw[], parentPath = "") {
title: string; routes.forEach((route) => {
toLowerCaseTitle: string; const path = route.path.startsWith("/")
path: string; ? route.path
name: string; : `${parentPath}/${route.path}`;
icon?: string; if (excludedRoutes.value.includes(route.path) || isExternal(route.path))
redirect?: string; return;
if (route.children) {
loadRoutes(route.children, path);
} else if (route.meta?.title) {
const title =
route.meta.title === "dashboard" ? "首页" : route.meta.title;
menuItems.value.push({
title,
path,
name: typeof route.name === "string" ? route.name : undefined,
icon: route.meta.icon,
redirect:
typeof route.redirect === "string" ? route.redirect : undefined,
});
}
});
} }
// 初始化路由数据
onMounted(() => {
loadRoutes(permissionStore.routes);
});
</script> </script>
<style scoped> <style scoped>
.search-content ul li { .search-result {
padding-left: 10px;
line-height: 40px;
text-align: left;
&:hover {
background-color: #f5f5f5;
}
}
.search-content {
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
} }
.search-space { .search-result ul li {
padding: 20px; padding: 10px;
color: #999; line-height: 40px;
text-align: center; text-align: left;
cursor: pointer;
}
.search-result ul li.active {
background-color: #e6f7ff;
}
.search-result ul li:hover {
background-color: #f5f5f5;
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: start;
svg {
display: flex;
align-items: center;
justify-content: center;
padding: 0 2px;
margin-right: 0.4em;
color: #909399;
background: rgb(125 125 125 / 10%);
border: 0;
border-radius: 2px;
box-shadow:
inset 0 -2px 0 0 #cdcde6,
inset 0 0 1px 1px #fff,
0 1px 2px 1px rgb(30 35 90 / 40%);
}
span {
font-size: 12px;
color: #909399;
}
} }
</style> </style>