增加字典
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<el-aside :width="width" class="common-aside">
|
||||
<div v-if="loading" class="loading-spinner"> <!-- Show spinner if loading -->
|
||||
<i class="el-icon-loading" style="font-size: 24px; color: #fff;"></i>
|
||||
<div v-if="loading" class="loading-spinner">
|
||||
<!-- Show spinner if loading -->
|
||||
<i class="el-icon-loading" style="font-size: 24px; color: #fff"></i>
|
||||
</div>
|
||||
<el-menu v-else
|
||||
<el-menu
|
||||
v-else
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:background-color="asideBgColor"
|
||||
@@ -31,37 +33,37 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAllDataStore, useMenuStore } from '@/stores';
|
||||
import MenuTreeItem from './MenuTreeItem.vue';
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useAllDataStore, useMenuStore } from "@/stores";
|
||||
import MenuTreeItem from "./MenuTreeItem.vue";
|
||||
|
||||
const emit = defineEmits(['menu-click']);
|
||||
const emit = defineEmits(["menu-click"]);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const menuStore = useMenuStore();
|
||||
const loading = computed(() => menuStore.loading);
|
||||
const loading = computed(() => menuStore.loading);
|
||||
|
||||
const store = useAllDataStore();
|
||||
const isCollapse = computed(() => store.state.isCollapse);
|
||||
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
|
||||
const width = computed(() => (store.state.isCollapse ? "64px" : "180px"));
|
||||
// 使用 Element Plus 的颜色变量,主题切换时会自动适配
|
||||
// 这些值会被 el-menu 组件使用,el-menu 会自动适配主题
|
||||
// 计算是否为暗色主题(响应式)
|
||||
const isDark = ref(document.documentElement.classList.contains('dark'));
|
||||
const isDark = ref(document.documentElement.classList.contains("dark"));
|
||||
|
||||
// 监听主题变化
|
||||
const updateTheme = () => {
|
||||
isDark.value = document.documentElement.classList.contains('dark');
|
||||
isDark.value = document.documentElement.classList.contains("dark");
|
||||
};
|
||||
|
||||
// 使用 MutationObserver 监听 html 元素的 class 变化(主题切换时更新)
|
||||
let themeObserver = null;
|
||||
|
||||
// 自定义颜色配置
|
||||
const BG_COLOR = '#062da3'; // 背景和 active 颜色
|
||||
const HOVER_COLOR = '#4f84ff'; // hover 颜色
|
||||
const BG_COLOR = "#062da3"; // 背景和 active 颜色
|
||||
const HOVER_COLOR = "#4f84ff"; // hover 颜色
|
||||
|
||||
// 将 hex 颜色转换为 rgba
|
||||
function hexToRgba(hex, alpha = 1) {
|
||||
@@ -74,7 +76,7 @@ function hexToRgba(hex, alpha = 1) {
|
||||
// 亮色主题使用自定义背景色,暗色主题使用默认背景
|
||||
const asideBgColor = computed(() => {
|
||||
if (isDark.value) {
|
||||
return 'var(--el-bg-color)';
|
||||
return "var(--el-bg-color)";
|
||||
}
|
||||
// 亮色主题使用 #062da3 背景
|
||||
return BG_COLOR;
|
||||
@@ -82,18 +84,18 @@ const asideBgColor = computed(() => {
|
||||
|
||||
const asideTextColor = computed(() => {
|
||||
if (isDark.value) {
|
||||
return 'var(--el-text-color-primary)';
|
||||
return "var(--el-text-color-primary)";
|
||||
}
|
||||
// 亮色主题使用白色文字
|
||||
return '#ffffff';
|
||||
return "#ffffff";
|
||||
});
|
||||
|
||||
const activeColor = ref('#ffffff'); // active 文字颜色为白色
|
||||
const activeColor = ref("#ffffff"); // active 文字颜色为白色
|
||||
|
||||
// hover 和 active 背景色
|
||||
const activeBgColor = computed(() => {
|
||||
if (isDark.value) {
|
||||
return 'rgba(255, 255, 255, 0.1)';
|
||||
return "rgba(255, 255, 255, 0.1)";
|
||||
}
|
||||
// 亮色主题 active 使用 #4f84ff
|
||||
return HOVER_COLOR;
|
||||
@@ -104,19 +106,29 @@ const activeBgColor = computed(() => {
|
||||
// 将后端数据转换为前端需要的格式
|
||||
const transformMenuData = (menus) => {
|
||||
if (!menus || menus.length === 0) {
|
||||
console.warn('菜单数据为空');
|
||||
console.warn("菜单数据为空");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// console.log('原始菜单数据:', menus);
|
||||
|
||||
|
||||
// 功能页面路径关键词,这些菜单不应该显示在侧边栏
|
||||
// 注意:只过滤真正的功能页面,不过滤包含这些关键词的父菜单
|
||||
const functionPageKeywords = ['/detail', '/add', '/edit', '/delete', '/create', '/update'];
|
||||
|
||||
const functionPageKeywords = [
|
||||
"/detail",
|
||||
"/add",
|
||||
"/edit",
|
||||
"/delete",
|
||||
"/create",
|
||||
"/update",
|
||||
];
|
||||
|
||||
// 需要隐藏的子菜单路径(这些子菜单不应该显示在侧边栏)
|
||||
const hiddenSubMenuPaths = ['/apps/knowledge/category', '/apps/knowledge/tag'];
|
||||
|
||||
const hiddenSubMenuPaths = [
|
||||
"/apps/knowledge/category",
|
||||
"/apps/knowledge/tag",
|
||||
];
|
||||
|
||||
// 判断是否是功能页面(更精确的判断)
|
||||
const isFunctionPage = (path) => {
|
||||
if (!path) return false;
|
||||
@@ -124,26 +136,28 @@ const transformMenuData = (menus) => {
|
||||
// 检查路径是否以这些关键词结尾,或者是这些关键词的组合
|
||||
// 例如:/apps/knowledge/detail 是功能页面
|
||||
// 但 /apps/knowledge 不是功能页面
|
||||
return functionPageKeywords.some(keyword => {
|
||||
return functionPageKeywords.some((keyword) => {
|
||||
// 检查路径是否以关键词结尾,或者路径中包含 /keyword/ 或 /keyword
|
||||
return lowerPath.endsWith(keyword) ||
|
||||
lowerPath.includes(`/${keyword}/`) ||
|
||||
lowerPath.endsWith(`/${keyword}`);
|
||||
return (
|
||||
lowerPath.endsWith(keyword) ||
|
||||
lowerPath.includes(`/${keyword}/`) ||
|
||||
lowerPath.endsWith(`/${keyword}`)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 判断是否是需要隐藏的子菜单
|
||||
const isHiddenSubMenu = (path) => {
|
||||
if (!path) return false;
|
||||
const lowerPath = path.toLowerCase();
|
||||
return hiddenSubMenuPaths.some(hiddenPath => {
|
||||
return lowerPath === hiddenPath || lowerPath.startsWith(hiddenPath + '/');
|
||||
return hiddenSubMenuPaths.some((hiddenPath) => {
|
||||
return lowerPath === hiddenPath || lowerPath.startsWith(hiddenPath + "/");
|
||||
});
|
||||
};
|
||||
|
||||
// 首先映射字段格式,只保留页面菜单(menu_type=1),并过滤掉功能页面和需要隐藏的子菜单
|
||||
const allMenus = menus
|
||||
.filter(menu => {
|
||||
.filter((menu) => {
|
||||
// 只显示页面菜单,不显示API权限菜单
|
||||
if (menu.menuType !== 1 && menu.menuType !== undefined) {
|
||||
// console.log('过滤掉非页面菜单:', menu);
|
||||
@@ -161,29 +175,29 @@ const transformMenuData = (menus) => {
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(menu => ({
|
||||
.map((menu) => ({
|
||||
id: menu.id,
|
||||
path: menu.path,
|
||||
icon: menu.icon || 'fa-circle',
|
||||
icon: menu.icon || "fa-circle",
|
||||
label: menu.name,
|
||||
route: menu.path,
|
||||
parentId: menu.parentId || 0,
|
||||
order: menu.order || 0,
|
||||
children: []
|
||||
children: [],
|
||||
}));
|
||||
|
||||
// console.log('过滤后的菜单数据:', allMenus);
|
||||
|
||||
// 构建菜单映射表(只包含有效的页面菜单)
|
||||
const menuMap = new Map();
|
||||
allMenus.forEach(menu => {
|
||||
allMenus.forEach((menu) => {
|
||||
menuMap.set(menu.id, menu);
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
// 如果父菜单不在当前数据中,将菜单项作为根菜单显示
|
||||
const rootMenus = [];
|
||||
allMenus.forEach(menu => {
|
||||
allMenus.forEach((menu) => {
|
||||
if (menu.parentId === 0 || !menu.parentId) {
|
||||
// 顶级菜单直接加入
|
||||
rootMenus.push(menu);
|
||||
@@ -199,34 +213,41 @@ const transformMenuData = (menus) => {
|
||||
} else {
|
||||
// 如果父菜单不存在,可能是父菜单被过滤掉了,或者父菜单不在当前返回的数据中
|
||||
// 这种情况下,将该菜单作为根菜单显示(避免菜单丢失)
|
||||
console.warn('菜单的父菜单不存在,将作为根菜单显示。菜单ID:', menu.id, '父菜单ID:', menu.parentId, '菜单路径:', menu.path);
|
||||
console.warn(
|
||||
"菜单的父菜单不存在,将作为根菜单显示。菜单ID:",
|
||||
menu.id,
|
||||
"父菜单ID:",
|
||||
menu.parentId,
|
||||
"菜单路径:",
|
||||
menu.path
|
||||
);
|
||||
rootMenus.push(menu);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// console.log('构建后的菜单树:', rootMenus);
|
||||
|
||||
// 按 order 排序(确保排序正确)
|
||||
const sortMenus = (menus) => {
|
||||
if (!menus || menus.length === 0) return;
|
||||
|
||||
|
||||
// 对当前层级的菜单进行排序
|
||||
menus.sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999; // 没有order的排在最后
|
||||
const orderB = Number(b.order) ?? 999999;
|
||||
const diff = orderA - orderB;
|
||||
|
||||
|
||||
// 如果 order 相同,按 id 排序(保证稳定性)
|
||||
if (diff === 0) {
|
||||
return (a.id || 0) - (b.id || 0);
|
||||
}
|
||||
|
||||
|
||||
return diff;
|
||||
});
|
||||
|
||||
|
||||
// 递归排序子菜单
|
||||
menus.forEach(menu => {
|
||||
menus.forEach((menu) => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
sortMenus(menu.children);
|
||||
}
|
||||
@@ -245,7 +266,7 @@ const fetchMenus = async () => {
|
||||
await menuStore.fetchMenus();
|
||||
// 菜单数据会自动通过 computed 更新
|
||||
} catch (error) {
|
||||
console.error('获取菜单异常:', error);
|
||||
console.error("获取菜单异常:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -270,22 +291,22 @@ onMounted(() => {
|
||||
themeObserver = new MutationObserver(() => {
|
||||
updateTheme();
|
||||
});
|
||||
|
||||
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
|
||||
// 初始化时检查一次主题
|
||||
updateTheme();
|
||||
|
||||
|
||||
// 延迟一点获取菜单,确保主题已初始化
|
||||
setTimeout(() => {
|
||||
fetchMenus();
|
||||
}, 100);
|
||||
|
||||
|
||||
// 监听菜单缓存刷新事件
|
||||
window.addEventListener('menu-cache-refreshed', handleMenuRefresh);
|
||||
window.addEventListener("menu-cache-refreshed", handleMenuRefresh);
|
||||
});
|
||||
|
||||
// 组件卸载时清理事件监听
|
||||
@@ -293,7 +314,7 @@ onUnmounted(() => {
|
||||
if (themeObserver) {
|
||||
themeObserver.disconnect();
|
||||
}
|
||||
window.removeEventListener('menu-cache-refreshed', handleMenuRefresh);
|
||||
window.removeEventListener("menu-cache-refreshed", handleMenuRefresh);
|
||||
});
|
||||
|
||||
// 计算属性:统一排序所有菜单项(不再区分有无子菜单)
|
||||
@@ -307,9 +328,9 @@ const sortedMenuList = computed(() => {
|
||||
}
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
|
||||
// 确保子菜单也排序
|
||||
sorted.forEach(menu => {
|
||||
sorted.forEach((menu) => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999;
|
||||
@@ -321,36 +342,36 @@ const sortedMenuList = computed(() => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return sorted;
|
||||
});
|
||||
|
||||
// 固定的仪表盘菜单项
|
||||
const dashboardMenuItem = {
|
||||
path: '/dashboard',
|
||||
label: '仪表盘',
|
||||
icon: 'fa-solid fa-gauge',
|
||||
route: '/dashboard',
|
||||
order: 0
|
||||
path: "/dashboard",
|
||||
label: "仪表盘",
|
||||
icon: "fa-solid fa-gauge",
|
||||
route: "/dashboard",
|
||||
order: 0,
|
||||
};
|
||||
|
||||
// 菜单点击事件:emit 通知父组件
|
||||
const handleMenuSelect = (index) => {
|
||||
// 如果是仪表盘路径,直接使用固定的仪表盘菜单项
|
||||
if (index === '/dashboard') {
|
||||
emit('menu-click', dashboardMenuItem);
|
||||
if (index === "/dashboard") {
|
||||
emit("menu-click", dashboardMenuItem);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const menuItem = findMenuItemByPath(list.value, index);
|
||||
if (menuItem && menuItem.path) {
|
||||
// 使用 path 而不是 route,确保菜单项有路径
|
||||
const menuToEmit = {
|
||||
...menuItem,
|
||||
route: menuItem.path, // 兼容性:添加 route 字段
|
||||
label: menuItem.name || menuItem.label
|
||||
label: menuItem.name || menuItem.label,
|
||||
};
|
||||
emit('menu-click', menuToEmit);
|
||||
emit("menu-click", menuToEmit);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -379,7 +400,7 @@ const findMenuItemByPath = (menus, path) => {
|
||||
position: relative;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
|
||||
|
||||
// 亮色主题下的特殊处理
|
||||
html:not(.dark) & {
|
||||
background-color: #062da3;
|
||||
@@ -392,14 +413,14 @@ const findMenuItemByPath = (menus, path) => {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
transition: var(--transition-fast);
|
||||
|
||||
|
||||
// 亮色主题下默认使用白色
|
||||
html:not(.dark) & {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
h3{
|
||||
h3 {
|
||||
line-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -408,7 +429,7 @@ h3{
|
||||
font-weight: bold;
|
||||
// 亮色主题使用白色,暗色主题使用默认文字颜色
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
|
||||
html:not(.dark) & {
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -419,102 +440,102 @@ h3{
|
||||
.el-menu-item {
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
|
||||
|
||||
// hover 状态:亮色主题使用 #4f84ff
|
||||
&:hover:not(.is-active) {
|
||||
background-color: #4f84ff !important;
|
||||
color: #ffffff !important;
|
||||
|
||||
|
||||
// 确保图标在 hover 状态下也有正确的颜色
|
||||
.icons {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// active 状态:使用 #4f84ff 背景色,白色文字
|
||||
&.is-active {
|
||||
background-color: #4f84ff !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
|
||||
|
||||
// 添加左侧边框作为指示器(使用更亮的颜色)
|
||||
border-left: 3px solid #ffffff;
|
||||
margin-left: -3px; // 使用负边距补偿,保持文字位置不变
|
||||
|
||||
|
||||
// 确保图标在 active 状态下也有正确的颜色
|
||||
.icons {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 子菜单项也需要相同的效果
|
||||
.el-sub-menu {
|
||||
.el-menu-item {
|
||||
&:hover:not(.is-active) {
|
||||
background-color: #4f84ff !important;
|
||||
color: #ffffff !important;
|
||||
|
||||
|
||||
.icons {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.is-active {
|
||||
background-color: #4f84ff !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
|
||||
|
||||
// 添加左侧边框作为指示器
|
||||
border-left: 3px solid #ffffff;
|
||||
margin-left: -3px; // 使用负边距补偿,保持文字位置不变
|
||||
|
||||
|
||||
.icons {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 暗色主题下的特殊处理
|
||||
html.dark & {
|
||||
.el-menu-item {
|
||||
&:hover:not(.is-active) {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
|
||||
|
||||
.icons {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(255, 255, 255, 0.12) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
border-left-color: var(--el-color-primary-light-3);
|
||||
|
||||
|
||||
.icons {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.el-sub-menu {
|
||||
.el-menu-item {
|
||||
&:hover:not(.is-active) {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
|
||||
|
||||
.icons {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(255, 255, 255, 0.12) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
border-left-color: var(--el-color-primary-light-3);
|
||||
|
||||
|
||||
.icons {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
@@ -523,4 +544,11 @@ h3{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title),
|
||||
:deep(.el-menu-item) {
|
||||
span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user