Selaa lähdekoodia

✨ feat(home): implement authentication system and main page features

- add AuthProvider for user authentication management
- create login page with form validation
- implement protected route component for authenticated pages
- develop main layout structure for application
- add home page with post feed and user interaction features
- create user profile page with follow functionality
- implement error and not found pages
- set up routing system with protected routes

✨ feat(components): add essential UI components

- create ErrorPage component for error handling
- add NotFoundPage for 404 scenarios
- implement ProtectedRoute for authentication check
- develop MainLayout as base layout component

♻️ refactor(home): restructure home index and add routing

- replace simple home page with router-based application structure
- integrate React Query for data fetching
- set up main application entry with AuthProvider and QueryClient
- implement routing configuration with createBrowserRouter
yourname 4 kuukautta sitten
vanhempi
sitoutus
75cb41068d

+ 43 - 0
src/client/home/components/ErrorPage.tsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import { useRouteError, useNavigate } from 'react-router';
+import { Alert, Button } from 'antd';
+
+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 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>
+      </div>
+    </div>
+  );
+};

+ 26 - 0
src/client/home/components/NotFoundPage.tsx

@@ -0,0 +1,26 @@
+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')}
+          >
+            返回首页
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 37 - 0
src/client/home/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/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';
+
+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 { 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;
+};

+ 16 - 39
src/client/home/index.tsx

@@ -1,51 +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'
 
