- 权限一览:formatRoleAuthzTree 子节点键 apps→children(zTree 只认 children,导致除分类外全不渲染) - 权限一览:所属部门按行所在组请求 UserGroupTree,多组用户只显示该组链路,(本组)标记指向该组 - 角色授权:用户管理新增「部门」列,添加/删除按(用户,组)维度,切组清空勾选避免误绑 - 编辑用户:切「所在组」时身份与角色属性双回显,该组角色未就绪拦截提交防误存;保存按组覆盖 - 组用户管理:左侧树滚动条收敛到树节点(页面级新增 group-user-tree 类,不动全局 .tree-container)
This commit is contained in:
Vendored
+18
-1
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
|
|||||||
@@ -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, // 与用户视图一致:应用一级默认闭合,点击才展开模块
|
||||||
|
|||||||
@@ -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 || '未知错误');
|
||||||
|
|||||||
@@ -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 || '未知错误');
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user