Bläddra i källkod

增加 移动端 注册页

yourname 7 månader sedan
förälder
incheckning
d97690dda9

+ 6 - 0
client/mobile/mobile_app.tsx

@@ -16,6 +16,7 @@ import 'dayjs/locale/zh-cn';
 import { AuthProvider, ThemeProvider, useAuth } from './hooks.tsx';
 import HomePage from './pages_index.tsx';
 import LoginPage from './pages_login.tsx';
+import RegisterPage from './pages_register.tsx';
 import { GlobalConfig } from "../share/types.ts";
 import { ExclamationTriangleIcon, HomeIcon, BellIcon, UserIcon } from '@heroicons/react/24/outline';
 import { NotificationsPage } from './pages_messages.tsx';
@@ -254,6 +255,11 @@ const App = () => {
       element: <LoginPage />,
       errorElement: <ErrorPage />
     },
+    {
+      path: '/mobile/register',
+      element: <RegisterPage />,
+      errorElement: <ErrorPage />
+    },
     {
       path: '/mobile',
       element: (

+ 1 - 1
client/mobile/pages_login.tsx

@@ -146,7 +146,7 @@ const LoginPage: React.FC = () => {
           <button
             type="button"
             className="text-sm text-blue-600 hover:text-blue-700"
-            onClick={() => navigate('/register')}
+            onClick={() => navigate('/mobile/register')}
           >
             注册账号
           </button>

+ 193 - 0
client/mobile/pages_register.tsx

@@ -0,0 +1,193 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router';
+import { ArrowRightIcon, EnvelopeIcon, LockClosedIcon, UserIcon } from '@heroicons/react/24/outline';
+import { AuthAPI } from './api.ts';
+import { handleApiError } from './utils.ts';
+
+const RegisterPage: React.FC = () => {
+  const navigate = useNavigate();
+  const [username, setUsername] = useState('');
+  const [email, setEmail] = useState('');
+  const [password, setPassword] = useState('');
+  const [confirmPassword, setConfirmPassword] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+
+  const handleRegister = async (e: React.FormEvent) => {
+    e.preventDefault();
+    
+    // 表单验证
+    if (!username.trim()) {
+      setError('用户名不能为空');
+      return;
+    }
+    
+    if (!email.trim()) {
+      setError('邮箱不能为空');
+      return;
+    }
+    
+    if (!password.trim()) {
+      setError('密码不能为空');
+      return;
+    }
+    
+    if (password !== confirmPassword) {
+      setError('两次输入的密码不一致');
+      return;
+    }
+    
+    setLoading(true);
+    setError(null);
+    
+    try {
+      await AuthAPI.register(username, email, password);
+      // 注册成功后跳转到登录页
+      navigate('/mobile/login');
+    } catch (err) {
+      setError(handleApiError(err));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="min-h-screen flex flex-col bg-gradient-to-b from-blue-500 to-blue-700 p-6">
+      {/* 顶部Logo和标题 */}
+      <div className="flex flex-col items-center justify-center mt-10 mb-8">
+        <div className="w-20 h-20 bg-white rounded-2xl flex items-center justify-center shadow-lg mb-4">
+          <svg className="w-12 h-12 text-blue-600" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <path d="M12 2L2 7L12 12L22 7L12 2Z" fill="currentColor" />
+            <path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
+            <path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
+          </svg>
+        </div>
+        <h1 className="text-3xl font-bold text-white">
+          {window.CONFIG?.APP_NAME || '移动应用'}
+        </h1>
+        <p className="text-blue-100 mt-2">创建您的账户</p>
+      </div>
+
+      {/* 注册表单 */}
+      <div className="bg-white rounded-xl shadow-xl p-6 w-full">
+        {error && (
+          <div className="bg-red-50 text-red-700 p-3 rounded-lg mb-4 text-sm">
+            {error}
+          </div>
+        )}
+        
+        <form onSubmit={handleRegister}>
+          <div className="mb-4">
+            <label className="block text-gray-700 text-sm font-medium mb-2" htmlFor="username">
+              用户名
+            </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"
+                value={username}
+                onChange={(e) => setUsername(e.target.value)}
+                className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+                placeholder="请输入用户名"
+              />
+            </div>
+          </div>
+          
+          <div className="mb-4">
+            <label className="block text-gray-700 text-sm font-medium mb-2" htmlFor="email">
+              邮箱
+            </label>
+            <div className="relative">
+              <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
+                <EnvelopeIcon className="h-5 w-5 text-gray-400" />
+              </div>
+              <input
+                id="email"
+                type="email"
+                value={email}
+                onChange={(e) => setEmail(e.target.value)}
+                className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+                placeholder="请输入邮箱"
+              />
+            </div>
+          </div>
+          
+          <div className="mb-4">
+            <label className="block text-gray-700 text-sm font-medium mb-2" htmlFor="password">
+              密码
+            </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="password"
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+                placeholder="请输入密码"
+              />
+            </div>
+          </div>
+          
+          <div className="mb-6">
+            <label className="block text-gray-700 text-sm font-medium mb-2" htmlFor="confirmPassword">
+              确认密码
+            </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="password"
+                value={confirmPassword}
+                onChange={(e) => setConfirmPassword(e.target.value)}
+                className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+                placeholder="请再次输入密码"
+              />
+            </div>
+          </div>
+          
+          <button
+            type="submit"
+            disabled={loading}
+            className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex items-center justify-center"
+          >
+            {loading ? (
+              <svg className="animate-spin -ml-1 mr-2 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
+                <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
+                <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+              </svg>
+            ) : (
+              <ArrowRightIcon className="h-5 w-5 mr-2" />
+            )}
+            {loading ? '注册中...' : '注册'}
+          </button>
+        </form>
+        
+        <div className="mt-6 text-center">
+          <button
+            type="button"
+            className="text-sm text-blue-600 hover:text-blue-700"
+            onClick={() => navigate('/mobile/login')}
+          >
+            已有账号?立即登录
+          </button>
+        </div>
+      </div>
+      
+      {/* 底部文本 */}
+      <div className="mt-auto pt-8 text-center text-blue-100 text-sm">
+        &copy; {new Date().getFullYear()} {window.CONFIG?.APP_NAME || '移动应用'} 
+        <p className="mt-1">保留所有权利</p>
+      </div>
+    </div>
+  );
+};
+
+export default RegisterPage;

+ 0 - 0
test/pages_know_info.test.ts → test/admin/know_info.spec.ts


+ 24 - 0
test/mobile/register.spec.ts

@@ -0,0 +1,24 @@
+
+import { test } from '@playwright/test';
+import { expect } from '@playwright/test';
+
+test('移动端注册测试', async ({ page, context }) => {
+
+    // Navigate to URL
+    await page.goto('https://pre-117-77-template.r.d8d.fun/mobile/register');
+
+    // Fill input field
+    await page.fill('#username', 'testuser');
+
+    // Fill input field
+    await page.fill('#email', 'testuser@example.com');
+
+    // Fill input field
+    await page.fill('#password', 'Test1234!');
+
+    // Fill input field
+    await page.fill('#confirmPassword', 'Test1234!');
+
+    // Click element
+    await page.click('button[type='submit']');
+});