Prechádzať zdrojové kódy

迁移入答题卡页面

yourname 6 mesiacov pred
rodič
commit
c198644976

+ 57 - 67
client/mobile/components/Exam/ExamAdmin.tsx

@@ -1,64 +1,58 @@
 import React, { useState, useEffect } from 'react';
 import { useSearchParams } from 'react-router';
 import { Table, Button, message, Input, QRCode, Modal, Tabs } from 'antd';
-import type { GetProp, TableProps } from 'antd';
+// import type { ColumnType } from 'antd/es/table';
+import type { GetProp , TableProps} from 'antd';
 import dayjs from 'dayjs';
-import {
-  useExamCurrentQuestion,
-  useExamRoomMessages,
-  useExamAnswerSubmission,
-  useExamSocketRoom
-} from './hooks/useSocketClient.ts';
+import { useCurrentQuestion, useRoomMessages, useAnswerManagement, useSocketRoom, useAnswerCache } from './hooks/useSocketClient.ts';
+
 import type { Answer, CumulativeResult } from './types.ts';
 
 type ColumnType = GetProp<TableProps,'columns'>[number]
 
-// 当前答题情况组件 - 添加移动端响应式样式
+// 当前答题情况组件
 function CurrentAnswers({ answers, columns }: { answers: Answer[], columns: any[] }) {
   return (
-    <div className="overflow-x-auto">
+    <div>
       <Table 
         columns={columns}
         dataSource={answers}
         rowKey={(record) => `${record.userId}-${record.date}`}
         pagination={false}
-        scroll={{ x: true }}
       />
     </div>
   );
 }
 
-// 每日统计组件 - 添加移动端响应式样式
+// 每日统计组件
 function DailyStatistics({ dailyAnswers, columns }: { dailyAnswers: {[key: string]: Answer[]}, columns: any[] }) {
   return (
-    <div className="overflow-x-auto">
+    <div>
       <Table 
         columns={columns}
         dataSource={Object.keys(dailyAnswers).map(date => ({ date }))}
         rowKey="date"
         pagination={false}
-        scroll={{ x: true }}
       />
     </div>
   );
 }
 
-// 累计结果组件 - 添加移动端响应式样式
+// 累计结果组件
 function CumulativeResults({ results, columns }: { results: CumulativeResult[], columns: any[] }) {
   return (
-    <div className="overflow-x-auto">
+    <div>
       <Table
         columns={columns}
         dataSource={results}
         rowKey="userId"
         pagination={false}
-        scroll={{ x: true }}
       />
     </div>
   );
 }
 
