feat(watch): 监测历史四槽同屏展示趋势
资源趋势 2×2 四图;探针记录默认全部槽位;表头内存/硬盘中文。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# 监测历史:4 槽同时展示趋势
|
||||
|
||||
**日期**: 2026-06-12
|
||||
|
||||
## 变更摘要
|
||||
|
||||
- 资源趋势 Tab 改为 **2×2 四槽同屏**,每槽独立曲线;共用时间范围与指标选择
|
||||
- 探针记录 Tab 默认展示 **全部槽位** 记录(不再单选服务器)
|
||||
- 探针表头 MEM/DISK 改为「内存」「硬盘」
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `frontend/src/pages/WatchMetricsPage.vue`
|
||||
- `frontend/src/components/watch/WatchProbeRecordsTable.vue`
|
||||
|
||||
## 验证
|
||||
|
||||
`npm run type-check`;打开 `#/watch-metrics`,4 槽图表同屏;探针记录含多机数据。
|
||||
@@ -37,8 +37,8 @@ const headers = [
|
||||
{ title: '状态', key: 'probe_status', width: 100 },
|
||||
{ title: '来源', key: 'source', width: 72 },
|
||||
{ title: 'CPU', key: 'cpu_pct', width: 64 },
|
||||
{ title: 'MEM', key: 'mem_pct', width: 64 },
|
||||
{ title: 'DISK', key: 'disk_pct', width: 64 },
|
||||
{ title: '内存', key: 'mem_pct', width: 64 },
|
||||
{ title: '硬盘', key: 'disk_pct', width: 64 },
|
||||
{ title: '↑', key: 'net_up_bps', width: 88 },
|
||||
{ title: '↓', key: 'net_down_bps', width: 88 },
|
||||
{ title: '耗时', key: 'duration_ms', width: 72 },
|
||||
|
||||
@@ -8,11 +8,11 @@ import { useWatchPins } from '@/composables/useWatchPins'
|
||||
|
||||
defineOptions({ name: 'WatchMetricsPage' })
|
||||
|
||||
const WATCH_SLOT_COUNT = 4
|
||||
|
||||
const route = useRoute()
|
||||
const tab = ref('trend')
|
||||
const hours = ref(24)
|
||||
const serverId = ref<number | null>(null)
|
||||
const sessionId = ref<number | null>(null)
|
||||
const points = ref<WatchTrendPoint[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedSeries = ref<Array<'cpu_pct' | 'mem_pct' | 'disk_pct' | 'net_up_bps' | 'net_down_bps'>>([
|
||||
@@ -21,21 +21,47 @@ const selectedSeries = ref<Array<'cpu_pct' | 'mem_pct' | 'disk_pct' | 'net_up_bp
|
||||
|
||||
const { slots, refreshPins } = useWatchPins()
|
||||
|
||||
const serverOptions = computed(() =>
|
||||
slots.value
|
||||
.filter((s) => !s.empty && s.server_id)
|
||||
.map((s) => ({ title: s.server_name || `ID ${s.server_id}`, value: s.server_id! })),
|
||||
const slotPanels = computed(() =>
|
||||
Array.from({ length: WATCH_SLOT_COUNT }, (_, slotIndex) => {
|
||||
const slot = slots.value.find((s) => s.slot_index === slotIndex)
|
||||
const empty = !slot || slot.empty
|
||||
return {
|
||||
slotIndex,
|
||||
empty,
|
||||
serverId: empty ? null : (slot.server_id ?? null),
|
||||
serverName: empty ? '' : (slot.server_name || `ID ${slot.server_id}`),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const pinnedServerIds = computed(() =>
|
||||
slotPanels.value
|
||||
.map((p) => p.serverId)
|
||||
.filter((id): id is number => id != null),
|
||||
)
|
||||
|
||||
const pointsByServer = computed(() => {
|
||||
const map = new Map<number, WatchTrendPoint[]>()
|
||||
for (const p of points.value) {
|
||||
const sid = p.server_id
|
||||
if (sid == null) continue
|
||||
const list = map.get(sid) ?? []
|
||||
list.push(p)
|
||||
map.set(sid, list)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
async function loadTrend() {
|
||||
if (!serverId.value) {
|
||||
const ids = pinnedServerIds.value
|
||||
if (!ids.length) {
|
||||
points.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await http.get<{ points: WatchTrendPoint[] }>('/watch/metrics', {
|
||||
server_ids: String(serverId.value),
|
||||
server_ids: ids.join(','),
|
||||
hours: hours.value,
|
||||
})
|
||||
points.value = data.points || []
|
||||
@@ -46,22 +72,18 @@ async function loadTrend() {
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshPins()
|
||||
const qSid = route.query.server_id
|
||||
const qSess = route.query.session_id
|
||||
if (qSid) serverId.value = Number(qSid)
|
||||
if (qSess) sessionId.value = Number(qSess)
|
||||
if (!serverId.value && serverOptions.value.length) {
|
||||
serverId.value = serverOptions.value[0].value
|
||||
}
|
||||
const qTab = route.query.tab
|
||||
if (qTab === 'records') tab.value = 'records'
|
||||
})
|
||||
|
||||
watch([serverId, hours], loadTrend, { immediate: true })
|
||||
watch([pinnedServerIds, hours], loadTrend, { immediate: true, deep: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container fluid class="pa-4 pa-md-6">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<h1 class="text-h6">监测历史</h1>
|
||||
<v-chip size="x-small" variant="tonal" class="ml-2" label>4 槽同时展示</v-chip>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" prepend-icon="mdi-arrow-left" :to="{ name: 'Servers' }">返回服务器</v-btn>
|
||||
</div>
|
||||
@@ -76,17 +98,6 @@ watch([serverId, hours], loadTrend, { immediate: true })
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="trend">
|
||||
<div class="d-flex flex-wrap ga-2 mb-4">
|
||||
<v-select
|
||||
v-model="serverId"
|
||||
:items="serverOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="服务器"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
style="min-width: 200px"
|
||||
/>
|
||||
<v-btn-toggle v-model="hours" mandatory density="compact" variant="outlined" divided>
|
||||
<v-btn :value="1" size="small">1h</v-btn>
|
||||
<v-btn :value="6" size="small">6h</v-btn>
|
||||
@@ -104,7 +115,7 @@ watch([serverId, hours], loadTrend, { immediate: true })
|
||||
]"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="曲线"
|
||||
label="曲线(各槽共用)"
|
||||
multiple
|
||||
chips
|
||||
density="compact"
|
||||
@@ -114,20 +125,62 @@ watch([serverId, hours], loadTrend, { immediate: true })
|
||||
/>
|
||||
</div>
|
||||
<v-progress-linear v-if="loading" indeterminate class="mb-2" />
|
||||
<WatchTrendChart v-if="points.length" :points="points" :series="selectedSeries" />
|
||||
<div v-else class="text-center text-medium-emphasis py-8">
|
||||
暂无趋势数据(需先 Pin 监测并等待探针采样)
|
||||
</div>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
v-for="panel in slotPanels"
|
||||
:key="panel.slotIndex"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-card variant="outlined" rounded="lg" class="watch-metrics-slot pa-3">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-chip size="x-small" variant="tonal" color="info" label>
|
||||
槽 {{ panel.slotIndex + 1 }}
|
||||
</v-chip>
|
||||
<span class="text-subtitle-2 text-truncate ml-2" :title="panel.serverName">
|
||||
{{ panel.empty ? '未占用' : panel.serverName }}
|
||||
</span>
|
||||
</div>
|
||||
<WatchTrendChart
|
||||
v-if="!panel.empty && panel.serverId != null && (pointsByServer.get(panel.serverId)?.length ?? 0) > 0"
|
||||
:points="pointsByServer.get(panel.serverId!) ?? []"
|
||||
:series="selectedSeries"
|
||||
:height="280"
|
||||
/>
|
||||
<div
|
||||
v-else-if="panel.empty"
|
||||
class="text-center text-medium-emphasis watch-metrics-slot__empty"
|
||||
>
|
||||
未添加监测
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-center text-medium-emphasis watch-metrics-slot__empty"
|
||||
>
|
||||
暂无趋势数据(需开启监测并等待探针采样)
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="records">
|
||||
<WatchProbeRecordsTable
|
||||
:server-id="serverId"
|
||||
:session-id="sessionId"
|
||||
:hours="hours"
|
||||
/>
|
||||
<WatchProbeRecordsTable :hours="hours" />
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.watch-metrics-slot {
|
||||
min-height: 320px;
|
||||
}
|
||||
.watch-metrics-slot__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 280px;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user