riku
2025-09-04 7f6661cca40e3530111d628222fa25462022ec78
2025.9.4
已修改5个文件
524 ■■■■ 文件已修改
public/underway_season_report.docx 补丁 | 查看 | 原始文档 | blame | 历史
src/api/dataAnalysisApi.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/chart/chart-map.js 202 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/calculate.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/historymode/component/MissionReport.vue 277 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/underway_season_report.docx
Binary files differ
src/api/dataAnalysisApi.js
@@ -76,5 +76,13 @@
        params: { startTime, endTime }
      })
      .then((res) => res.data);
  },
  fetchGridFusion({ startTime, endTime, area, factorTypes }) {
    return $http
      .post(`air/analysis/report/gridFusion`, area, {
        params: { startTime, endTime, factorTypes: factorTypes.join(',') }
      })
      .then((res) => res.data);
  }
};
src/utils/chart/chart-map.js
@@ -2,6 +2,12 @@
// import * as shanghaiMap from 'echarts-china-cities-js/echarts-china-cities-js/shanghai.js'; // 修正地图数据文件路径
// import 'echarts-china-cities-js/echarts-china-cities-js/shanghai.js'; // 修正地图数据文件路径
import { shanghai, jingan } from '@/utils/chart/shanghai.js'; // 确保路径正确
import calculate from '@/utils/map/calculate.js';
// 偏移纬度,对应约100米
const OFFSET_LAT = 0.00082;
// 偏移纬度,对应约100米
const OFFSET_LNG = 0.0009;
/**
 * 计算地图中心点和缩放比例
@@ -9,40 +15,47 @@
 * @returns
 */
function calCenterPointAndZoom(gridData) {
  let centerLng = 0;
  let centerLat = 0;
  const coordinates = [];
  let maxLng = -180;
  let minLng = 180;
  let maxLat = -90;
  let minLat = 90;
  let zoom = 100;
  let zoom = 1;
  gridData.forEach((g) => {
    centerLng += g.centerLng;
    centerLat += g.centerLat;
    maxLng = Math.max(maxLng, g.centerLng);
    minLng = Math.min(minLng, g.centerLng);
    maxLat = Math.max(maxLat, g.centerLat);
    minLat = Math.min(minLat, g.centerLat);
    g.coordinates.forEach((coordArr) => {
      coordinates.push({
        lng: coordArr[0],
        lat: coordArr[1]
  });
  if (gridData.length > 0) {
    centerLng /= gridData.length;
    centerLat /= gridData.length;
  }
      maxLng = Math.max(maxLng, coordArr[0]);
      minLng = Math.min(minLng, coordArr[0]);
      maxLat = Math.max(maxLat, coordArr[1]);
      minLat = Math.min(minLat, coordArr[1]);
    });
  });
  console.log((maxLng - centerLng) / zoom);
  console.log('zoom', zoom);
  return {
    centerLng,
    centerLat,
    zoom
    centerLng: (maxLng + minLng) / 2,
    centerLat: (maxLat + minLat) / 2,
    zoom,
    bounds: [
      minLng - OFFSET_LNG,
      minLat - OFFSET_LAT,
      maxLng + OFFSET_LNG,
      maxLat + OFFSET_LAT
    ]
  };
}
function generateGridMap(gridData) {
  const width = 800;
  const height = 400;
  // 1. 创建临时DOM元素
  const div = document.createElement('div');
  div.style.width = '800px';
  div.style.height = '400px';
  div.style.width = `${width}px`;
  div.style.height = `${height}px`;
  document.body.appendChild(div);
  // 注册上海市地图数据
  // console.log(shanghaiMap);
@@ -57,7 +70,8 @@
    value: [grid.centerLng, grid.centerLat, grid.value], // 中心点经纬度和数值
    coords: grid.coordinates // 网格四角经纬度坐标数组 [[lng1,lat1], [lng2,lat2], ...]
  }));
  const { centerLng, centerLat, zoom } = calCenterPointAndZoom(gridData);
  const { centerLng, centerLat, zoom, bounds } =
    calCenterPointAndZoom(gridData);
  // 2. 配置项
  const option = {
@@ -72,18 +86,6 @@
      label: { show: true }, // 显示地名标签
      itemStyle: { areaColor: '#ddddddff', borderColor: '#999' }
    },
    // series: [
    //   {
    //     type: 'scatter',
    //     coordinateSystem: 'geo',
    //     data: [
    //       {
    //         name: '黄浦区',
    //         value: [121.490317, 31.222771, 100] // 中心点坐标+数值
    //       }
    //     ]
    //   }
    // ]
    series: [
      {
        type: 'custom',
@@ -112,12 +114,136 @@
  };
  chart.setOption(option);
  // 3. 导出为图片(返回base64)
  return chart.getDataURL({
    type: 'png',
    pixelRatio: 1,
    backgroundColor: '#ddddddff'
  });
  // 将像素坐标转换为经纬度
  // const convert = (x, y) => {
  //   return chart.convertFromPixel(
  //     {
  //       geoIndex: 0
  //     },
  //     [x, y]
  //   );
  // };
  // 将经纬度转换为像素坐标
  const convert = (lng, lat) => {
    return chart.convertToPixel(
      {
        geoIndex: 0
      },
      [lng, lat]
    );
  };
  // 计算画布的左上角和右下角对应的经纬度
  // 计算网格区域的左上角和右下角经纬度对应的像素坐标
  const topLeft = convert(bounds[0], bounds[3]);
  const bottomRight = convert(bounds[2], bounds[1]);
  // 计算合适的缩放倍数
  const scale = Math.min(
    Math.abs(width / (bottomRight[0] - topLeft[0])),
    Math.abs(height / (bottomRight[1] - topLeft[1]))
  );
  console.log('scale', scale);
  // 地图缩放比例
  chart.setOption({
    geo: {
      zoom: scale
}
  });
  // 3. 导出为图片(返回base64)
  return new Promise((resolve, reject) => {
    // 延迟执行确保绘制完成
    setTimeout(() => {
      const url = chart.getDataURL({
        type: 'png',
        pixelRatio: 2,
        backgroundColor: '#fff'
      });
      resolve(url);
    }, 1000);
  });
  // return captureMapByBounds({
  //   chart: chart,
  //   bounds: bounds
  // }).catch((err) => {
  //   console.error('截图失败:', err);
  // });
}
/**
 * 根据经纬度范围截取地图区域
 * @param {Object} params - 截取参数
 * @param {Array} params.bounds - 经纬度范围 [minLng, minLat, maxLng, maxLat]
 * @param {Object} params.chart - ECharts实例
 * @returns {Promise<string>} 截取区域的base64图片
 */
