Archived
forked from admin/userbase
311 lines
11 KiB
Vue
311 lines
11 KiB
Vue
<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 行:所在组(只读)(仅编辑展示,默认选中当前组;提交时不带 groupId,不改变所在组) -->
|
||
<Row v-if="isEdit">
|
||
<Col span="24">
|
||
<FormItem label="所在组(只读):">
|
||
<Select v-model="form.userGroupIds" multiple transfer
|
||
placeholder="默认选中当前组(提交不改变所在组)">
|
||
<Option v-for="g in userGroups" :key="g.id" :value="g.id" :label="g.name" />
|
||
</Select>
|
||
</FormItem>
|
||
</Col>
|
||
</Row>
|
||
|
||
<!-- 第 6 行:用户身份(checkbox 多选,整行) -->
|
||
<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 { useDefaultStore } from "@/store";
|
||
|
||
/* ---------- 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[],
|
||
userGroupIds: [] as number[],
|
||
});
|
||
|
||
const form = reactive(defaultForm());
|
||
|
||
// 外部 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;
|
||
});
|
||
|
||
/* ---------- 校验规则 ---------- */
|
||
// 自定义确认密码校验:新增必填且必须与密码一致;编辑时两者皆空表示不修改,任一非空则必须一致
|
||
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();
|
||
}
|
||
};
|
||
|
||
// 密码红五星号仅新增显示;校验规则随 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: true, type: 'array' as const, min: 1, message: '请选择至少一个用户身份', trigger: 'change' },
|
||
],
|
||
}));
|
||
|
||
/* ---------- 提交 ---------- */
|
||
const formRef = ref();
|
||
|
||
// 密码变化时,若已填确认密码则重新校验确认密码,保持一致性提示
|
||
watch(() => form.password, () => {
|
||
if (form.confirmPassword) {
|
||
formRef.value?.validateField('confirmPassword');
|
||
}
|
||
});
|
||
|
||
async function handleSubmit() {
|
||
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> |