t14:前端模块行退役——左导航三支路自解析、AppContext 重构为通用壳两视图

- App.vue:引导只剩 GetUserInfo,之后并行取 fillable/managed/capabilities 三支构造导航
  (取不到按空集/false 走,fail-closed,不造兜底菜单树);摘 GetUserMod 调用与 setModules
- store:modules/IModule 状态与 setModules 删除,改存三支路数据 + managesForm() 判"这张表我管不管得动"
- AppContext.vue:重写为通用壳两视图(§5.1)——管理员视图=数据列表+复制填报路径
  (添加/导入 T18 接进这一档),填报者视图=仅填报 tab;判定只看 managed 是否非空,
  壳零自有权限逻辑;GetUserMod?appCode= 拉目标应用树那一路与 :modules prop 链整条拆除
- Data.vue:导出按钮改"该表单 ALL 档(managed 成员)",中心与快应用两种上下文从此同一条判据;
  AnswerDetail.vue 的管理动作区同判据;utils/modulePerms.ts(canExport/hasPage)删除
- utils/menuNav.ts:buildNavTree 与"从树渲染菜单"三条规则删除,只留导航数据结构与
  高亮/面包屑/默认跳转;menuRoute 的 /f/{code} 形态识别按任务书保留(分享直链/历史链接)
