From b33e28bc2843555355ecad59a80c83e3c26445a3 Mon Sep 17 00:00:00 2001
From: riku <risaku@163.com>
Date: 星期一, 01 九月 2025 17:29:36 +0800
Subject: [PATCH] 走航季度报告模块(待完成)

---
 src/utils/doc.js                                  |  182 +++++++++++++++
 src/components/map/ConfigManage.vue               |   28 +
 package-lock.json                                 |  115 +++++++++
 src/api/dataAnalysisApi.js                        |   60 +++++
 public/underway_season_report.docx                |    0 
 src/api/index.js                                  |    4 
 package.json                                      |    5 
 src/components.d.ts                               |    3 
 src/views/historymode/component/MissionReport.vue |  314 ++++++++++++++++++++++++++
 9 files changed, 697 insertions(+), 14 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 950fa42..2159988 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,14 +17,19 @@
         "@fortawesome/vue-fontawesome": "^3.0.6",
         "@vueuse/core": "^10.9.0",
         "axios": "^1.6.8",
+        "docxtemplater": "^3.65.3",
+        "docxtemplater-image-module-free": "^1.1.1",
         "echarts": "^5.5.0",
         "element-plus": "^2.6.2",
+        "file-saver": "^2.0.5",
         "jquery": "^3.7.1",
         "jquery-xml2json": "^0.0.8",
         "jquery.soap": "^1.7.3",
         "js-base64": "^3.7.7",
+        "jszip-utils": "^0.1.0",
         "moment": "^2.30.1",
         "pinia": "^2.1.7",
+        "pizzip": "^3.2.0",
         "strong-soap": "^4.1.3",
         "unplugin-vue-components": "^0.26.0",
         "vue": "^3.4.21",
@@ -2056,6 +2061,33 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/docxtemplater": {
+      "version": "3.65.3",
+      "resolved": "https://registry.npmmirror.com/docxtemplater/-/docxtemplater-3.65.3.tgz",
+      "integrity": "sha512-NMCUehaHAz1itLGBz+GhVMX6gQ/ipqDicPoTPJ+ss/i9Jx7CVPuPj8yNPmvMFGgDrkZV8tOTTz6h/YXAztBDPA==",
+      "dependencies": {
+        "@xmldom/xmldom": "^0.9.8"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/docxtemplater-image-module-free": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/docxtemplater-image-module-free/-/docxtemplater-image-module-free-1.1.1.tgz",
+      "integrity": "sha512-aWOzVQN7ggDYjfoy3pTTNrcrZ7/CJrQcI9cT+hmyHE6nRLR67nt5yPFPe9hm9VWbfYIED2fi+3itOnF0TE/RWQ==",
+      "dependencies": {
+        "xmldom": "^0.1.27"
+      }
+    },
+    "node_modules/docxtemplater/node_modules/@xmldom/xmldom": {
+      "version": "0.9.8",
+      "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.8.tgz",
+      "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==",
+      "engines": {
+        "node": ">=14.6"
+      }
+    },
     "node_modules/dom-serializer": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -2682,6 +2714,11 @@
       "engines": {
         "node": "^10.12.0 || >=12.0.0"
       }
+    },
+    "node_modules/file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
     },
     "node_modules/file-uri-to-path": {
       "version": "1.0.0",
@@ -3503,6 +3540,11 @@
         "verror": "1.10.0"
       }
     },
+    "node_modules/jszip-utils": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmmirror.com/jszip-utils/-/jszip-utils-0.1.0.tgz",
+      "integrity": "sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg=="
+    },
     "node_modules/keyv": {
       "version": "4.5.4",
       "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
@@ -4092,6 +4134,11 @@
         "node": ">=10"
       }
     },
+    "node_modules/pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
@@ -4231,6 +4278,14 @@
         "@vue/composition-api": {
           "optional": true
         }
+      }
+    },
+    "node_modules/pizzip": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/pizzip/-/pizzip-3.2.0.tgz",
+      "integrity": "sha512-X4NPNICxCfIK8VYhF6wbksn81vTiziyLbvKuORVAmolvnUzl1A1xmz9DAWKxPRq9lZg84pJOOAMq3OE61bD8IQ==",
+      "dependencies": {
+        "pako": "^2.1.0"
       }
     },
     "node_modules/pkg-types": {
@@ -5751,6 +5806,15 @@
       "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
       "dev": true
     },
+    "node_modules/xmldom": {
+      "version": "0.1.31",
+      "resolved": "https://registry.npmmirror.com/xmldom/-/xmldom-0.1.31.tgz",
+      "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==",
+      "deprecated": "Deprecated due to CVE-2021-21366 resolved in 0.5.0",
+      "engines": {
+        "node": ">=0.1"
+      }
+    },
     "node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
