improve(ui):优化实时监控页面

This commit is contained in:
qian committed 2026-05-17 08:37:28 +08:00
1 parent e412580ea6
commit f135e7f6fc
4 files changed
+290 -220

No files matched your search

+14
View File
@@ -332,9 +332,21 @@ const resizeObserver = new ResizeObserver(() => {
modelChart?.resize()
})
let resizeTimer: ReturnType<typeof setTimeout> | null = null
function onWindowResize() {
if (resizeTimer) clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
trendChart?.resize()
tenantTrendChart?.resize()
modelChart?.resize()
}, 200)
}
// === Lifecycle ===
onMounted(() => {
fetchAll()
window.addEventListener('resize', onWindowResize)
nextTick(() => {
const trendEl = document.getElementById('trend-chart')
const tenantEl = document.getElementById('tenant-trend-chart')
@@ -346,6 +358,8 @@ onMounted(() => {
})
onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize)
if (resizeTimer) clearTimeout(resizeTimer)
resizeObserver.disconnect()
if (trendChart) { trendChart.dispose(); trendChart = null }
if (tenantTrendChart) { tenantTrendChart.dispose(); tenantTrendChart = null }
@@ -272,6 +272,14 @@ const trendLoading = ref(false)
const trendData = ref<any[]>([])
const trendHours = ref(24)
let trendChart: echarts.ECharts | null = null
let resizeTimer: ReturnType<typeof setTimeout> | null = null
function onWindowResize() {
if (resizeTimer) clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
trendChart?.resize()
}, 200)
}
async function fetchHealthTrend() {
trendLoading.value = true
@@ -328,6 +336,8 @@ function handleTrendHoursChange(val: string | number | boolean) {
}
onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize)
if (resizeTimer) clearTimeout(resizeTimer)
if (trendChart) { trendChart.dispose(); trendChart = null }
})
@@ -379,6 +389,7 @@ async function handleClone(done: () => void) {
onMounted(() => {
fetchDetail()
window.addEventListener('resize', onWindowResize)
})
function formatJson(str: string): string {
+28 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, shallowRef, onMounted } from 'vue'
import { ref, shallowRef, onMounted, onUnmounted } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
@@ -207,8 +207,31 @@ async function fetchAll() {
}
}
const cpuChart = ref()
const memChart = ref()
const qpsChart = ref()
const tpmChart = ref()
let resizeTimer: ReturnType<typeof setTimeout> | null = null
function onWindowResize() {
if (resizeTimer) clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
cpuChart.value?.resize?.()
memChart.value?.resize?.()
qpsChart.value?.resize?.()
tpmChart.value?.resize?.()
}, 200)
}
onMounted(() => {
fetchAll()
window.addEventListener('resize', onWindowResize)
})
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize)
if (resizeTimer) clearTimeout(resizeTimer)
})
</script>
@@ -237,7 +260,7 @@ onMounted(() => {
</a-tag>
</div>
</template>
<v-chart v-if="systemHistory.length > 1" :option="cpuChartOption" style="height: 220px" autoresize />
<v-chart ref="cpuChart" v-if="systemHistory.length > 1" :option="cpuChartOption" style="height: 220px" autoresize />
<a-empty v-else description="暂无数据" style="padding: 60px 0" />
</a-card>
<a-card :bordered="false">
@@ -250,7 +273,7 @@ onMounted(() => {
</a-tag>
</div>
</template>
<v-chart v-if="systemHistory.length > 1" :option="memChartOption" style="height: 220px" autoresize />
<v-chart ref="memChart" v-if="systemHistory.length > 1" :option="memChartOption" style="height: 220px" autoresize />
<a-empty v-else description="暂无数据" style="padding: 60px 0" />
</a-card>
</div>
@@ -264,7 +287,7 @@ onMounted(() => {
<a-tag color="arcoblue" size="small">{{ num(dashboardData.api?.qps).toFixed(2) }}</a-tag>
</div>
</template>
<v-chart v-if="trafficData.length > 1" :option="qpsChartOption" style="height: 220px" autoresize />
<v-chart ref="qpsChart" v-if="trafficData.length > 1" :option="qpsChartOption" style="height: 220px" autoresize />
<a-empty v-else description="暂无数据" style="padding: 60px 0" />
</a-card>
<a-card :bordered="false">
@@ -274,7 +297,7 @@ onMounted(() => {
<a-tag color="orangered" size="small">{{ num(dashboardData.api?.tpm).toFixed(0) }}</a-tag>
</div>
</template>
<v-chart v-if="trafficData.length > 1" :option="tpmChartOption" style="height: 220px" autoresize />
<v-chart ref="tpmChart" v-if="trafficData.length > 1" :option="tpmChartOption" style="height: 220px" autoresize />
<a-empty v-else description="暂无数据" style="padding: 60px 0" />
</a-card>
</div>
@@ -7,6 +7,7 @@ import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/compon
import VChart from 'vue-echarts'
import PageHeader from '@/components/PageHeader.vue'
import request from '@/utils/request'
import { IconThunderbolt, IconSwap, IconClockCircle, IconCode, IconBarChart, IconLayers, IconApps, IconUserGroup, IconFire } from '@arco-design/web-vue/es/icon'
use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent])
@@ -41,7 +42,6 @@ function formatTime(ts: string) {
function updateCharts() {
const history = data.value.history || []
const bwHistory = data.value.bandwidth_history || []
const go = data.value.go_runtime || {}
concurrencyOption.value = {
tooltip: { trigger: 'axis' },
@@ -86,6 +86,7 @@ function updateCharts() {
}
// GC pause bar chart
const go = data.value.go_runtime || {}
const gcPauses = (go.gc?.recent_pause_ms || []).slice().reverse()
gcPauseOption.value = {
tooltip: { trigger: 'axis', valueFormatter: (v: number) => v?.toFixed(2) + ' ms' },
@@ -134,7 +135,17 @@ const rt = computed(() => data.value.runtime || {})
const goRt = computed(() => data.value.go_runtime || {})
const mem = computed(() => goRt.value.memory || {})
const gc = computed(() => goRt.value.gc || {})
const build = computed(() => goRt.value.build || {})
const heapUsagePercent = computed(() => {
const inuse = mem.value.heap_inuse_mb
const sys = mem.value.heap_sys_mb
if (!sys) return 0
return Math.min(100, (inuse / sys) * 100)
})
const concurrencyChart = ref()
const bandwidthChart = ref()
const gcPauseChart = ref()
const activeRequests = computed(() => {
const reqs: any[] = data.value.active_requests || []
@@ -155,20 +166,23 @@ const byChannel = computed(() => {
return entries.map(([k, v]) => ({ channel: k, count: v as number, pct: ((v as number) / max) * 100 }))
})
// Heap usage percentage for progress bar
const heapUsagePercent = computed(() => {
const inuse = mem.value.heap_inuse_mb
const sys = mem.value.heap_sys_mb
if (!sys) return 0
return Math.min(100, (inuse / sys) * 100)
})
let pollTimer: ReturnType<typeof setInterval> | null = null
let resizeTimer: ReturnType<typeof setTimeout> | null = null
function onWindowResize() {
if (resizeTimer) clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
concurrencyChart.value?.resize?.()
bandwidthChart.value?.resize?.()
gcPauseChart.value?.resize?.()
}, 200)
}
onMounted(() => {
fetchRealtime()
pollTimer = setInterval(fetchRealtime, 10000)
elapsedTimer = setInterval(() => { now.value = Date.now() }, 1000)
window.addEventListener('resize', onWindowResize)
})
onUnmounted(() => {
@@ -180,114 +194,101 @@ onUnmounted(() => {
clearInterval(elapsedTimer)
elapsedTimer = null
}
window.removeEventListener('resize', onWindowResize)
if (resizeTimer) clearTimeout(resizeTimer)
})
</script>
<template>
<div>
<PageHeader title="实时监控面板" description="并发请求、网络流量与运行时状态实时监控">
<PageHeader title="实时监控面板" description="并发请求、网络流量与运行状态">
<template #actions>
<div class="header-status">
<a-tag color="green" size="small">
自动刷新 10s
</a-tag>
<a-tag color="green" size="small">自动刷新 10s</a-tag>
<span v-if="lastUpdated" class="last-updated">最后更新 {{ fmtLastUpdated() }}</span>
</div>
</template>
</PageHeader>
<!-- Loading skeleton -->
<a-spin :loading="loading" style="width: 100%">
<div v-if="!loading">
<!-- Row 1: Concurrency & Bandwidth Metrics -->
<div class="row row-4 mb-16">
<a-card :bordered="false" class="metric-card">
<a-statistic title="当前并发" :value="conc.total_active || 0" :value-style="{ color: (conc.total_active || 0) > 0 ? '#165DFF' : undefined }" />
</a-card>
<a-card :bordered="false" class="metric-card">
<div class="dual-stat">
<div class="dual-stat-item">
<div class="dual-stat-dot" style="background: #00B42A" />
<span class="dual-stat-label">流式请求</span>
<span class="dual-stat-value" style="color: #00B42A">{{ conc.streaming_active || 0 }}</span>
</div>
<div class="dual-stat-divider" />
<div class="dual-stat-item">
<div class="dual-stat-dot" style="background: #FF7D00" />
<span class="dual-stat-label">非流式请求</span>
<span class="dual-stat-value" style="color: #FF7D00">{{ conc.non_streaming_active || 0 }}</span>
</div>
<!-- Key Metrics Cards -->
<div class="metrics-grid mb-16">
<div class="metric-card metric-card--blue">
<div class="mc-icon mc-icon--blue"><IconThunderbolt :size="22" /></div>
<div class="mc-block">
<div class="mc-label">当前并发</div>
<div class="mc-value mc-value--blue">{{ conc.total_active || 0 }}</div>
</div>
</a-card>
<a-card :bordered="false" class="metric-card">
<div class="dual-stat">
<div class="dual-stat-item">
<div class="dual-stat-dot" style="background: #165DFF" />
<span class="dual-stat-label">入站带宽</span>
<span class="dual-stat-value">{{ formatBytes(bw.bytes_in_per_sec || 0) }}/s</span>
</div>
<div class="dual-stat-divider" />
<div class="dual-stat-item">
<div class="dual-stat-dot" style="background: #00B42A" />
<span class="dual-stat-label">出站带宽</span>
<span class="dual-stat-value">{{ formatBytes(bw.bytes_out_per_sec || 0) }}/s</span>
</div>
<div class="mc-sep" />
<div class="mc-block mc-block--sub">
<div class="mc-pair"><span class="mc-dot" style="background:#00B42A" /> 流式 <strong>{{ conc.streaming_active || 0 }}</strong></div>
<div class="mc-pair"><span class="mc-dot" style="background:#FF7D00" /> 非流式 <strong>{{ conc.non_streaming_active || 0 }}</strong></div>
</div>
</div>
<div class="metric-card metric-card--green">
<div class="mc-icon mc-icon--green"><IconSwap :size="22" /></div>
<div class="mc-block">
<div class="mc-label">↓ 入站</div>
<div class="mc-value mc-value--blue">{{ formatBytes(bw.bytes_in_per_sec || 0) }}/s</div>
</div>
<div class="mc-sep" />
<div class="mc-block mc-block--sub">
<div class="mc-label">↑ 出站</div>
<div class="mc-value mc-value--green">{{ formatBytes(bw.bytes_out_per_sec || 0) }}/s</div>
</div>
</div>
<div class="metric-card metric-card--purple">
<div class="mc-icon mc-icon--purple"><IconClockCircle :size="22" /></div>
<div class="mc-block">
<div class="mc-label">运行时间</div>
<div class="mc-value">{{ goRt.uptime || '-' }}</div>
</div>
<div class="mc-sep" />
<div class="mc-block mc-block--sub">
<div class="mc-tag-row"><span class="mc-tag">Go {{ goRt.go_version || '-' }}</span></div>
<div class="mc-tag-row"><span class="mc-tag">{{ goRt.num_cpu || 0 }} 核</span></div>
</div>
</div>
<div class="metric-card metric-card--orange">
<div class="mc-icon mc-icon--orange"><IconCode :size="22" /></div>
<div class="mc-block">
<div class="mc-label">Goroutine</div>
<div class="mc-value mc-value--orange">{{ goRt.num_goroutine || 0 }}</div>
</div>
<div class="mc-sep" />
<div class="mc-block mc-block--sub">
<div class="mc-label">Heap 内存</div>
<div class="mc-value mc-value--orange">{{ (rt.heap_alloc_mb || 0).toFixed(0) }} MB</div>
</div>
</div>
</div>
<!-- Charts -->
<div class="row row-2 mb-16">
<a-card :bordered="false">
<template #title><span class="card-title"><IconBarChart :size="16" style="color:#165DFF" /> 并发趋势</span></template>
<v-chart ref="concurrencyChart" :option="concurrencyOption" style="height: 280px" autoresize />
</a-card>
<a-card :bordered="false" class="metric-card">
<a-statistic title="Goroutine" :value="rt.goroutine_count || 0" />
<div class="metric-sub">Heap: {{ (rt.heap_alloc_mb || 0).toFixed(0) }} MB</div>
<a-card :bordered="false">
<template #title><span class="card-title"><IconSwap :size="16" style="color:#00B42A" /> 带宽趋势</span></template>
<v-chart ref="bandwidthChart" :option="bandwidthOption" style="height: 280px" autoresize />
</a-card>
</div>
<!-- Row 2: Go Runtime Info -->
<div class="row row-3 mb-16">
<!-- Basic Info -->
<a-card :bordered="false" title="运行时信息">
<div class="info-grid">
<div class="info-item">
<span class="info-label">Go 版本</span>
<span class="info-value mono">{{ goRt.go_version || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">CPU 核数</span>
<span class="info-value">{{ goRt.num_cpu || 0 }}</span>
</div>
<div class="info-item">
<span class="info-label">Goroutine</span>
<span class="info-value">{{ goRt.num_goroutine || 0 }}</span>
</div>
<div class="info-item">
<span class="info-label">CGO 调用</span>
<span class="info-value">{{ goRt.num_cgo_call || 0 }}</span>
</div>
<div class="info-item">
<span class="info-label">运行时间</span>
<span class="info-value">{{ goRt.uptime || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">内存限制</span>
<span class="info-value">{{ fmtMB(gc.memory_limit_mb) }} MB</span>
</div>
</div>
<div class="build-info" v-if="build.commit || build.build_time">
<span class="info-label">构建</span>
<span class="info-value mono" v-if="build.commit">{{ build.commit }}</span>
<span class="info-value" v-if="build.build_time">{{ build.build_time }}</span>
</div>
</a-card>
<!-- Memory Breakdown -->
<a-card :bordered="false" title="内存分配">
<!-- Memory & GC -->
<div class="row row-2 mb-16">
<a-card :bordered="false">
<template #title><span class="card-title"><IconLayers :size="16" style="color:#722ED1" /> 内存分配</span></template>
<div class="mem-summary">
<div class="mem-stat">
<span class="mem-stat-value">{{ fmtMB(mem.alloc_mb) }}</span>
<span class="mem-stat-label">使用中 (Alloc)</span>
<span class="mem-stat-label">使用中</span>
</div>
<div class="mem-stat">
<span class="mem-stat-value">{{ fmtMB(mem.sys_mb) }}</span>
<span class="mem-stat-label">从 OS 获取 (Sys)</span>
<span class="mem-stat-label">从 OS 获取</span>
</div>
<div class="mem-stat">
<span class="mem-stat-value">{{ fmtMB(mem.total_alloc_mb) }}</span>
@@ -301,7 +302,7 @@ onUnmounted(() => {
</div>
<a-progress :percent="heapUsagePercent" :stroke-width="8" :show-text="false" color="#165DFF" />
</div>
<div class="info-grid" style="margin-top: 12px;">
<div class="info-grid" style="margin-top: 12px">
<div class="info-item">
<span class="info-label">堆空闲</span>
<span class="info-value">{{ fmtMB(mem.heap_idle_mb) }} MB</span>
@@ -318,27 +319,10 @@ onUnmounted(() => {
<span class="info-label">栈使用</span>
<span class="info-value">{{ fmtMB(mem.stack_inuse_mb) }} MB</span>
</div>
<div class="info-item">
<span class="info-label">MSpan</span>
<span class="info-value">{{ fmtMB(mem.mspan_inuse_mb) }} MB</span>
</div>
<div class="info-item">
<span class="info-label">MCache</span>
<span class="info-value">{{ fmtMB(mem.mcache_inuse_mb) }} MB</span>
</div>
<div class="info-item">
<span class="info-label">GC 元数据</span>
<span class="info-value">{{ fmtMB(mem.gc_sys_mb) }} MB</span>
</div>
<div class="info-item">
<span class="info-label">其他</span>
<span class="info-value">{{ fmtMB(mem.other_sys_mb) }} MB</span>
</div>
</div>
</a-card>
<!-- GC Stats -->
<a-card :bordered="false" title="GC 统计">
<a-card :bordered="false">
<template #title><span class="card-title"><IconCode :size="16" style="color:#FF7D00" /> GC 统计</span></template>
<div class="info-grid">
<div class="info-item">
<span class="info-label">GC 次数</span>
@@ -366,23 +350,14 @@ onUnmounted(() => {
</div>
</div>
<div class="gc-chart-title">最近 GC 暂停时长</div>
<v-chart :option="gcPauseOption" style="height: 120px" autoresize />
<v-chart ref="gcPauseChart" :option="gcPauseOption" style="height: 120px" autoresize />
</a-card>
</div>
<!-- Row 3: Charts -->
<!-- Distribution -->
<div class="row row-2 mb-16">
<a-card :bordered="false" title="并发趋势">
<v-chart :option="concurrencyOption" style="height: 260px" autoresize />
</a-card>
<a-card :bordered="false" title="带宽趋势">
<v-chart :option="bandwidthOption" style="height: 260px" autoresize />
</a-card>
</div>
<!-- Row 4: Breakdown with visual bars -->
<div class="row row-2 mb-16">
<a-card :bordered="false" title="按模型分布">
<a-card :bordered="false">
<template #title><span class="card-title"><IconApps :size="16" style="color:#165DFF" /> 按模型分布</span></template>
<div v-if="byModel.length === 0" class="empty-hint">暂无活跃请求</div>
<div v-for="item in byModel" :key="item.model" class="dist-row">
<div class="dist-label">
@@ -394,7 +369,8 @@ onUnmounted(() => {
</div>
</div>
</a-card>
<a-card :bordered="false" title="按渠道分布">
<a-card :bordered="false">
<template #title><span class="card-title"><IconUserGroup :size="16" style="color:#00B42A" /> 按渠道分布</span></template>
<div v-if="byChannel.length === 0" class="empty-hint">暂无活跃请求</div>
<div v-for="item in byChannel" :key="item.channel" class="dist-row">
<div class="dist-label">
@@ -408,8 +384,9 @@ onUnmounted(() => {
</a-card>
</div>
<!-- Row 5: Active Requests -->
<a-card :bordered="false" title="活跃请求" class="mb-16">
<!-- Active Requests -->
<a-card :bordered="false" class="mb-16">
<template #title><span class="card-title"><IconFire :size="16" style="color:#F53F3F" /> 活跃请求</span></template>
<template #extra>
<a-tag>{{ activeRequests.length }} 个请求</a-tag>
</template>
@@ -457,8 +434,6 @@ onUnmounted(() => {
gap: 16px;
}
.row-2 { grid-template-columns: repeat(2, 1fr); }
.row-3 { grid-template-columns: repeat(3, 1fr); }
.row-4 { grid-template-columns: repeat(4, 1fr); }
.mb-16 { margin-bottom: 16px; }
.table-scroll-wrapper {
@@ -478,85 +453,124 @@ onUnmounted(() => {
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
}
/* Metric card base */
.metric-card { min-height: 88px; }
.metric-card :deep(.arco-card-body) {
display: flex;
flex-direction: column;
justify-content: center;
}
/* Dual stat card */
.dual-stat {
display: flex;
align-items: stretch;
}
.dual-stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 0 8px;
}
.dual-stat-dot {
width: 6px;
height: 6px;
border-radius: 50%;
margin-bottom: 2px;
}
.dual-stat-label {
font-size: 12px;
color: var(--color-text-3);
line-height: 1;
}
.dual-stat-value {
font-size: 20px;
font-weight: 600;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
line-height: 1.4;
margin-top: 4px;
}
.dual-stat-divider {
width: 1px;
align-self: stretch;
background: var(--color-border);
flex-shrink: 0;
margin: 4px 0;
}
.metric-sub {
margin-top: 4px;
font-size: 12px;
color: var(--color-text-3);
}
.mono {
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 12px;
}
/* Runtime info styles */
.info-grid {
/* Metrics cards */
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px 16px;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.info-item {
.metric-card {
background: var(--color-bg-2);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-medium);
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
border-left: 4px solid transparent;
}
.info-label {
font-size: 13px;
.metric-card--blue {
border-left-color: #165DFF;
background: linear-gradient(135deg, rgba(22, 93, 255, 0.04), var(--color-bg-2) 70%);
}
.metric-card--green {
border-left-color: #00B42A;
background: linear-gradient(135deg, rgba(0, 180, 42, 0.04), var(--color-bg-2) 70%);
}
.metric-card--purple {
border-left-color: #722ED1;
background: linear-gradient(135deg, rgba(114, 46, 209, 0.04), var(--color-bg-2) 70%);
}
.metric-card--orange {
border-left-color: #FF7D00;
background: linear-gradient(135deg, rgba(255, 125, 0, 0.04), var(--color-bg-2) 70%);
}
.mc-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 14px;
flex-shrink: 0;
margin-right: 16px;
}
.mc-icon :deep(svg) {
color: #fff;
}
.mc-icon--blue { background: linear-gradient(135deg, #165DFF, #4080FF); }
.mc-icon--green { background: linear-gradient(135deg, #00B42A, #34D399); }
.mc-icon--purple { background: linear-gradient(135deg, #722ED1, #9254DE); }
.mc-icon--orange { background: linear-gradient(135deg, #FF7D00, #FFA940); }
.mc-block {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.mc-block--sub {
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
}
.mc-sep {
width: 1px;
align-self: stretch;
background: var(--color-border);
flex-shrink: 0;
margin: 4px 16px;
}
.mc-label {
font-size: 12px;
color: var(--color-text-3);
line-height: 1;
}
.info-value {
.mc-value {
font-size: 24px;
font-weight: 600;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
line-height: 1.3;
color: var(--color-text-1);
}
.mc-value--blue { color: #165DFF; }
.mc-value--green { color: #00B42A; }
.mc-value--orange { color: #FF7D00; }
.mc-pair {
font-size: 13px;
font-weight: 500;
color: var(--color-text-2);
display: flex;
align-items: center;
gap: 4px;
}
.mc-pair strong {
font-weight: 600;
color: var(--color-text-1);
}
.mc-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
}
.mc-tag-row {
display: flex;
}
.mc-tag {
display: inline-block;
font-size: 12px;
color: var(--color-text-2);
background: var(--color-fill-2);
padding: 2px 8px;
border-radius: 3px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
}
/* Memory summary */
/* Memory & GC */
.mem-summary {
display: flex;
gap: 24px;
@@ -581,8 +595,6 @@ onUnmounted(() => {
color: var(--color-text-3);
margin-top: 2px;
}
/* Heap bar */
.mem-bar-section {
margin-top: 8px;
}
@@ -593,28 +605,39 @@ onUnmounted(() => {
color: var(--color-text-2);
margin-bottom: 4px;
}
/* Build info */
.build-info {
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px 16px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--color-border);
font-size: 12px;
padding: 4px 0;
}
.build-info .info-label {
flex-shrink: 0;
.info-label {
font-size: 13px;
color: var(--color-text-3);
}
.info-value {
font-size: 13px;
font-weight: 500;
}
/* GC chart */
.gc-chart-title {
font-size: 12px;
color: var(--color-text-3);
margin: 12px 0 4px;
}
.card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
font-weight: 500;
}
/* Distribution bars */
.dist-row {
margin-bottom: 10px;
@@ -667,18 +690,17 @@ onUnmounted(() => {
}
/* Responsive */
@media (max-width: 1200px) {
.row-3 { grid-template-columns: 1fr 1fr; }
@media (max-width: 900px) {
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.row-4 { grid-template-columns: 1fr 1fr; }
.row-3 { grid-template-columns: 1fr; }
.row-2 { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
.row-4 { grid-template-columns: 1fr; }
.info-grid { grid-template-columns: 1fr; }
.mem-summary { flex-direction: column; gap: 12px; }
.dual-stat-value { font-size: 16px; }
.metrics-grid {
grid-template-columns: 1fr;
}
}
</style>