ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame^] | 1 | import React, { useState, useEffect } from 'react'; |
| 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'; |
| 36 | import { getUserStats, 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 = () => { |
| 41 | const { user } = useAuth(); |
| 42 | const [loading, setLoading] = useState(false); |
| 43 | const [editModalVisible, setEditModalVisible] = useState(false); |
| 44 | const [form] = Form.useForm(); |
| 45 | |
| 46 | // PT站统计数据 |
| 47 | const [ptStats, setPtStats] = useState({ |
| 48 | uploadSize: 157.89, // GB |
| 49 | downloadSize: 89.32, // GB |
| 50 | ratio: 1.77, |
| 51 | points: 12580, |
| 52 | userClass: 'Power User', |
| 53 | seedingCount: 12, |
| 54 | leechingCount: 2, |
| 55 | completedCount: 156, |
| 56 | invites: 3, |
| 57 | warnings: 0, |
| 58 | hitAndRuns: 0, |
| 59 | joinDate: '2023-05-15', |
| 60 | lastActive: '2024-12-28 15:30:00' |
| 61 | }); |
| 62 | |
| 63 | // 获取用户统计信息 |
| 64 | useEffect(() => { |
| 65 | if (user?.username) { |
| 66 | fetchUserStats(); |
| 67 | } |
| 68 | }, [user]); |
| 69 | |
| 70 | const fetchUserStats = async () => { |
| 71 | try { |
| 72 | setLoading(true); |
| 73 | const response = await getUserStats(user.username); |
| 74 | if (response && response.data) { |
| 75 | setPtStats(prevStats => ({ |
| 76 | ...prevStats, |
| 77 | ...response.data |
| 78 | })); |
| 79 | } |
| 80 | } catch (error) { |
| 81 | console.error('获取用户统计信息失败:', error); |
| 82 | // 使用默认数据,不显示错误信息 |
| 83 | } finally { |
| 84 | setLoading(false); |
| 85 | } |
| 86 | }; |
| 87 | |
| 88 | // 格式化文件大小 |
| 89 | const formatSize = (sizeInGB) => { |
| 90 | if (sizeInGB >= 1024) { |
| 91 | return `${(sizeInGB / 1024).toFixed(2)} TB`; |
| 92 | } |
| 93 | return `${sizeInGB.toFixed(2)} GB`; |
| 94 | }; |
| 95 | |
| 96 | // 获取分享率颜色 |
| 97 | const getRatioColor = (ratio) => { |
| 98 | if (ratio >= 2.0) return '#52c41a'; // 绿色 |
| 99 | if (ratio >= 1.0) return '#1890ff'; // 蓝色 |
| 100 | if (ratio >= 0.5) return '#faad14'; // 橙色 |
| 101 | return '#f5222d'; // 红色 |
| 102 | }; |
| 103 | |
| 104 | // 获取用户等级颜色 |
| 105 | const getUserClassColor = (userClass) => { |
| 106 | const classColors = { |
| 107 | 'User': 'default', |
| 108 | 'Power User': 'blue', |
| 109 | 'Elite User': 'purple', |
| 110 | 'Crazy User': 'gold', |
| 111 | 'Insane User': 'red', |
| 112 | 'Veteran User': 'green', |
| 113 | 'Extreme User': 'volcano', |
| 114 | 'VIP': 'magenta' |
| 115 | }; |
| 116 | return classColors[userClass] || 'default'; |
| 117 | }; |
| 118 | |
| 119 | // 显示编辑对话框 |
| 120 | const showEditModal = () => { |
| 121 | form.setFieldsValue({ |
| 122 | username: user?.username, |
| 123 | email: user?.email |
| 124 | }); |
| 125 | setEditModalVisible(true); |
| 126 | }; |
| 127 | |
| 128 | // 处理编辑提交 |
| 129 | const handleEditSubmit = async () => { |
| 130 | try { |
| 131 | const values = await form.validateFields(); |
| 132 | setLoading(true); |
| 133 | |
| 134 | const response = await updateUserProfile({ |
| 135 | username: user.username, |
| 136 | ...values |
| 137 | }); |
| 138 | |
| 139 | if (response && response.data) { |
| 140 | message.success('资料更新成功'); |
| 141 | setEditModalVisible(false); |
| 142 | // 可以触发AuthContext的用户信息更新 |
| 143 | } else { |
| 144 | message.error('更新失败,请重试'); |
| 145 | } |
| 146 | |
| 147 | } catch (error) { |
| 148 | console.error('更新失败:', error); |
| 149 | message.error(error.message || '更新失败,请重试'); |
| 150 | } finally { |
| 151 | setLoading(false); |
| 152 | } |
| 153 | }; |
| 154 | |
| 155 | // 头像上传处理 |
| 156 | const handleAvatarUpload = async (file) => { |
| 157 | const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'; |
| 158 | if (!isJpgOrPng) { |
| 159 | message.error('只能上传 JPG/PNG 格式的图片!'); |
| 160 | return false; |
| 161 | } |
| 162 | const isLt2M = file.size / 1024 / 1024 < 2; |
| 163 | if (!isLt2M) { |
| 164 | message.error('图片大小不能超过 2MB!'); |
| 165 | return false; |
| 166 | } |
| 167 | |
| 168 | try { |
| 169 | const formData = new FormData(); |
| 170 | formData.append('avatar', file); |
| 171 | formData.append('username', user.username); |
| 172 | |
| 173 | const response = await uploadAvatar(formData); |
| 174 | if (response && response.data) { |
| 175 | message.success('头像上传成功'); |
| 176 | // 可以触发AuthContext的用户信息更新或重新获取用户信息 |
| 177 | } else { |
| 178 | message.error('头像上传失败'); |
| 179 | } |
| 180 | } catch (error) { |
| 181 | console.error('头像上传失败:', error); |
| 182 | message.error('头像上传失败'); |
| 183 | } |
ybt | da5978b | 2025-05-31 15:58:05 +0800 | [diff] [blame] | 184 | |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame^] | 185 | return false; // 阻止默认上传行为 |
| 186 | }; |
| 187 | |
| 188 | // 头像上传配置 |
| 189 | const uploadProps = { |
| 190 | name: 'avatar', |
| 191 | showUploadList: false, |
| 192 | beforeUpload: handleAvatarUpload, |
| 193 | }; |
| 194 | |
| 195 | return ( |
| 196 | <div className="space-y-6"> |
| 197 | <div className="flex justify-between items-center"> |
| 198 | <Title level={2}>个人资料</Title> |
| 199 | <Button |
| 200 | type="primary" |
| 201 | icon={<EditOutlined />} |
| 202 | onClick={showEditModal} |
| 203 | > |
| 204 | 编辑资料 |
| 205 | </Button> |
| 206 | </div> |
| 207 | |
| 208 | <Row gutter={[24, 24]}> |
| 209 | {/* 用户基本信息卡片 */} |
| 210 | <Col xs={24} lg={8}> |
| 211 | <Card> |
| 212 | <div className="text-center"> |
| 213 | <div className="relative inline-block"> |
| 214 | <Avatar |
| 215 | size={120} |
| 216 | src={user?.avatar} |
| 217 | icon={<UserOutlined />} |
| 218 | className="mb-4" |
| 219 | /> |
| 220 | <Upload {...uploadProps}> |
| 221 | <Button |
| 222 | type="primary" |
| 223 | shape="circle" |
| 224 | icon={<CameraOutlined />} |
| 225 | size="small" |
| 226 | className="absolute bottom-0 right-0" |
| 227 | /> |
| 228 | </Upload> |
| 229 | </div> |
| 230 | |
| 231 | <Title level={3} className="mb-2">{user?.username || '用户'}</Title> |
| 232 | |
| 233 | <Space direction="vertical" className="w-full"> |
| 234 | <Tag |
| 235 | color={getUserClassColor(ptStats.userClass)} |
| 236 | className="text-lg px-3 py-1" |
| 237 | > |
| 238 | {ptStats.userClass} |
| 239 | </Tag> |
| 240 | |
| 241 | <Text type="secondary">邮箱:{user?.email || '未设置'}</Text> |
| 242 | <Text type="secondary">注册时间:{ptStats.joinDate}</Text> |
| 243 | <Text type="secondary">最后活跃:{ptStats.lastActive}</Text> |
| 244 | </Space> |
| 245 | </div> |
| 246 | </Card> |
| 247 | </Col> |
| 248 | |
| 249 | {/* PT站统计信息 */} |
| 250 | <Col xs={24} lg={16}> |
| 251 | <Card title={ |
| 252 | <Space> |
| 253 | <TrophyOutlined /> |
| 254 | <span>PT站统计</span> |
| 255 | </Space> |
| 256 | }> |
| 257 | <Row gutter={[16, 16]}> |
| 258 | {/* 上传下载统计 */} |
| 259 | <Col xs={12} sm={6}> |
| 260 | <Statistic |
| 261 | title="上传量" |
| 262 | value={formatSize(ptStats.uploadSize)} |
| 263 | prefix={<UploadOutlined style={{ color: '#52c41a' }} />} |
| 264 | valueStyle={{ color: '#52c41a' }} |
| 265 | /> |
| 266 | </Col> |
| 267 | <Col xs={12} sm={6}> |
| 268 | <Statistic |
| 269 | title="下载量" |
| 270 | value={formatSize(ptStats.downloadSize)} |
| 271 | prefix={<DownloadOutlined style={{ color: '#1890ff' }} />} |
| 272 | valueStyle={{ color: '#1890ff' }} |
| 273 | /> |
| 274 | </Col> |
| 275 | <Col xs={12} sm={6}> |
| 276 | <Statistic |
| 277 | title="分享率" |
| 278 | value={ptStats.ratio} |
| 279 | precision={2} |
| 280 | valueStyle={{ color: getRatioColor(ptStats.ratio) }} |
| 281 | /> |
| 282 | </Col> |
| 283 | <Col xs={12} sm={6}> |
| 284 | <Statistic |
| 285 | title="积分" |
| 286 | value={ptStats.points} |
| 287 | prefix={<HeartOutlined style={{ color: '#eb2f96' }} />} |
| 288 | valueStyle={{ color: '#eb2f96' }} |
| 289 | /> |
| 290 | </Col> |
| 291 | </Row> |
| 292 | </Card> |
| 293 | </Col> |
| 294 | </Row> |
| 295 | |
| 296 | {/* 分享率进度条 */} |
| 297 | <Card title="分享率分析"> |
| 298 | <Row gutter={[24, 16]}> |
| 299 | <Col xs={24} md={12}> |
| 300 | <div className="mb-4"> |
| 301 | <Text strong>当前分享率:{ptStats.ratio}</Text> |
| 302 | <Progress |
| 303 | percent={Math.min(ptStats.ratio * 50, 100)} // 转换为百分比显示 |
| 304 | strokeColor={getRatioColor(ptStats.ratio)} |
| 305 | format={() => ptStats.ratio} |
| 306 | /> |
| 307 | </div> |
| 308 | <Space wrap> |
| 309 | <Tag color="green">≥2.0 优秀</Tag> |
| 310 | <Tag color="blue">≥1.0 良好</Tag> |
| 311 | <Tag color="orange">≥0.5 及格</Tag> |
| 312 | <Tag color="red"><0.5 需要改善</Tag> |
| 313 | </Space> |
| 314 | </Col> |
| 315 | <Col xs={24} md={12}> |
| 316 | <div className="space-y-2"> |
| 317 | <Paragraph> |
| 318 | <Text strong>分享率说明:</Text> |
| 319 | </Paragraph> |
| 320 | <Paragraph type="secondary" className="text-sm"> |
| 321 | • 分享率 = 上传量 ÷ 下载量<br/> |
| 322 | • 保持良好的分享率有助于维护账号状态<br/> |
| 323 | • 建议长期做种热门资源提升分享率<br/> |
| 324 | • 分享率过低可能导致账号受限 |
| 325 | </Paragraph> |
| 326 | </div> |
| 327 | </Col> |
| 328 | </Row> |
| 329 | </Card> |
| 330 | |
| 331 | {/* 编辑资料对话框 */} |
| 332 | <Modal |
| 333 | title="编辑个人资料" |
| 334 | open={editModalVisible} |
| 335 | onOk={handleEditSubmit} |
| 336 | onCancel={() => setEditModalVisible(false)} |
| 337 | confirmLoading={loading} |
| 338 | okText="保存" |
| 339 | cancelText="取消" |
| 340 | > |
| 341 | <Form form={form} layout="vertical"> |
| 342 | <Form.Item |
| 343 | name="username" |
| 344 | label="用户名" |
| 345 | rules={[{ required: true, message: '请输入用户名' }]} |
| 346 | > |
| 347 | <Input placeholder="请输入用户名" /> |
| 348 | </Form.Item> |
| 349 | <Form.Item |
| 350 | name="email" |
| 351 | label="邮箱" |
| 352 | rules={[ |
| 353 | { required: true, message: '请输入邮箱' }, |
| 354 | { type: 'email', message: '请输入有效的邮箱地址' } |
| 355 | ]} |
| 356 | > |
| 357 | <Input placeholder="请输入邮箱" /> |
| 358 | </Form.Item> |
| 359 | </Form> |
| 360 | </Modal> |
ybt | da5978b | 2025-05-31 15:58:05 +0800 | [diff] [blame] | 361 | </div> |
ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame^] | 362 | ); |
| 363 | }; |
ybt | da5978b | 2025-05-31 15:58:05 +0800 | [diff] [blame] | 364 | |
| 365 | export default ProfilePage; |