Files
Nexus/frontend/src/pages/AlertsPage.vue
T
Nexus Agent 08a0157c95 feat: 调度表单重构、登录门控、批量任务与页面缓存对齐
调度页执行周期可视化/单次执行/分类选机/推送源对齐;登录 IP 门控与无缝导航;
服务器批量后台任务、执行记录、凭据合并、各页激活刷新与错误提示修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 11:17:21 +08:00

190 lines
6.0 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="20"
hover
density="comfortable"
@update:page="page = $event; loadAlerts()"
>
<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">{{ 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 { PaginatedResponse, AlertLogItem, AlertStatsResponse } from '@/types/api'
import { headersWithoutSort } from '@/constants/dataTable'
defineOptions({ name: 'AlertsPage' })
const snackbar = useSnackbar()
// ── State ──
const loading = ref(false)
const alerts = ref<AlertLogItem[]>([])
const total = ref(0)
const page = ref(1)
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') }
}
async function loadAlerts(silent = false) {
if (!silent) loading.value = true
try {
const res = await http.get<PaginatedResponse<AlertLogItem>>('/alert-history/', {
page: page.value,
per_page: 20,
type: typeFilter.value || undefined,
status: statusFilter.value || undefined,
})
alerts.value = res.items || []
total.value = res.total || 0
} catch { alerts.value = [] }
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>