backend/src/components/CommonHeader.vue
2026-01-27 18:01:54 +08:00

498 lines
14 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>
<div class="header">
<div class="l-content">
<el-button size="small" @click="handleCollapse">
<i class="fa fa-bars"></i>
</el-button>
<!-- <el-breadcrumb separator="/" class="bread">
<el-breadcrumb-item :to="{ path: '/' }"
style="position: relative !important; top: 1px !important;">首页</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index"
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null"
style="position: relative !important; top: 1px !important;">
{{ item.label }}
</el-breadcrumb-item>
</el-breadcrumb> -->
</div>
<div class="r-content">
<!-- 返回首页按钮 -->
<el-tooltip content="返回首页" placement="top">
<el-button circle @click="goHome" class="home-btn" title="返回首页">
<el-icon>
<HomeFilled />
</el-icon>
</el-button>
</el-tooltip>
<!-- 更新缓存按钮 -->
<el-button circle :icon="Refresh" @click="refreshCache" class="refresh-cache-btn" :loading="cacheLoading"
title="更新菜单缓存" />
<!-- 主题切换按钮 -->
<el-button circle :icon="themeIcon" @click="toggleTheme" class="theme-toggle-btn"
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'" />
<!-- 消息中心 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link" style="cursor: pointer;">
<el-button circle class="message-btn" title="消息中心">
<el-icon>
<Bell />
</el-icon>
</el-button>
</span>
<template #dropdown>
<el-dropdown-menu class="message-menu" style="width: 260px;">
<el-dropdown-item disabled>暂无新消息</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link" style="cursor: pointer;">
<img :src="getImageUrl('user')" class="user" />
<span class="user-name">{{ displayName }}</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon>
<User />
</el-icon>
<span>个人中心</span>
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<el-icon>
<SwitchButton />
</el-icon>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="message-center">
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
import { useAuthStore } from "@/stores/auth";
import { logout } from "@/api/login";
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
const router = useRouter();
const route = useRoute();
interface Menu {
id: number;
name: string;
path: string;
parentId: number;
}
interface Breadcrumb {
label: string;
path: string;
}
const menuStore = useMenuStore();
const tabsStore = useTabsStore();
const cacheLoading = ref(false);
// 使用 store 中的菜单数据
const menuList = computed(() => menuStore.menus);
// 加载菜单(从 store
async function loadMenu() {
await menuStore.fetchMenus();
}
// 更新缓存(手动刷新)
async function refreshCache() {
cacheLoading.value = true;
try {
await menuStore.refreshMenus();
// 重新加载动态路由
const { loadAndAddDynamicRoutes, resetDynamicRoutes } = await import('@/router/index');
// 重置路由加载状态,强制重新加载
resetDynamicRoutes();
await loadAndAddDynamicRoutes();
// 等待路由完全加载给Vue Router一些时间更新路由表
await new Promise(resolve => setTimeout(resolve, 200));
// 触发菜单刷新事件通知CommonAside组件刷新菜单
window.dispatchEvent(new CustomEvent('menu-cache-refreshed'));
ElMessage.success('更新成功');
} catch (error) {
console.error('Failed to refresh cache', error);
ElMessage.error('更新缓存失败,请检查网络连接');
} finally {
cacheLoading.value = false;
}
}
onMounted(loadMenu);
// 根据菜单列表和当前路径计算出的面包屑导航
const breadcrumbs = computed(() => {
let chain: Breadcrumb[] = [];
let currentPath = route.path || '/';
if (currentPath === '/' || currentPath === '') {
return [{ label: '仪表盘', path: '/' }];
}
let current = menuList.value.find(m => m.path === currentPath);
if (!current) {
const candidates = menuList.value.filter(m => currentPath.startsWith(m.path));
current = candidates.sort((a, b) => b.path.length - a.path.length)[0];
}
if (!current) return [];
chain.push({ label: current.name || 'Unknown', path: current.path });
let parentId = current.parentId;
while (parentId > 0) {
let parent = menuList.value.find(m => m.id === parentId);
if (parent && !chain.some(c => c.label === parent.name)) {
chain.push({ label: parent.name || 'Unknown', path: parent.path });
parentId = parent.parentId;
} else break;
}
chain = chain.reverse();
return chain;
});
const store = useAllDataStore();
const authStore = useAuthStore();
const getImageUrl = (user) => {
return new URL(`/src/assets/images/default_avatar.png`, import.meta.url).href;
};
// 计算显示名称:优先显示昵称(用户)或姓名(员工),否则显示用户名
const displayName = computed(() => {
const user = authStore.user;
if (!user) return '';
// 如果是用户登录优先显示name
if (user.name) {
return user.name;
}
// 最后显示account
return user.account || '';
});
const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
const goHome = () => {
tabsStore.closeAll();
router.push('/home');
};
const handleCommand = async (command) => {
if (command === 'profile') {
router.push('/user/userProfile');
} else if (command === 'logout') {
try {
// 从 localStorage 获取用户信息,传递给后端
const userInfo = authStore.user;
// 先调用后端退出登录接口(记录日志)
await logout(userInfo);
} catch (error) {
// 即使后端接口失败,也继续执行前端退出逻辑
console.error('退出登录接口调用失败:', error);
}
// 清除前端存储
authStore.clearToken();
// 清除缓存中的user数据
localStorage.removeItem('user');
sessionStorage.removeItem('user');
//清除租户数据
localStorage.removeItem('tenant');
sessionStorage.removeItem('tenant');
//清除tabs_list缓存
localStorage.removeItem('tabs_list');
localStorage.removeItem('active_tab');
sessionStorage.removeItem('tabs_list');
// 清除菜单缓存
menuStore.resetMenus();
// 清除所有以 menu_cache_ 开头的本地存储项
const menuCacheKeys: string[] = [];
// 遍历 localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('menu_cache_')) {
menuCacheKeys.push(key);
}
}
// 删除 localStorage 中的 menu_cache_ 项
menuCacheKeys.forEach(key => {
localStorage.removeItem(key);
});
// 遍历 sessionStorage
const sessionMenuCacheKeys: string[] = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('menu_cache_')) {
sessionMenuCacheKeys.push(key);
}
}
// 删除 sessionStorage 中的 menu_cache_ 项
sessionMenuCacheKeys.forEach(key => {
sessionStorage.removeItem(key);
});
// 重置 tabs store 状态
const { useTabsStore } = await import('@/stores');
const tabsStore = useTabsStore();
tabsStore.resetTabs();
router.push('/login');
}
};
// Element Plus 主题切换
const THEME_STORAGE_KEY = 'element-plus-theme';
const isDark = ref(false);
// 初始化主题:从 localStorage 读取或检测系统偏好
const initTheme = () => {
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
if (savedTheme) {
isDark.value = savedTheme === 'dark';
} else {
// 检测系统偏好
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
isDark.value = prefersDark;
}
applyTheme();
};
// 应用主题:在 html 元素上添加/移除 dark 类
const applyTheme = () => {
const htmlElement = document.documentElement;
if (isDark.value) {
htmlElement.classList.add('dark');
localStorage.setItem(THEME_STORAGE_KEY, 'dark');
} else {
htmlElement.classList.remove('dark');
localStorage.setItem(THEME_STORAGE_KEY, 'light');
}
};
// 切换主题
const toggleTheme = () => {
isDark.value = !isDark.value;
applyTheme();
};
// 计算当前主题
const currentTheme = computed(() => isDark.value ? 'dark' : 'light');
// 计算主题图标
const themeIcon = computed(() => isDark.value ? Sunny : Moon);
// 组件挂载时初始化主题
let mediaQuery: MediaQueryList | null = null;
let handleChange: ((e: MediaQueryListEvent) => void) | null = null;
onMounted(() => {
initTheme();
// 监听系统主题变化
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
handleChange = (e: MediaQueryListEvent) => {
// 如果用户没有手动设置主题,则跟随系统
if (!localStorage.getItem(THEME_STORAGE_KEY)) {
isDark.value = e.matches;
applyTheme();
}
};
mediaQuery.addEventListener('change', handleChange);
});
// 组件卸载时清理
onUnmounted(() => {
if (mediaQuery && handleChange) {
mediaQuery.removeEventListener('change', handleChange);
}
});
</script>
<style scoped lang="less">
.header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 100%;
padding: 0 40px;
// 使用 Element Plus 的背景色变量暗黑模式下自动适配
background-color: var(--el-bg-color);
color: var(--el-text-color-primary);
border-bottom: 1px solid var(--el-border-color-lighter);
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
// 亮色主题下使用 #3973ff 背景
html:not(.dark) & {
background-color: #3973ff;
border-bottom-color: rgba(79, 132, 255, 0.3);
}
}
.icons {
width: 20px;
height: 20px;
}
.l-content {
display: flex;
align-items: center;
.el-button {
margin-right: 20px;
// 使用 Element Plus 的填充色变量
background-color: var(--el-fill-color-light);
border-color: var(--el-border-color);
color: var(--el-text-color-primary);
&:hover {
background-color: var(--el-fill-color);
border-color: var(--el-border-color-dark);
color: var(--el-color-primary);
}
}
}
.r-content {
position: relative;
// z-index: 1000;
display: flex;
align-items: center;
gap: 16px;
.refresh-cache-btn,
.home-btn,
.theme-toggle-btn {
// 使用 Element Plus 的填充色变量
background-color: var(--el-fill-color-light);
border-color: var(--el-border-color);
color: var(--el-text-color-primary);
margin-left: 0 !important;
&:hover {
background-color: var(--el-fill-color);
border-color: var(--el-border-color-dark);
color: var(--el-color-primary);
}
}
.user {
width: 40px;
height: 40px;
border-radius: 50%;
// 使用 Element Plus 的边框颜色变量
// border: 2px solid var(--el-border-color);
transition: border-color 0.3s ease;
&:hover {
border-color: var(--el-color-primary);
}
}
.el-dropdown-link {
display: flex;
align-items: center;
cursor: pointer;
gap: 12px;
.user-name {
font-size: 14px;
color: var(--el-text-color-primary);
font-weight: 500;
white-space: nowrap;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
// 亮色主题下使用白色
html:not(.dark) & {
color: #ffffff;
}
}
}
}
// 下拉菜单样式 - 使用全局样式覆盖
:deep(.el-dropdown) {
.el-dropdown__popper {
.el-dropdown-menu {
background-color: var(--bg-color-overlay) !important;
border-color: var(--border-color) !important;
padding: 4px 0;
.el-dropdown-menu__item {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-color-primary) !important;
transition: background-color 0.2s ease, color 0.2s ease;
span {
margin-left: 0;
}
.el-icon {
color: var(--text-color-primary) !important;
}
&:not(.is-disabled):hover {
background-color: var(--fill-color-light) !important;
color: var(--text-color-primary) !important;
}
&.is-divided {
border-top-color: var(--border-color) !important;
}
}
}
}
}
:deep(.bread) {
// 面包屑使用白色
.el-breadcrumb__inner {
color: #ffffff !important;
}
.el-breadcrumb__inner.is-link {
color: #ffffff !important;
cursor: pointer !important;
&:hover {
color: rgba(255, 255, 255, 0.8) !important;
}
}
}
:deep(.el-tabs__nav) {
height: 36px !important;
}
:deep(.el-button) {
margin-left: 0 !important;
}
</style>