You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1181 lines
39 KiB
Vue

3 weeks ago
<template>
<div class="container">
<div class="sidebar">
<!-- 左侧树型列表-->
<el-card class="box-card" shadow="never">
<template #header>
<div class="card-header">
<span>资源列表</span>
<el-button link type="primary" @click="fetchData"></el-button>
</div>
</template>
<el-input v-model="filterText" placeholder="输入关键字过滤" prefix-icon="Search" style="margin-bottom: 15px" />
<el-tree
ref="treeRef"
:data="treeData"
:props="defaultProps"
:filter-node-method="filterNode"
node-key="id"
default-expand-all
highlight-current
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<el-icon v-if="data.type === 0"><OfficeBuilding /></el-icon>
<el-icon v-else-if="data.type === 1"><School /></el-icon>
<el-icon v-else-if="data.type === 2"><VideoCamera /></el-icon>
<span class="node-label" :class="{ 'ehv-text': data.type === 'STATION_EHV' }">
{{ node.label }}
</span>
</span>
</template>
</el-tree>
</el-card>
</div>
<div class="main-content">
<div v-if="viewMode === 'list'" class="box-card">
<!-- NVR查询表单-->
<el-form
:inline="true"
:model="NvrFormData"
@submit.prevent
class="QueryForm">
<el-form-item label="设备名称:">
<el-input v-model="NvrFormData.name" placeholder="请输入名称" clearable/>
</el-form-item>
<el-form-item label="状态" >
<el-select v-model="NvrFormData.status" placeholder="请选择" style="width: 210px;" clearable>
<el-option
v-for="item in optionsStore.getDictOptions('status').map(it=>{
return {
... it,
id: Number(it.id.substring(6))
}
})"
:key="item.id"
:label="item.codeName"
:value="Number(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item >
<el-button type="primary" class="queryBtn" @click="queryNvr"></el-button>
</el-form-item>
<el-form-item >
<el-button type="primary" :icon="Plus" style="width: 100px;" @click="openAddNvrDialog"></el-button>
<el-button type="danger" :icon="Delete" style="width: 100px;" @click="deleteNvrsBatch"></el-button>
</el-form-item>
</el-form>
<!-- NVR表格-->
<el-table :data="tableData"
style="width: 100%;padding: 10px"
stripe height="calc(100vh - 200px)"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"/>
<el-table-column prop="id" label="NVR ID" width="120" />
<el-table-column prop="name" label="设备名称" min-width="180" />
<el-table-column prop="ip" label="IP地址" width="140"/>
<el-table-column prop="port" label="端口号" width="140"/>
<el-table-column prop="account" label="账号" width="140"/>
<el-table-column prop="password" label="密码" width="140"/>
<el-table-column prop="driver" label="类型" width="100" align="center">
<template #default="scope">
<el-tag v-if="scope.row.driver === 0" type="success" effect="dark"></el-tag>
<el-tag v-if="scope.row.driver === 1" type="success"></el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status === 1" type="success" effect="dark">线</el-tag>
<el-tag v-if="scope.row.status === 0" type="danger">线</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="140"/>
<el-table-column label="操作" width="320" fixed="right" align="center">
<template #default="scope">
<el-button size="small" icon="Refresh" type="primary" @click="handleRefresh(scope.row)" >
同步 </el-button>
<!-- <el-button size="small" icon="Refresh" type="primary" @click="refreshChannel(scope.row)" :loading="scope.row.isLoading">-->
<!-- 刷新 </el-button>-->
<el-button size="small" icon="VideoCamera" type="primary" @click="showChannelList(scope.row)"> </el-button>
<el-button size="small" icon="EditPen" type="primary" @click="openNvrChangeDialog(scope.row)"> </el-button>
<el-button size="small" type="danger" icon="Delete" @click="deleteByIdNvr(scope.row.id)"> </el-button>
</template>
</el-table-column>
</el-table>
<DataSync
:percentage="refreshObj.percentage"
:is-syncing="refreshObj.isSyncing"
:is-success="refreshObj.isSuccess"
:status="refreshObj.status"
/>
<!-- 分页组件 -->
<div class="pagination-container" >
<el-pagination
v-model:current-page="PageParams.pageNum"
v-model:page-size="PageParams.pageSize"
:page-sizes="[10, 20, 30, 40]"
:total="PageParams.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- NVR新增或者编辑弹框-->
<el-dialog v-model="deviceDialogObj.deviceVisible"
@close="handleCancel"
:title="deviceDialogObj.title"
draggable
:close-on-click-modal="false"
width="500">
<el-form :model="deviceDialogObj.deviceForm"
ref="addOrUpdateFormRef"
:rules="addOrUpdateRules">
<el-form-item label="设备名称" prop="name">
<el-input v-model="deviceDialogObj.deviceForm.name" clearable />
</el-form-item>
<el-form-item label="设备账号" prop="account">
<el-input v-model="deviceDialogObj.deviceForm.account" clearable/>
</el-form-item>
<el-form-item label="设备密码" prop="password">
<el-input type="password" show-password v-model="deviceDialogObj.deviceForm.password" clearable/>
</el-form-item>
<el-form-item label="设备IP" prop="ip">
<el-input v-model="deviceDialogObj.deviceForm.ip" clearable/>
</el-form-item>
<el-form-item label="设备端口" prop="port">
<el-input v-model="deviceDialogObj.deviceForm.port" clearable/>
</el-form-item>
<el-form-item label="设备类型" prop="driver">
<el-select v-model="deviceDialogObj.deviceForm.driver" clearable>
<el-option v-for="item in optionsStore.getDictOptions('device_category').map(it=>{
return {
... it,
id: Number(it.id.substring(6))
}
})"
:key="item.id"
:label="item.codeName"
:value="item.id"/>
</el-select>
</el-form-item>
<el-form-item label="设备所在变电站" prop="stationId">
<el-select placeholder="请选择变电站" style="width: 240px"
v-model="deviceDialogObj.deviceForm.stationId"
:options="subStationList"
:props="StationProps"
>
</el-select>
</el-form-item>
<el-form-item>
<div class="form-actions">
<el-button @click="handleCancel"></el-button>
<el-button type="primary" @click="handleSave"></el-button>
</div>
</el-form-item>
</el-form>
</el-dialog>
</div>
<el-card v-else-if="viewMode === 'detail'" class="box-card">
<template #header>
<div class="card-header">
<div style="display: flex; align-items: center">
<el-button link icon="ArrowLeft" @click="backToList" style="margin-right: 10px; font-size: 16px">
返回列表
</el-button>
<el-divider direction="vertical" />
<el-tag size="small" style="margin-left: 10px" :type="currentNvrDetail?.status === 1 ? 'success' : 'info'">
{{ currentNvrDetail?.status=== 1 ? '设备在线' : '设备离线' }}
</el-tag>
</div>
</div>
</template>
<div class="detail-container" >
<!-- 通道查询表单 -->
<el-form :model="channelQuery"
:inline="true"
ref="channelQueryRef"
@submit.prevent>
<el-form-item label="名称" prop="name" placeholder="请选择">
<el-input v-model="channelQuery.name" placeholder="请输入名称" clearable />
</el-form-item>
<el-form-item label="云台类型" prop="type">
<el-select v-model="channelQuery.type" placeholder="请选择" style="width: 210px;" clearable>
<el-option
v-for="item in optionsStore.getDictOptions('camera_type').map(it=>{
return {
... it,
id: Number(it.id.substring(6))
}
})"
:key="item.id"
:label="item.codeName"
:value="Number(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item >
<el-button type="primary" class="queryBtn" @click="queryCamera"></el-button>
<el-button type="warning" icon="Refresh" @click="refreshCamera(currentNvrDetail)" plain :loading="refreshLoading">同步通道</el-button>
</el-form-item>
</el-form>
<!-- 通道列表 -->
<el-table :data="channelData"
height="350"
style="width: 100%"
:row-key="(row: any)=>row.id"
size="small" >
<el-table-column prop="name" label="名称" width="280" align="center"/>
<el-table-column prop="cameraNo" label="摄像机编号" width="100" align="center"/>
<el-table-column prop="channelId" label="通道号" width="180" align="center" />
<el-table-column prop="ipAddress" label="IP地址" width="180" align="center" />
<el-table-column prop="algInfo" label="算法配置" width="180" align="center" >
<template #default="{ row }">
<div class="alg-info-container">
<div v-if="row.algInfo && row.algInfo.length > 0">
<div v-for="(item,index) in row.algInfo" :key="index" class="alg-tag-item">
<el-tag>{{ALGORITHM_MAP[item]}}</el-tag>
</div>
</div>
<div v-else class="no-alg-info">
<span></span>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="enableInspection" label="抓图对比" width="180" align="center" >
<template #default="{ row }">
<el-tag type="primary" v-if="row.enableInspection==1"></el-tag>
<el-tag type="danger" v-else-if="row.enableInspection==0">关闭</el-tag>
<span v-else></span>
</template>
</el-table-column>
<el-table-column prop="installLocation" label="位置信息" width="180" align="center" >
<template #default="{ row }">
<span v-if="row.installLocation==''"></span>
</template>
</el-table-column>
<el-table-column prop="videoType" label="视频类型" width="180" align="center" >
<template #default="{ row }">
<span v-if="row.videoType==1"></span>
<span v-else-if="row.videoType==2">热成像</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column prop="type" label="云台类型" width="180" align="center" >
<template #default="{ row }">
<span v-if="row.type==0"></span>
<span v-else-if="row.type==1">球体</span>
<span v-else-if="row.type==2">云台</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column prop="channelDriver" label="通道类型" width="180" align="center" >
<template #default="{ row }">
<span v-if="row.channelDriver==0"></span>
<span v-else-if="row.channelDriver==1">大华</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag type="success" v-if="row.status==1">线</el-tag>
<el-tag type="danger" v-else-if="row.status==0">离线</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="250">
<template #default="{row}">
<el-button size="small" type="primary" link @click="openPlayDialog(row)" :loading="playLoading">预览</el-button>
<el-button size="small" type="primary" link @click="openControlDialog(row)"></el-button>
<el-button size="small" v-if="row.boxId" type="primary" link @click="openAlgDialog(row)"></el-button>
<el-button size="small" type="primary" link @click="openCameraEditDialog(row)"> </el-button>
<el-button size="small" type="warning" link @click="handleEditDel(row)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 通道编辑弹框-->
<el-dialog v-model="editCameraDialog.editVisible" title="编辑" :destroy-on-close="true" draggable>
<el-form
ref="ruleFormRef"
label-width="140px"
label-suffix=" :"
:rules="rules"
:model="editCameraDialog.cameraForm"
@submit.enter.prevent="handleSubmit"
>
<el-form-item label="摄像机名称" prop="name">
<el-input v-model="editCameraDialog.cameraForm.name" placeholder="请填写名称" clearable />
</el-form-item>
<el-form-item label="摄像机编号" prop="cameraNo">
<el-input v-model="editCameraDialog.cameraForm.cameraNo" placeholder="请填写编号" clearable />
</el-form-item>
<el-form-item label="安装位置" prop="installLocation">
<el-input v-model="editCameraDialog.cameraForm.installLocation" placeholder="请填写安装位置" clearable />
</el-form-item>
<el-form-item label="摄像机类型" prop="type">
<el-select v-model="editCameraDialog.cameraForm.type" clearable placeholder="请选择摄像机区分类型">
<el-option v-for="item in cameraType" :key="item.id" :label="item.codeName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="摄像机品牌" prop="channelDriver">
<el-select v-model="editCameraDialog.cameraForm.channelDriver" clearable placeholder="请选择摄像机品牌">
<el-option v-for="item in cameraProduct" :key="item.id" :label="item.codeName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="抓图对比功能" prop="enableInspection">
<el-select v-model="editCameraDialog.cameraForm.enableInspection" clearable placeholder="请选择">
<el-option v-for="item in inspectionType" :key="item.id" :label="item.codeName" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleEditClose"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</template>
</el-dialog>
<!-- 算法编辑的组件-->
<AlgConfigForm :camera-row="algDialogObj.cameraRow"
:title="algDialogObj.title"
@handleVisibleUpdate="handleVisibleUpdate"
@handleSubmit="handleAlgSubmit"
:alg-visible="algDialogObj.algVisible"
:alg-form="algDialogObj.algForm"/>
<!-- 分页组件-->
<div class="pagination-container" style="margin-top: 10px;">
<el-pagination
v-model:current-page="channelPage.pageNum"
v-model:page-size="channelPage.pageSize"
:page-sizes="[10, 20, 30, 40]"
:total="channelPage.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</el-card>
</div>
<!-- 视频预览的弹框-->
<el-dialog v-model="playDialogObj.playVisible"
:title="playDialogObj.title"
:before-close="handleClosePlay"
:destroy-on-close="true"
:close-on-click-modal="false"
>
<div
class="video-wrapper"
>
<VideoPlayer :camera-id="playDialogObj.id"
/>
</div>
</el-dialog>
<!-- 控制摄像头的弹框 -->
<el-dialog v-model="controlDialogObj.controlVisible"
:title="controlDialogObj.title"
:before-close="handleClose"
align-center
:destroy-on-close="true"
:close-on-click-modal="false"
>
<CameraControl
:camera-id="controlDialogObj.id"
:curCamera="controlDialogObj.curCamera"
/>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {onBeforeUnmount, onMounted, reactive, ref, watch} from 'vue';
// 引入需要的图标,增加了 ArrowLeft, Refresh, Document
import {Delete, OfficeBuilding, Plus, School, VideoCamera} from '@element-plus/icons-vue';
import {getTreeData} from "@/api/modules/monitor/Tree.js";
import type {TreeNode} from "@/api/types/monitor/Tree";
import {createNvrApi, getNvrListApi, listByNode, removeNvrApi, updateNvrApi} from "@/api/modules/monitor/Nvr";
import type {NvrForm, NvrQuery, NvrRow} from "@/api/types/monitor/nvr";
import {type ElForm, ElMessage, ElMessageBox, type TreeInstance} from "element-plus";
import {getCameraListApi, removeCameraApi, updateCameraApi} from "@/api/modules/monitor/Camera";
import type {CameraForm, CameraQuery, CameraRow} from "@/api/types/monitor/camera";
import {deviceLogin, refresh} from "@/api/modules/monitor/device";
import CameraControl from "@/views/sysmonitortree/sysMonitorTree/components/CameraControl.vue";
import VideoPlayer from "@/views/sysmonitortree/sysMonitorTree/components/VideoPlayer.vue";
import {useDictOptions} from "@/hooks/useDictOptions";
import {deepClone} from "@/utils/clone";
import {useOptionsStore} from "@/stores/modules/options";
import {getAllSubstationVO} from "@/api/modules/monitor/substation";
import AlgConfigForm from "@/views/sysmonitortree/sysMonitorTree/components/AlgConfigForm.vue";
import {handleAlgTask} from "@/api/modules/monitor/channel";
import {getAlgorithmTaskByCameraId} from "@/api/modules/monitor/algorithmTask";
import type {AlgorithmTaskVO, AlgTaskConfigDto} from "@/api/types/edgebox/EdgeBox";
import DataSync from "@/views/sysmonitortree/sysMonitorTree/components/DataSync.vue";
const refreshLoading=ref(false);
const playLoading = ref(false);
const filterText = ref('');
const treeRef = ref<TreeInstance>();
const treeData = ref<TreeNode[]>([]);
const tableData = ref<NvrRow[]>([]);
const currentSelectNode = ref<TreeNode>();
const loading = ref(false);
const channelData=ref<CameraRow[]>([]);
const ruleFormRef=ref();
const optionsStore = useOptionsStore();
const addOrUpdateFormRef=ref();
const editCameraDialog=reactive({
editVisible: false,
cameraForm: {} as CameraForm
})
const addOrUpdateRules = reactive({
name: [
{ required: true, message: '请输入设备名称', trigger: 'blur' },
],
account: [
{ required: true, message: '请输入账号', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
],
ip: [
{ required: true, message: '请输入IP地址', trigger: 'blur' },
],
port: [
{ required: true, message: '请输入端口号', trigger: 'blur' },
],
driver: [
{ required: true, message: '请输入NVR的类型', trigger: 'blur' },
],
manufacturer: [
{ required: true, message: '请输入设备厂商', trigger: 'blur' },
],
stationId: [
{ required: true, message: '请输入区域', trigger: 'blur' },
],
});
const NvrFormData=reactive<NvrQuery>({});
const channelQuery=reactive<CameraQuery>({});
const rules = reactive({
name: [{ required: true, message: '请填写名称' }],
cameraNo: [{ required: true, message: '请填写摄像头编号' }],
type: [{ required: true, message: '请填写类型' }],
channelDriver: [{ required: true, message: '请填写品牌' }],
});
// 算法的映射
const ALGORITHM_MAP: Record<number, string> = {
8: "明烟明火检测",
52: "人脸识别",
58: "脸部抓拍",
1:"安全帽检测"
};
// 处理算法提交
const handleAlgSubmit=async (algForm:AlgorithmTaskVO)=>{
try {
if (algForm.algInfo.length>3) {
ElMessage.warning("最多只能同时配置三个算法");
return;
}
const res=await handleAlgTask({
id: algForm.id,
url: algForm.url,
AlgInfo: algForm.algInfo,
TaskDesc: algForm.taskDesc,
UserData: algForm.userData,
cameraId: algDialogObj.cameraRow.id,
});
if(res.code==="0000")
{
handleVisibleUpdate();
await getChannelPage();
ElMessage.success('配置成功');
} else {
ElMessage.error('配置失败: ' + (res.message || '未知错误'));
}
} catch (error) {
console.error('配置算法失败:', error);
ElMessage.error('配置失败,请重试');
}
}
// 关闭配置算法的弹框
const handleVisibleUpdate=()=>{
algDialogObj.algVisible=false;
algDialogObj.algForm={};
}
// 配置算法的弹框对象
const algDialogObj=reactive({
algVisible: false,
title: '配置算法',
algForm:{} as AlgorithmTaskVO,
cameraRow:{} as CameraRow,
});
// 打开配置算法的弹框
const openAlgDialog=async (data:any)=>{
try {
// 获取当前摄像头配置的算法信息
const res=await getAlgorithmTaskByCameraId({
cameraId: data.id
})
algDialogObj.algForm=res.data || {};
algDialogObj.algVisible=true;
algDialogObj.cameraRow=data;
} catch (error) {
console.error('获取算法配置失败:', error);
ElMessage.error('获取算法配置失败,请重试');
}
}
// NVR条件查询
const queryNvr=async ()=>{
const res=await getNvrListApi(NvrFormData);
tableData.value=res.data.rows;
PageParams.pageNum=res.data.current;
PageParams.pageSize=res.data.limit;
PageParams.total=res.data.total;
}
// 打开新增NVR的弹框
const openAddNvrDialog=()=>{
deviceDialogObj.deviceVisible=true;
deviceDialogObj.title='新增设备';
}
// 新增或者修改设备的弹框
const deviceDialogObj = reactive({
deviceVisible: false,
title: '',
deviceForm:{} as NvrForm,
id: -1
});
interface SubStation {
id:number;
name:string
}
// 打开编辑NVR的弹框
const openNvrChangeDialog=(data:NvrRow)=>{
deviceDialogObj.deviceVisible=true;
deviceDialogObj.title='修改设备';
deviceDialogObj.deviceForm=deepClone(data);
console.log('编辑',deviceDialogObj.deviceForm)
deviceDialogObj.id=data.id;
}
// 保存NVR
const handleSave=async ()=>{
try {
// 表单验证
await addOrUpdateFormRef.value.validate();
if(deviceDialogObj.title=='新增设备')
{
// 执行上传并等待结果
const res=await createNvrApi(deviceDialogObj.deviceForm);
if(res.code=="0000")
{
// 重置表单数据,关闭弹窗
handleCancel();
//调用后端,进行一个设备的登录
const result=await deviceLogin({
id:res.data
})
console.log('登录信息',result);
// 刷新数据
await fetchTableData({});
ElMessage.success('新增成功');
}else {
ElMessage.warning("新增失败");
}
}else {
const res=await updateNvrApi(deviceDialogObj.deviceForm);
if(res.code=="0000")
{
// 重置表单数据,关闭弹窗
handleCancel();
// 刷新数据
await fetchTableData({});
ElMessage.success('修改成功');
}else {
ElMessage.warning("修改失败");
}
}
} catch (error) {
console.log('表单验证失败:', error);
}
}
// 记录NVR的Id集合
const ids=[] as number[];
// 表格多选点击事件
const handleSelectionChange=(data:NvrRow[])=>{
ids.length=0;
for(let i=0;i<data.length;i++)
{
ids.push(data[i].id || 1);
}
}
// 批量删除NVR
const deleteNvrsBatch=async ()=>{
try {
if(ids.length === 0) {
ElMessage.warning('请选择要删除的设备');
return;
}
const res=await removeNvrApi({
ids
});
if(res.code==="0000")
{
if(tableData.value.length==1 && PageParams.pageNum > 1)
{
PageParams.pageNum-=1;
}
await fetchTableData({});
ElMessage.success('批量删除成功');
}else {
ElMessage.error('批量删除失败: ' + (res.message || '未知错误'));
}
} catch (error) {
console.error('批量删除NVR失败:', error);
ElMessage.error('批量删除失败,请重试');
}
}
// 根据id删除NVR
const deleteByIdNvr=async (id: number)=>{
try {
await ElMessageBox.confirm(
'确定要删除该 NVR 设备吗?',
'删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
}
);
const res=await removeNvrApi({
ids: [id]
});
if(res.code==="0000")
{
if(tableData.value.length==1 && PageParams.pageNum > 1)
{
PageParams.pageNum-=1;
}
// 刷新数据
await fetchTableData({});
ElMessage.success('删除成功');
}
} catch (error) {
if (error === 'cancel') {
ElMessage.info('已取消删除');
} else {
console.error('删除NVR失败:', error);
}
}
}
const subStationList=ref<SubStation[]>([]);
const StationProps={
label: 'name',
value: 'id'
}
// 获取所有的区域
const getAllSubstationVOs= async ()=>{
try {
const res=await getAllSubstationVO();
if(res.code=='0000')
{
subStationList.value=res.data;
} else {
console.error('获取区域列表失败:', res.message);
}
} catch (error) {
console.error('获取区域列表失败:', error);
}
}
const openCameraEditDialog=(row:CameraRow)=>{
console.log(row);
editCameraDialog.editVisible=true;
editCameraDialog.cameraForm=deepClone(row) as CameraForm;
console.log(editCameraDialog.cameraForm);
}
const inspectionType = useDictOptions('inspection_type').value.map(item => {
return {
...item,
id: Number(item.id.substring(6))
};
});
const cameraProduct = useDictOptions('camera_driver').value.map(item => {
return {
...item,
id: Number(item.id.substring(6))
};
});
const cameraType = useDictOptions('camera_type').value.map(item => {
return {
...item,
id: Number(item.id.substring(6))
};
});
const handleSubmit=async ()=>{
try {
await ruleFormRef.value.validate();
const res=await updateCameraApi(editCameraDialog.cameraForm);
if(res.code==='0000')
{
ElMessage.success("修改成功");
// 刷新数据
await getChannelPage();
// 关闭弹框
editCameraDialog.editVisible=false;
}else {
ElMessage.success("修改失败");
// 重置数据
editCameraDialog.cameraForm={};
}
}catch(error)
{
console.log('表单验证失败:', error);
}
}
// 取消
const handleEditClose=()=>{
editCameraDialog.editVisible=false;
}
const handleEditDel = async (data: CameraRow) => {
try {
await ElMessageBox.confirm('确定要删除该摄像机吗?', '删除确认', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
});
await removeCameraApi({ ids: [data.id] });
ElMessage.success('删除成功');
await getChannelPage();
} catch (error) {
// 5. 捕获取消操作或接口报错
if (error === 'cancel') {
// 用户点击了取消按钮
ElMessage.info('已取消删除');
} else {
// 接口层面的报错(如果你的 axios 拦截器没有抛出全局提示,可以在这里处理)
console.error('删除失败:', error);
}
}
};
// 查询摄像机
const queryCamera= async()=>{
const res=await getCameraListApi({
...channelQuery,
nvrId:NvrId.value,
limit: channelPage.pageSize,
page: channelPage.pageNum
});
channelData.value=res.data.rows;
channelPage.pageNum=res.data.current;
channelPage.pageSize=res.data.limit;
channelPage.total=res.data.total;
}
const handleCancel=()=>{
deviceDialogObj.deviceVisible=false;
deviceDialogObj.deviceForm={};
}
// 新增:视图模式控制
const viewMode = ref('list');
const currentNvrDetail = ref<NvrRow>();
const defaultProps = {
children: 'children',
label: 'name'
};
// 分页查询参数
const PageParams = reactive({
pageNum: 1,
pageSize: 10,
total: 0
});
// 通道分页参数
const channelPage=reactive({
pageNum: 1,
pageSize: 10,
total: 0
})
// 视频控制对象
const controlDialogObj=reactive({
controlVisible:false,
title:'',
id: -1,
curCamera:{}
})
// 视频播放对象
const playDialogObj=reactive({
playVisible: false,
title:'',
id: -1
})
// 视频播放
const openPlayDialog=(data:CameraRow) =>{
if(data.status==0)
{
ElMessage.warning('摄像头不在线');
return;
}
playDialogObj.playVisible=true;
playDialogObj.title= '视频预览';
if (data.channelId !== undefined) {
playDialogObj.id = data.id;
} else {
console.warn('channelId 不存在于当前数据中');
}
}
// 视频控制
const openControlDialog=(data:CameraRow)=>{
if(data.status==0)
{
ElMessage.warning('摄像头不在线');
return;
}
controlDialogObj.controlVisible=true;
controlDialogObj.title="摄像头控制"
if (data.id !== undefined) {
controlDialogObj.id = data.id;
controlDialogObj.curCamera=data;
} else {
console.warn('channelId 不存在于当前数据中');
}
}
const handleClosePlay= async ()=>{
playDialogObj.playVisible=false
}
const handleClose=async ()=>{
controlDialogObj.controlVisible=false
}
// 记录NVR的id
const NvrId=ref();
// 获取通道列表
const showChannelList= async (data:NvrRow)=>{
viewMode.value='detail';
currentNvrDetail.value=data;
console.log(currentNvrDetail.value)
// 根据NVRid获取通道列表
const res=await getCameraListApi({
nvrId:data.id,
limit: channelPage.pageSize,
page: channelPage.pageNum
})
NvrId.value=data.id;
channelData.value=res.data.rows;
channelPage.pageNum=res.data.current;
channelPage.pageSize=res.data.limit;
channelPage.total=res.data.total;
}
// 分页获取通道列表
const getChannelPage=async () =>{
const res=await getCameraListApi({
nvrId:NvrId.value,
limit: channelPage.pageSize,
page: channelPage.pageNum
})
channelData.value=res.data.rows;
channelPage.pageNum=res.data.current;
channelPage.pageSize=res.data.limit;
channelPage.total=res.data.total;
}
// 点击节点
const handleNodeClick =async (data:TreeNode) => {
currentSelectNode.value = data;
if(data.type===3) return;
// 获取该节点下的所有NVR列表
viewMode.value = 'list';
PageParams.pageNum = 1;
if (data.type === 2) {
// 如果点击的是Nvr节点直接展示这个NVR的列表
const res=await getNvrListApi({
id: data.id
});
tableData.value = res.data.rows;
PageParams.pageNum=res.data.current;
PageParams.pageSize=res.data.limit;
PageParams.total=res.data.total;
} else {
// 获取该节点下的所有NVR列表
viewMode.value = 'list';
const res=await listByNode({
nodeId: data.id,
type: data.type,
...PageParams
})
tableData.value = res.data.rows;
PageParams.pageNum=res.data.current;
PageParams.pageSize=res.data.limit;
PageParams.total=res.data.total;
}
};
// 2. 请求后端分页接口
const fetchTableData = async (params:NvrQuery) => {
loading.value = true;
try {
// 调用后端
const res = await getNvrListApi({
...params,
page:PageParams.pageNum,
limit:PageParams.pageSize
});
tableData.value=res.data.rows;
PageParams.pageNum=res.data.current;
PageParams.pageSize=res.data.limit;
PageParams.total=res.data.total;
} catch (err) {
console.error('加载NVR列表失败', err);
} finally {
loading.value = false;
}
};
// 3. 分页事件处理
const handleSizeChange = (val:number) => {
PageParams.pageSize = val;
channelPage.pageSize=val;
if(viewMode.value=='list')
{
fetchTableData({});
}else if(viewMode.value=='detail') {
getChannelPage();
}
};
const handleCurrentChange = (val:number) => {
PageParams.pageNum = val;
channelPage.pageNum=val;
if(viewMode.value=='list')
{
fetchTableData({});
}else if(viewMode.value=='detail') {
getChannelPage();
}
};
// 摄像头通道刷新
const refreshCamera= async (data:NvrRow)=>{
try {
refreshLoading.value=true;
const res=await refresh({
id:Number(data.id),
deviceType:data.driver
});
if(res.code=="0000")
{
// 重新加载通道列表
await getChannelPage();
ElMessage.success('刷新成功');
} else {
ElMessage.error('刷新失败: ' + (res.message || '未知错误'));
}
} catch (error)
{
console.error('刷新通道失败:', error);
ElMessage.error('刷新失败,请重试');
} finally {
refreshLoading.value=false;
}
}
// 刷新通道对象
const refreshObj=reactive({
isSyncing:false,
isSuccess:false,
percentage:0,
status:'',
timer:null
})
let timer: ReturnType<typeof setInterval> | null = null;
const handleRefresh=(data:NvrRow)=>{
// // 1. 初始化状态
refreshObj.percentage= 0;
refreshObj.isSyncing = true;
refreshObj.isSuccess = false;
refreshObj.status = '';
if(timer) clearInterval(timer);
// 模拟进度条增长
timer = setInterval( async () => {
const step = Math.floor(Math.random() * 10) + 30;
refreshObj.percentage += step;
// 卡在 99% 等待后端最终响应
if (refreshObj.percentage >= 99) {
refreshObj.percentage = 99;
clearInterval(timer!);
// 向后端请求
await refreshChannel(data);
refreshObj.percentage = 100;
refreshObj.status = 'success';
refreshObj.isSuccess = true;
// 停留 2 秒让用户看清楚大对号和“同步完成”,然后关闭弹窗
setTimeout(() => {
refreshObj.isSyncing = false;
}, 2000);
}
}, 400);
};
// 刷新通道
const refreshChannel= async (data:NvrRow)=>{
try {
data.isLoading=true;
refreshLoading.value=true;
const res=await refresh({
id:Number(data.id),
deviceType:data.driver
});
if(res.code=="0000")
{
ElMessage.success('刷新成功');
} else {
ElMessage.error('刷新失败: ' + (res.message || '未知错误'));
}
} catch (error)
{
console.error('刷新通道失败:', error);
ElMessage.error('刷新失败,请重试');
} finally {
refreshLoading.value=false;
data.isLoading=false;
}
}
// 新增:返回列表页逻辑
const backToList = () => {
viewMode.value = 'list';
};
// 过滤树节点
watch(filterText, (val) => {
treeRef.value?.filter(val);
});
const filterNode = (value:string, data:TreeNode) => {
if (!value) return true;
return data.name.includes(value);
};
// 获取树型列表数据
const fetchData = async () => {
try {
const res = await getTreeData();
treeData.value = res.data;
} catch (error) {
console.error('获取数据失败', error);
ElMessage.error('获取资源列表失败,请重试');
}
};
onMounted(() => {
fetchData();
fetchTableData({});
getAllSubstationVOs();
});
// 组件销毁前清理定时器,防止内存泄漏
onBeforeUnmount(() => {
if (timer) clearInterval(timer);
});
</script>
<style scoped>
/* 保持原有布局 */
.container {
display: flex;
height: 100%;
}
.sidebar {
width: 320px;
padding: 10px;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding: 10px;
overflow: hidden;
}
.box-card {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.el-card__body) {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-tree-node {
display: flex;
align-items: center;
font-size: 14px;
}
.node-label {
margin-left: 6px;
}
.ehv-text {
color: #d32f2f;
font-weight: bold;
}
/* 详情页特有样式 */
.detail-container {
padding: 10px;
}
.detail-tabs {
margin-top: 20px;
height: 100%;
}
.video-wrapper {
width: 100%; /* 宽度跟随 Dialog */
/* 核心代码:强制保持 16:9 比例 */
aspect-ratio: 16 / 9;
background-color: #000; /* 视频未加载时显示黑色背景,像专业监控屏 */
position: relative; /* 为内部绝对定位做准备(如果需要) */
overflow: hidden;
border-radius: 4px; /* 稍微搞点圆角,好看 */
/* canvas absolute
可能需要给内部元素加上这个 */
:deep(video), :deep(canvas) {
width: 100%;
height: 100%;
display: block;
}
}
/* 算法配置列样式 */
.alg-info-container {
min-height: 60px; /* 设置最小高度,确保行高一致 */
display: flex;
flex-direction: column;
justify-content: center;
padding: 8px 0;
}
.alg-tag-item {
margin-bottom: 4px;
}
.alg-tag-item:last-child {
margin-bottom: 0;
}
.no-alg-info {
color: #909399;
font-size: 14px;
}
</style>