<!-- Description: 设备管理 - 新建/编辑 Author: 李亚光 Date: 2024-11-01 --> <script lang="ts" setup name="DeviceManageEdit"> import { ElMessage, type FormRules } from 'element-plus' import { getSceneAll } from '@/api/page/scene' import { getModelAll } from '@/api/page/model' import { addDevice, getModelRelations, getSceneRelations, updateDevice, updateModelRelations, updateSceneRelations } from '@/api/page/device' import drawArea from './drawArea.vue' const emits = defineEmits(['refresh']) const dataFormRef = ref() const dialogFormVisible = ref(false) // 对话框是否显示 const dialogStatus = ref('') // 对话框类型:create,update const multipleTableRef = ref() const dataForm = ref<{ [key: string]: string }>({ name: '', // 名称 code: '', // 编号 type: '', // 摄像头类型 input_stream_url: '', // RTSP地址 image_save_interval: '60', // 识别间隔 alarm_interval: '60', // 告警间隔 mode: '', gas_ip: '', relation_scene_name: '', relation_model_names: '', ip: '', }) // 表单 const textMap: { [key: string]: string } = { update: '编辑', create: '新增', detail: '详情', } // 表头显示标题 const btnLoading = ref(false) // 保存按钮的加载中状态 const rules: FormRules = { name: [{ required: true, message: '名称不能为空', trigger: ['blur', 'change'] }], code: [{ required: true, message: '编号不能为空', trigger: ['blur', 'change'] }], type: [{ required: true, message: '摄像头类型必选', trigger: ['blur', 'change'] }], input_stream_url: [{ required: true, message: '摄像头类型不能为空', trigger: ['blur', 'change'] }], image_save_interval: [{ required: true, message: '识别间隔不能为空', trigger: ['blur', 'change'] }], alarm_interval: [{ required: true, message: '告警间隔不能为空', trigger: ['blur', 'change'] }], mode: [{ required: true, message: '场景模式必选', trigger: ['blur', 'change'] }], gas_ip: [{ required: true, message: '甲烷IP必填', trigger: ['blur', 'change'] }], ip: [{ required: true, message: '摄像头IP必填', trigger: ['blur', 'change'] }], relation_model_names: [{ required: true, message: '关联算法必选', trigger: ['blur', 'change'] }], relation_scene_name: [{ required: true, message: '业务场景必选', trigger: ['blur', 'change'] }], } // 前端校验规则 const sceneList = ref<any[]>([]) const modelList = ref<any[]>([]) const modelAllList = ref<any[]>([]) const areaRef = ref() const showVideo = ref(false) // 电子围栏宽高以及是否显示 const drawWidth = ref(426.4) const drawHeight = ref(240) const showDraw = ref(true) const webRtcServer = ref(null) const loadingPonit = ref(true) // 初始化对话框 const initDialog = (row: any, dialogStatusValue: string) => { dialogStatus.value = dialogStatusValue dialogFormVisible.value = true showVideo.value = false if (dialogStatusValue === 'create') { resetForm() // nextTick(() => { // multipleTableRef.value?.resetFields() // }) } else { dataForm.value = JSON.parse(JSON.stringify(row)) for (const i in dataForm.value) { dataForm.value[i] = typeof dataForm.value[i] === 'number' ? String(dataForm.value[i]) : dataForm.value[i] } if (dataForm.value.mode === '1') { // 算法 getModelRelations(row.id).then((res) => { if (!res.data.length) { modelList.value = modelAllList.value return } modelList.value = res.data.map((item: any) => ({ ...item, name: item.algo_model_name, custom_threshold: item.threshold, id: item.algo_model_id, })) if (modelList.value.length !== modelAllList.value.length) { // 先判断是删除还是新建 if (modelList.value.length < modelAllList.value.length) { // 新建 const addArr = modelAllList.value.filter((item: any) => modelList.value.every((citem: any) => citem.name !== item.name)) addArr.forEach((element: any) => { modelList.value.push({ ...element, custom_threshold: '0.5', }) }) } } modelList.value = modelList.value.sort((a: any, b: any) => Number(a.id) - Number(b.id)) nextTick(() => { modelList.value.forEach((element: any) => { multipleTableRef.value!.toggleRowSelection( element, String(element.is_use) === '1', ) }) }) }) } else if (dataForm.value.mode === '2') { loadingPonit.value = true getSceneRelations({ id: row.id }).then(res => { if (res.data.range_points) { areaRef.value.setPonits(res.data.range_points) } else { areaRef.value.setPonits() } loadingPonit.value = false }) playerVideo() } } } defineExpose({ initDialog, }) // 选择的算法 const selectModelList = ref<any[]>([]) const selectModel = (value: any) => { selectModelList.value = value dataForm.value.relation_model_names = value.map((item: any) => item.name).join(',') } // 重置表单 function resetForm() { selectModelList.value = [] dataForm.value = { name: '', code: '', // 编号 type: '', // 摄像头类型 input_stream_url: '', // RTSP地址 image_save_interval: '60', // 识别间隔 alarm_interval: '60', // 告警间隔 mode: '', gas_ip: '', relation_scene_name: '', relation_model_names: '', ip: '', } nextTick(() => { dataFormRef.value.resetFields() }) } // 获取字典相关 const fetchDict = () => { getSceneAll({}).then((res) => { sceneList.value = res.data }) getModelAll({}).then((res) => { modelList.value = res.data.map((item: any) => ({ ...item, custom_threshold: 0.5 })).sort((a: any, b: any) => Number(a.id) - Number(b.id)) modelAllList.value = res.data.map((item: any) => ({ ...item, custom_threshold: 0.5 })).sort((a: any, b: any) => Number(a.id) - Number(b.id)) }) } fetchDict() // 新建 const add = () => { const confirm = () => { ElMessage.success(dialogStatus.value === 'update' ? '编辑成功' : '新建成功') emits('refresh') resetForm() cancel() // if (webRtcServer.value) { console.log('销毁视频') webRtcServer.value?.disconnect() webRtcServer.value = null showVideo.value = false // } } (dialogStatus.value === 'update' ? updateDevice : addDevice)(dataForm.value).then((res) => { // 算法 if (dataForm.value.mode === '1') { updateModelRelations({ id: dialogStatus.value === 'update' ? dataForm.value.id : res.data.id, list: [ ...selectModelList.value.map((item: any) => ({ algo_model_id: item.id, is_use: 1, threshold: item.custom_threshold, })), ...modelList.value.filter((item: any) => selectModelList.value.every((citem: any) => citem.id !== item.id)).map((item: any) => ({ algo_model_id: item.id, is_use: 0, threshold: item.custom_threshold, })), ], }).then((res) => { confirm() }) } else if (dataForm.value.mode === '2') { // console.log(JSON.stringify(areaRef.value.getPoints()), 'JSON.stringify(areaRef.value.getPoints())') // 场景 updateSceneRelations({ id: dialogStatus.value === 'update' ? dataForm.value.id : res.data.id, scene_id: sceneList.value.filter((item: any) => item.name === dataForm.value.relation_scene_name)[0]?.id || '', range_points: JSON.stringify(areaRef.value.getPoints()) }).then(() => { confirm() }) } else { confirm() } }) } const saveData = () => { // 验证 // 1.关联算法 if (dataForm.value.mode === '1' && selectModelList.value.some(item => !item.custom_threshold)) { ElMessage.warning('关联算法所有阈值应为有效值') return } // 2.关联场景 if (dataForm.value.mode === '2' && !dataForm.value.relation_scene_name) { ElMessage.warning('场景应为必选,若列表为空,应先添加场景') return } // 验证 dataFormRef.value.validate((valid: any) => { if (valid) { if (dialogStatus.value === 'create') { add() } else { add() } } }) } // 电子围栏恢复默认 const resetArea = () => { areaRef.value.setPonits() } // 清空电子围栏 const clearArea = () => { areaRef.value.setPonits('[]') } // 视频地址 function playerVideo() { if (dataForm.value.mode !== '2') { return } if (webRtcServer.value) { webRtcServer.value.disconnect() webRtcServer.value = null console.log('销毁视频') } if (dataForm.value.mode !== '2' || !dataForm.value.input_stream_url) { return } showVideo.value = true setTimeout(() => { nextTick(() => { webRtcServer.value = new WebRtcStreamer('video-rtsp-container', 'http://127.0.0.1:8000') webRtcServer.value.connect(dataForm.value.input_stream_url) const myVideo = document.getElementById('video-rtsp-container') as HTMLMediaElement myVideo.addEventListener('loadedmetadata', () => { let width = myVideo.videoWidth let height = myVideo.videoHeight console.log(width, height, '视频分辨率') if (width === 0 || height === 0) { width = 426.4 * 3 height = 240 * 3 } // 获取到视频分辨率后 设置 canvans 和 video 的 宽高比 const resolution = width / height console.log(resolution, '宽高比') myVideo.style.width = `${width / 3}px` myVideo.style.height = `${height / 3}px` // const drawWidth = ref(426.4) // const drawHeight = ref(240) // 设置canvans drawWidth.value = width / 3 drawHeight.value = height / 3 showDraw.value = false setTimeout(() => { showDraw.value = true if (!dataForm.value.id) return loadingPonit.value = true getSceneRelations({ id: dataForm.value.id }).then(res => { setTimeout(() => { if (res.data.range_points) { areaRef.value.setPonits(res.data.range_points) } else { areaRef.value.setPonits() } loadingPonit.value = false }, 500); }) }, 100) }) }) }) } watch(() => dataForm.value.mode, (newVal) => { if (newVal === '2' && dataForm.value.input_stream_url) { playerVideo() } }) watch(() => dialogFormVisible.value, (newVal) => { if (webRtcServer.value) { console.log('销毁视频') webRtcServer.value.disconnect() webRtcServer.value = null showVideo.value = false } }) onUnmounted(() => { if (webRtcServer.value) { console.log('销毁视频') webRtcServer.value.disconnect() webRtcServer.value = null showVideo.value = false } }) const cancel = () => { dialogFormVisible.value = false resetForm() if (webRtcServer.value) { console.log('销毁视频') webRtcServer.value.disconnect() webRtcServer.value = null showVideo.value = false } } </script> <template> <el-dialog v-model="dialogFormVisible" :title="textMap[dialogStatus]" append-to-body top="6vh"> <el-form ref="dataFormRef" :rules="rules" :model="dataForm" label-position="right" label-width="120px" :disabled="dialogStatus === 'detail'"> <el-row :gutter="24"> <el-col :span="12"> <el-form-item label="摄像头名称" prop="name"> <el-input v-model.trim="dataForm.name" type="text" placeholder="必填" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="摄像头编号" prop="code"> <el-input v-model.trim="dataForm.code" type="text" placeholder="必填" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="摄像头IP" prop="ip"> <el-input v-model.trim="dataForm.ip" type="text" placeholder="必填" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="RTSP地址" prop="input_stream_url"> <el-input v-model.trim="dataForm.input_stream_url" type="text" placeholder="必填" @blur="playerVideo" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="识别间隔(s)" prop="image_save_interval"> <el-input v-model.trim="dataForm.image_save_interval" type="number" :min="1" placeholder="必填" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="告警间隔(s)" prop="alarm_interval"> <el-input v-model.trim="dataForm.alarm_interval" type="number" :min="1" placeholder="必填" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="摄像头类型" prop="type"> <el-select v-model="dataForm.type" placeholder="摄像头类型" clearable class="select" style="width: 100%;"> <el-option value="1" label="摄像头" /> <el-option value="2" label="安全树" /> </el-select> </el-form-item> </el-col> <el-col v-if="dataForm.type === '2'" :span="12"> <el-form-item label="甲烷IP" prop="gas_ip"> <el-input v-model.trim="dataForm.gas_ip" type="text" placeholder="必填" /> </el-form-item> </el-col> </el-row> <el-row :gutter="24"> <el-col :span="12"> <el-form-item label="场景模式" prop="mode"> <el-radio-group v-model="dataForm.mode"> <el-radio label="2"> 业务场景 </el-radio> <el-radio label="1"> 算法关联 </el-radio> <el-radio label="0"> 无 </el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> <el-row v-if="dataForm.mode === '2'" :gutter="24"> <el-col :span="12"> <el-form-item label="业务场景" prop="relation_scene_name"> <el-select v-model="dataForm.relation_scene_name" placeholder="业务场景" clearable class="select" style="width: 100%;"> <el-option v-for="item in sceneList" :key="item.id" :value="item.name" :label="item.name" /> </el-select> </el-form-item> </el-col> </el-row> <el-row v-if="dataForm.mode === '1'" :gutter="24"> <el-col :span="18"> <el-form-item label="算法关联" prop="relation_model_names"> <el-table ref="multipleTableRef" :data="modelList" border stripe style="width: 100%;" height="300px" @selection-change="selectModel"> <el-table-column label="算法名称" align="center" value="name"> <template #default="scope"> {{ scope.row.name }} </template> </el-table-column> <el-table-column label="阈值" align="center"> <template #default="scope"> <el-input-number v-model="scope.row.custom_threshold" :min="0" controls-position="right" /> </template> </el-table-column> <el-table-column type="selection" width="55" align="center" /> </el-table> </el-form-item> </el-col> </el-row> <el-row v-show="dataForm.mode === '2'" :gutter="24"> <el-col :span="24"> <el-form-item label="监控区域绘制" prop=""> <!-- 电子围栏 --> <div v-loading="loadingPonit" style="border: 1px solid #000;position: relative;z-index: 9;"> <draw-area v-if="showDraw" ref="areaRef" :height="drawHeight" :width="drawWidth" :disabled="dialogStatus === 'detail'" v-show="dataForm.mode === '2'" /> <video v-if="showVideo" id="video-rtsp-container" autoplay muted width="426.4" height="240" style="object-fit: fill;width: 426.4px; height: 240px;position: absolute;top:0;left:0;z-index: 0;" /> </div> <div v-if="dialogStatus !== 'detail'" style="margin-left: 15px;height: 240px;"> <div><el-button type="primary" @click="resetArea" :disabled="loadingPonit">恢复默认</el-button></div> <div style="margin-top: 15px;width: 100%;" @click="clearArea"><el-button style="margin: 0 auto;" :disabled="loadingPonit">清空</el-button> </div> </div> </el-form-item> </el-col> </el-row> </el-form> <template #footer> <div class="dialog-footer"> <!-- dialogStatus --> <template v-if="dialogStatus !== 'detail'"> <el-button :loading="btnLoading" type="primary" @click="saveData"> 保存 </el-button> <el-button @click="cancel"> 取消 </el-button> </template> <template v-else> <el-button type="primary" @click="cancel"> 确认 </el-button> </template> </div> </template> </el-dialog> </template> <style lang="scss" scoped> .el-dialog { width: 700px; } .el-select { width: 100%; } .draw { height: 300px; width: 533.333px; border: 2px solid #000; padding: 0; margin: 0; box-sizing: content-box; position: relative; } </style>