MemberPage.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import debug from 'debug';
  2. const rpcLogger = debug('frontend:api:rpc');
  3. import React, { useEffect, useState } from 'react';
  4. import { UserIcon, EditIcon, ExternalLinkIcon, Loader2Icon } from '@heroicons/react/24/outline';
  5. import { useParams, useNavigate } from 'react-router-dom';
  6. import { userClient } from '@/client/api';
  7. import { useAuth } from '@/client/home/hooks/AuthProvider';
  8. import type { UserEntity } from '@/server/modules/users/user.entity';
  9. const UserProfilePage: React.FC = () => {
  10. const [user, setUser] = useState<UserEntity | null>(null);
  11. const [loading, setLoading] = useState(true);
  12. const { id: userId } = useParams<{ id: string }>();
  13. const id = Number(userId);
  14. const navigate = useNavigate();
  15. const { user: currentUser } = useAuth();
  16. // 获取用户资料
  17. const fetchUserProfile = async () => {
  18. if (!id) return;
  19. try {
  20. setLoading(true);
  21. rpcLogger('Fetching user profile for id: %s', id);
  22. const response = await userClient[':id'].$get({
  23. param: { id }
  24. });
  25. if (!response.ok) throw new Error('获取用户资料失败');
  26. const userData = await response.json();
  27. setUser(userData);
  28. } catch (error) {
  29. rpcLogger.error('Error fetching user profile:', error);
  30. } finally {
  31. setLoading(false);
  32. }
  33. };
  34. useEffect(() => {
  35. fetchUserProfile();
  36. }, [id]);
  37. if (loading) {
  38. return (
  39. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
  40. <Spin size="large" />
  41. </div>
  42. );
  43. }
  44. if (!user) {
  45. return (
  46. <div style={{ textAlign: 'center', padding: '50px' }}>
  47. <Title level={3}>用户不存在</Title>
  48. <Button onClick={() => navigate('/')}>返回首页</Button>
  49. </div>
  50. );
  51. }
  52. return (
  53. <Layout>
  54. <Content style={{ padding: '24px', maxWidth: 1200, margin: '0 auto', width: '100%' }}>
  55. <Card style={{ marginBottom: 24 }}>
  56. <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '24px 0' }}>
  57. <Avatar
  58. src={user.avatar || <UserOutlined style={{ fontSize: 64 }} />}
  59. size={128}
  60. style={{ marginBottom: 16 }}
  61. />
  62. <Title level={2} style={{ margin: 0 }}>{user.nickname || user.username}</Title>
  63. <div style={{ display: 'flex', margin: '16px 0' }}>
  64. <div style={{ textAlign: 'center', margin: '0 24px' }}>
  65. <Text strong style={{ fontSize: 24 }}>0</Text>
  66. <br />
  67. <Text>帖子</Text>
  68. </div>
  69. <div style={{ textAlign: 'center', margin: '0 24px' }}>
  70. <Text strong style={{ fontSize: 24 }}>{followerCount}</Text>
  71. <br />
  72. <Text>粉丝</Text>
  73. </div>
  74. <div style={{ textAlign: 'center', margin: '0 24px' }}>
  75. <Text strong style={{ fontSize: 24 }}>{followingCount}</Text>
  76. <br />
  77. <Text>关注</Text>
  78. </div>
  79. </div>
  80. {currentUser && currentUser.id !== user.id ? (
  81. <Button
  82. type={isFollowing ? "default" : "primary"}
  83. icon={isFollowing ? <UserDeleteOutlined /> : <UserAddOutlined />}
  84. onClick={handleFollowToggle}
  85. >
  86. {isFollowing ? '取消关注' : '关注'}
  87. </Button>
  88. ) : currentUser && currentUser.id === user.id ? (
  89. <Button icon={<EditOutlined />} onClick={() => navigate('/profile/edit')}>
  90. 编辑资料
  91. </Button>
  92. ) : null}
  93. {user.bio && (
  94. <Paragraph style={{ marginTop: 16, maxWidth: 600, textAlign: 'center' }}>
  95. {user.bio}
  96. </Paragraph>
  97. )}
  98. <div style={{ display: 'flex', alignItems: 'center', marginTop: 8 }}>
  99. {user.location && (
  100. <Text style={{ marginRight: 16 }}>{user.location}</Text>
  101. )}
  102. {user.website && (
  103. <Text
  104. style={{ color: '#1890ff', cursor: 'pointer' }}
  105. onClick={() => window.open(user.website, '_blank')}
  106. >
  107. {user.website}
  108. </Text>
  109. )}
  110. </div>
  111. </div>
  112. </Card>
  113. <Tabs defaultActiveKey="posts" style={{ marginBottom: 24 }}>
  114. <TabPane tab="帖子" key="posts">
  115. <List
  116. dataSource={[]} // 这里应该是用户的帖子数据
  117. renderItem={item => (
  118. <List.Item
  119. actions={[
  120. <Button icon={<HeartOutlined />}>
  121. {item.likesCount > 0 && item.likesCount}
  122. </Button>
  123. ]}
  124. >
  125. <List.Item.Meta
  126. description={
  127. <>
  128. <Paragraph>{item.content}</Paragraph>
  129. {item.images?.map((img, idx) => (
  130. <img key={idx} src={img} alt={`Post image ${idx}`} style={{ maxWidth: '100%', margin: '8px 0' }} />
  131. ))}
  132. <Text type="secondary">{new Date(item.createdAt).toLocaleString()}</Text>
  133. </>
  134. }
  135. />
  136. </List.Item>
  137. )}
  138. locale={{ emptyText: '该用户暂无帖子' }}
  139. />
  140. </TabPane>
  141. <TabPane tab="关注" key="following">
  142. <List
  143. dataSource={[]} // 这里应该是用户关注的人数据
  144. renderItem={item => (
  145. <List.Item
  146. actions={[
  147. <Button
  148. size="small"
  149. type={item.isFollowing ? "default" : "primary"}
  150. onClick={() => {/* 关注/取消关注逻辑 */}}
  151. >
  152. {item.isFollowing ? '已关注' : '关注'}
  153. </Button>
  154. ]}
  155. >
  156. <List.Item.Meta
  157. avatar={<Avatar src={item.avatar || <UserOutlined />} />}
  158. title={
  159. <Text
  160. style={{ cursor: 'pointer' }}
  161. onClick={() => navigate(`/users/${item.id}`)}
  162. >
  163. {item.nickname || item.username}
  164. </Text>
  165. }
  166. description={item.bio || '暂无简介'}
  167. />
  168. </List.Item>
  169. )}
  170. locale={{ emptyText: '该用户暂无关注' }}
  171. />
  172. </TabPane>
  173. <TabPane tab="粉丝" key="followers">
  174. <List
  175. dataSource={[]} // 这里应该是用户的粉丝数据
  176. renderItem={item => (
  177. <List.Item
  178. actions={[
  179. <Button
  180. size="small"
  181. type={item.isFollowing ? "default" : "primary"}
  182. onClick={() => {/* 关注/取消关注逻辑 */}}
  183. >
  184. {item.isFollowing ? '已关注' : '关注'}
  185. </Button>
  186. ]}
  187. >
  188. <List.Item.Meta
  189. avatar={<Avatar src={item.avatar || <UserOutlined />} />}
  190. title={
  191. <Text
  192. style={{ cursor: 'pointer' }}
  193. onClick={() => navigate(`/users/${item.id}`)}
  194. >
  195. {item.nickname || item.username}
  196. </Text>
  197. }
  198. description={item.bio || '暂无简介'}
  199. />
  200. </List.Item>
  201. )}
  202. locale={{ emptyText: '该用户暂无粉丝' }}
  203. />
  204. </TabPane>
  205. </Tabs>
  206. </Content>
  207. </Layout>
  208. );
  209. };
  210. export default UserProfilePage;