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>
<div class="nav-action-item" @click="showSearchModal">
<div @click="openSearchModal">
<svg-icon icon-class="search" />
<el-dialog
v-model="visible"
v-model="isModalVisible"
width="30%"
:append-to-body="true"
:show-close="false"
@close="hideSearchModal"
@close="closeSearchModal"
>
<template #header>
<el-input
ref="menuSearchInput"
v-model="searchKey"
ref="searchInputRef"
v-model="searchKeyword"
size="large"
placeholder="搜索"
placeholder="输入菜单名称关键字搜索"
clearable
@keyup.enter="inputSearch"
@input="inputSearch"
@keyup.enter="selectActiveResult"
@input="updateSearchResults"
@keydown.up.prevent="navigateResults('up')"
@keydown.down.prevent="navigateResults('down')"
@keydown.esc="closeSearchModal"
>
<template #prepend>
<el-button icon="Search" />
</template>
</el-input>
</template>
<div class="search-content">
<ul v-if="searchResult.length > 0">
<div class="search-result">
<ul v-if="displayResults.length > 0">
<li
v-for="(item, index) in searchResult"
:key="index"
@click="toMenu(item)"
v-for="(item, index) in displayResults"
:key="item.path"
: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-', '')" />
</el-icon>
<svg-icon v-else-if="item.icon" :icon-class="item.icon" />
@@ -41,84 +43,119 @@
{{ item.title }}
</li>
</ul>
<div v-else class="search-space">暂无数据</div>
<div v-else class="text-center py-5">暂无历史记录</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>
</div>
</template>
<script setup lang="ts">
import router from "@/router";
const visible = ref(false);
const searchKey = ref("");
import { usePermissionStore } from "@/store";
import { isExternal } from "@/utils";
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[]>([]);
function showSearchModal() {
searchKey.value = "";
searchResult.value = [];
visible.value = true;
const permissionStore = usePermissionStore();
const isModalVisible = ref(false);
const searchKeyword = ref("");
const searchInputRef = ref();
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(() => {
menuSearchInput.value.focus();
searchInputRef.value.focus();
}, 100);
}
function hideSearchModal() {
visible.value = false;
// 关闭搜索模态框
function closeSearchModal() {
isModalVisible.value = false;
}
function inputSearch() {
searchResult.value = [];
if (searchKey.value) {
let toLowerCaseKey = searchKey.value.toLowerCase();
searchResult.value = searchData.value.filter((item) =>
item.toLowerCaseTitle.includes(toLowerCaseKey)
// 更新搜索结果
function updateSearchResults() {
activeIndex.value = -1;
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
searchResults.value = menuItems.value.filter((item) =>
item.title.toLowerCase().includes(keyword)
);
} else {
searchResults.value = [];
}
}
onMounted(() => {
searchResult.value = [];
initSearchData(permissionStore.routes);
// 显示搜索结果或历史记录
const displayResults = computed(() => {
return searchResults.value.length > 0
? searchResults.value
: searchHistory.value.slice(0, 3);
});
function initSearchData(routes: RouteRecordRaw[], parentPath: string = "") {
parentPath = parentPath === "/" ? "" : parentPath;
routes.forEach((item: any) => {
if (noSearchRoutePath.value.includes(item.path)) {
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 selectActiveResult() {
if (displayResults.value.length > 0 && activeIndex.value >= 0) {
navigateToRoute(displayResults.value[activeIndex.value]);
}
}
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)) {
window.open(item.path, "_blank");
} else {
@@ -126,35 +163,82 @@ function toMenu(item: SearchItem) {
}
}
interface SearchItem {
title: string;
toLowerCaseTitle: string;
path: string;
name: string;
icon?: string;
redirect?: string;
function loadRoutes(routes: RouteRecordRaw[], parentPath = "") {
routes.forEach((route) => {
const path = route.path.startsWith("/")
? route.path
: `${parentPath}/${route.path}`;
if (excludedRoutes.value.includes(route.path) || isExternal(route.path))
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>
<style scoped>
.search-content ul li {
padding-left: 10px;
line-height: 40px;
text-align: left;
&:hover {
background-color: #f5f5f5;
}
}
.search-content {
.search-result {
max-height: 400px;
overflow-y: auto;
}
.search-space {
padding: 20px;
color: #999;
text-align: center;
.search-result ul li {
padding: 10px;
line-height: 40px;
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>