fix(permission): 角色节点联动已授权应用、所属部门按行收窄、树滚动条收敛
build / build (push) Successful in 2m53s

- 权限一览:formatRoleAuthzTree 子节点键 apps→children(zTree 只认 children,导致除分类外全不渲染)
- 权限一览:所属部门按行所在组请求 UserGroupTree,多组用户只显示该组链路,(本组)标记指向该组
- 角色授权:用户管理新增「部门」列,添加/删除按(用户,组)维度,切组清空勾选避免误绑
- 编辑用户:切「所在组」时身份与角色属性双回显,该组角色未就绪拦截提交防误存;保存按组覆盖
- 组用户管理:左侧树滚动条收敛到树节点(页面级新增 group-user-tree 类,不动全局 .tree-container)
This commit is contained in:
chenxing
2026-09-16 16:33:58 +08:00
parent 4237615238
commit af94e1d2cb
8 changed files with 134 additions and 53 deletions
+18 -1
View File
@@ -179,11 +179,18 @@ export interface GroupAppAssignRequest {
} }
export interface UserAppRoleBindRequest { export interface UserAppRoleBindRequest {
userId: number; userId: number;
/** 绑定组ID(可选):角色归属的组,空则后端兜底主组 */
groupId?: number;
roleIds?: number[]; roleIds?: number[];
} }
export interface AppRoleUserBinding {
userId: number;
/** 绑定发生时所选的组ID */
groupId: number;
}
export interface AppRoleUserBindRequest { export interface AppRoleUserBindRequest {
roleId: number; roleId: number;
userIds?: number[]; bindings?: AppRoleUserBinding[];
} }
export interface AppRoleModuleAssignRequest { export interface AppRoleModuleAssignRequest {
roleId: number; roleId: number;
@@ -677,6 +684,10 @@ export interface AppRoleUserVo {
id?: number; id?: number;
account?: string; account?: string;
realName?: string; realName?: string;
/** 绑定发生时所选的组ID */
groupId?: number;
/** 组路径(查询时按 groupId 现算,scope 裁剪后) */
groupPath?: string;
} }
export interface AppRoleAuthzModuleVo { export interface AppRoleAuthzModuleVo {
id?: number; id?: number;
@@ -2031,6 +2042,8 @@ declare global {
Config extends Alova2MethodConfig<UserRoleVo[]> & { Config extends Alova2MethodConfig<UserRoleVo[]> & {
params: { params: {
userId: number; userId: number;
/** 组ID(可选):传入时只返回该组下的绑定(编辑弹窗按所在组回显) */
groupId?: number;
}; };
} }
>( >(
@@ -2129,6 +2142,8 @@ declare global {
Config extends Alova2MethodConfig<UserGroupNodeVo[]> & { Config extends Alova2MethodConfig<UserGroupNodeVo[]> & {
params: { params: {
userId: number; userId: number;
/** 上下文组ID(可选):传入且用户确属该组时只画该组祖先链 */
groupId?: number;
}; };
} }
>( >(
@@ -3036,6 +3051,8 @@ declare global {
params: { params: {
roleId: number; roleId: number;
userId: number; userId: number;
/** 绑定组ID:按行精确删除 */
groupId: number;
}; };
} }
>( >(
+5 -4
View File
@@ -262,9 +262,10 @@ async function onSelectUser(user: UserItem) {
// 行自带组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);
// 三块数据并行加载,互不阻塞 // 三块数据并行加载,互不阻塞
// userGroupTree 为新增接口:apiDefinitions 未重新生成(npm run api:gen)时方法不存在,降级为友好报错 // userGroupTree 为新增接口:apiDefinitions 未重新生成(npm run api:gen)时方法不存在,降级为友好报错;
// groupId 为行所在组(每行是「用户×组」对),所属部门只画该组链路而非用户全部归属组
const userGroupTreeApi = (Apis.GroupAuthz as any).userGroupTree const userGroupTreeApi = (Apis.GroupAuthz as any).userGroupTree
? Apis.GroupAuthz.userGroupTree({ params: { userId }, cacheFor: null }) ? Apis.GroupAuthz.userGroupTree({ params: { userId, groupId: groupId ?? undefined }, cacheFor: null })
: Promise.reject(new Error("所属部门接口未生成,请重启后端后执行 npm run api:gen")); : Promise.reject(new Error("所属部门接口未生成,请重启后端后执行 npm run api:gen"));
const [permRes, deptRes, roleRes] = await Promise.allSettled([ const [permRes, deptRes, roleRes] = await Promise.allSettled([
Apis.GroupAuthz.userPermissionTree({ Apis.GroupAuthz.userPermissionTree({
@@ -371,9 +372,9 @@ function formatRoleAuthzTree(apps: any[], appListRes: any, typeRes: any): any[]
if (children.length === 0) continue; // 角色未勾选任何模块的应用不展示 if (children.length === 0) continue; // 角色未勾选任何模块的应用不展示
const cid = catMap.get(Number(app.id)) ?? -1; const cid = catMap.get(Number(app.id)) ?? -1;
if (!groups.has(cid)) { if (!groups.has(cid)) {
groups.set(cid, { id: cid, name: typeMap.get(cid) || "未分类", open: true, isParent: true, apps: [] }); groups.set(cid, { id: cid, name: typeMap.get(cid) || "未分类", open: true, isParent: true, children: [] });
} }
groups.get(cid).apps.push({ groups.get(cid).children.push({
id: `app_${app.id}_${cid}`, id: `app_${app.id}_${cid}`,
name: app.name || `应用${app.id}`, name: app.name || `应用${app.id}`,
open: false, // 与用户视图一致:应用一级默认闭合,点击才展开模块 open: false, // 与用户视图一致:应用一级默认闭合,点击才展开模块
+19 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="page-wrapper tree-wrapper group-user-page"> <div class="page-wrapper tree-wrapper group-user-page">
<Group class="tree-container" v-model:curTreeNode="selectedNode" :treeApi="treeApi" @on-change="onTreeChange" <Group class="tree-container group-user-tree" v-model:curTreeNode="selectedNode" :treeApi="treeApi"
@on-tree-data="onTreeData" /> @on-change="onTreeChange" @on-tree-data="onTreeData" />
<div class="main-container"> <div class="main-container">
<div class="search-wrap"> <div class="search-wrap">
<Button type="primary" @click="showAddUser"> <Button type="primary" @click="showAddUser">
@@ -371,6 +371,23 @@ function onSortSaved() {
left: 348px !important; left: 348px !important;
} }
/* 左侧树容器改为 flex 纵向布局并收起自身滚动,把滚动条收敛到树节点区域(.ztree 自带 overflow-y:auto),
避免树过长时把顶部 opera-ul 操作按钮条滚出屏幕;仅作用于本页,不影响全局 .tree-container */
.tree-wrapper.group-user-page .tree-container.group-user-tree {
display: flex;
flex-direction: column;
overflow: hidden;
.opera-ul {
flex-shrink: 0;
}
.group-tree-search-panel {
flex: 1;
min-height: 0;
}
}
.batch-status-body { .batch-status-body {
padding: 8px 12px; padding: 8px 12px;
@@ -68,10 +68,12 @@ async function handleSubmit(formData: Record<string, any>) {
// 新增失败(onError 已提示),不继续绑定角色 // 新增失败(onError 已提示),不继续绑定角色
return; return;
} }
// 新增成功后再绑定业务角色 // 新增成功后再绑定业务角色(组维度:绑定挂在创建分组上,即左侧当前选中组)
if (newId != null && appRolesToSave.length) { if (newId != null && appRolesToSave.length) {
try { try {
await Apis.AppRole.saveUserRoles({ data: { userId: newId, roleIds: appRolesToSave } }); await Apis.AppRole.saveUserRoles({
data: { userId: newId, groupId: currentGroupId.value ?? undefined, roleIds: appRolesToSave },
});
} catch (e: any) { } catch (e: any) {
// 全局拦截器已弹窗,这里仅记录日志避免二次弹窗 // 全局拦截器已弹窗,这里仅记录日志避免二次弹窗
console.warn('[AddUserContainer] 用户已创建,但角色属性保存失败:', e?.message || '未知错误'); console.warn('[AddUserContainer] 用户已创建,但角色属性保存失败:', e?.message || '未知错误');
+18 -25
View File
@@ -20,10 +20,8 @@ const userGroups = ref<{ id: number; name: string }[]>([]);
const userGroupIdsArr = ref<number[]>([]); const userGroupIdsArr = ref<number[]>([]);
// 角色属性下拉:来自角色授权页的全量角色 // 角色属性下拉:来自角色授权页的全量角色
const roleOptions = ref<{ id: number; name: string }[]>([]); const roleOptions = ref<{ id: number; name: string }[]>([]);
// 角色属性是否成功加载(控制编辑后是否回写 saveUserRoles,避免加载失败时误清空) // 角色属性是否成功加载(控制编辑后是否回写 saveUserRoles,避免下拉失败时误清空)
const roleOptionsLoaded = ref(false); const roleOptionsLoaded = ref(false);
// 已绑定角色是否成功加载(与 roleOptionsLoaded 同时为真才回写,防止误清空)
const userRolesLoaded = ref(false);
// 待保存的角色属性(用户多选结果) // 待保存的角色属性(用户多选结果)
let appRolesToSave: number[] = []; let appRolesToSave: number[] = [];
@@ -62,7 +60,6 @@ async function open(row: any, groups: any[], curGroupId?: number | null, onSucce
const groupList = groups || []; const groupList = groups || [];
onSuccessCallback = onSuccess || null; onSuccessCallback = onSuccess || null;
roleOptionsLoaded.value = false; roleOptionsLoaded.value = false;
userRolesLoaded.value = false;
userGroups.value = []; userGroups.value = [];
userGroupIdsArr.value = []; userGroupIdsArr.value = [];
roleOptions.value = []; roleOptions.value = [];
@@ -104,28 +101,17 @@ async function open(row: any, groups: any[], curGroupId?: number | null, onSucce
console.warn('[EditUserContainer] 加载角色列表失败:', e?.message || '未知错误'); console.warn('[EditUserContainer] 加载角色列表失败:', e?.message || '未知错误');
} }
// 3) 已绑定角色 -> appRoles 回填(相互独立) // 3) 角色属性回显不再在此加载:UserForm 内部监听 userGroupId(初始选中即触发),
// 所在组(只读)默认选中「当前组」:优先用进入编辑时的左侧树/列表组(curGroupId),否则取用户所属组的第一个 // 按组调 GET UserRoles 回显该组角色,并对「该组回显未就绪」拦截提交,防止误清空。
// 所在组默认选中「当前组」:优先用进入编辑时的左侧树/列表组(curGroupId),否则取用户所属组的第一个
const defaultGroupId = (curGroupId != null && userGroupIdsArr.value.includes(curGroupId)) const defaultGroupId = (curGroupId != null && userGroupIdsArr.value.includes(curGroupId))
? curGroupId ? curGroupId
: (userGroupIdsArr.value[0] ?? null); : (userGroupIdsArr.value[0] ?? null);
try { detailData.value = buildInitForm(row, defaultGroupId);
const userRolesResp = await Apis.GroupAuthz.userRoles({ params: { userId: row.id } });
const userRoleIds: number[] = (Array.isArray(userRolesResp) ? userRolesResp : [])
.map((r: any) => r.id)
.filter((x: any): x is number => x != null);
userRolesLoaded.value = true;
detailData.value = buildInitForm(row, userRoleIds, defaultGroupId);
} catch (e: any) {
// 已绑定角色加载失败:仍回填其它字段,但标记未加载成功(提交时不回写,避免误清空)
detailData.value = buildInitForm(row, [], defaultGroupId);
// 全局拦截器已弹窗,这里仅记录日志避免二次弹窗
console.warn('[EditUserContainer] 加载用户已绑定角色失败:', e?.message || '未知错误');
}
} }
/* ---------- 把行数据映射成表单回填数据 ---------- */ /* ---------- 把行数据映射成表单回填数据 ---------- */
function buildInitForm(row: any, userRoleIds: number[], defaultGroupId: number | null = null) { function buildInitForm(row: any, defaultGroupId: number | null = null) {
return { return {
id: row.id, id: row.id,
account: row.account || '', account: row.account || '',
@@ -135,10 +121,10 @@ function buildInitForm(row: any, userRoleIds: number[], defaultGroupId: number |
password: '', password: '',
confirmPassword: '', confirmPassword: '',
gender: row.gender ?? 0, gender: row.gender ?? 0,
// 用户身份回显唯一来源是 GET UserIdentity(UserForm 内部按「所在组」加载): // 用户身份/角色属性回显唯一来源是 GET UserIdentity / GET UserRoles(UserForm 内部按「所在组」加载):
// roles 列已停写,row.roles 为冻结历史值,误用回显会把废弃值固化进权威表 // roles 列已停写,row.roles 为冻结历史值,误用回显会把废弃值固化进权威表
userType: [], userType: [],
appRoles: userRoleIds || [], appRoles: [],
userGroupId: defaultGroupId ?? null, userGroupId: defaultGroupId ?? null,
}; };
} }
@@ -154,10 +140,17 @@ async function handleSubmit(formData: Record<string, any>) {
// 编辑失败(onError 已提示),不继续回写角色 // 编辑失败(onError 已提示),不继续回写角色
return; return;
} }
// 编辑成功后再回写业务角色(全量覆盖);仅当角色列表与已绑定角色都成功加载才回写,防止误清空 // 编辑成功后再回写业务角色(按组覆盖:只覆盖当前所在组下的绑定,不影响其他组);
if (roleOptionsLoaded.value && userRolesLoaded.value) { // 仅当角色下拉成功加载才回写,防止误清空(该组角色回显未就绪时 UserForm 已拦截提交)
if (roleOptionsLoaded.value) {
try { try {
await Apis.AppRole.saveUserRoles({ data: { userId: currentId.value as number, roleIds: appRolesToSave } }); await Apis.AppRole.saveUserRoles({
data: {
userId: currentId.value as number,
groupId: (formData.userGroupId as number | null) ?? undefined,
roleIds: appRolesToSave,
},
});
} catch (e: any) { } catch (e: any) {
// 全局拦截器已弹窗,这里仅记录日志避免二次弹窗 // 全局拦截器已弹窗,这里仅记录日志避免二次弹窗
console.warn('[EditUserContainer] 用户已更新,但角色属性保存失败:', e?.message || '未知错误'); console.warn('[EditUserContainer] 用户已更新,但角色属性保存失败:', e?.message || '未知错误');
+31 -5
View File
@@ -57,12 +57,12 @@
</Col> </Col>
</Row> </Row>
<!-- 第 5 行:所在组(仅编辑展示;切换只触发下方「用户身份」回显,不改变实际所在组) --> <!-- 第 5 行:所在组(仅编辑展示;切换只触发下方「用户身份/角色属性」回显,不改变实际所在组) -->
<Row v-if="isEdit"> <Row v-if="isEdit">
<Col span="24"> <Col span="24">
<FormItem label="所在组:"> <FormItem label="所在组:">
<Select v-model="form.userGroupId" transfer clearable <Select v-model="form.userGroupId" transfer clearable
placeholder="切换后回显该组身份(不改变实际所在组)"> placeholder="切换后回显该组的身份与角色属性(不改变实际所在组)">
<Option v-for="g in userGroups" :key="g.id" :value="g.id" :label="g.name" /> <Option v-for="g in userGroups" :key="g.id" :value="g.id" :label="g.name" />
</Select> </Select>
</FormItem> </FormItem>
@@ -155,6 +155,9 @@ const form = reactive(defaultForm());
// 编辑模式回显到的最新的身份基线(用于「非空清成空」二次确认;随所在组切换同步更新) // 编辑模式回显到的最新的身份基线(用于「非空清成空」二次确认;随所在组切换同步更新)
let baselineUserType: number[] = []; let baselineUserType: number[] = [];
// 编辑模式:当前所在组的角色属性是否已回显成功(未就绪时拦截提交,避免把上一组的角色写到新组)
let appRolesEchoed = false;
// 外部 initForm 变化时回填 // 外部 initForm 变化时回填
watch(() => props.initForm, (val) => { watch(() => props.initForm, (val) => {
if (val && Object.keys(val).length > 0) { if (val && Object.keys(val).length > 0) {
@@ -185,11 +188,19 @@ const userTypeOptions = computed(() => {
return SYS_ROLE_FALLBACK; return SYS_ROLE_FALLBACK;
}); });
/* ---------- 用户身份回显(唯一来源 GET UserIdentity) ---------- */ /* ---------- 所在组切换:身份 + 角色属性 双回显(唯一来源 GET UserIdentity / GET UserRoles) ---------- */
// 所在组 Select 切换(含弹窗打开时的初始选中)→ 后端解析 schoolId 后回显 {schoolId, roleIds}。 // 所在组 Select 切换(含弹窗打开时的初始选中)→ 后端解析 schoolId 后回显 {schoolId, roleIds};
// 角色属性为组维度(一行 = 用户×组×角色),同步按所选组回显该组下的角色,互不影响其他组绑定。
// 同一学校多组由后端返回同一 schoolId 与角色并集;前端不感知解析逻辑。 // 同一学校多组由后端返回同一 schoolId 与角色并集;前端不感知解析逻辑。
watch(() => form.userGroupId, (gid: number | null) => { watch(() => form.userGroupId, (gid: number | null) => {
if (!props.isEdit || gid == null || !form.id) return; if (!props.isEdit) return;
if (gid == null || !form.id) {
// 清空所在组:回显状态失效并拦截提交,避免把上一组的角色经「主组兜底」误存到别的组
appRolesEchoed = false;
return;
}
// 切组即置为未就绪:两组数据任一未回来前不允许提交
appRolesEchoed = false;
Apis.SysUser.userIdentity({ params: { userId: form.id, groupId: gid }, cacheFor: null }) Apis.SysUser.userIdentity({ params: { userId: form.id, groupId: gid }, cacheFor: null })
.then((resp: any) => { .then((resp: any) => {
const roleIds = Array.isArray(resp?.roleIds) ? resp.roleIds.map((v: any) => Number(v)) : []; const roleIds = Array.isArray(resp?.roleIds) ? resp.roleIds.map((v: any) => Number(v)) : [];
@@ -201,6 +212,16 @@ watch(() => form.userGroupId, (gid: number | null) => {
// 全局拦截器已弹窗,这里仅记录日志避免二次弹窗;schoolId 保持 null 时提交会被后端拒绝,不会误清空 // 全局拦截器已弹窗,这里仅记录日志避免二次弹窗;schoolId 保持 null 时提交会被后端拒绝,不会误清空
console.warn('[UserForm] 加载用户身份失败:', e?.message || '未知错误'); console.warn('[UserForm] 加载用户身份失败:', e?.message || '未知错误');
}); });
Apis.GroupAuthz.userRoles({ params: { userId: form.id, groupId: gid }, cacheFor: null })
.then((resp: any) => {
const ids = Array.isArray(resp) ? resp.map((r: any) => Number(r?.id)).filter((v: any) => v != null) : [];
form.appRoles = ids;
appRolesEchoed = true;
})
.catch((e: any) => {
// 回显失败保持未就绪:提交会被拦截,不会把上一组的角色误存到本组
console.warn('[UserForm] 加载该组角色属性失败:', e?.message || '未知错误');
});
}); });
/* ---------- 校验规则 ---------- */ /* ---------- 校验规则 ---------- */
@@ -291,6 +312,11 @@ async function handleSubmit() {
Message.warning('用户身份数据加载中,请稍候再提交'); Message.warning('用户身份数据加载中,请稍候再提交');
return false; return false;
} }
// 编辑模式角色属性回显未就绪时拦截:避免把上一组的角色属性误存到当前组
if (props.isEdit && !appRolesEchoed) {
Message.warning('角色属性加载中,请稍候再提交');
return false;
}
// 勾选由非空清成空:二次确认(空数组提交 = 清空该校身份) // 勾选由非空清成空:二次确认(空数组提交 = 清空该校身份)
if (props.isEdit && baselineUserType.length > 0 && form.userType.length === 0) { if (props.isEdit && baselineUserType.length > 0 && form.userType.length === 0) {
const ok = await MyConfirm({ const ok = await MyConfirm({
+25 -6
View File
@@ -37,7 +37,8 @@ import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
const props = defineProps<{ const props = defineProps<{
roleId: number; roleId: number;
existingUserIds: number[]; /** 已有「用户×组」绑定明细,用于按 (用户, 当前选中组) 去重/禁用 */
existingBindings: { id: number; groupId: number }[];
visible: boolean; visible: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -60,7 +61,10 @@ const saving = ref(false);
// 组织树数据源(与组用户管理一致) // 组织树数据源(与组用户管理一致)
const treeApi = () => Apis.SysGroup.getTree(); const treeApi = () => Apis.SysGroup.getTree();
const existingSet = computed(() => new Set(props.existingUserIds ?? [])); // 已有绑定集合:键为 `${userId}:${groupId}`,同一用户可在不同组下分别绑定
const existingKeys = computed(() =>
new Set((props.existingBindings ?? []).map(b => `${b.id}:${b.groupId}`))
);
const filteredCandidates = computed(() => { const filteredCandidates = computed(() => {
const k = kw.value.trim().toLowerCase(); const k = kw.value.trim().toLowerCase();
@@ -73,7 +77,7 @@ const filteredCandidates = computed(() => {
}); });
function isExisting(u: any) { function isExisting(u: any) {
return existingSet.value.has(u.id); return groupId.value != null && existingKeys.value.has(`${u.id}:${groupId.value}`);
} }
function isChecked(u: any) { function isChecked(u: any) {
return isExisting(u) || selectedIds.value.has(u.id); return isExisting(u) || selectedIds.value.has(u.id);
@@ -88,6 +92,8 @@ function toggle(u: any, v: boolean) {
async function onTreeChange(node: any) { async function onTreeChange(node: any) {
groupId.value = node?.id ?? null; groupId.value = node?.id ?? null;
// 绑定记「勾选时所在组」:切换组即清空勾选,避免把 A 组下勾的人误绑到 B 组
selectedIds.value = new Set();
if (!groupId.value) { if (!groupId.value) {
candidates.value = []; candidates.value = [];
return; return;
@@ -113,12 +119,25 @@ function onVisibleChange(v: boolean) {
} }
async function save() { async function save() {
if (!groupId.value) {
Message.warning("请先选择左侧组织节点");
return;
}
if (!selectedIds.value.size) {
Message.warning("请先勾选要添加的用户");
return;
}
saving.value = true; saving.value = true;
try { try {
// 全量覆盖:现有绑定 ∪ 本次新选 // 增量添加「用户×组」绑定(组 = 左侧树当前选中组);后端对已存在的 (userId, groupId) 幂等跳过
const all = new Set<number>([...existingSet.value, ...selectedIds.value]);
await Apis.AppRole.saveRoleUsers( await Apis.AppRole.saveRoleUsers(
{ data: { roleId: props.roleId, userIds: [...all] }, cacheFor: null } {
data: {
roleId: props.roleId,
bindings: [...selectedIds.value].map(id => ({ userId: id, groupId: groupId.value as number })),
},
cacheFor: null,
}
); );
Message.success("添加成功"); Message.success("添加成功");
emit("success"); emit("success");
+14 -8
View File
@@ -1,13 +1,13 @@
<template> <template>
<div class="role-user-manage"> <div class="role-user-manage">
<div class="rum-toolbar"> <div class="rum-toolbar">
<span class="rum-count">共 {{ userList.length }} 位用户</span> <span class="rum-count">共 {{ userList.length }} 条绑定</span>
<Button type="primary" icon="md-person-add" :disabled="!roleId" @click="openBind">添加用户</Button> <Button type="primary" icon="md-person-add" :disabled="!roleId" @click="openBind">添加用户</Button>
</div> </div>
<Table :columns="columns" :data="userList" :loading="loading" size="small" border></Table> <Table :columns="columns" :data="userList" :loading="loading" size="small" border></Table>
<RoleUserBindModal v-model:visible="bindVisible" :role-id="roleId" :existing-user-ids="existingIds" <RoleUserBindModal v-model:visible="bindVisible" :role-id="roleId" :existing-bindings="existingBindings"
@success="onBindSuccess" /> @success="onBindSuccess" />
</div> </div>
</template> </template>
@@ -31,12 +31,18 @@ const userList = ref<any[]>([]);
const loading = ref(false); const loading = ref(false);
const bindVisible = ref(false); const bindVisible = ref(false);
// 当前角色已绑定的用户 id 集合(传给弹窗做去重/禁用) // 当前角色已绑定的「用户×组」明细(传给弹窗按 (用户, 组) 去重/禁用)
const existingIds = computed(() => userList.value.map((u: any) => u.id)); const existingBindings = computed(() =>
userList.value
.filter((u: any) => u.id != null && u.groupId != null)
.map((u: any) => ({ id: u.id as number, groupId: u.groupId as number }))
);
// 部门列为查询时按 groupId 拼接的组路径(非超管按数据范围裁剪,学校管理员只看到本校路径)
const columns = [ const columns = [
{ title: "账号", key: "account", minWidth: 140 }, { title: "账号", key: "account", minWidth: 140 },
{ title: "姓名", key: "realName", minWidth: 110 }, { title: "姓名", key: "realName", minWidth: 110 },
{ title: "部门", key: "groupPath", minWidth: 200 },
{ {
title: "操作", key: "action", width: 90, align: "center", title: "操作", key: "action", width: 90, align: "center",
render: (h: any, { row }: any) => $utils.operatingTextHandle(h, "删除", "text-error", () => removeUser(row)), render: (h: any, { row }: any) => $utils.operatingTextHandle(h, "删除", "text-error", () => removeUser(row)),
@@ -61,13 +67,13 @@ async function loadUsers() {
function removeUser(row: any) { function removeUser(row: any) {
MyConfirm({ MyConfirm({
title: "移除用户", title: "移除绑定",
msg: `确定将用户【${row.realName || row.account}】从此角色移除?`, msg: `确定将用户【${row.realName || row.account}】在部门【${row.groupPath || '未知'}】下的该角色移除?`,
info: "移除后该用户不再拥有此角色所授权的权限。", info: "移除后该用户在该组下不再拥有此角色所授权的权限;其在其他组下的绑定不受影响。",
async onOk() { async onOk() {
try { try {
await Apis.AppRole.removeRoleUser( await Apis.AppRole.removeRoleUser(
{ params: { roleId: props.roleId, userId: row.id }, cacheFor: null } { params: { roleId: props.roleId, userId: row.id, groupId: row.groupId }, cacheFor: null }
); );
Message.success("已移除"); Message.success("已移除");
await loadUsers(); await loadUsers();