From dcb8217a78252f203c5d67c3cf6c6e543fcb96b1 Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Sat, 28 Feb 2026 16:27:30 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix(aiot):=20=E6=88=AA=E5=9B=BE=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E6=B7=BB=E5=8A=A0force=E5=8F=82=E6=95=B0=EF=BC=8C?= =?UTF-8?q?=E7=BB=95=E8=BF=87WVP=205=E5=88=86=E9=92=9F=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getSnapUrl新增force参数,refreshSnap时传force=true, 使WVP后端跳过Redis缓存重新向Edge请求截图。 Co-Authored-By: Claude Opus 4.6 --- apps/web-antd/src/api/aiot/device/index.ts | 3 ++- apps/web-antd/src/views/aiot/device/roi/index.vue | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/web-antd/src/api/aiot/device/index.ts b/apps/web-antd/src/api/aiot/device/index.ts index 7066818f4..b25fcd0a0 100644 --- a/apps/web-antd/src/api/aiot/device/index.ts +++ b/apps/web-antd/src/api/aiot/device/index.ts @@ -190,11 +190,12 @@ export function deleteRoi(roiId: string) { * 获取摄像头截图 URL * 截图接口需要认证,通过 query param 传递 access-token */ -export async function getSnapUrl(cameraCode: string): Promise { +export async function getSnapUrl(cameraCode: string, force = false): Promise { const token = await getWvpToken(); return ( `${apiURL}/aiot/device/roi/snap` + `?cameraCode=${encodeURIComponent(cameraCode)}` + + `&force=${force}` + `&access-token=${encodeURIComponent(token)}` + `&t=${Date.now()}` ); diff --git a/apps/web-antd/src/views/aiot/device/roi/index.vue b/apps/web-antd/src/views/aiot/device/roi/index.vue index 617dc7168..9103a17c6 100644 --- a/apps/web-antd/src/views/aiot/device/roi/index.vue +++ b/apps/web-antd/src/views/aiot/device/roi/index.vue @@ -134,14 +134,14 @@ function goBack() { // ==================== 截图 ==================== -async function buildSnapUrl() { +async function buildSnapUrl(force = false) { if (cameraCode.value) { - snapUrl.value = await getSnapUrl(cameraCode.value); + snapUrl.value = await getSnapUrl(cameraCode.value, force); } } async function refreshSnap() { - await buildSnapUrl(); + await buildSnapUrl(true); } // ==================== ROI 数据加载 ==================== From b9d146c58f3406b586098bd31615f5cfe5cd7ab6 Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Sat, 28 Feb 2026 17:05:51 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20ROI=20=E5=88=9B=E5=BB=BA=E6=97=B6=20?= =?UTF-8?q?deviceId=20=E9=BB=98=E8=AE=A4=E5=80=BC=E6=94=B9=E4=B8=BA=20edge?= =?UTF-8?q?=EF=BC=8C=E4=B8=8E=E8=BE=B9=E7=BC=98=E7=AB=AF=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原值 edge-001 与 Edge 端 EDGE_DEVICE_ID=edge 不匹配,导致 Redis Stream 事件被 Edge 过滤跳过,配置推送无法到达边缘端。 Co-Authored-By: Claude Opus 4.6 --- apps/web-antd/src/views/aiot/device/roi/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web-antd/src/views/aiot/device/roi/index.vue b/apps/web-antd/src/views/aiot/device/roi/index.vue index 9103a17c6..4bd7713b2 100644 --- a/apps/web-antd/src/views/aiot/device/roi/index.vue +++ b/apps/web-antd/src/views/aiot/device/roi/index.vue @@ -187,7 +187,7 @@ async function onRoiDrawn(data: { coordinates: string; roi_type: string }) { priority: 0, enabled: 1, description: '', - deviceId: 'edge-001', // 默认关联边缘设备 + deviceId: 'edge', // 默认关联边缘设备 }; try { await saveRoi(newRoi); From c7afa34f03a1625e80d870b0fa3b564b3061d80b Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Mon, 2 Mar 2026 14:21:13 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix(aiot):=20=E5=91=8A=E8=AD=A6=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=20Image=20=E7=BB=84=E4=BB=B6=E6=B7=BB=E5=8A=A0=20fall?= =?UTF-8?q?back=20=E5=85=9C=E5=BA=95=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 列表缩略图和详情弹窗的 Image 组件均添加 fallback SVG, 当图片 URL 无效或加载失败时显示友好占位符而非损坏图标。 Co-Authored-By: Claude Opus 4.6 --- apps/web-antd/src/views/aiot/alarm/list/index.vue | 2 ++ 1 file changed, 2 insertions(+) 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 2396c7c3f..13dfb88c7 100644 --- a/apps/web-antd/src/views/aiot/alarm/list/index.vue +++ b/apps/web-antd/src/views/aiot/alarm/list/index.vue @@ -252,6 +252,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ :width="40" :height="40" :preview="{ src: row.ossUrl || row.snapshotUrl }" + :fallback="`data:image/svg+xml;utf8,-`" style="object-fit: cover; border-radius: 4px; cursor: pointer" /> - @@ -356,6 +357,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ :src="currentAlert.ossUrl || currentAlert.snapshotUrl" :width="300" :preview="{ src: currentAlert.ossUrl || currentAlert.snapshotUrl }" + :fallback="`data:image/svg+xml;utf8,图片加载失败`" />
From 71fd2a8576acf6e13e6146de0df4c3403d08b62f Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Mon, 2 Mar 2026 16:49:08 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix(aiot):=20=E8=BE=B9=E7=BC=98=E8=AE=BE?= =?UTF-8?q?=E5=A4=87API=E5=88=87=E6=8D=A2=E5=88=B0WVP=E7=9B=B4=E8=B0=83?= =?UTF-8?q?=EF=BC=8CROI=E4=B8=8B=E6=8B=89=E6=A1=86=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - edge/index.ts: requestClient → wvpRequestClient,路径改为 /api/ai/device/* - roi/index.vue: 移除硬编码 edge-001 选项,从 WVP 动态加载设备列表 - 新建 ROI 默认 deviceId 取设备列表第一项,兜底 edge Co-Authored-By: Claude Opus 4.6 --- apps/web-antd/src/api/aiot/edge/index.ts | 17 +++++++------- .../src/views/aiot/device/roi/index.vue | 22 +++++++++++++++---- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/apps/web-antd/src/api/aiot/edge/index.ts b/apps/web-antd/src/api/aiot/edge/index.ts index 39cf9843c..3abc1091d 100644 --- a/apps/web-antd/src/api/aiot/edge/index.ts +++ b/apps/web-antd/src/api/aiot/edge/index.ts @@ -1,6 +1,6 @@ import type { PageParam, PageResult } from '@vben/request'; -import { requestClient } from '#/api/request'; +import { wvpRequestClient } from '#/api/aiot/request'; export namespace AiotEdgeApi { /** 边缘设备 VO */ @@ -34,22 +34,23 @@ export namespace AiotEdgeApi { /** 分页查询边缘设备列表 */ export function getDevicePage(params: PageParam) { - return requestClient.get>( - '/aiot/edge/device/page', + return wvpRequestClient.get>( + '/api/ai/device/page', { params }, ); } /** 获取设备详情 */ -export function getDevice(id: string) { - return requestClient.get( - `/aiot/edge/device/get?id=${id}`, +export function getDevice(deviceId: string) { + return wvpRequestClient.get( + '/api/ai/device/get', + { params: { deviceId } }, ); } /** 获取设备统计 */ export function getDeviceStatistics() { - return requestClient.get( - '/aiot/edge/device/statistics', + return wvpRequestClient.get( + '/api/ai/device/statistics', ); } diff --git a/apps/web-antd/src/views/aiot/device/roi/index.vue b/apps/web-antd/src/views/aiot/device/roi/index.vue index 4bd7713b2..e5dd122a0 100644 --- a/apps/web-antd/src/views/aiot/device/roi/index.vue +++ b/apps/web-antd/src/views/aiot/device/roi/index.vue @@ -35,6 +35,7 @@ import { pushConfig, saveRoi, } from '#/api/aiot/device'; +import { wvpRequestClient } from '#/api/aiot/request'; import RoiAlgorithmBind from './components/RoiAlgorithmBind.vue'; import RoiCanvas from './components/RoiCanvas.vue'; @@ -64,6 +65,17 @@ const selectedRoiId = ref(null); const selectedRoiBindings = ref([]); const snapUrl = ref(''); +const edgeDevices = ref>([]); + +async function loadEdgeDevices() { + try { + const list = await wvpRequestClient.get>('/api/ai/device/list'); + edgeDevices.value = (list as any) || []; + } catch { + edgeDevices.value = [{ deviceId: 'edge' }]; + } +} + const selectedRoi = computed(() => { if (!selectedRoiId.value) return null; return roiList.value.find((r) => r.roiId === selectedRoiId.value) || null; @@ -72,6 +84,7 @@ const selectedRoi = computed(() => { // ==================== 初始化 ==================== onMounted(async () => { + loadEdgeDevices(); const q = route.query; if (q.cameraCode) { cameraCode.value = String(q.cameraCode); @@ -187,7 +200,7 @@ async function onRoiDrawn(data: { coordinates: string; roi_type: string }) { priority: 0, enabled: 1, description: '', - deviceId: 'edge', // 默认关联边缘设备 + deviceId: edgeDevices.value[0]?.deviceId || 'edge', // 默认关联边缘设备 }; try { await saveRoi(newRoi); @@ -416,11 +429,12 @@ function handlePush() { placeholder="选择边缘设备" @change="updateRoiData(selectedRoi!)" > - edge-001(默认) - + + {{ dev.deviceId }} +
- 关联的边缘推理节点,默认 edge-001 + 关联的边缘推理节点
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 5/6] =?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({