fix: 🐛 混合布局左侧菜单丢失问题修复

closed #ICEVSD
This commit is contained in:
Ray.Hao
2025-06-18 16:21:34 +08:00
parent 39ef1c82f1
commit a4c67fe576
10 changed files with 31 additions and 231 deletions

View File

@@ -65,7 +65,7 @@ export function useStomp(options: UseStompOptions = {}) {
// 检查WebSocket端点是否配置 // 检查WebSocket端点是否配置
if (!brokerURL.value) { if (!brokerURL.value) {
console.error("WebSocket连接失败: 未配置WebSocket端点URL"); console.warn("WebSocket连接失败: 未配置WebSocket端点URL");
return; return;
} }
@@ -74,7 +74,7 @@ export function useStomp(options: UseStompOptions = {}) {
// 检查令牌是否为空,如果为空则不进行连接 // 检查令牌是否为空,如果为空则不进行连接
if (!currentToken) { if (!currentToken) {
console.error("WebSocket连接失败授权令牌为空请先登录"); console.warn("WebSocket连接失败授权令牌为空请先登录");
return; return;
} }

View File

@@ -85,47 +85,15 @@ const processedTopMenus = computed(() => {
}); });
}); });
const route = useRoute();
// 获取当前路由路径的顶部菜单路径
const getActiveTopMenuPath = () => {
const pathSegments = route.path.split("/").filter(Boolean);
return pathSegments.length > 0 ? `/${pathSegments[0]}` : "/";
};
// 监听路由变化,更新活跃的顶部菜单
watch(
() => route.path,
() => {
const newActiveTopMenuPath = getActiveTopMenuPath();
if (newActiveTopMenuPath !== appStore.activeTopMenuPath) {
appStore.activeTopMenu(newActiveTopMenuPath);
}
},
{ immediate: true }
);
/** /**
* 处理菜单点击事件,切换顶部菜单并加载对应的左侧菜单 * 处理菜单点击事件,切换顶部菜单并加载对应的左侧菜单
* @param routePath 点击的菜单路径 * @param routePath 点击的菜单路径
*/ */
const handleMenuSelect = (routePath: string) => { const handleMenuSelect = (routePath: string) => {
appStore.activeTopMenu(routePath); // 设置激活的顶部菜单 appStore.activeTopMenu(routePath); // 设置激活的顶部菜单
activateFirstLevelMenu(routePath); // 激活一级菜单并设置左侧二级菜单
};
/**
* 激活一级菜单并设置左侧二级菜单
* @param routePath 点击的菜单路径
*/
function activateFirstLevelMenu(routePath: string) {
permissionStore.updateSideMenu(routePath); // 更新左侧菜单 permissionStore.updateSideMenu(routePath); // 更新左侧菜单
navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单
// 使用 nextTick 确保侧边菜单更新完成后再跳转 };
nextTick(() => {
navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单
});
}
/** /**
* 跳转到左侧第一个可访问的菜单 * 跳转到左侧第一个可访问的菜单
@@ -134,43 +102,34 @@ function activateFirstLevelMenu(routePath: string) {
const navigateToFirstLeftMenu = (menus: RouteRecordRaw[]) => { const navigateToFirstLeftMenu = (menus: RouteRecordRaw[]) => {
if (menus.length === 0) return; if (menus.length === 0) return;
// 查找第一个可访问的菜单项 const [firstMenu] = menus;
const findFirstAccessibleRoute = (routes: RouteRecordRaw[]): RouteRecordRaw | null => {
for (const route of routes) {
// 跳过隐藏的菜单项
if (route.meta?.hidden) continue;
// 如果有子菜单,递归查找 // 如果第一个菜单有子菜单,递归跳转到第一个子菜单
if (route.children && route.children.length > 0) { if (firstMenu.children && firstMenu.children.length > 0) {
const childRoute = findFirstAccessibleRoute(route.children); navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[]);
if (childRoute) return childRoute; } else if (firstMenu.name) {
} else if (route.name && route.path) {
// 找到第一个有名称和路径的菜单项
return route;
}
}
return null;
};
const firstRoute = findFirstAccessibleRoute(menus);
if (firstRoute && firstRoute.name) {
console.log("🎯 Navigating to first menu:", firstRoute.name, firstRoute.path);
router.push({ router.push({
name: firstRoute.name, name: firstMenu.name,
query: query:
typeof firstRoute.meta?.params === "object" typeof firstMenu.meta?.params === "object"
? (firstRoute.meta.params as LocationQueryRaw) ? (firstMenu.meta.params as LocationQueryRaw)
: undefined, : undefined,
}); });
} }
}; };
// 当前激活的顶部菜单路径 // 获取当前路由路径的顶部菜单路径
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
onMounted(() => { onMounted(() => {
topMenus.value = permissionStore.routes.filter((item) => !item.meta || !item.meta.hidden); topMenus.value = permissionStore.routes.filter((item) => !item.meta || !item.meta.hidden);
// 初始化顶部菜单
const currentTopMenuPath =
useRoute().path.split("/").filter(Boolean).length > 1
? useRoute().path.match(/^\/[^/]+/)?.[0] || "/"
: "/";
appStore.activeTopMenu(currentTopMenuPath); // 设置激活的顶部菜单
permissionStore.updateSideMenu(currentTopMenuPath); // 更新左侧菜单
}); });
</script> </script>

View File

@@ -149,7 +149,7 @@ import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
const { t } = useI18n(); const { t } = useI18n();
import { LayoutMode, SidebarColor, ThemeMode } from "@/enums"; import { LayoutMode, SidebarColor, ThemeMode } from "@/enums";
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store"; import { useSettingsStore } from "@/store";
import { themeColorPresets } from "@/settings"; import { themeColorPresets } from "@/settings";
// 按钮图标 // 按钮图标
@@ -176,10 +176,7 @@ const layoutOptions: LayoutOption[] = [
// 使用统一的颜色预设配置 // 使用统一的颜色预设配置
const colorPresets = themeColorPresets; const colorPresets = themeColorPresets;
const route = useRoute();
const appStore = useAppStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const isDark = ref<boolean>(settingsStore.theme === ThemeMode.DARK); const isDark = ref<boolean>(settingsStore.theme === ThemeMode.DARK);
const sidebarColor = ref(settingsStore.sidebarColorScheme); const sidebarColor = ref(settingsStore.sidebarColorScheme);
@@ -221,13 +218,6 @@ const handleLayoutChange = (layout: LayoutMode) => {
if (settingsStore.layout === layout) return; if (settingsStore.layout === layout) return;
settingsStore.updateLayout(layout); settingsStore.updateLayout(layout);
if (layout === LayoutMode.MIX && route.name) {
const topLevelRoute = findTopLevelRoute(permissionStore.routes, route.name as string);
if (appStore.activeTopMenuPath !== topLevelRoute.path) {
appStore.activeTopMenu(topLevelRoute.path);
}
}
}; };
/** /**
@@ -313,39 +303,6 @@ const generateSettingsCode = (): string => {
};`; };`;
}; };
/**
* 查找路由的顶层父路由
*
* @param tree 树形数据
* @param findName 查找的名称
*/
function findTopLevelRoute(tree: any[], findName: string) {
const parentMap: any = {};
function buildParentMap(node: any, parent: any) {
parentMap[node.name] = parent;
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
buildParentMap(node.children[i], node);
}
}
}
for (let i = 0; i < tree.length; i++) {
buildParentMap(tree[i], null);
}
let currentNode = parentMap[findName];
while (currentNode) {
if (!parentMap[currentNode.name]) {
return currentNode;
}
currentNode = parentMap[currentNode.name];
}
return null;
}
/** /**
* 关闭抽屉前的回调 * 关闭抽屉前的回调
*/ */

