const state = { snapshot: null, selectedDeviceId: '', }; const metricGroups = [ { title: '温度与治疗参数', keys: [ ['AF', '设定温度'], ['F', '当前温度'], ['A', '设定超滤总量'], ['C', '超滤率'], ['B', '超滤量'], ['K', '剩余时间'], ], }, { title: '流量与压力', keys: [ ['L', '透析液流量'], ['D', '有效血流量'], ['H', '静脉压'], ['o', '动脉压'], ['J', '跨膜压'], ['U', '累计血流量'], ], }, { title: '电解质与血液监测', keys: [ ['G', '电导率'], ['Na', '钠'], ['HCO3', '碳酸氢根'], ['O2Sat', '血氧饱和度'], ['Hct', '红细胞比容'], ['Hb', '血红蛋白'], ['Tblood', '血液温度'], ['ktv', 'Kt/V'], ], }, { title: '血压数据', keys: [ ['N', '收缩压'], ['O', '舒张压'], ['P', '心率'], ['M', '血压监测时间'], ], }, ]; function $(selector) { return document.querySelector(selector); } function formatClock(date = new Date()) { return date.toLocaleTimeString('zh-CN', { hour12: false }); } function formatTime(value) { if (!value) { return '暂无'; } return new Date(value).toLocaleString('zh-CN', { hour12: false }); } function formatAge(ageMs) { if (ageMs === null || ageMs === undefined) { return '暂无数据'; } const seconds = Math.max(0, Math.floor(ageMs / 1000)); if (seconds < 60) { return `${seconds} 秒前`; } const minutes = Math.floor(seconds / 60); if (minutes < 60) { return `${minutes} 分钟前`; } return `${Math.floor(minutes / 60)} 小时前`; } function getDataStatusText(status) { if (status === 'active') { return '数据正常'; } if (status === 'stale') { return '数据超时'; } return '等待数据'; } function setText(id, value) { const element = $(id); if (element) { element.textContent = value; } } function render(snapshot) { state.snapshot = snapshot; if (!state.selectedDeviceId && snapshot.devices.length > 0) { state.selectedDeviceId = snapshot.devices[0].deviceId; } if (state.selectedDeviceId && !snapshot.devices.some((device) => device.deviceId === state.selectedDeviceId)) { state.selectedDeviceId = snapshot.devices[0] ? snapshot.devices[0].deviceId : ''; } setText('#dashboard-title', snapshot.title || '设备中央监测大屏'); setText('#refresh-time', `最后刷新 ${formatTime(snapshot.generatedAt)}`); setText('#online-count', snapshot.totals.online); setText('#offline-count', snapshot.totals.offline); setText('#active-count', snapshot.totals.active); setText('#stale-count', snapshot.totals.waiting + snapshot.totals.stale); setText('#device-total', `${snapshot.totals.devices} 台设备`); renderDeviceList(snapshot.devices); renderDetail(snapshot.devices.find((device) => device.deviceId === state.selectedDeviceId)); } function renderDeviceList(devices) { const container = $('#device-list'); container.innerHTML = ''; for (const device of devices) { const button = document.createElement('button'); button.type = 'button'; button.className = `device-card ${device.deviceId === state.selectedDeviceId ? 'selected' : ''}`; button.innerHTML = `
${escapeHtml(device.name)}
${escapeHtml(device.deviceId)} | ${escapeHtml(device.ip)} | ${formatAge(device.dataAgeMs)}
${device.online ? '在线' : '离线'} ${getDataStatusText(device.dataStatus)}
`; button.addEventListener('click', () => { state.selectedDeviceId = device.deviceId; render(state.snapshot); }); container.appendChild(button); } } function renderDetail(device) { const body = $('#detail-body'); if (!device) { $('#detail-title').textContent = '设备数据状态'; $('#detail-state').textContent = '未选择'; body.className = 'detail-body empty'; body.innerHTML = '

等待设备数据接入

'; return; } $('#detail-title').textContent = `${device.name} 数据状态`; $('#detail-state').textContent = `${device.online ? '在线' : '离线'} / ${getDataStatusText(device.dataStatus)}`; body.className = 'detail-body'; body.innerHTML = `
${renderTimeBox('最近连接', device.connectedAt)} ${renderTimeBox('最近实时数据', device.lastRealtimeAt)} ${renderTimeBox('最近血压数据', device.lastBloodPressureAt)}
${metricGroups.map((group) => renderMetricGroup(group, device.payload || {})).join('')} `; } function renderTimeBox(label, value) { return `
${label} ${formatTime(value)}
`; } function renderMetricGroup(group, payload) { return `

${group.title}

${group.keys.map(([key, label]) => renderMetric(key, label, payload[key])).join('')}
`; } function renderMetric(key, label, value) { const text = value === undefined || value === null || value === '' ? '--' : value; return `
${escapeHtml(String(text))}
`; } function escapeHtml(value) { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } async function fetchSnapshot() { const response = await fetch('/api/snapshot', { cache: 'no-store' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); } function startEvents() { if (!window.EventSource) { return false; } const events = new EventSource('/events'); events.addEventListener('snapshot', (event) => { render(JSON.parse(event.data)); }); events.onerror = () => { events.close(); startPolling(); }; return true; } function startPolling() { const load = () => { fetchSnapshot() .then(render) .catch(() => { setText('#refresh-time', '连接大屏服务失败'); }); }; load(); setInterval(load, 3000); } setInterval(() => { setText('#now-time', formatClock()); }, 1000); setText('#now-time', formatClock()); if (!startEvents()) { startPolling(); }