ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 1 | import React, { useState, useEffect, useRef } from 'react'; |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 2 | import { |
| 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'; |
| 21 | import { |
| 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'; |
| 35 | import { useAuth } from '@/features/auth/contexts/AuthContext'; |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 36 | import { updateUserProfile, uploadAvatar } from '@/api/user'; |
ybt | da5978b | 2025-05-31 15:58:05 +0800 | [diff] [blame] | 37 | |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 38 | const { Title, Text, Paragraph } = Typography; |
ybt | da5978b | 2025-05-31 15:58:05 +0800 | [diff] [blame] | 39 | |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 40 | const ProfilePage = () => { |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 41 | const { user, updateUserInfo } = useAuth(); |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 42 | const [loading, setLoading] = useState(false); |
| 43 | const [editModalVisible, setEditModalVisible] = useState(false); |
| 44 | const [form] = Form.useForm(); |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 45 | const hasUpdatedRef = useRef(false); // 用来追踪是否已经更新过用户信息 |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 46 | |
| 47 | // PT站统计数据 |
| 48 | const [ptStats, setPtStats] = useState({ |
ybt | 71fb264 | 2025-06-09 00:29:36 +0800 | [diff] [blame] | 49 | 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, |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 59 | warnings: 0, |
ybt | 0d010e5 | 2025-06-09 00:29:36 +0800 | [diff] [blame] | 60 | hitAndRuns: 0 |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 61 | }); |
| 62 | |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 63 | // 页面加载时更新用户信息 |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 64 | useEffect(() => { |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 65 | 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); |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 87 | } |
| 88 | }, [user]); |
| 89 | |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 90 | // 从用户数据中提取统计信息 |
| 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; |
ybt | 71fb264 | 2025-06-09 00:29:36 +0800 | [diff] [blame] | 96 | |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 97 | |
| 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); |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 110 | } |
| 111 | }; |
| 112 | |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 113 | |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 114 | // 格式化文件大小 |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 115 | const formatSize = (sizeInBytes) => { |
| 116 | if (sizeInBytes === 0) { |
| 117 | return '0 B'; |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 118 | } |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 119 | |
| 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]}`; |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 140 | }; |
| 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); |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 188 | // 资料更新成功后刷新用户信息 |
| 189 | try { |
| 190 | await updateUserInfo(user.username); |
| 191 | console.log('资料更新后用户信息已刷新'); |
| 192 | } catch (error) { |
| 193 | console.error('资料更新后刷新用户信息失败:', error); |
| 194 | } |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 195 | } 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('头像上传成功'); |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 228 | // 头像上传成功后更新用户信息 |
| 229 | try { |
| 230 | await updateUserInfo(user.username); |
| 231 | console.log('头像上传后用户信息已更新'); |
| 232 | } catch (error) { |
| 233 | console.error('头像上传后更新用户信息失败:', error); |
| 234 | } |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 235 | } else { |
| 236 | message.error('头像上传失败'); |
| 237 | } |
| 238 | } catch (error) { |
| 239 | console.error('头像上传失败:', error); |
| 240 | message.error('头像上传失败'); |
| 241 | } |
ybt | da5978b | 2025-05-31 15:58:05 +0800 | [diff] [blame] | 242 | |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 243 | 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> |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 257 | <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> |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 299 | </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 | > |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 331 | 用户等级 {user?.level || ptStats.level} |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 332 | </Tag> |
| 333 | |
| 334 | <Text type="secondary">邮箱:{user?.email || '未设置'}</Text> |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 335 | </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="分享率" |
ybt | 71fb264 | 2025-06-09 00:29:36 +0800 | [diff] [blame] | 369 | value={ptStats.ratio.toFixed(2)} |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 370 | 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"> |
ybt | 71fb264 | 2025-06-09 00:29:36 +0800 | [diff] [blame] | 391 | <Text strong>当前分享率:{ptStats.ratio.toFixed(2)}</Text> |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 392 | <Progress |
ybt | 3ec62e4 | 2025-06-11 22:46:22 +0800 | [diff] [blame^] | 393 | percent={ptStats.ratio >= 999 ? 100 : Math.min(ptStats.ratio * 50, 100)} // 转换为百分比显示 |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 394 | strokeColor={getRatioColor(ptStats.ratio)} |
ybt | 71fb264 | 2025-06-09 00:29:36 +0800 | [diff] [blame] | 395 | format={() => ptStats.ratio.toFixed(2)} |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 396 | /> |
| 397 | </div> |
| 398 | <Space wrap> |
ybt | 71fb264 | 2025-06-09 00:29:36 +0800 | [diff] [blame] | 399 | <Tag color="green">≥2.00 优秀</Tag> |
| 400 | <Tag color="blue">≥1.00 良好</Tag> |
| 401 | <Tag color="orange">≥0.50 及格</Tag> |
| 402 | <Tag color="red"><0.50 需要改善</Tag> |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 403 | </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> |
ybt | da5978b | 2025-05-31 15:58:05 +0800 | [diff] [blame] | 451 | </div> |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame] | 452 | ); |
| 453 | }; |
ybt | da5978b | 2025-05-31 15:58:05 +0800 | [diff] [blame] | 454 | |
| 455 | export default ProfilePage; |