-// 二维码组件 - 保持原样
+// 二维码组件
 function QRCodeSection({ classroom }: { classroom: string }) {
   return (
     <div className="text-center">
@@ -73,10 +67,14 @@ function QRCodeSection({ classroom }: { classroom: string }) {
 export default function ExamAdmin() {
   const [searchParams] = useSearchParams();
   const classroom = searchParams.get('classroom');
-  const { connected } = useExamSocketRoom(classroom);
-  const {currentQuestion} = useExamCurrentQuestion(classroom);
-  const lastMessage = useExamRoomMessages(classroom);
-  const { submitAnswer } = useExamAnswerSubmission(classroom);
+  useSocketRoom(classroom);
+  const {currentQuestion} = useCurrentQuestion(classroom);
+  const lastMessage = useRoomMessages(classroom);
+  const { 
+    submitAnswersToBackend, 
+    autoSettlement,
+    restartTraining 
+  } = useAnswerManagement(classroom);
   
   const [answers, setAnswers] = useState<Answer[]>([]);
   const [dailyAnswers, setDailyAnswers] = useState<{[key: string]: Answer[]}>({});
@@ -85,43 +83,40 @@ export default function ExamAdmin() {
   const [mark, setMark] = useState('');
   const [activeTab, setActiveTab] = useState('current');
 
+  // 使用新的 useAnswerCache hook
+  const { answers: cachedAnswers } = useAnswerCache(classroom, currentDate);
+
   // 更新答案状态
   useEffect(() => {
-    if (lastMessage?.message.type === 'answer') {
-      const answer = lastMessage.message.content;
-      setAnswers(prev => [...prev, answer]);
+    if (cachedAnswers && cachedAnswers.length > 0) {
+      setAnswers(cachedAnswers);
     }
-  }, [lastMessage]);
+  }, [cachedAnswers]);
 
   // 更新每日答题情况
   useEffect(() => {
-    if (currentDate && answers.length > 0) {
-      setDailyAnswers(prev => ({
+    if (currentDate && cachedAnswers) {
+      setDailyAnswers((prev: {[key: string]: Answer[]}) => ({
         ...prev,
-        [currentDate]: answers
+        [currentDate]: cachedAnswers
       }));
     }
-  }, [currentDate, answers]);
+  }, [currentDate, cachedAnswers]);
 
   useEffect(() => {
     if (currentQuestion) {
+      console.log('currentQuestion', currentQuestion);
       setCurrentDate(currentQuestion.date);
       setCurrentPrice(String(currentQuestion.price));
     }
   }, [currentQuestion]);
 
-  // 结算函数
+  // 添加结算函数
   const handleSettlement = async () => {
     if (!classroom || answers.length === 0) return;
 
     try {
-      await submitAnswer(currentDate, 'system', {
-        date: currentDate,
-        price: currentPrice,
-        holdingStock: '0',
-        holdingCash: '1',
-        userId: 'system'
-      });
+      await autoSettlement(currentDate);
       message.success('结算成功');
     } catch (error) {
       console.error('结算失败:', error);
@@ -129,11 +124,12 @@ export default function ExamAdmin() {
     }
   };
 
-  // 提交函数
+  // 修改提交函数
   const handleSubmit = async () => {
     if (!classroom || answers.length === 0) return;
 
     try {
+      await submitAnswersToBackend(currentDate);
       message.success('答案提交成功');
       setAnswers([]);
     } catch (error: any) {
@@ -145,6 +141,7 @@ export default function ExamAdmin() {
   // 重新开始
   const handleRestart = async () => {
     try {
+      await restartTraining();
       setAnswers([]);
       setDailyAnswers({});
       setCurrentDate('');
@@ -198,7 +195,7 @@ export default function ExamAdmin() {
     }
   ];
 
-  const resultColumns: ColumnType<CumulativeResult>[] = [
+  const resultColumns: ColumnType[] = [
     {
       title: '昵称',
       dataIndex: 'userId',
@@ -244,10 +241,11 @@ export default function ExamAdmin() {
     }
   ];
 
-  // 计算累计结果
+  // 计算累计结果的函数
   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()
     );
@@ -256,6 +254,7 @@ export default function ExamAdmin() {
       const answers = dailyAnswers[date] || [];
       answers.forEach((answer: Answer) => {
         const userId = answer.userId;
+        // 直接使用服务端计算好的收益数据
         const profitAmount = answer.profitAmount || 0;
         const profitPercent = answer.profitPercent || 0;
 
@@ -296,56 +295,47 @@ export default function ExamAdmin() {
   ];
 
   return (
-    <div className="p-4">
-      <div className="mb-4">
-        <h2 className="text-xl font-bold">答题卡管理</h2>
-        <div className="mt-2 text-sm text-gray-600">
-          <div>教室号: {classroom}</div>
-          <div>当前日期: {currentDate}</div>
-          <div>当前价格: {currentPrice}</div>
-          <div>Socket状态: {connected ? '已连接' : '未连接'}</div>
+    <div className="p-6">
+      <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-4">
+      <div className="mb-6">
         <Tabs 
           activeKey={activeTab} 
           onChange={setActiveTab}
           items={items}
-          destroyInactiveTabPane
         />
       </div>
 
-      {/* 底部按钮组 - 移动端优化布局 */}
-      <div className="grid grid-cols-2 gap-2 mb-4">
-        <Button 
-          onClick={handleSettlement} 
-          disabled={answers.length === 0}
-          block
-        >
+      {/* 底部按钮组 */}
+      <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}
-          block
-        >
+        <Button type="primary" onClick={handleSubmit} disabled={answers.length === 0}>
           收卷
         </Button>
         <Input
           value={mark}
           onChange={(e) => setMark(e.target.value)}
           placeholder="标记"
-          className="col-span-2"
+          style={{ width: 200 }}
         />
-        <Button onClick={() => message.info('标记已保存')} block>查看</Button>
-        <Button onClick={handleRestart} block>重开</Button>
+        <Button onClick={() => message.info('标记已保存')}>查看</Button>
+        <Button onClick={handleRestart}>重开</Button>
       </div>
 
       {/* 二维码区域 */}
       <QRCodeSection classroom={classroom || ''} />
     </div>
   );
-}
+} 

+ 16 - 19
client/mobile/components/Exam/ExamCard.tsx

@@ -1,27 +1,24 @@
-import React, { useState, useCallback, useEffect } from 'react';
+import React,{ useState, useCallback, useEffect } from 'react';
 import { useQuery } from '@tanstack/react-query';
 import { useSearchParams } from "react-router";
 import dayjs from 'dayjs';
 import { message } from 'antd';
-import {
-  useExamCurrentQuestion,
-  useExamRoomMessages,
-  useExamAnswerSubmission,
-  useExamUserAnswerHistory,
-  useExamSocketRoom
-} from './hooks/useSocketClient.ts';
+import { useCurrentQuestion, useRoomMessages, useAnswerSubmission, useUserAnswerHistory, useSocketRoom } from './hooks/useSocketClient.ts';
 import type { AnswerRecord, ClassroomData, Answer } from './types.ts';
 
+
+
+
 // 答题卡页面
 export default function ExamCard() {
   const [searchParams] = useSearchParams();
   const classroom = searchParams.get('classroom');
   const nickname = searchParams.get('nickname');
-  useExamSocketRoom(classroom);
-  const lastMessage = useExamRoomMessages(classroom);
-  const { submitAnswer } = useExamAnswerSubmission(classroom);
-  const { currentQuestion } = useExamCurrentQuestion(classroom);
-  const { userAnswers } = useExamUserAnswerHistory(classroom, nickname);
+  useSocketRoom(classroom);
+  const lastMessage = useRoomMessages(classroom);
+  const { submitAnswer } = useAnswerSubmission(classroom);
+  const {currentQuestion} = useCurrentQuestion(classroom);
+  const { userAnswers } = useUserAnswerHistory(classroom, nickname);
   const [currentDate, setCurrentDate] = useState('');
   const [currentPrice, setCurrentPrice] = useState('0');
   const [holdingStock, setHoldingStock] = useState('0');
@@ -158,7 +155,7 @@ export default function ExamCard() {
   }
 
   return (
-    <div className="flex flex-col items-center min-h-screen bg-gray-100 py-8 px-4">
+    <div className="flex flex-col items-center min-h-screen bg-gray-100 py-8">
       {/* 选择区域 */}
       <div className="w-full max-w-2xl">
         <div className="text-center mb-8">
@@ -180,12 +177,12 @@ export default function ExamCard() {
           </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={`min-w-[120px] py-8 px-4 text-3xl font-bold rounded-lg transition-colors ${
+            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'
@@ -201,7 +198,7 @@ export default function ExamCard() {
           <button
             onClick={handleChooseB}
             disabled={!isStarted}
-            className={`min-w-[120px] py-8 px-4 text-3xl font-bold rounded-lg transition-colors ${
+            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'
@@ -232,8 +229,8 @@ export default function ExamCard() {
             <div>号</div>
           </div>
 
-          {/* 表格内容 - 优化滚动体验 */}
-          <div className="max-h-60 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
+          {/* 表格内容 */}
+          <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>

+ 5 - 11
client/mobile/components/Exam/ExamIndex.tsx

@@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query';
 import { useNavigate, useSearchParams } from "react-router";
 import dayjs from 'dayjs';
 import { message } from 'antd';
-import { Skeleton } from 'antd';
 
 // 昵称输入页面
 function ExamIndex() {
@@ -55,18 +54,14 @@ function ExamIndex() {
   }, [nickname, navigate, classroom, classroomData]);
 
   if (isLoading) {
-    return (
-      <div className="flex flex-col items-center justify-center min-h-screen p-4">
-        <Skeleton active paragraph={{ rows: 4 }} />
-      </div>
-    );
+    return <div className="flex items-center justify-center min-h-screen">加载中...</div>;
   }
 
   return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4">
+    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
       <div className="w-full max-w-md space-y-8">
         <div className="text-center">
-          <h2 className="text-2xl font-bold text-gray-900">股票训练答题卡系统</h2>
+          <h2 className="text-3xl font-bold text-gray-900">股票训练答题卡系统</h2>
           <p className="mt-2 text-gray-600">
             {classroom ? `教室号: ${classroom}` : '请输入昵称开始答题'}
           </p>
@@ -84,15 +79,14 @@ function ExamIndex() {
             value={nickname}
             onChange={(e) => setNickname(e.target.value)}
             placeholder="请输入昵称"
-            className="w-full px-4 py-3 text-base border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
-            onKeyPress={(e) => e.key === 'Enter' && handleJoinTraining()}
+            className="w-full px-4 py-3 text-lg border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
           />
         </div>
 
         <button
           onClick={handleJoinTraining}
           disabled={!classroomData || classroomData.status !== "1"}
-          className={`w-full px-8 py-3 text-base font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors ${
+          className={`w-full px-8 py-3 text-lg font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors ${
             !classroomData || classroomData.status !== "1"
               ? 'bg-gray-400 cursor-not-allowed'
               : 'bg-blue-500 hover:bg-blue-600 focus:ring-blue-500'

+ 248 - 85
client/mobile/components/Exam/hooks/useSocketClient.ts

@@ -8,17 +8,13 @@ import type {
   ExamSocketRoomMessage,
   Answer
 } from '../types.ts';
+import { useAuth } from "../../../hooks.tsx";
 
 interface LoaderData {
   token: string;
   serverUrl: string;
 }
 
-// 系统消息默认值
-const SYSTEM_USER_ID = 'system';
-const DEFAULT_HOLDING_STOCK = '0';
-const DEFAULT_HOLDING_CASH = '1';
-
 // 工具函数:统一错误处理
 const handleAsyncOperation = async <T>(
   operation: () => Promise<T>,
@@ -40,8 +36,8 @@ interface ProfitResult {
 
 function calculateProfit(currentPrice: number, previousPrice: number, holdingStock: string): ProfitResult {
   if (holdingStock === '1') {
-    const profitAmount = currentPrice - previousPrice;
-    const profitPercent = ((currentPrice - previousPrice) / previousPrice) * 100;
+    const profitAmount = currentPrice - previousPrice;  // 金额收益
+    const profitPercent = ((currentPrice - previousPrice) / previousPrice) * 100;  // 百分比收益
     return {
       profitAmount,
       profitPercent
@@ -53,12 +49,10 @@ function calculateProfit(currentPrice: number, previousPrice: number, holdingSto
   };
 }
 
-// 核心Socket客户端Hook
-export function useExamSocketClient(roomId: string | null) {
-  const { token, serverUrl } = {
-    token: '',
-    serverUrl: '/wss'
-  };
+// 使用react-query管理socket客户端
+export function useSocketClient(roomId: string | null) {
+  const { token } = useAuth();
+  const serverUrl = '/';
 
   const { data: client } = useQuery({
     queryKey: ['socket-client'],
@@ -84,52 +78,31 @@ export function useExamSocketClient(roomId: string | null) {
   });
 
   const joinRoom = useCallback(async (roomId: string) => {
-    if (client && roomId) {
+    if (client) {
       await client.socket.joinRoom(roomId);
     }
   }, [client]);
 
   const leaveRoom = useCallback(async (roomId: string) => {
-    if (client && roomId) {
+    if (client) {
       await client.socket.leaveRoom(roomId);
     }
   }, [client]);
 
-  const sendRoomMessage = useCallback(async (roomId: string | null, message: ExamSocketMessage) => {
-    if (client && roomId) {
-      const convertedMessage = {
-        ...message,
-        timestamp: Date.now(),
-        content: {
-          ...message.content,
-          userId: message.content.userId || SYSTEM_USER_ID
-        }
-      };
-      await client.socket.sendRoomMessage(roomId, convertedMessage as any);
+  const sendRoomMessage = useCallback(async (roomId: string, message: ExamSocketMessage) => {
+    if (client) {
+      await client.socket.sendRoomMessage(roomId, message as any);
     }
   }, [client]);
 
   const onRoomMessage = useCallback((callback: (data: ExamSocketRoomMessage) => void) => {
     if (client) {
-      client.socket.onRoomMessage((data: any) => {
-        const convertedMessage: ExamSocketRoomMessage = {
-          roomId: data.roomId,
-          message: {
-            ...data.message,
-            timestamp: Number(data.message.timestamp),
-            content: {
-              ...data.message.content,
-              price: parseFloat(String(data.message.content.price))
-            }
-          }
-        };
-        callback(convertedMessage);
-      });
+      client.socket.onRoomMessage(callback);
     }
   }, [client]);
 
-  const getAnswers = useCallback(async (roomId: string | null, questionId: string): Promise<Answer[]> => {
-    if (!client || !roomId) return [];
+  const getAnswers = useCallback(async (roomId: string, questionId: string): Promise<Answer[]> => {
+    if (!client) return [];
 
     return handleAsyncOperation(async () => {
       const answersData = await client.redis.hgetall(`quiz:${roomId}:answers:${questionId}`);
@@ -142,11 +115,13 @@ export function useExamSocketClient(roomId: string | null) {
     }, '获取答案失败');
   }, [client]);
 
-  const storeAnswer = useCallback(async (roomId: string | null, questionId: string, userId: string, answer: QuizContent) => {
-    if (!client || !roomId) return;
+  const storeAnswer = useCallback(async (roomId: string, questionId: string, userId: string, answer: QuizContent) => {
+    if (!client) return;
 
+    // 获取历史价格数据
     const pricesData = await client.redis.hgetall(`quiz:${roomId}:prices`);
     if (!pricesData) {
+      // 如果没有历史数据,存储初始答案
       const initialAnswer: Answer = {
         ...answer,
         userId,
@@ -164,11 +139,13 @@ export function useExamSocketClient(roomId: string | null) {
       return;
     }
 
+    // 获取该用户的所有历史答案
     const dates = Object.keys(pricesData).sort();
     const allAnswers = await Promise.all(
       dates.map(date => getAnswers(roomId, date))
     );
 
+    // 过滤出当前用户的答案并添加价格信息
     const userAnswers = allAnswers
       .flat()
       .filter((a: Answer) => a.userId === userId)
@@ -182,6 +159,7 @@ export function useExamSocketClient(roomId: string | null) {
       })
       .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime());
 
+    // 计算收益
     let totalProfitAmount = 0;
     let totalProfitPercent = 0;
     
@@ -197,6 +175,7 @@ export function useExamSocketClient(roomId: string | null) {
       totalProfitPercent = (prevAnswer.totalProfitPercent || 0) + profitPercent;
     }
 
+    // 存储带有收益信息的答案
     const answerWithProfit: Answer = {
       ...answer,
       userId,
@@ -215,8 +194,8 @@ export function useExamSocketClient(roomId: string | null) {
     }
   }, [client, getAnswers]);
 
-  const cleanupRoom = useCallback(async (roomId: string | null, questionId?: string) => {
-    if (!client || !roomId) return;
+  const cleanupRoom = useCallback(async (roomId: string, questionId?: string) => {
+    if (!client) return;
 
     await handleAsyncOperation(async () => {
       if (questionId) {
@@ -230,35 +209,37 @@ export function useExamSocketClient(roomId: string | null) {
     }, '清理房间数据失败');
   }, [client]);
 
-  const sendNextQuestion = useCallback(async (roomId: string | null, state: QuizState) => {
-    if (!client || !roomId) return;
+  const sendNextQuestion = useCallback(async (roomId: string, state: QuizState) => {
+    if (!client) return;
 
     return handleAsyncOperation(async () => {
-      const messageContent: QuizContent = {
-        date: state.date,
-        price: state.price,
-        holdingStock: DEFAULT_HOLDING_STOCK,
-        holdingCash: DEFAULT_HOLDING_CASH,
-        userId: SYSTEM_USER_ID
-      };
-
-      const message: ExamSocketMessage = {
+      const message = {
         type: 'question',
-        content: messageContent
+        content: {
+          date: state.date,
+          price: state.price
+        }
       };
 
-      await storeAnswer(roomId, 'current_state', SYSTEM_USER_ID, messageContent);
+      // 存储当前问题状态
+      await storeAnswer(roomId, 'current_state', 'system', {
+        date: state.date,
+        price: state.price
+      });
+
+      // 存储价格历史记录
       await client.redis.hset(
         `quiz:${roomId}:prices`,
         state.date,
         JSON.stringify({ price: state.price })
       );
+
       await sendRoomMessage(roomId, message);
     }, '发送题目失败');
   }, [client, sendRoomMessage, storeAnswer]);
 
-  const getCurrentQuestion = useCallback(async (roomId: string | null): Promise<QuizState | null> => {
-    if (!client || !roomId) return null;
+  const getCurrentQuestion = useCallback(async (roomId: string): Promise<QuizState | null> => {
+    if (!client) return null;
 
     return handleAsyncOperation(async () => {
       const answers = await getAnswers(roomId, 'current_state');
@@ -273,8 +254,9 @@ export function useExamSocketClient(roomId: string | null) {
     }, '获取当前题目状态失败');
   }, [client, getAnswers]);
 
-  const getPriceHistory = useCallback(async (roomId: string | null, date: string): Promise<string> => {
-    if (!client || !roomId) return '0';
+  // 添加获取历史价格的函数
+  const getPriceHistory = useCallback(async (roomId: string, date: string): Promise<string> => {
+    if (!client) return '0';
 
     return handleAsyncOperation(async () => {
       const priceData = await client.redis.hget(`quiz:${roomId}:prices`, date);
@@ -301,8 +283,8 @@ export function useExamSocketClient(roomId: string | null) {
 }
 
 // Socket Room Hook
-export function useExamSocketRoom(roomId: string | null) {
-  const socketClient = useExamSocketClient(roomId);
+export function useSocketRoom(roomId: string | null) {
+  const socketClient = useSocketClient(roomId);
 
   const { data: roomConnection } = useQuery({
     queryKey: ['socket-room', roomId],
@@ -324,9 +306,9 @@ export function useExamSocketRoom(roomId: string | null) {
   };
 }
 
-// 当前题目状态Hook
-export function useExamCurrentQuestion(roomId: string | null) {
-  const socketClient = useExamSocketClient(roomId);
+// 使用react-query管理当前题目状态
+export function useCurrentQuestion(roomId: string | null) {
+  const socketClient = useSocketClient(roomId);
   
   const { data: currentQuestion, refetch } = useQuery({
     queryKey: ['current-question', roomId],
@@ -344,9 +326,9 @@ export function useExamCurrentQuestion(roomId: string | null) {
   };
 }
 
-// 房间消息Hook
-export function useExamRoomMessages(roomId: string | null) {
-  const socketClient = useExamSocketClient(roomId);
+// 使用react-query管理房间消息
+export function useRoomMessages(roomId: string | null) {
+  const socketClient = useSocketClient(roomId);
   const queryClient = useQueryClient();
   const [lastMessage, setLastMessage] = useState<ExamSocketRoomMessage | null>(null);
 
@@ -357,6 +339,7 @@ export function useExamRoomMessages(roomId: string | null) {
       setLastMessage(data);
       const { type, content } = data.message;
 
+      // 处理不同类型的消息
       switch (type) {
         case 'question':
           queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
@@ -367,15 +350,18 @@ export function useExamRoomMessages(roomId: string | null) {
           break;
         case 'settlement':
         case 'submit':
+          // 刷新所有用户的答题历史
           queryClient.invalidateQueries({ 
             queryKey: ['user-answers'],
             predicate: (query) => query.queryKey[1] === roomId 
           });
+          // 刷新当前答案缓存
           queryClient.invalidateQueries({ 
             queryKey: ['answers', roomId] 
           });
           break;
         case 'restart':
+          // 重置所有相关查询
           queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
           queryClient.invalidateQueries({ queryKey: ['answers', roomId] });
           queryClient.invalidateQueries({ 
@@ -388,30 +374,58 @@ export function useExamRoomMessages(roomId: string | null) {
     };
 
     socketClient.onRoomMessage(handleMessage);
-    return () => {
-      socketClient.client?.socket.offRoomMessage(handleMessage);
-    };
   }, [roomId, socketClient, queryClient]);
 
   return lastMessage;
 }
 
-// 用户答案历史Hook
-export function useExamUserAnswerHistory(roomId: string | null, userId: string | null) {
-  const socketClient = useExamSocketClient(roomId);
+// 使用react-query管理答案缓存
+export function useAnswerCache(roomId: string | null, date: string | null) {
+  const socketClient = useSocketClient(roomId);
+
+  const { data: answers, refetch: refetchAnswers } = useQuery({
+    queryKey: ['answers', roomId, date],
+    queryFn: async () => {
+      if (!roomId || !date || !socketClient) return [];
+      const answers = await socketClient.getAnswers(roomId, date);
+      const priceData = await socketClient.client?.redis.hget(`quiz:${roomId}:prices`, date);
+      if (!priceData) return answers;
+
+      const { price } = JSON.parse(priceData);
+      return answers.map((answer: Answer) => ({
+        ...answer,
+        price
+      }));
+    },
+    enabled: !!roomId && !!date && !!socketClient,
+    staleTime: 0
+  });
+
+  return {
+    answers: answers || [],
+    refetchAnswers
+  };
+}
+
+// 使用react-query管理用户答案历史
+export function useUserAnswerHistory(roomId: string | null, userId: string | null) {
+  const socketClient = useSocketClient(roomId);
 
   const { data: userAnswers, refetch: refetchUserAnswers } = useQuery({
     queryKey: ['user-answers', roomId, userId],
     queryFn: async () => {
       if (!roomId || !userId || !socketClient) return [];
       
+      // 先获取所有价格记录
       const pricesData = await socketClient.client?.redis.hgetall(`quiz:${roomId}:prices`);
       if (!pricesData) return [];
       
+      // 获取所有日期的答案
       const allAnswers = await Promise.all(
         Object.keys(pricesData).map(date => socketClient.getAnswers(roomId, date))
       );
       
+      // 过滤出当前用户的答案并添加价格信息
       const userAnswersWithPrice = allAnswers
         .flat()
         .filter((answer: Answer) => answer.userId === userId)
@@ -423,8 +437,9 @@ export function useExamUserAnswerHistory(roomId: string | null, userId: string |
             price: priceData.price
           };
         })
-        .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime());
+        .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime()); // 按日期排序
 
+      // 计算每条记录的收益
       let totalProfitAmount = 0;
       let totalProfitPercent = 0;
       const answersWithProfit = userAnswersWithPrice.map((answer: Answer, index: number) => {
@@ -469,9 +484,9 @@ export function useExamUserAnswerHistory(roomId: string | null, userId: string |
   };
 }
 
-// 答案提交Hook
-export function useExamAnswerSubmission(roomId: string | null) {
-  const { client, sendRoomMessage, storeAnswer } = useExamSocketClient(roomId);
+// 使用react-query管理答案提交
+export function useAnswerSubmission(roomId: string | null) {
+  const { client, sendRoomMessage, storeAnswer } = useSocketClient(roomId);
   const queryClient = useQueryClient();
 
   const submitAnswer = useCallback(async (date: string, nickname: string, answer: any) => {
@@ -491,9 +506,157 @@ export function useExamAnswerSubmission(roomId: string | null) {
   return { submitAnswer };
 }
 
-// 题目管理Hook
-export function useExamQuestionManagement(roomId: string | null) {
-  const socketClient = useExamSocketClient(roomId);
+// 使用react-query管理答案提交到后端
+export function useAnswerManagement(roomId: string | null) {
+  const socketClient = useSocketClient(roomId);
+  const queryClient = useQueryClient();
+
+  // 添加自动结算函数
+  const autoSettlement = useCallback(async (date: string) => {
+    if (!socketClient?.client) return;
+
+    return handleAsyncOperation(async () => {
+      // 获取当前所有答案
+      const answers = await socketClient.getAnswers(roomId, date);
+      const currentPrice = answers[0]?.price;  // 使用当前价格作为结算价格
+      
+      if (!currentPrice) return;
+
+      // 找出所有持股的用户
+      const holdingStockUsers = answers.filter((answer: Answer) => answer.holdingStock === '1');
+      
+      // 为每个持股用户创建一个结算记录
+      await Promise.all(holdingStockUsers.map(async (answer: Answer) => {
+        const settlementAnswer = {
+          ...answer,
+          date,
+          holdingStock: '0',  // 清仓
+          holdingCash: '1',   // 全部持币
+          price: currentPrice,
+          userId: answer.userId,
+        };
+        
+        // 存储结算记录
+        await socketClient.storeAnswer(roomId, date, answer.userId, settlementAnswer);
+      }));
+
+      // 发送结算消息通知客户端刷新
+      await socketClient.sendRoomMessage(roomId, {
+        type: 'settlement',
+        content: {
+          date,
+          price: currentPrice
+        }
+      });
+
+      // 刷新当前页面的数据
+      queryClient.invalidateQueries({ queryKey: ['answers', roomId, date] });
+    }, '自动结算失败');
+  }, [roomId, socketClient, queryClient]);
+
+  const submitAnswersToBackend = useCallback(async (date: string) => {
+    if (!socketClient) return;
+
+    return handleAsyncOperation(async () => {
+      const allAnswers = await socketClient.getAnswers(roomId, date);
+      
+      // 检查是否还有持股的用户
+      const hasHoldingStock = allAnswers.some((answer: Answer) => answer.holdingStock === '1');
+      if (hasHoldingStock) {
+        throw new Error('还有用户持股中,请先进行结算');
+      }
+
+      const priceData = await socketClient.client?.redis.hget(`quiz:${roomId}:prices`, date);
+      const { price } = priceData ? JSON.parse(priceData) : { price: '0' };
+      
+      // 获取前一天的价格
+      const allPrices = await socketClient.client?.redis.hgetall(`quiz:${roomId}:prices`);
+      const dates = Object.keys(allPrices || {}).sort();
+      const currentDateIndex = dates.indexOf(date);
+      const prevPrice = currentDateIndex > 0 
+        ? JSON.parse(allPrices![dates[currentDateIndex - 1]]).price 
+        : price;
+
+      // 计算每个用户的收益
+      const answersWithProfit = allAnswers.map((answer: Answer) => ({
+        ...answer,
+        price,
+        profit: calculateProfit(parseFloat(price), parseFloat(prevPrice), answer.holdingStock || '0')
+      }));
+      
+      const response = await fetch('/api/v1/classroom-answers', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          classroom_no: roomId,
+          date,
+          answers: answersWithProfit
+        })
+      });
+
+      const data = await response.json();
+      if (!data.success) {
+        throw new Error(data.message || '提交失败');
+      }
+
+      // 发送收卷消息通知客户端
+      await socketClient.sendRoomMessage(roomId, {
+        type: 'submit',
+        content: {
+          date,
+          price
+        }
+      });
+
+      await socketClient.cleanupRoom(roomId, date);
+      return data;
+    }, '提交答案到后端失败');
+  }, [roomId, socketClient]);
+
+  const { data: results, refetch: refetchResults } = useQuery({
+    queryKey: ['training-results', roomId],
+    queryFn: async () => {
+      if (!roomId) return null;
+      const response = await fetch(`/api/v1/classroom-results?classroom_no=${roomId}`);
+      const data = await response.json();
+      if (!data.success) {
+        throw new Error(data.message || '获取结果失败');
+      }
+      return data.data;
+    },
+    enabled: false
+  });
+
+  const restartTraining = useCallback(async () => {
+    if (!socketClient) return;
+
+    return handleAsyncOperation(async () => {
+      await socketClient.cleanupRoom(roomId);
+      // 发送重开消息
+      await socketClient.sendRoomMessage(roomId, {
+        type: 'restart',
+        content: {}
+      });
+      queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
+      queryClient.invalidateQueries({ queryKey: ['answers', roomId] });
+      queryClient.invalidateQueries({ queryKey: ['training-results', roomId] });
+    }, '重启训练失败');
+  }, [roomId, socketClient, queryClient]);
+
+  return {
+    autoSettlement,  // 暴露结算函数
+    submitAnswersToBackend,
+    results,
+    refetchResults,
+    restartTraining
+  };
+}
+
+// 使用react-query管理题目发送 - 直接使用 useSocketClient 中的 sendNextQuestion
+export function useQuestionManagement(roomId: string | null) {
+  const socketClient = useSocketClient(roomId);
   const queryClient = useQueryClient();
 
   const sendNextQuestion = useCallback(async (state: QuizState) => {
@@ -508,4 +671,4 @@ export function useExamQuestionManagement(roomId: string | null) {
   return {
     sendNextQuestion
   };
-}
+} 

+ 2 - 10
client/mobile/components/Exam/types.ts

@@ -1,12 +1,4 @@
-// Socket消息类型定义
-interface BaseSocketMessage {
-  type: string;
-  content: any;
-  timestamp?: number;
-  sender?: string;
-}
-
-type SocketMessageType = string;
+import type { SocketMessage as BaseSocketMessage, SocketMessageType } from '@d8d-appcontainer/types';
 
 // 基础答题记录
 export interface AnswerRecord {
@@ -70,4 +62,4 @@ export interface CumulativeResult {
   userId: string;
   totalProfitAmount: number;
   totalProfitPercent: number;
-}
+} 

+ 1 - 0
版本迭代需求.md

@@ -1,5 +1,6 @@
 2025.05.15 0.1.1
 
+迁移入答题卡页面
 添加答题卡数据表迁移
 迁移入股票后端api
 添加股票日期备注表迁移