<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>

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