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.

1221 lines
40 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="div-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-button type="danger" icon="Delete" @click="handleBatchDel" plain :loading="deleteBatchLoading">批量删除</el-button>
</el-form-item>
</el-form>
<!-- 通道列表 -->
<el-table :data="channelData"
height="350"
style="width: 100%" @selection-change="handleBatchChannel"
:row-key="(row: any)=>row.id"
size="small" >
<el-table-column type="selection" width="20"/>
<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";
import {fa} from "element-plus/es/locale";
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("修改失败");
// 重置数据
ruleFormRef.value.resetFields();
}
}catch(error)
{
console.log('表单验证失败:', error);
}
}
// 取消
const handleEditClose=()=>{
editCameraDialog.editVisible=false;
}
const cameraIds=[] as number[];
const deleteBatchLoading = ref(false);
// 表格多选点击事件
const handleBatchChannel=(data: CameraRow[])=>{
ids.length=0;
for(let i=0;i<data.length;i++)
{
cameraIds.push(data[i].id);
}
}
//
const handleBatchDel = async () => {
try {
if (cameraIds.length === 0) {
ElMessage.warning('请选择要删除的摄像机');
return;
}
await ElMessageBox.confirm('确定要删除这些摄像机吗?', '删除确认', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
});
deleteBatchLoading.value = true;
await removeCameraApi({ ids: cameraIds });
ElMessage.success('删除成功');
deleteBatchLoading.value = false;
await getChannelPage();
} catch (error) {
// 5. 捕获取消操作或接口报错
if (error === 'cancel') {
// 用户点击了取消按钮
ElMessage.info('已取消删除');
deleteBatchLoading.value = false;
} else {
deleteBatchLoading.value = false;
console.log(error)
}
}
}
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>
/* 保持原有布局 */
.div-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>