- mock:baseapi 去 GetUserMod 与两棵模块树;dataapi 补 /my/managed 与 /my/capabilities 桩
This commit is contained in:
chenxing
2026-10-20 13:21:23 +08:00
parent 4a7db3d22c
commit 6401a69c18
12 changed files with 324 additions and 472 deletions
+53 -25
View File
@@ -4,10 +4,9 @@ import layout from "./components/layout.vue";
import { useRoute, useRouter } from "vue-router";
import config from "@/assets/config/host.config";
import { baseApi } from "@/api/module/base";
import { resolveMenuPage } from "@/utils/menuRoute";
import { buildNavTree, firstNavPage, isSingleNav, type NavNode } from "@/utils/menuNav";
import { getMyCapabilities, listFillableForms, listManagedForms } from "@/api/module/fill";
import { firstNavPage, isSingleNav, type NavNode } from "@/utils/menuNav";
import { Message } from "view-ui-plus";
import type { IModule } from "@/types/Basics";
import { useDefaultStore } from "@/store";
const router = useRouter();
@@ -17,7 +16,7 @@ const isReady = ref(false);
/**
* 页面即组件(方案 §3.3):被 iframe 嵌入时自动剥壳——不渲染顶栏/左导航/面包屑,
* 也不发两个鉴权请求(嵌入上下文里本应用 /api/base/* 取不到会话会弹错误提示并画出带壳页面)。
* 也不发会话请求(嵌入上下文里本应用 /api/base/* 取不到会话会弹错误提示并画出带壳页面)。
* 显式 mode 参数(?mode=embed|page)的闭集判定在 T7 填报页落地,壳这层只兜 self != top 这一条。
*/
const isEmbedded = window.self !== window.top;
@@ -37,11 +36,36 @@ const layoutData = reactive({
baseHost: config.baseHost,
ssoHost: config.ssoHost,
userInfo: null as any,
/** 已按「path 空=权限锚点不进导航」规则过滤的可渲染菜单树(见 utils/menuNav.ts) */
/** 自解析出来的可渲染导航(契约 v2 §2.4 三支路;不再由 UserBase 模块树生成) */
navList: [] as NavNode[],
isAlone: false,
});
/** 导航节点 id:layout 只做高亮/面包屑对照,不参与任何判定,序号稳定即可 */
let navSeq = 1000;
function navItem(name: string, page: string, icon: string): NavNode {
return { id: navSeq++, modId: page, name, icon, page, external: false, raw: page, group: false, children: [] };
}
/**
* 三支路自解析(D5):可填的表单 / 我管理的表单 / 校管理员系统设置。
*
* <b>前端不自行判权</b>——每一支的画不画只看后端那三把接口给了什么:
* `fillable`(入集即可填)、`managed`(创建人 ∪ 表单管理员)、`capabilities.schoolAdmin`。
*/
function buildSelfResolvedNav(): NavNode[] {
const nav: NavNode[] = [navItem("我的填写", "/my", "ios-person"), navItem("可填的表单", "/my/fillable", "ios-create")];
if (store.managed.length) {
nav.push(navItem("我管理的表单", "/admin/data", "ios-list"));
nav.push(navItem("统计", "/admin/stats", "ios-pie"));
}
if (store.capabilities.schoolAdmin) {
nav.push(navItem("表单设计", "/admin/forms", "md-build"));
nav.push(navItem("系统设置", "/admin/app", "md-settings"));
}
return nav;
}
onMounted(async () => {
// 首次导航异步完成后再判默认跳转:mount 时 currentRoute 可能仍是初始 "/"
await router.isReady();
@@ -50,38 +74,42 @@ onMounted(async () => {
if (isEmbedded) return;
try {
// 1. 当前会话用户
// 会话用户(GetUserMod 已随模块行退役删除,引导只剩这一把 + 三支路自解析)
const user: any = await baseApi.getUserInfo().send();
const { id, name, profilePicture } = user || {};
// 存一份给页面用(如字典页判平台级维护权);权限判定仍在后端,这里只是不重复发一次请求
store.setUser(user || null);
// 2. 当前用户已授权的模块树(左导航数据源;path 空的权限锚点行也在内,由导航层跳过)
const modList: IModule[] = (await baseApi.getUserMod().send()) || [];
// 三支路并行取(互相独立,串行只会让首屏多等两个 RTT)
const [fillable, managed, capabilities] = await Promise.all([
listFillableForms().catch((e: any) => {
console.warn("[自解析] 可填清单取不到,按空集处理:", e?.message);
return [];
}),
listManagedForms().catch((e: any) => {
console.warn("[自解析] 管理清单取不到,按空集处理:", e?.message);
return [];
}),
getMyCapabilities().catch((e: any) => {
console.warn("[自解析] 能力位取不到,按无入口处理(fail-closed):", e?.message);
return { schoolAdmin: false };
})
]);
store.setFillable(fillable || []);
store.setManaged(managed || []);
store.setCapabilities(capabilities || null);
layoutData.userInfo = { id, name: name || "用户", avatar: profilePicture || "" };
layoutData.navList = buildNavTree(modList, resolveMenuPage);
layoutData.navList = buildSelfResolvedNav();
layoutData.isAlone = isSingleNav(layoutData.navList);
// 原始树留一份:按钮级权限(快应用的"导出"锚点)按它判,导航树已把锚点行过滤掉了
store.setModules(modList);
if (import.meta.env.DEV) {
// 冒烟取证:原始树 vs 可渲染导航(path 空的权限锚点应当只出现在前者)
console.table(
(modList || []).map((m: any) => ({
name: m?.name,
id: m?.id,
modId: m?.modId,
page: m?.page,
icon: m?.icon,
children: (m?.children || []).length
}))
);
console.table(
layoutData.navList.map((n) => ({ 渲染项: n.name, page: n.page, 分组头: n.group }))
layoutData.navList.map((n) => ({ 渲染项: n.name, page: n.page }))
);
}
// 入口页:跳第一个可跳转菜单项(无授权用户即「我的填写」)
// 入口页:跳第一个可跳转菜单项
if (router.currentRoute.value.path === "/") {
const first = firstNavPage(layoutData.navList);
if (first) router.replace(first);
@@ -91,7 +119,7 @@ onMounted(async () => {
if (error?.message !== "redirect") {
Message.error("载入用户信息失败,请检查后端服务是否正常!");
}
// 降级只降「导航为空」,不造兜底菜单树——静默回落会把「平台未配模块/未授权」伪装成正常
// 降级只降「导航为空」,不造兜底菜单树——静默回落会把"接口没通"伪装成正常
store.setUser(null);
layoutData.userInfo = { name: "用户", avatar: "" };
layoutData.navList = [];