pages_index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import React, { useState, useEffect } from 'react';
  2. import { useNavigate, useLocation } from 'react-router';
  3. import { HomeAPI } from './api/index.ts';
  4. import { MessageAPI } from './api/index.ts';
  5. import {
  6. HomeIcon,
  7. UserIcon,
  8. NewspaperIcon,
  9. BellIcon
  10. } from '@heroicons/react/24/outline';
  11. import { useAuth } from './hooks.tsx';
  12. import { formatRelativeTime } from './utils.ts';
  13. import { KnowInfo, UserMessage, MessageType, MessageStatus } from '../share/types.ts';
  14. // 首页组件
  15. const HomePage: React.FC = () => {
  16. const { user } = useAuth();
  17. const navigate = useNavigate();
  18. const location = useLocation();
  19. const [loading, setLoading] = useState(true);
  20. const [banners, setBanners] = useState<KnowInfo[]>([]);
  21. const [news, setNews] = useState<KnowInfo[]>([]);
  22. const [notices, setNotices] = useState<UserMessage[]>([]);
  23. const [activeTab, setActiveTab] = useState('news');
  24. // 模拟加载数据
  25. useEffect(() => {
  26. const fetchData = async () => {
  27. try {
  28. // 获取数据
  29. const [bannersRes, newsRes, messagesRes] = await Promise.all([
  30. HomeAPI.getBanners(),
  31. HomeAPI.getNews(),
  32. MessageAPI.getMessages({ type: MessageType.ANNOUNCE })
  33. ]);
  34. setBanners(bannersRes.data.map((item: KnowInfo) => ({
  35. id: item.id,
  36. title: item.title,
  37. cover_url: item.cover_url,
  38. content: item.content,
  39. category: 'banner',
  40. created_at: new Date().toISOString(),
  41. updated_at: new Date().toISOString(),
  42. sort_order: item.sort_order || 0
  43. } as KnowInfo)));
  44. setNews(newsRes.data);
  45. setNotices(messagesRes.data);
  46. setLoading(false);
  47. } catch (error) {
  48. console.error('获取首页数据失败:', error);
  49. setLoading(false);
  50. }
  51. };
  52. fetchData();
  53. }, []);
  54. // 处理轮播图点击
  55. const handleBannerClick = (link: string) => {
  56. navigate(link);
  57. };
  58. // 处理新闻点击
  59. const handleNewsClick = (id: number) => {
  60. navigate(`/news/${id}`);
  61. };
  62. // 处理通知点击
  63. const handleNoticeClick = (id: number) => {
  64. navigate(`/notices/${id}`);
  65. };
  66. return (
  67. <div className="pb-16">
  68. {/* 顶部用户信息 */}
  69. <div className="bg-blue-600 text-white p-4">
  70. <div className="flex items-center justify-between">
  71. <div className="flex items-center space-x-3">
  72. <div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
  73. {user?.avatar ? (
  74. <img
  75. src={user.avatar}
  76. alt={user?.nickname || user?.username || '用户'}
  77. className="w-10 h-10 rounded-full object-cover"
  78. />
  79. ) : (
  80. <UserIcon className="w-6 h-6" />
  81. )}
  82. </div>
  83. <div>
  84. <h2 className="text-lg font-medium">
  85. {user ? `您好,${user.nickname || user.username}` : '您好,游客'}
  86. </h2>
  87. <p className="text-sm text-white/80">
  88. {user ? '欢迎回来' : '请登录体验更多功能'}
  89. </p>
  90. </div>
  91. </div>
  92. <div className="relative">
  93. <BellIcon className="w-6 h-6" />
  94. {notices.some(notice => notice.user_status === MessageStatus.UNREAD) && (
  95. <span className="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full"></span>
  96. )}
  97. </div>
  98. </div>
  99. </div>
  100. {/* 轮播图 */}
  101. {!loading && banners.length > 0 && (
  102. <div className="relative w-full h-40 overflow-hidden mt-2">
  103. <div className="flex transition-transform duration-300"
  104. style={{ transform: `translateX(-${0 * 100}%)` }}>
  105. {banners.map((banner) => (
  106. <div
  107. key={banner.id}
  108. className="w-full h-40 flex-shrink-0 relative"
  109. onClick={() => handleBannerClick(banner.content || '')}
  110. >
  111. <img
  112. src={banner.cover_url || ''}
  113. alt={banner.title}
  114. className="w-full h-full object-cover"
  115. />
  116. <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3">
  117. <h3 className="text-white font-medium">{banner.title}</h3>
  118. </div>
  119. </div>
  120. ))}
  121. </div>
  122. {/* 指示器 */}
  123. <div className="absolute bottom-2 left-0 right-0 flex justify-center space-x-1">
  124. {banners.map((_, index) => (
  125. <span
  126. key={index}
  127. className={`w-2 h-2 rounded-full ${index === 0 ? 'bg-white' : 'bg-white/50'}`}
  128. ></span>
  129. ))}
  130. </div>
  131. </div>
  132. )}
  133. {/* 快捷入口 */}
  134. <div className="grid grid-cols-4 gap-2 p-4 bg-white rounded-lg shadow mt-4 mx-2">
  135. {[
  136. { icon: <HomeIcon className="w-6 h-6" />, name: '首页', path: '/' },
  137. { icon: <NewspaperIcon className="w-6 h-6" />, name: '资讯', path: '/news' },
  138. { icon: <BellIcon className="w-6 h-6" />, name: '通知', path: '/notices' },
  139. { icon: <UserIcon className="w-6 h-6" />, name: '我的', path: '/profile' }
  140. ].map((item, index) => (
  141. <div
  142. key={index}
  143. className="flex flex-col items-center justify-center p-2"
  144. onClick={() => navigate(item.path)}
  145. >
  146. <div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mb-1">
  147. {item.icon}
  148. </div>
  149. <span className="text-sm">{item.name}</span>
  150. </div>
  151. ))}
  152. </div>
  153. {/* 内容标签页 */}
  154. <div className="mt-4 mx-2">
  155. <div className="flex border-b border-gray-200">
  156. <button
  157. className={`flex-1 py-2 text-center ${activeTab === 'news' ? 'text-blue-600 border-b-2 border-blue-600 font-medium' : 'text-gray-500'}`}
  158. onClick={() => setActiveTab('news')}
  159. >
  160. 最新资讯
  161. </button>
  162. <button
  163. className={`flex-1 py-2 text-center ${activeTab === 'notices' ? 'text-blue-600 border-b-2 border-blue-600 font-medium' : 'text-gray-500'}`}
  164. onClick={() => setActiveTab('notices')}
  165. >
  166. 通知公告
  167. </button>
  168. </div>
  169. <div className="mt-2">
  170. {activeTab === 'news' ? (
  171. loading ? (
  172. <div className="flex justify-center p-4">
  173. <div className="w-6 h-6 border-2 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
  174. </div>
  175. ) : (
  176. <div className="space-y-4">
  177. {news.map((item) => (
  178. <div
  179. key={item.id}
  180. className="bg-white p-3 rounded-lg shadow flex items-start space-x-3"
  181. onClick={() => handleNewsClick(item.id)}
  182. >
  183. {item.cover_url && (
  184. <img
  185. src={item.cover_url}
  186. alt={item.title}
  187. className="w-20 h-20 object-cover rounded-md flex-shrink-0"
  188. />
  189. )}
  190. <div className={item.cover_url ? '' : 'w-full'}>
  191. <h3 className="font-medium text-gray-900 line-clamp-2">{item.title}</h3>
  192. <p className="text-sm text-gray-500 mt-1 line-clamp-2">
  193. {item.content?.substring(0, 100)}...
  194. </p>
  195. <div className="flex justify-between items-center mt-2">
  196. <span className="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
  197. {item.category}
  198. </span>
  199. <span className="text-xs text-gray-400">
  200. {formatRelativeTime(item.created_at)}
  201. </span>
  202. </div>
  203. </div>
  204. </div>
  205. ))}
  206. </div>
  207. )
  208. ) : (
  209. loading ? (
  210. <div className="flex justify-center p-4">
  211. <div className="w-6 h-6 border-2 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
  212. </div>
  213. ) : (
  214. <div className="space-y-3">
  215. {notices.map((item) => (
  216. <div
  217. key={item.id}
  218. className="bg-white p-3 rounded-lg shadow"
  219. onClick={() => handleNoticeClick(item.id)}
  220. >
  221. <div className="flex justify-between items-start">
  222. <h3 className={`font-medium ${item.user_status === MessageStatus.READ ? 'text-gray-700' : 'text-blue-600'}`}>
  223. {item.user_status === MessageStatus.UNREAD && (
  224. <span className="inline-block w-2 h-2 bg-blue-600 rounded-full mr-2"></span>
  225. )}
  226. {item.title}
  227. </h3>
  228. <span className="text-xs text-gray-400 mt-1">
  229. {formatRelativeTime(item.created_at)}
  230. </span>
  231. </div>
  232. <p className="text-sm text-gray-500 mt-2 line-clamp-2">{item.content}</p>
  233. </div>
  234. ))}
  235. </div>
  236. )
  237. )}
  238. </div>
  239. </div>
  240. {/* 底部导航 */}
  241. <div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex justify-around py-2">
  242. {[
  243. { icon: <HomeIcon className="w-6 h-6" />, name: '首页', path: '/' },
  244. { icon: <NewspaperIcon className="w-6 h-6" />, name: '资讯', path: '/news' },
  245. { icon: <BellIcon className="w-6 h-6" />, name: '通知', path: '/notices' },
  246. { icon: <UserIcon className="w-6 h-6" />, name: '我的', path: '/profile' }
  247. ].map((item, index) => (
  248. <div
  249. key={index}
  250. className="flex flex-col items-center"
  251. onClick={() => navigate(item.path)}
  252. >
  253. <div className={`${location.pathname === item.path ? 'text-blue-600' : 'text-gray-500'}`}>
  254. {item.icon}
  255. </div>
  256. <span className={`text-xs mt-1 ${location.pathname === item.path ? 'text-blue-600' : 'text-gray-500'}`}>
  257. {item.name}
  258. </span>
  259. </div>
  260. ))}
  261. </div>
  262. </div>
  263. );
  264. };
  265. export default HomePage;