Sfoglia il codice sorgente

✨ feat(post): add post creation and display functionality

- add postClient API client for post operations
- implement PostForm component with text and image upload
- create HomePage with post feed and creation functionality

✨ feat(user): add user following system and profile pages

- implement FollowPage for viewing following and followers
- create UserProfilePage with user information display
- add API endpoints for getting followers and following lists
- implement follow/unfollow functionality in UI components

✨ feat(api): add user relationship management endpoints

- add GET /users/{id}/followers endpoint to retrieve user followers
- add GET /users/{id}/following endpoint to retrieve user following list
- implement pagination for follower and following list responses
- add authentication middleware to protect user relationship endpoints
yourname 5 mesi fa
parent
commit
d9d7ffdb2d

+ 4 - 0
src/client/api.ts

@@ -70,3 +70,7 @@ export const userClient = hc<UserRoutes>('/', {
 export const roleClient = hc<RoleRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.roles;
+
+export const postClient = hc<PostRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.posts;

+ 119 - 0
src/client/home/components/PostForm.tsx

@@ -0,0 +1,119 @@
+import React, { useState } from 'react';
+import { Card, Input, Button, Upload, message } from 'antd';
+import { SendOutlined, PictureOutlined } from '@ant-design/icons';
+import type { UploadProps } from 'antd/es/upload';
+import { postClient } from '@/client/api';
+
+const { TextArea } = Input;
+
+interface PostFormProps {
+  onPostSuccess?: () => void;
+}
+
+const PostForm: React.FC<PostFormProps> = ({ onPostSuccess }) => {
+  const [content, setContent] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [imageUrls, setImageUrls] = useState<string[]>([]);
+
+  const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    setContent(e.target.value);
+  };
+
+  const uploadProps: UploadProps = {
+    name: 'file',
+    listType: 'picture-card',
+    maxCount: 4,
+    showUploadList: {
+      showRemoveIcon: true,
+    },
+    beforeUpload: (file) => {
+      const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+      if (!isJpgOrPng) {
+        message.error('只能上传JPG/PNG格式的图片');
+        return false;
+      }
+      const isLt2M = file.size / 1024 / 1024 < 2;
+      if (!isLt2M) {
+        message.error('图片大小不能超过2MB');
+        return false;
+      }
+      return true;
+    },
+    onChange: (info) => {
+      if (info.file.status === 'done') {
+        // 假设上传成功后返回图片URL
+        const url = info.file.response?.data?.url;
+        if (url) {
+          setImageUrls(prev => [...prev, url]);
+        }
+      } else if (info.file.status === 'error') {
+        message.error(`${info.file.name} 上传失败`);
+      }
+    },
+  };
+
+  const handleSubmit = async () => {
+    if (!content.trim() && imageUrls.length === 0) {
+      message.warning('内容或图片不能为空');
+      return;
+    }
+
+    try {
+      setLoading(true);
+      
+      const response = await postClient.$post({
+        json: {
+          content,
+          images: imageUrls.length > 0 ? imageUrls : undefined
+        }
+      });
+      
+      if (!response.ok) throw new Error('发布失败');
+      
+      message.success('发布成功');
+      setContent('');
+      setImageUrls([]);
+      onPostSuccess?.();
+    } catch (error) {
+      console.error('Error creating post:', error);
+      message.error((error as Error).message || '发布失败,请重试');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <Card style={{ marginBottom: 24 }}>
+      <TextArea
+        placeholder="分享你的想法..."
+        rows={4}
+        value={content}
+        onChange={handleContentChange}
+        maxLength={1000}
+        showCount
+      />
+      
+      <div style={{ margin: '16px 0' }}>
+        <Upload {...uploadProps}>
+          <div>
+            <PictureOutlined />
+            <div style={{ marginTop: 8 }}>上传图片</div>
+          </div>
+        </Upload>
+      </div>
+      
+      <div style={{ textAlign: 'right' }}>
+        <Button 
+          type="primary" 
+          icon={<SendOutlined />} 
+          onClick={handleSubmit}
+          loading={loading}
+        >
+          发布
+        </Button>
+      </div>
+    </Card>
+  );
+};
+
+export default PostForm;

+ 205 - 0
src/client/home/pages/FollowPage.tsx

@@ -0,0 +1,205 @@
+import React, { useEffect, useState } from 'react';
+import { Layout, Card, List, Avatar, Button, Input, Typography, Spin, Empty } from 'antd';
+import { SearchOutlined, UserAddOutlined, UserDeleteOutlined } from '@ant-design/icons';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { userClient } from '@/client/api';
+import { useAuth } from '@/client/admin/hooks/AuthProvider';
+import type { UserEntity } from '@/server/modules/users/user.entity';
+
+const { Content } = Layout;
+const { Title, Text } = Typography;
+
+type FollowType = 'following' | 'followers';
+
+const FollowPage: React.FC = () => {
+  const [users, setUsers] = useState<UserEntity[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [searchText, setSearchText] = useState('');
+  const [currentPage, setCurrentPage] = useState(1);
+  const [totalCount, setTotalCount] = useState(0);
+  const pageSize = 10;
+  
+  const navigate = useNavigate();
+  const location = useLocation();
+  const { user } = useAuth();
+  
+  // 从URL获取关注类型(following/followers)和用户ID
+  const searchParams = new URLSearchParams(location.search);
+  const type = (searchParams.get('type') || 'following') as FollowType;
+  const userId = searchParams.get('userId') || user?.id?.toString();
+
+  // 获取关注列表或粉丝列表
+  const fetchFollowList = async () => {
+    if (!userId) return;
+    
+    try {
+      setLoading(true);
+      
+      let response;
+      if (type === 'following') {
+        response = await userClient[userId].following.$get({
+          query: { page: currentPage, pageSize }
+        });
+      } else {
+        response = await userClient[userId].followers.$get({
+          query: { page: currentPage, pageSize }
+        });
+      }
+      
+      if (!response.ok) throw new Error('获取列表失败');
+      
+      const data = await response.json();
+      setUsers(data.data);
+      setTotalCount(data.pagination.total);
+    } catch (error) {
+      console.error(`Error fetching ${type} list:`, error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 关注/取消关注用户
+  const handleFollowToggle = async (targetUserId: number, isFollowing: boolean) => {
+    if (!user) return;
+    
+    try {
+      if (isFollowing) {
+        // 取消关注
+        await userClient[user.id].follow[targetUserId].$delete();
+      } else {
+        // 关注用户
+        await userClient[user.id].follow.$post({
+          json: { followingId: targetUserId }
+        });
+      }
+      
+      // 更新本地列表状态
+      setUsers(users.map(u => 
+        u.id === targetUserId 
+          ? { ...u, isFollowing: !isFollowing } 
+          : u
+      ));
+    } catch (error) {
+      console.error('Error toggling follow status:', error);
+    }
+  };
+
+  // 搜索用户
+  const handleSearch = () => {
+    // 实际应用中应该调用搜索API
+    console.log('Searching users:', searchText);
+    // fetchFollowList(); // 触发搜索请求
+  };
+
+  // 分页处理
+  const handlePageChange = (page: number) => {
+    setCurrentPage(page);
+  };
+
+  useEffect(() => {
+    fetchFollowList();
+  }, [type, userId, currentPage]);
+
+  return (
+    <Layout>
+      <Content style={{ padding: '24px', maxWidth: 800, margin: '0 auto', width: '100%' }}>
+        <Card>
+          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
+            <Title level={2} style={{ margin: 0 }}>
+              {type === 'following' ? '关注列表' : '粉丝列表'}
+            </Title>
+            
+            <div style={{ display: 'flex', width: '300px' }}>
+              <Input.Search
+                placeholder="搜索用户"
+                value={searchText}
+                onChange={e => setSearchText(e.target.value)}
+                onSearch={handleSearch}
+                enterButton={<SearchOutlined />}
+              />
+            </div>
+          </div>
+          
+          {loading ? (
+            <div style={{ display: 'flex', justifyContent: 'center', padding: '50px' }}>
+              <Spin size="large" />
+            </div>
+          ) : users.length === 0 ? (
+            <Empty description={type === 'following' ? '暂无关注' : '暂无粉丝'} />
+          ) : (
+            <>
+              <List
+                dataSource={users}
+                renderItem={item => (
+                  <List.Item
+                    actions={[
+                      user && user.id !== item.id && (
+                        <Button 
+                          size="small" 
+                          type={item.isFollowing ? "default" : "primary"}
+                          icon={item.isFollowing ? <UserDeleteOutlined /> : <UserAddOutlined />}
+                          onClick={() => handleFollowToggle(item.id, item.isFollowing)}
+                        >
+                          {item.isFollowing ? '取消关注' : '关注'}
+                        </Button>
+                      )
+                    ]}
+                  >
+                    <List.Item.Meta
+                      avatar={
+                        <Avatar 
+                          src={item.avatar || <UserAddOutlined />} 
+                          onClick={() => navigate(`/users/${item.id}`)}
+                          style={{ cursor: 'pointer' }}
+                        />
+                      }
+                      title={
+                        <Text 
+                          style={{ cursor: 'pointer' }}
+                          onClick={() => navigate(`/users/${item.id}`)}
+                        >
+                          {item.nickname || item.username}
+                        </Text>
+                      }
+                      description={
+                        <>
+                          {item.bio || '暂无简介'}
+                          <br />
+                          <Text type="secondary">
+                            {item.followingCount} 关注 · {item.followerCount} 粉丝
+                          </Text>
+                        </>
+                      }
+                    />
+                  </List.Item>
+                )}
+              />
+              
+              <div style={{ textAlign: 'right', marginTop: 16 }}>
+                <Button 
+                  disabled={currentPage === 1} 
+                  onClick={() => handlePageChange(currentPage - 1)}
+                  style={{ marginRight: 8 }}
+                >
+                  上一页
+                </Button>
+                <span>
+                  第 {currentPage} 页 / 共 {Math.ceil(totalCount / pageSize)} 页
+                </span>
+                <Button 
+                  disabled={currentPage >= Math.ceil(totalCount / pageSize)} 
+                  onClick={() => handlePageChange(currentPage + 1)}
+                  style={{ marginLeft: 8 }}
+                >
+                  下一页
+                </Button>
+              </div>
+            </>
+          )}
+        </Card>
+      </Content>
+    </Layout>
+  );
+};
+
+export default FollowPage;

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

@@ -0,0 +1,150 @@
+import React, { useEffect, useState } from 'react';
+import { Layout, List, Card, Avatar, Button, Input, Space, Typography, Spin, Empty } from 'antd';
+import { UserOutlined, MessageOutlined, HeartOutlined, SendOutlined } from '@ant-design/icons';
+import { useAuth } from '@/client/admin/hooks/AuthProvider';
+import { postClient } from '@/client/api';
+import type { PostEntity } from '@/server/modules/posts/post.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 { user } = useAuth();
+
+  // 获取首页内容流
+  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);
+    } 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[postId].like.$post();
+      setPosts(posts.map(post => 
+        post.id === postId ? { ...post, likesCount: post.likesCount + 1 } : post
+      ));
+    } catch (error) {
+      console.error('Error liking post:', 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' }}>
+          <Avatar icon={<UserOutlined />} style={{ marginRight: 16 }} />
+          <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;

+ 261 - 0
src/client/home/pages/UserProfilePage.tsx

@@ -0,0 +1,261 @@
+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/admin/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 = () => {
+    return null;
+  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 } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+  const { user: currentUser } = useAuth();
+
+  // 获取用户资料
+  const fetchUserProfile = async () => {
+    if (!id) return;
+    
+    try {
+      setLoading(true);
+      const response = await userClient[id].$get();
+      
+      if (!response.ok) throw new Error('获取用户资料失败');
+      
+      const userData = await response.json();
+      setUser(userData);
+      
+      // 获取关注状态
+      if (currentUser && currentUser.id !== Number(id)) {
+        const followStatus = await userClient[currentUser.id].following.$get({
+          query: { userId: id }
+        });
+        setIsFollowing(followStatus.ok);
+      }
+      
+      // 获取关注数量
+      const followers = await userClient[id].followers.$get({ query: { pageSize: 1 } });
+      const following = await userClient[id].following.$get({ query: { pageSize: 1 } });
+      
+      setFollowerCount(await followers.json().then(data => data.pagination.total));
+      setFollowingCount(await following.json().then(data => data.pagination.total));
+    } catch (error) {
+      console.error('Error fetching user profile:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 关注/取消关注用户
+  const handleFollowToggle = async () => {
+    if (!currentUser || !id) return;
+    
+    try {
+      if (isFollowing) {
+        await userClient[currentUser.id].follow[id].$delete();
+        setFollowerCount(prev => prev - 1);
+      } else {
+        await userClient[currentUser.id].follow.$post({ json: { followingId: 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;

+ 96 - 0
src/server/api/users/[id]/followers/get.ts

@@ -0,0 +1,96 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { FollowService } from '@/server/modules/follows/follow.service';
+import { UserSchema } from '@/server/modules/users/user.entity';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+
+// 参数Schema
+const ParamsSchema = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '用户ID'
+  })
+});
+
+// 查询参数Schema
+const QuerySchema = z.object({
+  page: z.coerce.number().int().positive().default(1).openapi({
+    example: 1,
+    description: '页码'
+  }),
+  pageSize: z.coerce.number().int().positive().default(10).openapi({
+    example: 10,
+    description: '每页条数'
+  })
+});
+
+// 响应Schema
+const FollowersListResponse = z.object({
+  data: z.array(UserSchema),
+  pagination: z.object({
+    total: z.number().openapi({ example: 100, description: '总记录数' }),
+    current: z.number().openapi({ example: 1, description: '当前页码' }),
+    pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/{id}/followers',
+  middleware: [authMiddleware],
+  request: {
+    params: ParamsSchema,
+    query: QuerySchema
+  },
+  responses: {
+    200: {
+      description: '成功获取粉丝列表',
+      content: { 'application/json': { schema: FollowersListResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '用户不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const { page, pageSize } = c.req.valid('query');
+    const followService = new FollowService(AppDataSource);
+    
+    const [follows, total] = await followService.getFollowers(id, page, pageSize);
+    
+    // 提取粉丝用户信息
+    const data = follows.map(follow => follow.follower);
+    
+    return c.json({
+      data,
+      pagination: {
+        total,
+        current: page,
+        pageSize
+      }
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '获取粉丝列表失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code as unknown as 400 | 404 | 500);
+  }
+});
+
+export default app;

+ 96 - 0
src/server/api/users/[id]/following/get.ts

@@ -0,0 +1,96 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { FollowService } from '@/server/modules/follows/follow.service';
+import { UserSchema } from '@/server/modules/users/user.entity';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+
+// 参数Schema
+const ParamsSchema = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '用户ID'
+  })
+});
+
+// 查询参数Schema
+const QuerySchema = z.object({
+  page: z.coerce.number().int().positive().default(1).openapi({
+    example: 1,
+    description: '页码'
+  }),
+  pageSize: z.coerce.number().int().positive().default(10).openapi({
+    example: 10,
+    description: '每页条数'
+  })
+});
+
+// 响应Schema
+const FollowingListResponse = z.object({
+  data: z.array(UserSchema),
+  pagination: z.object({
+    total: z.number().openapi({ example: 100, description: '总记录数' }),
+    current: z.number().openapi({ example: 1, description: '当前页码' }),
+    pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/{id}/following',
+  middleware: [authMiddleware],
+  request: {
+    params: ParamsSchema,
+    query: QuerySchema
+  },
+  responses: {
+    200: {
+      description: '成功获取关注列表',
+      content: { 'application/json': { schema: FollowingListResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '用户不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const { page, pageSize } = c.req.valid('query');
+    const followService = new FollowService(AppDataSource);
+    
+    const [follows, total] = await followService.getFollowing(id, page, pageSize);
+    
+    // 提取关注的用户信息
+    const data = follows.map(follow => follow.following);
+    
+    return c.json({
+      data,
+      pagination: {
+        total,
+        current: page,
+        pageSize
+      }
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '获取关注列表失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code as unknown as 400 | 404 | 500);
+  }
+});
+
+export default app;