权限一览修订
build / build (push) Successful in 3m40s

This commit is contained in:
chenxing
2026-09-16 09:44:54 +08:00
parent 711765234f
commit 4237615238
2 changed files with 145 additions and 9 deletions
+1
View File
@@ -85,6 +85,7 @@ export default {
'SysOapiLog.oapiLogDownload': ['GET', '/api/oapilog/Download/{id}'], 'SysOapiLog.oapiLogDownload': ['GET', '/api/oapilog/Download/{id}'],
'GroupAuthz.userRoles': ['GET', '/api/groupauthz/UserRoles'], 'GroupAuthz.userRoles': ['GET', '/api/groupauthz/UserRoles'],
'GroupAuthz.userPermissionTree': ['GET', '/api/groupauthz/UserPermissionTree'], 'GroupAuthz.userPermissionTree': ['GET', '/api/groupauthz/UserPermissionTree'],
'GroupAuthz.groupPermissionTree': ['GET', '/api/groupauthz/GroupPermissionTree'],
'GroupAuthz.userGroupTree': ['GET', '/api/groupauthz/UserGroupTree'], 'GroupAuthz.userGroupTree': ['GET', '/api/groupauthz/UserGroupTree'],
'GroupAuthz.searchUsers': ['GET', '/api/groupauthz/SearchUsers'], 'GroupAuthz.searchUsers': ['GET', '/api/groupauthz/SearchUsers'],
'GroupAuthz.hasModulePerm': ['GET', '/api/groupauthz/HasModulePerm'], 'GroupAuthz.hasModulePerm': ['GET', '/api/groupauthz/HasModulePerm'],
+144 -9
View File
@@ -46,7 +46,8 @@
<div class="side-body"> <div class="side-body">
<div v-if="treeLoading" class="loading-tip small">加载中...</div> <div v-if="treeLoading" class="loading-tip small">加载中...</div>
<div v-else-if="deptTree.length === 0" class="empty-tip small">暂无部门归属</div> <div v-else-if="deptTree.length === 0" class="empty-tip small">暂无部门归属</div>
<MyTree v-else :treeData="deptTree" :setting="treeSetting" /> <MyTree v-else :treeData="deptTree" :setting="treeSetting" :autoClick="false"
@on-tree-click="onDeptNodeClick" />
</div> </div>
</div> </div>
<div class="side-block"> <div class="side-block">
@@ -54,16 +55,17 @@
<div class="side-body"> <div class="side-body">
<div v-if="treeLoading" class="loading-tip small">加载中...</div> <div v-if="treeLoading" class="loading-tip small">加载中...</div>
<div v-else-if="roleTree.length === 0" class="empty-tip small">暂无所属角色</div> <div v-else-if="roleTree.length === 0" class="empty-tip small">暂无所属角色</div>
<MyTree v-else :treeData="roleTree" :setting="treeSetting" /> <MyTree v-else :treeData="roleTree" :setting="treeSetting" :autoClick="false"
@on-tree-click="onRoleNodeClick" />
</div> </div>
</div> </div>
</div> </div>
<!-- 右列:已授权应用(原有) --> <!-- 右列:已授权应用(原有);支持三种视图:用户聚合 / 部门树节点 / 角色树节点 -->
<div class="perm-main"> <div class="perm-main">
<div class="side-title">已授权应用</div> <div class="side-title">{{ permMainTitle }}</div>
<div class="side-body"> <div class="side-body">
<div v-if="treeLoading" class="loading-tip">权限加载中...</div> <div v-if="permLoading" class="loading-tip">权限加载中...</div>
<div v-else-if="permissionTree.length === 0" class="empty-tip">该用户暂无已授权应用模块</div> <div v-else-if="permissionTree.length === 0" class="empty-tip">{{ permEmptyTip }}</div>
<MyTree v-else ref="permTreeRef" :treeData="permissionTree" :setting="treeSetting" /> <MyTree v-else ref="permTreeRef" :treeData="permissionTree" :setting="treeSetting" />
</div> </div>
</div> </div>
@@ -74,7 +76,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref, computed } from "vue";
import { Input } from "view-ui-plus"; import { Input } from "view-ui-plus";
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue"; import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
import MyTree from "@/components/MyTree.vue"; import MyTree from "@/components/MyTree.vue";
@@ -146,6 +148,25 @@ const deptTree = ref<any[]>([]);
const roleTree = ref<any[]>([]); const roleTree = ref<any[]>([]);
const userLoading = ref(false); const userLoading = ref(false);
const treeLoading = ref(false); const treeLoading = ref(false);
// 右列「已授权应用」独立 loading:部门/角色节点点击只刷新右列,不能把左列两棵树刷成加载中
const permLoading = ref(false);
// 右列当前展示上下文:user=选中用户的聚合权限 / group=部门树节点的组授权 / role=角色树节点的角色授权
const permScope = ref<{ type: "user" | "group" | "role"; id?: number; label: string }>({
type: "user",
label: "",
});
const permMainTitle = computed(() => {
if (permScope.value.type === "group") return `已授权应用(部门:${permScope.value.label})`;
if (permScope.value.type === "role") return `已授权应用(角色:${permScope.value.label})`;
return "已授权应用";
});
const permEmptyTip = computed(() => {
if (permScope.value.type === "group") return "该部门暂无已授权应用模块";
if (permScope.value.type === "role") return "该角色暂无已授权应用模块";
return "该用户暂无已授权应用模块";
});
const treeSetting = ref({ const treeSetting = ref({
view: { dblClickExpand: false, showIcon: false, showLine: true }, view: { dblClickExpand: false, showIcon: false, showLine: true },
@@ -180,6 +201,7 @@ function clearPermPanels() {
permissionTree.value = []; permissionTree.value = [];
deptTree.value = []; deptTree.value = [];
roleTree.value = []; roleTree.value = [];
permScope.value = { type: "user", label: "" };
} }
async function loadGroupUsers(groupId?: number) { async function loadGroupUsers(groupId?: number) {
@@ -234,6 +256,8 @@ function onClearUser() {
async function onSelectUser(user: UserItem) { async function onSelectUser(user: UserItem) {
selectedUser.value = user; selectedUser.value = user;
treeLoading.value = true; treeLoading.value = true;
// 切换用户后右列回到「用户聚合权限」视图(覆盖可能存在的部门/角色节点视图)
permScope.value = { type: "user", id: user.id, label: "" };
const userId = user.id as number; const userId = user.id as number;
// 行自带组ID(多组用户每个组一行)优先;组列表模式回退左侧选中组 // 行自带组ID(多组用户每个组一行)优先;组列表模式回退左侧选中组
const groupId = user.groupId ?? (selectedGroup.value?.id ? Number(selectedGroup.value.id) : undefined); const groupId = user.groupId ?? (selectedGroup.value?.id ? Number(selectedGroup.value.id) : undefined);
@@ -251,10 +275,15 @@ async function onSelectUser(user: UserItem) {
Apis.GroupAuthz.userRoles({ params: { userId }, cacheFor: null }), Apis.GroupAuthz.userRoles({ params: { userId }, cacheFor: null }),
]); ]);
if (permRes.status === "fulfilled") { if (permRes.status === "fulfilled") {
permissionTree.value = formatPermissionTree((permRes.value as CategoryNode[]) || []); // 加载期间用户可能已点了部门/角色节点切走视图,仅在仍处于该用户视图时回填
if (permScope.value.type === "user" && permScope.value.id === userId) {
permissionTree.value = formatPermissionTree((permRes.value as CategoryNode[]) || []);
}
} else { } else {
console.warn(permRes.reason?.message || "加载权限树失败"); console.warn(permRes.reason?.message || "加载权限树失败");
permissionTree.value = []; if (permScope.value.type === "user" && permScope.value.id === userId) {
permissionTree.value = [];
}
} }
if (deptRes.status === "fulfilled") { if (deptRes.status === "fulfilled") {
deptTree.value = formatDeptTree((deptRes.value as GroupNode[]) || []); deptTree.value = formatDeptTree((deptRes.value as GroupNode[]) || []);
@@ -271,6 +300,112 @@ async function onSelectUser(user: UserItem) {
treeLoading.value = false; treeLoading.value = false;
} }
// 所属部门树节点点击:右列切换为「该组的组授权」视图(开通池=本组+祖先并集、就近继承;不含任何角色授权)
function onDeptNodeClick(node: any) {
const id = String(node?.id ?? "");
if (!id.startsWith("grp_")) return;
const groupId = Number(id.slice(4));
if (!groupId || (permScope.value.type === "group" && permScope.value.id === groupId)) return;
permScope.value = { type: "group", id: groupId, label: String(node.name || "") };
permLoading.value = true;
Apis.GroupAuthz.groupPermissionTree({ params: { groupId }, cacheFor: null })
.then((cats: any) => {
if (permScope.value.type === "group" && permScope.value.id === groupId) {
permissionTree.value = formatPermissionTree((cats as CategoryNode[]) || []);
}
})
.catch((e: any) => {
console.warn(e?.message || "加载组权限树失败");
if (permScope.value.type === "group" && permScope.value.id === groupId) {
permissionTree.value = [];
}
})
.finally(() => {
permLoading.value = false;
});
}
// 所属角色树节点点击:右列切换为「该角色的授权模块」视图。
// 复用角色授权面板的 RoleModuleTree 回显接口,仅保留勾选(checked)模块,无勾选模块的应用整棵不显示。
function onRoleNodeClick(node: any) {
const id = String(node?.id ?? "");
if (!id.startsWith("role_")) return; // 「平台角色」/应用名分组节点不响应
const roleId = Number(id.slice(5));
if (!roleId || (permScope.value.type === "role" && permScope.value.id === roleId)) return;
permScope.value = { type: "role", id: roleId, label: String(node.name || "") };
permLoading.value = true;
Promise.all([
Apis.AppRole.roleModuleTree({ params: { roleId }, cacheFor: null }),
Apis.AppSettings.appList({ params: { pageIndex: 1, pageSize: 9999 }, cacheFor: null }),
Apis.AppSettings.appTypeList({ cacheFor: null }),
])
.then(([apps, appListRes, typeRes]: any[]) => {
if (permScope.value.type === "role" && permScope.value.id === roleId) {
permissionTree.value = formatRoleAuthzTree(apps, appListRes, typeRes);
}
})
.catch((e: any) => {
console.warn(e?.message || "加载角色权限树失败");
if (permScope.value.type === "role" && permScope.value.id === roleId) {
permissionTree.value = [];
}
})
.finally(() => {
permLoading.value = false;
});
}
// 角色授权模块 → 按应用分类分组(应用无 categoryId 时落「未分类」,与角色授权面板同口径)
function formatRoleAuthzTree(apps: any[], appListRes: any, typeRes: any): any[] {
const appArr: any[] = appListRes?.content ?? appListRes ?? [];
const catMap = new Map<number, number>();
appArr.forEach((a: any) => {
if (a.id != null) catMap.set(Number(a.id), Number(a.categoryId));
});
const typeMap = new Map<number, string>();
(typeRes ?? []).forEach((t: any) => typeMap.set(Number(t.id), String(t.name || `分类${t.id}`)));
const groups = new Map<number, any>();
for (const app of apps || []) {
const children = filterCheckedModules(app.modules || []);
if (children.length === 0) continue; // 角色未勾选任何模块的应用不展示
const cid = catMap.get(Number(app.id)) ?? -1;
if (!groups.has(cid)) {
groups.set(cid, { id: cid, name: typeMap.get(cid) || "未分类", open: true, isParent: true, apps: [] });
}
groups.get(cid).apps.push({
id: `app_${app.id}_${cid}`,
name: app.name || `应用${app.id}`,
open: false, // 与用户视图一致:应用一级默认闭合,点击才展开模块
children,
});
}
// 分类按 id 升序,「未分类」(-1) 沉底;应用沿用接口返回顺序(后端已按 sortNo 排好)
return [...groups.values()].sort((a, b) => {
if (a.id === -1) return 1;
if (b.id === -1) return -1;
return a.id - b.id;
});
}
// 仅保留勾选(checked)的模块;父未勾选但子孙有勾选时保留父节点以维持层级
function filterCheckedModules(nodes: any[]): any[] {
const out: any[] = [];
for (const n of nodes || []) {
const children = filterCheckedModules(n.children || []);
if (n.checked || children.length > 0) {
out.push({
id: `mod_${n.id}`,
name: n.name || `模块${n.id}`,
open: true,
children,
isParent: (n.children || []).length > 0,
});
}
}
return out;
}
// 所属部门:祖先链树,用户直接归属的组加「本组」标记 // 所属部门:祖先链树,用户直接归属的组加「本组」标记
function formatDeptTree(nodes: GroupNode[]): any[] { function formatDeptTree(nodes: GroupNode[]): any[] {
return (nodes || []).map((n) => ({ return (nodes || []).map((n) => ({