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端点是否配置
if (!brokerURL.value) {
console.error("WebSocket连接失败: 未配置WebSocket端点URL");
console.warn("WebSocket连接失败: 未配置WebSocket端点URL");
return;
}
@@ -74,7 +74,7 @@ export function useStomp(options: UseStompOptions = {}) {
// 检查令牌是否为空,如果为空则不进行连接
if (!currentToken) {
console.error("WebSocket连接失败授权令牌为空请先登录");
console.warn("WebSocket连接失败授权令牌为空请先登录");
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 点击的菜单路径
*/
const handleMenuSelect = (routePath: string) => {
appStore.activeTopMenu(routePath); // 设置激活的顶部菜单
activateFirstLevelMenu(routePath); // 激活一级菜单并设置左侧二级菜单
};
/**
* 激活一级菜单并设置左侧二级菜单
* @param routePath 点击的菜单路径
*/
function activateFirstLevelMenu(routePath: string) {
permissionStore.updateSideMenu(routePath); // 更新左侧菜单
// 使用 nextTick 确保侧边菜单更新完成后再跳转
nextTick(() => {
navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单
});
}
navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单
};
/**
* 跳转到左侧第一个可访问的菜单
@@ -134,43 +102,34 @@ function activateFirstLevelMenu(routePath: string) {
const navigateToFirstLeftMenu = (menus: RouteRecordRaw[]) => {
if (menus.length === 0) return;
// 查找第一个可访问的菜单项
const findFirstAccessibleRoute = (routes: RouteRecordRaw[]): RouteRecordRaw | null => {
for (const route of routes) {
// 跳过隐藏的菜单项
if (route.meta?.hidden) continue;
const [firstMenu] = menus;
// 如果有子菜单,递归查找
if (route.children && route.children.length > 0) {
const childRoute = findFirstAccessibleRoute(route.children);
if (childRoute) return childRoute;
} 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);
// 如果第一个菜单有子菜单,递归跳转到第一个子菜单
if (firstMenu.children && firstMenu.children.length > 0) {
navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[]);
} else if (firstMenu.name) {
router.push({
name: firstRoute.name,
name: firstMenu.name,
query:
typeof firstRoute.meta?.params === "object"
? (firstRoute.meta.params as LocationQueryRaw)
typeof firstMenu.meta?.params === "object"
? (firstMenu.meta.params as LocationQueryRaw)
: undefined,
});
}
};
// 当前激活的顶部菜单路径
// 获取当前路由路径的顶部菜单路径
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
onMounted(() => {
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>

View File

@@ -149,7 +149,7 @@ import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
const { t } = useI18n();
import { LayoutMode, SidebarColor, ThemeMode } from "@/enums";
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
import { useSettingsStore } from "@/store";
import { themeColorPresets } from "@/settings";
// 按钮图标
@@ -176,10 +176,7 @@ const layoutOptions: LayoutOption[] = [
// 使用统一的颜色预设配置
const colorPresets = themeColorPresets;
const route = useRoute();
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const isDark = ref<boolean>(settingsStore.theme === ThemeMode.DARK);
const sidebarColor = ref(settingsStore.sidebarColorScheme);
@@ -221,13 +218,6 @@ const handleLayoutChange = (layout: LayoutMode) => {
if (settingsStore.layout === layout) return;
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 { resolve } from "path-browserify";
import { translateRouteTitle } from "@/utils/i18n";
import { usePermissionStore, useTagsViewStore, useSettingsStore, useAppStore } from "@/store";
import { usePermissionStore, useTagsViewStore, useSettingsStore } from "@/store";
import { LayoutMode } from "@/enums";
// ========================= 类型定义 =========================
interface ContextMenu {
visible: boolean;
x: number;
y: number;
}
// ========================= 组合式 API =========================
const instance = getCurrentInstance();
const proxy = instance?.proxy;
const router = useRouter();
@@ -77,9 +76,7 @@ const route = useRoute();
const permissionStore = usePermissionStore();
const tagsViewStore = useTagsViewStore();
const settingsStore = useSettingsStore();
const appStore = useAppStore();
// ========================= 响应式数据 =========================
const { visitedViews } = storeToRefs(tagsViewStore);
const layout = computed(() => settingsStore.layout);
@@ -96,7 +93,6 @@ const contextMenu = reactive<ContextMenu>({
// 滚动条引用
const scrollbarRef = ref();
// ========================= 计算属性 =========================
// 路由映射缓存,提升查找性能
const routePathMap = computed(() => {
const map = new Map<string, TagView>();
@@ -121,7 +117,6 @@ const isLastView = computed(() => {
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath;
});
// ========================= 核心函数 =========================
/**
* 递归提取固定标签
*/
@@ -155,45 +150,6 @@ const extractAffixTags = (routes: RouteRecordRaw[], basePath = "/"): TagView[] =
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 = () => {
nextTick(() => {
@@ -245,7 +201,6 @@ const updateCurrentTag = () => {
});
};
// ========================= 事件处理 =========================
/**
* 处理中键点击
*/
@@ -270,7 +225,8 @@ const openContextMenu = (tag: TagView, event: MouseEvent) => {
const leftPosition = event.clientX - offsetLeft + MENU_MARGIN;
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;
selectedTag.value = tag;
@@ -301,7 +257,6 @@ const handleScroll = (event: WheelEvent) => {
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 handleOutsideClick = () => {
closeContextMenu();
@@ -411,7 +353,6 @@ const useContextMenuManager = () => {
});
};
// ========================= 监听器和生命周期 =========================
// 监听路由变化
watch(
route,
@@ -422,17 +363,6 @@ watch(
{ immediate: true }
);
// 监听路由名变化(混合布局)
watch(
() => route.name,
(newRouteName) => {
if (newRouteName) {
updateTopMenuActive(newRouteName as string);
}
},
{ deep: true }
);
// 初始化
onMounted(() => {
initAffixTags();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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