-const Home = () => {
-  return (
-    
-    <div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
-    <div className="max-w-md w-full space-y-8">
-      {/* 系统介绍区域 */}
-      <div className="text-center">
-        <h1 className="text-4xl font-bold text-gray-900 mb-4">
-          {getGlobalConfig('APP_NAME')}
-        </h1>
-        <p className="text-lg text-gray-600 mb-8">
-          全功能应用Starter
-        </p>
-        <p className="text-base text-gray-500 mb-8">
-          这是一个基于Hono和React的应用Starter,提供了用户认证、文件管理、图表分析、地图集成和主题切换等常用功能。
-        </p>
-      </div>
+// 创建QueryClient实例
+const queryClient = new QueryClient();
 
-      {/* 管理入口按钮 */}
-      <div className="space-y-4">
-        <a
-          href="/admin"
-          className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-lg 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"
-        >
-          进入管理后台
-        </a>
-        
-        {/* 移动端入口按钮 */}
-        <a
-          href="/mobile"
-          className="w-full flex justify-center py-3 px-4 border border-blue-600 rounded-md shadow-sm text-lg font-medium text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
-        >
-          进入移动端
-        </a>
-        
-      </div>
-    </div>
-  </div>
+// 应用入口组件
+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(
-    <Home />
+    <App />
   )
 }

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

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

+ 203 - 0
src/client/home/pages/HomePage.tsx

@@ -0,0 +1,203 @@
+import React, { useEffect, useState } from 'react';
+import { Layout, List, Card, Avatar, Button, Input, Space, Typography, Spin, Empty, Divider } from 'antd';
+import { UserOutlined, MessageOutlined, HeartOutlined, SendOutlined, UserAddOutlined } from '@ant-design/icons';
+import { useAuth } from '@/client/home/hooks/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+import { postClient, userClient } from '@/client/api';
+import type { PostEntity } from '@/server/modules/posts/post.entity';
+import type { UserEntity } from '@/server/modules/users/user.entity';
+
+const { Header, Content, Footer } = Layout;
+const { Title, Text, Paragraph } = Typography;
+
+const HomePage: React.FC = () => {
+  const [posts, setPosts] = useState<PostEntity[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [content, setContent] = useState('');
+  const [recommendedUsers, setRecommendedUsers] = useState<UserEntity[]>([]);
+  const [hasFollowing, setHasFollowing] = useState(true);
+  const { user } = useAuth();
+  const navigate = useNavigate();
+
+  // 获取首页内容流
+  // 获取推荐用户
+  const fetchRecommendedUsers = async () => {
+    try {
+      const response = await userClient.recommended.$get({
+        query: { limit: 5 }
+      });
+      
+      if (!response.ok) throw new Error('获取推荐用户失败');
+      
+      const data = await response.json();
+      setRecommendedUsers(data.data);
+    } catch (error) {
+      console.error('Error fetching recommended users:', error);
+    }
+  };
+
+  // 获取首页内容流
+  const fetchPosts = async () => {
+    try {
+      setLoading(true);
+      const response = await postClient.$get({
+        query: {
+          page: 1,
+          pageSize: 10
+        }
+      });
+      
+      if (!response.ok) throw new Error('获取内容失败');
+      
+      const data = await response.json();
+      setPosts(data.data);
+      setHasFollowing(data.meta?.hasFollowing ?? true);
+      
+      // 如果没有关注,则获取推荐用户
+      if (!data.meta?.hasFollowing) {
+        await fetchRecommendedUsers();
+      }
+    } catch (error) {
+      console.error('Error fetching posts:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 创建新帖子
+  const handlePost = async () => {
+    if (!content.trim()) return;
+    
+    try {
+      const response = await postClient.$post({
+        json: {
+          content
+        }
+      });
+      
+      if (!response.ok) throw new Error('发布失败');
+      
+      const newPost = await response.json();
+      setPosts([newPost, ...posts]);
+      setContent('');
+    } catch (error) {
+      console.error('Error creating post:', error);
+    }
+  };
+
+  // 点赞帖子
+  const handleLike = async (postId: number) => {
+    try {
+      await postClient[':id'].like.$post({
+        param: { id: postId }
+      });
+      setPosts(posts.map(post =>
+        post.id === postId ? { ...post, likesCount: post.likesCount + 1 } : post
+      ));
+    } catch (error) {
+      console.error('Error liking post:', error);
+    }
+  };
+
+  // 关注用户
+  const handleFollow = async (userId: number) => {
+    try {
+      await userClient[':id'].follow.$post({
+        param: { id: userId }
+      });
+      
+      // 从推荐列表中移除已关注用户
+      setRecommendedUsers(recommendedUsers.filter(user => user.id !== userId));
+      
+      // 刷新帖子列表
+      fetchPosts();
+    } catch (error) {
+      console.error('Error following user:', error);
+    }
+  };
+
+  useEffect(() => {
+    fetchPosts();
+  }, []);
+
+  return (
+    <Layout className="min-h-screen">
+      <Header style={{ position: 'fixed', zIndex: 1, width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+        <Title level={3} style={{ color: 'white', margin: 0 }}>社交媒体平台</Title>
+        <div style={{ display: 'flex', alignItems: 'center' }}>
+          <Button type="text" style={{ color: 'white', marginRight: 16 }} onClick={() => navigate('/follow?type=following')}>
+            关注
+          </Button>
+          <Button type="text" style={{ color: 'white', marginRight: 16 }} onClick={() => navigate('/follow?type=followers')}>
+            粉丝
+          </Button>
+          <Avatar icon={<UserOutlined />} style={{ marginRight: 16 }} onClick={() => navigate(`/users/${user?.id}`)} />
+          <Text style={{ color: 'white' }}>{user?.username}</Text>
+        </div>
+      </Header>
+      
+      <Content style={{ padding: '0 50px', marginTop: 64 }}>
+        <div style={{ background: '#fff', padding: 24, marginTop: 20, borderRadius: 8, maxWidth: 800, margin: '0 auto' }}>
+          {/* 发布框 */}
+          <Card style={{ marginBottom: 24 }}>
+            <Space.Compact style={{ width: '100%' }}>
+              <Input.TextArea 
+                placeholder="分享你的想法..." 
+                rows={4}
+                value={content}
+                onChange={e => setContent(e.target.value)}
+              />
+              <Button type="primary" icon={<SendOutlined />} onClick={handlePost}>
+                发布
+              </Button>
+            </Space.Compact>
+          </Card>
+          
+          {/* 内容流 */}
+          <Title level={4}>最新动态</Title>
+          {loading ? (
+            <Spin size="large" style={{ display: 'block', margin: '40px auto' }} />
+          ) : posts.length === 0 ? (
+            <Empty description="暂无内容" />
+          ) : (
+            <List
+              dataSource={posts}
+              renderItem={item => (
+                <List.Item
+                  key={item.id}
+                  actions={[
+                    <Button icon={<HeartOutlined />} onClick={() => handleLike(item.id)}>
+                      {item.likesCount > 0 && item.likesCount}
+                    </Button>,
+                    <Button icon={<MessageOutlined />}>{item.commentsCount > 0 && item.commentsCount}</Button>,
+                    <Button>分享</Button>
+                  ]}
+                >
+                  <List.Item.Meta
+                    avatar={<Avatar src={item.user?.avatar || <UserOutlined />} />}
+                    title={item.user?.username}
+                    description={
+                      <>
+                        <Paragraph>{item.content}</Paragraph>
+                        {item.images?.map((img, idx) => (
+                          <img key={idx} src={img} alt={`Post image ${idx}`} style={{ maxWidth: '100%', margin: '8px 0' }} />
+                        ))}
+                        <Text type="secondary">{new Date(item.createdAt).toLocaleString()}</Text>
+                      </>
+                    }
+                  />
+                </List.Item>
+              )}
+            />
+          )}
+        </div>
+      </Content>
+      
+      <Footer style={{ textAlign: 'center' }}>
+        社交媒体平台 ©{new Date().getFullYear()} Created with React & Ant Design
+      </Footer>
+    </Layout>
+  );
+};
+
+export default HomePage;

+ 93 - 0
src/client/home/pages/LoginPage.tsx

@@ -0,0 +1,93 @@
+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 { useAuth } from '@/client/home/hooks/AuthProvider';
+
+const { Title } = Typography;
+
+const LoginPage: React.FC = () => {
+  const [loading, setLoading] = useState(false);
+  const [form] = Form.useForm();
+  const { login } = useAuth();
+  const navigate = useNavigate();
+
+  const handleSubmit = async (values: any) => {
+    try {
+      setLoading(true);
+      await login(values.username, values.password);
+      
+      message.success('登录成功');
+      navigate('/');
+    } catch (error) {
+      console.error('Login error:', error);
+      message.error((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>
+          
+          <Form.Item
+            name="password"
+            label="密码"
+            rules={[{ required: true, message: '请输入密码' }]}
+          >
+            <Input.Password
+              prefix={<LockOutlined className="text-primary" />}
+              placeholder="请输入密码"
+            />
+          </Form.Item>
+          
+          <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>
+  );
+};
+
+export default LoginPage;

+ 280 - 0
src/client/home/pages/MemberPage.tsx

@@ -0,0 +1,280 @@
+import debug from 'debug';
+const rpcLogger = debug('frontend:api:rpc');
+import React, { useEffect, useState } from 'react';
+import { Layout, Card, Avatar, Button, Typography, List, Spin, Tabs, Divider, Badge } from 'antd';
+import { UserOutlined, EditOutlined, HeartOutlined, UserAddOutlined, UserDeleteOutlined } from '@ant-design/icons';
+import { useParams, useNavigate } from 'react-router-dom';
+import { userClient } from '@/client/api';
+import { useAuth } from '@/client/home/hooks/AuthProvider';
+import type { UserEntity } from '@/server/modules/users/user.entity';
+
+const { Content } = Layout;
+const { Title, Text, Paragraph } = Typography;
+const { TabPane } = Tabs;
+
+const UserProfilePage: React.FC = () => {
+  const [user, setUser] = useState<UserEntity | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [isFollowing, setIsFollowing] = useState(false);
+  const [followerCount, setFollowerCount] = useState(0);
+  const [followingCount, setFollowingCount] = useState(0);
+  const { id: userId } = useParams<{ id: string }>();
+  const id = Number(userId);
+  const navigate = useNavigate();
+  const { user: currentUser } = useAuth();
+
+  // 获取用户资料
+  const fetchUserProfile = async () => {
+    if (!id) return;
+    
+    try {
+      setLoading(true);
+      rpcLogger('Fetching user profile for id: %s', id);
+      const response = await userClient[':id'].$get({
+        param: { id }
+      });
+      
+      if (!response.ok) throw new Error('获取用户资料失败');
+      
+      const userData = await response.json();
+      setUser(userData);
+      
+      // 获取关注状态
+      if (currentUser && currentUser.id !== Number(id)) {
+        rpcLogger('Checking follow status from user %s to user %s', currentUser.id, id);
+        const followStatus = await userClient[':id'].following['$get']({
+          param: { id },
+          query: { page: 1}
+        });
+        setIsFollowing(followStatus.ok);
+      }
+      
+      // 获取关注数量
+      rpcLogger('Fetching followers count for user: %s', id);
+      const followers = await userClient[':id'].followers['$get']({ 
+        param: { id },
+        query: { pageSize: 1 } 
+      });
+      rpcLogger('Fetching following count for user: %s', id);
+      const following = await userClient[':id'].following['$get']({ 
+        param: { id },
+        query: { pageSize: 1 } 
+      });
+      
+      setFollowerCount(await followers.json().then(data => data.pagination.total));
+      setFollowingCount(await following.json().then(data => data.pagination.total));
+    } catch (error) {
+      rpcLogger.error('Error fetching user profile:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 关注/取消关注用户
+  const handleFollowToggle = async () => {
+    if (!currentUser || !id) return;
+    
+    try {
+      if (isFollowing) {
+        rpcLogger('Unfollowing user: %s', id);
+        await userClient[':id'].follow['$delete']({ param: { id: Number(id) } });
+        setFollowerCount(prev => prev - 1);
+      } else {
+        rpcLogger('Following user: %s', id);
+        await userClient[':id'].follow['$post']({ 
+          param: { id: Number(id) },
+        });
+        setFollowerCount(prev => prev + 1);
+      }
+      setIsFollowing(!isFollowing);
+    } catch (error) {
+      console.error('Error toggling follow status:', error);
+    }
+  };
+
+  useEffect(() => {
+    fetchUserProfile();
+  }, [id]);
+
+  if (loading) {
+    return (
+      <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
+        <Spin size="large" />
+      </div>
+    );
+  }
+
+  if (!user) {
+    return (
+      <div style={{ textAlign: 'center', padding: '50px' }}>
+        <Title level={3}>用户不存在</Title>
+        <Button onClick={() => navigate('/')}>返回首页</Button>
+      </div>
+    );
+  }
+
+  return (
+    <Layout>
+      <Content style={{ padding: '24px', maxWidth: 1200, margin: '0 auto', width: '100%' }}>
+        <Card style={{ marginBottom: 24 }}>
+          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '24px 0' }}>
+            <Avatar 
+              src={user.avatar || <UserOutlined style={{ fontSize: 64 }} />} 
+              size={128} 
+              style={{ marginBottom: 16 }}
+            />
+            <Title level={2} style={{ margin: 0 }}>{user.nickname || user.username}</Title>
+            
+            <div style={{ display: 'flex', margin: '16px 0' }}>
+              <div style={{ textAlign: 'center', margin: '0 24px' }}>
+                <Text strong style={{ fontSize: 24 }}>0</Text>
+                <br />
+                <Text>帖子</Text>
+              </div>
+              <div style={{ textAlign: 'center', margin: '0 24px' }}>
+                <Text strong style={{ fontSize: 24 }}>{followerCount}</Text>
+                <br />
+                <Text>粉丝</Text>
+              </div>
+              <div style={{ textAlign: 'center', margin: '0 24px' }}>
+                <Text strong style={{ fontSize: 24 }}>{followingCount}</Text>
+                <br />
+                <Text>关注</Text>
+              </div>
+            </div>
+            
+            {currentUser && currentUser.id !== user.id ? (
+              <Button 
+                type={isFollowing ? "default" : "primary"} 
+                icon={isFollowing ? <UserDeleteOutlined /> : <UserAddOutlined />}
+                onClick={handleFollowToggle}
+              >
+                {isFollowing ? '取消关注' : '关注'}
+              </Button>
+            ) : currentUser && currentUser.id === user.id ? (
+              <Button icon={<EditOutlined />} onClick={() => navigate('/profile/edit')}>
+                编辑资料
+              </Button>
+            ) : null}
+            
+            {user.bio && (
+              <Paragraph style={{ marginTop: 16, maxWidth: 600, textAlign: 'center' }}>
+                {user.bio}
+              </Paragraph>
+            )}
+            
+            <div style={{ display: 'flex', alignItems: 'center', marginTop: 8 }}>
+              {user.location && (
+                <Text style={{ marginRight: 16 }}>{user.location}</Text>
+              )}
+              {user.website && (
+                <Text 
+                  style={{ color: '#1890ff', cursor: 'pointer' }}
+                  onClick={() => window.open(user.website, '_blank')}
+                >
+                  {user.website}
+                </Text>
+              )}
+            </div>
+          </div>
+        </Card>
+        
+        <Tabs defaultActiveKey="posts" style={{ marginBottom: 24 }}>
+          <TabPane tab="帖子" key="posts">
+            <List
+              dataSource={[]} // 这里应该是用户的帖子数据
+              renderItem={item => (
+                <List.Item
+                  actions={[
+                    <Button icon={<HeartOutlined />}>
+                      {item.likesCount > 0 && item.likesCount}
+                    </Button>
+                  ]}
+                >
+                  <List.Item.Meta
+                    description={
+                      <>
+                        <Paragraph>{item.content}</Paragraph>
+                        {item.images?.map((img, idx) => (
+                          <img key={idx} src={img} alt={`Post image ${idx}`} style={{ maxWidth: '100%', margin: '8px 0' }} />
+                        ))}
+                        <Text type="secondary">{new Date(item.createdAt).toLocaleString()}</Text>
+                      </>
+                    }
+                  />
+                </List.Item>
+              )}
+              locale={{ emptyText: '该用户暂无帖子' }}
+            />
+          </TabPane>
+          <TabPane tab="关注" key="following">
+            <List
+              dataSource={[]} // 这里应该是用户关注的人数据
+              renderItem={item => (
+                <List.Item
+                  actions={[
+                    <Button 
+                      size="small" 
+                      type={item.isFollowing ? "default" : "primary"}
+                      onClick={() => {/* 关注/取消关注逻辑 */}}
+                    >
+                      {item.isFollowing ? '已关注' : '关注'}
+                    </Button>
+                  ]}
+                >
+                  <List.Item.Meta
+                    avatar={<Avatar src={item.avatar || <UserOutlined />} />}
+                    title={
+                      <Text 
+                        style={{ cursor: 'pointer' }}
+                        onClick={() => navigate(`/users/${item.id}`)}
+                      >
+                        {item.nickname || item.username}
+                      </Text>
+                    }
+                    description={item.bio || '暂无简介'}
+                  />
+                </List.Item>
+              )}
+              locale={{ emptyText: '该用户暂无关注' }}
+            />
+          </TabPane>
+          <TabPane tab="粉丝" key="followers">
+            <List
+              dataSource={[]} // 这里应该是用户的粉丝数据
+              renderItem={item => (
+                <List.Item
+                  actions={[
+                    <Button 
+                      size="small" 
+                      type={item.isFollowing ? "default" : "primary"}
+                      onClick={() => {/* 关注/取消关注逻辑 */}}
+                    >
+                      {item.isFollowing ? '已关注' : '关注'}
+                    </Button>
+                  ]}
+                >
+                  <List.Item.Meta
+                    avatar={<Avatar src={item.avatar || <UserOutlined />} />}
+                    title={
+                      <Text 
+                        style={{ cursor: 'pointer' }}
+                        onClick={() => navigate(`/users/${item.id}`)}
+                      >
+                        {item.nickname || item.username}
+                      </Text>
+                    }
+                    description={item.bio || '暂无简介'}
+                  />
+                </List.Item>
+              )}
+              locale={{ emptyText: '该用户暂无粉丝' }}
+            />
+          </TabPane>
+        </Tabs>
+      </Content>
+    </Layout>
+  );
+};
+
+export default UserProfilePage;

+ 44 - 0
src/client/home/routes.tsx

@@ -0,0 +1,44 @@
+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 UserProfilePage from './pages/MemberPage';
+
+export const router = createBrowserRouter([
+  {
+    path: '/login',
+    element: <LoginPage />
+  },
+  {
+    path: '/member',
+    element: (
+      <ProtectedRoute>
+        <MainLayout />
+      </ProtectedRoute>
+    ),
+    children: [
+      {
+        path: '',
+        element: <HomePage />
+      },
+      {
+        path: 'users/:id',
+        element: <UserProfilePage />
+      },
+      {
+        path: '*',
+        element: <NotFoundPage />,
+        errorElement: <ErrorPage />
+      },
+    ],
+  },
+  {
+    path: '*',
+    element: <NotFoundPage />,
+    errorElement: <ErrorPage />
+  },
+]);