diff --git a/src/utils/treeCascade.ts b/src/utils/treeCascade.ts index 33a71ca..b686d50 100644 --- a/src/utils/treeCascade.ts +++ b/src/utils/treeCascade.ts @@ -1,12 +1,13 @@ /** * 树勾选级联工具 * - * 规则(与 zTree chkboxType {Y:"ps", N:"s"} 语义一致,由代码实现,zTree 侧保持 {Y:"",N:""}): - * 1. 勾选子节点 -> 其所有父/祖先节点全部勾选 - * 2. 勾选父节点 -> 其所有子孙节点全部勾选 - * 3. 取消父节点 -> 其所有子孙节点全部取消 - * 4. 取消子节点 -> 父/祖先保持原状(不受影响) - * 取消任意节点时,同时清除其 inheritFlag(子组继承标记),并向下传播清除,避免残留。 + * 级联由 zTree 原生 chkboxType 承担(两处页面均配置 {Y:"ps", N:"s"}): + * Y="ps" 勾选任意节点 -> 其父/祖先全部勾选(p)+ 其下子孙全部勾选(s) + * N="s" 取消节点 -> 其下子孙全部取消(s),不影响其父/祖先 + * 即:勾选子 -> 祖先全部勾选;勾选父 -> 子孙全部勾选;取消父 -> 子孙全部取消;取消子 -> 父/祖先不受影响。 + * + * zTree 原生级联只作用于 zTree 节点,本工具负责把应用子树最终的勾选态同步回本地数据 + * (AppItem.modules),作为模块明细 diff 提交 / 全量保存的依据。 */ export type ModuleVo = { @@ -24,79 +25,25 @@ export function walkModules(nodes: ModuleVo[] | undefined, fn: (n: ModuleVo) => }); } -function findModuleWithPath( - nodes: ModuleVo[] | undefined, - moduleId: number, - ancestors: ModuleVo[] = [] -): { node: ModuleVo; ancestors: ModuleVo[] } | null { - for (const n of nodes || []) { - if (Number(n.id) === moduleId) return { node: n, ancestors }; - const hit = findModuleWithPath(n.children, moduleId, [...ancestors, n]); - if (hit) return hit; - } - return null; -} - /** - * 本地数据(AppItem.modules 嵌套结构)级联。 - * 勾选:自身 + 祖先链 + 子孙全部勾选;取消:自身 + 子孙全部取消(祖先不动)。 + * 原生级联完成后调用:把 zTree 中某应用子树的最终勾选态同步回本地 AppItem.modules。 + * @param app 本地应用数据(提交依据) + * @param anyNode zTree 内该应用下任意节点(应用节点或模块节点),沿父链回溯到应用根节点 */ -export function applyModuleCascade(app: { modules?: ModuleVo[] } | null | undefined, moduleId: number, checked: boolean): void { - if (!app?.modules) return; - const hit = findModuleWithPath(app.modules, moduleId); - if (!hit) return; - const { node, ancestors } = hit; - node.checked = checked; - if (checked) { - ancestors.forEach((a) => { - a.checked = true; - }); - walkModules(node.children, (c) => { - c.checked = true; - }); - } else { - walkModules([node], (c) => { - c.checked = false; - c.inheritFlag = false; - }); +export function syncAppModulesFromTree(app: { modules?: ModuleVo[] } | null | undefined, anyNode: any): void { + if (!app?.modules || !anyNode) return; + let root: any = anyNode; + while (root && root.nodeType !== "app") { + root = root.getParentNode ? root.getParentNode() : null; } -} - -/** 应用整级勾选/取消:全部模块同步(取消时清除 inheritFlag) */ -export function setAppModulesChecked(app: { modules?: ModuleVo[] } | null | undefined, checked: boolean): void { - walkModules(app?.modules, (n) => { - n.checked = checked; - if (!checked) n.inheritFlag = false; - }); -} - -/** - * zTree 节点级联(仅同步视觉状态,checkTypeFlag / callbackFlag 均传 false,不触发 onCheck、不重复走 chkboxType)。 - * 勾选:自身 + 祖先链 + 子孙;取消:自身 + 子孙(祖先不动)。 - */ -export function checkNodeWithCascade(treeObj: any, node: any, checked: boolean): void { - if (!treeObj || !node) return; - const set = (n: any, v: boolean) => { - if (!v && n.inheritFlag !== undefined) n.inheritFlag = false; - // 注意:必须先调 checkNode,由 zTree 内部设置 checked 并调用 setChkClass 刷新复选框外观。 - // 若先手动 n.checked = v 再调 checkNode,会命中 zTree 的 - // `node.checked === checked && !checkTypeFlag` 提前 return,导致联动节点的外观不刷新。 - treeObj.checkNode(n, v, false, false); - }; - set(node, checked); - if (checked) { - let parent: any = node.getParentNode ? node.getParentNode() : null; - while (parent) { - set(parent, true); - parent = parent.getParentNode ? parent.getParentNode() : null; - } - } - walkZtreeChildren(node, (c) => set(c, checked)); -} - -function walkZtreeChildren(node: any, fn: (n: any) => void): void { - (node.children || []).forEach((c: any) => { - fn(c); - walkZtreeChildren(c, fn); + if (!root) return; + const checkedMap = new Map(); + (function collect(n: any) { + if (n.nodeType === "module" && n.rawId != null) checkedMap.set(Number(n.rawId), !!n.checked); + (n.children || []).forEach(collect); + })(root); + walkModules(app.modules, (m) => { + const v = checkedMap.get(Number(m.id)); + if (v !== undefined) m.checked = v; }); } \ No newline at end of file diff --git a/src/views/GroupAuth.vue b/src/views/GroupAuth.vue index 59e2bc9..8c30156 100644 --- a/src/views/GroupAuth.vue +++ b/src/views/GroupAuth.vue @@ -66,7 +66,7 @@ import Apis from "@/api"; import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue"; import MyTree from "@/components/MyTree.vue"; import { useDefaultStore } from "@/store"; -import { applyModuleCascade, checkNodeWithCascade, setAppModulesChecked, walkModules } from "@/utils/treeCascade"; +import { syncAppModulesFromTree, walkModules } from "@/utils/treeCascade"; defineOptions({ name: "GroupAuth" }); @@ -330,9 +330,9 @@ const treeSetting = { check: { enable: true, chkStyle: "checkbox", - // zTree 不级联,父子联动由 onTreeCheck 里的代码实现(checkNodeWithCascade): - // 勾选子 -> 祖先全部勾选;勾选父 -> 子孙全部勾选;取消父 -> 子孙全部取消;取消子 -> 父不受影响 - chkboxType: { Y: "", N: "" }, + // 原生级联即可满足需求: + // Y="ps" 勾选子->父/祖先全部勾选、勾选父->子孙全部勾选;N="s" 取消父->子孙全部取消、取消子->父/祖先不受影响 + chkboxType: { Y: "ps", N: "s" }, }, callback: {}, }; @@ -400,7 +400,7 @@ function findLocalApp(appId: number): AppItem | null { return null; } -// walkModules / 本地级联(setAppModulesChecked、applyModuleCascade)统一在 utils/treeCascade.ts +// walkModules / 本地勾选态同步(syncAppModulesFromTree)统一在 utils/treeCascade.ts function setLocalAppInherit(appId: number, inherit: boolean) { const app = findLocalApp(appId); @@ -421,17 +421,13 @@ function setLocalModuleState(appId: number, moduleId: number, patch: Partial 祖先/子孙全部勾选;取消子节点 -> 子孙取消、祖先不受影响 - * - 勾选/取消应用节点 -> 该应用全部模块勾选/取消 - * 提交:对比本次级联前后该应用内各模块的勾选状态,仅把发生变化的模块按 追加/删除 提交。 + * 勾选/取消勾选:级联由 zTree 原生 chkboxType(Y:"ps" N:"s")完成, + * 这里把该应用最终勾选态同步回本地数据(提交依据),仅按模块明细 diff 提交 追加/删除。 */ async function onTreeCheck(node: any) { const groupId = selectedGroupId.value; if (!groupId || !node) return; - const checked = !!node.checked; - const isApp = node.nodeType === "app"; const treeObj = getTreeObjByNode(node); const app = findLocalApp(node.appId); if (!app) { @@ -444,14 +440,8 @@ async function onTreeCheck(node: any) { walkModules(app.modules, (m) => before.set(Number(m.id), !!m.checked)); try { - // 本地数据(提交依据)与 zTree 视觉状态同步级联 - if (isApp) { - setAppModulesChecked(app, checked); - checkNodeWithCascade(treeObj, node, checked); - } else { - applyModuleCascade(app, node.rawId, checked); - checkNodeWithCascade(treeObj, node, checked); - } + // zTree 原生级联已生效,同步最终勾选态到本地数据 + syncAppModulesFromTree(app, node); syncInheritDoms(treeObj); const after = new Map(); diff --git a/src/views/role-authz/AppModuleCheckTree.vue b/src/views/role-authz/AppModuleCheckTree.vue index c4dabf1..c70041a 100644 --- a/src/views/role-authz/AppModuleCheckTree.vue +++ b/src/views/role-authz/AppModuleCheckTree.vue @@ -47,7 +47,7 @@ import { computed, ref, shallowRef, reactive, watch } from "vue"; import { debounce } from "lodash"; import Apis from "@/api"; import MyTree from "@/components/MyTree.vue"; -import { applyModuleCascade, checkNodeWithCascade, setAppModulesChecked, walkModules } from "@/utils/treeCascade"; +import { syncAppModulesFromTree, walkModules } from "@/utils/treeCascade"; const props = defineProps<{ roleId: number | null; roleName?: string }>(); @@ -73,7 +73,6 @@ type AuthzTreeNode = { type CategoryTreeItem = { cat: CategoryItem; treeData: AuthzTreeNode[]; weight: number }; const COLUMN_COUNT = 3; -const jq = () => (window as any).$; const loading = ref(false); const categoryList = shallowRef([]); @@ -87,9 +86,9 @@ const treeSetting = { check: { enable: true, chkStyle: "checkbox", - // zTree 不级联,父子联动由 onTreeCheck 里的代码实现(checkNodeWithCascade): - // 勾选子 -> 祖先全部勾选;勾选父 -> 子孙全部勾选;取消父 -> 子孙全部取消;取消子 -> 父不受影响 - chkboxType: { Y: "", N: "" }, + // 原生级联即可满足需求: + // Y="ps" 勾选子->父/祖先全部勾选、勾选父->子孙全部勾选;N="s" 取消父->子孙全部取消、取消子->父/祖先不受影响 + chkboxType: { Y: "ps", N: "s" }, }, callback: {}, }; @@ -230,29 +229,16 @@ function findLocalApp(appId: number): AppItem | null { } return null; } -function getTreeObjByNode(node: any) { - const $ = jq(); - const treeId = String(node?.tId || "").replace(/_\d+$/, ""); - return $?.fn?.zTree?.getZTreeObj(treeId) || null; -} -// walkModules / 本地级联(setAppModulesChecked、applyModuleCascade)统一在 utils/treeCascade.ts +// walkModules / 本地勾选态同步(syncAppModulesFromTree)统一在 utils/treeCascade.ts -// ---------------- 授权操作(代码级联,勾选即保存,全量覆盖 + 防抖) ---------------- +// ---------------- 授权操作(级联由 zTree 原生 chkboxType 完成,勾选即保存,全量覆盖 + 防抖) ---------------- function onTreeCheck(node: any) { const roleId = props.roleId; if (!roleId || !node) return; - const checked = !!node.checked; - const treeObj = getTreeObjByNode(node); + // zTree 原生级联已生效,把该应用最终勾选态同步回本地数据(saveNow 的提交依据) const app = findLocalApp(node.appId); - - // 本地数据(saveNow 的提交依据)与 zTree 视觉状态同步级联 - if (node.nodeType === "app") { - setAppModulesChecked(app, checked); - checkNodeWithCascade(treeObj, node, checked); - } else { - applyModuleCascade(app, node.rawId, checked); - checkNodeWithCascade(treeObj, node, checked); - } + if (!app) return; + syncAppModulesFromTree(app, node); scheduleSave(); }