瀏覽代碼

完成股票模块socket客户端集成

yourname 6 月之前
父節點
當前提交
64e61b9607
共有 3 個文件被更改,包括 168 次插入677 次删除
  1. 0 675
      client/stock/hooks/useSocketClient.ts
  2. 123 0
      client/stock/hooks/useStockSocketClient.ts
  3. 45 2
      client/stock/stock_app.tsx

+ 0 - 675
client/stock/hooks/useSocketClient.ts

@@ -1,675 +0,0 @@
-import { useEffect, useState, useCallback } from 'react';
-import { APIClient } from '@d8d-appcontainer/api';
-import { useQuery, useQueryClient } from '@tanstack/react-query';
-import type {
-  QuizContent,
-  QuizState,
-  ExamSocketMessage,
-  ExamSocketRoomMessage,
-  Answer
-} from '../types/exam.ts';
-
-interface LoaderData {
-  token: string;
-  serverUrl: string;
-}
-
-// 工具函数:统一错误处理
-const handleAsyncOperation = async <T>(
-  operation: () => Promise<T>,
-  errorMessage: string
-): Promise<T> => {
-  try {
-    return await operation();
-  } catch (error) {
-    console.error(`${errorMessage}:`, error);
-    throw error;
-  }
-};
-
-// 计算收益的辅助函数
-interface ProfitResult {
-  profitAmount: number;  // 金额收益
-  profitPercent: number; // 百分比收益
-}
-
-function calculateProfit(currentPrice: number, previousPrice: number, holdingStock: string): ProfitResult {
-  if (holdingStock === '1') {
-    const profitAmount = currentPrice - previousPrice;  // 金额收益
-    const profitPercent = ((currentPrice - previousPrice) / previousPrice) * 100;  // 百分比收益
-    return {
-      profitAmount,
-      profitPercent
-    };
-  }
-  return {
-    profitAmount: 0,
-    profitPercent: 0
-  };
-}
-
-// 使用react-query管理socket客户端
-export function useSocketClient(roomId: string | null) {
-  const { token, serverUrl } = {
-    token: '',
-    serverUrl: '/wss'
-  };
-
-  const { data: client } = useQuery({
-    queryKey: ['socket-client'],
-    queryFn: async () => {
-      if (!token || !serverUrl) return null;
-
-      const apiClient = new APIClient({
-        scope: 'user',
-        config: {
-          serverUrl,
-          type: 'socket',
-          token,
-        }
-      });
-
-      await apiClient.connect();
-      return apiClient;
-    },
-    enabled: !!token && !!serverUrl && !!roomId,
-    staleTime: Infinity,
-    retry: 3,
-    gcTime: 0
-  });
-
-  const joinRoom = useCallback(async (roomId: string) => {
-    if (client) {
-      await client.socket.joinRoom(roomId);
-    }
-  }, [client]);
-
-  const leaveRoom = useCallback(async (roomId: string) => {
-    if (client) {
-      await client.socket.leaveRoom(roomId);
-    }
-  }, [client]);
-
-  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(callback);
-    }
-  }, [client]);
-
-  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}`);
-      if (!answersData) return [];
-      
-      return Object.entries(answersData).map(([userId, data]) => ({
-        ...(JSON.parse(data) as QuizContent),
-        userId,
-      })) as Answer[];
-    }, '获取答案失败');
-  }, [client]);
-
-  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,
-        profitAmount: 0,
-        profitPercent: 0,
-        totalProfitAmount: 0,
-        totalProfitPercent: 0
-      };
-      
-      await client.redis.hset(
-        `quiz:${roomId}:answers:${questionId}`,
-        userId,
-        JSON.stringify(initialAnswer)
-      );
-      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)
-      .map((a: Answer) => {
-        if (!a.date) return a;
-        const priceData = JSON.parse(pricesData[a.date] || '{"price":"0"}');
-        return {
-          ...a,
-          price: priceData.price
-        };
-      })
-      .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime());
-
-    // 计算收益
-    let totalProfitAmount = 0;
-    let totalProfitPercent = 0;
-    
-    if (userAnswers.length > 0) {
-      const prevAnswer = userAnswers[userAnswers.length - 1];
-      const { profitAmount, profitPercent } = calculateProfit(
-        parseFloat(String(answer.price)),
-        parseFloat(String(prevAnswer.price)),
-        prevAnswer.holdingStock as string
-      );
-      
-      totalProfitAmount = (prevAnswer.totalProfitAmount || 0) + profitAmount;
-      totalProfitPercent = (prevAnswer.totalProfitPercent || 0) + profitPercent;
-    }
-
-    // 存储带有收益信息的答案
-    const answerWithProfit: Answer = {
-      ...answer,
-      userId,
-      profitAmount: userAnswers.length > 0 ? totalProfitAmount - (userAnswers[userAnswers.length - 1].totalProfitAmount || 0) : 0,
-      profitPercent: userAnswers.length > 0 ? totalProfitPercent - (userAnswers[userAnswers.length - 1].totalProfitPercent || 0) : 0,
-      totalProfitAmount,
-      totalProfitPercent
-    };
-
-    if (client?.redis) {
-      await client.redis.hset(
-        `quiz:${roomId}:answers:${questionId}`,
-        userId,
-        JSON.stringify(answerWithProfit)
-      );
-    }
-  }, [client, getAnswers]);
-
-  const cleanupRoom = useCallback(async (roomId: string, questionId?: string) => {
-    if (!client) return;
-
-    await handleAsyncOperation(async () => {
-      if (questionId) {
-        await client.redis.del(`quiz:${roomId}:answers:${questionId}`);
-      } else {
-        await Promise.all([
-          client.redis.delByPattern(`quiz:${roomId}:answers:*`),
-          client.redis.del(`quiz:${roomId}:prices`)
-        ]);
-      }
-    }, '清理房间数据失败');
-  }, [client]);
-
-  const sendNextQuestion = useCallback(async (roomId: string, state: QuizState) => {
-    if (!client) return;
-
-    return handleAsyncOperation(async () => {
-      const message = {
-        type: 'question',
-        content: {
-          date: state.date,
-          price: state.price
-        }
-      };
-
-      // 存储当前问题状态
-      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): Promise<QuizState | null> => {
-    if (!client) return null;
-
-    return handleAsyncOperation(async () => {
-      const answers = await getAnswers(roomId, 'current_state');
-      const currentState = answers[0];
-      if (currentState) {
-        return {
-          date: currentState.date || '',
-          price: currentState.price || '0'
-        };
-      }
-      return null;
-    }, '获取当前题目状态失败');
-  }, [client, getAnswers]);
-
-  // 添加获取历史价格的函数
-  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);
-      if (!priceData) return '0';
-      
-      const { price } = JSON.parse(priceData);
-      return String(price);
-    }, '获取历史价格失败');
-  }, [client]);
-
-  return {
-    client,
-    joinRoom,
-    leaveRoom,
-    sendRoomMessage,
-    onRoomMessage,
-    storeAnswer,
-    getAnswers,
-    cleanupRoom,
-    sendNextQuestion,
-    getCurrentQuestion,
-    getPriceHistory
-  };
-}
-
-// Socket Room Hook
-export function useSocketRoom(roomId: string | null) {
-  const socketClient = useSocketClient(roomId);
-
-  const { data: roomConnection } = useQuery({
-    queryKey: ['socket-room', roomId],
-    queryFn: async () => {
-      if (!roomId || !socketClient.client) return null;
-      await socketClient.joinRoom(roomId);
-      console.log(`Connected to room: ${roomId}`);
-      return { roomId, connected: true };
-    },
-    enabled: !!roomId && !!socketClient.client,
-    staleTime: Infinity,
-    gcTime: Infinity,
-    retry: false
-  });
-
-  return {
-    connected: roomConnection?.connected || false,
-    socketClient
-  };
-}
-
-// 使用react-query管理当前题目状态
-export function useCurrentQuestion(roomId: string | null) {
-  const socketClient = useSocketClient(roomId);
-  
-  const { data: currentQuestion, refetch } = useQuery({
-    queryKey: ['current-question', roomId],
-    queryFn: async () => {
-      if (!roomId || !socketClient) return null;
-      return socketClient.getCurrentQuestion(roomId);
-    },
-    enabled: !!roomId && !!socketClient,
-    staleTime: 0
-  });
-
-  return {
-    currentQuestion,
-    refetchQuestion: refetch
-  };
-}
-
-// 使用react-query管理房间消息
-export function useRoomMessages(roomId: string | null) {
-  const socketClient = useSocketClient(roomId);
-  const queryClient = useQueryClient();
-  const [lastMessage, setLastMessage] = useState<ExamSocketRoomMessage | null>(null);
-
-  useEffect(() => {
-    if (!roomId || !socketClient) return;
-
-    const handleMessage = (data: ExamSocketRoomMessage) => {
-      setLastMessage(data);
-      const { type, content } = data.message;
-
-      // 处理不同类型的消息
-      switch (type) {
-        case 'question':
-          queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
-          break;
-        case 'answer':
-          const { date } = content;
-          queryClient.invalidateQueries({ queryKey: ['answers', roomId, date] });
-          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({ 
-            queryKey: ['user-answers'],
-            predicate: (query) => query.queryKey[1] === roomId 
-          });
-          queryClient.invalidateQueries({ queryKey: ['training-results', roomId] });
-          break;
-      }
-    };
-
-    socketClient.onRoomMessage(handleMessage);
-  }, [roomId, socketClient, queryClient]);
-
-  return lastMessage;
-}
-
-// 使用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)
-        .map((answer: Answer) => {
-          if (!answer.date) return answer;
-          const priceData = JSON.parse(pricesData[answer.date] || '{"price":"0"}');
-          return {
-            ...answer,
-            price: priceData.price
-          };
-        })
-        .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) => {
-        if (index === 0) {
-          return { 
-            ...answer, 
-            profitAmount: 0,
-            profitPercent: 0,
-            totalProfitAmount: 0,
-            totalProfitPercent: 0
-          };
-        }
-
-        const prevAnswer = userAnswersWithPrice[index - 1];
-        const { profitAmount, profitPercent } = calculateProfit(
-          parseFloat(answer.price as string),
-          parseFloat(prevAnswer.price as string),
-          prevAnswer.holdingStock as string
-        );
-        
-        totalProfitAmount += profitAmount;
-        totalProfitPercent += profitPercent;
-
-        return {
-          ...answer,
-          profitAmount,
-          profitPercent,
-          totalProfitAmount,
-          totalProfitPercent
-        };
-      });
-
-      return answersWithProfit;
-    },
-    enabled: !!roomId && !!userId && !!socketClient,
-    staleTime: 0
-  });
-
-  return {
-    userAnswers: userAnswers || [],
-    refetchUserAnswers
-  };
-}
-
-// 使用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) => {
-    if (!client) return;
-
-    return handleAsyncOperation(async () => {
-      await storeAnswer(roomId, date, nickname, answer);
-      await sendRoomMessage(roomId, {
-        type: 'answer',
-        content: answer
-      });
-      queryClient.invalidateQueries({ queryKey: ['answers', roomId, date] });
-      queryClient.invalidateQueries({ queryKey: ['user-answers', roomId, nickname] });
-    }, '提交答案失败');
-  }, [roomId, client, storeAnswer, sendRoomMessage, queryClient]);
-
-  return { submitAnswer };
-}
-
-// 使用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) => {
-    if (!socketClient) return;
-
-    return handleAsyncOperation(async () => {
-      await socketClient.sendNextQuestion(roomId, state);
-      queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
-    }, '发送题目失败');
-  }, [roomId, socketClient, queryClient]);
-
-  return {
-    sendNextQuestion
-  };
-} 

+ 123 - 0
client/stock/hooks/useStockSocketClient.ts

@@ -0,0 +1,123 @@
+import { useEffect, useState, useCallback } from 'react';
+import { io, Socket } from 'socket.io-client';
+import { useAuth } from '../../mobile/hooks.tsx';
+
+interface StockPriceUpdate {
+  symbol: string;
+  price: number;
+  timestamp: Date;
+}
+
+interface ProfitSummary {
+  dailyStats: {
+    close: number;
+  };
+}
+
+interface StockSocketClient {
+  connect(): void;
+  disconnect(): void;
+  subscribe(symbol: string): void;
+  unsubscribe(symbol: string): void;
+  currentPrice: number | null;
+  lastUpdate: Date | null;
+  error: Error | null;
+  isConnected: boolean;
+}
+
+export function useStockSocket(): StockSocketClient {
+  const { token } = useAuth();
+  const [socket, setSocket] = useState<Socket | null>(null);
+  const [currentPrice, setCurrentPrice] = useState<number | null>(null);
+  const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
+  const [error, setError] = useState<Error | null>(null);
+  const [isConnected, setIsConnected] = useState(false);
+  const [currentDate, setCurrentDate] = useState<Date | null>(null);
+  const [profitSummary, setProfitSummary] = useState<ProfitSummary | null>(null);
+
+  // 初始化socket连接
+  useEffect(() => {
+    if (!token) return;
+
+    const newSocket = io('/', {
+      path: '/socket.io',
+      transports: ['websocket'],
+      query: { token },
+      reconnection: true,
+      reconnectionAttempts: 5,
+      reconnectionDelay: 1000,
+    });
+
+    newSocket.on('connect', () => {
+      console.log('Stock socket connected');
+      setIsConnected(true);
+    });
+
+    newSocket.on('disconnect', () => {
+      console.log('Stock socket disconnected');
+      setIsConnected(false);
+    });
+
+    newSocket.on('error', (err) => {
+      console.error('Stock socket error:', err);
+      setError(err);
+    });
+
+    // 监听股票价格更新
+    newSocket.on('stock:priceUpdate', (data: StockPriceUpdate) => {
+      setCurrentPrice(data.price);
+      setLastUpdate(new Date(data.timestamp));
+    });
+
+    // 监听当前日期更新
+    newSocket.on('stock:currentDate', (date: string) => {
+      setCurrentDate(new Date(date));
+    });
+
+    // 监听收益摘要更新
+    newSocket.on('stock:profitSummary', (summary: ProfitSummary) => {
+      setProfitSummary(summary);
+    });
+
+    setSocket(newSocket);
+
+    return () => {
+      newSocket.disconnect();
+    };
+  }, [token]);
+
+  const connect = useCallback(() => {
+    if (socket && !socket.connected) {
+      socket.connect();
+    }
+  }, [socket]);
+
+  const disconnect = useCallback(() => {
+    if (socket && socket.connected) {
+      socket.disconnect();
+    }
+  }, [socket]);
+
+  const subscribe = useCallback((symbol: string) => {
+    if (socket) {
+      socket.emit('stock:subscribe', { symbol });
+    }
+  }, [socket]);
+
+  const unsubscribe = useCallback((symbol: string) => {
+    if (socket) {
+      socket.emit('stock:unsubscribe', { symbol });
+    }
+  }, [socket]);
+
+  return {
+    connect,
+    disconnect,
+    subscribe,
+    unsubscribe,
+    currentPrice,
+    lastUpdate,
+    error,
+    isConnected,
+  };
+}

+ 45 - 2
client/stock/stock_app.tsx

@@ -1,7 +1,7 @@
 import React, { useRef, useState, useCallback, useEffect } from 'react';
 import { createRoot } from 'react-dom/client';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-// import { useSocketRoom, useQuestionManagement } from './hooks/useSocketClient.ts';
+import { useStockSocket } from './hooks/useStockSocketClient.ts';
 import { RouterProvider, createBrowserRouter, useSearchParams } from 'react-router';
 import { ToastContainer , toast} from 'react-toastify';
 import { StockChart, MemoToggle, TradePanel, useTradeRecords, useStockQueries, useProfitCalculator, ProfitDisplay, useStockDataFilter, DrawingToolbar } from './components/stock-chart/mod.ts';
@@ -12,8 +12,17 @@ const queryClient = new QueryClient();
 
 function StockApp() {
   const chartRef = useRef<StockChartRef>(null);
-//   const lastSentDateRef = useRef('');
   const [searchParams] = useSearchParams();
+  const {
+    connect,
+    disconnect,
+    subscribe,
+    unsubscribe,
+    currentPrice,
+    lastUpdate,
+    error,
+    isConnected
+  } = useStockSocket();
   const codeFromUrl = searchParams.get('code');
   const [stockCode, setStockCode] = useState(codeFromUrl || '001339' || undefined);//
 //   const classroom = searchParams.get('classroom');
@@ -95,6 +104,35 @@ function StockApp() {
     chartRef.current?.clearDrawings();
   }, []);
 
+  // 管理socket连接生命周期
+  useEffect(() => {
+    connect();
+    if (stockCode) {
+      subscribe(stockCode);
+    }
+
+    return () => {
+      if (stockCode) {
+        unsubscribe(stockCode);
+      }
+      disconnect();
+    };
+  }, [connect, disconnect, subscribe, unsubscribe, stockCode]);
+
+  // 同步socket数据到组件状态
+  useEffect(() => {
+    if (currentPrice && profitSummary) {
+      profitSummary.dailyStats.close = currentPrice;
+    }
+  }, [currentPrice, profitSummary]);
+
+  // 错误处理
+  useEffect(() => {
+    if (error) {
+      toast.error(`Socket错误: ${error.message}`);
+    }
+  }, [error]);
+
   useEffect(() => {
     const handleKeyPress = (event: KeyboardEvent) => {
       switch(event.key.toLowerCase()) {
@@ -141,6 +179,11 @@ function StockApp() {
 
   return (
     <div className="flex flex-col h-screen bg-gray-900">
+      {!isConnected && (
+        <div className="bg-yellow-600 text-white text-center py-1 text-sm">
+          正在尝试连接行情服务...
+        </div>
+      )}
       {/* 顶部行情和收益信息 */}
       <ProfitDisplay profitSummary={profitSummary} />