feat: 个人中心

This commit is contained in:
zhangyuanpeng
2024-11-14 13:16:58 +08:00
parent ec00cda60f
commit 251b60863a
3 changed files with 717 additions and 1 deletions

75
src/api/file/index.ts Normal file
View File

@@ -0,0 +1,75 @@
import { getToken } from "@/utils/cache";
import { ResultCodeEnum } from "@/enums/ResultCodeEnum";
// H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_APP_API_URL 作为请求路径
let baseApi = import.meta.env.VITE_APP_API_URL;
// #ifdef H5
baseApi = import.meta.env.VITE_APP_BASE_API;
// #endif
const FileAPI = {
/**
* 文件上传地址
*/
uploadUrl: baseApi + "/api/v1/files",
/**
* 上传文件
*
* @param filePath
*/
upload(filePath: string): Promise<FileInfo> {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: this.uploadUrl,
filePath: filePath,
name: "file",
header: {
Authorization: getToken() ? `Bearer ${getToken()}` : "",
},
formData: {},
success: (response) => {
const resData = JSON.parse(response.data) as ResponseData<FileInfo>;
// 业务状态码 00000 表示成功
if (resData.code === ResultCodeEnum.SUCCESS) {
resolve(resData.data);
} else {
// 其他业务处理失败
uni.showToast({
title: resData.msg || "文件上传失败",
icon: "none",
});
reject({
message: resData.msg || "业务处理失败",
code: resData.code,
});
}
},
fail: (error) => {
console.log("fail error", error);
uni.showToast({
title: "文件上传请求失败",
icon: "none",
duration: 2000,
});
reject({
message: "文件上传请求失败",
error,
});
},
});
});
},
};
export default FileAPI;
/**
* 文件API类型声明
*/
export interface FileInfo {
/** 文件名 */
name: string;
/** 文件路径 */
url: string;
}

View File

@@ -27,6 +27,63 @@ const UserAPI = {
data: queryParams,
});
},
/** 获取个人中心用户信息 */
getProfile() {
return request<UserProfileVO>({
url: `${USER_BASE_URL}/profile`,
method: "GET",
});
},
/** 修改个人中心用户信息 */
updateProfile(data: UserProfileForm) {
return request({
url: `${USER_BASE_URL}/profile`,
method: "PUT",
data: data,
});
},
/** 修改个人中心用户密码 */
changePassword(data: PasswordChangeForm) {
return request({
url: `${USER_BASE_URL}/password`,
method: "PUT",
data: data,
});
},
/**
* 发送手机/邮箱验证码
*
* @param contact 联系方式 手机号/邮箱
* @param contactType 联系方式类型 MOBILE:手机;EMAIL:邮箱
*/
sendVerificationCode(contact: string, contactType: string) {
return request({
url: `${USER_BASE_URL}/send-verification-code?contact=${contact}&contactType=${contactType}`,
method: "POST",
});
},
/** 绑定个人中心用户手机 */
bindMobile(data: MobileBindingForm) {
return request({
url: `${USER_BASE_URL}/mobile`,
method: "PUT",
data: data,
});
},
/** 绑定个人中心用户邮箱 */
bindEmail(data: EmailBindingForm) {
return request({
url: `${USER_BASE_URL}/email`,
method: "PUT",
data: data,
});
},
};
export default UserAPI;
@@ -93,3 +150,86 @@ export interface UserPageVO {
/** 用户名 */
username?: string;
}
/** 个人中心用户信息 */
export interface UserProfileVO {
/** 用户ID */
id?: number;
/** 用户名 */
username?: string;
/** 昵称 */
nickname?: string;
/** 头像URL */
avatar?: string;
/** 性别 */
gender?: number;
/** 手机号 */
mobile?: string;
/** 邮箱 */
email?: string;
/** 部门名称 */
deptName?: string;
/** 角色名称,多个使用英文逗号(,)分割 */
roleNames?: string;
/** 创建时间 */
createTime?: Date;
}
/** 个人中心用户信息表单 */
export interface UserProfileForm {
/** 用户ID */
id?: number;
/** 用户名 */
username?: string;
/** 昵称 */
nickname?: string;
/** 头像URL */
avatar?: string;
/** 性别 */
gender?: number;
/** 手机号 */
mobile?: string;
/** 邮箱 */
email?: string;
}
/** 修改密码表单 */
export interface PasswordChangeForm {
/** 原密码 */
oldPassword?: string;
/** 新密码 */
newPassword?: string;
/** 确认新密码 */
confirmPassword?: string;
}
/** 修改手机表单 */
export interface MobileBindingForm {
/** 手机号 */
mobile?: string;
/** 验证码 */
code?: string;
}
/** 修改邮箱表单 */
export interface EmailBindingForm {
/** 邮箱 */
email?: string;
/** 验证码 */
code?: string;
}