View File

@@ -58,16 +58,15 @@
import { useRoute, useRouter, type RouteRecordRaw } from "vue-router"; import { useRoute, useRouter, type RouteRecordRaw } from "vue-router";
import { resolve } from "path-browserify"; import { resolve } from "path-browserify";
import { translateRouteTitle } from "@/utils/i18n"; import { translateRouteTitle } from "@/utils/i18n";
import { usePermissionStore, useTagsViewStore, useSettingsStore, useAppStore } from "@/store"; import { usePermissionStore, useTagsViewStore, useSettingsStore } from "@/store";
import { LayoutMode } from "@/enums";
// ========================= 类型定义 =========================
interface ContextMenu { interface ContextMenu {
visible: boolean; visible: boolean;
x: number; x: number;
y: number; y: number;
} }
// ========================= 组合式 API =========================
const instance = getCurrentInstance(); const instance = getCurrentInstance();
const proxy = instance?.proxy; const proxy = instance?.proxy;
const router = useRouter(); const router = useRouter();
@@ -77,9 +76,7 @@ const route = useRoute();
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const appStore = useAppStore();
// ========================= 响应式数据 =========================
const { visitedViews } = storeToRefs(tagsViewStore); const { visitedViews } = storeToRefs(tagsViewStore);
const layout = computed(() => settingsStore.layout); const layout = computed(() => settingsStore.layout);
@@ -96,7 +93,6 @@ const contextMenu = reactive<ContextMenu>({
// 滚动条引用 // 滚动条引用
const scrollbarRef = ref(); const scrollbarRef = ref();
// ========================= 计算属性 =========================
// 路由映射缓存,提升查找性能 // 路由映射缓存,提升查找性能
const routePathMap = computed(() => { const routePathMap = computed(() => {
const map = new Map<string, TagView>(); const map = new Map<string, TagView>();
@@ -121,7 +117,6 @@ const isLastView = computed(() => {
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath; return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath;
}); });
// ========================= 核心函数 =========================
/** /**
* 递归提取固定标签 * 递归提取固定标签
*/ */
@@ -155,45 +150,6 @@ const extractAffixTags = (routes: RouteRecordRaw[], basePath = "/"): TagView[] =
return affixTags; return affixTags;
}; };
/**
* 查找路由的顶级父节点
*/
const findTopLevelParent = (
routes: RouteRecordRaw[],
targetName: string
): RouteRecordRaw | null => {
// 构建父子关系映射
const parentMap = new Map<string, RouteRecordRaw>();
const buildMap = (routeList: RouteRecordRaw[], parent: RouteRecordRaw | null = null) => {
routeList.forEach((route) => {
if (parent) {
parentMap.set(route.name as string, parent);
}
if (route.children?.length) {
buildMap(route.children, route);
}
});
};
buildMap(routes);
// 向上查找顶级父节点
let current = parentMap.get(targetName);
let topLevel = current;
while (current) {
const parent = parentMap.get(current.name as string);
if (!parent) break;
topLevel = current;
current = parent;
}
return topLevel || null;
};
// ========================= 标签操作 =========================
/** /**
* 初始化固定标签 * 初始化固定标签
*/ */
@@ -225,7 +181,7 @@ const addCurrentTag = () => {
}; };
/** /**
* 更新当前标签(优化版本) * 更新当前标签
*/ */
const updateCurrentTag = () => { const updateCurrentTag = () => {
nextTick(() => { nextTick(() => {
@@ -245,7 +201,6 @@ const updateCurrentTag = () => {
}); });
}; };
// ========================= 事件处理 =========================
/** /**
* 处理中键点击 * 处理中键点击
*/ */
@@ -270,7 +225,8 @@ const openContextMenu = (tag: TagView, event: MouseEvent) => {
const leftPosition = event.clientX - offsetLeft + MENU_MARGIN; const leftPosition = event.clientX - offsetLeft + MENU_MARGIN;
contextMenu.x = Math.min(leftPosition, maxLeft); contextMenu.x = Math.min(leftPosition, maxLeft);
contextMenu.y = layout.value === "mix" ? event.clientY - 50 : event.clientY; // 混合模式下,需要减去顶部菜单(fixed)的高度
contextMenu.y = layout.value === LayoutMode.MIX ? event.clientY - 50 : event.clientY;
contextMenu.visible = true; contextMenu.visible = true;
selectedTag.value = tag; selectedTag.value = tag;
@@ -301,7 +257,6 @@ const handleScroll = (event: WheelEvent) => {
scrollbarRef.value.setScrollLeft(newScrollLeft); scrollbarRef.value.setScrollLeft(newScrollLeft);
}; };
// ========================= 标签管理 =========================
/** /**
* 刷新标签 * 刷新标签
*/ */
@@ -378,20 +333,7 @@ const closeAllTags = (tag: TagView | null) => {
}); });
}; };
// ========================= 混合布局处理 ========================= // 右键菜单管理
/**
* 更新顶部菜单激活状态(混合布局)
*/
const updateTopMenuActive = (routeName: string) => {
if (layout.value !== "mix") return;
const topParent = findTopLevelParent(permissionStore.routes, routeName);
if (topParent && appStore.activeTopMenuPath !== topParent.path) {
appStore.activeTopMenu(topParent.path);
}
};
// ========================= 组合式函数:右键菜单管理 =========================
const useContextMenuManager = () => { const useContextMenuManager = () => {
const handleOutsideClick = () => { const handleOutsideClick = () => {
closeContextMenu(); closeContextMenu();
@@ -411,7 +353,6 @@ const useContextMenuManager = () => {
}); });
}; };
// ========================= 监听器和生命周期 =========================
// 监听路由变化 // 监听路由变化
watch( watch(
route, route,
@@ -422,17 +363,6 @@ watch(
{ immediate: true } { immediate: true }
); );
// 监听路由名变化(混合布局)
watch(
() => route.name,
(newRouteName) => {
if (newRouteName) {
updateTopMenuActive(newRouteName as string);
}
},
{ deep: true }
);
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
initAffixTags(); initAffixTags();

View File

@@ -31,28 +31,10 @@ export function useLayoutMenu() {
return path; return path;
}); });
// 监听顶部菜单路径变化,更新侧边菜单
watch(
() => activeTopMenuPath.value,
(newPath) => {
permissionStore.updateSideMenu(newPath);
},
{ immediate: true }
);
/**
* 处理顶部菜单点击
* @param path 菜单路径
*/
function handleTopMenuClick(path: string) {
appStore.activeTopMenu(path);
}
return { return {
routes, routes,
sideMenuRoutes, sideMenuRoutes,
activeMenu, activeMenu,
activeTopMenuPath, activeTopMenuPath,
handleTopMenuClick,
}; };
} }

