MapComponent.vue 7.92 KB
<script lang="ts">
  export default {
    components: { Spin },
    inheritAttrs: false,
  };
</script>
<script lang="ts" setup>
  import { computed, onMounted, reactive, ref, unref, watchEffect } from 'vue';
  import { RadioRecord } from '../../detail/config/util';
  import { MapComponentLayout, MapComponentValue } from './map.config';
  import {
    ClockCircleOutlined,
    PlayCircleOutlined,
    PauseCircleOutlined,
  } from '@ant-design/icons-vue';
  import { Button, Spin, Tooltip } from 'ant-design-vue';
  import { FrontComponent } from '../../const/const';
  import { buildUUID } from '/@/utils/uuid';
  import { useModal } from '/@/components/Modal';
  import HistoryDataModel from './HistoryDataModel.vue';
  import { HistoryModalOkEmitParams, HistoryModalParams } from './type';
  import { formatToDateTime } from '/@/utils/dateUtil';
  import { isEqual } from 'lodash-es';
  import { useAsyncQueue } from '/@/views/device/localtion/useAsyncQueue';
  import { useMessage } from '/@/hooks/web/useMessage';

  // useVisualBoardContext();
  type TrackRecord = Record<'lng' | 'lat' | 'ts', number>;

  const startMethodName = `trackPlayMethod_${buildUUID()}`;

  const wrapId = `bai-map-${buildUUID()}`;

  enum TrackAnimationStatus {
    PLAY = 1,
    DONE = 2,
    PAUSE = 3,
  }

  const props = withDefaults(
    defineProps<{
      value?: MapComponentValue;
      layout?: MapComponentLayout;
      radio?: RadioRecord;
      random?: boolean;
    }>(),
    {
      random: true,
    }
  );

  const wrapRef = ref<HTMLDivElement | null>(null);
  const trackAni = ref<Nullable<any>>(null);
  let mapInstance: Nullable<Recordable> = null;

  const trackList = ref<TrackRecord[]>([]);

  watchEffect(() => {
    if (
      props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_REAL &&
      props.value?.track?.length
    ) {
      const lng = props.value.track.at(0)!;
      const lat = props.value.track.at(1)!;
      const record = {
        lng: lng.value,
        lat: lat.value,
        ts: lng.ts,
      };
      if (unref(trackList).length && isEqual(unref(trackList).at(-1), record)) return;
      marketPoint({ lat: lat.value, lng: lng.value });
      trackList.value.push(record);

      randomAnimation(unref(trackList));
      // marketPoint(record);
    }
  });

  function marketPoint(params: Record<'lng' | 'lat', number>) {
    const { lng, lat } = params;
    const BMap = (window as any).BMapGL;
    const marker = new BMap.Marker(new BMap.Point(lng, lat));
    unref(mapInstance)?.centerAndZoom(new BMap.Point(lng, lat));
    unref(mapInstance)?.addOverlay(marker);
  }

  const prepare = ref(false);
  async function initMap() {
    const wrapEl = unref(wrapRef);
    if (!wrapEl) return;
    const BMapGL = (window as any).BMapGL;
    mapInstance = new BMapGL.Map(wrapId);
    const point = new BMapGL.Point(104.09457, 30.53189);
    mapInstance!.centerAndZoom(point, 15);
    mapInstance!.enableScrollWheelZoom(true);
    props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY &&
      props.random &&
      randomAnimation();
  }

  const randomAnimation = (path?: Record<'lng' | 'lat', number>[], clearOverlays = true) => {
    path = path || [
      {
        lng: 116.297611,
        lat: 40.047363,
      },
      {
        lng: 116.302839,
        lat: 40.048219,
      },
      {
        lng: 116.308301,
        lat: 40.050566,
      },
      {
        lng: 116.305732,
        lat: 40.054957,
      },
      {
        lng: 116.304754,
        lat: 40.057953,
      },
      {
        lng: 116.306487,
        lat: 40.058312,
      },
      {
        lng: 116.307223,
        lat: 40.056379,
      },
    ];

    const point: any[] = [];
    const BMapGL = (window as any).BMapGL;

    clearOverlays && unref(mapInstance)?.clearOverlays();

    for (const { lng, lat } of path) {
      point.push(new BMapGL.Point(lng, lat));
    }

    const pl = new BMapGL.Polyline(point);
    const BMapGLLib = (window as any).BMapGLLib;

    marketPoint({ lat: path.at(0)?.lat, lng: path.at(0)?.lng });
    const dynamicPlayMethod = {
      [startMethodName]() {
        const duration = 5000;
        const delay = 300;
        trackAni.value = new BMapGLLib.TrackAnimation(unref(mapInstance), pl, {
          overallView: true,
          tilt: 30,
          duration,
          delay,
        });
        trackAni.value!.start();
        setTimeout(() => {
          marketPoint({ lat: path.at(-1)?.lat, lng: path.at(-1)?.lng });
        }, duration + delay);
      },
    };

    (window as any)[startMethodName] = dynamicPlayMethod[startMethodName];

    setTimeout(`${startMethodName}()`);
  };

  const { setTask, executeFlag } = useAsyncQueue();
  onMounted(() => {
    let interval: Nullable<NodeJS.Timer> = setInterval(() => {
      if ((window as any).BMapGL) {
        prepare.value = true;
        executeFlag.value = true;
        clearInterval(interval!);
        interval = null;
      }
    }, 1000);
    if (!(window as any).BMapGL) {
      setTask(initMap);
      return;
    }
    initMap();
  });

  const timeRange = reactive<Record<'start' | 'end', Nullable<number>>>({
    start: null,
    end: null,
  });
  const getTimeRange = computed(() => {
    const { start, end } = timeRange;
    if (!start || !end) return `- 请选择`;
    return ` - 从 ${formatToDateTime(start, 'YYYY-MM-DD HH:mm:ss')} 到 ${formatToDateTime(
      end,
      'YYYY-MM-DD HH:mm:ss'
    )}`;
  });

  const [register, { openModal }] = useModal();

  const handleTrackSwitch = () => {
    openModal(true, {
      dataSource: props.value?.dataSource || [],
    } as HistoryModalParams);
  };

  const getTrackPlayStatus = computed(() => {
    return (trackAni.value || {})._status;
  });

  const { createMessage } = useMessage();
  const handlePlay = () => {
    const { start, end } = timeRange;

    if (!props.random && (!start || !end)) {
      createMessage.warning('请先选择时间范围');
    }
    if (unref(getTrackPlayStatus) === TrackAnimationStatus.DONE) unref(trackAni).start();
    else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PLAY) unref(trackAni).pause();
    else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PAUSE) unref(trackAni).continue();
  };

  const handleRenderHistroyData = (params: HistoryModalOkEmitParams) => {
    const { track, value } = params;
    track.length && randomAnimation(track as unknown as Record<'lng' | 'lat', number>[]);
    timeRange.start = value.startTs as number;
    timeRange.end = value.endTs as number;
  };
