Files
Nexus/frontend/src/pages/AlertsPage.vue
T
Nexus Agent 3864d120fb fix: alerts page size and schedule once fire_at parsing.
Wire alert history items-per-page to the API and parse fire_at ISO strings to datetime so single-shot push schedules save instead of 500.
2026-06-09 02:47:36 +08:00

217 lines
6.7 KiB
Vue

<template>
<v-container fluid class="pa-6">
<!-- Stats Cards -->
<v-row class="mb-4">
<v-col cols="12" sm="6" lg="3" v-for="stat in stats" :key="stat.label">
<v-list elevation="0" lines="two" rounded="lg" border>
<v-list-item>
<v-list-item-title class="text-body-small">{{ stat.label }}</v-list-item-title>
<v-list-item-title>{{ stat.value }}</v-list-item-title>
<template #append>
<v-icon :color="stat.color" size="30">{{ stat.icon }}</v-icon>
</template>
</v-list-item>
</v-list>
</v-col>
</v-row>
<!-- Alert Table -->
<v-card elevation="0" border rounded="lg">
<v-card-title class="d-flex align-center">
告警历史
<v-spacer />
<v-select
v-model="typeFilter"
:items="typeOptions"
item-title="label"
item-value="value"
label="类型"
variant="outlined"
density="compact"
hide-details
style="max-width: 140px"
clearable
@update:model-value="page = 1; loadAlerts()"
/>
<v-select
v-model="statusFilter"
:items="statusOptions"
item-title="label"
item-value="value"
label="状态"
variant="outlined"
density="compact"
hide-details
style="max-width: 140px"
class="ml-3"
clearable
@update:model-value="page = 1; loadAlerts()"
/>
</v-card-title>
<v-data-table-server
:items="alerts"
:headers="headers"
:items-length="total"
:loading="loading"
:page="page"
:items-per-page="itemsPerPage"
:items-per-page-options="dataTablePageOptions"
hover
density="comfortable"
@update:page="onPageChange"
@update:items-per-page="onItemsPerPageChange"
>
<template #item.alert_type="{ item }">
<v-chip :color="typeColor(item.alert_type)" size="small" variant="tonal" label>
<template #prepend>
<v-icon size="12">{{ typeIcon(item.alert_type) }}</v-icon>
</template>
{{ item.alert_type }}
</v-chip>
</template>
<template #item.server_name="{ item }">
<span class="font-weight-medium">{{ item.server_name }}</span>
</template>
<template #item.value="{ item }">
<span class="text-body-2">{{ item.value }}</span>
</template>
<template #item.is_recovery="{ item }">
<v-chip v-if="item.is_recovery" color="success" size="small" variant="tonal" label>已恢复</v-chip>
<v-chip v-else color="error" size="small" variant="tonal" label>告警中</v-chip>
</template>
<template #item.created_at="{ item }">
<span class="text-caption text-medium-emphasis">{{ formatDateTimeBeijing(item.created_at) }}</span>
</template>
<template #no-data>
<div class="text-center text-medium-emphasis py-6">暂无数据</div>
</template>
</v-data-table-server>
</v-card>
</v-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { usePageActivateRefresh } from '@/composables/usePageActivateRefresh'
import { http } from '@/api'
import { useSnackbar } from '@/composables/useSnackbar'
import type { AlertLogItem, AlertStatsResponse } from '@/types/api'
import { DATA_TABLE_ITEMS_PER_PAGE_OPTIONS, headersWithoutSort } from '@/constants/dataTable'
import { formatDateTimeBeijing } from '@/utils/datetime'
import { fetchPagePerPage } from '@/utils/paginatedFetch'
defineOptions({ name: 'AlertsPage' })
const snackbar = useSnackbar()
// ── State ──
const loading = ref(false)
const alerts = ref<AlertLogItem[]>([])
const total = ref(0)
const page = ref(1)
const itemsPerPage = ref(20)
const dataTablePageOptions = DATA_TABLE_ITEMS_PER_PAGE_OPTIONS
const typeFilter = ref<string | null>(null)
const statusFilter = ref<string | null>(null)
const stats = ref([
{ label: '今日告警', value: '0', color: 'blue', icon: 'mdi-alert' },
{ label: '活跃告警', value: '0', color: 'orange', icon: 'mdi-bell-ring' },
{ label: '已恢复', value: '0', color: 'green', icon: 'mdi-check-circle' },
{ label: 'Top 服务器', value: '—', color: 'red', icon: 'mdi-server' },
])
const typeOptions = [
{ label: 'CPU', value: 'cpu' },
{ label: '内存', value: 'memory' },
{ label: '磁盘', value: 'disk' },
{ label: '连接', value: 'connection' },
]
const statusOptions = [
{ label: '活跃', value: 'active' },
{ label: '已恢复', value: 'recovered' },
]
const headers = headersWithoutSort([
{ title: '类型', key: 'alert_type', width: 100 },
{ title: '服务器', key: 'server_name' },
{ title: '详情', key: 'value' },
{ title: '状态', key: 'is_recovery', width: 100 },
{ title: '时间', key: 'created_at', width: 160 },
])
// ── Data loading ──
async function loadStats() {
try {
const s = await http.get<AlertStatsResponse>('/alert-history/stats')
stats.value[0].value = String(s.today || 0)
stats.value[1].value = String(s.active || 0)
stats.value[2].value = String(s.recovered || 0)
stats.value[3].value = s.top_server || '—'
} catch { snackbar('加载统计失败', 'error') }
}
function onPageChange(p: number) {
page.value = p
loadAlerts()
}
function onItemsPerPageChange(n: number) {
itemsPerPage.value = n
page.value = 1
loadAlerts()
}
async function loadAlerts(silent = false) {
if (!silent) loading.value = true
try {
const res = await fetchPagePerPage<AlertLogItem>(
'/alert-history/',
{
type: typeFilter.value || undefined,
status: statusFilter.value || undefined,
},
page.value,
itemsPerPage.value,
)
alerts.value = res.items
total.value = res.total
if (itemsPerPage.value === -1) {
page.value = 1
}
} catch {
alerts.value = []
total.value = 0
snackbar('加载告警列表失败', 'error')
}
finally { if (!silent) loading.value = false }
}
// ── Helpers ──
function typeColor(t: string) {
if (t === 'cpu' || t === 'CPU') return 'error'
if (t === 'memory' || t === '内存') return 'warning'
if (t === 'disk' || t === '磁盘') return 'info'
return 'primary'
}
function typeIcon(t: string) {
if (t === 'cpu' || t === 'CPU') return 'mdi-chip'
if (t === 'memory' || t === '内存') return 'mdi-memory'
if (t === 'disk' || t === '磁盘') return 'mdi-harddisk'
return 'mdi-lan-disconnect'
}
async function refreshAlertsPage(silent = false) {
await Promise.all([loadStats(), loadAlerts(silent)])
}
usePageActivateRefresh((silent) => refreshAlertsPage(silent))
</script>