feat: Sentinel 横版卡片布局 — 折线图 + 全行展开

- 新增布局切换按钮: 网格/横版
- 横版: 每设备占满一行, 左信息 + 中折线图 + 右数字
- 复用 MiniChart 组件 (CPU/MEM/NET 三图)
- 切换时销毁旧图重建, 避免内存泄漏

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-13 21:43:53 +08:00
parent 63cd38cf22
commit 6a669efe0a
+101 -39
View File
@@ -1,9 +1,11 @@
<div class="plugin-page-root" style="padding:16px"> <div class="plugin-page-root" style="padding:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h2 style="margin:0">🛡️ Sentinel 集群监控</h2> <h2 style="margin:0">🛡️ Sentinel 集群监控</h2>
<div style="display:flex;gap:4px"> <div style="display:flex;gap:4px;align-items:center">
<button class="btn btn-sm btn-filled" id="tab-mon" onclick="switchTab('mon')">仪表盘</button> <button class="btn btn-sm btn-filled" id="tab-mon" onclick="switchTab('mon')">仪表盘</button>
<button class="btn btn-sm btn-outlined" id="tab-cfg" onclick="switchTab('cfg')">节点管理</button> <button class="btn btn-sm btn-outlined" id="tab-cfg" onclick="switchTab('cfg')">节点管理</button>
<span style="color:var(--border);margin:0 4px">|</span>
<button class="btn btn-sm btn-outlined" id="btn-layout" onclick="toggleLayout()" title="切换卡片布局">☷ 横版</button>
</div> </div>
</div> </div>
@@ -52,6 +54,15 @@
.node-sys { font-size:.75rem; color:var(--text-dim); margin-bottom:4px } .node-sys { font-size:.75rem; color:var(--text-dim); margin-bottom:4px }
.node-url { font-size:.75rem; color:var(--text-dim); margin-bottom:8px; word-break:break-all } .node-url { font-size:.75rem; color:var(--text-dim); margin-bottom:8px; word-break:break-all }
.node-notes { font-size:.75rem; color:var(--text-dim); font-style:italic; margin-bottom:8px } .node-notes { font-size:.75rem; color:var(--text-dim); font-style:italic; margin-bottom:8px }
/* 横版布局 */
#sentinel-grid.layout-row { display:flex;flex-direction:column;gap:12px }
#sentinel-grid.layout-row .node-card { display:grid;grid-template-columns:200px 1fr 180px;gap:16px;align-items:start }
#sentinel-grid.layout-row .node-card .card-left { min-width:0 }
#sentinel-grid.layout-row .node-card .card-mid { display:flex;gap:16px;flex-wrap:wrap }
#sentinel-grid.layout-row .node-card .card-mid .chart-box { flex:1;min-width:120px;height:60px }
#sentinel-grid.layout-row .node-card .card-right { text-align:right }
#sentinel-grid.layout-row .metric-bar { display:none }
#sentinel-grid.layout-row .metric-row { margin-bottom:0 }
.metric-row { display:flex; gap:12px; margin-bottom:8px } .metric-row { display:flex; gap:12px; margin-bottom:8px }
.metric { flex:1 } .metric { flex:1 }
.metric-label { font-size:.7rem; color:var(--text-dim); text-transform:uppercase } .metric-label { font-size:.7rem; color:var(--text-dim); text-transform:uppercase }
@@ -74,6 +85,32 @@
<script> <script>
var pollTimer = null; var pollTimer = null;
var editingId = null; var editingId = null;
var _chartInstances = {};
var _layoutMode = 'grid'; // 'grid' | 'row'
function toggleLayout() {
_layoutMode = _layoutMode === 'grid' ? 'row' : 'grid';
var btn = document.getElementById("btn-layout");
var grid = document.getElementById("sentinel-grid");
// 销毁旧图表
Object.keys(_chartInstances).forEach(function(k){ _chartInstances[k].forEach(function(c){ c.destroy(); }); });
_chartInstances = {};
_cardCache = {};
_firstRender = true;
grid.innerHTML = '<div class="card" style="text-align:center;padding:24px;color:var(--text-dim)">加载中...</div>';
if (_layoutMode === 'row') {
grid.className = 'layout-row';
grid.style = '';
btn.textContent = '⊞ 网格';
btn.className = 'btn btn-sm btn-filled';
} else {
grid.className = '';
grid.style = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px';
btn.textContent = '☷ 横版';
btn.className = 'btn btn-sm btn-outlined';
}
pollNodes();
}
function switchTab(t) { function switchTab(t) {
document.getElementById("view-mon").style.display = t === "mon" ? "block" : "none"; document.getElementById("view-mon").style.display = t === "mon" ? "block" : "none";
@@ -139,13 +176,16 @@
card.setAttribute('data-id', id); card.setAttribute('data-id', id);
var plat = n.platform || {}; var plat = n.platform || {};
var sysInfo = (plat.system||'') + (plat.release ? ' ' + plat.release : '') + (plat.machine ? ' | ' + plat.machine : ''); var sysInfo = (plat.system||'') + (plat.release ? ' ' + plat.release : '') + (plat.machine ? ' | ' + plat.machine : '');
var isRow = _layoutMode === 'row';
card.innerHTML = card.innerHTML =
(isRow ? '<div class="card-left">' : '') +
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+ '<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
(sysInfo.trim() ? '<div class="node-sys">'+esc(sysInfo)+'</div>' : '')+ (sysInfo.trim() ? '<div class="node-sys">'+esc(sysInfo)+'</div>' : '')+
'<div class="node-url">'+esc(n.url||'')+'</div>'+ '<div class="node-url">'+esc(n.url||'')+'</div>'+
(n.notes ? '<div class="node-notes">📝 '+esc(n.notes)+'</div>' : '')+ (n.notes ? '<div class="node-notes">📝 '+esc(n.notes)+'</div>' : '') +
(isRow ? '</div><div class="card-mid">' : '') +
'<div class="metrics-body"></div>'+ '<div class="metrics-body"></div>'+
'<div class="node-time"></div>'; (isRow ? '</div><div class="card-right"><div class="node-time"></div></div>' : '<div class="node-time"></div>');
grid.appendChild(card); grid.appendChild(card);
_cardCache[id] = card; _cardCache[id] = card;
} }
@@ -177,44 +217,66 @@
body.setAttribute('data-cpu', cpuVal); body.setAttribute('data-cpu', cpuVal);
body.setAttribute('data-mem', memVal); body.setAttribute('data-mem', memVal);
var isNew = !body.querySelector('.cpu-val'); var isNew = !body.querySelector('.cpu-val');
var isRow = _layoutMode === 'row';
if (isNew) { if (isNew) {
body.innerHTML = if (isRow) {
'<div class="metric-row">'+ body.innerHTML =
'<div class="metric"><div class="metric-label">CPU</div><div class="metric-value cpu-val">0%</div><div class="metric-bar"><div class="metric-bar-fill" style="width:0%"></div></div></div>'+ '<div class="chart-box"><canvas class="chart-cpu" style="width:100%;height:100%"></canvas></div>'+
'<div class="metric"><div style="display:flex;justify-content:space-between;align-items:baseline"><div class="metric-label">MEM</div><div class="mem-detail-val" style="font-size:.65rem;color:var(--text-dim)"></div></div><div class="metric-value mem-val">0%</div><div class="metric-bar"><div class="metric-bar-fill" style="width:0%"></div></div></div>'+ '<div class="chart-box"><canvas class="chart-mem" style="width:100%;height:100%"></canvas></div>'+
'</div>'+ '<div class="chart-box"><canvas class="chart-net" style="width:100%;height:100%"></canvas></div>';
'<div class="metric-row">'+ if (typeof MiniChart !== 'undefined') {
'<div class="metric"><div class="metric-label">NET ↓</div><div class="metric-value net-down" style="font-size:.9rem"></div></div>'+ _chartInstances[id] = [
'<div class="metric"><div class="metric-label">NET ↑</div><div class="metric-value net-up" style="font-size:.9rem"></div></div>'+ new MiniChart(body.querySelector('.chart-cpu'), '#7aa2f7'),
'</div>'; new MiniChart(body.querySelector('.chart-mem'), '#9ece6a'),
new MiniChart(body.querySelector('.chart-net'), '#e0af68'),
];
}
} else {
body.innerHTML =
'<div class="metric-row">'+
'<div class="metric"><div class="metric-label">CPU</div><div class="metric-value cpu-val">0%</div><div class="metric-bar"><div class="metric-bar-fill" style="width:0%"></div></div></div>'+
'<div class="metric"><div style="display:flex;justify-content:space-between;align-items:baseline"><div class="metric-label">MEM</div><div class="mem-detail-val" style="font-size:.65rem;color:var(--text-dim)"></div></div><div class="metric-value mem-val">0%</div><div class="metric-bar"><div class="metric-bar-fill" style="width:0%"></div></div></div>'+
'</div>'+
'<div class="metric-row">'+
'<div class="metric"><div class="metric-label">NET ↓</div><div class="metric-value net-down" style="font-size:.9rem"></div></div>'+
'<div class="metric"><div class="metric-label">NET ↑</div><div class="metric-value net-up" style="font-size:.9rem"></div></div>'+
'</div>';
}
} }
// 更新 NET + MEM 详情
body.querySelector('.net-down').textContent = fmtSpeed(net.rx_bytes_sec||0); if (isRow) {
body.querySelector('.net-up').textContent = fmtSpeed(net.tx_bytes_sec||0); var charts = _chartInstances[id];
var md = body.querySelector('.mem-detail-val'); if (charts && charts[0]) {
if (md) md.textContent = (mem.used_gb||0).toFixed(1) + '/' + (mem.total_gb||0).toFixed(1) + ' GB'; charts[0].update(cpuVal);
// 进度条: 先设旧值 → force reflow → 设新值 charts[1].update(memVal);
var barCpu = body.querySelectorAll('.metric-bar-fill')[0]; charts[2].update(net.rx_bytes_sec||0);
var barMem = body.querySelectorAll('.metric-bar-fill')[1]; }
barCpu.style.transition = 'none'; var right = card.querySelector('.card-right');
barMem.style.transition = 'none'; if (right) right.innerHTML =
barCpu.style.width = oldCpu+'%'; '<div class="metric-value" style="font-size:1.2rem">CPU '+(cpuVal).toFixed(1)+'%</div>'+
barMem.style.width = oldMem+'%'; '<div class="metric-value" style="font-size:1.2rem;margin-top:4px">MEM '+(memVal).toFixed(1)+'%</div>'+
barCpu.className = 'metric-bar-fill '+cpuBarCls; '<div style="font-size:.7rem;color:var(--text-dim);margin-top:4px">'+(mem.used_gb||0).toFixed(1)+'/'+(mem.total_gb||0).toFixed(1)+' GB</div>'+
barMem.className = 'metric-bar-fill '+memBarCls; '<div style="font-size:.75rem;color:var(--text-dim);margin-top:8px">↓ '+fmtSpeed(net.rx_bytes_sec||0)+'</div>'+
barCpu.offsetWidth; // force reflow '<div style="font-size:.75rem;color:var(--text-dim)">↑ '+fmtSpeed(net.tx_bytes_sec||0)+'</div>';
barMem.offsetWidth; } else {
barCpu.style.transition = ''; body.querySelector('.net-down').textContent = fmtSpeed(net.rx_bytes_sec||0);
barMem.style.transition = ''; body.querySelector('.net-up').textContent = fmtSpeed(net.tx_bytes_sec||0);
barCpu.style.width = cpuVal+'%'; var md = body.querySelector('.mem-detail-val');
barMem.style.width = memVal+'%'; if (md) md.textContent = (mem.used_gb||0).toFixed(1) + '/' + (mem.total_gb||0).toFixed(1) + ' GB';
// 数字跳动
animateValue(body.querySelector('.cpu-val'), oldCpu, cpuVal, 1, '%'); var barCpu = body.querySelectorAll('.metric-bar-fill')[0];
animateValue(body.querySelector('.mem-val'), oldMem, memVal, 1, '%'); var barMem = body.querySelectorAll('.metric-bar-fill')[1];
} else { barCpu.style.transition = 'none'; barMem.style.transition = 'none';
body.innerHTML = '<div style="color:var(--error);font-size:.85rem">'+(n.error||'离线')+'</div>'; barCpu.style.width = oldCpu+'%'; barMem.style.width = oldMem+'%';
} barCpu.className = 'metric-bar-fill '+cpuBarCls; barMem.className = 'metric-bar-fill '+memBarCls;
card.querySelector('.node-time').innerHTML = '<div style="font-size:.7rem;color:var(--text-dim);margin-top:8px">'+(n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—')+'</div>'; barCpu.offsetWidth; barMem.offsetWidth;
barCpu.style.transition = ''; barMem.style.transition = '';
barCpu.style.width = cpuVal+'%'; barMem.style.width = memVal+'%';
animateValue(body.querySelector('.cpu-val'), oldCpu, cpuVal, 1, '%');
animateValue(body.querySelector('.mem-val'), oldMem, memVal, 1, '%');
}
card.querySelector('.node-time').innerHTML = '<div style="font-size:.7rem;color:var(--text-dim);margin-top:8px">'+(n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—')+'</div>';
}); });
} }