// 工具函数
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 `
`;
return `
`;
}).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', function(ev) { if (ev.target === overlay) overlay.remove(); });
document.body.appendChild(overlay);
initPinchZoom(overlay.querySelector('img'));
}
function onImgThumbDblClick(img) {
const overlay = document.createElement('div');
overlay.className = 'fullscreen-overlay';
overlay.innerHTML = '
';
overlay.addEventListener('click', function(ev) { if (ev.target === overlay) overlay.remove(); });
document.body.appendChild(overlay);
initPinchZoom(overlay.querySelector('img'));
}
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(function(m) { return !m.isTemp; });
var tempEls = messagesContainer.querySelectorAll('[data-msg-id^="temp_"]');
tempEls.forEach(function(el) { el.remove(); });
}
// 双指缩放 + 拖拽
function initPinchZoom(img) {
var scale = 1, startDist = 0, startScale = 1;
var translateX = 0, translateY = 0;
var startX = 0, startY = 0, startTX = 0, startTY = 0;
var dragging = false;
function update() {
img.style.transform = 'translate(' + translateX + 'px,' + translateY + 'px) scale(' + scale + ')';
}
img.addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
dragging = false;
startDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
startScale = scale;
startTX = translateX;
startTY = translateY;
var cx = (e.touches[0].clientX + e.touches[1].clientX) / 2;
var cy = (e.touches[0].clientY + e.touches[1].clientY) / 2;
startX = cx; startY = cy;
} else if (e.touches.length === 1 && scale > 1) {
dragging = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTX = translateX;
startTY = translateY;
}
});
img.addEventListener('touchmove', function(e) {
if (e.touches.length === 2) {
e.preventDefault();
var dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
scale = Math.max(0.5, Math.min(5, startScale * (dist / startDist)));
var cx = (e.touches[0].clientX + e.touches[1].clientX) / 2;
var cy = (e.touches[0].clientY + e.touches[1].clientY) / 2;
translateX = startTX + (cx - startX);
translateY = startTY + (cy - startY);
update();
} else if (e.touches.length === 1 && dragging) {
translateX = startTX + (e.touches[0].clientX - startX);
translateY = startTY + (e.touches[0].clientY - startY);
update();
}
}, { passive: false });
img.addEventListener('touchend', function() {
dragging = false;
if (scale < 0.6) { scale = 0.5; translateX = 0; translateY = 0; update(); }
if (scale <= 1) { translateX = 0; translateY = 0; update(); }
});
}