修复路由和主题

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 {
// 基础颜色
--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;
}
// 暗色主题
[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 {
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;
}
[data-theme="dark"] .container-box {
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.09);
}
.header-bar {
display: flex;
@ -106,3 +32,12 @@ body {
justify-content: flex-end;
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"
:text-color="asideTextColor"
:active-text-color="activeColor"
active-background-color="activeBgColor"
:active-background-color="activeBgColor"
class="el-menu-vertical-demo"
@select="handleMenuSelect"
:default-active="route.path"
@ -73,31 +73,61 @@ const loading = ref(true);
const store = useAllDataStore();
const isCollapse = computed(() => store.state.isCollapse);
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
const asideBgColor = ref('#0074e9');
const asideTextColor = ref('#ffffff');
const activeColor = ref ('#fff');
const activeBgColor = ref ('#0074e9');
// 使 Element Plus
// el-menu 使el-menu
//
const isDark = ref(document.documentElement.classList.contains('dark'));
//
const updateThemeColors = () => {
try {
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);
//
}
//
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) => {
//
@ -213,25 +243,25 @@ const fetchMenus = async () => {
}
};
//
//
onMounted(() => {
//
updateThemeColors();
//
themeObserver = new MutationObserver(() => {
updateTheme();
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
//
updateTheme();
//
setTimeout(() => {
fetchMenus();
}, 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">
.common-aside {
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;
overflow: hidden;
position: relative;
display: block !important;
visibility: visible !important;
//
html:not(.dark) & {
background-color: #062da3;
}
}
.icons {
@ -318,8 +354,10 @@ const findMenuItemByPath = (menus, path) => {
margin-right: 8px;
font-size: 16px;
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;
font-size: 16px;
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>

View File

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

View File

@ -1,6 +1,10 @@
import { createApp } from 'vue'
import App from '@/App.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/css/all.min.css'
import router from './router'
@ -32,9 +36,7 @@ authStore.checkAuth()
// 如果用户已登录,在应用启动时加载动态路由
if (authStore.isLoggedIn) {
loadAndAddDynamicRoutes().then(() => {
// console.log('应用启动时已加载动态路由');
}).catch(err => {
loadAndAddDynamicRoutes().catch(err => {
console.error('应用启动时加载动态路由失败:', err);
});
}

View File

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

View File

@ -39,10 +39,42 @@ import { ref as vueRef } from 'vue';
export const useTabsStore = defineTabsStore('tabs', () => {
// 固定首页tab
const defaultDashboardPath = '/dashboard';
const tabList = vueRef([
{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' },
]);
const activeTab = vueRef(defaultDashboardPath);
// 从 localStorage 恢复 tabs 状态
function loadTabsFromStorage() {
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若已存在则激活
function addTab(tab) {
@ -51,6 +83,7 @@ export const useTabsStore = defineTabsStore('tabs', () => {
tabList.value.push(tab);
}
activeTab.value = tab.fullPath;
saveTabsToStorage(tabList.value, activeTab.value);
}
// 删除指定tab并切换激活tab
@ -69,6 +102,7 @@ export const useTabsStore = defineTabsStore('tabs', () => {
activeTab.value = defaultDashboardPath;
}
}
saveTabsToStorage(tabList.value, activeTab.value);
}
}
@ -77,12 +111,20 @@ export const useTabsStore = defineTabsStore('tabs', () => {
tabList.value = tabList.value.filter(
(t) => t.fullPath === defaultDashboardPath || t.fullPath === activeTab.value
);
saveTabsToStorage(tabList.value, activeTab.value);
}
// 关闭全部,只留首页
function closeAll() {
tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath);
activeTab.value = defaultDashboardPath;
saveTabsToStorage(tabList.value, activeTab.value);
}
// 设置激活tab不触发路由跳转仅用于更新状态
function setActiveTab(fullPath) {
activeTab.value = fullPath;
saveTabsToStorage(tabList.value, activeTab.value);
}
return {
@ -92,5 +134,7 @@ export const useTabsStore = defineTabsStore('tabs', () => {
removeTab,
closeOthers,
closeAll,
setActiveTab,
saveTabsToStorage,
};
});

View File

@ -3,7 +3,7 @@ import CommonAside from '@/components/CommonAside.vue';
import CommonHeader from '@/components/CommonHeader.vue';
import { useTabsStore } from '@/stores';
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'
const tabsStore = useTabsStore();
@ -11,7 +11,53 @@ const router = useRouter();
const route = useRoute();
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
// watch(tabsStore.activeTab)
const handleAsideMenuClick = (menuItem) => {
tabsStore.addTab({
title: menuItem.label,
@ -19,9 +65,8 @@ const handleAsideMenuClick = (menuItem) => {
name: menuItem.label,
icon: menuItem.icon
});
if (route.fullPath !== menuItem.path) {
router.push(menuItem.path);
}
// router.push watch
// keep-alive
};
const tabsCloseTab = (targetKey) => {
tabsStore.removeTab(targetKey);
@ -40,13 +85,24 @@ const closeAll = () => {
router.push(defaultDashboardPath);
};
// tabtab
// 使 immediate: false 使 flush: 'post' DOM
watch(
() => tabsStore.activeTab,
(newVal) => {
if (newVal && router.currentRoute.value.fullPath !== newVal) {
router.push(newVal);
(newVal, oldVal) => {
// oldVal undefined
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;
padding: 20px;
overflow-y: auto;
// overflow-y: auto fixed fixed viewport
.multi-tabs-wrapper {
position: relative;
zoom: 1;

View File

@ -311,9 +311,6 @@ const handleSubmit = () => {
share: formData.share, // Added share in params
};
//
// console.log(":", JSON.stringify(submitData, null, 2));
if (isEdit.value && currentId !== "new") {
// id JSON tag "id"
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}`;
}
console.log('图片上传成功URL:', fullUrl);
insertFn(fullUrl, file.name, fullUrl);
ElMessage.success('图片上传成功');
} else {
@ -97,7 +96,6 @@ const handleUploadVideo = async (file: File, insertFn: (url: string, poster?: st
fullUrl = `${base}${url}`;
}
console.log('视频上传成功URL:', fullUrl);
insertFn(fullUrl, '');
ElMessage.success('视频上传成功');
} else {
@ -130,7 +128,6 @@ const handleUploadAttachment = async (file: File, insertFn: (url: string, text?:
fullUrl = `${base}${url}`;
}
console.log('附件上传成功URL:', fullUrl);
insertFn(fullUrl, file.name);
ElMessage.success('附件上传成功');
} else {

View File

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

View File

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

View File

@ -3,7 +3,6 @@
<div class="header-bar">
<h2>用户管理</h2>
<div class="header-actions">
<el-button type="warning" @click="testElMessage">测试ElMessage</el-button>
<el-button type="primary" @click="handleAddUser">
<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) => {
dialogTitle.value = "修改密码";

View File

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