// WebSocket 连接管理 function initWebSocket() { const token = Store.getToken(); if (!token) return; const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; Store.ws = new WebSocket(`${protocol}//${location.host}?token=${token}`); Store.ws.onopen = () => { console.log('WebSocket 已连接'); Store.reconnectAttempts = 0; if (Store.currentRoom) Store.ws.send(JSON.stringify({ type: 'join', roomId: Store.currentRoom })); }; Store.ws.onmessage = (e) => { const data = JSON.parse(e.data); console.log('📩 WS 收到:', data.type); if (data.type === 'new_message') { const msg = data.message; if (Store.currentRoom === msg.room_id) appendMessage(msg); if (data.room_preview) { const room = Store.rooms.find(r => r.id === data.room_preview.room_id); if (room) { room.last_message = data.room_preview.last_message; room.last_time = data.room_preview.last_time; renderChatList(); } } } else if (data.type === 'purchase_updated') { if (Store.currentRoom) loadPurchases(); updateSummaryPreview(); } else if (data.type === 'room_created') { loadRooms(); } else if (data.type === 'room_updated') { const room = Store.rooms.find(r => r.id === data.room.id); if (room) { room.name = data.room.name; room.white_list = data.room.white_list; renderChatList(); } } else if (data.type === 'room_deleted') { Store.rooms = Store.rooms.filter(r => r.id !== data.roomId); renderChatList(); if (Store.currentRoom === data.roomId) showList(); } }; Store.ws.onerror = (e) => console.error('WebSocket 错误', e); Store.ws.onclose = (event) => { console.log('WebSocket 关闭,代码:', event.code); if (event.code === 1000) return; scheduleReconnect(); }; } function scheduleReconnect() { if (Store.wsReconnectTimer) clearTimeout(Store.wsReconnectTimer); const delay = Math.min(1000 * Math.pow(2, Store.reconnectAttempts), Store.MAX_RECONNECT_DELAY); Store.reconnectAttempts++; Store.wsReconnectTimer = setTimeout(() => { console.log('尝试重连...'); initWebSocket(); }, delay); } document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { if (!Store.ws || Store.ws.readyState !== WebSocket.OPEN) { if (Store.wsReconnectTimer) clearTimeout(Store.wsReconnectTimer); initWebSocket(); } } }); // 聊天列表 async function loadRooms() { const token = Store.getToken(); const res = await fetch('/api/rooms', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.status === 401) { Store.clearAuth(); location.reload(); return; } Store.rooms = await res.json(); renderChatList(); } function renderChatList() { const container = document.getElementById('chat-list-container'); const isAdmin = localStorage.getItem('isAdmin') === 'true'; const currentUser = localStorage.getItem('currentUser'); container.innerHTML = Store.rooms.map(room => { const lastMsg = room.last_message || '暂无消息'; const lastTime = room.last_time || ''; // 白名单标签仅管理员可见 let whiteListHtml = ''; if (isAdmin && room.white_list && room.white_list.trim() !== '') { const members = room.white_list.split(',').map(s => s.trim()).filter(Boolean).join(', '); whiteListHtml = '' + escapeHtml(members) + ''; } // 编辑按钮:管理员始终可见,普通用户仅自己群聊可见 const canEdit = isAdmin || (room.white_list && room.white_list.trim() === currentUser); const editBtnHtml = canEdit ? '
' + whiteListHtml + '' + '
' : ''; return '
' + '
' + escapeHtml(room.name.charAt(0)) + '
' + '
' + '
' + escapeHtml(room.name) + '
' + '
' + escapeHtml(lastMsg) + '
' + '
' + '
' + '
' + lastTime + '
' + editBtnHtml + '
' + '
'; }).join(''); } async function openPurchasePage(roomId) { Store.currentRoom = roomId; document.getElementById('list-page').classList.add('hidden'); document.getElementById('chat-page').classList.add('hidden'); document.getElementById('purchase-page').classList.remove('hidden'); document.getElementById('purchase-panel-body').innerHTML = '

加载中...

