fix: 🐛 混合布局左侧菜单丢失问题修复
closed #ICEVSD
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user