Files
form-center-front/src/utils/modulePerms.ts
T
chenxing d8f1a1fbfa
build / build (push) Successful in 3m8s
feat(t10-admin): 管理员体系前端——应用设置双 tab+单表单管理员+选人组件移植
- AppSettings.vue 应用设置页(基础/字典两 tab);修复 P86:Tabs 写 :value 而受控 prop 是 modelValue,致 ?tab=dicts 深链静默失效,改后深链实拍生效
- UserCheckModal.vue 自 notice 移植(view-ui-plus+ztree,sources 注入式数据源);public/libs 收录 jquery+ztree 本地件(index.html 引用)
- api/module/admin.ts + mock/adminapi.ts + types/admin.ts;AdminListPanel/FormAdminModal 组件
- routes/menuNav/menuRoute/modulePerms 适配管理员菜单与权限位;tree-icon 图标资源
2026-10-15 20:25:39 +08:00

90 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 按钮级/页面级权限的**前端呈现**判定(方案 §6.1 第②层、§6.2 权限锚点、V26 的前端半句)。
*
* 输入永远是 `GetUserMod` 返回的<b>原始模块树</b>(含 path 空的权限锚点行)——
* 不是 `utils/menuNav.ts` 过滤后的导航树:导航按规则跳过了 path 空节点,
* 拿过滤后的树来判按钮,锚点行永远不在里面,判据就成了猜。
*
* 三条纪律:
* 1. **这里只决定"按钮画不画"**,接口侧一律会话自取(契约 §7 身份行)——藏按钮不等于授权;
* 2. **中心与快应用两种上下文的导出载体不同形**(手册 §2.2 中心表没有导出行、方案 §6.2
* 快应用表的导出行是 path 空锚点),所以按上下文分派,不硬造一个 `fc:export:*` 协议值
* (中心面原来只有数据范围升级位那一个锚点值,T10 已把它退役、换成后端管理员两表;
* 前端这套按钮锚点判定与此无关,消费的仍是快应用自己那两行 path 空行);
* 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;
}