vue-amap高德地图绘制线路轨迹

车辆移动轨迹回放 + 报警事件标注 + 移动动画控制 + 地图渲染 + 高精度底图加载

高德地图2.0版本的vue3对应封装文档 https://vue-amap.guyixi.cn/

html 复制代码
<template>
  <el-dialog v-model="visible" title="轨迹回放" width="80%" destroy-on-close>
    <template #header="{ close, titleId, titleClass }">
      <div class="my-header">
        <span>查看轨迹</span>
        <span>(仅显示报警开始前一分钟到报警结束后一分钟数据)</span>
      </div>
    </template>

    <!-- 地图主体 -->
    <div style="height: 500px; width: 100%;" v-if="visible" v-loading="loading">
      <el-amap :zoom="zoom" :center="center" :resizeEnable="true" :rotation="rotation">

        <!-- 弹框 Marker(报警开始) -->
        <!-- 点击确定后继续播放动画 -->
        <el-amap-marker :visible="showStartInfo" :position="[startLng, startLat]"
          anchor="bottom-center" :z-index="999" :clickable="false">
          <template #default>
            <div class="alert" style="cursor: default;">
              <div class="alert-close">
                <el-icon :size="16" @click="showStartInfo = false">
                  <Close />
                </el-icon>
              </div>
              <div class="alert-title">开始报警</div>
              <div class="alert-content">车辆编号: {{ vehicleNumber }}</div>
              <div class="alert-content">开始报警时间: {{ beginTime }}</div>
              <div class="alert-btn">
                <el-button size="small" type="primary" @click="closeStartInfo">确定</el-button>
              </div>
            </div>
          </template>
        </el-amap-marker>

        <!-- 弹框 Marker(报警结束) -->
        <el-amap-marker :visible="showEndInfo" :position="[endLng, endLat]"
          anchor="bottom-center" :z-index="999" :clickable="false">
          <template #default>
            <div class="alert" style="cursor: default;">
              <div class="alert-close">
                <el-icon :size="16" @click="showEndInfo = false">
                  <Close />
                </el-icon>
              </div>
              <div class="alert-title">结束报警</div>
              <div class="alert-content">车辆编号: {{ vehicleNumber }}</div>
              <div class="alert-content">结束报警时间: {{ endTime }}</div>
              <div class="alert-btn">
                <el-button size="small" type="primary" @click="closeEndInfo">确定</el-button>
              </div>
            </div>
          </template>
        </el-amap-marker>

        <!-- 报警开始点 -->
        <el-amap-marker v-if="startLng && startLat" :position="[startLng, startLat]" anchor="bottom-center" />

        <!-- 报警结束点 -->
        <el-amap-marker v-if="endLng && endLat" :position="[endLng, endLat]" anchor="bottom-center" />

        <!-- 小车已经走过的轨迹(绿色) -->
        <el-amap-polyline v-if="passedPath.length > 0"
          :path="passedPath" stroke-color="#9ee492" :strokeOpacity="1"
          :stroke-weight="6" showDir lineJoin="round" :z-index="11" />

        <!-- 完整轨迹(蓝色) -->
        <el-amap-polyline v-if="trackPath.length > 0"
          :path="trackPath" stroke-color="#28F" :strokeOpacity="1"
          :stroke-weight="6" showDir lineJoin="round" :z-index="10" />

        <!-- 高精度瓦片底图 -->
        <el-amap-layer-custom-xyz
          :url="baseUrl + '高精度地图路径?x={x}&y={y}&z={z}'"
          proj="wgs84" :z-index="9" />

        <!-- 小车 Marker -->
        <el-amap-marker :position="currentMarker" :icon="myIcon"
          ref="markerRef" :offset="[-6.5, -2]" />
      </el-amap>
    </div>

    <!-- 底部控制栏:播放、暂停、继续、倍速 -->
    <div class="input-card">
      <div class="divCss">
        <el-button @click="startAnimation">开始动画</el-button>
        <el-button @click="pauseAnimation">暂停动画</el-button>
        <el-button @click="resumeAnimation">继续动画</el-button>
      </div>

      <!-- 切换速度(改变 moveAlong duration)-->
      <div class="div2Css">
        <el-radio-group v-model="radio">
          <el-radio :label="300">1X</el-radio>
          <el-radio :label="60">5X</el-radio>
          <el-radio :label="30">10X</el-radio>
          <el-radio :label="6">50X</el-radio>
        </el-radio-group>
      </div>
    </div>
  </el-dialog>
