feat(aiot): 告警截图展示 + 全局配置同步 + API兼容修复

- 告警列表新增截图缩略图列,支持预览大图
- 告警详情显示截图 URL 链接
- 摄像头管理页新增「同步全局配置」按钮
- 告警 API 路径修正: camera-summary → device-summary
- 告警 ID 兼容 alarmId 字符串格式
- Vite 代理新增 /uploads、/captures、/aiot/storage 路由

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 09:57:29 +08:00
parent 777e52986e
commit e54fcf1f8c
6 changed files with 94 additions and 8 deletions

View File

@@ -5,7 +5,8 @@ import { requestClient } from '#/api/request';
export namespace AiotAlarmApi {
/** AI 告警 VO */
export interface Alert {
id?: number;
id?: number | string;
alarmId?: string;
alertNo?: string;
cameraId?: string;
cameraName?: string;
@@ -81,21 +82,25 @@ export function getAlertPage(params: PageParam) {
}
/** 获取告警详情 */
export function getAlert(id: number) {
export function getAlert(id: number | string) {
return requestClient.get<AiotAlarmApi.Alert>(
`/aiot/alarm/alert/get?id=${id}`,
);
}
/** 处理告警 */
export function handleAlert(id: number, status: string, remark?: string) {
export function handleAlert(
id: number | string,
status: string,
remark?: string,
) {
return requestClient.put('/aiot/alarm/alert/handle', null, {
params: { id, status, remark },
});
}
/** 删除告警 */
export function deleteAlert(id: number) {
export function deleteAlert(id: number | string) {
return requestClient.delete(`/aiot/alarm/alert/delete?id=${id}`);
}
@@ -112,7 +117,7 @@ export function getAlertStatistics(startTime?: string, endTime?: string) {
/** 以摄像头维度获取告警汇总 */
export function getCameraAlertSummary(params: PageParam) {
return requestClient.get<PageResult<AiotAlarmApi.CameraAlertSummary>>(
'/aiot/alarm/camera-summary/page',
'/aiot/alarm/device-summary/page',
{ params },
);
}

View File

@@ -244,6 +244,13 @@ export function pushConfig(cameraId: string) {
});
}
/** 一次性推送全部配置到本地Edge */
export function pushAllConfig() {
return wvpRequestClient.post<Record<string, any>>(
'/aiot/device/config/push-all',
);
}
/** 导出摄像头配置 JSON */
export function exportConfig(cameraId: string) {
return wvpRequestClient.get<Record<string, any>>(

View File

@@ -119,6 +119,13 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
minWidth: 170,
formatter: 'formatDateTime',
},
{
field: 'snapshotUrl',
title: '截图',
width: 80,
align: 'center' as const,
slots: { default: 'snapshot' },
},
{
field: 'createdAt',
title: '创建时间',

View File

@@ -84,7 +84,12 @@ function getLevelColor(level?: string) {
/** 查看告警详情 */
async function handleView(row: AiotAlarmApi.Alert) {
try {
const alert = await getAlert(row.id as number);
const alertId = row.alarmId || row.id;
if (!alertId) {
message.error('告警ID为空');
return;
}
const alert = await getAlert(alertId);
currentAlert.value = alert;
detailVisible.value = true;
} catch (error) {
@@ -119,7 +124,7 @@ async function handleProcess(row: AiotAlarmApi.Alert, status: string) {
duration: 0,
});
try {
await handleAlert(row.id as number, status, remark);
await handleAlert(row.alarmId || row.id!, status, remark);
message.success(`${statusText}成功`);
handleRefresh();
} catch (error) {
@@ -217,6 +222,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
</Tag>
</template>
<!-- 截图缩略图列 -->
<template #snapshot="{ row }">
<Image
v-if="row.snapshotUrl || row.ossUrl"
:src="row.ossUrl || row.snapshotUrl"
:width="40"
:height="40"
:preview="{ src: row.ossUrl || row.snapshotUrl }"
style="object-fit: cover; border-radius: 4px; cursor: pointer"
/>
<span v-else class="text-gray-400">-</span>
</template>
<!-- 操作列 -->
<template #actions="{ row }">
<TableAction
@@ -322,6 +340,16 @@ const [Grid, gridApi] = useVbenVxeGrid({
:preview="{ src: currentAlert.ossUrl || currentAlert.snapshotUrl }"
/>
</div>
<div class="mt-1">
<a
:href="currentAlert.ossUrl || currentAlert.snapshotUrl"
target="_blank"
rel="noopener noreferrer"
class="text-blue-500 text-xs hover:underline"
>
{{ currentAlert.ossUrl || currentAlert.snapshotUrl }}
</a>
</div>
</div>
<!-- 检测区域 -->

View File

@@ -32,6 +32,7 @@ import {
getCameraList,
getMediaServerList,
getRoiByCameraId,
pushAllConfig,
saveCamera,
startCamera,
stopCamera,
@@ -278,6 +279,24 @@ async function handleExport(row: AiotDeviceApi.Camera) {
}
}
// ==================== 同步全局配置 ====================
const syncing = ref(false);
async function handleSyncAll() {
syncing.value = true;
try {
const res = await pushAllConfig();
message.success(
`同步完成ROI ${res.rois ?? 0} 条,算法绑定 ${res.binds ?? 0}`,
);
} catch {
message.error('同步全局配置失败');
} finally {
syncing.value = false;
}
}
// ==================== 分页 ====================
function handlePageChange(p: number, size: number) {
@@ -326,7 +345,12 @@ onMounted(() => {
/>
<Button type="primary" @click="loadData">查询</Button>
</div>
<Button type="primary" @click="handleAdd">添加摄像头</Button>
<Space>
<Button type="primary" :loading="syncing" @click="handleSyncAll">
同步全局配置
</Button>
<Button type="primary" @click="handleAdd">添加摄像头</Button>
</Space>
</div>
<!-- 表格 -->

View File

@@ -18,6 +18,21 @@ export default defineConfig(async () => {
changeOrigin: true,
target: 'http://127.0.0.1:8000',
},
// 告警截图静态文件 -> 告警服务 :8000
'/uploads': {
changeOrigin: true,
target: 'http://127.0.0.1:8000',
},
// Edge 本地截图COS 未配置时回退)-> 告警服务 :8000
'/captures': {
changeOrigin: true,
target: 'http://127.0.0.1:8000',
},
// COS 存储相关接口 -> 告警服务 :8000
'/admin-api/aiot/storage': {
changeOrigin: true,
target: 'http://127.0.0.1:8000',
},
// aiot/device/* -> WVP :18080按子路径分别 rewrite
// 注意:更具体的路径必须写在通配路径前面