init: UserBaseFront (frontend)

This commit is contained in:
chenxing
2026-08-10 14:41:13 +08:00
commit eea881b616
174 changed files with 32833 additions and 0 deletions
+295
View File
@@ -0,0 +1,295 @@
<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 行:手机号码2 / 性别 -->
<Row>
<Col span="12">
<FormItem label="手机号码2:">
<Input v-model="form.phone2" placeholder="请输入手机号码" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="性别:">
<RadioGroup v-model="form.gender">
<Radio :label="0">女</Radio>
<Radio :label="1">男</Radio>
</RadioGroup>
</FormItem>
</Col>
</Row>
<!-- 第 5 行:所在组(整行) -->
<Row>
<Col span="24">
<FormItem label="所在组:" prop="groupId">
<Select v-model="form.groupId" placeholder="请选择所在组" filterable transfer>
<Option v-for="g in groupOptions" :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="角色属性:" prop="role">
<Select v-model="form.role" placeholder="请选择" clearable transfer style="width: 220px">
<Option v-for="r in roleOptions" :key="r.value" :value="r.value" :label="r.label" />
</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, watch } from "vue";
/* ---------- Props / Emits ---------- */
const props = withDefaults(defineProps<{
modelValue: boolean;
initForm?: Record<string, any>;
groupOptions?: any[];
isEdit?: boolean;
loading?: boolean;
}>(), {
initForm: () => ({}),
groupOptions: () => [],
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: '',
phone2: '',
gender: 0,
groupId: undefined as number | undefined,
userType: [] as number[],
role: undefined as number | undefined,
});
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 });
/* ---------- 用户身份(业务身份多选)---------- */
const userTypeOptions = [
{ value: 1, label: '管理员' },
{ value: 2, label: '老师' },
{ value: 3, label: '校领导' },
{ value: 4, label: '局领导' },
{ value: 5, label: '学生' },
{ value: 6, label: '家长' },
{ value: 7, label: '工作号' },
];
/* ---------- 角色属性(具体角色单选,固定枚举)---------- */
const roleOptions = [
{ value: 1, label: '超级管理员' },
{ value: 2, label: '学校管理员' },
{ value: 3, label: '普通用户' },
];
/* ---------- 校验规则 ---------- */
// 自定义确认密码校验
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
if (!props.isEdit) {
// 新增:必填 + 必须与密码一致
if (!value) {
callback(new Error('请再次输入密码'));
} else if (value !== form.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
} else {
// 编辑:留空表示不修改密码;非空时必须与密码一致
if (value && value !== form.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
}
};
// 自定义密码校验(编辑时可留空)
const validatePassword = (rule: any, value: string, callback: any) => {
if (!props.isEdit && !value) {
callback(new Error('请输入密码'));
} else if (value && value.length < 6) {
callback(new Error('密码长度不能少于 6 位'));
} else {
callback();
}
};
const rules = {
account: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 30, message: '用户名长度 3-30 位', trigger: 'blur' },
],
realName: [
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
],
password: [{ validator: validatePassword, trigger: 'blur' }],
confirmPassword: [{ validator: validateConfirmPassword, trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' },
],
groupId: [
{ required: true, type: 'number' as const, message: '请选择所在组', trigger: 'change' },
],
userType: [
{ required: true, type: 'array' as const, min: 1, message: '请选择至少一个用户身份', trigger: 'change' },
],
role: [
{ required: true, type: 'number' as const, message: '请选择角色属性', trigger: 'change' },
],
};
/* ---------- 提交 ---------- */
const formRef = ref();
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>