新增搜索菜单按钮
This commit is contained in:
@@ -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}',
|
||||
|
||||
@@ -10,6 +10,8 @@ export default {
|
||||
desc: '核心网管理平台',
|
||||
loading: '请稍等...',
|
||||
inputPlease: '请输入',
|
||||
searchPlease: '搜索菜单...',
|
||||
searchTip: '输入关键词搜索菜单',
|
||||
selectPlease: '请选择',
|
||||
tipTitle: '提示',
|
||||
msgSuccess: '{msg} 成功',
|
||||
|
||||
@@ -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>
|
||||
|
||||
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