<template>
<a-spin :size="80" :loading="uploadeLoading" :tip="loadingTip" style="width: 100%; height: 100%">
<layout_1>
<a-scrollbar style="height: 800px; overflow: auto">
<div class="--search-line">
<div>
<div class="key">{{ $t(queryButtonValue[7]) }} </div>
<div class="val">
<a-range-picker
style="width: 280px"
:allow-clear="false"
v-model="param.timeRange"
:disabled-date="disabledDate">
<template #suffix-icon>
<svg-loader :width="20" :height="20" name="clock"></svg-loader>
</template>
<template #separator>
<svg-loader :width="16" :height="16" name="arrow-right"></svg-loader>
</template>
</a-range-picker>
</div>
</div>
<div>
<div class="key">{{ $t(queryButtonValue[10]) }} </div>
<div class="val select-input">
<a-tree-select
class="arco-tree-select --arco-select"
:data="treeData"
v-model="param.businessTypes"
:tree-checkable="true"
:tree-check-strictly="false"
tree-checked-strategy="child"
:max-tag-count="1"/>
</div>
</div>
<a-button class="huge" @click="search" type="primary">
<template #icon><icon-search size="18"/></template >{{ $t(queryButtonValue[1])}}
</a-button>
<a-button class="huge" @click="resetSearch">
<template #icon><svg-loader :width="20" :height="20" name="reset"></svg-loader></template >{{ $t(queryButtonValue[21])}}
</a-button >
<a-dropdown @select="handleSelect">
<a-button class="huge" :disabled="!tableData?.length">
<template #icon><icon-export size="18"/></template>{{ $t(queryButtonValue[3])}}
</a-button>
<template #content>
<a-doption value="excel">Excel</a-doption>
<a-doption value="pdf">PDF</a-doption>
<a-doption value="png">PNG</a-doption>
<a-doption value="jpeg">JPEG</a-doption>
</template>
</a-dropdown>
</div>
<div class="table-line" ref="exportContentRef">
<a-table
:data="tableData"
:loading="tableLoading"
:bordered="{ headerCell: true }"
:pagination="false"
:scroll="{ x: '100%', y: 550 }"
@row-click="handleRowClick">
<template #empty>
<div style="text-align: center">{{ $t("NoData")}}</div>
</template>
<template #columns>
<!-- 列定义省略 -->
</template>
</a-table>
<div class="--table-pager">
<a-config-provider :locale="arcoLang">
<a-pagination
:total="page.total"
v-model:current="page.currentPage"
v-model:page-size="page.pageSize"
@change="getTableData"
@page-size-change="getTableData"
show-total show-page-size show-jumper />
</a-config-provider>
</div>
<div v-if="showRecordChart" class="chart_box">
<div class="title">{{ detailTitle }}</div>
<div class="content" id="terminal_business_content_box">
<!-- 图表容器省略 -->
</div>
</div>
</div>
</a-scrollbar>
</layout_1>
</a-spin>
</template>
<script setup>
import { commonResponse, formatDateToYyyyMMddHHmmss, formatTime,} from "@/views/pages/_common";
import { queryButtonValue, queryColumnValue, queryName, statBusinessType,} from "@/views/pages/_common/enum";
import Layout_1 from "@/views/pages/_common/layout_1.vue";
import { exportTerminalBusinessDetailCountByDayBizType, exportTerminalBusinessStat, getTerminalBusinessStatList,} from "@/views/pages/business/_request";
import * as echarts from "echarts";
import html2canvas from "html2canvas";
import JsPDF from "jspdf";
import * as moment from "moment";
import { computed, inject, reactive, ref, watch } from "vue";
const arcoLang = inject("arcoLang");
const t = inject("t");
const exportType = ref('');
const uploadeLoading = ref(false);
const param = reactive({ timeRange:[null,null], businessTypes:[],});
let reqParam = { startTime:null, endTime:null, businessTypes:[],};
const page = reactive({ currentPage:1, pageSize:10, total:0,});
const treeData = [{ title:t(statBusinessType[0]), value:0, key:0, children:Object.keys(statBusinessType).slice(1).map((key)=>({ title:t(statBusinessType[key]), value:Number(key), key:Number(key),})), },];
watch(()=> param.businessTypes,(newValue)=>{
if(newValue.includes(0)){
param.businessTypes =[1,2,3,4,5];
}
});
const showRecordChart = ref(false);
const tableLoading = ref(false);
const exportContentRef = ref(null);
const tableData = ref([]);
const getTableData = ()=>{
tableLoading.value =true;
getTerminalBusinessStatList({...reqParam, currentPage: page.currentPage, pageSize: page.pageSize,}).then((response)=>{
tableLoading.value =false;
commonResponse({ response, onSuccess:()=>{ tableData.value = response.data; page.total = response.pagination.totalRecords;},});
}).catch((e)=>(tableLoading.value =true));
};
const disabledDate =(date)=>{ return date.getTime()>moment().format("x"); };
const search =()=>{
reqParam ={...reqParam,...param,};
reqParam.startTime =moment(reqParam.timeRange[0]).format("YYYY-MM-DDT00:00:00.000");
reqParam.endTime =moment(reqParam.timeRange[1]).format("YYYY-MM-DDT23:59:59.999");
delete reqParam.timeRange;
getTableData();
};
const resetSearch =()=>{
param.timeRange =[moment().add(-9,"days"),moment()];
param.businessTypes =[];
page.currentPage =1; page.total=0; tableData.value=[]; showRecordChart.value=false;
};
const loadingTip =computed(()=>{
if(!exportType.value) return t('ExportStatus_Exporting');
return `${exportType.value} ${t('Outputting')}`;
});
const handleSelect =(t)=>{ exportType.value = t; uploadeLoading.value =true; exportFormat(t); };
// 导出功能
const exportTable = async ()=>{
tableLoading.value =true;
try{
const response = await exportTerminalBusinessStat({...reqParam,});
tableLoading.value =false;
const blob =new Blob([response],{ type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download=t(queryName["RadioService"])+formatDateToYyyyMMddHHmmss(new Date());
link.click();
window.URL.revokeObjectURL(url);
}catch(error){
tableLoading.value =true;
console.error("导出.excel 图片失败:", error);
}finally{
setTimeout(()=> uploadeLoading.value =false,3000);
}
};
const exportFormat =(format)=>{
const title =t(queryName["RadioService"])+formatDateToYyyyMMddHHmmss(new Date());
if(format ==="excel"){ exportTable(); }
else if(format ==="pdf"){ exportPdf(exportContentRef.value, title); }
else if(format ==="png"){ exportPng(exportContentRef.value, title); }
else if(format ==="jpeg"){ exportJpeg(exportContentRef.value, title); }
};
const exportPdf = async (el, title)=>{
const contentHeight = el.scrollHeight;
const contentWidth = el.scrollWidth;
const getDynamicScale =(element)=>{
try{
if(!element ||!element.getBoundingClientRect){ return 1; }
const rect = element.getBoundingClientRect();
const area = rect.width * rect.height;
if(isNaN(area)|| area <=0){ return 1; }
const areaMB = area /1000000;
if(!isFinite(areaMB)){ return 1; }
if(areaMB >20){return 0.5;}
else if(areaMB >15){return 0.75;}
else if(areaMB >13){return 0.8;}
else if(areaMB >10){return 0.9;}
else if(areaMB >5){return 0.95;}
else if(areaMB >4){return 1;}
else if(areaMB >3){return 1.25;}
else if(areaMB >2.5){return 1.5;}
else if(areaMB >2){return 1.5;}
else{return 2;}
}catch(error){ return 1; }
};
const scale =getDynamicScale(el);
try{
const canvas = await html2canvas(el,{ allowTaint:true, taintTest:false, useCORS:true, logging:false, imageTimeout:0, scale: scale, scrollX:0, scrollY:0, width: contentWidth, height: contentHeight,});
let pageHeight =(contentWidth /592.28)*841.89;
let leftHeight = contentHeight;
let position =0;
let imgWidth =592.28;
let imgHeight =(592.28/ contentWidth)* contentHeight;
let pageData = canvas.toDataURL("image/jpeg",1.0);
let PDF =new JsPDF("","pt","a4");
if(leftHeight < pageHeight){
PDF.addImage(pageData,"JPEG",0,0, imgWidth, imgHeight);
}else{
while(leftHeight >0){
PDF.addImage(pageData,"JPEG",0, position, imgWidth, imgHeight);
leftHeight -= pageHeight;
position -= imgHeight;
if(leftHeight >0){ PDF.addPage(); }
}
}
PDF.save(title +".pdf");
}catch(error){ console.error("导出.pdf 图片失败:", error); }
finally{ setTimeout(()=> uploadeLoading.value =false,3000); }
};
const exportPng = async (el, title)=>{
const contentHeight = el.scrollHeight;
const contentWidth = el.scrollWidth;
try{
const canvas = await html2canvas(el,{ allowTaint:true, taintTest:false, useCORS:true, logging:false, imageTimeout:0, scale:2, scrollX:0, scrollY:0, width: contentWidth, height: contentHeight,});
let url = canvas.toDataURL("image/png");
let a = document.createElement("a");
a.href = url;
a.download = title +".png";
a.click();
setTimeout(()=> URL.revokeObjectURL(url),1000);
}catch(error){ console.error("导出.png 图片失败:", error); }
finally{ setTimeout(()=> uploadeLoading.value =false,3000); }
};
const exportJpeg = async (el, title)=>{
const contentHeight = el.scrollHeight;
const contentWidth = el.scrollWidth;
try{
const canvas = await html2canvas(el,{ allowTaint:true, taintTest:false, useCORS:true, logging:false, imageTimeout:0, scale:2, scrollX:0, scrollY:0, width: contentWidth, height: contentHeight,});
let url = canvas.toDataURL("image/jpeg",0.9);
let a = document.createElement("a");
a.href = url;
a.download = title +".jpg";
a.click();
setTimeout(()=> URL.revokeObjectURL(url),1000);
}catch(error){ console.error("导出.jpg 图片失败:", error); }
finally{ setTimeout(()=> uploadeLoading.value =false,3000); }
};
// 图表渲染逻辑省略...
const init =()=>{ resetSearch(); };
init();
</script>
<style scoped lang="less">
.table-line { box-sizing: border-box; margin-top:20px; height:calc(100%-60px); }
.select-input { width:160px; }
.title { margin-top:20px; margin-left:40px; padding-left:10px; }
.content { width:100%; margin-top:20px; &-item { position: relative; box-sizing: border-box; border:1px solid #e5e7ec; border-radius:10px; padding:20px; }}
</style>
<template>
<a-spin :size="80" :loading="uploadeLoading" :tip="loadingTip" style="width: 100%; height: 100%">
<layout_1 :auto-height="true">
<div class="--search-line">
<div>
<div class="key">{{ $t(queryButtonValue[7]) }} </div>
<div class="val">
<a-range-picker style="width: 280px" :allow-clear="false" :disabled-date="disabledDate" v-model="param.timeRange">
<template #suffix-icon><svg-loader :width="20" :height="20" name="clock"></svg-loader></template>
<template #separator><svg-loader :width="16" :height="16" name="arrow-right"></svg-loader></template>
</a-range-picker>
</div>
</div>
<div>
<div class="key">{{ $t(queryButtonValue[8]) }} </div>
<div class="val">
<a-tree-select class="arco-tree-select --arco-select" style="width: 230px" :field-names="{ key: 'serialNo', title:'name', children: 'children',}" :data="treeSelectNodeData" :multiple="true" :tree-checkable="true" tree-checked-strategy="child" :max-tag-count="1" v-model:model-value="param.repeaterSNs"></a-tree-select>
</div>
</div>
<a-button class="huge" @click="search" type="primary">
<template #icon><icon-search size="18"/></template >{{ $t(queryButtonValue[1])}}
</a-button>
<a-button class="huge" @click="resetSearch">
<template #icon><svg-loader :width="20" :height="20" name="reset"></svg-loader></template >{{ $t(queryButtonValue[21])}}
</a-button>
<a-dropdown @select="handleSelect">
<a-button class="huge" :disabled="!dataList?.length">
<template #icon><icon-export size="18"/></template >{{ $t(queryButtonValue[3])}}
</a-button >
<template #content>
<a-doption value="pdf">PDF</a-doption>
<a-doption value="png">PNG</a-doption>
<a-doption value="jpeg">JPEG</a-doption>
</template>
</a-dropdown>
</div>
<div class="statistics-box">
<a-scrollbar outer-style="height: 100%; width: calc(100%)" style="height: 100%; overflow: scroll">
<div class="statistics-box-id" ref="exportContentRef">
<div v-for="item in dataList" :key="item.repeaterSN">
<div class="title">{{ $t(queryColumnValue[50])}}({{ item.repeaterAlias }})</div>
<div class="content" id="transfer_business_content_box">
<!-- 图表区域省略 -->
</div>
</div>
</div>
</a-scrollbar>
</div>
</layout_1>
</a-spin>
</template>
<script setup>
import Layout_1 from "@/views/pages/_common/layout_1.vue";
import * as moment from "moment/moment";
import * as echarts from "echarts";
import {nextTick, reactive, ref, inject, computed} from "vue";
import html2canvas from "html2canvas";
import JsPDF from "jspdf";
import { qryTransferNodeList } from "@/views/pages/topology/_request";
import { getTransferBusinessData } from "@/views/pages/business/_request";
import { commonResponse, formatDateToYyyyMMddHHmmss,} from "@/views/pages/_common";
import { queryButtonValue, queryColumnValue, queryName,} from "@/views/pages/_common/enum";
import { colorChart, statBusinessType, exportSlot,} from "@/views/pages/_common/enum";
const t = inject("t");
const param = reactive({ timeRange:[null,null], repeaterSNs:[],});
let reqParam = { startTime:null, endTime:null, repeaterSNs:[],};
const treeSelectNodeData = ref([]);
const exportType = ref('');
const dataList = ref([]);
const tableLoading = ref(false);
const chartList = [];
const getAllData =()=>{
tableLoading.value =true;
getTransferBusinessData({...reqParam,}).then((response)=>{
tableLoading.value =false;
commonResponse({ response, onSuccess:()=>{ dataList.value = response.data; nextTick(()=>{initCharts();});},});
}).catch((e)=>(tableLoading.value =true));
};
const initCharts =()=>{
chartList.splice(0, chartList.length -1);
dataList.value?.forEach((item)=>{
chartList.push(initByDateLine(item.repeaterSN, item.businessDataDetailList1, item.businessDataDetailList2));
chartList.push(initBusinessTimePie(item.repeaterSN, item.datas));
});
window.addEventListener("resize",()=>{ chartList.forEach((item)=> item.resize({ width:"auto", height:"auto"})); });
};
const initByDateLine =(id, data, data2)=>{
// ECharts 初始化逻辑省略
const chartDom = document.querySelector(`#business_by_date_${id}`);
const chart = echarts.init(chartDom);
const option = { /* 配置项省略 */ };
option && chart.setOption(option);
return chart;
};
const initBusinessTimePie =(id, data)=>{
const chartDom = document.querySelector(`#business_by_time_pie_${id}`);
const chart = echarts.init(chartDom);
const option = { /* 饼图配置省略 */ };
option && chart.setOption(option);
return chart;
};
const calculatedPercentages =(datas)=>{
const total = Object.values(datas).reduce((sum, value)=> sum + value,0);
if(total ===0){ return Object.keys(datas).reduce((result, key)=>{ result[key]="0.00"; return result;},{}); }
return Object.keys(datas).reduce((result, key)=>{
const percentage = ((datas[key]/ total)*100).toFixed(2);
result[key]= percentage;
return result;
},{});
};
const search =()=>{
reqParam ={...reqParam,...param,};
reqParam.startTime =moment(reqParam.timeRange[0]).format("YYYY-MM-DDT00:00:00.000");
reqParam.endTime =moment(reqParam.timeRange[1]).format("YYYY-MM-DDT23:59:59.999");
delete reqParam.timeRange;
getAllData();
};
const resetSearch =()=>{
param.timeRange =[moment().add(-9,"days"),moment()];
param.repeaterSNs =[];
dataList.value =[];
};
const disabledDate =(date)=>{ return date.getTime()>moment().format("x"); };
const exportContentRef = ref(null);
const loadingTip =computed(()=>{ if(!exportType.value) return t('ExportStatus_Exporting'); return `${exportType.value} ${t('Outputting')}`; });
const handleSelect =(t)=>{ exportType.value = t; uploadeLoading.value =true; exportFormat(t); };
const exportFormat =(format)=>{
const title =t(queryName["RepeatedService"])+formatDateToYyyyMMddHHmmss(new Date());
if(format ==="pdf"){ exportPdf(exportContentRef.value, title); }
else if(format ==="png"){ exportPng(exportContentRef.value, title); }
else if(format ==="jpeg"){ exportJpeg(exportContentRef.value, title); }
};
const exportPdf = async (el, title)=>{
const PDF_WIDTH =595.28;
const PDF_HEIGHT =841.89;
const getDynamicScale =(element)=>{
try{
if(!element ||!element.getBoundingClientRect){ return 1; }
const rect = element.getBoundingClientRect();
const area = rect.width * rect.height;
if(isNaN(area)|| area <=0){ return 1; }
const areaMB = area /1000000;
if(!isFinite(areaMB)){ return 1; }
if(areaMB >20){return 0.5;}
else if(areaMB >15){return 0.75;}
else if(areaMB >13){return 0.8;}
else if(areaMB >10){return 0.9;}
else if(areaMB >5){return 0.95;}
else if(areaMB >3){return 1;}
else if(areaMB >2){return 1.25;}
else if(areaMB >1){return 1.5;}
else{return 2;}
}catch(error){ return 1; }
};
const scale =getDynamicScale(el);
try{
const canvas = await html2canvas(el,{ allowTaint:true, taintTest:false, useCORS:true, logging:false, imageTimeout:0, scale: scale,});
const contentWidth = canvas.width;
const contentHeight = canvas.height;
const imgWidth = PDF_WIDTH;
const imgHeight =(PDF_WIDTH / contentWidth)* contentHeight;
const totalPages =Math.ceil(imgHeight / PDF_HEIGHT);
const PDF =new JsPDF('p','pt','a4');
for(let i =0; i < totalPages; i++){
if(i >0) PDF.addPage();
const yPos = i * PDF_HEIGHT;
PDF.addImage( canvas,'PNG',0,-yPos, imgWidth, imgHeight );
}
PDF.save(`${title}.pdf`);
}catch(error){ console.error("导出.pdf 图片失败:", error); }
finally{ setTimeout(()=> uploadeLoading.value =false,3000); }
};
const exportPng = async (el, title)=>{
const contentHeight = el.scrollHeight;
const contentWidth = el.scrollWidth;
try{
const canvas = await html2canvas(el,{ allowTaint:true, taintTest:false, useCORS:true, logging:false, imageTimeout:0, scale:2, scrollX:0, scrollY:0, width: contentWidth, height: contentHeight,});
let url = canvas.toDataURL("image/png");
let a = document.createElement("a");
a.href = url;
a.download = title +".png";
a.click();
setTimeout(()=> URL.revokeObjectURL(url),1000);
}catch(error){ console.error("导出.png 图片失败:", error); }
finally{ setTimeout(()=> uploadeLoading.value =false,3000); }
};
const exportJpeg = async (el, title)=>{
const contentHeight = el.scrollHeight;
const contentWidth = el.scrollWidth;
try{
const canvas = await html2canvas(el,{ allowTaint:true, taintTest:false, useCORS:true, logging:false, imageTimeout:0, scale:2, scrollX:0, scrollY:0, width: contentWidth, height: contentHeight,});
let url = canvas.toDataURL("image/jpeg",0.9);
let a = document.createElement("a");
a.href = url;
a.download = title +".jpg";
a.click();
setTimeout(()=> URL.revokeObjectURL(url),1000);
}catch(error){ console.error("导出.jpg 图片失败:", error); }
finally{ setTimeout(()=> uploadeLoading.value =false,3000); }
};
const init =()=>{ resetSearch(); getTransferNodeList(); };
init();
</script>
<style scoped lang="less">
.statistics-box { box-sizing: border-box; height:calc(100vh -140px); .title { overflow: hidden; color: #192840; font-family:"PingFang SC"; font-size:20px; font-style: normal; font-weight:600; line-height:72px; height:72px; } }
.content { width:100%; &-item { position: relative; box-sizing: border-box; border:1px solid #e5e7ec; border-radius:10px; padding:20px; }}
</style>