| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- import React,{ useState, useCallback, useEffect } from 'react';
- import { useQuery } from '@tanstack/react-query';
- import { useSearchParams, useNavigate } from "react-router";
- import dayjs from 'dayjs';
- import { message } from 'antd';
- import { useSocketClient } from './hooks/useSocketClient.ts';
- import { ClassroomDataAPI } from '../../api/classroom_data.ts';
- import { ClassroomStatus } from '../../../share/types_stock.ts';
- import type { QuizState } from './types.ts';
- import type { AnswerRecord, Answer } from './types.ts';
- import { useAuth } from "../../hooks.tsx";
- // 答题卡页面
- export default function ExamCard() {
- const { user } = useAuth();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const classroom = searchParams.get('classroom');
- const {
- socketRoom: { joinRoom, leaveRoom, client },
- answerManagement,
- isConnected,
- } = useSocketClient(classroom as string);
- const [currentDate, setCurrentDate] = useState('');
- const [currentPrice, setCurrentPrice] = useState('0');
- const [holdingStock, setHoldingStock] = useState('0');
- const [holdingCash, setHoldingCash] = useState('0');
- const [isStarted, setIsStarted] = useState(false);
- const [answerRecords, setAnswerRecords] = useState<AnswerRecord[]>([]);
- const { data: classroomData, isLoading } = useQuery({
- queryKey: ['classroom', classroom],
- queryFn: async () => {
- if (!classroom) return null;
- const response = await ClassroomDataAPI.getClassroomDatas({ classroom_no: classroom });
- if (!response.data?.length) {
- message.error('获取教室数据失败');
- return null;
- }
- return response.data[0] || null;
- },
- enabled: !!classroom
- });
- // 初始化答题卡数据
- const initExamCard = useCallback(async () => {
- if (!classroom || !user?.username) {
- // globalThis.location.href = '/exam';
- navigate('/mobile')
- return;
- }
-
- if (classroomData && classroomData.status !== ClassroomStatus.OPEN) {
- message.error('该教室已关闭');
- // globalThis.location.href = '/exam';
- navigate('/mobile')
- return;
- }
-
- // 获取当前问题并更新状态
- const question = await answerManagement.getCurrentQuestion(classroom);
- setCurrentDate(question.date);
- setCurrentPrice(String(question.price));
- setIsStarted(true);
-
- // 获取用户回答记录
- if (user?.id) {
- try {
- const answers = await answerManagement.getUserAnswers(classroom, String(user.id));
- if (answers && answers.length > 0) {
- const lastAnswer = answers[answers.length - 1];
- setHoldingStock(lastAnswer.holdingStock);
- setHoldingCash(lastAnswer.holdingCash);
-
- const records = answers.map((answer: Answer, index: number): AnswerRecord => ({
- date: answer.date,
- price: String(answer.price || '0'),
- holdingStock: answer.holdingStock,
- holdingCash: answer.holdingCash,
- profitAmount: answer.profitAmount || 0,
- profitPercent: answer.profitPercent || 0,
- index: index + 1
- }));
-
- setAnswerRecords(records);
- }
- } catch (error) {
- console.error('获取用户回答记录失败:', error);
- }
- }
- }, [classroom, user, classroomData, answerManagement]);
- // 加入/离开房间
- useEffect(() => {
- if (!classroom) return;
-
- joinRoom(classroom);
- initExamCard();
-
- return () => {
- leaveRoom(classroom);
- };
- }, [classroom, joinRoom, leaveRoom]);
- // // 处理房间消息
- // useEffect(() => {
- // if (!lastMessage?.message) return;
- // const { type } = lastMessage.message;
-
- // // 只处理重开消息的UI重置
- // if (type === 'restart') {
- // setCurrentDate('');
- // setCurrentPrice('0');
- // setHoldingStock('0');
- // setHoldingCash('0');
- // setIsStarted(false);
- // setAnswerRecords([]);
- // }
- // }, [lastMessage]);
- // 处理选择A(持股)
- const handleChooseA = useCallback(async () => {
- setHoldingStock('1');
- setHoldingCash('0');
-
- if (classroom && user?.username && currentDate) {
- const answer = {
- date: currentDate,
- holdingStock: '1',
- holdingCash: '0',
- userId: String(user.id),
- price: currentPrice
- };
-
- try {
- await answerManagement.storeAnswer(
- classroom as string,
- currentDate,
- String(user.id),
- answer,
- (answers) => {
- const records = answers.map((answer: Answer, index: number): AnswerRecord => ({
- date: answer.date,
- price: String(answer.price || '0'),
- holdingStock: answer.holdingStock,
- holdingCash: answer.holdingCash,
- profitAmount: answer.profitAmount || 0,
- profitPercent: answer.profitPercent || 0,
- index: index + 1
- }));
- setAnswerRecords(records);
- }
- );
- } catch (error) {
- message.error('提交答案失败');
- }
- }
- }, [classroom, user, currentDate, currentPrice, answerManagement]);
- const handleChooseB = useCallback(async () => {
- setHoldingStock('0');
- setHoldingCash('1');
-
- if (classroom && user?.username && currentDate) {
- const answer = {
- date: currentDate,
- holdingStock: '0',
- holdingCash: '1',
- userId: String(user.id),
- price: currentPrice
- };
-
- try {
- await answerManagement.storeAnswer(
- classroom as string,
- currentDate,
- String(user.id),
- answer,
- (answers) => {
- const records = answers.map((answer: Answer, index: number): AnswerRecord => ({
- date: answer.date,
- price: String(answer.price || '0'),
- holdingStock: answer.holdingStock,
- holdingCash: answer.holdingCash,
- profitAmount: answer.profitAmount || 0,
- profitPercent: answer.profitPercent || 0,
- index: index + 1
- }));
- setAnswerRecords(records);
- }
- );
- } catch (error) {
- message.error('提交答案失败');
- }
- }
- }, [classroom, user, currentDate, currentPrice, answerManagement]);
- // 监听当前问题变化
- useEffect(() => {
- if (!client ) return;
- const handleQuestionUpdate = (question:QuizState ) => {
- setCurrentDate(question.date);
- setCurrentPrice(String(question.price));
- setIsStarted(true);
- };
- client.on('exam:question', handleQuestionUpdate);
- return () => {
- client.off('exam:question', handleQuestionUpdate);
- };
- }, [client]);
- // 监听重开
- useEffect(() => {
- if (!client ) return;
- const handleCleaned = () => {
- setCurrentDate('');
- setCurrentPrice('0');
- setHoldingStock('0');
- setHoldingCash('0');
- setIsStarted(false);
- setAnswerRecords([]);
- };
- client.on('exam:cleaned', handleCleaned);
- return () => {
- client.off('exam:cleaned', handleCleaned);
- };
- }, [client]);
- // 监听结算消息
- useEffect(() => {
- if (!client) return;
- const handleSettle = () => {
- handleChooseB();
- };
- client.on('exam:settle', handleSettle);
- return () => {
- client.off('exam:settle', handleSettle);
- };
- }, [client, handleChooseB]);
- if (isLoading || !classroomData) {
- return <div className="flex items-center justify-center min-h-screen">加载中...</div>;
- }
- return (
- <div className="flex flex-col items-center min-h-screen bg-gray-100 py-8">
- {!isConnected && (
- <div className="bg-yellow-600 text-white text-center py-1 text-sm">
- 正在尝试连接答题卡服务...
- </div>
- )}
- {/* 选择区域 */}
- <div className="w-full max-w-2xl">
- <div className="text-center mb-8">
- <h2 className="text-2xl font-bold mb-2">持股选A, 持币选B</h2>
- <div className="flex justify-center space-x-4 text-gray-600">
- {isStarted ? (
- <>
- <span>日期: {currentDate}</span>
- <span>价格: {currentPrice}</span>
- </>
- ) : (
- <div className="text-blue-600">
- <div className="mb-2">等待训练开始...</div>
- <div className="text-sm text-gray-500">
- 训练日期: {dayjs(classroomData.training_date).format('YYYY-MM-DD')}
- </div>
- </div>
- )}
- </div>
- </div>
- {/* 选择按钮 */}
- <div className="flex justify-center items-center space-x-4 mb-8 bg-white p-6 rounded-lg shadow-md">
- <button
- onClick={handleChooseA}
- disabled={!isStarted}
- className={`flex-1 py-8 text-3xl font-bold rounded-lg transition-colors ${
- !isStarted
- ? 'bg-gray-200 text-gray-400 cursor-not-allowed'
- : holdingStock === '1'
- ? 'bg-red-500 text-white'
- : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
- }`}
- >
- A
- </button>
- <div className="text-xl font-medium text-gray-700">
- {isStarted ? '开始' : '等待'}
- </div>
- <button
- onClick={handleChooseB}
- disabled={!isStarted}
- className={`flex-1 py-8 text-3xl font-bold rounded-lg transition-colors ${
- !isStarted
- ? 'bg-gray-200 text-gray-400 cursor-not-allowed'
- : holdingCash === '1'
- ? 'bg-green-500 text-white'
- : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
- }`}
- >
- B
- </button>
- </div>
- {/* 信息显示 */}
- <div className="bg-white p-6 rounded-lg shadow-md">
- <div className="grid grid-cols-2 gap-4 mb-4">
- <div className="text-gray-600">昵称: {user?.username || '未知用户'}</div>
- <div className="text-gray-600">代码: {classroomData.code}</div>
- </div>
- {/* 表格头部 */}
- <div className="grid grid-cols-8 gap-4 py-2 border-b border-gray-200 text-sm font-medium text-gray-600">
- <div>序</div>
- <div>训练日期</div>
- <div>持股</div>
- <div>持币</div>
- <div>价格</div>
- <div>收益(元)</div>
- <div>盈亏率</div>
- <div>号</div>
- </div>
- {/* 表格内容 */}
- <div className="max-h-60 overflow-y-auto">
- {[...answerRecords].reverse().map((record: AnswerRecord) => (
- <div key={record.date} className="grid grid-cols-8 gap-4 py-2 text-sm text-gray-800 hover:bg-gray-50">
- <div>{record.index}</div>
- <div>{dayjs(record.date).format('YYYY-MM-DD')}</div>
- <div className="text-red-500">{record.holdingStock}</div>
- <div className="text-green-500">{record.holdingCash}</div>
- <div>{record.price}</div>
- <div className={record.profitAmount >= 0 ? 'text-red-500' : 'text-green-500'}>
- {record.profitAmount.toFixed(2)}
- </div>
- <div className={record.profitPercent >= 0 ? 'text-red-500' : 'text-green-500'}>
- {record.profitPercent.toFixed(2)}%
- </div>
- <div>{record.index}</div>
- </div>
- ))}
- </div>
- </div>
- </div>
- </div>
- );
- }
|