手动代码级联与 zTree 内部状态机(checkNode 提前 return、setChkClass、repairParentChkClassWithSelf)反复交互,
导致除 open 外级联视觉完全失效、点击父节点无法联动子孙,整体错乱。
改为 zTree 原生 chkboxType {Y:"ps", N:"s"}:
- Y="ps":勾选任意节点 -> 父/祖先全部勾选 + 子孙全部勾选
- N="s":取消节点 -> 子孙全部取消,父/祖先不受影响
恰好对应需求 4 条规则(勾选子->祖先勾选;勾选父->子孙勾选;取消父->子孙取消;取消子->父/祖不动),
且视觉由 zTree 原生刷新,不再依赖手动 checkNode。
onTreeCheck 退化为只做本地数据同步:原生级联完成后把应用子树最终勾选态
(syncAppModulesFromTree)写回 AppItem.modules 作为提交依据;GroupAuth 仍按模块明细 diff,
角色授权页仍全量防抖保存。
This commit is contained in:
+25
-78
@@ -1,12 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* 树勾选级联工具
|
* 树勾选级联工具
|
||||||
*
|
*
|
||||||
* 规则(与 zTree chkboxType {Y:"ps", N:"s"} 语义一致,由代码实现,zTree 侧保持 {Y:"",N:""}):
|
* 级联由 zTree 原生 chkboxType 承担(两处页面均配置 {Y:"ps", N:"s"}):
|
||||||
* 1. 勾选子节点 -> 其所有父/祖先节点全部勾选
|
* Y="ps" 勾选任意节点 -> 其父/祖先全部勾选(p)+ 其下子孙全部勾选(s)
|
||||||
* 2. 勾选父节点 -> 其所有子孙节点全部勾选
|
* N="s" 取消节点 -> 其下子孙全部取消(s),不影响其父/祖先
|
||||||
* 3. 取消父节点 -> 其所有子孙节点全部取消
|
* 即:勾选子 -> 祖先全部勾选;勾选父 -> 子孙全部勾选;取消父 -> 子孙全部取消;取消子 -> 父/祖先不受影响。
|
||||||
* 4. 取消子节点 -> 父/祖先保持原状(不受影响)
|
*
|
||||||
* 取消任意节点时,同时清除其 inheritFlag(子组继承标记),并向下传播清除,避免残留。
|
* zTree 原生级联只作用于 zTree 节点,本工具负责把应用子树最终的勾选态同步回本地数据
|
||||||
|
* (AppItem.modules),作为模块明细 diff 提交 / 全量保存的依据。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ModuleVo = {
|
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 {
|
export function syncAppModulesFromTree(app: { modules?: ModuleVo[] } | null | undefined, anyNode: any): void {
|
||||||
if (!app?.modules) return;
|
if (!app?.modules || !anyNode) return;
|
||||||
const hit = findModuleWithPath(app.modules, moduleId);
|
let root: any = anyNode;
|
||||||
if (!hit) return;
|
while (root && root.nodeType !== "app") {
|
||||||
const { node, ancestors } = hit;
|
root = root.getParentNode ? root.getParentNode() : null;
|
||||||
node.checked = checked;
|
}
|
||||||
if (checked) {
|
if (!root) return;
|
||||||
ancestors.forEach((a) => {
|
const checkedMap = new Map<number, boolean>();
|
||||||
a.checked = true;
|
(function collect(n: any) {
|
||||||
});
|
if (n.nodeType === "module" && n.rawId != null) checkedMap.set(Number(n.rawId), !!n.checked);
|
||||||
walkModules(node.children, (c) => {
|
(n.children || []).forEach(collect);
|
||||||
c.checked = true;
|
})(root);
|
||||||
});
|
walkModules(app.modules, (m) => {
|
||||||
} else {
|
const v = checkedMap.get(Number(m.id));
|
||||||
walkModules([node], (c) => {
|
if (v !== undefined) m.checked = v;
|
||||||
c.checked = false;
|
|
||||||
c.inheritFlag = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 应用整级勾选/取消:全部模块同步(取消时清除 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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+9
-19
@@ -66,7 +66,7 @@ import Apis from "@/api";
|
|||||||
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
|
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
|
||||||
import MyTree from "@/components/MyTree.vue";
|
import MyTree from "@/components/MyTree.vue";
|
||||||
import { useDefaultStore } from "@/store";
|
import { useDefaultStore } from "@/store";
|
||||||
import { applyModuleCascade, checkNodeWithCascade, setAppModulesChecked, walkModules } from "@/utils/treeCascade";
|
import { syncAppModulesFromTree, walkModules } from "@/utils/treeCascade";
|
||||||
|
|
||||||
defineOptions({ name: "GroupAuth" });
|
defineOptions({ name: "GroupAuth" });
|
||||||
|
|
||||||
@@ -330,9 +330,9 @@ const treeSetting = {
|
|||||||
check: {
|
check: {
|
||||||
enable: true,
|
enable: true,
|
||||||
chkStyle: "checkbox",
|
chkStyle: "checkbox",
|
||||||
// zTree 不级联,父子联动由 onTreeCheck 里的代码实现(checkNodeWithCascade):
|
// 原生级联即可满足需求:
|
||||||
// 勾选子 -> 祖先全部勾选;勾选父 -> 子孙全部勾选;取消父 -> 子孙全部取消;取消子 -> 父不受影响
|
// Y="ps" 勾选子->父/祖先全部勾选、勾选父->子孙全部勾选;N="s" 取消父->子孙全部取消、取消子->父/祖先不受影响
|
||||||
chkboxType: { Y: "", N: "" },
|
chkboxType: { Y: "ps", N: "s" },
|
||||||
},
|
},
|
||||||
callback: {},
|
callback: {},
|
||||||
};
|
};
|
||||||
@@ -400,7 +400,7 @@ function findLocalApp(appId: number): AppItem | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// walkModules / 本地级联(setAppModulesChecked、applyModuleCascade)统一在 utils/treeCascade.ts
|
// walkModules / 本地勾选态同步(syncAppModulesFromTree)统一在 utils/treeCascade.ts
|
||||||
|
|
||||||
function setLocalAppInherit(appId: number, inherit: boolean) {
|
function setLocalAppInherit(appId: number, inherit: boolean) {
|
||||||
const app = findLocalApp(appId);
|
const app = findLocalApp(appId);
|
||||||
@@ -421,17 +421,13 @@ function setLocalModuleState(appId: number, moduleId: number, patch: Partial<App
|
|||||||
// ---------------- 授权操作 ----------------
|
// ---------------- 授权操作 ----------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 勾选/取消勾选(代码级联,仅提交模块明细):
|
* 勾选/取消勾选:级联由 zTree 原生 chkboxType(Y:"ps" N:"s")完成,
|
||||||
* - 勾选子节点 -> 祖先/子孙全部勾选;取消子节点 -> 子孙取消、祖先不受影响
|
* 这里把该应用最终勾选态同步回本地数据(提交依据),仅按模块明细 diff 提交 追加/删除。
|
||||||
* - 勾选/取消应用节点 -> 该应用全部模块勾选/取消
|
|
||||||
* 提交:对比本次级联前后该应用内各模块的勾选状态,仅把发生变化的模块按 追加/删除 提交。
|
|
||||||
*/
|
*/
|
||||||
async function onTreeCheck(node: any) {
|
async function onTreeCheck(node: any) {
|
||||||
const groupId = selectedGroupId.value;
|
const groupId = selectedGroupId.value;
|
||||||
if (!groupId || !node) return;
|
if (!groupId || !node) return;
|
||||||
|
|
||||||
const checked = !!node.checked;
|
|
||||||
const isApp = node.nodeType === "app";
|
|
||||||
const treeObj = getTreeObjByNode(node);
|
const treeObj = getTreeObjByNode(node);
|
||||||
const app = findLocalApp(node.appId);
|
const app = findLocalApp(node.appId);
|
||||||
if (!app) {
|
if (!app) {
|
||||||
@@ -444,14 +440,8 @@ async function onTreeCheck(node: any) {
|
|||||||
walkModules(app.modules, (m) => before.set(Number(m.id), !!m.checked));
|
walkModules(app.modules, (m) => before.set(Number(m.id), !!m.checked));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 本地数据(提交依据)与 zTree 视觉状态同步级联
|
// zTree 原生级联已生效,同步最终勾选态到本地数据
|
||||||
if (isApp) {
|
syncAppModulesFromTree(app, node);
|
||||||
setAppModulesChecked(app, checked);
|
|
||||||
checkNodeWithCascade(treeObj, node, checked);
|
|
||||||
} else {
|
|
||||||
applyModuleCascade(app, node.rawId, checked);
|
|
||||||
checkNodeWithCascade(treeObj, node, checked);
|
|
||||||
}
|
|
||||||
syncInheritDoms(treeObj);
|
syncInheritDoms(treeObj);
|
||||||
|
|
||||||
const after = new Map<number, boolean>();
|
const after = new Map<number, boolean>();
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import { computed, ref, shallowRef, reactive, watch } from "vue";
|
|||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import Apis from "@/api";
|
import Apis from "@/api";
|
||||||
import MyTree from "@/components/MyTree.vue";
|
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 }>();
|
const props = defineProps<{ roleId: number | null; roleName?: string }>();
|
||||||
|
|
||||||
@@ -73,7 +73,6 @@ type AuthzTreeNode = {
|
|||||||
type CategoryTreeItem = { cat: CategoryItem; treeData: AuthzTreeNode[]; weight: number };
|
type CategoryTreeItem = { cat: CategoryItem; treeData: AuthzTreeNode[]; weight: number };
|
||||||
|
|
||||||
const COLUMN_COUNT = 3;
|
const COLUMN_COUNT = 3;
|
||||||
const jq = () => (window as any).$;
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const categoryList = shallowRef<CategoryItem[]>([]);
|
const categoryList = shallowRef<CategoryItem[]>([]);
|
||||||
@@ -87,9 +86,9 @@ const treeSetting = {
|
|||||||
check: {
|
check: {
|
||||||
enable: true,
|
enable: true,
|
||||||
chkStyle: "checkbox",
|
chkStyle: "checkbox",
|
||||||
// zTree 不级联,父子联动由 onTreeCheck 里的代码实现(checkNodeWithCascade):
|
// 原生级联即可满足需求:
|
||||||
// 勾选子 -> 祖先全部勾选;勾选父 -> 子孙全部勾选;取消父 -> 子孙全部取消;取消子 -> 父不受影响
|
// Y="ps" 勾选子->父/祖先全部勾选、勾选父->子孙全部勾选;N="s" 取消父->子孙全部取消、取消子->父/祖先不受影响
|
||||||
chkboxType: { Y: "", N: "" },
|
chkboxType: { Y: "ps", N: "s" },
|
||||||
},
|
},
|
||||||
callback: {},
|
callback: {},
|
||||||
};
|
};
|
||||||
@@ -230,29 +229,16 @@ function findLocalApp(appId: number): AppItem | null {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function getTreeObjByNode(node: any) {
|
// walkModules / 本地勾选态同步(syncAppModulesFromTree)统一在 utils/treeCascade.ts
|
||||||
const $ = jq();
|
|
||||||
const treeId = String(node?.tId || "").replace(/_\d+$/, "");
|
|
||||||
return $?.fn?.zTree?.getZTreeObj(treeId) || null;
|
|
||||||
}
|
|
||||||
// walkModules / 本地级联(setAppModulesChecked、applyModuleCascade)统一在 utils/treeCascade.ts
|
|
||||||
|
|
||||||
// ---------------- 授权操作(代码级联,勾选即保存,全量覆盖 + 防抖) ----------------
|
// ---------------- 授权操作(级联由 zTree 原生 chkboxType 完成,勾选即保存,全量覆盖 + 防抖) ----------------
|
||||||
function onTreeCheck(node: any) {
|
function onTreeCheck(node: any) {
|
||||||
const roleId = props.roleId;
|
const roleId = props.roleId;
|
||||||
if (!roleId || !node) return;
|
if (!roleId || !node) return;
|
||||||
const checked = !!node.checked;
|
// zTree 原生级联已生效,把该应用最终勾选态同步回本地数据(saveNow 的提交依据)
|
||||||
const treeObj = getTreeObjByNode(node);
|
|
||||||
const app = findLocalApp(node.appId);
|
const app = findLocalApp(node.appId);
|
||||||
|
if (!app) return;
|
||||||
// 本地数据(saveNow 的提交依据)与 zTree 视觉状态同步级联
|
syncAppModulesFromTree(app, node);
|
||||||
if (node.nodeType === "app") {
|
|
||||||
setAppModulesChecked(app, checked);
|
|
||||||
checkNodeWithCascade(treeObj, node, checked);
|
|
||||||
} else {
|
|
||||||
applyModuleCascade(app, node.rawId, checked);
|
|
||||||
checkNodeWithCascade(treeObj, node, checked);
|
|
||||||
}
|
|
||||||
scheduleSave();
|
scheduleSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user