<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-button
|
type="primary"
|
class="el-button-custom"
|
@click="handleGenerateImg"
|
:loading="docLoading"
|
>
|
生成图片
|
</el-button>
|
</el-form-item>
|
</el-form>
|
<el-form-item>
|
<el-image :src="base64Url" fit="fill" :preview-src-list="[base64Url]" />
|
</el-form-item>
|
</CardDialog>
|
</template>
|
<script setup>
|
import { computed, ref } from 'vue';
|
import moment from 'moment';
|
import dataAnalysisApi from '@/api/dataAnalysisApi';
|
import { exportDocx } from '@/utils/doc';
|
import { radioOptions } from '@/constant/radio-options';
|
import { TYPE0 } from '@/constant/device-type';
|
import { FactorDatas } from '@/model/FactorDatas';
|
import factorDataParser from '@/utils/chart/factor-data-parser';
|
import chartToImg from '@/utils/chart/chart-to-img';
|
|
const formObj = ref({
|
timeArray: [new Date('2025-07-01T00:00:00'), new Date('2025-08-31T23:59:59')],
|
location: {}
|
});
|
|
const docLoading = ref(false);
|
|
const base64Url = ref(null);
|
|
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: [
|
{
|
_index: 1,
|
_startTime: '2025年07月29日',
|
_time: '09:00至14:30',
|
_kilometres: '1000',
|
_keyScene: '1个国控点(静安监测站)和2个市控点(和田中学、市北高新)',
|
_dataStatistics: [
|
{
|
factor: 'PM10',
|
minValue: 25,
|
maxValue: 68,
|
avgValue: 38
|
}
|
],
|
aqi: 30,
|
pollutionDegree: '优'
|
}
|
],
|
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……'
|
}
|
]
|
}
|
]
|
}
|
]
|
};
|
|
const handleClick = () => {
|
docLoading.value = true;
|
generateMissionSummary(params.value).then(() => {
|
generateMissionList(params.value).then(() => {
|
generateMissionDetail(params.value).then(() => {
|
generateClueByRiskArea(params.value).then(() => {
|
generateDocx();
|
});
|
});
|
});
|
});
|
};
|
|
const handleGenerateImg = () => {
|
generateClueByRiskArea(params.value).then(() => {});
|
};
|
|
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)
|
.join('、');
|
return item;
|
});
|
});
|
}
|
|
function generateMissionDetail(param) {
|
return dataAnalysisApi.fetchMissionDetail(param).then((res) => {
|
templateParam.missionDetailList = res.data.map((item, index) => {
|
const t = formatDateTimeRange(item.startTime, item.endTime).split(' ');
|
item._index = index + 1;
|
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);
|
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.dataStatistics
|
.map(
|
(e) =>
|
`${e.factor.des}(范围${e.minValue}–${e.maxValue}μg/m³,均值${e.avgValue}μg/m³)`
|
)
|
.join('、');
|
|
const factorNames = radioOptions(TYPE0).map((e) => e.name);
|
item._dataStatistics = item.dataStatistics.filter((e) => {
|
return factorNames.indexOf(e.factor) != -1;
|
});
|
|
return item;
|
});
|
});
|
}
|
|
function generateClueByRiskArea(param) {
|
return dataAnalysisApi.fetchClueByRiskArea(param).then((res) => {
|
templateParam.clueByAreaList = res.data.map((item, index) => {
|
return {
|
_index: index + 1,
|
_area: item.sceneInfo.name + '周边',
|
clueByFactorList: item.clueByFactorList.map((cbf) => {
|
return {
|
factor: cbf.factor,
|
clues: cbf.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)
|
});
|
});
|
|
if (base64Url.value == null) {
|
base64Url.value = images[0].url;
|
}
|
}
|
});
|
return images;
|
}
|
|
function generateDocx() {
|
exportDocx(
|
'/underway_season_report.docx',
|
templateParam,
|
`走航季度报告.docx`,
|
{
|
horizontalHeight: 250,
|
verticalWidth: 568,
|
scale: 2
|
}
|
).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>
|