index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import React, { useState, useEffect } from 'react';
  2. import { useNavigate, useLocation } from 'react-router';
  3. import {
  4. HomeIcon,
  5. UserIcon,
  6. NewspaperIcon,
  7. BellIcon
  8. } from '@heroicons/react/24/outline';
  9. import { useAuth } from '../hooks.tsx';
  10. import { formatRelativeTime } from '../utils.ts';
  11. interface BannerItem {
  12. id: number;
  13. title: string;
  14. image: string;
  15. link: string;
  16. }
  17. interface NewsItem {
  18. id: number;
  19. title: string;
  20. summary: string;
  21. publish_date: string;
  22. cover?: string;
  23. category: string;
  24. }
  25. interface NoticeItem {
  26. id: number;
  27. title: string;
  28. content: string;
  29. created_at: string;
  30. is_read: boolean;
  31. }
  32. // 首页组件
  33. const HomePage: React.FC = () => {
  34. const { user } = useAuth();
  35. const navigate = useNavigate();
  36. const location = useLocation();
  37. const [loading, setLoading] = useState(true);
  38. const [banners, setBanners] = useState<BannerItem[]>([]);
  39. const [news, setNews] = useState<NewsItem[]>([]);
  40. const [notices, setNotices] = useState<NoticeItem[]>([]);
  41. const [activeTab, setActiveTab] = useState('news');
  42. // 模拟加载数据
  43. useEffect(() => {
  44. // 模拟API请求
  45. setTimeout(() => {
  46. // 模拟轮播图数据
  47. setBanners([
  48. {
  49. id: 1,
  50. title: '欢迎使用移动端应用',
  51. image: 'https://images.unsplash.com/photo-1518655048521-f130df041f66?ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8cG9ydGZvbGlvJTIwYmFja2dyb3VuZHxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&w=1000&q=80',
  52. link: '/welcome'
  53. },
  54. {
  55. id: 2,
  56. title: '新功能上线了',
  57. image: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8cG9ydGZvbGlvJTIwYmFja2dyb3VuZHxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&w=1000&q=80',
  58. link: '/new-features'
  59. }
  60. ]);
  61. // 模拟新闻数据
  62. setNews([
  63. {
  64. id: 1,
  65. title: '用户体验升级,新版本发布',
  66. summary: '我们很高兴地宣布,新版本已经发布,带来了更好的用户体验和更多新功能。',
  67. publish_date: '2023-05-01T08:30:00',
  68. cover: 'https://images.unsplash.com/photo-1496171367470-9ed9a91ea931?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTB8fHRlY2h8ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60',
  69. category: '产品更新'
  70. },
  71. {
  72. id: 2,
  73. title: '新的数据分析功能上线',
  74. summary: '新的数据分析功能让您更深入地了解您的业务数据,提供更好的决策支持。',
  75. publish_date: '2023-04-25T14:15:00',
  76. cover: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTJ8fGNoYXJ0fGVufDB8fDB8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60',
  77. category: '功能介绍'
  78. },
  79. {
  80. id: 3,
  81. title: '如何提高工作效率的5个小技巧',
  82. summary: '这篇文章分享了5个可以立即实施的小技巧,帮助您提高日常工作效率。',
  83. publish_date: '2023-04-20T09:45:00',
  84. category: '使用技巧'
  85. }
  86. ]);
  87. // 模拟通知数据
  88. setNotices([
  89. {
  90. id: 1,
  91. title: '系统维护通知',
  92. content: '我们将于本周六凌晨2点至4点进行系统维护,期间系统可能会出现短暂不可用。',
  93. created_at: '2023-05-02T10:00:00',
  94. is_read: false
  95. },
  96. {
  97. id: 2,
  98. title: '您的账户信息已更新',
  99. content: '您的账户信息已成功更新,如非本人操作,请及时联系客服。',
  100. created_at: '2023-05-01T16:30:00',
  101. is_read: true
  102. }
  103. ]);
  104. setLoading(false);
  105. }, 800);
  106. }, []);
  107. // 处理轮播图点击
  108. const handleBannerClick = (link: string) => {
  109. navigate(link);
  110. };
  111. // 处理新闻点击
  112. const handleNewsClick = (id: number) => {
  113. navigate(`/news/${id}`);
  114. };
  115. // 处理通知点击
  116. const handleNoticeClick = (id: number) => {
  117. navigate(`/notices/${id}`);
  118. };
  119. return (
  120. <div className="pb-16">
  121. {/* 顶部用户信息 */}
  122. <div className="bg-blue-600 text-white p-4">
  123. <div className="flex items-center justify-between">
  124. <div className="flex items-center space-x-3">
  125. <div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
  126. {user?.avatar ? (
  127. <img
  128. src={user.avatar}
  129. alt={user?.nickname || user?.username || '用户'}
  130. className="w-10 h-10 rounded-full object-cover"
  131. />
  132. ) : (
  133. <UserIcon className="w-6 h-6" />
  134. )}
  135. </div>
  136. <div>
  137. <h2 className="text-lg font-medium">
  138. {user ? `您好,${user.nickname || user.username}` : '您好,游客'}
  139. </h2>
  140. <p className="text-sm text-white/80">
  141. {user ? '欢迎回来' : '请登录体验更多功能'}
  142. </p>
  143. </div>
  144. </div>
  145. <div className="relative">
  146. <BellIcon className="w-6 h-6" />
  147. {notices.some(notice => !notice.is_read) && (
  148. <span className="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full"></span>
  149. )}
  150. </div>
  151. </div>
  152. </div>
  153. {/* 轮播图 */}
  154. {!loading && banners.length > 0 && (
  155. <div className="relative w-full h-40 overflow-hidden mt-2">
  156. <div className="flex transition-transform duration-300"
  157. style={{ transform: `translateX(-${0 * 100}%)` }}>
  158. {banners.map((banner) => (
  159. <div
  160. key={banner.id}
  161. className="w-full h-40 flex-shrink-0 relative"
  162. onClick={() => handleBannerClick(banner.link)}
  163. >
  164. <img
  165. src={banner.image}
  166. alt={banner.title}
  167. className="w-full h-full object-cover"
  168. />
  169. <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3">
  170. <h3 className="text-white font-medium">{banner.title}</h3>
  171. </div>
  172. </div>
  173. ))}
  174. </div>
  175. {/* 指示器 */}
  176. <div className="absolute bottom-2 left-0 right-0 flex justify-center space-x-1">
  177. {banners.map((_, index) => (
  178. <span
  179. key={index}
  180. className={`w-2 h-2 rounded-full ${index === 0 ? 'bg-white' : 'bg-white/50'}`}
  181. ></span>
  182. ))}
  183. </div>
  184. </div>
  185. )}
  186. {/* 快捷入口 */}
  187. <div className="grid grid-cols-4 gap-2 p-4 bg-white rounded-lg shadow mt-4 mx-2">
  188. {[
  189. { icon: <HomeIcon className="w-6 h-6" />, name: '首页', path: '/' },
  190. { icon: <NewspaperIcon className="w-6 h-6" />, name: '资讯', path: '/news' },
  191. { icon: <BellIcon className="w-6 h-6" />, name: '通知', path: '/notices' },
  192. { icon: <UserIcon className="w-6 h-6" />, name: '我的', path: '/profile' }
  193. ].map((item, index) => (
  194. <div
  195. key={index}
  196. className="flex flex-col items-center justify-center p-2"
  197. onClick={() => navigate(item.path)}
  198. >
  199. <div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mb-1">
  200. {item.icon}
  201. </div>
  202. <span className="text-sm">{item.name}</span>
  203. </div>
  204. ))}
  205. </div>
  206. {/* 内容标签页 */}
  207. <div className="mt-4 mx-2">
  208. <div className="flex border-b border-gray-200">
  209. <button
  210. className={`flex-1 py-2 text-center ${activeTab === 'news' ? 'text-blue-600 border-b-2 border-blue-600 font-medium' : 'text-gray-500'}`}
  211. onClick={() => setActiveTab('news')}
  212. >
  213. 最新资讯
  214. </button>
  215. <button
  216. className={`flex-1 py-2 text-center ${activeTab === 'notices' ? 'text-blue-600 border-b-2 border-blue-600 font-medium' : 'text-gray-500'}`}
  217. onClick={() => setActiveTab('notices')}
  218. >
  219. 通知公告
  220. </button>
  221. </div>
  222. <div className="mt-2">
  223. {activeTab === 'news' ? (
  224. loading ? (
  225. <div className="flex justify-center p-4">
  226. <div className="w-6 h-6 border-2 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
  227. </div>
  228. ) : (
  229. <div className="space-y-4">
  230. {news.map((item) => (
  231. <div
  232. key={item.id}
  233. className="bg-white p-3 rounded-lg shadow flex items-start space-x-3"
  234. onClick={() => handleNewsClick(item.id)}
  235. >
  236. {item.cover && (
  237. <img
  238. src={item.cover}
  239. alt={item.title}
  240. className="w-20 h-20 object-cover rounded-md flex-shrink-0"
  241. />
  242. )}
  243. <div className={item.cover ? '' : 'w-full'}>
  244. <h3 className="font-medium text-gray-900 line-clamp-2">{item.title}</h3>
  245. <p className="text-sm text-gray-500 mt-1 line-clamp-2">{item.summary}</p>
  246. <div className="flex justify-between items-center mt-2">
  247. <span className="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
  248. {item.category}
  249. </span>
  250. <span className="text-xs text-gray-400">
  251. {formatRelativeTime(item.publish_date)}
  252. </span>
  253. </div>
  254. </div>
  255. </div>
  256. ))}
  257. </div>
  258. )
  259. ) : (
  260. loading ? (
  261. <div className="flex justify-center p-4">
  262. <div className="w-6 h-6 border-2 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
  263. </div>
  264. ) : (
  265. <div className="space-y-3">
  266. {notices.map((item) => (
  267. <div
  268. key={item.id}
  269. className="bg-white p-3 rounded-lg shadow"
  270. onClick={() => handleNoticeClick(item.id)}
  271. >
  272. <div className="flex justify-between items-start">
  273. <h3 className={`font-medium ${item.is_read ? 'text-gray-700' : 'text-blue-600'}`}>
  274. {!item.is_read && (
  275. <span className="inline-block w-2 h-2 bg-blue-600 rounded-full mr-2"></span>
  276. )}
  277. {item.title}
  278. </h3>
  279. <span className="text-xs text-gray-400 mt-1">
  280. {formatRelativeTime(item.created_at)}
  281. </span>
  282. </div>
  283. <p className="text-sm text-gray-500 mt-2 line-clamp-2">{item.content}</p>
  284. </div>
  285. ))}
  286. </div>
  287. )
  288. )}
  289. </div>
  290. </div>
  291. {/* 底部导航 */}
  292. <div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex justify-around py-2">
  293. {[
  294. { icon: <HomeIcon className="w-6 h-6" />, name: '首页', path: '/' },
  295. { icon: <NewspaperIcon className="w-6 h-6" />, name: '资讯', path: '/news' },
  296. { icon: <BellIcon className="w-6 h-6" />, name: '通知', path: '/notices' },
  297. { icon: <UserIcon className="w-6 h-6" />, name: '我的', path: '/profile' }
  298. ].map((item, index) => (
  299. <div
  300. key={index}
  301. className="flex flex-col items-center"
  302. onClick={() => navigate(item.path)}
  303. >
  304. <div className={`${location.pathname === item.path ? 'text-blue-600' : 'text-gray-500'}`}>
  305. {item.icon}
  306. </div>
  307. <span className={`text-xs mt-1 ${location.pathname === item.path ? 'text-blue-600' : 'text-gray-500'}`}>
  308. {item.name}
  309. </span>
  310. </div>
  311. ))}
  312. </div>
  313. </div>
  314. );
  315. };
  316. export default HomePage;