index.vue 11.3 KB
<template>
  <div class="chart-amap" ref="vChartRef">
    <device-latest-table ref="deviceLatestTableRef"></device-latest-table>
  </div>
</template>

<script setup lang="ts">
import { ref, PropType, toRefs, watch, render, h, onMounted, onUnmounted, reactive } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader'
import { CreateComponentType } from '@/packages/index.d'
import { useChartDataFetch } from '@/hooks'
import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
import { MarkerEnum, ThemeEnum, dataExtraInfoType, dataJsonType, dataJsonMarkersType, devicePartInfoInterface } from './config'
import { isArray } from '@/utils'
import inactive from './images/inactive.png'
import online from './images/online.png'
import offline from './images/offline.png'
import listView from './images/list.png'
import { getDeviceActiveTime } from '@/api/external/common/index'
import dayjs from 'dayjs'
import DeviceLatestTable from './components/DeviceLatestTable.vue'
import { getDeviceLatest, getProfileAttrs } from '@/api/external/common'
import { NButton } from 'naive-ui'

const props = defineProps({
  chartConfig: {
    type: Object as PropType<CreateComponentType>,
    required: true
  }
})

const deviceLatestTableRef = ref<Nullable<InstanceType<typeof DeviceLatestTable>>>(null);

const {
  amapKey,
  amapStyleKey,
  amapLon,
  amapLat,
  amapZindex,
  mapMarkerType,
  lang,
  amapStyleKeyCustom,
  features,
  viewMode,
  pitch,
  skyColor,
  marker,
  iconMarker,
  mpBorderConfig,
  bgColor
} = toRefs(props.chartConfig.option.mapOptions)

//官方没有高德地图api的ts,所以类型全用的any
let mapIns: any = null
let markers: any = []
let AMapIns: any = null
const vChartRef = ref<HTMLElement>()

const initMap = (newData: any) => {
  // 初始化
  AMapLoader.load({
    key: amapKey.value, //api服务key--另外需要在public中使用安全密钥!!!
    version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
    plugins: ['AMap.PlaceSearch', 'AMap.AutoComplete'] // 需要使用的的插件列表
  })
    .then(AMap => {
      AMapIns = AMap
      mapIns = new AMap.Map(vChartRef.value, {
        resizeEnable: true,
        zoom: amapZindex.value, // 地图显示的缩放级别
        center: [amapLon.value, amapLat.value],
        lang: lang.value,
        features: features.value,
        pitch: pitch.value, // 地图俯仰角度,有效范围 0 度- 83 度
        skyColor: skyColor.value,
        viewMode: viewMode.value, // 地图模式
        willReadFrequently: true,
        WebGLParams: {
          preserveDrawingBuffer: true,
        }
      })
      dataHandle(props.chartConfig.option.dataset) //处理地图标点
      let satellite = new AMap.TileLayer.Satellite()
      let roadNet = new AMap.TileLayer.RoadNet()
      if (newData.amapStyleKey === ThemeEnum.WEIXIN) {
        mapIns.add([satellite, roadNet])
      } else {
        mapIns.remove([satellite, roadNet])
        mapIns.setMapStyle(
          `amap://styles/${amapStyleKeyCustom.value !== '' ? amapStyleKeyCustom.value : amapStyleKey.value}`
        )
      }
      mapIns.on('click', () => {
        mapIns.clearInfoWindow()
      })
    })
    .catch(e => {
      console.error(e)
    })
}


//携带设备的tbDeviceId和别名或者名称
const devicePartInfo = reactive<devicePartInfoInterface>({
  tbDeviceId: '',
  name: '',
  alias: '',
  deviceProfileId: '',
})

