2026-06-12 02:56:21 +08:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
import type { WatchSlot } from '@/composables/useWatchPins'
|
|
|
|
|
import { formatBytesPerSec, formatRemaining, probeStatusColor } from '@/utils/watchFormat'
|
|
|
|
|
import WatchSparkline from '@/components/watch/WatchSparkline.vue'
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
slot: WatchSlot
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
remove: [slotIndex: number]
|
|
|
|
|
processes: [slot: WatchSlot]
|
|
|
|
|
history: [slot: WatchSlot]
|
2026-06-12 03:07:00 +08:00
|
|
|
pick: [slotIndex: number]
|
2026-06-12 02:56:21 +08:00
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const chartMetric = ref<'cpu_pct' | 'mem_pct' | 'disk_pct'>('cpu_pct')
|
|
|
|
|
|
|
|
|
|
function pct(v: number | null | undefined) {
|
|
|
|
|
return v != null ? `${v}%` : '—'
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<v-card
|
|
|
|
|
v-if="!slot.empty"
|
|
|
|
|
elevation="0"
|
|
|
|
|
border
|
|
|
|
|
rounded="lg"
|
2026-06-12 03:07:00 +08:00
|
|
|
class="watch-slot-card pa-3 watch-slot-card--filled"
|
2026-06-12 02:56:21 +08:00
|
|
|
:class="{ 'watch-slot-card--error': slot.metrics?.probe_status && slot.metrics.probe_status !== 'ok' }"
|
2026-06-12 03:07:00 +08:00
|
|
|
role="button"
|
|
|
|
|
tabindex="0"
|
|
|
|
|
@click="emit('history', slot)"
|
|
|
|
|
@keydown.enter.prevent="emit('history', slot)"
|
2026-06-12 02:56:21 +08:00
|
|
|
>
|
2026-06-12 03:07:00 +08:00
|
|
|
<div class="d-flex align-center mb-2" @click.stop>
|
2026-06-12 02:56:21 +08:00
|
|
|
<div class="text-subtitle-2 text-truncate flex-grow-1" :title="slot.server_name">
|
|
|
|
|
{{ slot.server_name || `ID ${slot.server_id}` }}
|
|
|
|
|
</div>
|
|
|
|
|
<v-chip size="x-small" variant="tonal" color="info" label>
|
|
|
|
|
槽 {{ slot.slot_index + 1 }}
|
|
|
|
|
</v-chip>
|
|
|
|
|
<v-btn icon="mdi-close" size="x-small" variant="text" @click="emit('remove', slot.slot_index)" />
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-12 03:07:00 +08:00
|
|
|
<div @click.stop>
|
2026-06-12 02:56:21 +08:00
|
|
|
<v-chip
|
|
|
|
|
v-if="slot.metrics?.probe_status"
|
|
|
|
|
size="x-small"
|
|
|
|
|
:color="probeStatusColor(slot.metrics.probe_status)"
|
|
|
|
|
variant="tonal"
|
|
|
|
|
class="mb-2"
|
|
|
|
|
label
|
|
|
|
|
>
|
|
|
|
|
{{ slot.metrics.probe_status === 'ok' ? '正常' : slot.metrics.probe_status }}
|
|
|
|
|
</v-chip>
|
|
|
|
|
|
|
|
|
|
<div class="d-flex justify-space-between text-caption mb-1">
|
|
|
|
|
<span>CPU {{ pct(slot.metrics?.cpu_pct) }}</span>
|
|
|
|
|
<span>MEM {{ pct(slot.metrics?.mem_pct) }}</span>
|
|
|
|
|
<span>DISK {{ pct(slot.metrics?.disk_pct) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="text-caption text-medium-emphasis mb-1">
|
|
|
|
|
↑ {{ formatBytesPerSec(slot.metrics?.net_up_bps) }}
|
|
|
|
|
· ↓ {{ formatBytesPerSec(slot.metrics?.net_down_bps) }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-caption text-medium-emphasis mb-2">
|
|
|
|
|
读 {{ formatBytesPerSec(slot.metrics?.disk_read_bps) }}
|
|
|
|
|
· 写 {{ formatBytesPerSec(slot.metrics?.disk_write_bps) }}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<v-btn-toggle v-model="chartMetric" density="compact" variant="outlined" divided class="mb-1">
|
|
|
|
|
<v-btn value="cpu_pct" size="x-small">CPU</v-btn>
|
|
|
|
|
<v-btn value="mem_pct" size="x-small">MEM</v-btn>
|
|
|
|
|
<v-btn value="disk_pct" size="x-small">DISK</v-btn>
|
|
|
|
|
</v-btn-toggle>
|
|
|
|
|
|
|
|
|
|
<WatchSparkline :points="slot.sparkline || []" :metric="chartMetric" />
|
|
|
|
|
|
|
|
|
|
<div v-if="slot.metrics?.error" class="text-caption text-error mt-1 text-truncate">
|
|
|
|
|
{{ slot.metrics.error }}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="d-flex align-center justify-space-between mt-2">
|
|
|
|
|
<span class="text-caption text-medium-emphasis">剩余 {{ formatRemaining(slot.remaining_sec) }}</span>
|
|
|
|
|
<div class="d-flex ga-1">
|
2026-06-12 03:07:00 +08:00
|
|
|
<v-btn size="x-small" variant="text" @click.stop="emit('processes', slot)">进程</v-btn>
|
|
|
|
|
<v-btn size="x-small" variant="text" @click.stop="emit('history', slot)">历史</v-btn>
|
2026-06-12 02:56:21 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-06-12 03:07:00 +08:00
|
|
|
</div>
|
2026-06-12 02:56:21 +08:00
|
|
|
</v-card>
|
|
|
|
|
|
|
|
|
|
<v-card
|
|
|
|
|
v-else
|
|
|
|
|
elevation="0"
|
|
|
|
|
border
|
|
|
|
|
rounded="lg"
|
|
|
|
|
class="watch-slot-card watch-slot-card--empty d-flex align-center justify-center pa-4"
|
|
|
|
|
variant="outlined"
|
2026-06-12 03:07:00 +08:00
|
|
|
role="button"
|
|
|
|
|
tabindex="0"
|
|
|
|
|
@click="emit('pick', slot.slot_index)"
|
|
|
|
|
@keydown.enter.prevent="emit('pick', slot.slot_index)"
|
2026-06-12 02:56:21 +08:00
|
|
|
>
|
|
|
|
|
<div class="text-center text-medium-emphasis">
|
|
|
|
|
<v-icon size="28" class="mb-1">mdi-plus-circle-outline</v-icon>
|
2026-06-12 03:07:00 +08:00
|
|
|
<div class="text-caption">槽 {{ slot.slot_index + 1 }} · 添加监测</div>
|
2026-06-12 02:56:21 +08:00
|
|
|
</div>
|
|
|
|
|
</v-card>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.watch-slot-card {
|
|
|
|
|
min-height: 220px;
|
|
|
|
|
}
|
|
|
|
|
.watch-slot-card--empty {
|
|
|
|
|
min-height: 220px;
|
|
|
|
|
border-style: dashed !important;
|
2026-06-12 03:07:00 +08:00
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
.watch-slot-card--empty:hover {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.04);
|
|
|
|
|
}
|
|
|
|
|
.watch-slot-card--filled {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
.watch-slot-card--filled:hover {
|
|
|
|
|
background: rgba(var(--v-theme-surface-variant), 0.3);
|
2026-06-12 02:56:21 +08:00
|
|
|
}
|
|
|
|
|
.watch-slot-card--error {
|
|
|
|
|
border-color: rgb(var(--v-theme-error)) !important;
|
|
|
|
|
}
|
|
|
|
|
</style>
|