PeerJS 聊天室 - 文件传输
生成我的ID <div id="username-setup" style="display: none;">
<input type="text" id="username-input" placeholder="设置用户名(如:张三)">
<button id="confirm-username-btn">确认用户名</button>
</div>
<div>
<input type="text" id="peer-id" placeholder="输入对方ID">
<button id="connect-btn" disabled>连接</button>
</div>
</div>
<div id="chat-container">
<div id="status">状态: 等待连接...</div>
<div id="chat-messages"></div>
<div>
<input type="text" id="message-input" placeholder="输入消息" disabled>
<button id="send-btn" disabled>发送</button>
<input type="file" id="file-input" accept="image/*,.pdf,.webp,.ggb,.docx,.xlsx,.pptx,.txt,.mp4,.mp3">
<label for="file-input" id="file-input-label">发送文件</label>
<button id="disconnect-btn">断开连接</button>
</div>
<div id="file-progress" style="display: none;">
<div>文件传输中...</div>
<div class="progress-container">
<div id="progress-bar" class="progress-bar"></div>
</div>
</div>
</div>
</div>
<div id="contacts-container">
<div id="contacts-header">
<span>联系人列表</span>
<button id="sound-toggle" title="提示音已开启">🔔</button>
</div>
<ul id="contacts-list"></ul>
</div>
<div id="context-menu">
<div id="recall-menu-item" class="context-menu-item">撤回消息</div>
<div id="delete-menu-item" class="context-menu-item">删除消息</div>
</div>
<script>
// 状态管理
const state = {
peer: null,
myId: null,
username: "匿名用户",
conn: null,
messageHistory: [],
soundEnabled: true,
selectedMessage: null,
contacts: [],
activeConnections: {},
fileTransfer: {
inProgress: false,
fileName: '',
fileType: '',
chunks: [],
receivedChunks: 0,
totalChunks: 0,
sender: ''
}
};
// Web Audio API 相关变量
let audioContext;
let gainNode;
// DOM元素
const generateBtn = document.getElementById('generate-btn');
const myIdDiv = document.getElementById('my-id');
const usernameInput = document.getElementById('username-input');
const confirmUsernameBtn = document.getElementById('confirm-username-btn');
const usernameSetupDiv = document.getElementById('username-setup');
const peerIdInput = document.getElementById('peer-id');
const connectBtn = document.getElementById('connect-btn');
const chatContainer = document.getElementById('chat-container');
const chatMessages = document.getElementById('chat-messages');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const fileInput = document.getElementById('file-input');
const fileInputLabel = document.getElementById('file-input-label');
const disconnectBtn = document.getElementById('disconnect-btn');
const statusDiv = document.getElementById('status');
const contextMenu = document.getElementById('context-menu');
const recallMenuItem = document.getElementById('recall-menu-item');
const deleteMenuItem = document.getElementById('delete-menu-item');
const fileProgress = document.getElementById('file-progress');
const progressBar = document.getElementById('progress-bar');
const contactsList = document.getElementById('contacts-list');
const soundToggle = document.getElementById('sound-toggle');
// 初始化音频
function initAudio() {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
gainNode = audioContext.createGain();
gainNode.gain.value = 0.3;
gainNode.connect(audioContext.destination);
} catch (e) {
console.error('音频初始化失败:', e);
state.soundEnabled = false;
soundToggle.textContent = '🔕';
soundToggle.title = '提示音不可用';
}
}
// 播放提示音
function playNotificationSound() {
if (!state.soundEnabled || !audioContext) return;
try {
const oscillator = audioContext.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.value = 880;
oscillator.connect(gainNode);
oscillator.start();
setTimeout(() => {
oscillator.stop();
}, 200);
} catch (e) {
console.error('播放提示音失败:', e);
}
}
// 切换提示音状态
function toggleSound() {
state.soundEnabled = !state.soundEnabled;
soundToggle.textContent = state.soundEnabled ? '🔔' : '🔕';
soundToggle.title = state.soundEnabled ? '提示音已开启' : '提示音已关闭';
if (state.soundEnabled) {
playNotificationSound();
}
}
// 初始化右键菜单
function initContextMenu() {
// 右键菜单项点击事件
recallMenuItem.addEventListener('click', recallMessage);
deleteMenuItem.addEventListener('click', deleteMessage);
// 点击页面其他地方隐藏右键菜单
document.addEventListener('click', hideContextMenu);
// 右键点击页面其他地方隐藏右键菜单
document.addEventListener('contextmenu', (e) => {
if (e.target !== contextMenu && !contextMenu.contains(e.target)) {
hideContextMenu();
}
});
}
// 隐藏右键菜单
function hideContextMenu() {
contextMenu.style.display = 'none';
state.selectedMessage = null;
}
// 撤回消息
function recallMessage() {
if (!state.selectedMessage || !state.conn) return;
const message = state.selectedMessage;
const isMe = message.sender === state.username;
if (!isMe) {
alert('只能撤回自己发送的消息');
return;
}
try {
state.conn.send({
type: 'message-action',
action: 'recall',
messageId: message.id,
username: state.username
});
message.isRecalled = true;
displayMessages();
} catch (e) {
console.error('撤回消息失败:', e);
addSystemMessage('系统: 撤回消息失败,请检查连接');
}
hideContextMenu();
}
// 删除消息
function deleteMessage() {
if (!state.selectedMessage) return;
const message = state.selectedMessage;
message.isDeleted = true;
displayMessages();
hideContextMenu();
}
// 添加系统消息
function addSystemMessage(text) {
const message = {
id: 'sys-' + Date.now(),
sender: 'system',
content: text,
timestamp: new Date().toLocaleTimeString(),
isSystem: true
};
state.messageHistory.push(message);
displayMessages();
}
// 更新联系人列表
function updateContactsList() {
contactsList.innerHTML = '';
state.contacts.forEach(contact => {
const li = document.createElement('li');
li.innerHTML = `
<div class="contact-name">${contact.username}</div>
<div class="contact-id">${contact.id}</div>
`;
li.dataset.id = contact.id;
li.addEventListener('dblclick', () => {
const confirmDelete = confirm(`确定要删除联系人 ${contact.username} 吗?这将断开与TA的连接`);
if (confirmDelete) {
if (state.activeConnections[contact.id]) {
state.activeConnections[contact.id].close();
delete state.activeConnections[contact.id];
}
if (state.conn && state.conn.peer === contact.id) {
state.conn.close();
state.conn = null;
updateUI();
statusDiv.textContent = "状态: 已断开连接";
}
state.contacts = state.contacts.filter(c => c.id !== contact.id);
updateContactsList();
addSystemMessage(`系统: 已断开与 ${contact.username} 的连接`);
}
});
contactsList.appendChild(li);
});
}
// 添加新联系人
function addContact(id, username) {
const existingContact = state.contacts.find(c => c.id === id);
if (existingContact) {
existingContact.username = username;
} else {
state.contacts.push({ id, username });
}
updateContactsList();
}
// 初始化PeerJS
function initPeer() {
if (typeof Peer === 'undefined') {
alert('PeerJS 库加载失败,请刷新页面或检查网络连接');
return;
}
state.peer = new Peer({
host: '0.peerjs.com',
port: 443,
path: '/',
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
}
});
state.peer.on('open', (id) => {
state.myId = id;
myIdDiv.textContent = `我的ID: ${id}`;
usernameSetupDiv.style.display = 'block';
addSystemMessage(`系统: 你的ID已生成`);
statusDiv.textContent = `状态: 请设置用户名`;
});
state.peer.on('connection', (conn) => {
chatContainer.style.display = 'block';
statusDiv.textContent = `状态: 检测到入站连接...`;
addSystemMessage(`系统: 有人正在连接你`);
conn.on('open', () => {
state.conn = conn;
state.activeConnections[conn.peer] = conn;
updateUI();
addSystemMessage(`系统: 连接已建立`);
statusDiv.textContent = `状态: 已连接`;
conn.send({
type: 'user-info',
username: state.username
});
});
setupConnectionEvents(conn);
});
state.peer.on('error', (err) => {
console.error('PeerJS错误:', err);
let errorMsg = `错误: ${err.type}`;
if (err.type === 'peer-unavailable') {
errorMsg = "对方ID不存在或未在线";
}
addSystemMessage(`系统: ${errorMsg}`);
statusDiv.textContent = `状态: ${errorMsg}`;
});
}
// 设置连接事件监听
function setupConnectionEvents(conn) {
conn.on('data', (data) => {
try {
if (data.type === 'chat') {
state.messageHistory.push(data.message);
displayMessages();
if (state.soundEnabled && data.message.sender !== state.username) {
playNotificationSound();
}
}
else if (data.type === 'user-info') {
addContact(conn.peer, data.username);
const message = {
id: 'user-join',
sender: 'system',
username: data.username,
content: `${data.username} 加入了聊天`,
timestamp: new Date().toLocaleTimeString(),
isSystem: true
};
state.messageHistory.push(message);
displayMessages();
}
else if (data.type === 'file-start' || data.type === 'file-chunk') {
handleReceivedFile(data);
}
else if (data.type === 'message-action') {
handleMessageAction(data);
}
} catch (e) {
console.error('处理数据错误:', e);
}
});
conn.on('close', () => {
addSystemMessage(`系统: 连接已断开`);
if (state.activeConnections[conn.peer]) {
delete state.activeConnections[conn.peer];
}
if (state.conn && state.conn.peer === conn.peer) {
state.conn = null;
updateUI();
statusDiv.textContent = "状态: 已断开连接";
}
});
conn.on('error', (err) => {
console.error('连接错误:', err);
addSystemMessage(`系统: 连接错误 - ${err.message}`);
});
}
// 处理接收到的文件
function handleReceivedFile(data) {
if (data.type === 'file-start') {
state.fileTransfer = {
inProgress: true,
fileName: data.fileName,
fileType: data.fileType,
chunks: new Array(data.totalChunks),
receivedChunks: 0,
totalChunks: data.totalChunks,
sender: data.username
};
fileProgress.style.display = 'block';
progressBar.style.width = '0%';
addSystemMessage(`系统: 开始接收文件 ${data.fileName}`);
}
else if (data.type === 'file-chunk' && state.fileTransfer.inProgress) {
state.fileTransfer.chunks[data.chunkIndex] = data.chunkData;
state.fileTransfer.receivedChunks++;
const progress = Math.round((state.fileTransfer.receivedChunks / state.fileTransfer.totalChunks) * 100);
progressBar.style.width = `${progress}%`;
if (state.fileTransfer.receivedChunks === state.fileTransfer.totalChunks) {
const fileData = state.fileTransfer.chunks.join('');
let previewContent = '';
if (state.fileTransfer.fileType.startsWith('image/')) {
previewContent = `<img src="${fileData}" class="file-preview" alt="${state.fileTransfer.fileName}">`;
} else if (state.fileTransfer.fileType === 'application/pdf') {
previewContent = `
<div class="file-icon">📄</div>
<div>PDF文件: ${state.fileTransfer.fileName}</div>
`;
} else if (state.fileTransfer.fileType === 'image/webp') {
previewContent = `<img src="${fileData}" class="file-preview" alt="${state.fileTransfer.fileName}">`;
} else if (state.fileTransfer.fileType === 'application/octet-stream' &&
state.fileTransfer.fileName.endsWith('.ggb')) {
previewContent = `
<div class="geogebra-icon"></div>
<div>GeoGebra文件: ${state.fileTransfer.fileName}</div>
`;
} else if (state.fileTransfer.fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
state.fileTransfer.fileName.endsWith('.docx')) {
previewContent = `
<div class="word-icon">📝</div>
<div>Word文档: ${state.fileTransfer.fileName}</div>
`;
} else if (state.fileTransfer.fileType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
state.fileTransfer.fileName.endsWith('.xlsx')) {
previewContent = `
<div class="excel-icon">📊</div>
<div>Excel文件: ${state.fileTransfer.fileName}</div>
`;
} else if (state.fileTransfer.fileType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
state.fileTransfer.fileName.endsWith('.pptx')) {
previewContent = `
<div class="ppt-icon">📽️</div>
<div>PowerPoint文件: ${state.fileTransfer.fileName}</div>
`;
} else if (state.fileTransfer.fileType === 'text/plain' ||
state.fileTransfer.fileName.endsWith('.txt')) {
previewContent = `
<div class="text-icon">📝</div>
<div>文本文件: ${state.fileTransfer.fileName}</div>
`;
} else if (state.fileTransfer.fileType === 'video/mp4' ||
state.fileTransfer.fileName.endsWith('.mp4')) {
previewContent = `
<div class="video-icon">🎬</div>
<div>视频文件: ${state.fileTransfer.fileName}</div>
<video controls>
<source src="${fileData}" type="video/mp4">
您的浏览器不支持视频播放
</video>
`;
} else if (state.fileTransfer.fileType === 'audio/mp3' ||
state.fileTransfer.fileName.endsWith('.mp3')) {
previewContent = `
<div class="audio-icon">🎵</div>
<div>音频文件: ${state.fileTransfer.fileName}</div>
<audio controls>
<source src="${fileData}" type="audio/mpeg">
您的浏览器不支持音频播放
</audio>
`;
} else {
previewContent = `
<div class="file-icon">📁</div>
<div>文件: ${state.fileTransfer.fileName}</div>
`;
}
const message = {
id: 'file-' + Date.now(),
sender: state.fileTransfer.sender,
username: state.fileTransfer.sender,
content: '发送了一个文件',
timestamp: new Date().toLocaleTimeString(),
isFile: true,
fileName: state.fileTransfer.fileName,
fileType: state.fileTransfer.fileType,
fileData: fileData,
previewContent: previewContent
};
state.messageHistory.push(message);
displayMessages();
fileProgress.style.display = 'none';
state.fileTransfer = {
inProgress: false
};
if (state.soundEnabled && state.fileTransfer.sender !== state.username) {
playNotificationSound();
}
addSystemMessage(`系统: 文件 ${state.fileTransfer.fileName} 接收完成`);
}
}
}
// 处理消息操作
function handleMessageAction(data) {
if (data.action === 'recall') {
const messageIndex = state.messageHistory.findIndex(msg => msg.id === data.messageId);
if (messageIndex !== -1) {
const message = state.messageHistory[messageIndex];
message.isRecalled = true;
message.recalledBy = data.username;
displayMessages();
}
}
}
// 显示消息
function displayMessages() {
chatMessages.innerHTML = '';
const visibleMessages = state.messageHistory.filter(msg => !msg.isDeleted);
visibleMessages.forEach(msg => {
const isMe = msg.sender === state.username;
const div = document.createElement('div');
div.className = `message ${isMe ? 'my-message' : 'other-message'}`;
div.dataset.messageId = msg.id;
if (msg.isSystem) {
div.innerHTML = `
<div class="system-message">${msg.content}</div>
`;
} else if (msg.isRecalled) {
div.innerHTML = `
<div class="message-username">${msg.username} ${isMe ? '(我)' : ''}</div>
<div class="message-content deleted-message">${isMe ? '你撤回了一条消息' : `${msg.username} 撤回了一条消息`}</div>
<div class="message-time">${msg.timestamp}</div>
`;
} else if (msg.isFile) {
div.innerHTML = `
<div class="message-username">${msg.username} ${isMe ? '(我)' : ''}</div>
<div class="message-content">${msg.isSending ? '正在发送文件...' : msg.content}</div>
<div class="file-message">
${msg.previewContent || ''}
<div class="file-download">
${msg.fileData ? `<a href="${msg.fileData}" download="${msg.fileName}">下载文件</a>` : ''}
</div>
</div>
<div class="message-time">${msg.timestamp}</div>
`;
} else {
div.innerHTML = `
<div class="message-username">${msg.username} ${isMe ? '(我)' : ''}</div>
<div class="message-content">${msg.content}</div>
<div class="message-time">${msg.timestamp}</div>
`;
}
chatMessages.appendChild(div);
if (!msg.isSystem) {
div.addEventListener('contextmenu', (e) => {
showContextMenu(e, msg);
});
}
});
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// 显示右键菜单
function showContextMenu(e, message) {
e.preventDefault();
state.selectedMessage = message;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = e.clientX;
let top = e.clientY;
contextMenu.style.display = 'block';
const menuWidth = contextMenu.offsetWidth;
const menuHeight = contextMenu.offsetHeight;
if (left + menuWidth > viewportWidth) {
left = viewportWidth - menuWidth - 5;
}
if (top + menuHeight > viewportHeight) {
top = viewportHeight - menuHeight - 5;
}
contextMenu.style.left = `${left}px`;
contextMenu.style.top = `${top}px`;
const isMe = message.sender === state.username;
recallMenuItem.style.display = isMe ? 'block' : 'none';
deleteMenuItem.style.display = 'block';
e.stopPropagation();
}
// 更新UI状态
function updateUI() {
const isConnected = state.conn !== null;
messageInput.disabled = !isConnected;
sendBtn.disabled = !isConnected;
fileInput.disabled = !isConnected;
fileInputLabel.style.opacity = isConnected ? '1' : '0.5';
}
// 连接到其他用户
function connectToPeer() {
const peerId = peerIdInput.value.trim();
if (!peerId) {
alert("请输入对方ID");
return;
}
chatContainer.style.display = 'block';
statusDiv.textContent = `状态: 正在连接...`;
if (state.activeConnections[peerId]) {
state.conn = state.activeConnections[peerId];
updateUI();
addSystemMessage(`系统: 已连接到已有会话`);
statusDiv.textContent = `状态: 已连接`;
return;
}
const conn = state.peer.connect(peerId, {
reliable: true,
serialization: 'json'
});
const timeout = setTimeout(() => {
if (!conn.open) {
conn.close();
addSystemMessage("连接超时,请检查网络");
statusDiv.textContent = "状态: 连接超时";
}
}, 10000);
conn.on('open', () => {
clearTimeout(timeout);
state.conn = conn;
state.activeConnections[peerId] = conn;
updateUI();
addSystemMessage(`系统: 已连接`);
statusDiv.textContent = `状态: 已连接`;
conn.send({
type: 'user-info',
username: state.username
});
addContact(peerId, "未知用户");
});
setupConnectionEvents(conn);
}
// 发送消息
function sendMessage() {
const text = messageInput.value.trim();
if (!text || !state.conn) return;
const message = {
id: Date.now().toString(),
sender: state.username,
username: state.username,
content: text,
timestamp: new Date().toLocaleTimeString()
};
state.messageHistory.push(message);
displayMessages();
try {
state.conn.send({
type: 'chat',
message: message
});
} catch (e) {
console.error('发送消息失败:', e);
addSystemMessage('系统: 发送消息失败,请检查连接');
}
messageInput.value = '';
}
// 处理文件选择
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file || !state.conn) return;
// 支持的文件类型
const validTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
'text/plain', // .txt
'video/mp4', // .mp4
'audio/mp3', // .mp3
'application/octet-stream' // 用于.ggb文件
];
// 检查文件类型或扩展名
const fileExt = file.name.split('.').pop().toLowerCase();
if (!validTypes.includes(file.type) &&
fileExt !== 'ggb' &&
fileExt !== 'docx' &&
fileExt !== 'xlsx' &&
fileExt !== 'pptx' &&
fileExt !== 'txt' &&
fileExt !== 'mp4' &&
fileExt !== 'mp3') {
addSystemMessage("系统: 只支持JPG、PNG、GIF、WebP图片、PDF文件、Word(.docx)、Excel(.xlsx)、PowerPoint(.pptx)、文本(.txt)、视频(.mp4)、音频(.mp3)和GeoGebra(.ggb)文件");
return;
}
const MAX_FILE_SIZE = file.type.startsWith('video/') || file.type.startsWith('audio/') || file.name.endsWith('.mp4') || file.name.endsWith('.mp3') ? 900 * 1024 * 1024 // 900MB for video/audio : 15 * 1024 * 1024; // 15MB for others
if (file.size > MAX_FILE_SIZE) {
const maxSize = MAX_FILE_SIZE === 900 * 1024 * 1024 ? '900MB' : '5MB';
addSystemMessage(系统: 文件 "${file.name}" 超过${maxSize}限制);
return;
}
sendFile(file);
fileInput.value = '';
});
// 发送文件
function sendFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
const fileData = e.target.result;
let previewContent = '';
if (file.type.startsWith('image/')) {
previewContent = `<img src="${fileData}" class="file-preview" alt="${file.name}">`;
} else if (file.type === 'application/pdf') {
previewContent = `
<div class="file-icon">📄</div>
<div>PDF文件: ${file.name}</div>
`;
} else if (file.name.endsWith('.ggb')) {
previewContent = `
<div class="geogebra-icon"></div>
<div>GeoGebra文件: ${file.name}</div>
`;
} else if (file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.name.endsWith('.docx')) {
previewContent = `
<div class="word-icon">📝</div>
<div>Word文档: ${file.name}</div>
`;
} else if (file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.name.endsWith('.xlsx')) {
previewContent = `
<div class="excel-icon">📊</div>
<div>Excel文件: ${file.name}</div>
`;
} else if (file.type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
file.name.endsWith('.pptx')) {
previewContent = `
<div class="ppt-icon">📽️</div>
<div>PowerPoint文件: ${file.name}</div>
`;
} else if (file.type === 'text/plain' ||
file.name.endsWith('.txt')) {
previewContent = `
<div class="text-icon">📝</div>
<div>文本文件: ${file.name}</div>
`;
} else if (file.type === 'video/mp4' ||
file.name.endsWith('.mp4')) {
previewContent = `
<div class="video-icon">🎬</div>
<div>视频文件: ${file.name}</div>
<video controls>
<source src="${fileData}" type="video/mp4">
您的浏览器不支持视频播放
</video>
`;
} else if (file.type === 'audio/mp3' ||
file.name.endsWith('.mp3')) {
previewContent = `
<div class="audio-icon">🎵</div>
<div>音频文件: ${file.name}</div>
<audio controls>
<source src="${fileData}" type="audio/mpeg">
您的浏览器不支持音频播放
</audio>
`;
} else {
previewContent = `
<div class="file-icon">📁</div>
<div>文件: ${file.name}</div>
`;
}
const message = {
id: 'file-' + Date.now(),
sender: state.username,
username: state.username,
content: '正在发送文件...',
timestamp: new Date().toLocaleTimeString(),
isFile: true,
fileName: file.name,
fileType: file.type || 'application/octet-stream', // 为.ggb文件设置默认类型
fileData: fileData,
previewContent: previewContent,
isSending: true
};
state.messageHistory.push(message);
displayMessages();
try {
const chunkSize = 16000;
const totalChunks = Math.ceil(fileData.length / chunkSize);
fileProgress.style.display = 'block';
progressBar.style.width = '0%';
state.conn.send({
type: 'file-start',
username: state.username,
fileName: file.name,
fileType: file.type || 'application/octet-stream',
totalChunks: totalChunks
});
let chunksSent = 0;
const sendNextChunk = (index) => {
if (index >= totalChunks) {
fileProgress.style.display = 'none';
const msgIndex = state.messageHistory.findIndex(m => m.id === message.id);
if (msgIndex !== -1) {
state.messageHistory[msgIndex].content = '发送了一个文件';
state.messageHistory[msgIndex].isSending = false;
displayMessages();
}
return;
}
const chunk = fileData.substring(index * chunkSize, (index + 1) * chunkSize);
state.conn.send({
type: 'file-chunk',
chunkIndex: index,
chunkData: chunk
});
chunksSent++;
const progress = Math.round((chunksSent / totalChunks) * 100);
progressBar.style.width = `${progress}%`;
setTimeout(() => sendNextChunk(index + 1), 0);
};
sendNextChunk(0);
} catch (e) {
console.error('发送文件失败:', e);
addSystemMessage('系统: 发送文件失败,请检查连接');
fileProgress.style.display = 'none';
}
};
reader.readAsDataURL(file);
}
// 断开连接
function disconnect() {
if (state.conn) {
state.conn.close();
if (state.activeConnections[state.conn.peer]) {
delete state.activeConnections[state.conn.peer];
}
state.conn = null;
}
updateUI();
addSystemMessage("系统: 已断开连接");
statusDiv.textContent = "状态: 已断开连接";
}
// 设置用户名
function setUsername() {
const username = usernameInput.value.trim();
if (!username) {
alert("用户名不能为空");
return;
}
state.username = username;
usernameSetupDiv.style.display = 'none';
connectBtn.disabled = false;
statusDiv.textContent = `状态: 就绪`;
addSystemMessage(`系统: 你的用户名已设置为 "${username}"`);
}
// 初始化应用
function initApp() {
initAudio();
initContextMenu();
generateBtn.addEventListener('click', initPeer);
confirmUsernameBtn.addEventListener('click', setUsername);
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') setUsername();
});
connectBtn.addEventListener('click', connectToPeer);
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
disconnectBtn.addEventListener('click', disconnect);
soundToggle.addEventListener('click', toggleSound);
setTimeout(() => {
if (state.soundEnabled) {
playNotificationSound();
}
}, 1000);
}
// 启动应用
initApp();
</script>