View File

@@ -38,14 +38,10 @@ import AppMain from "../components/AppMain/index.vue";
import BasicMenu from "../components/Menu/BasicMenu.vue"; import BasicMenu from "../components/Menu/BasicMenu.vue";
// 布局相关参数 // 布局相关参数
const { isShowTagsView, isShowLogo, isSidebarOpen, isMobile } = useLayout(); const { isShowTagsView, isShowLogo, isSidebarOpen } = useLayout();
// 菜单相关 // 菜单相关
const { routes } = useLayoutMenu(); const { routes } = useLayoutMenu();
// 添加调试日志
console.log("🔍 LeftLayout - isSidebarOpen:", isSidebarOpen.value);
console.log("🔍 LeftLayout - isMobile:", isMobile.value);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -108,8 +108,6 @@ function resolvePath(routePath: string) {
// 否则拼接 // 否则拼接
return `${activeTopMenuPath.value}/${routePath}`; return `${activeTopMenuPath.value}/${routePath}`;
} }
console.log("🎨 MixLayout rendered");
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -14,16 +14,12 @@ export function setupPermission() {
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start(); NProgress.start();
console.log("🚀 Route guard triggered:", { to: to.path, from: from.path });
const isLoggedIn = Auth.isLoggedIn(); const isLoggedIn = Auth.isLoggedIn();
if (isLoggedIn) { if (isLoggedIn) {
console.log("✅ User is logged in");
// 如果已登录但访问登录页,重定向到首页 // 如果已登录但访问登录页,重定向到首页
if (to.path === "/login") { if (to.path === "/login") {
console.log("🔄 Redirecting from login to home");
next({ path: "/" }); next({ path: "/" });
return; return;
} }
@@ -44,8 +40,7 @@ export function setupPermission() {
}); });
// 后置守卫,确保进度条关闭 // 后置守卫,确保进度条关闭
router.afterEach((to, from) => { router.afterEach(() => {
console.log("✅ Route navigation completed:", { to: to.path, from: from.path });
NProgress.done(); NProgress.done();
}); });
} }
@@ -64,14 +59,11 @@ async function handleAuthenticatedUser(
try { try {
// 检查用户信息是否存在 // 检查用户信息是否存在
if (!userStore.userInfo.username) { if (!userStore.userInfo.username) {
console.log("🔄 User info not found, fetching...");
await userStore.getUserInfo(); await userStore.getUserInfo();
} }
// 检查路由是否已生成 // 检查路由是否已生成
if (!permissionStore.routesLoaded) { if (!permissionStore.routesLoaded) {
console.log("🔄 Routes not loaded, generating...");
// 防止重复生成路由 // 防止重复生成路由
if (isGeneratingRoutes) { if (isGeneratingRoutes) {
console.log("⏳ Routes already generating, waiting..."); console.log("⏳ Routes already generating, waiting...");
@@ -82,14 +74,12 @@ async function handleAuthenticatedUser(
} }
// 路由生成完成后,重新导航到目标路由 // 路由生成完成后,重新导航到目标路由
console.log("🔄 Routes generated, redirecting to:", to.path);
next({ ...to, replace: true }); next({ ...to, replace: true });
return; return;
} }
// 路由已加载,检查路由是否存在 // 路由已加载,检查路由是否存在
if (to.matched.length === 0) { if (to.matched.length === 0) {
console.log("❌ Route not found, redirecting to 404");
next("/404"); next("/404");
return; return;
} }
@@ -100,7 +90,6 @@ async function handleAuthenticatedUser(
to.meta.title = title; to.meta.title = title;
} }
console.log("✅ Route access granted:", to.path);
next(); next();
} catch (error) { } catch (error) {
console.error("❌ Route guard error:", error); console.error("❌ Route guard error:", error);
@@ -117,15 +106,12 @@ async function generateAndAddRoutes(permissionStore: any) {
isGeneratingRoutes = true; isGeneratingRoutes = true;
try { try {
console.log("🔧 Generating dynamic routes...");
const dynamicRoutes = await permissionStore.generateRoutes(); const dynamicRoutes = await permissionStore.generateRoutes();
// 添加路由到路由器 // 添加路由到路由器
dynamicRoutes.forEach((route: RouteRecordRaw) => { dynamicRoutes.forEach((route: RouteRecordRaw) => {
router.addRoute(route); router.addRoute(route);
}); });
console.log("✅ All dynamic routes generated and added");
} finally { } finally {
isGeneratingRoutes = false; isGeneratingRoutes = false;
} }
@@ -141,12 +127,11 @@ async function waitForRoutesGeneration(permissionStore: any): Promise<void> {
clearInterval(checkInterval); clearInterval(checkInterval);
resolve(); resolve();
} }
}, 50); // 每50ms检查一次 }, 50);
// 超时保护最多等待5秒 // 超时保护最多等待5秒
setTimeout(() => { setTimeout(() => {
clearInterval(checkInterval); clearInterval(checkInterval);
console.warn("⚠️ Routes generation timeout");
resolve(); resolve();
}, 5000); }, 5000);
}); });
@@ -176,7 +161,6 @@ function redirectToLogin(to: RouteLocationNormalized, next: NavigationGuardNext)
const queryString = params.toString(); const queryString = params.toString();
const redirect = queryString ? `${to.path}?${queryString}` : to.path; const redirect = queryString ? `${to.path}?${queryString}` : to.path;
console.log("🔄 Redirecting to login with redirect:", redirect);
next(`/login?redirect=${encodeURIComponent(redirect)}`); next(`/login?redirect=${encodeURIComponent(redirect)}`);
} }

