Merge changes I53afd377,I95718173,Iebe50bff,Ieeaa92b5
* changes:
索要资源相关的前端以及管理员
帖子的相关用户前端与管理员后端
好友的相关前端
举报的管理员前端
diff --git a/src/components/AvatarWithFrame.css b/src/components/AvatarWithFrame.css
new file mode 100644
index 0000000..ffea91b
--- /dev/null
+++ b/src/components/AvatarWithFrame.css
@@ -0,0 +1,25 @@
+.avatar-container {
+ position: relative;
+ display: inline-block;
+ width: 100px;
+ height: 100px;
+}
+
+.user-avatar {
+ position: absolute;
+ width: 80px;
+ height: 80px;
+ top: 10px;
+ left: 10px;
+ z-index: 1;
+}
+
+.avatar-frame-image {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ z-index: 2;
+ pointer-events: none;
+}
\ No newline at end of file
diff --git a/src/components/AvatarWithFrame.jsx b/src/components/AvatarWithFrame.jsx
new file mode 100644
index 0000000..15be459
--- /dev/null
+++ b/src/components/AvatarWithFrame.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Avatar } from 'antd';
+import frameImage from './avatar-frame.png'; // 头像框图片素材
+import './AvatarWithFrame.css';
+
+const AvatarWithFrame = () => {
+ // 从localStorage获取用户数据
+ const userData = JSON.parse(localStorage.getItem('user')) || {};
+ const { image, decoration = '' } = userData;
+
+ // 检查是否包含头像框
+ const hasFrame = decoration.includes('头像框');
+
+ return (
+ <div className="avatar-container">
+ {/* 头像 */}
+ <Avatar
+ src={image}
+ size={80}
+ className={`user-avatar ${hasFrame ? 'with-frame' : ''}`}
+ />
+
+ {/* 头像框图片 - 根据decoration显示 */}
+ {hasFrame && (
+ <img
+ src={frameImage}
+ alt="头像框"
+ className="avatar-frame-image"
+ />
+ )}
+ </div>
+ );
+};
+
+export default AvatarWithFrame;
\ No newline at end of file
diff --git a/src/components/avatar-frame.png b/src/components/avatar-frame.png
new file mode 100644
index 0000000..16ad8f0
--- /dev/null
+++ b/src/components/avatar-frame.png
Binary files differ
diff --git a/src/introGuide.js b/src/introGuide.js
new file mode 100644
index 0000000..bcfe58e
--- /dev/null
+++ b/src/introGuide.js
@@ -0,0 +1,43 @@
+import introJs from 'intro.js';
+
+export const startIntroGuide = () => {
+ introJs().setOptions({
+ steps: [
+ {
+ intro: '欢迎加入我们,这是一个优质的种子资源分享社区。让我们快速了解一下主要功能,帮助您更好地使用我们的平台。'
+ },
+ {
+ element: document.querySelector('#torrent-activity'),
+ intro: '这里是首页,包括近期活动速递,资源推荐,您可以浏览最新活动,参加领取奖励,提升您的等级!'
+ },
+ {
+ element: document.querySelector('#community-posts'),
+ intro: '这里是社区交流中心,您可以发布帖子、评论互动、发布需求贴悬赏求助,还能查看我的帖子!'
+ },
+ {
+ element: document.querySelector('#torrent-list'),
+ intro: '这里是全站种子列表,您可以按分类浏览、搜索特定资源、查看详细信息和下载种子文件。所有优质资源都在这里!'
+ },
+ {
+ element: document.querySelector('#torrent-upload'),
+ intro: '这是快速上传种子入口,操作简单易上手,快来上传你的第一个资源!'
+ },
+ {
+ element: document.querySelector('#community-friends'),
+ intro: '这里是好友聊天区,您可以添加和管理好友、畅聊分享!'
+ },
+ {
+ element: document.querySelector('#torrent-shop'),
+ intro: '用您的积分兑换个人装饰、上传量和邀请码,提升使用体验,邀请更多好友。'
+ },
+ {
+ element: document.querySelector('#help-button'),
+ intro: '小贴士:您可以随时点击右上角的帮助按钮重新查看引导和查看文档。祝您使用愉快!'
+ }
+ ],
+ nextLabel: '下一步',
+ prevLabel: '上一步',
+ skipLabel: '跳过',
+ doneLabel: '完成'
+ }).start();
+};
diff --git a/src/pages/AuthStyle.css b/src/pages/AuthStyle.css
new file mode 100644
index 0000000..820c596
--- /dev/null
+++ b/src/pages/AuthStyle.css
@@ -0,0 +1,240 @@
+/* AuthStyle.css - 仅针对auth页面的样式,不影响其他页面 */
+
+/* 限定在.auth-wrapper内的样式才会生效 */
+.auth-wrapper {
+ /* 基础样式 */
+ --primary-color: #f9952f;
+ --primary-hover: #e08527;
+ --white: #ffffff;
+ --light-gray: #f5f5f5;
+ --dark-gray: #333333;
+ --error-color: #ff4d4f;
+
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+
+ /* 页面背景(仅影响.auth-wrapper内部) */
+ background-color: var(--light-gray);
+ background-image: linear-gradient(135deg, var(--primary-color) 0%, #f9c22f 100%);
+ background-size: 400% 400%;
+ min-height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 2rem;
+ animation: gradientBG 15s ease infinite;
+}
+
+@keyframes gradientBG {
+ 0% { background-position: 0% 50%; }
+ 50% { background-position: 100% 50%; }
+ 100% { background-position: 0% 50%; }
+}
+
+/* 容器样式 */
+.auth-wrapper .auth-container {
+ width: 100%;
+ max-width: 500px;
+ animation: fadeIn 0.5s ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.auth-wrapper .auth-header {
+ text-align: center;
+ margin-bottom: 2rem;
+ color: var(--white);
+}
+
+.auth-wrapper .auth-header h1 {
+ font-size: 2.5rem;
+ margin-bottom: 0.5rem;
+ text-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.auth-wrapper .auth-header p {
+ font-size: 1rem;
+ opacity: 0.9;
+}
+
+/* 卡片样式 */
+.auth-wrapper .auth-card {
+ background-color: var(--white);
+ border-radius: 10px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
+ padding: 2rem;
+ transition: all 0.3s ease;
+}
+
+.auth-wrapper .auth-card:hover {
+ box-shadow: 0 15px 35px rgba(0,0,0,0.15);
+}
+
+/* 标签页样式 */
+.auth-wrapper .tabs {
+ display: flex;
+ position: relative;
+ margin-bottom: 2rem;
+ border-bottom: 1px solid #eee;
+}
+
+.auth-wrapper .tab-btn {
+ flex: 1;
+ padding: 0.75rem;
+ text-align: center;
+ background: none;
+ border: none;
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--dark-gray);
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+}
+
+.auth-wrapper .tab-btn.active {
+ color: var(--primary-color);
+}
+
+.auth-wrapper .tab-indicator {
+ position: absolute;
+ bottom: -1px;
+ left: 0;
+ height: 3px;
+ width: 50%;
+ background-color: var(--primary-color);
+ transition: all 0.3s ease;
+}
+
+/* 表单样式 */
+.auth-wrapper .auth-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.auth-wrapper .form-group {
+ position: relative;
+}
+
+.auth-wrapper .form-group input {
+ width: 100%;
+ padding: 1rem;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+}
+
+.auth-wrapper .form-group input:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(249, 149, 47, 0.2);
+ outline: none;
+}
+
+.auth-wrapper .form-group.with-button {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.auth-wrapper .form-group.with-button input {
+ flex: 1;
+}
+
+/* 按钮样式 */
+.auth-wrapper .submit-btn {
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ padding: 1rem;
+ border-radius: 5px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ height: 50px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.auth-wrapper .submit-btn:hover {
+ background-color: var(--primary-hover);
+ transform: translateY(-2px);
+}
+
+.auth-wrapper .submit-btn:active {
+ transform: translateY(0);
+}
+
+.auth-wrapper .code-btn {
+ background-color: var(--light-gray);
+ color: var(--dark-gray);
+ border: none;
+ padding: 0 1rem;
+ border-radius: 5px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ white-space: nowrap;
+}
+
+.auth-wrapper .code-btn:hover:not(:disabled) {
+ background-color: #e0e0e0;
+}
+
+.auth-wrapper .code-btn:disabled {
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+/* 消息样式 */
+.auth-wrapper .message {
+ padding: 0.75rem 1rem;
+ border-radius: 5px;
+ margin-bottom: 1.5rem;
+ text-align: center;
+ transition: all 0.3s ease;
+}
+
+.auth-wrapper .message.error {
+ background-color: #fff2f0;
+ border: 1px solid var(--error-color);
+ color: var(--error-color);
+}
+
+/* 加载动画 */
+.auth-wrapper .spinner {
+ width: 20px;
+ height: 20px;
+ border: 3px solid rgba(255,255,255,0.3);
+ border-radius: 50%;
+ border-top-color: var(--white);
+ animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* 页脚样式 */
+.auth-wrapper .auth-footer {
+ text-align: center;
+ margin-top: 2rem;
+ color: var(--white);
+ font-size: 0.9rem;
+ opacity: 0.8;
+}
+
+/* 响应式设计 */
+@media (max-width: 600px) {
+ .auth-wrapper {
+ padding: 1rem;
+ }
+
+ .auth-wrapper .auth-card {
+ padding: 1.5rem;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/HelpPage.css b/src/pages/HelpPage.css
new file mode 100644
index 0000000..6809108
--- /dev/null
+++ b/src/pages/HelpPage.css
@@ -0,0 +1,78 @@
+.help-page-wrapper {
+ min-height: 100vh;
+ background-color: #fff7e6; /* 淡橙色背景 */
+ padding: 3rem 1rem; /* 上下3rem,左右1rem */
+ box-sizing: border-box;
+}
+
+.help-container {
+ max-width: 960px; /* 大约是4xl宽度 */
+ margin: 0 auto;
+ background: white;
+ border: 4px solid black;
+ border-radius: 0.5rem;
+ box-shadow: 0 0 10px rgb(0 0 0 / 0.1);
+ padding: 2rem;
+ box-sizing: border-box;
+}
+
+.help-title {
+ font-size: 2rem;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 1.5rem;
+ border-bottom: 2px solid black;
+ padding-bottom: 1rem;
+}
+
+.help-content {
+ max-width: none; /* 让内容最大宽度不限制 */
+ margin: 0 auto;
+ padding: 0 2rem; /* 左右边距2rem */
+ box-sizing: border-box;
+}
+
+/* markdown 内容排版,配合 rehype-highlight 样式 */
+.help-content p {
+ margin: 1rem 0;
+ line-height: 1.6;
+}
+
+.help-content h1 {
+ font-size: 1.5rem;
+ margin: 2rem 0 1rem;
+ font-weight: 700;
+ border-bottom: 1px solid #ccc;
+ padding-bottom: 0.5rem;
+}
+
+.help-content h2 {
+ font-size: 1.25rem;
+ margin: 1.5rem 0 1rem;
+ font-weight: 600;
+}
+
+.help-content code {
+ background-color: #f3f4f6;
+ padding: 0.2rem 0.4rem;
+ border-radius: 0.25rem;
+ font-family: monospace;
+ font-size: 0.9rem;
+}
+
+.help-content pre {
+ background-color: #f3f4f6;
+ padding: 1rem;
+ border-radius: 0.5rem;
+ overflow-x: auto;
+ margin: 1rem 0;
+ border: 1px solid #d1d5db;
+}
+
+.help-content blockquote {
+ border-left: 4px solid #9ca3af;
+ padding-left: 1rem;
+ font-style: italic;
+ color: #6b7280;
+ margin: 1rem 0;
+}
diff --git a/src/pages/HelpPage.jsx b/src/pages/HelpPage.jsx
new file mode 100644
index 0000000..ec5161b
--- /dev/null
+++ b/src/pages/HelpPage.jsx
@@ -0,0 +1,47 @@
+import React, { useEffect, useState } from 'react';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import rehypeHighlight from 'rehype-highlight';
+import './HelpPage.css'; // 引入样式
+import Navbar from '../components/Navbar'; // 导航栏组件
+
+export default function HelpPage() {
+ const [markdown, setMarkdown] = useState('');
+
+ useEffect(() => {
+ fetch('/help.md')
+ .then(res => {
+ if (!res.ok) throw new Error('Markdown 文件加载失败');
+ return res.text();
+ })
+ .then(text => setMarkdown(text))
+ .catch(() => setMarkdown('# 帮助文档加载失败,请稍后再试。'));
+ }, []);
+
+ return (
+ <div>
+ <Navbar />
+ <div className="help-page-wrapper">
+ <div className="help-container">
+ <h1 className="help-title">📚 帮助文档</h1>
+ <div className="help-content">
+ <ReactMarkdown
+ remarkPlugins={[remarkGfm]}
+ rehypePlugins={[rehypeHighlight]}
+ components={{
+ h1: ({ node, ...props }) => <h1 {...props} />,
+ h2: ({ node, ...props }) => <h2 {...props} />,
+ p: ({ node, ...props }) => <p {...props} />,
+ code: ({ node, ...props }) => <code {...props} />,
+ pre: ({ node, ...props }) => <pre {...props} />,
+ blockquote: ({ node, ...props }) => <blockquote {...props} />,
+ }}
+ >
+ {markdown}
+ </ReactMarkdown>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/src/pages/ShopPage.jsx b/src/pages/ShopPage.jsx
new file mode 100644
index 0000000..bb7d989
--- /dev/null
+++ b/src/pages/ShopPage.jsx
@@ -0,0 +1,187 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { message, Button } from 'antd';
+import { ArrowLeftOutlined } from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
+
+const ShopPage = () => {
+ const navigate = useNavigate();
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [shopItems, setShopItems] = useState([
+ {
+ id: 1,
+ type: 'decoration',
+ name: '头像框',
+ description: '好看的头像框可以让别人更好地记住你',
+ price: 100,
+ image: 'https://img.icons8.com/?size=100&id=11730&format=png&color=000000'
+ },
+ {
+ id: 2,
+ type: 'decoration',
+ name: '对话框',
+ description: '闪亮的对话框,让您的消息更醒目',
+ price: 150,
+ image: 'https://img.icons8.com/?size=100&id=143&format=png&color=000000'
+ },
+ {
+ id: 3,
+ type: 'upload',
+ name: '上传量+100MB',
+ description: '增加100MB的上传量',
+ price: 50,
+ image: 'https://img.icons8.com/?size=100&id=368&format=png&color=000000'
+ },
+ {
+ id: 4,
+ type: 'invite',
+ name: '邀请码',
+ description: '一个邀请码,用于邀请好友加入',
+ price: 200,
+ image: 'https://img.icons8.com/?size=100&id=5465&format=png&color=000000'
+ }
+ ]);
+
+ useEffect(() => {
+ // 从localStorage获取用户信息
+ const userData = localStorage.getItem('user');
+ if (userData) {
+ setUser(JSON.parse(userData));
+ }
+ }, []);
+
+ const handlePurchase = async (item) => {
+ if (!user) {
+ message.error('请先登录');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ let response;
+ const username = user.username;
+
+ switch (item.type) {
+ case 'decoration':
+ response = await axios.post('http://localhost:8080/shop/soldDecoration', null, {
+ params: {
+ buyername: username,
+ decoration: item.name,
+ price: item.price
+ }
+ });
+ break;
+ case 'upload':
+ response = await axios.post('http://localhost:8080/shop/soldUpload', null, {
+ params: {
+ buyername: username,
+ price: item.price,
+ upload: 100 // 假设每次购买增加100MB
+ }
+ });
+ break;
+ case 'invite':
+ response = await axios.post('http://localhost:8080/shop/soldInvite', null, {
+ params: {
+ buyername: username,
+ price: item.price
+ }
+ });
+ break;
+ default:
+ throw new Error('未知的商品类型');
+ }
+
+ if (response.data && response.data.success) {
+ message.success('购买成功!');
+ // 更新用户信息
+ user.credit -= item.price;
+ if(item.type=='decoration'){
+ user.decoration= user.decoration+" "+item.name;
+ }
+ localStorage.setItem('user', JSON.stringify(user));
+ setUser(user);
+ } else {
+ message.error(response.data.message || '购买失败');
+ }
+ } catch (error) {
+ console.error('购买出错:', error);
+ message.error(error.response?.data?.message || '购买过程中出错');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (!user) {
+ return (
+ <div style={{ padding: '20px', textAlign: 'center' }}>
+ <h2>请先登录后再访问商城</h2>
+ </div>
+ );
+ }
+
+ return (
+ <div className="shop-container" style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
+ {/* 添加返回按钮 */}
+ <Button
+ type="text"
+ icon={<ArrowLeftOutlined />}
+ onClick={() => navigate(-1)}
+ style={{ marginBottom: '20px', paddingLeft: 0 }}
+ >
+ 返回
+ </Button>
+
+ <h1 style={{ textAlign: 'center', marginBottom: '30px' }}>商城</h1>
+ <div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+ <div>
+ <h3>当前用户: {user.username}</h3>
+ <p>余额: {user.credit || 0} 积分</p>
+ </div>
+ </div>
+
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '20px' }}>
+ {shopItems.map((item) => (
+ <div key={item.id} style={{
+ border: '1px solid #ddd',
+ borderRadius: '8px',
+ padding: '15px',
+ display: 'flex',
+ flexDirection: 'column',
+ boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
+ }}>
+ <div style={{ height: '150px', backgroundColor: '#f5f5f5', display: 'flex', justifyContent: 'center', alignItems: 'center', marginBottom: '15px' }}>
+ {item.image ? (
+ <img src={item.image} alt={item.name} style={{ maxWidth: '100%', maxHeight: '100%' }} />
+ ) : (
+ <span>商品图片</span>
+ )}
+ </div>
+ <h3 style={{ margin: '0 0 10px 0' }}>{item.name}</h3>
+ <p style={{ color: '#666', flexGrow: 1 }}>{item.description}</p>
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '15px' }}>
+ <span style={{ fontSize: '18px', fontWeight: 'bold', color: '#f56c6c' }}>{item.price} 积分</span>
+ <button
+ onClick={() => handlePurchase(item)}
+ disabled={loading || (user.credit < item.price)}
+ style={{
+ padding: '8px 16px',
+ backgroundColor: user.credit >= item.price ? '#1890ff' : '#ccc',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: user.credit >= item.price ? 'pointer' : 'not-allowed'
+ }}
+ >
+ {loading ? '处理中...' : '购买'}
+ </button>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+};
+
+export default ShopPage;
\ No newline at end of file
diff --git a/src/pages/UserAuth.jsx b/src/pages/UserAuth.jsx
new file mode 100644
index 0000000..2982270
--- /dev/null
+++ b/src/pages/UserAuth.jsx
@@ -0,0 +1,274 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import './AuthStyle.css'; // 引入样式文件
+
+const UserAuth = () => {
+ // 状态管理
+ const [registerData, setRegisterData] = useState({
+ username: '',
+ password: '',
+ code: '',
+ email: '',
+ emailcode: ''
+ });
+ const [loginData, setLoginData] = useState({
+ username: '',
+ password: ''
+ });
+ const [activeTab, setActiveTab] = useState('login');
+ const [message, setMessage] = useState('');
+ const [error, setError] = useState('');
+ const [countdown, setCountdown] = useState(0);
+ const [isLoading, setIsLoading] = useState(false);
+
+ // 倒计时效果
+ useEffect(() => {
+ if (countdown > 0) {
+ const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
+ return () => clearTimeout(timer);
+ }
+ }, [countdown]);
+
+ // 输入处理
+ const handleChange = (e, formType) => {
+ const { name, value } = e.target;
+ formType === 'register'
+ ? setRegisterData(prev => ({ ...prev, [name]: value }))
+ : setLoginData(prev => ({ ...prev, [name]: value }));
+ };
+
+ // 发送验证码
+ const handleSendCode = async () => {
+ if (!registerData.email) {
+ setError('请输入邮箱地址');
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ const response = await axios.post('http://localhost:8080/user/sendCode', null, {
+ params: { email: registerData.email }
+ });
+
+ if (response.data?.success) {
+ setMessage('验证码已发送');
+ setError('');
+ setCountdown(60);
+ } else {
+ setError(response.data?.message || '发送失败');
+ }
+ } catch (err) {
+ setError(err.response?.data?.message || '发送出错');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 注册提交
+ const handleRegister = async (e) => {
+ e.preventDefault();
+ if (Object.values(registerData).some(v => !v)) {
+ setError('请填写所有字段');
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ const response = await axios.post('http://localhost:8080/user/register', {
+ username: registerData.username,
+ password: registerData.password,
+ email: registerData.email
+ }, {
+ params: {
+ code: registerData.code,
+ emailcode: registerData.emailcode
+ }
+ });
+
+ if (response.data?.success) {
+ setMessage('注册成功');
+ setActiveTab('login');
+ setRegisterData({
+ username: '',
+ password: '',
+ code: '',
+ email: '',
+ emailcode: ''
+ });
+ } else {
+ setError(response.data?.message || '注册失败');
+ }
+ } catch (err) {
+ setError(err.response?.data?.message || '注册出错');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 登录提交
+ const handleLogin = async (e) => {
+ e.preventDefault();
+ if (!loginData.username || !loginData.password) {
+ setError('请输入用户名和密码');
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ const response = await axios.post('http://localhost:8080/user/login', null, {
+ params: {
+ username: loginData.username,
+ password: loginData.password
+ }
+ });
+
+ if (response.data?.success) {
+ localStorage.setItem('user', JSON.stringify(response.data.user));
+ if(response.data.user.permission==0)
+ window.location.href = '/';
+ else
+ window.location.href = '/';
+ } else {
+ setError(response.data?.message || '登录失败');
+ }
+ } catch (err) {
+ setError(err.response?.data?.message || '登录出错');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div className="auth-wrapper">
+ <div className="auth-container">
+ <div className="auth-header">
+ <h1>PT Share</h1>
+ <p>专业资源分享平台</p>
+ </div>
+
+ <div className="auth-card">
+ <div className="tabs">
+ <button
+ className={`tab-btn ${activeTab === 'login' ? 'active' : ''}`}
+ onClick={() => setActiveTab('login')}
+ >
+ 登录
+ </button>
+ <button
+ className={`tab-btn ${activeTab === 'register' ? 'active' : ''}`}
+ onClick={() => setActiveTab('register')}
+ >
+ 注册
+ </button>
+ <div
+ className="tab-indicator"
+ style={{ left: activeTab === 'login' ? '0' : '50%' }}
+ />
+ </div>
+
+ <div className={`message ${error ? 'error' : ''}`}>
+ {error || message}
+ </div>
+
+ {activeTab === 'login' ? (
+ <form onSubmit={handleLogin} className="auth-form">
+ <div className="form-group">
+ <input
+ type="text"
+ name="username"
+ value={loginData.username}
+ onChange={(e) => handleChange(e, 'login')}
+ placeholder="用户名"
+ required
+ />
+ </div>
+ <div className="form-group">
+ <input
+ type="password"
+ name="password"
+ value={loginData.password}
+ onChange={(e) => handleChange(e, 'login')}
+ placeholder="密码"
+ required
+ />
+ </div>
+ <button type="submit" className="submit-btn" disabled={isLoading}>
+ {isLoading ? <div className="spinner"></div> : '登 录'}
+ </button>
+ </form>
+ ) : (
+ <form onSubmit={handleRegister} className="auth-form">
+ <div className="form-group">
+ <input
+ type="text"
+ name="username"
+ value={registerData.username}
+ onChange={(e) => handleChange(e, 'register')}
+ placeholder="用户名"
+ required
+ />
+ </div>
+ <div className="form-group">
+ <input
+ type="password"
+ name="password"
+ value={registerData.password}
+ onChange={(e) => handleChange(e, 'register')}
+ placeholder="密码"
+ required
+ />
+ </div>
+ <div className="form-group">
+ <input
+ type="text"
+ name="code"
+ value={registerData.code}
+ onChange={(e) => handleChange(e, 'register')}
+ placeholder="邀请码"
+ required
+ />
+ </div>
+ <div className="form-group">
+ <input
+ type="email"
+ name="email"
+ value={registerData.email}
+ onChange={(e) => handleChange(e, 'register')}
+ placeholder="邮箱"
+ required
+ />
+ </div>
+ <div className="form-group with-button">
+ <input
+ type="text"
+ name="emailcode"
+ value={registerData.emailcode}
+ onChange={(e) => handleChange(e, 'register')}
+ placeholder="验证码"
+ required
+ />
+ <button
+ type="button"
+ onClick={handleSendCode}
+ disabled={countdown > 0 || isLoading}
+ className="code-btn"
+ >
+ {countdown > 0 ? `${countdown}s` : '获取验证码'}
+ </button>
+ </div>
+ <button type="submit" className="submit-btn" disabled={isLoading}>
+ {isLoading ? <div className="spinner"></div> : '注 册'}
+ </button>
+ </form>
+ )}
+ </div>
+
+ <div className="auth-footer">
+ <p>© {new Date().getFullYear()} PT Share - 专业资源分享平台</p>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default UserAuth;
\ No newline at end of file
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