// function captureMapByBounds(params) {
//   const { bounds, chart } = params;
//   const [minLng, minLat, maxLng, maxLat] = bounds;
//   // 获取地图坐标系
//   // const geo = chart.getModel().getComponent('geo');
//   // if (!geo) return Promise.reject('未找到地图组件');
//   // 将经纬度转换为像素坐标
//   const convert = (lng, lat) => {
//     return chart.convertToPixel(
//       {
//         geoIndex: 0
//       },
//       [lng, lat]
//     );
//   };
//   // 计算四个角的像素坐标
//   const topLeft = convert(minLng, maxLat);
//   const bottomRight = convert(maxLng, minLat);
//   // 创建临时Canvas
//   const canvas = document.createElement('canvas');
//   const ctx = canvas.getContext('2d');
//   // 获取原始图表Canvas
//   const originalCanvas = chart.getDom().querySelector('canvas');
//   // 设置Canvas尺寸为截取区域大小
//   topLeft[0] -= 10;
//   topLeft[1] -= 10;
//   bottomRight[0] += 10;
//   bottomRight[1] += 10;
//   topLeft[0] = Math.max(topLeft[0], 0);
//   topLeft[1] = Math.max(topLeft[1], 0);
//   bottomRight[0] = Math.min(bottomRight[0], originalCanvas.width);
//   bottomRight[1] = Math.min(bottomRight[1], originalCanvas.height);
//   const width = bottomRight[0] - topLeft[0];
//   const height = bottomRight[1] - topLeft[1];
//   canvas.width = width;
//   canvas.height = height;
//   // 裁剪指定区域
//   ctx.drawImage(
//     originalCanvas,
//     topLeft[0],
//     topLeft[1], // 源图像裁剪起点
//     width,
//     height, // 源图像裁剪尺寸
//     0,
//     0, // 目标图像绘制起点
//     width,
//     height // 目标图像绘制尺寸
//   );
//   // 转换为base64图片
//   return new Promise((resolve) => {
//     // 延迟执行确保绘制完成
//     setTimeout(() => {
//       const base64 = canvas.toDataURL('image/png', 1.0);
//       resolve(base64);
//     }, 100);
//   });
// }
export default { generateGridMap };
src/utils/map/calculate.js
@@ -286,5 +286,40 @@
  gcj02towgs84,
  //从GPS转高德
  wgs84_To_Gcj02
  wgs84_To_Gcj02,
  /**
   * 计算一组经纬度坐标的中心点
   * @param {Array} coordinates - 经纬度数组,格式: [{lng: number, lat: number}, ...]
   * @returns {Object} 中心点坐标 {lng: number, lat: number}
   */
  calculateCenterCoordinates(coordinates) {
    if (coordinates.length === 0) return { lng: 0, lat: 0 };
    if (coordinates.length === 1) return coordinates[0];
    let x = 0,
      y = 0,
      z = 0;
    coordinates.forEach((p) => {
      const lng = (p.lng * Math.PI) / 180; // 经度转弧度
      const lat = (p.lat * Math.PI) / 180; // 纬度转弧度
      // 转换为三维坐标
      x += Math.cos(lat) * Math.cos(lng);
      y += Math.cos(lat) * Math.sin(lng);
      z += Math.sin(lat);
    });
    // 取平均值
    const avgX = x / coordinates.length;
    const avgY = y / coordinates.length;
    const avgZ = z / coordinates.length;
    // 转换回经纬度
    const lng = (Math.atan2(avgY, avgX) * 180) / Math.PI;
    const hypotenuse = Math.sqrt(avgX ** 2 + avgY ** 2);
    const lat = (Math.atan2(avgZ, hypotenuse) * 180) / Math.PI;
    return { lng, lat };
  }
};
src/views/historymode/component/MissionReport.vue
@@ -72,6 +72,7 @@
import chartMapAmap from '@/utils/chart/chart-map-amap';
import { Legend } from '@/model/Legend';
import { getHexColor, getColorBetweenTwoColors } from '@/utils/color';
import { getGridDataDetailFactorValue } from '@/model/GridDataDetail';
// 借用卫星遥测模块中的100米网格
const props = defineProps({
@@ -106,7 +107,8 @@
      cityName: formObj.value.location.cName,
      districtCode: formObj.value.location.dCode,
      districtName: formObj.value.location.dName
    }
    },
    factorTypes: radioOptions(TYPE0).map((e) => e.name)
  };
});
@@ -174,6 +176,31 @@
        }
      ]
    }
  ],
  gridFusionByAQIList: [
    {
      pollutionDegree: '优',
      _areaDes: '走航区域大小',
      _gridDes: '100米正方形网格',
      _missionDes: '20250729、20250730两次',
      highRiskGridList: [
        {
          index: 1,
          factor: 'PM2.5',
          // 标准色网格图
          gridImgUrl1: '',
          // 对比色网格图
          gridImgUrl2: '',
          factorValue: 20,
          // 四至范围,顺序为最小经度,最大经度,最小纬度,最大纬度
          bounds: [121.4945, 121.4955, 31.2304, 31.2314],
          _boundsDes: '四至范围',
          // 涉及街镇
          town: '',
          _scenesDes: '涉及的污染场景'
        }
      ]
    }
  ]
};
@@ -183,7 +210,9 @@
    generateMissionList(params.value).then(() => {
      generateMissionDetail(params.value).then(() => {
        generateClueByRiskArea(params.value).then(() => {
          generateGridFusion(params.value).then(() => {
          generateDocx();
          });
        });
      });
    });
