fix(授权): 父级勾选态改「任一子节点选中即选中」+ 无模块应用可授权(组授权/角色授权)
build / build (push) Failing after 2m6s

1. 父级选中态(两页口径统一)
   isAppChecked 由「全部子模块都勾选才 true」改为「任一子孙被勾选即 true」,
   新增 isModuleAnyChecked 替代不再使用的 isModuleAllChecked;buildModuleNode 同步。
   样式把 zTree checkbox_true_part / _focus 的 background-position 指向 checkbox_true_full
   的蓝勾位置——父节点 checked=true 但子节点未全选时,原生是"蓝底横杠"半选,
   折叠态几乎看不出部分授权,改成蓝勾后与全选视觉一致。

2. 无子模块应用(如「一周安排」)勾选后刷新即失效
   - GroupAuth:这类应用没有模块明细,原 buildModuleDiff 前后快照恒为空、根本不发请求。
     改为单独走 moduleId=0 的「整应用」提交(后端翻译成 -appId 哨兵记录);
     回显读后端应用级 checked / inheritFlag;setLocalAppInherit 支持应用级。
   - AppModuleCheckTree:onTreeCheck 直接记 app.checked;saveNow 把后端约定的 -appId
     并入 moduleIds —— 角色授权是模块 id 全量覆盖,不带哨兵每一项保存都会被静默清掉。

