物流轨迹地图
【代码】物流轨迹地图。
·
<template>
<view class="logistics-detail-page">
<map
name="logistics-map"
class="logistics-map"
:scale="mapScale"
:longitude="mapCenter.longitude"
:latitude="mapCenter.latitude"
:markers="markers"
:polyline="polylines"
:show-location="true"
>
</map>
<!-- 物流跟踪信息 -->
<view class="logistics-timeline">
<view class="timeline-header">
<text class="header-title">物流跟踪</text>
</view>
<view class="timeline-list">
<view
v-for="(item, index) in logisticsData"
:key="index"
class="timeline-item"
:class="{ 'is-first': index === 0 }"
>
<view class="timeline-dot"></view>
<view class="timeline-content">
<view class="content-header">
<text class="status-text">{{ item.status }}</text>
<text class="time-text">{{ item.time }}</text>
</view>
<view class="content-body">
<text class="context-text">{{ item.context }}</text>
<text v-if="item.areaName" class="area-text">{{ item.areaName }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import logisticsJson from './aaa.json';
interface LogisticsItem {
time: string;
context: string;
ftime: string;
areaCode: string | null;
areaName: string | null;
status: string;
location: string;
areaCenter: string | null;
areaPinYin: string | null;
statusCode: string;
}
const logisticsData = ref<LogisticsItem[]>([]);
const markers = ref<any[]>([]);
const polylines = ref<any[]>([]);
const mapCenter = ref({
longitude: 116.397428,
latitude: 39.90923
});
const mapScale = ref(10);
// 坐标系转换工具函数
const CoordTransform = {
// WGS84转GCJ02(GPS坐标转火星坐标)
wgs84ToGcj02(lng: number, lat: number): [number, number] {
const PI = 3.1415926535897932384626;
const a = 6378245.0;
const ee = 0.00669342162296594323;
if (this.outOfChina(lng, lat)) {
return [lng, lat];
}
let dLat = this.transformLat(lng - 105.0, lat - 35.0);
let dLng = this.transformLng(lng - 105.0, lat - 35.0);
const radLat = lat / 180.0 * PI;
let magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
const sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * PI);
dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * PI);
const mgLat = lat + dLat;
const mgLng = lng + dLng;
return [mgLng, mgLat];
},
// GCJ02转WGS84(火星坐标转GPS坐标)
gcj02ToWgs84(lng: number, lat: number): [number, number] {
const [mgLng, mgLat] = this.wgs84ToGcj02(lng, lat);
return [lng * 2 - mgLng, lat * 2 - mgLat];
},
// 判断是否在中国境外
outOfChina(lng: number, lat: number): boolean {
return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271;
},
// 纬度转换
transformLat(lng: number, lat: number): number {
let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lat * Math.PI) + 40.0 * Math.sin(lat / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(lat / 12.0 * Math.PI) + 320 * Math.sin(lat * Math.PI / 30.0)) * 2.0 / 3.0;
return ret;
},
// 经度转换
transformLng(lng: number, lat: number): number {
let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lng * Math.PI) + 40.0 * Math.sin(lng / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(lng / 12.0 * Math.PI) + 300.0 * Math.sin(lng / 30.0 * Math.PI)) * 2.0 / 3.0;
return ret;
}
};
// 处理坐标数据,确保坐标系正确
const processCoordinates = (areaCenter: string): { longitude: number; latitude: number } | null => {
if (!areaCenter) return null;
const [lngStr, latStr] = areaCenter.split(',');
const originalLng = parseFloat(lngStr);
const originalLat = parseFloat(latStr);
if (isNaN(originalLng) || isNaN(originalLat)) return null;
console.log('原始坐标:', originalLng, originalLat);
// 判断坐标系类型
// 根据坐标范围判断:中国境内的坐标很可能已经是GCJ02坐标系
// 经度范围:73-135,纬度范围:18-54
const isInChina = originalLng >= 73 && originalLng <= 135 && originalLat >= 18 && originalLat <= 54;
let correctedLng = originalLng;
let correctedLat = originalLat;
if (isInChina) {
// 如果在中国境内,假设数据已经是GCJ02坐标系,直接使用
console.log('检测到中国境内坐标,假设为GCJ02坐标系,直接使用');
} else {
// 如果不在中国境内,可能是WGS84坐标,转换为GCJ02
console.log('检测到境外坐标,从WGS84转换为GCJ02');
[correctedLng, correctedLat] = CoordTransform.wgs84ToGcj02(originalLng, originalLat);
}
console.log('最终使用坐标:', correctedLng, correctedLat);
return {
longitude: correctedLng,
latitude: correctedLat
};
};
// 处理物流数据
const processLogisticsData = async () => {
logisticsData.value = logisticsJson as LogisticsItem[];
// 按时间倒序排列(最新的在前面,但轨迹要从最早开始)
const sortedData = [...logisticsData.value].reverse();
// 提取有坐标的数据点,按时间顺序
const validPoints = sortedData.filter(item =>
item.areaCenter && item.areaCenter !== null
);
if (validPoints.length === 0) return;
// 创建地图标记点
const markerList: any[] = [];
const coordinateList: any[] = [];
validPoints.forEach((item, index) => {
if (item.areaCenter) {
const coords = processCoordinates(item.areaCenter);
if (!coords) return;
const { longitude, latitude } = coords;
// 确定标记点类型和样式
let iconPath = '/static/location-icon.svg';
let markerColor = '#1890ff';
if (index === 0) {
// 起点(最早的时间点)
iconPath = '/static/start-marker.svg';
markerColor = '#52c41a';
} else if (index === validPoints.length - 1) {
// 终点(最新的时间点)
iconPath = '/static/end-marker.svg';
markerColor = '#f5222d';
}
// 添加标记点
markerList.push({
id: index,
longitude,
latitude,
title: item.location,
iconPath,
width: 32,
height: 32,
callout: {
content: `${item.areaName || item.location}\n${item.time}`,
color: '#333333',
fontSize: 12,
borderRadius: 6,
bgColor: '#ffffff',
padding: 10,
display: 'BYCLICK',
textAlign: 'center'
}
});
// 添加坐标点用于绘制路线
coordinateList.push({
longitude,
latitude
});
}
});
markers.value = markerList;
// 基于路网绘制轨迹
if (coordinateList.length > 1) {
await drawRouteBasedOnRoadNetwork(coordinateList);
}
// 计算地图中心点和缩放级别
calculateMapBounds(coordinateList);
};
// 基于路网绘制轨迹
const drawRouteBasedOnRoadNetwork = async (coordinates: any[]) => {
console.log('开始绘制路网轨迹,坐标点数量:', coordinates.length);
const polylineList: any[] = [];
try {
// 为每两个相邻点之间规划路径
for (let i = 0; i < coordinates.length - 1; i++) {
const startPoint = coordinates[i];
const endPoint = coordinates[i + 1];
console.log(`规划第${i + 1}段路径:`, startPoint, '->', endPoint);
// 调用路径规划API获取路网路径
const routePoints = await getRoutePoints(startPoint, endPoint);
if (routePoints && routePoints.length > 0) {
console.log(`第${i + 1}段路径获取成功,路径点数量:`, routePoints.length);
// 使用路网路径绘制轨迹
const segmentColor = getSegmentColor(i, coordinates.length - 1);
polylineList.push({
points: routePoints,
color: segmentColor,
width: 6,
dottedLine: false,
arrowLine: i === coordinates.length - 2, // 只在最后一段显示箭头
borderColor: '#ffffff',
borderWidth: 2,
arrowIconPath: '/static/location-icon.svg'
});
} else {
console.log(`第${i + 1}段路径获取失败,使用直线连接`);
// 如果路径规划失败,使用直线连接
const segmentColor = getSegmentColor(i, coordinates.length - 1);
polylineList.push({
points: [startPoint, endPoint],
color: segmentColor,
width: 4,
dottedLine: true, // 用虚线表示非路网路径
arrowLine: false,
borderColor: '#ffffff',
borderWidth: 1
});
}
}
console.log('轨迹绘制完成,polyline数量:', polylineList.length);
polylines.value = polylineList;
} catch (error) {
console.error('路径规划失败,使用直线连接:', error);
// 降级方案:使用直线连接
fallbackToStraightLines(coordinates);
}
};
// 获取两点间的路网路径
const getRoutePoints = async (startPoint: any, endPoint: any): Promise<any[]> => {
try {
console.log('开始获取路网路径:', startPoint, endPoint);
// OSRM使用WGS84坐标系,需要将GCJ02转换为WGS84
// 但如果原始数据已经是GCJ02,我们需要转换
const [startLng, startLat] = CoordTransform.gcj02ToWgs84(startPoint.longitude, startPoint.latitude);
const [endLng, endLat] = CoordTransform.gcj02ToWgs84(endPoint.longitude, endPoint.latitude);
console.log('转换为WGS84坐标用于OSRM路径规划:', { startLng, startLat }, { endLng, endLat });
// 使用OpenStreetMap的免费路径规划服务
const response = await uni.request({
url: 'https://router.project-osrm.org/route/v1/driving/' +
`${startLng},${startLat};${endLng},${endLat}`,
method: 'GET',
data: {
overview: 'full',
geometries: 'geojson',
steps: 'true'
}
});
console.log('OSRM API响应状态:', response.statusCode);
const data = response.data as any;
if (data && data.code === 'Ok' && data.routes && data.routes.length > 0) {
const route = data.routes[0];
if (route.geometry && route.geometry.coordinates) {
const routePoints: any[] = [];
// 转换GeoJSON坐标格式 [lng, lat] 到 {longitude, latitude}
// 并将WGS84坐标转换为GCJ02坐标系用于地图显示
for (const coord of route.geometry.coordinates) {
const [gcjLng, gcjLat] = CoordTransform.wgs84ToGcj02(coord[0], coord[1]);
routePoints.push({
longitude: gcjLng,
latitude: gcjLat
});
}
console.log('成功获取OSRM路网路径点数量:', routePoints.length);
return routePoints;
}
}
console.log('OSRM API失败,尝试高德地图API');
// 如果OSRM失败,尝试使用高德地图API(如果有密钥)
return await getAmapRoutePoints(startPoint, endPoint);
} catch (error) {
console.error('OSRM路径规划失败:', error);
// 尝试高德地图API作为备选
return await getAmapRoutePoints(startPoint, endPoint);
}
};
// 高德地图路径规划(需要API密钥)
const getAmapRoutePoints = async (startPoint: any, endPoint: any): Promise<any[]> => {
try {
console.log('尝试使用高德地图API获取路径');
// 检查是否有有效的API密钥
const amapKey = 'YOUR_AMAP_KEY'; // 请替换为实际的高德地图API密钥
if (amapKey === 'YOUR_AMAP_KEY') {
console.log('高德地图API密钥未配置,跳过');
return [];
}
// 高德地图使用GCJ02坐标系,直接使用转换后的坐标
const response = await uni.request({
url: 'https://restapi.amap.com/v3/direction/driving',
method: 'GET',
data: {
key: amapKey,
origin: `${startPoint.longitude},${startPoint.latitude}`,
destination: `${endPoint.longitude},${endPoint.latitude}`,
output: 'json',
extensions: 'all'
}
});
console.log('高德地图API响应:', response);
const data = response.data as any;
if (data && data.status === '1' && data.route && data.route.paths && data.route.paths.length > 0) {
const path = data.route.paths[0];
if (path.steps && path.steps.length > 0) {
const routePoints: any[] = [];
// 解析每个步骤的polyline
for (const step of path.steps) {
if (step.polyline) {
const decodedPoints = decodePolyline(step.polyline);
routePoints.push(...decodedPoints);
}
}
console.log('高德地图成功获取路网路径点数量:', routePoints.length);
return routePoints;
}
}
console.log('高德地图API返回数据格式不正确或无路径');
return [];
} catch (error) {
console.error('高德地图路径规划失败:', error);
return [];
}
}
// 腾讯地图路径规划API备选方案
const getTencentRoutePoints = async (startPoint: any, endPoint: any): Promise<any[]> => {
try {
const response = await uni.request({
url: 'https://apis.map.qq.com/ws/direction/v1/driving/',
method: 'GET',
data: {
from: `${startPoint.latitude},${startPoint.longitude}`,
to: `${endPoint.latitude},${endPoint.longitude}`,
key: 'YOUR_TENCENT_KEY', // 需要替换为实际的腾讯地图API密钥
output: 'json'
}
});
const data = response.data as any;
if (data && data.status === 0 && data.result && data.result.routes && data.result.routes.length > 0) {
const route = data.result.routes[0];
if (route.polyline) {
return decodePolyline(route.polyline);
}
}
return [];
} catch (error) {
console.error('腾讯地图路径规划失败:', error);
return [];
}
};
// 解析高德地图polyline格式
const parsePolyline = (polyline: string): any[] => {
const points: any[] = [];
const coordinates = polyline.split(';');
for (const coord of coordinates) {
const [lng, lat] = coord.split(',').map(Number);
if (!isNaN(lng) && !isNaN(lat)) {
points.push({
longitude: lng,
latitude: lat
});
}
}
return points;
};
// 解码腾讯地图polyline编码
const decodePolyline = (encoded: string): any[] => {
const points: any[] = [];
let index = 0;
let lat = 0;
let lng = 0;
while (index < encoded.length) {
let b;
let shift = 0;
let result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
const dlat = ((result & 1) !== 0 ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
const dlng = ((result & 1) !== 0 ? ~(result >> 1) : (result >> 1));
lng += dlng;
points.push({
latitude: lat / 1e5,
longitude: lng / 1e5
});
}
return points;
};
// 降级方案:使用直线连接
const fallbackToStraightLines = (coordinates: any[]) => {
console.log('使用直线连接降级方案');
const polylineList: any[] = [];
// 创建一条连接所有点的主轨迹线
polylineList.push({
points: coordinates,
color: '#1890ff',
width: 5,
dottedLine: false,
arrowLine: true,
borderColor: '#ffffff',
borderWidth: 2
});
// 为每段添加渐变色效果
for (let i = 0; i < coordinates.length - 1; i++) {
const segmentColor = getSegmentColor(i, coordinates.length - 1);
polylineList.push({
points: [coordinates[i], coordinates[i + 1]],
color: segmentColor,
width: 4,
dottedLine: false,
arrowLine: i === coordinates.length - 2,
borderColor: '#ffffff',
borderWidth: 1
});
}
polylines.value = polylineList;
};
// 获取路段颜色(从绿色到红色的渐变)
const getSegmentColor = (index: number, total: number): string => {
const ratio = index / total;
if (ratio < 0.5) {
// 绿色到黄色
const r = Math.floor(255 * ratio * 2);
return `rgb(${r}, 255, 0)`;
} else {
// 黄色到红色
const g = Math.floor(255 * (1 - ratio) * 2);
return `rgb(255, ${g}, 0)`;
}
};
// 计算地图边界和中心点
const calculateMapBounds = (coordinates: any[]) => {
if (coordinates.length === 0) return;
console.log('计算地图边界,坐标点数量:', coordinates.length);
console.log('所有坐标点:', coordinates);
let minLng = coordinates[0].longitude;
let maxLng = coordinates[0].longitude;
let minLat = coordinates[0].latitude;
let maxLat = coordinates[0].latitude;
coordinates.forEach(coord => {
minLng = Math.min(minLng, coord.longitude);
maxLng = Math.max(maxLng, coord.longitude);
minLat = Math.min(minLat, coord.latitude);
maxLat = Math.max(maxLat, coord.latitude);
});
// 设置地图中心点
const centerLng = (minLng + maxLng) / 2;
const centerLat = (minLat + maxLat) / 2;
mapCenter.value = {
longitude: centerLng,
latitude: centerLat
};
console.log('地图中心点:', mapCenter.value);
console.log('坐标范围:', { minLng, maxLng, minLat, maxLat });
// 根据坐标范围调整缩放级别
const lngDiff = maxLng - minLng;
const latDiff = maxLat - minLat;
const maxDiff = Math.max(lngDiff, latDiff);
console.log('坐标差值:', { lngDiff, latDiff, maxDiff });
// 动态调整缩放级别
if (maxDiff > 10) {
mapScale.value = 5;
} else if (maxDiff > 5) {
mapScale.value = 7;
} else if (maxDiff > 1) {
mapScale.value = 10;
} else if (maxDiff > 0.1) {
mapScale.value = 13;
} else {
mapScale.value = 16; // 对于很近的点,使用更高的缩放级别
}
console.log('设置地图缩放级别:', mapScale.value);
};
onMounted(() => {
processLogisticsData();
});
</script>
<style lang="scss" scoped>
.logistics-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
}
.logistics-map {
width: 100%;
height: 50vh;
}
.logistics-timeline {
background-color: #ffffff;
margin-top: 20rpx;
border-radius: 16rpx 16rpx 0 0;
}
.timeline-header {
padding: 32rpx 32rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.header-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.timeline-list {
padding: 24rpx 32rpx;
}
.timeline-item {
position: relative;
padding-left: 48rpx;
padding-bottom: 32rpx;
&:last-child {
padding-bottom: 0;
}
&::after {
content: '';
position: absolute;
left: 18rpx;
top: 32rpx;
width: 2rpx;
height: calc(100% - 16rpx);
background-color: #e8e8e8;
}
&:last-child::after {
display: none;
}
&.is-first .timeline-dot {
background-color: #52c41a;
box-shadow: 0 0 0 4rpx rgba(82, 196, 26, 0.2);
}
}
.timeline-dot {
position: absolute;
left: 12rpx;
top: 8rpx;
width: 12rpx;
height: 12rpx;
background-color: #1890ff;
border-radius: 50%;
border: 2rpx solid #ffffff;
box-shadow: 0 0 0 2rpx #1890ff;
}
.timeline-content {
padding-top: 4rpx;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12rpx;
}
.status-text {
font-size: 28rpx;
font-weight: 500;
color: #333333;
flex: 1;
}
.time-text {
font-size: 24rpx;
color: #666666;
margin-left: 16rpx;
}
.content-body {
line-height: 1.6;
}
.context-text {
font-size: 26rpx;
color: #666666;
display: block;
margin-bottom: 8rpx;
}
.area-text {
font-size: 20rpx; /* 省市区名字字体较小 */
color: #999999; /* 省市区名字颜色较淡 */
background-color: #f5f5f5;
padding: 4rpx 12rpx;
border-radius: 12rpx;
display: inline-block;
}
</style>
更多推荐


所有评论(0)