235 lines
8.9 KiB
JavaScript
235 lines
8.9 KiB
JavaScript
// 工具函数
|
|
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 `<img class="msg-img" src="${f}" alt="${f}" loading="lazy">`;
|
|
return `<div><a href="${f}" target="_blank">📎 ${f.split('/').pop()}</a></div>`;
|
|
}).join('');
|
|
} catch(e) {}
|
|
}
|
|
return `
|
|
<div class="msg ${isMe ? 'me' : 'other'}" data-msg-id="${msg.id}">
|
|
<div class="msg-user">${name}</div>
|
|
<div class="msg-bubble">${contentHtml}${attachHtml}</div>
|
|
<div class="msg-time">${msg.timestamp}</div>
|
|
</div>`;
|
|
}
|
|
|
|
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(function(img) {
|
|
img.removeEventListener('click', onImageClick);
|
|
img.addEventListener('click', onImageClick);
|
|
});
|
|
document.querySelectorAll('.msg-bubble').forEach(function(bubble) {
|
|
bubble.removeEventListener('dblclick', onBubbleClick);
|
|
bubble.addEventListener('dblclick', onBubbleClick);
|
|
});
|
|
}
|
|
|
|
function onImageClick(e) {
|
|
e.stopPropagation();
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'fullscreen-overlay';
|
|
overlay.innerHTML = '<img src="' + e.target.src + '" alt="" style="transition: transform 0.1s; transform-origin: 0 0;">';
|
|
overlay.addEventListener('click', function(ev) { if (ev.target === overlay) overlay.remove(); });
|
|
document.body.appendChild(overlay);
|
|
initPinchZoom(overlay.querySelector('img'));
|
|
}
|
|
|
|
function onImgThumbClick(img) {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'fullscreen-overlay';
|
|
overlay.innerHTML = '<img src="' + img.src + '" style="max-width:100%;max-height:100%;object-fit:contain;transition: transform 0.1s; transform-origin: 0 0;" alt="">';
|
|
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 showTempBubble(text) {
|
|
clearTempBubble();
|
|
var tempId = 'temp_' + Date.now();
|
|
var tempMsg = { id: tempId, user: '小财', text: text, attachments: [], timestamp: '', isTemp: true };
|
|
Store.messageCache.push(tempMsg);
|
|
messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(tempMsg));
|
|
scrollToBottom();
|
|
}
|
|
|
|
// 双指缩放 + 拖拽
|
|
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(); }
|
|
});
|
|
}
|