修复路由和主题

This commit is contained in:
扫地僧 2025-11-02 11:47:51 +08:00
parent bfba8d7ad6
commit 01c47ccbd4
13 changed files with 561 additions and 254 deletions

View File

@ -1,79 +1,8 @@
// Element Plus 主题变量覆盖 // Element Plus Message z-index
:root { :root {
// 基础颜色
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
// 背景颜色
--el-bg-color: #ffffff;
--el-bg-color-page: #f2f3f5;
--el-bg-color-overlay: #ffffff;
// 文字颜色
--el-text-color-primary: #303133;
--el-text-color-regular: #606266;
--el-text-color-secondary: #909399;
--el-text-color-placeholder: #a8abb2;
--el-text-color-disabled: #c0c4cc;
// 边框颜色
--el-border-color: #dcdfe6;
--el-border-color-light: #e4e7ed;
--el-border-color-lighter: #ebeef5;
--el-border-color-extra-light: #f2f6fc;
--el-border-color-dark: #d4d7de;
--el-border-color-darker: #cdd0d6;
// 填充颜色
--el-fill-color: #f0f2f5;
--el-fill-color-light: #f5f7fa;
--el-fill-color-lighter: #fafafa;
--el-fill-color-extra-light: #fafcff;
--el-fill-color-dark: #ebedf0;
--el-fill-color-darker: #e6e8eb;
--el-fill-color-blank: #ffffff;
// Message z-index
--el-message-z-index: 9999; --el-message-z-index: 9999;
} }
// 暗色主题
[data-theme="dark"] {
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
--el-bg-color: #1d1e1f;
--el-bg-color-page: #141414;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
--el-text-color-secondary: #a3a6ad;
--el-text-color-placeholder: #6c6e72;
--el-text-color-disabled: #6c6e72;
--el-border-color: #4c4d4f;
--el-border-color-light: #414243;
--el-border-color-lighter: #363637;
--el-border-color-extra-light: #2b2b2c;
--el-border-color-dark: #58585b;
--el-border-color-darker: #636466;
--el-fill-color: #2b2b2c;
--el-fill-color-light: #262727;
--el-fill-color-lighter: #1d1e1f;
--el-fill-color-extra-light: #191919;
--el-fill-color-dark: #39393a;
--el-fill-color-darker: #424243;
--el-fill-color-blank: transparent;
}
// body 样式 // body 样式
body { body {
background-color: var(--el-bg-color-page); background-color: var(--el-bg-color-page);
@ -91,9 +20,6 @@ body {
transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s; transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
} }
[data-theme="dark"] .container-box {
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.09);
}
.header-bar { .header-bar {
display: flex; display: flex;
@ -106,3 +32,12 @@ body {
justify-content: flex-end; justify-content: flex-end;
margin: 14px 0 0 0; margin: 14px 0 0 0;
} }
// 修复 ElMessage 显示问题
// 只修复定位,保持 Element Plus 官方样式
.el-message {
// 确保消息固定在页面顶部中央,不受父容器影响
position: fixed !important;
z-index: var(--el-message-z-index, 9999) !important;
pointer-events: auto !important;
}

View File

@ -9,7 +9,7 @@
:background-color="asideBgColor" :background-color="asideBgColor"
:text-color="asideTextColor" :text-color="asideTextColor"
:active-text-color="activeColor" :active-text-color="activeColor"
active-background-color="activeBgColor" :active-background-color="activeBgColor"
class="el-menu-vertical-demo" class="el-menu-vertical-demo"
@select="handleMenuSelect" @select="handleMenuSelect"
:default-active="route.path" :default-active="route.path"
@ -73,31 +73,61 @@ const loading = ref(true);
const store = useAllDataStore(); const store = useAllDataStore();
const isCollapse = computed(() => store.state.isCollapse); const isCollapse = computed(() => store.state.isCollapse);
const width = computed(() => store.state.isCollapse ? '64px' : '180px'); const width = computed(() => store.state.isCollapse ? '64px' : '180px');
const asideBgColor = ref('#0074e9'); // 使 Element Plus
const asideTextColor = ref('#ffffff'); // el-menu 使el-menu
const activeColor = ref ('#fff'); //
const activeBgColor = ref ('#0074e9'); const isDark = ref(document.documentElement.classList.contains('dark'));
// //
const updateThemeColors = () => { const updateTheme = () => {
try { isDark.value = document.documentElement.classList.contains('dark');
const root = document.documentElement;
const bgColor = getComputedStyle(root).getPropertyValue('--aside-bg-color').trim();
const textColor = getComputedStyle(root).getPropertyValue('--aside-text-color').trim();
//
if (bgColor) {
asideBgColor.value = bgColor;
}
if (textColor) {
asideTextColor.value = textColor;
}
} catch (e) {
console.warn('更新主题颜色失败:', e);
//
}
}; };
// 使 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 transformMenuData = (menus) => {
// //
@ -213,25 +243,25 @@ const fetchMenus = async () => {
} }
}; };
// //
onMounted(() => { onMounted(() => {
// //
updateThemeColors(); themeObserver = new MutationObserver(() => {
updateTheme();
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
//
updateTheme();
// //
setTimeout(() => { setTimeout(() => {
fetchMenus(); fetchMenus();
}, 100); }, 100);
//
window.addEventListener('theme-change', updateThemeColors);
// CSS MutationObserver
const observer = new MutationObserver(updateThemeColors);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
}); });
// //
@ -304,12 +334,18 @@ const findMenuItemByPath = (menus, path) => {
<style scoped lang="less"> <style scoped lang="less">
.common-aside { .common-aside {
height: 100%; height: 100%;
background-color: var(--aside-bg-color, #0074e9); // 使 primary 使
background-color: var(--el-bg-color);
transition: width 0.3s, background-color 0.3s ease; transition: width 0.3s, background-color 0.3s ease;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
display: block !important; display: block !important;
visibility: visible !important; visibility: visible !important;
//
html:not(.dark) & {
background-color: #062da3;
}
} }
.icons { .icons {
@ -318,8 +354,10 @@ const findMenuItemByPath = (menus, path) => {
margin-right: 8px; margin-right: 8px;
font-size: 16px; font-size: 16px;
transition: var(--transition-fast); transition: var(--transition-fast);
&:hover {
color: var(--primary-color); // 使
html:not(.dark) & {
color: rgba(255, 255, 255, 0.9);
} }
} }
@ -330,6 +368,121 @@ h3{
justify-content: center; justify-content: center;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #fff; // 使使
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> </style>

View File

@ -46,7 +46,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { useAllDataStore } from "@/stores"; import { useAllDataStore } from "@/stores";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@ -138,6 +138,73 @@ const handleCommand = (command) => {
router.push('/login'); 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> </script>
<style scoped lang="less"> <style scoped lang="less">
@ -148,9 +215,17 @@ const handleCommand = (command) => {
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0 40px; padding: 0 40px;
background-color: var(--header-bg-color, #0074e9); // 使 Element Plus
color: var(--header-text-color, #fff); background-color: var(--el-bg-color);
transition: background-color 0.3s ease, color 0.3s ease; 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;
// 使 #062da3
html:not(.dark) & {
background-color: #062da3;
border-bottom-color: rgba(79, 132, 255, 0.3);
}
} }
.icons { .icons {
@ -164,13 +239,15 @@ const handleCommand = (command) => {
.el-button { .el-button {
margin-right: 20px; margin-right: 20px;
background-color: rgba(255, 255, 255, 0.1); // 使 Element Plus
border-color: rgba(255, 255, 255, 0.2); background-color: var(--el-fill-color-light);
color: var(--header-text-color, #fff); border-color: var(--el-border-color);
color: var(--el-text-color-primary);
&:hover { &:hover {
background-color: rgba(255, 255, 255, 0.2); background-color: var(--el-fill-color);
border-color: rgba(255, 255, 255, 0.3); border-color: var(--el-border-color-dark);
color: var(--el-color-primary);
} }
} }
} }
@ -186,8 +263,13 @@ const handleCommand = (command) => {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3); // 使 Element Plus
border: 2px solid var(--el-border-color);
transition: border-color 0.3s ease; transition: border-color 0.3s ease;
&:hover {
border-color: var(--el-color-primary);
}
} }
.el-dropdown-link { .el-dropdown-link {
@ -234,16 +316,17 @@ const handleCommand = (command) => {
} }
:deep(.bread) { :deep(.bread) {
// 使
.el-breadcrumb__inner { .el-breadcrumb__inner {
color: var(--header-text-color, #fff) !important; color: #ffffff !important;
} }
.el-breadcrumb__inner.is-link { .el-breadcrumb__inner.is-link {
color: var(--header-text-color, #fff) !important; color: #ffffff !important;
cursor: pointer !important; cursor: pointer !important;
&:hover { &:hover {
opacity: 0.8; color: rgba(255, 255, 255, 0.8) !important;
} }
} }
} }

View File

@ -1,6 +1,10 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from '@/App.vue' import App from '@/App.vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 导入 Element Plus 样式(必须)
import 'element-plus/dist/index.css'
// 导入 Element Plus 暗黑模式样式
import 'element-plus/theme-chalk/dark/css-vars.css'
import '@/assets/less/index.less' import '@/assets/less/index.less'
import '@/assets/css/all.min.css' import '@/assets/css/all.min.css'
import router from './router' import router from './router'
@ -32,9 +36,7 @@ authStore.checkAuth()
// 如果用户已登录,在应用启动时加载动态路由 // 如果用户已登录,在应用启动时加载动态路由
if (authStore.isLoggedIn) { if (authStore.isLoggedIn) {
loadAndAddDynamicRoutes().then(() => { loadAndAddDynamicRoutes().catch(err => {
// console.log('应用启动时已加载动态路由');
}).catch(err => {
console.error('应用启动时加载动态路由失败:', err); console.error('应用启动时加载动态路由失败:', err);
}); });
} }

View File

@ -47,67 +47,91 @@ const router = createRouter({
// 动态路由加载状态 // 动态路由加载状态
let dynamicRoutesAdded = false; let dynamicRoutesAdded = false;
// 路由加载 Promise用于确保路由加载完成
let routesLoadingPromise = null;
// 从 API 加载并添加动态路由 // 从 API 加载并添加动态路由
export async function loadAndAddDynamicRoutes() { export async function loadAndAddDynamicRoutes() {
// 如果已经有加载中的 Promise直接返回它
if (routesLoadingPromise) {
return routesLoadingPromise;
}
// 如果已经加载过,直接返回
if (dynamicRoutesAdded) { if (dynamicRoutesAdded) {
return Promise.resolve(); return Promise.resolve();
} }
try { // 创建加载 Promise
const { getAllMenus } = await import("@/api/menu"); routesLoadingPromise = (async () => {
const res = await getAllMenus(); try {
console.log('[路由加载] 开始从 API 加载动态路由...');
if (res && res.success && res.data) { // 直接从 API 获取菜单数据
// 保存菜单到 localStorage const { getAllMenus } = await import("@/api/menu");
localStorage.setItem('menus', JSON.stringify(res.data)); const res = await getAllMenus();
// 添加动态路由
addDynamicRoutes(res.data); if (res && res.success && res.data) {
return Promise.resolve(); console.log('[路由加载] API 返回菜单数量:', res.data.length);
} else { // 添加动态路由
// 如果 API 失败,尝试从 localStorage 加载 addDynamicRoutes(res.data);
const cachedMenus = JSON.parse(localStorage.getItem('menus') || '[]'); console.log('[路由加载] 动态路由加载完成');
if (cachedMenus.length) { dynamicRoutesAdded = true;
addDynamicRoutes(cachedMenus); routesLoadingPromise = null;
return Promise.resolve();
} else {
console.warn('[路由加载] API 返回数据格式异常:', res);
dynamicRoutesAdded = true;
routesLoadingPromise = null;
return Promise.resolve(); return Promise.resolve();
} }
} catch (error) {
console.error('[路由加载] 加载动态路由失败:', error);
// 即使出错也标记为已加载,避免无限重试
dynamicRoutesAdded = true;
routesLoadingPromise = null;
return Promise.resolve(); return Promise.resolve();
} }
} catch (error) { })();
console.error('加载动态路由失败:', error);
// 如果 API 失败,尝试从 localStorage 加载 return routesLoadingPromise;
const cachedMenus = JSON.parse(localStorage.getItem('menus') || '[]');
if (cachedMenus.length) {
addDynamicRoutes(cachedMenus);
return Promise.resolve();
}
return Promise.resolve();
}
} }
// 添加动态路由到 Main 的 children 中 // 添加动态路由到 Main 的 children 中
function addDynamicRoutes(menus) { function addDynamicRoutes(menus) {
if (dynamicRoutesAdded || !menus?.length) { if (!menus?.length) {
console.warn('[路由加载] 菜单数据为空,跳过添加动态路由');
return; return;
} }
console.log('[路由加载] 开始添加动态路由,菜单数量:', menus.length);
// 如果已经添加过,先移除旧路由(刷新时可能需要重新添加)
if (dynamicRoutesAdded) {
console.log('[路由加载] 检测到已添加的路由,先移除旧路由');
// 移除 Main 路由以便重新添加
if (router.hasRoute('Main')) {
router.removeRoute('Main');
}
dynamicRoutesAdded = false;
}
// 过滤掉 ID 为 1 的仪表盘菜单,因为它是静态路由 // 过滤掉 ID 为 1 的仪表盘菜单,因为它是静态路由
const filteredMenus = menus.filter(menu => menu.id !== 1); const filteredMenus = menus.filter(menu => menu.id !== 1);
const dynamicRoutes = convertMenusToRoutes(filteredMenus); const dynamicRoutes = convertMenusToRoutes(filteredMenus);
console.log('[路由加载] 转换后的动态路由数量:', dynamicRoutes.length);
// 打印路由树结构(只打印路径信息,不序列化组件) console.log('[路由加载] 动态路由列表:', dynamicRoutes.map(r => ({
// console.log('生成的路由树:', dynamicRoutes.map(r => ({ path: r.path,
// path: r.path, name: r.name,
// name: r.name, hasComponent: !!r.component,
// hasComponent: !!r.component, childrenCount: r.children?.length || 0
// childrenCount: r.children?.length || 0 })));
// })));
// 获取主路由 // 获取主路由
const mainRoute = router.getRoutes().find(r => r.name === 'Main'); const mainRoute = router.getRoutes().find(r => r.name === 'Main');
if (!mainRoute) { if (!mainRoute) {
console.error('找不到 Main 路由'); console.error('[路由加载] 找不到 Main 路由');
return; return;
} }
@ -116,10 +140,12 @@ function addDynamicRoutes(menus) {
const dashboardRoute = staticChildren.find(r => r.name === 'Dashboard'); const dashboardRoute = staticChildren.find(r => r.name === 'Dashboard');
// 移除旧的主路由 // 移除旧的主路由
router.removeRoute('Main'); if (router.hasRoute('Main')) {
router.removeRoute('Main');
}
// 重新添加主路由,仪表盘路由放在第一个,后面跟动态路由 // 重新添加主路由,仪表盘路由放在第一个,后面跟动态路由
router.addRoute({ const newMainRoute = {
...mainRoute, ...mainRoute,
redirect: "/dashboard", // 始终重定向到仪表盘 redirect: "/dashboard", // 始终重定向到仪表盘
children: [ children: [
@ -128,7 +154,10 @@ function addDynamicRoutes(menus) {
// 动态路由跟在后面 // 动态路由跟在后面
...dynamicRoutes ...dynamicRoutes
] ]
}); };
router.addRoute(newMainRoute);
console.log('[路由加载] Main 路由已更新,子路由总数:', newMainRoute.children.length);
// 添加知识库的子路由(详情页和编辑页) // 添加知识库的子路由(详情页和编辑页)
// 直接在 Main 路由下添加完整路径的子路由 // 直接在 Main 路由下添加完整路径的子路由
@ -152,30 +181,6 @@ function addDynamicRoutes(menus) {
}); });
dynamicRoutesAdded = true; dynamicRoutesAdded = true;
// 打印路由信息用于调试
const finalMainRoute = router.getRoutes().find(r => r.name === 'Main');
// console.log('动态路由已添加:', {
// redirect: "/dashboard",
// staticRoutesCount: dashboardRoute ? 1 : 0,
// dynamicRoutesCount: dynamicRoutes.length,
// totalChildrenCount: (dashboardRoute ? 1 : 0) + dynamicRoutes.length,
// childrenPaths: finalMainRoute?.children?.map(c => ({
// path: c.path,
// name: c.name,
// isStatic: c.meta?.isStatic || false,
// metaPath: c.meta?.menuPath
// }))
// });
// 测试路由解析(使用完整路径)
const testResolve = router.resolve('/dashboard');
// console.log('测试路由解析 /dashboard:', {
// matched: testResolve.matched.map(m => ({ name: m.name, path: m.path })),
// fullPath: testResolve.fullPath,
// name: testResolve.name,
// hasMatched: testResolve.matched.length > 0
// });
} }
// 查找第一个有效的路由(有组件的路由) // 查找第一个有效的路由(有组件的路由)
@ -222,16 +227,83 @@ router.beforeEach(async (to, from, next) => {
// 3. 已登录,确保动态路由已加载(必须在检查 404 之前) // 3. 已登录,确保动态路由已加载(必须在检查 404 之前)
if (!dynamicRoutesAdded) { if (!dynamicRoutesAdded) {
console.log('[路由守卫] 开始加载动态路由, 目标路径:', to.path);
await loadAndAddDynamicRoutes(); await loadAndAddDynamicRoutes();
// 路由加载后,使用原始路径重新导航(确保路由表已更新) console.log('[路由守卫] 动态路由加载完成, dynamicRoutesAdded:', dynamicRoutesAdded);
// 如果 to 已经是 404使用原始路径而不是 404 路由对象
const targetPath = to.matched.length === 0 || to.name === "NotFound" // 如果路由加载后仍然未添加API 失败)
? to.path // 如果匹配失败或者是 404使用原始路径 if (!dynamicRoutesAdded) {
: to.fullPath; console.warn('[路由守卫] 路由加载失败,可能是 API 错误或 token 无效');
next({ path: targetPath, replace: true }); // 重新检查 token如果 token 不存在或无效,跳转到登录页
const currentToken = localStorage.getItem('token');
if (!currentToken) {
console.log('[路由守卫] 未找到 token跳转到登录页');
next({ path: "/login", query: { redirect: to.path } });
return;
}
// 如果 token 存在但路由加载失败,可能是 token 过期或无效,清除 token 并跳转登录
console.warn('[路由守卫] Token 存在但路由加载失败,清除 token 并跳转登录页');
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
next({ path: "/login", query: { redirect: to.path } });
return;
}
// 路由加载完成后等待一个tick确保Vue Router内部路由表已更新
await new Promise(resolve => setTimeout(resolve, 0));
// 重新解析路径检查是否能匹配
const resolved = router.resolve(to.path);
console.log('[路由守卫] 路由解析结果:', {
path: to.path,
resolved: resolved.path,
matched: resolved.matched.length,
name: resolved.name,
matchedRoutes: resolved.matched.map(m => ({ name: m.name, path: m.path }))
});
// 如果能匹配到路由使用完整路径重新导航确保Vue Router正确更新
if (resolved.matched.length > 0 && resolved.name !== "NotFound") {
console.log('[路由守卫] 路由匹配成功,重新导航确保正确加载');
next({ path: to.fullPath || to.path, replace: true });
return;
}
// 如果无法匹配,使用原始路径重新导航
console.log('[路由守卫] 路由未匹配,重新导航到:', to.path);
next({ path: to.fullPath || to.path, replace: true });
return; return;
} }
// 3.5 如果路由已加载但当前路径未匹配,可能是刷新问题
// 注意:这里需要排除根路径和已知的静态路由
if (to.matched.length === 0 && to.name !== "NotFound" && to.path !== "/" && to.path !== "/dashboard") {
console.log('[路由守卫] 路由已加载但未匹配,尝试重新解析:', to.path);
// 重新解析路径,看看是否能够匹配
const resolved = router.resolve(to.path);
console.log('[路由守卫] 重新解析结果:', {
matched: resolved.matched.length,
name: resolved.name,
matchedRoutes: resolved.matched.map(m => ({ name: m.name, path: m.path }))
});
if (resolved.matched.length > 0 && resolved.name !== "NotFound") {
// 能够匹配,说明路由表正常,使用路径重新导航
console.log('[路由守卫] 重新解析匹配成功,重新导航');
next({ path: to.path, replace: true });
return;
} else {
console.warn('[路由守卫] 路由确实无法匹配,可能是路径错误:', to.path);
// 打印所有已注册的路由用于调试
const allRoutes = router.getRoutes();
console.log('[路由守卫] 所有已注册的路由:', allRoutes.map(r => ({
name: r.name,
path: r.path,
children: r.children?.map(c => ({ name: c.name, path: c.path }))
})));
}
}
// 4. 已登录且路由已加载,检查是否是 404 // 4. 已登录且路由已加载,检查是否是 404
// 注意:这里要在路由加载完成后才能正确判断是否是 404 // 注意:这里要在路由加载完成后才能正确判断是否是 404
if (to.name === "NotFound") { if (to.name === "NotFound") {

View File

@ -39,10 +39,42 @@ import { ref as vueRef } from 'vue';
export const useTabsStore = defineTabsStore('tabs', () => { export const useTabsStore = defineTabsStore('tabs', () => {
// 固定首页tab // 固定首页tab
const defaultDashboardPath = '/dashboard'; const defaultDashboardPath = '/dashboard';
const tabList = vueRef([
{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }, // 从 localStorage 恢复 tabs 状态
]); function loadTabsFromStorage() {
const activeTab = vueRef(defaultDashboardPath); try {
const savedTabs = localStorage.getItem('tabs_list');
const savedActiveTab = localStorage.getItem('active_tab');
if (savedTabs) {
const tabs = JSON.parse(savedTabs);
// 确保至少包含首页
const hasDashboard = tabs.some(t => t.fullPath === defaultDashboardPath);
if (!hasDashboard) {
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' });
}
return tabs;
}
} catch (e) {
console.warn('恢复 tabs 失败:', e);
}
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }];
}
// 保存 tabs 到 localStorage
function saveTabsToStorage(tabs, active) {
try {
localStorage.setItem('tabs_list', JSON.stringify(tabs));
if (active) {
localStorage.setItem('active_tab', active);
}
} catch (e) {
console.warn('保存 tabs 失败:', e);
}
}
const tabList = vueRef(loadTabsFromStorage());
const savedActiveTab = localStorage.getItem('active_tab');
const activeTab = vueRef(savedActiveTab || defaultDashboardPath);
// 添加tab若已存在则激活 // 添加tab若已存在则激活
function addTab(tab) { function addTab(tab) {
@ -51,6 +83,7 @@ export const useTabsStore = defineTabsStore('tabs', () => {
tabList.value.push(tab); tabList.value.push(tab);
} }
activeTab.value = tab.fullPath; activeTab.value = tab.fullPath;
saveTabsToStorage(tabList.value, activeTab.value);
} }
// 删除指定tab并切换激活tab // 删除指定tab并切换激活tab
@ -69,6 +102,7 @@ export const useTabsStore = defineTabsStore('tabs', () => {
activeTab.value = defaultDashboardPath; activeTab.value = defaultDashboardPath;
} }
} }
saveTabsToStorage(tabList.value, activeTab.value);
} }
} }
@ -77,12 +111,20 @@ export const useTabsStore = defineTabsStore('tabs', () => {
tabList.value = tabList.value.filter( tabList.value = tabList.value.filter(
(t) => t.fullPath === defaultDashboardPath || t.fullPath === activeTab.value (t) => t.fullPath === defaultDashboardPath || t.fullPath === activeTab.value
); );
saveTabsToStorage(tabList.value, activeTab.value);
} }
// 关闭全部,只留首页 // 关闭全部,只留首页
function closeAll() { function closeAll() {
tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath); tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath);
activeTab.value = defaultDashboardPath; activeTab.value = defaultDashboardPath;
saveTabsToStorage(tabList.value, activeTab.value);
}
// 设置激活tab不触发路由跳转仅用于更新状态
function setActiveTab(fullPath) {
activeTab.value = fullPath;
saveTabsToStorage(tabList.value, activeTab.value);
} }
return { return {
@ -92,5 +134,7 @@ export const useTabsStore = defineTabsStore('tabs', () => {
removeTab, removeTab,
closeOthers, closeOthers,
closeAll, closeAll,
setActiveTab,
saveTabsToStorage,
}; };
}); });

View File

@ -3,7 +3,7 @@ import CommonAside from '@/components/CommonAside.vue';
import CommonHeader from '@/components/CommonHeader.vue'; import CommonHeader from '@/components/CommonHeader.vue';
import { useTabsStore } from '@/stores'; import { useTabsStore } from '@/stores';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { ref, watch, reactive, nextTick } from 'vue'; import { ref, watch, reactive, nextTick, onMounted } from 'vue';
import { More, DArrowRight } from '@element-plus/icons-vue' import { More, DArrowRight } from '@element-plus/icons-vue'
const tabsStore = useTabsStore(); const tabsStore = useTabsStore();
@ -11,7 +11,53 @@ const router = useRouter();
const route = useRoute(); const route = useRoute();
const defaultDashboardPath = '/dashboard'; const defaultDashboardPath = '/dashboard';
// tab使
function restoreTabFromRoute() {
const currentPath = route.fullPath;
const currentName = route.name;
const currentMeta = route.meta || {};
// tabList
const existTab = tabsStore.tabList.find(t => t.fullPath === currentPath);
if (!existTab) {
// meta 使 name
const title = currentMeta.title || currentName || '页面';
// 使 addTab localStorage
tabsStore.addTab({
title: title,
fullPath: currentPath,
name: currentName || title,
icon: currentMeta.icon
});
} else {
//
tabsStore.setActiveTab(currentPath);
}
}
// tab
onMounted(() => {
//
nextTick(() => {
// localStorage tabs store
// tab
restoreTabFromRoute();
});
});
// / tab
watch(
() => route.fullPath,
(newPath) => {
if (newPath) {
restoreTabFromRoute();
}
},
{ immediate: false }
);
// 1. /Tab // 1. /Tab
// watch(tabsStore.activeTab)
const handleAsideMenuClick = (menuItem) => { const handleAsideMenuClick = (menuItem) => {
tabsStore.addTab({ tabsStore.addTab({
title: menuItem.label, title: menuItem.label,
@ -19,9 +65,8 @@ const handleAsideMenuClick = (menuItem) => {
name: menuItem.label, name: menuItem.label,
icon: menuItem.icon icon: menuItem.icon
}); });
if (route.fullPath !== menuItem.path) { // router.push watch
router.push(menuItem.path); // keep-alive
}
}; };
const tabsCloseTab = (targetKey) => { const tabsCloseTab = (targetKey) => {
tabsStore.removeTab(targetKey); tabsStore.removeTab(targetKey);
@ -40,13 +85,24 @@ const closeAll = () => {
router.push(defaultDashboardPath); router.push(defaultDashboardPath);
}; };
// tabtab // tabtab
// 使 immediate: false 使 flush: 'post' DOM
watch( watch(
() => tabsStore.activeTab, () => tabsStore.activeTab,
(newVal) => { (newVal, oldVal) => {
if (newVal && router.currentRoute.value.fullPath !== newVal) { // oldVal undefined
router.push(newVal); if (newVal && oldVal !== undefined && router.currentRoute.value.fullPath !== newVal) {
// 使 nextTick keep-alive
nextTick(() => {
router.push(newVal).catch(err => {
//
if (err.name !== 'NavigationDuplicated') {
console.error('路由跳转失败:', err);
}
});
});
} }
} },
{ flush: 'post' }
); );
// ========== ========== // // ========== ========== //
@ -173,6 +229,7 @@ function closeAllTabs() {
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
// overflow-y: auto fixed fixed viewport
.multi-tabs-wrapper { .multi-tabs-wrapper {
position: relative; position: relative;
zoom: 1; zoom: 1;

View File

@ -311,9 +311,6 @@ const handleSubmit = () => {
share: formData.share, // Added share in params share: formData.share, // Added share in params
}; };
//
// console.log(":", JSON.stringify(submitData, null, 2));
if (isEdit.value && currentId !== "new") { if (isEdit.value && currentId !== "new") {
// id JSON tag "id" // id JSON tag "id"
const knowledgeId = parseInt(currentId as string); const knowledgeId = parseInt(currentId as string);

View File

@ -64,7 +64,6 @@ const handleUploadImage = async (file: File, insertFn: (url: string, alt?: strin
fullUrl = `${base}${url}`; fullUrl = `${base}${url}`;
} }
console.log('图片上传成功URL:', fullUrl);
insertFn(fullUrl, file.name, fullUrl); insertFn(fullUrl, file.name, fullUrl);
ElMessage.success('图片上传成功'); ElMessage.success('图片上传成功');
} else { } else {
@ -97,7 +96,6 @@ const handleUploadVideo = async (file: File, insertFn: (url: string, poster?: st
fullUrl = `${base}${url}`; fullUrl = `${base}${url}`;
} }
console.log('视频上传成功URL:', fullUrl);
insertFn(fullUrl, ''); insertFn(fullUrl, '');
ElMessage.success('视频上传成功'); ElMessage.success('视频上传成功');
} else { } else {
@ -130,7 +128,6 @@ const handleUploadAttachment = async (file: File, insertFn: (url: string, text?:
fullUrl = `${base}${url}`; fullUrl = `${base}${url}`;
} }
console.log('附件上传成功URL:', fullUrl);
insertFn(fullUrl, file.name); insertFn(fullUrl, file.name);
ElMessage.success('附件上传成功'); ElMessage.success('附件上传成功');
} else { } else {

View File

@ -494,16 +494,13 @@ const copyFileUrl = (file: any) => {};
const downloadFile = (file: any) => { const downloadFile = (file: any) => {
// //
console.log("下载文件:", file.name || file.file_name);
}; };
const deleteFile = (file: any) => { const deleteFile = (file: any) => {
// //
console.log("删除文件:", file.name || file.file_name);
}; };
onMounted(() => { onMounted(() => {
console.log("文件管理模块已加载");
}); });
</script> </script>

View File

@ -87,15 +87,15 @@ const permissions = ref<Permission[]>([
]); ]);
const handleAddPermission = () => { const handleAddPermission = () => {
console.log("添加权限"); //
}; };
const handleEdit = (permission: Permission) => { const handleEdit = (permission: Permission) => {
console.log("编辑权限:", permission); //
}; };
const handleDelete = (permission: Permission) => { const handleDelete = (permission: Permission) => {
console.log("删除权限:", permission); //
}; };
</script> </script>

View File

@ -3,7 +3,6 @@
<div class="header-bar"> <div class="header-bar">
<h2>用户管理</h2> <h2>用户管理</h2>
<div class="header-actions"> <div class="header-actions">
<el-button type="warning" @click="testElMessage">测试ElMessage</el-button>
<el-button type="primary" @click="handleAddUser"> <el-button type="primary" @click="handleAddUser">
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
添加用户 添加用户
@ -433,34 +432,6 @@ const handleDelete = async (user: User) => {
}); });
}; };
// ElMessage
const testElMessage = () => {
console.log("测试 ElMessage");
console.log("ElMessage 类型:", typeof ElMessage);
console.log("ElMessage 对象:", ElMessage);
console.log("ElMessage.success:", ElMessage.success);
console.log("ElMessage.error:", ElMessage.error);
try {
ElMessage("这是普通消息");
setTimeout(() => {
ElMessage.success("这是成功消息");
}, 500);
setTimeout(() => {
ElMessage.error("这是错误消息");
}, 1000);
setTimeout(() => {
ElMessage.warning("这是警告消息");
}, 1500);
setTimeout(() => {
ElMessage.info("这是信息消息");
}, 2000);
} catch (e) {
console.error("ElMessage 调用失败:", e);
alert("ElMessage 调用失败: " + e.message);
}
};
// //
const handleChangePassword = async (user: User) => { const handleChangePassword = async (user: User) => {
dialogTitle.value = "修改密码"; dialogTitle.value = "修改密码";

View File

@ -47,7 +47,6 @@ const handleDeleteUser = async () => {
try { try {
await deleteUserApi(userId); await deleteUserApi(userId);
userInfo.value = null; // userInfo.value = null; //
console.log("用户删除成功");
} catch (error) { } catch (error) {
console.error("删除用户失败", error); console.error("删除用户失败", error);
} }