高德地图与Three.js 3D模型集成指南
本文档详细介绍了如何在高德地图中集成和使用Three.js的3D模型,涵盖了从模型加载、渲染、交互到动画的全过程。通过遵循这些实践,可以实现高效且美观的3D地图交互效应。主要知识点包括:模型加载与材质应用坐标系转换与对齐模型位置和旋转更新动画效果实现交互事件处理视觉反馈效果性能优化和资源管理请根据项目需求灵活运用这些技术,创建出更加丰富的3D地图可视化应用。
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效果 -
通过
AmbientLight
和DirectionLight
设置场景光照,对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;
}
关键概念解析:
-
网格结构:
-
每个3D模型由多个网格组成,存储在
event.children
中 -
每个网格包含几何数据和材质数据
-
-
坐标系调整:
-
Three.js和高德地图使用不同的坐标系
-
代码中计算Y轴最大值并应用偏移,确保模型正确放置
-
-
网格数据处理:
-
vecticesF3
:顶点位置数据 -
vecticesNormal3
:法线数据,用于光照计算 -
vecticesUV2
:UV贴图坐标,用于纹理映射
-
-
创建高德地图兼容的网格:
-
使用
AMaper.Object3D.MeshAcceptLights()
创建可接收光照的网格 -
转换并添加顶点、法线和UV数据
-
设置材质颜色和透明度
-
-
应用纹理和缩放:
-
如果材质有贴图,添加纹理
-
设置深度测试和透明度属性
-
应用缩放(
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; // 跳过首次加载的车辆动画
}
关键概念:
-
模型管理生命周期:
-
对离线车辆进行清理,释放资源
-
对新上线的车辆创建新模型
-
对已有车辆更新位置和朝向
-
-
userData存储自定义数据:
-
使用
mesh.userData
存储车辆ID和初始旋转角度 -
利用Three.js提供的这一机制,可以在模型中附加任意自定义数据
-
-
多层次对象管理:
-
使用多个对象存储不同数据:
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地图交互效应。
主要知识点包括:
-
模型加载与材质应用
-
坐标系转换与对齐
-
模型位置和旋转更新
-
动画效果实现
-
交互事件处理
-
视觉反馈效果
-
性能优化和资源管理
最后附上项目源码:
<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>
更多推荐
所有评论(0)