Files
UserbaseFront/src/views/group-user/user/EditUserContainer.vue
T
chenxing af94e1d2cb
build / build (push) Successful in 2m53s
fix(permission): 角色节点联动已授权应用、所属部门按行收窄、树滚动条收敛
- 权限一览:formatRoleAuthzTree 子节点键 apps→children(zTree 只认 children,导致除分类外全不渲染)
- 权限一览:所属部门按行所在组请求 UserGroupTree,多组用户只显示该组链路,(本组)标记指向该组
- 角色授权:用户管理新增「部门」列,添加/删除按(用户,组)维度,切组清空勾选避免误绑
- 编辑用户:切「所在组」时身份与角色属性双回显,该组角色未就绪拦截提交防误存;保存按组覆盖
- 组用户管理:左侧树滚动条收敛到树节点(页面级新增 group-user-tree 类,不动全局 .tree-container)
2026-09-16 16:33:58 +08:00

191 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<UserForm v-model="visible" :initForm="detailData" :userGroups="userGroups" :roleOptions="roleOptions"
:isEdit="true" :loading="submitting" @submit="handleSubmit" @cancel="visible = false" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import UserForm from "./UserForm.vue";
/* ---------- 对外暴露 ---------- */
const visible = ref(false);
// 编辑模式:当前编辑的用户 ID
const currentId = ref<number | null>(null);
// 编辑模式:回填数据(来自当前行 + 业务身份 roles + 角色属性 appRoles)
const detailData = ref<Record<string, any>>({});
// 所在组(只读):用户实际所属的多个组(ID + 完整路径名)
const userGroups = ref<{ id: number; name: string }[]>([]);
// 所在组(只读):默认选中的组 ID(来自 getGroups,失败则兜底当前组),仅用于展示,不提交
const userGroupIdsArr = ref<number[]>([]);
// 角色属性下拉:来自角色授权页的全量角色
const roleOptions = ref<{ id: number; name: string }[]>([]);
// 角色属性是否成功加载(控制编辑后是否回写 saveUserRoles,避免下拉失败时误清空)
const roleOptionsLoaded = ref(false);
// 待保存的角色属性(用户多选结果)
let appRolesToSave: number[] = [];
// 成功后回调
let onSuccessCallback: (() => void) | null = null;
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
(dto) => Apis.SysUser.edit({ pathParams: { id: currentId.value }, data: dto }),
{ initialForm: { id: null, account: '', realName: '', password: '', confirmPassword: '', idcard: '', phone: '', gender: 0, userType: [], schoolId: null }, resetAfterSubmiting: true }
);
onSuccess(() => {
Message.success('编辑用户成功!');
visible.value = false;
onSuccessCallback?.();
});
onError((event) => {
// 错误提示已由全局 alova 拦截器统一弹出,这里仅记录日志避免二次弹窗
console.warn('[EditUserContainer] 编辑失败:', event?.error?.message || '编辑失败');
});
/* ---------- 打开编辑弹窗 ---------- */
/**
* @param row - 当前行数据(来自 SysUser.list 接口返回的 SysUserVo,不含 groupId)
* @param groups - 所在组下拉(拍平的组织树,name 含完整路径),用于反查用户所属组的显示名
* @param curGroupId - 当前点击所在的组(左侧选中节点),作为"所在组"只读展示的兜底/默认高亮
* @param onSuccess - 编辑成功后的回调
*/
async function open(row: any, groups: any[], curGroupId?: number | null, onSuccess?: () => void) {
if (!row?.id) {
Message.error('用户 ID 不存在');
return;
}
currentId.value = row.id;
const groupList = groups || [];
onSuccessCallback = onSuccess || null;
roleOptionsLoaded.value = false;
userGroups.value = [];
userGroupIdsArr.value = [];
roleOptions.value = [];
detailData.value = {};
visible.value = true;
// 1) 用户所属组(权威来源 getUserGroups);失败或为空则用当前组兜底,绝不留空
try {
const groupIdsResp = await Apis.SysUser.getUserGroups({ pathParams: { userId: row.id }, cacheFor: null });
const ids: number[] = Array.isArray(groupIdsResp) ? groupIdsResp : [];
const groupMap = new Map<number, any>((groupList as any[]).map(g => [g.id, g]));
let groups_: { id: number; name: string }[] = ids.map(id => ({
id,
name: groupMap.get(id)?.name ?? ('组#' + id),
}));
if (groups_.length === 0 && curGroupId != null) {
groups_ = [{ id: curGroupId, name: groupMap.get(curGroupId)?.name ?? ('组#' + curGroupId) }];
}
userGroups.value = groups_;
userGroupIdsArr.value = groups_.map(g => g.id);
} catch (e: any) {
// getUserGroups 不可用(如后端未部署该接口)时,至少展示当前点击的组,避免空白
if (curGroupId != null) {
const groupMap = new Map<number, any>((groupList as any[]).map(g => [g.id, g]));
userGroups.value = [{ id: curGroupId, name: groupMap.get(curGroupId)?.name ?? ('组#' + curGroupId) }];
userGroupIdsArr.value = [curGroupId];
}
console.warn('[EditUserContainer] getUserGroups 失败,已用当前组兜底:', e?.message || e);
}
// 2) 角色属性下拉(相互独立,失败不影响所在组)
try {
const roleListResp = await Apis.AppRole.roleList({ params: { pageIndex: 1, pageSize: 999 } });
const list = roleListResp?.content || [];
roleOptions.value = list.map((r: any) => ({ id: r.id, name: r.name }));
roleOptionsLoaded.value = true;
} catch (e: any) {
// 全局拦截器已弹窗,这里仅记录日志避免二次弹窗
console.warn('[EditUserContainer] 加载角色列表失败:', e?.message || '未知错误');
}
// 3) 角色属性回显不再在此加载:UserForm 内部监听 userGroupId(初始选中即触发),
// 按组调 GET UserRoles 回显该组角色,并对「该组回显未就绪」拦截提交,防止误清空。
// 所在组默认选中「当前组」:优先用进入编辑时的左侧树/列表组(curGroupId),否则取用户所属组的第一个
const defaultGroupId = (curGroupId != null && userGroupIdsArr.value.includes(curGroupId))
? curGroupId
: (userGroupIdsArr.value[0] ?? null);
detailData.value = buildInitForm(row, defaultGroupId);
}
/* ---------- 把行数据映射成表单回填数据 ---------- */
function buildInitForm(row: any, defaultGroupId: number | null = null) {
return {
id: row.id,
account: row.account || '',
realName: row.realName || '',
idcard: row.idcard || '',
phone: row.phone || '',
password: '',
confirmPassword: '',
gender: row.gender ?? 0,
// 用户身份/角色属性回显唯一来源是 GET UserIdentity / GET UserRoles(UserForm 内部按「所在组」加载):
// roles 列已停写,row.roles 为冻结历史值,误用回显会把废弃值固化进权威表
userType: [],
appRoles: [],
userGroupId: defaultGroupId ?? null,
};
}
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
async function handleSubmit(formData: Record<string, any>) {
if (!currentId.value) return;
appRolesToSave = (formData.appRoles || []) as number[];
form.value = buildPayload(formData, currentId.value);
try {
await sendForm();
} catch {
// 编辑失败(onError 已提示),不继续回写角色
return;
}
// 编辑成功后再回写业务角色(按组覆盖:只覆盖当前所在组下的绑定,不影响其他组);
// 仅当角色下拉成功加载才回写,防止误清空(该组角色回显未就绪时 UserForm 已拦截提交)
if (roleOptionsLoaded.value) {
try {
await Apis.AppRole.saveUserRoles({
data: {
userId: currentId.value as number,
groupId: (formData.userGroupId as number | null) ?? undefined,
roleIds: appRolesToSave,
},
});
} catch (e: any) {
// 全局拦截器已弹窗,这里仅记录日志避免二次弹窗
console.warn('[EditUserContainer] 用户已更新,但角色属性保存失败:', e?.message || '未知错误');
}
}
}
/**
* 把表单字段映射成 SysUserRequest:
* - 只提交后端 SysUserRequest 里声明的字段
* - 编辑时不提交 groupId(后端 edit 会保留用户原分组,且 groupId 仅在 Create 分组校验)
* - 编辑时密码留空表示不修改,所以密码字段不传
* - 身份随 Select 当前选中组提交 {schoolId, roleIds}(未修改也提交原值),由后端按校全量覆盖并对齐
*/
function buildPayload(formData: Record<string, any>, id: number) {
const roleIds: number[] = (formData.userType || []).map((v: any) => Number(v));
const payload: any = {
id,
account: formData.account,
realName: formData.realName,
idcard: formData.idcard || undefined,
phone: formData.phone || undefined,
gender: formData.gender,
schoolId: formData.schoolId,
roleIds,
};
// 编辑时若填写了密码才提交(密码为空表示不修改)
if (formData.password) {
payload.password = formData.password;
payload.confirmPassword = formData.confirmPassword;
}
return payload;
}
defineExpose({ open, visible });
</script>