fix: 🐛 混合布局左侧菜单丢失问题修复
closed #ICEVSD
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭抽屉前的回调
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -108,8 +108,6 @@ function resolvePath(routePath: string) {
|
||||
// 否则拼接
|
||||
return `${activeTopMenuPath.value}/${routePath}`;
|
||||
}
|
||||
|
||||
console.log("🎨 MixLayout rendered");
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user