refactor(aiot-device): 规范整理摄像头管理和 ROI 配置页面代码

- 摄像头管理:代码按功能分区(列表状态/编辑弹窗/数据加载/增删改/拉流控制/配置导出)
- ROI 配置:代码按功能分区(摄像头选择/截图/数据加载/绘制/选择/编辑/删除/推送)
- getSnapUrl 适配 async 调用,截图 URL 携带 access-token 认证参数
- 统一中文注释风格

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 10:25:19 +08:00
parent eb11e7ed1f
commit f78eaa2ae1
2 changed files with 144 additions and 92 deletions

View File

@@ -1,4 +1,10 @@
<script setup lang="ts">
/**
* 摄像头管理页面
*
* 功能:摄像头列表展示、搜索筛选、新增/编辑/删除、拉流控制、ROI 配置跳转、配置导出
* 后端WVP 视频平台 StreamProxy API
*/
import type { AiotDeviceApi } from '#/api/aiot/device';
import { onMounted, reactive, ref } from 'vue';
@@ -35,6 +41,8 @@ defineOptions({ name: 'AiotDeviceCamera' });
const router = useRouter();
// ==================== 列表状态 ====================
const loading = ref(false);
const cameraList = ref<AiotDeviceApi.Camera[]>([]);
const roiCounts = ref<Record<string, number>>({});
@@ -44,11 +52,21 @@ const total = ref(0);
const searchQuery = ref('');
const searchPulling = ref<string | undefined>(undefined);
// 编辑弹窗
const columns = [
{ title: '应用名', dataIndex: 'app', width: 100 },
{ title: '流ID', dataIndex: 'stream', width: 120 },
{ title: '拉流地址', dataIndex: 'srcUrl', ellipsis: true },
{ title: '状态', key: 'pulling', width: 100 },
{ title: 'ROI', key: 'roiCount', width: 80, align: 'center' as const },
{ title: '操作', key: 'actions', width: 340, fixed: 'right' as const },
];
// ==================== 编辑弹窗状态 ====================
const editModalOpen = ref(false);
const editModalTitle = ref('添加摄像头');
const saving = ref(false);
const mediaServerOptions = ref<{ value: string; label: string }[]>([]);
const mediaServerOptions = ref<{ label: string; value: string }[]>([]);
const editForm = reactive<Partial<AiotDeviceApi.Camera>>({
id: undefined,
type: 'default',
@@ -65,14 +83,7 @@ const editForm = reactive<Partial<AiotDeviceApi.Camera>>({
ffmpegCmdKey: '',
});
const columns = [
{ title: '应用名', dataIndex: 'app', width: 100 },
{ title: '流ID', dataIndex: 'stream', width: 120 },
{ title: '拉流地址', dataIndex: 'srcUrl', ellipsis: true },
{ title: '状态', key: 'pulling', width: 100 },
{ title: 'ROI数量', key: 'roiCount', width: 100 },
{ title: '操作', key: 'actions', width: 340, fixed: 'right' as const },
];
// ==================== 数据加载 ====================
async function loadData() {
loading.value = true;
@@ -90,14 +101,14 @@ async function loadData() {
total.value = res.total || 0;
loadRoiCounts();
} catch {
message.error('加载失败');
message.error('加载摄像头列表失败');
} finally {
loading.value = false;
}
}
function loadRoiCounts() {
cameraList.value.forEach((cam) => {
for (const cam of cameraList.value) {
const cameraId = `${cam.app}/${cam.stream}`;
getRoiByCameraId(cameraId)
.then((data: any) => {
@@ -105,7 +116,7 @@ function loadRoiCounts() {
roiCounts.value = { ...roiCounts.value, [cameraId]: items.length };
})
.catch(() => {});
});
}
}
async function loadMediaServers() {
@@ -124,20 +135,24 @@ async function loadMediaServers() {
}
}
// ==================== 新增 / 编辑 ====================
function resetForm() {
editForm.id = undefined;
editForm.type = 'default';
editForm.app = 'live';
editForm.stream = '';
editForm.srcUrl = '';
editForm.timeout = 15;
editForm.rtspType = '0';
editForm.enable = true;
editForm.enableAudio = false;
editForm.enableMp4 = false;
editForm.enableDisableNoneReader = true;
editForm.relatesMediaServerId = '';
editForm.ffmpegCmdKey = '';
Object.assign(editForm, {
id: undefined,
type: 'default',
app: 'live',
stream: '',
srcUrl: '',
timeout: 15,
rtspType: '0',
enable: true,
enableAudio: false,
enableMp4: false,
enableDisableNoneReader: true,
relatesMediaServerId: '',
ffmpegCmdKey: '',
});
}
function handleAdd() {
@@ -148,19 +163,21 @@ function handleAdd() {
}
function handleEdit(row: AiotDeviceApi.Camera) {
editForm.id = row.id;
editForm.type = row.type || 'default';
editForm.app = row.app || '';
editForm.stream = row.stream || '';
editForm.srcUrl = row.srcUrl || '';
editForm.timeout = row.timeout ?? 15;
editForm.rtspType = row.rtspType || '0';
editForm.enable = row.enable ?? true;
editForm.enableAudio = row.enableAudio ?? false;
editForm.enableMp4 = row.enableMp4 ?? false;
editForm.enableDisableNoneReader = row.enableDisableNoneReader ?? true;
editForm.relatesMediaServerId = row.relatesMediaServerId || '';
editForm.ffmpegCmdKey = row.ffmpegCmdKey || '';
Object.assign(editForm, {
id: row.id,
type: row.type || 'default',
app: row.app || '',
stream: row.stream || '',
srcUrl: row.srcUrl || '',
timeout: row.timeout ?? 15,
rtspType: row.rtspType || '0',
enable: row.enable ?? true,
enableAudio: row.enableAudio ?? false,
enableMp4: row.enableMp4 ?? false,
enableDisableNoneReader: row.enableDisableNoneReader ?? true,
relatesMediaServerId: row.relatesMediaServerId || '',
ffmpegCmdKey: row.ffmpegCmdKey || '',
});
editModalTitle.value = '编辑摄像头';
editModalOpen.value = true;
loadMediaServers();
@@ -192,6 +209,8 @@ async function handleSave() {
}
}
// ==================== 删除 ====================
function handleDelete(row: AiotDeviceApi.Camera) {
Modal.confirm({
title: '删除确认',
@@ -209,6 +228,25 @@ function handleDelete(row: AiotDeviceApi.Camera) {
});
}
// ==================== 拉流控制 ====================
async function toggleStream(row: AiotDeviceApi.Camera) {
try {
if (row.pulling) {
await stopCamera(row.id!);
message.success('已停止拉流');
} else {
await startCamera(row.id!);
message.success('开始拉流');
}
loadData();
} catch {
message.error('操作失败');
}
}
// ==================== ROI 配置跳转 ====================
function handleRoiConfig(row: AiotDeviceApi.Camera) {
router.push({
path: '/aiot/device/roi',
@@ -221,20 +259,7 @@ function handleRoiConfig(row: AiotDeviceApi.Camera) {
});
}
async function toggleStream(row: AiotDeviceApi.Camera) {
try {
if (row.pulling) {
await stopCamera(row.id!);
message.success('已停止');
} else {
await startCamera(row.id!);
message.success('开始拉流');
}
loadData();
} catch {
message.error('操作失败');
}
}
// ==================== 配置导出 ====================
async function handleExport(row: AiotDeviceApi.Camera) {
const cameraId = `${row.app}/${row.stream}`;
@@ -253,12 +278,16 @@ async function handleExport(row: AiotDeviceApi.Camera) {
}
}
// ==================== 分页 ====================
function handlePageChange(p: number, size: number) {
page.value = p;
pageSize.value = size;
loadData();
}
// ==================== 初始化 ====================
onMounted(() => {
loadData();
});
@@ -351,11 +380,7 @@ onMounted(() => {
{{ record.pulling ? '停止' : '拉流' }}
</Button>
<Button size="small" @click="handleExport(record)">导出</Button>
<Button
size="small"
danger
@click="handleDelete(record)"
>
<Button size="small" danger @click="handleDelete(record)">
删除
</Button>
</Space>
@@ -364,7 +389,7 @@ onMounted(() => {
</Table>
</div>
<!-- 添加/编辑弹窗 -->
<!-- 新增/编辑弹窗 -->
<Modal
v-model:open="editModalOpen"
:title="editModalTitle"