@@ -7236,6 +7300,29 @@
         "esutils": "^2.0.2"
       }
     },
+    "docxtemplater": {
+      "version": "3.65.3",
+      "resolved": "https://registry.npmmirror.com/docxtemplater/-/docxtemplater-3.65.3.tgz",
+      "integrity": "sha512-NMCUehaHAz1itLGBz+GhVMX6gQ/ipqDicPoTPJ+ss/i9Jx7CVPuPj8yNPmvMFGgDrkZV8tOTTz6h/YXAztBDPA==",
+      "requires": {
+        "@xmldom/xmldom": "^0.9.8"
+      },
+      "dependencies": {
+        "@xmldom/xmldom": {
+          "version": "0.9.8",
+          "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.8.tgz",
+          "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A=="
+        }
+      }
+    },
+    "docxtemplater-image-module-free": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/docxtemplater-image-module-free/-/docxtemplater-image-module-free-1.1.1.tgz",
+      "integrity": "sha512-aWOzVQN7ggDYjfoy3pTTNrcrZ7/CJrQcI9cT+hmyHE6nRLR67nt5yPFPe9hm9VWbfYIED2fi+3itOnF0TE/RWQ==",
+      "requires": {
+        "xmldom": "^0.1.27"
+      }
+    },
     "dom-serializer": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -7723,6 +7810,11 @@
       "requires": {
         "flat-cache": "^3.0.4"
       }
+    },
+    "file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
     },
     "file-uri-to-path": {
       "version": "1.0.0",
@@ -8342,6 +8434,11 @@
         "verror": "1.10.0"
       }
     },
+    "jszip-utils": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmmirror.com/jszip-utils/-/jszip-utils-0.1.0.tgz",
+      "integrity": "sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg=="
+    },
     "keyv": {
       "version": "4.5.4",
       "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
@@ -8788,6 +8885,11 @@
         "p-limit": "^3.0.2"
       }
     },
+    "pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
+    },
     "parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
@@ -8879,6 +8981,14 @@
           "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",
           "requires": {}
         }
+      }
+    },
+    "pizzip": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/pizzip/-/pizzip-3.2.0.tgz",
+      "integrity": "sha512-X4NPNICxCfIK8VYhF6wbksn81vTiziyLbvKuORVAmolvnUzl1A1xmz9DAWKxPRq9lZg84pJOOAMq3OE61bD8IQ==",
+      "requires": {
+        "pako": "^2.1.0"
       }
     },
     "pkg-types": {
@@ -9986,6 +10096,11 @@
       "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
       "dev": true
     },
+    "xmldom": {
+      "version": "0.1.31",
+      "resolved": "https://registry.npmmirror.com/xmldom/-/xmldom-0.1.31.tgz",
+      "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ=="
+    },
     "yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
diff --git a/package.json b/package.json
index 9ca753d..f2b4b81 100644
--- a/package.json
+++ b/package.json
@@ -23,14 +23,19 @@
     "@fortawesome/vue-fontawesome": "^3.0.6",
     "@vueuse/core": "^10.9.0",
     "axios": "^1.6.8",
+    "docxtemplater": "^3.65.3",
+    "docxtemplater-image-module-free": "^1.1.1",
     "echarts": "^5.5.0",
     "element-plus": "^2.6.2",
+    "file-saver": "^2.0.5",
     "jquery": "^3.7.1",
     "jquery-xml2json": "^0.0.8",
     "jquery.soap": "^1.7.3",
     "js-base64": "^3.7.7",
+    "jszip-utils": "^0.1.0",
     "moment": "^2.30.1",
     "pinia": "^2.1.7",
+    "pizzip": "^3.2.0",
     "strong-soap": "^4.1.3",
     "unplugin-vue-components": "^0.26.0",
     "vue": "^3.4.21",
diff --git a/public/underway_season_report.docx b/public/underway_season_report.docx
new file mode 100644
index 0000000..1b28d15
--- /dev/null
+++ b/public/underway_season_report.docx
Binary files differ
diff --git a/src/api/dataAnalysisApi.js b/src/api/dataAnalysisApi.js
index fe9e1b0..0035d5c 100644
--- a/src/api/dataAnalysisApi.js
+++ b/src/api/dataAnalysisApi.js
@@ -16,5 +16,65 @@
     return $http
       .get(`air/analysis/pollution/trace/history`, { params: { missionCode } })
       .then((res) => res.data);
