<!-- Description: 诺成新监测桩画线工具 Author: 李亚光 Date: 2025-07-09 --> <script lang="ts" setup name="MapLineTool"> import AMap from '@/components/map/index.vue' import { getDeviceListPage } from '@/api/home/device/device' import { getNcxPositionList, addNcxPosition, editNcxPosition, deleteNcxPosition } from '@/api/home/tool/index' import { dayjs, ElMessage, ElMessageBox } from 'element-plus' const publicPath = window.location.href.split('#')[0] const mapContainer = ref() const loading = ref(false) const mapRef = ref() const polyEditor = ref() const isComplete = ref(false) // 点位列表 const deviceList = ref([]) // 线条数据 const lineList = ref([]) // 当前画线设备 const currentDevice = ref('') const currentCode = ref('') // 当前线条 const currentLine = ref() // 当前线条烈性 const lineType = ref('') // 操作类型 const handlerType = ref('add') // 右键菜单 const menuVisible = ref(false) const menuX = ref(0) const menuY = ref(0) const completeMap = () => { console.log('地图加载完成') isComplete.value = true if (deviceList.value.length) { // 绘制点位 mapRef.value.addMassMarks({ path: deviceList.value.map((item: any) => ({ lnglat: [item.lng, item.lat], name: item.devcode, style: 0, id: item.id, row: item })), zIndex: 112, zooms: [0, 100], style: [{ url: `${publicPath}/image/pipeline/warning-normal.png` }], size1: 25, size2: 25 }) } // 绘制线条 if (lineList.value.length) { lineList.value.forEach((item: any) => { drawLine(item) }) } mapRef.value.map.on('zoomchange', () => { if (menuVisible.value) { menuVisible.value = false handlerType.value = '' currentCode.value = '' currentLine.value = '' currentDevice.value = '' lineType.value = '' } }) mapRef.value.map.on('dragstart', () => { if (menuVisible.value) { menuVisible.value = false handlerType.value = '' currentCode.value = '' currentLine.value = '' currentDevice.value = '' lineType.value = '' } }) } // 获取诺成新设备 const fetchDevice = () => { getDeviceListPage({ manufacturerId: '4', devTypeIds: '11', offset: 1, limit: 9999 }).then(res => { console.log(res.data, '诺成新设备数据') deviceList.value = res.data.rows.filter( (item: any) => item.lat && item.lng && item.id && item.devcode ) if (isComplete.value) { // 绘制点位 mapRef.value.addMassMarks({ path: deviceList.value.map((item: any) => ({ lnglat: [item.lng, item.lat], name: item.devcode, style: 0, id: item.id, row: item })), zIndex: 112, zooms: [0, 100], style: [{ url: `${publicPath}/image/pipeline/warning-normal.png` }], size: 25 }) } }) } const drawLine = (data: any) => { const lng = data.lngGd.split(',') const lat = data.latGd.split(',') if (!lng.length || !lat.length || lng.length !== lat.length) { return } const polyline = new mapRef.value.AMap.Polyline({ path: lng.map((item: string, index: number) => [ Number(item), Number(lat[index]) ]), strokeColor: '#FF33FF', strokeWeight: 2, strokeOpacity: 0.9, zIndex: 199, bubble: true, extData: { deviceId: data.deviceId, deviceCode: data.deviceCode, type: data.type } }) mapRef.value.map.add([polyline]) polyline.on('rightclick', e => { if (polyEditor.value) { ElMessage.warning('正在编辑中,请先完成编辑') return } // 弹出菜单 // 获取地图容器偏移量 const containerRect = mapContainer.value.getBoundingClientRect() // 将地图坐标转换为页面像素坐标 const pixel = mapRef.value.map.lngLatToPixel(e.lnglat) // 计算菜单位置(考虑容器偏移) // menuX.value = containerRect.left + 300 + 35 // menuY.value = containerRect.top // 显示菜单 setTimeout(() => { menuVisible.value = true }, 100) handlerType.value = 'edit' const data = e.target.getExtData() currentCode.value = data.deviceCode currentLine.value = e.target currentDevice.value = data.deviceId lineType.value = data.type }) return polyline } // 获取设备指示带数据 const fetchLine = (reset = false) => { // 需要重置 if (reset) { // 清空地图线条 loading.value = true } getNcxPositionList({}) .then(res => { loading.value = false lineList.value = res.data.filter( (item: any) => item.deviceCode && item.deviceId && item.latGd && item.lngGd && item.type ) if (reset) { // 获取地图实例中的所有覆盖物 const overlays = mapRef.value.map.getAllOverlays() overlays.forEach(overlay => { if (overlay.type === 'AMap.Overlay') { mapRef.value.map.remove(overlay) } }) } // 绘制线条 if (isComplete.value) { lineList.value.forEach((item: any) => { drawLine(item) }) } }) .catch(() => { loading.value = false }) } fetchLine() fetchDevice() // 点击设备点位 const massMarksClick = (data: any) => { if (polyEditor.value) { ElMessage.warning('正在编辑中,请先完成编辑') return } if (menuVisible.value) { menuVisible.value = false handlerType.value = '' currentCode.value = '' currentLine.value = '' currentDevice.value = '' lineType.value = '' // return } currentDevice.value = data.event.data.id currentCode.value = data.event.data.row.devcode const line = lineList.value.filter( (item: any) => item.deviceId === currentDevice.value ) if (line.length === 2) { currentDevice.value = '' currentCode.value = '' ElMessage.warning('该设备已有两条线段,请勿重复添加') return } const line1 = lineList.value.filter( (item: any) => item.deviceId === currentDevice.value ) lineType.value = line1.length ? (line1[0].type === '1' ? '2' : '1') : '1' const polyline = drawLine({ deviceCode: currentCode.value, deviceId: currentDevice.value, latGd: `${data.event.data.row.lat},${data.event.data.row.lat}`, lngGd: `${data.event.data.row.lng},${ Number(data.event.data.row.lng) + (lineType.value === '1' ? 0.001 : -0.001) }`, type: lineType.value }) currentLine.value = polyline handlerType.value = 'add' mapRef.value.map.add([polyline]) polyEditor.value = new mapRef.value.AMap.PolylineEditor( mapRef.value.map, polyline ) polyEditor.value.setTarget(polyline) polyEditor.value.open() } // 取消线 const cancelLine = () => { if (!currentDevice.value || !currentLine.value) { return } polyEditor.value.close() currentLine.value.setMap(null) polyEditor.value = '' console.log(handlerType.value, 'handlerType.value') if (handlerType.value === 'edit') { const line = lineList.value.find( (item: any) => item.deviceId === currentDevice.value && item.type === item.type ) console.log(line) drawLine(line) } // 清空 setTimeout(() => { currentDevice.value = '' currentLine.value = '' currentCode.value = '' lineType.value = '' handlerType.value = '' }) ElMessage.success('操作成功') } const contextmenuFun = event => { event.preventDefault() // 获取鼠标相对于视口的位置 menuX.value = event.clientX - 180 menuY.value = event.clientY - 50 - 60 if (event.clientX + 120 >= document.documentElement.clientWidth) { menuX.value = document.documentElement.clientWidth - 180 - 120 } if (event.clientY + 82 >= document.documentElement.clientHeight) { menuY.value = document.documentElement.clientHeight - 50 - 60 - 82 } } // 保存线 const saveLine = () => { if (!currentDevice.value || !currentLine.value) { return } const path = currentLine.value.getPath().map((item: any) => ({ lng: item.lng, lat: item.lat })) // 判断是编辑还是新建 if (handlerType.value === 'add') { addNcxPosition({ deviceCode: currentCode.value, deviceId: currentDevice.value, type: lineType.value, latGd: path.map((item: any) => item.lat).join(), lngGd: path.map((item: any) => item.lng).join(), ts: dayjs().format('YYYY-MM-DD HH:mm:ss') }) .then(() => { ElMessage.success('保存成功') polyEditor.value.close() polyEditor.value = '' currentCode.value = '' currentDevice.value = '' currentLine.value = '' lineType.value = '' handlerType.value = '' // 重新获取线 fetchLine(true) }) .catch(() => { polyEditor.value.close() polyEditor.value = '' currentLine.value.setMap(null) }) } else if (handlerType.value === 'edit') { const line = lineList.value.find( (item: any) => item.deviceId === currentDevice.value && item.type === item.type ) if (line) { editNcxPosition({ ...line, latGd: path.map((item: any) => item.lat).join(), lngGd: path.map((item: any) => item.lng).join(), ts: dayjs().format('YYYY-MM-DD HH:mm:ss') }) .then(res => { ElMessage.success('保存成功') polyEditor.value.close() polyEditor.value = '' currentCode.value = '' currentDevice.value = '' currentLine.value = '' lineType.value = '' handlerType.value = '' // 重新获取线 fetchLine(true) }) .catch(() => { polyEditor.value.close() polyEditor.value = '' currentLine.value.setMap(null) }) } } } // 删除线 const deleteLine = () => { menuVisible.value = false ElMessageBox.confirm('确定要删除该折线吗?', '确认操作', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) .then(res => { const line = lineList.value.find( (item: any) => item.deviceId === currentDevice.value && item.type === item.type ) if (line) { deleteNcxPosition({ ids: [line.id], keys: [] }) .then(res => { ElMessage.success('删除成功') currentLine.value.setMap(null) currentCode.value = '' currentDevice.value = '' currentLine.value = '' lineType.value = '' handlerType.value = '' lineList.value = lineList.value.filter( (item: any) => item.deviceId === line.deviceId && item.type === line.type ) }) .catch(() => { handlerType.value = '' currentCode.value = '' currentLine.value = '' currentDevice.value = '' lineType.value = '' }) } else { handlerType.value = '' currentCode.value = '' currentLine.value = '' currentDevice.value = '' lineType.value = '' } }) .catch(() => { currentCode.value = '' currentDevice.value = '' currentLine.value = '' lineType.value = '' handlerType.value = '' }) } // 编辑折线 const editLine = () => { menuVisible.value = false if (polyEditor.value) { ElMessage.warning('正在编辑中,请先完成编辑') return } polyEditor.value = new mapRef.value.AMap.PolylineEditor( mapRef.value.map, currentLine.value ) polyEditor.value.setTarget(currentLine.value) polyEditor.value.open() } const detail = ref() // 弹窗实例 // 鼠标移入设备点位 const massMarksOver = (data: any) => { const target = data.event.data // 定义弹窗 detail.value = new mapRef.value.AMap.InfoWindow({ closeWhenClickMap: true, // 是否在鼠标点击地图后关闭信息窗体 autoMove: true, // 是否自动调整窗体到视野内 isCustom: true, // 自定义信息窗体 content: `<div style="background: #fff;padding:4px 6px 4px 6px;border-radius: 4px;font-weight: 400;color: #333">${target.row.devcode}</div>`, // 窗体内容(vue组件) offset: new mapRef.value.AMap.Pixel(75, 0) // 偏移 }) // 打开信息窗口 detail.value.open(data.map, data.event.data.lnglat) } // 鼠标移出设备点位 const massMarksOut = () => { if (detail.value) { detail.value.close() } } const calcHeight = () => { const height = document.body.clientHeight - 60 - 50 const container = document.getElementById('map') if (container) { container.style.height = `${height}px` } } window.addEventListener('resize', () => { calcHeight() }) onBeforeUnmount(() => { window.addEventListener('resize', () => {}) }) onMounted(() => { calcHeight() }) </script> <template> <div id="map-line-tool" ref="mapContainer" style="height: calc(100vh - 60px - 50px); position: relative" @contextmenu="contextmenuFun" > <!-- 按钮 --> <div class="btn-container"> <el-button type="primary" :disabled="!currentDevice || menuVisible" @click="saveLine" >保存</el-button > <el-button :disabled="!currentDevice || menuVisible" @click="cancelLine" >{{ handlerType === 'edit' ? '取消' : '删除'}}</el-button > <el-tooltip placement="bottom"> <template #content> <div class="tooltip-content"> <div>鼠标点击设备点位可开始画线</div> <div>鼠标右击折线可编辑/删除折线</div> </div> </template> <el-icon class="ml-2 icon" style="color: #909399; cursor: pointer"> <QuestionFilled /> </el-icon> </el-tooltip> </div> <!-- 右键菜单 --> <div v-show="menuVisible" class="context-menu" :style="{ left: `${menuX}px`, top: `${menuY}px` }" > <div @click="editLine">编辑折线</div> <div @click="deleteLine">删除折线</div> </div> <!-- 地图 --> <a-map ref="mapRef" :show-pieple-layer="true" @complete="completeMap" @massMarksClick="massMarksClick" @massMarksOver="massMarksOver" @massMarksOut="massMarksOut" /> <!-- 加载中遮罩 --> <div v-if="loading" style=" height: calc(100vh - 60px - 50px); position: absolute; z-index: 9999; width: 100%; top: 0; left: 0; cursor: not-allowed; " ></div> </div> </template> <style lang="scss" scoped> .btn-container { position: absolute; top: 5px; left: 5px; z-index: 99; } .btn-container { display: flex; align-items: center; } .icon { margin-left: 10px; } .context-menu { position: absolute; z-index: 1000; background: white; border-radius: 4px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 8px 0; min-width: 120px; } .context-menu > div { padding: 6px 16px; cursor: pointer; } .context-menu > div:hover { background-color: #f1f1f1; } </style>