| import React, { useState, useEffect, useRef } from 'react'; |
| import { |
| Typography, |
| Card, |
| Avatar, |
| Statistic, |
| Row, |
| Col, |
| Tag, |
| Progress, |
| Button, |
| Divider, |
| Space, |
| Tooltip, |
| message, |
| Modal, |
| Form, |
| Input, |
| Upload |
| } from 'antd'; |
| import { |
| UserOutlined, |
| EditOutlined, |
| UploadOutlined, |
| DownloadOutlined, |
| TrophyOutlined, |
| HeartOutlined, |
| WarningOutlined, |
| CheckCircleOutlined, |
| SyncOutlined, |
| GiftOutlined, |
| SettingOutlined, |
| CameraOutlined |
| } from '@ant-design/icons'; |
| import { useAuth } from '@/features/auth/contexts/AuthContext'; |
| import { updateUserProfile, uploadAvatar } from '@/api/user'; |
| |
| const { Title, Text, Paragraph } = Typography; |
| |
| const ProfilePage = () => { |
| const { user, updateUserInfo } = useAuth(); |
| const [loading, setLoading] = useState(false); |
| const [editModalVisible, setEditModalVisible] = useState(false); |
| const [form] = Form.useForm(); |
| const hasUpdatedRef = useRef(false); // 用来追踪是否已经更新过用户信息 |
| |
| // PT站统计数据 |
| const [ptStats, setPtStats] = useState({ |
| uploadSize: 0, // GB |
| downloadSize: 0, // GB |
| ratio: 0, |
| points: 0, // 确保初始值为0 |
| userClass: '新用户', |
| level: 1, |
| seedingCount: 0, |
| leechingCount: 0, |
| completedCount: 0, |
| invites: 0, |
| warnings: 0, |
| hitAndRuns: 0 |
| }); |
| |
| // 页面加载时更新用户信息 |
| useEffect(() => { |
| const handleUserInfoUpdate = async () => { |
| if (user?.username && updateUserInfo && !hasUpdatedRef.current) { |
| console.log('页面加载,正在更新用户信息...'); |
| hasUpdatedRef.current = true; // 标记为已更新 |
| try { |
| await updateUserInfo(user.username); |
| console.log('用户信息更新成功'); |
| } catch (error) { |
| console.error('更新用户信息失败:', error); |
| hasUpdatedRef.current = false; // 如果失败,重置标记以便重试 |
| } |
| } |
| }; |
| |
| handleUserInfoUpdate(); |
| }, [user?.username, updateUserInfo]); // 依赖用户名和更新函数 |
| |
| // 用户信息更新后,从用户数据中提取统计信息 |
| useEffect(() => { |
| if (user) { |
| console.log('用户数据变化,更新统计信息:', user); |
| updateStatsFromUser(user); |
| } |
| }, [user]); |
| |
| // 从用户数据中提取统计信息 |
| const updateStatsFromUser = (userData) => { |
| if (userData) { |
| const uploaded = Number(userData.uploaded) || 0; |
| const downloaded = Number(userData.downloaded) || 0; |
| const shareRatio = Number(userData.shareRatio) || 0; |
| |
| |
| const newStats = { |
| uploadSize: uploaded, // 保存原始字节值 |
| downloadSize: downloaded, // 保存原始字节值 |
| ratio: shareRatio, |
| points: Number(userData.points) || 0, |
| userClass: `用户等级`, |
| level: Number(userData.level) || 1 |
| }; |
| |
| console.log('原始数据:', { uploaded, downloaded, shareRatio }); |
| console.log('更新后的统计数据:', newStats); |
| setPtStats(newStats); |
| } |
| }; |
| |
| |
| // 格式化文件大小 |
| const formatSize = (sizeInBytes) => { |
| if (sizeInBytes === 0) { |
| return '0 B'; |
| } |
| |
| const units = ['B', 'KB', 'MB', 'GB', 'TB']; |
| const k = 1024; |
| let bytes = Math.abs(sizeInBytes); |
| let unitIndex = 0; |
| |
| // 找到合适的单位 |
| while (bytes >= k && unitIndex < units.length - 1) { |
| bytes /= k; |
| unitIndex++; |
| } |
| |
| // 根据大小决定小数位数 |
| let decimals = 2; |
| if (bytes >= 100) { |
| decimals = 0; // 大于等于100时不显示小数 |
| } else if (bytes >= 10) { |
| decimals = 1; // 10-99时显示1位小数 |
| } |
| |
| return `${bytes.toFixed(decimals)} ${units[unitIndex]}`; |
| }; |
| |
| // 获取分享率颜色 |
| const getRatioColor = (ratio) => { |
| if (ratio >= 2.0) return '#52c41a'; // 绿色 |
| if (ratio >= 1.0) return '#1890ff'; // 蓝色 |
| if (ratio >= 0.5) return '#faad14'; // 橙色 |
| return '#f5222d'; // 红色 |
| }; |
| |
| // 获取用户等级颜色 |
| const getUserClassColor = (userClass) => { |
| const classColors = { |
| 'User': 'default', |
| 'Power User': 'blue', |
| 'Elite User': 'purple', |
| 'Crazy User': 'gold', |
| 'Insane User': 'red', |
| 'Veteran User': 'green', |
| 'Extreme User': 'volcano', |
| 'VIP': 'magenta' |
| }; |
| return classColors[userClass] || 'default'; |
| }; |
| |
| // 显示编辑对话框 |
| const showEditModal = () => { |
| form.setFieldsValue({ |
| username: user?.username, |
| email: user?.email |
| }); |
| setEditModalVisible(true); |
| }; |
| |
| // 处理编辑提交 |
| const handleEditSubmit = async () => { |
| try { |
| const values = await form.validateFields(); |
| setLoading(true); |
| |
| const response = await updateUserProfile({ |
| username: user.username, |
| ...values |
| }); |
| |
| if (response && response.data) { |
| message.success('资料更新成功'); |
| setEditModalVisible(false); |
| // 资料更新成功后刷新用户信息 |
| try { |
| await updateUserInfo(user.username); |
| console.log('资料更新后用户信息已刷新'); |
| } catch (error) { |
| console.error('资料更新后刷新用户信息失败:', error); |
| } |
| } else { |
| message.error('更新失败,请重试'); |
| } |
| |
| } catch (error) { |
| console.error('更新失败:', error); |
| message.error(error.message || '更新失败,请重试'); |
| } finally { |
| setLoading(false); |
| } |
| }; |
| |
| // 头像上传处理 |
| const handleAvatarUpload = async (file) => { |
| const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'; |
| if (!isJpgOrPng) { |
| message.error('只能上传 JPG/PNG 格式的图片!'); |
| return false; |
| } |
| const isLt2M = file.size / 1024 / 1024 < 2; |
| if (!isLt2M) { |
| message.error('图片大小不能超过 2MB!'); |
| return false; |
| } |
| |
| try { |
| const formData = new FormData(); |
| formData.append('avatar', file); |
| formData.append('username', user.username); |
| |
| const response = await uploadAvatar(formData); |
| if (response && response.data) { |
| message.success('头像上传成功'); |
| // 头像上传成功后更新用户信息 |
| try { |
| await updateUserInfo(user.username); |
| console.log('头像上传后用户信息已更新'); |
| } catch (error) { |
| console.error('头像上传后更新用户信息失败:', error); |
| } |
| } else { |
| message.error('头像上传失败'); |
| } |
| } catch (error) { |
| console.error('头像上传失败:', error); |
| message.error('头像上传失败'); |
| } |
| |
| return false; // 阻止默认上传行为 |
| }; |
| |
| // 头像上传配置 |
| const uploadProps = { |
| name: 'avatar', |
| showUploadList: false, |
| beforeUpload: handleAvatarUpload, |
| }; |
| |
| return ( |
| <div className="space-y-6"> |
| <div className="flex justify-between items-center"> |
| <Title level={2}>个人资料</Title> |
| <Space> |
| <Button |
| icon={<SyncOutlined />} |
| onClick={async () => { |
| if (!user?.username) { |
| message.error('用户信息不完整,请重新登录'); |
| return; |
| } |
| |
| console.log('手动刷新用户信息...'); |
| setLoading(true); |
| |
| try { |
| // 重置标记,允许手动更新 |
| hasUpdatedRef.current = false; |
| |
| // 调用getUserInfo更新用户信息 |
| await updateUserInfo(user.username); |
| console.log('用户信息更新成功'); |
| |
| // 统计信息会通过 useEffect 自动更新 |
| |
| message.success('用户信息已更新'); |
| } catch (error) { |
| console.error('刷新用户信息失败:', error); |
| message.error('刷新失败: ' + (error.message || '网络错误')); |
| } finally { |
| setLoading(false); |
| } |
| }} |
| loading={loading} |
| > |
| 刷新信息 |
| </Button> |
| <Button |
| type="primary" |
| icon={<EditOutlined />} |
| onClick={showEditModal} |
| > |
| 编辑资料 |
| </Button> |
| </Space> |
| </div> |
| |
| <Row gutter={[24, 24]}> |
| {/* 用户基本信息卡片 */} |
| <Col xs={24} lg={8}> |
| <Card> |
| <div className="text-center"> |
| <div className="relative inline-block"> |
| <Avatar |
| size={120} |
| src={user?.avatar} |
| icon={<UserOutlined />} |
| className="mb-4" |
| /> |
| <Upload {...uploadProps}> |
| <Button |
| type="primary" |
| shape="circle" |
| icon={<CameraOutlined />} |
| size="small" |
| className="absolute bottom-0 right-0" |
| /> |
| </Upload> |
| </div> |
| |
| <Title level={3} className="mb-2">{user?.username || '用户'}</Title> |
| |
| <Space direction="vertical" className="w-full"> |
| <Tag |
| color={getUserClassColor(ptStats.userClass)} |
| className="text-lg px-3 py-1" |
| > |
| 用户等级 {user?.level || ptStats.level} |
| </Tag> |
| |
| <Text type="secondary">邮箱:{user?.email || '未设置'}</Text> |
| </Space> |
| </div> |
| </Card> |
| </Col> |
| |
| {/* PT站统计信息 */} |
| <Col xs={24} lg={16}> |
| <Card title={ |
| <Space> |
| <TrophyOutlined /> |
| <span>PT站统计</span> |
| </Space> |
| }> |
| <Row gutter={[16, 16]}> |
| {/* 上传下载统计 */} |
| <Col xs={12} sm={6}> |
| <Statistic |
| title="上传量" |
| value={formatSize(ptStats.uploadSize)} |
| prefix={<UploadOutlined style={{ color: '#52c41a' }} />} |
| valueStyle={{ color: '#52c41a' }} |
| /> |
| </Col> |
| <Col xs={12} sm={6}> |
| <Statistic |
| title="下载量" |
| value={formatSize(ptStats.downloadSize)} |
| prefix={<DownloadOutlined style={{ color: '#1890ff' }} />} |
| valueStyle={{ color: '#1890ff' }} |
| /> |
| </Col> |
| <Col xs={12} sm={6}> |
| <Statistic |
| title="分享率" |
| value={ptStats.ratio.toFixed(2)} |
| valueStyle={{ color: getRatioColor(ptStats.ratio) }} |
| /> |
| </Col> |
| <Col xs={12} sm={6}> |
| <Statistic |
| title="积分" |
| value={ptStats.points} |
| prefix={<HeartOutlined style={{ color: '#eb2f96' }} />} |
| valueStyle={{ color: '#eb2f96' }} |
| /> |
| </Col> |
| </Row> |
| </Card> |
| </Col> |
| </Row> |
| |
| {/* 分享率进度条 */} |
| <Card title="分享率分析"> |
| <Row gutter={[24, 16]}> |
| <Col xs={24} md={12}> |
| <div className="mb-4"> |
| <Text strong>当前分享率:{ptStats.ratio.toFixed(2)}</Text> |
| <Progress |
| percent={ptStats.ratio >= 999 ? 100 : Math.min(ptStats.ratio * 50, 100)} // 转换为百分比显示 |
| strokeColor={getRatioColor(ptStats.ratio)} |
| format={() => ptStats.ratio.toFixed(2)} |
| /> |
| </div> |
| <Space wrap> |
| <Tag color="green">≥2.00 优秀</Tag> |
| <Tag color="blue">≥1.00 良好</Tag> |
| <Tag color="orange">≥0.50 及格</Tag> |
| <Tag color="red"><0.50 需要改善</Tag> |
| </Space> |
| </Col> |
| <Col xs={24} md={12}> |
| <div className="space-y-2"> |
| <Paragraph> |
| <Text strong>分享率说明:</Text> |
| </Paragraph> |
| <Paragraph type="secondary" className="text-sm"> |
| • 分享率 = 上传量 ÷ 下载量<br/> |
| • 保持良好的分享率有助于维护账号状态<br/> |
| • 建议长期做种热门资源提升分享率<br/> |
| • 分享率过低可能导致账号受限 |
| </Paragraph> |
| </div> |
| </Col> |
| </Row> |
| </Card> |
| |
| {/* 编辑资料对话框 */} |
| <Modal |
| title="编辑个人资料" |
| open={editModalVisible} |
| onOk={handleEditSubmit} |
| onCancel={() => setEditModalVisible(false)} |
| confirmLoading={loading} |
| okText="保存" |
| cancelText="取消" |
| > |
| <Form form={form} layout="vertical"> |
| <Form.Item |
| name="username" |
| label="用户名" |
| rules={[{ required: true, message: '请输入用户名' }]} |
| > |
| <Input placeholder="请输入用户名" /> |
| </Form.Item> |
| <Form.Item |
| name="email" |
| label="邮箱" |
| rules={[ |
| { required: true, message: '请输入邮箱' }, |
| { type: 'email', message: '请输入有效的邮箱地址' } |
| ]} |
| > |
| <Input placeholder="请输入邮箱" /> |
| </Form.Item> |
| </Form> |
| </Modal> |
| </div> |
| ); |
| }; |
| |
| export default ProfilePage; |