2
0

ExamCard.tsx 9.0 KB

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