| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- import React, { useState, useEffect } from 'react';
- import { useSearchParams } from 'react-router';
- import { Table, Button, message, Input, QRCode, Modal, Tabs } from 'antd';
- // import type { ColumnType } from 'antd/es/table';
- import type { GetProp , TableProps} from 'antd';
- import dayjs from 'dayjs';
- import { useSocketClient } from './hooks/useSocketClient.ts';
- import type {
- QuizState,
- ExamSocketRoomMessage
- } from './types.ts';
- import type { Answer, CumulativeResult } from './types.ts';
- type ColumnType = GetProp<TableProps,'columns'>[number]
- // 当前答题情况组件
- function CurrentAnswers({ answers, columns }: { answers: Answer[], columns: any[] }) {
- return (
- <div>
- <Table
- columns={columns}
- dataSource={answers}
- rowKey={(record) => `${record.userId}-${record.date}`}
- pagination={false}
- />
- </div>
- );
- }
- // 每日统计组件
- function DailyStatistics({ dailyAnswers, columns }: { dailyAnswers: {[key: string]: Answer[]}, columns: any[] }) {
- return (
- <div>
- <Table
- columns={columns}
- dataSource={Object.keys(dailyAnswers).map(date => ({ date }))}
- rowKey="date"
- pagination={false}
- />
- </div>
- );
- }
- // 累计结果组件
- function CumulativeResults({ results, columns }: { results: CumulativeResult[], columns: any[] }) {
- return (
- <div>
- <Table
- columns={columns}
- dataSource={results}
- rowKey="userId"
- pagination={false}
- />
- </div>
- );
- }
- // 二维码组件
- function QRCodeSection({ classroom }: { classroom: string }) {
- return (
- <div className="text-center">
- <div className="text-gray-600 mb-2">扫码参与训练</div>
- <div className="inline-block p-4 bg-white rounded-lg shadow-md">
- <QRCode value={`${globalThis.location.origin}/mobile/exam/card?classroom=${classroom}`} />
- </div>
- </div>
- );
- }
- export default function ExamAdmin() {
- const [searchParams] = useSearchParams();
- const classroom = searchParams.get('classroom');
- const {
- socketRoom: { joinRoom, leaveRoom, client },
- answerManagement,
- isConnected,
- } = useSocketClient(classroom as string);
- const [answers, setAnswers] = useState<Answer[]>([]);
- const [dailyAnswers, setDailyAnswers] = useState<{[key: string]: Answer[]}>({});
- const [currentDate, setCurrentDate] = useState('');
- const [currentPrice, setCurrentPrice] = useState('0');
- const [mark, setMark] = useState('');
- const [activeTab, setActiveTab] = useState('current');
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
- const initExamData = async () => {
- if (!classroom) return;
- setLoading(true);
- setError(null);
- try {
- // 获取当前问题
- const question = await answerManagement.getCurrentQuestion(classroom);
- if (question) {
- setCurrentDate(question.date);
- setCurrentPrice(String(question.price));
- // 获取答题记录
- const answers = await answerManagement.getAnswers(
- classroom,
- ''
- );
-
- const processedAnswers = answers.map(answer => ({
- ...answer,
- profitAmount: answer.profitAmount || 0,
- profitPercent: answer.profitPercent || 0,
- holdingStock: answer.holdingStock || '0',
- holdingCash: answer.holdingCash || '0'
- }));
- const processedDailyAnswers:{[key: string]: Answer[]} = {};
- processedAnswers.forEach(val => {
- if(!processedDailyAnswers[val.date])
- processedDailyAnswers[val.date] = [];
- processedDailyAnswers[val.date].push(val)
- })
- setAnswers(processedAnswers);
- setDailyAnswers(processedDailyAnswers);
- }
- } catch (err) {
- console.error('初始化答题数据失败:', err);
- setError('初始化答题数据失败');
- } finally {
- setLoading(false);
- }
- };
- // 结算函数
- const handleSettlement = async () => {
- if (!classroom || answers.length === 0) return;
- setLoading(true);
-
- try {
- await answerManagement.sendSettleExam(classroom);
- message.success('结算成功');
- } catch (error) {
- console.error('结算失败:', error);
- message.error('结算失败');
- } finally {
- setLoading(false);
- }
- };
- const handleSubmit = async () => {
- if (!classroom || answers.length === 0) return;
- try {
- await answerManagement.cleanupRoom(classroom);
- message.success('答案提交成功');
- setAnswers([]);
- setDailyAnswers({});
- setCurrentDate('');
- setCurrentPrice('0');
- } catch (error: any) {
- console.error('提交答案失败:', error);
- message.error(error?.message || '提交答案失败');
- }
- };
- const handleRestart = async () => {
- if (!classroom) return;
- try {
- await answerManagement.cleanupRoom(classroom);
- setAnswers([]);
- setDailyAnswers({});
- setCurrentDate('');
- setCurrentPrice('0');
- message.success('已重新开始');
- } catch (error) {
- console.error('重新开始失败:', error);
- message.error('重新开始失败');
- }
- };
- const columns = [
- {
- title: '昵称',
- dataIndex: 'userId',
- key: 'userId',
- },
- {
- title: '日期',
- dataIndex: 'date',
- key: 'date',
- render: (text: string) => text ? dayjs(text).format('YYYY-MM-DD') : '-',
- },
- {
- title: '持股',
- dataIndex: 'holdingStock',
- key: 'holdingStock',
- },
- {
- title: '持币',
- dataIndex: 'holdingCash',
- key: 'holdingCash',
- },
- {
- title: '价格',
- dataIndex: 'price',
- key: 'price',
- render: (text: string | undefined) => text ? parseFloat(text).toFixed(2) : '-',
- },
- {
- title: '收益(元)',
- dataIndex: 'profitAmount',
- key: 'profitAmount',
- render: (text: number | undefined) => text !== undefined ? text.toFixed(2) : '-',
- },
- {
- title: '盈亏率',
- dataIndex: 'profitPercent',
- key: 'profitPercent',
- render: (text: number | undefined) => text !== undefined ? `${text.toFixed(2)}%` : '-',
- }
- ];
- const resultColumns: ColumnType[] = [
- {
- title: '昵称',
- dataIndex: 'userId',
- key: 'userId',
- },
- {
- title: '累计盈亏(元)',
- dataIndex: 'totalProfitAmount',
- key: 'totalProfitAmount',
- render: (text: number | undefined) => text !== undefined ? text.toFixed(2) : '-',
- },
- {
- title: '累计盈亏率',
- dataIndex: 'totalProfitPercent',
- key: 'totalProfitPercent',
- render: (text: number | undefined) => text !== undefined ? `${text.toFixed(2)}%` : '-',
- },
- ];
- const dailyAnswersColumns = [
- {
- title: '日期',
- dataIndex: 'date',
- key: 'date',
- render: (text: string) => dayjs(text).format('YYYY-MM-DD'),
- },
- {
- title: '答题人数',
- key: 'count',
- render: (_: any, record: { date: string }) => dailyAnswers[record.date]?.length || 0,
- },
- {
- title: '持股人数',
- key: 'holdingStockCount',
- render: (_: any, record: { date: string }) =>
- dailyAnswers[record.date]?.filter((a: any) => a.holdingStock === '1').length || 0,
- },
- {
- title: '持币人数',
- key: 'holdingCashCount',
- render: (_: any, record: { date: string }) =>
- dailyAnswers[record.date]?.filter((a: any) => a.holdingCash === '1').length || 0,
- }
- ];
- // 计算累计结果的函数
- const calculateCumulativeResults = (dailyAnswers: {[key: string]: Answer[]}): CumulativeResult[] => {
- const userResults = new Map<string, CumulativeResult>();
- // 按日期排序
- const sortedDates = Object.keys(dailyAnswers).sort((a: string, b: string) =>
- new Date(a).getTime() - new Date(b).getTime()
- );
-
- sortedDates.forEach(date => {
- const answers = dailyAnswers[date] || [];
- answers.forEach((answer: Answer) => {
- const userId = answer.userId;
- // 直接使用服务端计算好的收益数据
- const profitAmount = answer.profitAmount || 0;
- const profitPercent = answer.profitPercent || 0;
- if (!userResults.has(userId)) {
- userResults.set(userId, {
- userId,
- totalProfitAmount: 0,
- totalProfitPercent: 0
- });
- }
- const currentResult = userResults.get(userId)!;
- currentResult.totalProfitAmount += profitAmount;
- currentResult.totalProfitPercent += profitPercent;
- userResults.set(userId, currentResult);
- });
- });
- return Array.from(userResults.values());
- };
- const items = [
- {
- key: 'current',
- label: '当前答题情况',
- children: <CurrentAnswers answers={answers} columns={columns} />,
- },
- {
- key: 'daily',
- label: '每日答题统计',
- children: <DailyStatistics dailyAnswers={dailyAnswers} columns={dailyAnswersColumns} />,
- },
- {
- key: 'cumulative',
- label: '累计结果',
- children: <CumulativeResults
- results={calculateCumulativeResults(dailyAnswers)}
- columns={resultColumns}
- />,
- },
- ];
-
- // 加入/离开房间
- useEffect(() => {
- if (!classroom) return;
-
- joinRoom(classroom);
- initExamData();
- return () => {
- leaveRoom(classroom);
- };
- }, [classroom, joinRoom, leaveRoom]);
- // 监听答题消息并更新答案
- useEffect(() => {
- if (!classroom || !currentDate || !client) return;
- const handleAnswerMessage = async () => {
- try {
- const answers = await answerManagement.getAnswers(
- classroom as string,
- currentDate
- );
-
- const processedAnswers = answers.map(answer => ({
- ...answer,
- profitAmount: answer.profitAmount || 0,
- profitPercent: answer.profitPercent || 0,
- holdingStock: answer.holdingStock || '0',
- holdingCash: answer.holdingCash || '0'
- }));
- setAnswers(processedAnswers);
- setDailyAnswers(prev => ({
- ...prev,
- [currentDate]: processedAnswers
- }));
- } catch (error) {
- console.error('获取答案失败:', error);
- }
- };
- client.on('exam:answerUpdated', handleAnswerMessage);
- return () => {
- if (!client) return;
- client.off('exam:answerUpdated', handleAnswerMessage);
- };
- }, [classroom, currentDate, answerManagement, client]);
- // 监听当前问题变化
- useEffect(() => {
- if (!client ) return;
- const handleQuestionUpdate = (question:QuizState ) => {
- setCurrentDate(question.date);
- setCurrentPrice(String(question.price));
- };
- client.on('exam:question', handleQuestionUpdate);
- return () => {
- client.off('exam:question', handleQuestionUpdate);
- };
- }, [client]);
- return (
- <div className="p-6">
- {!isConnected && (
- <div className="bg-yellow-600 text-white text-center py-1 text-sm">
- 正在尝试连接答题卡服务...
- </div>
- )}
- <div className="mb-6 flex justify-between items-center">
- <div>
- <h2 className="text-2xl font-bold">答题卡管理</h2>
- <div className="mt-2 text-gray-600">
- <span className="mr-4">教室号: {classroom}</span>
- <span className="mr-4">当前日期: {currentDate}</span>
- <span>当前价格: {currentPrice}</span>
- </div>
- </div>
- </div>
- {/* 主要内容区域 */}
- <div className="mb-6">
- <Tabs
- activeKey={activeTab}
- onChange={setActiveTab}
- items={items}
- />
- </div>
- {/* 底部按钮组 */}
- <div className="flex items-center space-x-4 mb-8">
- <Button onClick={handleSettlement} disabled={answers.length === 0}>
- 结算
- </Button>
- <Button type="primary" onClick={handleSubmit} disabled={answers.length === 0}>
- 收卷
- </Button>
- <Input
- value={mark}
- onChange={(e) => setMark(e.target.value)}
- placeholder="标记"
- style={{ width: 200 }}
- />
- <Button onClick={() => message.info('标记已保存')}>查看</Button>
- <Button onClick={handleRestart}>重开</Button>
- </div>
- {/* 二维码区域 */}
- <QRCodeSection classroom={classroom || ''} />
- </div>
- );
- }
|