UserProfilePage.tsx 9.9 KB

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