feat(auth): 实现登录注册功能并重构 App 组件

- 新增登录和注册页面组件
- 实现用户认证和权限管理逻辑
- 重构 App 组件,使用 Router 和 AuthProvider
- 添加管理员面板和论坛页面组件

Change-Id: Iaa4502616970e75e3268537f73c75dac8f60e24d
diff --git a/src/features/auth/contexts/AuthContext.jsx b/src/features/auth/contexts/AuthContext.jsx
new file mode 100644
index 0000000..5f9b37e
--- /dev/null
+++ b/src/features/auth/contexts/AuthContext.jsx
@@ -0,0 +1,133 @@
+import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
+import { loginUser, registerUser, getUserInfo, logoutUser } from '../services/authApi';
+import { message } from 'antd';
+import { useNavigate } from 'react-router-dom'; // 导入 useNavigate
+
+const AuthContext = createContext(null);
+
+export const AuthProvider = ({ children }) => {
+  const [user, setUser] = useState(null);
+  const [loading, setLoading] = useState(true); // 初始加载状态
+  const [isAuthenticated, setIsAuthenticated] = useState(false);
+  const navigate = useNavigate();
+
+  const loadAuthData = useCallback(() => {
+    setLoading(true);
+    try {
+      const storedToken = localStorage.getItem('authToken'); // 假设您使用token
+      const storedUser = localStorage.getItem('user');
+
+      if (storedToken && storedUser) {
+        setUser(JSON.parse(storedUser));
+        setIsAuthenticated(true);
+        // 调用API获取最新的用户信息
+        getUserInfo().then(response => { 
+          if (response.data && response.data.user) {
+            setUser(response.data.user);
+            localStorage.setItem('user', JSON.stringify(response.data.user));
+          }
+        }).catch(error => {
+          console.error("获取用户信息失败", error);
+        });
+      } else {
+        setIsAuthenticated(false);
+        setUser(null);
+      }
+    } catch (error) {
+      console.error("Failed to load auth data from storage", error);
+      setIsAuthenticated(false);
+      setUser(null);
+    } finally {
+      setLoading(false);
+    }
+  }, []);
+
+  useEffect(() => {
+    loadAuthData();
+  }, [loadAuthData]);
+
+  const login = async (credentials) => {
+    setLoading(true);
+    try {
+      const response = await loginUser(credentials);
+      const { token, user: userData } = response.data;
+      
+      localStorage.setItem('authToken', token);
+      localStorage.setItem('user', JSON.stringify(userData));
+      setUser(userData);
+      setIsAuthenticated(true);
+      message.success('登录成功');
+      return userData;
+    } catch (error) {
+      console.error("Login failed", error);
+      setIsAuthenticated(false);
+      setUser(null);
+      message.error(error.message || '登录失败,请检查用户名和密码');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const register = async (userData) => {
+    setLoading(true);
+    try {
+      const response = await registerUser(userData);
+      const { token, user: newUser } = response.data;
+
+      localStorage.setItem('authToken', token);
+      localStorage.setItem('user', JSON.stringify(newUser));
+      setUser(newUser);
+      setIsAuthenticated(true);
+      message.success('注册成功');
+      return newUser;
+    } catch (error) {
+      console.error("Registration failed", error);
+      message.error(error.message || '注册失败,请稍后再试');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const logout = async () => {
+    try {
+      await logoutUser();
+      localStorage.removeItem('authToken');
+      localStorage.removeItem('user');
+      localStorage.removeItem('permissions'); // 移除旧的权限存储
+      setUser(null);
+      setIsAuthenticated(false);
+      message.success('已成功退出登录');
+      navigate('/login');
+      return true;
+    } catch (error) {
+      console.error("登出失败", error);
+      message.error('登出失败');
+      return false;
+    }
+  };
+
+  const hasRole = useCallback((roleName) => {
+    return user?.role === roleName;
+  }, [user]);
+
+  const value = {
+    user,
+    isAuthenticated,
+    loading,
+    login,
+    register,
+    logout,
+    hasRole,
+    reloadAuthData: loadAuthData // 暴露一个重新加载数据的方法
+  };
+
+  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
+};
+
+export const useAuth = () => {
+  const context = useContext(AuthContext);
+  if (context === undefined || context === null) { // 增加了对 null 的检查
+    throw new Error('useAuth must be used within an AuthProvider');
+  }
+  return context;
+};
\ No newline at end of file
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
diff --git a/src/features/auth/services/authApi.js b/src/features/auth/services/authApi.js
new file mode 100644
index 0000000..dd40a80
--- /dev/null
+++ b/src/features/auth/services/authApi.js
@@ -0,0 +1,165 @@
+// src/features/auth/services/authApi.js
+import request from "../../../services/request";
+import { message } from "antd";
+
+// 使用API前缀
+const API_PREFIX = "/user";
+const ADMIN_PREFIX = "/admin";
+
+// 导出API函数
+export const loginUser = (credentials) => {
+  return request.post(`${API_PREFIX}/login`, credentials).then((response) => {
+    if (response.data && response.data.success) {
+      // 保存token和用户信息到localStorage
+      localStorage.setItem("token", response.data.data.token);
+      localStorage.setItem("user", JSON.stringify(response.data.data.user));
+      return response.data;
+    } else {
+      return Promise.reject(new Error(response.data.message || "登录失败"));
+    }
+  });
+};
+
+export const adminLogin = (credentials) => {
+  return request.post(`${ADMIN_PREFIX}/login`, credentials).then((response) => {
+    if (response.data && response.data.success) {
+      // 保存token和用户信息到localStorage
+      localStorage.setItem("token", response.data.data.token);
+      localStorage.setItem("user", JSON.stringify(response.data.data.user));
+      return response.data;
+    } else {
+      return Promise.reject(new Error(response.data.message || "管理员登录失败"));
+    }
+  });
+};
+
+export const registerUser = (userData) => {
+  return request.post(`${API_PREFIX}/register`, userData).then((response) => {
+    if (response.data && response.data.success) {
+      return response.data;
+    } else {
+      return Promise.reject(new Error(response.data.message || "注册失败"));
+    }
+  });
+};
+
+export const updateUsername = (username, newUsername) => {
+  const token = localStorage.getItem("token");
+  return request
+    .post(`${API_PREFIX}/update/username`, 
+    { username, newUsername }, 
+    { headers: { token } })
+    .then((response) => {
+      if (response.data && response.data.success) {
+        // 更新本地存储的用户信息
+        const user = JSON.parse(localStorage.getItem("user") || "{}");
+        user.username = newUsername;
+        localStorage.setItem("user", JSON.stringify(user));
+        return response.data;
+      } else {
+        return Promise.reject(
+          new Error(response.data.message || "修改用户名失败")
+        );
+      }
+    });
+};
+
+export const updatePassword = (username, newPassword) => {
+  const token = localStorage.getItem("token");
+  return request
+    .post(`${API_PREFIX}/update/password`, 
+    { username, newPassword }, 
+    { headers: { token } })
+    .then((response) => {
+      if (response.data && response.data.success) {
+        return response.data;
+      } else {
+        return Promise.reject(
+          new Error(response.data.message || "修改密码失败")
+        );
+      }
+    });
+};
+
+export const updateEmail = (username, newEmail) => {
+  const token = localStorage.getItem("token");
+  return request
+    .post(`${API_PREFIX}/update/email`, 
+    { username, newEmail }, 
+    { headers: { token } })
+    .then((response) => {
+      if (response.data && response.data.success) {
+        // 更新本地存储的用户信息
+        const user = JSON.parse(localStorage.getItem("user") || "{}");
+        user.email = newEmail;
+        localStorage.setItem("user", JSON.stringify(user));
+        return response.data;
+      } else {
+        return Promise.reject(
+          new Error(response.data.message || "修改邮箱失败")
+        );
+      }
+    });
+};
+
+export const getUserInfo = (username) => {
+  const token = localStorage.getItem("token");
+  return request
+    .get(`${API_PREFIX}/get/info?username=${username}`, 
+    { headers: { token } })
+    .then((response) => {
+      if (response.data && response.data.success) {
+        return response.data;
+      } else {
+        return Promise.reject(
+          new Error(response.data.message || "获取用户信息失败")
+        );
+      }
+    });
+};
+
+export const getUserList = (username) => {
+  const token = localStorage.getItem("token");
+  return request
+    .get(`/user/list?username=${username}`, 
+    { headers: { token } })
+    .then((response) => {
+      if (response.data && response.data.success) {
+        return response.data;
+      } else {
+        return Promise.reject(
+          new Error(response.data.message || "获取用户列表失败")
+        );
+      }
+    });
+};
+
+export const deleteUser = (username, targetUsername) => {
+  const token = localStorage.getItem("token");
+  return request
+    .delete(`/user/delete`, 
+    { 
+      headers: { token },
+      data: { username, targetUsername }
+    })
+    .then((response) => {
+      if (response.data && response.data.success) {
+        return response.data;
+      } else {
+        return Promise.reject(
+          new Error(response.data.message || "删除用户失败")
+        );
+      }
+    });
+};
+
+export const logoutUser = () => {
+  // 清除本地存储
+  localStorage.removeItem("token");
+  localStorage.removeItem("user");
+
+  return Promise.resolve({
+    success: true,
+    message: "注销成功"
+  });
+};