View File

@@ -22,8 +22,6 @@ export const usePermissionStore = defineStore("permission", () => {
*/ */
function generateRoutes() { function generateRoutes() {
return new Promise<RouteRecordRaw[]>((resolve, reject) => { return new Promise<RouteRecordRaw[]>((resolve, reject) => {
console.log("🔧 Starting to generate routes...");
MenuAPI.getRoutes() MenuAPI.getRoutes()
.then((data) => { .then((data) => {
const dynamicRoutes = parseDynamicRoutes(data); const dynamicRoutes = parseDynamicRoutes(data);
@@ -31,7 +29,6 @@ export const usePermissionStore = defineStore("permission", () => {
routes.value = [...constantRoutes, ...dynamicRoutes]; routes.value = [...constantRoutes, ...dynamicRoutes];
routesLoaded.value = true; routesLoaded.value = true;
console.log("✅ Routes generation completed successfully");
resolve(dynamicRoutes); resolve(dynamicRoutes);
}) })
.catch((error) => { .catch((error) => {

View File

@@ -525,8 +525,6 @@ const fetchVisitTrendData = () => {
* @param data - 访问趋势数据 * @param data - 访问趋势数据
*/ */
const updateVisitTrendChartOptions = (data: VisitTrendVO) => { const updateVisitTrendChartOptions = (data: VisitTrendVO) => {
console.log("Updating visit trend chart options");
visitTrendChartOptions.value = { visitTrendChartOptions.value = {
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
@@ -610,8 +608,7 @@ const computeGrowthRateClass = (growthRate?: number): string => {
// 监听访问趋势日期范围的变化,重新获取趋势数据 // 监听访问趋势日期范围的变化,重新获取趋势数据
watch( watch(
() => visitTrendDateRange.value, () => visitTrendDateRange.value,
(newVal) => { () => {
console.log("Visit trend date range changed:", newVal);
fetchVisitTrendData(); fetchVisitTrendData();
}, },
{ immediate: true } { immediate: true }