blob: 791baca2dcc4c94bfff86f68d8b6d5a845b461c2 [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 }) => {
12 const { user, loading, logout } = useUser();
13 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
69 const handleAvatarUpload = async (e) => {
70 const file = e.target.files[0];
71 if (!file) return;
72
73 const formData = new FormData();
74 formData.append('file', file);
75
76 try {
77 const { data } = await axios.post(
78 `/echo/user/${user.userId}/uploadAvatar`,
79 formData,
80 { headers: { 'Content-Type': 'multipart/form-data' } }
81 );
82
83 if (data?.avatarUrl) {
84 setUserProfile((prev) => ({
85 ...prev,
86 avatarUrl: `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}`,
87 }));
223010091e2aea72025-06-08 16:35:54 +080088 toast.success('头像上传成功');
Krishya73cd8822025-06-07 15:48:41 +080089 } else {
223010091e2aea72025-06-08 16:35:54 +080090 toast.success('头像上传成功,但未返回新头像地址');
Krishya73cd8822025-06-07 15:48:41 +080091 }
92 } catch (err) {
93 console.error('上传失败:', err);
223010091e2aea72025-06-08 16:35:54 +080094 toast.error('头像上传失败,请重试');
Krishya73cd8822025-06-07 15:48:41 +080095 }
96 };
97
98 const handleLogout = () => {
99 logout();
223010091e2aea72025-06-08 16:35:54 +0800100 setLocation('/auth');
101 };
102
103 const confirmPasswordChange = () => {
104 confirmAlert({
105 title: '确认修改密码',
106 message: '确定要修改密码吗?修改成功后将自动登出。',
107 buttons: [
108 {
109 label: '确认',
110 onClick: handleChangePassword,
111 },
112 {
113 label: '取消',
114 },
115 ],
116 });
Krishya73cd8822025-06-07 15:48:41 +0800117 };
118
119 const handleChangePassword = async () => {
120 if (!oldPassword || !newPassword || !confirmPassword) {
223010091e2aea72025-06-08 16:35:54 +0800121 toast.error('请填写所有字段');
Krishya73cd8822025-06-07 15:48:41 +0800122 return;
123 }
124 if (newPassword !== confirmPassword) {
223010091e2aea72025-06-08 16:35:54 +0800125 toast.error('两次输入的新密码不一致');
Krishya73cd8822025-06-07 15:48:41 +0800126 return;
127 }
128
129 try {
130 await axios.post('/echo/user/password', {
131 user_id: user.userId,
132 old_password: oldPassword,
133 new_password: newPassword,
134 confirm_password: confirmPassword,
135 });
223010091e2aea72025-06-08 16:35:54 +0800136
137 toast.success('密码修改成功,请重新登录');
Krishya73cd8822025-06-07 15:48:41 +0800138 logout();
223010091e2aea72025-06-08 16:35:54 +0800139 setTimeout(() => {
140 window.location.reload();
141 }, 1500);
Krishya73cd8822025-06-07 15:48:41 +0800142 } catch (err) {
223010091e2aea72025-06-08 16:35:54 +0800143 toast.error(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
Krishya73cd8822025-06-07 15:48:41 +0800144 }
145 };
146
147 if (loading) return <p>正在加载用户信息...</p>;
148 if (error) return <p className="error">{error}</p>;
149 if (!userProfile) return null;
150
151 const {
152 avatarUrl,
153 nickname,
154 email,
155 gender,
156 bio,
157 interests,
158 level,
159 experience,
160 uploadAmount,
161 downloadAmount,
162 shareRate,
163 joinedDate,
164 } = userProfile;
165
166 return (
167 <div className="common-card">
168 <div className="right-content">
169 <div className="profile-header">
170 <div className="avatar-wrapper">
171 <img src={avatarUrl} alt={nickname} className="avatar" />
172 <label htmlFor="avatar-upload" className="avatar-upload-label">
173 上传头像
174 </label>
175 <input
176 type="file"
177 id="avatar-upload"
178 accept="image/*"
179 style={{ display: 'none' }}
180 onChange={handleAvatarUpload}
181 />
182 </div>
183 <h1>{nickname}</h1>
184 </div>
185
186 <div className="profile-details">
187 <p><strong>邮箱:</strong>{email}</p>
188 <p><strong>性别:</strong>{gender}</p>
189 <p><strong>个人简介:</strong>{bio}</p>
190 <p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
Krishya73cd8822025-06-07 15:48:41 +0800191 <p><strong>上传量:</strong>{uploadAmount}</p>
192 <p><strong>下载量:</strong>{downloadAmount}</p>
193 <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
194 <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
195
Krishya73cd8822025-06-07 15:48:41 +0800196 <div className="profile-actions">
197 <button onClick={() => setShowPwdModal(true)}>修改密码</button>
198 <button onClick={handleLogout}>退出登录</button>
199 </div>
200
Krishya73cd8822025-06-07 15:48:41 +0800201 {showPwdModal && (
223010091e2aea72025-06-08 16:35:54 +0800202 <div className="user-modal">
203 <div className="user-modal-content">
Krishya73cd8822025-06-07 15:48:41 +0800204 <h3>修改密码</h3>
205 <input
206 type="password"
207 placeholder="原密码"
208 value={oldPassword}
209 onChange={(e) => setOldPassword(e.target.value)}
210 />
211 <input
212 type="password"
213 placeholder="新密码"
214 value={newPassword}
215 onChange={(e) => setNewPassword(e.target.value)}
216 />
217 <input
218 type="password"
219 placeholder="确认新密码"
220 value={confirmPassword}
221 onChange={(e) => setConfirmPassword(e.target.value)}
222 />
223010091e2aea72025-06-08 16:35:54 +0800223 <div className="user-modal-buttons">
224 <button onClick={confirmPasswordChange}>确认修改</button>
Krishya73cd8822025-06-07 15:48:41 +0800225 <button onClick={() => setShowPwdModal(false)}>取消</button>
226 </div>
227 </div>
228 </div>
229 )}
230 </div>
231 </div>
232 </div>
233 );
234};
235
223010091e2aea72025-06-08 16:35:54 +0800236export default UserProfileBase;