You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
670 lines
16 KiB
670 lines
16 KiB
<template>
|
|
<div class="app-container">
|
|
<!-- 引入侧边栏组件 -->
|
|
<Menu
|
|
@menu-click="handleSidebarClick"
|
|
/>
|
|
|
|
<!-- 主内容区域 -->
|
|
<div class="main-content">
|
|
<div class="profile-container">
|
|
<!-- 页面标题 -->
|
|
<div class="page-header">
|
|
<h1 class="page-title">个人主页</h1>
|
|
<p class="page-subtitle">管理您的个人信息和账户设置</p>
|
|
</div>
|
|
|
|
<!-- 个人信息卡片 -->
|
|
<div class="profile-card">
|
|
<!-- 头像区域 -->
|
|
<div class="avatar-section">
|
|
<div class="avatar-container">
|
|
<img
|
|
:src="userProfile.avatar"
|
|
alt="用户头像"
|
|
class="avatar-image"
|
|
@error="handleAvatarError"
|
|
>
|
|
<div class="avatar-overlay" @click="triggerFileInput">
|
|
<span class="camera-icon">📷</span>
|
|
<span class="overlay-text">更换头像</span>
|
|
</div>
|
|
<input
|
|
type="file"
|
|
ref="fileInput"
|
|
@change="handleAvatarChange"
|
|
accept="image/*"
|
|
class="hidden-input"
|
|
>
|
|
</div>
|
|
<div class="username-display">{{ userProfile.username }}</div>
|
|
<div class="avatar-status">
|
|
<p v-if="avatarUploading" class="uploading-text">上传中...</p>
|
|
<p v-if="avatarUploadSuccess" class="success-text">头像更新成功</p>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 用户信息表单 -->
|
|
<div class="info-section">
|
|
<h2 class="section-title">修改密码</h2>
|
|
<form class="password-form" @submit.prevent="changePassword">
|
|
<div class="form-group">
|
|
<label for="currentPassword" class="form-label">当前密码</label>
|
|
<input
|
|
type="password"
|
|
id="currentPassword"
|
|
v-model="passwordForm.currentPassword"
|
|
class="form-input"
|
|
placeholder="请输入当前密码"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="newPassword" class="form-label">新密码</label>
|
|
<input
|
|
type="password"
|
|
id="newPassword"
|
|
v-model="passwordForm.newPassword"
|
|
class="form-input"
|
|
placeholder="请输入新密码"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="confirmPassword" class="form-label">确认新密码</label>
|
|
<input
|
|
type="password"
|
|
id="confirmPassword"
|
|
v-model="passwordForm.confirmPassword"
|
|
class="form-input"
|
|
placeholder="请再次输入新密码"
|
|
required
|
|
>
|
|
<p v-if="passwordMismatch" class="error-text">两次输入的密码不一致</p>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="save-button" :disabled="passwordSaving || passwordMismatch">
|
|
<span v-if="!passwordSaving">修改密码</span>
|
|
<span v-else>修改中...</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<!-- 操作反馈 -->
|
|
<div v-if="successMessage" class="success-message">
|
|
{{ successMessage }}
|
|
</div>
|
|
<div v-if="errorMessage" class="error-message">
|
|
{{ errorMessage }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import Menu from '../components/Menu.vue';
|
|
import { getUserProfile, updateAvatar, updatePassword } from '../api/profile';
|
|
|
|
// 处理侧边栏菜单点击
|
|
const handleSidebarClick = (menuItem) => {
|
|
console.log('点击了菜单项:', menuItem);
|
|
};
|
|
|
|
// 用户信息数据
|
|
const userProfile = ref({
|
|
username: '用户',
|
|
avatar: '/resource/avatar/4.png'
|
|
});
|
|
|
|
// 密码表单数据
|
|
const passwordForm = ref({
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: ''
|
|
});
|
|
|
|
// 状态变量
|
|
const fileInput = ref(null);
|
|
const avatarUploading = ref(false);
|
|
const avatarUploadSuccess = ref(false);
|
|
const profileSaving = ref(false);
|
|
const passwordSaving = ref(false);
|
|
const successMessage = ref('');
|
|
const errorMessage = ref('');
|
|
|
|
// 计算属性
|
|
const passwordMismatch = computed(() => {
|
|
return passwordForm.value.newPassword &&
|
|
passwordForm.value.confirmPassword &&
|
|
passwordForm.value.newPassword !== passwordForm.value.confirmPassword;
|
|
});
|
|
|
|
|
|
// 触发文件选择
|
|
const triggerFileInput = () => {
|
|
fileInput.value.click();
|
|
};
|
|
|
|
// 处理头像加载错误
|
|
const handleAvatarError = (event) => {
|
|
console.error('头像加载失败:', userProfile.value.avatar);
|
|
// 设置默认头像
|
|
userProfile.value.avatar = '/resource/avatar/4.png';
|
|
};
|
|
|
|
// 处理头像更改
|
|
const handleAvatarChange = async (event) => {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
// 检查文件类型
|
|
if (!file.type.startsWith('image/')) {
|
|
errorMessage.value = '请选择图片文件';
|
|
return;
|
|
}
|
|
|
|
// 检查文件大小 (限制为2MB)
|
|
if (file.size > 2 * 1024 * 1024) {
|
|
errorMessage.value = '图片大小不能超过2MB';
|
|
return;
|
|
}
|
|
|
|
// 开始上传
|
|
avatarUploading.value = true;
|
|
avatarUploadSuccess.value = false;
|
|
errorMessage.value = '';
|
|
|
|
try {
|
|
// 从localStorage获取token
|
|
const token = localStorage.getItem('token');
|
|
|
|
if (!token) {
|
|
errorMessage.value = '用户未登录,请先登录';
|
|
avatarUploading.value = false;
|
|
return;
|
|
}
|
|
|
|
// 创建FormData对象
|
|
const formData = new FormData();
|
|
formData.append('avatar', file);
|
|
formData.append('token', token);
|
|
|
|
console.log('准备上传头像文件:', file.name);
|
|
console.log('Token:', token);
|
|
|
|
// 调用API上传头像
|
|
const response = await updateAvatar(formData,{
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
}
|
|
});
|
|
|
|
console.log('收到响应:', response);
|
|
|
|
if (response.success) {
|
|
// 使用返回的头像路径
|
|
// 确保头像URL是完整的路径
|
|
let avatarPath = response.avatar;
|
|
if (avatarPath.startsWith('/resource/')) {
|
|
// 如果是相对路径,保持原样(通过代理访问)
|
|
avatarPath = avatarPath;
|
|
}
|
|
userProfile.value.avatar = avatarPath;
|
|
|
|
// 显示成功消息
|
|
avatarUploadSuccess.value = true;
|
|
successMessage.value = '头像更新成功';
|
|
|
|
// 3秒后隐藏成功提示
|
|
setTimeout(() => {
|
|
avatarUploadSuccess.value = false;
|
|
successMessage.value = '';
|
|
}, 3000);
|
|
} else {
|
|
errorMessage.value = response.message || '头像更新失败';
|
|
}
|
|
} catch (error) {
|
|
console.error('头像上传失败:', error);
|
|
console.error('错误详情:', {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
name: error.name
|
|
});
|
|
|
|
// 提供更详细的错误信息
|
|
if (error.response) {
|
|
// 服务器响应了错误状态码
|
|
console.error('服务器响应:', error.response.status, error.response.data);
|
|
errorMessage.value = `服务器错误 ${error.response.status}: ${JSON.stringify(error.response.data)}`;
|
|
} else if (error.request) {
|
|
// 请求已发出但没有收到响应
|
|
console.error('网络错误,未收到响应:', error.request);
|
|
errorMessage.value = '网络错误,请检查网络连接和服务器状态';
|
|
} else {
|
|
// 其他错误
|
|
errorMessage.value = `请求配置错误: ${error.message}`;
|
|
}
|
|
} finally {
|
|
avatarUploading.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
// 更新个人信息
|
|
const updateProfile = () => {
|
|
profileSaving.value = true;
|
|
errorMessage.value = '';
|
|
successMessage.value = '';
|
|
|
|
// 模拟API请求
|
|
setTimeout(() => {
|
|
profileSaving.value = false;
|
|
successMessage.value = '个人信息更新成功';
|
|
|
|
// 3秒后隐藏成功提示
|
|
setTimeout(() => {
|
|
successMessage.value = '';
|
|
}, 3000);
|
|
}, 1000);
|
|
};
|
|
|
|
// 修改密码
|
|
const changePassword = async () => {
|
|
if (passwordMismatch.value) {
|
|
return;
|
|
}
|
|
|
|
passwordSaving.value = true;
|
|
errorMessage.value = '';
|
|
successMessage.value = '';
|
|
|
|
try {
|
|
// 从localStorage获取token
|
|
const token = localStorage.getItem('token');
|
|
|
|
if (!token) {
|
|
errorMessage.value = '用户未登录,请先登录';
|
|
passwordSaving.value = false;
|
|
return;
|
|
}
|
|
|
|
// 调用API修改密码
|
|
const response = await updatePassword({
|
|
token,
|
|
currentPassword: passwordForm.value.currentPassword,
|
|
newPassword: passwordForm.value.newPassword
|
|
});
|
|
|
|
if (response.success) {
|
|
successMessage.value = '密码修改成功';
|
|
|
|
// 清空表单
|
|
passwordForm.value = {
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: ''
|
|
};
|
|
|
|
// 3秒后隐藏成功提示
|
|
setTimeout(() => {
|
|
successMessage.value = '';
|
|
}, 3000);
|
|
} else {
|
|
errorMessage.value = response.message || '密码修改失败';
|
|
}
|
|
} catch (error) {
|
|
console.error('密码修改失败:', error);
|
|
errorMessage.value = '密码修改时发生错误';
|
|
} finally {
|
|
passwordSaving.value = false;
|
|
}
|
|
};
|
|
|
|
// 组件挂载时获取用户信息
|
|
onMounted(async () => {
|
|
try {
|
|
// 从localStorage获取token
|
|
const token = localStorage.getItem('token');
|
|
|
|
console.log('Profile组件挂载,获取到的token:', token);
|
|
|
|
if (token) {
|
|
// 调用API获取用户信息
|
|
const response = await getUserProfile(token);
|
|
|
|
console.log('获取用户信息响应:', response);
|
|
|
|
if (response.success) {
|
|
// 更新用户信息
|
|
// 如果头像路径是相对路径,需要添加服务器前缀
|
|
let avatarUrl = response.user.avatar || '/resource/avatar/4.png';
|
|
if (avatarUrl.startsWith('/resource/')) {
|
|
avatarUrl = avatarUrl; // 相对路径,直接使用
|
|
}
|
|
|
|
console.log('设置头像URL:', avatarUrl);
|
|
|
|
userProfile.value = {
|
|
username: response.user.username,
|
|
avatar: avatarUrl
|
|
};
|
|
} else {
|
|
console.error('获取用户信息失败:', response.message);
|
|
errorMessage.value = response.message || '获取用户信息失败';
|
|
|
|
// 如果是token过期或无效,清除token并跳转到登录页
|
|
if (response.message && response.message.includes('登录')) {
|
|
localStorage.removeItem('token');
|
|
// 可以在这里添加跳转到登录页的逻辑
|
|
}
|
|
}
|
|
} else {
|
|
console.log('用户未登录');
|
|
errorMessage.value = '用户未登录,请先登录';
|
|
}
|
|
} catch (error) {
|
|
console.error('获取用户信息失败:', error);
|
|
errorMessage.value = '获取用户信息时发生错误';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.app-container {
|
|
display: flex;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.main-content {
|
|
flex: 1;
|
|
padding: 1.5rem;
|
|
overflow-y: auto;
|
|
height: 100%;
|
|
background-color: #f5f7fa;
|
|
transition: margin-left 0.3s ease;
|
|
}
|
|
|
|
/* 确保左侧导航栏完全固定 */
|
|
.app-container > :first-child {
|
|
position: fixed;
|
|
height: 100vh;
|
|
z-index: 10;
|
|
}
|
|
|
|
/* 为右侧内容添加左边距,避免被固定导航栏遮挡 */
|
|
.main-content {
|
|
margin-left: 240px; /* 与Menu.vue中的sidebar-container宽度相同 */
|
|
}
|
|
|
|
/* 当菜单折叠时调整内容区域 */
|
|
.app-container:has(.sidebar-container.collapsed) .main-content {
|
|
margin-left: 60px;
|
|
}
|
|
|
|
.profile-container {
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* 页面标题样式 */
|
|
.page-header {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
margin: 0 0 0.5rem 0;
|
|
}
|
|
|
|
.page-subtitle {
|
|
color: #6b7280;
|
|
margin: 0;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* 密码修改卡片 */
|
|
.password-card {
|
|
background-color: #fff;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
max-width: 600px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}
|
|
|
|
.password-card .section-title {
|
|
text-align: center;
|
|
}
|
|
|
|
/* 个人信息卡片 */
|
|
.profile-card {
|
|
background-color: #fff;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
max-width: 600px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}
|
|
|
|
/* 头像区域样式 */
|
|
.avatar-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin-bottom: 2rem;
|
|
padding-bottom: 1.5rem;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.avatar-container {
|
|
position: relative;
|
|
width: 100px;
|
|
height: 100px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.avatar-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 3px solid #e5e7eb;
|
|
}
|
|
|
|
.avatar-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.avatar-container:hover .avatar-overlay {
|
|
opacity: 1;
|
|
}
|
|
|
|
.camera-icon {
|
|
font-size: 1.5rem;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.overlay-text {
|
|
font-size: 0.75rem;
|
|
color: white;
|
|
}
|
|
|
|
.hidden-input {
|
|
display: none;
|
|
}
|
|
|
|
.avatar-status {
|
|
text-align: center;
|
|
}
|
|
|
|
.username-display {
|
|
text-align: center;
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
margin-top: 0.75rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.uploading-text, .success-text {
|
|
font-size: 0.875rem;
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.uploading-text {
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.success-text {
|
|
color: #10b981;
|
|
}
|
|
|
|
/* 信息区域样式 */
|
|
.info-section {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
margin: 0 0 1rem 0;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
text-align: center;
|
|
}
|
|
|
|
/* 表单样式 */
|
|
.profile-form, .password-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
max-width: 400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.form-group {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.form-input {
|
|
padding: 0.625rem 0.875rem;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.875rem;
|
|
transition: border-color 0.2s;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.form-input[readonly] {
|
|
background-color: #f9fafb;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.form-input::placeholder {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.save-button {
|
|
background-color: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 0.375rem;
|
|
padding: 0.625rem 1.25rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
min-width: 120px;
|
|
}
|
|
|
|
.save-button:hover:not(:disabled) {
|
|
background-color: #2563eb;
|
|
}
|
|
|
|
.save-button:disabled {
|
|
background-color: #9ca3af;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* 错误和成功消息样式 */
|
|
.error-text {
|
|
color: #ef4444;
|
|
font-size: 0.75rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.success-message, .error-message {
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 0.375rem;
|
|
margin-top: 1rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.success-message {
|
|
background-color: #d1fae5;
|
|
color: #065f46;
|
|
border: 1px solid #a7f3d0;
|
|
}
|
|
|
|
.error-message {
|
|
background-color: #fee2e2;
|
|
color: #991b1b;
|
|
border: 1px solid #fca5a5;
|
|
}
|
|
|
|
/* 响应式调整 */
|
|
@media (max-width: 768px) {
|
|
.main-content {
|
|
margin-left: 70px; /* 移动端侧边栏宽度 */
|
|
}
|
|
}
|
|
</style>
|