ExamCard.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import React, { useState, useCallback, useEffect } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { useSearchParams } from "react-router";
  4. import dayjs from 'dayjs';
  5. import { message } from 'antd';
  6. import {
  7. useExamCurrentQuestion,
  8. useExamRoomMessages,
  9. useExamAnswerSubmission,
  10. useExamUserAnswerHistory,
  11. useExamSocketRoom
  12. } from './hooks/useSocketClient.ts';
  13. import type { AnswerRecord, ClassroomData, Answer } from './types.ts';
  14. // 答题卡页面
  15. export default function ExamCard() {
  16. const [searchParams] = useSearchParams();
  17. const classroom = searchParams.get('classroom');
  18. const nickname = searchParams.get('nickname');
  19. useExamSocketRoom(classroom);
  20. const lastMessage = useExamRoomMessages(classroom);
  21. const { submitAnswer } = useExamAnswerSubmission(classroom);
  22. const { currentQuestion } = useExamCurrentQuestion(classroom);
  23. const { userAnswers } = useExamUserAnswerHistory(classroom, nickname);
  24. const [currentDate, setCurrentDate] = useState('');
  25. const [currentPrice, setCurrentPrice] = useState('0');
  26. const [holdingStock, setHoldingStock] = useState('0');
  27. const [holdingCash, setHoldingCash] = useState('0');
  28. const [isStarted, setIsStarted] = useState(false);
  29. const [answerRecords, setAnswerRecords] = useState<AnswerRecord[]>([]);
  30. const { data: classroomData, isLoading } = useQuery({
  31. queryKey: ['classroom', classroom],
  32. queryFn: async () => {
  33. if (!classroom) return null;
  34. const response = await fetch(`/api/v1/classroom?classroom_no=${classroom}`);
  35. const data = await response.json();
  36. if (!data.success) {
  37. message.error(data.message || '获取教室数据失败');
  38. return null;
  39. }
  40. return data.data?.[0] as ClassroomData || null;
  41. },
  42. enabled: !!classroom
  43. });
  44. useEffect(() => {
  45. if (!classroom || !nickname) {
  46. globalThis.location.href = '/exam';
  47. return;
  48. }
  49. if (classroomData && classroomData.status !== "1") {
  50. message.error('该教室已关闭');
  51. globalThis.location.href = '/exam';
  52. return;
  53. }
  54. }, [classroom, nickname, classroomData]);
  55. // 处理房间消息
  56. useEffect(() => {
  57. if (!lastMessage?.message) return;
  58. const { type } = lastMessage.message;
  59. // 只处理重开消息的UI重置
  60. if (type === 'restart') {
  61. setCurrentDate('');
  62. setCurrentPrice('0');
  63. setHoldingStock('0');
  64. setHoldingCash('0');
  65. setIsStarted(false);
  66. setAnswerRecords([]);
  67. }
  68. }, [lastMessage]);
  69. useEffect(() => {
  70. if (currentQuestion) {
  71. console.log('currentQuestion', currentQuestion);
  72. setCurrentDate(currentQuestion.date);
  73. setCurrentPrice(String(currentQuestion.price));
  74. setIsStarted(true);
  75. } else {
  76. // 如果没有当前问题,重置状态
  77. setCurrentDate('');
  78. setCurrentPrice('0');
  79. setIsStarted(false);
  80. }
  81. }, [currentQuestion]);
  82. // 处理选择A(持股)
  83. const handleChooseA = useCallback(async () => {
  84. setHoldingStock('1');
  85. setHoldingCash('0');
  86. if (classroom && nickname) {
  87. const answer = {
  88. date: currentDate,
  89. holdingStock: '1',
  90. holdingCash: '0',
  91. userId: nickname,
  92. price: currentPrice
  93. };
  94. try {
  95. await submitAnswer(currentDate, nickname, answer);
  96. } catch (error) {
  97. message.error('提交答案失败');
  98. }
  99. }
  100. }, [classroom, nickname, currentDate, currentPrice, submitAnswer]);
  101. // 处理选择B(持币)
  102. const handleChooseB = useCallback(async () => {
  103. setHoldingStock('0');
  104. setHoldingCash('1');
  105. if (classroom && nickname) {
  106. const answer = {
  107. date: currentDate,
  108. holdingStock: '0',
  109. holdingCash: '1',
  110. userId: nickname,
  111. price: currentPrice
  112. };
  113. try {
  114. await submitAnswer(currentDate, nickname, answer);
  115. } catch (error) {
  116. message.error('提交答案失败');
  117. }
  118. }
  119. }, [classroom, nickname, currentDate, currentPrice, submitAnswer]);
  120. // 初始化用户的答题记录
  121. useEffect(() => {
  122. if (userAnswers && userAnswers.length > 0) {
  123. const lastAnswer = userAnswers[userAnswers.length - 1];
  124. setHoldingStock(lastAnswer.holdingStock);
  125. setHoldingCash(lastAnswer.holdingCash);
  126. // 直接使用 userAnswers 中已计算好的数据
  127. const records = userAnswers.map((answer: Answer, index: number): AnswerRecord => ({
  128. date: answer.date,
  129. price: String(answer.price || '0'),
  130. holdingStock: answer.holdingStock,
  131. holdingCash: answer.holdingCash,
  132. profitAmount: answer.profitAmount || 0,
  133. profitPercent: answer.profitPercent || 0,
  134. index: index + 1
  135. }));
  136. setAnswerRecords(records);
  137. }
  138. }, [userAnswers]);
  139. if (isLoading || !classroomData) {
  140. return <div className="flex items-center justify-center min-h-screen">加载中...</div>;
  141. }
  142. return (
  143. <div className="flex flex-col items-center min-h-screen bg-gray-100 py-8 px-4">
  144. {/* 选择区域 */}
  145. <div className="w-full max-w-2xl">
  146. <div className="text-center mb-8">
  147. <h2 className="text-2xl font-bold mb-2">持股选A, 持币选B</h2>
  148. <div className="flex justify-center space-x-4 text-gray-600">
  149. {isStarted ? (
  150. <>
  151. <span>日期: {currentDate}</span>
  152. <span>价格: {currentPrice}</span>
  153. </>
  154. ) : (
  155. <div className="text-blue-600">
  156. <div className="mb-2">等待训练开始...</div>
  157. <div className="text-sm text-gray-500">
  158. 训练日期: {dayjs(classroomData.training_date).format('YYYY-MM-DD')}
  159. </div>
  160. </div>
  161. )}
  162. </div>
  163. </div>
  164. {/* 选择按钮 - 增大点击区域 */}
  165. <div className="flex justify-center items-center space-x-4 mb-8 bg-white p-6 rounded-lg shadow-md">
  166. <button
  167. onClick={handleChooseA}
  168. disabled={!isStarted}
  169. className={`min-w-[120px] py-8 px-4 text-3xl font-bold rounded-lg transition-colors ${
  170. !isStarted
  171. ? 'bg-gray-200 text-gray-400 cursor-not-allowed'
  172. : holdingStock === '1'
  173. ? 'bg-red-500 text-white'
  174. : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  175. }`}
  176. >
  177. A
  178. </button>
  179. <div className="text-xl font-medium text-gray-700">
  180. {isStarted ? '开始' : '等待'}
  181. </div>
  182. <button
  183. onClick={handleChooseB}
  184. disabled={!isStarted}
  185. className={`min-w-[120px] py-8 px-4 text-3xl font-bold rounded-lg transition-colors ${
  186. !isStarted
  187. ? 'bg-gray-200 text-gray-400 cursor-not-allowed'
  188. : holdingCash === '1'
  189. ? 'bg-green-500 text-white'
  190. : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  191. }`}
  192. >
  193. B
  194. </button>
  195. </div>
  196. {/* 信息显示 */}
  197. <div className="bg-white p-6 rounded-lg shadow-md">
  198. <div className="grid grid-cols-2 gap-4 mb-4">
  199. <div className="text-gray-600">昵称: {nickname}</div>
  200. <div className="text-gray-600">代码: {classroomData.code}</div>
  201. </div>
  202. {/* 表格头部 */}
  203. <div className="grid grid-cols-8 gap-4 py-2 border-b border-gray-200 text-sm font-medium text-gray-600">
  204. <div>序</div>
  205. <div>训练日期</div>
  206. <div>持股</div>
  207. <div>持币</div>
  208. <div>价格</div>
  209. <div>收益(元)</div>
  210. <div>盈亏率</div>
  211. <div>号</div>
  212. </div>
  213. {/* 表格内容 - 优化滚动体验 */}
  214. <div className="max-h-60 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
  215. {[...answerRecords].reverse().map((record: AnswerRecord) => (
  216. <div key={record.date} className="grid grid-cols-8 gap-4 py-2 text-sm text-gray-800 hover:bg-gray-50">
  217. <div>{record.index}</div>
  218. <div>{dayjs(record.date).format('YYYY-MM-DD')}</div>
  219. <div className="text-red-500">{record.holdingStock}</div>
  220. <div className="text-green-500">{record.holdingCash}</div>
  221. <div>{record.price}</div>
  222. <div className={record.profitAmount >= 0 ? 'text-red-500' : 'text-green-500'}>
  223. {record.profitAmount.toFixed(2)}
  224. </div>
  225. <div className={record.profitPercent >= 0 ? 'text-red-500' : 'text-green-500'}>
  226. {record.profitPercent.toFixed(2)}%
  227. </div>
  228. <div>{record.index}</div>
  229. </div>
  230. ))}
  231. </div>
  232. </div>
  233. </div>
  234. </div>
  235. );
  236. }