//创建信息弹窗
const createInfoWindow = async (extraInfo: dataExtraInfoType) => {
  try {
    const { name, alias, organizationDTO, deviceState, deviceProfile, deviceInfo, tbDeviceId, deviceProfileId } = extraInfo
    devicePartInfo.tbDeviceId = tbDeviceId
    devicePartInfo.alias = alias
    devicePartInfo.name = name
    devicePartInfo.deviceProfileId = deviceProfileId
    if (tbDeviceId.startsWith('@')) return //假的模拟数据则终止
    const res = await getDeviceActiveTime(tbDeviceId) //查询设备最后离线时间
    let { lastUpdateTs } = res[0]
    const lastUpdateFormatTs = dayjs(lastUpdateTs).format('YYYY-MM-DD HH:mm:ss')
    //以render方式渲染小组件里的边框组件
    const BorderInstance = await import(`../../../../Decorates/Borders/${mpBorderConfig.value.value}/index.vue`)
    const config = await import(`../../../../Decorates/Borders/${mpBorderConfig.value.value}/config.ts`)
    const BorderConfigInstance = new config.default()
    const id = Date.now().toString()
    setTimeout(() => {
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      render(h(BorderInstance.default, { chartConfig: BorderConfigInstance }), document.getElementById(id)!)
    }, 100)
    //
    const textOverflow = `width:16rem;text-overflow:ellipsis;overflow:hidden;word-break:break-all;white-space:nowrap;`
    const textOverflowFontBold = `width:12rem;text-overflow:ellipsis;overflow:hidden;word-break:break-all;white-space:nowrap;font-size:15px;font-weight:bold;`
    const deviceStateContainer = `display:flex;justify-content:space-between;align-items:center;`
    const deviceStateImg = `width:1.2rem;height:1.2rem;`
    const deviceStateText = `margin-left:0.6rem;font-weight:bold;`
    return `
    <div id="${id}" style="width:30rem;">
      <div style="display:flex;flex-direction:column;margin:3.5rem 5.5rem 2rem 6.5rem;position:relative;">
        <div style="display:flex;justify-content:space-between;align-items:center;color:white;">
          <span style="${textOverflowFontBold}">${alias || name}</span>
          ${deviceState === 'INACTIVE'
        ? `<div style="${deviceStateContainer}">
                <img onclick="handleOpenDrawer()" title="最新数据" style="${deviceStateImg};cursor:pointer;" src="${listView}"/>
                <img style="${deviceStateImg};margin-left:0.3rem" src="${inactive}"/>
                <span style="${deviceStateText}">待激活</span>
               </div>`
        : deviceState === 'ONLINE'
          ? `<div style="${deviceStateContainer}">
                <img onclick="handleOpenDrawer()" title="最新数据"  style="${deviceStateImg};cursor:pointer;" src="${listView}"/>
                <img style="${deviceStateImg};margin-left:0.3rem" src="${online}"/>
                <span style="${deviceStateText}">在线</span>
               </div>`
          : `<div style="${deviceStateContainer}">
                <img onclick="handleOpenDrawer()" title="最新数据"  style="${deviceStateImg};cursor:pointer;" src="${listView}"/>
                <img style="${deviceStateImg};margin-left:0.3rem" src="${offline}"/>
                <span style="${deviceStateText}">离线</span>
               </div>`
      }
        </div>
        <div style="display:flex;flex-direction:column;justify-content:space-between;color:white;margin-top:1rem;gap:0.95rem;">
          <div style="${textOverflow}">所属组织:${organizationDTO.name}</div>
          <div style="${textOverflow}">接入协议:${deviceProfile.transportType}</div>
          <div style="${textOverflow}">设备位置:${!deviceInfo.address ? '该设备暂无地理位置' : deviceInfo.address}</div>
          <div style="${textOverflow}">${deviceState === 'ONLINE' ? '在线' : deviceState === 'INACTIVE' ? '创建' : '离线'}时间:${lastUpdateFormatTs}</div>
        </div>
      </div>
    </div>
    `
  } catch (e) {
    console.error(e)
  }
}

const handleOpenDrawer = async () => {
  deviceLatestTableRef.value?.openDrawer()
  deviceLatestTableRef.value?.setDrawerTitle(devicePartInfo.alias || devicePartInfo.name)
  if (!devicePartInfo.tbDeviceId) return
  const resp = await getDeviceLatest(devicePartInfo.tbDeviceId)
  const respGetAttrs = await getProfileAttrs({ deviceProfileId: devicePartInfo.deviceProfileId })
  deviceLatestTableRef.value?.setValue(resp, respGetAttrs)
}