</template>




<script lang="ts" setup>
import { ElAmap, ElAmapMarker, ElAmapPolyline, ElAmapLayerCustomXyz } from '@vuemap/vue-amap';
import { queryAlarmRecordData } from '@/api/base/fenceAlarmRecord'
import offCar from '@/assets/map/car-offline.png'
import gcoord from "gcoord";
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const baseUrl = import.meta.env.VITE_APP_MAP1_API

// 显示/隐藏弹窗
const visible = ref(false)

// 地图基础属性
const zoom = ref(16.2)
const rotation = ref(58.5)
const center = ref<[number, number]>([125.02806, 43.995978])

// 原始轨迹、转换后轨迹、小车当前点、已走轨迹
const trackData = ref<any[]>([])
const trackPath = ref<[number, number][]>([])
const currentMarker = ref<[number, number] | null>(null)
const passedPath = ref<[number, number][]>([])

// 报警信息
let startLng = ref(0)
let startLat = ref(0)
let endLng = ref(0)
let endLat = ref(0)
let showStartInfo = ref(false)
let showEndInfo = ref(false)
let beginTime = ref('')
let endTime = ref('')
let vehicleNumber = ref('')

// 播放速度
let radio = ref(300)

// 小车图标(只创建 1 次)
const myIcon = new AMap.Icon({
  image: offCar,
  size: new AMap.Size(12, 24),
  imageSize: new AMap.Size(12, 24)
})

// 打开弹窗 → 获取轨迹
const open = (val: any) => {
  visible.value = true
  getAlarmRecordData(val)
}

const loading = ref(false)

// 请求轨迹数据并处理
const getAlarmRecordData = async (val: any) => {
  loading.value = true
  try {
    beginTime.value = val.beginTime
    endTime.value = val.endTime
    vehicleNumber.value = val.vehicleNumber

    // 请求后端轨迹
    const res = await queryAlarmRecordData({ id: val.id });

    if (!res.data || res.data.length === 0) {
      proxy?.$modal.msgWarning(res.msg);
      visible.value = false;
      return;
    }

    const processed: any[] = [];
    let lastSecond = '';

    // 轨迹处理:WGS84 → GCJ02;同一秒只保留第一条
    res.data.forEach(item => {
      const [lon, lat] = gcoord.transform([item.lon, item.lat], gcoord.WGS84, gcoord.GCJ02);
      const tmSecond = item.tm.split('.')[0];

      if (tmSecond !== lastSecond) {
        processed.push({ ...item, lon, lat, tm: tmSecond });
        lastSecond = tmSecond;
      }
    });

    // 小车完整轨迹
    trackData.value = processed;
    trackPath.value = processed.map(item => [item.lon, item.lat]);

    // 小车初始位置
    currentMarker.value = trackPath.value[0];
    passedPath.value = [];

    // 标记报警点
    const startPoint = processed.find(i => i.tm === val.beginTime)
    if (startPoint) {
      startLng.value = startPoint.lon
      startLat.value = startPoint.lat
    }

    const endPoint = processed.find(i => i.tm === val.endTime)
    if (endPoint) {
      endLng.value = endPoint.lon
      endLat.value = endPoint.lat
    }
  } finally {
    loading.value = false
  }
}

// 小车 Marker 实例
const markerRef = ref<any>(null)
const animationStarted = ref(false)

