class WebWeChat { constructor() { this.socket = null; this.currentUser = null; self.currentContact = null; this.contacts = []; this.activeCall = null; this.callTimer = null; this.callStartTime = null; this.peerConnection = null; this.localStream = null; this.init(); } init() { this.initSocket(); this.bindEvents(); this.loadContacts(); this.setupWebRTC(); // 如果是移动设备(窄屏),默认打开联系人侧栏以便选择对话 if (window.innerWidth <= 768) { this.showContactsSidebar(); } } initSocket() { // 连接WebSocket服务器 this.socket = io({ transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: 5 }); // Socket事件监听 this.socket.on('connect', () => { console.log('WebSocket连接成功'); this.authenticate(); }); this.socket.on('disconnect', () => { console.log('WebSocket断开连接'); }); this.socket.on('user_status', (data) => { this.updateUserStatus(data.account, data.online); }); this.socket.on('private_message', (data) => { this.receiveMessage(data); }); this.socket.on('private_message_sent', (data) => { console.log('消息已发送:', data); }); this.socket.on('incoming_call', (data) => { this.handleIncomingCall(data); }); this.socket.on('call_accepted', (data) => { this.handleCallAccepted(data); }); this.socket.on('call_ended', (data) => { this.handleCallEnded(data); }); this.socket.on('webrtc_signal', (signal) => { this.handleWebRTCSignal(signal); }); this.socket.on('file_received', (data) => { this.showFileNotification(data); }); } authenticate() { // 从localStorage获取用户信息 const account = localStorage.getItem('user_account'); if (account) { this.socket.emit('authenticate', { account: account }); this.currentUser = account; } } bindEvents() { // 发送消息 document.getElementById('sendBtn').addEventListener('click', () => { this.sendMessage(); }); document.getElementById('messageInput').addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); // 语音通话 document.getElementById('voiceCallBtn').addEventListener('click', () => { this.startVoiceCall(); }); document.getElementById('endCallBtn').addEventListener('click', () => { this.endCall(); }); document.getElementById('answerCallBtn').addEventListener('click', () => { this.answerCall(); }); // 文件上传 document.getElementById('fileUploadBtn').addEventListener('click', () => { this.showFileUploadModal(); }); document.getElementById('selectFileBtn').addEventListener('click', () => { document.getElementById('fileInput').click(); }); document.getElementById('fileInput').addEventListener('change', (e) => { this.handleFileSelect(e.target.files[0]); }); // 联系人列表 document.querySelectorAll('[data-tab="contacts"]').forEach(btn => { btn.addEventListener('click', () => { this.showContactsSidebar(); }); }); document.getElementById('closeContactsBtn').addEventListener('click', () => { this.hideContactsSidebar(); }); // 手机端顶部打开联系人按钮 const openContactsBtn = document.getElementById('openContactsBtn'); if (openContactsBtn) { openContactsBtn.addEventListener('click', () => { this.showContactsSidebar(); }); } // 文件列表 document.querySelectorAll('[data-tab="files"]').forEach(btn => { btn.addEventListener('click', () => { this.showFilesSidebar(); }); }); document.getElementById('closeFilesBtn').addEventListener('click', () => { this.hideFilesSidebar(); }); // 关闭文件上传模态框 document.getElementById('closeFileModal').addEventListener('click', () => { this.hideFileUploadModal(); }); // 拖放文件上传 const uploadArea = document.getElementById('uploadArea'); uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.style.borderColor = '#0a84ff'; }); uploadArea.addEventListener('dragleave', () => { uploadArea.style.borderColor = '#38383a'; }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.style.borderColor = '#38383a'; const file = e.dataTransfer.files[0]; if (file) { this.handleFileSelect(file); } }); } loadContacts() { // 从API加载联系人 fetch('/api/users') .then(response => response.json()) .then(contacts => { this.contacts = contacts; this.renderContacts(); }) .catch(error => { console.error('加载联系人失败:', error); }); } renderContacts() { const contactsList = document.getElementById('contactsList'); const chatList = document.getElementById('chatList'); contactsList.innerHTML = ''; chatList.innerHTML = ''; this.contacts.forEach(contact => { // 联系人侧边栏 const contactItem = document.createElement('div'); contactItem.className = 'contact-item'; contactItem.innerHTML = `
${contact.name}
${contact.org}
`; contactItem.addEventListener('click', () => { this.selectContact(contact); this.hideContactsSidebar(); }); contactsList.appendChild(contactItem); // 聊天列表 const chatItem = document.createElement('div'); chatItem.className = 'chat-item'; chatItem.innerHTML = `
${contact.name}
${contact.org}
`; chatItem.addEventListener('click', () => { this.selectContact(contact); }); chatList.appendChild(chatItem); }); } selectContact(contact) { this.currentContact = contact; // 更新UI document.getElementById('partnerName').textContent = contact.name; document.getElementById('partnerStatus').textContent = contact.online ? '在线' : '离线'; document.getElementById('partnerStatus').style.color = contact.online ? '#30d158' : '#6c6c70'; // 启用通话按钮 document.getElementById('voiceCallBtn').disabled = !contact.online; // 显示聊天区域 this.showChatArea(); // 加载聊天历史(这里简化处理) this.loadChatHistory(contact.account); } showChatArea() { // 隐藏欢迎消息,显示聊天消息区域 const welcomeMsg = document.querySelector('.welcome-message'); if (welcomeMsg) { welcomeMsg.style.display = 'none'; } } sendMessage() { const input = document.getElementById('messageInput'); const message = input.value.trim(); if (!message || !this.currentContact) return; // 发送消息 this.socket.emit('private_message', { to: this.currentContact.account, from: this.currentUser, message: message }); // 在界面上显示消息 this.addMessage({ from: this.currentUser, message: message, timestamp: Date.now() / 1000, isOwn: true }); // 清空输入框 input.value = ''; this.adjustTextareaHeight(input); } receiveMessage(data) { // 如果是当前联系人的消息,直接显示 if (data.from === this.currentContact?.account) { this.addMessage({ from: data.from, message: data.message, timestamp: data.timestamp, isOwn: false }); } else { // 显示通知 this.showNotification(`收到来自 ${data.from} 的新消息`); } } addMessage(data) { const messagesContainer = document.getElementById('chatMessages'); const messageDiv = document.createElement('div'); messageDiv.className = `message ${data.isOwn ? 'sent' : 'received'}`; const contact = this.contacts.find(c => c.account === data.from); const contactName = contact ? contact.name : data.from; const time = new Date(data.timestamp * 1000); const timeStr = time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); messageDiv.innerHTML = `
${this.escapeHtml(data.message)}
${timeStr}
`; messagesContainer.appendChild(messageDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } adjustTextareaHeight(textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; } startVoiceCall() { if (!this.currentContact || !this.currentContact.online) return; const callId = 'call_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); this.activeCall = { id: callId, partner: this.currentContact.account, type: 'voice' }; // 显示通话界面 this.showCallInterface(); document.getElementById('callPartnerName').textContent = this.currentContact.name; document.getElementById('callStatus').textContent = '正在呼叫...'; document.getElementById('answerCallBtn').style.display = 'none'; // 通知对方 this.socket.emit('start_call', { call_id: callId, to: this.currentContact.account, from: this.currentUser }); // 开始获取本地音频流 this.startLocalStream(); } handleIncomingCall(data) { // 找到联系人 const contact = this.contacts.find(c => c.account === data.from); if (!contact) return; this.activeCall = { id: data.call_id, partner: data.from, type: 'incoming' }; // 显示来电界面 this.showCallInterface(); document.getElementById('callPartnerName').textContent = contact.name; document.getElementById('callStatus').textContent = '来电中...'; document.getElementById('answerCallBtn').style.display = 'flex'; // 播放铃声 this.playRingtone(); } answerCall() { if (!this.activeCall) return; this.stopRingtone(); // 通知对方已接听 this.socket.emit('answer_call', { call_id: this.activeCall.id }); // 开始通话计时 this.startCallTimer(); document.getElementById('callStatus').textContent = '通话中...'; document.getElementById('answerCallBtn').style.display = 'none'; // 开始获取本地音频流并在就绪后建立WebRTC连接 this.startLocalStream() .then(() => { this.setupWebRTCCall(); }) .catch(() => { console.error('无法获取本地音频,无法建立通话'); }); } endCall() { if (!this.activeCall) return; this.stopRingtone(); this.stopCallTimer(); // 通知对方结束通话 this.socket.emit('end_call', { call_id: this.activeCall.id }); // 关闭本地流 if (this.localStream) { this.localStream.getTracks().forEach(track => track.stop()); this.localStream = null; } // 关闭PeerConnection if (this.peerConnection) { this.peerConnection.close(); this.peerConnection = null; } // 隐藏通话界面 this.hideCallInterface(); this.activeCall = null; } handleCallAccepted(data) { if (!this.activeCall || this.activeCall.id !== data.call_id) return; // 开始通话计时 this.startCallTimer(); document.getElementById('callStatus').textContent = '通话中...'; // 确保本地音频就绪后建立WebRTC连接并创建offer this.startLocalStream() .then(() => { this.setupWebRTCCall(); }) .catch(() => { console.error('无法获取本地音频,无法建立通话'); }); } handleCallEnded(data) { if (this.activeCall && this.activeCall.id === data.call_id) { this.endCall(); } } showCallInterface() { document.getElementById('callInterface').style.display = 'flex'; } hideCallInterface() { document.getElementById('callInterface').style.display = 'none'; } startCallTimer() { this.callStartTime = Date.now(); this.callTimer = setInterval(() => { const elapsed = Date.now() - this.callStartTime; const minutes = Math.floor(elapsed / 60000); const seconds = Math.floor((elapsed % 60000) / 1000); document.getElementById('callTimer').textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; }, 1000); } stopCallTimer() { if (this.callTimer) { clearInterval(this.callTimer); this.callTimer = null; } } playRingtone() { // 这里可以添加铃声播放逻辑 console.log('播放铃声'); } stopRingtone() { // 停止铃声 console.log('停止铃声'); } setupWebRTC() { // 简单的WebRTC配置 this.peerConnectionConfig = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; } startLocalStream() { return navigator.mediaDevices.getUserMedia({ audio: true, video: false }) .then(stream => { this.localStream = stream; const localAudio = document.getElementById('localAudio'); if (localAudio) { localAudio.srcObject = stream; } return stream; }) .catch(error => { console.error('获取音频流失败:', error); throw error; }); } setupWebRTCCall() { this.peerConnection = new RTCPeerConnection(this.peerConnectionConfig); // 添加本地音频流 if (this.localStream) { this.localStream.getTracks().forEach(track => { this.peerConnection.addTrack(track, this.localStream); }); } // 处理远程流 this.peerConnection.ontrack = (event) => { const remoteAudio = document.getElementById('remoteAudio'); if (remoteAudio) { remoteAudio.srcObject = event.streams[0]; } }; // 处理ICE候选 this.peerConnection.onicecandidate = (event) => { if (event.candidate) { this.socket.emit('webrtc_signal', { to: this.activeCall.partner, signal: { type: 'candidate', candidate: event.candidate } }); } }; // 如果是发起方,创建offer if (this.activeCall.type === 'voice') { this.peerConnection.createOffer() .then(offer => this.peerConnection.setLocalDescription(offer)) .then(() => { this.socket.emit('webrtc_signal', { to: this.activeCall.partner, signal: { type: 'offer', sdp: this.peerConnection.localDescription } }); }) .catch(error => { console.error('创建offer失败:', error); }); } } handleWebRTCSignal(signal) { if (!this.peerConnection) return; switch (signal.type) { case 'offer': { const desc = signal.sdp || signal; this.peerConnection.setRemoteDescription(new RTCSessionDescription(desc)) .then(() => this.peerConnection.createAnswer()) .then(answer => this.peerConnection.setLocalDescription(answer)) .then(() => { this.socket.emit('webrtc_signal', { to: this.activeCall.partner, signal: { type: 'answer', sdp: this.peerConnection.localDescription } }); }) .catch(error => { console.error('处理offer失败:', error); }); break; } case 'answer': { const desc = signal.sdp || signal; this.peerConnection.setRemoteDescription(new RTCSessionDescription(desc)) .catch(error => { console.error('处理answer失败:', error); }); break; } case 'candidate': { const candidate = signal.candidate || signal; this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)) .catch(error => { console.error('添加ICE候选失败:', error); }); break; } } } showFileUploadModal() { document.getElementById('fileUploadModal').style.display = 'flex'; } hideFileUploadModal() { document.getElementById('fileUploadModal').style.display = 'none'; document.getElementById('filePreview').innerHTML = ''; document.getElementById('fileInput').value = ''; } async handleFileSelect(file) { if (!file) return; // 显示文件预览 const filePreview = document.getElementById('filePreview'); filePreview.innerHTML = `
${file.name}
${this.formatFileSize(file.size)}
`; // 添加发送按钮事件 document.getElementById('uploadFileBtn').addEventListener('click', async () => { await this.uploadFile(file); }); } async uploadFile(file) { const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { // 发送文件消息 this.socket.emit('file_uploaded', { to: this.currentContact.account, from: this.currentUser, filename: result.filename, url: result.url }); // 显示文件消息 this.addMessage({ from: this.currentUser, message: `[文件] ${result.filename}`, timestamp: Date.now() / 1000, isOwn: true }); this.hideFileUploadModal(); } else { alert(result.message); } } catch (error) { console.error('文件上传失败:', error); alert('文件上传失败'); } } showFileNotification(data) { // 如果是当前联系人的文件,显示消息 if (data.from === this.currentContact?.account) { this.addMessage({ from: data.from, message: `[文件] ${data.filename}`, timestamp: data.timestamp, isOwn: false }); // 添加下载链接 const messagesContainer = document.getElementById('chatMessages'); const fileLink = document.createElement('div'); fileLink.className = 'message received'; fileLink.innerHTML = `
下载文件: ${data.filename}
`; messagesContainer.appendChild(fileLink); messagesContainer.scrollTop = messagesContainer.scrollHeight; } else { this.showNotification(`收到来自 ${data.from} 的文件: ${data.filename}`); } } showContactsSidebar() { document.getElementById('contactsSidebar').classList.add('active'); } hideContactsSidebar() { document.getElementById('contactsSidebar').classList.remove('active'); } showFilesSidebar() { this.loadFiles(); document.getElementById('filesSidebar').classList.add('active'); } hideFilesSidebar() { document.getElementById('filesSidebar').classList.remove('active'); } async loadFiles() { try { const response = await fetch('/api/files'); const files = await response.json(); this.renderFiles(files); } catch (error) { console.error('加载文件失败:', error); } } renderFiles(files) { const filesList = document.getElementById('filesList'); filesList.innerHTML = ''; files.forEach(file => { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = `
${file.filename}
${this.formatFileSize(file.size)}
上传者: ${file.uploader}
`; filesList.appendChild(fileItem); }); } updateUserStatus(account, online) { // 更新联系人状态 const contact = this.contacts.find(c => c.account === account); if (contact) { contact.online = online; this.renderContacts(); // 如果是当前联系人,更新状态显示 if (this.currentContact && this.currentContact.account === account) { document.getElementById('partnerStatus').textContent = online ? '在线' : '离线'; document.getElementById('partnerStatus').style.color = online ? '#30d158' : '#6c6c70'; document.getElementById('voiceCallBtn').disabled = !online; } } } formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } showNotification(message) { // 简单的浏览器通知 if ('Notification' in window && Notification.permission === 'granted') { new Notification('Web WeChat', { body: message, icon: '/static/favicon.ico' }); } } loadChatHistory(account) { // 这里可以加载聊天历史 const messagesContainer = document.getElementById('chatMessages'); messagesContainer.innerHTML = ''; // 暂时显示一条欢迎消息 if (messagesContainer.children.length === 0) { const welcomeMsg = document.createElement('div'); welcomeMsg.className = 'welcome-message'; welcomeMsg.innerHTML = `

开始与 ${this.currentContact.name} 聊天

输入消息开始对话

`; messagesContainer.appendChild(welcomeMsg); } } } // 页面加载完成后初始化应用 document.addEventListener('DOMContentLoaded', () => { window.wechatApp = new WebWeChat(); // 请求通知权限 if ('Notification' in window) { Notification.requestPermission(); } });