improve(ui):优化实时监控页面
This commit is contained in:
1 parent
e412580ea6
commit
f135e7f6fc
4 files changed
+290
-220
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user