Merge branch 'feature'
This commit is contained in:
@@ -188,14 +188,19 @@ export function deleteRoi(roiId: string) {
|
||||
|
||||
/**
|
||||
* 获取摄像头截图 URL
|
||||
* 截图接口需要认证,通过 query param 传递 access-token
|
||||
*
|
||||
* /snap 端点会自动处理缓存逻辑:
|
||||
* - 有 Redis 缓存时直接 302 重定向到 COS presigned URL(快)
|
||||
* - 无缓存时触发 Edge 截图,等待完成后重定向(首次较慢)
|
||||
* - force=true 时强制触发 Edge 截新图
|
||||
*/
|
||||
export async function getSnapUrl(cameraCode: string): Promise<string> {
|
||||
export async function getSnapUrl(cameraCode: string, force = false): Promise<string> {
|
||||
const token = await getWvpToken();
|
||||
return (
|
||||
`${apiURL}/aiot/device/roi/snap` +
|
||||
`?cameraCode=${encodeURIComponent(cameraCode)}` +
|
||||
`&access-token=${encodeURIComponent(token)}` +
|
||||
(force ? `&force=true` : '') +
|
||||
`&t=${Date.now()}`
|
||||
);
|
||||
}
|
||||
@@ -255,3 +260,19 @@ export function exportConfig(cameraId: string) {
|
||||
{ params: { cameraId } },
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 告警图片代理 ====================
|
||||
|
||||
/**
|
||||
* 构造告警图片代理 URL(通过 WVP 下载 COS 图片后返回字节流)
|
||||
* @param imagePath COS 对象路径或完整 URL
|
||||
*/
|
||||
export async function getAlertImageUrl(imagePath: string): Promise<string> {
|
||||
if (!imagePath) return '';
|
||||
const token = await getWvpToken();
|
||||
return (
|
||||
`${apiURL}/aiot/device/alert/image` +
|
||||
`?imagePath=${encodeURIComponent(imagePath)}` +
|
||||
`&access-token=${encodeURIComponent(token)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<PageResult<AiotEdgeApi.Device>>(
|
||||
'/aiot/edge/device/page',
|
||||
return wvpRequestClient.get<PageResult<AiotEdgeApi.Device>>(
|
||||
'/api/ai/device/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取设备详情 */
|
||||
export function getDevice(id: string) {
|
||||
return requestClient.get<AiotEdgeApi.Device>(
|
||||
`/aiot/edge/device/get?id=${id}`,
|
||||
export function getDevice(deviceId: string) {
|
||||
return wvpRequestClient.get<AiotEdgeApi.Device>(
|
||||
'/api/ai/device/get',
|
||||
{ params: { deviceId } },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取设备统计 */
|
||||
export function getDeviceStatistics() {
|
||||
return requestClient.get<AiotEdgeApi.DeviceStatistics>(
|
||||
'/aiot/edge/device/statistics',
|
||||
return wvpRequestClient.get<AiotEdgeApi.DeviceStatistics>(
|
||||
'/api/ai/device/statistics',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 | string>(null);
|
||||
const selectedRoiBindings = ref<AiotDeviceApi.RoiAlgoBinding[]>([]);
|
||||
const snapUrl = ref('');
|
||||
|
||||
const edgeDevices = ref<Array<{ deviceId: string }>>([]);
|
||||
|
||||
async function loadEdgeDevices() {
|
||||
try {
|
||||
const list = await wvpRequestClient.get<Array<{ deviceId: string }>>('/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);
|
||||
@@ -134,14 +147,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 数据加载 ====================
|
||||
@@ -187,7 +200,7 @@ async function onRoiDrawn(data: { coordinates: string; roi_type: string }) {
|
||||
priority: 0,
|
||||
enabled: 1,
|
||||
description: '',
|
||||
deviceId: 'edge-001', // 默认关联边缘设备
|
||||
deviceId: edgeDevices.value[0]?.deviceId || 'edge', // 默认关联边缘设备
|
||||
};
|
||||
try {
|
||||
await saveRoi(newRoi);
|
||||
@@ -416,11 +429,12 @@ function handlePush() {
|
||||
placeholder="选择边缘设备"
|
||||
@change="updateRoiData(selectedRoi!)"
|
||||
>
|
||||
<Select.Option value="edge-001">edge-001(默认)</Select.Option>
|
||||
<!-- 未来支持动态加载 -->
|
||||
<Select.Option v-for="dev in edgeDevices" :key="dev.deviceId" :value="dev.deviceId">
|
||||
{{ dev.deviceId }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #999">
|
||||
关联的边缘推理节点,默认 edge-001
|
||||
关联的边缘推理节点
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="颜色">
|
||||
|
||||
Reference in New Issue
Block a user