You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/frontend/src/views/alert/AlertPage.vue

214 lines
9.1 KiB
Vue

<template>
<div>
<!-- 统计区 -->
<el-row :gutter="16" style="margin-bottom:20px">
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center;padding:8px"><div style="color:#909399;font-size:12px">未处理</div><div style="font-size:24px;font-weight:bold;color:#e6a23c">{{ stats.unresolved }}</div></div></el-card></el-col>
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center;padding:8px"><div style="color:#909399;font-size:12px">采集失败</div><div style="font-size:24px;font-weight:bold">{{ stats.collectFail }}</div></div></el-card></el-col>
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center;padding:8px"><div style="color:#909399;font-size:12px">设备离线</div><div style="font-size:24px;font-weight:bold">{{ stats.deviceOffline }}</div></div></el-card></el-col>
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center;padding:8px"><div style="color:#909399;font-size:12px">产量异常</div><div style="font-size:24px;font-weight:bold">{{ stats.productionAnomaly }}</div></div></el-card></el-col>
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center;padding:8px"><div style="color:#909399;font-size:12px">未知设备</div><div style="font-size:24px;font-weight:bold">{{ stats.unknownDevice }}</div></div></el-card></el-col>
</el-row>
<!-- 查询条件 -->
<el-form :inline="true" class="mb-16">
<el-form-item label="告警类型">
<el-select v-model="query.alertType" clearable placeholder="全部">
<el-option label="采集失败" value="collect_fail" />
<el-option label="设备离线" value="device_offline" />
<el-option label="产量异常" value="production_anomaly" />
<el-option label="未知设备" value="unknown_device" />
<el-option label="服务错误" value="service_error" />
</el-select>
</el-form-item>
<el-form-item label="处理状态">
<el-select v-model="query.isResolved" clearable placeholder="全部">
<el-option label="未处理" :value="0" />
<el-option label="已处理" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker v-model="query.dateRange" type="daterange" value-format="YYYY-MM-DD" clearable />
</el-form-item>
<el-form-item label="机床">
<el-select v-model="query.machineId" filterable clearable placeholder="全部">
<el-option v-for="m in machineList" :key="m.id" :label="m.name" :value="m.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="query.keyword" placeholder="标题/详情" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<div style="margin-bottom:12px">
<el-button type="primary" @click="batchResolve" :disabled="!selectedRows.length || !selectedRows.some((r: Alert) => !r.isResolved)">批量标记已处理</el-button>
</div>
<!-- 列表 -->
<el-table :data="tableData" border stripe v-loading="loading" @selection-change="(rows: Alert[]) => selectedRows = rows">
<el-table-column type="selection" width="50" fixed="left" align="center" />
<el-table-column prop="createdAt" label="告警时间" sortable />
<el-table-column label="告警类型" align="center">
<template #default="{ row }">
<el-tag :type="alertTypeTag(row.alertType)" size="small">{{ alertTypeLabel(row.alertType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" show-overflow-tooltip />
<el-table-column prop="machineName" label="机床" show-overflow-tooltip align="center" />
<el-table-column prop="detail" label="详情" show-overflow-tooltip />
<el-table-column label="处理状态" align="center">
<template #default="{ row }">
<el-tag :type="row.isResolved ? 'success' : 'danger'" size="small">{{ row.isResolved ? '已处理' : '未处理' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<div style="white-space:nowrap">
<el-button v-if="!row.isResolved" link type="primary" @click="handleResolve(row)">标记已处理</el-button>
<el-button v-else link type="primary" @click="viewDetail(row)">查看</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="page.page"
v-model:page-size="page.pageSize"
:page-sizes="[20, 50, 100]"
:total="page.total"
background
layout="total, sizes, prev, pager, next, jumper"
/>
<!-- 告警详情弹窗 -->
<el-dialog v-model="detailVisible" title="告警详情" width="600px" destroy-on-close>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="告警时间">{{ detailRow.createdAt }}</el-descriptions-item>
<el-descriptions-item label="告警类型">
<el-tag :type="alertTypeTag(detailRow.alertType)" size="small">{{ alertTypeLabel(detailRow.alertType) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="标题">{{ detailRow.title }}</el-descriptions-item>
<el-descriptions-item label="机床">{{ detailRow.machineName || '-' }}</el-descriptions-item>
<el-descriptions-item label="详情">{{ detailRow.detail }}</el-descriptions-item>
<el-descriptions-item label="处理状态">
<el-tag :type="detailRow.isResolved ? 'success' : 'danger'" size="small">{{ detailRow.isResolved ? '已处理' : '未处理' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处理时间">{{ detailRow.resolvedAt || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
import type { ApiResponse, Alert, Machine } from '@/types'
/** */
interface AlertStats {
unresolved: number
collectFail: number
deviceOffline: number
productionAnomaly: number
unknownDevice: number
}
const loading = ref(false)
const tableData = ref<Alert[]>([])
const selectedRows = ref<Alert[]>([])
const stats = ref<AlertStats>({} as AlertStats)
const machineList = ref<Machine[]>([])
const detailVisible = ref(false)
const detailRow = ref<Alert>({} as Alert)
const query = reactive({
alertType: '',
isResolved: 0 as number | undefined,
dateRange: null as string[] | null,
machineId: undefined as number | undefined,
keyword: '',
})
const page = reactive({ page: 1, pageSize: 20, total: 0 })
// 告警类型标签映射
function alertTypeTag(type: string): string {
const map: Record<string, string> = {
collect_fail: 'warning',
device_offline: 'danger',
production_anomaly: 'warning',
unknown_device: 'info',
service_error: 'danger',
}
return map[type] || 'info'
}
function alertTypeLabel(type: string): string {
const map: Record<string, string> = {
collect_fail: '采集失败',
device_offline: '设备离线',
production_anomaly: '产量异常',
unknown_device: '未知设备',
service_error: '服务错误',
}
return map[type] || type
}
async function loadData() {
loading.value = true
try {
const [s, d] = await Promise.all([
request.get<AlertStats>('/admin/alert/statistics'),
request.get<{ items: Alert[]; total: number }>('/admin/alert', { params: { ...query, ...page } }),
])
stats.value = s.data || {} as AlertStats
tableData.value = d.data?.items || []
page.total = d.data?.total || 0
} finally {
loading.value = false
}
}
function resetQuery() {
Object.assign(query, { alertType: '', isResolved: 0, dateRange: null, machineId: undefined, keyword: '' })
loadData()
}
async function handleResolve(row: Alert) {
await ElMessageBox.confirm('确定标记为已处理?', '提示', { type: 'warning' })
await request.post('/admin/alert/resolve', { id: row.id })
ElMessage.success('已标记为已处理')
loadData()
}
async function batchResolve() {
const unresolved = selectedRows.value.filter((r: Alert) => !r.isResolved)
await ElMessageBox.confirm(`确定对选中的${unresolved.length}项标记为已处理?`, '提示', { type: 'warning' })
await request.post('/admin/alert/batch-resolve', { ids: unresolved.map((r: Alert) => r.id) })
ElMessage.success('批量标记成功')
loadData()
}
function viewDetail(row: Alert) {
detailRow.value = row
detailVisible.value = true
}
async function loadDrops() {
const r = await request.get<{ items: Machine[] }>('/admin/machine/list')
machineList.value = r.data?.items || []
}
onMounted(() => {
loadData()
loadDrops()
})
</script>