| import React, { useState, useEffect } from 'react'; |
| import axios from 'axios'; |
| import { message, Button, Form, Input, Radio, Upload, Avatar, Modal, Card, Statistic, Row, Col } from 'antd'; |
| import { UserOutlined, LockOutlined, LogoutOutlined, UploadOutlined, DownloadOutlined, StarOutlined, MoneyCollectOutlined, ArrowLeftOutlined } from '@ant-design/icons'; |
| import { useNavigate } from 'react-router-dom'; |
| import AvatarWithFrame from '../components/AvatarWithFrame.jsx'; |
| |
| const UserCenter = () => { |
| const [user, setUser] = useState(null); |
| const [form] = Form.useForm(); |
| const [avatarForm] = Form.useForm(); |
| const [passwordForm] = Form.useForm(); |
| const [loading, setLoading] = useState(false); |
| const [avatarLoading, setAvatarLoading] = useState(false); |
| const [passwordLoading, setPasswordLoading] = useState(false); |
| const [isModalVisible, setIsModalVisible] = useState(false); |
| const [fadeAnimation, setFadeAnimation] = useState(false); |
| const navigate = useNavigate(); |
| |
| // Add fade-in animation when component mounts |
| useEffect(() => { |
| setFadeAnimation(true); |
| }, []); |
| |
| useEffect(() => { |
| const userData = localStorage.getItem('user'); |
| if (userData) { |
| const parsedUser = JSON.parse(userData); |
| setUser(parsedUser); |
| form.setFieldsValue({ sex: parsedUser.sex || '男' }); |
| } else { |
| message.error('请先登录'); |
| navigate('/login'); |
| } |
| }, [form, navigate]); |
| |
| const handleGoBack = () => { |
| navigate(-1); // 返回上一页 |
| }; |
| |
| const handleSexChange = async () => { |
| try { |
| const values = await form.validateFields(); |
| setLoading(true); |
| const response = await axios.post('http://localhost:8080/user/changesex', null, { |
| params: { |
| username: user.username, |
| sex: values.sex |
| } |
| }); |
| |
| if (response.data && response.data.success) { |
| message.success('性别修改成功'); |
| const updatedUser = { ...user, sex: values.sex }; |
| localStorage.setItem('user', JSON.stringify(updatedUser)); |
| setUser(updatedUser); |
| |
| // Add a subtle success animation effect |
| message.config({ |
| duration: 2, |
| maxCount: 1, |
| }); |
| } else { |
| message.error(response.data.message || '性别修改失败'); |
| } |
| } catch (error) { |
| console.error('修改性别出错:', error); |
| message.error(error.response?.data?.message || '修改性别过程中出错'); |
| } finally { |
| setLoading(false); |
| } |
| }; |
| |
| const handleAvatarChange = async (info) => { |
| if (info.file.status === 'uploading') { |
| setAvatarLoading(true); |
| return; |
| } |
| |
| if (info.file.status === 'done') { |
| try { |
| const uploadRes = info.file.response; |
| if (!uploadRes?.success) { |
| throw new Error(uploadRes?.message || '上传失败'); |
| } |
| |
| const updateRes = await axios.post('http://localhost:8080/user/changeimage', null, { |
| params: { |
| username: user.username, |
| image: uploadRes.url |
| } |
| }); |
| |
| if (updateRes.data?.success) { |
| message.success('头像更新成功'); |
| const updatedUser = { |
| ...user, |
| image: uploadRes.url, |
| ...updateRes.data.user |
| }; |
| localStorage.setItem('user', JSON.stringify(updatedUser)); |
| setUser(updatedUser); |
| |
| // Add a subtle animation when avatar updates |
| message.config({ |
| duration: 2, |
| maxCount: 1, |
| }); |
| } else { |
| throw new Error(updateRes.data?.message || '更新失败'); |
| } |
| } catch (err) { |
| message.error(err.message); |
| } finally { |
| setAvatarLoading(false); |
| } |
| } |
| |
| if (info.file.status === 'error') { |
| message.error(info.file.response?.message || '上传出错'); |
| setAvatarLoading(false); |
| } |
| }; |
| |
| const handlePasswordChange = async () => { |
| try { |
| const values = await passwordForm.validateFields(); |
| setPasswordLoading(true); |
| const response = await axios.post('http://localhost:8080/user/changePassword', null, { |
| params: { |
| username: user.username, |
| oldpassword: values.oldPassword, |
| newpassword: values.newPassword |
| } |
| }); |
| |
| if (response.data && response.data.success) { |
| message.success('密码修改成功'); |
| passwordForm.resetFields(); |
| setIsModalVisible(false); |
| |
| // Add a subtle success animation effect |
| message.config({ |
| duration: 2, |
| maxCount: 1, |
| }); |
| } else { |
| message.error(response.data.message || '密码修改失败'); |
| } |
| } catch (error) { |
| console.error('修改密码出错:', error); |
| message.error(error.response?.data?.message || '修改密码过程中出错'); |
| } finally { |
| setPasswordLoading(false); |
| } |
| }; |
| |
| const handleLogout = () => { |
| localStorage.removeItem('user'); |
| message.success('已退出登录'); |
| navigate('/'); // 退出后跳转到登录页 |
| }; |
| |
| const uploadProps = { |
| name: 'avatar', |
| action: 'http://localhost:8080/user/uploadimage', |
| showUploadList: false, |
| onChange: handleAvatarChange, |
| beforeUpload: (file) => { |
| const isImage = ['image/jpeg', 'image/png', 'image/gif'].includes(file.type); |
| if (!isImage) { |
| message.error('只能上传JPG/PNG/GIF图片!'); |
| return false; |
| } |
| const isLt10M = file.size / 1024 / 1024 < 10; |
| if (!isLt10M) { |
| message.error('图片必须小于10MB!'); |
| return false; |
| } |
| return true; |
| }, |
| transformResponse: (data) => { |
| try { |
| return JSON.parse(data); |
| } catch { |
| return { success: false, message: '解析响应失败' }; |
| } |
| } |
| }; |
| |
| if (!user) { |
| return <div style={{ padding: '20px', textAlign: 'center' }}>加载中...</div>; |
| } |
| |
| const calculateRatio = () => { |
| if (user.user_download === 0) return '∞'; |
| return (user.user_upload / user.user_download).toFixed(2); |
| }; |
| |
| // Dynamic styles with the primary color #ffbd19 |
| const primaryColor = '#ffbd19'; |
| const secondaryColor = '#ffffff'; // White for contrast |
| const cardBackgroundColor = '#ffffff'; |
| const cardShadow = '0 4px 12px rgba(255, 189, 25, 0.1)'; |
| const textColor = '#333333'; |
| const borderColor = '#ffbd19'; |
| |
| return ( |
| <div style={{ |
| maxWidth: '1000px', |
| margin: '0 auto', |
| padding: '20px', |
| animation: fadeAnimation ? 'fadeIn 0.5s ease-in' : 'none' |
| }}> |
| {/* CSS animations */} |
| <style>{` |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .stat-card:hover { |
| transform: translateY(-5px); |
| transition: transform 0.3s ease; |
| box-shadow: 0 6px 16px rgba(255, 189, 25, 0.2); |
| } |
| |
| .section-title { |
| position: relative; |
| padding-bottom: 10px; |
| margin-bottom: 20px; |
| } |
| |
| .section-title::after { |
| content: ''; |
| position: absolute; |
| bottom: 0; |
| left: 0; |
| width: 50px; |
| height: 3px; |
| background-color: ${primaryColor}; |
| border-radius: 3px; |
| } |
| |
| .avatar-upload-hint { |
| transition: all 0.3s ease; |
| } |
| |
| .avatar-upload-hint:hover { |
| background-color: rgba(255, 189, 25, 0.2); |
| } |
| `}</style> |
| |
| {/* 添加返回按钮 */} |
| <Button |
| type="text" |
| icon={<ArrowLeftOutlined />} |
| onClick={handleGoBack} |
| style={{ |
| marginBottom: '20px', |
| color: textColor, |
| transition: 'all 0.3s' |
| }} |
| onMouseOver={(e) => e.currentTarget.style.color = primaryColor} |
| onMouseOut={(e) => e.currentTarget.style.color = textColor} |
| > |
| 返回 |
| </Button> |
| |
| <h1 style={{ |
| textAlign: 'center', |
| marginBottom: '30px', |
| color: textColor, |
| fontWeight: 'bold' |
| }}> |
| 用户中心 |
| </h1> |
| |
| <div style={{ |
| display: 'flex', |
| flexDirection: 'column', |
| gap: '30px' |
| }}> |
| {/* 基本信息展示 */} |
| <div style={{ |
| backgroundColor: cardBackgroundColor, |
| padding: '20px', |
| borderRadius: '8px', |
| boxShadow: cardShadow |
| }}> |
| <div style={{ |
| display: 'flex', |
| alignItems: 'center', |
| marginBottom: '20px' |
| }}> |
| <Upload {...uploadProps}> |
| <div style={{ position: 'relative', cursor: 'pointer' }}> |
| <AvatarWithFrame user={user} /> |
| <div style={{ |
| position: 'absolute', |
| bottom: -25, |
| right: 20, |
| backgroundColor: 'rgba(0,0,0,0.5)', |
| color: 'white', |
| padding: '2px 8px', |
| borderRadius: '4px', |
| fontSize: '12px' |
| }}> |
| <UploadOutlined /> 修改 |
| </div> |
| </div> |
| </Upload> |
| <div style={{ |
| marginLeft: '20px', // Adjusted position |
| flex: 1 |
| }}> |
| <h2 style={{ margin: '0', color: textColor }}>{user.username}</h2> |
| <p style={{ margin: '5px 0 0', color: '#666' }}>用户等级: Lv {user.grade_id}</p> |
| </div> |
| </div> |
| </div> |
| |
| {/* 数据统计展示区 */} |
| <Card |
| title={ |
| <div className="section-title"> |
| 数据统计 |
| </div> |
| } |
| bordered={false} |
| style={{ |
| borderRadius: '8px', |
| boxShadow: cardShadow |
| }} |
| > |
| <Row gutter={16}> |
| <Col span={6}> |
| <Card |
| className="stat-card" |
| hoverable |
| style={{ |
| borderRadius: '8px', |
| textAlign: 'center', |
| transition: 'all 0.3s' |
| }} |
| > |
| <Statistic |
| title="积分" |
| value={user.credit || 0} |
| prefix={<MoneyCollectOutlined style={{ color: primaryColor }} />} |
| valueStyle={{ color: '#3f8600' }} |
| /> |
| </Card> |
| </Col> |
| <Col span={6}> |
| <Card |
| className="stat-card" |
| hoverable |
| style={{ |
| borderRadius: '8px', |
| textAlign: 'center', |
| transition: 'all 0.3s' |
| }} |
| > |
| <Statistic |
| title="上传量 (MB)" |
| value={(user.user_upload / (1024 * 1024)).toFixed(2) || 0} |
| prefix={<UploadOutlined style={{ color: primaryColor }} />} |
| valueStyle={{ color: '#1890ff' }} |
| /> |
| </Card> |
| </Col> |
| <Col span={6}> |
| <Card |
| className="stat-card" |
| hoverable |
| style={{ |
| borderRadius: '8px', |
| textAlign: 'center', |
| transition: 'all 0.3s' |
| }} |
| > |
| <Statistic |
| title="下载量 (MB)" |
| value={(user.user_download / (1024 * 1024)).toFixed(2) || 0} |
| prefix={<DownloadOutlined style={{ color: primaryColor }} />} |
| valueStyle={{ color: '#722ed1' }} |
| /> |
| </Card> |
| </Col> |
| <Col span={6}> |
| <Card |
| className="stat-card" |
| hoverable |
| style={{ |
| borderRadius: '8px', |
| textAlign: 'center', |
| transition: 'all 0.3s' |
| }} |
| > |
| <Statistic |
| title="分享率" |
| value={calculateRatio()} |
| prefix={<StarOutlined style={{ color: primaryColor }} />} |
| valueStyle={{ color: '#faad14' }} |
| /> |
| </Card> |
| </Col> |
| </Row> |
| </Card> |
| |
| {/* 性别设置 */} |
| <div style={{ |
| backgroundColor: cardBackgroundColor, |
| padding: '20px', |
| borderRadius: '8px', |
| boxShadow: cardShadow |
| }}> |
| <h3 className="section-title">性别设置</h3> |
| <Form form={form} layout="inline"> |
| <Form.Item name="sex"> |
| <Radio.Group> |
| <Radio value="男">男</Radio> |
| <Radio value="女">女</Radio> |
| <Radio value="保密">保密</Radio> |
| </Radio.Group> |
| </Form.Item> |
| <Form.Item> |
| <Button |
| type="primary" |
| onClick={handleSexChange} |
| loading={loading} |
| style={{ |
| backgroundColor: primaryColor, |
| borderColor: primaryColor, |
| transition: 'all 0.3s' |
| }} |
| onMouseOver={(e) => { |
| e.currentTarget.style.backgroundColor = '#ffc940'; |
| e.currentTarget.style.borderColor = '#ffc940'; |
| }} |
| onMouseOut={(e) => { |
| e.currentTarget.style.backgroundColor = primaryColor; |
| e.currentTarget.style.borderColor = primaryColor; |
| }} |
| > |
| 确认修改 |
| </Button> |
| </Form.Item> |
| </Form> |
| </div> |
| |
| {/* 修改密码 */} |
| <div style={{ |
| backgroundColor: cardBackgroundColor, |
| padding: '20px', |
| borderRadius: '8px', |
| boxShadow: cardShadow |
| }}> |
| <h3 className="section-title">修改密码</h3> |
| <Button |
| type="primary" |
| onClick={() => setIsModalVisible(true)} |
| icon={<LockOutlined />} |
| style={{ |
| backgroundColor: primaryColor, |
| borderColor: primaryColor, |
| transition: 'all 0.3s' |
| }} |
| onMouseOver={(e) => { |
| e.currentTarget.style.backgroundColor = '#ffc940'; |
| e.currentTarget.style.borderColor = '#ffc940'; |
| }} |
| onMouseOut={(e) => { |
| e.currentTarget.style.backgroundColor = primaryColor; |
| e.currentTarget.style.borderColor = primaryColor; |
| }} |
| > |
| 修改密码 |
| </Button> |
| </div> |
| |
| {/* 退出登录 */} |
| <div style={{ textAlign: 'center' }}> |
| <Button |
| danger |
| onClick={handleLogout} |
| icon={<LogoutOutlined />} |
| style={{ |
| transition: 'all 0.3s' |
| }} |
| onMouseOver={(e) => e.currentTarget.style.opacity = '0.8'} |
| onMouseOut={(e) => e.currentTarget.style.opacity = '1'} |
| > |
| 退出登录 |
| </Button> |
| </div> |
| </div> |
| |
| {/* 修改密码模态框 */} |
| <Modal |
| title={ |
| <div style={{ |
| color: primaryColor, |
| fontWeight: 'bold' |
| }}> |
| 修改密码 |
| </div> |
| } |
| open={isModalVisible} |
| onCancel={() => setIsModalVisible(false)} |
| footer={null} |
| centered |
| width={400} |
| className="modal-content" |
| style={{ |
| borderRadius: '8px', |
| boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)' |
| }} |
| > |
| <Form |
| form={passwordForm} |
| layout="vertical" |
| onFinish={handlePasswordChange} |
| > |
| <Form.Item |
| name="oldPassword" |
| label="旧密码" |
| rules={[{ required: true, message: '请输入旧密码' }]} |
| > |
| <Input.Password |
| placeholder="请输入当前密码" |
| style={{ borderRadius: '4px' }} |
| /> |
| </Form.Item> |
| <Form.Item |
| name="newPassword" |
| label="新密码" |
| rules={[ |
| { required: true, message: '请输入新密码' }, |
| { min: 3, message: '密码长度不能少于3位' } |
| ]} |
| > |
| <Input.Password |
| placeholder="请输入新密码" |
| style={{ borderRadius: '4px' }} |
| /> |
| </Form.Item> |
| <Form.Item |
| name="confirmPassword" |
| label="确认新密码" |
| dependencies={['newPassword']} |
| rules={[ |
| { required: true, message: '请确认新密码' }, |
| ({ getFieldValue }) => ({ |
| validator(_, value) { |
| if (!value || getFieldValue('newPassword') === value) { |
| return Promise.resolve(); |
| } |
| return Promise.reject(new Error('两次输入的密码不一致!')); |
| }, |
| }), |
| ]} |
| > |
| <Input.Password |
| placeholder="请再次输入新密码" |
| style={{ borderRadius: '4px' }} |
| /> |
| </Form.Item> |
| <Form.Item> |
| <Button |
| type="primary" |
| htmlType="submit" |
| loading={passwordLoading} |
| style={{ |
| width: '100%', |
| backgroundColor: primaryColor, |
| borderColor: primaryColor, |
| transition: 'all 0.3s' |
| }} |
| onMouseOver={(e) => { |
| e.currentTarget.style.backgroundColor = '#ffc940'; |
| e.currentTarget.style.borderColor = '#ffc940'; |
| }} |
| onMouseOut={(e) => { |
| e.currentTarget.style.backgroundColor = primaryColor; |
| e.currentTarget.style.borderColor = primaryColor; |
| }} |
| > |
| 确认修改 |
| </Button> |
| </Form.Item> |
| </Form> |
| </Modal> |
| </div> |
| ); |
| }; |
| |
| export default UserCenter; |