View File

@@ -1,5 +1,506 @@
<template>
<view>
<text>profile</text>
<view v-if="userProfile" class="profile-card">
<wd-cell-group border>
<wd-cell class="avatar-cell" title="头像" center is-link>
<view class="avatar">
<view v-if="!userProfile.avatar" class="img" @click="avatarUpload">
<wd-icon name="fill-camera" custom-class="img-icon" />
</view>
<wd-img
v-if="userProfile.avatar"
round
width="80px"
height="80px"
:src="userProfile.avatar"
mode="aspectFit"
custom-class="profile-img"
@click="avatarUpload"
/>
</view>
</wd-cell>
<wd-cell
title="昵称"
:value="userProfile.nickname"
is-link
@click="handleOpenDialog(DialogType.ACCOUNT)"
/>
<wd-cell
title="性别"
:value="userProfile.gender === 1 ? '男' : userProfile.gender === 2 ? '女' : '未知'"
is-link
@click="handleOpenDialog(DialogType.ACCOUNT)"
/>
<wd-cell title="用户名" :value="userProfile.username" />
<wd-cell title="部门" :value="userProfile.deptName" />
<wd-cell title="角色" :value="userProfile.roleNames" />
<wd-cell title="创建日期" :value="userProfile.createTime" />
</wd-cell-group>
</view>
<view class="profile-card">
<wd-cell-group border>
<wd-cell
title="账户密码"
label="定期修改密码有助于保护账户安全"
value="修改"
is-link
@click="handleOpenDialog(DialogType.PASSWORD)"
/>
<wd-cell
title="绑定手机"
:value="userProfile?.mobile"
is-link
@click="handleOpenDialog(DialogType.MOBILE)"
/>
<wd-cell
title="绑定邮箱"
:value="userProfile?.email ? userProfile.email : '未绑定邮箱'"
is-link
@click="handleOpenDialog(DialogType.EMAIL)"
/>
</wd-cell-group>
</view>
<!--头像裁剪-->
<wd-img-cropper v-model="avatarShow" :img-src="originalSrc" @confirm="handleAvatarConfirm" />
<!--用户信息编辑弹出框-->
<wd-popup v-model="dialog.visible" position="bottom" custom-style="min-height: 200px;">
<wd-form
v-if="dialog.type === DialogType.ACCOUNT"
ref="userProfileFormRef"
:model="userProfileForm"
class="edit-form"
>
<wd-cell-group border>
<wd-input
v-model="userProfileForm.nickname"
label="昵称"
label-width="80px"
placeholder="请输入昵称"
prop="nickname"
:rules="rules.nickname"
/>
<wd-cell title="性别" title-width="80px" center prop="gender" :rules="rules.gender">
<wd-radio-group v-model="userProfileForm.gender" shape="button" class="ef-radio-group">
<wd-radio :value="1"></wd-radio>
<wd-radio :value="2"></wd-radio>
</wd-radio-group>
</wd-cell>
</wd-cell-group>
<view class="footer">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
<wd-form
v-if="dialog.type === DialogType.PASSWORD"
ref="passwordChangeFormRef"
:model="passwordChangeForm"
class="edit-form"
>
<wd-cell-group border>
<wd-input
v-model="passwordChangeForm.oldPassword"
label="原密码"
label-width="80px"
show-password
clearable
placeholder="请输入原密码"
prop="oldPassword"
:rules="rules.oldPassword"
/>
<wd-input
v-model="passwordChangeForm.newPassword"
label="新密码"
label-width="80px"
show-password
clearable
placeholder="请输入新密码"
prop="newPassword"
:rules="rules.newPassword"
/>
<wd-input
v-model="passwordChangeForm.confirmPassword"
label="确认密码"
label-width="80px"
show-password
clearable
placeholder="请确认新密码"
prop="confirmPassword"
:rules="rules.confirmPassword"
/>
</wd-cell-group>
<view class="footer">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
<wd-form
v-if="dialog.type === DialogType.MOBILE"
ref="mobileBindingFormRef"
:model="mobileBindingForm"
class="edit-form"
>
<wd-cell-group border>
<wd-input
v-model="mobileBindingForm.mobile"
label="手机号码"
label-width="80px"
clearable
placeholder="请输入手机号码"
prop="mobile"
:rules="rules.mobile"
/>
<wd-input
v-model="mobileBindingForm.code"
label="验证码"
label-width="80px"
clearable
placeholder="请输入验证码"
prop="code"
:rules="rules.code"
>
<template #suffix>
<wd-button
plain
:disabled="mobileCountdown > 0"
@click="handleSendVerificationCode('MOBILE')"
>
{{ mobileCountdown > 0 ? `${mobileCountdown}s后重新发送` : "发送验证码" }}
</wd-button>
</template>
</wd-input>
</wd-cell-group>
<view class="footer">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
<wd-form
v-if="dialog.type === DialogType.EMAIL"
ref="emailBindingFormRef"
:model="emailBindingForm"
class="edit-form"
>
<wd-cell-group border>
<wd-input
v-model="emailBindingForm.email"
label="邮箱"
label-width="80px"
clearable
placeholder="请输入邮箱"
prop="email"
:rules="rules.email"
/>
<wd-input
v-model="emailBindingForm.code"
label="验证码"
label-width="80px"
clearable
placeholder="请输入验证码"
prop="code"
:rules="rules.code"
>
<template #suffix>
<wd-button
plain
:disabled="emailCountdown > 0"
@click="handleSendVerificationCode('EMAIL')"
>
{{ emailCountdown > 0 ? `${emailCountdown}s后重新发送` : "发送验证码" }}
</wd-button>
</template>
</wd-input>
</wd-cell-group>
<view class="footer">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import UserAPI, {
type UserProfileVO,
UserProfileForm,
PasswordChangeForm,
MobileBindingForm,
EmailBindingForm,
} from "@/api/system/user";
import FileAPI, { type FileInfo } from "@/api/file";
const originalSrc = ref<string>(""); //选取的原图路径
const avatarShow = ref<boolean>(false); //显示头像裁剪
const userProfile = ref<UserProfileVO>(); //用户信息
/** 加载用户信息 */
const loadUserProfile = async () => {
userProfile.value = await UserAPI.getProfile();
};
// 头像选择
function avatarUpload() {
uni.chooseImage({
count: 1,
success: (res) => {
originalSrc.value = res.tempFilePaths[0];
avatarShow.value = true;
},
});
}
// 头像裁剪完成
function handleAvatarConfirm(event) {
const { tempFilePath } = event;
FileAPI.upload(tempFilePath).then((fileInfo: FileInfo) => {
const avatarForm: UserProfileForm = {
avatar: fileInfo.url,
};
// 头像路径保存至后端
UserAPI.updateProfile(avatarForm).then(() => {
uni.showToast({ title: "头像上传成功", icon: "none" });
loadUserProfile();
});
});
}
const validatorConfirmPassword = (value) => {
if (!value) {
return Promise.reject("请确认密码");
} else {
if (value !== passwordChangeForm.newPassword) {
return Promise.reject("两次输入的密码不一致");
} else {
return Promise.resolve();
}
}
};
// 本页面中所有的校验规则
const rules = reactive({
nickname: [{ required: true, message: "请填写昵称" }],
gender: [{ required: true, message: "请选择性别" }],
oldPassword: [{ required: true, message: "请填写原密码" }],
newPassword: [{ required: true, message: "请填写新密码" }],
confirmPassword: [{ required: true, message: "请确认密码", validator: validatorConfirmPassword }],
mobile: [{ required: true, pattern: /^1[3-9]\d{9}$/, message: "请填写正确的手机号码" }],
code: [{ required: true, message: "请填写验证码" }],
email: [
{
required: true,
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
message: "请填写正确的邮箱地址",
},
],
});
enum DialogType {
ACCOUNT = "account",
PASSWORD = "password",
MOBILE = "mobile",
EMAIL = "email",
}
const dialog = reactive({
visible: false,
type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱
});
const userProfileForm = reactive<UserProfileForm>({});
const passwordChangeForm = reactive<PasswordChangeForm>({});
const mobileBindingForm = reactive<MobileBindingForm>({});
const emailBindingForm = reactive<EmailBindingForm>({});
const userProfileFormRef = ref();
const passwordChangeFormRef = ref();
const mobileBindingFormRef = ref();
const emailBindingFormRef = ref();
const mobileCountdown = ref(0);
const mobileTimer = ref<NodeJS.Timeout | null>(null);
const emailCountdown = ref(0);
const emailTimer = ref<NodeJS.Timeout | null>(null);
/**
* 打开弹窗
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
*/
const handleOpenDialog = (type: DialogType) => {
dialog.type = type;
dialog.visible = true;
switch (type) {
case DialogType.ACCOUNT:
// 初始化表单数据
userProfileForm.nickname = userProfile.value?.nickname;
userProfileForm.gender = userProfile.value?.gender;
break;
case DialogType.PASSWORD:
passwordChangeForm.oldPassword = "";
passwordChangeForm.newPassword = "";
passwordChangeForm.confirmPassword = "";
break;
case DialogType.MOBILE:
mobileBindingForm.mobile = "";
mobileBindingForm.code = "";
break;
case DialogType.EMAIL:
emailBindingForm.email = "";
emailBindingForm.code = "";
break;
}
};
/**
* 发送验证码
*
* @param contactType 联系方式类型 MOBILE: 手机号码 EMAIL: 邮箱
*/
const handleSendVerificationCode = async (contactType: string) => {
if (contactType === "MOBILE") {
mobileBindingFormRef.value.validate("mobile").then(({ valid }) => {
if (valid) {
UserAPI.sendVerificationCode(mobileBindingForm.mobile, "MOBILE").then(() => {
uni.showToast({ title: "验证码已发送", icon: "none" });
mobileCountdown.value = 60;
mobileTimer.value = setInterval(() => {
if (mobileCountdown.value > 0) {
mobileCountdown.value -= 1;
} else {
clearInterval(mobileTimer.value!);
}
}, 1000);
});
}
});
} else if (contactType === "EMAIL") {
emailBindingFormRef.value.validate("email").then(({ valid }) => {
if (valid) {
UserAPI.sendVerificationCode(emailBindingForm.email, "EMAIL").then(() => {
uni.showToast({ title: "验证码已发送", icon: "none" });
emailCountdown.value = 60;
emailTimer.value = setInterval(() => {
if (emailCountdown.value > 0) {
emailCountdown.value -= 1;
} else {
clearInterval(emailTimer.value!);
}
}, 1000);
});
}
});
}
};
// 提交表单
function handleSubmit() {
if (dialog.type === DialogType.ACCOUNT) {
userProfileFormRef.value.validate().then(({ valid }) => {
if (valid) {
UserAPI.updateProfile(userProfileForm).then(() => {
uni.showToast({ title: "账号资料修改成功", icon: "none" });
dialog.visible = false;
loadUserProfile();
});
}
});
} else if (dialog.type === DialogType.PASSWORD) {
passwordChangeFormRef.value.validate().then(({ valid }) => {
if (valid) {
UserAPI.changePassword(passwordChangeForm).then(() => {
uni.showToast({ title: "密码修改成功", icon: "none" });
dialog.visible = false;
});
}
});
} else if (dialog.type === DialogType.MOBILE) {
mobileBindingFormRef.value.validate().then(({ valid }) => {
if (valid) {
UserAPI.bindMobile(mobileBindingForm).then(() => {
uni.showToast({ title: "手机号绑定成功", icon: "none" });
dialog.visible = false;
loadUserProfile();
});
}
});
} else if (dialog.type === DialogType.EMAIL) {
emailBindingFormRef.value.validate().then(({ valid }) => {
if (valid) {
UserAPI.bindEmail(emailBindingForm).then(() => {
uni.showToast({ title: "邮箱绑定成功", icon: "none" });
dialog.visible = false;
loadUserProfile();
});
}
});
}
}
onMounted(() => {
// #ifdef H5
document.addEventListener("touchstart", touchstartListener, { passive: false });
document.addEventListener("touchmove", touchmoveListener, { passive: false });
// #endif
loadUserProfile();
});
// 页面销毁前移除事件监听
onBeforeUnmount(() => {
// #ifdef H5
document.removeEventListener("touchstart", touchstartListener);
document.removeEventListener("touchmove", touchmoveListener);
// #endif
});
// 禁用浏览器双指缩放,使头像裁剪时双指缩放能够起作用
function touchstartListener(event) {
if (event.touches.length > 1) {
event.preventDefault();
}
}
// 禁用浏览器下拉刷新,使头像裁剪时能够移动图片
function touchmoveListener(event) {
event.preventDefault();
}
</script>
<style lang="scss" scoped>
.profile-card {
padding: 5px;
margin-bottom: 12px;
line-height: 1.1;
background-color: rgb(255, 255, 255);
border-radius: 16px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.05);
.avatar-cell {
:deep(.wd-cell__body) {
align-items: center;
}
.avatar {
display: flex;
align-items: center;
justify-content: right;
.img {
position: relative;
width: 80px;
height: 80px;
background-color: rgba(0, 0, 0, 0.04);
border-radius: 50%;
.img-icon {
position: absolute;
top: 50%;
left: 50%;
font-size: 60px;
color: #fff;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
}
}
}
}
.edit-form {
padding-top: 20px;
.ef-radio-group {
line-height: 1;
text-align: left;
}
.footer {
padding: 12px;
}
}
</style>