完善用户管理和登录的表单校验

This commit is contained in:
ranranran12123 2025-05-16 14:40:38 +08:00
parent 6cbd8f93e6
commit 0336f21f83
2 changed files with 363 additions and 45 deletions

View File

@ -10,15 +10,13 @@
<div class="input-box">
<input class="password" :type="type" placeholder="密码" v-model="userPassword"/>
<div @click="showPwd">
<!-- 隐藏图标 -->
<img src="../assets/login/hidePassword.png" v-show="showPassword === false" alt=""/>
<!-- 显示图标 -->
<img src="../assets/login/showPassword.png" v-show="showPassword === true" alt=""/>
</div>
<!-- <div @click="showPwd">-->
<!-- <img src="../assets/login/hidePassword.png" v-show="!showPassword" alt=""/>-->
<!-- <img src="../assets/login/showPassword.png" v-show="showPassword" alt=""/>-->
<!-- </div>-->
</div>
<div class="error-messages" v-if="userPassword && passwordErrors.length">
<!-- <div class="error-messages" v-if="false">-->
<!-- <div class="error-messages" v-if="false">-->
<div v-for="error in passwordErrors" :key="error" class="error-message">
{{ error }}
</div>
@ -45,7 +43,7 @@ const store = userStore()
const router = useRouter()
//
let showPassword = ref(false)
// let showPassword = ref(false)
//
let type = ref('password')
const userAccount = ref('');
@ -137,14 +135,14 @@ const onLogin = async () => {
/**
* 显示密码
*/
const showPwd = () => {
showPassword.value = !showPassword.value
if (showPassword.value == false) {
type.value = 'password'
} else {
type.value = 'text'
}
}
// const showPwd = () => {
// showPassword.value = !showPassword.value
// if (showPassword.value == false) {
// type.value = 'password'
// } else {
// type.value = 'text'
// }
// }
</script>
<style scoped>

View File

@ -13,7 +13,7 @@
/>
</a-form-item>
<a-button type="primary" @click="showModal">新增用户</a-button>
<!-- 新增用户-->
<!-- 新增用户-->
<a-modal
v-model:open="openUser"
title="新增用户"
@ -41,7 +41,21 @@
</a-form-item>
<a-form-item label="头像" name="userAvatar">
<a-input v-model:value="formData.userAvatar" />
<a-upload
name="file"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:before-upload="beforeUpload"
:custom-request="handleUpload"
>
<img v-if="formData.userAvatar" :src="previewImage" alt="avatar" style="width: 100%"/>
<div v-else>
<loading-outlined v-if="uploadLoading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">上传头像</div>
</div>
</a-upload>
</a-form-item>
<a-form-item label="手机号" name="phoneNumber">
@ -161,24 +175,83 @@
<a-form
v-if="isEditMode"
:model="editForm"
:rules="editFormRules"
ref="editFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<!-- 可编辑表单 -->
<a-form-item label="昵称">
<a-input v-model:value="editForm.nickName" />
<a-form-item label="昵称" name="nickName">
<a-input
v-model:value="editForm.nickName"
:maxlength="6"
@input="handleEditNicknameInput"
@keypress="handleMaxLength(6)"
>
<template #suffix>
<span v-if="editForm.nickName.length > 0" class="input-tip">
{{ editForm.nickName.length }}/6
<span v-if="editForm.nickName.length > 6" class="error-tip">超过最大长度</span>
</span>
</template>
</a-input>
</a-form-item>
<a-form-item label="头像URL">
<a-input v-model:value="editForm.userAvatar" />
<a-form-item label="头像" name="userAvatar">
<a-upload
name="file"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:before-upload="beforeUpload"
:custom-request="handleEditUpload"
>
<img v-if="editForm.userAvatar" :src="editPreviewImage" alt="avatar" style="width: 100%"/>
<div v-else>
<loading-outlined v-if="editUploadLoading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">上传头像</div>
</div>
</a-upload>
</a-form-item>
<a-form-item label="手机号">
<a-input v-model:value="editForm.phoneNumber" />
<a-form-item label="手机号" name="phoneNumber">
<a-input
v-model:value="editForm.phoneNumber"
:maxlength="11"
@input="handleEditPhoneInput"
>
<template #suffix>
<span v-if="editForm.phoneNumber.length > 0" class="input-tip">
{{ editForm.phoneNumber.length }}/11
<span v-if="editForm.phoneNumber.length > 11" class="error-tip">超过最大长度</span>
</span>
</template>
</a-input>
</a-form-item>
<a-form-item label="账户">
<a-input v-model:value="editForm.userAccount" disabled />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="editForm.userPassword" />
<a-form-item label="密码" name="userPassword">
<a-input
v-model:value="editForm.userPassword"
type="password"
:maxlength="10"
@input="handleEditPasswordInput"
@keypress="handleMaxLength(10)"
>
<template #suffix>
<span v-if="editForm.userPassword.length > 0" class="input-tip">
{{ editForm.userPassword.length }}/10
<div class="password-strength">
<span :class="{ 'strength-ok': editHasLower }">小写字母</span>
<span :class="{ 'strength-ok': editHasUpper }">大写字母</span>
<span :class="{ 'strength-ok': editHasNumber }">数字</span>
</div>
</span>
</template>
</a-input>
</a-form-item>
<a-form-item label="邀请码">
<a-input v-model:value="editForm.invitationCode" />
@ -191,7 +264,7 @@
</a-select>
</a-form-item>
<a-form-item label="上级用户ID">
<a-input-number v-model:value="editForm.parentUserId" />
<a-input-number v-model:value="editForm.parentUserId" :min="0" />
</a-form-item>
<a-form-item label="上级用户列表">
<a-input
@ -222,9 +295,9 @@
<span class="info-label">账户信息</span>
<div class="account-detail">
<span>{{ selectedUser.userAccount }}</span>
<!-- <a-tag class="role-tag">-->
<!-- {{ roleMap[editForm.userRole] }}-->
<!-- </a-tag>-->
<!-- <a-tag class="role-tag">-->
<!-- {{ roleMap[editForm.userRole] }}-->
<!-- </a-tag>-->
</div>
</div>
</div>
@ -263,12 +336,94 @@
import { onMounted, ref,computed } from "vue";
import myAxios from "../../api/myAxios.ts";
import { message } from "ant-design-vue";
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import type { UploadProps } from 'ant-design-vue';
import {
UserOutlined,
IdcardOutlined,
SmileOutlined,
PhoneOutlined,
} from '@ant-design/icons-vue';
const uploadLoading = ref(false);
const previewImage = ref('');
//
const beforeUpload: UploadProps['beforeUpload'] = file => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('图片大小不能超过5MB!');
return false;
}
return true;
};
//
const handleUpload = async ({ file }: { file: File }) => {
const form = new FormData();
form.append('file', file); // fileformData
try {
uploadLoading.value = true;
const storedToken = localStorage.getItem('token');
//
const res:any = await myAxios.post('/file/upload?biz=avatar', form, {
headers: {
Authorization: storedToken,
//
}
});
console.log(res)
// dataURL
if (res.code === 1) { // 200
formData.value.userAvatar = res.data;
previewImage.value = URL.createObjectURL(file);
message.success('上传成功');
} else {
message.error(res.message || '上传失败');
}
} catch (error:any) {
console.error('上传失败详情:', error);
message.error(`上传失败:${error.response?.data?.error || error.message}`);
} finally {
uploadLoading.value = false;
}
};
//
const handleEditNicknameInput = (e: any) => {
//
const trimmedValue = e.target.value.trim().slice(0, 6);
editForm.value.nickName = trimmedValue;
//
editFormRef.value?.validateField('nickName');
};
const handleEditPhoneInput = (e: any) => {
//
const numericValue = e.target.value.replace(/\D/g, '').slice(0, 11);
editForm.value.phoneNumber = numericValue;
//
editFormRef.value?.validateField('phoneNumber');
};
const handleEditPasswordInput = (e: any) => {
//
editForm.value.userPassword = e.target.value.slice(0, 10);
};
//
const handleMaxLength = (max: number) => (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement && e.target.value.length >= max) {
e.preventDefault();
}
};
//
// const roleColorMap = {
@ -487,7 +642,59 @@ const pagination = ref({
showTotal: (total: number) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
});
//
//
const editFormRules = {
nickName: [
{
required: true,
message: '请输入昵称',
trigger: ['input', 'blur']
},
{
min: 1,
max: 6,
message: '昵称长度需为1-6位',
trigger: ['input', 'blur']
}
],
phoneNumber: [
{
required: true,
message: '请输入手机号',
trigger: ['input', 'blur']
},
{
pattern: /^1[3-9]\d{9}$/,
message: '必须是有效的11位手机号',
trigger: ['input', 'blur']
}
],
userPassword: [
{
min: 6,
max: 10,
message: '长度需为6-10位',
trigger: ['input', 'blur']
},
{
validator: (_: any, value: string) => {
if (value && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return Promise.reject('必须包含大小写字母和数字');
}
return Promise.resolve();
},
trigger: ['input', 'blur']
}
]
};
//
const editHasLower = computed(() => /[a-z]/.test(editForm.value.userPassword))
const editHasUpper = computed(() => /[A-Z]/.test(editForm.value.userPassword))
const editHasNumber = computed(() => /\d/.test(editForm.value.userPassword))
const editFormRef = ref(); //
const handleTableChange = (pag: any, sorter: any) => {
searchParams.value = {
@ -675,12 +882,46 @@ const editForm = ref({
// };
// showDrawer
//
const editUploadLoading = ref(false);
const editPreviewImage = ref('');
//
const handleEditUpload = async ({ file }: { file: File }) => {
const form = new FormData();
form.append('file', file);
try {
editUploadLoading.value = true;
const storedToken = localStorage.getItem('token');
const res: any = await myAxios.post('/file/upload?biz=avatar', form, {
headers: {
Authorization: storedToken,
}
});
if (res.code === 1) {
editForm.value.userAvatar = res.data;
editPreviewImage.value = URL.createObjectURL(file);
message.success('头像更新成功');
} else {
message.error(res.message || '头像上传失败');
}
} catch (error: any) {
console.error('编辑头像上传失败:', error);
message.error(`上传失败:${error.response?.data?.error || error.message}`);
} finally {
editUploadLoading.value = false;
}
};
const showDrawer = (record: any) => {
selectedUser.value = record;
editForm.value = {
id: record.id,
nickName: record.nickName,
userAvatar: record.userAvatar,
userAvatar: record.userAvatar||'',
phoneNumber: record.phoneNumber,
userAccount: record.userAccount,
userPassword: record.userPassword,
@ -704,13 +945,44 @@ const handleSave = async () => {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
//
const payload = {
...editForm.value,
SuperUserList: editForm.value.SuperUserList.split(',').map(Number)
//
const processValue = (value: any) => {
if (value === "" || value === "无") return null; // ""null
return value;
};
const res:any = await myAxios.post('/userInfo/update', payload, {
//
const processNumber = (value: any) => {
const num = Number(value);
return isNaN(num) ? null : num;
};
//
const payload = {
id: editForm.value.id,
nickName: processValue(editForm.value.nickName),
userAvatar: processValue(editForm.value.userAvatar),
phoneNumber: processValue(editForm.value.phoneNumber),
userAccount: editForm.value.userAccount, //
userPassword: editForm.value.userPassword || undefined, //
invitationCode: processValue(editForm.value.invitationCode),
userRole: editForm.value.userRole || 'user',
parentUserId: processNumber(editForm.value.parentUserId),
SuperUserList: editForm.value.SuperUserList
? editForm.value.SuperUserList.split(',')
.map(item => processNumber(item.trim()))
.filter(num => num !== null)
: []
};
// undefined
const cleanPayload = Object.fromEntries(
Object.entries(payload).filter(([_, v]) => v !== undefined)
);
console.log("最终请求体:", JSON.stringify(cleanPayload, null, 2));
const res:any = await myAxios.post('/userInfo/update', cleanPayload, {
headers: {
Authorization: storedToken,
'Content-Type': 'application/json'
@ -719,20 +991,16 @@ const handleSave = async () => {
if (res.code === 1) {
message.success('更新成功');
//
Object.assign(selectedUser.value, editForm.value);
//
await getUserList();
isEditMode.value = false;
} else {
message.error(res.message || '更新失败');
}
} catch (error) {
} catch (error:any) {
console.error('更新失败:', error);
message.error('更新失败,请检查网络');
message.error(error.response?.data?.message || '更新失败,请检查网络');
}
};
//
const openUser = ref<boolean>(false);
const formRef = ref();
@ -993,4 +1261,56 @@ const reset = () => {
.strength-ok::before {
background: #52c41a !important;
}
.avatar-uploader :deep(.ant-upload) {
width: 100%;
height: 100%;
overflow: hidden;
}
.ant-upload-text {
margin-top: 8px;
}
.input-tip {
font-size: 0.875rem;
color: #666;
}
.error-tip {
color: #f5222d;
margin-left: 4px;
}
.password-strength {
margin-top: 4px;
font-size: 0.875rem;
}
.strength-ok {
color: #52c41a;
}
.avatar-uploader {
width: 100%;
max-width: 200px;
margin: 0 auto;
}
.avatar-uploader .ant-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .ant-upload:hover {
border-color: #40a9ff;
}
.avatar-uploader img {
width: 100%;
height: 100%;
display: block;
}
</style>