增强资源监控页异常摘要与趋势观察能力

This commit is contained in:
liuwei
2026-05-06 10:35:56 +08:00
parent 9f3f6ffbae
commit 0b8b60e3aa

View File

@@ -46,24 +46,76 @@
</div>
</div>
<el-card class="alert-strip-card" shadow="hover">
<div class="alert-strip-card__head">
<div>
<h3>观察摘要</h3>
<p>把当前最值得关注的风险先提到最上面,避免你每次都要自己扫完整页。</p>
</div>
<el-tag size="mini" :type="alertItems.length ? 'warning' : 'success'">
{% raw %}{{ alertItems.length ? `${alertItems.length} 条关注项` : '状态平稳' }}{% endraw %}
</el-tag>
</div>
<div v-if="alertItems.length" class="alert-strip-list">
<div
v-for="item in alertItems"
:key="item.key"
class="alert-pill"
:class="`alert-pill--${item.level}`">
<span class="alert-pill__title">{% raw %}{{ item.title }}{% endraw %}</span>
<span class="alert-pill__text">{% raw %}{{ item.text }}{% endraw %}</span>
</div>
</div>
<div v-else class="alert-strip-empty">
当前没有明显异常项,主机资源、进程和基础设施都处于可接受范围。
</div>
</el-card>
<div class="stats-grid">
<el-card class="stat-card" shadow="hover">
<div class="stat-card__label">CPU 使用率</div>
<div class="stat-card__value">{% raw %}{{ formatPercent(cpu.usage_percent) }}{% endraw %}</div>
<div class="stat-card__meta">{% raw %}{{ cpu.logical_count || 0 }}{% endraw %} 线程 · load1 {% raw %}{{ formatNumber(cpu.load_1, 2) }}{% endraw %}</div>
<el-progress :percentage="normalizePercent(cpu.usage_percent)" :stroke-width="10" :status="progressStatus(cpu.usage_percent, 65, 85)"></el-progress>
<div class="trend-mini">
<div class="trend-mini__head">
<span>最近趋势</span>
<span>{% raw %}{{ trendSummary('cpu') }}{% endraw %}</span>
</div>
<svg class="trend-mini__svg" viewBox="0 0 120 36" preserveAspectRatio="none">
<path class="trend-mini__path" :d="trendPath('cpu')"></path>
</svg>
</div>
</el-card>
<el-card class="stat-card" shadow="hover">
<div class="stat-card__label">内存使用率</div>
<div class="stat-card__value">{% raw %}{{ formatPercent(memory.usage_percent) }}{% endraw %}</div>
<div class="stat-card__meta">{% raw %}{{ formatBytes(memory.used_bytes) }}{% endraw %} / {% raw %}{{ formatBytes(memory.total_bytes) }}{% endraw %}</div>
<el-progress :percentage="normalizePercent(memory.usage_percent)" :stroke-width="10" :status="progressStatus(memory.usage_percent, 70, 88)"></el-progress>
<div class="trend-mini">
<div class="trend-mini__head">
<span>最近趋势</span>
<span>{% raw %}{{ trendSummary('memory') }}{% endraw %}</span>
</div>
<svg class="trend-mini__svg" viewBox="0 0 120 36" preserveAspectRatio="none">
<path class="trend-mini__path" :d="trendPath('memory')"></path>
</svg>
</div>
</el-card>
<el-card class="stat-card" shadow="hover">
<div class="stat-card__label">主盘使用率</div>
<div class="stat-card__value">{% raw %}{{ formatPercent(disk.primary_usage_percent) }}{% endraw %}</div>
<div class="stat-card__meta">{% raw %}{{ formatBytes(disk.primary_used_bytes) }}{% endraw %} / {% raw %}{{ formatBytes(disk.primary_total_bytes) }}{% endraw %}</div>
<el-progress :percentage="normalizePercent(disk.primary_usage_percent)" :stroke-width="10" :status="progressStatus(disk.primary_usage_percent, 75, 90)"></el-progress>
<div class="trend-mini">
<div class="trend-mini__head">
<span>最近趋势</span>
<span>{% raw %}{{ trendSummary('disk') }}{% endraw %}</span>
</div>
<svg class="trend-mini__svg" viewBox="0 0 120 36" preserveAspectRatio="none">
<path class="trend-mini__path" :d="trendPath('disk')"></path>
</svg>
</div>
</el-card>
<el-card class="stat-card" shadow="hover">
<div class="stat-card__label">网络速率</div>
@@ -73,6 +125,15 @@
<span>累计上行 {% raw %}{{ formatBytes(network.bytes_sent) }}{% endraw %}</span>
<span>累计下行 {% raw %}{{ formatBytes(network.bytes_recv) }}{% endraw %}</span>
</div>
<div class="trend-mini">
<div class="trend-mini__head">
<span>下载趋势</span>
<span>{% raw %}{{ trendSummary('network') }}{% endraw %}</span>
</div>
<svg class="trend-mini__svg" viewBox="0 0 120 36" preserveAspectRatio="none">
<path class="trend-mini__path" :d="trendPath('network')"></path>
</svg>
</div>
</el-card>
</div>
@@ -269,6 +330,12 @@
md2imgWarming: false,
autoRefreshTimer: null,
md2imgHealth: null,
trendHistory: {
cpu: [],
memory: [],
disk: [],
network: []
},
statusOverview: {
timestamp: '',
server: {},
@@ -330,6 +397,91 @@
const pidText = browser.pid ? `pid=${browser.pid}` : 'pid=-';
const sourceText = browser.launch_source ? `source=${browser.launch_source}` : 'source=-';
return `${loopText} · ${pidText} · ${sourceText}`;
},
alertItems() {
const items = [];
const cpuUsage = this.normalizePercent(this.cpu.usage_percent);
const memoryUsage = this.normalizePercent(this.memory.usage_percent);
const diskUsage = this.normalizePercent(this.disk.primary_usage_percent);
const mysqlStatus = this.infrastructure.mysql.status || 'healthy';
const redisStatus = this.infrastructure.redis.status || 'healthy';
if (cpuUsage >= 85) {
items.push({
key: 'cpu-danger',
level: 'danger',
title: 'CPU 过高',
text: `当前 CPU 使用率 ${this.formatPercent(cpuUsage)},建议先排查是否存在短时计算高峰或死循环。`
});
} else if (cpuUsage >= 65) {
items.push({
key: 'cpu-warning',
level: 'warning',
title: 'CPU 偏高',
text: `当前 CPU 使用率 ${this.formatPercent(cpuUsage)},建议继续观察最近几轮走势。`
});
}
if (memoryUsage >= 88) {
items.push({
key: 'memory-danger',
level: 'danger',
title: '内存紧张',
text: `当前内存使用率 ${this.formatPercent(memoryUsage)},可能会影响模型调用和浏览器截图稳定性。`
});
} else if (memoryUsage >= 70) {
items.push({
key: 'memory-warning',
level: 'warning',
title: '内存偏高',
text: `当前内存使用率 ${this.formatPercent(memoryUsage)},建议留意后续是否持续抬升。`
});
}
if (diskUsage >= 90) {
items.push({
key: 'disk-danger',
level: 'danger',
title: '磁盘接近满载',
text: `主盘使用率 ${this.formatPercent(diskUsage)},建议尽快清理日志、缓存或下载目录。`
});
} else if (diskUsage >= 75) {
items.push({
key: 'disk-warning',
level: 'warning',
title: '磁盘占用偏高',
text: `主盘使用率 ${this.formatPercent(diskUsage)},建议关注媒体目录和日志增长速度。`
});
}
if (mysqlStatus !== 'healthy') {
items.push({
key: 'mysql-status',
level: mysqlStatus === 'danger' ? 'danger' : 'warning',
title: 'MySQL 需要关注',
text: this.infrastructure.mysql.summary || 'MySQL 当前状态异常'
});
}
if (redisStatus !== 'healthy') {
items.push({
key: 'redis-status',
level: redisStatus === 'danger' ? 'danger' : 'warning',
title: 'Redis 需要关注',
text: this.infrastructure.redis.summary || 'Redis 当前状态异常'
});
}
if ((this.process.open_files || 0) >= 500) {
items.push({
key: 'process-open-files',
level: 'warning',
title: '打开文件数偏多',
text: `当前进程已打开 ${this.process.open_files || 0} 个文件,若持续增长可能存在句柄泄漏。`
});
}
return items.slice(0, 5);
}
},
mounted() {
@@ -355,6 +507,11 @@
const response = await axios.get('/api/system_status_overview');
if (response.data && response.data.success) {
this.statusOverview = response.data.data || this.statusOverview;
// 趋势只保留页面级最近若干采样点:
// 1. 不落库,避免为了可视化引入额外存储复杂度;
// 2. 足够支撑“我刚刷新几轮,它是在升还是在降”的观察场景;
// 3. 同一页内只看短时变化,比长期历史更符合当前需求。
this.recordTrendSnapshot();
if (showToast) {
this.$message.success('资源监控已刷新');
}
@@ -372,6 +529,55 @@
if (Number.isNaN(number)) return 0;
return Math.max(0, Math.min(100, Number(number.toFixed(1))));
},
recordTrendSnapshot() {
this.pushTrendPoint('cpu', this.normalizePercent(this.cpu.usage_percent));
this.pushTrendPoint('memory', this.normalizePercent(this.memory.usage_percent));
this.pushTrendPoint('disk', this.normalizePercent(this.disk.primary_usage_percent));
this.pushTrendPoint('network', Number(this.network.download_speed_bps || 0));
},
pushTrendPoint(key, value) {
const numeric = Number(value || 0);
if (!Object.prototype.hasOwnProperty.call(this.trendHistory, key) || Number.isNaN(numeric)) {
return;
}
const next = (this.trendHistory[key] || []).slice();
next.push({
value: numeric,
timestamp: Date.now()
});
// 只保留最近 18 个点10 秒轮询时大约能覆盖近 3 分钟。
while (next.length > 18) {
next.shift();
}
this.$set(this.trendHistory, key, next);
},
trendSummary(key) {
const points = this.trendHistory[key] || [];
if (points.length < 2) return '采样中';
const firstValue = Number(points[0].value || 0);
const lastValue = Number(points[points.length - 1].value || 0);
const delta = lastValue - firstValue;
if (Math.abs(delta) < 0.5) return '基本持平';
if (key === 'network') {
return delta > 0 ? '下载升高' : '下载回落';
}
return delta > 0 ? `较前上升 ${delta.toFixed(1)}` : `较前下降 ${Math.abs(delta).toFixed(1)}`;
},
trendPath(key) {
const points = this.trendHistory[key] || [];
if (!points.length) return '';
const width = 120;
const height = 36;
const values = points.map(item => Number(item.value || 0));
const maxValue = Math.max(...values, 1);
const minValue = Math.min(...values, 0);
const range = Math.max(maxValue - minValue, 1);
return values.map((value, index) => {
const x = values.length === 1 ? 0 : (index / (values.length - 1)) * width;
const y = height - (((value - minValue) / range) * (height - 4)) - 2;
return `${index === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`;
}).join(' ');
},
progressStatus(value, warningThreshold, dangerThreshold) {
const percent = this.normalizePercent(value);
if (percent >= dangerThreshold) return 'exception';
@@ -523,12 +729,39 @@
.health-title { font-size: 12px; font-weight: 700; color: #334155; }
.health-brief { font-size: 12px; color: #475569; }
.health-time { font-size: 12px; color: #94a3b8; }
.alert-strip-card { border-radius: 20px; }
.alert-strip-card__head {
display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 12px;
}
.alert-strip-card__head h3 { font-size: 18px; margin-bottom: 4px; color: #0f172a; }
.alert-strip-card__head p { font-size: 13px; color: #64748b; }
.alert-strip-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.alert-pill {
display: flex; flex-direction: column; gap: 6px; padding: 14px 16px; border-radius: 16px;
border: 1px solid rgba(148,163,184,0.12); background: rgba(248,250,252,0.78);
}
.alert-pill--warning { box-shadow: inset 0 0 0 1px rgba(245,158,11,0.10); }
.alert-pill--danger { box-shadow: inset 0 0 0 1px rgba(239,68,68,0.12); }
.alert-pill__title { color: #0f172a; font-size: 14px; font-weight: 700; }
.alert-pill__text { color: #64748b; font-size: 12px; line-height: 1.7; }
.alert-strip-empty {
padding: 14px 16px; border-radius: 16px; background: rgba(240,253,244,0.7);
border: 1px solid rgba(134,239,172,0.5); color: #166534; font-size: 13px;
}
.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
.stat-card { border-radius: 20px; }
.stat-card__label { color: #64748b; font-size: 13px; margin-bottom: 10px; }
.stat-card__value { color: #0f172a; font-size: 30px; font-weight: 700; line-height: 1; margin-bottom: 10px; }
.stat-card__meta { color: #475569; font-size: 12px; margin-bottom: 12px; min-height: 18px; }
.network-balance { margin-top: 12px; display: flex; justify-content: space-between; gap: 10px; color: #64748b; font-size: 12px; }
.trend-mini { margin-top: 12px; padding-top: 12px; border-top: 1px dashed rgba(148,163,184,0.20); }
.trend-mini__head {
display: flex; align-items: center; justify-content: space-between; gap: 8px; color: #64748b; font-size: 12px; margin-bottom: 8px;
}
.trend-mini__svg { width: 100%; height: 36px; display: block; }
.trend-mini__path {
fill: none; stroke: #2563eb; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round;
}
.monitor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.workspace-card { border-radius: 20px; }
.workspace-card--wide { grid-column: 1 / -1; }
@@ -565,6 +798,7 @@
.stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.monitor-grid { grid-template-columns: 1fr; }
.infra-grid { grid-template-columns: 1fr; }
.alert-strip-list { grid-template-columns: 1fr; }
}
@media (max-width: 960px) {
.page-hero { flex-direction: column; align-items: flex-start; }