onMounted(() => {
  (window as any).handleOpenDrawer = handleOpenDrawer;
});

onUnmounted(() => {
  (window as any).handleOpenDrawer = null;
});

//地图鼠标hover
const mapClick = (markerInstance: any, markerItem: dataJsonMarkersType) => {
  markerInstance.setExtData({
    extraInfo: markerItem.extraInfo
  })
  // mouseover
  markerInstance.on('click', async (e: any) => {
    const { extraInfo } = e.target.getExtData()
    if (extraInfo.tbDeviceId.startsWith('@')) return //假的模拟数据则终止弹窗
    let infoWindow = new AMapIns.InfoWindow({
      content: await createInfoWindow(extraInfo),
      offset: new AMapIns.Pixel(3, -30)
    })
    infoWindow.open(mapIns, e.target.getPosition())
  })
  // markerInstance.on('click', () => {
  //   mapIns.clearInfoWindow()
  // })
}

const dataHandle = (newData: dataJsonType) => {
  if (!mapIns && !AMapIns) {
    initMap(props.chartConfig.option)
    return
  }
  if (isArray(newData.markers)) {
    // 先清除旧标记
    mapIns.remove(markers)
    markers = []
    // 记录新标记
    if (mapMarkerType.value === MarkerEnum.MARKER) {
      newData.markers.forEach((markerItem: dataJsonMarkersType) => {
        const markerInstance = new AMapIns.Marker({
          position: [markerItem.position[0], markerItem.position[1]],
          offset: new AMapIns.Pixel(-13, 5),
          icon: iconMarker.value
        })
        // 原作者这种方式添加,属于JS API 1.4.8版本的
        markers.push(markerInstance)
        markerInstance.setMap(mapIns)
        // mapIns.add(markerInstance)
        mapClick(markerInstance, markerItem)
      })
    } else if (mapMarkerType.value === MarkerEnum.CIRCLE_MARKER) {
      newData.markers.forEach((markerItem: dataJsonMarkersType) => {
        const markerInstance = new AMapIns.CircleMarker({
          center: [
            !markerItem.position[0] ? 0 : markerItem.position[0],
            !markerItem.position[1] ? 0 : markerItem.position[1]
          ],
          radius: markerItem.value, //圆圈半径大小
          ...marker.value
        })
        // markers.push(markerInstance) //原作者这种方式添加,属于JS API 1.4.8版本的
        // markerInstance.setMap(mapIns)
        mapIns.add(markerInstance)
        mapClick(markerInstance, markerItem) // circle点击无效
      })
    }
  }
}

const stopWatch = watch(
  () => props.chartConfig.option.mapOptions,
  option => {
    initMap(option)
  },
  {
    immediate: true,
    deep: true
  }
)

watch(
  () => props.chartConfig.option.dataset,
  newData => {
    try {
      dataHandle(newData)
    } catch (error) {
      console.log(error)
    }
  },
  {
    deep: false
  }
)

// 预览
useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any) => {
  stopWatch()
  setTimeout(() => {
    dataHandle(newData)
  }, 1000)
})
</script>

<style>
/**去除高德地图原生弹窗 */
.amap-info-outer,
.amap-menu-outer {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0) !important;
}

.amap-info-outer {
  background-color: rgba(0, 0, 0, 0) !important;
}

.amap-info-close {
  display: none !important;
}

.amap-info-content {
  overflow: hidden !important;
}

/**去除高德地图原生弹窗*/
.amap-info-content .go-border-box {
  position: absolute;
  transform: scale(0.68);
  top: 0;
  z-index: -100;
}

.amap-info-content .go-border-box svg {
  background-color: v-bind(bgColor);
}
</style>
<style lang="scss" scoped>
.chart-amap {
  position: relative;

  .search-box {
    cursor: pointer;
    position: absolute;
    right: -25px;
    top: 15px;
    z-index: 9999;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    background-color: rgba(255, 255, 255, 0.2);
  }
}
</style>