ExamCard.tsx 11 KB

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