+  },
+
+  /**
+   * 鑾峰彇璧拌埅浠诲姟姹囨�荤粺璁�
+   * @param {*} startTime 寮�濮嬫椂闂达紝鏍煎紡YYYY-MM-DD HH:mm:ss
+   * @param {*} endTime 缁撴潫鏃堕棿锛屾牸寮廦YYY-MM-DD HH:mm:ss
+   * @param {*} area 鍖哄煙
+   * @returns
+   */
+  fetchMissionSummary({ startTime, endTime, area }) {
+    return $http
+      .post(`air/analysis/report/missionSummary`, area, {
+        params: { startTime, endTime }
+      })
+      .then((res) => res.data);
+  },
+
+  /**
+   * 鑾峰彇璧拌埅浠诲姟娓呭崟
+   * @param {*} startTime 寮�濮嬫椂闂达紝鏍煎紡YYYY-MM-DD HH:mm:ss
+   * @param {*} endTime 缁撴潫鏃堕棿锛屾牸寮廦YYY-MM-DD HH:mm:ss
+   * @param {*} area 鍖哄煙
+   * @returns
+   */
+  fetchMissionList({ startTime, endTime, area }) {
+    return $http
+      .post(`air/analysis/report/missionList`, area, {
+        params: { startTime, endTime }
+      })
+      .then((res) => res.data);
+  },
+
+  /**
+   * 鑾峰彇璧拌埅浠诲姟璇︽儏
+   * @param {*} startTime 寮�濮嬫椂闂达紝鏍煎紡YYYY-MM-DD HH:mm:ss
+   * @param {*} endTime 缁撴潫鏃堕棿锛屾牸寮廦YYY-MM-DD HH:mm:ss
+   * @param {*} area 鍖哄煙
+   * @returns
+   */
+  fetchMissionDetail({ startTime, endTime, area }) {
+    return $http
+      .post(`air/analysis/report/missionDetail`, area, {
+        params: { startTime, endTime }
+      })
+      .then((res) => res.data);
+  },
+
+  /**
+   * 鑾峰彇璧拌埅鍏稿瀷闅愭偅鍖哄煙
+   * @param {*} startTime 寮�濮嬫椂闂达紝鏍煎紡YYYY-MM-DD HH:mm:ss
+   * @param {*} endTime 缁撴潫鏃堕棿锛屾牸寮廦YYY-MM-DD HH:mm:ss
+   * @param {*} area 鍖哄煙
+   * @returns
+   */
+  fetchClueByRiskArea({ startTime, endTime, area }) {
+    return $http
+      .post(`air/analysis/report/clueByRiskArea`, area, {
+        params: { startTime, endTime }
+      })
+      .then((res) => res.data);
   }
 };
diff --git a/src/api/index.js b/src/api/index.js
index edabfb4..ef12b54 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -1,8 +1,8 @@
 import axios from 'axios';
 import { ElMessage } from 'element-plus';
 
-const openLog = false;
-const debug = false;
+const openLog = true;
+const debug = true;
 
 let ip1 = 'http://47.100.191.150:9029/';
 let ws = `47.100.191.150:9031`;
diff --git a/src/components.d.ts b/src/components.d.ts
index fa422c5..462a661 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -14,6 +14,7 @@
     'CardDialog copy': typeof import('./components/CardDialog copy.vue')['default']
     CheckButton: typeof import('./components/common/CheckButton.vue')['default']
     ConfigManage: typeof import('./components/map/ConfigManage.vue')['default']
+    copy: typeof import('./components/CardDialog copy.vue')['default']
     CoreHeader: typeof import('./components/core/CoreHeader.vue')['default']
     CoreMenu: typeof import('./components/core/CoreMenu.vue')['default']
     DataSummary: typeof import('./components/monitor/DataSummary.vue')['default']
@@ -30,7 +31,6 @@
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
-    ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -38,7 +38,6 @@
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElInput: typeof import('element-plus/es')['ElInput']
-    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
diff --git a/src/components/map/ConfigManage.vue b/src/components/map/ConfigManage.vue
index 7c573f8..819c31f 100644
--- a/src/components/map/ConfigManage.vue
+++ b/src/components/map/ConfigManage.vue
@@ -27,20 +27,28 @@
             璁惧绠$悊
           </el-button>
         </el-dropdown-item>
+        <el-dropdown-item>
+          <el-button
+            type="info"
+            icon="Document"
+            plain
+            @click="reportVisible = !reportVisible"
+          >
+            璧拌埅鎶ュ憡
+          </el-button>
+        </el-dropdown-item>
       </el-dropdown-menu>
     </template>
   </el-dropdown>
   <MissionManage v-model="missionVisible"></MissionManage>
   <DeviceManage v-model="deviceVisible"></DeviceManage>
+  <MissionReport v-model="reportVisible"></MissionReport>
 </template>
-<script>
-export default {
-  data() {
-    return {
-      missionVisible: false,
-      deviceVisible: false
-    };
-  },
-  methods: {}
-};
+<script setup>
+import MissionReport from '@/views/historymode/component/MissionReport.vue';
+import { ref } from 'vue';
+
+const missionVisible = ref(false);
+const deviceVisible = ref(false);
+const reportVisible = ref(false);
 </script>
