From 0d56b2f22110f22600c039114c979f0ac9eebb0a Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Wed, 4 Mar 2026 09:21:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(aiot):=20=E6=88=AA=E5=9B=BE=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E9=80=82=E9=85=8D=20+=20=E5=91=8A=E8=AD=A6?= =?UTF-8?q?=20API=20=E5=88=87=E6=8D=A2=E5=88=B0=20WVP=20+=20=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=BB=A3=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 截图 / ROI: - getSnapUrl: 非 force 模式直接返回 /snap/image 代理 URL(从 DB 读持久化截图, 不触发 Edge),force 模式先请求 Edge 截图再返回代理 URL - RoiCanvas: 添加 ResizeObserver 确保容器尺寸变化时重新初始化 canvas, onImageError 兜底初始化 canvas(截图失败仍可绘制/查看 ROI), snapUrl watcher 触发 canvas 重初始化 告警: - alarm/index.ts: requestClient → wvpRequestClient,路径从 /aiot/alarm/alert/* 切换到 /aiot/device/alert/*(走 WVP 后端) - Alert 类型新增 imagePath、receivedAt、extraData 字段 - alarm list: 缩略图和详情图片通过 WVP /alert/image 代理端点显示, 避免 COS presigned URL 过期问题 - device/index.ts: 新增 getAlertImageUrl() 构造告警图片代理 URL Co-Authored-By: Claude Opus 4.6 --- apps/web-antd/src/api/aiot/alarm/index.ts | 32 +++++++++------ apps/web-antd/src/api/aiot/device/index.ts | 36 +++++++++++++++-- .../src/views/aiot/alarm/list/index.vue | 40 +++++++++++-------- .../aiot/device/roi/components/RoiCanvas.vue | 17 ++++++++ 4 files changed, 91 insertions(+), 34 deletions(-) diff --git a/apps/web-antd/src/api/aiot/alarm/index.ts b/apps/web-antd/src/api/aiot/alarm/index.ts index 0c32d7ddb..a90a19f12 100644 --- a/apps/web-antd/src/api/aiot/alarm/index.ts +++ b/apps/web-antd/src/api/aiot/alarm/index.ts @@ -1,11 +1,12 @@ import type { PageParam, PageResult } from '@vben/request'; -import { requestClient } from '#/api/request'; +import { wvpRequestClient } from '#/api/aiot/request'; export namespace AiotAlarmApi { /** AI 告警 VO */ export interface Alert { id?: number | string; + alertId?: string; alarmId?: string; alertNo?: string; cameraId?: string; @@ -23,6 +24,7 @@ export namespace AiotAlarmApi { triggerTime?: string; message?: string; bbox?: string; + imagePath?: string; snapshotUrl?: string; ossUrl?: string; status?: string; @@ -34,9 +36,11 @@ export namespace AiotAlarmApi { lastFrameTime?: string; workOrderId?: number; aiAnalysis?: Record; + receivedAt?: string; createdAt?: string; updatedAt?: string; logInfo?: AlertLogInfo; + extraData?: string; } /** 告警日志信息 */ @@ -74,20 +78,20 @@ export namespace AiotAlarmApi { } } -// ==================== 告警管理 API ==================== +// ==================== 告警管理 API(WVP 后端)==================== /** 分页查询告警列表 */ export function getAlertPage(params: PageParam) { - return requestClient.get>( - '/aiot/alarm/alert/page', - { params }, + return wvpRequestClient.get>( + '/aiot/device/alert/list', + { params: { page: params.pageNo, count: params.pageSize, ...params } }, ); } /** 获取告警详情 */ export function getAlert(id: number | string) { - return requestClient.get( - `/aiot/alarm/alert/get?id=${id}`, + return wvpRequestClient.get( + `/aiot/device/alert/${id}`, ); } @@ -97,20 +101,22 @@ export function handleAlert( status: string, remark?: string, ) { - return requestClient.put('/aiot/alarm/alert/handle', null, { + return wvpRequestClient.put('/aiot/device/alert/handle', null, { params: { id, status, remark }, }); } /** 删除告警 */ export function deleteAlert(id: number | string) { - return requestClient.delete(`/aiot/alarm/alert/delete?id=${id}`); + return wvpRequestClient.delete('/aiot/device/alert/delete', { + params: { alertId: id }, + }); } /** 获取告警统计 */ export function getAlertStatistics(startTime?: string, endTime?: string) { - return requestClient.get( - '/aiot/alarm/alert/statistics', + return wvpRequestClient.get( + '/aiot/device/alert/statistics', { params: { startTime, endTime } }, ); } @@ -119,8 +125,8 @@ export function getAlertStatistics(startTime?: string, endTime?: string) { /** 以摄像头维度获取告警汇总 */ export function getCameraAlertSummary(params: PageParam) { - return requestClient.get>( - '/aiot/alarm/device-summary/page', + return wvpRequestClient.get>( + '/aiot/device/alert/statistics', { params }, ); } diff --git a/apps/web-antd/src/api/aiot/device/index.ts b/apps/web-antd/src/api/aiot/device/index.ts index b25fcd0a0..10553bbe0 100644 --- a/apps/web-antd/src/api/aiot/device/index.ts +++ b/apps/web-antd/src/api/aiot/device/index.ts @@ -188,16 +188,28 @@ export function deleteRoi(roiId: string) { /** * 获取摄像头截图 URL - * 截图接口需要认证,通过 query param 传递 access-token + * + * 非 force 模式:直接返回 /snap/image 代理 URL(从 DB 读取持久化截图,不触发 Edge) + * force 模式:先触发 Edge 截新图(更新 DB),再返回代理 URL */ export async function getSnapUrl(cameraCode: string, force = false): Promise { const token = await getWvpToken(); + if (force) { + // force 时先触发一次截图请求(确保 Edge 截新图并更新 DB) + try { + await wvpRequestClient.get('/aiot/device/roi/snap', { + params: { cameraCode, force: true }, + }); + } catch { + /* 截图请求可能超时,但 DB 会被更新 */ + } + } + // 返回代理 URL(WVP 从 DB 读 cos_key → 生成 presigned URL → 下载返回) return ( - `${apiURL}/aiot/device/roi/snap` + + `${apiURL}/aiot/device/roi/snap/image` + `?cameraCode=${encodeURIComponent(cameraCode)}` + - `&force=${force}` + `&access-token=${encodeURIComponent(token)}` + - `&t=${Date.now()}` + (force ? `&t=${Date.now()}` : '') ); } @@ -256,3 +268,19 @@ export function exportConfig(cameraId: string) { { params: { cameraId } }, ); } + +// ==================== 告警图片代理 ==================== + +/** + * 构造告警图片代理 URL(通过 WVP 下载 COS 图片后返回字节流) + * @param imagePath COS 对象路径或完整 URL + */ +export async function getAlertImageUrl(imagePath: string): Promise { + if (!imagePath) return ''; + const token = await getWvpToken(); + return ( + `${apiURL}/aiot/device/alert/image` + + `?imagePath=${encodeURIComponent(imagePath)}` + + `&access-token=${encodeURIComponent(token)}` + ); +} diff --git a/apps/web-antd/src/views/aiot/alarm/list/index.vue b/apps/web-antd/src/views/aiot/alarm/list/index.vue index 13dfb88c7..09874d144 100644 --- a/apps/web-antd/src/views/aiot/alarm/list/index.vue +++ b/apps/web-antd/src/views/aiot/alarm/list/index.vue @@ -2,7 +2,7 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { AiotAlarmApi } from '#/api/aiot/alarm'; -import { h, ref } from 'vue'; +import { h, onMounted, ref } from 'vue'; import { Page } from '@vben/common-ui'; @@ -10,6 +10,7 @@ import { Button, Image, message, Modal, Tag } from 'ant-design-vue'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { getAlert, getAlertPage, handleAlert } from '#/api/aiot/alarm'; +import { getAlertImageUrl } from '#/api/aiot/device'; import { ALERT_LEVEL_OPTIONS, @@ -21,6 +22,21 @@ import { defineOptions({ name: 'AiotAlarmList' }); +/** 告警图片 URL 缓存(imagePath → 代理 URL) */ +const imageUrlCache = ref>({}); + +/** 获取告警图片代理 URL(异步构造后缓存) */ +function getImageProxyUrl(row: AiotAlarmApi.Alert): string { + const imagePath = row.imagePath || row.ossUrl || row.snapshotUrl; + if (!imagePath) return ''; + if (imageUrlCache.value[imagePath]) return imageUrlCache.value[imagePath]!; + // 异步构造并缓存 + getAlertImageUrl(imagePath).then((url) => { + imageUrlCache.value[imagePath] = url; + }); + return ''; +} + /** 格式化持续时长(毫秒 → 可读文本) */ function formatDuration(ms: number | null | undefined): string { // 当 duration_ms 为 null 时,说明告警仍在进行中 @@ -247,11 +263,11 @@ const [Grid, gridApi] = useVbenVxeGrid({