feat(auth): 树勾选级联+点击文字勾选+树默认闭合
build / build (push) Successful in 3m19s

- 新增共享级联工具 utils/treeCascade.ts:勾选子->祖先全部勾选;勾选父->子孙全部勾选/取消;取消子->父/祖不受影响(zTree 侧 chkboxType 置空,由代码级联)
- GroupAuth.vue:接入代码级联,模块明细按 diff 提交(追加/删除),取消时同步清理子组继承标记
- AppModuleCheckTree.vue:角色授权接入同一套级联逻辑
- 组/角色授权树与应用节点默认闭合,点击文字等价勾选框(MyTree onClick 同步触发 onCheck)
- PermissionView.vue「已授权应用」应用一级默认不展开模块
This commit is contained in:
chenxing
2026-09-15 16:51:43 +08:00
parent e0252eb997
commit e1cdc26697
4 changed files with 172 additions and 99 deletions
+100
View File
@@ -0,0 +1,100 @@
/**
* 树勾选级联工具
*
* 规则(与 zTree chkboxType {Y:"ps", N:"s"} 语义一致,由代码实现,zTree 侧保持 {Y:"",N:""}):
* 1. 勾选子节点 -> 其所有父/祖先节点全部勾选
* 2. 勾选父节点 -> 其所有子孙节点全部勾选
* 3. 取消父节点 -> 其所有子孙节点全部取消
* 4. 取消子节点 -> 父/祖先保持原状(不受影响)
* 取消任意节点时,同时清除其 inheritFlag(子组继承标记),并向下传播清除,避免残留。
*/
export type ModuleVo = {
id?: number;
name?: string;
checked?: boolean;
inheritFlag?: boolean;
children?: ModuleVo[];
};
export function walkModules(nodes: ModuleVo[] | undefined, fn: (n: ModuleVo) => void): void {
(nodes || []).forEach((n) => {
fn(n);
walkModules(n.children, fn);
});
}
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 嵌套结构)级联。
* 勾选:自身 + 祖先链 + 子孙全部勾选;取消:自身 + 子孙全部取消(祖先不动)。
*/
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;
});
}
}
/** 应用整级勾选/取消:全部模块同步(取消时清除 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) => {
n.checked = v;
if (!v && n.inheritFlag !== undefined) n.inheritFlag = false;
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);
});
}
+57 -57
View File
@@ -42,7 +42,7 @@
<span class="inherit-cap">子组继承</span>
</div>
<div v-show="!collapsedMap[String(item.cat.id)]" class="cat-card-body">
<MyTree :treeData="item.treeData" :setting="treeSetting" :autoClick="false"
<MyTree :treeData="item.treeData" :setting="treeSetting" :autoClick="false" :isCheck="true"
@on-tree-check="onTreeCheck" />
</div>
</div>
@@ -62,11 +62,11 @@
<script lang="ts" setup>
import { computed, onMounted, reactive, ref, shallowRef, watch } from "vue";
import { Message } from "view-ui-plus";
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";
defineOptions({ name: "GroupAuth" });
@@ -249,7 +249,7 @@ function buildAppNode(app: AppItem): AuthzTreeNode {
return {
id: `app_${appId}`,
name: String(app.name || ""),
open: true,
open: false, // 一级默认闭合,点击才展开模块
checked: isAppChecked(app),
inheritFlag: isAppInherit(app),
nodeType: "app",
@@ -330,7 +330,8 @@ const treeSetting = {
check: {
enable: true,
chkStyle: "checkbox",
// 不自动级联:模块逐条独立授权;应用节点勾选后由代码手动同步子节点
// zTree 不级联,父子联动由 onTreeCheck 里的代码实现(checkNodeWithCascade):
// 勾选子 -> 祖先全部勾选;勾选父 -> 子孙全部勾选;取消父 -> 子孙全部取消;取消子 -> 父不受影响
chkboxType: { Y: "", N: "" },
},
callback: {},
@@ -342,18 +343,6 @@ function getTreeObjByNode(node: any) {
return $?.fn?.zTree?.getZTreeObj(treeId) || null;
}
/** 模块勾选变化后,按本地数据回算应用节点的勾选/继承态并同步到 zTree */
function syncAppNodeState(treeObj: any, appId: number) {
if (!treeObj) return;
const app = findLocalApp(appId);
if (!app) return;
const appNode = treeObj.getNodesByFilter((n: any) => n.id === `app_${appId}`, true);
if (!appNode) return;
appNode.checked = isAppChecked(app);
appNode.inheritFlag = isAppInherit(app);
treeObj.checkNode(appNode, appNode.checked, false);
}
/** 按 zTree 当前节点状态刷新所有"子组继承"复选框的勾选与禁用态 */
function syncInheritDoms(treeObj: any) {
const $ = jq();
@@ -411,21 +400,7 @@ function findLocalApp(appId: number): AppItem | null {
return null;
}
function walkModules(nodes: AppModuleVo[], fn: (n: AppModuleVo) => void) {
for (const node of nodes) {
fn(node);
walkModules(node.children || [], fn);
}
}
function setLocalAppChecked(appId: number, checked: boolean) {
const app = findLocalApp(appId);
if (!app) return;
walkModules(app.modules || [], (n) => {
n.checked = checked;
if (!checked) n.inheritFlag = false;
});
}
// walkModules / 本地级联(setAppModulesChecked、applyModuleCascade)统一在 utils/treeCascade.ts
function setLocalAppInherit(appId: number, inherit: boolean) {
const app = findLocalApp(appId);
@@ -445,7 +420,12 @@ function setLocalModuleState(appId: number, moduleId: number, patch: Partial<App
// ---------------- 授权操作 ----------------
/** 勾选/取消勾选:应用节点 => moduleId=0 整应用处理;模块节点 => 单模块处理 */
/**
* 勾选/取消勾选(代码级联,仅提交模块明细):
* - 勾选子节点 -> 祖先/子孙全部勾选;取消子节点 -> 子孙取消、祖先不受影响
* - 勾选/取消应用节点 -> 该应用全部模块勾选/取消
* 提交:对比本次级联前后该应用内各模块的勾选状态,仅把发生变化的模块按 追加/删除 提交。
*/
async function onTreeCheck(node: any) {
const groupId = selectedGroupId.value;
if (!groupId || !node) return;
@@ -453,43 +433,63 @@ async function onTreeCheck(node: any) {
const checked = !!node.checked;
const isApp = node.nodeType === "app";
const treeObj = getTreeObjByNode(node);
const app = findLocalApp(node.appId);
if (!app) {
await reloadCurrent();
return;
}
// 级联前快照:用于计算本次实际变化的模块明细
const before = new Map<number, boolean>();
walkModules(app.modules, (m) => before.set(Number(m.id), !!m.checked));
try {
await Apis.GroupAuthz.setGroupModule({
data: [{
groupId,
appId: node.appId,
moduleId: isApp ? 0 : node.rawId,
mode: checked ? addMode.value : deleteMode.value,
isAll: checked && node.inheritFlag ? 1 : 0,
}],
cacheFor: null,
});
// 本地数据(提交依据)与 zTree 视觉状态同步级联
if (isApp) {
setLocalAppChecked(node.appId, checked);
// 应用节点整批操作:手动同步 zTree 子节点勾选态与继承标记
if (treeObj) {
const affected = treeObj.transformToArray([node]) || [];
affected.forEach((n: any) => {
if (n !== node) treeObj.checkNode(n, checked, false);
n.checked = checked;
if (!checked) n.inheritFlag = false;
});
}
Message.success(checked ? "已授权该应用全部模块" : "已取消该应用全部模块授权");
setAppModulesChecked(app, checked);
checkNodeWithCascade(treeObj, node, checked);
} else {
if (!checked) node.inheritFlag = false;
setLocalModuleState(node.appId, node.rawId, checked ? { checked: true } : { checked: false, inheritFlag: false });
syncAppNodeState(treeObj, node.appId);
applyModuleCascade(app, node.rawId, checked);
checkNodeWithCascade(treeObj, node, checked);
}
syncInheritDoms(treeObj);
const after = new Map<number, boolean>();
walkModules(app.modules, (m) => after.set(Number(m.id), !!m.checked));
const changes = buildModuleDiff(groupId, node.appId, before, after);
if (changes.length > 0) {
await Apis.GroupAuthz.setGroupModule({ data: changes, cacheFor: null });
}
} catch (e: any) {
console.warn(e?.message || "操作失败");
await reloadCurrent();
}
}
/** 对比前后模块勾选快照,仅输出变化的模块(追加/删除);父/祖先节点只做展示,不进提交 */
function buildModuleDiff(
groupId: number,
appId: number,
before: Map<number, boolean>,
after: Map<number, boolean>
): Array<Record<string, any>> {
const changes: Array<Record<string, any>> = [];
const ids = new Set<number>([...before.keys(), ...after.keys()]);
ids.forEach((id) => {
const prev = before.get(id);
const next = after.get(id);
if (prev === next) return;
changes.push({
groupId,
appId,
moduleId: id,
mode: next ? addMode.value : deleteMode.value,
isAll: 0,
});
});
return changes;
}
/** 子组继承开关:应用节点 => moduleId=0 整应用处理;模块节点 => 单模块处理 */
async function onInheritToggle(treeId: string, node: any, inherit: boolean) {
const $ = jq();
+1 -1
View File
@@ -320,7 +320,7 @@ function formatApps(apps: AppNode[], catId?: number): any[] {
return (apps || []).map((app) => ({
id: `app_${app.id}_${catId || 0}`,
name: app.name || `应用${app.id}`,
open: true,
open: false, // 应用一级默认闭合,点击才展开模块
children: formatModules(app.modules || []),
}));
}
+14 -41
View File
@@ -28,7 +28,7 @@
<span class="cat-count">{{ item.treeData.length }}个应用</span>
</div>
<div v-show="!collapsedMap[String(item.cat.id)]" class="cat-card-body">
<MyTree :treeData="item.treeData" :setting="treeSetting" :autoClick="false"
<MyTree :treeData="item.treeData" :setting="treeSetting" :autoClick="false" :isCheck="true"
@on-tree-check="onTreeCheck" />
</div>
</div>
@@ -47,6 +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";
const props = defineProps<{ roleId: number | null; roleName?: string }>();
@@ -86,7 +87,8 @@ const treeSetting = {
check: {
enable: true,
chkStyle: "checkbox",
// 不自动级联:模块逐条独立授权(角色无"子组继承"概念)
// zTree 不级联,父子联动由 onTreeCheck 里的代码实现(checkNodeWithCascade):
// 勾选子 -> 祖先全部勾选;勾选父 -> 子孙全部勾选;取消父 -> 子孙全部取消;取消子 -> 父不受影响
chkboxType: { Y: "", N: "" },
},
callback: {},
@@ -131,7 +133,7 @@ function buildAppNode(app: AppItem): AuthzTreeNode {
return {
id: `app_${appId}`,
name: String(app.name || ""),
open: true,
open: false, // 一级默认闭合,点击才展开模块
checked: isAppChecked(app),
nodeType: "app",
rawId: appId,
@@ -228,57 +230,28 @@ function findLocalApp(appId: number): AppItem | null {
}
return null;
}
function walkModules(nodes: AppModuleVo[], fn: (n: AppModuleVo) => void) {
for (const node of nodes) {
fn(node);
walkModules(node.children || [], fn);
}
}
function setLocalAppChecked(appId: number, checked: boolean) {
const app = findLocalApp(appId);
if (!app) return;
walkModules(app.modules || [], (n) => { n.checked = checked; });
}
function setLocalModuleState(appId: number, moduleId: number, patch: Partial<AppModuleVo>) {
const app = findLocalApp(appId);
if (!app) return;
walkModules(app.modules || [], (n) => { if (Number(n.id) === moduleId) Object.assign(n, patch); });
}
function getTreeObjByNode(node: any) {
const $ = jq();
const treeId = String(node?.tId || "").replace(/_\d+$/, "");
return $?.fn?.zTree?.getZTreeObj(treeId) || null;
}
function syncAppNodeState(treeObj: any, appId: number) {
if (!treeObj) return;
const app = findLocalApp(appId);
if (!app) return;
const appNode = treeObj.getNodesByFilter((n: any) => n.id === `app_${appId}`, true);
if (!appNode) return;
appNode.checked = isAppChecked(app);
treeObj.checkNode(appNode, appNode.checked, false);
}
// walkModules / 本地级联(setAppModulesChecked、applyModuleCascade)统一在 utils/treeCascade.ts
// ---------------- 授权操作(勾选即保存,全量覆盖 + 防抖) ----------------
// ---------------- 授权操作(代码级联,勾选即保存,全量覆盖 + 防抖) ----------------
function onTreeCheck(node: any) {
const roleId = props.roleId;
if (!roleId || !node) return;
const checked = !!node.checked;
const isApp = node.nodeType === "app";
const treeObj = getTreeObjByNode(node);
const app = findLocalApp(node.appId);
if (isApp) {
setLocalAppChecked(node.appId, checked);
if (treeObj) {
const affected = treeObj.transformToArray([node]) || [];
affected.forEach((n: any) => {
if (n !== node) treeObj.checkNode(n, checked, false);
n.checked = checked;
});
}
// 本地数据(saveNow 的提交依据)与 zTree 视觉状态同步级联
if (node.nodeType === "app") {
setAppModulesChecked(app, checked);
checkNodeWithCascade(treeObj, node, checked);
} else {
setLocalModuleState(node.appId, node.rawId, { checked });
syncAppNodeState(treeObj, node.appId);
applyModuleCascade(app, node.rawId, checked);
checkNodeWithCascade(treeObj, node, checked);
}
scheduleSave();
}