用户中心 商城 登陆注册前端

Change-Id: I4165f66073210b296608a5cfa0e8329e3d9bc8e3
diff --git a/src/pages/UserCenter.jsx b/src/pages/UserCenter.jsx
new file mode 100644
index 0000000..2b71e65
--- /dev/null
+++ b/src/pages/UserCenter.jsx
@@ -0,0 +1,599 @@
+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;
\ No newline at end of file