feat: Sentinel 横版卡片布局 — 折线图 + 全行展开
- 新增布局切换按钮: 网格/横版 - 横版: 每设备占满一行, 左信息 + 中折线图 + 右数字 - 复用 MiniChart 组件 (CPU/MEM/NET 三图) - 切换时销毁旧图重建, 避免内存泄漏 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
<div class="plugin-page-root" style="padding:16px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<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-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>
|
||||
|
||||
@@ -52,6 +54,15 @@
|
||||
.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-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 { flex:1 }
|
||||
.metric-label { font-size:.7rem; color:var(--text-dim); text-transform:uppercase }
|
||||
@@ -74,6 +85,32 @@
|
||||
<script>
|
||||
var pollTimer = 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) {
|
||||
document.getElementById("view-mon").style.display = t === "mon" ? "block" : "none";
|
||||
@@ -139,13 +176,16 @@
|
||||
card.setAttribute('data-id', id);
|
||||
var plat = n.platform || {};
|
||||
var sysInfo = (plat.system||'') + (plat.release ? ' ' + plat.release : '') + (plat.machine ? ' | ' + plat.machine : '');
|
||||
var isRow = _layoutMode === 'row';
|
||||
card.innerHTML =
|
||||
(isRow ? '<div class="card-left">' : '') +
|
||||
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
|
||||
(sysInfo.trim() ? '<div class="node-sys">'+esc(sysInfo)+'</div>' : '')+
|
||||
'<div class="node-url">'+esc(n.url||'')+'</div>'+
|
||||
(n.notes ? '<div class="node-notes">📝 '+esc(n.notes)+'</div>' : '') +
|
||||
(isRow ? '</div><div class="card-mid">' : '') +
|
||||
'<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);
|
||||
_cardCache[id] = card;
|
||||
}
|
||||
@@ -177,7 +217,21 @@
|
||||
body.setAttribute('data-cpu', cpuVal);
|
||||
body.setAttribute('data-mem', memVal);
|
||||
var isNew = !body.querySelector('.cpu-val');
|
||||
var isRow = _layoutMode === 'row';
|
||||
if (isNew) {
|
||||
if (isRow) {
|
||||
body.innerHTML =
|
||||
'<div class="chart-box"><canvas class="chart-cpu" style="width:100%;height:100%"></canvas></div>'+
|
||||
'<div class="chart-box"><canvas class="chart-mem" style="width:100%;height:100%"></canvas></div>'+
|
||||
'<div class="chart-box"><canvas class="chart-net" style="width:100%;height:100%"></canvas></div>';
|
||||
if (typeof MiniChart !== 'undefined') {
|
||||
_chartInstances[id] = [
|
||||
new MiniChart(body.querySelector('.chart-cpu'), '#7aa2f7'),
|
||||
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>'+
|
||||
@@ -188,31 +242,39 @@
|
||||
'<div class="metric"><div class="metric-label">NET ↑</div><div class="metric-value net-up" style="font-size:.9rem"></div></div>'+
|
||||
'</div>';
|
||||
}
|
||||
// 更新 NET + MEM 详情
|
||||
}
|
||||
|
||||
if (isRow) {
|
||||
var charts = _chartInstances[id];
|
||||
if (charts && charts[0]) {
|
||||
charts[0].update(cpuVal);
|
||||
charts[1].update(memVal);
|
||||
charts[2].update(net.rx_bytes_sec||0);
|
||||
}
|
||||
var right = card.querySelector('.card-right');
|
||||
if (right) right.innerHTML =
|
||||
'<div class="metric-value" style="font-size:1.2rem">CPU '+(cpuVal).toFixed(1)+'%</div>'+
|
||||
'<div class="metric-value" style="font-size:1.2rem;margin-top:4px">MEM '+(memVal).toFixed(1)+'%</div>'+
|
||||
'<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>'+
|
||||
'<div style="font-size:.75rem;color:var(--text-dim);margin-top:8px">↓ '+fmtSpeed(net.rx_bytes_sec||0)+'</div>'+
|
||||
'<div style="font-size:.75rem;color:var(--text-dim)">↑ '+fmtSpeed(net.tx_bytes_sec||0)+'</div>';
|
||||
} else {
|
||||
body.querySelector('.net-down').textContent = fmtSpeed(net.rx_bytes_sec||0);
|
||||
body.querySelector('.net-up').textContent = fmtSpeed(net.tx_bytes_sec||0);
|
||||
var md = body.querySelector('.mem-detail-val');
|
||||
if (md) md.textContent = (mem.used_gb||0).toFixed(1) + '/' + (mem.total_gb||0).toFixed(1) + ' GB';
|
||||
// 进度条: 先设旧值 → force reflow → 设新值
|
||||
|
||||
var barCpu = body.querySelectorAll('.metric-bar-fill')[0];
|
||||
var barMem = body.querySelectorAll('.metric-bar-fill')[1];
|
||||
barCpu.style.transition = 'none';
|
||||
barMem.style.transition = 'none';
|
||||
barCpu.style.width = oldCpu+'%';
|
||||
barMem.style.width = oldMem+'%';
|
||||
barCpu.className = 'metric-bar-fill '+cpuBarCls;
|
||||
barMem.className = 'metric-bar-fill '+memBarCls;
|
||||
barCpu.offsetWidth; // force reflow
|
||||
barMem.offsetWidth;
|
||||
barCpu.style.transition = '';
|
||||
barMem.style.transition = '';
|
||||
barCpu.style.width = cpuVal+'%';
|
||||
barMem.style.width = memVal+'%';
|
||||
// 数字跳动
|
||||
barCpu.style.transition = 'none'; barMem.style.transition = 'none';
|
||||
barCpu.style.width = oldCpu+'%'; barMem.style.width = oldMem+'%';
|
||||
barCpu.className = 'metric-bar-fill '+cpuBarCls; barMem.className = 'metric-bar-fill '+memBarCls;
|
||||
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, '%');
|
||||
} else {
|
||||
body.innerHTML = '<div style="color:var(--error);font-size:.85rem">'+(n.error||'离线')+'</div>';
|
||||
}
|
||||
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>';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user