riku
2025-09-01 b33e28bc2843555355ecad59a80c83e3c26445a3
走航季度报告模块(待完成)
已修改6个文件
已添加3个文件
711 ■■■■■ 文件已修改
package-lock.json 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/underway_season_report.docx 补丁 | 查看 | 原始文档 | blame | 历史
src/api/dataAnalysisApi.js 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/ConfigManage.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/doc.js 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/historymode/component/MissionReport.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
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",
public/underway_season_report.docx
Binary files differ
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 ç»“束时间,格式YYYY-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 ç»“束时间,格式YYYY-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 ç»“束时间,格式YYYY-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 ç»“束时间,格式YYYY-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);
  }
};
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`;
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']
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>
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();
      });
    });
  });
};
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次( %)和轻度污染X次( %)等',
  sryProbCount: 10,
  srySceneCount: 5,
  sryProbByFactor:
    '颗粒物(PM)相关X处,占比 %,主要涉及工地扬尘污染问题、道路扬尘污染问题等;VOC相关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³)、PM₁₀(范围25–68 Î¼g/m³,均值38 Î¼g/m³)、NO₂(范围22–54 Î¼g/m³,均值32 Î¼g/m³)、CO(范围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,
    `走航季度报告.docx`,
    {
      horizontalHeight: 368,
      verticalWidth: 266,
      scale: 1.367
    }
  ).finally(() => (docLoading.value = false));
}
/**
 * æ ¹æ®å¼€å§‹æ—¶é—´å’Œç»“束时间生成季度描述
 * @param {Date} startTime - å¼€å§‹æ—¶é—´
 * @param {Date} endTime - ç»“束时间
 * @returns {string} æ ¼å¼åŒ–的季度描述字符串
 */
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月
  }
  // ä¸æ˜¯å­£åº¦ç¬¬ä¸€å¤©åˆ™è¿”回具体日期范围
  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 = ['', '第一季度', '第二季度', '第三季度', '第四季度'];
  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å¹´MM月DD日 HH:mm至HH:mm"格式
 * @param {Date|string} startTime - å¼€å§‹æ—¶é—´ï¼ˆDate对象或可被moment解析的字符串)
 * @param {Date|string} endTime - ç»“束时间(Date对象或可被moment解析的字符串)
 * @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å¹´MM月DD日');
  const startTimePart = startMoment.format('HH:mm');
  const endTimePart = endMoment.format('HH:mm');
  return `${datePart} ${startTimePart}至${endTimePart}`;
}
</script>