P76(T8 验收遗留):快应用上下文的导出按钮此前恒按中心会话树判"导出"锚点 (Data.vue exportable 读 store.modules),锚点判定不生效(fail-closed 方向, 有授权也不画按钮)。本轮把 AppContext 拉到的**目标应用树**经 modules prop 下传, Data.vue 的权限判据改为 permsTree(app=应用树 / center=会话树),canExport 纯函数不动。 Stats.vue 未接:本页无锚点判定也无 store.modules 引用,加 prop 是死代码(差异记 T9 报告)。 P77(T8 验收遗留):应用上下文内的"详情"不再跳回中心路由 /admin/data/detail。 形态二选一取**子路由 /app/:appCode/data/detail**(同一个 AnswerDetail.vue): 地址保留 uuid 可深链/刷新、详情页只有一份实现、back() 本就按 app 回应用数据视图, 写 parentPath 反而造假(menuRoute 的 ROUTE_PATHS 只收静态 path,应用段导航在页内轻导航)。 goDetail 按 props.context 分派;back() 改读 route.params.appCode,撤掉 query.app 双轨。 观察项①(T8 验收记档):确认跨触底页勾选**会被清空**——view-ui-plus 的 data 监听器 用 makeObjBaseData 重建 objData,_isChecked 只认行上的 _checked(点选不回写行), 整组替换后勾选视觉与 getSelection 归零而"已选 N 条"仍累计。新增 withSelectionState() 在 reload/loadMore 两条装载路径按 selected 回填 _checked,selected 为唯一事实源。 演练桩(本地件,dev:mock 生效):baseapi 增 fc-drill 快应用模块树(方案 §6.2 五行含 导入/导出两个 path 空锚点),dataapi 增 /api/form/resolve 桩(快应用数据视图唯一的 合规取数路径是 code→resolve→uuid,契约 §1)。其余外指 appCode 仍旧返回空树(P3 口径)。 门禁:npm run build ✓ / npm run type-check 0 错 ✓; t1~t9 全量复跑:t5 13/0/1、t6 22/0/2、t7 12/0/0、t8 21/0/1、t9 12/0/1 (skip 均为 headless Edge --dump-dom 吐 0 字节的 P73 环境事实,不假通过)。 本提交含 T6~T8 三轮堆在工作区的既有改动(沿 T8 验收 §5 推送分层)。
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 按钮级/页面级权限的**前端呈现**判定(方案 §6.1 第②层、§6.2 权限锚点、V26 的前端半句)。
|
||||
*
|
||||
* 输入永远是 `GetUserMod` 返回的<b>原始模块树</b>(含 path 空的权限锚点行)——
|
||||
* 不是 `utils/menuNav.ts` 过滤后的导航树:导航按规则跳过了 path 空节点,
|
||||
* 拿过滤后的树来判按钮,锚点行永远不在里面,判据就成了猜。
|
||||
*
|
||||
* 三条纪律:
|
||||
* 1. **这里只决定"按钮画不画"**,接口侧一律会话自取(契约 §7 身份行)——藏按钮不等于授权;
|
||||
* 2. **中心与快应用两种上下文的导出载体不同形**(手册 §2.2 中心表没有导出行、方案 §6.2
|
||||
* 快应用表的导出行是 path 空锚点),所以按上下文分派,不硬造一个 `fc:export:*` 协议值
|
||||
* (契约 §8 一期只定义了 `fc:scope:all` 一个值);
|
||||
* 3. **快应用上下文拿不到锚点就是不给**(fail-closed),不做"没配就当有"的宽定。
|
||||
*/
|
||||
import type { IModule } from "@/types/Basics";
|
||||
|
||||
/** 深度优先展平(顺序不承诺,只做集合判定) */
|
||||
export function flattenModules(tree: IModule[] | null | undefined): IModule[] {
|
||||
const out: IModule[] = [];
|
||||
for (const node of tree ?? []) {
|
||||
if (!node) continue;
|
||||
out.push(node);
|
||||
out.push(...flattenModules(node.children));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 授权页面行 path 集合(path 空的权限锚点不在内)——与后端 FormModuleKeys.paths 同一判据 */
|
||||
export function authorizedPaths(tree: IModule[] | null | undefined): string[] {
|
||||
return flattenModules(tree)
|
||||
.map((m) => String(m.page ?? "").trim())
|
||||
.filter((p) => p !== "");
|
||||
}
|
||||
|
||||
/** 权限锚点行(path 为空的节点)——契约 §8"锚点行形态"注记 / P47:只认 path 空节点 */
|
||||
export function anchorRows(tree: IModule[] | null | undefined): IModule[] {
|
||||
return flattenModules(tree).filter((m) => String(m.page ?? "").trim() === "");
|
||||
}
|
||||
|
||||
/** 是否被授权了某个页面行(path 精确匹配) */
|
||||
export function hasPage(tree: IModule[] | null | undefined, path: string): boolean {
|
||||
return authorizedPaths(tree).includes(path);
|
||||
}
|
||||
|
||||
/** 是否存在某个权限锚点行(按行名称匹配——快应用"导出"行的形态就是 path 空 + 名称"导出") */
|
||||
export function hasAnchorNamed(tree: IModule[] | null | undefined, name: string): boolean {
|
||||
const want = name.trim();
|
||||
return anchorRows(tree).some((m) => String(m.name ?? "").trim() === want);
|
||||
}
|
||||
|
||||
export type DataExportContext = "center" | "app";
|
||||
|
||||
/**
|
||||
* 导出按钮显隐(V26 前端半句:"无导出授权者前端无按钮";接口拒是 T4 已验的另一半)。
|
||||
*
|
||||
* - `center`:中心管理端。手册 §2.2 的中心模块行表里<b>没有导出行</b>,后端 T4 的导出端点
|
||||
* 按 `/admin/data` 页面行把关,所以前端同口径按页面行判——不自造一个中心侧没有的锚点。
|
||||
* - `app`:快应用上下文。方案 §6.2 的快应用典型配法里"导出"是 <b>path 空的按钮级锚点行</b>,
|
||||
* 授权了才在树里出现;拿不到该锚点就不画按钮(fail-closed)。
|
||||
*/
|
||||
export function canExport(tree: IModule[] | null | undefined, context: DataExportContext): boolean {
|
||||
if (context === "app") return hasAnchorNamed(tree, "导出");
|
||||
return hasPage(tree, "/admin/data");
|
||||
}
|
||||
|
||||
/**
|
||||
* 快应用树里的填报目标(模块行 path 形如 `/f/{code}`,方案 §6.2 表第一行)。
|
||||
* 数据/统计页要的是表单 uuid,而 uuid 只能由 code 经 resolve 得到(契约 §1"resolve 是读接口的唯一入口")——
|
||||
* 快应用上下文不授权定义管理行,拿不到表单列表,这条是唯一的合规取数路径。
|
||||
*/
|
||||
export function fillCodesOfModules(tree: IModule[] | null | undefined): string[] {
|
||||
const codes: string[] = [];
|
||||
for (const p of authorizedPaths(tree)) {
|
||||
const m = /^\/f\/([^/?#]+)/.exec(p);
|
||||
if (m && !codes.includes(m[1])) codes.push(m[1]);
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 受限身份可见集的来源(V26 第三半句的前端侧口径):
|
||||
* 学生身份只见"填报"路由 = 导航树里只有填报行——这是模块授权的直接结果,
|
||||
* 前端<b>不再自己按角色过滤一遍</b>(任务书 T8:范围以后端为准,前端不自行过滤)。
|
||||
* 本函数只做"树里有几项可跳转"的取证辅助,供断言用。
|
||||
*/
|
||||
export function navigableCount(tree: IModule[] | null | undefined): number {
|
||||
return authorizedPaths(tree).length;
|
||||
}
|
||||
Reference in New Issue
Block a user