feat: 个人中心Alpha开发阶段

This commit is contained in:
ray
2024-08-19 00:30:42 +08:00
parent 100e80ff3b
commit 375fc37af0
2 changed files with 235 additions and 101 deletions

View File

@@ -162,6 +162,20 @@ class UserAPI {
data: data, data: data,
}); });
} }
/**
* 发送手机/邮箱验证码
*
* @param contact 联系方式 手机号/邮箱
* @param contactType 联系方式类型 MOBILE:手机;EMAIL:邮箱
*/
static sendVerificationCode(contact: string, contactType: string) {
return request({
url: `${USER_BASE_URL}/send-verification-code`,
method: "get",
params: { contact: contact, contactType: contactType },
});
}
} }
export default UserAPI; export default UserAPI;
@@ -317,6 +331,8 @@ export interface PasswordChangeForm {
oldPassword?: string; oldPassword?: string;
/** 新密码 */ /** 新密码 */
newPassword?: string; newPassword?: string;
/** 确认新密码 */
confirmPassword?: string;
} }
/** 手机绑定表单 */ /** 手机绑定表单 */

View File

@@ -2,34 +2,35 @@
<div class="app-container"> <div class="app-container">
<el-tabs tab-position="left"> <el-tabs tab-position="left">
<!-- 基本设置 Tab Pane --> <!-- 基本设置 Tab Pane -->
<el-tab-pane label="基本设置"> <el-tab-pane label="账号信息">
<div class="w-full"> <div class="w-full">
<el-card class="flex-1"> <el-card>
<div class=""> <!-- 头像和昵称部分 -->
<!-- 头像和昵称部分 --> <div class="relative w-100px h-100px flex-center">
<div class="relative w-100px h-100px flex-center"> <el-avatar :src="userProfile.avatar" :size="100" />
<el-avatar :src="userProfile.avatar" :size="100" /> <el-button
<el-button type="info"
type="info" class="absolute bottom-0 right-0 cursor-pointer"
class="absolute bottom-0 right-0 cursor-pointer" circle
circle :icon="Camera"
:icon="Camera" size="small"
size="small" @click="triggerFileUpload"
@click="triggerFileUpload" />
/> <input
<input type="file"
type="file" ref="fileInput"
ref="fileInput" style="display: none"
style="display: none" @change="handleFileChange"
@change="handleFileChange" />
/> </div>
</div> <div class="mt-5">
<div class="mt-5"> {{ userProfile.nickname }}
{{ userProfile.nickname }} <el-icon
<el-icon class="align-middle cursor-pointer"> class="align-middle cursor-pointer"
<Edit /> @click="handleOpenDialog(DialogType.ACCOUNT)"
</el-icon> >
</div> <Edit />
</el-icon>
</div> </div>
<!-- 用户信息描述 --> <!-- 用户信息描述 -->
<el-descriptions :column="1" class="mt-10"> <el-descriptions :column="1" class="mt-10">
@@ -177,12 +178,29 @@
</el-tabs> </el-tabs>
<!-- 弹窗 --> <!-- 弹窗 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" :width="400"> <el-dialog :title="dialog.title" v-model="dialog.visible" :width="500">
<!-- 账号资料 -->
<el-form
v-if="dialog.type === DialogType.ACCOUNT"
:model="userProfileForm"
ref="userProfileFormRef"
:label-width="100"
>
<el-form-item label="昵称">
<el-input v-model="userProfileForm.nickname" />
</el-form-item>
<el-form-item label="性别">
<dictionary v-model="userProfileForm.gender" code="gender" />
</el-form-item>
</el-form>
<!-- 修改密码 -->
<el-form <el-form
v-if="dialog.type === DialogType.PASSWORD" v-if="dialog.type === DialogType.PASSWORD"
:model="passwordChangeForm" :model="passwordChangeForm"
:rules="passwordChangeRules" :rules="passwordChangeRules"
ref="passwordChangeFormRef" ref="passwordChangeFormRef"
:label-width="100"
> >
<el-form-item label="原密码" prop="oldPassword"> <el-form-item label="原密码" prop="oldPassword">
<el-input <el-input
@@ -199,55 +217,70 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="确认密码" prop="confirmPassword"> <el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="confirmPassword" show-password /> <el-input
type="password"
v-model="passwordChangeForm.confirmPassword"
show-password
/>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 绑定手机 -->
<el-form <el-form
v-else-if="dialog.type === DialogType.MOBILE" v-else-if="dialog.type === DialogType.MOBILE"
:model="mobileBindingForm" :model="mobileBindingForm"
:rules="mobileBindingRules" :rules="mobileBindingRules"
ref="mobileBindingFormRef" ref="mobileBindingFormRef"
:label-width="100"
> >
<el-form-item label="手机号码" prop="mobile"> <el-form-item label="手机号码" prop="mobile">
<el-input v-model="mobileBindingForm.mobile" /> <el-input v-model="mobileBindingForm.mobile" style="width: 250px" />
</el-form-item>
<el-form-item>
<el-button
:disabled="mobileCountdown > 0"
@click="handleSendMobileCode"
>
{{
mobileCountdown > 0
? `${mobileCountdown}s后重新发送`
: "发送验证码"
}}
</el-button>
</el-form-item> </el-form-item>
<el-form-item label="验证码" prop="code"> <el-form-item label="验证码" prop="code">
<el-input v-model="mobileBindingForm.code" /> <el-input v-model="mobileBindingForm.code" style="width: 250px">
<template #append>
<el-button
class="ml-5"
:disabled="mobileCountdown > 0"
@click="handleSendVerificationCode('MOBILE')"
>
{{
mobileCountdown > 0
? `${mobileCountdown}s后重新发送`
: "发送验证码"
}}
</el-button>
</template>
</el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 绑定邮箱 -->
<el-form <el-form
v-else-if="dialog.type === DialogType.EMAIL" v-else-if="dialog.type === DialogType.EMAIL"
:model="emailBindingForm" :model="emailBindingForm"
:rules="emailBindingRules" :rules="emailBindingRules"
ref="emailBindingFormRef" ref="emailBindingFormRef"
:label-width="100"
> >
<el-form-item label="邮箱" prop="email"> <el-form-item label="邮箱" prop="email">
<el-input v-model="emailBindingForm.email" /> <el-input v-model="emailBindingForm.email" style="width: 250px" />
</el-form-item>
<el-form-item>
<el-button
:disabled="emailCountdown > 0"
@click="handleSendEmailCode"
>
{{
emailCountdown > 0 ? `${emailCountdown}s后重新发送` : "发送验证码"
}}
</el-button>
</el-form-item> </el-form-item>
<el-form-item label="验证码" prop="code"> <el-form-item label="验证码" prop="code">
<el-input v-model="emailBindingForm.code" /> <el-input v-model="emailBindingForm.code" style="width: 250px">
<template #append>
<el-button
class="ml-5"
:disabled="emailCountdown > 0"
@click="handleSendVerificationCode('EMAIL')"
>
{{
emailCountdown > 0
? `${emailCountdown}s后重新发送`
: "发送验证码"
}}
</el-button>
</template>
</el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -263,18 +296,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import UserAPI, { import UserAPI, {
UserProfileVO, UserProfileVO,
UserProfileForm,
PasswordChangeForm, PasswordChangeForm,
MobileBindingForm, MobileBindingForm,
EmailBindingForm, EmailBindingForm,
UserProfileForm,
} from "@/api/user"; } from "@/api/user";
import FileAPI from "@/api/file";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { Camera } from "@element-plus/icons-vue"; import { Camera } from "@element-plus/icons-vue";
const userStore = useUserStore(); const userStore = useUserStore();
const userProfile = ref<UserProfileVO>({}); const userProfile = ref<UserProfileVO>({});
enum DialogType { enum DialogType {
ACCOUNT = "account",
PASSWORD = "password", PASSWORD = "password",
MOBILE = "mobile", MOBILE = "mobile",
EMAIL = "email", EMAIL = "email",
@@ -283,11 +321,10 @@ enum DialogType {
const dialog = reactive({ const dialog = reactive({
visible: false, visible: false,
title: "", title: "",
type: "" as DialogType, // 修改密码、绑定手机、绑定邮箱 type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱
}); });
const confirmPassword = ref(""); const userProfileForm = reactive<UserProfileForm>({});
const passwordChangeForm = reactive<PasswordChangeForm>({}); const passwordChangeForm = reactive<PasswordChangeForm>({});
const mobileBindingForm = reactive<MobileBindingForm>({}); const mobileBindingForm = reactive<MobileBindingForm>({});
const emailBindingForm = reactive<EmailBindingForm>({}); const emailBindingForm = reactive<EmailBindingForm>({});
@@ -298,6 +335,7 @@ const mobileTimer = ref<NodeJS.Timeout | null>(null);
const emailCountdown = ref(0); const emailCountdown = ref(0);
const emailTimer = ref<NodeJS.Timeout | null>(null); const emailTimer = ref<NodeJS.Timeout | null>(null);
// 修改密码校验规则
const passwordChangeRules = { const passwordChangeRules = {
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }], oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
newPassword: [{ required: true, message: "请输入新密码", trigger: "blur" }], newPassword: [{ required: true, message: "请输入新密码", trigger: "blur" }],
@@ -306,66 +344,132 @@ const passwordChangeRules = {
], ],
}; };
// 手机号校验规则
const mobileBindingRules = { const mobileBindingRules = {
mobile: [{ required: true, message: "请输入手机号", trigger: "blur" }], mobile: [
mobileCode: [{ required: true, message: "请输入验证码", trigger: "blur" }], { required: true, message: "请输入手机号", trigger: "blur" },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
}; };
// 邮箱校验规则
const emailBindingRules = { const emailBindingRules = {
email: [{ required: true, message: "请输入邮箱", trigger: "blur" }], email: [
emailCode: [{ required: true, message: "请输入验证码", trigger: "blur" }], { required: true, message: "请输入邮箱", trigger: "blur" },
{
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
message: "请输入正确的邮箱地址",
trigger: "blur",
},
],
code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
}; };
/**
* 打开弹窗
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
*/
const handleOpenDialog = (type: DialogType) => { const handleOpenDialog = (type: DialogType) => {
dialog.type = type; dialog.type = type;
dialog.visible = true; dialog.visible = true;
}; switch (type) {
case DialogType.ACCOUNT:
const handleSendMobileCode = async () => { dialog.title = "账号资料";
if (!mobileBindingForm.value.mobile) { // 初始化表单数据
return; userProfileForm.id = userProfile.value.id;
userProfileForm.nickname = userProfile.value.nickname;
userProfileForm.gender = userProfile.value.gender;
break;
case DialogType.PASSWORD:
dialog.title = "修改密码";
break;
case DialogType.MOBILE:
dialog.title = "绑定手机";
break;
case DialogType.EMAIL:
dialog.title = "绑定邮箱";
break;
} }
// await UserAPI.sendMobileCode(mobileBindingForm.value.mobile);
mobileCountdown.value = 60;
mobileTimer.value = setInterval(() => {
if (mobileCountdown.value > 0) {
mobileCountdown.value -= 1;
} else {
clearInterval(mobileTimer.value!);
}
}, 1000);
}; };
const handleSendEmailCode = async () => { /**
if (!emailBindingForm.email) { * 发送验证码
return; *
* @param contactType 联系方式类型 MOBILE: 手机号码 EMAIL: 邮箱
*/
const handleSendVerificationCode = async (contactType: string) => {
if (contactType === "MOBILE") {
if (!mobileBindingForm.mobile) {
ElMessage.error("请输入手机号");
return;
}
// 验证手机号格式
const reg = /^1[3-9]\d{9}$/;
if (!reg.test(mobileBindingForm.mobile)) {
ElMessage.error("手机号格式不正确");
return;
}
mobileCountdown.value = 60;
mobileTimer.value = setInterval(() => {
if (mobileCountdown.value > 0) {
mobileCountdown.value -= 1;
} else {
clearInterval(mobileTimer.value!);
}
}, 1000);
} else if (contactType === "EMAIL") {
if (!emailBindingForm.email) {
ElMessage.error("请输入邮箱");
return;
}
// 验证邮箱格式
const reg = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/;
if (!reg.test(emailBindingForm.email)) {
ElMessage.error("邮箱格式不正确");
return;
}
emailCountdown.value = 60;
emailTimer.value = setInterval(() => {
if (emailCountdown.value > 0) {
emailCountdown.value -= 1;
} else {
clearInterval(emailTimer.value!);
}
}, 1000);
} }
// await UserAPI.sendEmailCode(emailBindingForm.value.email);
emailCountdown.value = 60;
emailTimer.value = setInterval(() => {
if (emailCountdown.value > 0) {
emailCountdown.value -= 1;
} else {
clearInterval(emailTimer.value!);
}
}, 1000);
}; };
/**
* 提交表单
*/
const handleSubmit = async () => { const handleSubmit = async () => {
if (dialog.type === DialogType.PASSWORD) { if (dialog.type === DialogType.ACCOUNT) {
if (passwordChangeForm.newPassword !== confirmPassword.value) { UserAPI.updateProfile(userProfileForm.id, userProfileForm).then(() => {
ElMessage.success("账号资料修改成功");
dialog.visible = false;
loadUserProfile();
});
} else if (dialog.type === DialogType.PASSWORD) {
if (passwordChangeForm.newPassword !== passwordChangeForm.confirmPassword) {
ElMessage.error("两次输入的密码不一致"); ElMessage.error("两次输入的密码不一致");
return; return;
} }
await UserAPI.changePassword(passwordChangeForm); UserAPI.changePassword(passwordChangeForm).then(() => {
ElMessage.success("密码修改成功");
dialog.visible = false;
});
} else if (dialog.type === "mobile") { } else if (dialog.type === "mobile") {
//await UserAPI.bindMobile(mobileBindingForm.value); //await UserAPI.bindMobile(mobileBindingForm.value);
} else if (dialog.type === "email") { } else if (dialog.type === "email") {
//await UserAPI.bindEmail(emailBindingForm.value); //await UserAPI.bindEmail(emailBindingForm.value);
} }
dialog.visible = false;
}; };
const fileInput = ref<HTMLInputElement | null>(null); const fileInput = ref<HTMLInputElement | null>(null);
@@ -374,15 +478,31 @@ const triggerFileUpload = () => {
fileInput.value?.click(); fileInput.value?.click();
}; };
const handleFileChange = (event: Event) => { const handleFileChange = async (event: Event) => {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const file = target.files ? target.files[0] : null; const file = target.files ? target.files[0] : null;
if (file) { if (file) {
// Handle the file upload here // 调用文件上传API
console.log("Selected file:", file); try {
const data = await FileAPI.upload(file);
// 更新用户头像
userProfile.value.avatar = data.url;
// 更新用户信息
await UserAPI.updateProfile(userProfile.value.id, {
avatar: data.url,
});
} catch (error) {
ElMessage.error("头像上传失败");
}
} }
}; };
/** 加载用户信息 */
const loadUserProfile = async () => {
const data = await UserAPI.getProfile(userStore.user.userId);
userProfile.value = data;
};
onMounted(async () => { onMounted(async () => {
if (mobileTimer.value) { if (mobileTimer.value) {
clearInterval(mobileTimer.value); clearInterval(mobileTimer.value);
@@ -390,9 +510,7 @@ onMounted(async () => {
if (emailTimer.value) { if (emailTimer.value) {
clearInterval(emailTimer.value); clearInterval(emailTimer.value);
} }
await loadUserProfile();
const data = await UserAPI.getProfile(userStore.user.userId);
userProfile.value = data;
}); });
</script> </script>