Ver Fonte

✨ feat(home-shadcn): 初始化基于shadcn/ui的客户端应用

- 创建ErrorPage和NotFoundPage错误处理组件
- 实现ProtectedRoute路由守卫组件
- 添加AuthProvider认证上下文和hooks
- 构建HomePage、LoginPage、RegisterPage和MemberPage基础页面
- 配置路由系统和MainLayout布局组件
- 集成React Query、React Router和Tailwind CSS
yourname há 4 meses atrás
pai
commit
75743f4833

+ 49 - 0
src/client/home-shadcn/components/ErrorPage.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { useRouteError, useNavigate } from 'react-router';
+
+export const ErrorPage = () => {
+  const navigate = useNavigate();
+  const error = useRouteError() as any;
+  const errorMessage = error?.statusText || error?.message || '未知错误';
+  
+  return (
+    <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>
+  );
+};

+ 35 - 0
src/client/home-shadcn/components/NotFoundPage.tsx

@@ -0,0 +1,35 @@
+import React from 'react';
+import { useNavigate } from 'react-router';
+
+export const NotFoundPage = () => {
+  const navigate = useNavigate();
+  
+  return (
+    <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>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 37 - 0
src/client/home-shadcn/components/ProtectedRoute.tsx

@@ -0,0 +1,37 @@
+import React, { useEffect } from 'react';
+import { 
+  useNavigate,
+} from 'react-router';
+import { useAuth } from '../hooks/AuthProvider';
+
+
+
+
+
+export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
+  const { isAuthenticated, isLoading } = useAuth();
+  const navigate = useNavigate();
+  
+  useEffect(() => {
+    // 只有在加载完成且未认证时才重定向
+    if (!isLoading && !isAuthenticated) {
+      navigate('/login', { replace: true });
+    }
+  }, [isAuthenticated, isLoading, navigate]);
+  
+  // 显示加载状态,直到认证检查完成
+  if (isLoading) {
+    return (
+      <div className="flex justify-center items-center h-screen">
+        <div className="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12"></div>
+      </div>
+    );
+  }
+  
+  // 如果未认证且不再加载中,不显示任何内容(等待重定向)
+  if (!isAuthenticated) {
+    return null;
+  }
+  
+  return children;
+};

+ 140 - 0
src/client/home-shadcn/hooks/AuthProvider.tsx

@@ -0,0 +1,140 @@
+import React, { useState, useEffect, createContext, useContext } from 'react';
+
+import {
+  useQuery,
+  useQueryClient,
+} from '@tanstack/react-query';
+import axios from 'axios';
+import 'dayjs/locale/zh-cn';
+import type {
+  AuthContextType
+} from '@/share/types';
+import { authClient } from '@/client/api';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+
+export type User = InferResponseType<typeof authClient.me.$get, 200>;
+
+
+// 创建认证上下文
+const AuthContext = createContext<AuthContextType<User> | null>(null);
+
+// 认证提供器组件
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  const [user, setUser] = useState<User | null>(null);
+  const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
+  const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
+  const queryClient = useQueryClient();
+
+  // 声明handleLogout函数
+  const handleLogout = async () => {
+    try {
+      // 如果已登录,调用登出API
+      if (token) {
+        await authClient.logout.$post();
+      }
+    } catch (error) {
+      console.error('登出请求失败:', error);
+    } finally {
+      // 清除本地状态
+      setToken(null);
+      setUser(null);
+      setIsAuthenticated(false);
+      localStorage.removeItem('token');
+      // 清除Authorization头
+      delete axios.defaults.headers.common['Authorization'];
+      console.log('登出时已删除全局Authorization头');
+      // 清除所有查询缓存
+      queryClient.clear();
+    }
+  };
+
+  // 使用useQuery检查登录状态
+  const { isFetching: isLoading } = useQuery({
+    queryKey: ['auth', 'status', token],
+    queryFn: async () => {
+      if (!token) {
+        setIsAuthenticated(false);
+        setUser(null);
+        return null;
+      }
+
+      try {
+        // 设置全局默认请求头
+        axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
+        // 使用API验证当前用户
+        const res = await authClient.me.$get();
+        if (res.status !== 200) {
+          const result = await res.json();
+          throw new Error(result.message)
+        }
+        const currentUser = await res.json();
+        setUser(currentUser);
+        setIsAuthenticated(true);
+        return { isValid: true, user: currentUser };
+      } catch (error) {
+        return { isValid: false };
+      }
+    },
+    enabled: !!token,
+    refetchOnWindowFocus: false,
+    retry: false
+  });
+
+  const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise<void> => {
+    try {
+      // 使用AuthAPI登录
+      const response = await authClient.login.$post({
+        json: {
+          username,
+          password
+        }
+      })
+      if (response.status !== 200) {
+        const result = await response.json()
+        throw new Error(result.message);
+      }
+
+      const result = await response.json()
+
+      // 保存token和用户信息
+      const { token: newToken, user: newUser } = result;
+
+      // 设置全局默认请求头
+      axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
+
+      // 保存状态
+      setToken(newToken);
+      setUser(newUser);
+      setIsAuthenticated(true);
+      localStorage.setItem('token', newToken);
+
+    } catch (error) {
+      console.error('登录失败:', error);
+      throw error;
+    }
+  };
+
+  return (
+    <AuthContext.Provider
+      value={{
+        user,
+        token,
+        login: handleLogin,
+        logout: handleLogout,
+        isAuthenticated,
+        isLoading
+      }}
+    >
+      {children}
+    </AuthContext.Provider>
+  );
+};
+
+// 使用上下文的钩子
+export const useAuth = () => {
+  const context = useContext(AuthContext);
+  if (!context) {
+    throw new Error('useAuth必须在AuthProvider内部使用');
+  }
+  return context;
+};

