diff --git a/chuan-next/src/app/HomePage.tsx b/chuan-next/src/app/HomePage.tsx index 5dc0e74..99f1d28 100644 --- a/chuan-next/src/app/HomePage.tsx +++ b/chuan-next/src/app/HomePage.tsx @@ -10,7 +10,7 @@ import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer'; import DesktopShare from '@/components/DesktopShare'; import WeChatGroup from '@/components/WeChatGroup'; import { WebRTCUnsupportedModal } from '@/components/WebRTCUnsupportedModal'; -import { useWebRTCSupport } from '@/hooks/useWebRTCSupport'; +import { useWebRTCSupport } from '@/hooks/connection'; export default function HomePage() { const searchParams = useSearchParams(); diff --git a/chuan-next/src/components/ConnectionStatus.tsx b/chuan-next/src/components/ConnectionStatus.tsx index 8f644fb..ca0af37 100644 --- a/chuan-next/src/components/ConnectionStatus.tsx +++ b/chuan-next/src/components/ConnectionStatus.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useMemo } from 'react'; import { cn } from '@/lib/utils'; -import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore'; +import { useWebRTCStore } from '@/hooks/index'; interface ConnectionStatusProps { // 房间信息 - 只需要这个基本信息 diff --git a/chuan-next/src/components/DesktopShare.tsx b/chuan-next/src/components/DesktopShare.tsx index f05c697..e16725c 100644 --- a/chuan-next/src/components/DesktopShare.tsx +++ b/chuan-next/src/components/DesktopShare.tsx @@ -1,13 +1,12 @@ "use client"; -import React, { useState, useCallback, useEffect } from 'react'; -import { useSearchParams, useRouter } from 'next/navigation'; +import React, { useState, useCallback } from 'react'; +import { useURLHandler } from '@/hooks/ui'; import { Button } from '@/components/ui/button'; import { Share, Monitor } from 'lucide-react'; import WebRTCDesktopReceiver from '@/components/webrtc/WebRTCDesktopReceiver'; import WebRTCDesktopSender from '@/components/webrtc/WebRTCDesktopSender'; -import { ConnectionStatus } from '@/components/ConnectionStatus'; -import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore'; +import { useWebRTCStore } from '@/hooks/index'; interface DesktopShareProps { @@ -22,55 +21,28 @@ export default function DesktopShare({ onStopSharing, onJoinSharing }: DesktopShareProps) { - const searchParams = useSearchParams(); - const router = useRouter(); const [mode, setMode] = useState<'share' | 'view'>('share'); // 使用全局WebRTC状态 const webrtcState = useWebRTCStore(); - // 从URL参数中获取初始模式和房间代码 - useEffect(() => { - const urlMode = searchParams.get('mode'); - const type = searchParams.get('type'); - const urlCode = searchParams.get('code'); - - if (type === 'desktop' && urlMode) { - if (urlMode === 'send') { - setMode('share'); - } else if (urlMode === 'receive') { - setMode('view'); - // 如果URL中有房间代码,将在DesktopShareReceiver组件中自动加入 - } + // 使用统一的URL处理器,带模式转换 + const { updateMode, getCurrentRoomCode } = useURLHandler({ + featureType: 'desktop', + onModeChange: setMode, + onAutoJoinRoom: onJoinSharing, + modeConverter: { + fromURL: (urlMode) => urlMode === 'send' ? 'share' : 'view', + toURL: (componentMode) => componentMode === 'share' ? 'send' : 'receive' } - }, [searchParams]); - - // 更新URL参数 - const updateMode = useCallback((newMode: 'share' | 'view') => { - setMode(newMode); - const currentUrl = new URL(window.location.href); - currentUrl.searchParams.set('type', 'desktop'); - currentUrl.searchParams.set('mode', newMode === 'share' ? 'send' : 'receive'); - // 清除代码参数,避免模式切换时的混乱 - currentUrl.searchParams.delete('code'); - router.replace(currentUrl.pathname + currentUrl.search); - }, [router]); + }); // 获取初始房间代码(用于接收者模式) const getInitialCode = useCallback(() => { - const urlMode = searchParams.get('mode'); - const type = searchParams.get('type'); - const code = searchParams.get('code'); - console.log('[DesktopShare] getInitialCode 调用, URL参数:', { type, urlMode, code }); - - if (type === 'desktop' && urlMode === 'receive') { - const result = code || ''; - console.log('[DesktopShare] getInitialCode 返回:', result); - return result; - } - console.log('[DesktopShare] getInitialCode 返回空字符串'); - return ''; - }, [searchParams]); + const code = getCurrentRoomCode(); + console.log('[DesktopShare] getInitialCode 返回:', code); + return code; + }, [getCurrentRoomCode]); // 连接状态变化处理 - 现在不需要了,因为使用全局状态 const handleConnectionChange = useCallback((connection: any) => { diff --git a/chuan-next/src/components/WebRTCConnectionStatus.tsx b/chuan-next/src/components/WebRTCConnectionStatus.tsx index aed1ffc..1578be4 100644 --- a/chuan-next/src/components/WebRTCConnectionStatus.tsx +++ b/chuan-next/src/components/WebRTCConnectionStatus.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { AlertCircle, Wifi, WifiOff, Loader2, RotateCcw } from 'lucide-react'; -import { WebRTCConnection } from '@/hooks/webrtc/useSharedWebRTCManager'; +import { WebRTCConnection } from '@/hooks/connection/useSharedWebRTCManager'; interface Props { webrtc: WebRTCConnection; diff --git a/chuan-next/src/components/WebRTCFileTransfer.tsx b/chuan-next/src/components/WebRTCFileTransfer.tsx index 107313c..02e82ff 100644 --- a/chuan-next/src/components/WebRTCFileTransfer.tsx +++ b/chuan-next/src/components/WebRTCFileTransfer.tsx @@ -1,9 +1,9 @@ "use client"; import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; -import { useSearchParams, useRouter } from 'next/navigation'; -import { useSharedWebRTCManager } from '@/hooks/webrtc/useSharedWebRTCManager'; -import { useFileTransferBusiness } from '@/hooks/webrtc/useFileTransferBusiness'; +import { useSharedWebRTCManager, useConnectionState, useRoomConnection } from '@/hooks/connection'; +import { useFileTransferBusiness, useFileListSync, useFileStateManager } from '@/hooks/file-transfer'; +import { useURLHandler } from '@/hooks/ui'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/toast-simple'; import { Upload, Download } from 'lucide-react'; @@ -20,29 +20,20 @@ interface FileInfo { } export const WebRTCFileTransfer: React.FC = () => { - const searchParams = useSearchParams(); - const router = useRouter(); const { showToast } = useToast(); - // 独立的文件状态 - const [selectedFiles, setSelectedFiles] = useState([]); - const [fileList, setFileList] = useState([]); - const [downloadedFiles, setDownloadedFiles] = useState>(new Map()); + // 基础状态 + const [mode, setMode] = useState<'send' | 'receive'>('send'); + const [pickupCode, setPickupCode] = useState(''); const [currentTransferFile, setCurrentTransferFile] = useState<{ fileId: string; fileName: string; progress: number; } | null>(null); - // 房间状态 - const [pickupCode, setPickupCode] = useState(''); - const [mode, setMode] = useState<'send' | 'receive'>('send'); - const [hasProcessedInitialUrl, setHasProcessedInitialUrl] = useState(false); - const [isJoiningRoom, setIsJoiningRoom] = useState(false); // 添加加入房间状态 - const urlProcessedRef = useRef(false); // 使用 ref 防止重复处理 URL const fileInputRef = useRef(null); - // 创建共享连接 - 使用 useMemo 稳定引用 + // 创建共享连接 const connection = useSharedWebRTCManager(); const stableConnection = useMemo(() => connection, [connection.isConnected, connection.isConnecting, connection.isWebSocketConnected, connection.error]); @@ -63,268 +54,70 @@ export const WebRTCFileTransfer: React.FC = () => { onFileProgress } = useFileTransferBusiness(stableConnection); - // 加入房间 (接收模式) - 提前定义以供 useEffect 使用 + // 使用自定义 hooks + const { syncFileListToReceiver } = useFileListSync({ + sendFileList, + mode, + pickupCode, + isConnected, + isPeerConnected: connection.isPeerConnected, + getChannelState: connection.getChannelState + }); + + const { + selectedFiles, + setSelectedFiles, + fileList, + setFileList, + downloadedFiles, + setDownloadedFiles, + handleFileSelect, + clearFiles, + resetFiles, + updateFileStatus, + updateFileProgress + } = useFileStateManager({ + mode, + pickupCode, + syncFileListToReceiver, + isPeerConnected: connection.isPeerConnected + }); + + const { joinRoom: originalJoinRoom, isJoiningRoom } = useRoomConnection({ + connect, + isConnecting, + isConnected + }); + + // 包装joinRoom函数以便设置pickupCode const joinRoom = useCallback(async (code: string) => { - console.log('=== 加入房间 ==='); - console.log('取件码:', code); - - const trimmedCode = code.trim(); - - // 检查取件码格式 - if (!trimmedCode || trimmedCode.length !== 6) { - showToast('请输入正确的6位取件码', "error"); - return; - } + setPickupCode(code); + await originalJoinRoom(code); + }, [originalJoinRoom]); - // 防止重复调用 - 检查是否已经在连接或已连接 - if (isConnecting || isConnected || isJoiningRoom) { - console.log('已在连接中或已连接,跳过重复的房间状态检查'); - return; - } - - setIsJoiningRoom(true); - - try { - // 先检查房间状态 - console.log('检查房间状态...'); - - const response = await fetch(`/api/room-info?code=${trimmedCode}`); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: 无法检查房间状态`); - } - - const result = await response.json(); - - if (!result.success) { - let errorMessage = result.message || '房间不存在或已过期'; - if (result.message?.includes('expired')) { - errorMessage = '房间已过期,请联系发送方重新创建'; - } else if (result.message?.includes('not found')) { - errorMessage = '房间不存在,请检查取件码是否正确'; - } - showToast(errorMessage, "error"); - return; - } - - // 检查发送方是否在线 (使用新的字段名) - if (!result.sender_online) { - showToast('发送方不在线,请确认取件码是否正确或联系发送方', "error"); - return; - } - - console.log('房间状态检查通过,开始连接...'); - setPickupCode(trimmedCode); - - connect(trimmedCode, 'receiver'); - - showToast(`正在连接到房间: ${trimmedCode}`, "success"); - } catch (error) { - console.error('检查房间状态失败:', error); - let errorMessage = '检查房间状态失败'; - - if (error instanceof Error) { - if (error.message.includes('network') || error.message.includes('fetch')) { - errorMessage = '网络连接失败,请检查网络状况'; - } else if (error.message.includes('timeout')) { - errorMessage = '请求超时,请重试'; - } else if (error.message.includes('HTTP 404')) { - errorMessage = '房间不存在,请检查取件码'; - } else if (error.message.includes('HTTP 500')) { - errorMessage = '服务器错误,请稍后重试'; - } else { - errorMessage = error.message; - } - } - - showToast(errorMessage, "error"); - } finally { - setIsJoiningRoom(false); // 重置加入房间状态 - } - }, [isConnecting, isConnected, isJoiningRoom, showToast, connect]); // 添加isJoiningRoom依赖 + const { updateMode } = useURLHandler({ + featureType: 'webrtc', + onModeChange: setMode, + onAutoJoinRoom: joinRoom + }); - // 从URL参数中获取初始模式(仅在首次加载时处理) - useEffect(() => { - // 使用 ref 确保只处理一次,避免严格模式的重复调用 - if (urlProcessedRef.current) { - console.log('URL已处理过,跳过重复处理'); - return; - } - - const urlMode = searchParams.get('mode') as 'send' | 'receive'; - const type = searchParams.get('type'); - const code = searchParams.get('code'); - - // 只在首次加载且URL中有webrtc类型时处理 - if (!hasProcessedInitialUrl && type === 'webrtc' && urlMode && ['send', 'receive'].includes(urlMode)) { - console.log('=== 处理初始URL参数 ==='); - console.log('URL模式:', urlMode, '类型:', type, '取件码:', code); - - // 立即标记为已处理,防止重复 - urlProcessedRef.current = true; - - setMode(urlMode); - setHasProcessedInitialUrl(true); - - if (code && urlMode === 'receive') { - console.log('URL中有取件码,自动加入房间'); - // 防止重复调用 - 检查连接状态和加入房间状态 - if (!isConnecting && !isConnected && !isJoiningRoom) { - // 直接调用异步函数,不依赖 joinRoom - const autoJoinRoom = async () => { - const trimmedCode = code.trim(); - - if (!trimmedCode || trimmedCode.length !== 6) { - showToast('请输入正确的6位取件码', "error"); - return; - } - - setIsJoiningRoom(true); - - try { - console.log('检查房间状态...'); - const response = await fetch(`/api/room-info?code=${trimmedCode}`); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: 无法检查房间状态`); - } - - const result = await response.json(); - - if (!result.success) { - let errorMessage = result.message || '房间不存在或已过期'; - if (result.message?.includes('expired')) { - errorMessage = '房间已过期,请联系发送方重新创建'; - } else if (result.message?.includes('not found')) { - errorMessage = '房间不存在,请检查取件码是否正确'; - } - showToast(errorMessage, "error"); - return; - } - - if (!result.sender_online) { - showToast('发送方不在线,请确认取件码是否正确或联系发送方', "error"); - return; - } - - console.log('房间状态检查通过,开始连接...'); - setPickupCode(trimmedCode); - connect(trimmedCode, 'receiver'); - showToast(`正在连接到房间: ${trimmedCode}`, "success"); - } catch (error) { - console.error('检查房间状态失败:', error); - let errorMessage = '检查房间状态失败'; - - if (error instanceof Error) { - if (error.message.includes('network') || error.message.includes('fetch')) { - errorMessage = '网络连接失败,请检查网络状况'; - } else if (error.message.includes('timeout')) { - errorMessage = '请求超时,请重试'; - } else if (error.message.includes('HTTP 404')) { - errorMessage = '房间不存在,请检查取件码'; - } else if (error.message.includes('HTTP 500')) { - errorMessage = '服务器错误,请稍后重试'; - } else { - errorMessage = error.message; - } - } - - showToast(errorMessage, "error"); - } finally { - setIsJoiningRoom(false); - } - }; - - autoJoinRoom(); - } else { - console.log('已在连接中或加入房间中,跳过重复处理'); - } - } - } - }, [searchParams, hasProcessedInitialUrl, isConnecting, isConnected, isJoiningRoom, showToast, connect]); // 添加isJoiningRoom依赖 - - // 更新URL参数 - const updateMode = useCallback((newMode: 'send' | 'receive') => { - console.log('=== 手动切换模式 ==='); - console.log('新模式:', newMode); - - setMode(newMode); - const params = new URLSearchParams(searchParams.toString()); - params.set('type', 'webrtc'); - params.set('mode', newMode); - - // 如果切换到发送模式,移除code参数 - if (newMode === 'send') { - params.delete('code'); - } - - router.push(`?${params.toString()}`, { scroll: false }); - }, [searchParams, router]); + useConnectionState({ + isWebSocketConnected, + isConnected, + isConnecting, + error: error || '', + pickupCode, + fileListLength: fileList.length, + currentTransferFile, + setCurrentTransferFile, + updateFileListStatus: setFileList + }); // 生成文件ID const generateFileId = () => { return Date.now().toString(36) + Math.random().toString(36).substr(2); }; - // 文件列表同步防抖标志 - const syncTimeoutRef = useRef(null); - - // 统一的文件列表同步函数,带防抖功能 - const syncFileListToReceiver = useCallback((fileInfos: FileInfo[], reason: string) => { - // 只有在发送模式、连接已建立且有房间时才发送文件列表 - if (mode !== 'send' || !pickupCode || !isConnected || !connection.isPeerConnected) { - console.log('跳过文件列表同步:', { mode, pickupCode: !!pickupCode, isConnected, isPeerConnected: connection.isPeerConnected }); - return; - } - - // 清除之前的延时发送 - if (syncTimeoutRef.current) { - clearTimeout(syncTimeoutRef.current); - } - - // 延时发送,避免频繁发送 - syncTimeoutRef.current = setTimeout(() => { - if (connection.isPeerConnected && connection.getChannelState() === 'open') { - console.log(`发送文件列表到接收方 (${reason}):`, fileInfos.map(f => f.name)); - sendFileList(fileInfos); - } - }, 150); - }, [mode, pickupCode, isConnected, connection.isPeerConnected, connection.getChannelState, sendFileList]); - - // 清理防抖定时器 - useEffect(() => { - return () => { - if (syncTimeoutRef.current) { - clearTimeout(syncTimeoutRef.current); - } - }; - }, []); - - // 文件选择处理 - const handleFileSelect = (files: File[]) => { - console.log('=== 文件选择 ==='); - console.log('新文件:', files.map(f => f.name)); - - // 更新选中的文件 - setSelectedFiles(prev => [...prev, ...files]); - - // 创建对应的文件信息 - const newFileInfos: FileInfo[] = files.map(file => ({ - id: generateFileId(), - name: file.name, - size: file.size, - type: file.type, - status: 'ready', - progress: 0 - })); - - setFileList(prev => { - const updatedList = [...prev, ...newFileInfos]; - console.log('更新后的文件列表:', updatedList); - return updatedList; - }); - }; - // 创建房间 (发送模式) const generateCode = async () => { if (selectedFiles.length === 0) { @@ -390,23 +183,17 @@ export const WebRTCFileTransfer: React.FC = () => { // 清空状态 setPickupCode(''); - setFileList([]); - setDownloadedFiles(new Map()); + resetFiles(); - // 如果是接收模式,更新URL移除code参数 - if (mode === 'receive') { - const params = new URLSearchParams(searchParams.toString()); - params.delete('code'); - router.push(`?${params.toString()}`, { scroll: false }); - } + // 如果是接收模式,需要手动更新URL + // URL处理逻辑已经移到 hook 中 }; - // 处理文件列表更新 + // 处理文件列表更新 useEffect(() => { const cleanup = onFileListReceived((fileInfos: FileInfo[]) => { console.log('=== 收到文件列表更新 ==='); console.log('文件列表:', fileInfos); - console.log('当前模式:', mode); if (mode === 'receive') { setFileList(fileInfos); @@ -416,6 +203,99 @@ export const WebRTCFileTransfer: React.FC = () => { return cleanup; }, [onFileListReceived, mode]); + // 处理文件接收 + useEffect(() => { + const cleanup = onFileReceived((fileData: { id: string; file: File }) => { + console.log('=== 接收到文件 ==='); + console.log('文件:', fileData.file.name, 'ID:', fileData.id); + + // 更新下载的文件 + setDownloadedFiles(prev => new Map(prev.set(fileData.id, fileData.file))); + + // 更新文件状态 + updateFileStatus(fileData.id, 'completed', 100); + }); + + return cleanup; + }, [onFileReceived, updateFileStatus]); + + // 监听文件级别的进度更新 + useEffect(() => { + const cleanup = onFileProgress((progressInfo) => { + // 检查连接状态,如果连接断开则忽略进度更新 + if (!isConnected || error) { + console.log('连接已断开,忽略进度更新:', progressInfo.fileName); + return; + } + + console.log('=== 文件进度更新 ==='); + console.log('文件:', progressInfo.fileName, 'ID:', progressInfo.fileId, '进度:', progressInfo.progress); + + // 更新当前传输文件信息 + setCurrentTransferFile({ + fileId: progressInfo.fileId, + fileName: progressInfo.fileName, + progress: progressInfo.progress + }); + + // 更新文件进度 + updateFileProgress(progressInfo.fileId, progressInfo.fileName, progressInfo.progress); + + // 当传输完成时清理 + if (progressInfo.progress >= 100 && mode === 'send') { + setCurrentTransferFile(null); + } + }); + + return cleanup; + }, [onFileProgress, mode, isConnected, error, updateFileProgress]); + + // 处理文件请求(发送方监听) + useEffect(() => { + const cleanup = onFileRequested((fileId: string, fileName: string) => { + console.log('=== 收到文件请求 ==='); + console.log('文件:', fileName, 'ID:', fileId, '当前模式:', mode); + + if (mode === 'send') { + // 检查连接状态 + if (!isConnected || error) { + console.log('连接已断开,无法发送文件'); + showToast('连接已断开,无法发送文件', "error"); + return; + } + + // 在发送方的selectedFiles中查找对应文件 + const file = selectedFiles.find(f => f.name === fileName); + + if (!file) { + console.error('找不到匹配的文件:', fileName); + showToast(`无法找到文件: ${fileName}`, "error"); + return; + } + + console.log('找到匹配文件,开始发送:', file.name, 'ID:', fileId, '文件大小:', file.size); + + // 更新发送方文件状态为downloading + updateFileStatus(fileId, 'downloading', 0); + + // 发送文件 + try { + sendFile(file, fileId); + } catch (sendError) { + console.error('发送文件失败:', sendError); + showToast(`发送文件失败: ${fileName}`, "error"); + + // 重置文件状态 + updateFileStatus(fileId, 'ready', 0); + } + } else { + console.warn('接收模式下收到文件请求,忽略'); + } + }); + + return cleanup; + }, [onFileRequested, mode, selectedFiles, sendFile, isConnected, error, showToast, updateFileStatus]); + // 处理连接错误 const [lastError, setLastError] = useState(''); useEffect(() => { @@ -482,52 +362,6 @@ export const WebRTCFileTransfer: React.FC = () => { return cleanup; }, [onFileReceived]); - // 监听文件级别的进度更新 - useEffect(() => { - const cleanup = onFileProgress((progressInfo) => { - // 检查连接状态,如果连接断开则忽略进度更新 - if (!isConnected || error) { - console.log('连接已断开,忽略进度更新:', progressInfo.fileName); - return; - } - - console.log('=== 文件进度更新 ==='); - console.log('文件:', progressInfo.fileName, 'ID:', progressInfo.fileId, '进度:', progressInfo.progress); - - // 更新当前传输文件信息 - setCurrentTransferFile({ - fileId: progressInfo.fileId, - fileName: progressInfo.fileName, - progress: progressInfo.progress - }); - - // 更新文件列表中对应文件的进度 - setFileList(prev => prev.map(item => { - if (item.id === progressInfo.fileId || item.name === progressInfo.fileName) { - const newProgress = progressInfo.progress; - const newStatus = newProgress >= 100 ? 'completed' as const : 'downloading' as const; - - console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${newProgress}`); - return { ...item, progress: newProgress, status: newStatus }; - } - return item; - })); - - // 当传输完成时显示提示 - if (progressInfo.progress >= 100 && mode === 'send') { - // 移除不必要的Toast - 传输完成状态在UI中已经显示 - setCurrentTransferFile(null); - } - }); - - return cleanup; - }, [onFileProgress, mode, isConnected, error]); - - // 实时更新传输进度(旧逻辑 - 删除) - // useEffect(() => { - // ...已删除的旧代码... - // }, [...]); - // 处理文件请求(发送方监听) useEffect(() => { const cleanup = onFileRequested((fileId: string, fileName: string) => { @@ -792,13 +626,6 @@ export const WebRTCFileTransfer: React.FC = () => { fileInputRef.current?.click(); }; - // 清空文件 - const clearFiles = () => { - console.log('=== 清空文件 ==='); - setSelectedFiles([]); - setFileList([]); - }; - // 下载文件到本地 const downloadFile = (fileId: string) => { const file = downloadedFiles.get(fileId); diff --git a/chuan-next/src/components/WebRTCTextImageTransfer.tsx b/chuan-next/src/components/WebRTCTextImageTransfer.tsx index 57be875..ec6a9db 100644 --- a/chuan-next/src/components/WebRTCTextImageTransfer.tsx +++ b/chuan-next/src/components/WebRTCTextImageTransfer.tsx @@ -1,68 +1,34 @@ "use client"; -import React, { useState, useEffect, useCallback } from 'react'; -import { useSearchParams, useRouter } from 'next/navigation'; -import { Button } from '@/components/ui/button'; -import { Send, Download, X } from 'lucide-react'; +import React, { useState, useCallback } from 'react'; +import { useURLHandler } from '@/hooks/ui'; +import { useWebRTCStore } from '@/hooks/ui/webRTCStore'; import { WebRTCTextSender } from '@/components/webrtc/WebRTCTextSender'; import { WebRTCTextReceiver } from '@/components/webrtc/WebRTCTextReceiver'; -import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore'; +import { Button } from '@/components/ui/button'; +import { MessageSquare, Send, Download, X } from 'lucide-react'; export const WebRTCTextImageTransfer: React.FC = () => { - const searchParams = useSearchParams(); - const router = useRouter(); - // 状态管理 const [mode, setMode] = useState<'send' | 'receive'>('send'); - const [hasProcessedInitialUrl, setHasProcessedInitialUrl] = useState(false); const [previewImage, setPreviewImage] = useState(null); // 使用全局WebRTC状态 const webrtcState = useWebRTCStore(); - // 从URL参数中获取初始模式 - useEffect(() => { - const urlMode = searchParams.get('mode') as 'send' | 'receive'; - const type = searchParams.get('type'); - const code = searchParams.get('code'); - - if (!hasProcessedInitialUrl && type === 'message' && urlMode && ['send', 'receive'].includes(urlMode)) { - console.log('=== 处理初始URL参数 ==='); - console.log('URL模式:', urlMode, '类型:', type, '取件码:', code); - - setMode(urlMode); - setHasProcessedInitialUrl(true); - } - }, [searchParams, hasProcessedInitialUrl]); - - // 更新URL参数 - const updateMode = useCallback((newMode: 'send' | 'receive') => { - console.log('=== 切换模式 ===', newMode); - - setMode(newMode); - const params = new URLSearchParams(searchParams.toString()); - params.set('type', 'message'); - params.set('mode', newMode); - - if (newMode === 'send') { - params.delete('code'); - } - - router.push(`?${params.toString()}`, { scroll: false }); - }, [searchParams, router]); + // 使用统一的URL处理器 + const { updateMode, getCurrentRoomCode, clearURLParams } = useURLHandler({ + featureType: 'message', + onModeChange: setMode + }); // 重新开始函数 const handleRestart = useCallback(() => { setPreviewImage(null); - - const params = new URLSearchParams(searchParams.toString()); - params.set('type', 'message'); - params.set('mode', mode); - params.delete('code'); - router.push(`?${params.toString()}`, { scroll: false }); - }, [searchParams, mode, router]); + clearURLParams(); + }, [clearURLParams]); - const code = searchParams.get('code') || ''; + const code = getCurrentRoomCode(); // 连接状态变化处理 - 现在不需要了,因为使用全局状态 const handleConnectionChange = useCallback((connection: any) => { diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx index 152772e..18fd5ff 100644 --- a/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx +++ b/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx @@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Monitor, Square } from 'lucide-react'; import { useToast } from '@/components/ui/toast-simple'; -import { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness'; +import { useDesktopShareBusiness } from '@/hooks/desktop-share'; import DesktopViewer from '@/components/DesktopViewer'; import { ConnectionStatus } from '@/components/ConnectionStatus'; @@ -19,7 +19,6 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec const [inputCode, setInputCode] = useState(initialCode || ''); const [isLoading, setIsLoading] = useState(false); const [isJoiningRoom, setIsJoiningRoom] = useState(false); // 添加加入房间状态 - const [showDebug, setShowDebug] = useState(false); const hasTriedAutoJoin = React.useRef(false); // 添加 ref 来跟踪是否已尝试自动加入 const { showToast } = useToast(); diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx index 4742b37..5db23aa 100644 --- a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx +++ b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx @@ -2,9 +2,9 @@ import React, { useState, useCallback, useEffect } from 'react'; import { Button } from '@/components/ui/button'; -import { Share, Monitor, Copy, Play, Square, Repeat, Users, Wifi, WifiOff } from 'lucide-react'; +import { Share, Monitor, Play, Square, Repeat } from 'lucide-react'; import { useToast } from '@/components/ui/toast-simple'; -import { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness'; +import { useDesktopShareBusiness } from '@/hooks/desktop-share'; import RoomInfoDisplay from '@/components/RoomInfoDisplay'; import { ConnectionStatus } from '@/components/ConnectionStatus'; diff --git a/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx b/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx index 709a107..03c731a 100644 --- a/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx +++ b/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx @@ -1,9 +1,9 @@ "use client"; import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { useSharedWebRTCManager } from '@/hooks/webrtc/useSharedWebRTCManager'; -import { useTextTransferBusiness } from '@/hooks/webrtc/useTextTransferBusiness'; -import { useFileTransferBusiness } from '@/hooks/webrtc/useFileTransferBusiness'; +import { useSharedWebRTCManager } from '@/hooks/connection'; +import { useTextTransferBusiness } from '@/hooks/text-transfer'; +import { useFileTransferBusiness } from '@/hooks/file-transfer'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useToast } from '@/components/ui/toast-simple'; diff --git a/chuan-next/src/components/webrtc/WebRTCTextSender.tsx b/chuan-next/src/components/webrtc/WebRTCTextSender.tsx index 0db7382..a55fe02 100644 --- a/chuan-next/src/components/webrtc/WebRTCTextSender.tsx +++ b/chuan-next/src/components/webrtc/WebRTCTextSender.tsx @@ -1,9 +1,9 @@ "use client"; import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { useSharedWebRTCManager } from '@/hooks/webrtc/useSharedWebRTCManager'; -import { useTextTransferBusiness } from '@/hooks/webrtc/useTextTransferBusiness'; -import { useFileTransferBusiness } from '@/hooks/webrtc/useFileTransferBusiness'; +import { useSharedWebRTCManager } from '@/hooks/connection'; +import { useTextTransferBusiness } from '@/hooks/text-transfer'; +import { useFileTransferBusiness } from '@/hooks/file-transfer'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/toast-simple'; import { MessageSquare, Image, Send, Copy } from 'lucide-react'; diff --git a/chuan-next/src/hooks/connection/index.ts b/chuan-next/src/hooks/connection/index.ts new file mode 100644 index 0000000..9d79b6a --- /dev/null +++ b/chuan-next/src/hooks/connection/index.ts @@ -0,0 +1,6 @@ +// 连接相关的hooks +export { useConnectionState } from './useConnectionState'; +export { useRoomConnection } from './useRoomConnection'; +export { useSharedWebRTCManager } from './useSharedWebRTCManager'; +export { useWebRTCManager } from './useWebRTCManager'; +export { useWebRTCSupport } from './useWebRTCSupport'; diff --git a/chuan-next/src/hooks/connection/useConnectionState.ts b/chuan-next/src/hooks/connection/useConnectionState.ts new file mode 100644 index 0000000..e8182de --- /dev/null +++ b/chuan-next/src/hooks/connection/useConnectionState.ts @@ -0,0 +1,137 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useToast } from '@/components/ui/toast-simple'; + +interface UseConnectionStateProps { + isWebSocketConnected: boolean; + isConnected: boolean; + isConnecting: boolean; + error: string; + pickupCode: string; + fileListLength: number; + currentTransferFile: any; + setCurrentTransferFile: (file: any) => void; + updateFileListStatus: (callback: (prev: any[]) => any[]) => void; +} + +export const useConnectionState = ({ + isWebSocketConnected, + isConnected, + isConnecting, + error, + pickupCode, + fileListLength, + currentTransferFile, + setCurrentTransferFile, + updateFileListStatus +}: UseConnectionStateProps) => { + const { showToast } = useToast(); + const [lastError, setLastError] = useState(''); + + // 处理连接错误 + useEffect(() => { + if (error && error !== lastError) { + console.log('=== 连接错误处理 ==='); + console.log('错误信息:', error); + + // 根据错误类型显示不同的提示 + let errorMessage = error; + + if (error.includes('WebSocket')) { + errorMessage = '服务器连接失败,请检查网络连接或稍后重试'; + } else if (error.includes('数据通道')) { + errorMessage = '数据通道连接失败,请重新尝试连接'; + } else if (error.includes('连接超时')) { + errorMessage = '连接超时,请检查网络状况或重新尝试'; + } else if (error.includes('连接失败')) { + errorMessage = 'WebRTC连接失败,可能是网络环境限制,请尝试刷新页面'; + } else if (error.includes('信令错误')) { + errorMessage = '信令服务器错误,请稍后重试'; + } else if (error.includes('创建连接失败')) { + errorMessage = '无法建立P2P连接,请检查网络设置'; + } + + // 显示错误提示 + showToast(errorMessage, "error"); + setLastError(error); + + // 如果是严重连接错误,清理传输状态 + if (error.includes('连接失败') || error.includes('数据通道连接失败') || error.includes('WebSocket')) { + console.log('严重连接错误,清理传输状态'); + setCurrentTransferFile(null); + + // 重置所有正在传输的文件状态 + updateFileListStatus((prev: any[]) => prev.map(item => + item.status === 'downloading' + ? { ...item, status: 'ready' as const, progress: 0 } + : item + )); + } + } + }, [error, lastError, showToast, setCurrentTransferFile, updateFileListStatus]); + + // 监听连接状态变化和清理传输状态 + useEffect(() => { + console.log('=== 连接状态变化 ==='); + console.log('WebSocket连接状态:', isWebSocketConnected); + console.log('WebRTC连接状态:', isConnected); + console.log('连接中状态:', isConnecting); + + // 当连接断开或有错误时,清理所有传输状态 + const shouldCleanup = (!isWebSocketConnected && !isConnected && !isConnecting && pickupCode) || + ((!isConnected && !isConnecting) || error); + + if (shouldCleanup) { + const hasCurrentTransfer = !!currentTransferFile; + const hasFileList = fileListLength > 0; + + // 只有在之前有连接活动时才显示断开提示和清理状态 + if (hasFileList || hasCurrentTransfer) { + if (!isWebSocketConnected && pickupCode) { + showToast('与服务器的连接已断开,请重新连接', "error"); + } + + console.log('连接断开,清理传输状态'); + + if (currentTransferFile) { + setCurrentTransferFile(null); + } + + // 重置所有正在下载的文件状态 + updateFileListStatus((prev: any[]) => { + const hasDownloadingFiles = prev.some(item => item.status === 'downloading'); + if (hasDownloadingFiles) { + console.log('重置正在传输的文件状态'); + return prev.map(item => + item.status === 'downloading' + ? { ...item, status: 'ready' as const, progress: 0 } + : item + ); + } + return prev; + }); + } + } + + // WebSocket连接成功时的提示 + if (isWebSocketConnected && isConnecting && !isConnected) { + console.log('WebSocket已连接,正在建立P2P连接...'); + } + + }, [isWebSocketConnected, isConnected, isConnecting, pickupCode, error, showToast, currentTransferFile, fileListLength, setCurrentTransferFile, updateFileListStatus]); + + // 监听连接状态变化并提供日志 + useEffect(() => { + console.log('=== WebRTC连接状态变化 ==='); + console.log('连接状态:', { + isConnected, + isConnecting, + isWebSocketConnected, + pickupCode, + fileListLength + }); + }, [isConnected, isConnecting, isWebSocketConnected, pickupCode, fileListLength]); + + return { + lastError + }; +}; diff --git a/chuan-next/src/hooks/connection/useRoomConnection.ts b/chuan-next/src/hooks/connection/useRoomConnection.ts new file mode 100644 index 0000000..1fa77db --- /dev/null +++ b/chuan-next/src/hooks/connection/useRoomConnection.ts @@ -0,0 +1,103 @@ +import { useState, useCallback } from 'react'; +import { useToast } from '@/components/ui/toast-simple'; + +interface UseRoomConnectionProps { + connect: (code: string, role: 'sender' | 'receiver') => void; + isConnecting: boolean; + isConnected: boolean; +} + +export const useRoomConnection = ({ connect, isConnecting, isConnected }: UseRoomConnectionProps) => { + const { showToast } = useToast(); + const [isJoiningRoom, setIsJoiningRoom] = useState(false); + + const validateRoomCode = (code: string): string | null => { + const trimmedCode = code.trim(); + if (!trimmedCode || trimmedCode.length !== 6) { + return '请输入正确的6位取件码'; + } + return null; + }; + + const checkRoomStatus = async (code: string) => { + const response = await fetch(`/api/room-info?code=${code}`); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: 无法检查房间状态`); + } + + const result = await response.json(); + + if (!result.success) { + let errorMessage = result.message || '房间不存在或已过期'; + if (result.message?.includes('expired')) { + errorMessage = '房间已过期,请联系发送方重新创建'; + } else if (result.message?.includes('not found')) { + errorMessage = '房间不存在,请检查取件码是否正确'; + } + throw new Error(errorMessage); + } + + if (!result.sender_online) { + throw new Error('发送方不在线,请确认取件码是否正确或联系发送方'); + } + + return result; + }; + + const handleNetworkError = (error: Error): string => { + if (error.message.includes('network') || error.message.includes('fetch')) { + return '网络连接失败,请检查网络状况'; + } else if (error.message.includes('timeout')) { + return '请求超时,请重试'; + } else if (error.message.includes('HTTP 404')) { + return '房间不存在,请检查取件码'; + } else if (error.message.includes('HTTP 500')) { + return '服务器错误,请稍后重试'; + } else { + return error.message; + } + }; + + // 加入房间 (接收模式) + const joinRoom = useCallback(async (code: string) => { + console.log('=== 加入房间 ==='); + console.log('取件码:', code); + + // 验证输入 + const validationError = validateRoomCode(code); + if (validationError) { + showToast(validationError, "error"); + return; + } + + // 防止重复调用 + if (isConnecting || isConnected || isJoiningRoom) { + console.log('已在连接中或已连接,跳过重复的房间状态检查'); + return; + } + + setIsJoiningRoom(true); + + try { + console.log('检查房间状态...'); + await checkRoomStatus(code.trim()); + + console.log('房间状态检查通过,开始连接...'); + connect(code.trim(), 'receiver'); + showToast(`正在连接到房间: ${code.trim()}`, "success"); + + } catch (error) { + console.error('检查房间状态失败:', error); + const errorMessage = error instanceof Error ? handleNetworkError(error) : '检查房间状态失败'; + showToast(errorMessage, "error"); + } finally { + setIsJoiningRoom(false); + } + }, [isConnecting, isConnected, isJoiningRoom, showToast, connect]); + + return { + joinRoom, + isJoiningRoom + }; +}; diff --git a/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts b/chuan-next/src/hooks/connection/useSharedWebRTCManager.ts similarity index 99% rename from chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts rename to chuan-next/src/hooks/connection/useSharedWebRTCManager.ts index a67a878..c83514e 100644 --- a/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts +++ b/chuan-next/src/hooks/connection/useSharedWebRTCManager.ts @@ -1,6 +1,6 @@ import { useState, useRef, useCallback } from 'react'; import { getWsUrl } from '@/lib/config'; -import { useWebRTCStore } from './webRTCStore'; +import { useWebRTCStore } from '../ui/webRTCStore'; // 基础连接状态 interface WebRTCState { diff --git a/chuan-next/src/hooks/webrtc/useWebRTCManager.ts b/chuan-next/src/hooks/connection/useWebRTCManager.ts similarity index 98% rename from chuan-next/src/hooks/webrtc/useWebRTCManager.ts rename to chuan-next/src/hooks/connection/useWebRTCManager.ts index c710b2f..336e34b 100644 --- a/chuan-next/src/hooks/webrtc/useWebRTCManager.ts +++ b/chuan-next/src/hooks/connection/useWebRTCManager.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { WebRTCManager } from './core/WebRTCManager'; -import { WebRTCConnectionState, WebRTCMessage, MessageHandler, DataHandler } from './core/types'; +import { WebRTCManager } from '../webrtc/WebRTCManager'; +import { WebRTCConnectionState, WebRTCMessage, MessageHandler, DataHandler } from '../webrtc/types'; import { WebRTCConnection } from './useSharedWebRTCManager'; interface WebRTCManagerConfig { diff --git a/chuan-next/src/hooks/useWebRTCSupport.ts b/chuan-next/src/hooks/connection/useWebRTCSupport.ts similarity index 100% rename from chuan-next/src/hooks/useWebRTCSupport.ts rename to chuan-next/src/hooks/connection/useWebRTCSupport.ts diff --git a/chuan-next/src/hooks/desktop-share/index.ts b/chuan-next/src/hooks/desktop-share/index.ts new file mode 100644 index 0000000..2353600 --- /dev/null +++ b/chuan-next/src/hooks/desktop-share/index.ts @@ -0,0 +1,2 @@ +// 桌面共享相关的hooks +export { useDesktopShareBusiness } from './useDesktopShareBusiness'; diff --git a/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts b/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts similarity index 99% rename from chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts rename to chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts index 638153f..40be18f 100644 --- a/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts +++ b/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts @@ -1,5 +1,5 @@ import { useState, useRef, useCallback, useEffect } from 'react'; -import { useSharedWebRTCManager } from './useSharedWebRTCManager'; +import { useSharedWebRTCManager } from '../connection/useSharedWebRTCManager'; interface DesktopShareState { isSharing: boolean; diff --git a/chuan-next/src/hooks/file-transfer/index.ts b/chuan-next/src/hooks/file-transfer/index.ts new file mode 100644 index 0000000..f2b1252 --- /dev/null +++ b/chuan-next/src/hooks/file-transfer/index.ts @@ -0,0 +1,4 @@ +// 文件传输相关的hooks +export { useFileTransferBusiness } from './useFileTransferBusiness'; +export { useFileStateManager } from './useFileStateManager'; +export { useFileListSync } from './useFileListSync'; diff --git a/chuan-next/src/hooks/file-transfer/useFileListSync.ts b/chuan-next/src/hooks/file-transfer/useFileListSync.ts new file mode 100644 index 0000000..6e9edc5 --- /dev/null +++ b/chuan-next/src/hooks/file-transfer/useFileListSync.ts @@ -0,0 +1,65 @@ +import { useRef, useCallback, useEffect } from 'react'; + +interface FileInfo { + id: string; + name: string; + size: number; + type: string; + status: 'ready' | 'downloading' | 'completed'; + progress: number; +} + +interface UseFileListSyncProps { + sendFileList: (fileInfos: FileInfo[]) => void; + mode: 'send' | 'receive'; + pickupCode: string; + isConnected: boolean; + isPeerConnected: boolean; + getChannelState: () => string; +} + +export const useFileListSync = ({ + sendFileList, + mode, + pickupCode, + isConnected, + isPeerConnected, + getChannelState +}: UseFileListSyncProps) => { + const syncTimeoutRef = useRef(null); + + // 统一的文件列表同步函数,带防抖功能 + const syncFileListToReceiver = useCallback((fileInfos: FileInfo[], reason: string) => { + // 只有在发送模式、连接已建立且有房间时才发送文件列表 + if (mode !== 'send' || !pickupCode || !isConnected || !isPeerConnected) { + console.log('跳过文件列表同步:', { mode, pickupCode: !!pickupCode, isConnected, isPeerConnected }); + return; + } + + // 清除之前的延时发送 + if (syncTimeoutRef.current) { + clearTimeout(syncTimeoutRef.current); + } + + // 延时发送,避免频繁发送 + syncTimeoutRef.current = setTimeout(() => { + if (isPeerConnected && getChannelState() === 'open') { + console.log(`发送文件列表到接收方 (${reason}):`, fileInfos.map(f => f.name)); + sendFileList(fileInfos); + } + }, 150); + }, [mode, pickupCode, isConnected, isPeerConnected, getChannelState, sendFileList]); + + // 清理防抖定时器 + useEffect(() => { + return () => { + if (syncTimeoutRef.current) { + clearTimeout(syncTimeoutRef.current); + } + }; + }, []); + + return { + syncFileListToReceiver + }; +}; diff --git a/chuan-next/src/hooks/file-transfer/useFileStateManager.ts b/chuan-next/src/hooks/file-transfer/useFileStateManager.ts new file mode 100644 index 0000000..56e325a --- /dev/null +++ b/chuan-next/src/hooks/file-transfer/useFileStateManager.ts @@ -0,0 +1,166 @@ +import { useState, useCallback, useEffect } from 'react'; + +interface FileInfo { + id: string; + name: string; + size: number; + type: string; + status: 'ready' | 'downloading' | 'completed'; + progress: number; +} + +interface UseFileStateManagerProps { + mode: 'send' | 'receive'; + pickupCode: string; + syncFileListToReceiver: (fileInfos: FileInfo[], reason: string) => void; + isPeerConnected: boolean; +} + +export const useFileStateManager = ({ + mode, + pickupCode, + syncFileListToReceiver, + isPeerConnected +}: UseFileStateManagerProps) => { + const [selectedFiles, setSelectedFiles] = useState([]); + const [fileList, setFileList] = useState([]); + const [downloadedFiles, setDownloadedFiles] = useState>(new Map()); + + // 生成文件ID + const generateFileId = useCallback(() => { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + }, []); + + // 文件选择处理 + const handleFileSelect = useCallback((files: File[]) => { + console.log('=== 文件选择 ==='); + console.log('新文件:', files.map(f => f.name)); + + // 更新选中的文件 + setSelectedFiles(prev => [...prev, ...files]); + + // 创建对应的文件信息 + const newFileInfos: FileInfo[] = files.map(file => ({ + id: generateFileId(), + name: file.name, + size: file.size, + type: file.type, + status: 'ready', + progress: 0 + })); + + setFileList(prev => { + const updatedList = [...prev, ...newFileInfos]; + console.log('更新后的文件列表:', updatedList); + return updatedList; + }); + }, [generateFileId]); + + // 清空文件 + const clearFiles = useCallback(() => { + console.log('=== 清空文件 ==='); + setSelectedFiles([]); + setFileList([]); + }, []); + + // 重置状态 + const resetFiles = useCallback(() => { + console.log('=== 重置文件状态 ==='); + setSelectedFiles([]); + setFileList([]); + setDownloadedFiles(new Map()); + }, []); + + // 更新文件状态 + const updateFileStatus = useCallback((fileId: string, status: FileInfo['status'], progress?: number) => { + setFileList(prev => prev.map(item => + item.id === fileId + ? { ...item, status, progress: progress ?? item.progress } + : item + )); + }, []); + + // 更新文件进度 + const updateFileProgress = useCallback((fileId: string, fileName: string, progress: number) => { + const newStatus = progress >= 100 ? 'completed' as const : 'downloading' as const; + setFileList(prev => prev.map(item => { + if (item.id === fileId || item.name === fileName) { + console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${progress}`); + return { ...item, progress, status: newStatus }; + } + return item; + })); + }, []); + + // 数据通道第一次打开时初始化 + useEffect(() => { + if (isPeerConnected && mode === 'send' && fileList.length > 0) { + console.log('P2P连接已建立,数据通道首次打开,初始化文件列表'); + syncFileListToReceiver(fileList, '数据通道初始化'); + } + }, [isPeerConnected, mode, syncFileListToReceiver]); + + // 监听fileList大小变化并同步 + useEffect(() => { + if (isPeerConnected && mode === 'send' && pickupCode) { + console.log('fileList大小变化,同步到接收方:', fileList.length); + syncFileListToReceiver(fileList, 'fileList大小变化'); + } + }, [fileList.length, isPeerConnected, mode, pickupCode, syncFileListToReceiver]); + + // 监听selectedFiles变化,同步更新fileList + useEffect(() => { + // 只有在发送模式下且已有房间时才处理文件列表同步 + if (mode !== 'send' || !pickupCode) return; + + console.log('=== selectedFiles变化,同步文件列表 ===', { + selectedFilesCount: selectedFiles.length, + fileListCount: fileList.length, + selectedFileNames: selectedFiles.map(f => f.name) + }); + + // 根据selectedFiles创建新的文件信息列表 + const newFileInfos: FileInfo[] = selectedFiles.map(file => { + // 尝试找到现有的文件信息,保持已有的状态 + const existingFileInfo = fileList.find(info => info.name === file.name && info.size === file.size); + return existingFileInfo || { + id: generateFileId(), + name: file.name, + size: file.size, + type: file.type, + status: 'ready' as const, + progress: 0 + }; + }); + + // 检查文件列表是否真正发生变化 + const fileListChanged = + newFileInfos.length !== fileList.length || + newFileInfos.some(newFile => + !fileList.find(oldFile => oldFile.name === newFile.name && oldFile.size === newFile.size) + ); + + if (fileListChanged) { + console.log('文件列表发生变化,更新:', { + before: fileList.map(f => f.name), + after: newFileInfos.map(f => f.name) + }); + + setFileList(newFileInfos); + } + }, [selectedFiles, mode, pickupCode, fileList, generateFileId]); + + return { + selectedFiles, + setSelectedFiles, + fileList, + setFileList, + downloadedFiles, + setDownloadedFiles, + handleFileSelect, + clearFiles, + resetFiles, + updateFileStatus, + updateFileProgress + }; +}; diff --git a/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts b/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts similarity index 96% rename from chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts rename to chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts index 32e0f0d..e91282f 100644 --- a/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts +++ b/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts @@ -1,5 +1,5 @@ import { useState, useCallback, useRef, useEffect } from 'react'; -import type { WebRTCConnection } from './useSharedWebRTCManager'; +import type { WebRTCConnection } from '../connection/useSharedWebRTCManager'; // 文件传输状态 interface FileTransferState { @@ -17,7 +17,6 @@ interface FileTransferState { interface FileReceiveProgress { fileId: string; fileName: string; - receivedChunks: number; totalChunks: number; progress: number; } @@ -178,7 +177,6 @@ export function useFileTransferBusiness(connection: WebRTCConnection) { receiveProgress.current.set(metadata.id, { fileId: metadata.id, fileName: metadata.name, - receivedChunks: 0, totalChunks, progress: 0 }); @@ -301,16 +299,22 @@ export function useFileTransferBusiness(connection: WebRTCConnection) { return; } + // 检查是否已经接收过这个块,避免重复计数 + const alreadyReceived = fileInfo.chunks[chunkIndex] !== undefined; + // 数据有效,保存到缓存 fileInfo.chunks[chunkIndex] = data; - fileInfo.receivedChunks++; + + // 只有在首次接收时才增加计数 + if (!alreadyReceived) { + fileInfo.receivedChunks++; + } - // 更新接收进度跟踪 + // 更新接收进度跟踪 - 使用 fileInfo 的计数,避免双重计数 const progressInfo = receiveProgress.current.get(fileId); if (progressInfo) { - progressInfo.receivedChunks++; progressInfo.progress = progressInfo.totalChunks > 0 ? - (progressInfo.receivedChunks / progressInfo.totalChunks) * 100 : 0; + (fileInfo.receivedChunks / progressInfo.totalChunks) * 100 : 0; // 只有当这个文件是当前活跃文件时才更新全局进度 if (activeReceiveFile.current === fileId) { @@ -537,8 +541,8 @@ export function useFileTransferBusiness(connection: WebRTCConnection) { } } - // 更新进度 - const progress = (status.acknowledgedChunks.size / totalChunks) * 100; + // 更新进度 - 基于已发送的块数,这样与接收方的进度更同步 + const progress = ((chunkIndex + 1) / totalChunks) * 100; updateState({ progress }); fileProgressCallbacks.current.forEach(cb => cb({ diff --git a/chuan-next/src/hooks/index.ts b/chuan-next/src/hooks/index.ts new file mode 100644 index 0000000..59e055b --- /dev/null +++ b/chuan-next/src/hooks/index.ts @@ -0,0 +1,19 @@ +// 按功能分类的hooks导出 + +// 连接相关 +export * from './connection'; + +// 文件传输相关 +export * from './file-transfer'; + +// 桌面共享相关 +export * from './desktop-share'; + +// 文本传输相关 +export * from './text-transfer'; + +// UI状态管理相关 +export * from './ui'; + +// 核心WebRTC功能 +export * from './webrtc'; diff --git a/chuan-next/src/hooks/text-transfer/index.ts b/chuan-next/src/hooks/text-transfer/index.ts new file mode 100644 index 0000000..9dd27fe --- /dev/null +++ b/chuan-next/src/hooks/text-transfer/index.ts @@ -0,0 +1,2 @@ +// 文本传输相关的hooks +export { useTextTransferBusiness } from './useTextTransferBusiness'; diff --git a/chuan-next/src/hooks/webrtc/useTextTransferBusiness.ts b/chuan-next/src/hooks/text-transfer/useTextTransferBusiness.ts similarity index 98% rename from chuan-next/src/hooks/webrtc/useTextTransferBusiness.ts rename to chuan-next/src/hooks/text-transfer/useTextTransferBusiness.ts index 39682bd..b9b473b 100644 --- a/chuan-next/src/hooks/webrtc/useTextTransferBusiness.ts +++ b/chuan-next/src/hooks/text-transfer/useTextTransferBusiness.ts @@ -1,5 +1,5 @@ import { useState, useCallback, useRef, useEffect } from 'react'; -import type { WebRTCConnection } from './useSharedWebRTCManager'; +import type { WebRTCConnection } from '../connection/useSharedWebRTCManager'; // 文本传输状态 interface TextTransferState { diff --git a/chuan-next/src/hooks/ui/index.ts b/chuan-next/src/hooks/ui/index.ts new file mode 100644 index 0000000..6ae9dc9 --- /dev/null +++ b/chuan-next/src/hooks/ui/index.ts @@ -0,0 +1,3 @@ +// UI状态管理相关的hooks +export { useURLHandler } from './useURLHandler'; +export { useWebRTCStore } from './webRTCStore'; diff --git a/chuan-next/src/hooks/ui/useURLHandler.ts b/chuan-next/src/hooks/ui/useURLHandler.ts new file mode 100644 index 0000000..060ccbf --- /dev/null +++ b/chuan-next/src/hooks/ui/useURLHandler.ts @@ -0,0 +1,123 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import { useToast } from '@/components/ui/toast-simple'; + +// 支持的功能类型 +export type FeatureType = 'webrtc' | 'message' | 'desktop'; + +// 支持的模式映射 +const MODE_MAPPINGS: Record = { + webrtc: { send: 'send', receive: 'receive' }, + message: { send: 'send', receive: 'receive' }, + desktop: { send: 'send', receive: 'receive' } // desktop内部可能使用 share/view,但URL统一使用send/receive +}; + +interface UseURLHandlerProps { + featureType: FeatureType; + onModeChange: (mode: T) => void; + onAutoJoinRoom?: (code: string) => void; + modeConverter?: { + // 将URL模式转换为组件内部模式 + fromURL: (urlMode: 'send' | 'receive') => T; + // 将组件内部模式转换为URL模式 + toURL: (componentMode: T) => 'send' | 'receive'; + }; +} + +export const useURLHandler = ({ + featureType, + onModeChange, + onAutoJoinRoom, + modeConverter +}: UseURLHandlerProps) => { + const searchParams = useSearchParams(); + const router = useRouter(); + const [hasProcessedInitialUrl, setHasProcessedInitialUrl] = useState(false); + const urlProcessedRef = useRef(false); + + // 从URL参数中获取初始模式(仅在首次加载时处理) + useEffect(() => { + // 使用 ref 确保只处理一次,避免严格模式的重复调用 + if (urlProcessedRef.current) { + console.log('URL已处理过,跳过重复处理'); + return; + } + + const urlMode = searchParams.get('mode') as 'send' | 'receive'; + const type = searchParams.get('type') as FeatureType; + const code = searchParams.get('code'); + + // 只在首次加载且URL中有对应功能类型时处理 + if (!hasProcessedInitialUrl && type === featureType && urlMode && ['send', 'receive'].includes(urlMode)) { + console.log(`=== 处理初始URL参数 [${featureType}] ===`); + console.log('URL模式:', urlMode, '类型:', type, '取件码:', code); + + // 立即标记为已处理,防止重复 + urlProcessedRef.current = true; + + // 转换模式(如果有转换器的话) + const componentMode = modeConverter ? modeConverter.fromURL(urlMode) : urlMode as T; + onModeChange(componentMode); + setHasProcessedInitialUrl(true); + + // 自动加入房间(只在receive模式且有code时) + if (code && urlMode === 'receive' && onAutoJoinRoom) { + console.log('URL中有取件码,自动加入房间'); + onAutoJoinRoom(code); + } + } + }, [searchParams, hasProcessedInitialUrl, featureType, onModeChange, onAutoJoinRoom, modeConverter]); + + // 更新URL参数 + const updateMode = useCallback((newMode: T) => { + console.log(`=== 手动切换模式 [${featureType}] ===`); + console.log('新模式:', newMode); + + onModeChange(newMode); + + const params = new URLSearchParams(searchParams.toString()); + params.set('type', featureType); + + // 转换模式(如果有转换器的话) + const urlMode = modeConverter ? modeConverter.toURL(newMode) : newMode as string; + params.set('mode', urlMode); + + // 如果切换到发送模式,移除code参数 + if (urlMode === 'send') { + params.delete('code'); + } + + router.push(`?${params.toString()}`, { scroll: false }); + }, [searchParams, router, featureType, onModeChange, modeConverter]); + + // 更新URL中的房间代码 + const updateRoomCode = useCallback((code: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('type', featureType); + params.set('code', code); + router.push(`?${params.toString()}`, { scroll: false }); + }, [searchParams, router, featureType]); + + // 获取当前URL中的房间代码 + const getCurrentRoomCode = useCallback(() => { + return searchParams.get('code') || ''; + }, [searchParams]); + + // 清除URL参数 + const clearURLParams = useCallback(() => { + const params = new URLSearchParams(searchParams.toString()); + params.delete('type'); + params.delete('mode'); + params.delete('code'); + + const newURL = params.toString() ? `?${params.toString()}` : '/'; + router.push(newURL, { scroll: false }); + }, [searchParams, router]); + + return { + updateMode, + updateRoomCode, + getCurrentRoomCode, + clearURLParams + }; +}; diff --git a/chuan-next/src/hooks/webrtc/webRTCStore.ts b/chuan-next/src/hooks/ui/webRTCStore.ts similarity index 100% rename from chuan-next/src/hooks/webrtc/webRTCStore.ts rename to chuan-next/src/hooks/ui/webRTCStore.ts diff --git a/chuan-next/src/hooks/useConnectionStatus.ts b/chuan-next/src/hooks/useConnectionStatus.ts deleted file mode 100644 index 8b13789..0000000 --- a/chuan-next/src/hooks/useConnectionStatus.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/chuan-next/src/hooks/webrtc/core/DataChannelManager.ts b/chuan-next/src/hooks/webrtc/DataChannelManager.ts similarity index 100% rename from chuan-next/src/hooks/webrtc/core/DataChannelManager.ts rename to chuan-next/src/hooks/webrtc/DataChannelManager.ts diff --git a/chuan-next/src/hooks/webrtc/core/MessageRouter.ts b/chuan-next/src/hooks/webrtc/MessageRouter.ts similarity index 100% rename from chuan-next/src/hooks/webrtc/core/MessageRouter.ts rename to chuan-next/src/hooks/webrtc/MessageRouter.ts diff --git a/chuan-next/src/hooks/webrtc/core/PeerConnectionManager.ts b/chuan-next/src/hooks/webrtc/PeerConnectionManager.ts similarity index 100% rename from chuan-next/src/hooks/webrtc/core/PeerConnectionManager.ts rename to chuan-next/src/hooks/webrtc/PeerConnectionManager.ts diff --git a/chuan-next/src/hooks/webrtc/core/WebRTCManager.ts b/chuan-next/src/hooks/webrtc/WebRTCManager.ts similarity index 100% rename from chuan-next/src/hooks/webrtc/core/WebRTCManager.ts rename to chuan-next/src/hooks/webrtc/WebRTCManager.ts diff --git a/chuan-next/src/hooks/webrtc/core/WebSocketManager.ts b/chuan-next/src/hooks/webrtc/WebSocketManager.ts similarity index 100% rename from chuan-next/src/hooks/webrtc/core/WebSocketManager.ts rename to chuan-next/src/hooks/webrtc/WebSocketManager.ts diff --git a/chuan-next/src/hooks/webrtc/index.ts b/chuan-next/src/hooks/webrtc/index.ts new file mode 100644 index 0000000..45c1e35 --- /dev/null +++ b/chuan-next/src/hooks/webrtc/index.ts @@ -0,0 +1,7 @@ +// WebRTC核心功能 +export * from './DataChannelManager'; +export * from './MessageRouter'; +export * from './PeerConnectionManager'; +export * from './WebRTCManager'; +export * from './WebSocketManager'; +export * from './types'; diff --git a/chuan-next/src/hooks/webrtc/core/types.ts b/chuan-next/src/hooks/webrtc/types.ts similarity index 100% rename from chuan-next/src/hooks/webrtc/core/types.ts rename to chuan-next/src/hooks/webrtc/types.ts