feat:后台用户关系绑定

This commit is contained in:
dzc committed 2026-07-20 15:52:06 +08:00
1 parent 507bea5469
commit f125d8d1bf
14 files changed
+2394 -295

No files matched your search

@@ -0,0 +1,70 @@
export const DEFAULT_USER_IDENTITY = "DEFAULT"
export const STUDENT_USER_IDENTITY = "STUDENT"
export interface UserStudentInfo {
studentNo: string
schoolName: string
className: string
extJson: string
}
export interface UserInfoFormModel {
userId: string
username: string
password?: string
salt: string
wxOpenid: string
qqOpenid: string
lockFlag: string
phone?: string
deptId: string
roleList: any[]
postList: any[]
nickname: string
name: string
email: string
avatar?: string
gender: number
staffingType: string
identityTypes: string[]
studentInfo: UserStudentInfo
post: Array<string | number>
role: Array<string | number>
}
export const createEmptyStudentInfo = (): UserStudentInfo => ({
studentNo: "",
schoolName: "",
className: "",
extJson: "",
})
export const createEmptyUserInfo = (deptId = ""): UserInfoFormModel => ({
userId: "",
username: "",
password: "",
salt: "",
wxOpenid: "",
qqOpenid: "",
lockFlag: "0",
phone: "",
deptId,
roleList: [],
postList: [],
nickname: "",
name: "",
email: "",
avatar: "",
gender: 0,
staffingType: "2",
identityTypes: [DEFAULT_USER_IDENTITY],
studentInfo: createEmptyStudentInfo(),
post: [],
role: [],
})
export const getUserIdentityPayload = (model: UserInfoFormModel) => ({
identityTypes: [...model.identityTypes],
phone: model.identityTypes.includes(DEFAULT_USER_IDENTITY) ? model.phone : undefined,
studentInfo: model.identityTypes.includes(STUDENT_USER_IDENTITY) ? { ...model.studentInfo } : undefined,
})
@@ -0,0 +1,312 @@
<template>
<el-form ref="formRef" :model="model" :rules="formRules" label-width="90px" v-loading="loading || optionsLoading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.username')" prop="username">
<el-input v-model.trim="model.username" :disabled="isEdit && !auth('user_sysUser_editUsername')"
placeholder="请输入用户名" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.password')" prop="password">
<el-input v-model="model.password" clearable show-password placeholder="请输入密码" type="password" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.name')" prop="name">
<el-input v-model="model.name" clearable placeholder="请输入姓名" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="model.gender">
<el-radio :label="0">未知</el-radio>
<el-radio :label="1">男</el-radio>
<el-radio :label="2">女</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col v-if="!isDeptAdmin" :span="12" class="mb20">
<el-form-item :label="$t('sysuser.role')" prop="role">
<el-select v-model="model.role" clearable multiple placeholder="请选择角色" class="w100">
<el-option v-for="item in roleData" :key="item.roleId" :label="item.roleName"
:value="item.roleId" />
</el-select>
</el-form-item>
</el-col>
<el-col v-if="!isDeptAdmin" :span="12" class="mb20">
<el-form-item :label="$t('sysuser.post')" prop="post">
<el-select v-model="model.post" clearable multiple placeholder="请选择岗位" class="w100">
<el-option v-for="item in postData" :key="item.postId" :label="item.postName"
:value="item.postId" />
</el-select>
</el-form-item>
</el-col>
<el-col v-if="!isEdit" :span="12" class="mb20">
<el-form-item :label="$t('sysuser.dept')" prop="deptId">
<el-tree-select v-model="model.deptId" :data="deptData"
:props="{ value: 'id', label: 'name', children: 'children', disabled: 'isLock' }" filterable
check-strictly class="w100" clearable placeholder="请选择所属部门">
<template #default="{ node, data }">
<el-tooltip v-if="data.isLock" effect="dark" content="无数据权限" placement="right">
<span>
{{ node.label }}
<SvgIcon name="ele-Lock" />
</span>
</el-tooltip>
</template>
</el-tree-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.email')" prop="email">
<el-input v-model="model.email" clearable placeholder="请输入邮箱" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.nickname')" prop="nickname">
<el-input v-model="model.nickname" clearable placeholder="请输入昵称" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="编制" prop="staffingType">
<dict-select v-model="model.staffingType"
:disabled="isEdit && !auth('user_sysUser_editStaffingType')" dict-code="user_staffing_type" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.lockFlag')" prop="lockFlag">
<el-radio-group v-model="model.lockFlag">
<el-radio v-for="(item, index) in lock_flag" :key="index" :label="item.value" border>
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-divider>身份信息</el-divider>
</el-col>
<el-col :span="24" class="mb20">
<div class="identity-block">
<div class="identity-block__title">身份选择</div>
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item label="用户身份" prop="identityTypes">
<dict-select v-model="model.identityTypes" dict-code="user_identity_type" multiple
placeholder="请选择用户身份" />
</el-form-item>
</el-col>
</el-row>
</div>
</el-col>
<el-col v-if="hasDefaultIdentity" :span="24" class="mb20">
<div class="identity-block">
<div class="identity-block__title">默认身份</div>
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item label="手机号" prop="phone" required>
<el-input v-model="model.phone" clearable placeholder="请输入手机号" />
<div class="identity-field-tip">该手机号与默认身份绑定</div>
</el-form-item>
</el-col>
</el-row>
</div>
</el-col>
<template v-if="hasStudentIdentity">
<el-col :span="24" class="mb20">
<div class="identity-block">
<div class="identity-block__title">学生身份</div>
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item label="学号" prop="studentInfo.studentNo">
<el-input v-model.trim="model.studentInfo.studentNo" clearable
placeholder="请输入学号" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学校名称" prop="studentInfo.schoolName">
<el-input v-model.trim="model.studentInfo.schoolName" clearable
placeholder="请输入学校名称" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="班级名称" prop="studentInfo.className">
<el-input v-model.trim="model.studentInfo.className" clearable
placeholder="请输入班级名称" />
</el-form-item>
</el-col>
</el-row>
</div>
</el-col>
</template>
</el-row>
</el-form>
</template>
<script setup lang="ts" name="UserInfoForm">
import type { FormInstance, FormRules } from "element-plus"
import { validatePhone, validateUsername } from "/@/api/admin/user"
import { deptTree } from "/@/api/admin/dept"
import { list as roleList } from "/@/api/admin/role"
import { list as postList } from "/@/api/admin/post"
import { useDict } from "/@/hooks/dict"
import { useMessage } from "/@/hooks/message"
import { useUserInfo } from "/@/stores/userInfo"
import { auth } from "/@/utils/authFunction"
import { rule } from "/@/utils/validate"
import { DEFAULT_USER_IDENTITY, STUDENT_USER_IDENTITY, type UserInfoFormModel } from "./user-info-form"
const props = withDefaults(
defineProps<{
model: UserInfoFormModel
defaultDeptId?: string
loading?: boolean
}>(),
{
defaultDeptId: "",
loading: false,
},
)
const {
userInfos: { roleCodes },
} = useUserInfo()
// @ts-ignore
const { lock_flag } = useDict("lock_flag")
const formRef = ref<FormInstance>()
const optionsLoading = ref(false)
const deptData = ref<any[]>([])
const roleData = ref<any[]>([])
const postData = ref<any[]>([])
const model = computed(() => props.model)
const isEdit = computed(() => Boolean(model.value.userId))
const isDeptAdmin = computed(() => roleCodes.includes("DEPT_ADMIN"))
const hasDefaultIdentity = computed(() => model.value.identityTypes.includes(DEFAULT_USER_IDENTITY))
const hasStudentIdentity = computed(() => model.value.identityTypes.includes(STUDENT_USER_IDENTITY))
const validateIdentityPhone = (_currentRule: any, value: string, callback: (error?: Error) => void) => {
if (!hasDefaultIdentity.value) {
callback()
return
}
if (!value) {
callback(new Error("手机号不能为空"))
return
}
rule.validatePhone(_currentRule, value, callback)
}
const validatePhoneUnique = (currentRule: any, value: string, callback: (error?: Error) => void) => {
if (!hasDefaultIdentity.value || !value) {
callback()
return
}
validatePhone(currentRule, value, callback, isEdit.value)
}
const formRules: FormRules = {
username: [
{ required: true, message: "用户名不能为空", trigger: "blur" },
{ min: 5, max: 60, message: "用户名称长度必须介于5-60个字符之间", trigger: "blur" },
{ pattern: /^[^\u4e00-\u9fa5]+$/, message: "用户名称不能包含中文", trigger: "blur" },
{
validator: (currentRule: any, value: any, callback: any) => {
validateUsername(currentRule, value, callback, isEdit.value)
},
trigger: "blur",
},
],
password: [
{ required: true, message: "密码不能为空", trigger: "blur" },
{ min: 6, max: 20, message: "用户密码长度必须介于6-20个字符之间", trigger: "blur" },
],
name: [
{ validator: rule.overLength, trigger: "blur" },
{ required: true, message: "姓名不能为空", trigger: "blur" },
{ validator: rule.validatorPersonName, trigger: "blur" },
],
deptId: [{ required: true, message: "部门不能为空", trigger: "change" }],
role: isDeptAdmin.value ? [] : [{ required: true, message: "角色不能为空", trigger: "change" }],
phone: [
{ validator: validateIdentityPhone, trigger: "blur" },
{ validator: validatePhoneUnique, trigger: "blur" },
],
email: [{ validator: rule.overLength, trigger: "blur" }],
lockFlag: [{ required: true, message: "状态不能为空", trigger: "change" }],
identityTypes: [{ required: true, type: "array", min: 1, message: "用户身份不能为空", trigger: "change" }],
"studentInfo.studentNo": [
{ required: true, message: "学号不能为空", trigger: "blur" },
{ validator: rule.overLength, trigger: "blur" },
],
}
watch(
() => [...model.value.identityTypes],
() => {
nextTick(() => {
formRef.value?.clearValidate([
"phone",
"studentInfo.studentNo",
"studentInfo.schoolName",
"studentInfo.className",
"studentInfo.extJson",
])
})
},
)
const prepare = async () => {
optionsLoading.value = true
try {
const requests: Promise<any>[] = [roleList(), postList()]
if (!isEdit.value) requests.push(deptTree())
const [roleRes, postRes, deptRes] = await Promise.all(requests)
roleData.value = roleRes.data || []
postData.value = postRes.data || []
if (deptRes) deptData.value = deptRes.data || []
if (!isEdit.value) {
if (!model.value.deptId) model.value.deptId = props.defaultDeptId
if (!isDeptAdmin.value && !model.value.role.length && roleData.value.length) {
model.value.role = [roleData.value[0].roleId]
}
if (!isDeptAdmin.value && !model.value.post.length && postData.value.length) {
model.value.post = [postData.value[0].postId]
}
}
} catch (error: any) {
useMessage().error(error?.msg || "加载用户表单选项失败")
} finally {
optionsLoading.value = false
}
}
const validate = async () => Boolean(await formRef.value?.validate().catch(() => false))
const clearValidate = () => formRef.value?.clearValidate()
defineExpose({ prepare, validate, clearValidate })
</script>
<style scoped lang="scss">
.identity-block {
padding: 14px 16px 0;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
background-color: var(--el-fill-color-lighter);
}
.identity-block__title {
margin-bottom: 14px;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.identity-field-tip {
margin-top: 4px;
font-size: 13px;
line-height: 1.4;
color: var(--el-text-color-secondary);
}
</style>