后端配套改动见 UserBaseService 同名提交(AppLevelModuleId / resolveModuleIds / VO 新增 checked)。
This commit is contained in:
chenxing
2026-10-20 12:18:54 +08:00
parent e8ab9280bf
commit 32b73ba407
2 changed files with 87 additions and 16 deletions
+55 -7
View File
@@ -90,6 +90,10 @@ type AppItem = {
id?: number;
name?: string;
sortNo?: number;
/** 应用级勾选态:仅当 modules 为空时有意义(对应后端 module_id=0 的整应用授权记录) */
checked?: boolean;
/** 应用级子组继承:仅当 modules 为空时有意义 */
inheritFlag?: boolean;
modules?: AppModuleVo[];
};
@@ -221,19 +225,22 @@ function countTotalModules(nodes: AppModuleVo[]): number {
function isAppChecked(app: AppItem): boolean {
const modules = app.modules || [];
if (modules.length === 0) return false;
return modules.every((m) => isModuleAllChecked(m));
// 无子模块应用(如「一周安排」):勾选态来自后端返回的整应用授权记录
if (modules.length === 0) return !!app.checked;
// 父级展示口径:只要有一个子节点被勾选就算选中(不采用 zTree 半选,见样式里 true_part 的覆写)
return modules.some((m) => isModuleAnyChecked(m));
}
function isAppInherit(app: AppItem): boolean {
const modules = app.modules || [];
if (modules.length === 0) return false;
if (modules.length === 0) return !!app.inheritFlag;
return modules.every((m) => isModuleAllInherit(m));
}
function isModuleAllChecked(node: AppModuleVo): boolean {
if (!node.checked) return false;
return (node.children || []).every((c) => isModuleAllChecked(c));
/** 自身或任一子孙被勾选 */
function isModuleAnyChecked(node: AppModuleVo): boolean {
if (node.checked) return true;
return (node.children || []).some((c) => isModuleAnyChecked(c));
}
function isModuleAllInherit(node: AppModuleVo): boolean {
@@ -248,7 +255,8 @@ function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
id: `mod_${m.id}`,
name: String(m.name || ""),
open: true,
checked: !!m.checked,
// 父模块节点沿用「有子节点选中即选中」的展示口径,与顶层应用节点一致
checked: isModuleAnyChecked(m),
inheritFlag: !!m.inheritFlag,
nodeType: "module",
rawId: Number(m.id),
@@ -423,6 +431,10 @@ function findLocalApp(appId: number): AppItem | null {
function setLocalAppInherit(appId: number, inherit: boolean) {
const app = findLocalApp(appId);
if (!app) return;
if ((app.modules || []).length === 0) {
app.inheritFlag = inherit;
return;
}
walkModules(app.modules || [], (n) => {
if (n.checked) n.inheritFlag = inherit;
});
@@ -453,6 +465,33 @@ async function onTreeCheck(node: any) {
return;
}
// 无子模块应用(如「一周安排」):授权粒度即整个应用,落 moduleId=0 的整应用记录。
// 这类应用没有可 diff 的模块明细,必须单独走应用级提交,否则勾选只是个视觉状态、刷新即失效。
const appModules = app.modules || [];
if (appModules.length === 0) {
const checked = !!node.checked;
try {
if (checked !== !!app.checked) {
app.checked = checked;
await Apis.GroupAuthz.setGroupModule({
data: [{
groupId,
appId: node.appId,
moduleId: 0,
mode: checked ? addMode.value : deleteMode.value,
isAll: 0,
}],
cacheFor: null,
});
}
syncInheritDoms(treeObj);
} catch (e: any) {
console.warn(e?.message || "操作失败");
await reloadCurrent();
}
return;
}
// 级联前快照:用于计算本次实际变化的模块明细
const before = new Map<number, boolean>();
walkModules(app.modules, (m) => before.set(Number(m.id), !!m.checked));
@@ -736,6 +775,15 @@ onMounted(async () => {
}
}
/* 父级勾选样式(2026-10-20):
zTree 的「半选」在两个状态位各有一套图标——true_part(父已勾但子未全勾)/
false_part(父未勾但子有勾)。这里把 true_part 的图标换成 true_full 的蓝底勾,
让"只要有一个子节点被选中"的父节点统一呈现为选中态,避免折叠时看着像没授权。 */
:deep(.ztree li span.button.chk.checkbox_true_part),
:deep(.ztree li span.button.chk.checkbox_true_part_focus) {
background-position: -26px -5px;
}
/* zTree 节点行尾的"子组继承"复选框:
float 会跟随节点缩进产生阶梯,改为绝对定位锚定卡片体右边界,所有层级右对齐成一列 */
:deep(.inherit-wrap) {
+32 -9
View File
@@ -64,7 +64,7 @@ type AppModuleVo = {
checked?: boolean;
children?: AppModuleVo[];
};
type AppItem = { id?: number; name?: string; sortNo?: number; modules?: AppModuleVo[] };
type AppItem = { id?: number; name?: string; sortNo?: number; checked?: boolean; modules?: AppModuleVo[] };
type CategoryItem = { id?: number; name?: string; apps?: AppItem[] };
type AuthzTreeNode = {
id: string;
@@ -118,12 +118,16 @@ function countTotalModules(nodes: AppModuleVo[]): number {
}
function isAppChecked(app: AppItem): boolean {
const modules = app.modules || [];
if (modules.length === 0) return false;
return modules.every((m) => isModuleAllChecked(m));
// 无子模块应用(如「一周安排」):勾选态来自后端返回的整应用授权记录
if (modules.length === 0) return !!app.checked;
// 父级展示口径:只要有一个子节点被勾选就算选中(不采用 zTree 半选,见样式里 true_part 的覆写)
return modules.some((m) => isModuleAnyChecked(m));
}
function isModuleAllChecked(node: AppModuleVo): boolean {
if (!node.checked) return false;
return (node.children || []).every((c) => isModuleAllChecked(c));
/** 自身或任一子孙被勾选 */
function isModuleAnyChecked(node: AppModuleVo): boolean {
if (node.checked) return true;
return (node.children || []).some((c) => isModuleAnyChecked(c));
}
// 空 children 不挂键:zTree 只要见到 children 是数组就强制 isParent=true 而渲染 +/−(叶子也会出现展开图标)
function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
@@ -132,7 +136,7 @@ function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
id: `mod_${m.id}`,
name: String(m.name || ""),
open: true,
checked: !!m.checked,
checked: isModuleAnyChecked(m),
nodeType: "module",
rawId: Number(m.id),
appId,
@@ -255,7 +259,12 @@ function onTreeCheck(node: any) {
// zTree 原生级联已生效,把该应用最终勾选态同步回本地数据(saveNow 的提交依据)
const app = findLocalApp(node.appId);
if (!app) return;
syncAppModulesFromTree(app, node);
if ((app.modules || []).length === 0) {
// 无子模块应用:没有模块明细可同步,直接记整应用勾选态
app.checked = !!node.checked;
} else {
syncAppModulesFromTree(app, node);
}
scheduleSave();
}
@@ -265,7 +274,14 @@ const saveNow = async () => {
const moduleIds: number[] = [];
for (const cat of categoryList.value || []) {
for (const app of cat.apps || []) {
walkModules(app.modules || [], (n) => { if (n.checked) moduleIds.push(Number(n.id)); });
const mods = app.modules || [];
if (mods.length === 0) {
// 无子模块应用:用 -appId 哨兵表示「整应用授权」(后端 AppLevelModuleId 约定),
// 否则这类应用永远收集不到 id、保存时被整表覆盖清掉。
if (app.checked) moduleIds.push(-Number(app.id));
continue;
}
walkModules(mods, (n) => { if (n.checked) moduleIds.push(Number(n.id)); });
}
}
try {
@@ -441,6 +457,13 @@ watch(
text-align: center;
}
/* 父级勾选样式(2026-10-20):把 zTree 半选状态位 true_part 的图标换成 true_full 的蓝底勾,
让「只要有一个子节点被选中」的父节点统一呈现为选中态(与组授权页口径一致)。 */
:deep(.ztree li span.button.chk.checkbox_true_part),
:deep(.ztree li span.button.chk.checkbox_true_part_focus) {
background-position: -26px -5px;
}
:deep(.ztree li a) {
max-width: calc(100% - 30px);
overflow: hidden;