08a0157c95
调度页执行周期可视化/单次执行/分类选机/推送源对齐;登录 IP 门控与无缝导航; 服务器批量后台任务、执行记录、凭据合并、各页激活刷新与错误提示修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
6.0 KiB
Vue
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>
|