增强首页基础设施卡片压力等级展示
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user