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