209 lines
8.7 KiB
JavaScript
209 lines
8.7 KiB
JavaScript
// 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';
|
|
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 = `<span class="white-list-tag" title="${escapeHtml(members)}">${escapeHtml(members)}</span>`;
|
|
}
|
|
return `
|
|
<div class="chat-item" data-room-id="${room.id}">
|
|
<div class="avatar" onclick="openPurchasePage('${room.id}')">${escapeHtml(room.name.charAt(0))}</div>
|
|
<div class="chat-info" onclick="openPurchasePage('${room.id}')">
|
|
<div class="chat-name">${escapeHtml(room.name)}</div>
|
|
<div class="last-msg">${escapeHtml(lastMsg)}</div>
|
|
</div>
|
|
<div class="chat-right">
|
|
<div class="chat-time" onclick="openPurchasePage('${room.id}')">${lastTime}</div>
|
|
${isAdmin ? `<div class="room-actions">
|
|
${whiteListHtml}
|
|
<button class="edit-room-btn" onclick="event.stopPropagation(); openManageRoom('${room.id}')" title="编辑群聊">
|
|
<svg viewBox="0 0 24 24"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg>
|
|
</button>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}).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 = '<p style="text-align:center;color:#888;">加载中...</p>';
|
|
loadPurchases();
|
|
}
|
|
|
|
async function openChatFromPurchase() {
|
|
if (!Store.currentRoom) return;
|
|
openChat(Store.currentRoom);
|
|
}
|
|
|
|
function backToPurchase() {
|
|
clearTempBubble();
|
|
document.getElementById('chat-page').classList.add('hidden');
|
|
document.getElementById('purchase-page').classList.remove('hidden');
|
|
loadPurchases();
|
|
}
|
|
|
|
async function openChat(roomId) {
|
|
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?.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 }));
|
|
}
|
|
|
|
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) {}
|
|
}
|