blob: dd28dc236d1e4e335a29daeef50dbcf3a77d665a [file] [log] [blame]
Krishya73cd8822025-06-07 15:48:41 +08001import React, { useEffect, useState } from 'react';
2import axios from 'axios';
3import { useUser } from '../../context/UserContext';
4import { useLocation } from 'wouter';
223010091e2aea72025-06-08 16:35:54 +08005import toast from 'react-hot-toast';
6import { confirmAlert } from 'react-confirm-alert';
7import 'react-confirm-alert/src/react-confirm-alert.css';
Krishya34493be2025-06-09 22:45:46 +08008import UserLevelExperience from './UserLevelExperience';
9
Krishya73cd8822025-06-07 15:48:41 +080010
11const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
12
13const UserProfileBase = ({ onLoadExperienceInfo }) => {
22301009207e2db2025-06-09 00:27:28 +080014 const { user, loading, logout ,saveUser} = useUser();
Krishya73cd8822025-06-07 15:48:41 +080015 const [userProfile, setUserProfile] = useState(null);
16 const [error, setError] = useState(null);
17
Krishya73cd8822025-06-07 15:48:41 +080018 const [showPwdModal, setShowPwdModal] = useState(false);
19 const [oldPassword, setOldPassword] = useState('');
20 const [newPassword, setNewPassword] = useState('');
21 const [confirmPassword, setConfirmPassword] = useState('');
22
Krishya73cd8822025-06-07 15:48:41 +080023 const [, setLocation] = useLocation();
24
25 useEffect(() => {
26 if (loading) return;
27 if (!user || !user.userId) {
28 setError('未登录或用户信息缺失');
29 setUserProfile(null);
30 return;
31 }
32
33 const fetchUserProfile = async () => {
34 try {
35 setError(null);
36 const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
37 if (!raw) {
38 setError('用户数据为空');
39 setUserProfile(null);
40 return;
41 }
42
43 const profile = {
44 avatarUrl: raw.avatarUrl
45 ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
46 : DEFAULT_AVATAR_URL,
47 nickname: raw.username || '未知用户',
48 email: raw.email || '未填写',
49 gender: raw.gender || '保密',
50 bio: raw.description || '无',
51 interests: raw.hobbies ? raw.hobbies.split(',') : [],
52 level: raw.level || '未知',
53 experience: raw.experience ?? 0,
54 uploadAmount: raw.uploadCount ?? 0,
55 downloadAmount: raw.downloadCount ?? 0,
56 shareRate: raw.shareRate ?? 0,
57 joinedDate: raw.registrationTime,
58 };
59
60 setUserProfile(profile);
Krishya73cd8822025-06-07 15:48:41 +080061 if (onLoadExperienceInfo) onLoadExperienceInfo(user.userId);
62 } catch (err) {
63 setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
64 setUserProfile(null);
65 }
66 };
67
68 fetchUserProfile();
69 }, [user, loading, onLoadExperienceInfo]);
70
22301009207e2db2025-06-09 00:27:28 +080071const handleAvatarUpload = async (e) => {
72 const file = e.target.files[0];
73 if (!file) return;
Krishya73cd8822025-06-07 15:48:41 +080074
22301009207e2db2025-06-09 00:27:28 +080075 const formData = new FormData();
76 formData.append('file', file);
Krishya73cd8822025-06-07 15:48:41 +080077
22301009207e2db2025-06-09 00:27:28 +080078 try {
79 const { data } = await axios.post(
80 `/echo/user/${user.userId}/uploadAvatar`,
81 formData,
82 { headers: { 'Content-Type': 'multipart/form-data' } }
83 );
Krishya73cd8822025-06-07 15:48:41 +080084
22301009207e2db2025-06-09 00:27:28 +080085 if (data?.avatarUrl) {
86 // 加时间戳避免缓存
87 const newAvatarUrl = `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}?t=${Date.now()}`;
88
89 setUserProfile((prev) => ({
90 ...prev,
91 avatarUrl: newAvatarUrl,
92 }));
93
94 saveUser({
95 ...user,
96 avatarUrl: newAvatarUrl,
97 });
98
99 toast.success('头像上传成功');
100 } else {
101 toast.success('头像上传成功,但未返回新头像地址');
Krishya73cd8822025-06-07 15:48:41 +0800102 }
22301009207e2db2025-06-09 00:27:28 +0800103 } catch (err) {
104 console.error('上传失败:', err);
105 toast.error('头像上传失败,请重试');
106 }
107};
108
Krishya73cd8822025-06-07 15:48:41 +0800109
110 const handleLogout = () => {
111 logout();
223010091e2aea72025-06-08 16:35:54 +0800112 setLocation('/auth');
113 };
114
115 const confirmPasswordChange = () => {
116 confirmAlert({
117 title: '确认修改密码',
118 message: '确定要修改密码吗?修改成功后将自动登出。',
119 buttons: [
120 {
121 label: '确认',
122 onClick: handleChangePassword,
123 },
124 {
125 label: '取消',
126 },
127 ],
128 });
Krishya73cd8822025-06-07 15:48:41 +0800129 };
130
131 const handleChangePassword = async () => {
132 if (!oldPassword || !newPassword || !confirmPassword) {
223010091e2aea72025-06-08 16:35:54 +0800133 toast.error('请填写所有字段');
Krishya73cd8822025-06-07 15:48:41 +0800134 return;
135 }
136 if (newPassword !== confirmPassword) {
223010091e2aea72025-06-08 16:35:54 +0800137 toast.error('两次输入的新密码不一致');
Krishya73cd8822025-06-07 15:48:41 +0800138 return;
139 }
140
141 try {
142 await axios.post('/echo/user/password', {
143 user_id: user.userId,
144 old_password: oldPassword,
145 new_password: newPassword,
146 confirm_password: confirmPassword,
147 });
223010091e2aea72025-06-08 16:35:54 +0800148
149 toast.success('密码修改成功,请重新登录');
Krishya73cd8822025-06-07 15:48:41 +0800150 logout();
223010091e2aea72025-06-08 16:35:54 +0800151 setTimeout(() => {
152 window.location.reload();
153 }, 1500);
Krishya73cd8822025-06-07 15:48:41 +0800154 } catch (err) {
223010091e2aea72025-06-08 16:35:54 +0800155 toast.error(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
Krishya73cd8822025-06-07 15:48:41 +0800156 }
157 };
158
159 if (loading) return <p>正在加载用户信息...</p>;
160 if (error) return <p className="error">{error}</p>;
161 if (!userProfile) return null;
162
163 const {
164 avatarUrl,
165 nickname,
166 email,
167 gender,
168 bio,
169 interests,
170 level,
171 experience,
172 uploadAmount,
173 downloadAmount,
174 shareRate,
175 joinedDate,
176 } = userProfile;
177
178 return (
179 <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 />
194 </div>
195 <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>
Krishya73cd8822025-06-07 15:48:41 +0800203 <p><strong>上传量:</strong>{uploadAmount}</p>
204 <p><strong>下载量:</strong>{downloadAmount}</p>
205 <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
206 <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
207
Krishya34493be2025-06-09 22:45:46 +0800208
Krishya73cd8822025-06-07 15:48:41 +0800209 <div className="profile-actions">
210 <button onClick={() => setShowPwdModal(true)}>修改密码</button>
211 <button onClick={handleLogout}>退出登录</button>
212 </div>
213
Krishya73cd8822025-06-07 15:48:41 +0800214 {showPwdModal && (
223010091e2aea72025-06-08 16:35:54 +0800215 <div className="user-modal">
216 <div className="user-modal-content">
Krishya73cd8822025-06-07 15:48:41 +0800217 <h3>修改密码</h3>
218 <input
219 type="password"
220 placeholder="原密码"
221 value={oldPassword}
222 onChange={(e) => setOldPassword(e.target.value)}
223 />
224 <input
225 type="password"
226 placeholder="新密码"
227 value={newPassword}
228 onChange={(e) => setNewPassword(e.target.value)}
229 />
230 <input
231 type="password"
232 placeholder="确认新密码"
233 value={confirmPassword}
234 onChange={(e) => setConfirmPassword(e.target.value)}
235 />
223010091e2aea72025-06-08 16:35:54 +0800236 <div className="user-modal-buttons">
237 <button onClick={confirmPasswordChange}>确认修改</button>
Krishya73cd8822025-06-07 15:48:41 +0800238 <button onClick={() => setShowPwdModal(false)}>取消</button>
239 </div>
240 </div>
241 </div>
242 )}
243 </div>
244 </div>
245 </div>
246 );
247};
248
223010091e2aea72025-06-08 16:35:54 +0800249export default UserProfileBase;