'; var room = Store.rooms.find(function(r) { return r.id === roomId; }); document.getElementById('purchase-page-title').textContent = (room ? room.name : '') + '(清单)'; loadPurchases(); } async function openChatFromPurchase() { if (!Store.currentRoom) return; await openChat(Store.currentRoom, true); } function backToPurchase() { clearTempBubble(); document.getElementById('chat-page').classList.add('hidden'); document.getElementById('purchase-page').classList.remove('hidden'); loadPurchases(); } async function openChat(roomId, fromPurchase = false) { Store.currentRoom = roomId; clearTempBubble(); document.getElementById('list-page').classList.add('hidden'); document.getElementById('purchase-page').classList.add('hidden'); document.getElementById('chat-page').classList.remove('hidden'); const room = Store.rooms.find(r => r.id === roomId); document.getElementById('current-chat-name').textContent = (room ? room.name : '') + '(聊天)'; const token = Store.getToken(); const res = await fetch('/api/rooms/' + roomId + '/messages', { headers: { 'Authorization': 'Bearer ' + token } }); const msgs = await res.json(); renderAllMessages(msgs); if (Store.ws && Store.ws.readyState === WebSocket.OPEN) Store.ws.send(JSON.stringify({ type: 'join', roomId })); // 从采购页跳转过来时,自动发送缓存的文本 if (fromPurchase && Store.pendingPurchaseText) { const text = Store.pendingPurchaseText; Store.pendingPurchaseText = ''; document.getElementById('msg-input').value = text; document.getElementById('msg-input').style.height = 'auto'; document.getElementById('msg-input').style.height = document.getElementById('msg-input').scrollHeight + 'px'; setTimeout(() => sendMessage(), 300); } } function showList() { clearTempBubble(); document.getElementById('chat-page').classList.add('hidden'); document.getElementById('purchase-page').classList.add('hidden'); document.getElementById('list-page').classList.remove('hidden'); Store.currentRoom = null; loadRooms(); updateSummaryPreview(); } async function sendMessage() { const input = document.getElementById('msg-input'); const text = input.value.trim(); if (!text && Store.pendingUploads.length === 0) return; if (!Store.currentRoom) return; const token = Store.getToken(); try { const res = await fetch('/api/rooms/' + Store.currentRoom + '/messages', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text || '', attachments: Store.pendingUploads }) }); const newMsg = await res.json(); appendMessage(newMsg); input.value = ''; Store.pendingUploads = []; insertTempBubble(); } catch(e) { alert('发送失败,请重试'); } } async function handleFileSelect(event) { const files = event.target.files; if (!files.length) return; const token = Store.getToken(); const placeholderId = 'placeholder_' + Date.now(); const placeholderMsg = { id: placeholderId, room_id: Store.currentRoom, user: localStorage.getItem('currentUser'), text: '图片发送中...', attachments: [], timestamp: new Date().toLocaleTimeString('zh-CN', { hour12: false }), isPlaceholder: true }; appendMessage(placeholderMsg); try { for (let file of files) { let blob = file; if (file.size > 1 * 1024 * 1024) blob = await compressImage(file); const formData = new FormData(); formData.append('file', blob, file.name || 'image.jpg'); const res = await fetch('/api/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); const data = await res.json(); if (data.path) Store.pendingUploads.push(data.path); } Store.messageCache = Store.messageCache.filter(m => m.id !== placeholderId); const placeholderEl = messagesContainer.querySelector(`[data-msg-id="${placeholderId}"]`); if (placeholderEl) placeholderEl.remove(); sendMessage(); } catch(e) { Store.messageCache = Store.messageCache.filter(m => m.id !== placeholderId); const placeholderEl = messagesContainer.querySelector(`[data-msg-id="${placeholderId}"]`); if (placeholderEl) placeholderEl.remove(); alert('图片发送失败,请重试'); } } async function updateSummaryPreview() { const token = Store.getToken(); try { const res = await fetch('/api/summary', { headers: { 'Authorization': `Bearer ${token}` } }); const summary = await res.json(); const totalItems = summary.reduce((s, r) => s + r.purchase_count, 0); const totalAmount = summary.reduce((s, r) => s + r.total_amount, 0); const preview = document.getElementById('summary-preview'); if (preview) preview.innerText = `共 ${totalItems} 条,合计 ¥${totalAmount}`; } catch(e) {} }