+100
-16
@@ -19,11 +19,74 @@ const layoutData = reactive({
|
|||||||
isAlone: false,
|
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" {
|
function roleOfPath(p: string): "teacher" | "parent" {
|
||||||
if (p.startsWith("/par")) return "parent";
|
if (p.startsWith("/par")) return "parent";
|
||||||
return "teacher";
|
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() {
|
async function initShell() {
|
||||||
// 深链/刷新时初始导航是异步懒加载,不等就绪会读到初始路径 "/",
|
// 深链/刷新时初始导航是异步懒加载,不等就绪会读到初始路径 "/",
|
||||||
// 下方「登录后跳转第一个模块」就会把 /assign/middle 等直达路由弹回首页
|
// 下方「登录后跳转第一个模块」就会把 /assign/middle 等直达路由弹回首页
|
||||||
@@ -64,38 +127,34 @@ async function initShell() {
|
|||||||
|
|
||||||
isLogin.value = true;
|
isLogin.value = true;
|
||||||
|
|
||||||
// 登录后跳转第一个可用模块(仅限落在默认页:"/" 与其静态重定向 /assign/high;
|
// 登录/刷新后按已授权模块校验当前路由:默认落地页会被送往第一个可用模块,
|
||||||
// 深链到 /assign/middle 等具体页时保持原位不动)
|
// 未授权的深链同样在此被拦截(不再"平台菜单未配置也可直访")
|
||||||
const curPath = router.currentRoute.value.path;
|
collectAuthorized(modList || []);
|
||||||
if (curPath === "/" || curPath === "/assign/high") {
|
evaluateAccess();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("加载用户信息失败", error);
|
console.error("加载用户信息失败", error);
|
||||||
if (error?.message !== "redirect") {
|
if (error?.message !== "redirect") {
|
||||||
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
||||||
}
|
}
|
||||||
// 降级:使用默认值("/" 即我发布的页,无需跳转)
|
// 降级:使用默认值;拿不到授权列表时按无权限处理(fail-closed),不放行业务页面
|
||||||
layoutData.userInfo = { name: "管理员", avatar: "" };
|
layoutData.userInfo = { name: "管理员", avatar: "" };
|
||||||
layoutData.modList = [];
|
layoutData.modList = [];
|
||||||
layoutData.isAlone = true;
|
layoutData.isAlone = true;
|
||||||
isLogin.value = true;
|
isLogin.value = true;
|
||||||
|
accessTitle.value = "用户信息加载失败";
|
||||||
|
accessSub.value = "无法确认当前账号权限,请刷新页面重试;若持续失败请联系管理员。";
|
||||||
|
accessDenied.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(initShell);
|
onMounted(initShell);
|
||||||
|
|
||||||
// 端分离:路由跨角色前缀(/par 家长端、其它发布端)切换时,重新设定视角并初始化壳层
|
// 路由变化即复检授权(含菜单点击、深链);跨角色前缀(/par 家长端、其它发布端)
|
||||||
|
// 切换时再整体重拉身份与菜单
|
||||||
watch(
|
watch(
|
||||||
() => router.currentRoute.value.path,
|
() => router.currentRoute.value.path,
|
||||||
(p, prev) => {
|
(p, prev) => {
|
||||||
|
evaluateAccess();
|
||||||
if (roleOfPath(p) !== roleOfPath(prev || "")) initShell();
|
if (roleOfPath(p) !== roleOfPath(prev || "")) initShell();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -105,7 +164,11 @@ watch(
|
|||||||
<layout v-if="isLogin" :layoutData="layoutData" />
|
<layout v-if="isLogin" :layoutData="layoutData" />
|
||||||
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
||||||
<div class="main-wrapper">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -134,4 +197,25 @@ body {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: 0 2px;
|
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>
|
</style>
|
||||||
@@ -5,7 +5,7 @@ import type { RouteRecordRaw } from "vue-router";
|
|||||||
*
|
*
|
||||||
* modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 数据):
|
* modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 数据):
|
||||||
* 120=高中分班、121=初中分班、122=小学分班
|
* 120=高中分班、121=初中分班、122=小学分班
|
||||||
* (平台菜单未配置时也可直访对应路由;平台实际分配的 modId 与建议值不同时,
|
* (未授权模块的深链由 App.vue 按 GetUserMod 结果拦截;平台实际分配的 modId 与建议值不同时,
|
||||||
* 只需改此处 meta.modId 一处,见《分班管理-计划任务.md》§5)
|
* 只需改此处 meta.modId 一处,见《分班管理-计划任务.md》§5)
|
||||||
*/
|
*/
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
|
|||||||
Reference in New Issue
Block a user