// P2P文件传输系统(多人房间版本) // 全局变量 let websocket = null; let clientConnections = new Map(); // 存储与其他客户端的P2P连接 let selectedFiles = []; let currentPickupCode = ''; let currentRole = ''; // 'sender' or 'receiver' let currentClientId = ''; // 当前客户端ID let fileTransfers = new Map(); // 存储文件传输状态 let isP2PConnected = false; // P2P连接状态 let isConnecting = false; // 是否正在连接中 let pendingChunkMeta = null; // 待处理的数据块元数据 // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', () => { initializeEventListeners(); }); // 初始化事件监听器 function initializeEventListeners() { // 文件选择事件 document.getElementById('fileInput').addEventListener('change', handleFileSelect); // 取件码输入事件 document.getElementById('pickupCodeInput').addEventListener('input', (e) => { e.target.value = e.target.value.toUpperCase(); if (e.target.value.length === 6) { // 自动连接 setTimeout(() => joinRoom(), 100); } }); // 拖拽上传 setupDragAndDrop(); } // 设置拖拽上传 function setupDragAndDrop() { const dropArea = document.querySelector('.border-dashed'); dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.classList.add('border-blue-400'); }); dropArea.addEventListener('dragleave', () => { dropArea.classList.remove('border-blue-400'); }); dropArea.addEventListener('drop', (e) => { e.preventDefault(); dropArea.classList.remove('border-blue-400'); const files = Array.from(e.dataTransfer.files); if (files.length > 0) { // 添加新文件到现有列表 selectedFiles = [...selectedFiles, ...files]; displaySelectedFiles(); // 如果已经生成了取件码,自动更新房间文件列表 if (currentPickupCode && currentRole === 'sender') { updateRoomFiles(); } } }); } // 处理文件选择 function handleFileSelect(event) { const files = Array.from(event.target.files); if (files.length > 0) { // 添加新文件到现有列表 selectedFiles = [...selectedFiles, ...files]; displaySelectedFiles(); // 如果已经生成了取件码,自动更新房间文件列表 if (currentPickupCode && currentRole === 'sender') { updateRoomFiles(); } } } // 显示选中的文件 function displaySelectedFiles() { const container = document.getElementById('selectedFiles'); const filesList = document.getElementById('filesList'); if (selectedFiles.length === 0) { container.classList.add('hidden'); return; } container.classList.remove('hidden'); filesList.innerHTML = ''; selectedFiles.forEach((file, index) => { const fileItem = document.createElement('div'); fileItem.className = 'flex items-center justify-between bg-gray-50 p-3 rounded-lg'; fileItem.innerHTML = `
${getFileIcon(file.type)}
${file.name}
${formatFileSize(file.size)}
`; filesList.appendChild(fileItem); }); } // 移除文件 function removeFile(index) { selectedFiles.splice(index, 1); displaySelectedFiles(); // 如果已经生成了取件码,需要更新房间文件列表 if (currentPickupCode && currentRole === 'sender') { updateRoomFiles(); } } // 添加更多文件 function addMoreFiles() { document.getElementById('fileInput').click(); } // 更新房间文件列表 async function updateRoomFiles() { if (!currentPickupCode || currentRole !== 'sender') 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 fetch('/api/update-room-files', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: currentPickupCode, files: fileInfos }) }); const data = await response.json(); if (data.success) { console.log('房间文件列表已更新'); showNotification('文件列表已更新', 'success'); // 通过WebSocket通知所有接收方文件列表更新 if (websocket && websocket.readyState === WebSocket.OPEN) { const updateMsg = { type: 'file-list-updated', payload: { files: fileInfos } }; websocket.send(JSON.stringify(updateMsg)); } } else { console.error('更新文件列表失败:', data.message); showNotification('更新文件列表失败: ' + data.message, 'error'); } } catch (error) { console.error('更新文件列表请求失败:', error); showNotification('更新文件列表失败,请重试', 'error'); } } // 生成取件码 async function generatePickupCode() { 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 fetch('/api/create-room', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ files: fileInfos }) }); const data = await response.json(); if (data.success) { currentPickupCode = data.code; currentRole = 'sender'; showPickupCode(data.code); connectWebSocket(); } else { alert('生成取件码失败: ' + data.message); } } catch (error) { console.error('生成取件码失败:', error); alert('生成取件码失败,请重试'); } } // 显示取件码 function showPickupCode(code) { document.getElementById('pickupCodeDisplay').textContent = code; document.getElementById('pickupCodeSection').classList.remove('hidden'); // 不隐藏生成取件码按钮,改为"添加更多文件" const generateBtn = document.getElementById('generateCodeBtn'); generateBtn.textContent = '➕ 添加更多文件'; generateBtn.onclick = addMoreFiles; } // 复制取件码 function copyPickupCode() { navigator.clipboard.writeText(currentPickupCode).then(() => { alert('取件码已复制到剪贴板'); }); } // 重置发送方 function resetSender() { selectedFiles = []; currentPickupCode = ''; currentRole = ''; currentClientId = ''; if (websocket) { websocket.close(); } document.getElementById('selectedFiles').classList.add('hidden'); document.getElementById('pickupCodeSection').classList.add('hidden'); document.getElementById('generateCodeBtn').classList.remove('hidden'); document.getElementById('fileInput').value = ''; document.getElementById('roomStatusSection').classList.add('hidden'); } // 加入房间 async function joinRoom() { const code = document.getElementById('pickupCodeInput').value.trim(); if (code.length !== 6) { alert('请输入6位取件码'); return; } try { const response = await fetch(`/api/room-info?code=${code}`); const data = await response.json(); if (data.success) { currentPickupCode = code; currentRole = 'receiver'; displayReceiverFiles(data.files); connectWebSocket(); } else { alert(data.message); } } catch (error) { console.error('连接失败:', error); alert('连接失败,请检查取件码是否正确'); } } // WebSocket连接函数 function connectWebSocket() { console.log('尝试连接WebSocket, 角色:', currentRole, '取件码:', currentPickupCode); if (!currentPickupCode || !currentRole) { console.error('缺少必要参数:取件码或角色'); showNotification('连接参数错误', 'error'); return; } if (isConnecting) { console.log('已在连接中,跳过'); return; } isConnecting = true; // 如果已经有连接,先关闭 if (websocket) { console.log('关闭现有WebSocket连接'); websocket.close(); websocket = null; } const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws/p2p?code=${currentPickupCode}&role=${currentRole}`; console.log('WebSocket URL:', wsUrl); try { websocket = new WebSocket(wsUrl); websocket.onopen = () => { console.log('WebSocket连接已建立, 当前角色:', currentRole); isConnecting = false; updateConnectionStatus(true); // 连接建立后,启用P2P功能 if (currentRole === 'receiver') { console.log('接收方WebSocket连接成功,启用下载功能'); updateP2PStatus(true); // 接收方连接成功后立即启用下载 showNotification('连接成功,可以开始下载文件', 'success'); } // 发送方在WebSocket连接建立后显示房间状态 if (currentRole === 'sender') { console.log('发送方初始化完成'); showRoomStatus(); } }; websocket.onmessage = async (event) => { try { const message = JSON.parse(event.data); console.log('收到WebSocket消息:', message); await handleWebSocketMessage(message); } catch (error) { console.error('解析WebSocket消息失败:', error, event.data); } }; websocket.onerror = (error) => { console.error('WebSocket错误:', error); isConnecting = false; updateConnectionStatus(false); updateP2PStatus(false); showNotification('WebSocket连接失败,请检查网络连接', 'error'); }; websocket.onclose = (event) => { console.log('WebSocket连接已关闭, 代码:', event.code, '原因:', event.reason); isConnecting = false; updateConnectionStatus(false); updateP2PStatus(false); websocket = null; // 如果不是正常关闭且还需要连接,尝试重连 if (event.code !== 1000 && currentPickupCode && !isConnecting) { console.log('WebSocket异常关闭,5秒后尝试重连'); showNotification('连接断开,5秒后自动重连...', 'info'); setTimeout(() => { if (currentPickupCode && !websocket && !isConnecting) { console.log('尝试重新连接WebSocket'); connectWebSocket(); } }, 5000); } }; // 设置连接超时 setTimeout(() => { if (websocket && websocket.readyState === WebSocket.CONNECTING) { console.log('WebSocket连接超时'); websocket.close(); showNotification('连接超时,请重试', 'error'); } }, 10000); } catch (error) { console.error('创建WebSocket连接失败:', error); isConnecting = false; showNotification('无法创建WebSocket连接: ' + error.message, 'error'); } } // 处理WebSocket消息 async function handleWebSocketMessage(message) { console.log('处理WebSocket消息:', message.type, message); switch (message.type) { case 'file-list': // 接收到文件列表 if (currentRole === 'receiver') { displayReceiverFiles(message.payload.files); } break; case 'file-list-updated': // 文件列表更新(通知接收方) if (currentRole === 'receiver') { console.log('收到文件列表更新通知'); displayReceiverFiles(message.payload.files); showNotification('文件列表已更新,发现新文件!', 'info'); } break; case 'room-status': // 房间状态更新 updateRoomStatus(message.payload); break; case 'new-receiver': // 新接收方加入 if (currentRole === 'sender') { console.log('新接收方加入:', message.payload.client_id); showNotification('有新用户加入房间', 'info'); } break; case 'new-sender': // 新发送方加入 if (currentRole === 'receiver') { console.log('新发送方加入:', message.payload.client_id); } break; case 'client-left': // 客户端离开 console.log('客户端离开:', message.payload.client_id, message.payload.role); break; case 'file-request': // 文件请求 if (currentRole === 'sender') { await handleFileRequest(message.payload); } break; case 'file-info': // 文件信息(接收方) if (currentRole === 'receiver') { initFileTransfer(message.payload); } break; case 'file-chunk': // 文件数据块(接收方) if (currentRole === 'receiver') { receiveFileChunk(message.payload); } break; case 'file-complete': // 文件传输完成(接收方) if (currentRole === 'receiver') { completeFileDownload(message.payload.file_id); } break; default: console.log('未知消息类型:', message.type); } } // 更新连接状态 function updateConnectionStatus(connected) { const senderStatus = document.getElementById('senderStatus'); const receiverStatus = document.getElementById('receiverStatus'); if (currentRole === 'sender' && senderStatus) { senderStatus.innerHTML = connected ? `
WebSocket已连接
` : `
连接断开
`; } if (currentRole === 'receiver' && receiverStatus) { // 接收方的状态更新由updateP2PStatus处理 } } // 更新房间状态显示 function updateRoomStatus(status) { console.log('更新房间状态:', status); const totalClients = status.sender_count + status.receiver_count; // 更新发送方界面的房间状态 if (currentRole === 'sender') { const onlineCountEl = document.getElementById('onlineCount'); const senderCountEl = document.getElementById('senderCount'); const receiverCountEl = document.getElementById('receiverCount'); if (onlineCountEl) onlineCountEl.textContent = totalClients; if (senderCountEl) senderCountEl.textContent = status.sender_count; if (receiverCountEl) receiverCountEl.textContent = status.receiver_count; const clientsList = document.getElementById('clientsList'); if (clientsList) { clientsList.innerHTML = ''; status.clients.forEach(client => { if (client.id !== currentClientId) { // 不显示自己 const clientDiv = document.createElement('div'); clientDiv.className = 'text-xs text-blue-600'; const role = client.role === 'sender' ? '📤 发送' : '📥 接收'; const joinTime = new Date(client.joined_at).toLocaleTimeString(); clientDiv.textContent = `${role} - ${joinTime}`; clientsList.appendChild(clientDiv); } }); } // 显示房间状态区域 const roomStatusSection = document.getElementById('roomStatusSection'); if (roomStatusSection) { roomStatusSection.classList.remove('hidden'); } } // 更新接收方界面的房间状态 if (currentRole === 'receiver') { const receiverOnlineCountEl = document.getElementById('receiverOnlineCount'); const receiverSenderCountEl = document.getElementById('receiverSenderCount'); const receiverReceiverCountEl = document.getElementById('receiverReceiverCount'); if (receiverOnlineCountEl) receiverOnlineCountEl.textContent = totalClients; if (receiverSenderCountEl) receiverSenderCountEl.textContent = status.sender_count; if (receiverReceiverCountEl) receiverReceiverCountEl.textContent = status.receiver_count; const clientsList = document.getElementById('receiverClientsList'); if (clientsList) { clientsList.innerHTML = ''; status.clients.forEach(client => { if (client.id !== currentClientId) { // 不显示自己 const clientDiv = document.createElement('div'); clientDiv.className = 'text-xs text-blue-600'; const role = client.role === 'sender' ? '📤 发送' : '📥 接收'; const joinTime = new Date(client.joined_at).toLocaleTimeString(); clientDiv.textContent = `${role} - ${joinTime}`; clientsList.appendChild(clientDiv); } }); } } } // 显示房间状态区域 function showRoomStatus() { if (currentRole === 'sender') { const roomStatusSection = document.getElementById('roomStatusSection'); if (roomStatusSection) { roomStatusSection.classList.remove('hidden'); } } } // 处理文件请求(简化版本,通过WebSocket发送文件) async function handleFileRequest(payload) { console.log('处理文件请求:', payload); const fileId = payload.file_id; const requesterId = payload.requester; 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, '给客户端:', requesterId); showNotification(`开始发送文件: ${file.name}`, 'info'); // 通过WebSocket发送文件(简化实现) await sendFileViaWebSocket(file, requestId); } // 通过WebSocket发送文件 async function sendFileViaWebSocket(file, requestId) { // 发送文件信息 const fileInfo = { type: 'file-info', payload: { file_id: requestId, name: file.name, size: file.size, mime_type: file.type, last_modified: file.lastModified } }; websocket.send(JSON.stringify(fileInfo)); // 分块发送文件 const chunkSize = 65536; // 64KB chunks (提高传输速度) let offset = 0; const sendChunk = () => { if (offset >= file.size) { // 发送完成消息 const completeMsg = { type: 'file-complete', payload: { file_id: requestId } }; websocket.send(JSON.stringify(completeMsg)); console.log('文件发送完成:', file.name); showNotification(`文件发送完成: ${file.name}`, 'success'); return; } const slice = file.slice(offset, offset + chunkSize); const reader = new FileReader(); reader.onload = (e) => { const chunk = e.target.result; // 发送块元数据和数据 const chunkData = { type: 'file-chunk', payload: { file_id: requestId, offset: offset, data: Array.from(new Uint8Array(chunk)), // 转换为数组以便JSON序列化 is_last: offset + chunk.byteLength >= file.size } }; websocket.send(JSON.stringify(chunkData)); offset += chunk.byteLength; // 减少延时提高传输速度 setTimeout(sendChunk, 10); // 从50ms减少到10ms }; reader.readAsArrayBuffer(slice); }; sendChunk(); } // 初始化文件传输(接收方) function initFileTransfer(fileInfo) { console.log('初始化文件传输:', fileInfo); const transferKey = fileInfo.file_id; if (!fileTransfers.has(transferKey)) { fileTransfers.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); showTransferProgress(fileInfo.file_id, 'downloading', fileInfo.name); } } // 接收文件数据块(接收方) function receiveFileChunk(chunkData) { const transferKey = chunkData.file_id; const transfer = fileTransfers.get(transferKey); if (!transfer) { console.error('未找到对应的文件传输:', transferKey); return; } // 将数组转换回Uint8Array const chunkArray = new Uint8Array(chunkData.data); // 存储数据块 transfer.chunks.push({ offset: chunkData.offset, data: chunkArray }); transfer.receivedSize += chunkArray.length; // 更新进度 const progress = (transfer.receivedSize / transfer.totalSize) * 100; updateTransferProgress(chunkData.file_id, progress, transfer.receivedSize, transfer.totalSize); console.log(`文件块接收进度: ${progress.toFixed(1)}% (${transfer.receivedSize}/${transfer.totalSize})`); // 检查是否是最后一块 if (chunkData.is_last || transfer.receivedSize >= transfer.totalSize) { console.log('文件接收完成,开始合并数据块'); assembleAndDownloadFile(transferKey); } } // 完成文件下载(接收方) function completeFileDownload(fileId) { console.log('文件传输完成:', fileId); // 这个函数可能不需要,因为在receiveFileChunk中已经处理了完成逻辑 } // 组装文件并触发下载 function assembleAndDownloadFile(transferKey) { const transfer = fileTransfers.get(transferKey); if (!transfer) { console.error('未找到文件传输信息:', transferKey); return; } // 按偏移量排序数据块 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); // 清理传输信息 fileTransfers.delete(transferKey); // 显示完成状态 hideTransferProgress(transfer.fileId); // 恢复下载按钮 const button = document.querySelector(`button[onclick="downloadFile('${transfer.fileId}')"]`); if (button) { button.disabled = false; button.textContent = '📥 下载'; } const transferTime = (Date.now() - transfer.startTime) / 1000; const speed = (transfer.totalSize / transferTime / 1024 / 1024).toFixed(2); console.log(`文件下载完成: ${transfer.fileName}`); console.log(`传输时间: ${transferTime.toFixed(1)}秒,平均速度: ${speed} MB/s`); // 显示成功消息 showNotification(`文件 "${transfer.fileName}" 下载完成!传输速度: ${speed} MB/s`, 'success'); } // 显示接收方文件列表 function displayReceiverFiles(files) { console.log('displayReceiverFiles被调用, WebSocket状态:', websocket ? websocket.readyState : 'null'); document.getElementById('codeInputSection').classList.add('hidden'); document.getElementById('receiverFilesSection').classList.remove('hidden'); const filesList = document.getElementById('receiverFilesList'); filesList.innerHTML = ''; files.forEach((file, index) => { const fileItem = document.createElement('div'); fileItem.className = 'flex items-center justify-between bg-gray-50 p-3 rounded-lg'; fileItem.innerHTML = `
${getFileIcon(file.type)}
${file.name}
${formatFileSize(file.size)}
`; filesList.appendChild(fileItem); }); // 只有在WebSocket未连接时才显示连接中状态 if (!websocket || websocket.readyState !== WebSocket.OPEN) { console.log('WebSocket未连接,显示连接中状态'); updateP2PStatus(false); } else { console.log('WebSocket已连接,启用下载功能'); updateP2PStatus(true); } } // 下载文件(多人房间版本) function downloadFile(fileId) { if (!websocket || websocket.readyState !== WebSocket.OPEN) { alert('WebSocket连接未建立,请重新连接'); return; } console.log('请求下载文件:', fileId); // 找到文件名(从按钮的父元素中获取) const button = document.querySelector(`button[onclick="downloadFile('${fileId}')"]`); let fileName = fileId; // 默认使用fileId if (button) { const fileNameEl = button.parentElement.querySelector('.font-medium'); if (fileNameEl) { fileName = fileNameEl.textContent; } } // 生成请求ID用于跟踪请求 const requestId = 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); // 通过WebSocket发送文件请求 const request = { type: 'file-request', payload: { file_id: fileId, request_id: requestId } }; websocket.send(JSON.stringify(request)); // 不在这里显示进度条,等收到file-info消息时再显示 // 禁用下载按钮防止重复点击 if (button) { button.disabled = true; button.textContent = '⏳ 请求中...'; } } // 更新P2P连接状态 function updateP2PStatus(connected) { console.log('updateP2PStatus被调用, connected:', connected, 'currentRole:', currentRole); const receiverStatus = document.getElementById('receiverStatus'); const downloadButtons = document.querySelectorAll('button[onclick^="downloadFile"]'); console.log('receiverStatus元素:', receiverStatus); console.log('找到的下载按钮数量:', downloadButtons.length); if (currentRole === 'receiver' && receiverStatus) { if (connected) { console.log('设置为已连接状态'); receiverStatus.innerHTML = `
已连接,可以下载文件
`; // 启用下载按钮 downloadButtons.forEach(btn => { console.log('启用下载按钮:', btn); btn.disabled = false; btn.classList.remove('opacity-50', 'cursor-not-allowed'); btn.classList.add('hover:bg-blue-600'); if (btn.textContent === '⏳ 请求中...') { btn.textContent = '📥 下载'; } }); } else { console.log('设置为连接中状态'); receiverStatus.innerHTML = `
正在建立连接...
`; // 禁用下载按钮 downloadButtons.forEach(btn => { btn.disabled = true; btn.classList.add('opacity-50', 'cursor-not-allowed'); btn.classList.remove('hover:bg-blue-600'); }); } } else { console.log('条件不满足: currentRole=' + currentRole + ', receiverStatus存在=' + !!receiverStatus); } } // 显示传输进度 function showTransferProgress(fileId, type, fileName = null) { const progressContainer = document.getElementById('transferProgress'); const progressList = document.getElementById('progressList'); if (!progressContainer || !progressList) return; // 如果已经存在相同文件ID的进度条,先删除 const existingProgress = document.getElementById(`progress-${fileId}`); if (existingProgress) { existingProgress.remove(); } progressContainer.classList.remove('hidden'); const displayName = fileName || fileId; const progressItem = document.createElement('div'); progressItem.id = `progress-${fileId}`; progressItem.className = 'bg-gray-100 p-3 rounded-lg'; progressItem.innerHTML = `
文件: ${displayName} ${type === 'uploading' ? '上传中' : '下载中'}
0%
`; progressList.appendChild(progressItem); } // 更新传输进度 function updateTransferProgress(fileId, progress, received, total) { const progressItem = document.getElementById(`progress-${fileId}`); if (!progressItem) return; const progressBar = progressItem.querySelector('.bg-blue-500'); const progressText = progressItem.querySelector('.text-sm.text-gray-500:last-child'); if (progressBar && progressText) { progressBar.style.width = `${progress}%`; progressText.textContent = `${progress.toFixed(1)}% (${formatFileSize(received)}/${formatFileSize(total)})`; } } // 隐藏传输进度 function hideTransferProgress(fileId) { const progressItem = document.getElementById(`progress-${fileId}`); if (progressItem) { progressItem.remove(); // 如果没有其他传输,隐藏进度容器 const progressList = document.getElementById('progressList'); if (progressList && progressList.children.length === 0) { document.getElementById('transferProgress').classList.add('hidden'); } } } // 显示通知 function showNotification(message, type = 'info') { // 创建通知元素 const notification = document.createElement('div'); notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 max-w-sm ${ type === 'success' ? 'bg-green-500 text-white' : type === 'error' ? 'bg-red-500 text-white' : 'bg-blue-500 text-white' }`; notification.textContent = message; document.body.appendChild(notification); // 3秒后自动移除 setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 3000); } // 工具函数 function getFileIcon(mimeType) { if (mimeType.startsWith('image/')) return '🖼️'; if (mimeType.startsWith('video/')) return '🎥'; if (mimeType.startsWith('audio/')) return '🎵'; if (mimeType.includes('pdf')) return '📄'; if (mimeType.includes('zip') || mimeType.includes('rar')) return '📦'; return '📄'; } function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 页面卸载时清理 window.addEventListener('beforeunload', () => { if (websocket) { websocket.close(); } clientConnections.forEach((conn) => { if (conn.peerConnection) { conn.peerConnection.close(); } }); });