1. 基础概念与环境准备

1.1 必要的库与导入

在使用3D模型前,需要导入必要的库和加载器:

import AMapLoader from "@amap/amap-jsapi-loader";
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';

说明

  • AMapLoader: 用于加载高德地图API

  • OBJLoader: Three.js提供的OBJ模型加载器,用于加载3D模型的几何数据

  • MTLLoader: Three.js提供的材质加载器,用于加载模型的材质和纹理

2. 地图初始化与3D层配置

2.1 初始化高德地图

async initMap() {
  try {
    this.AMaper = await AMapLoader.load({
      // 这里写你的 web key
      key: "a94e7a66ed28a3d1d095868ad1bec1c3",
      plugins: [""],
    });
    this.map = new this.AMaper.Map("container", {
      viewMode: '3D',
      showBuildingBlock: false,
      center: [122.037275, 37.491067],
      pitch: 60,
      zoom: 17
    });
    // 初始化光线
    this.map.AmbientLight = new this.AMaper.Lights.AmbientLight([1, 1, 1], 1);
    this.map.DirectionLight = new this.AMaper.Lights.DirectionLight([1, 0, 1], [1, 1, 1], 1);
  } catch (e) {
    console.log(e);
  }
}

说明

  • 设置viewMode: '3D'启用3D视图

  • pitch: 60设置地图的俯仰角度,使地图呈现3D效果

  • 通过AmbientLightDirectionLight设置场景光照,对3D模型的显示效果至关重要

3. 3D模型加载

3.1 创建3D层

