<!-- 巡检任务 --> <script setup lang="ts"> import { ElMessage, ElMessageBox, ElTooltip, dayjs } from 'element-plus' import controlImg from '@/assets/tempImages/control.png' import dog from '@/assets/tempImages/dog-picture.jpg' import dogArm from '@/assets/tempImages/dog-arm.jpg' import sgjc from '@/assets/tempImages/sgjc.jpg' import tyx from '@/assets/tempImages/tyx.jpg' import yrg from '@/assets/tempImages/yrg.jpg' import Amap from '@/components/aMap/index.vue' import { getDictByCode } from '@/api/system/dict' import useSocket from '@/store/modules/websocket' import { getDeviceList, getRouteLog } from '@/api/patrol/manage' import { controlDevice, delRoute, getRouteByDog, getRouteList } from '@/api/patrol/navigation' const socket = useSocket() const mapRef = ref() // 地图组件 const currentDevice: any = ref({ id: '', robotName: '--', // 设备名称 robotCode: '--', // 设备编号 robotStatus: '--', // 1,有效,0无效 robotCell: '--', // 电量 routeId: '', // 正在执行路线id route: '--', speed: '--', name: '--', // 小区名称 address: '--', // 小区地址 }) // 点击更换设备 const changeDevice = () => { ElMessageBox.confirm( '确定更换设备吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', }, ).then(() => { ElMessage.info('敬请期待') }) } // 点击解除绑定 const unbind = () => { ElMessageBox.confirm( '确定解除绑定吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', }, ).then(() => { ElMessage.info('敬请期待') }) } // -------------------------------------------字典------------------------------------------ const approvalLogTypeMap = ref({}) as any // 是否加急{1: 是} // 获取字典值 async function getDict() { // 是否加急 const res = await getDictByCode('patrolContent') res.data.forEach((item: any) => { approvalLogTypeMap.value[`${item.value}`] = item.name }) } // -----------------------------------------------表格------------------------------------------------------ // 表格表头 const columns: any = ref([ { text: '巡检内容', value: 'patrolTypeName', align: 'center' }, { text: '巡检情况', value: 'patrolContent', align: 'center' }, { text: '甲烷浓度(ppm.m)', value: 'situation', align: 'center' }, { text: '时间', value: 'createTime', align: 'center' }, ]) const listQuery = ref({ routeInfoId: '', limit: 10, offset: 1, }) const list: any = ref([]) // 数据列表 const total = ref() const listLoading = ref(false) // 表格loading // 巡检情况日志获取 const fetchRouteLog = () => { console.log('巡检情况日志获取') listQuery.value.routeInfoId = currentDevice.value.routeId getRouteLog(listQuery.value).then((res) => { list.value = res.data.rows.map((item: any) => { const url = `${window.localStorage.getItem('BaseUrl')}/static/${item.patrolPicture}` return { ...item, picture: !item.patrolPicture ? '' : url, patrolTypeName: item.patrolType ? approvalLogTypeMap.value[item.patrolType] : item.patrolType, } }) total.value = res.data.total }) } // 页数发生变化后的操作,可能是页码变化,可能是每页容量变化,此函数必写 function changePage(val: { size: number; page: number }) { if (val && val.size) { listQuery.value.limit = val.size } if (val && val.page) { listQuery.value.offset = val.page } fetchRouteLog() } // -------------------------------------------------设备相关------------------------------------------------- // 根据设备区查询当前巡检路线 const fetchRouteByDog = (id: string) => { getRouteByDog({ id }).then((res) => { currentDevice.value.routeId = res.data[0].id // 正在执行路线id fetchRouteLog() }) } // 获取设备列表 const fetchDeviceList = () => { const params = { robotName: '', } getDeviceList(params).then((res) => { // 先找到所有在线的狗 const onLineDogList = res.data.rows.filter((item: { robotStatus: string }) => item.robotStatus === '1') || [] if (onLineDogList.length) { currentDevice.value.id = res.data.rows[0].id currentDevice.value.robotName = res.data.rows[0].robotName // 设备名称 currentDevice.value.robotCode = res.data.rows[0].robotCode // 设备编号 currentDevice.value.robotStatus = `${res.data.rows[0].robotStatus}` === '1' ? '工作中' : '离线' // 1,有效,0无效 currentDevice.value.robotCell = `${res.data.rows[0].robotCell}%` // 电量 // currentDevice.value.routeId = res.data.rows[0].routeId // 正在执行路线id fetchRouteByDog(currentDevice.value.id) } else { ElMessage.warning('暂无在线的机器狗') } }) } // --------------------------------------------开始巡检------------------------------------------------------------------------- // 开始巡检、急停、恢复 const control = (type: string) => { if (type === 'a/') { mapRef.value.createDogMarker() } if (currentDevice.value.id) { const params = { command: type, robotDogId: currentDevice.value.id, } controlDevice(params).then(() => { ElMessage.success('操作成功') }) } } // ----------------------------------------------巡检路线---------------------------------------------------------------- const routeList: any = ref([]) // 巡检路线列表 const routePointsList: any = ref([]) // 巡检路线列表 // 获取路线列表 const fetchRouteList = async () => { const params = { communityAddress: '', // 小区地址 communityName: '', // 小区名称 robotName: '', // 机器人名称 routeName: '', // 巡航线名称 limit: 999999, offset: 1, } const res = await getRouteList(params) if (res && res.data && res.data.rows && res.data.rows.length) { routeList.value = res.data.rows currentDevice.value.route = routeList.value[0].id } } // 监听巡检路线变化 watch(() => currentDevice.value.route, (newValue) => { const index = routeList.value.findIndex((item: { id: string }) => item.id === newValue) if (index !== -1) { if (routeList.value[index].routeInfoDetailList && Array.isArray(routeList.value[index].routeInfoDetailList) && routeList.value[index].routeInfoDetailList.length) { routePointsList.value = routeList.value[index].routeInfoDetailList.map((item: any) => { return { ...item, ...routeList.value[index], } }) console.log('切换的路线的信息:', routePointsList.value) } else { mapRef.value.removeMarkerLayer() console.log('找不到路线信息', routeList.value[index]) } } }, { immediate: true }) // ---------------------------------------图片----------------------------------------------------------------------- watch(() => socket.data, (newVal: any) => { // 监听到新的巡检情况 if (newVal && newVal.type === 'AI') { // 报警推送 // let tempData = {} if (currentDevice.value.robotCode === newVal.devcode) { // 找到当前查看的狗 // =========================查询巡检路线========================== // const index = routeList.value.findIndex((item: { id: string }) => item.id === newVal.routeInfoId) const url = `${window.localStorage.getItem('BaseUrl')}/static/${newVal.picture}}` list.value.shift({ ...newVal, picture: url, patrolTypeName: newVal.patrolType ? approvalLogTypeMap.value[newVal.patrolType] : newVal.patrolType, createTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), // 时间 }) // patrolType === 10011代表燃气泄漏,如果isAlarm为1则需要再地图上打点,并弹框显示信息, // 如果isAlarm为0,如果已经打点且 if (newVal.patrolType === '10011') { // } } } else if (newVal && newVal.type === 'gps') { // 狗的实时位置 if (currentDevice.value.robotCode === newVal.devcode) { // 找到当前查看的狗 mapRef.value.startMoving(null, [newVal.gps.gps_x, newVal.gps.gps_y]) } } }, { deep: true, immediate: true, }) // -------------------------------------------实时视频-------------------------------------- const webTrcServeDog: any = ref(null) const webTrcServeArm: any = ref(null) const webTrcServeDogDom: any = ref(null) const webTrcServeArmDom: any = ref(null) const dogVideoLoading = ref(false) const armVideoLoading = ref(false) const videoDogUrl: any = ref('rtsp://176.139.87.16/axis-media/media.amp') // 狗视频地址 const videoArmUrl: any = ref('rtsp://176.139.87.16/axis-media/media.amp') // 机械臂视频地址 // 播放视频 function playVideo() { webTrcServeDog.value.connect(videoDogUrl.value) webTrcServeArm.value.connect(videoArmUrl.value) webTrcServeDogDom.value = document.getElementById('dog-video-container') webTrcServeArmDom.value = document.getElementById('arm-video-container') webTrcServeDogDom.value.addEventListener('connecting', handleConnectingDog) webTrcServeDogDom.value.addEventListener('connected', handleConnectedDog) webTrcServeDogDom.value.addEventListener('disconnecting', handleDisconnectingDog) webTrcServeDogDom.value.addEventListener('disconnected', handleDisconnectedDog) webTrcServeDogDom.value.addEventListener('error', handleErrorDog) webTrcServeArmDom.value.addEventListener('connecting', handleConnectingArm) webTrcServeArmDom.value.addEventListener('connected', handleConnectedArm) webTrcServeArmDom.value.addEventListener('disconnecting', handleDisconnectingArm) webTrcServeArmDom.value.addEventListener('disconnected', handleDisconnectedArm) webTrcServeArmDom.value.addEventListener('error', handleErrorArm) } // 销毁视频 function destroyVideo() { if (webTrcServeDog.value) { webTrcServeDog.value.disconnect() webTrcServeDog.value = null webTrcServeDogDom.value.removeEventListener('connecting', handleConnectingDog) webTrcServeDogDom.value.removeEventListener('connected', handleConnectedDog) webTrcServeDogDom.value.removeEventListener('disconnecting', handleDisconnectingDog) webTrcServeDogDom.value.removeEventListener('disconnected', handleDisconnectedDog) webTrcServeDogDom.value.removeEventListener('error', handleErrorDog) } if (webTrcServeArm.value) { webTrcServeArm.value.disconnect() webTrcServeArm.value = null webTrcServeArmDom.value.removeEventListener('connecting', handleConnectingArm) webTrcServeArmDom.value.removeEventListener('connected', handleConnectedArm) webTrcServeArmDom.value.removeEventListener('disconnecting', handleDisconnectingArm) webTrcServeArmDom.value.removeEventListener('disconnected', handleDisconnectedArm) webTrcServeArmDom.value.removeEventListener('error', handleErrorArm) } } // 定义 loading 状态 const loading = ref(false) function handleConnectingArm() { console.log('机械臂:开始尝试建立连接') armVideoLoading.value = true } function handleConnectedArm() { console.log('机械臂:连接已成功建立') armVideoLoading.value = false } function handleDisconnectingArm() { console.log('机械臂:开始断开连接') armVideoLoading.value = true } function handleDisconnectedArm() { console.log('机械臂:连接已断开') armVideoLoading.value = false } function handleErrorArm(event: any) { console.log('机械臂:播放出错:', event) armVideoLoading.value = false } function handleErrorDog(event: any) { console.log('狗:播放出错:', event) armVideoLoading.value = false } function handleConnectingDog() { console.log('狗:开始尝试建立连接') dogVideoLoading.value = true } function handleConnectedDog() { console.log('狗:连接已成功建立') dogVideoLoading.value = false } function handleDisconnectingDog() { console.log('狗:开始断开连接') dogVideoLoading.value = true } function handleDisconnectedDog() { console.log('狗:连接已断开') dogVideoLoading.value = false } // -------------------------------------------------------------------------------------------- onMounted(async () => { await getDict() // 获取字典 await fetchRouteList() // 获取路线列表 fetchDeviceList() // 获取设备列表 // 实时视频 setTimeout(() => { nextTick(() => { webTrcServeDog.value = new WebRtcStreamer('dog-video-container', 'http://127.0.0.1:8000') webTrcServeArm.value = new WebRtcStreamer('arm-video-container', 'http://127.0.0.1:8000') playVideo() }) }) }) onBeforeUnmount(() => { destroyVideo() // 销毁实时视频 }) </script> <template> <app-container> <div class="patrol-manage"> <div class="box box-top"> <div class="box-top-item"> <amap ref="mapRef" :patrol-info-list="routePointsList" /> </div> <!-- <img v-if="hoveredPointImage" :src="hoveredPointImage" alt="Point Image" style="width: 600px; height: 500px;position: fixed;top: 100px;right: 10px;"> --> <div style="height: 100%; background-color: #dcdfe6; width: 1px;" /> <div class="box-top-item"> <div class="box-title"> <div class="title"> <span>关联设备</span> <img style="width: 20px; height: 20px" src="../../../assets/tempImages/icon-link.svg" > </div> <div> <el-button type="primary" link @click="changeDevice"> 更换设备 </el-button> <el-button type="danger" link @click="unbind"> 解除绑定 </el-button> </div> </div> <div class="box-main"> <img style="width: 200px; height: 200px" src="../../../assets/tempImages/dog.png" > <div class="box-device"> <span> <span class="title">设备名称: </span> <span class="text">{{ currentDevice.robotName }}</span> </span> <span> <span class="title">设备状态: </span> <span class="text" :style="{ color: `${currentDevice.robotStatus}` === '工作中' ? '#afcc85' : '#ccc' }">{{ currentDevice.robotStatus }}</span> </span> <span> <span class="title">设备电量: </span> <span class="text">{{ currentDevice.robotCell }}</span> </span> <div style="display: flex;"> <div class="title"> 巡检路线: </div> <!-- <span class="text">{{ currentDevice.route }}</span> --> <el-select v-model="currentDevice.route" style="width: 160px;" disabled> <el-option v-for="item in routeList" :key="item.id" :value="item.id" :label="item.routeName" /> </el-select> </div> <span> <span class="title">当前速度: </span> <span class="text">{{ currentDevice.speed }}</span> </span> </div> </div> <div class="box-bottom" style="display: flex;justify-content: space-around;"> <el-button type="primary" @click="control('a/')"> 开始巡检 </el-button> </div> </div> </div> <div class="box-middle"> <table-container title="巡检情况" style="margin-top: 0px !important;"> <normal-table :data="list" :total="total" :pagination="true" :columns="columns" :query="listQuery" :height="500" :list-loading="listLoading" @change="changePage" > <template #preColumns> <el-table-column label="序号" width="55" align="center"> <template #default="scope"> {{ (listQuery.offset - 1) * listQuery.limit + scope.$index + 1 }} </template> </el-table-column> </template> <template #columns> <el-table-column label="图片" width="160" align="center" fixed="right"> <template #default="scope"> <el-image v-if="scope.row.picture !== ''" style="width: 100px; height: 100px" :src="scope.row.picture" :preview-src-list="[scope.row.picture]" /> <span v-else>/</span> </template> </el-table-column> <!-- <el-table-column label="操作" width="160" align="center" fixed="right"> <template #default="scope"> <el-button type="primary" link size="small" class="table-text-button" @click="handleEdit(scope.row)" > 详情 </el-button> </template> </el-table-column> --> </template> </normal-table> </table-container> </div> <div class="box box-bottom"> <div class="title"> <span>设备控制</span> <div> <el-button type="danger" @click="control('s/')"> 急停 </el-button> <el-button type="primary" @click="control('d/')"> 恢复 </el-button> </div> </div> <!-- <div style="margin-top: 10px;display: flex;justify-content: space-around;"> <div style="position: relative;"> <div class="control-title"> 机器狗 </div> <div name="four" style="width: 150px; height: 130px;"> <el-image style="width: 130px; height: 130px;position: absolute;top: 5px;left: 50px;" :src="controlImg" fit="fill"/> <el-image style="width: 200px; height: 200px;position: absolute;top: 5px;left: 200px;" :src="dog" fit="fill" /> <div> <div class="round-btn" style="top: 8px;left: 95px;" @mousedown="controlWithSpeed($event, 'up')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 90px;left: 95px;" @mousedown="controlWithSpeed($event, 'down')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 48px;left: 53px;" @mousedown="controlWithSpeed($event, 'left')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 48px;left: 135px;" @mousedown="controlWithSpeed($event, 'right')" @mouseup="controlWithSpeed($event, 'stop')" /> </div> </div> </div> <div style="position: relative;"> <div class="control-title" style="background-color: #a43bd1;"> 机械臂 </div> <div name="four" style="width: 320px; height: 130px;"> <img style="width: 130px; height: 130px;position: absolute;top: 5px;left: 50px;" :src="controlImg" fit="fill"> <img style="width: 200px; height: 200px;position: absolute;top: 5px;left: 200px;" :src="dogArm" fit="fill"> <div> <div class="round-btn" style="top: 8px;left: 95px;" @mousedown="controlWithSpeed($event, 'up')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 90px;left: 95px;" @mousedown="controlWithSpeed($event, 'down')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 48px;left: 53px;" @mousedown="controlWithSpeed($event, 'left')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 48px;left: 135px;" @mousedown="controlWithSpeed($event, 'right')" @mouseup="controlWithSpeed($event, 'stop')" /> </div> </div> </div> </div> --> <!-- 实时视频模块 --> <div style="display: flex;justify-content: space-around;overflow: auto;"> <div v-loading="dogVideoLoading"> <h5 class="inspection-title">巡检精灵</h5> <video id="dog-video-container" autoplay width="640" height="360" controls style="background-color: #000;" /> </div> <div v-loading="armVideoLoading" style="margin-left: 20px;"> <h5 class="inspection-title">机械臂</h5> <video id="arm-video-container" autoplay width="640" height="360" controls style="background-color: #000;" /> </div> </div> </div> </div> </app-container> </template> <style scoped lang="scss"> .patrol-manage { display: flex; flex-direction: column; // gap: 10px; } .box { width: 100%; background-color: #fff; padding: 10px; border-radius: 7px; } .box-top { display: grid; /* 将容器划分为两列,左边占 2 份,右边占 1 份 */ grid-template-columns: 5fr 0.05fr 1.3fr; /* 设置列之间的间距为 20 像素 */ gap: 20px; .box-top-item { display: flex; flex-direction: column; // justify-content: center; // align-items: center; .box-title { font-size: 14px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; .title { display: flex; justify-content: space-between; align-items: center; } } .box-main { display:flex; flex-direction: column; height: 100%; // flex-direction: column; justify-content: space-around; align-items: center; .box-device { display: flex; flex-direction: column; line-height: 29px; color: rgba(0,0,0,1); font-size: 14px; text-align: left; font-family: SourceHanSansSC-regular; .text { font-weight: 600; } .title { margin-right: 12px; } } } } } .box-bottom { display: flex; flex-direction: column; margin-top: 10px; .title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .control-title { color: white; background-color: #ff9f11; border-bottom-right-radius: 10px; border-top-right-radius: 10px; line-height: 30px; font-weight: bold; text-align: center; letter-spacing: 1px; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif; writing-mode: vertical-rl; text-orientation: upright; width: 30px; height: 100px; } .round-btn { position: absolute; top: 14px; left: 129px; width: 42px; height: 42px; z-index: 111111; border-radius: 21px; cursor: pointer; &:hover { background-color: rgb(61 125 254 / 53%); } } } .image-points-container { position: relative; display: inline-block; } canvas { position: absolute; top: 0; left: 0; pointer-events: none; } .hover-point { position: absolute; width: 20px; height: 20px; border-radius: 50%; background-color: rgba(0, 30, 66, 0.5); pointer-events: none; } .inspection-title { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 20px; font-weight: bold; text-align: center; color: #fff; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); background: linear-gradient(to right, #6a11cb, #2575fc); padding: 6px 10px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); margin: 20px auto; width: fit-content; transition: all 0.3s ease; &:hover { transform: scale(1.1); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); } } </style> <style lang="scss"> .patrol-manage { // 单元格样式 .el-table__cell { position: static !important; // 解决el-image 和 el-table冲突层级冲突问题 z-index: 99999999; } /* 隐藏默认的悬浮提示内容 */ .el-tooltip__content { display: none; } .el-tooltip.is-open .el-tooltip__content { display: block; } } </style>