feat(ops): 添加QPS脉搏线图并优化指标布局

- 添加实时QPS/TPS历史数据追踪(最近60个数据点)
- 在平均QPS/TPS上方添加SVG脉搏线图(sparkline)
- 将延迟和TTFT卡片的指标布局从2列改为3列
- 恢复Max指标显示(P95/P90/P50/Avg/Max)
This commit is contained in:
IanShaw027
2026-01-11 11:49:34 +08:00
parent 645609d441
commit 89a725a433
6 changed files with 255 additions and 21 deletions

View File

@@ -162,6 +162,25 @@ const displayRealTimeTps = computed(() => {
return typeof v === 'number' && Number.isFinite(v) ? v : 0
})
// Sparkline history (keep last 60 data points)
const qpsHistory = ref<number[]>([])
const tpsHistory = ref<number[]>([])
const MAX_HISTORY_POINTS = 60
watch([displayRealTimeQps, displayRealTimeTps], ([newQps, newTps]) => {
// Add new data points
qpsHistory.value.push(newQps)
tpsHistory.value.push(newTps)
// Keep only last N points
if (qpsHistory.value.length > MAX_HISTORY_POINTS) {
qpsHistory.value.shift()
}
if (tpsHistory.value.length > MAX_HISTORY_POINTS) {
tpsHistory.value.shift()
}
})
const qpsPeakLabel = computed(() => {
const v = overview.value?.qps?.peak
if (typeof v !== 'number') return '-'
@@ -866,6 +885,16 @@ function openJobsDetails() {
<!-- Average QPS/TPS -->
<div class="col-span-2">
<!-- Sparkline -->
<svg v-if="qpsHistory.length > 1" class="mb-2 h-8 w-full" viewBox="0 0 200 32" preserveAspectRatio="none">
<polyline
:points="qpsHistory.map((v, i) => `${(i / (qpsHistory.length - 1)) * 200},${32 - (v / Math.max(...qpsHistory, 1)) * 28}`).join(' ')"
fill="none"
stroke="currentColor"
stroke-width="2"
class="text-blue-500"
/>
</svg>
<div class="text-[10px] font-bold uppercase text-gray-400">{{ t('admin.ops.average') }}</div>
<div class="mt-1 flex items-center gap-4 text-sm font-medium text-gray-600 dark:text-gray-400">
<span>QPS: <span class="font-bold">{{ qpsAvgLabel }}</span></span>
@@ -974,7 +1003,7 @@ function openJobsDetails() {
</div>
<span class="text-xs font-bold text-gray-400">ms (P99)</span>
</div>
<div class="mt-3 grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
<div class="mt-3 grid grid-cols-3 gap-x-3 gap-y-1 text-xs">
<div class="flex justify-between">
<span class="text-gray-500">P95:</span>
<span class="font-bold" :class="getLatencyColor(durationP95Ms)">{{ durationP95Ms ?? '-' }}ms</span>
@@ -991,6 +1020,10 @@ function openJobsDetails() {
<span class="text-gray-500">Avg:</span>
<span class="font-bold" :class="getLatencyColor(durationAvgMs)">{{ durationAvgMs ?? '-' }}ms</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Max:</span>
<span class="font-bold" :class="getLatencyColor(durationMaxMs)">{{ durationMaxMs ?? '-' }}ms</span>
</div>
</div>
</div>
@@ -1015,7 +1048,7 @@ function openJobsDetails() {
</div>
<span class="text-xs font-bold text-gray-400">ms (P99)</span>
</div>
<div class="mt-3 grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
<div class="mt-3 grid grid-cols-3 gap-x-3 gap-y-1 text-xs">
<div class="flex justify-between">
<span class="text-gray-500">P95:</span>
<span class="font-bold" :class="getLatencyColor(ttftP95Ms)">{{ ttftP95Ms ?? '-' }}ms</span>
@@ -1032,6 +1065,10 @@ function openJobsDetails() {
<span class="text-gray-500">Avg:</span>
<span class="font-bold" :class="getLatencyColor(ttftAvgMs)">{{ ttftAvgMs ?? '-' }}ms</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Max:</span>
<span class="font-bold" :class="getLatencyColor(ttftMaxMs)">{{ ttftMaxMs ?? '-' }}ms</span>
</div>
</div>
</div>