diff --git a/src/utils/doc.js b/src/utils/doc.js
new file mode 100644
index 0000000..8082330
--- /dev/null
+++ b/src/utils/doc.js
@@ -0,0 +1,182 @@
+import JSZipUtils from 'jszip-utils';
+import docxtemplater from 'docxtemplater';
+import ImageModule from 'docxtemplater-image-module-free';
+import Pizzip from 'pizzip';
+import FileSaver from 'file-saver';
+
+/**
+ * 绛夋瘮渚嬬缉鏀惧浘鐗�
+ * 鏍规嵁鍥剧墖鐨勯暱瀹芥瘮杩涜涓嶅悓鏂瑰紡鐨勭缉鏀�
+ * 濡傛灉瀹藉害澶т簬楂樺害锛堟í鎷嶅浘鐗囷級锛屽垯鎸夌収璁惧畾楂樺害绛夋瘮缂╂斁锛�
+ * 濡傛灉瀹藉害灏忎簬楂樺害锛堢珫鎷嶅浘鐗囷級锛屽垯鎸夌収璁惧畾瀹藉害绛夋瘮缂╂斁锛�
+ * @param {Number} horizontalHeight 璁惧畾楂樺害
+ * @param {Number} verticalWidth 璁惧畾瀹藉害
+ * @param {*} img
+ * @param {*} tagValue
+ * @param {*} tagName
+ * @returns
+ */
+function getSizeProportional(
+  horizontalHeight,
+  verticalWidth,
+  img,
+  tagValue,
+  tagName
+) {
+  return new Promise(function (resolve, reject) {
+    const image = new Image();
+    image.src = tagValue;
+    image.onload = function () {
+      let width = image.width;
+      let height = image.height;
+      // console.log('width height', width, height);
+
+      if (width > height && horizontalHeight && height > horizontalHeight) {
+        const scale = image.height / horizontalHeight;
+        height = horizontalHeight;
+        width = image.width / scale;
+      } else if (width <= height && verticalWidth && width > verticalWidth) {
+        const scale = image.width / verticalWidth;
+        width = verticalWidth;
+        height = image.height / scale;
+      }
+      // console.log('scale', width, height);
+
+      resolve([width, height]);
+    };
+    image.onerror = function (e) {
+      console.log('img, tagValue, tagName : ', img, tagValue, tagName);
+      alert('An error occured while loading ' + tagValue);
+      reject(e);
+    };
+  });
+}
+
+/**
+ * 鍥哄畾澶у皬缂╂斁鍥剧墖
+ * 鏍规嵁鍥剧墖鐨勯暱瀹芥瘮杩涜涓嶅悓鏂瑰紡鐨勭缉鏀�
+ * 濡傛灉瀹藉害澶т簬楂樺害锛堟í鎷嶅浘鐗囷級锛屽垯鎸夌収璁惧畾楂樺害鍜屽楂樻瘮缂╂斁锛�
+ * 濡傛灉瀹藉害灏忎簬楂樺害锛堢珫鎷嶅浘鐗囷級锛屽垯鎸夌収璁惧畾瀹藉害鍜屽楂樻瘮缂╂斁锛�
+ * @param {*} horizontalHeight 璁惧畾楂樺害
+ * @param {*} verticalWidth 璁惧畾瀹藉害
+ * @param {*} scale 缂╂斁姣斾緥锛岄暱杈归櫎鐭竟鐨勬瘮渚嬬郴鏁�
+ * @param {*} img
+ * @param {*} tagValue
+ * @param {*} tagName
+ * @returns
+ */
+function getSizeFixed(
+  horizontalHeight,
+  verticalWidth,
+  scale,
+  img,
+  tagValue,
+  tagName
+) {
+  return new Promise(function (resolve, reject) {
+    const image = new Image();
+    image.src = tagValue;
+    image.onload = function () {
+      let width = image.width;
+      let height = image.height;
+
+      if (
+        width > height &&
+        horizontalHeight &&
+        height > horizontalHeight &&
+        scale
+      ) {
+        height = horizontalHeight;
+        width = horizontalHeight * scale;
+      } else if (
+        width <= height &&
+        verticalWidth &&
+        width > verticalWidth &&
+        scale
+      ) {
+        width = verticalWidth;
+        height = verticalWidth * scale;
+      }
+      resolve([width, height]);
+    };
+    image.onerror = function (e) {
+      console.log('img, tagValue, tagName : ', img, tagValue, tagName);
+      alert('An error occured while loading ' + tagValue);
+      reject(e);
+    };
+  });
+}
+
+/**
+ * 鑾峰彇鍥剧墖閰嶇疆淇℃伅
+ * @param {Number} horizontalHeight 鍥剧墖瀹藉害澶т簬楂樺害鏃讹紝闄愬埗鍏堕珮搴�
+ * @param {Number} verticalWidth 鍥剧墖瀹藉害灏忎簬楂樺害鏃讹紝闄愬埗鍏跺搴�
+ * @returns
+ */
+function getImageOptions(options) {
+  const horizontalHeight = options ? options.horizontalHeight : undefined;
+  const verticalWidth = options ? options.verticalWidth : undefined;
+  return {
+    centered: false,
+    fileType: 'docx',
+    getImage(tagValue) {
+      // In this case tagValue will be a URL tagValue = "https://docxtemplater.com/puffin.png"
+      return new Promise(function (resolve, reject) {
+        JSZipUtils.getBinaryContent(tagValue, function (error, content) {
+          if (error) {
+            return reject(error);
+          }
+          return resolve(content);
+        });
+      });
+    },
+
+    getSize(img, tagValue, tagName) {
+      // return getSizeProportional(horizontalHeight, verticalWidth, img, tagValue, tagName)
+      return getSizeFixed(
+        horizontalHeight,
+        verticalWidth,
+        options.scale,
+        img,
+        tagValue,
+        tagName
+      );
+    }
+  };
+}
+
+export const exportDocx = (tempDocpath, data, zipName, imageSize) => {
+  return new Promise((resolve, reject) => {
+    JSZipUtils.getBinaryContent(tempDocpath, (error, content) => {
+      if (error) {
+        reject(error);
+        throw error;
+      }
+      const zip = new Pizzip(content);
+      let doc = new docxtemplater()
+        .setOptions({ paragraphLoop: true })
+        .loadZip(zip);
+
+      if (imageSize) {
+        const imageOptions = getImageOptions(imageSize);
+        doc.attachModule(new ImageModule(imageOptions));
+      }
+      doc.compile();
+      doc.resolveData(data).then(() => {
+        try {
+          doc.render();
+        } catch (error) {
+          console.log(error);
+          throw error;
+        }
+        const out = doc.getZip().generate({
+          type: 'blob',
+          mimeType:
+            'application/vnd.openxmlformats-officedocumnet.wordprocessingml.document'
+        });
+        FileSaver.saveAs(out, zipName);
+        resolve();
+      });
+    });
+  });
+};
diff --git a/src/views/historymode/component/MissionReport.vue b/src/views/historymode/component/MissionReport.vue
new file mode 100644
index 0000000..32fd62f
--- /dev/null
+++ b/src/views/historymode/component/MissionReport.vue
@@ -0,0 +1,314 @@
+<template>
+  <!-- <el-button type="primary" class="el-button-custom" @click="handleClick">
+    涓嬭浇鎶ュ憡
+  </el-button> -->
+  <CardDialog v-bind="$attrs" title="璧拌埅鎶ュ憡鐢熸垚">
+    <el-form ref="formRef" label-width="120px">
+      <el-form-item label="鍖哄煙" prop="area">
+        <OptionLocation2
+          :level="3"
+          :initValue="false"
+          :checkStrictly="false"
+          :allOption="false"
+          v-model="formObj.location"
+        ></OptionLocation2>
+      </el-form-item>
+      <OptionTime v-model="formObj.timeArray"></OptionTime>
+      <el-form-item>
+        <el-button
+          type="primary"
+          class="el-button-custom"
+          @click="handleClick"
+          :loading="docLoading"
+        >
+          涓嬭浇鎶ュ憡
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </CardDialog>
+</template>
+<script setup>
+import { computed, ref } from 'vue';
+import moment from 'moment';
+import dataAnalysisApi from '@/api/dataAnalysisApi';
+import { exportDocx } from '@/utils/doc';
+
+const formObj = ref({
+  timeArray: [new Date('2025-07-01T00:00:00'), new Date('2025-08-31T23:59:59')],
+  location: {}
+});
+
+const docLoading = ref(false);
+
+const params = computed(() => {
+  return {
+    startTime: moment(formObj.value.timeArray[0]).format('YYYY-MM-DD HH:mm:ss'),
+    endTime: moment(formObj.value.timeArray[1]).format('YYYY-MM-DD HH:mm:ss'),
+    // startTime: formObj.value.timeArray[0],
+    // endTime: formObj.value.timeArray[1],
+    area: {
+      provinceCode: formObj.value.location.pCode,
+      provinceName: formObj.value.location.pName,
+      cityCode: formObj.value.location.cCode,
+      cityName: formObj.value.location.cName,
+      districtCode: formObj.value.location.dCode,
+      districtName: formObj.value.location.dName
+    }
+  };
+});
+
+const templateParam = {
+  sryTime: '2025骞寸涓夊搴︼紙7-9鏈堬級',
+  sryArea: '闈欏畨鍖�',
+  sryCount: '5',
+  sryKm: '1000',
+  sryRegion: '鍖哄煙1銆佸尯鍩�2',
+  sryCountByDegree: '浼榅娆★紙 %锛夈�佽壇X娆★紙 %锛夊拰杞诲害姹℃煋X娆★紙 %锛夌瓑',
+  sryProbCount: 10,
+  srySceneCount: 5,
+  sryProbByFactor:
+    '棰楃矑鐗╋紙PM锛夌浉鍏砐澶勶紝鍗犳瘮 %锛屼富瑕佹秹鍙婂伐鍦版壃灏樻薄鏌撻棶棰樸�侀亾璺壃灏樻薄鏌撻棶棰樼瓑锛沄OC鐩稿叧X澶勶紝鍗犳瘮 %锛屼富瑕佹秹鍙婂姞娌圭珯娌规皵娉勯湶銆侀楗补鐑熸薄鏌撶瓑',
+  missionInfoList: [
+    {
+      missionCode: '',
+      _time: '',
+      region: '',
+      _airQulity: 'AQI锛�30锛堜紭锛�',
+      mainFactor: '',
+      _abnormalFactors: '',
+      sceneCount: 0
+    }
+  ],
+  missionDetailList: [
+    {
+      _startTime: '2025骞�07鏈�29鏃�',
+      _time: '09:00鑷�14:30',
+      _kilometres: '1000',
+      _keyScene: '1涓浗鎺х偣锛堥潤瀹夌洃娴嬬珯锛夊拰2涓競鎺х偣锛堝拰鐢颁腑瀛︺�佸競鍖楅珮鏂帮級',
+      _dataStat:
+        'PM鈧�.鈧咃紙鑼冨洿30鈥�35 渭g/m鲁锛屽潎鍊�35.51 渭g/m鲁锛夈�丳M鈧佲個锛堣寖鍥�25鈥�68 渭g/m鲁锛屽潎鍊�38 渭g/m鲁锛夈�丯O鈧傦紙鑼冨洿22鈥�54 渭g/m鲁锛屽潎鍊�32 渭g/m鲁锛夈�丆O锛堣寖鍥�2.08鈥�6.39 mg/m鲁锛屽潎鍊�3.398 mg/m鲁锛夊拰NO锛堣寖鍥�1鈥�106 渭g/m鲁锛屽潎鍊�20.97 渭g/m鲁锛�',
+      aqi: 30,
+      pollutionDegree: '浼�'
+    }
+  ]
+};
+
+const handleClick = () => {
+  generateMissionSummary(params.value).then((res) => {
+    // generateDocx();
+    generateMissionList(params.value).then((res) => {
+      generateMissionDetail(params.value).then((res) => {
+        //     generateClueByRiskArea(params.value).then((res) => {});
+      });
+    });
+  });
+};
+
+function generateMissionSummary(param) {
+  return dataAnalysisApi.fetchMissionSummary(param).then((res) => {
+    templateParam.sryTime = getQuarterDescription(
+      new Date(res.data.startTime),
+      new Date(res.data.endTime)
+    );
+    templateParam.sryArea = res.data.area.districtName;
+    templateParam.sryCount = res.data.count;
+    templateParam.sryKm = Math.round(res.data.kilometres / 1000);
+    templateParam.sryRegion = res.data.regionList.join('銆�');
+    templateParam.sryCountByDegree =
+      res.data.countByDegree
+        .map((item) => {
+          return `${item.first}${item.second}娆★紙${Math.round(item.third * 1000) / 10}%锛塦;
+        })
+        .join('銆�') + '绛�';
+    templateParam.sryProbCount = res.data.probCount;
+    templateParam.srySceneCount = res.data.highRiskSceneCount;
+    templateParam.sryProbByFactor = res.data.probByFactor
+      .map((item) => {
+        return `${item.first}鐩稿叧${item.second}澶勶紝鍗犳瘮 ${Math.round(item.third * 1000) / 10}%锛屼富瑕佹秹鍙�${getPollutingProblemTypes(item.first)}绛塦;
+      })
+      .join('锛�');
+  });
+}
+
+function generateMissionList(param) {
+  return dataAnalysisApi.fetchMissionList(param).then((res) => {
+    templateParam.missionInfoList = res.data.map((item) => {
+      item._time = formatDateTimeRange(item.startTime, item.endTime);
+      item._airQulity = `AQI锛�${item.aqi}锛�${item.pollutionDegree}锛塦;
+      item._abnormalFactors = item.abnormalFactors
+        .map((factor) => factor.des)
+        .join('銆�');
+      return item;
+    });
+  });
+}
+
+function generateMissionDetail(param) {
+  return dataAnalysisApi.fetchMissionDetail(param).then((res) => {
+    templateParam.missionDetailList = res.data.map((item) => {
+      const t = formatDateTimeRange(item.startTime, item.endTime).split(' ');
+      item._startTime = t[0];
+      item._time = t[1];
+      item._kilometres = Math.round(item.kilometres / 1000);
+
+      const keySceneMap = new Map();
+      item.keyScene.forEach((e) => {
+        if (!keySceneMap.has(e.type)) {
+          keySceneMap.set(e.type, { scenes: [], count: 0 });
+        }
+        keySceneMap.get(e.type).scenes.push(e.scene);
+        keySceneMap.get(e.type).count++;
+      });
+      item._keyScene = [...keySceneMap]
+        .map(
+          ([type, info]) =>
+            `${info.count}涓�${type}锛�${info.scenes.map((s) => s.name).join('銆�')}锛塦
+        )
+        .join('銆�');
+      item._dataStat = item.dataStatistic
+        .map(
+          (e) =>
+            `${e.factor.des}锛堣寖鍥�${e.minValue}鈥�${e.maxValue}渭g/m鲁锛屽潎鍊�${e.avgValue}渭g/m鲁锛塦
+        )
+        .join('銆�');
+
+      return item;
+    });
+  });
+}
+
+function generateClueByRiskArea(param) {
+  return dataAnalysisApi.fetchClueByRiskArea(param).then((res) => {});
+}
+
+function generateDocx() {
+  docLoading.value = true;
+  exportDocx(
+    '/underway_season_report.docx',
+    templateParam,
+    `璧拌埅瀛e害鎶ュ憡.docx`,
+    {
+      horizontalHeight: 368,
+      verticalWidth: 266,
+      scale: 1.367
+    }
+  ).finally(() => (docLoading.value = false));
+}
+
+/**
+ * 鏍规嵁寮�濮嬫椂闂村拰缁撴潫鏃堕棿鐢熸垚瀛e害鎻忚堪
+ * @param {Date} startTime - 寮�濮嬫椂闂�
+ * @param {Date} endTime - 缁撴潫鏃堕棿
+ * @returns {string} 鏍煎紡鍖栫殑瀛e害鎻忚堪瀛楃涓�
+ */
+function getQuarterDescription(startTime, endTime) {
+  // 楠岃瘉鏃ユ湡瀵硅薄鏈夋晥鎬�
+  if (
+    !(startTime instanceof Date) ||
+    !(endTime instanceof Date) ||
+    isNaN(startTime.getTime()) ||
+    isNaN(endTime.getTime())
+  ) {
+    return '';
+  }
+
+  const startYear = startTime.getFullYear();
+  const startMonth = startTime.getMonth();
+  const startDate = startTime.getDate();
+  const endYear = endTime.getFullYear();
+  const endMonth = endTime.getMonth();
+  const endDate = endTime.getDate();
+
+  // 鍒ゆ柇鏄惁涓哄搴︾涓�澶�
+  let quarter = null;
+  if (startDate === 1) {
+    if (startMonth === 0)
+      quarter = 1; // Q1:1鏈�
+    else if (startMonth === 3)
+      quarter = 2; // Q2:4鏈�
+    else if (startMonth === 6)
+      quarter = 3; // Q3:7鏈�
+    else if (startMonth === 9) quarter = 4; // Q4:10鏈�
+  }
+
+  // 涓嶆槸瀛e害绗竴澶╁垯杩斿洖鍏蜂綋鏃ユ湡鑼冨洿
+  if (!quarter) {
+    return `${startYear}骞�${startMonth + 1}鏈�${startDate}鏃�-${endYear}骞�${endMonth + 1}鏈�${endDate}鏃;
+  }
+
+  // 楠岃瘉鏄惁涓哄搴斿搴︽渶鍚庝竴涓湀
+  const expectedEndMonth = quarter * 3 - 1; // Q1:2(3鏈�), Q2:5(6鏈�), Q3:8(9鏈�), Q4:11(12鏈�)
+  if (endMonth !== expectedEndMonth) {
+    return `${startYear}骞�${startMonth + 1}鏈�${startDate}鏃�-${endYear}骞�${endMonth + 1}鏈�${endDate}鏃;
+  }
+
+  // 楠岃瘉鏄惁涓哄搴︽渶鍚庝竴澶�
+  const lastDayOfEndMonth = new Date(endYear, endMonth + 1, 0).getDate();
+  if (endDate !== lastDayOfEndMonth) {
+    return `${startYear}骞�${startMonth + 1}鏈�${startDate}鏃�-${endYear}骞�${endMonth + 1}鏈�${endDate}鏃;
+  }
+
+  const quarterNames = ['', '绗竴瀛e害', '绗簩瀛e害', '绗笁瀛e害', '绗洓瀛e害'];
+  const monthRanges = ['', '1-3鏈�', '4-6鏈�', '7-9鏈�', '10-12鏈�'];
+  return `${startYear}骞�${quarterNames[quarter]}锛�${monthRanges[quarter]}锛塦;
+}
+
+/**
+ * 鏍规嵁绌烘皵璐ㄩ噺鐩戞祴鍥犲瓙杩斿洖鍙兘娑夊強鐨勬薄鏌撻棶棰樼被鍨�
+ * @param {string|string[]} factors - 绌烘皵璐ㄩ噺鐩戞祴鍥犲瓙锛屾敮鎸佸崟涓洜瀛愬瓧绗︿覆鎴栧洜瀛愭暟缁�
+ * @returns {string} 鍙兘娑夊強鐨勬薄鏌撻棶棰樼被鍨嬫弿杩帮紝澶氫釜绫诲瀷鐢ㄩ】鍙峰垎闅�
+ */
+function getPollutingProblemTypes(factors) {
+  // 鐩戞祴鍥犲瓙涓庢薄鏌撻棶棰樼被鍨嬬殑鏄犲皠鍏崇郴
+  const factorProblemMap = {
+    '棰楃矑鐗�(PM)': ['宸ュ湴鎵皹姹℃煋闂', '閬撹矾鎵皹姹℃煋闂'],
+    PM25: ['宸ュ湴鎵皹姹℃煋闂', '閬撹矾鎵皹姹℃煋闂'],
+    PM10: ['宸ュ湴鎵皹姹℃煋闂', '閬撹矾鎵皹姹℃煋闂'],
+    SO2: ['鐕冪叅鐢靛巶', '閽㈤搧鍘�', '鍖栧伐鍘�', '鏈夎壊閲戝睘鍐剁偧鍘�'],
+    NO2: ['鏈哄姩杞﹀熬姘旀帓鏀鹃棶棰�'],
+    姘哀鍖栫墿: ['鏈哄姩杞﹀熬姘旀帓鏀鹃棶棰�'],
+    CO: ['鏈哄姩杞﹀熬姘旀帓鏀鹃棶棰�'],
+    O3: ['鍔犳补绔�', '鏈哄姩杞﹀熬姘旀帓鏀鹃棶棰�'],
+    VOCs: ['鍔犳补绔欐补姘旀硠闇�', '椁愰ギ娌圭儫姹℃煋']
+  };
+
+  // 鏍囧噯鍖栬緭鍏ヤ负鏁扮粍
+  const factorArray = Array.isArray(factors) ? factors : [factors];
+
+  // 鏀堕泦鎵�鏈夊彲鑳界殑闂绫诲瀷骞跺幓閲�
+  const enterpriseSet = new Set();
+  factorArray.forEach((factor) => {
+    const trimmedFactor = factor.trim();
+    factorProblemMap[trimmedFactor].forEach((problem) => {
+      enterpriseSet.add(problem);
+    });
+  });
+
+  // 杞崲涓烘牸寮忓寲瀛楃涓茶繑鍥�
+  return Array.from(enterpriseSet).join('銆�');
+}
+
+/**
+ * 灏嗗紑濮嬪拰缁撴潫鏃堕棿鏍煎紡鍖栦负"YYYY骞碝M鏈圖D鏃� HH:mm鑷矵H:mm"鏍煎紡
+ * @param {Date|string} startTime - 寮�濮嬫椂闂达紙Date瀵硅薄鎴栧彲琚玬oment瑙f瀽鐨勫瓧绗︿覆锛�
+ * @param {Date|string} endTime - 缁撴潫鏃堕棿锛圖ate瀵硅薄鎴栧彲琚玬oment瑙f瀽鐨勫瓧绗︿覆锛�
+ * @returns {string} 鏍煎紡鍖栧悗鐨勬椂闂磋寖鍥村瓧绗︿覆
+ */
+function formatDateTimeRange(startTime, endTime) {
+  // 楠岃瘉杈撳叆鏈夋晥鎬�
+  if (!startTime || !endTime) return '';
+
+  const startMoment = moment(startTime);
+  const endMoment = moment(endTime);
+
+  // 妫�鏌ユ棩鏈熸槸鍚︽湁鏁�
+  if (!startMoment.isValid() || !endMoment.isValid()) return '';
+
+  // 鏍煎紡鍖栨棩鏈熼儴鍒嗗拰鏃堕棿閮ㄥ垎
+  const datePart = startMoment.format('YYYY骞碝M鏈圖D鏃�');
+  const startTimePart = startMoment.format('HH:mm');
+  const endTimePart = endMoment.format('HH:mm');
+
+  return `${datePart} ${startTimePart}鑷�${endTimePart}`;
+}
+</script>

--
Gitblit v1.9.3