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
+46 -14
View File
@@ -1,25 +1,34 @@
import { defineStore } from "pinia";
import type { IModule } from "@/types/Basics";
import type { Capabilities, FillableForm, ManagedForm } from "@/types/data";
/**
* 全局 store(notice/front 基线形态)。
* T6 起开第一个业务切片:会话用户本体——字典管理页要按它判「能不能进平台级上下文维护」
* (UserBase 侧只有 type==1 超管可写 sch_id=0 的行,方案 §6.5;后端 FormAdminGuard 同判)。
* 这里存的是**展示与门禁提示用的会话身份**,不是权限来源:接口侧一律会话自取,
* 前端藏掉按钮不等于授权(契约 §7 身份行)。
*
* 存两类东西:
* 1. **会话用户本体**——展示与门禁提示用(字典页判"能不能进平台级上下文维护")。
* 这里存的是展示位,**不是权限来源**:接口侧一律会话自取,前端藏掉按钮不等于授权(契约 §7 身份行);
* 2. **菜单自解析的三支路数据**(契约 v2 §2.4/D5)——可填清单、我管理的清单、能力位。
* 原第四项"GetUserMod 原始模块树"随模块行整条退役删除(§19 作废-10):中心不再有任何
* 按授权树判定的地方,"两棵树"的缺口(B13/P78)因此不复存在。
*/
/** 中心表单的归属应用(快应用行的 appCode 不是这个值) */
export const CENTER_APP_CODE = "form-center";
/** 列表行的唯一键:同 code 在不同 app_code 下是两张表(契约 §1 的 uk=(sch_id, app_code, code)) */
const rowKey = (appCode: string, code: string) => `${appCode}#${code}`;
export const useDefaultStore = defineStore("defaultStore", {
state: () => ({
userRole: "school" as string,
/** /api/base/GetUserInfo 原样下发的会话用户(未拿到时为 null) */
user: null as Record<string, any> | null,
/**
* /api/base/GetUserMod 的<b>原始模块树</b>(T8 起留一份给按钮级判定用)。
* 必须是原始树而不是 `utils/menuNav.ts` 过滤后的导航树——导航按规则跳过了
* path 空的权限锚点行,而按钮位(快应用的"导出")正是这类行(方案 §6.2)。
* 判定函数在 `utils/modulePerms.ts`;这里只是避免同一棵树重复发一次请求。
*/
modules: [] as IModule[]
/** 三支路之一:可填清单(后端按 audience 门筛过,入集即可填) */
fillable: [] as FillableForm[],
/** 三支路之二:我管理的清单(创建人 ∪ 表单管理员) */
managed: [] as ManagedForm[],
/** 三支路之三:能力位("系统设置"入口的唯一数据源) */
capabilities: { schoolAdmin: false } as Capabilities
}),
getters: {
/** 超管(UserBase UserTypeEnum.SUPER_ADMIN=1):唯一能操作平台级行的身份 */
@@ -33,6 +42,10 @@ export const useDefaultStore = defineStore("defaultStore", {
/** 会话用户 id:判断"这条答卷是不是我提交的"(编辑/删除入口显隐)用 */
sessionUserId(state): number | null {
return state.user?.id ?? null;
},
/** "我管理的表单"键集:导出/详情管理按钮的显隐判据(ALL 档的清单形态) */
managedKeys(state): Set<string> {
return new Set(state.managed.map((r) => rowKey(r.appCode, r.code)));
}
},
actions: {
@@ -42,8 +55,27 @@ export const useDefaultStore = defineStore("defaultStore", {
setUser(user: Record<string, any> | null) {
this.user = user || null;
},
setModules(modules: IModule[] | null) {
this.modules = modules || [];
setFillable(rows: FillableForm[] | null) {
this.fillable = rows || [];
},
setManaged(rows: ManagedForm[] | null) {
this.managed = rows || [];
},
setCapabilities(caps: Capabilities | null) {
this.capabilities = { schoolAdmin: !!caps?.schoolAdmin };
}
}
});
/**
* "这张表我管不管得动"的判定(managed 清单成员 = 该表 ALL 档持有者,契约 v2 C3/C4②)。
*
* <p>前端只用它决定**画不画按钮**(导出、详情里的管理动作、快应用的数据/统计 tab);
* 真正的把关在后端(C4 三段门 + 行级范围构造器),藏按钮不等于授权。
* 刻意不做"拿不到清单就当有权限"的宽定——fail-closed 与原模块行判定同向。</p>
*/
export function managesForm(managed: ManagedForm[], appCode: string | null | undefined, code: string | null | undefined): boolean {
if (!code) return false;
const key = rowKey(appCode || CENTER_APP_CODE, code);
return managed.some((r) => rowKey(r.appCode, r.code) === key);
}