blob: 4b25a985cfbd36fd44d5088e8cdf593f6397b25a [file] [log] [blame]
ybt3ec62e42025-06-11 22:46:22 +08001import React, { useState, useEffect, useRef } from 'react';
ybtbac75f22025-06-08 22:31:15 +08002import {
3 Typography,
4 Card,
5 Avatar,
6 Statistic,
7 Row,
8 Col,
9 Tag,
10 Progress,
11 Button,
12 Divider,
13 Space,
14 Tooltip,
15 message,
16 Modal,
17 Form,
18 Input,
19 Upload
20} from 'antd';
21import {
22 UserOutlined,
23 EditOutlined,
24 UploadOutlined,
25 DownloadOutlined,
26 TrophyOutlined,
27 HeartOutlined,
28 WarningOutlined,
29 CheckCircleOutlined,
30 SyncOutlined,
31 GiftOutlined,
32 SettingOutlined,
33 CameraOutlined
34} from '@ant-design/icons';
35import { useAuth } from '@/features/auth/contexts/AuthContext';
ybt3ec62e42025-06-11 22:46:22 +080036import { updateUserProfile, uploadAvatar } from '@/api/user';
ybtda5978b2025-05-31 15:58:05 +080037
ybtbac75f22025-06-08 22:31:15 +080038const { Title, Text, Paragraph } = Typography;
ybtda5978b2025-05-31 15:58:05 +080039
ybtbac75f22025-06-08 22:31:15 +080040const ProfilePage = () => {
ybt3ec62e42025-06-11 22:46:22 +080041 const { user, updateUserInfo } = useAuth();
ybtbac75f22025-06-08 22:31:15 +080042 const [loading, setLoading] = useState(false);
43 const [editModalVisible, setEditModalVisible] = useState(false);
44 const [form] = Form.useForm();
ybt3ec62e42025-06-11 22:46:22 +080045 const hasUpdatedRef = useRef(false); // 用来追踪是否已经更新过用户信息
ybtbac75f22025-06-08 22:31:15 +080046
47 // PT站统计数据
48 const [ptStats, setPtStats] = useState({
ybt71fb2642025-06-09 00:29:36 +080049 uploadSize: 0, // GB
50 downloadSize: 0, // GB
51 ratio: 0,
52 points: 0, // 确保初始值为0
53 userClass: '新用户',
54 level: 1,
55 seedingCount: 0,
56 leechingCount: 0,
57 completedCount: 0,
58 invites: 0,
ybtbac75f22025-06-08 22:31:15 +080059 warnings: 0,
ybt0d010e52025-06-09 00:29:36 +080060 hitAndRuns: 0
ybtbac75f22025-06-08 22:31:15 +080061 });
62
ybt3ec62e42025-06-11 22:46:22 +080063 // 页面加载时更新用户信息
ybtbac75f22025-06-08 22:31:15 +080064 useEffect(() => {
ybt3ec62e42025-06-11 22:46:22 +080065 const handleUserInfoUpdate = async () => {
66 if (user?.username && updateUserInfo && !hasUpdatedRef.current) {
67 console.log('页面加载,正在更新用户信息...');
68 hasUpdatedRef.current = true; // 标记为已更新
69 try {
70 await updateUserInfo(user.username);
71 console.log('用户信息更新成功');
72 } catch (error) {
73 console.error('更新用户信息失败:', error);
74 hasUpdatedRef.current = false; // 如果失败,重置标记以便重试
75 }
76 }
77 };
78
79 handleUserInfoUpdate();
80 }, [user?.username, updateUserInfo]); // 依赖用户名和更新函数
81
82 // 用户信息更新后,从用户数据中提取统计信息
83 useEffect(() => {
84 if (user) {
85 console.log('用户数据变化,更新统计信息:', user);
86 updateStatsFromUser(user);
ybtbac75f22025-06-08 22:31:15 +080087 }
88 }, [user]);
89
ybt3ec62e42025-06-11 22:46:22 +080090 // 从用户数据中提取统计信息
91 const updateStatsFromUser = (userData) => {
92 if (userData) {
93 const uploaded = Number(userData.uploaded) || 0;
94 const downloaded = Number(userData.downloaded) || 0;
95 const shareRatio = Number(userData.shareRatio) || 0;
ybt71fb2642025-06-09 00:29:36 +080096
ybt3ec62e42025-06-11 22:46:22 +080097
98 const newStats = {
99 uploadSize: uploaded, // 保存原始字节值
100 downloadSize: downloaded, // 保存原始字节值
101 ratio: shareRatio,
102 points: Number(userData.points) || 0,
103 userClass: `用户等级`,
104 level: Number(userData.level) || 1
105 };
106
107 console.log('原始数据:', { uploaded, downloaded, shareRatio });
108 console.log('更新后的统计数据:', newStats);
109 setPtStats(newStats);
ybtbac75f22025-06-08 22:31:15 +0800110 }
111 };
112
ybt3ec62e42025-06-11 22:46:22 +0800113
ybtbac75f22025-06-08 22:31:15 +0800114 // 格式化文件大小
ybt3ec62e42025-06-11 22:46:22 +0800115 const formatSize = (sizeInBytes) => {
116 if (sizeInBytes === 0) {
117 return '0 B';
ybtbac75f22025-06-08 22:31:15 +0800118 }
ybt3ec62e42025-06-11 22:46:22 +0800119
120 const units = ['B', 'KB', 'MB', 'GB', 'TB'];
121 const k = 1024;
122 let bytes = Math.abs(sizeInBytes);
123 let unitIndex = 0;
124
125 // 找到合适的单位
126 while (bytes >= k && unitIndex < units.length - 1) {
127 bytes /= k;
128 unitIndex++;
129 }
130
131 // 根据大小决定小数位数
132 let decimals = 2;
133 if (bytes >= 100) {
134 decimals = 0; // 大于等于100时不显示小数
135 } else if (bytes >= 10) {
136 decimals = 1; // 10-99时显示1位小数
137 }
138
139 return `${bytes.toFixed(decimals)} ${units[unitIndex]}`;
ybtbac75f22025-06-08 22:31:15 +0800140 };
141
142 // 获取分享率颜色
143 const getRatioColor = (ratio) => {
144 if (ratio >= 2.0) return '#52c41a'; // 绿色
145 if (ratio >= 1.0) return '#1890ff'; // 蓝色
146 if (ratio >= 0.5) return '#faad14'; // 橙色
147 return '#f5222d'; // 红色
148 };
149
150 // 获取用户等级颜色
151 const getUserClassColor = (userClass) => {
152 const classColors = {
153 'User': 'default',
154 'Power User': 'blue',
155 'Elite User': 'purple',
156 'Crazy User': 'gold',
157 'Insane User': 'red',
158 'Veteran User': 'green',
159 'Extreme User': 'volcano',
160 'VIP': 'magenta'
161 };
162 return classColors[userClass] || 'default';
163 };
164
165 // 显示编辑对话框
166 const showEditModal = () => {
167 form.setFieldsValue({
168 username: user?.username,
169 email: user?.email
170 });
171 setEditModalVisible(true);
172 };
173
174 // 处理编辑提交
175 const handleEditSubmit = async () => {
176 try {
177 const values = await form.validateFields();
178 setLoading(true);
179
180 const response = await updateUserProfile({
181 username: user.username,
182 ...values
183 });
184
185 if (response && response.data) {
186 message.success('资料更新成功');
187 setEditModalVisible(false);
ybt3ec62e42025-06-11 22:46:22 +0800188 // 资料更新成功后刷新用户信息
189 try {
190 await updateUserInfo(user.username);
191 console.log('资料更新后用户信息已刷新');
192 } catch (error) {
193 console.error('资料更新后刷新用户信息失败:', error);
194 }
ybtbac75f22025-06-08 22:31:15 +0800195 } else {
196 message.error('更新失败,请重试');
197 }
198
199 } catch (error) {
200 console.error('更新失败:', error);
201 message.error(error.message || '更新失败,请重试');
202 } finally {
203 setLoading(false);
204 }
205 };
206
207 // 头像上传处理
208 const handleAvatarUpload = async (file) => {
209 const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
210 if (!isJpgOrPng) {
211 message.error('只能上传 JPG/PNG 格式的图片!');
212 return false;
213 }
214 const isLt2M = file.size / 1024 / 1024 < 2;
215 if (!isLt2M) {
216 message.error('图片大小不能超过 2MB!');
217 return false;
218 }
219
220 try {
221 const formData = new FormData();
222 formData.append('avatar', file);
223 formData.append('username', user.username);
224
225 const response = await uploadAvatar(formData);
226 if (response && response.data) {
227 message.success('头像上传成功');
ybt3ec62e42025-06-11 22:46:22 +0800228 // 头像上传成功后更新用户信息
229 try {
230 await updateUserInfo(user.username);
231 console.log('头像上传后用户信息已更新');
232 } catch (error) {
233 console.error('头像上传后更新用户信息失败:', error);
234 }
ybtbac75f22025-06-08 22:31:15 +0800235 } else {
236 message.error('头像上传失败');
237 }
238 } catch (error) {
239 console.error('头像上传失败:', error);
240 message.error('头像上传失败');
241 }
ybtda5978b2025-05-31 15:58:05 +0800242
ybtbac75f22025-06-08 22:31:15 +0800243 return false; // 阻止默认上传行为
244 };
245
246 // 头像上传配置
247 const uploadProps = {
248 name: 'avatar',
249 showUploadList: false,
250 beforeUpload: handleAvatarUpload,
251 };
252
253 return (
254 <div className="space-y-6">
255 <div className="flex justify-between items-center">
256 <Title level={2}>个人资料</Title>
ybt3ec62e42025-06-11 22:46:22 +0800257 <Space>
258 <Button
259 icon={<SyncOutlined />}
260 onClick={async () => {
261 if (!user?.username) {
262 message.error('用户信息不完整,请重新登录');
263 return;
264 }
265
266 console.log('手动刷新用户信息...');
267 setLoading(true);
268
269 try {
270 // 重置标记,允许手动更新
271 hasUpdatedRef.current = false;
272
273 // 调用getUserInfo更新用户信息
274 await updateUserInfo(user.username);
275 console.log('用户信息更新成功');
276
277 // 统计信息会通过 useEffect 自动更新
278
279 message.success('用户信息已更新');
280 } catch (error) {
281 console.error('刷新用户信息失败:', error);
282 message.error('刷新失败: ' + (error.message || '网络错误'));
283 } finally {
284 setLoading(false);
285 }
286 }}
287 loading={loading}
288 >
289 刷新信息
290 </Button>
291 <Button
292 type="primary"
293 icon={<EditOutlined />}
294 onClick={showEditModal}
295 >
296 编辑资料
297 </Button>
298 </Space>
ybtbac75f22025-06-08 22:31:15 +0800299 </div>
300
301 <Row gutter={[24, 24]}>
302 {/* 用户基本信息卡片 */}
303 <Col xs={24} lg={8}>
304 <Card>
305 <div className="text-center">
306 <div className="relative inline-block">
307 <Avatar
308 size={120}
309 src={user?.avatar}
310 icon={<UserOutlined />}
311 className="mb-4"
312 />
313 <Upload {...uploadProps}>
314 <Button
315 type="primary"
316 shape="circle"
317 icon={<CameraOutlined />}
318 size="small"
319 className="absolute bottom-0 right-0"
320 />
321 </Upload>
322 </div>
323
324 <Title level={3} className="mb-2">{user?.username || '用户'}</Title>
325
326 <Space direction="vertical" className="w-full">
327 <Tag
328 color={getUserClassColor(ptStats.userClass)}
329 className="text-lg px-3 py-1"
330 >
ybt3ec62e42025-06-11 22:46:22 +0800331 用户等级 {user?.level || ptStats.level}
ybtbac75f22025-06-08 22:31:15 +0800332 </Tag>
333
334 <Text type="secondary">邮箱:{user?.email || '未设置'}</Text>
ybtbac75f22025-06-08 22:31:15 +0800335 </Space>
336 </div>
337 </Card>
338 </Col>
339
340 {/* PT站统计信息 */}
341 <Col xs={24} lg={16}>
342 <Card title={
343 <Space>
344 <TrophyOutlined />
345 <span>PT站统计</span>
346 </Space>
347 }>
348 <Row gutter={[16, 16]}>
349 {/* 上传下载统计 */}
350 <Col xs={12} sm={6}>
351 <Statistic
352 title="上传量"
353 value={formatSize(ptStats.uploadSize)}
354 prefix={<UploadOutlined style={{ color: '#52c41a' }} />}
355 valueStyle={{ color: '#52c41a' }}
356 />
357 </Col>
358 <Col xs={12} sm={6}>
359 <Statistic
360 title="下载量"
361 value={formatSize(ptStats.downloadSize)}
362 prefix={<DownloadOutlined style={{ color: '#1890ff' }} />}
363 valueStyle={{ color: '#1890ff' }}
364 />
365 </Col>
366 <Col xs={12} sm={6}>
367 <Statistic
368 title="分享率"
ybt71fb2642025-06-09 00:29:36 +0800369 value={ptStats.ratio.toFixed(2)}
ybtbac75f22025-06-08 22:31:15 +0800370 valueStyle={{ color: getRatioColor(ptStats.ratio) }}
371 />
372 </Col>
373 <Col xs={12} sm={6}>
374 <Statistic
375 title="积分"
376 value={ptStats.points}
377 prefix={<HeartOutlined style={{ color: '#eb2f96' }} />}
378 valueStyle={{ color: '#eb2f96' }}
379 />
380 </Col>
381 </Row>
382 </Card>
383 </Col>
384 </Row>
385
386 {/* 分享率进度条 */}
387 <Card title="分享率分析">
388 <Row gutter={[24, 16]}>
389 <Col xs={24} md={12}>
390 <div className="mb-4">
ybt71fb2642025-06-09 00:29:36 +0800391 <Text strong>当前分享率:{ptStats.ratio.toFixed(2)}</Text>
ybtbac75f22025-06-08 22:31:15 +0800392 <Progress
ybt3ec62e42025-06-11 22:46:22 +0800393 percent={ptStats.ratio >= 999 ? 100 : Math.min(ptStats.ratio * 50, 100)} // 转换为百分比显示
ybtbac75f22025-06-08 22:31:15 +0800394 strokeColor={getRatioColor(ptStats.ratio)}
ybt71fb2642025-06-09 00:29:36 +0800395 format={() => ptStats.ratio.toFixed(2)}
ybtbac75f22025-06-08 22:31:15 +0800396 />
397 </div>
398 <Space wrap>
ybt71fb2642025-06-09 00:29:36 +0800399 <Tag color="green">≥2.00 优秀</Tag>
400 <Tag color="blue">≥1.00 良好</Tag>
401 <Tag color="orange">≥0.50 及格</Tag>
402 <Tag color="red">&lt;0.50 需要改善</Tag>
ybtbac75f22025-06-08 22:31:15 +0800403 </Space>
404 </Col>
405 <Col xs={24} md={12}>
406 <div className="space-y-2">
407 <Paragraph>
408 <Text strong>分享率说明:</Text>
409 </Paragraph>
410 <Paragraph type="secondary" className="text-sm">
411 分享率 = 上传量 ÷ 下载量<br/>
412 保持良好的分享率有助于维护账号状态<br/>
413 建议长期做种热门资源提升分享率<br/>
414 分享率过低可能导致账号受限
415 </Paragraph>
416 </div>
417 </Col>
418 </Row>
419 </Card>
420
421 {/* 编辑资料对话框 */}
422 <Modal
423 title="编辑个人资料"
424 open={editModalVisible}
425 onOk={handleEditSubmit}
426 onCancel={() => setEditModalVisible(false)}
427 confirmLoading={loading}
428 okText="保存"
429 cancelText="取消"
430 >
431 <Form form={form} layout="vertical">
432 <Form.Item
433 name="username"
434 label="用户名"
435 rules={[{ required: true, message: '请输入用户名' }]}
436 >
437 <Input placeholder="请输入用户名" />
438 </Form.Item>
439 <Form.Item
440 name="email"
441 label="邮箱"
442 rules={[
443 { required: true, message: '请输入邮箱' },
444 { type: 'email', message: '请输入有效的邮箱地址' }
445 ]}
446 >
447 <Input placeholder="请输入邮箱" />
448 </Form.Item>
449 </Form>
450 </Modal>
ybtda5978b2025-05-31 15:58:05 +0800451 </div>
ybtbac75f22025-06-08 22:31:15 +0800452 );
453};
ybtda5978b2025-05-31 15:58:05 +0800454
455export default ProfilePage;