ExamCard.tsx 8.7 KB

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