功能:摄像头状态指示器 + 截图缓存优化 + 截图预热
1. 摄像头列表状态指示器(绿/红点) - 新增状态列显示摄像头截图可用性 - 绿色=截图正常,红色=截图失败,灰色=加载中 - 使用 /snap/image 代理端点检测状态 2. 截图缓存优化(getSnapUrl 重写) - 非强制模式:返回稳定URL(无时间戳),浏览器缓存5分钟 - 强制模式:先触发边缘截图,再返回带时间戳URL破缓存 - 使用 /snap/image 代理端点,避免COS预签名URL过期问题 3. 截图预热(新增摄像头时) - 添加摄像头成功后自动触发首次截图 - 预热失败不影响主流程(非阻塞) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,9 +32,11 @@ import {
|
||||
getCameraList,
|
||||
getMediaServerList,
|
||||
getRoiByCameraId,
|
||||
getSnapUrl,
|
||||
pushAllConfig,
|
||||
saveCamera,
|
||||
} from '#/api/aiot/device';
|
||||
import { wvpRequestClient } from '#/api/aiot/request';
|
||||
|
||||
defineOptions({ name: 'AiotDeviceCamera' });
|
||||
|
||||
@@ -45,12 +47,14 @@ const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const cameraList = ref<AiotDeviceApi.Camera[]>([]);
|
||||
const roiCounts = ref<Record<string, number>>({});
|
||||
const cameraStatus = ref<Record<string, boolean | null>>({});
|
||||
const page = ref(1);
|
||||
const pageSize = ref(15);
|
||||
const total = ref(0);
|
||||
const searchQuery = ref('');
|
||||
|
||||
const columns = [
|
||||
{ title: '状态', key: 'status', width: 60, align: 'center' as const },
|
||||
{ title: '应用名', dataIndex: 'app', width: 120 },
|
||||
{ title: '流ID', dataIndex: 'stream', width: 150 },
|
||||
{ title: '拉流地址', dataIndex: 'srcUrl', ellipsis: true },
|
||||
@@ -113,6 +117,7 @@ async function loadData() {
|
||||
cameraList.value = res.list || [];
|
||||
total.value = res.total || 0;
|
||||
loadRoiCounts();
|
||||
loadCameraStatus();
|
||||
} catch {
|
||||
message.error('加载摄像头列表失败');
|
||||
} finally {
|
||||
@@ -133,6 +138,24 @@ function loadRoiCounts() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadCameraStatus() {
|
||||
for (const cam of cameraList.value) {
|
||||
const cameraCode = cam.cameraCode;
|
||||
if (!cameraCode) continue;
|
||||
wvpRequestClient
|
||||
.get('/aiot/device/roi/snap/image', {
|
||||
params: { cameraCode },
|
||||
responseType: 'blob',
|
||||
})
|
||||
.then(() => {
|
||||
cameraStatus.value = { ...cameraStatus.value, [cameraCode]: true };
|
||||
})
|
||||
.catch(() => {
|
||||
cameraStatus.value = { ...cameraStatus.value, [cameraCode]: false };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMediaServers() {
|
||||
try {
|
||||
const data = await getMediaServerList();
|
||||
@@ -252,7 +275,19 @@ async function handleSave() {
|
||||
await saveCamera({ ...editForm });
|
||||
message.success(editForm.id ? '编辑成功' : '添加成功');
|
||||
editModalOpen.value = false;
|
||||
loadData();
|
||||
// 新增摄像头成功后自动触发首次截图预热(非阻塞)
|
||||
if (!editForm.id) {
|
||||
loadData().then(() => {
|
||||
const saved = cameraList.value.find(
|
||||
(c) => c.app === editForm.app && c.stream === editForm.stream,
|
||||
);
|
||||
if (saved?.cameraCode) {
|
||||
getSnapUrl(saved.cameraCode, true).catch(() => {});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
loadData();
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '保存失败');
|
||||
} finally {
|
||||
@@ -382,7 +417,19 @@ onMounted(() => {
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'roiCount'">
|
||||
<template v-if="column.key === 'status'">
|
||||
<Badge
|
||||
:status="
|
||||
cameraStatus[record.cameraCode] === null ||
|
||||
cameraStatus[record.cameraCode] === undefined
|
||||
? 'default'
|
||||
: cameraStatus[record.cameraCode]
|
||||
? 'success'
|
||||
: 'error'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'roiCount'">
|
||||
<Badge
|
||||
:count="roiCounts[record.cameraCode] ?? 0"
|
||||
:number-style="{ backgroundColor: '#1677ff' }"
|
||||
|
||||
Reference in New Issue
Block a user