MemberPage.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import debug from 'debug';
  2. const rpcLogger = debug('frontend:api:rpc');
  3. import React, { useEffect, useState } from 'react';
  4. import { UserIcon, PencilIcon } 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 MemberPage: 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. console.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 className="flex justify-center items-center min-h-[50vh]">
  40. <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
  41. </div>
  42. );
  43. }
  44. if (!user) {
  45. return (
  46. <div className="text-center py-12">
  47. <h2 className="text-2xl font-bold text-gray-900 mb-4">用户不存在</h2>
  48. <button
  49. onClick={() => navigate('/')}
  50. 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"
  51. >
  52. 返回首页
  53. </button>
  54. </div>
  55. );
  56. }
  57. return (
  58. <div className="min-h-screen bg-gray-50">
  59. <div className="container mx-auto px-4 py-8 max-w-4xl">
  60. {/* 用户资料卡片 */}
  61. <div className="bg-white rounded-lg shadow-sm p-6 mb-8">
  62. <div className="flex flex-col items-center">
  63. <div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center mb-4">
  64. {user.avatar ? (
  65. <img src={user.avatar} alt={user.nickname || user.username} className="h-full w-full object-cover rounded-full" />
  66. ) : (
  67. <UserIcon className="h-12 w-12 text-gray-500" />
  68. )}
  69. </div>
  70. <h1 className="text-2xl font-bold text-gray-900 mb-1">{user.nickname || user.username}</h1>
  71. <div className="flex space-x-8 my-4">
  72. <div className="text-center">
  73. <p className="text-2xl font-semibold text-gray-900">0</p>
  74. <p className="text-sm text-gray-500">内容</p>
  75. </div>
  76. <div className="text-center">
  77. <p className="text-2xl font-semibold text-gray-900">0</p>
  78. <p className="text-sm text-gray-500">关注</p>
  79. </div>
  80. <div className="text-center">
  81. <p className="text-2xl font-semibold text-gray-900">0</p>
  82. <p className="text-sm text-gray-500">粉丝</p>
  83. </div>
  84. </div>
  85. {currentUser && currentUser.id === user.id && (
  86. <button
  87. onClick={() => navigate('/profile/edit')}
  88. 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"
  89. >
  90. <PencilIcon className="w-4 h-4 mr-2" />
  91. 编辑资料
  92. </button>
  93. )}
  94. {(user as any).bio && (
  95. <p className="mt-4 text-center text-gray-600 max-w-lg">
  96. {(user as any).bio}
  97. </p>
  98. )}
  99. <div className="flex items-center mt-4 space-x-4">
  100. {(user as any).location && (
  101. <div className="flex items-center text-gray-600">
  102. <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  103. <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" />
  104. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
  105. </svg>
  106. <span className="text-sm">{(user as any).location}</span>
  107. </div>
  108. )}
  109. {(user as any).website && (
  110. <a
  111. href={(user as any).website}
  112. target="_blank"
  113. rel="noopener noreferrer"
  114. className="flex items-center text-blue-600 hover:text-blue-800"
  115. >
  116. <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  117. <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" />
  118. </svg>
  119. <span className="text-sm truncate max-w-[150px]">{(user as any).website}</span>
  120. </a>
  121. )}
  122. </div>
  123. </div>
  124. </div>
  125. {/* 用户内容区域 */}
  126. <div className="bg-white rounded-lg shadow-sm p-6">
  127. <h2 className="text-xl font-semibold mb-6">个人资料</h2>
  128. <div className="space-y-4">
  129. <div className="border-b border-gray-100 pb-4">
  130. <h3 className="text-sm font-medium text-gray-500 mb-1">用户名</h3>
  131. <p className="text-gray-900">{user.username}</p>
  132. </div>
  133. <div className="border-b border-gray-100 pb-4">
  134. <h3 className="text-sm font-medium text-gray-500 mb-1">电子邮箱</h3>
  135. <p className="text-gray-900">{user.email || '未设置'}</p>
  136. </div>
  137. <div className="border-b border-gray-100 pb-4">
  138. <h3 className="text-sm font-medium text-gray-500 mb-1">注册时间</h3>
  139. <p className="text-gray-900">{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '未知'}</p>
  140. </div>
  141. <div className="border-b border-gray-100 pb-4">
  142. <h3 className="text-sm font-medium text-gray-500 mb-1">最后登录</h3>
  143. <p className="text-gray-900">{user.updatedAt ? new Date(user.updatedAt).toLocaleString() : '从未登录'}</p>
  144. </div>
  145. </div>
  146. <div className="mt-8">
  147. <button
  148. onClick={() => navigate('/profile/security')}
  149. 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"
  150. >
  151. 安全设置
  152. </button>
  153. </div>
  154. </div>
  155. </div>
  156. </div>
  157. );
  158. };
  159. export default MemberPage;