page.jsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. "use client";
  2. import React, { useState, useEffect, useCallback } from "react";
  3. import { useRouter } from "next/navigation";
  4. import {
  5. ChevronLeft,
  6. MessageSquare,
  7. ArrowUpCircle,
  8. ArrowDownCircle,
  9. Gift,
  10. Users,
  11. Video,
  12. Zap,
  13. } from "lucide-react";
  14. import Image from "next/image";
  15. import InfiniteScroll from "react-infinite-scroll-component";
  16. import Link from "next/link";
  17. const PAGE_SIZE = 10;
  18. const PersonalCenter = () => {
  19. const router = useRouter();
  20. const [user, setUser] = useState(null);
  21. const [predictions, setPredictions] = useState([]);
  22. const [points, setPoints] = useState([]);
  23. const [isLoading, setIsLoading] = useState(true);
  24. const [predictionPage, setPredictionPage] = useState(1);
  25. const [pointPage, setPointPage] = useState(1);
  26. const [hasMorePredictions, setHasMorePredictions] = useState(true);
  27. const [hasMorePoints, setHasMorePoints] = useState(true);
  28. const [activities, setActivities] = useState([]);
  29. const fetchInitialData = useCallback(async () => {
  30. try {
  31. setIsLoading(true);
  32. const storedUser = JSON.parse(
  33. localStorage.getItem("currentUser") || "null"
  34. );
  35. setUser(storedUser);
  36. if (storedUser && storedUser.id) {
  37. const [
  38. predictionResponse,
  39. pointResponse,
  40. userResponse,
  41. activitiesResponse,
  42. ] = await Promise.all([
  43. fetch(
  44. `/api/prediction?username=${encodeURIComponent(
  45. storedUser.username
  46. )}&current=1&pageSize=${PAGE_SIZE}`
  47. ),
  48. fetch(
  49. `/api/point-history?userId=${encodeURIComponent(
  50. storedUser.id
  51. )}&current=1&pageSize=${PAGE_SIZE}`
  52. ),
  53. fetch(`/api/user?id=${encodeURIComponent(storedUser.id)}`),
  54. fetch("/api/new-activities", {
  55. headers: {
  56. "x-from-frontend": "true",
  57. },
  58. }),
  59. ]);
  60. const [predictionData, pointData, userData, activitiesData] =
  61. await Promise.all([
  62. predictionResponse.json(),
  63. pointResponse.json(),
  64. userResponse.json(),
  65. activitiesResponse.json(),
  66. ]);
  67. if (predictionData.success) {
  68. setPredictions(predictionData.data);
  69. setHasMorePredictions(predictionData.data.length === PAGE_SIZE);
  70. } else {
  71. console.error("Failed to fetch predictions:", predictionData.error);
  72. }
  73. if (pointData.success) {
  74. setPoints(pointData.data);
  75. setHasMorePoints(pointData.data.length === PAGE_SIZE);
  76. } else {
  77. console.error("Failed to fetch point history:", pointData.error);
  78. }
  79. if (userData.success && userData.data) {
  80. const updatedUser = { ...storedUser, points: userData.data.points };
  81. setUser(updatedUser);
  82. localStorage.setItem("currentUser", JSON.stringify(updatedUser));
  83. } else {
  84. console.error("Failed to fetch user data:", userData.error);
  85. }
  86. // 处理活动数据
  87. if (activitiesData.success) {
  88. setActivities(activitiesData.data);
  89. } else {
  90. console.error("Failed to fetch activities:", activitiesData.error);
  91. }
  92. } else {
  93. // 如果用户未登录,仍然获取活动数据
  94. const activitiesResponse = await activitiesPromise;
  95. const activitiesData = await activitiesResponse.json();
  96. if (activitiesData.success) {
  97. setActivities(activitiesData.data);
  98. } else {
  99. console.error("Failed to fetch activities:", activitiesData.error);
  100. }
  101. }
  102. } catch (error) {
  103. console.error("Error fetching data:", error);
  104. } finally {
  105. setIsLoading(false);
  106. }
  107. }, []);
  108. useEffect(() => {
  109. fetchInitialData();
  110. }, [fetchInitialData]);
  111. const loadMorePredictions = async () => {
  112. if (!hasMorePredictions || !user) return;
  113. try {
  114. const response = await fetch(
  115. `/api/prediction?username=${encodeURIComponent(
  116. user.username
  117. )}&current=${predictionPage + 1}&pageSize=${PAGE_SIZE}`
  118. );
  119. const data = await response.json();
  120. if (data.success) {
  121. setPredictions((prev) => [...prev, ...data.data]);
  122. setPredictionPage((prev) => prev + 1);
  123. setHasMorePredictions(data.data.length === PAGE_SIZE);
  124. } else {
  125. console.error("Failed to fetch more predictions:", data.error);
  126. }
  127. } catch (error) {
  128. console.error("Error fetching more predictions:", error);
  129. }
  130. };
  131. const loadMorePoints = async () => {
  132. if (!hasMorePoints || !user) return;
  133. try {
  134. const response = await fetch(
  135. `/api/point-history?userId=${encodeURIComponent(user.id)}&current=${
  136. pointPage + 1
  137. }&pageSize=${PAGE_SIZE}`
  138. );
  139. const data = await response.json();
  140. if (data.success) {
  141. setPoints((prev) => [...prev, ...data.data]);
  142. setPointPage((prev) => prev + 1);
  143. setHasMorePoints(data.data.length === PAGE_SIZE);
  144. } else {
  145. console.error("Failed to fetch more point history:", data.error);
  146. }
  147. } catch (error) {
  148. console.error("Error fetching more point history:", error);
  149. }
  150. };
  151. const handleExchangePoints = () => {
  152. console.log("兑换积分");
  153. // router.push('/exchange-points');
  154. };
  155. if (isLoading) {
  156. return (
  157. <div className="bg-blue-600 text-white min-h-screen p-4">Loading...</div>
  158. );
  159. }
  160. if (!user) {
  161. return (
  162. <div className="bg-blue-600 text-white min-h-screen p-4">
  163. User not found
  164. </div>
  165. );
  166. }
  167. return (
  168. <div className="bg-blue-600 text-white min-h-screen p-4">
  169. <div className="flex items-center mb-4">
  170. <ChevronLeft
  171. className="cursor-pointer"
  172. onClick={() => router.back()}
  173. size={32}
  174. />
  175. <h1 className="text-xl font-bold ml-4">个人中心</h1>
  176. </div>
  177. <div className="bg-white text-black rounded-lg p-4 mb-4">
  178. <div className="flex items-center justify-between">
  179. <div className="flex items-center">
  180. <Image
  181. src="/images/cluo.webp"
  182. alt="User Avatar"
  183. width={50}
  184. height={50}
  185. className="rounded-full mr-2"
  186. />
  187. <div>
  188. <h2 className="text-xl font-bold">{user.username}</h2>
  189. <p className="text-gray-600">积分: {user.points}</p>
  190. </div>
  191. </div>
  192. <button
  193. className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-2 rounded transition duration-300"
  194. onClick={() => handleExchangePoints()}
  195. >
  196. 兑换积分
  197. </button>
  198. </div>
  199. </div>
  200. {activities && activities.length > 0 && (
  201. <div className="bg-white text-black rounded-lg px-4 py-2 mb-4 shadow-md">
  202. <h3 className="text-lg font-bold mb-1 text-blue-600">最新活动</h3>
  203. {activities.map((activity) => (
  204. <Link
  205. key={activity._id}
  206. href={activity.link}
  207. className="flex items-center hover:bg-gray-100 p-2 rounded transition duration-300"
  208. >
  209. {activity.icon && (
  210. <div className="w-5 h-5 mr-2 flex-shrink-0">
  211. <Image
  212. src={activity.icon}
  213. alt={activity.title}
  214. width={20}
  215. height={20}
  216. className="object-contain"
  217. />
  218. </div>
  219. )}
  220. <p>{activity.title}</p>
  221. </Link>
  222. ))}
  223. </div>
  224. )}
  225. <div className="bg-white text-black rounded-lg p-4 mb-4">
  226. <h3 className="text-lg font-bold mb-2 text-blue-600">预测记录</h3>
  227. <div id="predictionScroll" className="h-[300px] overflow-auto">
  228. <InfiniteScroll
  229. dataLength={predictions.length}
  230. next={loadMorePredictions}
  231. hasMore={hasMorePredictions}
  232. loader={<h4 className="text-center text-gray-500">加载中...</h4>}
  233. endMessage={
  234. <p className="text-center text-gray-500">没有更多记录了</p>
  235. }
  236. scrollableTarget="predictionScroll"
  237. >
  238. {predictions.map((prediction) => (
  239. <div
  240. key={prediction._id}
  241. className="mb-4 border-b border-gray-200 pb-4 hover:bg-gray-50 transition duration-150 ease-in-out"
  242. >
  243. <p className="font-bold text-blue-700 mb-2">
  244. {prediction.matchInfo}
  245. </p>
  246. <p className="text-gray-600 mb-2">
  247. 比赛时间:{" "}
  248. <span className="font-medium text-black">
  249. {prediction.matchTime}
  250. </span>
  251. </p>
  252. <div className="grid grid-cols-2 gap-2">
  253. <p className="mb-1">
  254. 胜负预测:{" "}
  255. <span
  256. className={`font-medium ${
  257. prediction.whoWillWin === "home"
  258. ? "text-red-600"
  259. : prediction.whoWillWin === "away"
  260. ? "text-green-600"
  261. : "text-yellow-600"
  262. }`}
  263. >
  264. {prediction.whoWillWin === "home"
  265. ? "主胜"
  266. : prediction.whoWillWin === "away"
  267. ? "客胜"
  268. : "平局"}
  269. </span>
  270. </p>
  271. <p className="mb-1">
  272. 结果:{" "}
  273. <span
  274. className={`font-medium ${
  275. prediction.whoWillWinResult === "correct"
  276. ? "text-green-600"
  277. : prediction.whoWillWinResult === "incorrect"
  278. ? "text-red-600"
  279. : "text-gray-600"
  280. }`}
  281. >
  282. {prediction.whoWillWinResult === "correct"
  283. ? "正确"
  284. : prediction.whoWillWinResult === "incorrect"
  285. ? "错误"
  286. : "待定"}
  287. </span>
  288. </p>
  289. <p className="mb-1">
  290. 首先得分:{" "}
  291. <span
  292. className={`font-medium ${
  293. prediction.firstTeamToScore === "home"
  294. ? "text-red-600"
  295. : prediction.firstTeamToScore === "away"
  296. ? "text-green-600"
  297. : "text-gray-600"
  298. }`}
  299. >
  300. {prediction.firstTeamToScore === "home"
  301. ? "主队"
  302. : prediction.firstTeamToScore === "away"
  303. ? "客队"
  304. : "无进球"}
  305. </span>
  306. </p>
  307. <p className="mb-1">
  308. 结果:{" "}
  309. <span
  310. className={`font-medium ${
  311. prediction.firstTeamToScoreResult === "correct"
  312. ? "text-green-600"
  313. : prediction.firstTeamToScoreResult === "incorrect"
  314. ? "text-red-600"
  315. : "text-gray-600"
  316. }`}
  317. >
  318. {prediction.firstTeamToScoreResult === "correct"
  319. ? "正确"
  320. : prediction.firstTeamToScoreResult === "incorrect"
  321. ? "错误"
  322. : "待定"}
  323. </span>
  324. </p>
  325. <p className="mb-1">
  326. 总进球数预测:{" "}
  327. <span className="font-medium text-purple-600">
  328. {prediction.totalGoals}
  329. </span>
  330. </p>
  331. <p className="mb-1">
  332. 结果:{" "}
  333. <span
  334. className={`font-medium ${
  335. prediction.totalGoalsResult === "correct"
  336. ? "text-green-600"
  337. : prediction.totalGoalsResult === "incorrect"
  338. ? "text-red-600"
  339. : "text-gray-600"
  340. }`}
  341. >
  342. {prediction.totalGoalsResult === "correct"
  343. ? "正确"
  344. : prediction.totalGoalsResult === "incorrect"
  345. ? "错误"
  346. : "待定"}
  347. </span>
  348. </p>
  349. </div>
  350. <p className="mt-2">
  351. 获得积分:{" "}
  352. <span className="font-medium text-orange-600">
  353. {prediction.pointsEarned !== null
  354. ? prediction.pointsEarned
  355. : "待定"}
  356. </span>
  357. </p>
  358. </div>
  359. ))}
  360. </InfiniteScroll>
  361. </div>
  362. </div>
  363. <div className="bg-white text-black rounded-lg p-4 mb-4">
  364. <h3 className="text-lg font-bold mb-2 text-blue-700">积分记录</h3>
  365. <div id="pointScroll" style={{ height: "250px", overflow: "auto" }}>
  366. <InfiniteScroll
  367. dataLength={points.length}
  368. next={loadMorePoints}
  369. hasMore={hasMorePoints}
  370. loader={<h4>Loading...</h4>}
  371. endMessage={<p className="text-center">没有更多记录了</p>}
  372. scrollableTarget="pointScroll"
  373. >
  374. {points.map((point) => (
  375. <div key={point.id} className="mb-3 flex items-start">
  376. <div className="mr-3 mt-1 flex-shrink-0">
  377. {point.points > 0 ? (
  378. <ArrowUpCircle className="text-green-500 w-5 h-5" />
  379. ) : (
  380. <ArrowDownCircle className="text-red-500 w-5 h-5" />
  381. )}
  382. </div>
  383. <div className="flex-grow mr-4">
  384. <p className="text-sm text-gray-600">
  385. {new Date(point.createdAt).toLocaleDateString()}
  386. </p>
  387. <p className="font-medium">{point.reason}</p>
  388. </div>
  389. <div
  390. className={`flex-shrink-0 font-bold ${
  391. point.points > 0 ? "text-green-600" : "text-red-600"
  392. }`}
  393. >
  394. {point.points > 0 ? "+" : ""}
  395. {point.points}
  396. </div>
  397. </div>
  398. ))}
  399. </InfiniteScroll>
  400. </div>
  401. </div>
  402. </div>
  403. );
  404. };
  405. export default PersonalCenter;