+ 28 - 0
src/client/home-shadcn/index.tsx

@@ -0,0 +1,28 @@
+import { createRoot } from 'react-dom/client'
+import { getGlobalConfig } from '../utils/utils'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { AuthProvider } from './hooks/AuthProvider'
+import { RouterProvider } from 'react-router-dom'
+import { router } from './routes'
+
+// 创建QueryClient实例
+const queryClient = new QueryClient();
+
+// 应用入口组件
+const App = () => {
+  return (
+    <QueryClientProvider client={queryClient}>
+      <AuthProvider>
+        <RouterProvider router={router} />
+      </AuthProvider>
+    </QueryClientProvider>
+  )
+};
+
+const rootElement = document.getElementById('root')
+if (rootElement) {
+  const root = createRoot(rootElement)
+  root.render(
+    <App />
+  )
+}

+ 14 - 0
src/client/home-shadcn/layouts/MainLayout.tsx

@@ -0,0 +1,14 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+  Outlet
+} from 'react-router';
+
+/**
+ * 主布局组件
+ * 包含侧边栏、顶部导航和内容区域
+ */
+export const MainLayout = () => {
+  return (
+    <Outlet />
+  );
+};

+ 103 - 0
src/client/home-shadcn/pages/HomePage.tsx

@@ -0,0 +1,103 @@
+import React from 'react';
+import { useAuth } from '@/client/home/hooks/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+
+const HomePage: React.FC = () => {
+  const { user } = useAuth();
+  const navigate = useNavigate();
+
+  return (
+    <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="bg-white rounded-lg shadow-sm p-6 md:p-8">
+          <h2 className="text-2xl font-bold text-gray-900 mb-4">欢迎使用网站模板</h2>
+          <p className="text-gray-600 mb-6">
+            这是一个通用的网站首页模板,您可以根据需要进行自定义。
+            已包含基础的登录、注册和用户中心功能。
+          </p>
+          
+          <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
+            <div className="bg-blue-50 p-5 rounded-lg text-center">
+              <div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
+                <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
+                </svg>
+              </div>
+              <h3 className="font-semibold text-lg mb-2">用户认证</h3>
+              <p className="text-gray-600 text-sm">完整的登录、注册功能,保护您的网站安全</p>
+            </div>
+            
+            <div className="bg-green-50 p-5 rounded-lg text-center">
+              <div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
+                <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
+                </svg>
+              </div>
+              <h3 className="font-semibold text-lg mb-2">用户中心</h3>
+              <p className="text-gray-600 text-sm">用户可以查看和管理个人信息</p>
+            </div>
+            
+            <div className="bg-purple-50 p-5 rounded-lg text-center">
+              <div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
+                <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
+                </svg>
+              </div>
+              <h3 className="font-semibold text-lg mb-2">响应式设计</h3>
+              <p className="text-gray-600 text-sm">适配各种设备屏幕,提供良好的用户体验</p>
+            </div>
+          </div>
+        </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>
+  );
+};
+
+export default HomePage;

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

+ 49 - 0
src/client/home-shadcn/routes.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { createBrowserRouter, Navigate } from 'react-router';
+import { ProtectedRoute } from './components/ProtectedRoute';
+import { ErrorPage } from './components/ErrorPage';
+import { NotFoundPage } from './components/NotFoundPage';
+import HomePage from './pages/HomePage';
+import { MainLayout } from './layouts/MainLayout';
+import LoginPage from './pages/LoginPage';
+import RegisterPage from './pages/RegisterPage';
+import MemberPage from './pages/MemberPage';
+
+export const router = createBrowserRouter([
+  {
+    path: '/',
+    element: <HomePage />
+  },
+  {
+    path: '/login',
+    element: <LoginPage />
+  },
+  {
+    path: '/register',
+    element: <RegisterPage />
+  },
+  {
+    path: '/member',
+    element: (
+      <ProtectedRoute>
+        <MainLayout />
+      </ProtectedRoute>
+    ),
+    children: [
+      {
+        path: '',
+        element: <MemberPage />
+      },
+      {
+        path: '*',
+        element: <NotFoundPage />,
+        errorElement: <ErrorPage />
+      },
+    ],
+  },
+  {
+    path: '*',
+    element: <NotFoundPage />,
+    errorElement: <ErrorPage />
+  },
+]);