2025-10-28 16:08:40 +08:00

398 lines
10 KiB
Vue

<template>
<div class="sidebar-container">
<!-- Logo -->
<div class="logo" @click="goHome">
<img src="@/assets/imgs/logo.webp" alt="Logo" class="logo-img" />
<!-- <transition name="fade">
<span v-if="!isCollapse" class="logo-text">管理系统</span>
</transition> -->
</div>
<!-- 调试信息 -->
<div v-if="menuData.length === 0" class="debug-info">
<p>加载失败...</p>
</div>
<!-- 菜单 -->
<el-menu
:default-active="activeMenu"
class="sidebar-menu"
background-color="var(--sidebar-bg)"
text-color="var(--text-color)"
active-text-color="var(--primary-color)"
:collapse="isCollapse"
:collapse-transition="false"
:router="false"
>
<template v-for="menu in menuData" :key="menu.id">
<template v-if="menu.parent_id === 0">
<el-menu-item :index="menu.path" @click="handleMenuClick(menu.path)">
<i :class="menu.icon"></i>
<template #title>
<span class="ml-1">{{ menu.name }}</span>
</template>
</el-menu-item>
</template>
</template>
</el-menu>
<!-- 底部设置和用户信息 -->
<div class="user-info-bottom">
<div class="bottom-settings">
<!-- <el-menu
:default-active="activeMenu === '/settings' ? '/settings' : ''"
class="sidebar-menu bottom-menu"
background-color="#f5f5f5"
text-color="#333"
active-text-color="#409eff"
:collapse="isCollapse"
:collapse-transition="false"
:router="false"
>
<el-menu-item index="/settings" @click="handleSettingsClick">
<i class="fa-solid fa-sliders"></i>
<template #title>
<span class="ml-1">设置</span>
</template>
</el-menu-item>
</el-menu> -->
</div>
<el-dropdown @command="handleUserCommand" placement="right-end">
<div class="user-dropdown-trigger">
<img
:src="getAvatarUrl()"
alt="User Avatar"
class="user-avatar-bottom"
/>
</div>
<template #dropdown>
<div v-if="!isCollapse" class="user-info-text">
<div class="user-nickname">{{ getUserName() }}</div>
</div>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
个人中心
</el-dropdown-item>
<el-dropdown-item command="logout" divided>
<el-icon><SwitchButton /></el-icon>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { User, SwitchButton } from "@element-plus/icons-vue";
import { authAPI, menuAPI } from "@/services/api";
// 定义 props
const props = defineProps({
isCollapse: {
type: Boolean,
default: false,
},
});
// 定义 emits
const emit = defineEmits(["toggle-collapse"]);
const router = useRouter();
const route = useRoute();
// 菜单数据
const menuData = ref<any[]>([]);
// 当前激活的菜单项
const activeMenu = computed(() => {
return route.path;
});
// 点击logo返回首页
const goHome = () => {
router.push("/");
};
// 创建一个函数来安全地访问 localStorage
const getAvatarUrl = () => {
try {
// 检查 window 和 localStorage 是否存在
if (typeof window !== "undefined" && window.localStorage) {
try {
const userItem = window.localStorage.getItem("user");
const userInfo = userItem ? JSON.parse(userItem) : {};
return userInfo.avatar || "/src/assets/imgs/default_avatar.png";
} catch (error) {
console.warn("解析用户信息失败:", error);
return "/src/assets/imgs/default_avatar.png";
}
}
// 如果 localStorage 不可用,返回默认头像
return "/src/assets/imgs/default_avatar.png";
} catch (error) {
console.warn("获取头像失败:", error);
return "/src/assets/imgs/default_avatar.png";
}
};
// 创建一个函数来安全地访问 localStorage 中的用户名
const getUserName = () => {
try {
// 检查 window 和 localStorage 是否存在
if (typeof window !== "undefined" && window.localStorage) {
const userItem = window.localStorage.getItem("user");
const userInfo = userItem ? JSON.parse(userItem) : {};
return userInfo.nickname || userInfo.username || "用户";
}
// 如果 localStorage 不可用,返回默认用户名
return "用户";
} catch (error) {
console.warn("获取用户名失败:", error);
return "用户";
}
};
// 用户命令处理函数
const handleUserCommand = async (command: string) => {
if (command === "profile") {
router.push("/profile");
} else if (command === "logout") {
try {
// 调用登出API
await authAPI.logout();
// 清除本地登录状态
if (typeof window !== "undefined" && window.localStorage) {
window.localStorage.removeItem("user");
window.localStorage.removeItem("token");
window.localStorage.removeItem("isAuthenticated");
}
// 跳转到登录页面
router.push("/login");
ElMessage.success("退出登录成功");
} catch (error) {
console.error("退出登录失败:", error);
ElMessage.error("退出登录失败");
}
}
};
// 菜单点击处理函数
const handleMenuClick = (path: string) => {
const clickedMenu = menuData.value.find((menu) => menu.path === path);
if (clickedMenu) {
// 将菜单ID作为路由参数传递
router.push({
path: path,
query: {
id: clickedMenu.id.toString(),
},
});
}
};
// 设置按钮点击处理函数
// const handleSettingsClick = () => {
// router.push("/settings");
// };
// 获取菜单数据
const loadMenuData = async () => {
try {
// console.log("开始加载主侧边栏菜单数据...");
const response = await menuAPI.getTopLevelMenus();
// console.log("主侧边栏菜单API响应:", response);
if (response && response.success) {
// 假设响应数据格式符合预期,直接使用 data 赋值
menuData.value =
response.data.map((item: any) => ({
id: item.Id,
name: item.Name,
path: item.Path,
parent_id: item.ParentId,
icon: item.Icon,
children: item.Children || [],
})) || [];
// console.log("主侧边栏菜单数据加载成功:", menuData.value);
} else {
console.error("获取菜单数据失败:", response?.message || "未知错误");
menuData.value = [];
}
} catch (error) {
console.error("获取菜单数据异常:", error);
menuData.value = [];
}
};
// 构建菜单树
const buildMenuTree = (menus: any[]) => {
const menuMap = new Map();
const rootMenus: any[] = [];
// 创建菜单映射
menus.forEach((menu) => {
menuMap.set(menu.id, { ...menu, children: [] });
});
// 构建树形结构
menus.forEach((menu) => {
if (menu.parent_id === 0) {
rootMenus.push(menuMap.get(menu.id));
} else {
const parent = menuMap.get(menu.parent_id);
if (parent) {
parent.children.push(menuMap.get(menu.id));
}
}
});
return rootMenus;
};
// 组件挂载时加载菜单数据
onMounted(() => {
loadMenuData();
});
</script>
<style lang="scss" scoped>
.sidebar-container {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--sidebar-bg);
transition: var(--transition-base);
.logo {
display: flex;
align-items: center;
justify-content: center;
padding: 20px 0;
border-bottom: 1px solid var(--border-color);
transition: var(--transition-base);
.logo-img {
height: 40px;
}
.logo-text {
font-size: 18px;
font-weight: bold;
color: var(--text-color);
}
}
}
.user-info-bottom {
text-align: center;
margin-top: auto;
.bottom-settings {
// margin-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.user-dropdown-trigger {
width: 40px;
height: 40px;
border-radius: var(--border-radius-full);
cursor: pointer;
transition: var(--transition-base);
margin: 20px 0;
border: 2px solid var(--border-color);
&:hover {
border-color: var(--primary-color);
transform: scale(1.05);
}
img {
object-fit: cover;
border-radius: var(--border-radius-full);
}
}
}
.bottom-menu {
border-right: none;
flex: none !important;
}
.sidebar-menu {
border-right: none;
.el-menu-item {
i {
margin-right: 0 !important;
}
}
}
/* 顶部菜单占据主要空间 */
.sidebar-menu:not(.bottom-menu) {
flex: 1;
}
// 菜单项样式 - 统一管理
.sidebar-menu :deep(.el-menu-item) {
height: 50px;
line-height: 50px;
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
transition: var(--transition-fast);
&:hover {
background-color: var(--background-hover) !important;
color: var(--primary-color) !important;
i {
color: var(--primary-color) !important;
}
}
i {
margin-right: 8px;
font-size: 16px;
transition: var(--transition-fast);
}
}
.sidebar-menu :deep(.el-menu-item.is-active) {
background-color: var(--background-hover) !important;
color: var(--primary-color) !important;
i {
color: var(--primary-color) !important;
}
}
.user-nickname {
font-size: 14px;
font-weight: bold;
color: var(--text-color);
text-align: center;
padding: 20px 40px;
}
.debug-info {
padding: 20px;
text-align: center;
color: var(--text-secondary);
font-size: 12px;
background: var(--warning-bg);
border: 1px dashed var(--warning-color);
margin: 10px;
border-radius: var(--border-radius);
}
</style>