跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScript大前端

基于 JsPDF 和 html2canvas 实现前端图表与列表数据多格式导出

介绍在前端项目中利用 JsPDF 和 html2canvas 库实现图表及列表数据的多种格式导出功能。通过结合 ECharts 图表与 Arco Design 表格,实现了将业务数据导出为 PDF、PNG、JPEG 以及 Excel 文件。方案采用纯前端生成 PDF 和图片,减少服务器压力;Excel 则通过后端返回 Blob 流处理。代码示例展示了 Vue 3 环境下的具体实现逻辑,包括动态缩放适配页面高度、Canvas 渲染转换及文件下载触发机制,适用于报表导出、文档存档等场景。

芝士奶盖发布于 2026/3/30更新于 2026/10/1595 浏览
基于 JsPDF 和 html2canvas 实现前端图表与列表数据多格式导出

背景

XNMS(增强型网络管理系统)是一款远程监控和管理常规中转台的软件。中转台是系统的核心设备,所有业务都通过其进行中转。因此,只要对中转台进行监控,就能全面掌握系统的运行状况。而中转台通常部署室外,容易受到日晒雨淋等自然条件影响,造成设备损坏。为保证通讯系统正常运行,工作人员需要对中转台进行实时监控,发现中转台的异常问题,从而采取相关措施进行补救。

通过 XNMS 软件,工作人员可实时监控常规中转台的各项参数和告警情况,对异常问题进行排查;还可以查询或统计某时间段内中转台或终端的业务,从而全面了解常规系统的运行状况。

项目采用:Arco Design + Java + MySQL + SpringBoot + Vue3 + Nginx。

JsPDF 是一个流行的、纯 JavaScript 编写的开源库,用于在浏览器中生成 PDF 文档。

核心特性

  1. 纯前端生成

    • 无需后端服务器参与
    • 完全在浏览器中运行
    • 减少服务器压力和网络传输
  2. 主要功能示例

// 创建 PDF 实例
const pdf = new jsPDF();
// 添加文本
pdf.text('Hello World!', 10, 10);
// 添加图片(支持 base64、DataURL)
pdf.addImage(imageData, 'JPEG', 15, 40, 180, 160);
// 添加页面
pdf.addPage();
// 设置字体和样式
pdf.setFont("helvetica", "bold");
pdf.setTextColor(255, 0, 0);
// 保存文件
pdf.save('document.pdf');
  1. 常见使用场景

    • 报表导出:将数据表格、图表导出为 PDF
    • 文档生成:发票、合同、证书等
    • 内容存档:保存网页内容、文章、报告
    • 打印优化:生成适合打印的格式
  2. 与其他库配合使用

    • html2canvas + jsPDF:将 HTML 转换为 PDF(本方案采用的方式)
    • AutoTable 插件:专门处理表格导出
    • 各种字体插件:支持中文字体等
  3. 优点

    • ✅ 轻量级(核心库约 100KB)
    • ✅ API 简单易用
    • ✅ 支持自定义字体
    • ✅ 丰富的插件生态系统
    • ✅ 良好的浏览器兼容性
  4. 局限性

    • ❌ 复杂布局处理有限
    • ❌ 大量内容时性能可能受影响
    • ❌ 某些 CSS 样式不支持
    • ❌ 中文字体需要额外配置

替代方案对比略。

页面展示

图表导出 PDF/JPG/PNG

图表导出 PDF

图表导出 PDF

图表导出 JPG

图表导出 JPG

图表导出 PNG

图表导出 PNG

列表数据生成 Excel/PDF/JPG/PNG

列表数据生成 Excel

列表数据生成 Excel

列表数据生成 PDF

列表数据生成 PDF

列表数据生成 JPG

列表数据生成 PNG

代码实现

列表数据生成 Excel/PDF/JPG/PNG

<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]) }}&nbsp;</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]) }}&nbsp;</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>

图表导出 PDF/JPG/PNG

<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]) }}&nbsp;</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]) }}&nbsp;</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>

目录

  1. 背景
  2. 核心特性
  3. 页面展示
  4. 图表导出 PDF/JPG/PNG
  5. 列表数据生成 Excel/PDF/JPG/PNG
  6. 代码实现
  7. 列表数据生成 Excel/PDF/JPG/PNG
  8. 图表导出 PDF/JPG/PNG

更多推荐文章

查看全部
  • Python 实现炸金花游戏的核心逻辑与代码示例
  • Ollama Windows 安装与使用指南:本地运行 Llama 等模型
  • Face Fusion 与 Stable Diffusion 集成方案及创意玩法解析
  • 基于 IP-Adapter 与 AnimateDiff 的 AI 动漫短剧工业化流水线
  • Android 免 Root 自动抢红包工具 AutoRobRedPackage 使用指南
  • Flutter 三方库 arcane_helper_utils 鸿蒙化适配指南
  • ToDesk ToClaw AI 科技新闻日报自动化实战指南
  • 部署 Qwen2.5-VL-32B 踩坑实录:多卡推理 vLLM 死锁与 llama.cpp 对比
  • 找羊开源加热台硬件与固件设计解析
  • 学术论文避坑指南:降低重复率与 AIGC 检测风险的策略
  • GitHub Copilot 模型对比与选型策略
  • 从文件到屏幕:Python/Java 字符编码、解码、文本处理底层逻辑
  • 车载AVM开发:核心算法与C++实战
  • Cursor Chat 三大模式:Agent、Ask、Plan 解析
  • Flutter eip55 库在鸿蒙系统的适配指南:以太坊地址校验
  • 2026 年主流 AI 论文写作工具盘点与使用指南
  • 基于 Python 的牙科全结构化录入与牙位可视化 AI 系统架构
  • 解决 WSL2 突然无法连接网络问题
  • Axure 制作 AI 自动对话机器人原型教程
  • 基于平台化与低代码重构制造业 EDI 系统方案

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online