feat(aiot-device): 重写摄像头管理页面,迁移 WVP 功能
从 WVP 的 cameraConfig/index.vue 迁移至 Vue3 + Ant Design: - 搜索过滤:支持名称/地址搜索和拉流状态筛选 - 摄像头列表:显示应用名、流ID、拉流地址、状态 - ROI 数量徽标:异步加载每个摄像头的 ROI 数量 - 操作按钮:ROI 配置(跳转)、拉流/停止、导出配置 - 配置导出:JSON 文件下载 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,110 +1,226 @@
|
||||
<script setup lang="ts">
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { AiotDeviceApi } from '#/api/aiot/device';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Input,
|
||||
message,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getChannelPage } from '#/api/aiot/device';
|
||||
import {
|
||||
exportConfig,
|
||||
getCameraList,
|
||||
getRoiByCameraId,
|
||||
startCamera,
|
||||
stopCamera,
|
||||
} from '#/api/aiot/device';
|
||||
|
||||
defineOptions({ name: 'AiotDeviceCamera' });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
/** 配置 ROI */
|
||||
function handleConfigRoi(row: AiotDeviceApi.Channel) {
|
||||
router.push({
|
||||
path: '/aiot/device/roi',
|
||||
query: { channelId: row.channelId },
|
||||
const loading = ref(false);
|
||||
const cameraList = ref<AiotDeviceApi.Camera[]>([]);
|
||||
const roiCounts = ref<Record<string, number>>({});
|
||||
const page = ref(1);
|
||||
const pageSize = ref(15);
|
||||
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: 100 },
|
||||
{ title: '操作', key: 'actions', width: 260, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getCameraList({
|
||||
page: page.value,
|
||||
count: pageSize.value,
|
||||
query: searchQuery.value || undefined,
|
||||
pulling:
|
||||
searchPulling.value === undefined
|
||||
? undefined
|
||||
: searchPulling.value === 'true',
|
||||
});
|
||||
cameraList.value = res.list || [];
|
||||
total.value = res.total || 0;
|
||||
loadRoiCounts();
|
||||
} catch {
|
||||
message.error('加载失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadRoiCounts() {
|
||||
cameraList.value.forEach((cam) => {
|
||||
const cameraId = `${cam.app}/${cam.stream}`;
|
||||
getRoiByCameraId(cameraId)
|
||||
.then((data: any) => {
|
||||
const items = Array.isArray(data) ? data : [];
|
||||
roiCounts.value = { ...roiCounts.value, [cameraId]: items.length };
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{
|
||||
field: 'channelId',
|
||||
title: '通道编号',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '通道名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'manufacturer',
|
||||
title: '厂商',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 80,
|
||||
slots: { default: 'status' },
|
||||
},
|
||||
{
|
||||
field: 'ptztypeText',
|
||||
title: 'PTZ 类型',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getChannelPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
function handleRoiConfig(row: AiotDeviceApi.Camera) {
|
||||
router.push({
|
||||
path: '/aiot/device/roi',
|
||||
query: {
|
||||
cameraId: `${row.app}/${row.stream}`,
|
||||
app: row.app,
|
||||
stream: row.stream,
|
||||
srcUrl: row.srcUrl,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'channelId',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiotDeviceApi.Channel>,
|
||||
});
|
||||
}
|
||||
|
||||
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}`;
|
||||
try {
|
||||
const data = await exportConfig(cameraId);
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `config_${row.app}_${row.stream}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
message.error('导出失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(p: number, size: number) {
|
||||
page.value = p;
|
||||
pageSize.value = size;
|
||||
loadData();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="摄像头管理">
|
||||
<!-- 状态列 -->
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.status === 'ON' ? 'success' : 'default'">
|
||||
{{ row.status === 'ON' ? '在线' : '离线' }}
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '配置ROI',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
onClick: handleConfigRoi.bind(null, row),
|
||||
},
|
||||
]"
|
||||
<div style="padding: 16px">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 8px;
|
||||
"
|
||||
>
|
||||
<Input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索摄像头名称/地址"
|
||||
style="width: 250px"
|
||||
allow-clear
|
||||
@press-enter="loadData"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
<Select
|
||||
v-model:value="searchPulling"
|
||||
placeholder="拉流状态"
|
||||
style="width: 130px"
|
||||
allow-clear
|
||||
:options="[
|
||||
{ value: 'true', label: '拉流中' },
|
||||
{ value: 'false', label: '未拉流' },
|
||||
]"
|
||||
@change="loadData"
|
||||
/>
|
||||
<Button type="primary" @click="loadData">查询</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="cameraList"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['15', '25', '50'],
|
||||
showTotal: (t: number) => `共 ${t} 条`,
|
||||
onChange: handlePageChange,
|
||||
}"
|
||||
:scroll="{ x: 900 }"
|
||||
row-key="id"
|
||||
bordered
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'pulling'">
|
||||
<Tag :color="record.pulling ? 'success' : 'default'">
|
||||
{{ record.pulling ? '拉流中' : '未拉流' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'roiCount'">
|
||||
<Badge
|
||||
:count="roiCounts[`${record.app}/${record.stream}`] ?? 0"
|
||||
:number-style="{ backgroundColor: '#1677ff' }"
|
||||
show-zero
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleRoiConfig(record)"
|
||||
>
|
||||
ROI配置
|
||||
</Button>
|
||||
<Button
|
||||
:type="record.pulling ? 'default' : 'primary'"
|
||||
:danger="record.pulling"
|
||||
size="small"
|
||||
@click="toggleStream(record)"
|
||||
>
|
||||
{{ record.pulling ? '停止' : '拉流' }}
|
||||
</Button>
|
||||
<Button size="small" @click="handleExport(record)">导出</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user