<script lang="ts" setup name="Map"> import { ElMessage } from 'element-plus' import type { Ref } from 'vue' import { getCurrentInstance, ref } from 'vue' import dayjs from 'dayjs' import L from 'leaflet' import 'leaflet/dist/leaflet.css' // import aMap from '@/components/aMap/aMap.vue' import { getDataHisList, getDevInfo, getDevMap } from '@/api/ptz/dev' import useWebsocketStore from '@/store/modules/websocket' import type { lineDataI } from '@/components/Echart/echart-interface' import { videoTree } from '@/api/ycjg/aqbb' import 'leaflet.label' import 'proj4' import 'proj4leaflet' import { getDevListPage } from '@/api/ycjg/dev' import './L.TileLayer.NoGap.js' const { proxy } = getCurrentInstance() as any const treeRef = ref() const toParentsMap = ref({ lat: '', lng: '', }) const filterText = ref('') const defaultProps = ref({ children: 'children', label: 'name', isDisabled: 'disabled', }) const treeData = ref([]) as any // 树数据 const map: any = ref(null) const baseLayer = ref([]) const keyword = ref('') const isRightShow = ref(false) let treeClickCount = 0 const popupOpen = ref(false) // 筛选树节点 function filterNode(value: any, data: { name: string | any[] }) { if (value === '' || value === null) { return true } return data.name.includes(value) } /** * 制作icon以及popup的显示内容 * @param deviceStatus 设备状态 * @param monitorName * @param deviceStatusName 设备状态名称 * @param secretLevelName 密级 * @param location 详细位置 */ const makePopupMessage = ( deviceStatus: number, monitorName = '', deviceStatusName = '', secretLevelName = '', location = '', ) => { let icon let color switch (deviceStatus) { case 0: icon = './marker/gray.png' color = 'gray' break case 1: icon = './marker/green.png' color = 'green' break } const message = `<div style="padding: 10px;width: 230px;font-size: 16px;"> <div style="font-weight: bold;margin-bottom: 10px">${monitorName || ''}</div> <div style="color: ` + `${color}` + `"><span style="color: black">设备状态:</span>${deviceStatusName || ''}</div> <div style="word-break: break-all; "><span>密级:</span>${secretLevelName || ''}</div> <div style="word-break: break-all; "><span>详细位置:</span>${location || ''}</div> </div>` return { message, icon, } } const currentNode = ref(null) // 显示marker信息 function handleClosePopup() { console.log('关闭popup') map.value.closePopup() } // 双击树节点 function handleNodeClick(data: any, node: any, self: any) { console.log('获取设备信息', data) currentNode.value = data const now = new Date().getTime() if (now - treeClickCount < 300) { // 双击事件的判断,300毫秒内重复点击 if (data.children.length !== 0) { // 点击父亲 return } if (data.pid === 0) { return } if (data && data.device && data.device.latitude && data.device.longitude) { const { message } = makePopupMessage(data.device.deviceStatus, data.device.monitorName, data.device.deviceStatusName, data.device.secretLevelName, data.device.location) map.value.panTo([data.device.latitude, data.device.longitude]) const popup = L.popup({ closeButton: false }).setLatLng([data.device.latitude, data.device.longitude]).setContent(message) map.value.openPopup(popup) popupOpen.value = true map.value.setView([data.device.latitude, data.device.longitude], 18) } else { ElMessage.warning('未获取到此设备的经纬度,请在设备管理中查看此设备的详细信息') } } treeClickCount = now } // 查询设备列表并往地图上添加marker function search(init = true) { map.value.eachLayer((layer: any) => { if (layer !== baseLayer.value[0]) { layer.remove() } }) getDevListPage({ keyword: keyword.value, devType: '', offset: 1, limit: 1000, }).then((res) => { res.data.rows.forEach((item: any) => { const { message, icon } = makePopupMessage(item.deviceStatus, item.monitorName, item.deviceStatusName, item.secretLevelName, item.location) const popup = L.popup().setContent(message) const divIcon = L.divIcon({ html: `<div> <image src="${icon}" width="30" height="30" style="margin-bottom: -3px"> <div style="margin-left: calc(-50% - 5px);border-radius:5px;display: inline-block;white-space: nowrap;background: rgb(255,255,255);text-align: left;height: 20px;padding: 0 4px">${item.monitorName || ''}</div> </div>`, className: 'my-div-icon', iconSize: [30, 30], }) const marker = L.marker([item.latitude, item.longitude], { icon: divIcon }).addTo(map.value) marker.bindPopup(popup, { closeButton: false }) popup.on('close', () => { console.log('监听到popup关闭') popupOpen.value = false }) marker.on('click', () => { if (popupOpen.value) { // 如果弹窗已经打开,再次点击标记将关闭弹窗 marker.closePopup() popupOpen.value = false } else { // 首次点击将打开弹窗 marker.openPopup() popupOpen.value = true } }) }) if (init && res && res.data && res.data.rows) { if (!res.data.rows.length) { ElMessage.warning('查无结果') } else { let lat = proxy.$position.lat let lng = proxy.$position.lng console.log('执行定位, 默认北京', lat, lng) for (let i = 0; i < res.data.rows.length; i++) { if (res.data.rows[i].latitude && res.data.rows[i].longitude) { lat = res.data.rows[i].latitude lng = res.data.rows[i].longitude console.log('找到第一个有明确定位的设备:lat, lng', lat, lng) break } } map.value.setView([lat, lng]) } } if (currentNode.value) { handleNodeClick(currentNode.value, null, null) } }) } /** * Proj框架:坐标系转换 * EPSG:900913是一种投影坐标系 */ L.CRS.Baidu = new L.Proj.CRS( 'EPSG:900913', '+proj=merc +a=6378206 +b=6356584.314245179 +lat_ts=0.0 +lon_0=0.0 +x_0=0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs', { resolutions: (function () { const level = 19 var res = [] res[0] = 2 ** 18 // 2的18次幂 for (var i = 1; i < level; i++) { res[i] = 2 ** (18 - i) } return res })(), origin: [0, 0], // L.bounds 返回数组下标下界 bounds: L.bounds([20037508.342789244, 0], [0, 20037508.342789244]), }) onBeforeMount(() => { toParentsMap.value.lat = proxy.$position.lat toParentsMap.value.lng = proxy.$position.lng }) // 处理树的数据 function solveData(data: any) { data.forEach((item: any) => { if (item.device) { item.name = `${item.name} (${item.device.deviceStatusName})` console.log('修改后的name', item.name) } if (item.children && item.children.length) { solveData(item.children) } }) return data } onMounted(() => { videoTree().then((response) => { if (response.code === 200) { treeData.value = response.data // treeData.value = solveData(treeData.value) } }) map.value = L.map('map', { minZoom: 5, maxZoom: 18, center: [39.91459528, 116.26499505], zoom: window.localStorage.getItem('zoom') ? Number(window.localStorage.getItem('zoom')) : 15, zoomControl: false, attributionControl: false, crs: L.CRS.Baidu, }) const str = '/static/tiles/{z}/{x}/{y}.png' baseLayer.value.push(L.tileLayer(str, { tms: true, noWrap: true, }).addTo(map.value)) search(false) // 当地图缩放结束后更新标记点位置 map.value.on('zoomend', () => { console.log('监听缩放结束') search(false) }) }) const unwatch = watch(filterText, (newVal) => { treeRef.value.filter(newVal) }) onBeforeUnmount(() => { unwatch() }) </script> <template> <div id="map" style="height: calc(100vh - 110px);" /> <!-- <div class="input"> <el-input v-model="keyword" type="text" placeholder="设备名称关键字" clearable style="width: 200px;" /> <el-button type="primary" style="margin-left: 5px;" @click="search"> 查询 </el-button> </div> --> <el-card class="tree-area"> <el-input v-model="filterText" placeholder="设备名称过滤" style="width: 240px;" /> <el-tree ref="treeRef" class="filter-tree" style="width: 100%;height: 100%;" :data="treeData" :filter-node-method="filterNode" node-key="id" :default-expand-all="true" :props="defaultProps" @node-click="handleNodeClick" > <template #default="{ node, data }"> <span style="display: flex;align-items: center;"> <el-icon v-if="data.device.deviceStatusName === '在线'" style="margin-right: 5px;"> <svg-icon name="icon-online" /> </el-icon> <el-icon v-if="data.device.deviceStatusName === '离线'" style="margin-right: 5px;"> <svg-icon name="icon-offline" /> </el-icon> <el-tooltip class="box-item" effect="dark" :content="node.label" placement="right" > <template #content> <span>{{ node.label }}</span> <span v-if="data.device.deviceTypeName">({{ data.device.deviceTypeName }})</span> </template> <span v-if="data.device.deviceTypeName" :style="{ 'color': data.device.deviceStatusName === '在线' ? '#0e932e' : '#606266', 'font-weight': data.device.deviceStatusName === '在线' ? 600 : 500 }">{{ node.label }}</span> <span v-else>{{ node.label }}</span> </el-tooltip> <el-icon v-if="data.device.deviceType === '0'" style="margin-left: 5px;"> <svg-icon name="icon-qiang" :color="data.device.deviceStatusName === '在线' ? '#0e932e' : '#979797'" /> </el-icon> <el-icon v-if="data.device.deviceType === '1' || data.device.deviceType === '2'" style="margin-left: 5px;"> <svg-icon name="icon-ball" :color="data.device.deviceStatusName === '在线' ? '#0e932e' : '#979797'" /> </el-icon> <el-icon v-if="data.device.deviceType === '3'" style="margin-left: 5px;"> <svg-icon name="icon-ball" :color="data.device.deviceStatusName === '在线' ? '#0e932e' : '#979797'" /> </el-icon> <el-icon v-if="data.device.deviceType === '4'" style="margin-left: 5px;"> <svg-icon name="icon-ytqiang" :color="data.device.deviceStatusName === '在线' ? '#0e932e' : '#979797'" /> </el-icon> </span> </template> </el-tree> </el-card> <div class="cover"> <div class="green" /> <div class="gray" /> <div class="item"> 正常 </div> <div class="item"> 离线 </div> </div> <div v-if="isRightShow" class="right" /> </template> <style lang="scss"> .leaflet-popup { margin-bottom: 35px !important; } </style> <style lang="scss" scoped> .map { width: 100%; height: 100%; padding: 0; margin: 0; } .right { z-index: 1000 !important; position: absolute; right: 20px; top: 10px; width: 350px; height: calc(100vh - var(--g-topbar-height) - var(--g-header-height) - 20px); background: #faefe0; border-radius: 10px; padding: 20px 0; .devName { padding-left: 10px; margin-bottom: 10px; font-size: 19px; font-weight: bold; } .title { font-size: 17px; font-weight: bold; margin: 10px 0; color: #464646; } .close { width: 20px; height: 20px; position: absolute; right: 10px; top: 20px; } } .input { position: absolute; top: 20px; left: 20px; width: 280px; height: 34px; display: flex; z-index: 1001; } .tree-area { position: absolute; top: 10px; left: 10px; width: 280px; // height: 200px; height: calc(100% - 160px); display: flex; z-index: 1000; overflow: auto; } .cover { position: absolute; bottom: 25px; left: 5px; width: 120px; height: 120px; border-radius: 10px; background: #faefe0; z-index: 1111; display: flex; flex-flow: column wrap; justify-content: space-evenly; align-items: center; .item { width: 50px; height: 45px; padding-top: 10px; font-weight: bold; color: #5f5f5f; } .green { background: url("../../assets/images/marker/green.png") no-repeat center / cover; width: 45px; height: 45px; } .red { background: url("../../assets/images/marker/red.png") no-repeat center / cover; width: 45px; height: 45px; } .gray { background: url("../../assets/images/marker/gray.png") no-repeat center / cover; width: 45px; height: 45px; } } </style> <style> .my-div-icon { color: black; font-size: 14px; text-align: center; pointer-events: none; /* Prevent mouse events from firing */ } </style>