Archived
forked from admin/userbase
This commit is contained in:
Vendored
+39
-3
@@ -2659,6 +2659,42 @@ declare global {
|
|||||||
): Alova2Method<null, 'SysGroup.sortChildren', Config>;
|
): Alova2Method<null, 'SysGroup.sortChildren', Config>;
|
||||||
};
|
};
|
||||||
SysUser: {
|
SysUser: {
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [GET] getGroups
|
||||||
|
*
|
||||||
|
* **path:** /api/sysuser/Groups/{userId}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Path Parameters**
|
||||||
|
* ```ts
|
||||||
|
* type PathParameters = {
|
||||||
|
* // userId
|
||||||
|
* userId: number
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = number[]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
getGroups<
|
||||||
|
Config extends Alova2MethodConfig<number[]> & {
|
||||||
|
pathParams: {
|
||||||
|
/**
|
||||||
|
* userId
|
||||||
|
*/
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<number[], 'SysUser.getGroups', Config>;
|
||||||
/**
|
/**
|
||||||
* ---
|
* ---
|
||||||
*
|
*
|
||||||
@@ -2693,16 +2729,16 @@ declare global {
|
|||||||
*
|
*
|
||||||
* **Response**
|
* **Response**
|
||||||
* ```ts
|
* ```ts
|
||||||
* type Response = null
|
* type Response = number
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
add<
|
add<
|
||||||
Config extends Alova2MethodConfig<null> & {
|
Config extends Alova2MethodConfig<number> & {
|
||||||
data: SysUserRequest;
|
data: SysUserRequest;
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
config: Config
|
config: Config
|
||||||
): Alova2Method<null, 'SysUser.add', Config>;
|
): Alova2Method<number, 'SysUser.add', Config>;
|
||||||
/**
|
/**
|
||||||
* ---
|
* ---
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -257,7 +257,9 @@ function showAddUser() {
|
|||||||
/* ================= 编辑 ================= */
|
/* ================= 编辑 ================= */
|
||||||
const editRef = ref();
|
const editRef = ref();
|
||||||
function handleEdit(row: any) {
|
function handleEdit(row: any) {
|
||||||
editRef.value?.open(row, groupOptions.value, () => {
|
// 当前点击所在的组(SysUserVo 不含 groupId,故以左侧选中节点为准,供"所在组"只读展示并默认选中)
|
||||||
|
const curGroupId = selectedNode.value?.id ?? null;
|
||||||
|
editRef.value?.open(row, groupOptions.value, curGroupId, () => {
|
||||||
tableRef.value?.reloadData(OperatorEnum.edit);
|
tableRef.value?.reloadData(OperatorEnum.edit);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<UserForm v-model="visible" :initForm="initFormData" :groupOptions="groupOptions" :isEdit="false"
|
<UserForm v-model="visible" :initForm="initFormData" :roleOptions="roleOptions" :isEdit="false"
|
||||||
:loading="submitting" @submit="handleSubmit" @cancel="visible = false" />
|
:loading="submitting" @submit="handleSubmit" @cancel="visible = false" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -10,19 +10,24 @@ import UserForm from "./UserForm.vue";
|
|||||||
/* ---------- 对外暴露:由父组件(group-user/index)调用 ---------- */
|
/* ---------- 对外暴露:由父组件(group-user/index)调用 ---------- */
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
|
|
||||||
// 所在组下拉数据(拍平后的组织树)
|
// 当前所在组(新增时由左侧选中节点带入,作为创建分组,不展示)
|
||||||
const groupOptions = ref<any[]>([]);
|
const currentGroupId = ref<number | null>(null);
|
||||||
|
|
||||||
// 新增模式:始终传空初始值;若带 defaultGroupId 则预填 groupId
|
// 新增模式:始终传空初始值
|
||||||
const initFormData = ref<Record<string, any>>({});
|
const initFormData = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
// 角色属性下拉:来自角色授权页的全量角色
|
||||||
|
const roleOptions = ref<{ id: number; name: string }[]>([]);
|
||||||
|
// 待保存的角色属性(用户多选结果)
|
||||||
|
let appRolesToSave: number[] = [];
|
||||||
|
|
||||||
// 成功后回调
|
// 成功后回调
|
||||||
let onSuccessCallback: (() => void) | null = null;
|
let onSuccessCallback: (() => void) | null = null;
|
||||||
|
|
||||||
// useForm 托管提交:loading=submitting、sendForm 发请求、onSuccess/onError 处理结果
|
// useForm 托管提交:loading=submitting、sendForm 发请求、onSuccess/onError 处理结果
|
||||||
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
||||||
(dto) => Apis.SysUser.add({ data: dto }),
|
(dto) => Apis.SysUser.add({ data: dto }),
|
||||||
{ initialForm: { account: '', realName: '', password: '', confirmPassword: '', idcard: '', phone: '', phone2: '', gender: 0, groupId: undefined, userType: [], role: undefined }, resetAfterSubmiting: true }
|
{ initialForm: { account: '', realName: '', password: '', confirmPassword: '', idcard: '', phone: '', gender: 0, userType: [] }, resetAfterSubmiting: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
onSuccess(() => {
|
onSuccess(() => {
|
||||||
@@ -31,45 +36,75 @@ onSuccess(() => {
|
|||||||
onSuccessCallback?.();
|
onSuccessCallback?.();
|
||||||
});
|
});
|
||||||
onError((event) => {
|
onError((event) => {
|
||||||
Message.error(event?.error?.message || '新增失败');
|
const msg = event?.error?.message || '新增失败';
|
||||||
|
Message.error(normalizeAccountError(msg, form.value.account));
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------- 打开弹窗 ---------- */
|
/* ---------- 打开弹窗 ---------- */
|
||||||
function open(groups: any[], defaultGroupId?: number | null, onSuccess?: () => void) {
|
function open(groups: any[], defaultGroupId?: number | null, onSuccess?: () => void) {
|
||||||
groupOptions.value = groups || [];
|
// 当前所在组作为创建分组(提交时回写 groupId),新增模式不展示
|
||||||
// 若已选中某个组织节点,新增弹窗自动带入该组;未选时不带入
|
currentGroupId.value = defaultGroupId != null ? defaultGroupId : null;
|
||||||
initFormData.value = defaultGroupId != null ? { groupId: defaultGroupId } : {};
|
initFormData.value = {};
|
||||||
onSuccessCallback = onSuccess || null;
|
onSuccessCallback = onSuccess || null;
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
|
// 加载角色属性下拉(失败时静默降级,不影响新增主流程)
|
||||||
|
Apis.AppRole.roleList({ params: { pageIndex: 1, pageSize: 999 } })
|
||||||
|
.then((resp: any) => {
|
||||||
|
roleOptions.value = (resp?.content || []).map((r: any) => ({ id: r.id, name: r.name }));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
roleOptions.value = [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
|
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
|
||||||
async function handleSubmit(formData: Record<string, any>) {
|
async function handleSubmit(formData: Record<string, any>) {
|
||||||
|
appRolesToSave = (formData.appRoles || []) as number[];
|
||||||
form.value = buildPayload(formData);
|
form.value = buildPayload(formData);
|
||||||
await sendForm();
|
let newId: number | undefined;
|
||||||
|
try {
|
||||||
|
newId = (await sendForm()) as number;
|
||||||
|
} catch {
|
||||||
|
// 新增失败(onError 已提示),不继续绑定角色
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 新增成功后再绑定业务角色
|
||||||
|
if (newId != null && appRolesToSave.length) {
|
||||||
|
try {
|
||||||
|
await Apis.AppRole.saveUserRoles({ data: { userId: newId, roleIds: appRolesToSave } });
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error('用户已创建,但角色属性保存失败:' + (e?.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把表单字段映射成 SysUserRequest:
|
* 把表单字段映射成 SysUserRequest:
|
||||||
* - 只提交后端 SysUserRequest 里声明的字段
|
* - 只提交后端 SysUserRequest 里声明的字段
|
||||||
* - "用户身份"(userType checkbox) + "角色属性"(role select) 合并为 roles[](去重)
|
* - 角色仅来自"用户身份"勾选(userType),groupId 由当前所在组回写
|
||||||
*/
|
*/
|
||||||
function buildPayload(formData: Record<string, any>) {
|
function buildPayload(formData: Record<string, any>) {
|
||||||
const roleSet = new Set<number>();
|
const roles: number[] = (formData.userType || []).map((v: any) => Number(v));
|
||||||
(formData.userType || []).forEach((v: any) => roleSet.add(Number(v)));
|
|
||||||
if (formData.role != null && formData.role !== '') roleSet.add(Number(formData.role));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account: formData.account,
|
account: formData.account,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
confirmPassword: formData.confirmPassword,
|
confirmPassword: formData.confirmPassword,
|
||||||
realName: formData.realName,
|
realName: formData.realName,
|
||||||
idcard: formData.idcard || undefined,
|
idcard: formData.idcard || undefined,
|
||||||
phone: formData.phone,
|
phone: formData.phone || undefined,
|
||||||
groupId: formData.groupId,
|
gender: formData.gender,
|
||||||
roles: Array.from(roleSet),
|
groupId: currentGroupId.value,
|
||||||
|
roles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 工具:账号重复提示标准化 ---------- */
|
||||||
|
function normalizeAccountError(msg: string, account: string): string {
|
||||||
|
if (account && /账号/.test(msg) && /(已存在|重复|已被其他用户使用)/.test(msg)) {
|
||||||
|
return `账号${account}已存在`;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ open, visible });
|
defineExpose({ open, visible });
|
||||||
</script>
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<UserForm v-model="visible" :initForm="detailData" :groupOptions="groupOptions" :isEdit="true"
|
<UserForm v-model="visible" :initForm="detailData" :userGroups="userGroups" :roleOptions="roleOptions"
|
||||||
:loading="submitting" @submit="handleSubmit" @cancel="visible = false" />
|
:isEdit="true" :loading="submitting" @submit="handleSubmit" @cancel="visible = false" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -9,12 +9,21 @@ import UserForm from "./UserForm.vue";
|
|||||||
|
|
||||||
/* ---------- 对外暴露 ---------- */
|
/* ---------- 对外暴露 ---------- */
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const groupOptions = ref<any[]>([]);
|
|
||||||
|
|
||||||
// 编辑模式:当前编辑的用户 ID
|
// 编辑模式:当前编辑的用户 ID
|
||||||
const currentId = ref<number | null>(null);
|
const currentId = ref<number | null>(null);
|
||||||
// 编辑模式:回填数据(来自当前行,辅以 roles[] 拆分到 userType/role)
|
// 编辑模式:回填数据(来自当前行 + 业务身份 roles + 角色属性 appRoles)
|
||||||
const detailData = ref<Record<string, any>>({});
|
const detailData = ref<Record<string, any>>({});
|
||||||
|
// 所在组(只读):用户实际所属的多个组(ID + 完整路径名)
|
||||||
|
const userGroups = ref<{ id: number; name: string }[]>([]);
|
||||||
|
// 角色属性下拉:来自角色授权页的全量角色
|
||||||
|
const roleOptions = ref<{ id: number; name: string }[]>([]);
|
||||||
|
// 角色属性是否成功加载(控制编辑后是否回写 saveUserRoles,避免加载失败时误清空)
|
||||||
|
const roleOptionsLoaded = ref(false);
|
||||||
|
// 已绑定角色是否成功加载(与 roleOptionsLoaded 同时为真才回写,防止误清空)
|
||||||
|
const userRolesLoaded = ref(false);
|
||||||
|
// 待保存的角色属性(用户多选结果)
|
||||||
|
let appRolesToSave: number[] = [];
|
||||||
|
|
||||||
// 成功后回调
|
// 成功后回调
|
||||||
let onSuccessCallback: (() => void) | null = null;
|
let onSuccessCallback: (() => void) | null = null;
|
||||||
@@ -22,7 +31,7 @@ let onSuccessCallback: (() => void) | null = null;
|
|||||||
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
|
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
|
||||||
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
||||||
(dto) => Apis.SysUser.edit({ pathParams: { id: currentId.value }, data: dto }),
|
(dto) => Apis.SysUser.edit({ pathParams: { id: currentId.value }, data: dto }),
|
||||||
{ initialForm: { id: null, account: '', realName: '', password: '', confirmPassword: '', idcard: '', phone: '', phone2: '', gender: 0, groupId: undefined, userType: [], role: undefined }, resetAfterSubmiting: true }
|
{ initialForm: { id: null, account: '', realName: '', password: '', confirmPassword: '', idcard: '', phone: '', gender: 0, userType: [] }, resetAfterSubmiting: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
onSuccess(() => {
|
onSuccess(() => {
|
||||||
@@ -31,33 +40,84 @@ onSuccess(() => {
|
|||||||
onSuccessCallback?.();
|
onSuccessCallback?.();
|
||||||
});
|
});
|
||||||
onError((event) => {
|
onError((event) => {
|
||||||
Message.error(event?.error?.message || '编辑失败');
|
const msg = event?.error?.message || '编辑失败';
|
||||||
|
Message.error(normalizeAccountError(msg, form.value.account));
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------- 打开编辑弹窗 ---------- */
|
/* ---------- 打开编辑弹窗 ---------- */
|
||||||
/**
|
/**
|
||||||
* @param row - 当前行数据(来自 SysUser.list 接口返回的 SysUserVo)
|
* @param row - 当前行数据(来自 SysUser.list 接口返回的 SysUserVo,不含 groupId)
|
||||||
* @param groups - 所在组下拉(拍平的组织树)
|
* @param groups - 所在组下拉(拍平的组织树,name 含完整路径),用于反查用户所属组的显示名
|
||||||
|
* @param curGroupId - 当前点击所在的组(左侧选中节点),作为"所在组"只读展示的兜底/默认高亮
|
||||||
* @param onSuccess - 编辑成功后的回调
|
* @param onSuccess - 编辑成功后的回调
|
||||||
*/
|
*/
|
||||||
function open(row: any, groups: any[], onSuccess?: () => void) {
|
async function open(row: any, groups: any[], curGroupId?: number | null, onSuccess?: () => void) {
|
||||||
if (!row?.id) {
|
if (!row?.id) {
|
||||||
Message.error('用户 ID 不存在');
|
Message.error('用户 ID 不存在');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentId.value = row.id;
|
currentId.value = row.id;
|
||||||
groupOptions.value = groups || [];
|
const groupList = groups || [];
|
||||||
detailData.value = buildInitForm(row);
|
|
||||||
onSuccessCallback = onSuccess || null;
|
onSuccessCallback = onSuccess || null;
|
||||||
|
roleOptionsLoaded.value = false;
|
||||||
|
userRolesLoaded.value = false;
|
||||||
|
userGroups.value = [];
|
||||||
|
roleOptions.value = [];
|
||||||
|
detailData.value = {};
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
|
|
||||||
|
// 1) 用户所属组(权威来源 getGroups);失败或为空则用当前组兜底,绝不留空
|
||||||
|
try {
|
||||||
|
const groupIdsResp = await Apis.SysUser.getGroups({ pathParams: { userId: row.id } });
|
||||||
|
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_;
|
||||||
|
} catch (e: any) {
|
||||||
|
// getGroups 不可用(如后端未部署该接口)时,至少展示当前点击的组,避免空白
|
||||||
|
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) }];
|
||||||
|
}
|
||||||
|
console.warn('[EditUserContainer] getGroups 失败,已用当前组兜底:', 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) {
|
||||||
|
Message.error('加载角色列表失败:' + (e?.message || '未知错误'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 已绑定角色 -> appRoles 回填(相互独立)
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
} catch (e: any) {
|
||||||
|
// 已绑定角色加载失败:仍回填其它字段,但标记未加载成功(提交时不回写,避免误清空)
|
||||||
|
detailData.value = buildInitForm(row, []);
|
||||||
|
Message.error('加载用户已绑定角色失败:' + (e?.message || '未知错误'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 把行数据映射成表单回填数据 ---------- */
|
/* ---------- 把行数据映射成表单回填数据 ---------- */
|
||||||
function buildInitForm(row: any) {
|
function buildInitForm(row: any, userRoleIds: number[]) {
|
||||||
const roles: number[] = Array.isArray(row.roles) ? row.roles : [];
|
const roles: number[] = Array.isArray(row.roles) ? row.roles : [];
|
||||||
// 用户身份(业务身份)= 1..7;角色属性(具体角色)= 1..3
|
// 用户身份(业务身份)= 1..7;角色属性已移除(超管手动入库、管理员走勾选)
|
||||||
const userType = roles.filter(r => r >= 1 && r <= 7);
|
const userType = roles.filter(r => r >= 1 && r <= 7);
|
||||||
const role = roles.filter(r => r >= 1 && r <= 3).slice(-1)[0];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -65,42 +125,52 @@ function buildInitForm(row: any) {
|
|||||||
realName: row.realName || '',
|
realName: row.realName || '',
|
||||||
idcard: row.idcard || '',
|
idcard: row.idcard || '',
|
||||||
phone: row.phone || '',
|
phone: row.phone || '',
|
||||||
groupId: row.groupId,
|
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
phone2: '',
|
gender: row.gender ?? 0,
|
||||||
gender: 0,
|
|
||||||
userType,
|
userType,
|
||||||
role,
|
appRoles: userRoleIds || [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
|
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
|
||||||
async function handleSubmit(formData: Record<string, any>) {
|
async function handleSubmit(formData: Record<string, any>) {
|
||||||
if (!currentId.value) return;
|
if (!currentId.value) return;
|
||||||
|
appRolesToSave = (formData.appRoles || []) as number[];
|
||||||
form.value = buildPayload(formData, currentId.value);
|
form.value = buildPayload(formData, currentId.value);
|
||||||
|
try {
|
||||||
await sendForm();
|
await sendForm();
|
||||||
|
} catch {
|
||||||
|
// 编辑失败(onError 已提示),不继续回写角色
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 编辑成功后再回写业务角色(全量覆盖);仅当角色列表与已绑定角色都成功加载才回写,防止误清空
|
||||||
|
if (roleOptionsLoaded.value && userRolesLoaded.value) {
|
||||||
|
try {
|
||||||
|
await Apis.AppRole.saveUserRoles({ data: { userId: currentId.value as number, roleIds: appRolesToSave } });
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error('用户已更新,但角色属性保存失败:' + (e?.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把表单字段映射成 SysUserRequest:
|
* 把表单字段映射成 SysUserRequest:
|
||||||
* - 只提交后端 SysUserRequest 里声明的字段
|
* - 只提交后端 SysUserRequest 里声明的字段
|
||||||
* - "用户身份"(userType checkbox) + "角色属性"(role select) 合并为 roles[](去重)
|
* - 编辑时不提交 groupId(后端 edit 会保留用户原分组,且 groupId 仅在 Create 分组校验)
|
||||||
* - 编辑时密码留空表示不修改,所以密码字段不传
|
* - 编辑时密码留空表示不修改,所以密码字段不传
|
||||||
*/
|
*/
|
||||||
function buildPayload(formData: Record<string, any>, id: number) {
|
function buildPayload(formData: Record<string, any>, id: number) {
|
||||||
const roleSet = new Set<number>();
|
const roles: number[] = (formData.userType || []).map((v: any) => Number(v));
|
||||||
(formData.userType || []).forEach((v: any) => roleSet.add(Number(v)));
|
|
||||||
if (formData.role != null && formData.role !== '') roleSet.add(Number(formData.role));
|
|
||||||
|
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
id,
|
id,
|
||||||
account: formData.account,
|
account: formData.account,
|
||||||
realName: formData.realName,
|
realName: formData.realName,
|
||||||
idcard: formData.idcard || undefined,
|
idcard: formData.idcard || undefined,
|
||||||
phone: formData.phone,
|
phone: formData.phone || undefined,
|
||||||
groupId: formData.groupId,
|
gender: formData.gender,
|
||||||
roles: Array.from(roleSet),
|
roles,
|
||||||
};
|
};
|
||||||
// 编辑时若填写了密码才提交(密码为空表示不修改)
|
// 编辑时若填写了密码才提交(密码为空表示不修改)
|
||||||
if (formData.password) {
|
if (formData.password) {
|
||||||
@@ -110,5 +180,13 @@ function buildPayload(formData: Record<string, any>, id: number) {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 工具:账号重复提示标准化 ---------- */
|
||||||
|
function normalizeAccountError(msg: string, account: string): string {
|
||||||
|
if (account && /账号/.test(msg) && /(已存在|重复|已被其他用户使用)/.test(msg)) {
|
||||||
|
return `账号${account}已存在`;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ open, visible });
|
defineExpose({ open, visible });
|
||||||
</script>
|
</script>
|
||||||
@@ -45,14 +45,9 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第 4 行:手机号码2 / 性别 -->
|
<!-- 第 4 行:性别 -->
|
||||||
<Row>
|
<Row>
|
||||||
<Col span="12">
|
<Col span="24">
|
||||||
<FormItem label="手机号码2:">
|
|
||||||
<Input v-model="form.phone2" placeholder="请输入手机号码" />
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span="12">
|
|
||||||
<FormItem label="性别:">
|
<FormItem label="性别:">
|
||||||
<RadioGroup v-model="form.gender">
|
<RadioGroup v-model="form.gender">
|
||||||
<Radio :label="0">女</Radio>
|
<Radio :label="0">女</Radio>
|
||||||
@@ -62,12 +57,12 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第 5 行:所在组(整行) -->
|
<!-- 第 5 行:所在组(只读)(仅编辑展示,显示用户全部所属组;不提交) -->
|
||||||
<Row>
|
<Row v-if="isEdit">
|
||||||
<Col span="24">
|
<Col span="24">
|
||||||
<FormItem label="所在组:" prop="groupId">
|
<FormItem label="所在组(只读):">
|
||||||
<Select v-model="form.groupId" placeholder="请选择所在组" filterable transfer>
|
<Select :value="userGroupIds" multiple disabled transfer class="group-readonly">
|
||||||
<Option v-for="g in groupOptions" :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>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -86,16 +81,18 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第 7 行:角色属性(整行) -->
|
<!-- 第 7 行:角色属性(业务角色多选,可编辑;来自角色授权页) -->
|
||||||
<Row>
|
<Row>
|
||||||
<Col span="24">
|
<Col span="24">
|
||||||
<FormItem label="角色属性:" prop="role">
|
<FormItem label="角色属性:">
|
||||||
<Select v-model="form.role" placeholder="请选择" clearable transfer style="width: 220px">
|
<Select v-model="form.appRoles" multiple filterable clearable transfer
|
||||||
<Option v-for="r in roleOptions" :key="r.value" :value="r.value" :label="r.label" />
|
placeholder="可选择业务角色(来自角色授权)">
|
||||||
|
<Option v-for="r in roleOptions" :key="r.id" :value="r.id" :label="r.name" />
|
||||||
</Select>
|
</Select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -106,18 +103,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, watch } from "vue";
|
import { computed, reactive, ref, watch } from "vue";
|
||||||
|
|
||||||
/* ---------- Props / Emits ---------- */
|
/* ---------- Props / Emits ---------- */
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
initForm?: Record<string, any>;
|
initForm?: Record<string, any>;
|
||||||
groupOptions?: any[];
|
userGroups?: { id: number; name: string }[];
|
||||||
|
roleOptions?: { id: number; name: string }[];
|
||||||
isEdit?: boolean;
|
isEdit?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
initForm: () => ({}),
|
initForm: () => ({}),
|
||||||
groupOptions: () => [],
|
userGroups: () => [],
|
||||||
|
roleOptions: () => [],
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
@@ -140,11 +139,9 @@ const defaultForm = (): Record<string, any> => ({
|
|||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
idcard: '',
|
idcard: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
phone2: '',
|
|
||||||
gender: 0,
|
gender: 0,
|
||||||
groupId: undefined as number | undefined,
|
|
||||||
userType: [] as number[],
|
userType: [] as number[],
|
||||||
role: undefined as number | undefined,
|
appRoles: [] as number[],
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = reactive(defaultForm());
|
const form = reactive(defaultForm());
|
||||||
@@ -158,6 +155,9 @@ watch(() => props.initForm, (val) => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true, deep: true });
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
|
// 所在组(只读) 多选值:用户全部所属组的 ID 列表
|
||||||
|
const userGroupIds = computed(() => (props.userGroups || []).map(g => g.id));
|
||||||
|
|
||||||
/* ---------- 用户身份(业务身份多选)---------- */
|
/* ---------- 用户身份(业务身份多选)---------- */
|
||||||
const userTypeOptions = [
|
const userTypeOptions = [
|
||||||
{ value: 1, label: '管理员' },
|
{ value: 1, label: '管理员' },
|
||||||
@@ -169,74 +169,80 @@ const userTypeOptions = [
|
|||||||
{ value: 7, label: '工作号' },
|
{ value: 7, label: '工作号' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ---------- 角色属性(具体角色单选,固定枚举)---------- */
|
|
||||||
const roleOptions = [
|
|
||||||
{ value: 1, label: '超级管理员' },
|
|
||||||
{ value: 2, label: '学校管理员' },
|
|
||||||
{ value: 3, label: '普通用户' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/* ---------- 校验规则 ---------- */
|
/* ---------- 校验规则 ---------- */
|
||||||
// 自定义确认密码校验
|
// 自定义确认密码校验:新增必填且必须与密码一致;编辑时两者皆空表示不修改,任一非空则必须一致
|
||||||
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
|
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
|
||||||
|
const pwd = form.password || '';
|
||||||
|
const conf = value || '';
|
||||||
if (!props.isEdit) {
|
if (!props.isEdit) {
|
||||||
// 新增:必填 + 必须与密码一致
|
if (!conf) return callback(new Error('请再次输入密码'));
|
||||||
if (!value) {
|
if (conf !== pwd) return callback(new Error('两次输入的密码不一致'));
|
||||||
callback(new Error('请再次输入密码'));
|
return callback();
|
||||||
} else if (value !== form.password) {
|
}
|
||||||
callback(new Error('两次输入的密码不一致'));
|
// 编辑:都为空 -> 不修改密码,放行
|
||||||
} else {
|
if (!pwd && !conf) return callback();
|
||||||
|
// 修改密码时两次输入必须同时填写且一致
|
||||||
|
if (!pwd || !conf) return callback(new Error('修改密码时两次输入必须一致'));
|
||||||
|
if (pwd !== conf) return callback(new Error('两次输入的密码不一致'));
|
||||||
callback();
|
callback();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 编辑:留空表示不修改密码;非空时必须与密码一致
|
|
||||||
if (value && value !== form.password) {
|
|
||||||
callback(new Error('两次输入的密码不一致'));
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自定义密码校验(编辑时可留空)
|
// 自定义密码校验:新增必填(>=6位);编辑可留空,留空表示不修改
|
||||||
const validatePassword = (rule: any, value: string, callback: any) => {
|
const validatePassword = (rule: any, value: string, callback: any) => {
|
||||||
if (!props.isEdit && !value) {
|
const pwd = value || '';
|
||||||
callback(new Error('请输入密码'));
|
if (!props.isEdit) {
|
||||||
} else if (value && value.length < 6) {
|
if (!pwd) return callback(new Error('请输入密码'));
|
||||||
callback(new Error('密码长度不能少于 6 位'));
|
if (pwd.length < 6) return callback(new Error('密码长度不能少于 6 位'));
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
if (!pwd) return callback();
|
||||||
|
if (pwd.length < 6) return callback(new Error('密码长度不能少于 6 位'));
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用户名格式校验:新增时强制仅数字/字母;编辑时账号只读不可改,跳过格式校验
|
||||||
|
const validateAccount = (rule: any, value: string, callback: any) => {
|
||||||
|
if (props.isEdit) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value && !/^[A-Za-z0-9]+$/.test(value)) {
|
||||||
|
callback(new Error('用户名只能由数字或字母组成'));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rules = {
|
// 密码红五星号仅新增显示;校验规则随 isEdit 变化
|
||||||
|
const rules = computed(() => ({
|
||||||
account: [
|
account: [
|
||||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
{ min: 3, max: 30, message: '用户名长度 3-30 位', trigger: 'blur' },
|
{ min: 3, max: 30, message: '用户名长度 3-30 位', trigger: 'blur' },
|
||||||
|
{ validator: validateAccount, trigger: 'blur' },
|
||||||
],
|
],
|
||||||
realName: [
|
realName: [
|
||||||
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
|
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
password: [{ validator: validatePassword, trigger: 'blur' }],
|
password: [{ required: !props.isEdit, validator: validatePassword, trigger: 'blur' }],
|
||||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: 'blur' }],
|
confirmPassword: [{ required: !props.isEdit, validator: validateConfirmPassword, trigger: 'blur' }],
|
||||||
phone: [
|
phone: [
|
||||||
{ required: true, message: '请输入手机号码', trigger: 'blur' },
|
|
||||||
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
groupId: [
|
|
||||||
{ required: true, type: 'number' as const, message: '请选择所在组', trigger: 'change' },
|
|
||||||
],
|
|
||||||
userType: [
|
userType: [
|
||||||
{ required: true, type: 'array' as const, min: 1, message: '请选择至少一个用户身份', trigger: 'change' },
|
{ required: true, type: 'array' as const, min: 1, message: '请选择至少一个用户身份', trigger: 'change' },
|
||||||
],
|
],
|
||||||
role: [
|
}));
|
||||||
{ required: true, type: 'number' as const, message: '请选择角色属性', trigger: 'change' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ---------- 提交 ---------- */
|
/* ---------- 提交 ---------- */
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
|
|
||||||
|
// 密码变化时,若已填确认密码则重新校验确认密码,保持一致性提示
|
||||||
|
watch(() => form.password, () => {
|
||||||
|
if (form.confirmPassword) {
|
||||||
|
formRef.value?.validateField('confirmPassword');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
try {
|
try {
|
||||||
await formRef.value?.validate();
|
await formRef.value?.validate();
|
||||||
@@ -292,4 +298,15 @@ function onVisibleChange(val: boolean) {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0 18px;
|
gap: 0 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 所在组(只读):保留 disabled(防止误删标签),但去掉灰底灰字,显示为正常白底深色 */
|
||||||
|
.user-form :deep(.group-readonly.ivu-select-disabled .ivu-select-selection) {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #515a6e;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form :deep(.group-readonly.ivu-select-disabled .ivu-select-arrow) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user