- host.config:新增④按站推导 ssoHost(base.* → sso.*),logout 加空值保护,修线上退出落到 base 域 - layout:记 lastMenuPage,二级页高亮跟随来源节点并自动展开所在分组(expandToActive) - App.vue:权限口径=入口页(菜单节点)∪落地页(meta.parentId),落地页一律放行只谈高亮;initShell 首行 await router.isReady() 修深链接竞态 - routes:/signups 补 meta.parentId,修「报名情况不让点」(落地页漏声明会被当入口拦) - menuRoute:抽 flattenMenuNodes / firstMenuPath 共用口径
This commit is contained in:
+94
-45
@@ -2,7 +2,7 @@
|
||||
import { ref, onBeforeMount, watch, computed, inject } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { normalizeModList } from "@/utils/menuRoute";
|
||||
import { normalizeModList, flattenMenuNodes } from "@/utils/menuRoute";
|
||||
|
||||
const props = defineProps<{ layoutData: Record<string, any> }>();
|
||||
const route = useRoute();
|
||||
@@ -33,17 +33,7 @@ watch(
|
||||
() => routeChange(route)
|
||||
);
|
||||
|
||||
const flaten = (tree: any[], pid = 0): any[] => {
|
||||
const arr: any[] = [];
|
||||
tree.forEach((element: any) => {
|
||||
const { id, name, modId, page, external } = element;
|
||||
arr.push({ id, name, modId, page, external, pid });
|
||||
if (element.children && element.children.length) {
|
||||
arr.push(...flaten(element.children, id));
|
||||
}
|
||||
});
|
||||
return arr;
|
||||
};
|
||||
const flaten = (tree: any[], pid = 0): any[] => flattenMenuNodes(tree, pid);
|
||||
|
||||
/* 侧边栏渲染用的菜单树:page 已由 normalizeModList 统一归一为内部路由。
|
||||
跳转(linkMenu)与高亮(currPath 对照)必须共用这一份,否则平台里只配了
|
||||
@@ -61,53 +51,106 @@ watch(
|
||||
|
||||
const menuArr = computed(() => flaten(displayModList.value));
|
||||
|
||||
/** 菜单页面集合:判断某个 route.path 本身是否就是一个菜单节点 */
|
||||
const menuPageSet = computed(
|
||||
() => new Set(menuArr.value.map((x: any) => x.page).filter(Boolean) as string[])
|
||||
);
|
||||
|
||||
/**
|
||||
* 上一个「菜单节点」高亮值 —— 二级页面高亮的关键状态。
|
||||
*
|
||||
* 需求:从哪个节点点按钮进的二级页面(发布活动 / 活动详情等不在菜单里的路由),
|
||||
* 左侧就选中那个节点。冷启动或刷新时没有来源,退回路由 meta.parentId 的静态契约。
|
||||
*/
|
||||
const lastMenuPage = ref<string>("");
|
||||
|
||||
/** 沿 pid 向上收集祖先(支持多层嵌套),得到面包屑的前缀 */
|
||||
const ancestorsOf = (node: any): any[] => {
|
||||
const arr: any[] = [];
|
||||
let p = node?.pid ? menuArr.value.find((x: any) => x.id === node.pid) : null;
|
||||
while (p) {
|
||||
arr.unshift({ name: p.name, page: p.page });
|
||||
p = p.pid ? menuArr.value.find((x: any) => x.id === p.pid) : null;
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
const routeChange = (to: any) => {
|
||||
const meta: any = to.meta || {};
|
||||
const { modId, title, parentId } = meta;
|
||||
const navArr: any[] = [];
|
||||
|
||||
// 优先按 path 匹配菜单(平台模块 id 与路由 meta.modId 不保证一致),
|
||||
// 匹配不到再退回 modId 对照
|
||||
const find: any =
|
||||
menuArr.value.find((x: any) => x.page && x.page === to.path) ||
|
||||
menuArr.value.find((x: any) => x.modId === modId);
|
||||
if (find) {
|
||||
currPath.value = find.page || "";
|
||||
if (find.pid) {
|
||||
const pfind = menuArr.value.find((x: any) => x.id === find.pid);
|
||||
if (pfind) navArr.push({ name: pfind.name });
|
||||
}
|
||||
navArr.push({ name: find.name || title, page: find.page });
|
||||
navPath.value = navArr;
|
||||
// 1) 目标路由本身就是菜单节点 → 选中它(to.path 不含 query,列表页翻页/筛选不会改高亮)
|
||||
const self: any = menuArr.value.find((x: any) => x.page && x.page === to.path);
|
||||
if (self) {
|
||||
currPath.value = self.page;
|
||||
lastMenuPage.value = self.page;
|
||||
navPath.value = [...ancestorsOf(self), { name: self.name || title, page: self.page }];
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentId) {
|
||||
const parent = menuArr.value.find((x: any) => x.modId === parentId);
|
||||
if (parent) {
|
||||
currPath.value = parent.page || "";
|
||||
if (parent.pid) {
|
||||
const gp = menuArr.value.find((x: any) => x.id === parent.pid);
|
||||
if (gp) navArr.push({ name: gp.name });
|
||||
}
|
||||
navArr.push({ name: parent.name, page: parent.page });
|
||||
navArr.push({ name: title });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
}
|
||||
// 2) 二级页面(不在菜单里的路由):父节点优先取「进入它的那个菜单节点」,
|
||||
// 这样从「全部活动」进的发布活动页就选「全部活动」,而不是写死的 meta.parentId;
|
||||
// 没有来源(冷启动/刷新 / 直接粘 URL)时才退回 meta.parentId → modId 的静态契约
|
||||
const origin: any = lastMenuPage.value
|
||||
? menuArr.value.find((x: any) => x.page === lastMenuPage.value)
|
||||
: null;
|
||||
const declared: any =
|
||||
(parentId != null && menuArr.value.find((x: any) => x.modId === parentId)) ||
|
||||
(parentId != null && menuArr.value.find((x: any) => x.id === parentId)) ||
|
||||
(modId != null && menuArr.value.find((x: any) => x.modId === modId));
|
||||
const parent: any = origin || declared;
|
||||
if (parent) {
|
||||
currPath.value = parent.page || lastMenuPage.value;
|
||||
if (parent.page) lastMenuPage.value = parent.page;
|
||||
navPath.value = [
|
||||
...ancestorsOf(parent),
|
||||
{ name: parent.name, page: parent.page },
|
||||
{ name: title },
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
const r = router.getRoutes().find((rt: any) => (rt.meta as any)?.modId === modId);
|
||||
// 3) 菜单里没有、但路由表里按 modId 能查到(单模块 isAlone 等):只更新面包屑
|
||||
const r: any = router.getRoutes().find((rt: any) => (rt.meta as any)?.modId === modId);
|
||||
if (r) {
|
||||
currPath.value = r.path;
|
||||
if (menuPageSet.value.has(r.path)) lastMenuPage.value = r.path;
|
||||
navPath.value = [{ name: (r.meta as any).title, page: r.path }];
|
||||
return;
|
||||
}
|
||||
|
||||
currPath.value = "";
|
||||
navPath.value = [];
|
||||
// 4) 全都匹配不到:保持现有高亮与面包屑,**不清空**
|
||||
// 清空会让左侧菜单突然全部失选(比"高亮停在上一个节点"更像 bug)
|
||||
};
|
||||
|
||||
/**
|
||||
* 展开当前选中项所在的分组。
|
||||
* 分组默认是收起的(childState 未设置),选中项若在收起的分组里就"看不见选中"了;
|
||||
* 二级页高亮到某个一级分组下的子节点时尤其明显。只展开、不收起,
|
||||
* 避免用户手动展开的分组被自动关掉。
|
||||
*/
|
||||
const expandToActive = () => {
|
||||
const p = currPath.value;
|
||||
if (!p) return;
|
||||
(displayModList.value || []).forEach((item: any) => {
|
||||
if (Array.isArray(item.children) && item.children.some((c: any) => c?.page === p)) {
|
||||
item.childState = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 菜单到达后重跑一次高亮计算:首屏路由早于菜单就位(menuArr 为空时算不出高亮),
|
||||
刷新停在二级页时尤其明显。只在菜单内容变化时触发,展开/收起不触发 */
|
||||
watch(
|
||||
() => menuArr.value.map((x: any) => `${x.modId}|${x.page}`).join(","),
|
||||
() => {
|
||||
routeChange(route);
|
||||
expandToActive();
|
||||
}
|
||||
);
|
||||
|
||||
watch(currPath, expandToActive);
|
||||
|
||||
const linkMenu = (item: any) => {
|
||||
const page: string = item?.page || "";
|
||||
if (!page) {
|
||||
@@ -134,9 +177,15 @@ const linkUserInfo = () => {
|
||||
window.location.href = baseHost.value + "/v/userinfo";
|
||||
};
|
||||
const logout = () => {
|
||||
let url = window.location.href;
|
||||
url = "/logout?service=" + url;
|
||||
window.location.href = ssoHost.value + url;
|
||||
// 必须带 ssoHost:ssoHost 为空时 "/logout" 是**同源相对路径**,会落到 base 域,
|
||||
// 会话不会注销(只是从当前页跳回首页,看着像点了没反应)。
|
||||
// 线上 ssoHost 来自 env-config;未下发时由 host.config 的 ④ 按 base.* → sso.* 推导。
|
||||
if (!ssoHost.value) {
|
||||
Message.warning("未取到单点登录地址,无法安全退出,请稍后重试或联系管理员");
|
||||
return;
|
||||
}
|
||||
const url = window.location.href;
|
||||
window.location.href = ssoHost.value + "/logout?service=" + url;
|
||||
};
|
||||
const childChange = (index: number) => {
|
||||
const arr = displayModList.value || [];
|
||||
|
||||
Reference in New Issue
Block a user