|
|
|
@ -0,0 +1,326 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="change-info-container">
|
|
|
|
|
<h2 class="title">修改个人信息</h2>
|
|
|
|
|
|
|
|
|
|
<!-- 头像区域 -->
|
|
|
|
|
<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"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 文件选择按钮 -->
|
|
|
|
|
<label class="upload-btn">
|
|
|
|
|
选择新头像
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<!-- 个人简介 -->
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label class="label">个人简介</label>
|
|
|
|
|
<textarea
|
|
|
|
|
v-model.trim="formData.bio"
|
|
|
|
|
rows="4"
|
|
|
|
|
class="textarea"
|
|
|
|
|
:class="{ 'input-error': errors.bio }"
|
|
|
|
|
></textarea>
|
|
|
|
|
<p v-if="errors.bio" class="error-tip">{{ errors.bio }}</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提示组件
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
|
|
|
|
// 从状态管理中获取用户ID和登录状态
|
|
|
|
|
const userId = ref(userStore.userInfo.userid);
|
|
|
|
|
const isLoggedIn = userStore.isLoggedIn;
|
|
|
|
|
|
|
|
|
|
// 初始化表单数据
|
|
|
|
|
const formData = ref({
|
|
|
|
|
nickname: '',
|
|
|
|
|
bio: ''
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 头像相关变量
|
|
|
|
|
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 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
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 表单验证(根据接口文档规则)
|
|
|
|
|
const validateForm = () => {
|
|
|
|
|
const newErrors = {};
|
|
|
|
|
|
|
|
|
|
// 昵称验证(接口要求:必填,≤20字符)
|
|
|
|
|
if (!formData.value.nickname) {
|
|
|
|
|
newErrors.nickname = '昵称不能为空';
|
|
|
|
|
} else if (formData.value.nickname.length > 20) {
|
|
|
|
|
newErrors.nickname = '昵称最多20字符';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 简介验证(接口要求:≤100字符)
|
|
|
|
|
if (formData.value.bio.length > 100) {
|
|
|
|
|
newErrors.bio = '简介最多100字符';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errors.value = newErrors;
|
|
|
|
|
return Object.keys(newErrors).length === 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 提交表单(调用接口文档中的 /user/info 接口)
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
if (!validateForm()) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 构建接口要求的 FormData
|
|
|
|
|
const formDataToSubmit = new FormData();
|
|
|
|
|
// 添加头像(可选,有选择时才添加)
|
|
|
|
|
if (avatarFile.value) {
|
|
|
|
|
formDataToSubmit.append('avatar', avatarFile.value);
|
|
|
|
|
}
|
|
|
|
|
// 添加其他字段(接口要求的 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
|
|
|
|
|
}
|
|
|
|
|
ElMessage.success('修改成功!');
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(res.msg || '接口返回异常');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error(`修改失败:${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* 样式与原代码基本一致,仅调整无关字段的布局 */
|
|
|
|
|
.change-info-container {
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
margin: 2rem auto;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
color: #34495e;
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar-section {
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
padding-bottom: 2rem;
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar-preview {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.current-avatar, .new-avatar {
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border: 2px solid #bdc3c7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.new-avatar {
|
|
|
|
|
border-color: #3498db; /* 新头像边框用蓝色区分 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upload-btn {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
background: #3498db;
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
transition: background 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upload-btn:hover {
|
|
|
|
|
background: #2980b9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-input {
|
|
|
|
|
display: none; /* 隐藏原生文件输入框 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-item {
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.label {
|
|
|
|
|
display: block;
|
|
|
|
|
color: #34495e;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input, .textarea {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
border: 1px solid #bdc3c7;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
transition: border-color 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input:focus, .textarea:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: #3498db;
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-error {
|
|
|
|
|
border-color: #e74c3c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error-tip {
|
|
|
|
|
color: #e74c3c;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
margin-top: 0.3rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.textarea {
|
|
|
|
|
resize: vertical;
|
|
|
|
|
min-height: 80px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.submit-btn {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
background: #3498db;
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.submit-btn:hover {
|
|
|
|
|
background: #2980b9;
|
|
|
|
|
}
|
|
|
|
|
</style>
|