// 开始播放轨迹动画
const startAnimation = () => {
  if (!trackPath.value.length) return;
  const marker = markerRef.value?.$$getInstance()
  if (!marker) return;

  // 每次播放前重置位置和轨迹
  marker.setPosition(trackPath.value[0])
  passedPath.value = []

  // 清除之前的 moving 事件,防止重复触发
  marker.clearEvents('moving')

  let startTriggered = false
  let endTriggered = false

  // 开始移动
  marker.moveAlong(trackPath.value, {
    duration: radio.value,
    autoRotation: true,
  })

  // 绑定 moving 事件(实时更新轨迹 + 触发报警)
  marker.on('moving', (e: any) => {
    passedPath.value = e.passedPath
    const idx = e.passedPath.length - 1
    const tm = trackData.value[idx]?.tm

    // 进入报警开始时间 → 暂停
    if (!startTriggered && tm === beginTime.value) {
      startTriggered = true
      marker.pauseMove()
      showStartInfo.value = true
    }

    // 进入报警结束时间 → 暂停
    if (!endTriggered && tm === endTime.value) {
      endTriggered = true
      marker.pauseMove()
      showEndInfo.value = true
    }
  })

  animationStarted.value = true
}

// 暂停动画
const pauseAnimation = () => {
  markerRef.value?.$$getInstance()?.pauseMove()
}

// 继续动画
const resumeAnimation = () => {
  if (!animationStarted.value) return;
  markerRef.value?.$$getInstance()?.resumeMove()
}

// 重置动画(切换速度/关闭弹窗时调用)
const resetMarker = () => {
  const marker = markerRef.value?.$$getInstance()
  if (!marker) return;

  marker.pauseMove()
  marker.clearEvents('moving')
  marker.stopMove()

  passedPath.value = []
  if (trackPath.value.length > 0) {
    marker.setPosition(trackPath.value[0])
    currentMarker.value = trackPath.value[0]
  }

  animationStarted.value = false
  showStartInfo.value = false
  showEndInfo.value = false
}

// 切换速度 → 重置动画重新播放
watch(radio, () => resetMarker())

// 弹窗关闭 → 重置
watch(visible, (val) => !val && resetMarker())

// 点击报警弹框的"确定"按钮 → 继续动画
const closeStartInfo = () => {
  showStartInfo.value = false
  resumeAnimation()
}

const closeEndInfo = () => {
  showEndInfo.value = false
  resumeAnimation()
}

defineExpose({ open })
</script>




<style scoped lang="scss">
.el-dialog__body {
  padding: 0;
}

.my-header {
  span:nth-child(2) {
    font-size: 12px;
  }
}

.input-card {
  display: flex;
  justify-content: space-around;
  align-items: center;
  margin: 10px;
  flex-direction: column;
}

.div2Css {
  margin-top: 15px;
}

.alert {
  font-size: 12px;
  width: 210px;
  background: #fff;
  padding: 10px;
  border-radius: 4px;

  &-title {
    font-size: 13px;
    font-weight: bold;
    color: #ff0000;
    margin-bottom: 10px;
    margin-top: -15px;
  }

  &-content {
    margin-bottom: 3px;
  }

  &-btn {
    text-align: right;
  }

  &-close {
    text-align: right;

    .el-icon {
      cursor: pointer;
    }
  }
}
</style>
相关推荐
一个处女座的程序猿O(∩_∩)O1 小时前
React Native vs React Web:深度对比与架构解析
前端·react native·react.js
n***i951 小时前
前端技术的下一场进化:从工程化走向智能化的全面重构
前端·重构
@大迁世界1 小时前
紧急:React 19 和 Next.js 的 React 服务器组件存在关键漏洞
服务器·前端·javascript·react.js·前端框架
晓得迷路了1 小时前
栗子前端技术周刊第 109 期 - Vite 8 Beta、JavaScript 三十周年、Prettier 3.7...
前端·javascript·vite
Terry_Tsang1 小时前
ceph mon 报错 full ratio(s) out of order 解决方法
服务器·前端·ceph
韩曙亮2 小时前
【Web APIs】元素偏移量 offset 系列属性 ④ ( offset 属性案例 - 放大镜效果 )
前端·javascript·css·html·offset·dom·web apis
宁雨桥2 小时前
前端网页加载进度条实现指南:Vue3+Vite工程化场景
前端·javascript·性能优化
Mike_jia2 小时前
ZabbixWatch:打造现代化运维监控大屏,让数据掌控触手可及
前端
老华带你飞2 小时前
作业管理|基于Java作业管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·后端