fix(aiot): 截图持久化适配 + 告警 API 切换到 WVP + 图片代理
截图 / 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Record<string, string>>({});
|
||||
|
||||
/** 获取告警图片代理 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({
|
||||
<!-- 截图缩略图列 -->
|
||||
<template #snapshot="{ row }">
|
||||
<Image
|
||||
v-if="row.snapshotUrl || row.ossUrl"
|
||||
:src="row.ossUrl || row.snapshotUrl"
|
||||
v-if="getImageProxyUrl(row)"
|
||||
:src="getImageProxyUrl(row)"
|
||||
:width="40"
|
||||
:height="40"
|
||||
:preview="{ src: row.ossUrl || row.snapshotUrl }"
|
||||
:preview="{ src: getImageProxyUrl(row) }"
|
||||
:fallback="`data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40'><rect width='40' height='40' fill='%23f0f0f0'/><text x='50%25' y='55%25' dominant-baseline='middle' text-anchor='middle' fill='%23bbb' font-size='12'>-</text></svg>`"
|
||||
style="object-fit: cover; border-radius: 4px; cursor: pointer"
|
||||
/>
|
||||
@@ -350,26 +366,16 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
</div>
|
||||
|
||||
<!-- 告警截图 -->
|
||||
<div v-if="currentAlert.ossUrl || currentAlert.snapshotUrl">
|
||||
<div v-if="getImageProxyUrl(currentAlert)">
|
||||
<span class="text-gray-500">告警截图:</span>
|
||||
<div class="mt-2">
|
||||
<Image
|
||||
:src="currentAlert.ossUrl || currentAlert.snapshotUrl"
|
||||
:src="getImageProxyUrl(currentAlert)"
|
||||
:width="300"
|
||||
:preview="{ src: currentAlert.ossUrl || currentAlert.snapshotUrl }"
|
||||
:preview="{ src: getImageProxyUrl(currentAlert) }"
|
||||
:fallback="`data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='200'><rect width='300' height='200' fill='%23f5f5f5'/><text x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23bbb' font-size='14'>图片加载失败</text></svg>`"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- 检测区域 -->
|
||||
|
||||
@@ -47,17 +47,32 @@ watch(() => props.drawMode, () => {
|
||||
watch(() => props.snapUrl, () => {
|
||||
loading.value = true;
|
||||
errorMsg.value = '';
|
||||
nextTick(() => initCanvas());
|
||||
});
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initCanvas();
|
||||
if (wrapper.value && typeof ResizeObserver !== 'undefined') {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (wrapper.value && wrapper.value.clientWidth > 0) {
|
||||
initCanvas();
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(wrapper.value);
|
||||
}
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
function onImageLoad() {
|
||||
@@ -68,6 +83,8 @@ function onImageLoad() {
|
||||
function onImageError() {
|
||||
loading.value = false;
|
||||
errorMsg.value = '截图加载失败,请确认摄像头正在拉流';
|
||||
// 关键:截图失败也初始化 canvas,使 ROI 区域可见可操作
|
||||
nextTick(() => initCanvas());
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
|
||||
Reference in New Issue
Block a user