mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-15 09:44:45 +08:00
feat:hook拆分
This commit is contained in:
@@ -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<string>('');
|
||||
|
||||
// 从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<File[]>([]);
|
||||
const [pickupCode, setPickupCode] = useState<string>('');
|
||||
const [pickupLink, setPickupLink] = useState<string>('');
|
||||
const [currentRole, setCurrentRole] = useState<'sender' | 'receiver'>('sender');
|
||||
|
||||
// 接收方状态
|
||||
const [receiverFiles, setReceiverFiles] = useState<FileInfo[]>([]);
|
||||
const [transferProgresses, setTransferProgresses] = useState<TransferProgress[]>([]);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
|
||||
// 房间状态
|
||||
const [roomStatus, setRoomStatus] = useState<RoomStatus | null>(null);
|
||||
|
||||
// 文件传输状态
|
||||
const [fileTransfers, setFileTransfers] = useState<Map<string, FileTransferData>>(new Map());
|
||||
const [completedDownloads, setCompletedDownloads] = useState<Set<string>>(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<WebSocketMessage>) => {
|
||||
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 (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50">
|
||||
@@ -680,63 +206,25 @@ export default function HomePage() {
|
||||
<FileTransfer
|
||||
selectedFiles={selectedFiles}
|
||||
onFilesChange={setSelectedFiles}
|
||||
onGenerateCode={handleGenerateCode}
|
||||
onGenerateCode={generateCode}
|
||||
pickupCode={pickupCode}
|
||||
pickupLink={pickupLink}
|
||||
onCopyCode={() => 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' && (
|
||||
<div className="mt-6 glass-card rounded-2xl p-6 animate-fade-in-up">
|
||||
<h3 className="text-xl font-semibold text-slate-800 mb-4 text-center">实时状态</h3>
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl">
|
||||
<div className="text-3xl font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent">
|
||||
{(roomStatus?.sender_count || 0) + (roomStatus?.receiver_count || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 mt-1">在线用户</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-emerald-50 to-teal-50 rounded-xl">
|
||||
<div className="text-3xl font-bold text-emerald-600">
|
||||
{roomStatus?.sender_count || 0}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 mt-1">发送方</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl">
|
||||
<div className="text-3xl font-bold text-purple-600">
|
||||
{roomStatus?.receiver_count || 0}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 mt-1">接收方</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<RoomStatusDisplay roomStatus={roomStatus} currentRole={currentRole} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="text" className="mt-6 animate-fade-in-up">
|
||||
@@ -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() {
|
||||
<TabsContent value="desktop" className="mt-6 animate-fade-in-up">
|
||||
<DesktopShare
|
||||
onStartSharing={async () => {
|
||||
// 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() {
|
||||
</div>
|
||||
|
||||
{/* 确认对话框 */}
|
||||
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>切换传输模式</DialogTitle>
|
||||
<DialogDescription>
|
||||
{(() => {
|
||||
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}?`;
|
||||
})()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={cancelTabSwitch}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={confirmTabSwitch}>
|
||||
确认打开
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<TabSwitchDialog
|
||||
open={showConfirmDialog}
|
||||
onOpenChange={setShowConfirmDialog}
|
||||
onConfirm={confirmTabSwitch}
|
||||
onCancel={cancelTabSwitch}
|
||||
description={getModeDescription()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
39
chuan-next/src/components/RoomStatusDisplay.tsx
Normal file
39
chuan-next/src/components/RoomStatusDisplay.tsx
Normal file
@@ -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<RoomStatusDisplayProps> = ({ roomStatus, currentRole }) => {
|
||||
if (!roomStatus || currentRole !== 'sender') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 glass-card rounded-2xl p-6 animate-fade-in-up">
|
||||
<h3 className="text-xl font-semibold text-slate-800 mb-4 text-center">实时状态</h3>
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl">
|
||||
<div className="text-3xl font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent">
|
||||
{(roomStatus?.sender_count || 0) + (roomStatus?.receiver_count || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 mt-1">在线用户</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-emerald-50 to-teal-50 rounded-xl">
|
||||
<div className="text-3xl font-bold text-emerald-600">
|
||||
{roomStatus?.sender_count || 0}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 mt-1">发送方</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl">
|
||||
<div className="text-3xl font-bold text-purple-600">
|
||||
{roomStatus?.receiver_count || 0}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 mt-1">接收方</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
40
chuan-next/src/components/TabSwitchDialog.tsx
Normal file
40
chuan-next/src/components/TabSwitchDialog.tsx
Normal file
@@ -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<TabSwitchDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
description
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>切换传输模式</DialogTitle>
|
||||
<DialogDescription>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>
|
||||
确认打开
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
62
chuan-next/src/hooks/useFileReceiver.ts
Normal file
62
chuan-next/src/hooks/useFileReceiver.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
78
chuan-next/src/hooks/useFileSender.ts
Normal file
78
chuan-next/src/hooks/useFileSender.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
203
chuan-next/src/hooks/useFileTransfer.ts
Normal file
203
chuan-next/src/hooks/useFileTransfer.ts
Normal file
@@ -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<Map<string, FileTransferData>>(new Map());
|
||||
const [completedDownloads, setCompletedDownloads] = useState<Set<string>>(new Set());
|
||||
const [transferProgresses, setTransferProgresses] = useState<TransferProgress[]>([]);
|
||||
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
|
||||
};
|
||||
};
|
||||
183
chuan-next/src/hooks/useRoomManager.ts
Normal file
183
chuan-next/src/hooks/useRoomManager.ts
Normal file
@@ -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<File[]>([]);
|
||||
const [pickupCode, setPickupCode] = useState<string>('');
|
||||
const [pickupLink, setPickupLink] = useState<string>('');
|
||||
const [currentRole, setCurrentRole] = useState<'sender' | 'receiver'>('sender');
|
||||
const [receiverFiles, setReceiverFiles] = useState<FileInfo[]>([]);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [roomStatus, setRoomStatus] = useState<RoomStatus | null>(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
|
||||
};
|
||||
};
|
||||
110
chuan-next/src/hooks/useTabManager.ts
Normal file
110
chuan-next/src/hooks/useTabManager.ts
Normal file
@@ -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<string>('');
|
||||
|
||||
// 从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
|
||||
};
|
||||
};
|
||||
38
chuan-next/src/hooks/useUrlHandler.ts
Normal file
38
chuan-next/src/hooks/useUrlHandler.ts
Normal file
@@ -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<void>;
|
||||
}
|
||||
|
||||
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,避免重复触发
|
||||
};
|
||||
28
chuan-next/src/hooks/useUtilities.ts
Normal file
28
chuan-next/src/hooks/useUtilities.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
119
chuan-next/src/hooks/useWebSocketHandler.ts
Normal file
119
chuan-next/src/hooks/useWebSocketHandler.ts
Normal file
@@ -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<void>;
|
||||
}
|
||||
|
||||
export const useWebSocketHandler = ({
|
||||
currentRole,
|
||||
setReceiverFiles,
|
||||
setRoomStatus,
|
||||
setIsConnecting,
|
||||
initFileTransfer,
|
||||
receiveFileChunk,
|
||||
completeFileDownload,
|
||||
handleFileRequest
|
||||
}: UseWebSocketHandlerProps) => {
|
||||
const { showToast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const handleWebSocketMessage = (event: CustomEvent<WebSocketMessage>) => {
|
||||
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
|
||||
]);
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user