// 工具函数 if (typeof marked !== 'undefined') marked.setOptions({ breaks: true, gfm: true, sanitize: false }); function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function compressImage(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); let width = img.width, height = img.height; if (width > 1200) { height = height * (1200 / width); width = 1200; } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => resolve(blob), 'image/jpeg', 0.8); }; img.src = e.target.result; }; reader.readAsDataURL(file); }); } // 消息渲染 const messagesContainer = document.getElementById('messages-container'); function renderMessageHTML(msg) { const currentUser = localStorage.getItem('currentUser'); const isMe = msg.user === currentUser; const name = msg.user === '小财' ? '小财' : msg.user; let contentHtml = msg.text ? escapeHtml(msg.text) : ''; if (msg.user === '小财' && typeof marked !== 'undefined' && msg.text) { try { contentHtml = marked.parse(msg.text); } catch(e) {} } let attachHtml = ''; if (msg.attachments && msg.attachments.length) { try { const files = typeof msg.attachments === 'string' ? JSON.parse(msg.attachments) : msg.attachments; attachHtml = files.map(f => { const isImage = /\.(jpg|jpeg|png|gif)$/i.test(f); if (isImage) return `${f}`; return `
📎 ${f.split('/').pop()}
`; }).join(''); } catch(e) {} } return `
${name}
${contentHtml}${attachHtml}
${msg.timestamp}
`; } function renderAllMessages(msgs) { Store.messageCache = msgs || []; messagesContainer.innerHTML = Store.messageCache.map(renderMessageHTML).join(''); scrollToBottom(); bindMessageEvents(); } function appendMessage(msg) { // 去重:轮询和 WebSocket 可能投递同一条消息 if (Store.messageCache.some(m => m.id === msg.id)) return; Store.messageCache.push(msg); messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(msg)); scrollToBottom(); bindMessageEvents(); clearTempBubble(); } function replaceMessage(msgId, newMsg) { const idx = Store.messageCache.findIndex(m => m.id == msgId); if (idx !== -1) { Store.messageCache[idx] = newMsg; const oldEl = messagesContainer.querySelector(`[data-msg-id="${msgId}"]`); if (oldEl) oldEl.outerHTML = renderMessageHTML(newMsg); } else { appendMessage(newMsg); } bindMessageEvents(); } function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; } // 图片/气泡单击 function bindMessageEvents() { document.querySelectorAll('.msg-img').forEach(img => { img.removeEventListener('click', onImageClick); img.addEventListener('click', onImageClick); }); document.querySelectorAll('.msg-bubble').forEach(bubble => { bubble.removeEventListener('click', onBubbleClick); bubble.addEventListener('click', onBubbleClick); }); } function onImageClick(e) { e.stopPropagation(); const overlay = document.createElement('div'); overlay.className = 'fullscreen-overlay'; overlay.innerHTML = ''; overlay.addEventListener('click', () => overlay.remove()); document.body.appendChild(overlay); } function onImgThumbDblClick(img) { const overlay = document.createElement('div'); overlay.className = 'fullscreen-overlay'; overlay.innerHTML = ''; overlay.addEventListener('click', () => overlay.remove()); document.body.appendChild(overlay); var fsImg = overlay.querySelector('#fs-img'); var pressTimer; fsImg.addEventListener('touchstart', function() { pressTimer = setTimeout(function() { var a = document.createElement('a'); a.href = fsImg.src; a.download = fsImg.src.split('/').pop().split('?')[0] || 'image.jpg'; a.click(); clearTimeout(pressTimer); }, 800); }); fsImg.addEventListener('touchend', function() { clearTimeout(pressTimer); }); fsImg.addEventListener('touchmove', function() { clearTimeout(pressTimer); }); } function onBubbleClick(e) { e.stopPropagation(); const bubble = e.currentTarget; const clone = bubble.cloneNode(true); const overlay = document.createElement('div'); overlay.className = 'fullscreen-overlay'; const container = document.createElement('div'); container.className = 'fullscreen-text'; container.innerHTML = clone.innerHTML; overlay.appendChild(container); overlay.addEventListener('click', () => overlay.remove()); document.body.appendChild(overlay); } // 临时气泡 + 降级轮询 function insertTempBubble() { clearTempBubble(); const tempId = 'temp_' + Date.now(); const tempMsg = { id: tempId, user: '小财', text: '输入中...', attachments: [], timestamp: '', isTemp: true }; Store.messageCache.push(tempMsg); messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(tempMsg)); scrollToBottom(); Store.tempBubbleTimer = setTimeout(() => { clearTempBubble(); }, 8000); if (Store.pollTimer) clearInterval(Store.pollTimer); let pollCount = 0; Store.pollTimer = setInterval(async () => { pollCount++; if (!Store.currentRoom || pollCount > 5) { clearInterval(Store.pollTimer); Store.pollTimer = null; return; } try { const token = Store.getToken(); const res = await fetch('/api/rooms/' + Store.currentRoom + '/messages', { headers: { 'Authorization': `Bearer ${token}` } }); const msgs = await res.json(); const lastCacheId = Store.messageCache.filter(m => !m.isTemp && !m.isPlaceholder).slice(-1)[0]?.id; const newMsgs = msgs.filter(m => !lastCacheId || m.id > lastCacheId); if (newMsgs.length) { clearInterval(Store.pollTimer); Store.pollTimer = null; clearTempBubble(); newMsgs.forEach(m => appendMessage(m)); } } catch(e) {} }, 2000); } function clearTempBubble() { if (Store.tempBubbleTimer) { clearTimeout(Store.tempBubbleTimer); Store.tempBubbleTimer = null; } if (Store.pollTimer) { clearInterval(Store.pollTimer); Store.pollTimer = null; } Store.messageCache = Store.messageCache.filter(m => !m.isTemp); const tempEls = messagesContainer.querySelectorAll('[data-msg-id^="temp_"]'); tempEls.forEach(el => el.remove()); }