async loadModel() {
  this.isLoadingModels = true;
  try {
    this.object3Dlayer = new this.AMaper.Object3DLayer();
    this.map.add(this.object3Dlayer);
    // ...

说明

  • Object3DLayer是高德地图提供的3D对象层,所有3D模型都需要添加到此层上

  • this.map.add(this.object3Dlayer)将3D层添加到地图中

3.2 加载材质和模型文件

    const materials = await new Promise((resolve, reject) => {
      new MTLLoader().load('http://localhost:9000/whgt/test.mtl', resolve, null, reject);
    });
​
    const event = await new Promise((resolve, reject) => {
      new OBJLoader().setMaterials(materials).load('http://localhost:9000/whgt/test.obj', resolve, null, reject);
    });
​
    this.vehicleBaseEvent = event;

说明

  • 首先加载MTL材质文件,它定义了3D模型的外观、颜色、反光等属性

  • 然后加载OBJ模型文件,并将已加载的材质应用到模型上

  • 使用Promise包装加载过程,实现异步加载

  • 将加载好的模型保存在vehicleBaseEvent中,以便后续使用

4. 创建3D网格(Mesh)

4.1 异步创建新的网格

async createMesh() {
  const materials = await new MTLLoader().loadAsync('http://localhost:9000/whgt/Electric_G_Power_Vehi_0411010527_texture.mtl');
  const event = await new OBJLoader().setMaterials(materials).loadAsync('http://localhost:9000/whgt/Electric_G_Power_Vehi_0411010527_texture.obj');
  return this.createMeshFromEvent(event);
}

说明

  • 这个函数用于创建新的3D网格,特别是用于创建车辆的3D模型

  • loadAsync方法是现代Promise写法,简化了加载过程

4.2 从加载事件创建网格对象

createMeshFromEvent(event) {
  const meshes = event.children;
  const createdMeshes = [];
​
  // 第一步:计算所有网格的Y轴最大值(在反向坐标系中,最大Y对应模型底部)
  let maxY = -Infinity;
  for (let i = 0; i < meshes.length; i++) {
    const meshData = meshes[i];
    const vecticesF3 = meshData.geometry.attributes.position;
    const vectexCount = vecticesF3.count;
​
    for (let j = 0; j < vectexCount; j++) {
      const s = j * 3;
      // 在高德地图坐标系中Y轴是反向的
      const y = -vecticesF3.array[s + 1];
      if (y > maxY) {
        maxY = y;
      }
    }
  }
​
  // 计算Y轴偏移量,使底部对齐原点
  // 在反向坐标系中,使用maxY作为偏移而不是minY
  const yOffset = -maxY;
​
  // 第二步:创建几何体并应用偏移
  for (let i = 0; i < meshes.length; i++) {
    const meshData = meshes[i];
    const vecticesF3 = meshData.geometry.attributes.position;
    const vecticesNormal3 = meshData.geometry.attributes.normal;
    const vecticesUV2 = meshData.geometry.attributes.uv;
    const vectexCount = vecticesF3.count;
​
    const mesh = new this.AMaper.Object3D.MeshAcceptLights();
    const geometry = mesh.geometry;
    const material = meshData.material[0] || meshData.material;
    const c = material.color;
    const opacity = material.opacity;
​
    for (let j = 0; j < vectexCount; j++) {
      const s = j * 3;
      geometry.vertices.push(
        vecticesF3.array[s],
        vecticesF3.array[s + 2],
        -vecticesF3.array[s + 1] + yOffset // 添加Y轴偏移量
      );
      if (vecticesNormal3) {
        geometry.vertexNormals.push(
          vecticesNormal3.array[s],
          vecticesNormal3.array[s + 2],
          -vecticesNormal3.array[s + 1]
        );
      }
      if (vecticesUV2) {
        geometry.vertexUVs.push(
          vecticesUV2.array[j * 2],
          1 - vecticesUV2.array[j * 2 + 1]
        );
      }
      geometry.vertexColors.push(c.r, c.g, c.b, opacity);
    }
​
    if (material.map) {
      mesh.textures.push(require('@/assets/3d/Electric_G_Power_Vehi_0411010527_texture.png'));
    }
    mesh.DEPTH_TEST = material.depthTest;
    mesh.transparent = opacity < 1;
    mesh.scale(200, 200, 200);
​
    createdMeshes.push(mesh);
  }
  return createdMeshes;
}

关键概念解析

  1. 网格结构

    • 每个3D模型由多个网格组成,存储在event.children

    • 每个网格包含几何数据和材质数据

  2. 坐标系调整

    • Three.js和高德地图使用不同的坐标系

    • 代码中计算Y轴最大值并应用偏移,确保模型正确放置

  3. 网格数据处理

    • vecticesF3:顶点位置数据

    • vecticesNormal3:法线数据,用于光照计算

    • vecticesUV2:UV贴图坐标,用于纹理映射

  4. 创建高德地图兼容的网格

    • 使用AMaper.Object3D.MeshAcceptLights()创建可接收光照的网格

    • 转换并添加顶点、法线和UV数据

    • 设置材质颜色和透明度

  5. 应用纹理和缩放

    • 如果材质有贴图,添加纹理

    • 设置深度测试和透明度属性

    • 应用缩放(mesh.scale(200, 200, 200))使模型适合地图比例

5. 模型管理与动画

5.1 模型位置与旋转更新

// 添加动画循环方法
startAnimationLoop() {
  const animate = () => {
    this.updateVehicleAnimations();
    this.animationFrameId = requestAnimationFrame(animate);
  };
  this.animationFrameId = requestAnimationFrame(animate);
}
​
// 更新车辆动画状态
updateVehicleAnimations() {
  const now = Date.now();
​
  Object.keys(this.vehiclePositions).forEach(vehicleId => {
    const positionData = this.vehiclePositions[vehicleId];
​
    // 如果没有正在进行的动画或者动画已经完成,则跳过
    if (!positionData || !positionData.animating) {
      return;
    }
​
    // 计算动画进度
    const elapsed = now - positionData.startTime;
    const duration = positionData.duration;
    let progress = Math.min(elapsed / duration, 1);
​
    // 如果动画已经完成
    if (progress >= 1) {
      // 设置到最终位置
      const meshes = this.vehicleMeshes[vehicleId];
      if (meshes) {
        meshes.forEach(mesh => {
          mesh.position(new this.AMaper.LngLat(
            positionData.targetLng,
            positionData.targetLat,
            positionData.targetAlt
          ));
        });
      }
​
      // 标记动画完成
      this.vehiclePositions[vehicleId].animating = false;
​
      // 更新警告标记位置
      this.updateWarningMarkerPosition(vehicleId);
      return;
    }
​
    // 使用缓动函数使动画更平滑
    progress = this.easeInOutQuad(progress);
​
    // 计算当前位置
    const currentLng = positionData.startLng + (positionData.targetLng - positionData.startLng) * progress;
    const currentLat = positionData.startLat + (positionData.targetLat - positionData.startLat) * progress;
    const currentAlt = positionData.startAlt + (positionData.targetAlt - positionData.startAlt) * progress;
​
    // 更新车辆位置
    const meshes = this.vehicleMeshes[vehicleId];
    if (meshes) {
      meshes.forEach(mesh => {
        mesh.position(new this.AMaper.LngLat(currentLng, currentLat, currentAlt));
      });
    }
​
    // 同步更新警告标记位置
    if (this.vehicleWarningMarkers[vehicleId]) {
      // 获取车辆朝向
      const heading = this.vehicleHeadings[vehicleId] || 0;
​
      // 计算左上方的偏移量(根据车辆的朝向调整)
      const offsetDistance = 5;
      // 使用三角函数计算经纬度偏移
      const headingRadians = heading * (Math.PI / 180);
      // 向左前方偏移 - 调整角度使其位于左上角
      const offsetAngle = headingRadians + Math.PI * 0.75; // 左前方45度
      const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
      const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;
​
      this.vehicleWarningMarkers[vehicleId].setPosition([
        currentLng + lngOffset,
        currentLat + latOffset,
        currentAlt
      ]);
    }
  });
}
​
// 缓动函数 - 使动画更平滑
easeInOutQuad(t) {
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}

说明

  • startAnimationLoop:创建动画循环,使用requestAnimationFrame实现高效渲染

  • updateVehicleAnimations:管理所有车辆模型的动画状态和位置更新

  • 动画原理:计算当前时间和起始时间的差值,得到动画进度,再据此计算当前位置

  • 使用缓动函数(easeInOutQuad)使动画更加平滑自然,避免线性运动的机械感

5.2 模型加载与位置更新

// 修改 updateVehiclePositions 方法
async updateVehiclePositions() {
  // 步骤1:获取当前在线车辆ID列表
  const onlineVehicleIds = new Set(this.vehicles.map(v => v.id));
​
  // 步骤2:清理已离线的车辆模型
  Object.keys(this.vehicleMeshes).forEach(vehicleId => {
    if (!onlineVehicleIds.has(vehicleId)) {
      // 从地图移除模型
      this.vehicleMeshes[vehicleId].forEach(mesh => {
        this.object3Dlayer.remove(mesh);
      });
      // 删除缓存
      delete this.vehicleMeshes[vehicleId];
      // 删除朝向记录
      delete this.vehicleHeadings[vehicleId];
      // 删除位置记录
      delete this.vehiclePositions[vehicleId];
      // 删除警告标记
      if (this.vehicleWarningMarkers[vehicleId]) {
        this.map.remove(this.vehicleWarningMarkers[vehicleId]);
        delete this.vehicleWarningMarkers[vehicleId];
      }
    }
  });
​
  // 步骤3:更新或创建在线车辆模型
  for (const vehicle of this.vehicles) {
    if (!this.vehicleMeshes[vehicle.id]) {
      const meshes = await this.createMesh();
      // 确保每个子网格都有userData
      meshes.forEach(mesh => {
        mesh.userData = {
          ...mesh.userData,  // 保留原有数据
          vehicleId: vehicle.id
        };
        this.object3Dlayer.add(mesh);
      });
      this.vehicleMeshes[vehicle.id] = meshes;
​
      // 新创建的车辆模型,设置初始朝向
      if (!this.vehicleHeadings[vehicle.id]) {
        this.vehicleHeadings[vehicle.id] = vehicle.heading;
​
        // 首次加载时对所有网格应用相同的初始旋转
        meshes.forEach(mesh => {
          // 记录初始旋转角度到mesh的userData中
          mesh.userData.initialRotation = Math.PI + vehicle.heading + 180;
          mesh.rotateZ(mesh.userData.initialRotation);
        });
      }
​
      // 初始化位置数据,首次不需要动画
      const position = new this.AMaper.LngLat(vehicle.longitude, vehicle.latitude, vehicle.altitude);
      meshes.forEach(mesh => {
        mesh.position(position);
      });
​
      this.vehiclePositions[vehicle.id] = {
        startLng: vehicle.longitude,
        startLat: vehicle.latitude,
        startAlt: vehicle.altitude,
        targetLng: vehicle.longitude,
        targetLat: vehicle.latitude,
        targetAlt: vehicle.altitude,
        startTime: Date.now(),
        duration: 1000, // 1秒动画
        animating: false
      };
​
      // 关键改动:立即检查并创建警告标记(不等待下一次更新)
      if (this.isVehicleWarning(vehicle)) {
        this.createWarningMarker(vehicle);
      }
​
      continue; // 跳过首次加载的车辆动画
    }

关键概念

  1. 模型管理生命周期

    • 对离线车辆进行清理,释放资源

    • 对新上线的车辆创建新模型

    • 对已有车辆更新位置和朝向

  2. userData存储自定义数据

    • 使用mesh.userData存储车辆ID和初始旋转角度

    • 利用Three.js提供的这一机制,可以在模型中附加任意自定义数据

  3. 多层次对象管理

    • 使用多个对象存储不同数据:vehicleMeshes存储模型,vehicleHeadings存储朝向,vehiclePositions存储位置

6. 模型交互与事件处理

6.1 模型点击事件处理

// 修改点击事件处理方法
async handleMapClick(e) {
  if (this.isLoadingModels) {
    console.warn('模型尚未加载完成');
    return;
  }
  try {
    const px = new this.AMaper.Pixel(e.pixel.x, e.pixel.y);
    const obj = this.map.getObject3DByContainerPos(px, [this.object3Dlayer], false);
​
    // 先移除之前的选中效果
    this.removeSelectionEffect();
​
    if (!obj || !obj.object) {
      console.log('未点击到任何模型');
      return;
    }
​
    // 遍历对象层级查找vehicleId
    let currentObj = obj.object;
    while (currentObj) {
      if (currentObj.userData?.vehicleId) {
        const vehicleId = currentObj.userData.vehicleId;
        const vehicle = this.vehicles.find(v => v.id === vehicleId);
​
        if (vehicle) {
          this.selectedVehicle = vehicle;
          this.isPopupVisible = true;
​
          // 添加选中效果
          this.addSelectionEffect(vehicleId);
​
          // 调用 getTrack 方法并传递 vehicleId
          const response = await this.getTrack({ vehicleId });
​
          // 移除之前的轨迹
          if (this.currentTrack) {
            this.map.remove(this.currentTrack);
            this.currentTrack = null;
          }
          
          // ...后续代码省略
          
          return;
        } else {
          console.log(`未找到 ID 为 ${vehicleId} 的车辆信息`);
        }
      }
      currentObj = currentObj.parent;
    }
​
    console.log('点击的模型未关联车辆信息');
  } catch (error) {
    console.error('点击处理发生错误:', error);
  }
}

说明

  • getObject3DByContainerPos:高德地图提供的射线拾取方法,用于识别用户点击的3D对象

  • 通过遍历对象层级(currentObj = currentObj.parent)查找关联的车辆ID

  • 使用选中效果和弹窗提供用户反馈

  • 实现了3D模型的交互功能

6.2 鼠标悬停交互

handleMapMove(e) {
  const px = new this.AMaper.Pixel(e.pixel.x, e.pixel.y);
  const obj = this.map.getObject3DByContainerPos(px, [this.object3Dlayer], false) || {};
​
  if (obj.object) {
    this.map.setDefaultCursor('pointer');
  } else {
    this.map.setDefaultCursor('default');
  }
}

说明

  • 当鼠标悬停在3D模型上时,改变鼠标指针样式,提供用户反馈

  • 使用与点击相同的射线拾取技术

7. 选中效果与视觉反馈

7.1 添加选中效果

// 添加选中效果
addSelectionEffect(vehicleId) {
  // 先移除之前的效果
  this.removeSelectionEffect();

  // 获取车辆模型位置
  const meshes = this.vehicleMeshes[vehicleId];
  if (!meshes || meshes.length === 0) return;

  const mesh = meshes[0];
  const position = mesh.position();

  // 创建扩散效果
  const radius = 10; // 初始半径(米)
  const maxRadius = 40; // 最大半径(米)
  const ringCount = 4; // 同时显示的环数
  const effects = [];

  for (let i = 0; i < ringCount; i++) {
    const circle = new this.AMaper.Circle({
      center: [position.lng, position.lat],
      radius: radius + (i * (maxRadius - radius) / ringCount),
      strokeColor: '#1890FF',
      strokeWeight: 3,
      strokeOpacity: 0.8 - (i * 0.2),
      fillColor: '#1890FF',
      fillOpacity: 0.4 - (i * 0.1),
      zIndex: 99 - i,
    });
    this.map.add(circle);
    effects.push(circle);
  }

  // 存储效果对象和数据
  this.selectionEffect = {
    circles: effects,
    vehicleId: vehicleId,
    animation: {
      startRadius: radius,
      maxRadius: maxRadius,
      currentPhase: 0,
      ringCount: ringCount,
      lastUpdateTime: Date.now(),
      animationSpeed: 0.3 // 控制动画速度
    }
  };

  // 开始动画
  this.animateSelectionEffect();
}

说明

  • 使用高德地图的Circle对象创建选中效果

  • 通过设置多个同心圆并控制其透明度和大小,实现视觉上的扩散动画

  • 存储选中效果的状态信息,以便后续动画和清理

7.2 动画效果实现

// 更新选中效果动画 - 修复闪烁问题
animateSelectionEffect() {
  if (!this.selectionEffect) return;
​
  const effect = this.selectionEffect;
  const now = Date.now();
  const delta = (now - effect.animation.lastUpdateTime) / 1000; // 秒
  effect.animation.lastUpdateTime = now;
​
  // 更新动画阶段
  effect.animation.currentPhase = (effect.animation.currentPhase + delta * effect.animation.animationSpeed) % 1;
​
  // 检查车辆是否仍然存在
  const vehicleId = effect.vehicleId;
  const meshes = this.vehicleMeshes[vehicleId];
  if (!meshes || meshes.length === 0) {
    this.removeSelectionEffect();
    return;
  }
​
  // 获取车辆当前位置
  const mesh = meshes[0];
  const position = mesh.position();
​
  // 使用循环模式,让圆环连续出现
  for (let i = 0; i < effect.animation.ringCount; i++) {
    // 计算每个环的相位偏移,确保均匀分布
    const phaseOffset = i / effect.animation.ringCount;
​
    // 添加偏移量到当前相位
    let phase = (effect.animation.currentPhase + phaseOffset) % 1;
​
    // 计算当前半径
    const radiusDelta = effect.animation.maxRadius - effect.animation.startRadius;
    const currentRadius = effect.animation.startRadius + (phase * radiusDelta);
​
    // 计算不透明度 - 关键修改:使用平滑的不透明度曲线
    let opacity;
    if (phase < 0.1) {
      // 淡入阶段 (0-10%)
      opacity = phase * 10 * 0.8; // 从0逐渐增加到0.8
    } else if (phase > 0.7) {
      // 淡出阶段 (70-100%)
      const fadeOutPhase = (phase - 0.7) / 0.3; // 归一化到0-1
      opacity = 0.8 * (1 - fadeOutPhase); // 从0.8逐渐减少到0
    } else {
      // 正常显示阶段 (10-70%)
      opacity = 0.8;
    }
​
    // 确保在相位接近1时完全透明,避免闪烁
    if (phase > 0.95) {
      opacity = 0;
    }
​
    // 更新圆的位置、大小和不透明度
    const circle = effect.circles[i];
    circle.setCenter([position.lng, position.lat]);
    circle.setRadius(currentRadius);
    circle.setOptions({
      strokeOpacity: opacity,
      fillOpacity: opacity / 2
    });
  }
​
  // 继续动画循环
  requestAnimationFrame(() => this.animateSelectionEffect());
}

说明

  • 使用requestAnimationFrame实现高效的动画循环

  • 通过计算时间差来保证动画速度一致

  • 使用相位偏移让多个圆环错开,形成连续的波浪效果

  • 实现了淡入淡出的不透明度变化,让动画更加平滑自然

8. 高级3D效果与性能优化

8.1 警告标记与模型关联

// 新增方法:创建警告标记(简化DOM结构,提高渲染速度)
createWarningMarker(vehicle) {
  if (!vehicle || !this.vehicleMeshes[vehicle.id]) return;
​
  // 获取车辆模型位置
  const meshes = this.vehicleMeshes[vehicle.id];
  if (meshes && meshes.length > 0) {
    const mesh = meshes[0];
    const position = mesh.position();
​
    // 获取车辆朝向
    const heading = this.vehicleHeadings[vehicle.id] || 0;
​
    // 计算左上方的偏移量
    const offsetDistance = 5;
    const headingRadians = heading * (Math.PI / 180);
    const offsetAngle = headingRadians + Math.PI * 0.75;
    const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
    const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;
​
    // 创建警告标记(恢复高度效果)
    const warningMarker = new this.AMaper.Marker({
      position: [
        position.lng + lngOffset,
        position.lat + latOffset,
        position.alt
      ],
      content: `
        <div style="
          width: 36px; 
          height: 36px; 
          background-color: #ff0000; 
          border-radius: 50%; 
          display: flex; 
          justify-content: center; 
          align-items: center; 
          color: white; 
          font-weight: bold; 
          font-size: 24px;
          box-shadow: 0 0 10px #ff0000, 0 0 20px rgba(255,0,0,0.5);
          border: 2px solid white;
          position: relative;
          margin-top: -60px; /* 通过CSS上移,造成高度效果 */
          transform: translateY(-30px);
          text-align: center;
          line-height: 36px;
        ">
          <span style="display: inline-block; position: relative; top: -1px;">!</span>
          <div style="
            position: absolute;
            bottom: -60px;
            left: 50%;
            width: 2px;
            height: 60px;
            background: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,0,0,0));
            transform: translateX(-50%);
          "></div>
        </div>
      `,
      offset: new this.AMaper.Pixel(-18, -18),
      zIndex: 999
    });
​
    this.map.add(warningMarker);
    this.vehicleWarningMarkers[vehicle.id] = warningMarker;
  }
}

说明

  • 结合了HTML+CSS和地图标记,实现了3D模型之上的警告指示

  • 计算偏移量时考虑了车辆的朝向,使标记位置更加合理

  • 通过CSS实现了视觉上的高度效果和警告提示的渐变连接线

  • 设置了较高的zIndex确保警告标记在最上层

9. 资源管理与优化

9.1 清理资源与内存管理

beforeDestroy() {
  clearInterval(this.syncVehicleDataInterval);
  if (this.stationMarkers) {
    this.stationMarkers.forEach(marker => marker.setMap(null));
  }
​
  // 清除动画帧
  if (this.animationFrameId) {
    cancelAnimationFrame(this.animationFrameId);
  }
​
  // 清理警告标记
  Object.keys(this.vehicleWarningMarkers).forEach(vehicleId => {
    const warningMarker = this.vehicleWarningMarkers[vehicleId];
    if (warningMarker) {
      this.map.remove(warningMarker);
    }
  });
​
  // 清理连接线标记
  if (this.vehicleLineMarkers) {
    Object.keys(this.vehicleLineMarkers).forEach(vehicleId => {
      const lineMarker = this.vehicleLineMarkers[vehicleId];
      if (lineMarker) {
        this.map.remove(lineMarker);
      }
    });
  }
}

说明

  • 在组件销毁时清理所有资源,防止内存泄漏

  • 取消动画帧请求,停止动画循环

  • 移除所有地图标记和3D对象


总结

本文档详细介绍了如何在高德地图中集成和使用Three.js的3D模型,涵盖了从模型加载、渲染、交互到动画的全过程。通过遵循这些实践,可以实现高效且美观的3D地图交互效应。

主要知识点包括:

  1. 模型加载与材质应用

  2. 坐标系转换与对齐

  3. 模型位置和旋转更新

  4. 动画效果实现

  5. 交互事件处理

  6. 视觉反馈效果

  7. 性能优化和资源管理

最后附上项目源码:
 

<template>
  <div id="app">
    <div id="container"></div>
    <!-- 恢复定位按钮 -->
    <button id="reset-camera-button" @click="resetCamera">恢复定位</button>
    <!-- 使用 Popup 组件 -->
    <LeftPopup :isPopupVisible="isPopupVisible" @close="handlePopupClose" @clear-selection="clearVehicleSelection"
      :vehicleInfo="selectedVehicle" @clear-right-selection="clearRightSelection" />
    <!-- 移除紧急制动按钮 -->
    <button id="use-vehicle-button" :class="{ disabled: !selectedVehicle }"
      :style="{ right: isRightPopupExpanded ? '30px' : '330px' }" @click="handleUseVehicle"
      :title="!selectedVehicle ? '当前未选中车辆' : ''">
      派单
    </button>
    <!-- 监控按钮,只在选中车辆时显示 -->
    <button id="monitor-button" v-if="selectedVehicle" @click="openMonitorPopup"
      :style="{ right: isRightPopupExpanded ? '30px' : '470px' }">
      监控
    </button>
    <!-- 任务按钮,仅当车辆有订单时显示 -->
    <button id="task-button" v-if="selectedVehicle && hasActiveOrder" @click="handleTaskManage"
      :style="{ right: isRightPopupExpanded ? '30px' : '400px' }">
      任务
    </button>
    <!-- 使用监控组件 -->
    <MonitorPopup :isPopupVisible="isMonitorPopupVisible" @close="closeMonitorPopup" :vehicleInfo="selectedVehicle" />
    <!-- 使用 RightPopup 组件 -->
    <RightPopup :isExpanded="isRightPopupExpanded" @toggle="toggleRightPopup" :vehicles="allVehicles"
      @select="handleVehicleSelect" :selectedVehicle="selectedVehicle" />
    <!-- 站点信息弹窗 -->
    <StationPopup :isPopupVisible="isStationPopupVisible" @close="handleStationPopupClose"
      :stationInfo="selectedStation" :position="stationPosition" />
    <!-- 用车弹窗 -->
    <UseVehiclePopup :isPopupVisible="isUseVehiclePopupVisible" @close="handleUseVehiclePopupClose"
      :vehicleInfo="selectedVehicle" />
    <!-- 任务管理弹窗 -->
    <TaskPopup :isPopupVisible="isTaskPopupVisible" @close="handleTaskPopupClose" :vehicleInfo="selectedVehicle" />
  </div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import LeftPopup from '../../../components/PopupLeft';
import RightPopup from '../../../components/PopupRight';
import StationPopup from '../../../components/StationPopup';
import UseVehiclePopup from '../../../components/UseVehiclePopup';
import TaskPopup from '../../../components/TaskPopup';
import MonitorPopup from '../../../components/MonitorPopup';
import { listOnlineVehicles } from '@/api/ugvms/vehicles';
import { getTrack } from '@/api/ugvms/vehicles';
import { listStaion } from "@/api/ugvms/station";
import { orderByVehicleId } from "@/api/ugvms/order";

export default {
  name: "App",
  components: {
    LeftPopup,
    RightPopup,
    StationPopup,
    UseVehiclePopup,
    TaskPopup,
    MonitorPopup,
  },
  data() {
    return {
      map: null,
      AMaper: null,
      isPopupVisible: false,
      object3Dlayer: null,
      isRightPopupExpanded: false,
      vehicles: [], // 存储车辆信息
      allVehicles: [], // 存储所有车辆信息
      vehicleMeshes: {}, // 存储每个车辆的 3D 模型
      vehicleAnimations: {}, // 存储车辆动画状态
      selectedVehicle: null,
      isLoadingModels: false,
      currentTrack: null, // 保存当前显示的轨迹对象
      currentTrackMarkers: null, // 保存轨迹起点和终点标记
      stations: [], // 存储站点信息
      selectedStation: null, // 当前选中的站点信息
      isStationPopupVisible: false, // 控制站点信息弹窗的显示状态
      stationPosition: { x: 0, y: 0 },
      isUseVehiclePopupVisible: false, // 控制用车弹窗的显示状态
      isTaskPopupVisible: false, // 控制任务弹窗的显示状态
      hasActiveOrder: false, // 当前选中车辆是否有活动订单
      vehicleHeadings: {}, // 存储每个车辆的朝向历史
      vehiclePositions: {}, // 存储每个车辆的当前位置和目标位置
      animationFrameId: null, // 存储动画帧ID
      selectionEffect: null, // 存储选中效果对象
      isMonitorPopupVisible: false, // 监控弹窗是否可见
      vehicleWarningMarkers: {}, // 存储每个车辆的警告标记
      vehicleLineMarkers: {}, // 存储车辆警告标记的连接线
    };
  },
  async mounted() {
    await this.initMap();
    this.loadModel();
    this.fetchVehicleData();
    this.fetchStationData();  // 新增:获取站点数据
    console.log('Starting  vehicle data sync interval');
    this.syncVehicleDataInterval = setInterval(() => {
      // console.log(' 触发车辆数据获取,时间:', new Date());
      this.fetchVehicleData();
    }, 5000);

    // 添加鼠标点击事件监听器
    this.map.on('click', (e) => {
      this.handleMapClick(e);
    });

    // 添加鼠标移入事件监听器
    this.map.on('mousemove', (e) => {
      this.handleMapMove(e);
    });

    // 调整高德地图水印位置和样式
    this.adjustMapLogo();

    // 启动动画循环
    this.startAnimationLoop();
  },
  beforeDestroy() {
    clearInterval(this.syncVehicleDataInterval);
    if (this.stationMarkers) {
      this.stationMarkers.forEach(marker => marker.setMap(null));
    }

    // 清除动画帧
    if (this.animationFrameId) {
      cancelAnimationFrame(this.animationFrameId);
    }

    // 清理警告标记
    Object.keys(this.vehicleWarningMarkers).forEach(vehicleId => {
      const warningMarker = this.vehicleWarningMarkers[vehicleId];
      if (warningMarker) {
        this.map.remove(warningMarker);
      }
    });

    // 清理连接线标记
    if (this.vehicleLineMarkers) {
      Object.keys(this.vehicleLineMarkers).forEach(vehicleId => {
        const lineMarker = this.vehicleLineMarkers[vehicleId];
        if (lineMarker) {
          this.map.remove(lineMarker);
        }
      });
    }
  },
  methods: {
    async initMap() {
      try {
        this.AMaper = await AMapLoader.load({
          // 这里写你的 web key
          key: "a94e7a66ed28a3d1d095868ad1bec1c3",
          plugins: [""],
        });
        this.map = new this.AMaper.Map("container", {
          viewMode: '3D',
          showBuildingBlock: false,
          center: [122.037275, 37.491067],
          pitch: 60,
          zoom: 17
        });
        // 初始化光线
        this.map.AmbientLight = new this.AMaper.Lights.AmbientLight([1, 1, 1], 1);

        this.map.DirectionLight = new this.AMaper.Lights.DirectionLight([1, 0, 1], [1, 1, 1], 1);
      } catch (e) {
        console.log(e);
      }
    },
    async loadModel() {
      this.isLoadingModels = true;
      try {
        this.object3Dlayer = new this.AMaper.Object3DLayer();
        this.map.add(this.object3Dlayer);

        const materials = await new Promise((resolve, reject) => {
          new MTLLoader().load('http://localhost:9000/whgt/test.mtl', resolve, null, reject);
        });

        const event = await new Promise((resolve, reject) => {
          new OBJLoader().setMaterials(materials).load('http://localhost:9000/whgt/test.obj', resolve, null, reject);
        });

        this.vehicleBaseEvent = event;
      } finally {
        this.isLoadingModels = false;
      }
    },

    // 获取站点数据并将其展示在地图上
    async fetchStationData() {
      try {
        const response = await listStaion();
        if (response.code === 200) {
          this.stations = response.rows;
          this.displayStationsOnMap();
        } else {
          console.error('Failed to fetch station data:', response.msg);
        }
      } catch (error) {
        console.error('Failed to fetch station data:', error);
      }
    },
    displayStationsOnMap() {
      if (!this.map || !this.stations.length) return;

      // 清除之前的站点标记
      if (this.stationMarkers) {
        this.stationMarkers.forEach(marker => marker.setMap(null));
      }

      this.stationMarkers = this.stations.map(station => {
        const iconSrc = this.getIconSrc(station.icon);
        const marker = new this.AMaper.Marker({
          position: [station.longitude, station.latitude, 0], // 设置 z 坐标为 0
          icon: new this.AMaper.Icon({
            image: iconSrc,
            size: new this.AMaper.Size(32, 32), // 设置图标大小
            imageSize: new this.AMaper.Size(32, 32), // 设置图片大小
          }),
          title: station.name,
        });

        marker.setMap(this.map);

        // 添加点击事件
        marker.on('click', () => {
          this.handleStationClick(station);
        });

        return marker;
      });
    },
    handleStationClick(station) {
      // 将经纬度转换为容器坐标
      const lnglat = new this.AMaper.LngLat(station.longitude, station.latitude);
      const pixel = this.map.lngLatToContainer(lnglat);

      // 获取地图容器位置
      const mapContainer = document.getElementById('container');
      const rect = mapContainer.getBoundingClientRect();

      // 计算页面坐标(考虑滚动偏移)
      const x = pixel.x + rect.left - mapContainer.scrollLeft;
      const y = pixel.y + rect.top - mapContainer.scrollTop;

      this.selectedStation = station;
      this.stationPosition = { x, y };
      this.isStationPopupVisible = true;
    },
    getIconSrc(iconName) {
      switch (iconName) {
        case 'warehouse':
          return require('@/assets/drawable/amap_warehouse.png');
        case 'station':
          return require('@/assets/drawable/amap_station.png');
        case 'stop':
          return require('@/assets/drawable/amap_stop.png');
        default:
          return require('@/assets/drawable/amap_na.png'); // 可以根据需要设置默认图片
      }
    },

    // 监听左侧弹窗中的关闭功能,取消右侧弹窗中的选中选项
    clearVehicleSelection() {
      this.selectedVehicle = null;
      // 移除选中效果
      this.removeSelectionEffect();
      // 强制重新渲染右侧组件(如果存在缓存问题)
      this.allVehicles = [...this.allVehicles];
    },

    // 处理站点信息弹窗的关闭事件
    handleStationPopupClose() {
      this.isStationPopupVisible = false; // 隐藏弹窗
      this.selectedStation = null; // 清空选中的站点信息
    },

    // 重置摄像机到初始位置
    resetCamera() {
      if (this.map) {
        // 重置摄像机到初始位置
        this.map.setCenter([122.037275, 37.491067]); // 初始中心点
        this.map.setZoom(17); // 初始缩放级别
        this.map.setPitch(60); // 初始俯仰角
        this.map.setRotation(0); // 重置地图朝向为正北方向

        // 取消选中的车辆
        this.selectedVehicle = null;
        // 移除选中效果
        this.removeSelectionEffect();
        // 关闭左侧弹窗
        this.isPopupVisible = false;

        // 隐藏轨迹
        if (this.currentTrack) {
          this.map.remove(this.currentTrack);
          this.currentTrack = null;
        }

        // 隐藏轨迹标记
        if (this.currentTrackMarkers) {
          this.currentTrackMarkers.forEach(marker => {
            this.map.remove(marker);
          });
          this.currentTrackMarkers = null;
        }
      }
    },
    // 修改后的方法
    async createMesh() {
      const materials = await new MTLLoader().loadAsync('http://localhost:9000/whgt/Electric_G_Power_Vehi_0411010527_texture.mtl');
      const event = await new OBJLoader().setMaterials(materials).loadAsync('http://localhost:9000/whgt/Electric_G_Power_Vehi_0411010527_texture.obj');
      return this.createMeshFromEvent(event);
    },

    createMeshFromEvent(event) {
      const meshes = event.children;
      const createdMeshes = [];

      // 第一步:计算所有网格的Y轴最大值(在反向坐标系中,最大Y对应模型底部)
      let maxY = -Infinity;
      for (let i = 0; i < meshes.length; i++) {
        const meshData = meshes[i];
        const vecticesF3 = meshData.geometry.attributes.position;
        const vectexCount = vecticesF3.count;

        for (let j = 0; j < vectexCount; j++) {
          const s = j * 3;
          // 在高德地图坐标系中Y轴是反向的
          const y = -vecticesF3.array[s + 1];
          if (y > maxY) {
            maxY = y;
          }
        }
      }

      // 计算Y轴偏移量,使底部对齐原点
      // 在反向坐标系中,使用maxY作为偏移而不是minY
      const yOffset = -maxY;

      // 第二步:创建几何体并应用偏移
      for (let i = 0; i < meshes.length; i++) {
        const meshData = meshes[i];
        const vecticesF3 = meshData.geometry.attributes.position;
        const vecticesNormal3 = meshData.geometry.attributes.normal;
        const vecticesUV2 = meshData.geometry.attributes.uv;
        const vectexCount = vecticesF3.count;

        const mesh = new this.AMaper.Object3D.MeshAcceptLights();
        const geometry = mesh.geometry;
        const material = meshData.material[0] || meshData.material;
        const c = material.color;
        const opacity = material.opacity;

        for (let j = 0; j < vectexCount; j++) {
          const s = j * 3;
          geometry.vertices.push(
            vecticesF3.array[s],
            vecticesF3.array[s + 2],
            -vecticesF3.array[s + 1] + yOffset // 添加Y轴偏移量
          );
          if (vecticesNormal3) {
            geometry.vertexNormals.push(
              vecticesNormal3.array[s],
              vecticesNormal3.array[s + 2],
              -vecticesNormal3.array[s + 1]
            );
          }
          if (vecticesUV2) {
            geometry.vertexUVs.push(
              vecticesUV2.array[j * 2],
              1 - vecticesUV2.array[j * 2 + 1]
            );
          }
          geometry.vertexColors.push(c.r, c.g, c.b, opacity);
        }

        if (material.map) {
          mesh.textures.push(require('@/assets/3d/Electric_G_Power_Vehi_0411010527_texture.png'));
        }
        mesh.DEPTH_TEST = material.depthTest;
        mesh.transparent = opacity < 1;
        mesh.scale(200, 200, 200);

        createdMeshes.push(mesh);
      }
      return createdMeshes;
    },

    // 添加动画循环方法
    startAnimationLoop() {
      const animate = () => {
        this.updateVehicleAnimations();
        this.animationFrameId = requestAnimationFrame(animate);
      };
      this.animationFrameId = requestAnimationFrame(animate);
    },

    // 更新车辆动画状态
    updateVehicleAnimations() {
      const now = Date.now();

      Object.keys(this.vehiclePositions).forEach(vehicleId => {
        const positionData = this.vehiclePositions[vehicleId];

        // 如果没有正在进行的动画或者动画已经完成,则跳过
        if (!positionData || !positionData.animating) {
          return;
        }

        // 计算动画进度
        const elapsed = now - positionData.startTime;
        const duration = positionData.duration;
        let progress = Math.min(elapsed / duration, 1);

        // 如果动画已经完成
        if (progress >= 1) {
          // 设置到最终位置
          const meshes = this.vehicleMeshes[vehicleId];
          if (meshes) {
            meshes.forEach(mesh => {
              mesh.position(new this.AMaper.LngLat(
                positionData.targetLng,
                positionData.targetLat,
                positionData.targetAlt
              ));
            });
          }

          // 标记动画完成
          this.vehiclePositions[vehicleId].animating = false;

          // 更新警告标记位置
          this.updateWarningMarkerPosition(vehicleId);
          return;
        }

        // 使用缓动函数使动画更平滑
        progress = this.easeInOutQuad(progress);

        // 计算当前位置
        const currentLng = positionData.startLng + (positionData.targetLng - positionData.startLng) * progress;
        const currentLat = positionData.startLat + (positionData.targetLat - positionData.startLat) * progress;
        const currentAlt = positionData.startAlt + (positionData.targetAlt - positionData.startAlt) * progress;

        // 更新车辆位置
        const meshes = this.vehicleMeshes[vehicleId];
        if (meshes) {
          meshes.forEach(mesh => {
            mesh.position(new this.AMaper.LngLat(currentLng, currentLat, currentAlt));
          });
        }

        // 同步更新警告标记位置
        if (this.vehicleWarningMarkers[vehicleId]) {
          // 获取车辆朝向
          const heading = this.vehicleHeadings[vehicleId] || 0;

          // 计算左上方的偏移量(根据车辆的朝向调整)
          const offsetDistance = 5;
          // 使用三角函数计算经纬度偏移
          const headingRadians = heading * (Math.PI / 180);
          // 向左前方偏移 - 调整角度使其位于左上角
          const offsetAngle = headingRadians + Math.PI * 0.75; // 左前方45度
          const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
          const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;

          this.vehicleWarningMarkers[vehicleId].setPosition([
            currentLng + lngOffset,
            currentLat + latOffset,
            currentAlt
          ]);
        }
      });
    },

    // 缓动函数 - 使动画更平滑
    easeInOutQuad(t) {
      return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
    },

    // 修改 updateVehiclePositions 方法
    async updateVehiclePositions() {
      // 步骤1:获取当前在线车辆ID列表
      const onlineVehicleIds = new Set(this.vehicles.map(v => v.id));

      // 步骤2:清理已离线的车辆模型
      Object.keys(this.vehicleMeshes).forEach(vehicleId => {
        if (!onlineVehicleIds.has(vehicleId)) {
          // 从地图移除模型
          this.vehicleMeshes[vehicleId].forEach(mesh => {
            this.object3Dlayer.remove(mesh);
          });
          // 删除缓存
          delete this.vehicleMeshes[vehicleId];
          // 删除朝向记录
          delete this.vehicleHeadings[vehicleId];
          // 删除位置记录
          delete this.vehiclePositions[vehicleId];
          // 删除警告标记
          if (this.vehicleWarningMarkers[vehicleId]) {
            this.map.remove(this.vehicleWarningMarkers[vehicleId]);
            delete this.vehicleWarningMarkers[vehicleId];
          }
        }
      });

      // 步骤3:更新或创建在线车辆模型
      for (const vehicle of this.vehicles) {
        if (!this.vehicleMeshes[vehicle.id]) {
          const meshes = await this.createMesh();
          // 确保每个子网格都有userData
          meshes.forEach(mesh => {
            mesh.userData = {
              ...mesh.userData,  // 保留原有数据
              vehicleId: vehicle.id
            };
            this.object3Dlayer.add(mesh);
          });
          this.vehicleMeshes[vehicle.id] = meshes;

          // 新创建的车辆模型,设置初始朝向
          if (!this.vehicleHeadings[vehicle.id]) {
            this.vehicleHeadings[vehicle.id] = vehicle.heading;

            // 首次加载时对所有网格应用相同的初始旋转
            meshes.forEach(mesh => {
              // 记录初始旋转角度到mesh的userData中
              mesh.userData.initialRotation = Math.PI + vehicle.heading + 180;
              mesh.rotateZ(mesh.userData.initialRotation);
            });
          }

          // 初始化位置数据,首次不需要动画
          const position = new this.AMaper.LngLat(vehicle.longitude, vehicle.latitude, vehicle.altitude);
          meshes.forEach(mesh => {
            mesh.position(position);
          });

          this.vehiclePositions[vehicle.id] = {
            startLng: vehicle.longitude,
            startLat: vehicle.latitude,
            startAlt: vehicle.altitude,
            targetLng: vehicle.longitude,
            targetLat: vehicle.latitude,
            targetAlt: vehicle.altitude,
            startTime: Date.now(),
            duration: 1000, // 1秒动画
            animating: false
          };

          // 关键改动:立即检查并创建警告标记(不等待下一次更新)
          if (this.isVehicleWarning(vehicle)) {
            this.createWarningMarker(vehicle);
          }

          continue; // 跳过首次加载的车辆动画
        }

        // 确保有车辆朝向记录
        if (!this.vehicleHeadings[vehicle.id]) {
          this.vehicleHeadings[vehicle.id] = vehicle.heading;
        }

        // 计算需要旋转的角度差
        const currentHeading = vehicle.heading;
        const prevHeading = this.vehicleHeadings[vehicle.id];
        const rotationDelta = currentHeading - prevHeading;

        // 获取车辆当前位置信息
        let positionData = this.vehiclePositions[vehicle.id];

        // 如果没有位置信息,初始化它
        if (!positionData) {
          const meshes = this.vehicleMeshes[vehicle.id];
          if (meshes && meshes.length > 0) {
            const position = meshes[0].position();
            positionData = {
              startLng: position.lng,
              startLat: position.lat,
              startAlt: position.alt || 0,
              targetLng: vehicle.longitude,
              targetLat: vehicle.latitude,
              targetAlt: vehicle.altitude,
              startTime: Date.now(),
              duration: 1000, // 1秒动画
              animating: true
            };
            this.vehiclePositions[vehicle.id] = positionData;
          }
        } else {
          // 更新现有的位置信息,开始新的动画
          // 如果位置变化明显,则开始动画
          const distanceThreshold = 0.0000001; // 可以根据需要调整阈值
          const significantChange =
            Math.abs(positionData.targetLng - vehicle.longitude) > distanceThreshold ||
            Math.abs(positionData.targetLat - vehicle.latitude) > distanceThreshold;

          if (significantChange) {
            // 如果当前有动画正在进行,使用当前实际位置作为起点
            if (positionData.animating) {
              const meshes = this.vehicleMeshes[vehicle.id];
              if (meshes && meshes.length > 0) {
                const currentPos = meshes[0].position();
                positionData.startLng = currentPos.lng;
                positionData.startLat = currentPos.lat;
                positionData.startAlt = currentPos.alt || 0;
              }
            } else {
              // 否则使用上一个目标位置作为起点
              positionData.startLng = positionData.targetLng;
              positionData.startLat = positionData.targetLat;
              positionData.startAlt = positionData.targetAlt;
            }

            // 设置新的目标位置
            positionData.targetLng = vehicle.longitude;
            positionData.targetLat = vehicle.latitude;
            positionData.targetAlt = vehicle.altitude;
            positionData.startTime = Date.now();
            positionData.animating = true;
          }
        }

        // 只有当朝向有明显变化时才进行旋转
        if (Math.abs(rotationDelta) > 0.001) {
          // 更新旋转
          const meshes = this.vehicleMeshes[vehicle.id];
          meshes.forEach(mesh => {
            mesh.userData.vehicleId = vehicle.id; // 确保ID正确
            // 计算实际需要的旋转角度
            const targetRotation = Math.PI + currentHeading + 180;
            const currentRotation = mesh.userData.initialRotation || 0;
            const actualRotationDelta = targetRotation - currentRotation;
            mesh.rotateZ(actualRotationDelta);
            // 更新初始旋转角度
            mesh.userData.initialRotation = targetRotation;
          });

          // 更新朝向记录
          this.vehicleHeadings[vehicle.id] = currentHeading;
        }

        // 更新车辆警告标记
        const isWarning = this.isVehicleWarning(vehicle);
        const hasWarning = !!this.vehicleWarningMarkers[vehicle.id];

        if (isWarning && !hasWarning) {
          // 新增警告情况:创建警告标记
          this.createWarningMarker(vehicle);
        } else if (!isWarning && hasWarning) {
          // 警告解除:移除警告标记
          this.map.remove(this.vehicleWarningMarkers[vehicle.id]);
          delete this.vehicleWarningMarkers[vehicle.id];

          // 同时移除连接线(如果存在)
          if (this.vehicleLineMarkers && this.vehicleLineMarkers[vehicle.id]) {
            this.map.remove(this.vehicleLineMarkers[vehicle.id]);
            delete this.vehicleLineMarkers[vehicle.id];
          }
        } else if (isWarning && hasWarning) {
          // 警告继续存在:更新位置
          this.updateWarningMarkerPosition(vehicle.id);
        }
      }
    },

    // 新增方法:创建警告标记(简化DOM结构,提高渲染速度)
    createWarningMarker(vehicle) {
      if (!vehicle || !this.vehicleMeshes[vehicle.id]) return;

      // 获取车辆模型位置
      const meshes = this.vehicleMeshes[vehicle.id];
      if (meshes && meshes.length > 0) {
        const mesh = meshes[0];
        const position = mesh.position();

        // 获取车辆朝向
        const heading = this.vehicleHeadings[vehicle.id] || 0;

        // 计算左上方的偏移量
        const offsetDistance = 5;
        const headingRadians = heading * (Math.PI / 180);
        const offsetAngle = headingRadians + Math.PI * 0.75;
        const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
        const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;

        // 创建警告标记(恢复高度效果)
        const warningMarker = new this.AMaper.Marker({
          position: [
            position.lng + lngOffset,
            position.lat + latOffset,
            position.alt
          ],
          content: `
            <div style="
              width: 36px; 
              height: 36px; 
              background-color: #ff0000; 
              border-radius: 50%; 
              display: flex; 
              justify-content: center; 
              align-items: center; 
              color: white; 
              font-weight: bold; 
              font-size: 24px;
              box-shadow: 0 0 10px #ff0000, 0 0 20px rgba(255,0,0,0.5);
              border: 2px solid white;
              position: relative;
              margin-top: -60px; /* 通过CSS上移,造成高度效果 */
              transform: translateY(-30px);
              text-align: center;
              line-height: 36px;
            ">
              <span style="display: inline-block; position: relative; top: -1px;">!</span>
              <div style="
                position: absolute;
                bottom: -60px;
                left: 50%;
                width: 2px;
                height: 60px;
                background: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,0,0,0));
                transform: translateX(-50%);
              "></div>
            </div>
          `,
          offset: new this.AMaper.Pixel(-18, -18),
          zIndex: 999
        });

        this.map.add(warningMarker);
        this.vehicleWarningMarkers[vehicle.id] = warningMarker;
      }
    },
    async fetchVehicleData() {
      try {
        const response = await listOnlineVehicles();
        // 添加过滤条件:只保留 online 为 true 的车辆
        this.vehicles = response.data.filter(v => v.online === true);
        this.allVehicles = response.data.filter(v => v.online !== null);

        // 如果当前有选中的车辆,更新 selectedVehicle 的数据
        if (this.selectedVehicle) {
          const latestVehicle = this.vehicles.find(v => v.id === this.selectedVehicle.id);
          if (latestVehicle) {
            this.selectedVehicle = latestVehicle; // 更新 selectedVehicle
            this.checkVehicleOrders(); // 刷新车辆订单状态
          } else {
            // 如果选中的车辆已离线,关闭弹窗并清除轨迹
            this.isPopupVisible = false;
            this.selectedVehicle = null;
            this.hasActiveOrder = false;

            // 清除轨迹和标记
            if (this.currentTrack) {
              this.map.remove(this.currentTrack);
              this.currentTrack = null;
            }

            if (this.currentTrackMarkers) {
              this.currentTrackMarkers.forEach(marker => {
                this.map.remove(marker);
              });
              this.currentTrackMarkers = null;
            }
          }
        }

        this.updateVehiclePositions();
      } catch (error) {
        console.error('Failed to fetch onlineVehicle data:', error);
      }
    },
    // 修改点击事件处理方法
    async handleMapClick(e) {
      if (this.isLoadingModels) {
        console.warn('模型尚未加载完成');
        return;
      }
      try {
        const px = new this.AMaper.Pixel(e.pixel.x, e.pixel.y);
        const obj = this.map.getObject3DByContainerPos(px, [this.object3Dlayer], false);

        // 先移除之前的选中效果
        this.removeSelectionEffect();

        if (!obj || !obj.object) {
          console.log('未点击到任何模型');
          return;
        }

        // 遍历对象层级查找vehicleId
        let currentObj = obj.object;
        while (currentObj) {
          if (currentObj.userData?.vehicleId) {
            const vehicleId = currentObj.userData.vehicleId;
            const vehicle = this.vehicles.find(v => v.id === vehicleId);

            if (vehicle) {
              this.selectedVehicle = vehicle;
              this.isPopupVisible = true;

              // 添加选中效果
              this.addSelectionEffect(vehicleId);

              // 调用 getTrack 方法并传递 vehicleId
              const response = await this.getTrack({ vehicleId });

              // 移除之前的轨迹
              if (this.currentTrack) {
                this.map.remove(this.currentTrack);
                this.currentTrack = null;
              }

              // 验证轨迹数据格式
              const path = response.data;
              if (!path || !Array.isArray(path) || path.length === 0) {
                console.warn('轨迹数据为空或格式不正确');
                return;
              }

              // 验证每个点是否有正确的经纬度
              const validPath = path.filter(point =>
                Array.isArray(point) &&
                point.length === 2 &&
                typeof point[0] === 'number' &&
                typeof point[1] === 'number'
              );

              if (validPath.length === 0) {
                console.warn('没有有效的轨迹点');
                return;
              }

              // 创建新的轨迹
              this.currentTrack = new this.AMaper.Polyline({
                path: validPath,
                strokeColor: "#0066FF",
                strokeWeight: 6,
                strokeOpacity: 0.8,
                lineJoin: 'round',
                lineCap: 'round',
                showDir: true,
                strokeStyle: 'dashed',
                strokeDasharray: [8, 4],
                outlineColor: '#FFFFFF',
                outlineWeight: 2,
                borderWeight: 3, // 添加边框
                dirColor: '#ff6a00', // 使用亮橙色箭头,更加明显
                zIndex: 120 // 确保箭头在其他元素上方
              });

              // 在轨迹上添加起点和终点标记
              if (validPath.length > 0) {
                // 添加起点标记
                const startMarker = new this.AMaper.Marker({
                  position: validPath[0],
                  content: '<div style="background-color: #1890FF; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
                  offset: new this.AMaper.Pixel(-6, -6),
                  zIndex: 121
                });

                // 添加终点标记
                const endMarker = new this.AMaper.Marker({
                  position: validPath[validPath.length - 1],
                  content: '<div style="background-color: #ff6a00; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
                  offset: new this.AMaper.Pixel(-6, -6),
                  zIndex: 121
                });

                this.map.add([this.currentTrack, startMarker, endMarker]);

                // 存储标记,以便稍后移除
                this.currentTrackMarkers = [startMarker, endMarker];
              } else {
                this.map.add(this.currentTrack);
              }

              return;
            } else {
              console.log(`未找到 ID 为 ${vehicleId} 的车辆信息`);
            }
          }
          currentObj = currentObj.parent;
        }

        console.log('点击的模型未关联车辆信息');
      } catch (error) {
        console.error('点击处理发生错误:', error);
      }
    },
    handleMapMove(e) {
      const px = new this.AMaper.Pixel(e.pixel.x, e.pixel.y);
      const obj = this.map.getObject3DByContainerPos(px, [this.object3Dlayer], false) || {};

      if (obj.object) {
        this.map.setDefaultCursor('pointer');
      } else {
        this.map.setDefaultCursor('default');
      }
    },
    toggleRightPopup() {
      this.isRightPopupExpanded = !this.isRightPopupExpanded;
    },
    getTrack(vehicleId) {
      return getTrack(vehicleId);
    },
    handlePopupClose() {
      this.isPopupVisible = false;
      // 移除选中效果
      this.removeSelectionEffect();
      // 隐藏轨迹
      if (this.currentTrack) {
        this.map.remove(this.currentTrack);
        this.currentTrack = null;
      }
      // 隐藏轨迹标记
      if (this.currentTrackMarkers) {
        this.currentTrackMarkers.forEach(marker => {
          this.map.remove(marker);
        });
        this.currentTrackMarkers = null;
      }
    },
    // 处理右侧组件选中车辆事件
    async handleVehicleSelect(vehicle) {
      // 从 vehicles 数组中获取最新的车辆数据
      const latestVehicle = this.vehicles.find(v => v.id === vehicle.id);
      if (!latestVehicle) {
        console.error('未找到对应的车辆信息');
        return;
      }

      // 先移除之前的选中效果
      this.removeSelectionEffect();

      this.selectedVehicle = latestVehicle; // 使用最新的车辆数据
      this.isPopupVisible = true; // 弹出左侧弹窗

      // 添加选中效果
      this.addSelectionEffect(latestVehicle.id);

      // 找到对应的车辆模型
      const meshes = this.vehicleMeshes[latestVehicle.id];
      if (meshes && meshes.length > 0) {
        // 聚焦到该模型的位置
        const mesh = meshes[0]; // 取第一个子网格
        const position = mesh.position(); // 获取模型的位置
        this.map.setCenter([position.lng, position.lat]); // 设置地图中心点
        this.map.setZoom(19); // 调整缩放级别
        this.map.setPitch(60); // 调整俯仰角
      }

      // 更新车辆实时轨迹
      await this.updateVehicleTrack({ "vehicleId": latestVehicle.id });

      // 检查车辆订单状态
      await this.checkVehicleOrders();
    },

    // 更新车辆轨迹
    async updateVehicleTrack(vehicleId) {
      try {
        // 调用 API 获取轨迹数据
        const response = await this.getTrack(vehicleId);

        // 移除之前的轨迹
        if (this.currentTrack) {
          this.map.remove(this.currentTrack);
          this.currentTrack = null;
        }

        // 验证轨迹数据格式
        const path = response.data;
        if (!path || !Array.isArray(path) || path.length === 0) {
          console.warn('轨迹数据为空或格式不正确');
          return;
        }

        // 验证每个点是否有正确的经纬度
        const validPath = path.filter(point =>
          Array.isArray(point) &&
          point.length === 2 &&
          typeof point[0] === 'number' &&
          typeof point[1] === 'number'
        );

        if (validPath.length === 0) {
          console.warn('没有有效的轨迹点');
          return;
        }

        // 创建新的轨迹
        this.currentTrack = new this.AMaper.Polyline({
          path: validPath,
          strokeColor: "#0066FF",
          strokeWeight: 6,
          strokeOpacity: 0.8,
          lineJoin: 'round',
          lineCap: 'round',
          showDir: true,
          strokeStyle: 'dashed',
          strokeDasharray: [8, 4],
          outlineColor: '#FFFFFF',
          outlineWeight: 2,
          borderWeight: 3, // 添加边框
          dirColor: '#ff6a00', // 使用亮橙色箭头,更加明显
          zIndex: 120 // 确保箭头在其他元素上方
        });

        // 在轨迹上添加起点和终点标记
        if (validPath.length > 0) {
          // 添加起点标记
          const startMarker = new this.AMaper.Marker({
            position: validPath[0],
            content: '<div style="background-color: #1890FF; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
            offset: new this.AMaper.Pixel(-6, -6),
            zIndex: 121
          });

          // 添加终点标记
          const endMarker = new this.AMaper.Marker({
            position: validPath[validPath.length - 1],
            content: '<div style="background-color: #ff6a00; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
            offset: new this.AMaper.Pixel(-6, -6),
            zIndex: 121
          });

          this.map.add([this.currentTrack, startMarker, endMarker]);

          // 存储标记,以便稍后移除
          this.currentTrackMarkers = [startMarker, endMarker];
        } else {
          this.map.add(this.currentTrack);
        }
      } catch (error) {
        console.error('Failed to update vehicle track:', error);
      }
    },

    clearRightSelection() {
      this.selectedVehicle = null; // 清除选中的车辆,从而清除右侧组件的选择状态
    },

    handleUseVehicle() {
      if (!this.selectedVehicle) return;
      // 显示用车弹窗
      this.isUseVehiclePopupVisible = true;
    },

    // 处理用车弹窗关闭事件
    handleUseVehiclePopupClose() {
      this.isUseVehiclePopupVisible = false;
      this.checkVehicleOrders(); // 刷新车辆订单状态
    },

    // 处理任务按钮点击事件
    handleTaskManage() {
      if (!this.selectedVehicle) return;
      this.isTaskPopupVisible = true;
    },

    // 处理任务弹窗关闭事件
    handleTaskPopupClose() {
      this.isTaskPopupVisible = false;
      this.checkVehicleOrders(); // 刷新车辆订单状态
    },

    // 检查当前选中车辆是否有活动订单
    async checkVehicleOrders() {
      if (!this.selectedVehicle) return;

      try {
        const response = await orderByVehicleId(this.selectedVehicle.id);
        if (response.code === 200 && response.data.length > 0) {
          // 查找是否有进行中的订单
          const activeOrder = response.data.find(order => order.status === 10);
          this.hasActiveOrder = !!activeOrder;
        } else {
          this.hasActiveOrder = false;
        }
      } catch (error) {
        console.error('获取车辆订单失败:', error);
        this.hasActiveOrder = false;
      }
    },

    // 修改 adjustMapLogo 方法
    adjustMapLogo() {
      // 给地图容器添加一个延时,确保地图元素已完全加载
      setTimeout(() => {
        // 查找高德地图的logo元素
        const logoElements = document.querySelectorAll('.amap-logo,.amap-copyright');

        logoElements.forEach(element => {
          // 将水印移到左下角固定位置,便于用按钮遮挡
          element.style.bottom = '10px';
          element.style.left = '10px';
          element.style.transform = 'scale(0.7)';
          element.style.opacity = '0.3';
          element.style.zIndex = '100';
        });

        // 调整恢复定位按钮位置,使其遮挡水印
        const resetButton = document.getElementById('reset-camera-button');
        if (resetButton) {
          resetButton.style.left = '10px';
          resetButton.style.bottom = '10px';
        }
      }, 1000);
    },

    // 添加选中效果
    addSelectionEffect(vehicleId) {
      // 先移除之前的效果
      this.removeSelectionEffect();

      // 获取车辆模型位置
      const meshes = this.vehicleMeshes[vehicleId];
      if (!meshes || meshes.length === 0) return;

      const mesh = meshes[0];
      const position = mesh.position();

      // 创建扩散效果
      const radius = 10; // 初始半径(米)
      const maxRadius = 40; // 最大半径(米)
      const ringCount = 4; // 同时显示的环数
      const effects = [];

      for (let i = 0; i < ringCount; i++) {
        const circle = new this.AMaper.Circle({
          center: [position.lng, position.lat],
          radius: radius + (i * (maxRadius - radius) / ringCount),
          strokeColor: '#1890FF',
          strokeWeight: 3,
          strokeOpacity: 0.8 - (i * 0.2),
          fillColor: '#1890FF',
          fillOpacity: 0.4 - (i * 0.1),
          zIndex: 99 - i,
        });
        this.map.add(circle);
        effects.push(circle);
      }

      // 存储效果对象和数据
      this.selectionEffect = {
        circles: effects,
        vehicleId: vehicleId,
        animation: {
          startRadius: radius,
          maxRadius: maxRadius,
          currentPhase: 0,
          ringCount: ringCount,
          lastUpdateTime: Date.now(),
          animationSpeed: 0.3 // 控制动画速度
        }
      };

      // 开始动画
      this.animateSelectionEffect();
    },

    // 移除选中效果
    removeSelectionEffect() {
      if (this.selectionEffect) {
        // 移除所有圆形
        this.selectionEffect.circles.forEach(circle => {
          this.map.remove(circle);
        });
        this.selectionEffect = null;
      }
    },

    // 更新选中效果动画 - 修复闪烁问题
    animateSelectionEffect() {
      if (!this.selectionEffect) return;

      const effect = this.selectionEffect;
      const now = Date.now();
      const delta = (now - effect.animation.lastUpdateTime) / 1000; // 秒
      effect.animation.lastUpdateTime = now;

      // 更新动画阶段
      effect.animation.currentPhase = (effect.animation.currentPhase + delta * effect.animation.animationSpeed) % 1;

      // 检查车辆是否仍然存在
      const vehicleId = effect.vehicleId;
      const meshes = this.vehicleMeshes[vehicleId];
      if (!meshes || meshes.length === 0) {
        this.removeSelectionEffect();
        return;
      }

      // 获取车辆当前位置
      const mesh = meshes[0];
      const position = mesh.position();

      // 使用循环模式,让圆环连续出现
      for (let i = 0; i < effect.animation.ringCount; i++) {
        // 计算每个环的相位偏移,确保均匀分布
        const phaseOffset = i / effect.animation.ringCount;

        // 添加偏移量到当前相位
        let phase = (effect.animation.currentPhase + phaseOffset) % 1;

        // 计算当前半径
        const radiusDelta = effect.animation.maxRadius - effect.animation.startRadius;
        const currentRadius = effect.animation.startRadius + (phase * radiusDelta);

        // 计算不透明度 - 关键修改:使用平滑的不透明度曲线
        let opacity;
        if (phase < 0.1) {
          // 淡入阶段 (0-10%)
          opacity = phase * 10 * 0.8; // 从0逐渐增加到0.8
        } else if (phase > 0.7) {
          // 淡出阶段 (70-100%)
          const fadeOutPhase = (phase - 0.7) / 0.3; // 归一化到0-1
          opacity = 0.8 * (1 - fadeOutPhase); // 从0.8逐渐减少到0
        } else {
          // 正常显示阶段 (10-70%)
          opacity = 0.8;
        }

        // 确保在相位接近1时完全透明,避免闪烁
        if (phase > 0.95) {
          opacity = 0;
        }

        // 更新圆的位置、大小和不透明度
        const circle = effect.circles[i];
        circle.setCenter([position.lng, position.lat]);
        circle.setRadius(currentRadius);
        circle.setOptions({
          strokeOpacity: opacity,
          fillOpacity: opacity / 2
        });
      }

      // 继续动画循环
      requestAnimationFrame(() => this.animateSelectionEffect());
    },

    // 保留缓动函数以备后用
    easeInOutSine(x) {
      return -(Math.cos(Math.PI * x) - 1) / 2;
    },

    // 新增监控相关方法
    // 打开监控弹窗
    openMonitorPopup() {
      if (this.selectedVehicle) {
        this.isMonitorPopupVisible = true;
      }
    },

    // 关闭监控弹窗
    closeMonitorPopup() {
      this.isMonitorPopupVisible = false;
    },

    // 判断车辆是否处于警告状态
    isVehicleWarning(vehicle) {
      if (!vehicle) return false;

      // 检查符合警告条件的情况
      return vehicle.drivingCmdControlMode === -1 ||
        vehicle.driveSystemError === 1 ||
        vehicle.driveSystemError === 2 ||
        vehicle.errorPowerBattery === 1 ||
        vehicle.errorMotor === 1 ||
        vehicle.errorDc === 1;
    },

    // 更新警告标记位置
    updateWarningMarkerPosition(vehicleId) {
      const warningMarker = this.vehicleWarningMarkers[vehicleId];
      if (!warningMarker) return;

      const meshes = this.vehicleMeshes[vehicleId];
      if (meshes && meshes.length > 0) {
        const mesh = meshes[0];
        const position = mesh.position();

        // 获取车辆朝向
        const heading = this.vehicleHeadings[vehicleId] || 0;

        // 计算左上方的偏移量(根据车辆的朝向调整)
        const offsetDistance = 5;
        // 使用三角函数计算经纬度偏移,左上角相对于车辆头部位置
        const headingRadians = heading * (Math.PI / 180);
        // 向左前方偏移 - 调整角度使其位于左上角
        const offsetAngle = headingRadians + Math.PI * 0.75; // 左前方45度
        const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
        const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;

        warningMarker.setPosition([
          position.lng + lngOffset,
          position.lat + latOffset,
          position.alt
        ]);
      }
    },

    // 更新或添加车辆警告标记(保留此方法以兼容可能的其他调用)
    updateVehicleWarningMarker(vehicle) {
      if (!vehicle || !this.vehicleMeshes[vehicle.id]) return;

      const isWarning = this.isVehicleWarning(vehicle);
      const hasWarning = !!this.vehicleWarningMarkers[vehicle.id];

      // 简化判断逻辑
      if (isWarning && !hasWarning) {
        // 需要警告但没有标记:创建新标记
        this.createWarningMarker(vehicle);
      } else if (!isWarning && hasWarning) {
        // 不需要警告但有标记:移除标记
        this.map.remove(this.vehicleWarningMarkers[vehicle.id]);
        delete this.vehicleWarningMarkers[vehicle.id];

        // 同时移除连接线(如果存在)
        if (this.vehicleLineMarkers && this.vehicleLineMarkers[vehicle.id]) {
          this.map.remove(this.vehicleLineMarkers[vehicle.id]);
          delete this.vehicleLineMarkers[vehicle.id];
        }
      } else if (isWarning && hasWarning) {
        // 需要警告且已有标记:更新位置
        this.updateWarningMarkerPosition(vehicle.id);
      }
    },
  },
};
</script>

<style>
#container {
  padding: 0px;
  margin: 0px;
  width: 100%;
  height: 90.8vh;
}

/* 在样式部分添加信息卡片样式 */
.vehicle-info-card {
  padding: 15px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

/* 美化恢复定位按钮 */
#reset-camera-button {
  position: absolute;
  bottom: 10px;
  left: 10px;
  z-index: 1001;
  /* 确保按钮在水印上方 */
  padding: 10px 15px;
  background-color: rgba(52, 152, 219, 0.9);
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 100px;
  /* 确保按钮足够宽以遮挡水印 */
  min-height: 36px;
  /* 确保按钮足够高以遮挡水印 */
}

#reset-camera-button:hover {
  background-color: rgba(41, 128, 185, 1);
  transform: scale(1.05);
}

#reset-camera-button:before {
  content: "↻";
  margin-right: 5px;
  font-size: 16px;
}

#use-vehicle-button {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 1000;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background-color: #4CAF50;
  color: white;
  border: none;
  font-size: 16px;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}

#use-vehicle-button:hover:not(.disabled) {
  background-color: #45a049;
  transform: scale(1.05);
}

#use-vehicle-button.disabled {
  background-color: #95a5a6;
  cursor: not-allowed;
  opacity: 0.7;
}

/* 任务按钮样式 */
#task-button {
  position: fixed;
  bottom: 20px;
  right: 90px;
  /* 位于派单按钮左侧 */
  z-index: 1000;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background-color: #3498db;
  color: white;
  border: none;
  font-size: 16px;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}

#task-button:hover {
  background-color: #2980b9;
  transform: scale(1.05);
}

/* 调整高德地图水印样式 */
.amap-logo {
  opacity: 0.3 !important;
  transform: scale(0.7) !important;
}

.amap-copyright {
  opacity: 0.3 !important;
  transform: scale(0.7) !important;
}

/* 监控按钮样式 */
#monitor-button {
  position: fixed;
  bottom: 20px;
  /* 位于任务按钮左侧 */
  z-index: 1000;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background-color: #e67e22;
  color: white;
  border: none;
  font-size: 16px;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}

#monitor-button:hover {
  background-color: #d35400;
  transform: scale(1.05);
}
</style>

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