新增搜索菜单按钮
This commit is contained in:
@@ -10,6 +10,8 @@ export default {
|
|||||||
desc: 'Core Network Management Platform',
|
desc: 'Core Network Management Platform',
|
||||||
loading: 'Please wait...',
|
loading: 'Please wait...',
|
||||||
inputPlease: 'Please input',
|
inputPlease: 'Please input',
|
||||||
|
searchPlease: 'Search menus...',
|
||||||
|
searchTip: 'Enter keywords to search menus',
|
||||||
selectPlease: 'please select',
|
selectPlease: 'please select',
|
||||||
tipTitle: 'Prompt',
|
tipTitle: 'Prompt',
|
||||||
msgSuccess: 'Success {msg}',
|
msgSuccess: 'Success {msg}',
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export default {
|
|||||||
desc: '核心网管理平台',
|
desc: '核心网管理平台',
|
||||||
loading: '请稍等...',
|
loading: '请稍等...',
|
||||||
inputPlease: '请输入',
|
inputPlease: '请输入',
|
||||||
|
searchPlease: '搜索菜单...',
|
||||||
|
searchTip: '输入关键词搜索菜单',
|
||||||
selectPlease: '请选择',
|
selectPlease: '请选择',
|
||||||
tipTitle: '提示',
|
tipTitle: '提示',
|
||||||
msgSuccess: '{msg} 成功',
|
msgSuccess: '{msg} 成功',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import svgDark from '@/assets/svg/dark.svg';
|
|||||||
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
|
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
|
||||||
import { viewTransitionTheme } from 'antdv-pro-layout';
|
import { viewTransitionTheme } from 'antdv-pro-layout';
|
||||||
import { ProModal } from 'antdv-pro-modal';
|
import { ProModal } from 'antdv-pro-modal';
|
||||||
|
import SearchMenu from './SearchMenu.vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useFullscreen } from '@vueuse/core';
|
import { useFullscreen } from '@vueuse/core';
|
||||||
@@ -84,6 +85,11 @@ function fnChangeLocale(e: any) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-space :size="12" align="center">
|
<a-space :size="12" align="center">
|
||||||
|
<!-- 搜索功能 -->
|
||||||
|
<span >
|
||||||
|
<SearchMenu />
|
||||||
|
</span>
|
||||||
|
|
||||||
<span v-roles:has="[TENANTADMIN_ROLE_KEY]">
|
<span v-roles:has="[TENANTADMIN_ROLE_KEY]">
|
||||||
<a-tooltip placement="bottom">
|
<a-tooltip placement="bottom">
|
||||||
<template #title>{{ t('loayouts.rightContent.alarm') }}</template>
|
<template #title>{{ t('loayouts.rightContent.alarm') }}</template>
|
||||||
|
|||||||
377
src/layouts/components/SearchMenu.vue
Normal file
377
src/layouts/components/SearchMenu.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user