yunzer_go/pc/src/components/CommonAside.vue

472 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
<el-menu v-else
:collapse="isCollapse"
:collapse-transition="false"
:background-color="asideBgColor"
:text-color="asideTextColor"
:active-text-color="activeColor"
:active-background-color="activeBgColor"
class="el-menu-vertical-demo"
@select="handleMenuSelect"
:default-active="route.path"
>
<h3 v-if="!isCollapse">管理后台</h3>
<h3 v-else>管理</h3>
<!-- 固定的仪表盘菜单项 -->
<el-menu-item index="/dashboard">
<i :class="['icons', 'fa', 'fa-solid', 'fa-gauge']"></i>
<template #title>
<span>仪表盘</span>
</template>
</el-menu-item>
<template v-for="item in sortedMenuList" :key="item.path">
<el-menu-item
v-if="!item.children || item.children.length === 0"
:index="item.path"
>
<i :class="['icons', 'fa', item.icon]"></i>
<template #title>
<span>{{ item.label }}</span>
</template>
</el-menu-item>
<el-sub-menu
v-else
:index="item.path"
>
<template #title>
<i :class="['icons', 'fa', item.icon]"></i>
<span>{{ item.label }}</span>
</template>
<el-menu-item
v-for="subItem in item.children"
:key="subItem.path"
:index="subItem.path"
>
<i :class="['icons', 'fa', subItem.icon && typeof subItem.icon === 'string' ? subItem.icon.trim() : '']"></i>
<template #title>
<span>{{ subItem.label }}</span>
</template>
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-aside>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAllDataStore } from '@/stores';
import { getAllMenus } from '@/api/menu';
const emit = defineEmits(['menu-click']);
const router = useRouter();
const route = useRoute();
const list = ref([]);
const loading = ref(true);
const store = useAllDataStore();
const isCollapse = computed(() => store.state.isCollapse);
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
// 使用 Element Plus 的颜色变量,主题切换时会自动适配
// 这些值会被 el-menu 组件使用el-menu 会自动适配主题
// 计算是否为暗色主题(响应式)
const isDark = ref(document.documentElement.classList.contains('dark'));
// 监听主题变化
const updateTheme = () => {
isDark.value = document.documentElement.classList.contains('dark');
};
// 使用 MutationObserver 监听 html 元素的 class 变化(主题切换时更新)
let themeObserver = null;
// 自定义颜色配置
const BG_COLOR = '#062da3'; // 背景和 active 颜色
const HOVER_COLOR = '#4f84ff'; // hover 颜色
// 将 hex 颜色转换为 rgba
function hexToRgba(hex, alpha = 1) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
// 亮色主题使用自定义背景色,暗色主题使用默认背景
const asideBgColor = computed(() => {
if (isDark.value) {
return 'var(--el-bg-color)';
}
// 亮色主题使用 #062da3 背景
return BG_COLOR;
});
const asideTextColor = computed(() => {
if (isDark.value) {
return 'var(--el-text-color-primary)';
}
// 亮色主题使用白色文字
return '#ffffff';
});
const activeColor = ref('#ffffff'); // active 文字颜色为白色
// hover 和 active 背景色
const activeBgColor = computed(() => {
if (isDark.value) {
return 'rgba(255, 255, 255, 0.1)';
}
// 亮色主题 active 使用 #4f84ff
return HOVER_COLOR;
});
// 不再需要手动更新主题颜色,因为使用了 Element Plus 的 CSS 变量,会自动适配
// 将后端数据转换为前端需要的格式
const transformMenuData = (menus) => {
// 首先映射字段格式
const mappedMenus = menus.map(menu => ({
id: menu.id,
path: menu.path,
icon: menu.icon || 'fa-circle',
label: menu.name,
route: menu.path,
parentId: menu.parentId || 0,
order: menu.order || 0,
children: []
}));
// 构建树形结构
const menuMap = new Map();
const rootMenus = [];
// 先创建所有菜单的映射
mappedMenus.forEach(menu => {
menuMap.set(menu.id, menu);
});
// 构建树形结构
mappedMenus.forEach(menu => {
if (menu.parentId === 0) {
rootMenus.push(menu);
} else {
const parent = menuMap.get(menu.parentId);
if (parent) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(menu);
} else {
// 如果找不到父节点,作为根节点处理
rootMenus.push(menu);
}
}
});
// 按 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 => {
if (menu.children && menu.children.length > 0) {
sortMenus(menu.children);
}
});
};
// 先排序根菜单
sortMenus(rootMenus);
return rootMenus;
};
// 获取菜单数据
const fetchMenus = async () => {
loading.value = true;
try {
// 直接从接口获取菜单数据,不使用缓存
const res = await getAllMenus();
if (res && res.success && res.data) {
const menuData = res.data;
// 转换并排序菜单数据
const transformedMenus = transformMenuData(menuData);
list.value = transformedMenus;
} else {
console.error('获取菜单失败:', res?.message || '未知错误');
list.value = [];
loading.value = false;
return;
}
} catch (error) {
console.error('获取菜单异常:', error);
list.value = [];
} finally {
loading.value = false;
}
};
// 组件挂载时初始化主题监听和获取菜单
onMounted(() => {
// 初始化主题监听
themeObserver = new MutationObserver(() => {
updateTheme();
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
// 初始化时检查一次主题
updateTheme();
// 延迟一点获取菜单,确保主题已初始化
setTimeout(() => {
fetchMenus();
}, 100);
});
// 计算属性:统一排序所有菜单项(不再区分有无子菜单)
const sortedMenuList = computed(() => {
// 创建副本并排序,确保按 order 排序
const sorted = [...list.value].sort((a, b) => {
const orderA = Number(a.order) ?? 999999;
const orderB = Number(b.order) ?? 999999;
if (orderA === orderB) {
return (a.id || 0) - (b.id || 0);
}
return orderA - orderB;
});
// 确保子菜单也排序
sorted.forEach(menu => {
if (menu.children && menu.children.length > 0) {
menu.children.sort((a, b) => {
const orderA = Number(a.order) ?? 999999;
const orderB = Number(b.order) ?? 999999;
if (orderA === orderB) {
return (a.id || 0) - (b.id || 0);
}
return orderA - orderB;
});
}
});
return sorted;
});
// 固定的仪表盘菜单项
const dashboardMenuItem = {
path: '/dashboard',
label: '仪表盘',
icon: 'fa-solid fa-gauge',
route: '/dashboard',
order: 0
};
// 菜单点击事件emit 通知父组件
const handleMenuSelect = (index) => {
// 如果是仪表盘路径,直接使用固定的仪表盘菜单项
if (index === '/dashboard') {
emit('menu-click', dashboardMenuItem);
return;
}
const menuItem = findMenuItemByPath(list.value, index);
if (menuItem && menuItem.route) {
emit('menu-click', menuItem);
}
};
// 递归查找菜单项
const findMenuItemByPath = (menus, path) => {
for (const menu of menus) {
if (menu.path === path) {
return menu;
}
if (menu.children && menu.children.length > 0) {
const found = findMenuItemByPath(menu.children, path);
if (found) return found;
}
}
return null;
};
</script>
<style scoped lang="less">
.common-aside {
height: 100%;
// 亮色主题使用 primary 浅色暗色主题使用默认背景
background-color: var(--el-bg-color);
transition: width 0.3s, background-color 0.3s ease;
overflow: hidden;
position: relative;
display: block !important;
visibility: visible !important;
// 亮色主题下的特殊处理
html:not(.dark) & {
background-color: #062da3;
}
}
.icons {
width: 16px;
height: 16px;
margin-right: 8px;
font-size: 16px;
transition: var(--transition-fast);
// 亮色主题下默认使用白色
html:not(.dark) & {
color: rgba(255, 255, 255, 0.9);
}
}
h3{
line-height: 80px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
// 亮色主题使用白色暗色主题使用默认文字颜色
color: var(--el-text-color-primary);
html:not(.dark) & {
color: #ffffff;
}
}
// 菜单项样式优化 hover active 状态的视觉效果
:deep(.el-menu) {
.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;
}
}
}
}
}
}
</style>