| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- import { useEffect, useState, useCallback } from 'react';
- import { useQuery, useQueryClient } from '@tanstack/react-query';
- import { io, Socket } from 'socket.io-client';
- import type {
- QuizContent,
- QuizState,
- ExamSocketMessage,
- ExamSocketRoomMessage,
- Answer,
- CumulativeResult
- } from '../types.ts';
- interface FullExamSocketMessage extends Omit<ExamSocketMessage, 'timestamp'> {
- id: string;
- from: string;
- timestamp: string;
- }
- import { useAuth } from "../../../hooks.tsx";
- // 工具函数:统一错误处理
- 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 };
- }
- // 提前声明函数
- function getAnswers(client: Socket | null, roomId: string, questionId: string): Promise<Answer[]> {
- if (!client) return Promise.resolve([]);
- return new Promise((resolve) => {
- client.emit('exam:getAnswers', { roomId, questionId }, (answers: Answer[]) => {
- resolve(answers || []);
- });
- });
- }
- export function useSocketClient(roomId: string | null) {
- const { token } = useAuth();
- const queryClient = useQueryClient();
- const [socket, setSocket] = useState<Socket | null>(null);
- const [currentQuestion, setCurrentQuestion] = useState<QuizState | null>(null);
- const [lastMessage, setLastMessage] = useState<ExamSocketRoomMessage | null>(null);
- const [userAnswers, setUserAnswers] = useState<Answer[]>([]);
- // 初始化socket连接
- const { data: client } = useQuery({
- queryKey: ['socket-client', token],
- queryFn: async () => {
- if (!token) return null;
- const newSocket = io('/', {
- path: '/socket.io',
- transports: ['websocket'],
- withCredentials: true,
- query: {
- socket_token: token
- },
- reconnection: true,
- reconnectionAttempts: 5,
- reconnectionDelay: 1000,
- });
- newSocket.on('connect', () => {
- console.log('Socket connected');
- });
- newSocket.on('disconnect', () => {
- console.log('Socket disconnected');
- });
- newSocket.on('error', (error) => {
- console.error('Socket error:', error);
- });
- setSocket(newSocket);
- return newSocket;
- },
- enabled: !!token && !!roomId,
- staleTime: Infinity,
- gcTime: 0,
- retry: 3,
- });
- // 加入房间
- const joinRoom = useCallback(async (roomId: string) => {
- if (client) {
- client.emit('exam:join', { roomId });
- }
- }, [client]);
- // 离开房间
- const leaveRoom = useCallback(async (roomId: string) => {
- if (client) {
- client.emit('exam:leave', { roomId });
- }
- }, [client]);
- // 发送房间消息
- const sendRoomMessage = useCallback(async (roomId: string, message: ExamSocketMessage) => {
- if (client) {
- client.emit('exam:message', { roomId, message });
- }
- }, [client]);
- // 监听房间消息
- const onRoomMessage = useCallback((callback: (data: ExamSocketRoomMessage) => void) => {
- if (client) {
- client.on('exam:message', (data) => {
- setLastMessage(data);
- callback(data);
- });
- }
- }, [client]);
- // 监听当前问题变化
- useEffect(() => {
- if (!client || !roomId) return;
- const handleQuestionUpdate = (question:QuizState ) => {
- setCurrentQuestion(question);
- };
- client.on('exam:question', handleQuestionUpdate);
- return () => {
- client.off('exam:question', handleQuestionUpdate);
- };
- }, [client, roomId]);
- // // 监听用户答案变化
- // useEffect(() => {
- // if (!client || !roomId) return;
- // const handleAnswersUpdate = async () => {
- // const answers = await getAnswers(client, roomId, 'current_state');
- // setUserAnswers(answers);
- // };
- // client.on('exam:answers', handleAnswersUpdate);
- // return () => {
- // client.off('exam:answers', handleAnswersUpdate);
- // };
- // }, [client, roomId]);
- // 存储答案
- const storeAnswer = useCallback(async (roomId: string, questionId: string, userId: string, answer: QuizContent, callback?: (success: boolean) => void) => {
- if (!client) return;
- return handleAsyncOperation(async () => {
- // 获取历史价格数据
- const pricesData = await new Promise<any>((resolve) => {
- client.emit('exam:getPrices', { roomId }, resolve);
- });
- if (!pricesData) {
- // 存储初始答案
- const initialAnswer: Answer = {
- ...answer,
- userId,
- holdingStock: '0',
- holdingCash: '0',
- profitAmount: 0,
- profitPercent: 0,
- totalProfitAmount: 0,
- totalProfitPercent: 0
- };
-
- client.emit('exam:storeAnswer', {
- roomId,
- questionId,
- userId,
- answer: initialAnswer
- }, (success: boolean) => {
- callback?.(success);
- });
- return;
- }
- // 获取该用户的所有历史答案
- const dates = Object.keys(pricesData).sort();
- const allUserAnswers = await getUserAnswers(roomId, userId);
- const userAnswers = allUserAnswers
- .filter((a: Answer) => dates.includes(a.date || ''))
- .map((a: Answer) => ({
- ...a,
- price: pricesData[a.date || '']?.price || '0'
- }))
- .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
- };
- client.emit('exam:storeAnswer', {
- roomId,
- questionId,
- userId,
- answer: answerWithProfit
- }, (success: boolean) => {
- callback?.(success);
-
- // 提交成功后获取最新回答记录
- if (success && roomId && answerWithProfit.userId) {
- setUserAnswers([...userAnswers, answerWithProfit])
- }
- });
- }, '存储答案失败');
- }, [client]);
- // 清理房间数据
- const cleanupRoom = useCallback(async (roomId: string, questionId?: string) => {
- if (!client) return;
- await handleAsyncOperation(async () => {
- if (questionId) {
- client.emit('exam:cleanup', { roomId, questionId });
- } else {
- client.emit('exam:cleanup', { roomId });
- }
- }, '清理房间数据失败');
- }, [client]);
- // 发送下一题
- const sendNextQuestion = useCallback(async (roomId: string, state: QuizState) => {
- if (!client) return;
- return handleAsyncOperation(async () => {
- const message: FullExamSocketMessage = {
- id: `question-${Date.now()}`,
- type: 'question',
- from: 'system',
- timestamp: Date.now().toString(),
- content: {
- date: state.date,
- price: state.price,
- holdingStock: '0',
- holdingCash: '0',
- userId: 'system'
- }
- };
- // 存储当前问题状态
- await storeAnswer(roomId, 'current_state', 'system', {
- date: state.date,
- price: state.price,
- holdingStock: '0',
- holdingCash: '0',
- userId: 'system'
- });
- // 存储价格历史记录
- client.emit('exam:storePrice', {
- roomId,
- date: state.date,
- price: state.price
- });
- await sendRoomMessage(roomId, message);
- }, '发送题目失败');
- }, [client, sendRoomMessage, storeAnswer]);
- // 获取历史价格
- const getPriceHistory = useCallback(async (roomId: string, date: string): Promise<string> => {
- if (!client) return '0';
- return handleAsyncOperation(async () => {
- return new Promise((resolve) => {
- client.emit('exam:getPrice', { roomId, date }, (price: string) => {
- resolve(price || '0');
- });
- });
- }, '获取历史价格失败');
- }, [client]);
- // 获取答案 (封装为useCallback)
- const getAnswersCallback = useCallback((roomId: string, questionId: string): Promise<Answer[]> => {
- if (!client) return Promise.resolve([]);
- return handleAsyncOperation(async () => {
- return getAnswers(client, roomId, questionId);
- }, '获取答案失败');
- }, [client]);
- // 清理socket连接
- useEffect(() => {
- return () => {
- if (socket) {
- socket.disconnect();
- }
- };
- }, [socket]);
- // 导出所有功能作为单个对象
- const socketRoom = {
- client,
- joinRoom,
- leaveRoom,
- sendRoomMessage,
- onRoomMessage
- };
- // 获取用户答案
- const getUserAnswers = useCallback(async (roomId: string, userId: string): Promise<Answer[]> => {
- if (!client || !roomId || !userId) return Promise.resolve([]);
- return handleAsyncOperation(async () => {
- return new Promise((resolve) => {
- client.emit('exam:getUserAnswers', { roomId, userId }, (answers: Answer[]) => {
- resolve(answers || []);
- });
- });
- }, '获取用户答案失败');
- }, [client]);
- const getCurrentQuestion = useCallback(async (roomId: string): Promise<QuizState> => {
- if (!client) return Promise.reject(new Error('Socket not connected'));
-
- return handleAsyncOperation(async () => {
- return new Promise((resolve, reject) => {
- client.emit('exam:currentQuestion', { roomId }, (question: QuizState) => {
- if (!question) {
- reject(new Error('No current question available'));
- return;
- }
- resolve(question);
- });
- });
- }, '获取当前问题失败');
- }, [client])
- const answerManagement = {
- storeAnswer,
- getAnswers: getAnswersCallback,
- cleanupRoom,
- sendNextQuestion,
- getPriceHistory,
- getUserAnswers,
- getCurrentQuestion
- };
- // 计算累计结果
- // const calculateCumulativeResults = useCallback((answers: Answer[]): CumulativeResult[] => {
- // const userResults = new Map<string, CumulativeResult>();
-
- // answers.forEach((answer) => {
- // const userId = answer.userId;
- // if (!userResults.has(userId)) {
- // userResults.set(userId, {
- // userId,
- // totalProfitAmount: answer.totalProfitAmount || 0,
- // totalProfitPercent: answer.totalProfitPercent || 0
- // });
- // }
- // });
- // return Array.from(userResults.values());
- // }, []);
- return {
- socketRoom,
- answerManagement,
- // calculateCumulativeResults,
- currentQuestion,
- setCurrentQuestion,
- lastMessage,
- userAnswers,
- // 兼容旧版导入
- ...socketRoom,
- ...answerManagement
- };
- }
|