// 全局变量
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; // 待处理的数据块元数据
// 通知系统
function showNotification(message, type = 'info', duration = 5000) {
// 移除现有通知
const existing = document.querySelector('.notification');
if (existing) {
existing.remove();
}
const notification = document.createElement('div');
notification.className = `notification ${type}`;
const icons = {
success: ``,
error: ``,
warning: ``,
info: ``
};
notification.innerHTML = `
${icons[type]}
${message}
`;
document.body.appendChild(notification);
// 动画显示
setTimeout(() => notification.classList.add('show'), 100);
// 自动消失
if (duration > 0) {
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, duration);
}
}
// 复制取件码增强
function copyPickupCode(event) {
// 阻止事件冒泡
if (event) {
event.stopPropagation();
event.preventDefault();
}
const code = document.getElementById('pickupCodeDisplay').textContent;
navigator.clipboard.writeText(code).then(() => {
showNotification('取件码已复制到剪贴板!', 'success', 3000);
// 添加视觉反馈
const codeDisplay = document.getElementById('pickupCodeDisplay');
const originalText = codeDisplay.textContent;
codeDisplay.textContent = '✅ 已复制';
codeDisplay.classList.add('success-bounce');
setTimeout(() => {
codeDisplay.textContent = originalText;
codeDisplay.classList.remove('success-bounce');
}, 1500);
}).catch(() => {
showNotification('复制失败,请手动复制取件码', 'error');
});
}
// 复制取件链接
function copyPickupLink(event) {
// 阻止事件冒泡
if (event) {
event.stopPropagation();
event.preventDefault();
}
const link = document.getElementById('pickupLinkDisplay').textContent;
navigator.clipboard.writeText(link).then(() => {
showNotification('取件链接已复制到剪贴板!', 'success', 3000);
// 添加视觉反馈
const linkDisplay = document.getElementById('pickupLinkDisplay');
const originalText = linkDisplay.textContent;
linkDisplay.textContent = '✅ 已复制';
linkDisplay.classList.add('success-bounce');
setTimeout(() => {
linkDisplay.textContent = originalText;
linkDisplay.classList.remove('success-bounce');
}, 1500);
}).catch(() => {
showNotification('复制失败,请手动复制链接', 'error');
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
initializeEventListeners();
initializeAnimations();
handleUrlParams(); // 处理URL参数
});
// 标签页切换函数
function switchTab(tab) {
// 移除所有标签页的活动状态
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active', 'border-blue-500', 'bg-blue-50', 'text-blue-600', 'border-green-500', 'bg-green-50', 'text-green-600');
btn.classList.add('border-transparent', 'text-gray-600');
});
// 隐藏所有标签页内容
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
content.classList.remove('active');
});
// 激活选中的标签页
if (tab === 'send') {
const sendTab = document.getElementById('sendTab');
const sendContent = document.getElementById('sendContent');
sendTab.classList.remove('border-transparent', 'text-gray-600');
sendTab.classList.add('active', 'border-blue-500', 'bg-blue-50', 'text-blue-600');
sendContent.classList.remove('hidden');
sendContent.classList.add('active');
} else if (tab === 'receive') {
const receiveTab = document.getElementById('receiveTab');
const receiveContent = document.getElementById('receiveContent');
receiveTab.classList.remove('border-transparent', 'text-gray-600');
receiveTab.classList.add('active', 'border-green-500', 'bg-green-50', 'text-green-600');
receiveContent.classList.remove('hidden');
receiveContent.classList.add('active');
}
}
// 处理URL参数
function handleUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
if (code && code.length === 6) {
// 切换到接收标签页
switchTab('receive');
// 自动填入取件码
const codeInput = document.getElementById('pickupCodeInput');
codeInput.value = code.toUpperCase();
// 触发输入事件以应用样式
codeInput.dispatchEvent(new Event('input'));
// 显示通知并自动连接
showNotification('检测到取件码,正在自动连接...', 'info', 3000);
setTimeout(() => {
joinRoom();
}, 1000);
}
}
// 初始化动画效果
function initializeAnimations() {
// 为主要元素添加进入动画
const leftPanel = document.querySelector('.lg\\:grid-cols-2 > div:first-child');
const rightPanel = document.querySelector('.lg\\:grid-cols-2 > div:last-child');
if (leftPanel) {
leftPanel.classList.add('slide-in-left');
}
if (rightPanel) {
rightPanel.classList.add('slide-in-right');
}
// 标题动画
const title = document.querySelector('h1');
if (title) {
title.classList.add('fade-in-down');
}
// 为按钮添加点击反馈效果
const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
button.classList.add('click-feedback');
// 添加悬停音效反馈(视觉)
button.addEventListener('mouseenter', () => {
if (!button.disabled) {
button.style.transform = 'translateY(-1px) scale(1.02)';
}
});
button.addEventListener('mouseleave', () => {
button.style.transform = '';
});
});
}
// 初始化事件监听器
function initializeEventListeners() {
// 文件选择事件
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
// 取件码输入事件 - 增强用户体验
const codeInput = document.getElementById('pickupCodeInput');
codeInput.addEventListener('input', (e) => {
// 只允许字母和数字,自动转大写
let value = e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase();
e.target.value = value;
// 视觉反馈
if (value.length > 0) {
e.target.classList.remove('border-gray-200');
e.target.classList.add('border-blue-300');
} else {
e.target.classList.add('border-gray-200');
e.target.classList.remove('border-blue-300');
}
// 长度验证和自动连接
if (value.length === 6) {
e.target.classList.remove('border-blue-300');
e.target.classList.add('border-green-400');
showNotification('取件码格式正确,正在连接...', 'info', 3000);
// 自动连接
setTimeout(() => joinRoom(), 500);
} else if (value.length > 6) {
e.target.value = value.substring(0, 6);
}
});
// 取件码输入框焦点事件
codeInput.addEventListener('focus', () => {
codeInput.classList.add('ring-4', 'ring-blue-100');
});
codeInput.addEventListener('blur', () => {
codeInput.classList.remove('ring-4', 'ring-blue-100');
});
// 回车键快速连接
codeInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && e.target.value.length === 6) {
joinRoom();
}
});
// 拖拽上传
setupDragAndDrop();
}
// 设置拖拽上传
function setupDragAndDrop() {
const dropArea = document.getElementById('fileDropZone');
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('drag-over');
});
dropArea.addEventListener('dragenter', (e) => {
e.preventDefault();
dropArea.classList.add('drag-over');
});
dropArea.addEventListener('dragleave', (e) => {
e.preventDefault();
// 只有当鼠标离开dropArea本身时才移除样式
if (!dropArea.contains(e.relatedTarget)) {
dropArea.classList.remove('drag-over');
}
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
// 添加新文件到现有列表
selectedFiles = [...selectedFiles, ...files];
displaySelectedFiles();
// 显示成功动画
dropArea.classList.add('success-bounce');
setTimeout(() => {
dropArea.classList.remove('success-bounce');
}, 1000);
// 如果已经生成了取件码,自动更新房间文件列表
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() {
console.log('displaySelectedFiles called, selectedFiles count:', selectedFiles.length);
const fileDropZone = document.getElementById('fileDropZone');
const fileListArea = document.getElementById('fileListArea');
const filesList = document.getElementById('filesList');
const fileCount = document.getElementById('fileCount');
console.log('Elements found:', {
fileDropZone: !!fileDropZone,
fileListArea: !!fileListArea,
filesList: !!filesList,
fileCount: !!fileCount
});
if (selectedFiles.length === 0) {
fileDropZone.style.display = 'block';
fileListArea.classList.add('hidden');
return;
}
// 隐藏初始选择区域,显示文件列表区域
fileDropZone.style.display = 'none';
fileListArea.classList.remove('hidden');
fileListArea.classList.add('fade-in-up');
// 更新文件计数
if (fileCount) {
fileCount.textContent = `${selectedFiles.length} 个文件`;
}
filesList.innerHTML = '';
selectedFiles.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item flex items-center justify-between bg-gray-50 p-2 rounded-lg border hover:shadow-sm';
// 安全地获取文件信息
const fileType = file.type || 'application/octet-stream';
const fileName = file.name || '未知文件';
const fileSize = file.size || 0;
fileItem.innerHTML = `
${getFileIcon(fileType)}
${fileName}
${formatFileSize(fileSize)}
`;
filesList.appendChild(fileItem);
});
}
// 处理拖拽区域点击
function handleDropZoneClick(event) {
event.stopPropagation();
document.getElementById('fileInput').click();
}
// 添加更多文件
function addMoreFiles() {
document.getElementById('fileInput').click();
}
// 移除文件
function removeFile(index, event) {
// 阻止事件冒泡
if (event) {
event.stopPropagation();
event.preventDefault();
}
selectedFiles.splice(index, 1);
// 如果没有文件了,回到初始选择状态
if (selectedFiles.length === 0) {
const fileDropZone = document.getElementById('fileDropZone');
const fileListArea = document.getElementById('fileListArea');
fileDropZone.style.display = 'block';
fileListArea.classList.add('hidden');
} else {
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) {
const pickupCodeDisplay = document.getElementById('pickupCodeDisplay');
const pickupLinkDisplay = document.getElementById('pickupLinkDisplay');
pickupCodeDisplay.textContent = code;
// 生成特定链接
const baseUrl = window.location.origin;
const pickupLink = `${baseUrl}/?code=${code}`;
pickupLinkDisplay.textContent = pickupLink;
document.getElementById('pickupCodeSection').classList.remove('hidden');
// 不隐藏生成取件码按钮,改为"添加更多文件"
const generateBtn = document.getElementById('generateCodeBtn');
generateBtn.textContent = '➕ 添加更多文件';
generateBtn.onclick = addMoreFiles;
}
// 重置发送方
function resetSender(event) {
// 阻止事件冒泡
if (event) {
event.stopPropagation();
event.preventDefault();
}
selectedFiles = [];
currentPickupCode = '';
currentRole = '';
currentClientId = '';
if (websocket) {
websocket.close();
}
// 重置界面
const fileDropZone = document.getElementById('fileDropZone');
const fileListArea = document.getElementById('fileListArea');
const pickupCodeSection = document.getElementById('pickupCodeSection');
const generateBtn = document.getElementById('generateCodeBtn');
const fileInput = document.getElementById('fileInput');
const roomStatusSection = document.getElementById('roomStatusSection');
// 显示初始选择区域
fileDropZone.style.display = 'block';
fileListArea.classList.add('hidden');
pickupCodeSection.classList.add('hidden');
roomStatusSection.classList.add('hidden');
// 重置按钮
generateBtn.textContent = '生成取件码';
generateBtn.onclick = generatePickupCode;
// 清空文件输入
fileInput.value = '';
showNotification('已重置,可以重新选择文件', 'info', 2000);
}
// 加入房间
async function joinRoom() {
const codeInput = document.getElementById('pickupCodeInput');
const code = codeInput.value.trim();
const joinButton = document.querySelector('button[onclick="joinRoom()"]');
// 输入验证
if (code.length !== 6) {
showNotification('请输入6位取件码', 'warning');
codeInput.classList.add('error-shake');
codeInput.focus();
setTimeout(() => codeInput.classList.remove('error-shake'), 500);
return;
}
// 防止重复点击
if (isConnecting) {
return;
}
isConnecting = true;
joinButton.disabled = true;
joinButton.classList.add('loading');
const originalText = joinButton.textContent;
joinButton.textContent = '连接中...';
try {
showNotification('正在验证取件码...', 'info', 3000);
const response = await fetch(`/api/room-info?code=${code}`);
const data = await response.json();
if (data.success) {
currentPickupCode = code;
currentRole = 'receiver';
showNotification('取件码验证成功!正在获取文件列表...', 'success', 3000);
displayReceiverFiles(data.files);
connectWebSocket();
// 隐藏输入界面
document.getElementById('codeInputSection').classList.add('hidden');
} else {
showNotification(data.message || '取件码无效或已过期', 'error');
codeInput.classList.add('error-shake');
setTimeout(() => codeInput.classList.remove('error-shake'), 500);
}
} catch (error) {
console.error('连接失败:', error);
showNotification('连接失败,请检查网络连接或稍后重试', 'error');
codeInput.classList.add('error-shake');
setTimeout(() => codeInput.classList.remove('error-shake'), 500);
} finally {
isConnecting = false;
joinButton.disabled = false;
joinButton.classList.remove('loading');
joinButton.textContent = originalText;
}
}
// 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 = 'file-item flex items-center justify-between bg-gray-50 p-2 rounded-lg border hover:shadow-sm transition-all';
fileItem.innerHTML = `
${getFileIcon(file.type)}
${file.name}
${formatFileSize(file.size)}
`;
filesList.appendChild(fileItem);
});
// 显示文件列表后,检查连接状态
console.log('文件列表显示完成,当前WebSocket状态:', websocket ? websocket.readyState : 'null');
// 延迟一点检查状态,确保DOM更新完成
setTimeout(() => {
if (websocket && websocket.readyState === WebSocket.OPEN) {
console.log('WebSocket已连接,启用下载功能');
updateP2PStatus(true);
} else {
console.log('WebSocket未连接,显示连接中状态');
updateP2PStatus(false);
}
}, 100);
}
// 下载文件(多人房间版本)
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-green-600');
// 更新按钮内容
const svg = btn.querySelector('svg');
if (svg) {
svg.innerHTML = ``;
}
const textNode = btn.childNodes[btn.childNodes.length - 1];
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
textNode.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-green-600');
// 更新按钮内容为等待状态
const svg = btn.querySelector('svg');
if (svg) {
svg.innerHTML = ``;
}
const textNode = btn.childNodes[btn.childNodes.length - 1];
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
textNode.textContent = '等待连接';
}
});
}
} 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');
progressContainer.classList.add('fade-in-up');
const displayName = fileName || fileId;
const progressItem = document.createElement('div');
progressItem.id = `progress-${fileId}`;
progressItem.className = 'bg-white border border-gray-200 p-4 rounded-xl shadow-sm';
progressItem.innerHTML = `
${displayName}
${type === 'uploading' ? '正在发送' : '正在接收'}
`;
progressList.appendChild(progressItem);
}
// 更新传输进度
function updateTransferProgress(fileId, progress, received, total) {
const progressBar = document.getElementById(`progress-bar-${fileId}`);
const progressPercent = document.getElementById(`progress-percent-${fileId}`);
const progressSize = document.getElementById(`progress-size-${fileId}`);
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
if (progressPercent) {
progressPercent.textContent = `${progress.toFixed(1)}%`;
}
if (progressSize) {
progressSize.textContent = `${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();
}
});
});