@@ -348,23 +377,195 @@
  return images;
}
function handleMixClick() {
  const tags = [1, 2];
  const fetchGridData = () => {
    gridApi.mixUnderwayGridData(props.groupId, tags).then((res) => {
      const gridData = res.data.map((v) => {
        const data = v.pm25;
        const grid = gridCellList.value.find((g) => {
          return g.cellIndex == v.cellId;
function generateGridFusion(param) {
  return dataAnalysisApi.fetchGridFusion(param).then((res) => {
    const promiseList = [];
    templateParam.gridFusionByAQIList = [];
    res.data.forEach((item) => {
      const scenes = [];
      item.missionList.forEach((m) => {
        m.keyScene.map((s) => {
          if (scenes.indexOf(s.name) == -1) {
            scenes.push(s.name);
          }
        });
        const { color, nextColor, range, nextRange } =
          Legend.getStandardColorAndNext('PM25', data);
      });
      const gfbAQI = {
        pollutionDegree: item.pollutionDegree,
        _areaDes: `走航区域经过${scenes.join('、')}`,
        _gridDes: `${item.gridLen}米正方形网格`,
        _missionDes: `${item.missionList.map((m) => m.missioncode).join('、')}${item.missionList.length}次`
      };
      const _highRiskGridList = [];
      item.highRiskGridList.forEach((g, i) => {
        const p = generateGridFusionImg(g.factorType, item.gridFusionList).then(
          (url) => {
            const { url1, url2 } = url;
            _highRiskGridList.push({
              index: i + 1,
              factor: g.factorType,
              // 标准色网格图
              gridImgUrl1: url1,
              // 对比色网格图
              gridImgUrl2: url2,
              factorValue: g.factorValue,
              // 四至范围,顺序为最小经度,最大经度,最小纬度,最大纬度
              _boundsDes: `经度${g.bounds[0]}至${g.bounds[1]},纬度${g.bounds[2]}至${g.bounds[3]}`,
              // 涉及街镇
              town: g.town,
              _scenesDes: g.highRiskScenes.map((s) => s.name).join('、')
            });
          }
        );
        promiseList.push(p);
      });
      gfbAQI.highRiskGridList = _highRiskGridList;
      templateParam.gridFusionByAQIList.push(gfbAQI);
    });
    return Promise.all(promiseList).then(() => {
      return templateParam.gridFusionByAQIList;
    });
    // templateParam.gridFusionByAQIList = res.data.map((item) => {
    //   const scenes = [];
    //   item.missionList.forEach((m) => {
    //     m.keyScene.map((s) => {
    //       if (scenes.indexOf(s.name) == -1) {
    //         scenes.push(s.name);
    //       }
    //     });
    //   });
    //   return {
    //     pollutionDegree: item.pollutionDegree,
    //     _areaDes: `走航区域经过${scenes.join('、')}`,
    //     _gridDes: `${item.gridLen}米正方形网格`,
    //     _missionDes: `${item.missionList.map((m) => m.missioncode).join('、')}${item.missionList.length}次`,
    //     highRiskGridList: item.highRiskGridList.map(async (g, i) => {
    //       const { url1, url2 } = await generateGridFusionImg(
    //         g.factorType,
    //         item.gridFusionList
    //       );
    //       return {
    //         index: i + 1,
    //         factor: g.factorType,
    //         // 标准色网格图
    //         gridImgUrl1: url1,
    //         // 对比色网格图
    //         gridImgUrl2: url2,
    //         factorValue: g.factorValue,
    //         // 四至范围,顺序为最小经度,最大经度,最小纬度,最大纬度
    //         _boundsDes: `经度${g.bounds[0]}至${g.bounds[1]},纬度${g.bounds[2]}至${g.bounds[3]}`,
    //         // 涉及街镇
    //         town: g.town,
    //         _scenesDes: g.highRiskScenes.map((s) => s.name).join('、')
    //       };
    //     })
    //   };
    // });
  });
}
async function generateGridFusionImg(factorName, dataList) {
  var min = 1000000;
  var max = 0;
  dataList.forEach((v) => {
    min = Math.min(min, getGridDataDetailFactorValue(v.data, factorName));
    max = Math.max(max, getGridDataDetailFactorValue(v.data, factorName));
  });
  const gridData = dataList.map((v) => {
    const data = getGridDataDetailFactorValue(v.data, factorName);
    const grid = v.cell;
    // 标准色
    const {
      color: color1,
      nextColor: nextColor1,
      range: range1,
      nextRange: nextRange1
    } = Legend.getStandardColorAndNext(factorName, data);
    const ratio1 = (data - range1) / (nextRange1 - range1);
    const _color1 = getColorBetweenTwoColors(
      color1.map((v) => v * 255),
      nextColor1.map((v) => v * 255),
      ratio1
    );
    // 对比色
    const { color, nextColor, range, nextRange } = Legend.getCustomColorAndNext(
      data,
      min,
      max
    );
        const ratio = (data - range) / (nextRange - range);
        const _color = getColorBetweenTwoColors(
          color.map((v) => v * 255),
          nextColor.map((v) => v * 255),
          ratio
        );
    return [
      {
        centerLng: grid.longitude,
        centerLat: grid.latitude,
        value: _color1,
        coordinates: [
          [grid.point1Lon, grid.point1Lat],
          [grid.point2Lon, grid.point2Lat],
          [grid.point3Lon, grid.point3Lat],
          [grid.point4Lon, grid.point4Lat]
        ]
      },
      {
        centerLng: grid.longitude,
        centerLat: grid.latitude,
        value: _color,
        coordinates: [
          [grid.point1Lon, grid.point1Lat],
          [grid.point2Lon, grid.point2Lat],
          [grid.point3Lon, grid.point3Lat],
          [grid.point4Lon, grid.point4Lat]
        ]
      }
    ];
  });
  if (gridData[0] == undefined || gridData[1] == undefined) {
    console.log(gridData);
  }
  const url1 = await chartMap.generateGridMap(gridData[0]);
  const url2 = await chartMap.generateGridMap(gridData[1]);
  if (gridBase64Url.value == null) {
    gridBase64Url.value = url1;
  }
  return {
    url1,
    url2
  };
}
function handleMixClick({ tags = [10, 11], factorName = 'PM25' }) {
  generateGridFusion(params.value).then(() => {});
  // const fetchGridData = () => {
  //   gridApi.mixUnderwayGridData(props.groupId, tags).then((res) => {
  //     var min = 1000000;
  //     var max = 0;
  //     res.data.forEach((v) => {
  //       min = Math.min(min, getGridDataDetailFactorValue(v, factorName));
  //       max = Math.max(max, getGridDataDetailFactorValue(v, factorName));
  //     });
  //     const gridData = res.data.map((v) => {
  //       const data = getGridDataDetailFactorValue(v, factorName);
  //       const grid = gridCellList.value.find((g) => {
  //         return g.cellIndex == v.cellId;
  //       });
  //       // const { color, nextColor, range, nextRange } =
  //       //   Legend.getStandardColorAndNext('PM25', data);
  //       // const ratio = (data - range) / (nextRange - range);
  //       // const _color = getColorBetweenTwoColors(
  //       //   color.map((v) => v * 255),
  //       //   nextColor.map((v) => v * 255),
  //       //   ratio
  //       // );
        // // 根据遥测数据计算网格颜色
        //         const { color, nextColor, range, nextRange } =
@@ -376,35 +577,37 @@
        //           nextColor.map((v) => v * 255),
        //           ratio
        //         );
        return {
          centerLng: grid.longitude,
          centerLat: grid.latitude,
          value: _color,
          coordinates: [
            [grid.point1Lon, grid.point1Lat],
            [grid.point2Lon, grid.point2Lat],
            [grid.point3Lon, grid.point3Lat],
            [grid.point4Lon, grid.point4Lat]
          ]
        };
      });
      // chartMapAmap.generateGridMap(gridData).then((url) => {
  //       return {
  //         centerLng: grid.longitude,
  //         centerLat: grid.latitude,
  //         value: _color,
  //         coordinates: [
  //           [grid.point1Lon, grid.point1Lat],
  //           [grid.point2Lon, grid.point2Lat],
  //           [grid.point3Lon, grid.point3Lat],
  //           [grid.point4Lon, grid.point4Lat]
  //         ]
  //       };
  //     });
  //     // chartMapAmap.generateGridMap(gridData).then((url) => {
  //     //   gridBase64Url.value = url;
  //     // });
  //     chartMap.generateGridMap(gridData).then((url) => {
      //   gridBase64Url.value = url;
      // });
      gridBase64Url.value = chartMap.generateGridMap(gridData);
    });
  };
  //   });
  // };
  if (gridCellList.value.length == 0) {
    gridApi
      .fetchGridCell(props.groupId)
      .then((res) => {
        gridCellList.value = res.data;
      })
      .then(() => fetchGridData());
  } else {
    fetchGridData();
  }
  // if (gridCellList.value.length == 0) {
  //   gridApi
  //     .fetchGridCell(props.groupId)
  //     .then((res) => {
  //       gridCellList.value = res.data;
  //     })
  //     .then(() => fetchGridData());
  // } else {
  //   fetchGridData();
  // }
}
function generateDocx() {