228 lines
9.9 KiB
JavaScript
228 lines
9.9 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 === 'query_pending') {
|
|
showTempBubble(data.text);
|
|
} else if (data.type === 'clear_temp') {
|
|
clearTempBubble();
|
|
} 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 = '<span class="white-list-tag" title="' + escapeHtml(members) + '">' + escapeHtml(members) + '</span>';
|
|
}
|
|
// 编辑按钮:管理员始终可见,普通用户仅自己群聊可见
|
|
const canEdit = isAdmin || (room.white_list && room.white_list.trim() === currentUser);
|
|
const editBtnHtml = canEdit ? '<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>' : '';
|
|
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>' +
|
|
editBtnHtml +
|
|
'</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>';
|
|
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) {}
|
|
}
|