ソースを参照

Merge remote-tracking branch 'upstream/starter' into sns

yourname 5 ヶ月 前
コミット
adcf8f1852

+ 35 - 29
src/client/home/components/ErrorPage.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
 import { useRouteError, useNavigate } from 'react-router';
-import { Alert, Button } from 'antd';
 
 export const ErrorPage = () => {
   const navigate = useNavigate();
@@ -8,34 +7,41 @@ export const ErrorPage = () => {
   const errorMessage = error?.statusText || error?.message || '未知错误';
   
   return (
-    <div className="flex flex-col items-center justify-center flex-grow p-4"
-    >
-      <div className="max-w-3xl w-full">
-        <h1 className="text-2xl font-bold mb-4">发生错误</h1>
-        <Alert 
-          type="error"
-          message={error?.message || '未知错误'}
-          description={
-            error?.stack ? (
-              <pre className="text-xs overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded">
-                {error.stack}
-              </pre>
-            ) : null
-          }
-          className="mb-4"
-        />
-        <div className="flex gap-4">
-          <Button 
-            type="primary" 
-            onClick={() => navigate(0)}
-          >
-            重新加载
-          </Button>
-          <Button 
-            onClick={() => navigate('/admin')}
-          >
-            返回首页
-          </Button>
+    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 p-4">
+      <div className="w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl">
+        <div className="bg-red-50 dark:bg-red-900/30 px-6 py-4 border-b border-red-100 dark:border-red-800">
+          <h1 className="text-2xl font-bold text-red-600 dark:text-red-400">发生错误</h1>
+        </div>
+        <div className="p-6">
+          <div className="flex items-start mb-4">
+            <div className="flex-shrink-0 bg-red-100 dark:bg-red-900/50 p-3 rounded-full">
+              <svg className="w-8 h-8 text-red-500 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+              </svg>
+            </div>
+            <div className="ml-4">
+              <h3 className="text-lg font-medium text-gray-900 dark:text-white">{error?.message || '未知错误'}</h3>
+              {error?.stack && (
+                <pre className="mt-2 text-xs text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 p-3 rounded overflow-x-auto max-h-40">
+                  {error.stack}
+                </pre>
+              )}
+            </div>
+          </div>
+          <div className="flex gap-4">
+            <button
+              onClick={() => navigate(0)}
+              className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
+            >
+              重新加载
+            </button>
+            <button
+              onClick={() => navigate('/')}
+              className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
+            >
+              返回首页
+            </button>
+          </div>
         </div>
       </div>
     </div>

+ 21 - 12
src/client/home/components/NotFoundPage.tsx

@@ -1,24 +1,33 @@
 import React from 'react';
 import { useNavigate } from 'react-router';
-import { Button } from 'antd';
 
 export const NotFoundPage = () => {
   const navigate = useNavigate();
   
   return (
-    <div className="flex flex-col items-center justify-center flex-grow p-4">
-      <div className="max-w-3xl w-full">
-        <h1 className="text-2xl font-bold mb-4">404 - 页面未找到</h1>
-        <p className="mb-6 text-gray-600 dark:text-gray-300">
-          您访问的页面不存在或已被移除
-        </p>
-        <div className="flex gap-4">
-          <Button 
-            type="primary" 
-            onClick={() => navigate('/admin')}
+    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 p-4">
+      <div className="w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl">
+        <div className="bg-blue-50 dark:bg-blue-900/30 px-6 py-4 border-b border-blue-100 dark:border-blue-800">
+          <h1 className="text-2xl font-bold text-blue-600 dark:text-blue-400">404 - 页面未找到</h1>
+        </div>
+        <div className="p-6">
+          <div className="flex items-start mb-6">
+            <div className="flex-shrink-0 bg-blue-100 dark:bg-blue-900/50 p-3 rounded-full">
+              <svg className="w-10 h-10 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+              </svg>
+            </div>
+            <div className="ml-4">
+              <p className="text-lg text-gray-700 dark:text-gray-300">您访问的页面不存在或已被移除</p>
+              <p className="mt-2 text-gray-500 dark:text-gray-400">请检查URL是否正确或返回首页</p>
+            </div>
+          </div>
+          <button
+            onClick={() => navigate('/')}
+            className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 w-full"
           >
             返回首页
-          </Button>
+          </button>
         </div>
       </div>
     </div>

+ 1 - 1
src/client/home/hooks/AuthProvider.tsx

@@ -49,7 +49,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
   };
 
   // 使用useQuery检查登录状态
-  const { isLoading } = useQuery({
+  const { isFetching: isLoading } = useQuery({
     queryKey: ['auth', 'status', token],
     queryFn: async () => {
       if (!token) {

+ 107 - 67
src/client/home/pages/LoginPage.tsx

@@ -1,91 +1,131 @@
 import React, { useState } from 'react';
-import { Form, Input, Button, Card, Typography, message, Divider } from 'antd';
-import { UserOutlined, LockOutlined } from '@ant-design/icons';
-import { Link, useNavigate } from 'react-router-dom';
-import { authClient } from '@/client/api';
+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 { Title } = Typography;
-
 const LoginPage: React.FC = () => {
+  const { register, handleSubmit, formState: { errors } } = useForm();
+  const [showPassword, setShowPassword] = useState(false);
   const [loading, setLoading] = useState(false);
-  const [form] = Form.useForm();
   const { login } = useAuth();
   const navigate = useNavigate();
 
-  const handleSubmit = async (values: any) => {
+  const onSubmit = async (data: any) => {
     try {
       setLoading(true);
-      await login(values.username, values.password);
-      
-      message.success('登录成功');
+      await login(data.username, data.password);
       navigate('/');
     } catch (error) {
       console.error('Login error:', error);
-      message.error((error as Error).message || '登录失败,请检查用户名和密码');
+      alert((error as Error).message || '登录失败,请检查用户名和密码');
     } finally {
       setLoading(false);
     }
   };
 
   return (
-    <div className="flex justify-center items-center min-h-screen bg-gray-50">
-      <Card className="w-full max-w-md shadow-lg">
-        <div className="text-center mb-6">
-          <Title level={2}>社交媒体平台</Title>
-          <p className="text-gray-500">登录您的账号</p>
-        </div>
-        
-        <Form
-          form={form}
-          name="login_form"
-          layout="vertical"
-          onFinish={handleSubmit}
-        >
-          <Form.Item
-            name="username"
-            label="用户名"
-            rules={[{ required: true, message: '请输入用户名' }]}
-          >
-            <Input 
-              prefix={<UserOutlined className="text-primary" />} 
-              placeholder="请输入用户名" 
-            />
-          </Form.Item>
+    <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.Item
-            name="password"
-            label="密码"
-            rules={[{ required: true, message: '请输入密码' }]}
-          >
-            <Input.Password
-              prefix={<LockOutlined className="text-primary" />}
-              placeholder="请输入密码"
-            />
-          </Form.Item>
+          <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>
           
-          <Form.Item>
-            <Button 
-              type="primary" 
-              htmlType="submit" 
-              className="w-full h-10 text-base"
-              loading={loading}
-            >
-              登录
-            </Button>
-          </Form.Item>
-        </Form>
-        
-        <Divider>还没有账号?</Divider>
-        
-        <Button 
-          type="default" 
-          className="w-full"
-          onClick={() => navigate('/register')}
-        >
-          注册账号
-        </Button>
-      </Card>
+          <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>
   );
 };

+ 190 - 0
src/client/home/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;

+ 1 - 1
src/server/api/auth/register/create.ts

@@ -18,7 +18,7 @@ const RegisterSchema = z.object({
   email: z.string().email().openapi({
     example: 'john@example.com',
     description: '邮箱'
-  })
+  }).optional()
 })
 
 const TokenResponseSchema = z.object({