ExamCard.tsx 11 KB

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