feat(auth): 实现登录注册功能并重构 App 组件
- 新增登录和注册页面组件
- 实现用户认证和权限管理逻辑
- 重构 App 组件,使用 Router 和 AuthProvider
- 添加管理员面板和论坛页面组件
Change-Id: Iaa4502616970e75e3268537f73c75dac8f60e24d
diff --git a/src/features/auth/pages/LoginPage.jsx b/src/features/auth/pages/LoginPage.jsx
new file mode 100644
index 0000000..19434a4
--- /dev/null
+++ b/src/features/auth/pages/LoginPage.jsx
@@ -0,0 +1,155 @@
+// src/features/auth/pages/LoginPage.jsx
+import React, { useState } from "react";
+import { useNavigate, Link } from "react-router-dom";
+import {
+ Form,
+ Input,
+ Button,
+ Checkbox,
+ Card,
+ Typography,
+ Space,
+ Divider,
+ message,
+} from "antd";
+import { UserOutlined, LockOutlined } from "@ant-design/icons";
+import { useAuth } from "../contexts/AuthContext"; // 使用新的 AuthContext
+// import { loginUser } from '../services/authApi'; // 如果不直接在 context 中调用 API
+
+const { Title, Text } = Typography;
+
+const LoginPage = () => {
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+ const { login, isAuthenticated, user } = useAuth(); // 从 Context 获取 login 方法等
+
+ React.useEffect(() => {
+ // 如果已经登录,并且有用户信息,则重定向到首页
+ if (isAuthenticated && user) {
+ navigate("/");
+ }
+ }, [isAuthenticated, user, navigate]);
+
+ const onFinish = async (values) => {
+ setLoading(true);
+ try {
+ await login({ username: values.username, password: values.password });
+ // 登录成功后的导航由 AuthContext 内部或 ProtectedRoute 处理
+ // AuthContext 已经包含成功提示,这里不再重复提示
+ navigate("/"); // 或者根据用户角色导航到不同页面
+ } catch (error) {
+ // 错误消息由 AuthContext 中的 login 方法或 request 拦截器处理
+ console.error("Login page error:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <div className="flex justify-center items-center min-h-screen bg-slate-100 p-4">
+ {" "}
+ {/* Tailwind: bg-gray-100 -> bg-slate-100 */}
+ <Card className="w-full max-w-md shadow-lg rounded-lg">
+ {" "}
+ {/* Tailwind: rounded-lg */}
+ <div className="text-center mb-8">
+ {" "}
+ {/* Tailwind: mb-6 -> mb-8 */}
+ <Title level={2} className="!mb-2 text-slate-700">
+ PT站登录
+ </Title>{" "}
+ {/* Tailwind: text-slate-700 */}
+ <Text type="secondary">欢迎回来,请登录您的账号</Text>
+ </div>
+ <Form
+ name="login_form" // 最好给表单一个唯一的名字
+ initialValues={{ remember: true }}
+ onFinish={onFinish}
+ size="large"
+ layout="vertical"
+ className="space-y-6" // Tailwind: 间距控制
+ >
+ <Form.Item
+ name="username"
+ rules={[{ required: true, message: "请输入您的用户名!" }]}
+ >
+ <Input
+ prefix={<UserOutlined className="site-form-item-icon" />}
+ placeholder="用户名"
+ />
+ </Form.Item>
+ <Form.Item
+ name="password"
+ rules={[{ required: true, message: "请输入您的密码!" }]}
+ >
+ <Input.Password
+ prefix={<LockOutlined className="site-form-item-icon" />}
+ placeholder="密码"
+ />
+ </Form.Item>
+ <Form.Item className="!mb-0">
+ {" "}
+ {/* Tailwind: !mb-0 覆盖antd默认margin */}
+ <div className="flex justify-between items-center">
+ <Form.Item name="remember" valuePropName="checked" noStyle>
+ <Checkbox>记住我</Checkbox>
+ </Form.Item>
+ <Link
+ to="/forgot-password"
+ className="text-blue-600 hover:text-blue-700 hover:underline"
+ >
+ {" "}
+ {/* Tailwind: hover:underline */}
+ 忘记密码?
+ </Link>
+ </div>
+ </Form.Item>
+ <Form.Item>
+ <Button
+ type="primary"
+ htmlType="submit"
+ className="w-full !text-base"
+ loading={loading}
+ >
+ {" "}
+ {/* Tailwind: !text-base (示例) */}登 录
+ </Button>
+ </Form.Item>
+ <Divider plain>
+ <span className="text-slate-500">或</span>
+ </Divider>{" "}
+ {/* Tailwind: text-slate-500 */}
+ <div className="text-center">
+ <Text type="secondary" className="mr-1">
+ 还没有账号?
+ </Text>
+ <Link
+ to="/register"
+ className="font-medium text-blue-600 hover:text-blue-700 hover:underline"
+ >
+ 立即注册
+ </Link>
+ </div>
+ </Form>
+ {/* 提示信息部分可以保留或移除 */}
+ <div className="mt-8 p-4 bg-slate-50 rounded-md border border-slate-200">
+ {" "}
+ {/* Tailwind: border, border-slate-200 */}
+ <Text
+ type="secondary"
+ className="block mb-2 font-semibold text-slate-600"
+ >
+ 测试账号提示
+ </Text>
+ <ul className="space-y-1 text-sm text-slate-500 list-disc list-inside">
+ <li>管理员: admin / admin123</li>
+ <li>普通用户: user / user123</li>
+ {/* ...其他测试账号 */}
+ </ul>
+ </div>
+ </Card>
+ </div>
+ );
+};
+
+export default LoginPage;
diff --git a/src/features/auth/pages/RegisterPage.jsx b/src/features/auth/pages/RegisterPage.jsx
new file mode 100644
index 0000000..c4fb06d
--- /dev/null
+++ b/src/features/auth/pages/RegisterPage.jsx
@@ -0,0 +1,130 @@
+// src/features/auth/pages/RegisterPage.jsx
+import React, { useState } from 'react';
+import { useNavigate, Link } from 'react-router-dom';
+import { Form, Input, Button, Card, Typography, Divider, message } from 'antd';
+import { UserOutlined, LockOutlined, MailOutlined } from '@ant-design/icons';
+import { useAuth } from '../contexts/AuthContext'; // 使用新的 AuthContext
+
+const { Title, Text } = Typography;
+
+const RegisterPage = () => {
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+ const { register, isAuthenticated, user } = useAuth(); // 从 Context 获取 register 方法
+
+ React.useEffect(() => {
+ if (isAuthenticated && user) {
+ navigate('/'); // 如果已登录,跳转到首页
+ }
+ }, [isAuthenticated, user, navigate]);
+
+ const onFinish = async (values) => {
+ setLoading(true);
+ try {
+ // 从表单值中移除 'confirm' 字段,因为它不需要发送到后端
+ const { confirm, ...registrationData } = values;
+ await register(registrationData); // 使用 context 中的 register 方法
+ message.success('注册成功!将跳转到登录页...');
+ setTimeout(() => {
+ navigate('/login');
+ }, 1500); // 延迟跳转,让用户看到成功消息
+ } catch (error) {
+ // 错误消息由 AuthContext 中的 register 方法处理或 request 拦截器处理
+ console.error('Registration page error:', error);
+ // message.error(error.message || '注册失败,请重试'); // 如果 Context 未处理错误提示
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <div className="flex justify-center items-center min-h-screen bg-slate-100 p-4">
+ <Card className="w-full max-w-md shadow-lg rounded-lg">
+ <div className="text-center mb-8">
+ <Title level={2} className="!mb-2 text-slate-700">创建您的账户</Title>
+ <Text type="secondary">加入我们的PT社区</Text>
+ </div>
+
+ <Form
+ name="register_form"
+ onFinish={onFinish}
+ size="large"
+ layout="vertical"
+ className="space-y-4" // 调整表单项间距
+ >
+ <Form.Item
+ name="username"
+ rules={[
+ { required: true, message: '请输入您的用户名!' },
+ { min: 3, message: '用户名至少需要3个字符' },
+ { max: 20, message: '用户名不能超过20个字符' },
+ { pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' }
+ ]}
+ hasFeedback // 显示校验状态图标
+ >
+ <Input prefix={<UserOutlined />} placeholder="用户名" />
+ </Form.Item>
+
+ <Form.Item
+ name="email"
+ rules={[
+ { required: true, message: '请输入您的邮箱地址!' },
+ { type: 'email', message: '请输入一个有效的邮箱地址!' }
+ ]}
+ hasFeedback
+ >
+ <Input prefix={<MailOutlined />} placeholder="邮箱" />
+ </Form.Item>
+
+ <Form.Item
+ name="password"
+ rules={[
+ { required: true, message: '请输入您的密码!' },
+ { min: 6, message: '密码至少需要6个字符' }
+ // 可以添加更复杂的密码强度校验规则
+ ]}
+ hasFeedback
+ >
+ <Input.Password prefix={<LockOutlined />} placeholder="密码" />
+ </Form.Item>
+
+ <Form.Item
+ name="confirm"
+ dependencies={['password']}
+ hasFeedback
+ rules={[
+ { required: true, message: '请再次输入您的密码!' },
+ ({ getFieldValue }) => ({
+ validator(_, value) {
+ if (!value || getFieldValue('password') === value) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error('两次输入的密码不一致!'));
+ },
+ }),
+ ]}
+ >
+ <Input.Password prefix={<LockOutlined />} placeholder="确认密码" />
+ </Form.Item>
+
+ <Form.Item className="!mt-6"> {/* 增加注册按钮的上边距 */}
+ <Button type="primary" htmlType="submit" className="w-full !text-base" loading={loading}>
+ 注 册
+ </Button>
+ </Form.Item>
+
+ <Divider plain><span className="text-slate-500">或</span></Divider>
+
+ <div className="text-center">
+ <Text type="secondary" className="mr-1">已经有账户了?</Text>
+ <Link to="/login" className="font-medium text-blue-600 hover:text-blue-700 hover:underline">
+ 前往登录
+ </Link>
+ </div>
+ </Form>
+ </Card>
+ </div>
+ );
+};
+
+export default RegisterPage;
\ No newline at end of file