+100
-16
@@ -19,11 +19,74 @@ const layoutData = reactive({
|
||||
isAlone: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 路由级访问拦截:以 GetUserMod 返回的已授权模块为准。
|
||||
* 平台侧未授权的模块(含深链直访)不放行——跳转首个已授权模块,
|
||||
* 一个授权都没有(或 GetUserMod 失败)时出示无权限页,宁可错杀不放过。
|
||||
*/
|
||||
const authPaths = new Set<string>();
|
||||
const authModIds = new Set<number>();
|
||||
const accessDenied = ref(false);
|
||||
const accessTitle = ref("");
|
||||
const accessSub = ref("");
|
||||
|
||||
function roleOfPath(p: string): "teacher" | "parent" {
|
||||
if (p.startsWith("/par")) return "parent";
|
||||
return "teacher";
|
||||
}
|
||||
|
||||
/** 从平台模块树收集已授权的内部路由 path 与 modId(与侧边栏同用 resolveMenuPage 解析) */
|
||||
function collectAuthorized(mods: any[]) {
|
||||
authPaths.clear();
|
||||
authModIds.clear();
|
||||
const walk = (list: any[]) => {
|
||||
(list || []).forEach((m: any) => {
|
||||
const r = resolveMenuPage(m.page, m.modId ?? m.id, m.name);
|
||||
if (r.path && !r.external) authPaths.add(r.path);
|
||||
const mid = Number(m.modId ?? m.id);
|
||||
if (!Number.isNaN(mid)) authModIds.add(mid);
|
||||
if (m.children && m.children.length) walk(m.children);
|
||||
});
|
||||
};
|
||||
walk(mods);
|
||||
}
|
||||
|
||||
/** 首个已授权模块的内部路由 path(菜单顺序,顶层优先取其第一个子项),无可解析项返回空串 */
|
||||
function firstAuthorizedPath(): string {
|
||||
for (const m of layoutData.modList) {
|
||||
const items: any[] = m.children && m.children.length ? m.children : [m];
|
||||
for (const item of items) {
|
||||
const r = resolveMenuPage(item.page, item.modId ?? item.id, item.name);
|
||||
if (r.path && !r.external) return r.path;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function evaluateAccess() {
|
||||
const cur = router.currentRoute.value;
|
||||
const meta: any = cur.meta || {};
|
||||
// 无 modId 的辅助路由("/" 等重定向)放行,由重定向目标再受检
|
||||
if (!meta.modId) {
|
||||
accessDenied.value = false;
|
||||
return;
|
||||
}
|
||||
if (authPaths.has(cur.path) || authModIds.has(Number(meta.modId))) {
|
||||
accessDenied.value = false;
|
||||
return;
|
||||
}
|
||||
const first = firstAuthorizedPath();
|
||||
if (first && first !== cur.path) {
|
||||
router.replace(first).catch(() => {
|
||||
accessDenied.value = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
accessTitle.value = "暂无访问权限";
|
||||
accessSub.value = "当前账号未获授权使用分班管理,请联系管理员在基础平台配置模块权限。";
|
||||
accessDenied.value = true;
|
||||
}
|
||||
|
||||
async function initShell() {
|
||||
// 深链/刷新时初始导航是异步懒加载,不等就绪会读到初始路径 "/",
|
||||
// 下方「登录后跳转第一个模块」就会把 /assign/middle 等直达路由弹回首页
|
||||
@@ -64,38 +127,34 @@ async function initShell() {
|
||||
|
||||
isLogin.value = true;
|
||||
|
||||
// 登录后跳转第一个可用模块(仅限落在默认页:"/" 与其静态重定向 /assign/high;
|
||||
// 深链到 /assign/middle 等具体页时保持原位不动)
|
||||
const curPath = router.currentRoute.value.path;
|
||||
if (curPath === "/" || curPath === "/assign/high") {
|
||||
const firstModule = (modList || [])[0];
|
||||
const firstRaw = firstModule?.children?.length
|
||||
? firstModule.children[0]
|
||||
: firstModule;
|
||||
const first = resolveMenuPage(firstRaw?.page, firstRaw?.modId ?? firstRaw?.id, firstRaw?.name);
|
||||
if (!first.external && first.path && first.path !== "/") {
|
||||
router.replace(first.path);
|
||||
}
|
||||
}
|
||||
// 登录/刷新后按已授权模块校验当前路由:默认落地页会被送往第一个可用模块,
|
||||
// 未授权的深链同样在此被拦截(不再"平台菜单未配置也可直访")
|
||||
collectAuthorized(modList || []);
|
||||
evaluateAccess();
|
||||
} catch (error: any) {
|
||||
console.error("加载用户信息失败", error);
|
||||
if (error?.message !== "redirect") {
|
||||
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
||||
}
|
||||
// 降级:使用默认值("/" 即我发布的页,无需跳转)
|
||||
// 降级:使用默认值;拿不到授权列表时按无权限处理(fail-closed),不放行业务页面
|
||||
layoutData.userInfo = { name: "管理员", avatar: "" };
|
||||
layoutData.modList = [];
|
||||
layoutData.isAlone = true;
|
||||
isLogin.value = true;
|
||||
accessTitle.value = "用户信息加载失败";
|
||||
accessSub.value = "无法确认当前账号权限,请刷新页面重试;若持续失败请联系管理员。";
|
||||
accessDenied.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(initShell);
|
||||
|
||||
// 端分离:路由跨角色前缀(/par 家长端、其它发布端)切换时,重新设定视角并初始化壳层
|
||||
// 路由变化即复检授权(含菜单点击、深链);跨角色前缀(/par 家长端、其它发布端)
|
||||
// 切换时再整体重拉身份与菜单
|
||||
watch(
|
||||
() => router.currentRoute.value.path,
|
||||
(p, prev) => {
|
||||
evaluateAccess();
|
||||
if (roleOfPath(p) !== roleOfPath(prev || "")) initShell();
|
||||
}
|
||||
);
|
||||
@@ -105,7 +164,11 @@ watch(
|
||||
<layout v-if="isLogin" :layoutData="layoutData" />
|
||||
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
||||
<div class="main-wrapper">
|
||||
<router-view />
|
||||
<div v-if="accessDenied" class="no-access-panel">
|
||||
<p class="no-access-title">{{ accessTitle }}</p>
|
||||
<p class="no-access-sub">{{ accessSub }}</p>
|
||||
</div>
|
||||
<router-view v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -134,4 +197,25 @@ body {
|
||||
vertical-align: middle;
|
||||
margin: 0 2px;
|
||||
}
|
||||
/* 无权限/加载失败占位页:内容区居中,顶部栏与已授权菜单保持可用 */
|
||||
.no-access-panel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.no-access-panel .no-access-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1d2939;
|
||||
}
|
||||
.no-access-panel .no-access-sub {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@ import type { RouteRecordRaw } from "vue-router";
|
||||
*
|
||||
* modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 数据):
|
||||
* 120=高中分班、121=初中分班、122=小学分班
|
||||
* (平台菜单未配置时也可直访对应路由;平台实际分配的 modId 与建议值不同时,
|
||||
* (未授权模块的深链由 App.vue 按 GetUserMod 结果拦截;平台实际分配的 modId 与建议值不同时,
|
||||
* 只需改此处 meta.modId 一处,见《分班管理-计划任务.md》§5)
|
||||
*/
|
||||
const routes: RouteRecordRaw[] = [
|
||||
|
||||
Reference in New Issue
Block a user