功能:摄像头页面增加区域选择器

1. 编辑弹窗新增「所属区域」下拉框,支持搜索,数据来自 IoT 平台
2. 表格新增「区域」列显示已绑定的区域名称
3. API 新增 getAreaList 函数查询 vsp-service 区域列表接口
4. Vite 代理新增 /api/area → vsp-service:8000
5. Camera 类型新增 areaId 字段
This commit is contained in:
2026-03-23 16:49:27 +08:00
parent d3eb97eb8b
commit 84ec762d09
3 changed files with 70 additions and 1 deletions

View File

@@ -45,6 +45,7 @@ export namespace AiotDeviceApi {
mediaServerId?: string;
streamKey?: string;
createTime?: string;
areaId?: number; // 所属区域ID
}
/** ROI 区域 */
@@ -299,3 +300,15 @@ export async function getAlertImageUrl(imagePath: string): Promise<string> {
`&access-token=${encodeURIComponent(token)}`
);
}
// ==================== 区域列表 ====================
/** 获取区域列表(从 IoT 平台代理查询) */
export async function getAreaList(): Promise<
{ id: number; areaName: string; parentId?: number }[]
> {
const resp = await fetch(`${apiURL}/api/area/list`);
const json = await resp.json();
if (json.code === 0) return json.data || [];
return [];
}

View File

@@ -30,6 +30,7 @@ import {
import {
deleteCamera,
getAreaList,
getCameraList,
getMediaServerList,
getRoiByCameraId,
@@ -59,6 +60,7 @@ const columns = [
{ title: '摄像头名称', dataIndex: 'cameraName', width: 150 },
{ title: '拉流地址', dataIndex: 'srcUrl', ellipsis: true },
{ title: '边缘设备', dataIndex: 'edgeDeviceId', width: 100 },
{ title: '区域', key: 'areaName', width: 100 },
{ title: '状态', key: 'status', width: 60, align: 'center' as const },
{ title: 'ROI', key: 'roiCount', width: 80, align: 'center' as const },
{ title: '操作', key: 'actions', width: 240, fixed: 'right' as const },
@@ -70,6 +72,7 @@ const editModalOpen = ref(false);
const editModalTitle = ref('添加摄像头');
const saving = ref(false);
const mediaServerOptions = ref<{ label: string; value: string }[]>([]);
const areaOptions = ref<{ label: string; value: number }[]>([]);
const editForm = reactive<Partial<AiotDeviceApi.Camera>>({
id: undefined,
type: 'default',
@@ -86,6 +89,7 @@ const editForm = reactive<Partial<AiotDeviceApi.Camera>>({
enableDisableNoneReader: true,
relatesMediaServerId: '',
ffmpegCmdKey: '',
areaId: undefined as number | undefined,
});
// 从已有摄像头中提取应用名选项
@@ -108,6 +112,15 @@ const appOptions = computed(() => {
.sort((a, b) => b.count - a.count);
});
// 区域名称映射area_id → area_name
const areaNameMap = computed(() => {
const map: Record<number, string> = {};
areaOptions.value.forEach((a) => {
map[a.value] = a.label;
});
return map;
});
// ==================== 数据加载 ====================
async function loadData() {
@@ -173,6 +186,18 @@ async function loadMediaServers() {
}
}
async function loadAreaOptions() {
try {
const list = await getAreaList();
areaOptions.value = list.map((a: any) => ({
label: a.areaName || a.name || `区域${a.id}`,
value: a.id,
}));
} catch {
areaOptions.value = [];
}
}
// ==================== 新增 / 编辑 ====================
function resetForm() {
@@ -192,6 +217,7 @@ function resetForm() {
enableDisableNoneReader: true,
relatesMediaServerId: '',
ffmpegCmdKey: '',
areaId: undefined,
});
}
@@ -234,6 +260,7 @@ function handleAdd() {
editModalTitle.value = '添加摄像头';
editModalOpen.value = true;
loadMediaServers();
loadAreaOptions();
// 自动填充流ID
autoFillStreamId();
}
@@ -256,10 +283,12 @@ function handleEdit(row: AiotDeviceApi.Camera) {
enableDisableNoneReader: row.enableDisableNoneReader ?? true,
relatesMediaServerId: row.relatesMediaServerId || '',
ffmpegCmdKey: row.ffmpegCmdKey || '',
areaId: row.areaId || undefined,
});
editModalTitle.value = '编辑摄像头';
editModalOpen.value = true;
loadMediaServers();
loadAreaOptions();
}
async function handleSave() {
@@ -368,6 +397,7 @@ watch(
onMounted(() => {
loadData();
loadAreaOptions();
});
</script>
@@ -421,7 +451,13 @@ onMounted(() => {
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<template v-if="column.key === 'areaName'">
<Tag v-if="record.areaId && areaNameMap[record.areaId]" color="blue">
{{ areaNameMap[record.areaId] }}
</Tag>
<span v-else style="color: #999">未绑定</span>
</template>
<template v-else-if="column.key === 'status'">
<Badge
:status="
cameraStatus[record.cameraCode] === null ||
@@ -501,6 +537,21 @@ onMounted(() => {
placeholder="选择绑定的边缘设备"
/>
</Form.Item>
<Form.Item label="所属区域">
<Select
v-model:value="editForm.areaId"
:options="areaOptions"
placeholder="选择所属区域(用于工单派发)"
allow-clear
show-search
:filter-option="
(input: string, option: any) =>
(option?.label ?? '')
.toLowerCase()
.includes(input.toLowerCase())
"
/>
</Form.Item>
<Form.Item label="拉流地址" required>
<Input
v-model:value="editForm.srcUrl"

View File

@@ -33,6 +33,11 @@ export default defineConfig(async () => {
changeOrigin: true,
target: 'http://127.0.0.1:8000',
},
// 区域列表 API -> 告警服务 :8000
'/api/area': {
changeOrigin: true,
target: 'http://127.0.0.1:8000',
},
// aiot/device/* -> WVP :18080按子路径分别 rewrite
// 注意:更具体的路径必须写在通配路径前面