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;