/* DCTS Dashboard Nodes Table Component (增量 DOM 更新与 Skeleton) */ import { escapeHtml } from '../api.js'; export function renderNodesTableSkeleton() { const tbody = document.getElementById('nodes-table-body'); if (!tbody) return; const skeletonRows = Array.from({ length: 4 }).map(() => `
`).join(''); tbody.innerHTML = skeletonRows; } // 心跳时间容错:字段缺失或非法时间戳一律显示占位符,避免渲染出 "Invalid Date" function formatHeartbeat(ts) { if (!ts) return '—'; const d = new Date(ts); if (Number.isNaN(d.getTime())) return '—'; return d.toLocaleTimeString('zh-CN'); } // 行重建签名:仅状态/凭据决定徽章与操作按钮。槽位、负载、心跳是秒级高频字段, // 走 textContent 原位刷新——整行 innerHTML 重建会销毁用户正悬停/聚焦的按钮。 function rowSignature(node) { return `${node.status || ''}|${node.token_status || ''}`; } function volatileValues(node) { return { slots: `${Number(node.active_slots || 0)} / ${Number(node.max_slots || 4)}`, usage: `${Number(node.cpu_usage || 0).toFixed(1)}% / ${Number(node.memory_usage || 0).toFixed(1)}%`, heartbeat: formatHeartbeat(node.last_heartbeat), }; } function updateVolatileCells(tr, node) { const v = volatileValues(node); const slotsCell = tr.querySelector('.col-slots'); if (slotsCell && slotsCell.textContent !== v.slots) slotsCell.textContent = v.slots; const usageCell = tr.querySelector('.col-usage'); if (usageCell && usageCell.textContent !== v.usage) usageCell.textContent = v.usage; const hbCell = tr.querySelector('.col-heartbeat'); if (hbCell && hbCell.textContent !== v.heartbeat) hbCell.textContent = v.heartbeat; } function buildRowHtml(node, nodeId) { const isPending = node.status === 'pending_approval'; const isOnline = node.status === 'online' || node.status === 'active'; const isDisabled = node.status === 'disabled'; let statusBadge; if (isPending) { statusBadge = '待审批'; } else if (isDisabled) { statusBadge = '已停用'; } else if (isOnline) { statusBadge = '在线'; } else { statusBadge = '离线'; } let tokenBadge; if (isPending) { tokenBadge = '待授权'; } else if (node.token_status === 'active') { tokenBadge = '有效'; } else { tokenBadge = '无凭据'; } let actionBtns; if (isPending) { actionBtns = `
`; } else { // 启停切换:disabled 时显示「启用」(绿 ▶),否则显示「停用」(琥珀 ⏸)。 // 停用用琥珀+暂停符号,表达"可恢复的软暂停"——节点保持存活待命,与「重发 Token」 // (蓝 🔄,轮换凭据、旧 token 立即失效)是两类不同运维操作,颜色与图标刻意区分。 const toggleBtn = isDisabled ? `` : ``; actionBtns = `
${toggleBtn}
`; } const v = volatileValues(node); return ` ${nodeId} ${v.slots} ${v.usage} ${statusBadge} ${tokenBadge} ${v.heartbeat} ${actionBtns} `; } export function renderNodesTable(nodes) { const tbody = document.getElementById('nodes-table-body'); const countBadge = document.getElementById('node-count-badge'); if (countBadge) { countBadge.textContent = `${nodes ? nodes.length : 0} 个节点`; } if (!tbody) return; if (!nodes || nodes.length === 0) { const emptyHtml = ` 暂无计算节点记录。启动 DCTS Node Worker 提交注册申请后,在此审批授权接入集群。 `; if (tbody.innerHTML.trim() !== emptyHtml.trim()) { tbody.innerHTML = emptyHtml; } return; } // 构建目前现有 tr 的 Map (nodeId -> tr Element) const existingRows = new Map(); tbody.querySelectorAll('tr[data-node-row-id]').forEach(tr => { existingRows.set(tr.getAttribute('data-node-row-id'), tr); }); // 如果 tbody 里包含 skeleton 或 empty-cell,清空 if (tbody.querySelector('.skeleton-row, .empty-cell')) { tbody.innerHTML = ''; existingRows.clear(); } const updatedRowElements = []; nodes.forEach(node => { const nodeId = escapeHtml(node.node_id || node.id || 'N/A'); const sig = rowSignature(node); let tr = existingRows.get(nodeId); if (tr && tr.getAttribute('data-sig') === sig) { // 状态/凭据未变:仅原位刷新高频数值单元格,不触碰徽章与操作按钮 updateVolatileCells(tr, node); } else { const html = buildRowHtml(node, nodeId); if (!tr) { tr = document.createElement('tr'); tr.setAttribute('data-node-row-id', nodeId); } tr.innerHTML = html; tr.setAttribute('data-sig', sig); } existingRows.delete(nodeId); updatedRowElements.push(tr); }); // 移除不再存在的旧 tr existingRows.forEach(tr => tr.remove()); // 按顺序重排/挂载到 tbody updatedRowElements.forEach((tr, idx) => { if (tbody.children[idx] !== tr) { tbody.insertBefore(tr, tbody.children[idx] || null); } }); }