const fs = require('fs');
|
const http = require('http');
|
const path = require('path');
|
|
const CONTENT_TYPES = {
|
'.html': 'text/html; charset=utf-8',
|
'.css': 'text/css; charset=utf-8',
|
'.js': 'application/javascript; charset=utf-8',
|
'.json': 'application/json; charset=utf-8',
|
};
|
|
function getDashboardDir(options = {}) {
|
if (options.dashboardDir) {
|
return options.dashboardDir;
|
}
|
|
if (process.pkg) {
|
return path.join(path.dirname(process.execPath), 'runtime', 'dashboard');
|
}
|
|
return path.join(__dirname, 'dashboard');
|
}
|
|
function nowIso() {
|
return new Date().toISOString();
|
}
|
|
function buildDashboardSnapshot({ devices = [], cache, tcpService, config = {} }) {
|
const connectionMap = new Map();
|
const cacheMap = cache && typeof cache.getSnapshotMap === 'function'
|
? cache.getSnapshotMap()
|
: new Map();
|
const staleDataMs = Number(config.staleDataMs) > 0 ? Number(config.staleDataMs) : 180000;
|
const now = Date.now();
|
|
if (tcpService && typeof tcpService.getConnectionSnapshot === 'function') {
|
for (const item of tcpService.getConnectionSnapshot()) {
|
connectionMap.set(item.deviceId, item);
|
}
|
}
|
|
const deviceSnapshots = devices.map((device) => {
|
const connection = connectionMap.get(device.deviceId) || null;
|
const cacheEntry = cacheMap.get(device.deviceId) || null;
|
const lastUpdateAt = cacheEntry ? cacheEntry.lastUpdateAt : 0;
|
const ageMs = lastUpdateAt > 0 ? now - lastUpdateAt : null;
|
const dataStatus = !lastUpdateAt
|
? 'waiting'
|
: (ageMs <= staleDataMs ? 'active' : 'stale');
|
|
return {
|
deviceId: device.deviceId,
|
name: device.name || device.deviceId,
|
ip: device.ip,
|
online: Boolean(connection),
|
dataStatus,
|
dataAgeMs: ageMs,
|
connectedAt: connection ? connection.connectedAt : 0,
|
lastSocketDataAt: connection ? connection.lastDataAt : 0,
|
lastUpdateAt,
|
lastRealtimeAt: cacheEntry ? cacheEntry.lastRealtimeAt : 0,
|
lastBloodPressureAt: cacheEntry ? cacheEntry.lastBloodPressureAt : 0,
|
payload: cacheEntry ? cacheEntry.payload : {},
|
};
|
});
|
|
const onlineCount = deviceSnapshots.filter((item) => item.online).length;
|
const activeCount = deviceSnapshots.filter((item) => item.dataStatus === 'active').length;
|
const waitingCount = deviceSnapshots.filter((item) => item.dataStatus === 'waiting').length;
|
const staleCount = deviceSnapshots.filter((item) => item.dataStatus === 'stale').length;
|
|
return {
|
title: config.title || 'JH2028 设备中央监测大屏',
|
generatedAt: now,
|
generatedAtText: nowIso(),
|
totals: {
|
devices: deviceSnapshots.length,
|
online: onlineCount,
|
offline: deviceSnapshots.length - onlineCount,
|
active: activeCount,
|
waiting: waitingCount,
|
stale: staleCount,
|
},
|
devices: deviceSnapshots,
|
};
|
}
|
|
class DashboardService {
|
constructor(options = {}) {
|
this.config = options.config || {};
|
this.logger = options.logger || console;
|
this.getSnapshot = typeof options.getSnapshot === 'function' ? options.getSnapshot : () => ({ devices: [] });
|
this.dashboardDir = getDashboardDir(options);
|
this.server = null;
|
this.clients = new Set();
|
}
|
|
async start() {
|
if (this.config.enabled === false) {
|
this.logger.info('[DASHBOARD] 大屏服务未启用');
|
return;
|
}
|
|
this.server = http.createServer((request, response) => {
|
this.handleRequest(request, response);
|
});
|
|
await new Promise((resolve, reject) => {
|
this.server.once('listening', resolve);
|
this.server.once('error', reject);
|
this.server.listen({
|
host: this.config.host || '0.0.0.0',
|
port: this.config.port || 9100,
|
});
|
});
|
|
const host = this.config.host || '0.0.0.0';
|
const port = this.config.port || 9100;
|
const localUrl = `http://127.0.0.1:${port}`;
|
const listenText = host === '0.0.0.0'
|
? '监听所有网卡,局域网访问请使用服务器真实 IP'
|
: `监听地址=${host}`;
|
|
this.logger.info(`[DASHBOARD] 大屏服务已启动 ${listenText} 本机访问=${localUrl}`);
|
}
|
|
async stop() {
|
for (const client of this.clients) {
|
client.end();
|
}
|
|
this.clients.clear();
|
|
if (!this.server) {
|
return;
|
}
|
|
await new Promise((resolve) => {
|
this.server.close(resolve);
|
});
|
this.server = null;
|
this.logger.info('[DASHBOARD] 大屏服务已停止');
|
}
|
|
handleRequest(request, response) {
|
const url = new URL(request.url, 'http://localhost');
|
|
if (url.pathname === '/api/snapshot') {
|
this.sendJson(response, this.getSnapshot());
|
return;
|
}
|
|
if (url.pathname === '/events') {
|
this.handleEvents(response);
|
return;
|
}
|
|
const pathname = url.pathname === '/' ? '/index.html' : url.pathname;
|
this.serveStatic(pathname, response);
|
}
|
|
handleEvents(response) {
|
response.writeHead(200, {
|
'Content-Type': 'text/event-stream; charset=utf-8',
|
'Cache-Control': 'no-cache',
|
Connection: 'keep-alive',
|
'Access-Control-Allow-Origin': '*',
|
});
|
response.write('\n');
|
this.clients.add(response);
|
this.writeEvent(response, this.getSnapshot());
|
|
response.on('close', () => {
|
this.clients.delete(response);
|
});
|
}
|
|
broadcastSnapshot() {
|
const snapshot = this.getSnapshot();
|
|
for (const client of this.clients) {
|
this.writeEvent(client, snapshot);
|
}
|
}
|
|
writeEvent(response, snapshot) {
|
response.write(`event: snapshot\n`);
|
response.write(`data: ${JSON.stringify(snapshot)}\n\n`);
|
}
|
|
sendJson(response, payload) {
|
response.writeHead(200, {
|
'Content-Type': 'application/json; charset=utf-8',
|
'Cache-Control': 'no-cache',
|
});
|
response.end(JSON.stringify(payload));
|
}
|
|
serveStatic(pathname, response) {
|
const normalizedPath = path.normalize(pathname.replace(/^\/+/, ''));
|
|
if (path.isAbsolute(normalizedPath) || normalizedPath.startsWith('..')) {
|
response.writeHead(403);
|
response.end('Forbidden');
|
return;
|
}
|
|
const filePath = path.join(this.dashboardDir, normalizedPath);
|
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
response.writeHead(404);
|
response.end('Not Found');
|
return;
|
}
|
|
const extension = path.extname(filePath).toLowerCase();
|
response.writeHead(200, {
|
'Content-Type': CONTENT_TYPES[extension] || 'application/octet-stream',
|
});
|
fs.createReadStream(filePath).pipe(response);
|
}
|
}
|
|
module.exports = {
|
DashboardService,
|
buildDashboardSnapshot,
|
getDashboardDir,
|
};
|