Files
ai-xiaocai/server/public/js/ws.js

188 lines
8.0 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="openChat('${room.id}')">${escapeHtml(room.name.charAt(0))}</div>
<div class="chat-info" onclick="openChat('${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="openChat('${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 openChat(roomId, autoOpenPurchase = false) {
Store.currentRoom = roomId;
clearTempBubble();
document.getElementById('list-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 || '';
document.getElementById('purchase-panel-overlay').classList.add('hidden');
if (autoOpenPurchase) setTimeout(() => openPurchasePanel(), 200);
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('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) {}
}