摄像头预警

master
lion 5 days ago
parent 5c37cb8f69
commit e4274637f9

@ -8,6 +8,7 @@ const tab = ref('health'); const loading = ref(false); const devices = ref<Camer
const venues = ref<Array<{ id: number; name: string }>>([]); const filters = ref<{ venue_id?: number; status?: string; keyword: string }>({ keyword: '' })
const healthFilter = ref<'all' | 'normal' | 'abnormal'>('all')
const retrying = ref<Record<number, boolean>>({})
const acknowledging = ref<Record<number, boolean>>({})
const summary = ref<Record<string, number>>({})
const recipientText = ref('')
const statusLabel = (s?: string) => ({ ONLINE_PUSHING: '推送正常', PUSH_SUSPECT: '疑似停止', PUSH_STALE: '推送停止', NETWORK_DOWN: '网络不可达', LOGIN_FAILED: '登录认证失败', ARM_FAILED: '布防失败', NO_PUSH_YET: '尚未收到推送', SERVICE_UNAVAILABLE: '监测服务不可用', UNKNOWN: '未知', DISABLED: '已停用', RECONNECTING: '重连中' }[String(s || 'UNKNOWN')] || String(s || '未知'))
@ -31,7 +32,7 @@ function openEdit(row: CameraDevice) { editing.value = row; form.value = { ...ro
async function save() { try { if (editing.value) await updateCameraDevice(editing.value.id, form.value); else await createCameraDevice(form.value); Message.success('已保存'); formVisible.value = false; await load() } catch (e: any) { Message.error(e?.response?.data?.message || e?.message || '保存失败') } }
async function retry(row: CameraDevice) { if (retrying.value[row.id]) return; Modal.confirm({ title: '确认重新连接', content: '请确认摄像头已正常在线或已经重启,再重新链接。', okText: '确认重新连接', cancelText: '关闭', onOk: async () => { retrying.value[row.id] = true; try { await retryCameraDevice(row.id); Message.success('重试指令已下发,正在等待登录、布防和客流推送验证'); await new Promise((resolve) => setTimeout(resolve, 5000)); await load(); retrying.value[row.id] = false } catch (e: any) { retrying.value[row.id] = false; Message.error(e?.response?.data?.message || e?.message || '重试失败') } } }) }
async function remove(row: CameraDevice) { Modal.confirm({ title: '停用摄像头', content: `确定停用 ${row.name}(${row.ip})吗?`, okText: '确认停用', cancelText: '关闭', onOk: async () => { await deleteCameraDevice(row.id); Message.success('已停用'); await load() } }) }
async function ack(row: CameraAlert) { await acknowledgeCameraAlert(row.id); Message.success('已确认'); await load() }
async function ack(row: CameraAlert) { if (acknowledging.value[row.id]) return; acknowledging.value[row.id] = true; try { await acknowledgeCameraAlert(row.id); Message.success('已确认'); await load() } catch (e: any) { const status = e?.response?.status; Message.error(status === 429 ? '操作过于频繁,请稍后再试' : e?.response?.data?.message || e?.message || '确认处理失败') } finally { acknowledging.value[row.id] = false } }
async function saveSettings() { if (!settings.value) return; settings.value.recipient_emails = recipientText.value.split(',').map(x => x.trim()).filter(Boolean); await updateCameraSettings(settings.value); Message.success('通知设置已保存') }
let refreshTimer: ReturnType<typeof setInterval> | undefined
onMounted(() => { void Promise.all([load(), loadVenues()]); refreshTimer = setInterval(() => { if (!loading.value && !Object.values(retrying.value).some(Boolean)) void load() }, 60000) })
@ -46,7 +47,7 @@ onUnmounted(() => { if (refreshTimer) clearInterval(refreshTimer) })
<template #columns><a-table-column title="场馆"><template #cell="{ record }">{{ record.venue?.name || record.venue_name || '未绑定' }}</template></a-table-column><a-table-column title="摄像头"><template #cell="{ record }">{{ record.name }}<div class="muted">{{ record.ip }} / 通道 {{ record.channel }}</div></template></a-table-column><a-table-column title="状态"><template #cell="{ record }"><a-tag :color="statusColor(record.health?.status)">{{ statusLabel(record.health?.status) }}</a-tag></template></a-table-column><a-table-column title="最后推送" :width="190"><template #cell="{ record }">{{ formatTime(record.health?.last_pdc_at_source || record.health?.last_pdc_at) }}</template></a-table-column><a-table-column title="网络连通性" :width="140"><template #cell="{ record }">{{ record.health?.tcp_reachable === true ? '网络正常' : record.health?.tcp_reachable === false ? '网络不可达' : '未检测' }}</template></a-table-column><a-table-column title="操作"><template #cell="{ record }"><a-space><a-button size="small" @click="openEdit(record)">编辑</a-button><a-button size="small" type="primary" :loading="retrying[record.id]" :disabled="retrying[record.id]" @click="retry(record)">{{ retrying[record.id] ? '重新连接中' : '重新连接' }}</a-button><a-button size="small" status="danger" @click="remove(record)">停用</a-button></a-space></template></a-table-column></template>
</a-table>
</a-tab-pane>
<a-tab-pane key="alerts" title="待处理告警"><a-table :data="alerts" :pagination="false"><template #columns><a-table-column title="时间"><template #cell="{ record }">{{ formatTime(record.first_occurred_at) }}</template></a-table-column><a-table-column title="场馆"><template #cell="{ record }">{{ record.venue?.name || '—' }}</template></a-table-column><a-table-column title="设备"><template #cell="{ record }">{{ record.camera?.name }}({{ record.camera?.ip }})</template></a-table-column><a-table-column title="原因" data-index="reason"/><a-table-column title="操作"><template #cell="{ record }"><a-button size="small" @click="ack(record)">确认处理</a-button></template></a-table-column></template></a-table></a-tab-pane>
<a-tab-pane key="alerts" title="待处理告警"><a-table :data="alerts" :pagination="false"><template #columns><a-table-column title="时间"><template #cell="{ record }">{{ formatTime(record.first_occurred_at) }}</template></a-table-column><a-table-column title="场馆"><template #cell="{ record }">{{ record.venue?.name || '—' }}</template></a-table-column><a-table-column title="设备"><template #cell="{ record }">{{ record.camera?.name }}({{ record.camera?.ip }})</template></a-table-column><a-table-column title="原因" data-index="reason"/><a-table-column title="操作"><template #cell="{ record }"><a-button size="small" :loading="acknowledging[record.id]" :disabled="acknowledging[record.id]" @click="ack(record)">确认处理</a-button></template></a-table-column></template></a-table></a-tab-pane>
<a-tab-pane key="settings" title="通知设置"><a-form v-if="settings" :model="settings" layout="vertical" style="max-width: 680px"><a-form-item label="接收邮箱"><a-input v-model="recipientText" placeholder="多个邮箱用逗号分隔" /></a-form-item><a-form-item label="通知开始时间"><a-time-picker v-model="settings.notification_start_time" format="HH:mm" /></a-form-item><a-form-item label="通知结束时间"><a-time-picker v-model="settings.notification_end_time" format="HH:mm" /></a-form-item><a-form-item label="客流推送超时时间"><a-input-number v-model="settings.default_push_timeout_seconds" :min="60" :max="86400" /><div class="form-help">超过这个时间没有收到该摄像头的客流数据,系统会判定为推送异常,并继续进行自动重连判断。单位:秒。</div></a-form-item><a-form-item><a-button type="primary" @click="saveSettings">保存设置</a-button></a-form-item></a-form></a-tab-pane>
</a-tabs>
</a-card>

Loading…
Cancel
Save