2
0
Просмотр исходного кода

✨ feat(auth): 合并登录和注册功能为统一认证页面

- 创建AuthPage组件,整合登录和注册功能,支持模式切换
- 实现快速注册功能,仅需输入用户名,系统自动创建账号
- 注册成功后使用默认密码(123456)自动登录
- 删除原LoginPage和RegisterPage组件
- 更新路由配置,将注册路由重定向到统一认证页面

♻️ refactor(routes): 更新移动端路由配置

- 将登录路由指向新的AuthPage组件
- 移除注册路由,重定向到登录页面
- 清理路由文件中不再使用的导入
yourname 6 месяцев назад
Родитель
Сommit
06bb216b8e

+ 194 - 0
src/client/mobile/pages/AuthPage.tsx

@@ -0,0 +1,194 @@
+import React, { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { EyeIcon, EyeSlashIcon, UserIcon, LockClosedIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '@/client/mobile/hooks/AuthProvider';
+import { authClient } from '@/client/api';
+
+type AuthMode = 'login' | 'register';
+
+const AuthPage: React.FC = () => {
+  const [authMode, setAuthMode] = useState<AuthMode>('login');
+  const { register, handleSubmit, formState: { errors } } = useForm();
+  const [showPassword, setShowPassword] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const { login } = useAuth();
+  const navigate = useNavigate();
+
+  const onSubmit = async (data: any) => {
+    try {
+      setLoading(true);
+      
+      if (authMode === 'login') {
+        // 登录逻辑
+        await login(data.username, data.password);
+        navigate('/');
+      } else {
+        // 简单注册逻辑
+        const response = await authClient.register.simple.$post({
+          json: {
+            username: data.username,
+          }
+        });
+        
+        if (response.status !== 201) {
+          const result = await response.json();
+          throw new Error(result.message || '注册失败');
+        }
+        
+        const result = await response.json();
+        
+        // 注册成功后自动登录
+        localStorage.setItem('token', result.token);
+        await login(data.username, '123456'); // 使用默认密码登录
+        
+        // 跳转到首页
+        navigate('/');
+      }
+    } catch (error) {
+      console.error('Authentication error:', error);
+      alert((error as Error).message || `${authMode === 'login' ? '登录' : '注册'}失败,请稍后重试`);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const toggleAuthMode = () => {
+    setAuthMode(authMode === 'login' ? 'register' : 'login');
+  };
+
+  return (
+    <div className="flex justify-center items-center min-h-screen bg-gray-100">
+      <div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
+        <div className="p-6 sm:p-8">
+          <div className="text-center mb-8">
+            <h2 className="text-2xl font-bold text-gray-900">
+              {authMode === 'login' ? '网站登录' : '快速注册'}
+            </h2>
+            <p className="mt-2 text-sm text-gray-600">
+              {authMode === 'login' ? '登录您的账号以继续' : '输入用户名即可快速注册'}
+            </p>
+          </div>
+          
+          <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
+            <div>
+              <label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
+                用户名
+              </label>
+              <div className="relative">
+                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
+                  <UserIcon className="h-5 w-5 text-gray-400" />
+                </div>
+                <input
+                  id="username"
+                  type="text"
+                  className={`w-full pl-10 pr-3 py-2 border ${errors.username ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
+                  placeholder="请输入用户名"
+                  {...register('username', { 
+                    required: '用户名不能为空',
+                    minLength: { value: 3, message: '用户名至少3个字符' },
+                    maxLength: { value: 20, message: '用户名不能超过20个字符' }
+                  })}
+                />
+              </div>
+              {errors.username && (
+                <p className="mt-1 text-sm text-red-600">{errors.username.message?.toString()}</p>
+              )}
+            </div>
+            
+            {authMode === 'login' && (
+              <div>
+                <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
+                  密码
+                </label>
+                <div className="relative">
+                  <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
+                    <LockClosedIcon className="h-5 w-5 text-gray-400" />
+                  </div>
+                  <input
+                    id="password"
+                    type={showPassword ? 'text' : 'password'}
+                    className={`w-full pl-10 pr-10 py-2 border ${errors.password ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
+                    placeholder="请输入密码"
+                    {...register('password', { 
+                      required: '密码不能为空',
+                      minLength: { value: 6, message: '密码至少6个字符' }
+                    })}
+                  />
+                  <button 
+                    type="button"
+                    className="absolute inset-y-0 right-0 pr-3 flex items-center"
+                    onClick={() => setShowPassword(!showPassword)}
+                  >
+                    {showPassword ? (
+                      <EyeSlashIcon className="h-5 w-5 text-gray-400" />
+                    ) : (
+                      <EyeIcon className="h-5 w-5 text-gray-400" />
+                    )}
+                  </button>
+                </div>
+                {errors.password && (
+                  <p className="mt-1 text-sm text-red-600">{errors.password.message?.toString()}</p>
+                )}
+              </div>
+            )}
+
+            {authMode === 'register' && (
+              <div className="bg-blue-50 border border-blue-200 rounded-md p-3">
+                <p className="text-sm text-blue-800">
+                  💡 快速注册:只需输入用户名,系统将自动为您创建账号,默认密码为 <strong>123456</strong>
+                </p>
+              </div>
+            )}
+            
+            <div>
+              <button
+                type="submit"
+                disabled={loading}
+                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
+              >
+                {loading ? (
+                  authMode === 'login' ? '登录中...' : '注册中...'
+                ) : (
+                  authMode === 'login' ? '登录' : '快速注册'
+                )}
+              </button>
+            </div>
+          </form>
+          
+          <div className="mt-6">
+            <div className="relative">
+              <div className="absolute inset-0 flex items-center">
+                <div className="w-full border-t border-gray-300"></div>
+              </div>
+              <div className="relative flex justify-center text-sm">
+                <span className="px-2 bg-white text-gray-500">
+                  {authMode === 'login' ? '还没有账号?' : '已有账号?'}
+                </span>
+              </div>
+            </div>
+            
+            <div className="mt-4">
+              <button
+                type="button"
+                onClick={toggleAuthMode}
+                className="w-full flex justify-center items-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+              >
+                {authMode === 'login' ? (
+                  <>
+                    快速注册
+                    <ArrowRightIcon className="ml-2 h-4 w-4" />
+                  </>
+                ) : (
+                  '返回登录'
+                )}
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default AuthPage;

+ 0 - 133
src/client/mobile/pages/LoginPage.tsx

@@ -1,133 +0,0 @@
-import React, { useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { EyeIcon, EyeSlashIcon, UserIcon, LockClosedIcon } from '@heroicons/react/24/outline';
-import { useNavigate } from 'react-router-dom';
-import { useAuth } from '@/client/mobile/hooks/AuthProvider';
-
-const LoginPage: React.FC = () => {
-  const { register, handleSubmit, formState: { errors } } = useForm();
-  const [showPassword, setShowPassword] = useState(false);
-  const [loading, setLoading] = useState(false);
-  const { login } = useAuth();
-  const navigate = useNavigate();
-
-  const onSubmit = async (data: any) => {
-    try {
-      setLoading(true);
-      await login(data.username, data.password);
-      navigate('/');
-    } catch (error) {
-      console.error('Login error:', error);
-      alert((error as Error).message || '登录失败,请检查用户名和密码');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  return (
-    <div className="flex justify-center items-center min-h-screen bg-gray-100">
-      <div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
-        <div className="p-6 sm:p-8">
-          <div className="text-center mb-8">
-            <h2 className="text-2xl font-bold text-gray-900">网站登录</h2>
-            <p className="mt-2 text-sm text-gray-600">登录您的账号以继续</p>
-          </div>
-          
-          <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
-            <div>
-              <label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
-                用户名
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <UserIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="username"
-                  type="text"
-                  className={`w-full pl-10 pr-3 py-2 border ${errors.username ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请输入用户名"
-                  {...register('username', { 
-                    required: '用户名不能为空',
-                    minLength: { value: 3, message: '用户名至少3个字符' }
-                  })}
-                />
-              </div>
-              {errors.username && (
-                <p className="mt-1 text-sm text-red-600">{errors.username.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
-                密码
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <LockClosedIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="password"
-                  type={showPassword ? 'text' : 'password'}
-                  className={`w-full pl-10 pr-10 py-2 border ${errors.password ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请输入密码"
-                  {...register('password', { 
-                    required: '密码不能为空',
-                    minLength: { value: 6, message: '密码至少6个字符' }
-                  })}
-                />
-                <button 
-                  type="button"
-                  className="absolute inset-y-0 right-0 pr-3 flex items-center"
-                  onClick={() => setShowPassword(!showPassword)}
-                >
-                  {showPassword ? (
-                    <EyeSlashIcon className="h-5 w-5 text-gray-400" />
-                  ) : (
-                    <EyeIcon className="h-5 w-5 text-gray-400" />
-                  )}
-                </button>
-              </div>
-              {errors.password && (
-                <p className="mt-1 text-sm text-red-600">{errors.password.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <button
-                type="submit"
-                disabled={loading}
-                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
-              >
-                {loading ? '登录中...' : '登录'}
-              </button>
-            </div>
-          </form>
-          
-          <div className="mt-6">
-            <div className="relative">
-              <div className="absolute inset-0 flex items-center">
-                <div className="w-full border-t border-gray-300"></div>
-              </div>
-              <div className="relative flex justify-center text-sm">
-                <span className="px-2 bg-white text-gray-500">还没有账号?</span>
-              </div>
-            </div>
-            
-            <div className="mt-4">
-              <button
-                type="button"
-                onClick={() => navigate('/mobile/register')}
-                className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
-              >
-                注册账号
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-export default LoginPage;

+ 0 - 190
src/client/mobile/pages/RegisterPage.tsx

@@ -1,190 +0,0 @@
-import React, { useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { EyeIcon, EyeSlashIcon, UserIcon, LockClosedIcon } from '@heroicons/react/24/outline';
-import { useNavigate } from 'react-router-dom';
-import { useAuth } from '@/client/mobile/hooks/AuthProvider';
-import { authClient } from '@/client/api';
-
-const RegisterPage: React.FC = () => {
-  const { register, handleSubmit, watch, formState: { errors } } = useForm();
-  const [showPassword, setShowPassword] = useState(false);
-  const [showConfirmPassword, setShowConfirmPassword] = useState(false);
-  const [loading, setLoading] = useState(false);
-  const { login } = useAuth();
-  const navigate = useNavigate();
-  const password = watch('password', '');
-
-  const onSubmit = async (data: any) => {
-    try {
-      setLoading(true);
-      
-      // 调用注册API
-      const response = await authClient.register.$post({
-        json: {
-          username: data.username,
-          password: data.password,
-        }
-      });
-      
-      if (response.status !== 201) {
-        const result = await response.json();
-        throw new Error(result.message || '注册失败');
-      }
-      
-      // 注册成功后自动登录
-      await login(data.username, data.password);
-      
-      // 跳转到首页
-      navigate('/');
-    } catch (error) {
-      console.error('Registration error:', error);
-      alert((error as Error).message || '注册失败,请稍后重试');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  return (
-    <div className="flex justify-center items-center min-h-screen bg-gray-100">
-      <div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
-        <div className="p-6 sm:p-8">
-          <div className="text-center mb-8">
-            <h2 className="text-2xl font-bold text-gray-900">账号注册</h2>
-            <p className="mt-2 text-sm text-gray-600">创建新账号以开始使用</p>
-          </div>
-          
-          <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
-            <div>
-              <label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
-                用户名
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <UserIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="username"
-                  type="text"
-                  className={`w-full pl-10 pr-3 py-2 border ${errors.username ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请输入用户名"
-                  {...register('username', { 
-                    required: '用户名不能为空',
-                    minLength: { value: 3, message: '用户名至少3个字符' },
-                    maxLength: { value: 20, message: '用户名不能超过20个字符' }
-                  })}
-                />
-              </div>
-              {errors.username && (
-                <p className="mt-1 text-sm text-red-600">{errors.username.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
-                密码
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <LockClosedIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="password"
-                  type={showPassword ? 'text' : 'password'}
-                  className={`w-full pl-10 pr-10 py-2 border ${errors.password ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请输入密码"
-                  {...register('password', { 
-                    required: '密码不能为空',
-                    minLength: { value: 6, message: '密码至少6个字符' },
-                    maxLength: { value: 30, message: '密码不能超过30个字符' }
-                  })}
-                />
-                <button 
-                  type="button"
-                  className="absolute inset-y-0 right-0 pr-3 flex items-center"
-                  onClick={() => setShowPassword(!showPassword)}
-                >
-                  {showPassword ? (
-                    <EyeSlashIcon className="h-5 w-5 text-gray-400" />
-                  ) : (
-                    <EyeIcon className="h-5 w-5 text-gray-400" />
-                  )}
-                </button>
-              </div>
-              {errors.password && (
-                <p className="mt-1 text-sm text-red-600">{errors.password.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
-                确认密码
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <LockClosedIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="confirmPassword"
-                  type={showConfirmPassword ? 'text' : 'password'}
-                  className={`w-full pl-10 pr-10 py-2 border ${errors.confirmPassword ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请再次输入密码"
-                  {...register('confirmPassword', { 
-                    required: '请确认密码',
-                    validate: value => value === password || '两次密码输入不一致'
-                  })}
-                />
-                <button 
-                  type="button"
-                  className="absolute inset-y-0 right-0 pr-3 flex items-center"
-                  onClick={() => setShowConfirmPassword(!showConfirmPassword)}
-                >
-                  {showConfirmPassword ? (
-                    <EyeSlashIcon className="h-5 w-5 text-gray-400" />
-                  ) : (
-                    <EyeIcon className="h-5 w-5 text-gray-400" />
-                  )}
-                </button>
-              </div>
-              {errors.confirmPassword && (
-                <p className="mt-1 text-sm text-red-600">{errors.confirmPassword.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <button
-                type="submit"
-                disabled={loading}
-                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
-              >
-                {loading ? '注册中...' : '注册'}
-              </button>
-            </div>
-          </form>
-          
-          <div className="mt-6">
-            <div className="relative">
-              <div className="absolute inset-0 flex items-center">
-                <div className="w-full border-t border-gray-300"></div>
-              </div>
-              <div className="relative flex justify-center text-sm">
-                <span className="px-2 bg-white text-gray-500">已有账号?</span>
-              </div>
-            </div>
-            
-            <div className="mt-4">
-              <button
-                type="button"
-                onClick={() => navigate('/mobile/login')}
-                className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
-              >
-                返回登录
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-export default RegisterPage;

+ 3 - 4
src/client/mobile/routes.tsx

@@ -9,14 +9,13 @@ import { ClassroomPage } from './pages/ClassroomPage';
 // import { StockDataPage } from './pages/StockDataPage';
 // import { StockXunlianCodesPage } from './pages/StockXunlianCodesPage';
 // import { DateNotesPage } from './pages/DateNotesPage';
-import LoginPage from './pages/LoginPage';
+import AuthPage from './pages/AuthPage';
 import StockHomePage from './pages/StockHomePage';
 import { XunlianPage } from './pages/XunlianPage';
 import { StockMain } from './components/stock/stock_main';
 import ExamIndex from './components/Exam/ExamIndex';
 import ExamAdmin from './components/Exam/ExamAdmin';
 import ExamCard from './components/Exam/ExamCard';
-import RegisterPage from './pages/RegisterPage';
 import MemberPage from './pages/MemberPage';
 import { MainLayout } from './layouts/MainLayout';
 
@@ -27,11 +26,11 @@ export const router = createBrowserRouter([
   },
   {
     path: '/mobile/login',
-    element: <LoginPage />
+    element: <AuthPage />
   },
   {
     path: '/mobile/register',
-    element: <RegisterPage />
+    element: <Navigate to="/mobile/login" replace />
   },
   {
     path: '/mobile',