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

🗑️ style(mobile): 重命名登录页面并更新移动端路由

- 将Login.tsx重命名为LoginPage.tsx以统一命名规范
- 更新移动端路由配置,移除旧登录页面引用
- 新增RegisterPage和MemberPage组件路由配置

✨ feat(mobile): 新增注册和个人中心功能

- 创建RegisterPage组件,集成表单验证和注册API调用
- 创建MemberPage组件,展示用户个人信息和统计数据
- 在StockHomePage顶部导航栏添加用户登录状态检测和快捷入口

♻️ refactor(mobile): 优化登录页面用户体验

- 使用react-hook-form替代useState管理表单状态
- 添加实时表单验证和错误提示
- 集成密码显示/隐藏切换功能
- 统一使用标准路由跳转逻辑
yourname 7 месяцев назад
Родитель
Сommit
84601dd2e1

+ 0 - 144
src/client/mobile/pages/Login.tsx

@@ -1,144 +0,0 @@
-import React, { useState } from 'react';
-import { useNavigate } from 'react-router';
-import { ArrowRightIcon, LockClosedIcon, UserIcon } from '@heroicons/react/24/outline';
-import { useAuth } from '@/client/mobile/hooks/AuthProvider';
-import { getGlobalConfig } from '@/client/utils/utils';
-
-
-// 登录页面组件
-export const LoginPage: React.FC = () => {
-  const { login } = useAuth();
-  const navigate = useNavigate();
-  const [username, setUsername] = useState('');
-  const [password, setPassword] = useState('');
-  const [loading, setLoading] = useState(false);
-  const [error, setError] = useState<string | null>(null);
-
-  const handleLogin = async (e: React.FormEvent) => {
-    e.preventDefault();
-    
-    if (!username.trim() || !password.trim()) {
-      setError('用户名和密码不能为空');
-      return;
-    }
-    
-    setLoading(true);
-    setError(null);
-    
-    try {
-      
-      const user = await login(username, password);
-      navigate(user.roles?.some(role => role.name === 'admin') ? '/' : '/mobile/classroom');
-    } catch (err) {
-      setError( err instanceof Error ? err.message : '登录失败');
-    } 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">
-          {getGlobalConfig('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={handleLogin}>
-          <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-6">
-            <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>
-          
-          <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 flex items-center justify-between">
-          <button
-            type="button"
-            className="text-sm text-blue-600 hover:text-blue-700"
-            onClick={() => navigate('/mobile/register')}
-          >
-            注册账号
-          </button>
-          <button
-            type="button"
-            className="text-sm text-blue-600 hover:text-blue-700"
-          >
-            忘记密码?
-          </button>
-        </div>
-      </div>
-      
-      {/* 底部文本 */}
-      <div className="mt-auto pt-8 text-center text-blue-100 text-sm">
-        &copy; {new Date().getFullYear()} {getGlobalConfig('APP_NAME') || '移动应用'} 
-        <p className="mt-1">保留所有权利</p>
-      </div>
-    </div>
-  );
-};

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

@@ -0,0 +1,133 @@
+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/home/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('/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;

+ 153 - 0
src/client/mobile/pages/MemberPage.tsx

@@ -0,0 +1,153 @@
+import debug from 'debug';
+import React from 'react';
+import { UserIcon, PencilIcon } from '@heroicons/react/24/outline';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
+import type { InferResponseType } from 'hono/client';
+import { userClient } from '@/client/api';
+import { useAuth, User } from '@/client/home/hooks/AuthProvider';
+
+const MemberPage: React.FC = () => {
+  const navigate = useNavigate();
+  const { user, logout } = useAuth();
+
+  if (!user) {
+    return (
+      <div className="text-center py-12">
+        <h2 className="text-2xl font-bold text-gray-900 mb-4">用户不存在</h2>
+        <button
+          onClick={() => navigate('/')}
+          className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+        >
+          返回首页
+        </button>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="container mx-auto px-4 py-8 max-w-4xl">
+        {/* 用户资料卡片 */}
+        <div className="bg-white rounded-lg shadow-sm p-6 mb-8">
+          <div className="flex flex-col items-center">
+            <div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center mb-4">
+              {user.avatar ? (
+                <img src={user.avatar} alt={user.nickname || user.username} className="h-full w-full object-cover rounded-full" />
+              ) : (
+                <UserIcon className="h-12 w-12 text-gray-500" />
+              )}
+            </div>
+            
+            <h1 className="text-2xl font-bold text-gray-900 mb-1">{user.nickname || user.username}</h1>
+            
+            <div className="flex space-x-8 my-4">
+              <div className="text-center">
+                <p className="text-2xl font-semibold text-gray-900">0</p>
+                <p className="text-sm text-gray-500">内容</p>
+              </div>
+              <div className="text-center">
+                <p className="text-2xl font-semibold text-gray-900">0</p>
+                <p className="text-sm text-gray-500">关注</p>
+              </div>
+              <div className="text-center">
+                <p className="text-2xl font-semibold text-gray-900">0</p>
+                <p className="text-sm text-gray-500">粉丝</p>
+              </div>
+            </div>
+            
+            <div className="flex">
+              <button
+                onClick={() => navigate('/profile/edit')}
+                className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center"
+              >
+                <PencilIcon className="w-4 h-4 mr-2" />
+                编辑资料
+              </button>
+              
+              <button
+                onClick={async () => {
+                  await logout();
+                  navigate('/');
+                }}
+                className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ml-4"
+              >
+                退出登录
+              </button>
+
+            </div>
+            
+            {(user as any).bio && (
+              <p className="mt-4 text-center text-gray-600 max-w-lg">
+                {(user as any).bio}
+              </p>
+            )}
+            
+            <div className="flex items-center mt-4 space-x-4">
+              {(user as any).location && (
+                <div className="flex items-center text-gray-600">
+                  <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
+                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
+                  </svg>
+                  <span className="text-sm">{(user as any).location}</span>
+                </div>
+              )}
+              {(user as any).website && (
+                <a
+                  href={(user as any).website}
+                  target="_blank"
+                  rel="noopener noreferrer"
+                  className="flex items-center text-blue-600 hover:text-blue-800"
+                >
+                  <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6v6m0 0v6m0-6h-6" />
+                  </svg>
+                  <span className="text-sm truncate max-w-[150px]">{(user as any).website}</span>
+                </a>
+              )}
+            </div>
+          </div>
+        </div>
+        
+        {/* 用户内容区域 */}
+        <div className="bg-white rounded-lg shadow-sm p-6">
+          <h2 className="text-xl font-semibold mb-6">个人资料</h2>
+          
+          <div className="space-y-4">
+            <div className="border-b border-gray-100 pb-4">
+              <h3 className="text-sm font-medium text-gray-500 mb-1">用户名</h3>
+              <p className="text-gray-900">{user.username}</p>
+            </div>
+            
+            <div className="border-b border-gray-100 pb-4">
+              <h3 className="text-sm font-medium text-gray-500 mb-1">电子邮箱</h3>
+              <p className="text-gray-900">{user.email || '未设置'}</p>
+            </div>
+            
+            <div className="border-b border-gray-100 pb-4">
+              <h3 className="text-sm font-medium text-gray-500 mb-1">注册时间</h3>
+              <p className="text-gray-900">{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '未知'}</p>
+            </div>
+            
+            <div className="border-b border-gray-100 pb-4">
+              <h3 className="text-sm font-medium text-gray-500 mb-1">最后登录</h3>
+              <p className="text-gray-900">{user.updatedAt ? new Date(user.updatedAt).toLocaleString() : '从未登录'}</p>
+            </div>
+          </div>
+          
+          <div className="mt-8">
+            <button
+              onClick={() => navigate('/profile/security')}
+              className="w-full 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>
+  );
+};
+
+export default MemberPage;

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

@@ -0,0 +1,190 @@
+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/home/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('/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;

+ 72 - 24
src/client/mobile/pages/StockHomePage.tsx

@@ -14,31 +14,79 @@ export default function StockHomePage() {
     }
   };
 
+  
   return (
-    <div className="min-h-screen bg-gray-50 p-4 md:p-8">
-      <h1 className="text-3xl font-bold text-center text-gray-800 mb-8 md:mb-12">
-        股票训练系统
-      </h1>
-      <div className="flex flex-col gap-4 max-w-md mx-auto">
-        <button
-          onClick={handleClassroomClick}
-          className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg shadow-md transition-colors duration-200 text-center"
-        >
-          解盘室
-        </button>
-        <button
-          onClick={() => navigate('/mobile/exam')}
-          className="bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-6 rounded-lg shadow-md transition-colors duration-200 text-center"
-        >
-          考试模式
-        </button>
-        <button
-          onClick={() => navigate('/mobile/xunlian')}
-          className="bg-purple-600 hover:bg-purple-700 text-white font-medium py-3 px-6 rounded-lg shadow-md transition-colors duration-200 text-center"
-        >
-          训练模式
-        </button>
-      </div>
+    <div className="min-h-screen bg-gray-50 flex flex-col">
+      {/* 顶部导航 */}
+      <header className="bg-blue-600 text-white shadow-md fixed w-full z-10">
+        <div className="container mx-auto px-4 py-3 flex justify-between items-center">
+          <h1 className="text-xl font-bold">股票训练系统</h1>
+          {user ? (
+            <div className="flex items-center space-x-4">
+              <div className="flex items-center cursor-pointer" onClick={() => navigate(`/member`)}>
+                <div className="w-8 h-8 rounded-full bg-white text-blue-600 flex items-center justify-center mr-2">
+                  <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+                    <path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
+                  </svg>
+                </div>
+                <span className="hidden md:inline">{user.username}</span>
+              </div>
+            </div>
+          ) : (
+            <div className="flex space-x-2">
+              <button 
+                onClick={() => navigate('/login')}
+                className="px-3 py-1 rounded text-sm bg-white text-blue-600 hover:bg-blue-50"
+              >
+                登录
+              </button>
+              <button 
+                onClick={() => navigate('/register')}
+                className="px-3 py-1 rounded text-sm bg-white text-blue-600 hover:bg-blue-50"
+              >
+                注册
+              </button>
+            </div>
+          )}
+        </div>
+      </header>
+      
+      {/* 主内容区 */}
+      <main className="flex-grow container mx-auto px-4 pt-24 pb-12">
+        
+        <div className="flex flex-col gap-4 max-w-md mx-auto">
+          <button
+            onClick={handleClassroomClick}
+            className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg shadow-md transition-colors duration-200 text-center"
+          >
+            解盘室
+          </button>
+          <button
+            onClick={() => navigate('/mobile/exam')}
+            className="bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-6 rounded-lg shadow-md transition-colors duration-200 text-center"
+          >
+            考试模式
+          </button>
+          <button
+            onClick={() => navigate('/mobile/xunlian')}
+            className="bg-purple-600 hover:bg-purple-700 text-white font-medium py-3 px-6 rounded-lg shadow-md transition-colors duration-200 text-center"
+          >
+            训练模式
+          </button>
+        </div>
+      </main>
+      
+      {/* 页脚 */}
+      <footer className="bg-white border-t border-gray-200 py-4">
+        <div className="container mx-auto px-4 text-center text-gray-500 text-sm">
+          网站模板 ©{new Date().getFullYear()} Created with React & Tailwind CSS
+          <div className="mt-2 space-x-4">
+            <a href="/admin" className="text-blue-600 hover:underline">管理后台</a>
+            <span className="text-gray-300">|</span>
+            <a href="/ui" className="text-blue-600 hover:underline">Api</a>
+          </div>
+        </div>
+      </footer>
     </div>
   );
 }

+ 11 - 1
src/client/mobile/routes.tsx

@@ -9,13 +9,15 @@ import { ClassroomPage } from './pages/ClassroomPage';
 // import { StockDataPage } from './pages/StockDataPage';
 // import { StockXunlianCodesPage } from './pages/StockXunlianCodesPage';
 // import { DateNotesPage } from './pages/DateNotesPage';
-import { LoginPage } from './pages/Login';
+import LoginPage from './pages/LoginPage';
 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';
 
 export const router = createBrowserRouter([
   {
@@ -26,6 +28,10 @@ export const router = createBrowserRouter([
     path: '/mobile/login',
     element: <LoginPage />
   },
+  {
+    path: '/mobile/register',
+    element: <RegisterPage />
+  },
   {
     path: '/mobile',
     element: (
@@ -34,6 +40,10 @@ export const router = createBrowserRouter([
       </ProtectedRoute>
     ),
     children: [
+      {
+        path: '',
+        element: <MemberPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,