blob: 2b71e6513c1f810ac5918da7dcce5f6379e33f4b [file] [log] [blame]
2130105001e07a12025-06-09 18:00:40 +08001import React, { useState, useEffect } from 'react';
2import axios from 'axios';
3import { message, Button, Form, Input, Radio, Upload, Avatar, Modal, Card, Statistic, Row, Col } from 'antd';
4import { UserOutlined, LockOutlined, LogoutOutlined, UploadOutlined, DownloadOutlined, StarOutlined, MoneyCollectOutlined, ArrowLeftOutlined } from '@ant-design/icons';
5import { useNavigate } from 'react-router-dom';
6import AvatarWithFrame from '../components/AvatarWithFrame.jsx';
7
8const UserCenter = () => {
9 const [user, setUser] = useState(null);
10 const [form] = Form.useForm();
11 const [avatarForm] = Form.useForm();
12 const [passwordForm] = Form.useForm();
13 const [loading, setLoading] = useState(false);
14 const [avatarLoading, setAvatarLoading] = useState(false);
15 const [passwordLoading, setPasswordLoading] = useState(false);
16 const [isModalVisible, setIsModalVisible] = useState(false);
17 const [fadeAnimation, setFadeAnimation] = useState(false);
18 const navigate = useNavigate();
19
20 // Add fade-in animation when component mounts
21 useEffect(() => {
22 setFadeAnimation(true);
23 }, []);
24
25 useEffect(() => {
26 const userData = localStorage.getItem('user');
27 if (userData) {
28 const parsedUser = JSON.parse(userData);
29 setUser(parsedUser);
30 form.setFieldsValue({ sex: parsedUser.sex || '男' });
31 } else {
32 message.error('请先登录');
33 navigate('/login');
34 }
35 }, [form, navigate]);
36
37 const handleGoBack = () => {
38 navigate(-1); // 返回上一页
39 };
40
41 const handleSexChange = async () => {
42 try {
43 const values = await form.validateFields();
44 setLoading(true);
45 const response = await axios.post('http://localhost:8080/user/changesex', null, {
46 params: {
47 username: user.username,
48 sex: values.sex
49 }
50 });
51
52 if (response.data && response.data.success) {
53 message.success('性别修改成功');
54 const updatedUser = { ...user, sex: values.sex };
55 localStorage.setItem('user', JSON.stringify(updatedUser));
56 setUser(updatedUser);
57
58 // Add a subtle success animation effect
59 message.config({
60 duration: 2,
61 maxCount: 1,
62 });
63 } else {
64 message.error(response.data.message || '性别修改失败');
65 }
66 } catch (error) {
67 console.error('修改性别出错:', error);
68 message.error(error.response?.data?.message || '修改性别过程中出错');
69 } finally {
70 setLoading(false);
71 }
72 };
73
74 const handleAvatarChange = async (info) => {
75 if (info.file.status === 'uploading') {
76 setAvatarLoading(true);
77 return;
78 }
79
80 if (info.file.status === 'done') {
81 try {
82 const uploadRes = info.file.response;
83 if (!uploadRes?.success) {
84 throw new Error(uploadRes?.message || '上传失败');
85 }
86
87 const updateRes = await axios.post('http://localhost:8080/user/changeimage', null, {
88 params: {
89 username: user.username,
90 image: uploadRes.url
91 }
92 });
93
94 if (updateRes.data?.success) {
95 message.success('头像更新成功');
96 const updatedUser = {
97 ...user,
98 image: uploadRes.url,
99 ...updateRes.data.user
100 };
101 localStorage.setItem('user', JSON.stringify(updatedUser));
102 setUser(updatedUser);
103
104 // Add a subtle animation when avatar updates
105 message.config({
106 duration: 2,
107 maxCount: 1,
108 });
109 } else {
110 throw new Error(updateRes.data?.message || '更新失败');
111 }
112 } catch (err) {
113 message.error(err.message);
114 } finally {
115 setAvatarLoading(false);
116 }
117 }
118
119 if (info.file.status === 'error') {
120 message.error(info.file.response?.message || '上传出错');
121 setAvatarLoading(false);
122 }
123 };
124
125 const handlePasswordChange = async () => {
126 try {
127 const values = await passwordForm.validateFields();
128 setPasswordLoading(true);
129 const response = await axios.post('http://localhost:8080/user/changePassword', null, {
130 params: {
131 username: user.username,
132 oldpassword: values.oldPassword,
133 newpassword: values.newPassword
134 }
135 });
136
137 if (response.data && response.data.success) {
138 message.success('密码修改成功');
139 passwordForm.resetFields();
140 setIsModalVisible(false);
141
142 // Add a subtle success animation effect
143 message.config({
144 duration: 2,
145 maxCount: 1,
146 });
147 } else {
148 message.error(response.data.message || '密码修改失败');
149 }
150 } catch (error) {
151 console.error('修改密码出错:', error);
152 message.error(error.response?.data?.message || '修改密码过程中出错');
153 } finally {
154 setPasswordLoading(false);
155 }
156 };
157
158 const handleLogout = () => {
159 localStorage.removeItem('user');
160 message.success('已退出登录');
161 navigate('/'); // 退出后跳转到登录页
162 };
163
164 const uploadProps = {
165 name: 'avatar',
166 action: 'http://localhost:8080/user/uploadimage',
167 showUploadList: false,
168 onChange: handleAvatarChange,
169 beforeUpload: (file) => {
170 const isImage = ['image/jpeg', 'image/png', 'image/gif'].includes(file.type);
171 if (!isImage) {
172 message.error('只能上传JPG/PNG/GIF图片!');
173 return false;
174 }
175 const isLt10M = file.size / 1024 / 1024 < 10;
176 if (!isLt10M) {
177 message.error('图片必须小于10MB!');
178 return false;
179 }
180 return true;
181 },
182 transformResponse: (data) => {
183 try {
184 return JSON.parse(data);
185 } catch {
186 return { success: false, message: '解析响应失败' };
187 }
188 }
189 };
190
191 if (!user) {
192 return <div style={{ padding: '20px', textAlign: 'center' }}>加载中...</div>;
193 }
194
195 const calculateRatio = () => {
196 if (user.user_download === 0) return '∞';
197 return (user.user_upload / user.user_download).toFixed(2);
198 };
199
200 // Dynamic styles with the primary color #ffbd19
201 const primaryColor = '#ffbd19';
202 const secondaryColor = '#ffffff'; // White for contrast
203 const cardBackgroundColor = '#ffffff';
204 const cardShadow = '0 4px 12px rgba(255, 189, 25, 0.1)';
205 const textColor = '#333333';
206 const borderColor = '#ffbd19';
207
208 return (
209 <div style={{
210 maxWidth: '1000px',
211 margin: '0 auto',
212 padding: '20px',
213 animation: fadeAnimation ? 'fadeIn 0.5s ease-in' : 'none'
214 }}>
215 {/* CSS animations */}
216 <style>{`
217 @keyframes fadeIn {
218 from { opacity: 0; transform: translateY(10px); }
219 to { opacity: 1; transform: translateY(0); }
220 }
221
222 .stat-card:hover {
223 transform: translateY(-5px);
224 transition: transform 0.3s ease;
225 box-shadow: 0 6px 16px rgba(255, 189, 25, 0.2);
226 }
227
228 .section-title {
229 position: relative;
230 padding-bottom: 10px;
231 margin-bottom: 20px;
232 }
233
234 .section-title::after {
235 content: '';
236 position: absolute;
237 bottom: 0;
238 left: 0;
239 width: 50px;
240 height: 3px;
241 background-color: ${primaryColor};
242 border-radius: 3px;
243 }
244
245 .avatar-upload-hint {
246 transition: all 0.3s ease;
247 }
248
249 .avatar-upload-hint:hover {
250 background-color: rgba(255, 189, 25, 0.2);
251 }
252 `}</style>
253
254 {/* 添加返回按钮 */}
255 <Button
256 type="text"
257 icon={<ArrowLeftOutlined />}
258 onClick={handleGoBack}
259 style={{
260 marginBottom: '20px',
261 color: textColor,
262 transition: 'all 0.3s'
263 }}
264 onMouseOver={(e) => e.currentTarget.style.color = primaryColor}
265 onMouseOut={(e) => e.currentTarget.style.color = textColor}
266 >
267 返回
268 </Button>
269
270 <h1 style={{
271 textAlign: 'center',
272 marginBottom: '30px',
273 color: textColor,
274 fontWeight: 'bold'
275 }}>
276 用户中心
277 </h1>
278
279 <div style={{
280 display: 'flex',
281 flexDirection: 'column',
282 gap: '30px'
283 }}>
284 {/* 基本信息展示 */}
285 <div style={{
286 backgroundColor: cardBackgroundColor,
287 padding: '20px',
288 borderRadius: '8px',
289 boxShadow: cardShadow
290 }}>
291 <div style={{
292 display: 'flex',
293 alignItems: 'center',
294 marginBottom: '20px'
295 }}>
296 <Upload {...uploadProps}>
297 <div style={{ position: 'relative', cursor: 'pointer' }}>
298 <AvatarWithFrame user={user} />
299 <div style={{
300 position: 'absolute',
301 bottom: -25,
302 right: 20,
303 backgroundColor: 'rgba(0,0,0,0.5)',
304 color: 'white',
305 padding: '2px 8px',
306 borderRadius: '4px',
307 fontSize: '12px'
308 }}>
309 <UploadOutlined /> 修改
310 </div>
311 </div>
312 </Upload>
313 <div style={{
314 marginLeft: '20px', // Adjusted position
315 flex: 1
316 }}>
317 <h2 style={{ margin: '0', color: textColor }}>{user.username}</h2>
318 <p style={{ margin: '5px 0 0', color: '#666' }}>用户等级: Lv {user.grade_id}</p>
319 </div>
320 </div>
321 </div>
322
323 {/* 数据统计展示区 */}
324 <Card
325 title={
326 <div className="section-title">
327 数据统计
328 </div>
329 }
330 bordered={false}
331 style={{
332 borderRadius: '8px',
333 boxShadow: cardShadow
334 }}
335 >
336 <Row gutter={16}>
337 <Col span={6}>
338 <Card
339 className="stat-card"
340 hoverable
341 style={{
342 borderRadius: '8px',
343 textAlign: 'center',
344 transition: 'all 0.3s'
345 }}
346 >
347 <Statistic
348 title="积分"
349 value={user.credit || 0}
350 prefix={<MoneyCollectOutlined style={{ color: primaryColor }} />}
351 valueStyle={{ color: '#3f8600' }}
352 />
353 </Card>
354 </Col>
355 <Col span={6}>
356 <Card
357 className="stat-card"
358 hoverable
359 style={{
360 borderRadius: '8px',
361 textAlign: 'center',
362 transition: 'all 0.3s'
363 }}
364 >
365 <Statistic
366 title="上传量 (MB)"
367 value={(user.user_upload / (1024 * 1024)).toFixed(2) || 0}
368 prefix={<UploadOutlined style={{ color: primaryColor }} />}
369 valueStyle={{ color: '#1890ff' }}
370 />
371 </Card>
372 </Col>
373 <Col span={6}>
374 <Card
375 className="stat-card"
376 hoverable
377 style={{
378 borderRadius: '8px',
379 textAlign: 'center',
380 transition: 'all 0.3s'
381 }}
382 >
383 <Statistic
384 title="下载量 (MB)"
385 value={(user.user_download / (1024 * 1024)).toFixed(2) || 0}
386 prefix={<DownloadOutlined style={{ color: primaryColor }} />}
387 valueStyle={{ color: '#722ed1' }}
388 />
389 </Card>
390 </Col>
391 <Col span={6}>
392 <Card
393 className="stat-card"
394 hoverable
395 style={{
396 borderRadius: '8px',
397 textAlign: 'center',
398 transition: 'all 0.3s'
399 }}
400 >
401 <Statistic
402 title="分享率"
403 value={calculateRatio()}
404 prefix={<StarOutlined style={{ color: primaryColor }} />}
405 valueStyle={{ color: '#faad14' }}
406 />
407 </Card>
408 </Col>
409 </Row>
410 </Card>
411
412 {/* 性别设置 */}
413 <div style={{
414 backgroundColor: cardBackgroundColor,
415 padding: '20px',
416 borderRadius: '8px',
417 boxShadow: cardShadow
418 }}>
419 <h3 className="section-title">性别设置</h3>
420 <Form form={form} layout="inline">
421 <Form.Item name="sex">
422 <Radio.Group>
423 <Radio value="男">男</Radio>
424 <Radio value="女">女</Radio>
425 <Radio value="保密">保密</Radio>
426 </Radio.Group>
427 </Form.Item>
428 <Form.Item>
429 <Button
430 type="primary"
431 onClick={handleSexChange}
432 loading={loading}
433 style={{
434 backgroundColor: primaryColor,
435 borderColor: primaryColor,
436 transition: 'all 0.3s'
437 }}
438 onMouseOver={(e) => {
439 e.currentTarget.style.backgroundColor = '#ffc940';
440 e.currentTarget.style.borderColor = '#ffc940';
441 }}
442 onMouseOut={(e) => {
443 e.currentTarget.style.backgroundColor = primaryColor;
444 e.currentTarget.style.borderColor = primaryColor;
445 }}
446 >
447 确认修改
448 </Button>
449 </Form.Item>
450 </Form>
451 </div>
452
453 {/* 修改密码 */}
454 <div style={{
455 backgroundColor: cardBackgroundColor,
456 padding: '20px',
457 borderRadius: '8px',
458 boxShadow: cardShadow
459 }}>
460 <h3 className="section-title">修改密码</h3>
461 <Button
462 type="primary"
463 onClick={() => setIsModalVisible(true)}
464 icon={<LockOutlined />}
465 style={{
466 backgroundColor: primaryColor,
467 borderColor: primaryColor,
468 transition: 'all 0.3s'
469 }}
470 onMouseOver={(e) => {
471 e.currentTarget.style.backgroundColor = '#ffc940';
472 e.currentTarget.style.borderColor = '#ffc940';
473 }}
474 onMouseOut={(e) => {
475 e.currentTarget.style.backgroundColor = primaryColor;
476 e.currentTarget.style.borderColor = primaryColor;
477 }}
478 >
479 修改密码
480 </Button>
481 </div>
482
483 {/* 退出登录 */}
484 <div style={{ textAlign: 'center' }}>
485 <Button
486 danger
487 onClick={handleLogout}
488 icon={<LogoutOutlined />}
489 style={{
490 transition: 'all 0.3s'
491 }}
492 onMouseOver={(e) => e.currentTarget.style.opacity = '0.8'}
493 onMouseOut={(e) => e.currentTarget.style.opacity = '1'}
494 >
495 退出登录
496 </Button>
497 </div>
498 </div>
499
500 {/* 修改密码模态框 */}
501 <Modal
502 title={
503 <div style={{
504 color: primaryColor,
505 fontWeight: 'bold'
506 }}>
507 修改密码
508 </div>
509 }
510 open={isModalVisible}
511 onCancel={() => setIsModalVisible(false)}
512 footer={null}
513 centered
514 width={400}
515 className="modal-content"
516 style={{
517 borderRadius: '8px',
518 boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
519 }}
520 >
521 <Form
522 form={passwordForm}
523 layout="vertical"
524 onFinish={handlePasswordChange}
525 >
526 <Form.Item
527 name="oldPassword"
528 label="旧密码"
529 rules={[{ required: true, message: '请输入旧密码' }]}
530 >
531 <Input.Password
532 placeholder="请输入当前密码"
533 style={{ borderRadius: '4px' }}
534 />
535 </Form.Item>
536 <Form.Item
537 name="newPassword"
538 label="新密码"
539 rules={[
540 { required: true, message: '请输入新密码' },
541 { min: 3, message: '密码长度不能少于3位' }
542 ]}
543 >
544 <Input.Password
545 placeholder="请输入新密码"
546 style={{ borderRadius: '4px' }}
547 />
548 </Form.Item>
549 <Form.Item
550 name="confirmPassword"
551 label="确认新密码"
552 dependencies={['newPassword']}
553 rules={[
554 { required: true, message: '请确认新密码' },
555 ({ getFieldValue }) => ({
556 validator(_, value) {
557 if (!value || getFieldValue('newPassword') === value) {
558 return Promise.resolve();
559 }
560 return Promise.reject(new Error('两次输入的密码不一致!'));
561 },
562 }),
563 ]}
564 >
565 <Input.Password
566 placeholder="请再次输入新密码"
567 style={{ borderRadius: '4px' }}
568 />
569 </Form.Item>
570 <Form.Item>
571 <Button
572 type="primary"
573 htmlType="submit"
574 loading={passwordLoading}
575 style={{
576 width: '100%',
577 backgroundColor: primaryColor,
578 borderColor: primaryColor,
579 transition: 'all 0.3s'
580 }}
581 onMouseOver={(e) => {
582 e.currentTarget.style.backgroundColor = '#ffc940';
583 e.currentTarget.style.borderColor = '#ffc940';
584 }}
585 onMouseOut={(e) => {
586 e.currentTarget.style.backgroundColor = primaryColor;
587 e.currentTarget.style.borderColor = primaryColor;
588 }}
589 >
590 确认修改
591 </Button>
592 </Form.Item>
593 </Form>
594 </Modal>
595 </div>
596 );
597};
598
599export default UserCenter;