From 32b73ba4074a84c0972853b05464f1f88d5823fb Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 20 Oct 2026 12:18:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=8E=88=E6=9D=83):=20=E7=88=B6=E7=BA=A7?= =?UTF-8?q?=E5=8B=BE=E9=80=89=E6=80=81=E6=94=B9=E3=80=8C=E4=BB=BB=E4=B8=80?= =?UTF-8?q?=E5=AD=90=E8=8A=82=E7=82=B9=E9=80=89=E4=B8=AD=E5=8D=B3=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E3=80=8D+=20=E6=97=A0=E6=A8=A1=E5=9D=97=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=8F=AF=E6=8E=88=E6=9D=83=EF=BC=88=E7=BB=84=E6=8E=88?= =?UTF-8?q?=E6=9D=83/=E8=A7=92=E8=89=B2=E6=8E=88=E6=9D=83=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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)。 --- src/views/GroupAuth.vue | 62 ++++++++++++++++++--- src/views/role-authz/AppModuleCheckTree.vue | 41 +++++++++++--- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/views/GroupAuth.vue b/src/views/GroupAuth.vue index e13e9a5..4da3713 100644 --- a/src/views/GroupAuth.vue +++ b/src/views/GroupAuth.vue @@ -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(); 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) { diff --git a/src/views/role-authz/AppModuleCheckTree.vue b/src/views/role-authz/AppModuleCheckTree.vue index 03bdefe..2b27e18 100644 --- a/src/views/role-authz/AppModuleCheckTree.vue +++ b/src/views/role-authz/AppModuleCheckTree.vue @@ -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;