</script>

<template>
  <div class="w-full h-full flex justify-center items-center flex-col p-2">
    <div
      class="w-full flex justify-end"
      v-if="props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY"
    >
      <Button
        v-if="!random"
        type="text"
        class="!px-2 flex-auto !text-left truncate"
        @click="handleTrackSwitch"
      >
        <div class="w-full truncate text-gray-500 flex items-center">
          <ClockCircleOutlined />
          <span class="mx-1">历史</span>
          <Tooltip :title="getTimeRange.replace('-', '')">
            <span class="truncate">
              {{ getTimeRange }}
            </span>
          </Tooltip>
        </div>
      </Button>
      <Button type="text" class="!px-2 !text-gray-500" @click="handlePlay">
        <PlayCircleOutlined v-show="getTrackPlayStatus !== TrackAnimationStatus.PLAY" />
        <PauseCircleOutlined
          class="!ml-0"
          v-show="getTrackPlayStatus === TrackAnimationStatus.PLAY"
        />
        <span>
          {{ getTrackPlayStatus !== TrackAnimationStatus.PLAY ? '播放轨迹' : '暂停播放' }}
        </span>
      </Button>
    </div>
    <Spin
      class="w-full h-full !flex flex-col justify-center items-center pointer-events-none"
      :spinning="!prepare"
      tip="地图加载中..."
    />
    <div v-show="prepare" ref="wrapRef" :id="wrapId" class="w-full h-full no-drag"></div>
    <HistoryDataModel @register="register" @ok="handleRenderHistroyData" />
  </div>
</template>