blob: c90cb631f5513e1b873886d0cabc2494b42eaa85 [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';
Krishya73cd8822025-06-07 15:48:41 +08008
9const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
10
11const UserProfileBase = ({ onLoadExperienceInfo }) => {
22301009207e2db2025-06-09 00:27:28 +080012 const { user, loading, logout ,saveUser} = useUser();
Krishya73cd8822025-06-07 15:48:41 +080013 const [userProfile, setUserProfile] = useState(null);
14 const [error, setError] = useState(null);
15
Krishya73cd8822025-06-07 15:48:41 +080016 const [showPwdModal, setShowPwdModal] = useState(false);
17 const [oldPassword, setOldPassword] = useState('');
18 const [newPassword, setNewPassword] = useState('');
19 const [confirmPassword, setConfirmPassword] = useState('');
20
Krishya73cd8822025-06-07 15:48:41 +080021 const [, setLocation] = useLocation();
22
23 useEffect(() => {
24 if (loading) return;
25 if (!user || !user.userId) {
26 setError('未登录或用户信息缺失');
27 setUserProfile(null);
28 return;
29 }
30
31 const fetchUserProfile = async () => {
32 try {
33 setError(null);
34 const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
35 if (!raw) {
36 setError('用户数据为空');
37 setUserProfile(null);
38 return;
39 }
40
41 const profile = {
42 avatarUrl: raw.avatarUrl
43 ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
44 : DEFAULT_AVATAR_URL,
45 nickname: raw.username || '未知用户',
46 email: raw.email || '未填写',
47 gender: raw.gender || '保密',
48 bio: raw.description || '无',
49 interests: raw.hobbies ? raw.hobbies.split(',') : [],
50 level: raw.level || '未知',
51 experience: raw.experience ?? 0,
52 uploadAmount: raw.uploadCount ?? 0,
53 downloadAmount: raw.downloadCount ?? 0,
54 shareRate: raw.shareRate ?? 0,
55 joinedDate: raw.registrationTime,
56 };
57
58 setUserProfile(profile);
Krishya73cd8822025-06-07 15:48:41 +080059 if (onLoadExperienceInfo) onLoadExperienceInfo(user.userId);
60 } catch (err) {
61 setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
62 setUserProfile(null);
63 }
64 };
65
66 fetchUserProfile();
67 }, [user, loading, onLoadExperienceInfo]);
68
22301009207e2db2025-06-09 00:27:28 +080069const handleAvatarUpload = async (e) => {
70 const file = e.target.files[0];
71 if (!file) return;
Krishya73cd8822025-06-07 15:48:41 +080072
22301009207e2db2025-06-09 00:27:28 +080073 const formData = new FormData();
74 formData.append('file', file);
Krishya73cd8822025-06-07 15:48:41 +080075
22301009207e2db2025-06-09 00:27:28 +080076 try {
77 const { data } = await axios.post(
78 `/echo/user/${user.userId}/uploadAvatar`,
79 formData,
80 { headers: { 'Content-Type': 'multipart/form-data' } }
81 );
Krishya73cd8822025-06-07 15:48:41 +080082
22301009207e2db2025-06-09 00:27:28 +080083 if (data?.avatarUrl) {
84 // 加时间戳避免缓存
85 const newAvatarUrl = `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}?t=${Date.now()}`;
86
87 setUserProfile((prev) => ({
88 ...prev,
89 avatarUrl: newAvatarUrl,
90 }));
91
92 saveUser({
93 ...user,
94 avatarUrl: newAvatarUrl,
95 });
96
97 toast.success('头像上传成功');
98 } else {
99 toast.success('头像上传成功,但未返回新头像地址');
Krishya73cd8822025-06-07 15:48:41 +0800100 }
22301009207e2db2025-06-09 00:27:28 +0800101 } catch (err) {
102 console.error('上传失败:', err);
103 toast.error('头像上传失败,请重试');
104 }
105};
106
Krishya73cd8822025-06-07 15:48:41 +0800107
108 const handleLogout = () => {
109 logout();
223010091e2aea72025-06-08 16:35:54 +0800110 setLocation('/auth');
111 };
112
113 const confirmPasswordChange = () => {
114 confirmAlert({
115 title: '确认修改密码',
116 message: '确定要修改密码吗?修改成功后将自动登出。',
117 buttons: [
118 {
119 label: '确认',
120 onClick: handleChangePassword,
121 },
122 {
123 label: '取消',
124 },
125 ],
126 });
Krishya73cd8822025-06-07 15:48:41 +0800127 };
128
129 const handleChangePassword = async () => {
130 if (!oldPassword || !newPassword || !confirmPassword) {
223010091e2aea72025-06-08 16:35:54 +0800131 toast.error('请填写所有字段');
Krishya73cd8822025-06-07 15:48:41 +0800132 return;
133 }
134 if (newPassword !== confirmPassword) {
223010091e2aea72025-06-08 16:35:54 +0800135 toast.error('两次输入的新密码不一致');
Krishya73cd8822025-06-07 15:48:41 +0800136 return;
137 }
138
139 try {
140 await axios.post('/echo/user/password', {
141 user_id: user.userId,
142 old_password: oldPassword,
143 new_password: newPassword,
144 confirm_password: confirmPassword,
145 });
223010091e2aea72025-06-08 16:35:54 +0800146
147 toast.success('密码修改成功,请重新登录');
Krishya73cd8822025-06-07 15:48:41 +0800148 logout();
223010091e2aea72025-06-08 16:35:54 +0800149 setTimeout(() => {
150 window.location.reload();
151 }, 1500);
Krishya73cd8822025-06-07 15:48:41 +0800152 } catch (err) {
223010091e2aea72025-06-08 16:35:54 +0800153 toast.error(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
Krishya73cd8822025-06-07 15:48:41 +0800154 }
155 };
156
157 if (loading) return <p>正在加载用户信息...</p>;
158 if (error) return <p className="error">{error}</p>;
159 if (!userProfile) return null;
160
161 const {
162 avatarUrl,
163 nickname,
164 email,
165 gender,
166 bio,
167 interests,
168 level,
169 experience,
170 uploadAmount,
171 downloadAmount,
172 shareRate,
173 joinedDate,
174 } = userProfile;
175
176 return (
177 <div className="common-card">
178 <div className="right-content">
179 <div className="profile-header">
180 <div className="avatar-wrapper">
181 <img src={avatarUrl} alt={nickname} className="avatar" />
182 <label htmlFor="avatar-upload" className="avatar-upload-label">
183 上传头像
184 </label>
185 <input
186 type="file"
187 id="avatar-upload"
188 accept="image/*"
189 style={{ display: 'none' }}
190 onChange={handleAvatarUpload}
191 />
192 </div>
193 <h1>{nickname}</h1>
194 </div>
195
196 <div className="profile-details">
197 <p><strong>邮箱:</strong>{email}</p>
198 <p><strong>性别:</strong>{gender}</p>
199 <p><strong>个人简介:</strong>{bio}</p>
200 <p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
Krishya73cd8822025-06-07 15:48:41 +0800201 <p><strong>上传量:</strong>{uploadAmount}</p>
202 <p><strong>下载量:</strong>{downloadAmount}</p>
203 <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
204 <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
205
Krishya73cd8822025-06-07 15:48:41 +0800206 <div className="profile-actions">
207 <button onClick={() => setShowPwdModal(true)}>修改密码</button>
208 <button onClick={handleLogout}>退出登录</button>
209 </div>
210
Krishya73cd8822025-06-07 15:48:41 +0800211 {showPwdModal && (
223010091e2aea72025-06-08 16:35:54 +0800212 <div className="user-modal">
213 <div className="user-modal-content">
Krishya73cd8822025-06-07 15:48:41 +0800214 <h3>修改密码</h3>
215 <input
216 type="password"
217 placeholder="原密码"
218 value={oldPassword}
219 onChange={(e) => setOldPassword(e.target.value)}
220 />
221 <input
222 type="password"
223 placeholder="新密码"
224 value={newPassword}
225 onChange={(e) => setNewPassword(e.target.value)}
226 />
227 <input
228 type="password"
229 placeholder="确认新密码"
230 value={confirmPassword}
231 onChange={(e) => setConfirmPassword(e.target.value)}
232 />
223010091e2aea72025-06-08 16:35:54 +0800233 <div className="user-modal-buttons">
234 <button onClick={confirmPasswordChange}>确认修改</button>
Krishya73cd8822025-06-07 15:48:41 +0800235 <button onClick={() => setShowPwdModal(false)}>取消</button>
236 </div>
237 </div>
238 </div>
239 )}
240 </div>
241 </div>
242 </div>
243 );
244};
245
223010091e2aea72025-06-08 16:35:54 +0800246export default UserProfileBase;