工具
颜色
粗细 3
操作
1 人在线 已同步
在线人员 (0)
坐标:— 当前工具:画笔 沙盘推演 · 多人协作
function setSyncStatus(status) { const dot = document.getElementById('sync-dot'); const txt = document.getElementById('sync-status'); dot.className = 'sync-dot' + (status === 'syncing' ? ' syncing' : status === 'error' ? ' error' : ''); txt.textContent = status === 'syncing' ? '同步中…' : status === 'error' ? '同步失败' : '已同步'; } async function syncStroke(s) { try { setSyncStatus('syncing'); await col.add({ ...s, user: session.nickname, ts: Date.now() }); setSyncStatus('ok'); } catch(e) { setSyncStatus('error'); } } async function syncAction(action) { try { setSyncStatus('syncing'); await col.add({ ...action, user: session.nickname, ts: Date.now() }); setSyncStatus('ok'); } catch(e) { setSyncStatus('error'); } } // 监听实时数据 let lastTs = Date.now(); col.where({ ts: db.command.gt(lastTs - 1000) }) .watch({ onChange(snapshot) { const docs = snapshot.docs || []; docs.forEach(doc => { if (doc.user === session.nickname) return; // 忽略自己的 if (doc.type === 'clear') { strokes = []; undoStack = []; redraw(); // 重绘保留底图 } else if (doc.type === 'undo') { strokes = strokes.filter(s => s.id !== doc.id); redraw(); } else if (doc.tool) { strokes.push(doc); drawStroke(doc); } }); }, onError(err) { setSyncStatus('error'); } }); // 加载历史笔迹(最近2小时),按时间顺序重放所有操作 try { const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000; const r = await col.where({ ts: db.command.gt(twoHoursAgo) }).orderBy('ts','asc').limit(1000).get(); r.data.forEach(doc => { if (doc.type === 'clear') { strokes = []; undoStack = []; } else if (doc.type === 'undo') { strokes = strokes.filter(s => s.id !== doc.id); } else if (doc.tool) { strokes.push(doc); } }); redraw(); } catch(e) {} // ── 在线人员追踪(通过后端API,避免前端直连数据库权限问题)── const mySessionId = uid(); const HEARTBEAT_INTERVAL = 15000; // 渲染在线人员列表 function renderOnlinePanel(users) { const ROLE_LABEL = { super:'掌门', admin:'长老', user:'散修' }; const ROLE_CLASS = { super:'role-super', admin:'role-admin', user:'role-user' }; const COLORS = ['#ff6b6b','#4ecdc4','#45b7d1','#96ceb4','#ffeaa7','#dda0dd','#98d8c8','#f7dc6f']; document.getElementById('online-count').textContent = users.length; document.getElementById('online-panel-count').textContent = users.length; document.getElementById('online-panel-list').innerHTML = users.length ? users.map((u, i) => `
${escapeHtml(u.nickname || '—')} ${ROLE_LABEL[u.role]||'散修'}
`).join('') : '
暂无其他人在线
'; } async function refreshOnlineList() { const res = await API.Online.list(); if (res.ok) renderOnlinePanel(res.users || []); } // 加入在线列表 await API.Online.join(mySessionId); await refreshOnlineList(); // 心跳 + 定时刷新列表 const heartbeatTimer = setInterval(() => API.Online.heartbeat(mySessionId), HEARTBEAT_INTERVAL); const onlineRefreshTimer = setInterval(refreshOnlineList, 10000); // 离开时清理 window.addEventListener('beforeunload', () => { API.Online.leave(mySessionId); clearInterval(heartbeatTimer); clearInterval(onlineRefreshTimer); }); document.addEventListener('visibilitychange', () => { if (document.hidden) API.Online.leave(mySessionId); else { API.Online.join(mySessionId); refreshOnlineList(); } }); })();