blob: c6e80e09ea2365efe9d3f5306afa2056d1b33c68 [file] [log] [blame]
2230100977253112025-04-15 21:30:39 +08001import React, { useEffect, useState } from 'react';
2import axios from 'axios';
3import './UserProfile.css';
22301009df48f962025-06-05 13:40:44 +08004import { useUser } from '../../context/UserContext';
Krishya6bf199c2025-06-06 21:14:23 +08005import { useLocation } from 'wouter';
6
22301009df48f962025-06-05 13:40:44 +08007const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
2230100977253112025-04-15 21:30:39 +08008
223010097ff51f22025-04-15 21:35:28 +08009const UserProfile = () => {
Krishya6bf199c2025-06-06 21:14:23 +080010 const { user, loading, logout } = useUser();
2230100977253112025-04-15 21:30:39 +080011 const [userProfile, setUserProfile] = useState(null);
Krishya6bf199c2025-06-06 21:14:23 +080012 const [experienceInfo, setExperienceInfo] = useState(null);
2230100977253112025-04-15 21:30:39 +080013 const [error, setError] = useState(null);
14
Krishya6bf199c2025-06-06 21:14:23 +080015 // 修改密码状态
16 const [showPwdModal, setShowPwdModal] = useState(false);
17 const [oldPassword, setOldPassword] = useState('');
18 const [newPassword, setNewPassword] = useState('');
19 const [confirmPassword, setConfirmPassword] = useState('');
20
21 // 退出登录
22 const [, setLocation] = useLocation();
23
2230100977253112025-04-15 21:30:39 +080024 useEffect(() => {
22301009df48f962025-06-05 13:40:44 +080025 if (loading) return;
2230100937ebec12025-06-03 21:17:04 +080026 if (!user || !user.userId) {
27 setError('未登录或用户信息缺失');
28 setUserProfile(null);
29 return;
30 }
31
2230100977253112025-04-15 21:30:39 +080032 const fetchUserProfile = async () => {
33 try {
2230100937ebec12025-06-03 21:17:04 +080034 setError(null);
22301009df48f962025-06-05 13:40:44 +080035 const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
2230100937ebec12025-06-03 21:17:04 +080036 if (!raw) {
37 setError('用户数据为空');
38 setUserProfile(null);
39 return;
2230100977253112025-04-15 21:30:39 +080040 }
2230100937ebec12025-06-03 21:17:04 +080041
42 const profile = {
Krishya6bf199c2025-06-06 21:14:23 +080043 avatarUrl: raw.avatarUrl
44 ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
45 : DEFAULT_AVATAR_URL,
2230100937ebec12025-06-03 21:17:04 +080046 nickname: raw.username || '未知用户',
47 email: raw.email || '未填写',
48 gender: raw.gender || '保密',
49 bio: raw.description || '无',
50 interests: raw.hobbies ? raw.hobbies.split(',') : [],
51 level: raw.level || '未知',
52 experience: raw.experience ?? 0,
22301009df48f962025-06-05 13:40:44 +080053 uploadAmount: raw.uploadCount ?? 0,
54 downloadAmount: raw.downloadCount ?? 0,
55 shareRate: raw.shareRate ?? 0,
56 joinedDate: raw.registrationTime,
2230100937ebec12025-06-03 21:17:04 +080057 };
58
59 setUserProfile(profile);
2230100977253112025-04-15 21:30:39 +080060 } catch (err) {
22301009df48f962025-06-05 13:40:44 +080061 setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
2230100937ebec12025-06-03 21:17:04 +080062 setUserProfile(null);
2230100977253112025-04-15 21:30:39 +080063 }
64 };
65
Krishya6bf199c2025-06-06 21:14:23 +080066 const fetchExperienceInfo = async () => {
67 try {
68 const { data } = await axios.get('/echo/level/getExperience', {
69 params: { user_id: user.userId },
70 });
71 setExperienceInfo(data);
72 } catch (err) {
73 console.error('经验信息获取失败:', err);
74 }
75 };
76
2230100977253112025-04-15 21:30:39 +080077 fetchUserProfile();
Krishya6bf199c2025-06-06 21:14:23 +080078 fetchExperienceInfo();
2230100937ebec12025-06-03 21:17:04 +080079 }, [user, loading]);
2230100977253112025-04-15 21:30:39 +080080
22301009df48f962025-06-05 13:40:44 +080081 const handleAvatarUpload = async (e) => {
82 const file = e.target.files[0];
83 if (!file) return;
84
85 const formData = new FormData();
86 formData.append('file', file);
87
88 try {
Krishya6bf199c2025-06-06 21:14:23 +080089 const { data } = await axios.post(
90 `/echo/user/${user.userId}/uploadAvatar`,
91 formData,
92 { headers: { 'Content-Type': 'multipart/form-data' } }
93 );
22301009df48f962025-06-05 13:40:44 +080094
95 if (data?.avatarUrl) {
96 setUserProfile((prev) => ({
97 ...prev,
98 avatarUrl: `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}`,
99 }));
100 alert('头像上传成功');
101 } else {
102 alert('头像上传成功,但未返回新头像地址');
103 }
104 } catch (err) {
105 console.error('上传失败:', err);
106 alert('头像上传失败,请重试');
107 }
108 };
109
Krishya6bf199c2025-06-06 21:14:23 +0800110 const handleLogout = () => {
111 logout();
112 setLocation('/auth'); // 退出后跳转登录页
113 // window.location.reload(); // 或跳转登录页
114 };
115
116 const handleChangePassword = async () => {
117 if (!oldPassword || !newPassword || !confirmPassword) {
118 alert('请填写所有字段');
119 return;
120 }
121 if (newPassword !== confirmPassword) {
122 alert('两次输入的新密码不一致');
123 return;
124 }
125
126 try {
127 // await axios.post('/echo/user/password', {
128 // user_id: user.userId,
129 // oldPassword,
130 // newPassword,
131 // });
132 await axios.post('/echo/user/password', {
133 user_id: user.userId,
134 old_password: oldPassword,
135 new_password: newPassword,
136 confirm_password: confirmPassword,
137 });
138 alert('密码修改成功,请重新登录');
139 logout();
140 window.location.reload();
141 } catch (err) {
142 alert(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
143 }
144 };
145
2230100937ebec12025-06-03 21:17:04 +0800146 if (loading) return <p>正在加载用户信息...</p>;
147 if (error) return <p className="error">{error}</p>;
148 if (!userProfile) return null;
2230100977253112025-04-15 21:30:39 +0800149
22301009df48f962025-06-05 13:40:44 +0800150 const {
151 avatarUrl,
152 nickname,
153 email,
154 gender,
155 bio,
156 interests,
157 level,
158 experience,
159 uploadAmount,
160 downloadAmount,
161 shareRate,
162 joinedDate,
163 } = userProfile;
164
Krishya6bf199c2025-06-06 21:14:23 +0800165 const progressPercent = experienceInfo
166 ? Math.min(
167 100,
168 ((experienceInfo.current_experience || 0) /
169 (experienceInfo.next_level_experience || 1)) *
170 100
171 ).toFixed(2)
172 : 0;
173
174 const expToNextLevel = experienceInfo
175 ? (experienceInfo.next_level_experience - experienceInfo.current_experience)
176 : null;
177
2230100977253112025-04-15 21:30:39 +0800178 return (
Krishyac6b24832025-06-05 20:13:20 +0800179 <div className="common-card">
180 <div className="right-content">
181 <div className="profile-header">
182 <div className="avatar-wrapper">
183 <img src={avatarUrl} alt={nickname} className="avatar" />
184 <label htmlFor="avatar-upload" className="avatar-upload-label">
185 上传头像
186 </label>
187 <input
188 type="file"
189 id="avatar-upload"
190 accept="image/*"
191 style={{ display: 'none' }}
192 onChange={handleAvatarUpload}
193 />
2230100937ebec12025-06-03 21:17:04 +0800194 </div>
Krishyac6b24832025-06-05 20:13:20 +0800195 <h1>{nickname}</h1>
196 </div>
197
198 <div className="profile-details">
199 <p><strong>邮箱:</strong>{email}</p>
200 <p><strong>性别:</strong>{gender}</p>
201 <p><strong>个人简介:</strong>{bio}</p>
202 <p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
203 <p><strong>等级:</strong>{level}</p>
204 <p><strong>经验:</strong>{experience}</p>
205 <p><strong>上传量:</strong>{uploadAmount}</p>
206 <p><strong>下载量:</strong>{downloadAmount}</p>
207 <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
208 <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
Krishya6bf199c2025-06-06 21:14:23 +0800209
210 {experienceInfo && (
211 <>
212 <p><strong>距离下一等级还需:</strong>{expToNextLevel} 经验值</p>
213 <div className="exp-bar-wrapper">
214 <div className="exp-bar" style={{ width: `${progressPercent}%` }} />
215 </div>
216 <p className="exp-progress-text">{progressPercent}%</p>
217 </>
218 )}
219
220 {/* 修改密码与退出登录按钮 */}
221 <div className="profile-actions">
222 <button onClick={() => setShowPwdModal(true)}>修改密码</button>
223 <button onClick={handleLogout}>退出登录</button>
224 </div>
225
226 {/* 修改密码弹窗 */}
227 {showPwdModal && (
228 <div className="modal">
229 <div className="modal-content">
230 <h3>修改密码</h3>
231 <input
232 type="password"
233 placeholder="原密码"
234 value={oldPassword}
235 onChange={(e) => setOldPassword(e.target.value)}
236 />
237 <input
238 type="password"
239 placeholder="新密码"
240 value={newPassword}
241 onChange={(e) => setNewPassword(e.target.value)}
242 />
243 <input
244 type="password"
245 placeholder="确认新密码"
246 value={confirmPassword}
247 onChange={(e) => setConfirmPassword(e.target.value)}
248 />
249 <div className="modal-buttons">
250 <button onClick={handleChangePassword}>确认修改</button>
251 <button onClick={() => setShowPwdModal(false)}>取消</button>
252 </div>
253 </div>
254 </div>
255 )}
Krishyac0f7e9b2025-04-22 15:28:28 +0800256 </div>
257 </div>
2230100977253112025-04-15 21:30:39 +0800258 </div>
259 );
260};
261
Krishya6bf199c2025-06-06 21:14:23 +0800262export default UserProfile;
263