From 7e89ae52ea3b97429a116bd3d8e4b2ba05e02164 Mon Sep 17 00:00:00 2001
From: riku <risaku@163.com>
Date: 星期二, 30 九月 2025 09:39:39 +0800
Subject: [PATCH] 2025.9.30 新增单次走航报告自动生成功能

---
 src/components/mission/missionReportDownload.js   |  250 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/api/dataAnalysisApi.js                        |   24 ++++
 src/components/mission/MissionManage.vue          |   12 +-
 src/views/historymode/component/MissionReport.vue |   12 +-
 public/underway_mission_report.docx               |    0 
 5 files changed, 287 insertions(+), 11 deletions(-)

diff --git a/public/underway_mission_report.docx b/public/underway_mission_report.docx
new file mode 100644
index 0000000..4325a5a
--- /dev/null
+++ b/public/underway_mission_report.docx
Binary files differ
diff --git a/src/api/dataAnalysisApi.js b/src/api/dataAnalysisApi.js
index 25a2c97..66726a6 100644
--- a/src/api/dataAnalysisApi.js
+++ b/src/api/dataAnalysisApi.js
@@ -33,6 +33,14 @@
       .then((res) => res.data);
   },
 
+  fetchOneMissionSummary(missionCode) {
+    return $http
+      .get(`air/analysis/report/missionSummary/one`, {
+        params: { missionCode }
+      })
+      .then((res) => res.data);
+  },
+
   /**
    * 鑾峰彇璧拌埅浠诲姟娓呭崟
    * @param {*} startTime 寮�濮嬫椂闂达紝鏍煎紡YYYY-MM-DD HH:mm:ss
@@ -63,6 +71,14 @@
       .then((res) => res.data);
   },
 
+  fetchOneMissionDetail(missionCode) {
+    return $http
+      .get(`air/analysis/report/missionDetail/one`, {
+        params: { missionCode }
+      })
+      .then((res) => res.data);
+  },
+
   /**
    * 鑾峰彇璧拌埅鍏稿瀷闅愭偅鍖哄煙
    * @param {*} startTime 寮�濮嬫椂闂达紝鏍煎紡YYYY-MM-DD HH:mm:ss
@@ -78,6 +94,14 @@
       .then((res) => res.data);
   },
 
+  fetchOneClueByRiskArea(missionCode) {
+    return $http
+      .get(`air/analysis/report/clueByRiskArea/one`, {
+        params: { missionCode }
+      })
+      .then((res) => res.data);
+  },
+
   fetchGridFusion({ startTime, endTime, area, factorTypes }) {
     return $http
       .post(`air/analysis/report/gridFusion`, area, {
diff --git a/src/components/mission/MissionManage.vue b/src/components/mission/MissionManage.vue
index 9c1c51f..87f8e22 100644
--- a/src/components/mission/MissionManage.vue
+++ b/src/components/mission/MissionManage.vue
@@ -60,14 +60,14 @@
                 class="el-button-custom"
                 @click="deleteMission(row)"
               ></el-button>
-              <!-- <el-button
+              <el-button
                 :loading="row.downloadLoading"
                 type="primary"
                 size="small"
                 icon="Document"
                 class="el-button-custom"
                 @click="downloadReport(row)"
-              ></el-button> -->
+              ></el-button>
             </template>
           </el-table-column>
         </el-table>
@@ -118,6 +118,7 @@
 import { mapStores } from 'pinia';
 import { useMissionStore } from '@/stores/mission';
 import { useFetchData } from '@/composables/fetchData';
+import { downloadReport } from '@/components/mission/missionReportDownload.js';
 
 export default {
   setup() {
@@ -163,9 +164,10 @@
     },
     downloadReport(row) {
       row.downloadLoading = true;
-      missionApi
-        .downloadReport(row.missionCode)
-        .finally(() => (row.downloadLoading = false));
+      // missionApi
+      //   .downloadReport(row.missionCode)
+      //   .finally(() => (row.downloadLoading = false));
+      downloadReport(row).finally(() => (row.downloadLoading = false));
     },
     // eslint-disable-next-line no-unused-vars
     timeFormatter(row, col, cellValue, index) {
diff --git a/src/components/mission/missionReportDownload.js b/src/components/mission/missionReportDownload.js
new file mode 100644
index 0000000..24be359
--- /dev/null
+++ b/src/components/mission/missionReportDownload.js
@@ -0,0 +1,250 @@
+import moment from 'moment';
+import { exportDocx } from '@/utils/doc';
+import dataAnalysisApi from '@/api/dataAnalysisApi';
+import { FactorDatas } from '@/model/FactorDatas';
+import factorDataParser from '@/utils/chart/factor-data-parser';
+import chartToImg from '@/utils/chart/chart-to-img';
+
+const groupBy = (array, func) => {
+  const groups = {};
+  array.forEach((item) => {
+    const key = func(item);
+    if (!groups[key]) {
+      groups[key] = [];
+    }
+    groups[key].push(item);
+  });
+  return Object.keys(groups).map((key) => ({
+    key,
+    values: groups[key]
+  }));
+};
+
+const templateParam = () => {
+  return {
+    // 璧拌埅浠诲姟鎯呭喌
+    cityName: '涓婃捣甯�',
+    districtName: '闀垮畞鍖�',
+    createTime: '2024骞�9鏈�9鏃�',
+    missionTime: '2024骞�9鏈�6鏃�',
+    missionDate: '9鏈�6鏃�',
+    startTime: '13:00',
+    endTime: '17:00',
+    missionPeriod: '涓嬪崍',
+    region: '浠欓湠绔�',
+    radius: '2鍏噷',
+    // 璧拌埅褰撴棩澶╂皵鐘跺喌
+    pollutionDegree: '浼�',
+    aqi: 46,
+    mainFactor: '鏃�',
+    factorAvgDes:
+      'PM2.5锛�46渭g/m3锛夈�丳M10锛�66渭g/m3锛夈�丱3-8H锛�138渭g/m3锛夈�丼O2锛�15渭g/m3锛夈�丯O2锛�41渭g/m3锛夈�丆O锛�0.7mg/m3锛�',
+    weather: '澶氫簯锛�18-28鈩冿紝涓滃崡椋�2绾�',
+    // 璧拌埅璺嚎鍥�
+    roadMapUrl: '',
+    // 璧拌埅鐩戞祴鍥犲瓙鎯呭喌
+    focusFactor: 'PM2.5銆丳M10銆乂OCs銆丱3',
+    mainFactorStatus:
+      '璧拌埅鏈熼棿锛孭M2.5銆丳M10绛夌洃娴嬪洜瀛愭祿搴﹀潎鍊奸珮浜庨暱瀹佺┖姘旇川閲忚儗鏅紝',
+    // mainFactorStatus:'璧拌埅鏈熼棿锛孭M2.5銆丳M10绛夌洃娴嬪洜瀛愭祿搴﹀潎鍊煎潎澶勪簬浼橈紝涓庨暱瀹佺┖姘旇川閲忚儗鏅熀鏈竴鑷达紝',
+    factorDetailList: [
+      {
+        factor: 'PM2.5',
+        avgValue: 22,
+        minValue: 19,
+        maxValue: 25
+      }
+    ],
+    clueByAreaList: [
+      {
+        _index: 1,
+        _area: '鏌愭煇鍖哄煙鍛ㄨ竟',
+        clueByFactorList: [
+          {
+            factor: 'PM鈧�.鈧�',
+            clues: [
+              {
+                _factorNames: 'PM2.5',
+                _time: '10:22:28 - 10:22:34',
+                _riskRegion: '闀垮畞鍖烘竻婧矾鍙箰涓滆矾',
+                _exceptionType: '蹇�熶笂鍗�',
+                _chart: '',
+                _conclusion:
+                  '鍦�10:22:28鑷�10:22:34涔嬮棿锛屽嚭鐜板揩閫熶笂鍗囷紝VOC鏈�浣庡�间负135.95渭g/m鲁锛屾渶楂樺�间负135.95渭g/m鲁锛屽潎鍊间负135.95渭g/m鲁锛屽彂鐜�3涓闄╂簮锛屽寘鍚�2涓姞娌圭珯锛�1涓苯淇��',
+                _scenes:
+                  '1.涓婃捣渚濆痉姹借溅缁翠慨鏈夐檺鍏徃锛屾苯淇紒涓氾紝浣嶄簬涓婃捣甯傞暱瀹佸尯鍖楄櫣璺�1079鍙凤紝璺濅粰闇炵珯1887绫炽�俓r\n鈥︹��'
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  };
+};
+
+function genMission(param, mission) {
+  param.cityName = mission.cityName;
+  param.districtName = mission.districtName;
+  param.createTime = moment().format('YYYY-MM-DD');
+  param.missionTime = moment(mission.startTime).format('YYYY骞碝M鏈圖D鏃�');
+  param.missionDate = moment(mission.startTime).format('MM鏈圖D鏃�');
+  param.startTime = moment(mission.startTime).format('HH:mm');
+  param.endTime = moment(mission.endTime).format('HH:mm');
+  // 鏍规嵁startTime璁$畻鎵�澶勬椂娈�
+  const hour = moment(mission.startTime).hour();
+  if (hour >= 0 && hour < 6) {
+    param.missionPeriod = '鍑屾櫒';
+  } else if (hour >= 6 && hour < 9) {
+    param.missionPeriod = '鏃╀笂';
+  } else if (hour >= 9 && hour < 12) {
+    param.missionPeriod = '涓婂崍';
+  } else if (hour >= 12 && hour < 14) {
+    param.missionPeriod = '涓崍';
+  } else if (hour >= 14 && hour < 18) {
+    param.missionPeriod = '涓嬪崍';
+  } else if (hour >= 18 && hour < 22) {
+    param.missionPeriod = '鏅氫笂';
+  } else {
+    param.missionPeriod = '娣卞';
+  }
+  param.region = mission.region;
+  param.radius = mission.radius ? `${mission.radius}鍏噷` : '';
+
+  // 璧拌埅褰撴棩澶╂皵鐘跺喌
+  // param.pollutionDegree = mission.pollutionDegree;
+  // param.aqi = mission.aqi;
+  // param.mainFactor = mission.mainFactor;
+  // param.factorAvgDes = mission.factorAvgDes;
+  // param.weather = mission.weather;
+}
+
+function genMissionSummary(param, mission) {
+  return dataAnalysisApi
+    .fetchOneMissionSummary(mission.missionCode)
+    .then((res) => {
+      param.focusRegion = res.data.focusRegion.join('銆�');
+    });
+}
+
+function genRoadMap(param, mission) {}
+
+function genFactorDetail(param, mission, focusFactor) {
+  return dataAnalysisApi
+    .fetchOneMissionDetail(mission.missionCode)
+    .then((res) => {
+      param.focusFactor = focusFactor.join('銆�');
+      param.mainFactorStatus =
+        '璧拌埅鏈熼棿锛孭M2.5銆丳M10绛夌洃娴嬪洜瀛愭祿搴﹀潎鍊奸珮浜庨暱瀹佺┖姘旇川閲忚儗鏅紝';
+      param.factorDetailList = res.data.dataStatistics
+        .filter((item) => {
+          return focusFactor.includes(item.factor);
+        })
+        .forEach((item) => {
+          item.avgValue = item.avgValue.toFixed(2);
+          item.minValue = item.minValue.toFixed(2);
+          item.maxValue = item.maxValue.toFixed(2);
+        });
+    });
+}
+
+function genClueByRiskArea(param, mission) {
+  return dataAnalysisApi
+    .fetchOneClueByRiskArea(mission.missionCode)
+    .then((res) => {
+      const clues = res.data.flatMap((item) =>
+        item.clueByFactorList.flatMap((cbf) => cbf.clues)
+      );
+      param.clueByAreaList = groupBy(
+        clues,
+        (clue) => clue.pollutedArea.address
+      ).map((item) => {
+        const { key: area, values: clues } = item;
+        return {
+          _area: area,
+          clues: clues.map((clue) => {
+            return {
+              _factorNames: Object.keys(clue.pollutedData.statisticMap)
+                .map((e) => e)
+                .join('銆�'),
+              _time:
+                moment(clue.pollutedData.startTime).format('HH:mm:ss') +
+                ' - ' +
+                moment(clue.pollutedData.endTime).format('HH:mm:ss'),
+              _riskRegion: clue.pollutedArea.address
+                ? clue.pollutedArea.address
+                : '',
+              _exceptionType: clue.pollutedData.exception,
+              _images: generateChartImg(clue.pollutedData),
+              _conclusion: clue.pollutedSource.conclusion,
+              _scenes:
+                clue.pollutedSource.sceneList.length > 0
+                  ? clue.pollutedSource.sceneList
+                      .map(
+                        (s, index) =>
+                          `${index + 1}. ${s.name}锛�${s.type}锛屼綅浜�${s.location}锛岃窛${s.closestStation.name}${parseInt(s.length)}绫筹紱`
+                      )
+                      .join('\n')
+                  : '鏃�'
+            };
+          })
+        };
+      });
+    });
+}
+
+function generateChartImg(pollutedData) {
+  const exceptionIndexArr = [];
+  pollutedData.dataVoList.forEach((e) => {
+    const i = pollutedData.historyDataList.findIndex((v) => v.time == e.time);
+    exceptionIndexArr.push([i - 1 < 0 ? 0 : i - 1, i]);
+  });
+
+  const factorDatas = new FactorDatas();
+  const images = [];
+  factorDatas.setData(pollutedData.historyDataList, 0, () => {
+    for (const key in pollutedData.statisticMap) {
+      const value = pollutedData.statisticMap[key];
+      const _chartOptions = factorDataParser.parseData(factorDatas, [
+        {
+          label: value.factorName,
+          name: value.factorName,
+          value: value.factorId + ''
+        }
+      ]);
+      _chartOptions.forEach((o) => {
+        images.push({
+          url: chartToImg.generateEchartsImage(o, exceptionIndexArr, 20)
+        });
+      });
+    }
+  });
+  return images;
+}
+
+async function genTemplateParams(mission, focusFactor) {
+  const param = templateParam();
+  await genMission(param, mission);
+  await genMissionSummary(param, mission);
+  await genRoadMap(param, mission);
+  await genFactorDetail(param, mission, focusFactor);
+  await genClueByRiskArea(param, mission);
+  Object.keys(param).forEach((key) => {
+    if (typeof param[key] === 'string') {
+      param[key] = param[key]?.trim() || '';
+    }
+  });
+  console.log('genTemplateParams', param);
+  return param;
+}
+
+function downloadReport(mission, focusFactor = ['PM25', 'PM10']) {
+  return genTemplateParams(mission, focusFactor).then((param) => {
+    exportDocx('/underway_mission_report.docx', param, `璧拌埅浠诲姟鎶ュ憡.docx`, {
+      horizontalHeight: 250,
+      verticalWidth: 568,
+      scale: 2
+    });
+  });
+}
+
+export { downloadReport };
diff --git a/src/views/historymode/component/MissionReport.vue b/src/views/historymode/component/MissionReport.vue
index 8cb9b57..a4ddba2 100644
--- a/src/views/historymode/component/MissionReport.vue
+++ b/src/views/historymode/component/MissionReport.vue
@@ -23,19 +23,19 @@
         >
           涓嬭浇鎶ュ憡
         </el-button>
-        <el-button
+        <!-- <el-button
           type="primary"
           class="el-button-custom"
           @click="handleGenerateImg"
           :loading="docLoading"
         >
           鐢熸垚鍥剧墖
-        </el-button>
+        </el-button> -->
       </el-form-item>
-      <el-form-item>
+      <!-- <el-form-item>
         <el-image :src="base64Url" fit="fill" :preview-src-list="[base64Url]" />
-      </el-form-item>
-      <el-form-item>
+      </el-form-item> -->
+      <!-- <el-form-item>
         <el-button
           type="primary"
           class="el-button-custom"
@@ -53,7 +53,7 @@
             />
           </el-form-item>
         </el-form-item>
-      </el-form-item>
+      </el-form-item> -->
     </el-form>
   </CardDialog>
 </template>

--
Gitblit v1.9.3