用户个人中心、兴趣小组
Change-Id: I0e2f3f4ad586f237505613238cbb7bebb6118b63
diff --git a/src/pages/UserCenter/UserCollect.css b/src/pages/UserCenter/UserCollect.css
index fe7b3bd..79caaf6 100644
--- a/src/pages/UserCenter/UserCollect.css
+++ b/src/pages/UserCenter/UserCollect.css
@@ -1,11 +1,11 @@
-.user-center {
+/* .user-center {
max-width: 100%;
padding: 3%;
font-family: Arial, sans-serif;
display: flex;
gap: 10%;
background: #333;
-}
+} */
.post-item {
margin-bottom: 20px;
diff --git a/src/pages/UserCenter/UserDynamics.css b/src/pages/UserCenter/UserDynamics.css
index 40a11b4..5380c57 100644
--- a/src/pages/UserCenter/UserDynamics.css
+++ b/src/pages/UserCenter/UserDynamics.css
@@ -17,12 +17,13 @@
}
.dynamic-card {
- background: #fff;
- border: 1px solid #eee;
+ background: #E4D8C9;
+ /* border: 1px solid #eee; */
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
+ width: 100%;
}
.dynamic-header {
diff --git a/src/pages/UserCenter/UserDynamics.jsx b/src/pages/UserCenter/UserDynamics.jsx
index 05d9046..d494c52 100644
--- a/src/pages/UserCenter/UserDynamics.jsx
+++ b/src/pages/UserCenter/UserDynamics.jsx
@@ -29,7 +29,7 @@
return (
<div className="user-dynamics-container">
- <h2 className="user-dynamics-title">我的动态</h2>
+ {/* <h2 className="user-dynamics-title">我的动态</h2> */}
{dynamics.length === 0 ? (
<div className="user-dynamics-empty">暂无动态</div>
) : (
@@ -49,13 +49,6 @@
<div className="dynamic-content">
{item.title && <h4 className="dynamic-title">{item.title}</h4>}
<p>{item.content}</p>
- {/* {item.images && (
- <div className="dynamic-images">
- {JSON.parse(item.images).map((img, index) => (
- <img key={index} src={img} alt={`图${index + 1}`} />
- ))}
- </div>
- )} */}
{item.images && (
<div className="dynamic-images">
{(() => {
diff --git a/src/pages/UserCenter/UserFriends.css b/src/pages/UserCenter/UserFriends.css
index 2533959..37f6c9b 100644
--- a/src/pages/UserCenter/UserFriends.css
+++ b/src/pages/UserCenter/UserFriends.css
@@ -19,6 +19,8 @@
.friend-info {
flex: 1;
+ /* 字体大小 */
+ font-size: 1.3em;
}
.friend-info h4 {
diff --git a/src/pages/UserCenter/UserFriends.jsx b/src/pages/UserCenter/UserFriends.jsx
index 5540911..491c19c 100644
--- a/src/pages/UserCenter/UserFriends.jsx
+++ b/src/pages/UserCenter/UserFriends.jsx
@@ -335,13 +335,25 @@
return (
<div className="user-subpage-card">
- <h2>我的好友</h2>
- <div>
- <input type="text" value={email} placeholder="请输入好友邮箱" onChange={e => setEmail(e.target.value)} />
+
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginLeft: '-12%' }}>
+ <div className="search">
+ <input
+ type="text"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ placeholder="搜索邮箱添加好友"
+ />
+ </div>
+
<button className="btn submit" onClick={handleSubmit}>
添加
</button>
</div>
+
+ {/* 增加间距 */}
+ <div style={{ marginBottom: '30px' }}></div>
+
<div className="friends-list">
{friends.length === 0 && <p>暂无好友</p>}
{friends.map((friend) => (
@@ -350,7 +362,7 @@
<div className="friend-info">
<p><strong>{friend.nickname}</strong></p>
<p>{friend.email}</p>
- <button className="send-message-btn" onClick={() => openChat(friend)}>
+ <button style={{ padding: '6px 12px', fontSize: '15px', float: 'right' }} onClick={() => openChat(friend)}>
发送私信
</button>
</div>
diff --git a/src/pages/UserCenter/UserLevelExperience.jsx b/src/pages/UserCenter/UserLevelExperience.jsx
new file mode 100644
index 0000000..60dcd55
--- /dev/null
+++ b/src/pages/UserCenter/UserLevelExperience.jsx
@@ -0,0 +1,375 @@
+// import React, { useState, useEffect } from 'react';
+// import axios from 'axios';
+
+// const UserLevelExperience = ({ userId }) => {
+// const [experienceInfo, setExperienceInfo] = useState(null);
+// const [error, setError] = useState(null);
+// const [isLoading, setIsLoading] = useState(false);
+// const [upgradeResult, setUpgradeResult] = useState(null);
+// const [hasCheckedIn, setHasCheckedIn] = useState(false);
+
+// useEffect(() => {
+// if (!userId) return;
+// fetchAllLevelData();
+// }, [userId]);
+
+// useEffect(() => {
+// // 自动触发升级判断
+// if (
+// experienceInfo &&
+// experienceInfo.current_experience >= experienceInfo.next_level_experience
+// ) {
+// checkUpgrade();
+// }
+// }, [experienceInfo]);
+
+// const fetchAllLevelData = async () => {
+// try {
+// setIsLoading(true);
+// setError(null);
+
+// const { data } = await axios.get('/echo/level/getExperience', {
+// params: { user_id: userId },
+// });
+
+// const normalizedData = {
+// ...data,
+// current_level: data.current_level || data.level,
+// };
+
+// setExperienceInfo(normalizedData);
+
+// const today = new Date().toDateString();
+// const lastCheckIn = localStorage.getItem('lastCheckIn');
+// setHasCheckedIn(lastCheckIn === today);
+// } catch (err) {
+// console.error('经验信息获取失败:', err);
+// setError('获取经验信息失败');
+// } finally {
+// setIsLoading(false);
+// }
+// };
+
+// const updateExperience = async (source, amount = 10) => {
+// try {
+// setIsLoading(true);
+// setError(null);
+
+// const { data } = await axios.post('/echo/level/updateExperience', {
+// user_id: userId,
+// experience: amount,
+// source: source,
+// });
+
+// setExperienceInfo((prev) => ({
+// ...prev,
+// current_experience: data.current_experience,
+// }));
+
+// alert(`获得${amount}点经验值!来源:${source}`);
+
+// if (source === 'check-in') {
+// localStorage.setItem('lastCheckIn', new Date().toDateString());
+// setHasCheckedIn(true);
+// }
+// } catch (err) {
+// console.error('更新经验失败:', err);
+// setError(err.response?.data?.message || '更新经验失败');
+// } finally {
+// setIsLoading(false);
+// }
+// };
+
+// const checkUpgrade = async () => {
+// try {
+// setIsLoading(true);
+// setError(null);
+
+// const { data } = await axios.get('/echo/level/upgrade-check', {
+// params: { user_id: userId },
+// });
+
+// if (data.can_upgrade) {
+// await performUpgrade(); // 自动触发
+// }
+// } catch (err) {
+// console.error('检查升级失败:', err);
+// setError(err.response?.data?.message || '检查升级失败');
+// } finally {
+// setIsLoading(false);
+// }
+// };
+
+
+// const performUpgrade = async () => {
+// try {
+// setIsLoading(true);
+// setError(null);
+
+// const { data } = await axios.post('/echo/level/upgrades', {
+// user_id: userId,
+// can_upgrade: true,
+// });
+
+// console.log('升级响应数据:', data); // 保留调试日志
+
+// setExperienceInfo((prev) => ({
+// ...prev,
+// current_level: data.new_level, // 修复:使用正确的字段名
+// current_experience: 0,
+// next_level_experience: prev.next_level_experience * 2,
+// }));
+
+// setUpgradeResult(data);
+// alert(`恭喜!您已升级到等级 ${data.new_level}!`); // 修复:使用正确的字段名
+// } catch (err) {
+// console.error('升级失败:', err);
+// setError(err.response?.data?.message || '升级失败');
+// } finally {
+// setIsLoading(false);
+// }
+// };
+
+
+// if (error) return <p className="error">{error}</p>;
+// if (isLoading) return <p>加载中...</p>;
+// if (!experienceInfo) return <p>加载经验信息中...</p>;
+
+// const { current_experience, next_level_experience, current_level } = experienceInfo;
+// const progressPercent = Math.min(
+// 100,
+// (current_experience / (next_level_experience || 1)) * 100
+// ).toFixed(2);
+
+// const expToNextLevel = Math.max(0, next_level_experience - current_experience); // 防止负数
+
+// return (
+// <div className="level-experience-section">
+// {/* <h3>等级与经验</h3> */}
+// <p><strong>当前等级:</strong>{current_level || '未知'}</p>
+// <p><strong>当前经验:</strong>{current_experience}</p>
+// <p><strong>距离下一等级还需:</strong>{expToNextLevel} 经验值</p>
+
+// <div className="exp-bar-wrapper">
+// <div className="exp-bar" style={{ width: `${progressPercent}%` }} />
+// </div>
+// <p className="exp-progress-text">{progressPercent}%</p>
+
+
+// {upgradeResult && (
+// <div className="upgrade-success">
+// {/* 使用与状态一致的字段名 */}
+// <p>恭喜!您已成功升级到等级 {upgradeResult.new_level}!</p>
+// </div>
+// )}
+
+
+// <div className="level-actions">
+// <button onClick={() => updateExperience('check-in', 15)} disabled={hasCheckedIn}>
+// {hasCheckedIn ? '今日已签到' : '每日签到 (+15经验)'}
+// </button>
+// <button onClick={() => updateExperience('task', 30)}>完成任务 (+30经验)</button>
+// <button onClick={() => updateExperience('upload', 50)}>上传种子 (+50经验)</button>
+// {/* <button onClick={checkUpgrade}>检查升级</button> */}
+// </div>
+// </div>
+// );
+// };
+
+// export default UserLevelExperience;
+
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+
+const UserLevelExperience = ({ userId }) => {
+ const [experienceInfo, setExperienceInfo] = useState(null);
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [upgradeResult, setUpgradeResult] = useState(null);
+ const [hasCheckedIn, setHasCheckedIn] = useState(false);
+
+ useEffect(() => {
+ if (!userId) return;
+ fetchAllLevelData();
+ }, [userId]);
+
+ useEffect(() => {
+ // 自动触发升级判断
+ if (
+ experienceInfo &&
+ experienceInfo.current_experience >= experienceInfo.next_level_experience
+ ) {
+ checkUpgrade();
+ }
+ }, [experienceInfo]);
+
+ const fetchAllLevelData = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const { data } = await axios.get('/echo/level/getExperience', {
+ params: { user_id: userId },
+ });
+
+ const normalizedData = {
+ ...data,
+ current_level: data.current_level || data.level,
+ };
+
+ setExperienceInfo(normalizedData);
+
+ const today = new Date().toDateString();
+ const lastCheckIn = localStorage.getItem('lastCheckIn');
+ setHasCheckedIn(lastCheckIn === today);
+ } catch (err) {
+ console.error('经验信息获取失败:', err);
+ setError('获取经验信息失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const updateExperience = async (source, amount = 10) => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const { data } = await axios.post('/echo/level/updateExperience', {
+ user_id: userId,
+ experience: amount,
+ source: source,
+ });
+
+ setExperienceInfo((prev) => ({
+ ...prev,
+ current_experience: data.current_experience,
+ }));
+
+ alert(`获得${amount}点经验值!来源:${source}`);
+
+ if (source === 'check-in') {
+ localStorage.setItem('lastCheckIn', new Date().toDateString());
+ setHasCheckedIn(true);
+ }
+ } catch (err) {
+ console.error('更新经验失败:', err);
+ setError(err.response?.data?.message || '更新经验失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const checkUpgrade = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const { data } = await axios.get('/echo/level/upgrade-check', {
+ params: { user_id: userId },
+ });
+
+ if (data.can_upgrade) {
+ if (window.confirm('您已满足升级条件,是否要升级?')) {
+ await performUpgrade();
+ }
+ } else {
+ // 区分是经验不足还是已达最高等级
+ if (data.is_max_level) {
+ alert('您已达到最高等级!');
+ } else {
+ alert(`还不能升级,还需要${data.next_level_experience - data.current_experience}点经验值`);
+ }
+ }
+ } catch (err) {
+ console.error('检查升级失败:', err);
+ setError(err.response?.data?.message || '检查升级失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const performUpgrade = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const { data } = await axios.post('/echo/level/upgrades', {
+ user_id: userId,
+ can_upgrade: true,
+ });
+
+ console.log('升级响应数据:', data);
+
+ // 正确处理升级结果
+ if (data.status === 'success') {
+ setExperienceInfo((prev) => ({
+ ...prev,
+ current_level: data.new_level,
+ current_experience: 0,
+ next_level_experience: prev.next_level_experience * 2,
+ }));
+
+ setUpgradeResult(data);
+ alert(`恭喜!您已升级到等级 ${data.new_level}!`);
+ } else {
+ throw new Error(data.message || '升级失败');
+ }
+ } catch (err) {
+ console.error('升级失败:', err);
+ setError(err.message || '升级失败');
+ alert(err.message || '升级失败,请稍后再试');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (error) return <p className="error">{error}</p>;
+ if (isLoading) return <p>加载中...</p>;
+ if (!experienceInfo) return <p>加载经验信息中...</p>;
+
+ const { current_experience, next_level_experience, current_level } = experienceInfo;
+ const progressPercent = Math.min(
+ 100,
+ (current_experience / (next_level_experience || 1)) * 100
+ ).toFixed(2);
+
+ const expToNextLevel = Math.max(0, next_level_experience - current_experience);
+
+ return (
+ <div className="level-experience-section">
+ <h3>等级与经验</h3>
+ <p><strong>当前等级:</strong>{current_level || '未知'}</p>
+ <p><strong>当前经验:</strong>{current_experience}</p>
+ <p><strong>距离下一等级还需:</strong>{expToNextLevel} 经验值</p>
+
+ <div className="exp-bar-wrapper">
+ <div className="exp-bar" style={{ width: `${progressPercent}%` }} />
+ </div>
+ <p className="exp-progress-text">{progressPercent}%</p>
+
+ {upgradeResult && (
+ <div className="upgrade-success">
+ <p>恭喜!您已成功升级到等级 {upgradeResult.new_level}!</p>
+ </div>
+ )}
+
+ {error && (
+ <div className="upgrade-error">
+ <p>{error}</p>
+ </div>
+ )}
+
+ <div className="level-actions">
+ <button onClick={() => updateExperience('check-in', 15)} disabled={hasCheckedIn}>
+ {hasCheckedIn ? '今日已签到' : '每日签到 (+15经验)'}
+ </button>
+ <button onClick={() => updateExperience('task', 30)}>完成任务 (+30经验)</button>
+ <button onClick={() => updateExperience('upload', 50)}>上传种子 (+50经验)</button>
+ <button onClick={checkUpgrade}>检查升级</button>
+ </div>
+ </div>
+ );
+};
+
+export default UserLevelExperience;
diff --git a/src/pages/UserCenter/UserProfile.css b/src/pages/UserCenter/UserProfile.css
index b230c1e..cb71be7 100644
--- a/src/pages/UserCenter/UserProfile.css
+++ b/src/pages/UserCenter/UserProfile.css
@@ -54,10 +54,25 @@
margin-top: 40px;
width: 80%;
padding-top: 10%;
- padding-right: 15%;
+ padding-right: 10%;
padding-bottom: 10%;
padding-left: 10%;
+ margin-left: 3%;
+ margin-right: 5%;
+
+ /* padding: 10% 20%; */
}
+
+/* .common-card {
+ background-color: #e9ded2;
+ border-radius: 16px;
+ margin: 0 auto;
+ margin-top: 40px;
+ padding: 10% 20%;
+ margin-left: 5%;
+ margin-right: 5%;
+} */
+
.avatar-wrapper {
position: relative;
display: inline-block;
@@ -119,15 +134,7 @@
text-align: center;
}
-.common-card {
- background-color: #e9ded2;
- border-radius: 16px;
- margin: 0 auto;
- margin-top: 40px;
- padding: 10% 20%;
- margin-left: 5%;
- margin-right: 5%;
-}
+
.avatar-wrapper {
position: relative;
diff --git a/src/pages/UserCenter/UserProfile.jsx b/src/pages/UserCenter/UserProfile.jsx
index c6e80e0..bd87c99 100644
--- a/src/pages/UserCenter/UserProfile.jsx
+++ b/src/pages/UserCenter/UserProfile.jsx
@@ -1,263 +1,283 @@
-import React, { useEffect, useState } from 'react';
-import axios from 'axios';
-import './UserProfile.css';
-import { useUser } from '../../context/UserContext';
-import { useLocation } from 'wouter';
+// import React, { useEffect, useState } from 'react';
+// import axios from 'axios';
+// import './UserProfile.css';
+// import { useUser } from '../../context/UserContext';
+// import { useLocation } from 'wouter';
-const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
+// const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
+
+// const UserProfile = () => {
+// const { user, loading, logout } = useUser();
+// const [userProfile, setUserProfile] = useState(null);
+// const [experienceInfo, setExperienceInfo] = useState(null);
+// const [error, setError] = useState(null);
+
+// // 修改密码状态
+// const [showPwdModal, setShowPwdModal] = useState(false);
+// const [oldPassword, setOldPassword] = useState('');
+// const [newPassword, setNewPassword] = useState('');
+// const [confirmPassword, setConfirmPassword] = useState('');
+
+// // 退出登录
+// const [, setLocation] = useLocation();
+
+// useEffect(() => {
+// if (loading) return;
+// if (!user || !user.userId) {
+// setError('未登录或用户信息缺失');
+// setUserProfile(null);
+// return;
+// }
+
+// const fetchUserProfile = async () => {
+// try {
+// setError(null);
+// const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
+// if (!raw) {
+// setError('用户数据为空');
+// setUserProfile(null);
+// return;
+// }
+
+// const profile = {
+// avatarUrl: raw.avatarUrl
+// ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
+// : DEFAULT_AVATAR_URL,
+// nickname: raw.username || '未知用户',
+// email: raw.email || '未填写',
+// gender: raw.gender || '保密',
+// bio: raw.description || '无',
+// interests: raw.hobbies ? raw.hobbies.split(',') : [],
+// level: raw.level || '未知',
+// experience: raw.experience ?? 0,
+// uploadAmount: raw.uploadCount ?? 0,
+// downloadAmount: raw.downloadCount ?? 0,
+// shareRate: raw.shareRate ?? 0,
+// joinedDate: raw.registrationTime,
+// };
+
+// setUserProfile(profile);
+// } catch (err) {
+// setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
+// setUserProfile(null);
+// }
+// };
+
+// const fetchExperienceInfo = async () => {
+// try {
+// const { data } = await axios.get('/echo/level/getExperience', {
+// params: { user_id: user.userId },
+// });
+// setExperienceInfo(data);
+// } catch (err) {
+// console.error('经验信息获取失败:', err);
+// }
+// };
+
+// fetchUserProfile();
+// fetchExperienceInfo();
+// }, [user, loading]);
+
+// const handleAvatarUpload = async (e) => {
+// const file = e.target.files[0];
+// if (!file) return;
+
+// const formData = new FormData();
+// formData.append('file', file);
+
+// try {
+// const { data } = await axios.post(
+// `/echo/user/${user.userId}/uploadAvatar`,
+// formData,
+// { headers: { 'Content-Type': 'multipart/form-data' } }
+// );
+
+// if (data?.avatarUrl) {
+// setUserProfile((prev) => ({
+// ...prev,
+// avatarUrl: `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}`,
+// }));
+// alert('头像上传成功');
+// } else {
+// alert('头像上传成功,但未返回新头像地址');
+// }
+// } catch (err) {
+// console.error('上传失败:', err);
+// alert('头像上传失败,请重试');
+// }
+// };
+
+// const handleLogout = () => {
+// logout();
+// setLocation('/auth'); // 退出后跳转登录页
+// // window.location.reload(); // 或跳转登录页
+// };
+
+// const handleChangePassword = async () => {
+// if (!oldPassword || !newPassword || !confirmPassword) {
+// alert('请填写所有字段');
+// return;
+// }
+// if (newPassword !== confirmPassword) {
+// alert('两次输入的新密码不一致');
+// return;
+// }
+
+// try {
+// // await axios.post('/echo/user/password', {
+// // user_id: user.userId,
+// // oldPassword,
+// // newPassword,
+// // });
+// await axios.post('/echo/user/password', {
+// user_id: user.userId,
+// old_password: oldPassword,
+// new_password: newPassword,
+// confirm_password: confirmPassword,
+// });
+// alert('密码修改成功,请重新登录');
+// logout();
+// window.location.reload();
+// } catch (err) {
+// alert(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
+// }
+// };
+
+// if (loading) return <p>正在加载用户信息...</p>;
+// if (error) return <p className="error">{error}</p>;
+// if (!userProfile) return null;
+
+// const {
+// avatarUrl,
+// nickname,
+// email,
+// gender,
+// bio,
+// interests,
+// level,
+// experience,
+// uploadAmount,
+// downloadAmount,
+// shareRate,
+// joinedDate,
+// } = userProfile;
+
+// const progressPercent = experienceInfo
+// ? Math.min(
+// 100,
+// ((experienceInfo.current_experience || 0) /
+// (experienceInfo.next_level_experience || 1)) *
+// 100
+// ).toFixed(2)
+// : 0;
+
+// const expToNextLevel = experienceInfo
+// ? (experienceInfo.next_level_experience - experienceInfo.current_experience)
+// : null;
+
+// return (
+// <div className="common-card">
+// <div className="right-content">
+// <div className="profile-header">
+// <div className="avatar-wrapper">
+// <img src={avatarUrl} alt={nickname} className="avatar" />
+// <label htmlFor="avatar-upload" className="avatar-upload-label">
+// 上传头像
+// </label>
+// <input
+// type="file"
+// id="avatar-upload"
+// accept="image/*"
+// style={{ display: 'none' }}
+// onChange={handleAvatarUpload}
+// />
+// </div>
+// <h1>{nickname}</h1>
+// </div>
+
+// <div className="profile-details">
+// <p><strong>邮箱:</strong>{email}</p>
+// <p><strong>性别:</strong>{gender}</p>
+// <p><strong>个人简介:</strong>{bio}</p>
+// <p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
+// <p><strong>等级:</strong>{level}</p>
+// <p><strong>经验:</strong>{experience}</p>
+// <p><strong>上传量:</strong>{uploadAmount}</p>
+// <p><strong>下载量:</strong>{downloadAmount}</p>
+// <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
+// <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
+
+// {experienceInfo && (
+// <>
+// <p><strong>距离下一等级还需:</strong>{expToNextLevel} 经验值</p>
+// <div className="exp-bar-wrapper">
+// <div className="exp-bar" style={{ width: `${progressPercent}%` }} />
+// </div>
+// <p className="exp-progress-text">{progressPercent}%</p>
+// </>
+// )}
+
+// {/* 修改密码与退出登录按钮 */}
+// <div className="profile-actions">
+// <button onClick={() => setShowPwdModal(true)}>修改密码</button>
+// <button onClick={handleLogout}>退出登录</button>
+// </div>
+
+// {/* 修改密码弹窗 */}
+// {showPwdModal && (
+// <div className="modal">
+// <div className="modal-content">
+// <h3>修改密码</h3>
+// <input
+// type="password"
+// placeholder="原密码"
+// value={oldPassword}
+// onChange={(e) => setOldPassword(e.target.value)}
+// />
+// <input
+// type="password"
+// placeholder="新密码"
+// value={newPassword}
+// onChange={(e) => setNewPassword(e.target.value)}
+// />
+// <input
+// type="password"
+// placeholder="确认新密码"
+// value={confirmPassword}
+// onChange={(e) => setConfirmPassword(e.target.value)}
+// />
+// <div className="modal-buttons">
+// <button onClick={handleChangePassword}>确认修改</button>
+// <button onClick={() => setShowPwdModal(false)}>取消</button>
+// </div>
+// </div>
+// </div>
+// )}
+// </div>
+// </div>
+// </div>
+// );
+// };
+
+// export default UserProfile;
+
+import React from 'react';
+import UserProfileBase from './UserProfileBase';
+import UserLevelExperience from './UserLevelExperience';
const UserProfile = () => {
- const { user, loading, logout } = useUser();
- const [userProfile, setUserProfile] = useState(null);
- const [experienceInfo, setExperienceInfo] = useState(null);
- const [error, setError] = useState(null);
+ const [userId, setUserId] = React.useState(null);
- // 修改密码状态
- const [showPwdModal, setShowPwdModal] = useState(false);
- const [oldPassword, setOldPassword] = useState('');
- const [newPassword, setNewPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
-
- // 退出登录
- const [, setLocation] = useLocation();
-
- useEffect(() => {
- if (loading) return;
- if (!user || !user.userId) {
- setError('未登录或用户信息缺失');
- setUserProfile(null);
- return;
- }
-
- const fetchUserProfile = async () => {
- try {
- setError(null);
- const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
- if (!raw) {
- setError('用户数据为空');
- setUserProfile(null);
- return;
- }
-
- const profile = {
- avatarUrl: raw.avatarUrl
- ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
- : DEFAULT_AVATAR_URL,
- nickname: raw.username || '未知用户',
- email: raw.email || '未填写',
- gender: raw.gender || '保密',
- bio: raw.description || '无',
- interests: raw.hobbies ? raw.hobbies.split(',') : [],
- level: raw.level || '未知',
- experience: raw.experience ?? 0,
- uploadAmount: raw.uploadCount ?? 0,
- downloadAmount: raw.downloadCount ?? 0,
- shareRate: raw.shareRate ?? 0,
- joinedDate: raw.registrationTime,
- };
-
- setUserProfile(profile);
- } catch (err) {
- setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
- setUserProfile(null);
- }
- };
-
- const fetchExperienceInfo = async () => {
- try {
- const { data } = await axios.get('/echo/level/getExperience', {
- params: { user_id: user.userId },
- });
- setExperienceInfo(data);
- } catch (err) {
- console.error('经验信息获取失败:', err);
- }
- };
-
- fetchUserProfile();
- fetchExperienceInfo();
- }, [user, loading]);
-
- const handleAvatarUpload = async (e) => {
- const file = e.target.files[0];
- if (!file) return;
-
- const formData = new FormData();
- formData.append('file', file);
-
- try {
- const { data } = await axios.post(
- `/echo/user/${user.userId}/uploadAvatar`,
- formData,
- { headers: { 'Content-Type': 'multipart/form-data' } }
- );
-
- if (data?.avatarUrl) {
- setUserProfile((prev) => ({
- ...prev,
- avatarUrl: `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}`,
- }));
- alert('头像上传成功');
- } else {
- alert('头像上传成功,但未返回新头像地址');
- }
- } catch (err) {
- console.error('上传失败:', err);
- alert('头像上传失败,请重试');
- }
+ const loadExperienceInfo = (id) => {
+ setUserId(id);
};
- const handleLogout = () => {
- logout();
- setLocation('/auth'); // 退出后跳转登录页
- // window.location.reload(); // 或跳转登录页
- };
-
- const handleChangePassword = async () => {
- if (!oldPassword || !newPassword || !confirmPassword) {
- alert('请填写所有字段');
- return;
- }
- if (newPassword !== confirmPassword) {
- alert('两次输入的新密码不一致');
- return;
- }
-
- try {
- // await axios.post('/echo/user/password', {
- // user_id: user.userId,
- // oldPassword,
- // newPassword,
- // });
- await axios.post('/echo/user/password', {
- user_id: user.userId,
- old_password: oldPassword,
- new_password: newPassword,
- confirm_password: confirmPassword,
- });
- alert('密码修改成功,请重新登录');
- logout();
- window.location.reload();
- } catch (err) {
- alert(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
- }
- };
-
- if (loading) return <p>正在加载用户信息...</p>;
- if (error) return <p className="error">{error}</p>;
- if (!userProfile) return null;
-
- const {
- avatarUrl,
- nickname,
- email,
- gender,
- bio,
- interests,
- level,
- experience,
- uploadAmount,
- downloadAmount,
- shareRate,
- joinedDate,
- } = userProfile;
-
- const progressPercent = experienceInfo
- ? Math.min(
- 100,
- ((experienceInfo.current_experience || 0) /
- (experienceInfo.next_level_experience || 1)) *
- 100
- ).toFixed(2)
- : 0;
-
- const expToNextLevel = experienceInfo
- ? (experienceInfo.next_level_experience - experienceInfo.current_experience)
- : null;
-
return (
- <div className="common-card">
- <div className="right-content">
- <div className="profile-header">
- <div className="avatar-wrapper">
- <img src={avatarUrl} alt={nickname} className="avatar" />
- <label htmlFor="avatar-upload" className="avatar-upload-label">
- 上传头像
- </label>
- <input
- type="file"
- id="avatar-upload"
- accept="image/*"
- style={{ display: 'none' }}
- onChange={handleAvatarUpload}
- />
- </div>
- <h1>{nickname}</h1>
- </div>
-
- <div className="profile-details">
- <p><strong>邮箱:</strong>{email}</p>
- <p><strong>性别:</strong>{gender}</p>
- <p><strong>个人简介:</strong>{bio}</p>
- <p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
- <p><strong>等级:</strong>{level}</p>
- <p><strong>经验:</strong>{experience}</p>
- <p><strong>上传量:</strong>{uploadAmount}</p>
- <p><strong>下载量:</strong>{downloadAmount}</p>
- <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
- <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
-
- {experienceInfo && (
- <>
- <p><strong>距离下一等级还需:</strong>{expToNextLevel} 经验值</p>
- <div className="exp-bar-wrapper">
- <div className="exp-bar" style={{ width: `${progressPercent}%` }} />
- </div>
- <p className="exp-progress-text">{progressPercent}%</p>
- </>
- )}
-
- {/* 修改密码与退出登录按钮 */}
- <div className="profile-actions">
- <button onClick={() => setShowPwdModal(true)}>修改密码</button>
- <button onClick={handleLogout}>退出登录</button>
- </div>
-
- {/* 修改密码弹窗 */}
- {showPwdModal && (
- <div className="modal">
- <div className="modal-content">
- <h3>修改密码</h3>
- <input
- type="password"
- placeholder="原密码"
- value={oldPassword}
- onChange={(e) => setOldPassword(e.target.value)}
- />
- <input
- type="password"
- placeholder="新密码"
- value={newPassword}
- onChange={(e) => setNewPassword(e.target.value)}
- />
- <input
- type="password"
- placeholder="确认新密码"
- value={confirmPassword}
- onChange={(e) => setConfirmPassword(e.target.value)}
- />
- <div className="modal-buttons">
- <button onClick={handleChangePassword}>确认修改</button>
- <button onClick={() => setShowPwdModal(false)}>取消</button>
- </div>
- </div>
- </div>
- )}
- </div>
- </div>
+ <div>
+ <UserProfileBase onLoadExperienceInfo={loadExperienceInfo} />
+ {userId && <UserLevelExperience userId={userId} />}
</div>
);
};
-export default UserProfile;
-
+export default UserProfile;
\ No newline at end of file
diff --git a/src/pages/UserCenter/UserProfileBase.jsx b/src/pages/UserCenter/UserProfileBase.jsx
new file mode 100644
index 0000000..7a1f726
--- /dev/null
+++ b/src/pages/UserCenter/UserProfileBase.jsx
@@ -0,0 +1,221 @@
+import React, { useEffect, useState } from 'react';
+import axios from 'axios';
+import { useUser } from '../../context/UserContext';
+import { useLocation } from 'wouter';
+
+const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
+
+const UserProfileBase = ({ onLoadExperienceInfo }) => {
+ const { user, loading, logout } = useUser();
+ const [userProfile, setUserProfile] = useState(null);
+ const [error, setError] = useState(null);
+
+ // 修改密码状态
+ const [showPwdModal, setShowPwdModal] = useState(false);
+ const [oldPassword, setOldPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+
+ // 退出登录
+ const [, setLocation] = useLocation();
+
+ useEffect(() => {
+ if (loading) return;
+ if (!user || !user.userId) {
+ setError('未登录或用户信息缺失');
+ setUserProfile(null);
+ return;
+ }
+
+ const fetchUserProfile = async () => {
+ try {
+ setError(null);
+ const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
+ if (!raw) {
+ setError('用户数据为空');
+ setUserProfile(null);
+ return;
+ }
+
+ const profile = {
+ avatarUrl: raw.avatarUrl
+ ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
+ : DEFAULT_AVATAR_URL,
+ nickname: raw.username || '未知用户',
+ email: raw.email || '未填写',
+ gender: raw.gender || '保密',
+ bio: raw.description || '无',
+ interests: raw.hobbies ? raw.hobbies.split(',') : [],
+ level: raw.level || '未知',
+ experience: raw.experience ?? 0,
+ uploadAmount: raw.uploadCount ?? 0,
+ downloadAmount: raw.downloadCount ?? 0,
+ shareRate: raw.shareRate ?? 0,
+ joinedDate: raw.registrationTime,
+ };
+
+ setUserProfile(profile);
+ // 加载经验信息
+ if (onLoadExperienceInfo) onLoadExperienceInfo(user.userId);
+ } catch (err) {
+ setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
+ setUserProfile(null);
+ }
+ };
+
+ fetchUserProfile();
+ }, [user, loading, onLoadExperienceInfo]);
+
+ const handleAvatarUpload = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const { data } = await axios.post(
+ `/echo/user/${user.userId}/uploadAvatar`,
+ formData,
+ { headers: { 'Content-Type': 'multipart/form-data' } }
+ );
+
+ if (data?.avatarUrl) {
+ setUserProfile((prev) => ({
+ ...prev,
+ avatarUrl: `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}`,
+ }));
+ alert('头像上传成功');
+ } else {
+ alert('头像上传成功,但未返回新头像地址');
+ }
+ } catch (err) {
+ console.error('上传失败:', err);
+ alert('头像上传失败,请重试');
+ }
+ };
+
+ const handleLogout = () => {
+ logout();
+ setLocation('/auth'); // 退出后跳转登录页
+ };
+
+ const handleChangePassword = async () => {
+ if (!oldPassword || !newPassword || !confirmPassword) {
+ alert('请填写所有字段');
+ return;
+ }
+ if (newPassword !== confirmPassword) {
+ alert('两次输入的新密码不一致');
+ return;
+ }
+
+ try {
+ await axios.post('/echo/user/password', {
+ user_id: user.userId,
+ old_password: oldPassword,
+ new_password: newPassword,
+ confirm_password: confirmPassword,
+ });
+ alert('密码修改成功,请重新登录');
+ logout();
+ window.location.reload();
+ } catch (err) {
+ alert(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
+ }
+ };
+
+ if (loading) return <p>正在加载用户信息...</p>;
+ if (error) return <p className="error">{error}</p>;
+ if (!userProfile) return null;
+
+ const {
+ avatarUrl,
+ nickname,
+ email,
+ gender,
+ bio,
+ interests,
+ level,
+ experience,
+ uploadAmount,
+ downloadAmount,
+ shareRate,
+ joinedDate,
+ } = userProfile;
+
+ return (
+ <div className="common-card">
+ <div className="right-content">
+ <div className="profile-header">
+ <div className="avatar-wrapper">
+ <img src={avatarUrl} alt={nickname} className="avatar" />
+ <label htmlFor="avatar-upload" className="avatar-upload-label">
+ 上传头像
+ </label>
+ <input
+ type="file"
+ id="avatar-upload"
+ accept="image/*"
+ style={{ display: 'none' }}
+ onChange={handleAvatarUpload}
+ />
+ </div>
+ <h1>{nickname}</h1>
+ </div>
+
+ <div className="profile-details">
+ <p><strong>邮箱:</strong>{email}</p>
+ <p><strong>性别:</strong>{gender}</p>
+ <p><strong>个人简介:</strong>{bio}</p>
+ <p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
+ {/* <p><strong>等级:</strong>{level}</p>
+ <p><strong>经验:</strong>{experience}</p> */}
+ <p><strong>上传量:</strong>{uploadAmount}</p>
+ <p><strong>下载量:</strong>{downloadAmount}</p>
+ <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
+ <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
+
+ {/* 修改密码与退出登录按钮 */}
+ <div className="profile-actions">
+ <button onClick={() => setShowPwdModal(true)}>修改密码</button>
+ <button onClick={handleLogout}>退出登录</button>
+ </div>
+
+ {/* 修改密码弹窗 */}
+ {showPwdModal && (
+ <div className="modal">
+ <div className="modal-content">
+ <h3>修改密码</h3>
+ <input
+ type="password"
+ placeholder="原密码"
+ value={oldPassword}
+ onChange={(e) => setOldPassword(e.target.value)}
+ />
+ <input
+ type="password"
+ placeholder="新密码"
+ value={newPassword}
+ onChange={(e) => setNewPassword(e.target.value)}
+ />
+ <input
+ type="password"
+ placeholder="确认新密码"
+ value={confirmPassword}
+ onChange={(e) => setConfirmPassword(e.target.value)}
+ />
+ <div className="modal-buttons">
+ <button onClick={handleChangePassword}>确认修改</button>
+ <button onClick={() => setShowPwdModal(false)}>取消</button>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default UserProfileBase;
\ No newline at end of file