ExamAdmin.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import React, { useState, useEffect } from 'react';
  2. import { useSearchParams } from 'react-router';
  3. import { Table, Button, message, Input, QRCode, Modal, Tabs } from 'antd';
  4. // import type { ColumnType } from 'antd/es/table';
  5. import type { GetProp , TableProps} from 'antd';
  6. import dayjs from 'dayjs';
  7. import { useCurrentQuestion, useRoomMessages, useAnswerManagement, useSocketRoom, useAnswerCache } from './hooks/useSocketClient.ts';
  8. import type { Answer, CumulativeResult } from './types.ts';
  9. type ColumnType = GetProp<TableProps,'columns'>[number]
  10. // 当前答题情况组件
  11. function CurrentAnswers({ answers, columns }: { answers: Answer[], columns: any[] }) {
  12. return (
  13. <div>
  14. <Table
  15. columns={columns}
  16. dataSource={answers}
  17. rowKey={(record) => `${record.userId}-${record.date}`}
  18. pagination={false}
  19. />
  20. </div>
  21. );
  22. }
  23. // 每日统计组件
  24. function DailyStatistics({ dailyAnswers, columns }: { dailyAnswers: {[key: string]: Answer[]}, columns: any[] }) {
  25. return (
  26. <div>
  27. <Table
  28. columns={columns}
  29. dataSource={Object.keys(dailyAnswers).map(date => ({ date }))}
  30. rowKey="date"
  31. pagination={false}
  32. />
  33. </div>
  34. );
  35. }
  36. // 累计结果组件
  37. function CumulativeResults({ results, columns }: { results: CumulativeResult[], columns: any[] }) {
  38. return (
  39. <div>
  40. <Table
  41. columns={columns}
  42. dataSource={results}
  43. rowKey="userId"
  44. pagination={false}
  45. />
  46. </div>
  47. );
  48. }
  49. // 二维码组件
  50. function QRCodeSection({ classroom }: { classroom: string }) {
  51. return (
  52. <div className="text-center">
  53. <div className="text-gray-600 mb-2">扫码参与训练</div>
  54. <div className="inline-block p-4 bg-white rounded-lg shadow-md">
  55. <QRCode value={`${globalThis.location.origin}/exam?classroom=${classroom}`} />
  56. </div>
  57. </div>
  58. );
  59. }
  60. export default function ExamAdmin() {
  61. const [searchParams] = useSearchParams();
  62. const classroom = searchParams.get('classroom');
  63. useSocketRoom(classroom);
  64. const {currentQuestion} = useCurrentQuestion(classroom);
  65. const lastMessage = useRoomMessages(classroom);
  66. const {
  67. submitAnswersToBackend,
  68. autoSettlement,
  69. restartTraining
  70. } = useAnswerManagement(classroom);
  71. const [answers, setAnswers] = useState<Answer[]>([]);
  72. const [dailyAnswers, setDailyAnswers] = useState<{[key: string]: Answer[]}>({});
  73. const [currentDate, setCurrentDate] = useState('');
  74. const [currentPrice, setCurrentPrice] = useState('0');
  75. const [mark, setMark] = useState('');
  76. const [activeTab, setActiveTab] = useState('current');
  77. // 使用新的 useAnswerCache hook
  78. const { answers: cachedAnswers } = useAnswerCache(classroom, currentDate);
  79. // 更新答案状态
  80. useEffect(() => {
  81. if (cachedAnswers && cachedAnswers.length > 0) {
  82. setAnswers(cachedAnswers);
  83. }
  84. }, [cachedAnswers]);
  85. // 更新每日答题情况
  86. useEffect(() => {
  87. if (currentDate && cachedAnswers) {
  88. setDailyAnswers((prev: {[key: string]: Answer[]}) => ({
  89. ...prev,
  90. [currentDate]: cachedAnswers
  91. }));
  92. }
  93. }, [currentDate, cachedAnswers]);
  94. useEffect(() => {
  95. if (currentQuestion) {
  96. console.log('currentQuestion', currentQuestion);
  97. setCurrentDate(currentQuestion.date);
  98. setCurrentPrice(String(currentQuestion.price));
  99. }
  100. }, [currentQuestion]);
  101. // 添加结算函数
  102. const handleSettlement = async () => {
  103. if (!classroom || answers.length === 0) return;
  104. try {
  105. await autoSettlement(currentDate);
  106. message.success('结算成功');
  107. } catch (error) {
  108. console.error('结算失败:', error);
  109. message.error('结算失败');
  110. }
  111. };
  112. // 修改提交函数
  113. const handleSubmit = async () => {
  114. if (!classroom || answers.length === 0) return;
  115. try {
  116. await submitAnswersToBackend(currentDate);
  117. message.success('答案提交成功');
  118. setAnswers([]);
  119. } catch (error: any) {
  120. console.error('提交答案失败:', error);
  121. message.error(error?.message || '提交答案失败');
  122. }
  123. };
  124. // 重新开始
  125. const handleRestart = async () => {
  126. try {
  127. await restartTraining();
  128. setAnswers([]);
  129. setDailyAnswers({});
  130. setCurrentDate('');
  131. setCurrentPrice('0');
  132. message.success('已重新开始');
  133. } catch (error) {
  134. console.error('重新开始失败:', error);
  135. message.error('重新开始失败');
  136. }
  137. };
  138. const columns = [
  139. {
  140. title: '昵称',
  141. dataIndex: 'userId',
  142. key: 'userId',
  143. },
  144. {
  145. title: '日期',
  146. dataIndex: 'date',
  147. key: 'date',
  148. render: (text: string) => text ? dayjs(text).format('YYYY-MM-DD') : '-',
  149. },
  150. {
  151. title: '持股',
  152. dataIndex: 'holdingStock',
  153. key: 'holdingStock',
  154. },
  155. {
  156. title: '持币',
  157. dataIndex: 'holdingCash',
  158. key: 'holdingCash',
  159. },
  160. {
  161. title: '价格',
  162. dataIndex: 'price',
  163. key: 'price',
  164. render: (text: string | undefined) => text ? parseFloat(text).toFixed(2) : '-',
  165. },
  166. {
  167. title: '收益(元)',
  168. dataIndex: 'profitAmount',
  169. key: 'profitAmount',
  170. render: (text: number | undefined) => text !== undefined ? text.toFixed(2) : '-',
  171. },
  172. {
  173. title: '盈亏率',
  174. dataIndex: 'profitPercent',
  175. key: 'profitPercent',
  176. render: (text: number | undefined) => text !== undefined ? `${text.toFixed(2)}%` : '-',
  177. }
  178. ];
  179. const resultColumns: ColumnType[] = [
  180. {
  181. title: '昵称',
  182. dataIndex: 'userId',
  183. key: 'userId',
  184. },
  185. {
  186. title: '累计盈亏(元)',
  187. dataIndex: 'totalProfitAmount',
  188. key: 'totalProfitAmount',
  189. render: (text: number | undefined) => text !== undefined ? text.toFixed(2) : '-',
  190. },
  191. {
  192. title: '累计盈亏率',
  193. dataIndex: 'totalProfitPercent',
  194. key: 'totalProfitPercent',
  195. render: (text: number | undefined) => text !== undefined ? `${text.toFixed(2)}%` : '-',
  196. },
  197. ];
  198. const dailyAnswersColumns = [
  199. {
  200. title: '日期',
  201. dataIndex: 'date',
  202. key: 'date',
  203. render: (text: string) => dayjs(text).format('YYYY-MM-DD'),
  204. },
  205. {
  206. title: '答题人数',
  207. key: 'count',
  208. render: (_: any, record: { date: string }) => dailyAnswers[record.date]?.length || 0,
  209. },
  210. {
  211. title: '持股人数',
  212. key: 'holdingStockCount',
  213. render: (_: any, record: { date: string }) =>
  214. dailyAnswers[record.date]?.filter((a: any) => a.holdingStock === '1').length || 0,
  215. },
  216. {
  217. title: '持币人数',
  218. key: 'holdingCashCount',
  219. render: (_: any, record: { date: string }) =>
  220. dailyAnswers[record.date]?.filter((a: any) => a.holdingCash === '1').length || 0,
  221. }
  222. ];
  223. // 计算累计结果的函数
  224. const calculateCumulativeResults = (dailyAnswers: {[key: string]: Answer[]}): CumulativeResult[] => {
  225. const userResults = new Map<string, CumulativeResult>();
  226. // 按日期排序
  227. const sortedDates = Object.keys(dailyAnswers).sort((a: string, b: string) =>
  228. new Date(a).getTime() - new Date(b).getTime()
  229. );
  230. sortedDates.forEach(date => {
  231. const answers = dailyAnswers[date] || [];
  232. answers.forEach((answer: Answer) => {
  233. const userId = answer.userId;
  234. // 直接使用服务端计算好的收益数据
  235. const profitAmount = answer.profitAmount || 0;
  236. const profitPercent = answer.profitPercent || 0;
  237. if (!userResults.has(userId)) {
  238. userResults.set(userId, {
  239. userId,
  240. totalProfitAmount: 0,
  241. totalProfitPercent: 0
  242. });
  243. }
  244. const currentResult = userResults.get(userId)!;
  245. currentResult.totalProfitAmount += profitAmount;
  246. currentResult.totalProfitPercent += profitPercent;
  247. userResults.set(userId, currentResult);
  248. });
  249. });
  250. return Array.from(userResults.values());
  251. };
  252. const items = [
  253. {
  254. key: 'current',
  255. label: '当前答题情况',
  256. children: <CurrentAnswers answers={answers} columns={columns} />,
  257. },
  258. {
  259. key: 'daily',
  260. label: '每日答题统计',
  261. children: <DailyStatistics dailyAnswers={dailyAnswers} columns={dailyAnswersColumns} />,
  262. },
  263. {
  264. key: 'cumulative',
  265. label: '累计结果',
  266. children: <CumulativeResults results={calculateCumulativeResults(dailyAnswers)} columns={resultColumns} />,
  267. },
  268. ];
  269. return (
  270. <div className="p-6">
  271. <div className="mb-6 flex justify-between items-center">
  272. <div>
  273. <h2 className="text-2xl font-bold">答题卡管理</h2>
  274. <div className="mt-2 text-gray-600">
  275. <span className="mr-4">教室号: {classroom}</span>
  276. <span className="mr-4">当前日期: {currentDate}</span>
  277. <span>当前价格: {currentPrice}</span>
  278. </div>
  279. </div>
  280. </div>
  281. {/* 主要内容区域 */}
  282. <div className="mb-6">
  283. <Tabs
  284. activeKey={activeTab}
  285. onChange={setActiveTab}
  286. items={items}
  287. />
  288. </div>
  289. {/* 底部按钮组 */}
  290. <div className="flex items-center space-x-4 mb-8">
  291. <Button onClick={handleSettlement} disabled={answers.length === 0}>
  292. 结算
  293. </Button>
  294. <Button type="primary" onClick={handleSubmit} disabled={answers.length === 0}>
  295. 收卷
  296. </Button>
  297. <Input
  298. value={mark}
  299. onChange={(e) => setMark(e.target.value)}
  300. placeholder="标记"
  301. style={{ width: 200 }}
  302. />
  303. <Button onClick={() => message.info('标记已保存')}>查看</Button>
  304. <Button onClick={handleRestart}>重开</Button>
  305. </div>
  306. {/* 二维码区域 */}
  307. <QRCodeSection classroom={classroom || ''} />
  308. </div>
  309. );
  310. }