feat:hook拆分

This commit is contained in:
MatrixSeven
2025-08-02 16:46:20 +08:00
parent 0942d11019
commit b43ea79c47
13 changed files with 1073 additions and 720 deletions

View File

@@ -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>
);
}

View 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>
);
};

View 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>
);
};

View 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
};
};

View 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
};
};

View 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
};
};

View 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
};
};

View 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
};
};

View 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避免重复触发
};

View 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
};
};

View File

@@ -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);
};

View 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
]);
};

View File

@@ -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 = {