improve(ui):优化监控页面

This commit is contained in:
qian committed 2026-05-16 23:27:39 +08:00
1 parent 78cfd8aaef
commit e412580ea6
1 file changed
+246 -60
+246 -60
View File
@@ -1,23 +1,20 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, shallowRef, onMounted } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, MarkLineComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import PageHeader from '@/components/PageHeader.vue'
import request from '@/utils/request'
use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, MarkLineComponent])
const loading = ref(false)
const timeRange = ref(5) // minutes
const dashboardData = ref<any>({})
async function fetchDashboard() {
loading.value = true
try {
const res = await request.get('/admin/monitor/dashboard', { params: { minutes: timeRange.value } })
dashboardData.value = res.data?.data?.data || {}
} catch {
// error auto-shown by Axios interceptor
} finally {
loading.value = false
}
}
const systemHistory = ref<any[]>([])
const trafficData = ref<any[]>([])
function num(v: any, fallback = 0): number {
return typeof v === 'number' ? v : fallback
@@ -34,18 +31,184 @@ function colorThreshold(v: any, warnAt: number, critAt: number): string {
return '#00b42a'
}
let pollTimer: ReturnType<typeof setInterval> | null = null
// --- time formatting ---
function fmtTimeSec(ts: string) {
const d = new Date(ts)
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
function fmtTimeMin(ts: string) {
const d = new Date(ts)
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}
// --- full chart options with time axis ---
interface ChartPoint { time: string; value: number }
function buildTimeSeriesChart(
points: ChartPoint[],
opts: {
color: string
unit: string
yMin?: number
yMax?: number
decimals?: number
warnLine?: number
critLine?: number
},
) {
const times = points.map(p => p.time)
const values = points.map(p => p.value)
const dec = opts.decimals ?? 1
const maxVal = Math.max(...values, 1)
const markLine: any = { silent: true, symbol: 'none', data: [] as any[] }
if (opts.warnLine != null) {
markLine.data.push({ yAxis: opts.warnLine, lineStyle: { color: '#ff7d00', type: 'dashed', width: 1 }, label: { formatter: '警告', fontSize: 11, color: '#ff7d00' } })
}
if (opts.critLine != null) {
markLine.data.push({ yAxis: opts.critLine, lineStyle: { color: '#f53f3f', type: 'dashed', width: 1 }, label: { formatter: '严重', fontSize: 11, color: '#f53f3f' } })
}
return {
tooltip: {
trigger: 'axis',
formatter(params: any) {
const p = params?.[0]
if (!p) return ''
return `${p.axisValue}<br/><span style="color:${opts.color}">●</span> ${p.value.toFixed(dec)} ${opts.unit}`
},
},
grid: { left: 52, right: 16, top: 12, bottom: 28 },
xAxis: {
type: 'category',
data: times,
boundaryGap: false,
axisLabel: { fontSize: 11, color: '#86909c', interval: 'auto' as const },
axisLine: { lineStyle: { color: '#e5e6eb' } },
axisTick: { show: false },
},
yAxis: {
type: 'value',
min: opts.yMin ?? 0,
max: opts.yMax ?? (maxVal * 1.3),
axisLabel: { fontSize: 11, color: '#86909c', formatter: (v: number) => v.toFixed(v >= 100 ? 0 : dec) },
splitLine: { lineStyle: { type: 'dashed', color: '#e5e6eb' } },
},
series: [{
type: 'line',
data: values,
smooth: true,
showSymbol: false,
lineStyle: { width: 2, color: opts.color },
areaStyle: {
color: {
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: opts.color + '25' },
{ offset: 1, color: opts.color + '05' },
],
},
},
markLine: markLine.data.length > 0 ? markLine : undefined,
}],
}
}
const cpuChartOption = shallowRef<any>({})
const memChartOption = shallowRef<any>({})
const qpsChartOption = shallowRef<any>({})
const tpmChartOption = shallowRef<any>({})
function updateCharts() {
// CPU trend
if (systemHistory.value.length > 1) {
cpuChartOption.value = buildTimeSeriesChart(
systemHistory.value.map((h: any) => ({
time: fmtTimeSec(h.timestamp),
value: h.cpu?.percent ?? 0,
})),
{ color: '#165DFF', unit: '%', decimals: 1, yMax: 100, warnLine: 60, critLine: 85 },
)
}
// Memory trend
if (systemHistory.value.length > 1) {
memChartOption.value = buildTimeSeriesChart(
systemHistory.value.map((h: any) => ({
time: fmtTimeSec(h.timestamp),
value: h.memory?.used_percent ?? 0,
})),
{ color: '#00B42A', unit: '%', decimals: 0, yMax: 100, warnLine: 60, critLine: 85 },
)
}
// QPS trend
if (trafficData.value.length > 1) {
qpsChartOption.value = buildTimeSeriesChart(
trafficData.value.map((t: any) => ({
time: fmtTimeMin(t.time),
value: (t.requests ?? 0) / 60,
})),
{ color: '#722ED1', unit: 'req/s', decimals: 2 },
)
}
// TPM trend
if (trafficData.value.length > 1) {
tpmChartOption.value = buildTimeSeriesChart(
trafficData.value.map((t: any) => ({
time: fmtTimeMin(t.time),
value: t.tokens ?? 0,
})),
{ color: '#F77234', unit: 'tokens', decimals: 0 },
)
}
}
// --- data fetching ---
async function fetchDashboard() {
try {
const res = await request.get('/admin/monitor/dashboard', { params: { minutes: timeRange.value } })
dashboardData.value = res.data?.data?.data || {}
} catch {
// error auto-shown by Axios interceptor
}
}
async function fetchSystemHistory() {
try {
const res = await request.get('/admin/monitor/system', { params: { minutes: timeRange.value } })
systemHistory.value = res.data?.data?.data?.history || []
} catch {
// keep previous data
}
}
async function fetchTraffic() {
try {
const res = await request.get('/admin/monitor/traffic', { params: { minutes: timeRange.value } })
trafficData.value = res.data?.data?.data || []
} catch {
// keep previous data
}
}
async function fetchAll() {
loading.value = true
try {
await Promise.all([fetchDashboard(), fetchSystemHistory(), fetchTraffic()])
updateCharts()
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDashboard()
pollTimer = setInterval(fetchDashboard, 30000)
})
onUnmounted(() => {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
fetchAll()
})
</script>
@@ -53,7 +216,7 @@ onUnmounted(() => {
<div>
<PageHeader title="系统监控" description="实时监控系统运行状态">
<template #actions>
<a-radio-group v-model="timeRange" type="button" @change="fetchDashboard">
<a-radio-group v-model="timeRange" type="button" @change="fetchAll">
<a-radio :value="5">5分钟</a-radio>
<a-radio :value="15">15分钟</a-radio>
<a-radio :value="30">30分钟</a-radio>
@@ -63,33 +226,61 @@ onUnmounted(() => {
</PageHeader>
<a-spin :loading="loading" style="width: 100%">
<!-- System resource cards -->
<!-- System resource trends -->
<div class="grid grid-cols-2 gap-4 mb-4">
<a-card :bordered="false">
<template #title>
<div class="card-title">
<span>CPU 使用率</span>
<a-tag :color="colorThreshold(dashboardData.system?.cpu?.percent, 60, 85) === '#00b42a' ? 'green' : colorThreshold(dashboardData.system?.cpu?.percent, 60, 85) === '#ff7d00' ? 'orangered' : 'red'" size="small">
{{ num(dashboardData.system?.cpu?.percent).toFixed(1) }}%
</a-tag>
</div>
</template>
<v-chart 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">
<template #title>
<div class="card-title">
<span>内存使用率</span>
<a-tag :color="colorThreshold(dashboardData.system?.memory?.used_percent, 60, 85) === '#00b42a' ? 'green' : colorThreshold(dashboardData.system?.memory?.used_percent, 60, 85) === '#ff7d00' ? 'orangered' : 'red'" size="small">
{{ num(dashboardData.system?.memory?.used_percent).toFixed(0) }}%
({{ num(dashboardData.system?.memory?.used_mb, 0).toFixed(0) }} / {{ num(dashboardData.system?.memory?.total_mb, 0).toFixed(0) }} MB)
</a-tag>
</div>
</template>
<v-chart v-if="systemHistory.length > 1" :option="memChartOption" style="height: 220px" autoresize />
<a-empty v-else description="暂无数据" style="padding: 60px 0" />
</a-card>
</div>
<!-- API traffic trends -->
<div class="grid grid-cols-2 gap-4 mb-4">
<a-card :bordered="false">
<template #title>
<div class="card-title">
<span>QPS (请求/秒)</span>
<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 />
<a-empty v-else description="暂无数据" style="padding: 60px 0" />
</a-card>
<a-card :bordered="false">
<template #title>
<div class="card-title">
<span>TPM (Token/分钟)</span>
<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 />
<a-empty v-else description="暂无数据" style="padding: 60px 0" />
</a-card>
</div>
<!-- Quick stats row -->
<div class="grid grid-cols-4 gap-4 mb-4">
<a-card :bordered="false">
<a-statistic title="CPU 使用率" :value="num(dashboardData.system?.cpu?.percent)" :precision="1">
<template #suffix>
<span style="font-size: 14px">%</span>
</template>
</a-statistic>
<div style="margin-top: 8px">
<a-progress :percent="pct(dashboardData.system?.cpu?.percent)" :show-text="false"
:color="colorThreshold(dashboardData.system?.cpu?.percent, 60, 85)" />
</div>
</a-card>
<a-card :bordered="false">
<a-statistic title="内存使用率" :value="num(dashboardData.system?.memory?.used_percent)" :precision="0">
<template #suffix>
<span style="font-size: 14px">%</span>
</template>
</a-statistic>
<div style="margin-top: 4px; font-size: 12px; color: var(--color-text-3)">
{{ num(dashboardData.system?.memory?.used_mb, 0).toFixed(0) }} / {{ num(dashboardData.system?.memory?.total_mb, 0).toFixed(0) }} MB
</div>
<div style="margin-top: 8px">
<a-progress :percent="pct(dashboardData.system?.memory?.used_percent)" :show-text="false"
:color="colorThreshold(dashboardData.system?.memory?.used_percent, 60, 85)" />
</div>
</a-card>
<a-card :bordered="false">
<a-statistic title="磁盘使用率" :value="num(dashboardData.system?.disk?.used_percent)" :precision="1">
<template #suffix>
@@ -110,16 +301,6 @@ onUnmounted(() => {
Heap: {{ num(dashboardData.system?.runtime?.heap_alloc_mb, 0).toFixed(0) }} MB
</div>
</a-card>
</div>
<!-- API Metrics row -->
<div class="grid grid-cols-4 gap-4 mb-4">
<a-card :bordered="false">
<a-statistic title="QPS (请求/秒)" :value="num(dashboardData.api?.qps)" :precision="2" />
</a-card>
<a-card :bordered="false">
<a-statistic title="TPM (Token/分钟)" :value="num(dashboardData.api?.tpm)" :precision="0" />
</a-card>
<a-card :bordered="false">
<a-statistic title="P99 延迟 (ms)" :value="num(dashboardData.api?.latency?.p99)" :precision="0" />
</a-card>
@@ -184,4 +365,9 @@ onUnmounted(() => {
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
.gap-4 { gap: 16px; }
.mb-4 { margin-bottom: 16px; }
.card-title {
display: flex;
align-items: center;
gap: 8px;
}
</style>