新增搜索菜单按钮

This commit is contained in:
lai
2025-09-08 19:30:30 +08:00
parent 89c2a61737
commit 627f847d5e
4 changed files with 387 additions and 0 deletions

View File

@@ -10,6 +10,8 @@ export default {
desc: 'Core Network Management Platform',
loading: 'Please wait...',
inputPlease: 'Please input',
searchPlease: 'Search menus...',
searchTip: 'Enter keywords to search menus',
selectPlease: 'please select',
tipTitle: 'Prompt',
msgSuccess: 'Success {msg}',

View File

@@ -10,6 +10,8 @@ export default {
desc: '核心网管理平台',
loading: '请稍等...',
inputPlease: '请输入',
searchPlease: '搜索菜单...',
searchTip: '输入关键词搜索菜单',
selectPlease: '请选择',
tipTitle: '提示',
msgSuccess: '{msg} 成功',

View File

@@ -4,6 +4,7 @@ import svgDark from '@/assets/svg/dark.svg';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { viewTransitionTheme } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import SearchMenu from './SearchMenu.vue';
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useFullscreen } from '@vueuse/core';
@@ -84,6 +85,11 @@ function fnChangeLocale(e: any) {
<template>
<a-space :size="12" align="center">
<!-- 搜索功能 -->
<span >
<SearchMenu />
</span>
<span v-roles:has="[TENANTADMIN_ROLE_KEY]">
<a-tooltip placement="bottom">
<template #title>{{ t('loayouts.rightContent.alarm') }}</template>

View File

@@ -0,0 +1,377 @@
<template>
<!-- 搜索按钮 -->
<a-tooltip placement="bottom">
<template #title>{{ t('common.search') }}</template>
<a-button
type="text"
style="color: inherit"
@click="fnClickSearch"
>
<template #icon>
<SearchOutlined />
</template>
</a-button>
</a-tooltip>
<!-- 搜索弹窗 -->
<ProModal
:drag="false"
:center-y="true"
:width="600"
:minHeight="400"
:mask-closable="true"
v-model:open="searchModalOpen"
:title="t('common.search')"
:footer="null"
@cancel="fnCloseSearch"
>
<div class="search-modal-content">
<a-input
ref="searchInputRef"
v-model:value="searchKeyword"
:placeholder="t('common.searchPlease')"
size="large"
style="margin-bottom: 16px"
@keydown="fnHandleKeydown"
>
<template #prefix>
<SearchOutlined style="color: #bfbfbf" />
</template>
</a-input>
<div class="search-results">
<div v-if="filteredMenus.length === 0" class="no-results">
<a-empty
:description="searchKeyword ? t('common.noData') : t('common.searchTip')"
:image="false"
/>
</div>
<div v-else class="menu-list">
<div
v-for="menu in filteredMenus"
:key="menu.key"
class="menu-item"
@click="fnSelectMenu(menu)"
>
<div class="menu-icon">
<!-- 处理自定义图标字体 -->
<IconFont
v-if="menu.icon && menu.icon.startsWith('icon-')"
:type="menu.icon"
class="icon"
/>
<!-- 处理Ant Design图标组件 -->
<component
:is="menu.icon"
v-else-if="menu.icon && !menu.icon.startsWith('icon-')"
class="icon"
/>
<!-- 默认图标 -->
<FolderOutlined v-else class="icon" />
</div>
<div class="menu-info">
<div class="menu-title">{{ menu.title }}</div>
</div>
<div class="menu-action">
<RightOutlined />
</div>
</div>
</div>
</div>
</div>
</ProModal>
</template>
<script setup lang="ts">
import {
getMenuData,
clearMenuItem
} from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import IconFont from '@/components/IconFont/index.vue';
import { ref, computed, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import useI18n from '@/hooks/useI18n';
import useUserStore from '@/store/modules/user';
import useRouterStore from '@/store/modules/router';
const { t } = useI18n();
const userStore = useUserStore();
const routerStore = useRouterStore();
const router = useRouter();
// 搜索相关状态
const searchModalOpen = ref<boolean>(false);
const searchKeyword = ref<string>('');
const searchInputRef = ref();
// 获取所有可搜索的菜单项
const searchableMenus = computed(() => {
const menus: Array<{
title: string;
path: string;
icon?: string;
key: string;
routeName?: string;
}> = [];
// 使用和BasicLayout完全相同的菜单数据获取逻辑
const getMenuDataForSearch = () => {
// 动态路由添加到菜单面板
const rootRoute = router.getRoutes().find(r => r.name === 'Root');
if (rootRoute) {
const children = routerStore.setRootRouterData(rootRoute.children);
const buildRouterData = routerStore.buildRouterData;
if (buildRouterData.length > 0) {
rootRoute.children = children.concat(buildRouterData);
} else {
rootRoute.children = children;
}
// 根据用户角色过滤
if (!userStore.roles.includes('tenant')) {
rootRoute.children = rootRoute.children.filter(
item => item.name !== 'Index'
);
}
}
// 使用和BasicLayout相同的处理方法
const { menuData } = getMenuData(clearMenuItem(router.getRoutes()));
return menuData;
};
// 递归获取所有路由项
const getRouteItems = (routes: any[], parentPath = '') => {
routes.forEach((route: any) => {
// 跳过根路径和不显示的菜单
if (route.path === '/' || route.path === '' || route.meta?.hideInMenu) {
// 继续处理子路由
if (route.children && route.children.length > 0) {
getRouteItems(route.children, route.path === '/' ? '' : route.path);
}
return;
}
// 构建完整路径
let fullPath = route.path;
if (!route.path.startsWith('/') && parentPath) {
fullPath = `${parentPath}/${route.path}`;
}
// 只添加有meta.title的路由项
if (route.meta && route.meta.title) {
try {
const title = t(route.meta.title);
// 避免重复添加已存在的路由
const exists = menus.find(m => m.routeName === route.name || m.path === fullPath);
if (!exists) {
menus.push({
title: title,
path: fullPath,
icon: route.meta.icon,
key: route.name || fullPath,
routeName: route.name
});
}
} catch (error) {
// 如果翻译失败,使用原始标题
const exists = menus.find(m => m.routeName === route.name || m.path === fullPath);
if (!exists) {
menus.push({
title: route.meta.title,
path: fullPath,
icon: route.meta.icon,
key: route.name || fullPath,
routeName: route.name
});
}
}
}
// 处理子路由
if (route.children && route.children.length > 0) {
getRouteItems(route.children, fullPath);
}
});
};
// 使用和菜单面板相同的数据源
const menuRoutes = getMenuDataForSearch();
if (menuRoutes && menuRoutes.length > 0) {
getRouteItems(menuRoutes);
}
return menus;
});
// 过滤的菜单项
const filteredMenus = computed(() => {
if (!searchKeyword.value.trim()) {
return searchableMenus.value.slice(0, 10); // 默认显示前10个
}
return searchableMenus.value.filter(menu =>
menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
menu.path.toLowerCase().includes(searchKeyword.value.toLowerCase())
).slice(0, 10);
});
/**打开搜索弹窗 */
function fnClickSearch() {
searchModalOpen.value = true;
searchKeyword.value = '';
nextTick(() => {
searchInputRef.value?.focus();
});
}
/**关闭搜索弹窗 */
function fnCloseSearch() {
searchModalOpen.value = false;
searchKeyword.value = '';
}
/**选择菜单项并跳转 */
function fnSelectMenu(menu: any) {
try {
// 优先使用路由名称跳转
if (menu.routeName) {
router.push({ name: menu.routeName });
} else {
router.push(menu.path);
}
fnCloseSearch();
} catch (error) {
// 如果跳转失败,尝试直接使用路径
try {
router.push(menu.path);
fnCloseSearch();
} catch (secondError) {
// 可以在这里添加错误提示
}
}
}
/**键盘事件处理 */
function fnHandleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
fnCloseSearch();
}
}
</script>
<style lang="less" scoped>
.search-modal-content {
.search-results {
max-height: 400px;
overflow-y: auto;
.no-results {
text-align: center;
padding: 40px 0;
}
.menu-list {
.menu-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 4px;
&:hover {
background-color: #f5f5f5;
.menu-action {
opacity: 1;
}
}
.menu-icon {
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background-color: #f0f2f5;
border-radius: 6px;
.icon {
font-size: 16px;
color: #666;
}
}
.menu-info {
flex: 1;
min-width: 0;
.menu-title {
font-size: 14px;
font-weight: 500;
color: #262626;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu-path {
font-size: 12px;
color: #8c8c8c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.menu-action {
opacity: 0;
transition: opacity 0.2s ease;
color: #bfbfbf;
font-size: 12px;
}
}
}
}
}
// 暗黑主题支持
[data-theme="dark"] {
.search-modal-content {
.search-results {
.menu-list {
.menu-item {
&:hover {
background-color: #303030;
}
.menu-icon {
background-color: #262626;
.icon {
color: #bfbfbf;
}
}
.menu-info {
.menu-title {
color: #f0f0f0;
}
.menu-path {
color: #8c8c8c;
}
}
}
}
}
}
}
</style>