This repository has been archived on 2026-09-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
UserBaseFront/src/views/group-user/user/UserForm.vue
T
chenxing 886cce1b9e
deploy-userbase-front / build-and-deploy (push) Successful in 3m36s
跨校身份更改
2026-08-27 20:13:16 +08:00

357 lines
13 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>
<Modal v-model="show" :mask-closable="false" width="800" scrollable :title="title"
@on-visible-change="onVisibleChange">
<Form ref="formRef" :model="form" :rules="rules" :label-width="130" class="user-form width-95">
<!-- 第 1 行:用户名 / 真实姓名 -->
<Row>
<Col span="12">
<FormItem label="用户名:" prop="account">
<Input v-model="form.account" :disabled="isEdit" placeholder="请输入用户名" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="真实姓名:" prop="realName">
<Input v-model="form.realName" placeholder="请输入真实姓名" />
</FormItem>
</Col>
</Row>
<!-- 第 2 行:密码 / 确认密码(编辑时变为「重置密码」语义,但视觉一致) -->
<Row>
<Col span="12">
<FormItem label="密码:" prop="password">
<Input v-model="form.password" type="password" :placeholder="isEdit ? '不修改请留空' : '请输入密码'" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="确认密码:" prop="confirmPassword">
<Input v-model="form.confirmPassword" type="password"
:placeholder="isEdit ? '不修改请留空' : '请再次确认密码'" />
</FormItem>
</Col>
</Row>
<!-- 第 3 行:身份证号 / 手机号码 -->
<Row>
<Col span="12">
<FormItem label="身份证号:" prop="idcard">
<Input v-model="form.idcard" placeholder="请输入身份证号" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="手机号码:" prop="phone">
<Input v-model="form.phone" placeholder="请输入手机号码" />
</FormItem>
</Col>
</Row>
<!-- 第 4 行:性别 -->
<Row>
<Col span="24">
<FormItem label="性别:">
<RadioGroup v-model="form.gender">
<Radio :label="0">女</Radio>
<Radio :label="1">男</Radio>
</RadioGroup>
</FormItem>
</Col>
</Row>
<!-- 第 5 行:所在组(仅编辑展示;切换只触发下方「用户身份」回显,不改变实际所在组) -->
<Row v-if="isEdit">
<Col span="24">
<FormItem label="所在组:">
<Select v-model="form.userGroupId" transfer clearable
placeholder="切换后回显该组身份(不改变实际所在组)">
<Option v-for="g in userGroups" :key="g.id" :value="g.id" :label="g.name" />
</Select>
</FormItem>
</Col>
</Row>
<!-- 第 6 行:用户身份(checkbox 多选,整行;编辑时跟随所在组 Select 回显) -->
<Row>
<Col span="24">
<FormItem label="用户身份:" prop="userType">
<CheckboxGroup v-model="form.userType" class="user-type-group">
<Checkbox v-for="opt in userTypeOptions" :key="opt.value" :label="opt.value">
{{ opt.label }}
</Checkbox>
</CheckboxGroup>
</FormItem>
</Col>
</Row>
<!-- 第 7 行:角色属性(业务角色多选,可编辑;来自角色授权页) -->
<Row>
<Col span="24">
<FormItem label="角色属性:">
<Select v-model="form.appRoles" multiple filterable clearable transfer
placeholder="可选择业务角色(来自角色授权)">
<Option v-for="r in roleOptions" :key="r.id" :value="r.id" :label="r.name" />
</Select>
</FormItem>
</Col>
</Row>
</Form>
<template #footer>
<Button type="primary" :loading="loading" @click="handleSubmit">提交</Button>
<Button @click="handleCancel">取消</Button>
</template>
</Modal>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from "vue";
import { Message } from "view-ui-plus";
import { useDefaultStore } from "@/store";
import MyConfirm from "@/components/MyConfirm";
/* ---------- Props / Emits ---------- */
const props = withDefaults(defineProps<{
modelValue: boolean;
initForm?: Record<string, any>;
userGroups?: { id: number; name: string }[];
roleOptions?: { id: number; name: string }[];
isEdit?: boolean;
loading?: boolean;
}>(), {
initForm: () => ({}),
userGroups: () => [],
roleOptions: () => [],
isEdit: false,
loading: false,
});
const emit = defineEmits(['update:modelValue', 'submit', 'cancel']);
const show = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
});
const title = computed(() => props.isEdit ? '编辑用户' : '新增用户');
/* ---------- 表单数据 ---------- */
const defaultForm = (): Record<string, any> => ({
id: null,
account: '',
realName: '',
password: '',
confirmPassword: '',
idcard: '',
phone: '',
gender: 0,
userType: [] as number[],
appRoles: [] as number[],
userGroupId: null as number | null,
schoolId: null as number | null,
});
const form = reactive(defaultForm());
// 编辑模式回显到的最新的身份基线(用于「非空清成空」二次确认;随所在组切换同步更新)
let baselineUserType: number[] = [];
// 外部 initForm 变化时回填
watch(() => props.initForm, (val) => {
if (val && Object.keys(val).length > 0) {
Object.assign(form, defaultForm(), val);
} else {
Object.assign(form, defaultForm());
}
}, { immediate: true, deep: true });
/* ---------- 用户身份(业务身份多选)---------- */
// 唯一真源:后端 SysRoleEnum(经 GetSysDict.sysrole 下发),杜绝前端硬编码漂移。
// 若字典未加载(接口异常)则回退到与 SysRoleEnum 完全一致的兜底值。
const SYS_ROLE_FALLBACK = [
{ value: 1, label: '老师' },
{ value: 2, label: '校领导' },
{ value: 3, label: '学生' },
{ value: 4, label: '家长' },
{ value: 5, label: '局领导' },
{ value: 6, label: '工作号' },
{ value: 7, label: '管理员' },
];
const store = useDefaultStore();
const userTypeOptions = computed(() => {
const sysrole = (store.getSysDict as any)?.sysrole;
if (Array.isArray(sysrole) && sysrole.length) {
return sysrole.map((it: any) => ({ value: it.k, label: it.v }));
}
return SYS_ROLE_FALLBACK;
});
/* ---------- 用户身份回显(唯一来源 GET UserIdentity) ---------- */
// 所在组 Select 切换(含弹窗打开时的初始选中)→ 后端解析 schoolId 后回显 {schoolId, roleIds}。
// 同一学校多组由后端返回同一 schoolId 与角色并集;前端不感知解析逻辑。
watch(() => form.userGroupId, (gid: number | null) => {
if (!props.isEdit || gid == null || !form.id) return;
Apis.SysUser.userIdentity({ params: { userId: form.id, groupId: gid }, cacheFor: null })
.then((resp: any) => {
const roleIds = Array.isArray(resp?.roleIds) ? resp.roleIds.map((v: any) => Number(v)) : [];
form.schoolId = resp?.schoolId ?? null;
form.userType = roleIds;
baselineUserType = roleIds;
})
.catch((e: any) => {
// 全局拦截器已弹窗,这里仅记录日志避免二次弹窗;schoolId 保持 null 时提交会被后端拒绝,不会误清空
console.warn('[UserForm] 加载用户身份失败:', e?.message || '未知错误');
});
});
/* ---------- 校验规则 ---------- */
// 自定义确认密码校验:新增必填且必须与密码一致;编辑时两者皆空表示不修改,任一非空则必须一致
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
const pwd = form.password || '';
const conf = value || '';
if (!props.isEdit) {
if (!conf) return callback(new Error('请再次输入密码'));
if (conf !== pwd) return callback(new Error('两次输入的密码不一致'));
return callback();
}
// 编辑:都为空 -> 不修改密码,放行
if (!pwd && !conf) return callback();
// 修改密码时两次输入必须同时填写且一致
if (!pwd || !conf) return callback(new Error('修改密码时两次输入必须一致'));
if (pwd !== conf) return callback(new Error('两次输入的密码不一致'));
callback();
};
// 自定义密码校验:新增必填(>=6位);编辑可留空,留空表示不修改
const validatePassword = (rule: any, value: string, callback: any) => {
const pwd = value || '';
if (!props.isEdit) {
if (!pwd) return callback(new Error('请输入密码'));
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 {
callback();
}
};
// 用户身份校验拆分:新增强制 ≥1 必填;编辑允许为空(空数组提交 = 清空该校身份,
// 否则切到一个尚无身份数据的学校后连改手机号都会被 min:1 卡死)
const validateUserType = (rule: any, value: number[], callback: any) => {
if (props.isEdit) return callback();
if (!value || value.length === 0) return callback(new Error('请选择至少一个用户身份'));
callback();
};
// 密码红五星号仅新增显示;校验规则随 isEdit 变化
const rules = computed(() => ({
account: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 30, message: '用户名长度 3-30 位', trigger: 'blur' },
{ validator: validateAccount, trigger: 'blur' },
],
realName: [
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
],
password: [{ required: !props.isEdit, validator: validatePassword, trigger: 'blur' }],
confirmPassword: [{ required: !props.isEdit, validator: validateConfirmPassword, trigger: 'blur' }],
phone: [
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' },
],
userType: [
{ required: !props.isEdit, validator: validateUserType, trigger: 'change' },
],
}));
/* ---------- 提交 ---------- */
const formRef = ref();
// 密码变化时,若已填确认密码则重新校验确认密码,保持一致性提示
watch(() => form.password, () => {
if (form.confirmPassword) {
formRef.value?.validateField('confirmPassword');
}
});
async function handleSubmit() {
// 编辑模式身份回显未就绪(schoolId 为空)时拦截:避免在身份数据未加载时提交造成误清空
if (props.isEdit && form.schoolId == null) {
Message.warning('用户身份数据加载中,请稍候再提交');
return false;
}
// 勾选由非空清成空:二次确认(空数组提交 = 清空该校身份)
if (props.isEdit && baselineUserType.length > 0 && form.userType.length === 0) {
const ok = await MyConfirm({
msg: '确定清空该用户在此学校的身份吗?',
info: '清空后该校用户将无任何身份',
});
if (!ok) return false;
}
try {
await formRef.value?.validate();
} catch {
return false;
}
emit('submit', { ...form });
}
function handleCancel() {
emit('cancel');
}
function onVisibleChange(val: boolean) {
if (!val) {
emit('cancel');
}
}
</script>
<style lang="scss" scoped>
.user-form {
margin: 0 auto;
max-height: 60vh;
overflow-y: auto;
overflow-x: hidden;
padding-right: 12px;
:deep(.ivu-form-item) {
display: block;
margin-bottom: 0;
}
:deep(.ivu-form-item-label) {
font-size: 13px;
color: #515a6e;
}
:deep(.ivu-row) {
margin-left: 0 !important;
margin-right: 0 !important;
margin-bottom: 22px;
}
:deep(.ivu-row:last-child) {
margin-bottom: 0;
}
}
/* 用户身份多选:横向换行,避免撑爆 Col */
.user-type-group {
display: flex;
flex-wrap: wrap;
gap: 0 18px;
}
</style>