From b43ea79c475f46e5e33d1e165619e81204a77b31 Mon Sep 17 00:00:00 2001 From: MatrixSeven Date: Sat, 2 Aug 2025 16:46:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:hook=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chuan-next/src/app/HomePage-new.tsx | 871 ++++-------------- .../src/components/RoomStatusDisplay.tsx | 39 + chuan-next/src/components/TabSwitchDialog.tsx | 40 + chuan-next/src/hooks/useFileReceiver.ts | 62 ++ chuan-next/src/hooks/useFileSender.ts | 78 ++ chuan-next/src/hooks/useFileTransfer.ts | 203 ++++ chuan-next/src/hooks/useRoomManager.ts | 183 ++++ chuan-next/src/hooks/useTabManager.ts | 110 +++ chuan-next/src/hooks/useUrlHandler.ts | 38 + chuan-next/src/hooks/useUtilities.ts | 28 + chuan-next/src/hooks/useWebSocket.ts | 13 +- chuan-next/src/hooks/useWebSocketHandler.ts | 119 +++ chuan-next/src/lib/config.ts | 9 +- 13 files changed, 1073 insertions(+), 720 deletions(-) create mode 100644 chuan-next/src/components/RoomStatusDisplay.tsx create mode 100644 chuan-next/src/components/TabSwitchDialog.tsx create mode 100644 chuan-next/src/hooks/useFileReceiver.ts create mode 100644 chuan-next/src/hooks/useFileSender.ts create mode 100644 chuan-next/src/hooks/useFileTransfer.ts create mode 100644 chuan-next/src/hooks/useRoomManager.ts create mode 100644 chuan-next/src/hooks/useTabManager.ts create mode 100644 chuan-next/src/hooks/useUrlHandler.ts create mode 100644 chuan-next/src/hooks/useUtilities.ts create mode 100644 chuan-next/src/hooks/useWebSocketHandler.ts diff --git a/chuan-next/src/app/HomePage-new.tsx b/chuan-next/src/app/HomePage-new.tsx index e340e0a..e85a448 100644 --- a/chuan-next/src/app/HomePage-new.tsx +++ b/chuan-next/src/app/HomePage-new.tsx @@ -1,638 +1,164 @@ "use client"; -import { useState, useEffect, useCallback } from 'react'; -import { useSearchParams, useRouter } from 'next/navigation'; +import React, { useEffect } from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; +import { Upload, MessageSquare, Monitor } from 'lucide-react'; import Hero from '@/components/Hero'; import FileTransfer from '@/components/FileTransfer'; import TextTransfer from '@/components/TextTransfer'; import DesktopShare from '@/components/DesktopShare'; -import { useWebSocket } from '@/hooks/useWebSocket'; -import { FileInfo, TransferProgress, WebSocketMessage, RoomStatus } from '@/types'; -import { Upload, MessageSquare, Monitor } from 'lucide-react'; -import { useToast } from '@/components/ui/toast-simple'; -import { apiPost, apiGet, debugApiConfig } from '@/lib/api-utils'; +import { RoomStatusDisplay } from '@/components/RoomStatusDisplay'; +import { TabSwitchDialog } from '@/components/TabSwitchDialog'; -interface FileTransferData { - fileId: string; - chunks: Array<{ offset: number; data: Uint8Array }>; - totalSize: number; - receivedSize: number; - fileName: string; - mimeType: string; - startTime: number; -} +// Hooks +import { useFileTransfer } from '@/hooks/useFileTransfer'; +import { useRoomManager } from '@/hooks/useRoomManager'; +import { useFileSender } from '@/hooks/useFileSender'; +import { useFileReceiver } from '@/hooks/useFileReceiver'; +import { useWebSocketHandler } from '@/hooks/useWebSocketHandler'; +import { useTabManager } from '@/hooks/useTabManager'; +import { useUtilities } from '@/hooks/useUtilities'; +import { useUrlHandler } from '@/hooks/useUrlHandler'; export default function HomePage() { - const searchParams = useSearchParams(); - const router = useRouter(); - const { websocket, isConnected, connect, disconnect, sendMessage } = useWebSocket(); - const { showToast } = useToast(); - - // URL参数管理 - const [activeTab, setActiveTab] = useState<'file' | 'text' | 'desktop'>('file'); - - // 确认对话框状态 - const [showConfirmDialog, setShowConfirmDialog] = useState(false); - const [pendingTabSwitch, setPendingTabSwitch] = useState(''); - - // 从URL参数中获取初始状态 + // 文件传输相关 + const { + fileTransfers, + transferProgresses, + initFileTransfer, + receiveFileChunk, + completeFileDownload, + clearTransfers, + setTransferProgresses + } = useFileTransfer(); + + // 房间管理相关 + const { + selectedFiles, + pickupCode, + pickupLink, + currentRole, + receiverFiles, + isConnecting, + roomStatus, + isConnected, + websocket, + setSelectedFiles, + setReceiverFiles, + setRoomStatus, + setIsConnecting, + setCurrentRole, + resetConnectingState, + generateCode, + joinRoom, + updateFileList, + handleRemoveFile, + clearFiles, + resetRoom, + sendMessage, + disconnect, + connect + } = useRoomManager(); + + // Tab管理相关 + const { + activeTab, + showConfirmDialog, + setShowConfirmDialog, + handleTabChange, + confirmTabSwitch, + cancelTabSwitch, + getModeDescription, + updateUrlParams + } = useTabManager(isConnected, pickupCode, isConnecting); + + // 工具函数 + const { copyToClipboard, showNotification } = useUtilities(); + + // 文件发送处理 + const { handleFileRequest } = useFileSender(selectedFiles, sendMessage); + + // 文件接收处理 + const { downloadFile } = useFileReceiver( + receiverFiles, + transferProgresses, + setTransferProgresses, + websocket, + sendMessage + ); + + // WebSocket连接状态变化处理 useEffect(() => { - const type = searchParams.get('type') as 'file' | 'text' | 'desktop'; - const mode = searchParams.get('mode') as 'send' | 'receive'; - - if (type && ['file', 'text', 'desktop'].includes(type)) { - setActiveTab(type); - } - }, [searchParams]); + resetConnectingState(); + }, [resetConnectingState]); - // 更新URL参数 - const updateUrlParams = useCallback((tab: string, mode?: string) => { - const params = new URLSearchParams(searchParams.toString()); - params.set('type', tab); - if (mode) { - params.set('mode', mode); - } - router.push(`?${params.toString()}`, { scroll: false }); - }, [searchParams, router]); - - // 发送方状态 - const [selectedFiles, setSelectedFiles] = useState([]); - const [pickupCode, setPickupCode] = useState(''); - const [pickupLink, setPickupLink] = useState(''); - const [currentRole, setCurrentRole] = useState<'sender' | 'receiver'>('sender'); - - // 接收方状态 - const [receiverFiles, setReceiverFiles] = useState([]); - const [transferProgresses, setTransferProgresses] = useState([]); - const [isConnecting, setIsConnecting] = useState(false); - - // 房间状态 - const [roomStatus, setRoomStatus] = useState(null); - - // 文件传输状态 - const [fileTransfers, setFileTransfers] = useState>(new Map()); - const [completedDownloads, setCompletedDownloads] = useState>(new Set()); - - // 显示通知 - const showNotification = useCallback((message: string, type: 'success' | 'error' | 'info' = 'success') => { - console.log(`[${type.toUpperCase()}] ${message}`); - showToast(message, type); - }, [showToast]); - - // 处理tab切换 - const handleTabChange = useCallback((value: string) => { - // 检查是否已经建立连接或生成取件码 - const hasActiveConnection = isConnected || pickupCode || isConnecting; - - if (hasActiveConnection && value !== activeTab) { - // 如果已有活跃连接且要切换到不同的tab,显示确认对话框 - setPendingTabSwitch(value); - setShowConfirmDialog(true); - return; - } - - // 如果没有活跃连接,正常切换 - setActiveTab(value as 'file' | 'text' | 'desktop'); - updateUrlParams(value); - }, [updateUrlParams, isConnected, pickupCode, isConnecting, activeTab]); - - // 监听WebSocket连接状态变化,重置连接中状态 + // 额外的连接状态重置逻辑 useEffect(() => { - if (isConnected && isConnecting) { + if (isConnected) { setIsConnecting(false); - console.log('WebSocket连接已建立,重置连接状态'); + console.log('WebSocket已连接,重置连接中状态'); } - }, [isConnected, isConnecting]); + }, [isConnected, setIsConnecting]); - // 确认切换tab - const confirmTabSwitch = useCallback(() => { - if (pendingTabSwitch) { - const currentUrl = window.location.origin + window.location.pathname; - const newUrl = `${currentUrl}?type=${pendingTabSwitch}`; - - // 在新标签页打开 - window.open(newUrl, '_blank'); - - // 关闭对话框并清理状态 - setShowConfirmDialog(false); - setPendingTabSwitch(''); - } - }, [pendingTabSwitch]); - - // 取消切换tab - const cancelTabSwitch = useCallback(() => { - setShowConfirmDialog(false); - setPendingTabSwitch(''); - }, []); - - // 初始化文件传输 - const initFileTransfer = useCallback((fileInfo: any) => { - console.log('初始化文件传输:', fileInfo); - const transferKey = fileInfo.file_id; - - setFileTransfers(prev => { - const newMap = new Map(prev); - newMap.set(transferKey, { - fileId: fileInfo.file_id, - chunks: [], - totalSize: fileInfo.size, - receivedSize: 0, - fileName: fileInfo.name, - mimeType: fileInfo.mime_type, - startTime: Date.now() - }); - console.log('添加文件传输记录:', transferKey); - return newMap; - }); - - setTransferProgresses(prev => { - const updated = prev.map(p => p.fileId === fileInfo.file_id - ? { ...p, status: 'downloading' as const, totalSize: fileInfo.size } - : p - ); - console.log('更新传输进度为下载中:', updated); - return updated; - }); - }, []); - - // 组装并下载文件 - const assembleAndDownloadFile = useCallback((transferKey: string, transfer: FileTransferData) => { - // 按偏移量排序数据块 - transfer.chunks.sort((a, b) => a.offset - b.offset); - - // 合并所有数据块 - const totalSize = transfer.chunks.reduce((sum, chunk) => sum + chunk.data.length, 0); - const mergedData = new Uint8Array(totalSize); - let currentOffset = 0; - - transfer.chunks.forEach((chunk) => { - mergedData.set(chunk.data, currentOffset); - currentOffset += chunk.data.length; - }); - - // 创建Blob并触发下载 - const blob = new Blob([mergedData], { type: transfer.mimeType }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = transfer.fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - // 清理状态 - setFileTransfers(prev => { - const newMap = new Map(prev); - newMap.delete(transferKey); - return newMap; - }); - - setTransferProgresses(prev => - prev.filter(p => p.fileId !== transferKey) - ); - - const transferTime = (Date.now() - transfer.startTime) / 1000; - const speed = (transfer.totalSize / transferTime / 1024 / 1024).toFixed(2); - showNotification(`文件 "${transfer.fileName}" 下载完成!传输速度: ${speed} MB/s`); - }, [showNotification]); - - // 接收文件数据块 - const receiveFileChunk = useCallback((chunkData: any) => { - console.log('接收文件数据块:', chunkData); - const transferKey = chunkData.file_id; - - setFileTransfers(prev => { - const newMap = new Map(prev); - const transfer = newMap.get(transferKey); - - if (transfer) { - // 检查是否已经完成,如果已经完成就不再处理新的数据块 - if (transfer.receivedSize >= transfer.totalSize) { - console.log('文件已完成,忽略额外的数据块'); - return newMap; - } - - const chunkArray = new Uint8Array(chunkData.data); - transfer.chunks.push({ - offset: chunkData.offset, - data: chunkArray - }); - transfer.receivedSize += chunkArray.length; - - // 确保不超过总大小 - if (transfer.receivedSize > transfer.totalSize) { - transfer.receivedSize = transfer.totalSize; - } - - const progress = (transfer.receivedSize / transfer.totalSize) * 100; - console.log(`文件 ${transferKey} 进度: ${progress.toFixed(2)}%`); - - // 更新进度 - setTransferProgresses(prev => { - const updated = prev.map(p => p.fileId === transferKey - ? { - ...p, - progress, - receivedSize: transfer.receivedSize, - totalSize: transfer.totalSize - } - : p - ); - console.log('更新进度状态:', updated); - return updated; - }); - - // 检查是否完成 - if (chunkData.is_last || transfer.receivedSize >= transfer.totalSize) { - console.log('文件接收完成,准备下载'); - // 标记为完成,等待 file-complete 消息统一处理下载 - setTransferProgresses(prev => - prev.map(p => p.fileId === transferKey - ? { ...p, status: 'completed' as const, progress: 100, receivedSize: transfer.totalSize } - : p - ) - ); - } - } else { - console.warn('未找到对应的文件传输:', transferKey); - } - - return newMap; - }); - }, []); - - // 完成文件下载 - const completeFileDownload = useCallback((fileId: string) => { - console.log('文件传输完成,开始下载:', fileId); - - // 检查是否已经完成过下载 - if (completedDownloads.has(fileId)) { - console.log('文件已经下载过,跳过重复下载:', fileId); - return; - } - - // 标记为已完成 - setCompletedDownloads(prev => new Set([...prev, fileId])); - - // 查找对应的文件传输数据 - const transfer = fileTransfers.get(fileId); - if (transfer) { - assembleAndDownloadFile(fileId, transfer); - - // 清理传输进度,移除已完成的文件进度显示 - setTimeout(() => { - setTransferProgresses(prev => - prev.filter(p => p.fileId !== fileId) - ); - }, 2000); // 2秒后清理,让用户看到完成状态 - } else { - console.warn('未找到文件传输数据:', fileId); - } - }, [fileTransfers, assembleAndDownloadFile, completedDownloads]); - - // 处理文件请求(发送方) - const handleFileRequest = useCallback(async (payload: any) => { - const fileId = payload.file_id; - const requestId = payload.request_id; - - const fileIndex = parseInt(fileId.replace('file_', '')); - const file = selectedFiles[fileIndex]; - - if (!file) { - console.error('未找到请求的文件:', fileId); - return; - } - - console.log('开始发送文件:', file.name); - showNotification(`开始发送文件: ${file.name}`); - - // 发送文件信息 - sendMessage({ - type: 'file-info', - payload: { - file_id: requestId, - name: file.name, - size: file.size, - mime_type: file.type, - last_modified: file.lastModified - } - }); - - // 分块发送文件 - const chunkSize = 65536; - let offset = 0; - - const sendChunk = () => { - if (offset >= file.size) { - sendMessage({ - type: 'file-complete', - payload: { file_id: requestId } - }); - showNotification(`文件发送完成: ${file.name}`); - return; - } - - const slice = file.slice(offset, offset + chunkSize); - const reader = new FileReader(); - - reader.onload = (e) => { - const chunk = e.target?.result as ArrayBuffer; - - sendMessage({ - type: 'file-chunk', - payload: { - file_id: requestId, - offset: offset, - data: Array.from(new Uint8Array(chunk)), - is_last: offset + chunk.byteLength >= file.size - } - }); - - offset += chunk.byteLength; - setTimeout(sendChunk, 10); - }; - - reader.readAsArrayBuffer(slice); + // 监听WebSocket错误事件 + useEffect(() => { + const handleWebSocketError = (event: CustomEvent) => { + console.error('WebSocket连接错误:', event.detail); + setIsConnecting(false); + showNotification('连接失败,请检查网络或重试', 'error'); }; + + const handleWebSocketConnected = (event: CustomEvent) => { + console.log('WebSocket连接成功:', event.detail); + setIsConnecting(false); + showNotification('连接成功!', 'success'); + }; + + window.addEventListener('websocket-error', handleWebSocketError as EventListener); + window.addEventListener('websocket-connected', handleWebSocketConnected as EventListener); - sendChunk(); - }, [selectedFiles, sendMessage, showNotification]); + return () => { + window.removeEventListener('websocket-error', handleWebSocketError as EventListener); + window.removeEventListener('websocket-connected', handleWebSocketConnected as EventListener); + }; + }, [setIsConnecting, showNotification]); // WebSocket消息处理 - useEffect(() => { - const handleWebSocketMessage = (event: CustomEvent) => { - const message = event.detail; - console.log('HomePage收到WebSocket消息:', message.type, message); + useWebSocketHandler({ + currentRole, + setReceiverFiles, + setRoomStatus, + setIsConnecting, + initFileTransfer, + receiveFileChunk, + completeFileDownload, + handleFileRequest + }); + + // URL参数处理 + useUrlHandler({ + isConnected, + pickupCode, + setCurrentRole, + joinRoom + }); + + // 处理添加更多文件 + const handleAddMoreFiles = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.onchange = async (e) => { + const files = Array.from((e.target as HTMLInputElement).files || []); + const newFiles = [...selectedFiles, ...files]; + setSelectedFiles(newFiles); - switch (message.type) { - case 'file-list': - console.log('处理file-list消息'); - if (currentRole === 'receiver') { - setReceiverFiles((message.payload.files as FileInfo[]) || []); - setIsConnecting(false); - } - break; - - case 'file-list-updated': - console.log('处理file-list-updated消息'); - if (currentRole === 'receiver') { - setReceiverFiles((message.payload.files as FileInfo[]) || []); - showNotification('文件列表已更新,发现新文件!'); - } - break; - - case 'room-status': - console.log('处理room-status消息'); - setRoomStatus(message.payload as unknown as RoomStatus); - break; - - case 'file-info': - console.log('处理file-info消息'); - if (currentRole === 'receiver') { - initFileTransfer(message.payload); - } - break; - - case 'file-chunk': - console.log('处理file-chunk消息'); - if (currentRole === 'receiver') { - receiveFileChunk(message.payload); - } - break; - - case 'file-complete': - console.log('处理file-complete消息'); - if (currentRole === 'receiver') { - completeFileDownload(message.payload.file_id as string); - } - break; - - case 'file-request': - console.log('处理file-request消息'); - if (currentRole === 'sender') { - handleFileRequest(message.payload); - } - break; + if (pickupCode && files.length > 0) { + updateFileList(newFiles); } }; - - window.addEventListener('websocket-message', handleWebSocketMessage as EventListener); - return () => { - window.removeEventListener('websocket-message', handleWebSocketMessage as EventListener); - }; - }, [currentRole, showNotification, initFileTransfer, receiveFileChunk, completeFileDownload, handleFileRequest]); - - // 生成取件码 - const handleGenerateCode = useCallback(async () => { - if (selectedFiles.length === 0) return; - - const fileInfos = selectedFiles.map((file, index) => ({ - id: 'file_' + index, - name: file.name, - size: file.size, - type: file.type, - lastModified: file.lastModified - })); - - try { - const response = await apiPost('/api/create-room', { files: fileInfos }); - - const data = await response.json(); - if (data.success) { - const code = data.code; - setPickupCode(code); - setCurrentRole('sender'); - - const baseUrl = window.location.origin; - const link = `${baseUrl}/?type=file&mode=receive&code=${code}`; - setPickupLink(link); - - connect(code, 'sender'); - showNotification('取件码生成成功!'); - } else { - showNotification('生成取件码失败: ' + data.message, 'error'); - } - } catch (error) { - console.error('生成取件码失败:', error); - showNotification('生成取件码失败,请重试', 'error'); - } - }, [selectedFiles, connect, showNotification]); - - // 加入房间 - const handleJoinRoom = useCallback(async (code: string) => { - // 防止重复连接 - if (isConnecting || (isConnected && pickupCode === code)) { - console.log('已在连接中或已连接,跳过重复请求'); - return; - } - - setIsConnecting(true); - - try { - const response = await apiGet(`/api/room-info?code=${code}`); - const data = await response.json(); - - if (data.success) { - setPickupCode(code); - setCurrentRole('receiver'); - setReceiverFiles(data.files || []); - // 开始连接WebSocket - connect(code, 'receiver'); - showNotification('连接成功!', 'success'); - // 注意:isConnecting状态会在WebSocket连接建立后自动重置 - } else { - showNotification(data.message || '取件码不存在或已过期', 'error'); - setIsConnecting(false); - } - } catch (error) { - console.error('API调用失败:', error); - showNotification('取件码不存在或已过期', 'error'); - setIsConnecting(false); - } - }, [connect, showNotification, isConnecting, isConnected, pickupCode]); - - // 处理URL参数中的取件码(仅在首次加载时) - useEffect(() => { - const code = searchParams.get('code'); - const type = searchParams.get('type'); - const mode = searchParams.get('mode'); - - // 只有在完整的URL参数情况下才自动加入房间: - // 1. 有效的6位取件码 - // 2. 当前未连接 - // 3. 不是已经连接的同一个房间码 - // 4. 必须是完整的链接:有type、mode=receive和code参数 - // 5. 不是文字类型(文字类型由TextTransfer组件处理) - if (code && - code.length === 6 && - !isConnected && - pickupCode !== code.toUpperCase() && - type && - type !== 'text' && - mode === 'receive') { - console.log('自动加入文件房间:', code.toUpperCase()); - setCurrentRole('receiver'); - handleJoinRoom(code.toUpperCase()); - } - }, [searchParams]); // 移除依赖,只在URL变化时触发 - - // 下载文件 - const handleDownloadFile = useCallback((fileId: string) => { - console.log('开始下载文件:', fileId); - if (!websocket || websocket.readyState !== WebSocket.OPEN) { - showNotification('连接未建立,请重试', 'error'); - return; - } - - // 检查是否已有同文件的进行中传输 - const existingProgress = transferProgresses.find(p => p.originalFileId === fileId && p.status !== 'completed'); - if (existingProgress) { - console.log('文件已在下载中,跳过重复请求:', fileId); - showNotification('文件正在下载中...', 'info'); - return; - } - - const requestId = 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); - console.log('生成请求ID:', requestId); - - sendMessage({ - type: 'file-request', - payload: { - file_id: fileId, - request_id: requestId - } - }); - - // 更新传输状态 - const newProgress = { - fileId: requestId, // 传输的唯一标识 - originalFileId: fileId, // 原始文件ID,用于UI匹配 - fileName: receiverFiles.find(f => f.id === fileId)?.name || fileId, - progress: 0, - receivedSize: 0, - totalSize: 0, - status: 'pending' as const - }; - - console.log('添加传输进度:', newProgress); - setTransferProgresses(prev => [ - ...prev.filter(p => p.originalFileId !== fileId), // 移除该文件的旧进度记录 - newProgress - ]); - }, [websocket, sendMessage, receiverFiles, showNotification, transferProgresses]); - - // 通过WebSocket更新文件列表 - const updateFileList = useCallback((files: File[]) => { - if (!pickupCode || !websocket || websocket.readyState !== WebSocket.OPEN) { - console.log('无法更新文件列表: pickupCode=', pickupCode, 'websocket状态=', websocket?.readyState); - return; - } - - const fileInfos = files.map((file, index) => ({ - id: 'file_' + index, - name: file.name, - size: file.size, - type: file.type, - lastModified: file.lastModified - })); - - console.log('通过WebSocket发送文件列表更新:', fileInfos); - sendMessage({ - type: 'update-file-list', - payload: { - files: fileInfos - } - }); - - showNotification('文件列表已更新'); - }, [pickupCode, websocket, sendMessage, showNotification]); - - // 处理文件删除后的同步 - const handleRemoveFile = useCallback((updatedFiles: File[]) => { - if (pickupCode) { - updateFileList(updatedFiles); - } - }, [pickupCode, updateFileList]); - - // 清空文件列表但保持房间连接 - const handleClearFiles = useCallback(() => { - setSelectedFiles([]); - setTransferProgresses([]); - setFileTransfers(new Map()); - // 保持 pickupCode, pickupLink, roomStatus 和 websocket 连接 - if (pickupCode) { - updateFileList([]); - showNotification('文件列表已清空,房间保持连接', 'success'); - } - }, [pickupCode, updateFileList, showNotification]); - - // 完全重置状态(关闭房间) - const handleReset = useCallback(() => { - setSelectedFiles([]); - setPickupCode(''); - setPickupLink(''); - setReceiverFiles([]); - setTransferProgresses([]); - setRoomStatus(null); - setFileTransfers(new Map()); - disconnect(); - showNotification('已断开连接', 'info'); - }, [disconnect, showNotification]); - - // 复制到剪贴板 - const copyToClipboard = useCallback(async (text: string, successMessage: string) => { - try { - await navigator.clipboard.writeText(text); - showNotification(successMessage, 'success'); - } catch (err) { - console.error('复制失败:', err); - showNotification('复制失败,请手动复制', 'error'); - } - }, [showNotification]); + input.click(); + }; return (
@@ -680,63 +206,25 @@ export default function HomePage() { copyToClipboard(pickupCode, '取件码已复制到剪贴板!')} onCopyLink={() => copyToClipboard(pickupLink, '取件链接已复制到剪贴板!')} - onAddMoreFiles={() => { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = true; - input.onchange = async (e) => { - const files = Array.from((e.target as HTMLInputElement).files || []); - const newFiles = [...selectedFiles, ...files]; - setSelectedFiles(newFiles); - - if (pickupCode && files.length > 0) { - updateFileList(newFiles); - } - }; - input.click(); - }} + onAddMoreFiles={handleAddMoreFiles} onRemoveFile={handleRemoveFile} - onClearFiles={handleClearFiles} - onReset={handleReset} - onJoinRoom={handleJoinRoom} + onClearFiles={clearFiles} + onReset={resetRoom} + onJoinRoom={joinRoom} receiverFiles={receiverFiles} - onDownloadFile={handleDownloadFile} + onDownloadFile={downloadFile} transferProgresses={transferProgresses} isConnected={isConnected} isConnecting={isConnecting} disabled={isConnecting} /> - {roomStatus && currentRole === 'sender' && ( -
-

实时状态

-
-
-
- {(roomStatus?.sender_count || 0) + (roomStatus?.receiver_count || 0)} -
-
在线用户
-
-
-
- {roomStatus?.sender_count || 0} -
-
发送方
-
-
-
- {roomStatus?.receiver_count || 0} -
-
接收方
-
-
-
- )} + @@ -756,21 +244,17 @@ export default function HomePage() { if (!response.ok) { const errorMessage = data.error || '创建文字传输房间失败'; showNotification(errorMessage, 'error'); - return ''; // 返回空字符串而不是抛出错误 + return ''; } - // 注释掉这里的成功提示,让 TextTransfer 组件来处理 - // showNotification('文字传输房间创建成功!', 'success'); return data.code; } catch (error) { console.error('创建文字传输房间失败:', error); showNotification('网络错误,请重试', 'error'); - return ''; // 返回空字符串而不是抛出错误 + return ''; } }} onReceiveText={async (code: string) => { - // 文字内容现在通过WebSocket获取,不再需要HTTP API - // 这个函数保留是为了兼容性,实际内容通过WebSocket的text-content消息获取 console.log('onReceiveText被调用,但文字内容将通过WebSocket获取:', code); return ''; }} @@ -778,12 +262,9 @@ export default function HomePage() { isConnected={isConnected} currentRole={currentRole} onCreateWebSocket={(code: string, role: 'sender' | 'receiver') => { - // 如果已有连接,先关闭 if (websocket) { disconnect(); } - - // 创建新的WebSocket连接 connect(code, role); }} /> @@ -792,15 +273,13 @@ export default function HomePage() { { - // TODO: 实现桌面共享功能 showNotification('桌面共享功能开发中', 'info'); - return 'DEF456'; // 模拟返回连接码 + return 'DEF456'; }} onStopSharing={async () => { showNotification('桌面共享已停止', 'info'); }} onJoinSharing={async (code: string) => { - // TODO: 实现桌面查看功能 showNotification('桌面共享功能开发中', 'info'); }} /> @@ -813,53 +292,13 @@ export default function HomePage() {
{/* 确认对话框 */} - - - - 切换传输模式 - - {(() => { - let currentMode = ''; - let targetMode = ''; - - switch (activeTab) { - case 'file': - currentMode = '文件传输'; - break; - case 'text': - currentMode = '文字传输'; - break; - case 'desktop': - currentMode = '桌面共享'; - break; - } - - switch (pendingTabSwitch) { - case 'file': - targetMode = '文件传输'; - break; - case 'text': - targetMode = '文字传输'; - break; - case 'desktop': - targetMode = '桌面共享'; - break; - } - - return `当前${currentMode}会话进行中,是否要在新标签页中打开${targetMode}?`; - })()} - - - - - - - - + ); } diff --git a/chuan-next/src/components/RoomStatusDisplay.tsx b/chuan-next/src/components/RoomStatusDisplay.tsx new file mode 100644 index 0000000..74129ed --- /dev/null +++ b/chuan-next/src/components/RoomStatusDisplay.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { RoomStatus } from '@/types'; + +interface RoomStatusDisplayProps { + roomStatus: RoomStatus | null; + currentRole: 'sender' | 'receiver'; +} + +export const RoomStatusDisplay: React.FC = ({ roomStatus, currentRole }) => { + if (!roomStatus || currentRole !== 'sender') { + return null; + } + + return ( +
+

实时状态

+
+
+
+ {(roomStatus?.sender_count || 0) + (roomStatus?.receiver_count || 0)} +
+
在线用户
+
+
+
+ {roomStatus?.sender_count || 0} +
+
发送方
+
+
+
+ {roomStatus?.receiver_count || 0} +
+
接收方
+
+
+
+ ); +}; diff --git a/chuan-next/src/components/TabSwitchDialog.tsx b/chuan-next/src/components/TabSwitchDialog.tsx new file mode 100644 index 0000000..9453e25 --- /dev/null +++ b/chuan-next/src/components/TabSwitchDialog.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; + +interface TabSwitchDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void; + onCancel: () => void; + description: string; +} + +export const TabSwitchDialog: React.FC = ({ + open, + onOpenChange, + onConfirm, + onCancel, + description +}) => { + return ( + + + + 切换传输模式 + + {description} + + + + + + + + + ); +}; diff --git a/chuan-next/src/hooks/useFileReceiver.ts b/chuan-next/src/hooks/useFileReceiver.ts new file mode 100644 index 0000000..57f1aae --- /dev/null +++ b/chuan-next/src/hooks/useFileReceiver.ts @@ -0,0 +1,62 @@ +import { useCallback } from 'react'; +import { FileInfo, TransferProgress } from '@/types'; +import { useToast } from '@/components/ui/toast-simple'; + +export const useFileReceiver = ( + receiverFiles: FileInfo[], + transferProgresses: TransferProgress[], + setTransferProgresses: (progresses: TransferProgress[] | ((prev: TransferProgress[]) => TransferProgress[])) => void, + websocket: WebSocket | null, + sendMessage: (message: any) => void +) => { + const { showToast } = useToast(); + + // 下载文件 + const downloadFile = useCallback((fileId: string) => { + console.log('开始下载文件:', fileId); + if (!websocket || websocket.readyState !== WebSocket.OPEN) { + showToast('连接未建立,请重试', 'error'); + return; + } + + // 检查是否已有同文件的进行中传输 + const existingProgress = transferProgresses.find(p => p.originalFileId === fileId && p.status !== 'completed'); + if (existingProgress) { + console.log('文件已在下载中,跳过重复请求:', fileId); + showToast('文件正在下载中...', 'info'); + return; + } + + const requestId = 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + console.log('生成请求ID:', requestId); + + sendMessage({ + type: 'file-request', + payload: { + file_id: fileId, + request_id: requestId + } + }); + + // 更新传输状态 + const newProgress = { + fileId: requestId, // 传输的唯一标识 + originalFileId: fileId, // 原始文件ID,用于UI匹配 + fileName: receiverFiles.find(f => f.id === fileId)?.name || fileId, + progress: 0, + receivedSize: 0, + totalSize: 0, + status: 'pending' as const + }; + + console.log('添加传输进度:', newProgress); + setTransferProgresses(prev => [ + ...prev.filter(p => p.originalFileId !== fileId), // 移除该文件的旧进度记录 + newProgress + ]); + }, [websocket, sendMessage, receiverFiles, showToast, transferProgresses, setTransferProgresses]); + + return { + downloadFile + }; +}; diff --git a/chuan-next/src/hooks/useFileSender.ts b/chuan-next/src/hooks/useFileSender.ts new file mode 100644 index 0000000..8652437 --- /dev/null +++ b/chuan-next/src/hooks/useFileSender.ts @@ -0,0 +1,78 @@ +import { useCallback } from 'react'; +import { useToast } from '@/components/ui/toast-simple'; + +export const useFileSender = (selectedFiles: File[], sendMessage: (message: any) => void) => { + const { showToast } = useToast(); + + // 处理文件请求(发送方) + const handleFileRequest = useCallback(async (payload: any) => { + const fileId = payload.file_id; + const requestId = payload.request_id; + + const fileIndex = parseInt(fileId.replace('file_', '')); + const file = selectedFiles[fileIndex]; + + if (!file) { + console.error('未找到请求的文件:', fileId); + return; + } + + console.log('开始发送文件:', file.name); + showToast(`开始发送文件: ${file.name}`); + + // 发送文件信息 + sendMessage({ + type: 'file-info', + payload: { + file_id: requestId, + name: file.name, + size: file.size, + mime_type: file.type, + last_modified: file.lastModified + } + }); + + // 分块发送文件 + const chunkSize = 65536; + let offset = 0; + + const sendChunk = () => { + if (offset >= file.size) { + sendMessage({ + type: 'file-complete', + payload: { file_id: requestId } + }); + showToast(`文件发送完成: ${file.name}`); + return; + } + + const slice = file.slice(offset, offset + chunkSize); + const reader = new FileReader(); + + reader.onload = (e) => { + const chunk = e.target?.result as ArrayBuffer; + + sendMessage({ + type: 'file-chunk', + payload: { + file_id: requestId, + offset: offset, + data: Array.from(new Uint8Array(chunk)), + is_last: offset + chunk.byteLength >= file.size + } + }); + + offset += chunk.byteLength; + setTimeout(sendChunk, 10); + }; + + reader.readAsArrayBuffer(slice); + }; + + sendChunk(); + }, [selectedFiles, sendMessage, showToast]); + + return { + handleFileRequest + }; +}; diff --git a/chuan-next/src/hooks/useFileTransfer.ts b/chuan-next/src/hooks/useFileTransfer.ts new file mode 100644 index 0000000..2ca61ee --- /dev/null +++ b/chuan-next/src/hooks/useFileTransfer.ts @@ -0,0 +1,203 @@ +import { useState, useCallback, useRef } from 'react'; +import { FileInfo, TransferProgress } from '@/types'; +import { useToast } from '@/components/ui/toast-simple'; + +interface FileTransferData { + fileId: string; + chunks: Array<{ offset: number; data: Uint8Array }>; + totalSize: number; + receivedSize: number; + fileName: string; + mimeType: string; + startTime: number; +} + +export const useFileTransfer = () => { + const [fileTransfers, setFileTransfers] = useState>(new Map()); + const [completedDownloads, setCompletedDownloads] = useState>(new Set()); + const [transferProgresses, setTransferProgresses] = useState([]); + const { showToast } = useToast(); + + // 初始化文件传输 + const initFileTransfer = useCallback((fileInfo: any) => { + console.log('初始化文件传输:', fileInfo); + const transferKey = fileInfo.file_id; + + setFileTransfers(prev => { + const newMap = new Map(prev); + newMap.set(transferKey, { + fileId: fileInfo.file_id, + chunks: [], + totalSize: fileInfo.size, + receivedSize: 0, + fileName: fileInfo.name, + mimeType: fileInfo.mime_type, + startTime: Date.now() + }); + console.log('添加文件传输记录:', transferKey); + return newMap; + }); + + setTransferProgresses(prev => { + const updated = prev.map(p => p.fileId === fileInfo.file_id + ? { ...p, status: 'downloading' as const, totalSize: fileInfo.size } + : p + ); + console.log('更新传输进度为下载中:', updated); + return updated; + }); + }, []); + + // 组装并下载文件 + const assembleAndDownloadFile = useCallback((transferKey: string, transfer: FileTransferData) => { + // 按偏移量排序数据块 + transfer.chunks.sort((a, b) => a.offset - b.offset); + + // 合并所有数据块 + const totalSize = transfer.chunks.reduce((sum, chunk) => sum + chunk.data.length, 0); + const mergedData = new Uint8Array(totalSize); + let currentOffset = 0; + + transfer.chunks.forEach((chunk) => { + mergedData.set(chunk.data, currentOffset); + currentOffset += chunk.data.length; + }); + + // 创建Blob并触发下载 + const blob = new Blob([mergedData], { type: transfer.mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = transfer.fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + // 清理状态 + setFileTransfers(prev => { + const newMap = new Map(prev); + newMap.delete(transferKey); + return newMap; + }); + + setTransferProgresses(prev => + prev.filter(p => p.fileId !== transferKey) + ); + + const transferTime = (Date.now() - transfer.startTime) / 1000; + const speed = (transfer.totalSize / transferTime / 1024 / 1024).toFixed(2); + showToast(`文件 "${transfer.fileName}" 下载完成!传输速度: ${speed} MB/s`); + }, [showToast]); + + // 接收文件数据块 + const receiveFileChunk = useCallback((chunkData: any) => { + console.log('接收文件数据块:', chunkData); + const transferKey = chunkData.file_id; + + setFileTransfers(prev => { + const newMap = new Map(prev); + const transfer = newMap.get(transferKey); + + if (transfer) { + // 检查是否已经完成,如果已经完成就不再处理新的数据块 + if (transfer.receivedSize >= transfer.totalSize) { + console.log('文件已完成,忽略额外的数据块'); + return newMap; + } + + const chunkArray = new Uint8Array(chunkData.data); + transfer.chunks.push({ + offset: chunkData.offset, + data: chunkArray + }); + transfer.receivedSize += chunkArray.length; + + // 确保不超过总大小 + if (transfer.receivedSize > transfer.totalSize) { + transfer.receivedSize = transfer.totalSize; + } + + const progress = (transfer.receivedSize / transfer.totalSize) * 100; + console.log(`文件 ${transferKey} 进度: ${progress.toFixed(2)}%`); + + // 更新进度 + setTransferProgresses(prev => { + const updated = prev.map(p => p.fileId === transferKey + ? { + ...p, + progress, + receivedSize: transfer.receivedSize, + totalSize: transfer.totalSize + } + : p + ); + console.log('更新进度状态:', updated); + return updated; + }); + + // 检查是否完成 + if (chunkData.is_last || transfer.receivedSize >= transfer.totalSize) { + console.log('文件接收完成,准备下载'); + // 标记为完成,等待 file-complete 消息统一处理下载 + setTransferProgresses(prev => + prev.map(p => p.fileId === transferKey + ? { ...p, status: 'completed' as const, progress: 100, receivedSize: transfer.totalSize } + : p + ) + ); + } + } else { + console.warn('未找到对应的文件传输:', transferKey); + } + + return newMap; + }); + }, []); + + // 完成文件下载 + const completeFileDownload = useCallback((fileId: string) => { + console.log('文件传输完成,开始下载:', fileId); + + // 检查是否已经完成过下载 + if (completedDownloads.has(fileId)) { + console.log('文件已经下载过,跳过重复下载:', fileId); + return; + } + + // 标记为已完成 + setCompletedDownloads(prev => new Set([...prev, fileId])); + + // 查找对应的文件传输数据 + const transfer = fileTransfers.get(fileId); + if (transfer) { + assembleAndDownloadFile(fileId, transfer); + + // 清理传输进度,移除已完成的文件进度显示 + setTimeout(() => { + setTransferProgresses(prev => + prev.filter(p => p.fileId !== fileId) + ); + }, 2000); // 2秒后清理,让用户看到完成状态 + } else { + console.warn('未找到文件传输数据:', fileId); + } + }, [fileTransfers, assembleAndDownloadFile, completedDownloads]); + + // 清理传输状态 + const clearTransfers = useCallback(() => { + setFileTransfers(new Map()); + setCompletedDownloads(new Set()); + setTransferProgresses([]); + }, []); + + return { + fileTransfers, + transferProgresses, + initFileTransfer, + receiveFileChunk, + completeFileDownload, + clearTransfers, + setTransferProgresses + }; +}; diff --git a/chuan-next/src/hooks/useRoomManager.ts b/chuan-next/src/hooks/useRoomManager.ts new file mode 100644 index 0000000..525366c --- /dev/null +++ b/chuan-next/src/hooks/useRoomManager.ts @@ -0,0 +1,183 @@ +import { useState, useCallback } from 'react'; +import { FileInfo, RoomStatus } from '@/types'; +import { useWebSocket } from '@/hooks/useWebSocket'; +import { useToast } from '@/components/ui/toast-simple'; +import { apiPost, apiGet } from '@/lib/api-utils'; + +export const useRoomManager = () => { + const [selectedFiles, setSelectedFiles] = useState([]); + const [pickupCode, setPickupCode] = useState(''); + const [pickupLink, setPickupLink] = useState(''); + const [currentRole, setCurrentRole] = useState<'sender' | 'receiver'>('sender'); + const [receiverFiles, setReceiverFiles] = useState([]); + const [isConnecting, setIsConnecting] = useState(false); + const [roomStatus, setRoomStatus] = useState(null); + + const { websocket, isConnected, connect, disconnect, sendMessage } = useWebSocket(); + const { showToast } = useToast(); + + // 生成取件码 + const generateCode = useCallback(async () => { + if (selectedFiles.length === 0) return; + + const fileInfos = selectedFiles.map((file, index) => ({ + id: 'file_' + index, + name: file.name, + size: file.size, + type: file.type, + lastModified: file.lastModified + })); + + try { + const response = await apiPost('/api/create-room', { files: fileInfos }); + + const data = await response.json(); + if (data.success) { + const code = data.code; + setPickupCode(code); + setCurrentRole('sender'); + + const baseUrl = window.location.origin; + const link = `${baseUrl}/?type=file&mode=receive&code=${code}`; + setPickupLink(link); + + connect(code, 'sender'); + showToast('取件码生成成功!'); + } else { + showToast('生成取件码失败: ' + data.message, 'error'); + } + } catch (error) { + console.error('生成取件码失败:', error); + showToast('生成取件码失败,请重试', 'error'); + } + }, [selectedFiles, connect, showToast]); + + // 加入房间 + const joinRoom = useCallback(async (code: string) => { + // 防止重复连接 + if (isConnecting || (isConnected && pickupCode === code)) { + console.log('已在连接中或已连接,跳过重复请求'); + return; + } + + setIsConnecting(true); + + try { + const response = await apiGet(`/api/room-info?code=${code}`); + const data = await response.json(); + + if (data.success) { + setPickupCode(code); + setCurrentRole('receiver'); + setReceiverFiles(data.files || []); + // 开始连接WebSocket + connect(code, 'receiver'); + console.log('房间信息获取成功,开始建立WebSocket连接'); + // 注意:isConnecting状态会在WebSocket连接建立后自动重置 + // 不在这里显示成功消息,等WebSocket连接成功后再显示 + } else { + showToast(data.message || '取件码不存在或已过期', 'error'); + setIsConnecting(false); + } + } catch (error) { + console.error('API调用失败:', error); + showToast('取件码不存在或已过期', 'error'); + setIsConnecting(false); + } + }, [connect, showToast, isConnecting, isConnected, pickupCode]); + + // 通过WebSocket更新文件列表 + const updateFileList = useCallback((files: File[]) => { + if (!pickupCode || !websocket || websocket.readyState !== WebSocket.OPEN) { + console.log('无法更新文件列表: pickupCode=', pickupCode, 'websocket状态=', websocket?.readyState); + return; + } + + const fileInfos = files.map((file, index) => ({ + id: 'file_' + index, + name: file.name, + size: file.size, + type: file.type, + lastModified: file.lastModified + })); + + console.log('通过WebSocket发送文件列表更新:', fileInfos); + sendMessage({ + type: 'update-file-list', + payload: { + files: fileInfos + } + }); + + showToast('文件列表已更新'); + }, [pickupCode, websocket, sendMessage, showToast]); + + // 处理文件删除后的同步 + const handleRemoveFile = useCallback((updatedFiles: File[]) => { + if (pickupCode) { + updateFileList(updatedFiles); + } + }, [pickupCode, updateFileList]); + + // 清空文件列表但保持房间连接 + const clearFiles = useCallback(() => { + setSelectedFiles([]); + if (pickupCode) { + updateFileList([]); + showToast('文件列表已清空,房间保持连接', 'success'); + } + }, [pickupCode, updateFileList, showToast]); + + // 完全重置状态(关闭房间) + const resetRoom = useCallback(() => { + setSelectedFiles([]); + setPickupCode(''); + setPickupLink(''); + setReceiverFiles([]); + setRoomStatus(null); + disconnect(); + showToast('已断开连接', 'info'); + }, [disconnect, showToast]); + + // 重置连接状态 + const resetConnectingState = useCallback(() => { + if (isConnected && isConnecting) { + setIsConnecting(false); + console.log('WebSocket连接已建立,重置连接状态'); + } + }, [isConnected, isConnecting]); + + return { + // 状态 + selectedFiles, + pickupCode, + pickupLink, + currentRole, + receiverFiles, + isConnecting, + roomStatus, + isConnected, + websocket, + + // 状态更新函数 + setSelectedFiles, + setReceiverFiles, + setRoomStatus, + setIsConnecting, + setCurrentRole, + resetConnectingState, + + // 房间操作 + generateCode, + joinRoom, + updateFileList, + handleRemoveFile, + clearFiles, + resetRoom, + + // WebSocket 相关 + sendMessage, + disconnect, + connect + }; +}; diff --git a/chuan-next/src/hooks/useTabManager.ts b/chuan-next/src/hooks/useTabManager.ts new file mode 100644 index 0000000..f8da63e --- /dev/null +++ b/chuan-next/src/hooks/useTabManager.ts @@ -0,0 +1,110 @@ +import { useState, useCallback, useEffect } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; + +export const useTabManager = (isConnected: boolean, pickupCode: string, isConnecting: boolean) => { + const searchParams = useSearchParams(); + const router = useRouter(); + const [activeTab, setActiveTab] = useState<'file' | 'text' | 'desktop'>('file'); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [pendingTabSwitch, setPendingTabSwitch] = useState(''); + + // 从URL参数中获取初始状态 + useEffect(() => { + const type = searchParams.get('type') as 'file' | 'text' | 'desktop'; + + if (type && ['file', 'text', 'desktop'].includes(type)) { + setActiveTab(type); + } + }, [searchParams]); + + // 更新URL参数 + const updateUrlParams = useCallback((tab: string, mode?: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('type', tab); + if (mode) { + params.set('mode', mode); + } + router.push(`?${params.toString()}`, { scroll: false }); + }, [searchParams, router]); + + // 处理tab切换 + const handleTabChange = useCallback((value: string) => { + // 检查是否已经建立连接或生成取件码 + const hasActiveConnection = isConnected || pickupCode || isConnecting; + + if (hasActiveConnection && value !== activeTab) { + // 如果已有活跃连接且要切换到不同的tab,显示确认对话框 + setPendingTabSwitch(value); + setShowConfirmDialog(true); + return; + } + + // 如果没有活跃连接,正常切换 + setActiveTab(value as 'file' | 'text' | 'desktop'); + updateUrlParams(value); + }, [updateUrlParams, isConnected, pickupCode, isConnecting, activeTab]); + + // 确认切换tab + const confirmTabSwitch = useCallback(() => { + if (pendingTabSwitch) { + const currentUrl = window.location.origin + window.location.pathname; + const newUrl = `${currentUrl}?type=${pendingTabSwitch}`; + + // 在新标签页打开 + window.open(newUrl, '_blank'); + + // 关闭对话框并清理状态 + setShowConfirmDialog(false); + setPendingTabSwitch(''); + } + }, [pendingTabSwitch]); + + // 取消切换tab + const cancelTabSwitch = useCallback(() => { + setShowConfirmDialog(false); + setPendingTabSwitch(''); + }, []); + + // 获取模式描述 + const getModeDescription = useCallback(() => { + let currentMode = ''; + let targetMode = ''; + + switch (activeTab) { + case 'file': + currentMode = '文件传输'; + break; + case 'text': + currentMode = '文字传输'; + break; + case 'desktop': + currentMode = '桌面共享'; + break; + } + + switch (pendingTabSwitch) { + case 'file': + targetMode = '文件传输'; + break; + case 'text': + targetMode = '文字传输'; + break; + case 'desktop': + targetMode = '桌面共享'; + break; + } + + return `当前${currentMode}会话进行中,是否要在新标签页中打开${targetMode}?`; + }, [activeTab, pendingTabSwitch]); + + return { + activeTab, + showConfirmDialog, + setShowConfirmDialog, + handleTabChange, + confirmTabSwitch, + cancelTabSwitch, + getModeDescription, + updateUrlParams + }; +}; diff --git a/chuan-next/src/hooks/useUrlHandler.ts b/chuan-next/src/hooks/useUrlHandler.ts new file mode 100644 index 0000000..4ea8c54 --- /dev/null +++ b/chuan-next/src/hooks/useUrlHandler.ts @@ -0,0 +1,38 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'next/navigation'; + +interface UseUrlHandlerProps { + isConnected: boolean; + pickupCode: string; + setCurrentRole: (role: 'sender' | 'receiver') => void; + joinRoom: (code: string) => Promise; +} + +export const useUrlHandler = ({ isConnected, pickupCode, setCurrentRole, joinRoom }: UseUrlHandlerProps) => { + const searchParams = useSearchParams(); + + // 处理URL参数中的取件码(仅在首次加载时) + useEffect(() => { + const code = searchParams.get('code'); + const type = searchParams.get('type'); + const mode = searchParams.get('mode'); + + // 只有在完整的URL参数情况下才自动加入房间: + // 1. 有效的6位取件码 + // 2. 当前未连接 + // 3. 不是已经连接的同一个房间码 + // 4. 必须是完整的链接:有type、mode=receive和code参数 + // 5. 不是文字类型(文字类型由TextTransfer组件处理) + if (code && + code.length === 6 && + !isConnected && + pickupCode !== code.toUpperCase() && + type && + type !== 'text' && + mode === 'receive') { + console.log('自动加入文件房间:', code.toUpperCase()); + setCurrentRole('receiver'); + joinRoom(code.toUpperCase()); + } + }, [searchParams]); // 只依赖 searchParams,避免重复触发 +}; diff --git a/chuan-next/src/hooks/useUtilities.ts b/chuan-next/src/hooks/useUtilities.ts new file mode 100644 index 0000000..4aa0e57 --- /dev/null +++ b/chuan-next/src/hooks/useUtilities.ts @@ -0,0 +1,28 @@ +import { useCallback } from 'react'; +import { useToast } from '@/components/ui/toast-simple'; + +export const useUtilities = () => { + const { showToast } = useToast(); + + // 复制到剪贴板 + const copyToClipboard = useCallback(async (text: string, successMessage: string) => { + try { + await navigator.clipboard.writeText(text); + showToast(successMessage, 'success'); + } catch (err) { + console.error('复制失败:', err); + showToast('复制失败,请手动复制', 'error'); + } + }, [showToast]); + + // 显示通知 + const showNotification = useCallback((message: string, type: 'success' | 'error' | 'info' = 'success') => { + console.log(`[${type.toUpperCase()}] ${message}`); + showToast(message, type); + }, [showToast]); + + return { + copyToClipboard, + showNotification + }; +}; diff --git a/chuan-next/src/hooks/useWebSocket.ts b/chuan-next/src/hooks/useWebSocket.ts index 18de884..bbd684f 100644 --- a/chuan-next/src/hooks/useWebSocket.ts +++ b/chuan-next/src/hooks/useWebSocket.ts @@ -38,7 +38,7 @@ export function useWebSocket(): UseWebSocketReturn { // 连接到Go后端的WebSocket - 使用配置文件中的URL const baseWsUrl = getWebSocketUrl(); - const wsUrl = `${baseWsUrl}/p2p?code=${code}&role=${role}`; + const wsUrl = `${baseWsUrl}?code=${code}&role=${role}`; console.log('连接WebSocket:', wsUrl); @@ -49,6 +49,12 @@ export function useWebSocket(): UseWebSocketReturn { setIsConnected(true); setWebsocket(ws); + // 发送连接建立确认事件 + const connectEvent = new CustomEvent('websocket-connected', { + detail: { code, role } + }); + window.dispatchEvent(connectEvent); + // 发送初始连接信息 const message = { type: 'connect', @@ -98,10 +104,13 @@ export function useWebSocket(): UseWebSocketReturn { ws.onerror = (error) => { console.error('WebSocket错误:', error); + console.error('WebSocket状态:', ws.readyState); + console.error('WebSocket URL:', wsUrl); + setIsConnected(false); // 发送连接错误事件 const errorEvent = new CustomEvent('websocket-error', { - detail: { error } + detail: { error, url: wsUrl, readyState: ws.readyState } }); window.dispatchEvent(errorEvent); }; diff --git a/chuan-next/src/hooks/useWebSocketHandler.ts b/chuan-next/src/hooks/useWebSocketHandler.ts new file mode 100644 index 0000000..a90cf0e --- /dev/null +++ b/chuan-next/src/hooks/useWebSocketHandler.ts @@ -0,0 +1,119 @@ +import { useEffect } from 'react'; +import { WebSocketMessage, FileInfo, RoomStatus } from '@/types'; +import { useToast } from '@/components/ui/toast-simple'; + +interface UseWebSocketHandlerProps { + currentRole: 'sender' | 'receiver'; + setReceiverFiles: (files: FileInfo[]) => void; + setRoomStatus: (status: RoomStatus | null) => void; + setIsConnecting: (connecting: boolean) => void; + initFileTransfer: (fileInfo: any) => void; + receiveFileChunk: (chunkData: any) => void; + completeFileDownload: (fileId: string) => void; + handleFileRequest: (payload: any) => Promise; +} + +export const useWebSocketHandler = ({ + currentRole, + setReceiverFiles, + setRoomStatus, + setIsConnecting, + initFileTransfer, + receiveFileChunk, + completeFileDownload, + handleFileRequest +}: UseWebSocketHandlerProps) => { + const { showToast } = useToast(); + + useEffect(() => { + const handleWebSocketMessage = (event: CustomEvent) => { + const message = event.detail; + console.log('收到WebSocket消息:', message.type, message); + + switch (message.type) { + case 'file-list': + console.log('处理file-list消息'); + if (currentRole === 'receiver') { + setReceiverFiles((message.payload.files as FileInfo[]) || []); + setIsConnecting(false); + } + break; + + case 'file-list-updated': + console.log('处理file-list-updated消息'); + if (currentRole === 'receiver') { + setReceiverFiles((message.payload.files as FileInfo[]) || []); + showToast('文件列表已更新,发现新文件!'); + } + break; + + case 'room-status': + console.log('处理room-status消息'); + setRoomStatus(message.payload as unknown as RoomStatus); + break; + + case 'file-info': + console.log('处理file-info消息'); + if (currentRole === 'receiver') { + initFileTransfer(message.payload); + } + break; + + case 'file-chunk': + console.log('处理file-chunk消息'); + if (currentRole === 'receiver') { + receiveFileChunk(message.payload); + } + break; + + case 'file-complete': + console.log('处理file-complete消息'); + if (currentRole === 'receiver') { + completeFileDownload(message.payload.file_id as string); + } + break; + + case 'file-request': + console.log('处理file-request消息'); + if (currentRole === 'sender') { + handleFileRequest(message.payload); + } + break; + + case 'connected': + case 'connection-established': + console.log('WebSocket连接已建立'); + setIsConnecting(false); + showToast('连接成功!', 'success'); + break; + + case 'text-content': + console.log('处理text-content消息'); + // 文本内容由TextTransfer组件处理 + setIsConnecting(false); + break; + + default: + // 对于任何其他消息类型,也重置连接状态(说明连接已建立) + console.log('收到消息,连接已建立,重置连接状态'); + setIsConnecting(false); + break; + } + }; + + window.addEventListener('websocket-message', handleWebSocketMessage as EventListener); + return () => { + window.removeEventListener('websocket-message', handleWebSocketMessage as EventListener); + }; + }, [ + currentRole, + setReceiverFiles, + setRoomStatus, + setIsConnecting, + initFileTransfer, + receiveFileChunk, + completeFileDownload, + handleFileRequest, + showToast + ]); +}; diff --git a/chuan-next/src/lib/config.ts b/chuan-next/src/lib/config.ts index a8242f8..fcbf74a 100644 --- a/chuan-next/src/lib/config.ts +++ b/chuan-next/src/lib/config.ts @@ -27,11 +27,16 @@ const getCurrentBaseUrl = () => { // 动态获取 WebSocket URL const getCurrentWsUrl = () => { if (typeof window !== 'undefined') { + // 在开发模式下,始终使用后端的WebSocket地址 + if (window.location.hostname === 'localhost' && window.location.port === '3000') { + return 'ws://localhost:8080/ws/p2p'; + } + // 在生产模式下,使用当前域名 const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - return `${protocol}//${window.location.host}/ws`; + return `${protocol}//${window.location.host}/ws/p2p`; } // 服务器端默认值 - return 'ws://localhost:8080/ws'; + return 'ws://localhost:8080/ws/p2p'; }; export const config = {