498 lines
14 KiB
Vue
498 lines
14 KiB
Vue
<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>
|