实现修改个人信息功能

main
Hacker-00001 2 days ago
parent 0fc3a5e109
commit 5fc904198b

@ -1,36 +1,36 @@
#本地开发环境
lj:
db:
host: localhost
password: 1243969857
redis:
host: localhost
port: 6379
password: 1243969857
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
##本地开发环境
# lj:
# db:
# host: localhost
# password: lzt&264610
# redis:
# host: localhost
# port: 6379
# password: 123456
# rabbitmq:
# host: localhost
# port: 5672
# username: guest
# password: guest
# minio:
# endpoint: http://localhost:9000
# accessKey: minioadmin
# secretKey: minioadmin
#lj:
# db:
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: 192.168.59.129
# port: 6379
# password: Forely123!
# rabbitmq:
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
# minio:
# endpoint: http://192.168.59.129:9000
# accessKey: forely
# secretKey: Forely123!
lj:
db:
host: 192.168.125.128
password: MySQL@5678
redis:
host: 192.168.125.128
port: 6379
password: Redis@9012
rabbitmq:
host: 192.168.125.128
port: 5672
username: rabbit_admin
password: Rabbit@3456
minio:
endpoint: http://192.168.125.128:9000
accessKey: minio_admin
secretKey: Minio@1234

@ -216,7 +216,8 @@ async function login() {
userStore.login({
avatar:require ('@/assets/default-avatar/boy_1.png'),
userName: '珈人一号',
username: loginForm.value.userFlag, // 使
password: loginForm.value.password,
userid:1
});

@ -171,7 +171,7 @@ export const usePostDetailStore = defineStore("postDetail", {
},
// 发送评论或回复
async sendComment(newCommentData) {
if (!content || !this.post?.postId) return;
if (!newCommentData.content || !this.post?.postId) return;
const RequestData = {
id: null,
postId: newCommentData.postId, // 帖子ID
@ -202,7 +202,7 @@ export const usePostDetailStore = defineStore("postDetail", {
repliesLoading: false,
};
// 新增评论后刷新评论列表或插入到对应位置
if (!parentCommentId) {
if (!newCommentData.parentCommentId) {
// 一级评论,插入到最前面
this.comments.unshift(commentObj);
this.post.commentCount = (this.post.commentCount || 0) + 1; // 更新帖子评论数

@ -7,6 +7,13 @@ export const useUserStore = defineStore('user', {
avatar: '', // 用户头像 URL
username: '', // 用户名
userid: 0, // 用户 ID
moto:'',// 用户简介
phone: '', // 用户手机号
email: '', // 用户邮箱
studentID: '', // 学号
college: '', // 学院
gender: 0, // 性别 0: 未知, 1: 男, 2: 女
password: ''
},
}),
actions: {

@ -26,7 +26,7 @@ request.interceptors.request.use(
const token = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
if (token) {
config.headers['Authorization'] = token;
config.headers['Authorization'] = `Bearer ${token}`;
config.headers['X-Refresh-Token'] = refreshToken;
}
}

@ -6,137 +6,179 @@
<div class="avatar-section">
<p class="section-title">头像</p>
<div class="avatar-preview">
<!-- 当前头像从接口获取初始值 -->
<img
:src="currentAvatar"
alt="当前头像"
class="current-avatar"
>
<!-- 新头像预览选择文件后显示 -->
<img
v-if="previewAvatar"
:src="previewAvatar"
alt="新头像预览"
class="new-avatar"
>
<img :src="currentAvatar" alt="当前头像" class="current-avatar">
<img v-if="previewAvatar" :src="previewAvatar" alt="新头像预览" class="new-avatar">
</div>
<!-- 文件选择按钮 -->
<label class="upload-btn">
选择新头像
<input
type="file"
accept="image/*"
@change="handleAvatarChange"
class="file-input"
>
<input type="file" accept="image/*" @change="handleAvatarChange" class="file-input">
</label>
<!-- 头像错误提示 -->
<p v-if="errors.avatar" class="error-tip">{{ errors.avatar }}</p>
</div>
<!-- 表单 -->
<form @submit.prevent="handleSubmit" class="form">
<!-- 昵称 -->
<div class="form-item">
<label class="label">昵称</label>
<input
type="text"
v-model.trim="formData.nickname"
class="input"
:class="{ 'input-error': errors.nickname }"
>
<p v-if="errors.nickname" class="error-tip">{{ errors.nickname }}</p>
<div class="form-row">
<!-- 用户名 -->
<div class="form-item half">
<label class="label">用户名</label>
<input
type="text"
v-model.trim="formData.username"
class="input"
:class="{ 'input-error': errors.username }"
maxlength="20"
placeholder="请输入用户名"
>
<p v-if="errors.username" class="error-tip">{{ errors.username }}</p>
</div>
<!-- 手机号 -->
<div class="form-item half">
<label class="label">手机号</label>
<input
type="text"
v-model.trim="formData.phone"
class="input"
:class="{ 'input-error': errors.phone }"
maxlength="11"
placeholder="请输入11位手机号"
>
<p v-if="errors.phone" class="error-tip">{{ errors.phone }}</p>
</div>
</div>
<div class="form-row">
<!-- 邮箱 -->
<div class="form-item half">
<label class="label">邮箱</label>
<input
type="email"
v-model.trim="formData.email"
class="input"
:class="{ 'input-error': errors.email }"
placeholder="请输入邮箱"
>
<p v-if="errors.email" class="error-tip">{{ errors.email }}</p>
</div>
<!-- 学号 -->
<div class="form-item half">
<label class="label">学号</label>
<input
type="text"
v-model.trim="formData.studentId"
class="input"
:class="{ 'input-error': errors.studentId }"
maxlength="20"
placeholder="请输入学号"
>
<p v-if="errors.studentId" class="error-tip">{{ errors.studentId }}</p>
</div>
</div>
<div class="form-row">
<!-- 性别 -->
<div class="form-item half">
<label class="label">性别</label>
<select v-model="formData.gender" class="input" :class="{ 'input-error': errors.gender }">
<option value="0">未知</option>
<option value="1"></option>
<option value="2"></option>
</select>
<p v-if="errors.gender" class="error-tip">{{ errors.gender }}</p>
</div>
<!-- 学院 -->
<div class="form-item half">
<label class="label">学院</label>
<input
type="text"
v-model.trim="formData.college"
class="input"
:class="{ 'input-error': errors.college }"
maxlength="30"
placeholder="请输入学院"
>
<p v-if="errors.college" class="error-tip">{{ errors.college }}</p>
</div>
</div>
<!-- 个人简介 -->
<div class="form-item">
<label class="label">个人简介</label>
<textarea
v-model.trim="formData.bio"
v-model.trim="formData.moto"
rows="4"
class="textarea"
:class="{ 'input-error': errors.bio }"
:class="{ 'input-error': errors.moto }"
maxlength="100"
placeholder="请输入个人简介最多100字"
></textarea>
<p v-if="errors.bio" class="error-tip">{{ errors.bio }}</p>
<p v-if="errors.moto" class="error-tip">{{ errors.moto }}</p>
</div>
<!-- 密码 -->
<div class="form-item">
<label class="label">新密码</label>
<input
type="password"
v-model.trim="formData.password"
class="input"
:class="{ 'input-error': errors.password }"
placeholder="如需修改请输入新密码"
>
<p v-if="errors.password" class="error-tip">{{ errors.password }}</p>
</div>
<!-- 提交按钮 -->
<button type="submit" class="submit-btn">保存修改</button>
</form>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user.js'; // Pinia
import request from '@/utils/request'; //
import { ElMessage } from 'element-plus'; // 使Element Plus
import { ref, computed } from 'vue';
import { useUserStore } from '@/stores/user';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';
import { useRouter } from 'vue-router'
const router = useRouter();
const userStore = useUserStore();
// ID
const userId = ref(userStore.userInfo.userid);
const isLoggedIn = userStore.isLoggedIn;
const userInfo = computed(() => userStore.userInfo);
//
const formData = ref({
nickname: '',
bio: ''
username: userInfo.value.username || '',
phone: userInfo.value.phone || '',
email: userInfo.value.email || '',
studentId: userInfo.value.studentId || '',
gender: userInfo.value.gender !== undefined ? String(userInfo.value.gender) : '0',
college: userInfo.value.college || '',
moto: userInfo.value.moto || '',
password: userInfo.value.password || ''
});
//
const avatarFile = ref(null); //
const previewAvatar = ref(''); // URL
const currentAvatar = ref(''); //
const errors = ref({}); //
//
onMounted(async () => {
if (!isLoggedIn) {
ElMessage.error('请先登录');
router.push({ name: 'Login' });
return;
}
try {
// 使request
const res = await request.get(`/user/info/${userId.value}`);
if (res.code === 200) {
formData.value.nickname = res.data.nickname;
formData.value.bio = res.data.bio;
currentAvatar.value = res.data.avatar;
} else {
throw new Error(res.msg || '接口返回异常');
}
} catch (error) {
ElMessage.error(`获取用户信息失败:${error.message}`);
}
});
const avatarFile = ref(null);
const previewAvatar = ref('');
const currentAvatar = ref(userInfo.value.avatar);
const errors = ref({});
const router=useRouter();
//
const handleAvatarChange = (e) => {
const file = e.target.files[0];
if (!file) return;
//
if (!file.type.startsWith('image/')) {
errors.value.avatar = '请选择图片文件jpg/png等';
return;
}
if (file.size > 5 * 1024 * 1024) { // 5MB
if (file.size > 5 * 1024 * 1024) {
errors.value.avatar = '图片大小不能超过5MB';
return;
}
//
errors.value.avatar = null;
// URL
const reader = new FileReader();
reader.onload = (e) => previewAvatar.value = e.target.result;
reader.readAsDataURL(file);
//
avatarFile.value = file;
};
@ -145,51 +187,111 @@ const validateForm = () => {
const newErrors = {};
// 20
if (!formData.value.nickname) {
newErrors.nickname = '昵称不能为空';
} else if (formData.value.nickname.length > 20) {
newErrors.nickname = '昵称最多20字符';
if (!formData.value.username) {
newErrors.username = '用户名不能为空';
} else if (formData.value.username.length > 20) {
newErrors.username = '用户名最多20字符';
}
//
if (!formData.value.phone) {
newErrors.phone = '手机号不能为空';
} else if (!/^\d{11}$/.test(formData.value.phone)) {
newErrors.phone = '手机号格式不正确';
}
//
if (!formData.value.email) {
newErrors.email = '邮箱不能为空';
} else if (!/^[\w.-]+@[\w.-]+\.\w+$/.test(formData.value.email)) {
newErrors.email = '邮箱格式不正确';
}
// 100
if (formData.value.bio.length > 100) {
newErrors.bio = '简介最多100字符';
//
if (!formData.value.studentId) {
newErrors.studentId = '学号不能为空';
} else if (formData.value.studentId.length > 20) {
newErrors.studentId = '学号最多20字符';
}
//
if (!['0', '1', '2'].includes(formData.value.gender)) {
newErrors.gender = '请选择性别';
}
//
if (!formData.value.college) {
newErrors.college = '学院不能为空';
} else if (formData.value.college.length > 30) {
newErrors.college = '学院最多30字符';
}
//
if (formData.value.moto && formData.value.moto.length > 100) {
newErrors.moto = '简介最多100字符';
}
//
if (formData.value.password && formData.value.password.length < 6) {
newErrors.password = '密码长度不能少于6位';
}
errors.value = newErrors;
return Object.keys(newErrors).length === 0;
};
// /user/info
//
const handleSubmit = async () => {
if (!validateForm()) return;
try {
// FormData
const formDataToSubmit = new FormData();
//
let avatarUrl = userInfo.value.avatar;
//
if (avatarFile.value) {
formDataToSubmit.append('avatar', avatarFile.value);
const formDataObj = new FormData();
formDataObj.append('file', avatarFile.value);
const uploadRes = await request.post('/user/info/avatar', formDataObj, {
headers: { 'Content-Type': 'multipart/form-data' }
});
if (uploadRes.code !== 200) {
throw new Error(uploadRes.msg || '头像上传失败');
}
avatarUrl = uploadRes.data; // URL
currentAvatar.value = avatarUrl;
userInfo.value.avatar = avatarUrl; //
}
//
const updateInfoData = {
username: formData.value.username,
phone: formData.value.phone,
email: formData.value.email,
studentId: formData.value.studentId,
avatar: userInfo.value.avatar,
gender: Number(formData.value.gender),
college: formData.value.college,
};
const updateInfoRes = await request.post('/user/info/update', updateInfoData);
if (updateInfoRes.code !== 200) {
throw new Error(updateInfoRes.msg || '修改用户信息失败');
}
// UserUpdateDTO
formDataToSubmit.append('nickname', formData.value.nickname);
formDataToSubmit.append('bio', formData.value.bio);
// PUT /user/info 使request
const res = await request.put(`/user/info/${userId.value}`, formDataToSubmit, {
headers: { 'Content-Type': 'multipart/form-data' } //
});
// code=200
if (res.code === 200) {
// URL
if (avatarFile.value) {
currentAvatar.value = res.data.avatar; // URL
Object.assign(userInfo.value, updateInfoData);
if (formData.value.password) {
const passwordRes = await request.post('/user/info/password', null, {
params: { password: formData.value.password }
});
if (passwordRes.code !== 200) {
throw new Error(passwordRes.msg || '修改密码失败');
}
ElMessage.success('修改成功!');
} else {
throw new Error(res.msg || '接口返回异常');
}
ElMessage.success('修改成功!');
router.push('/');
} catch (error) {
ElMessage.error(`修改失败:${error.message}`);
}
@ -197,21 +299,22 @@ const handleSubmit = async () => {
</script>
<style scoped>
/* 样式与原代码基本一致,仅调整无关字段的布局 */
.change-info-container {
max-width: 600px;
max-width: 700px;
margin: 2rem auto;
padding: 2rem;
padding: 2.5rem 2rem 2rem 2rem;
background: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: 0 4px 16px rgba(52, 152, 219, 0.08);
}
.title {
color: #2c3e50;
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 2rem;
font-size: 1.7rem;
font-weight: 700;
margin-bottom: 2.2rem;
letter-spacing: 1px;
text-align: center;
}
.section-title {
@ -240,56 +343,76 @@ const handleSubmit = async () => {
border-radius: 50%;
object-fit: cover;
border: 2px solid #bdc3c7;
background: #fff;
}
.new-avatar {
border-color: #3498db; /* 新头像边框用蓝色区分 */
border-color: #3498db;
}
.upload-btn {
display: inline-block;
padding: 0.5rem 1rem;
background: #3498db;
padding: 0.5rem 1.2rem;
background: linear-gradient(90deg, #3498db 60%, #6dd5fa 100%);
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-size: 0.95rem;
font-weight: 500;
transition: background 0.3s ease;
margin-top: 0.5rem;
}
.upload-btn:hover {
background: #2980b9;
background: linear-gradient(90deg, #2980b9 60%, #3498db 100%);
}
.file-input {
display: none; /* 隐藏原生文件输入框 */
display: none;
}
.form {
margin-top: 1.5rem;
}
.form-row {
display: flex;
gap: 2rem;
margin-bottom: 1.5rem;
}
.form-item {
margin-bottom: 1.5rem;
flex: 1;
min-width: 0;
}
.form-item.half {
width: 50%;
}
.label {
display: block;
color: #34495e;
font-size: 0.9rem;
font-size: 0.95rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.input, .textarea {
.input, .textarea, select.input {
width: 100%;
padding: 0.75rem;
border: 1px solid #bdc3c7;
border-radius: 4px;
font-size: 0.9rem;
font-size: 0.95rem;
transition: border-color 0.3s ease;
background: #fff;
}
.input:focus, .textarea:focus {
.input:focus, .textarea:focus, select.input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.08);
}
.input-error {
@ -309,18 +432,20 @@ const handleSubmit = async () => {
.submit-btn {
width: 100%;
padding: 0.75rem;
background: #3498db;
padding: 0.85rem;
background: linear-gradient(90deg, #3498db 60%, #6dd5fa 100%);
color: white;
border: none;
border-radius: 4px;
font-size: 0.9rem;
font-weight: 500;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.3s ease;
margin-top: 0.5rem;
letter-spacing: 1px;
}
.submit-btn:hover {
background: #2980b9;
background: linear-gradient(90deg, #2980b9 60%, #3498db 100%);
}
</style>

@ -6,10 +6,23 @@
<img :src="userInfo.avatar" alt="用户头像" />
</div>
<div class="user-details">
<h2 class="username">{{ userInfo.userName }}</h2>
<p class="user-status">ONLINE</p>
<p class="user-motto">{{ userInfo.motto || '暂无个性签名' }}</p>
</div>
<h2 class="username">{{ userInfo.username }}</h2>
<p class="user-status">ONLINE</p>
<div class="user-info-list">
<div class="user-info-row">
<span class="user-info-label">学院</span>
<span class="user-info-value">{{ userInfo.college || '未填写' }}</span>
</div>
<div class="user-info-row">
<span class="user-info-label">性别</span>
<span class="user-info-value">
<span v-if="userInfo.gender === 1"></span>
<span v-else-if="userInfo.gender === 2"></span>
<span v-else></span>
</span>
</div>
</div>
</div>
</div>
<div class="user-posts">
@ -189,6 +202,28 @@ const goToPostDetail = (postId) => {
.user-details {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.user-info-list {
margin-top: 8px;
width: 100%;
}
.user-info-row {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.user-info-label {
font-weight: bold;
color: #333;
min-width: 48px;
}
.user-info-value {
color: #444;
}
.username {

Loading…
Cancel
Save