增强首页基础设施卡片压力等级展示

This commit is contained in:
liuwei
2026-05-06 08:59:19 +08:00
parent ef5db2babd
commit 905380a77a

View File

@@ -163,6 +163,38 @@
{% raw %}{{ getHealthStatusText(service.status) }}{% endraw %}
</span>
</div>
<div v-if="service.highlights && service.highlights.length" class="health-service-highlights">
<div
v-for="highlight in service.highlights"
:key="highlight.label"
class="health-service-highlight"
:class="highlight.tone ? `health-service-highlight--${highlight.tone}` : ''">
<span class="health-service-highlight__label">{% raw %}{{ highlight.label }}{% endraw %}</span>
<span class="health-service-highlight__value">{% raw %}{{ highlight.value }}{% endraw %}</span>
</div>
</div>
<div v-if="service.meters && service.meters.length" class="health-service-meter-list">
<div v-for="meter in service.meters" :key="meter.label" class="health-service-meter">
<div class="health-service-meter__head">
<span class="health-service-meter__label">{% raw %}{{ meter.label }}{% endraw %}</span>
<div class="health-service-meter__meta">
<span class="health-service-meter__number">{% raw %}{{ meter.displayValue }}{% endraw %}</span>
<span
v-if="meter.levelText"
class="health-service-meter__badge"
:class="meter.levelClass ? `health-service-meter__badge--${meter.levelClass}` : ''">
{% raw %}{{ meter.levelText }}{% endraw %}
</span>
</div>
</div>
<div class="health-service-meter__track">
<div
class="health-service-meter__fill"
:class="meter.levelClass ? `health-service-meter__fill--${meter.levelClass}` : ''"
:style="{ width: `${meter.percent}%` }"></div>
</div>
</div>
</div>
<div class="health-service-metrics">
<div v-for="metric in service.metrics" :key="metric.label" class="health-service-metric">
<span class="health-service-metric__label">{% raw %}{{ metric.label }}{% endraw %}</span>
@@ -606,6 +638,44 @@
if (Number.isNaN(numeric)) return String(value);
return numeric.toFixed(fractionDigits);
},
normalizePercent(value) {
const numeric = Number(value || 0);
if (Number.isNaN(numeric)) return 0;
return Math.max(0, Math.min(100, numeric));
},
buildRiskLevel(percent, warningThreshold = 50, dangerThreshold = 80) {
const normalized = this.normalizePercent(percent);
if (normalized >= dangerThreshold) {
return { text: '高压', className: 'danger' };
}
if (normalized >= warningThreshold) {
return { text: '偏高', className: 'warning' };
}
return { text: '平稳', className: 'healthy' };
},
buildPositiveLevel(percent, warningThreshold = 70, healthyThreshold = 90) {
const normalized = this.normalizePercent(percent);
if (normalized >= healthyThreshold) {
return { text: '优秀', className: 'healthy' };
}
if (normalized >= warningThreshold) {
return { text: '可接受', className: 'warning' };
}
return { text: '偏低', className: 'danger' };
},
buildMeter(label, percent, displayValue, levelBuilder) {
const normalized = this.normalizePercent(percent);
const level = typeof levelBuilder === 'function'
? levelBuilder(normalized)
: { text: '', className: '' };
return {
label,
percent: normalized,
displayValue,
levelText: level.text || '',
levelClass: level.className || ''
};
},
countHealthyInfrastructureServices(infrastructure) {
const mysql = infrastructure.mysql || {};
const redis = infrastructure.redis || {};
@@ -623,8 +693,32 @@
title: 'MySQL',
status: mysql.status || 'warning',
summary: mysql.summary || '暂无状态',
highlights: [
{
label: '数据库',
value: mysql.database || '-',
tone: 'neutral'
},
{
label: '版本',
value: mysql.version || '-',
tone: 'neutral'
},
{
label: '慢SQL阈值',
value: `${this.formatMetricNumber(mysql.slow_query_threshold_ms)} ms`,
tone: 'info'
}
],
meters: [
this.buildMeter(
'连接负载',
mysql.connection_usage_percent,
`${this.formatMetricNumber(mysql.connection_usage_percent, 1)}%`,
(percent) => this.buildRiskLevel(percent, 45, 80)
)
],
metrics: [
{ label: '连接负载', value: `${this.formatMetricNumber(mysql.connection_usage_percent, 1)}%` },
{ label: '连接数', value: `${this.formatMetricNumber(mysql.threads_connected)} / ${mysql.max_connections || '-'}` },
{ label: '运行线程', value: this.formatMetricNumber(mysql.threads_running) },
{ label: 'QPS', value: this.formatMetricNumber(mysql.questions_per_second, 2) },
@@ -637,12 +731,42 @@
title: 'Redis',
status: redis.status || 'warning',
summary: redis.summary || '暂无状态',
highlights: [
{
label: 'DB',
value: redis.db_index ?? '-',
tone: 'neutral'
},
{
label: '峰值内存',
value: redis.used_memory_peak_human || '-',
tone: 'neutral'
},
{
label: '阻塞客户端',
value: this.formatMetricNumber(redis.blocked_clients),
tone: Number(redis.blocked_clients || 0) > 0 ? 'warning' : 'healthy'
}
],
meters: [
this.buildMeter(
'内存占用',
redis.memory_usage_percent,
`${this.formatMetricNumber(redis.memory_usage_percent, 1)}%`,
(percent) => this.buildRiskLevel(percent, 60, 80)
),
this.buildMeter(
'命中率',
redis.hit_rate_percent,
`${this.formatMetricNumber(redis.hit_rate_percent, 1)}%`,
(percent) => this.buildPositiveLevel(percent, 70, 90)
)
],
metrics: [
{ label: 'Key 数量', value: this.formatMetricNumber(redis.key_count) },
{ label: '客户端', value: this.formatMetricNumber(redis.connected_clients) },
{ label: 'OPS/s', value: this.formatMetricNumber(redis.ops_per_sec) },
{ label: '内存占用', value: redis.used_memory_human || '-' },
{ label: '命中率', value: `${this.formatMetricNumber(redis.hit_rate_percent, 1)}%` },
{ label: '运行时间', value: this.formatCompactDuration(redis.uptime_seconds) }
]
}
@@ -1320,6 +1444,143 @@
color: #64748b;
}
.health-service-highlights {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.health-service-highlight {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(148, 163, 184, 0.16);
color: #475569;
font-size: 11px;
}
.health-service-highlight--healthy {
color: #047857;
background: rgba(16, 185, 129, 0.10);
border-color: rgba(16, 185, 129, 0.16);
}
.health-service-highlight--warning {
color: #b45309;
background: rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.16);
}
.health-service-highlight--info {
color: #1d4ed8;
background: rgba(59, 130, 246, 0.10);
border-color: rgba(59, 130, 246, 0.16);
}
.health-service-highlight__label {
color: inherit;
opacity: 0.86;
}
.health-service-highlight__value {
color: #0f172a;
font-weight: 600;
max-width: 180px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.health-service-meter-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 12px;
}
.health-service-meter__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 6px;
}
.health-service-meter__label {
font-size: 11px;
color: #64748b;
}
.health-service-meter__meta {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.health-service-meter__number {
font-size: 12px;
color: #0f172a;
font-weight: 700;
}
.health-service-meter__badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
background: rgba(148, 163, 184, 0.12);
color: #64748b;
}
.health-service-meter__badge--healthy {
color: #047857;
background: rgba(16, 185, 129, 0.12);
}
.health-service-meter__badge--warning {
color: #b45309;
background: rgba(245, 158, 11, 0.14);
}
.health-service-meter__badge--danger {
color: #b91c1c;
background: rgba(239, 68, 68, 0.14);
}
.health-service-meter__track {
width: 100%;
height: 8px;
border-radius: 999px;
background: rgba(226, 232, 240, 0.88);
overflow: hidden;
}
.health-service-meter__fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, rgba(16, 185, 129, 0.68), rgba(5, 150, 105, 0.92));
}
.health-service-meter__fill--healthy {
background: linear-gradient(90deg, rgba(16, 185, 129, 0.68), rgba(5, 150, 105, 0.92));
}
.health-service-meter__fill--warning {
background: linear-gradient(90deg, rgba(245, 158, 11, 0.70), rgba(217, 119, 6, 0.95));
}
.health-service-meter__fill--danger {
background: linear-gradient(90deg, rgba(239, 68, 68, 0.72), rgba(220, 38, 38, 0.96));
}
.health-service-panel__badge {
display: inline-flex;
align-items: center;