Newer
Older
smartwell_front / src / views / home / tool / index.vue
<!--
  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>