522 lines
14 KiB
Vue
522 lines
14 KiB
Vue
<template>
|
||
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
|
||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||
<div class="form-title">账号信息</div>
|
||
<!-- 账号 -->
|
||
<el-form-item label="账号">
|
||
<el-input v-model="form.account" :disabled="!isAdd" placeholder="请输入账号" />
|
||
</el-form-item>
|
||
|
||
<!-- 密码 -->
|
||
<el-form-item label="密码" prop="password" v-if="isAdd">
|
||
<el-input v-model="form.password" type="password" autocomplete="new-password" show-password
|
||
:placeholder="isAdd ? '请输入密码(至少6位)' : '留空则不修改密码'" />
|
||
</el-form-item>
|
||
|
||
<!-- 确认密码 -->
|
||
<el-form-item label="确认密码" prop="confirmPassword" v-if="isAdd">
|
||
<el-input v-model="form.confirmPassword" type="password" autocomplete="new-password" show-password
|
||
:placeholder="isAdd ? '请再次输入密码' : '留空则不修改密码'" />
|
||
</el-form-item>
|
||
|
||
<el-divider></el-divider>
|
||
<div class="form-title">个人信息</div>
|
||
<!-- 头像 -->
|
||
<el-form-item label="头像">
|
||
<el-upload class="avatar-uploader" :show-file-list="false" :action="uploadUrl" :headers="uploadHeaders"
|
||
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
|
||
<img v-if="form.avatar" :src="getAvatarUrl(form.avatar)" class="avatar" />
|
||
<el-icon v-else class="avatar-uploader-icon">
|
||
<Plus />
|
||
</el-icon>
|
||
</el-upload>
|
||
</el-form-item>
|
||
|
||
<!-- 裁剪对话框 -->
|
||
<CropImage ref="cropImageRef" @confirm="handleCropConfirm" />
|
||
|
||
<!-- 姓名 -->
|
||
<el-form-item label="姓名">
|
||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||
</el-form-item>
|
||
|
||
<!-- 性别 -->
|
||
<el-form-item label="性别">
|
||
<el-radio-group v-model="form.sex" placeholder="请选择性别">
|
||
<el-radio-button label="未知" :value="0" />
|
||
<el-radio-button label="男" :value="1" />
|
||
<el-radio-button label="女" :value="2" />
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<!-- 生日 -->
|
||
<el-form-item label="生日">
|
||
<el-date-picker v-model="form.birth" type="date" placeholder="请选择日期" style="width: 100%"
|
||
value-format="YYYY-MM-DD" />
|
||
</el-form-item>
|
||
|
||
<!-- 电话 -->
|
||
<el-form-item label="电话">
|
||
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||
</el-form-item>
|
||
|
||
<!-- 邮箱 -->
|
||
<el-form-item label="邮箱">
|
||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||
</el-form-item>
|
||
|
||
<!-- 状态 -->
|
||
<el-form-item label="状态">
|
||
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
|
||
<el-option label="启用" :value="1" />
|
||
<el-option label="禁用" :value="0" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 对话框脚部 -->
|
||
<template #footer>
|
||
<el-button @click="handleCancel">取消</el-button>
|
||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||
</template>
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from "vue";
|
||
import { ElMessage } from "element-plus";
|
||
import { Plus } from '@element-plus/icons-vue';
|
||
import type { UploadProps, UploadRequestOptions } from 'element-plus';
|
||
import { createUser, updateUser, getUserDetail } from "@/api/babyhealth";
|
||
import { uploadAvatar } from '@/api/file';
|
||
|
||
const props = defineProps({
|
||
modelValue: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
isEdit: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
statusDict: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits(["update:modelValue", "submit", "close"]);
|
||
|
||
const visible = ref(false);
|
||
const formRef = ref<any>(null);
|
||
const isAdd = ref(false);
|
||
|
||
const form = ref<any>({
|
||
id: null,
|
||
account: "",
|
||
password: "",
|
||
confirmPassword: "",
|
||
name: "",
|
||
sex: 0,
|
||
birth: "",
|
||
phone: "",
|
||
email: "",
|
||
status: 1,
|
||
avatar: "",
|
||
});
|
||
|
||
const dialogTitle = computed(() => {
|
||
return isAdd.value ? "添加用户" : "编辑用户";
|
||
});
|
||
|
||
// 密码验证规则
|
||
const validatePassword = (rule: any, value: any, callback: any) => {
|
||
if (isAdd.value) {
|
||
// 新增用户时,密码必填
|
||
if (!value) {
|
||
callback(new Error("请输入密码"));
|
||
return;
|
||
}
|
||
if (value.length < 6) {
|
||
callback(new Error("密码长度不能少于6位"));
|
||
return;
|
||
}
|
||
} else {
|
||
// 编辑用户时,如果填写了密码,则必须符合规则
|
||
if (value && value.length < 6) {
|
||
callback(new Error("密码长度不能少于6位"));
|
||
return;
|
||
}
|
||
}
|
||
callback();
|
||
};
|
||
|
||
// 确认密码验证规则
|
||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
||
if (isAdd.value) {
|
||
// 新增用户时,确认密码必填
|
||
if (!value) {
|
||
callback(new Error("请再次输入密码"));
|
||
return;
|
||
}
|
||
if (value !== form.value.password) {
|
||
callback(new Error("两次输入的密码不一致"));
|
||
return;
|
||
}
|
||
} else {
|
||
// 编辑用户时,如果填写了密码,则确认密码必须一致
|
||
if (form.value.password && value !== form.value.password) {
|
||
callback(new Error("两次输入的密码不一致"));
|
||
return;
|
||
}
|
||
// 如果填写了确认密码但没填密码,提示错误
|
||
if (value && !form.value.password) {
|
||
callback(new Error("请先输入密码"));
|
||
return;
|
||
}
|
||
}
|
||
callback();
|
||
};
|
||
|
||
// 表单验证规则
|
||
const rules = {
|
||
account: [
|
||
{ required: true, message: "请输入账号", trigger: "blur" },
|
||
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
|
||
],
|
||
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||
password: [{ validator: validatePassword, trigger: "blur" }],
|
||
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
|
||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
|
||
};
|
||
|
||
// 监听 modelValue
|
||
watch(
|
||
() => props.modelValue,
|
||
(newVal) => {
|
||
visible.value = newVal;
|
||
}
|
||
);
|
||
|
||
// 监听 visible 变化
|
||
watch(visible, (newVal) => {
|
||
if (!newVal) {
|
||
emit("update:modelValue", false);
|
||
}
|
||
});
|
||
|
||
// 监听 statusDict 变化,用于调试
|
||
watch(
|
||
() => props.statusDict,
|
||
(newVal) => { },
|
||
{ immediate: true, deep: true }
|
||
);
|
||
|
||
// 上传配置
|
||
const uploadUrl = import.meta.env.VITE_API_BASE_URL + "/admin/upload";
|
||
const uploadHeaders = {
|
||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||
};
|
||
|
||
// 拼接头像完整URL
|
||
const getAvatarUrl = (url: string) => {
|
||
if (!url) return '';
|
||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||
return url;
|
||
}
|
||
return `${import.meta.env.VITE_API_BASE_URL}${url}`;
|
||
};
|
||
|
||
// 头像上传前校验 - 只校验格式,打开裁剪对话框
|
||
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||
const isImage = rawFile.type.startsWith('image/');
|
||
const isLt2M = rawFile.size / 1024 / 1024 < 2;
|
||
|
||
if (!isImage) {
|
||
ElMessage.error('只能上传图片文件!');
|
||
return false;
|
||
}
|
||
if (!isLt2M) {
|
||
ElMessage.error('图片大小不能超过 2MB!');
|
||
return false;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
// 上传文件到服务器(裁剪后调用)
|
||
const uploadFile = async (file: File) => {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
// 使用uploadAvatar接口上传文件
|
||
const response = await uploadAvatar(formData, { cate: 'user_avatar' });
|
||
|
||
if (response.code === 200 || response.code === 201) {
|
||
// 更新用户头像
|
||
form.value.avatar = response.data.url || response.data.path || '';
|
||
ElMessage.success("头像上传成功");
|
||
} else {
|
||
ElMessage.error(response.msg || "上传失败");
|
||
}
|
||
} catch (error) {
|
||
console.error('上传失败:', error);
|
||
ElMessage.error("上传失败,请重试");
|
||
}
|
||
};
|
||
|
||
// 裁剪确认后的处理
|
||
const handleCropConfirm = async (file: File) => {
|
||
await uploadFile(file);
|
||
};
|
||
|
||
// 头像上传成功
|
||
const handleAvatarSuccess = (response: any) => {
|
||
if (response.code === 200 || response.code === 201) {
|
||
form.value.avatar = response.data.url || response.data.path || '';
|
||
ElMessage.success('头像上传成功');
|
||
} else {
|
||
ElMessage.error(response.msg || '上传失败');
|
||
}
|
||
};
|
||
|
||
const loadUserData = async (user: any) => {
|
||
try {
|
||
// 处理两种调用方式:传递用户对象或用户 ID
|
||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||
if (!userId) {
|
||
throw new Error("未提供有效的用户 ID");
|
||
}
|
||
|
||
const res = await getUserDetail(userId);
|
||
|
||
const data = res.data || res;
|
||
|
||
// 确保 sex 和 status 都是数字类型
|
||
const sexValue =
|
||
data.sex !== undefined && data.sex !== null
|
||
? Number(data.sex)
|
||
: 1;
|
||
|
||
const statusValue =
|
||
data.status !== undefined && data.status !== null
|
||
? Number(data.status)
|
||
: 1;
|
||
|
||
form.value = {
|
||
id: data.id,
|
||
account: data.account,
|
||
password: "",
|
||
confirmPassword: "",
|
||
name: data.name,
|
||
sex: sexValue,
|
||
birth: data.birth,
|
||
phone: data.phone,
|
||
email: data.email,
|
||
status: statusValue,
|
||
avatar: data.avatar || "",
|
||
};
|
||
} catch (e: any) {
|
||
console.error("Failed to load user data:", e);
|
||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||
ElMessage.error(errorMsg);
|
||
throw e;
|
||
}
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
visible.value = false;
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
// 表单验证
|
||
if (!formRef.value) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await formRef.value.validate();
|
||
} catch (error) {
|
||
ElMessage.warning("请检查表单填写是否正确");
|
||
return;
|
||
}
|
||
|
||
// 验证密码一致性
|
||
if (isAdd.value) {
|
||
// 新增用户时,密码必填
|
||
if (!form.value.password) {
|
||
ElMessage.error("请输入密码");
|
||
return;
|
||
}
|
||
if (form.value.password !== form.value.confirmPassword) {
|
||
ElMessage.error("两次输入的密码不一致");
|
||
return;
|
||
}
|
||
} else {
|
||
// 编辑用户时,如果填写了密码,则必须填写确认密码且一致
|
||
if (form.value.password) {
|
||
if (form.value.password !== form.value.confirmPassword) {
|
||
ElMessage.error("两次输入的密码不一致");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
if (isAdd.value) {
|
||
// 新增用户
|
||
const submitData: any = {
|
||
account: form.value.account,
|
||
password: form.value.password || '',
|
||
name: form.value.name,
|
||
sex: form.value.sex,
|
||
birth: form.value.birth,
|
||
phone: form.value.phone,
|
||
email: form.value.email,
|
||
status: form.value.status,
|
||
avatar: form.value.avatar,
|
||
};
|
||
|
||
await createUser(submitData);
|
||
ElMessage.success("添加成功");
|
||
} else {
|
||
// 编辑用户
|
||
if (!form.value.id || form.value.id === 0) {
|
||
ElMessage.error("用户ID不能为空");
|
||
return;
|
||
}
|
||
|
||
const submitData: any = {
|
||
id: form.value.id,
|
||
account: form.value.account,
|
||
name: form.value.name,
|
||
sex: form.value.sex,
|
||
birth: form.value.birth,
|
||
phone: form.value.phone,
|
||
email: form.value.email,
|
||
status: form.value.status,
|
||
avatar: form.value.avatar,
|
||
};
|
||
|
||
// 只有在填写了密码时才添加到提交数据中
|
||
if (form.value.password) {
|
||
submitData.password = form.value.password;
|
||
}
|
||
|
||
await updateUser(form.value.id, submitData);
|
||
ElMessage.success("更新成功");
|
||
}
|
||
|
||
visible.value = false;
|
||
emit("submit");
|
||
} catch (e: any) {
|
||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||
ElMessage.error(errorMsg);
|
||
}
|
||
};
|
||
|
||
// 暴露方法给父组件
|
||
defineExpose({
|
||
loadUserData,
|
||
openAdd: () => {
|
||
isAdd.value = true;
|
||
form.value = {
|
||
id: 0,
|
||
account: "",
|
||
password: "",
|
||
confirmPassword: "",
|
||
name: "",
|
||
sex: 0,
|
||
birth: "",
|
||
phone: "",
|
||
email: "",
|
||
status: 1,
|
||
avatar: "",
|
||
};
|
||
visible.value = true;
|
||
// 清除表单验证
|
||
if (formRef.value) {
|
||
formRef.value.clearValidate();
|
||
}
|
||
},
|
||
openEdit: (user: any) => {
|
||
isAdd.value = false;
|
||
visible.value = true;
|
||
// 清除表单验证
|
||
if (formRef.value) {
|
||
formRef.value.clearValidate();
|
||
}
|
||
// 异步加载用户详细信息
|
||
loadUserData(user);
|
||
},
|
||
open: (user?: any) => {
|
||
if (user) {
|
||
isAdd.value = false;
|
||
loadUserData(user);
|
||
} else {
|
||
isAdd.value = true;
|
||
form.value = {
|
||
id: 0,
|
||
account: "",
|
||
password: "",
|
||
confirmPassword: "",
|
||
name: "",
|
||
sex: 0,
|
||
birth: "",
|
||
phone: "",
|
||
email: "",
|
||
status: 1,
|
||
avatar: "",
|
||
};
|
||
}
|
||
visible.value = true;
|
||
// 清除表单验证
|
||
if (formRef.value) {
|
||
formRef.value.clearValidate();
|
||
}
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.form-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.avatar-uploader {
|
||
.avatar {
|
||
width: 100px;
|
||
height: 100px;
|
||
border-radius: 6px;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
|
||
.el-upload {
|
||
border: 1px dashed var(--el-border-color);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: var(--el-transition-duration-fast);
|
||
|
||
&:hover {
|
||
border-color: var(--el-color-primary);
|
||
}
|
||
|
||
.avatar-uploader-icon {
|
||
font-size: 28px;
|
||
color: #8c939d;
|
||
width: 100px;
|
||
height: 100px;
|
||
text-align: center;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
}
|
||
</style>
|