<template>
|
<view class="profile-container">
|
<!-- 头像区域 -->
|
<view class="avatar-section" @tap="changeAvatar">
|
<view class="avatar-wrapper">
|
<image :src="avatarUrl" mode="aspectFill" class="avatar" />
|
<view class="edit-icon">
|
<text class="plus">+</text>
|
</view>
|
</view>
|
<text class="tip">点击更换头像</text>
|
</view>
|
|
<!-- 个人信息 -->
|
<view class="info-card">
|
<view class="card-header">
|
<text class="title">个人信息</text>
|
</view>
|
<view class="info-list">
|
<view class="info-item">
|
<text class="label">用户名称</text>
|
<text class="value">{{ user.userName }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">手机号码</text>
|
<text class="value">{{ user.phonenumber }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">用户邮箱</text>
|
<text class="value">{{ user.email }}</text>
|
</view>
|
<view class="info-item" v-if="user.dept">
|
<text class="label">所属部门</text>
|
<text class="value">{{ user.dept.deptName }} / {{ postGroup }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">所属角色</text>
|
<text class="value">{{ roleGroup }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">创建日期</text>
|
<text class="value">{{ user.createTime }}</text>
|
</view>
|
</view>
|
</view>
|
|
<!-- 基本资料 -->
|
<view class="info-card">
|
<view class="card-header">
|
<text class="title">基本资料</text>
|
</view>
|
<view class="form-list">
|
<view class="form-item">
|
<text class="label required">用户昵称</text>
|
<input
|
v-model="form.nickName"
|
placeholder="请输入用户昵称"
|
maxlength="30"
|
/>
|
</view>
|
<view class="form-item">
|
<text class="label required">手机号码</text>
|
<input
|
v-model="form.phonenumber"
|
type="number"
|
placeholder="请输入手机号码"
|
maxlength="11"
|
/>
|
</view>
|
<view class="form-item">
|
<text class="label required">邮箱</text>
|
<input
|
v-model="form.email"
|
placeholder="请输入邮箱"
|
maxlength="50"
|
/>
|
</view>
|
<view class="form-item">
|
<text class="label">性别</text>
|
<view class="gender-types">
|
<view
|
class="gender-item"
|
:class="{ active: form.sex === '0' }"
|
@tap="form.sex = '0'"
|
>
|
<text class="radio"></text>
|
<text>男</text>
|
</view>
|
<view
|
class="gender-item"
|
:class="{ active: form.sex === '1' }"
|
@tap="form.sex = '1'"
|
>
|
<text class="radio"></text>
|
<text>女</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
<button class="save-btn" @tap="saveProfile">保存</button>
|
</view>
|
|
<!-- 修改密码 -->
|
<view class="info-card">
|
<view class="card-header">
|
<text class="title">修改密码</text>
|
</view>
|
<view class="form-list">
|
<view class="form-item">
|
<text class="label required">旧密码</text>
|
<input
|
v-model="pwdForm.oldPassword"
|
:password="true"
|
placeholder="请输入旧密码"
|
/>
|
</view>
|
<view class="form-item">
|
<text class="label required">新密码</text>
|
<input
|
v-model="pwdForm.newPassword"
|
:password="true"
|
placeholder="请输入新密码(8-20位)"
|
/>
|
</view>
|
<view class="form-item">
|
<text class="label required">确认密码</text>
|
<input
|
v-model="pwdForm.confirmPassword"
|
:password="true"
|
placeholder="请确认新密码"
|
/>
|
</view>
|
</view>
|
<button class="save-btn" @tap="savePassword">保存</button>
|
</view>
|
|
<!-- 退出登录 -->
|
<button class="logout-btn" @tap="logout">退出登录</button>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, computed } from "vue";
|
import { onLoad } from "@dcloudio/uni-app";
|
import { useUserStore } from "@/stores/user";
|
|
const userStore = useUserStore();
|
|
// 用户信息(来自 /system/user/profile)
|
const user = ref({});
|
const roleGroup = ref("");
|
const postGroup = ref("");
|
|
// 基本资料表单
|
const form = ref({
|
nickName: "",
|
phonenumber: "",
|
email: "",
|
sex: "0",
|
});
|
|
// 修改密码表单
|
const pwdForm = ref({
|
oldPassword: "",
|
newPassword: "",
|
confirmPassword: "",
|
});
|
|
// 头像地址(相对路径拼接服务器地址)
|
const avatarUrl = computed(() => {
|
const avatar = user.value.avatar;
|
if (!avatar) return "/static/avatar/default.png";
|
if (/^https?:\/\//.test(avatar)) return avatar;
|
// #ifdef H5
|
return `/api${avatar}`;
|
// #endif
|
// #ifndef H5
|
return `${userStore.baseUrlHt}${avatar}`;
|
// #endif
|
});
|
|
onLoad(() => {
|
loadProfile();
|
});
|
|
// 加载个人信息
|
const loadProfile = async () => {
|
// 先用 store 中的用户信息兜底显示
|
if (userStore.userInfo) {
|
user.value = { ...userStore.userInfo };
|
fillForm();
|
}
|
try {
|
const res = await uni.$uapi.get("/system/user/profile");
|
user.value = res.data || {};
|
roleGroup.value = res.roleGroup || "";
|
postGroup.value = res.postGroup || "";
|
fillForm();
|
} catch (e) {
|
// 拦截器已统一提示
|
}
|
};
|
|
// 用用户信息回填基本资料表单
|
const fillForm = () => {
|
form.value.nickName = user.value.nickName || "";
|
form.value.phonenumber = user.value.phonenumber || "";
|
form.value.email = user.value.email || "";
|
form.value.sex = String(user.value.sex ?? "0");
|
};
|
|
// 更换头像
|
const changeAvatar = () => {
|
uni.chooseImage({
|
count: 1,
|
sizeType: ["compressed"],
|
sourceType: ["album", "camera"],
|
success: (res) => {
|
uploadAvatar(res.tempFilePaths[0]);
|
},
|
});
|
};
|
|
// 上传头像
|
const uploadAvatar = (filePath) => {
|
uni.showLoading({ title: "上传中...", mask: true });
|
const token = uni.getStorageSync("token");
|
// #ifdef H5
|
const uploadUrl = "/api/system/user/profile/avatar";
|
// #endif
|
// #ifndef H5
|
const uploadUrl = `${userStore.baseUrlHt}/system/user/profile/avatar`;
|
// #endif
|
|
uni.uploadFile({
|
url: uploadUrl,
|
filePath,
|
name: "avatarfile",
|
header: { Authorization: `Bearer ${token}` },
|
success: (res) => {
|
uni.hideLoading();
|
if (res.statusCode === 200) {
|
try {
|
const data = JSON.parse(res.data);
|
if (data.code === 200) {
|
user.value.avatar = data.imgUrl;
|
userStore.refreshUserInfo();
|
uni.showToast({ title: "头像修改成功", icon: "success" });
|
} else {
|
uni.showToast({ title: data.msg || "上传失败", icon: "none" });
|
}
|
} catch (e) {
|
uni.showToast({ title: "响应解析失败", icon: "none" });
|
}
|
} else {
|
uni.showToast({
|
title: `上传失败,状态码: ${res.statusCode}`,
|
icon: "none",
|
});
|
}
|
},
|
fail: () => {
|
uni.hideLoading();
|
uni.showToast({ title: "上传失败", icon: "none" });
|
},
|
});
|
};
|
|
// 保存基本资料
|
const saveProfile = async () => {
|
if (!form.value.nickName) {
|
uni.showToast({ title: "用户昵称不能为空", icon: "none" });
|
return;
|
}
|
if (!form.value.phonenumber) {
|
uni.showToast({ title: "手机号码不能为空", icon: "none" });
|
return;
|
}
|
if (!/^1[3|4|5|6|7|8|9][0-9]\d{8}$/.test(form.value.phonenumber)) {
|
uni.showToast({ title: "请输入正确的手机号码", icon: "none" });
|
return;
|
}
|
if (!form.value.email) {
|
uni.showToast({ title: "邮箱地址不能为空", icon: "none" });
|
return;
|
}
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.email)) {
|
uni.showToast({ title: "请输入正确的邮箱地址", icon: "none" });
|
return;
|
}
|
|
uni.showLoading({ title: "保存中...", mask: true });
|
try {
|
await uni.$uapi.post("/system/user/profile/updateProfile", {
|
nickName: form.value.nickName,
|
phonenumber: form.value.phonenumber,
|
email: form.value.email,
|
sex: form.value.sex,
|
});
|
uni.hideLoading();
|
uni.showToast({ title: "修改成功", icon: "success" });
|
|
// 同步本地显示与 store
|
user.value.nickName = form.value.nickName;
|
user.value.phonenumber = form.value.phonenumber;
|
user.value.email = form.value.email;
|
user.value.sex = form.value.sex;
|
userStore.refreshUserInfo();
|
} catch (e) {
|
uni.hideLoading();
|
}
|
};
|
|
// 密码复杂度校验:需同时包含大写、小写、数字和特殊字符其中三项
|
const checkPwdLegal = (pwd) => {
|
if (!pwd || pwd.length < 8 || pwd.length > 20) return false;
|
let count = 0;
|
if (/[a-z]/.test(pwd)) count++;
|
if (/[A-Z]/.test(pwd)) count++;
|
if (/[0-9]/.test(pwd)) count++;
|
if (/[\W_]/.test(pwd)) count++;
|
return count >= 3;
|
};
|
|
// 保存密码
|
const savePassword = async () => {
|
const { oldPassword, newPassword, confirmPassword } = pwdForm.value;
|
if (!oldPassword) {
|
uni.showToast({ title: "旧密码不能为空", icon: "none" });
|
return;
|
}
|
if (!newPassword) {
|
uni.showToast({ title: "新密码不能为空", icon: "none" });
|
return;
|
}
|
if (newPassword.length < 8 || newPassword.length > 20) {
|
uni.showToast({ title: "密码长度在 8 到 20 个字符", icon: "none" });
|
return;
|
}
|
if (!checkPwdLegal(newPassword)) {
|
uni.showToast({
|
title: "密码必须同时包含大写、小写、数字和特殊字符其中三项",
|
icon: "none",
|
});
|
return;
|
}
|
if (!confirmPassword) {
|
uni.showToast({ title: "确认密码不能为空", icon: "none" });
|
return;
|
}
|
if (confirmPassword !== newPassword) {
|
uni.showToast({ title: "两次输入的密码不一致", icon: "none" });
|
return;
|
}
|
|
uni.showLoading({ title: "保存中...", mask: true });
|
try {
|
await uni.$uapi.post("/system/user/profile/updatePwd", {
|
oldPassword,
|
newPassword,
|
});
|
uni.hideLoading();
|
uni.showToast({ title: "密码修改成功", icon: "success" });
|
pwdForm.value = { oldPassword: "", newPassword: "", confirmPassword: "" };
|
} catch (e) {
|
uni.hideLoading();
|
}
|
};
|
|
// 退出登录
|
const logout = () => {
|
uni.showModal({
|
title: "提示",
|
content: "确定要退出登录吗?",
|
success: (res) => {
|
if (res.confirm) {
|
uni.clearStorageSync();
|
uni.reLaunch({
|
url: "/pages/login/Login",
|
});
|
}
|
},
|
});
|
};
|
</script>
|
|
<style lang="scss">
|
.profile-container {
|
min-height: 100vh;
|
background: $bg-color;
|
padding-bottom: 40rpx;
|
|
.avatar-section {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
padding: 60rpx 0 40rpx;
|
background: $primary-gradient;
|
|
.avatar-wrapper {
|
position: relative;
|
margin-bottom: 16rpx;
|
|
.avatar {
|
width: 160rpx;
|
height: 160rpx;
|
border-radius: 50%;
|
border: 4rpx solid rgba(255, 255, 255, 0.4);
|
}
|
|
.edit-icon {
|
position: absolute;
|
right: 0;
|
bottom: 0;
|
width: 48rpx;
|
height: 48rpx;
|
background: #fff;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
.plus {
|
font-size: 40rpx;
|
font-weight: bold;
|
color: $primary-color;
|
line-height: 1;
|
}
|
}
|
}
|
|
.tip {
|
font-size: 24rpx;
|
color: rgba(255, 255, 255, 0.9);
|
}
|
}
|
|
.info-card {
|
background: #fff;
|
margin: 20rpx 20rpx 0;
|
border-radius: $radius-lg;
|
overflow: hidden;
|
box-shadow: $shadow-sm;
|
|
.card-header {
|
padding: 30rpx;
|
border-bottom: 1rpx solid #eee;
|
|
.title {
|
font-size: 32rpx;
|
color: $text-primary;
|
font-weight: bold;
|
}
|
}
|
|
.info-list {
|
.info-item {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 28rpx 30rpx;
|
border-bottom: 1rpx solid #f5f5f5;
|
|
&:last-child {
|
border-bottom: none;
|
}
|
|
.label {
|
font-size: 28rpx;
|
color: $text-regular;
|
}
|
|
.value {
|
font-size: 28rpx;
|
color: $text-primary;
|
text-align: right;
|
flex: 1;
|
margin-left: 20rpx;
|
}
|
}
|
}
|
|
.form-list {
|
padding: 0 30rpx;
|
|
.form-item {
|
margin-top: 30rpx;
|
|
&:last-child {
|
margin-bottom: 30rpx;
|
}
|
|
.label {
|
font-size: 28rpx;
|
color: $text-primary;
|
margin-bottom: 16rpx;
|
display: block;
|
|
&.required::before {
|
content: "*";
|
color: $danger;
|
margin-right: 4rpx;
|
}
|
}
|
|
input {
|
width: 100%;
|
height: 88rpx;
|
background: $bg-color;
|
border-radius: $radius-lg;
|
padding: 0 30rpx;
|
font-size: 28rpx;
|
color: $text-primary;
|
box-sizing: border-box;
|
}
|
|
.gender-types {
|
display: flex;
|
gap: 30rpx;
|
|
.gender-item {
|
flex: 1;
|
height: 88rpx;
|
background: $bg-color;
|
border-radius: $radius-lg;
|
display: flex;
|
align-items: center;
|
padding: 0 30rpx;
|
|
.radio {
|
width: 32rpx;
|
height: 32rpx;
|
border: 2rpx solid $text-secondary;
|
border-radius: 50%;
|
margin-right: 16rpx;
|
position: relative;
|
|
&::after {
|
content: "";
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
transform: translate(-50%, -50%);
|
width: 16rpx;
|
height: 16rpx;
|
background: $primary-color;
|
border-radius: 50%;
|
opacity: 0;
|
transition: all 0.3s;
|
}
|
}
|
|
text {
|
font-size: 28rpx;
|
color: $text-regular;
|
}
|
|
&.active {
|
background: $primary-light;
|
|
.radio {
|
border-color: $primary-color;
|
|
&::after {
|
opacity: 1;
|
}
|
}
|
|
text {
|
color: $primary-color;
|
}
|
}
|
}
|
}
|
}
|
}
|
|
.save-btn {
|
width: calc(100% - 60rpx);
|
height: 80rpx;
|
line-height: 80rpx;
|
margin: 0 30rpx 30rpx;
|
font-size: 30rpx;
|
color: #fff;
|
background: $primary-gradient;
|
border-radius: $radius-xl;
|
box-shadow: 0 4rpx 12rpx rgba($primary-color, 0.3);
|
|
&:active {
|
opacity: 0.85;
|
}
|
}
|
}
|
|
.logout-btn {
|
width: calc(100% - 60rpx);
|
height: 88rpx;
|
line-height: 88rpx;
|
margin: 40rpx auto 0;
|
font-size: 32rpx;
|
color: $danger;
|
background: #fff;
|
border-radius: $radius-lg;
|
|
&:active {
|
opacity: 0.8;
|
}
|
}
|
}
|
</style>
|