VUE中通过DOM导出PDF

最终效果

在这里插入图片描述

前端导出PDF的核心在于样式的绘制上,这里其实直接使用CSS进行绘制和布局就行,只不过需要计算好每页DIV盒子的大小,防止一页放不下造成样式错乱。

项目依赖

项目是Vue3 + TS

npm i html2canvas@1.4.1
npm i jspdf@3.0.1

工具类(htmlToPdf.ts)

import {App} from 'vue';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    htmlToPdfInstall: (name: string, title: string) => Promise<void>;
    htmlToPdfPrint: (name: string, title: string) => Promise<void>;
  }
}

export default {
  // 下载
  install(app: App) {
    app.config.globalProperties.htmlToPdfInstall = async function (name: string, title: string): Promise<void> {
      const ele = document.querySelector(`#${name}`) as HTMLElement;
      if (!ele) {
        console.error(`Element with id '${name}' not found.`);
        return;
      }

      const eleW = ele.offsetWidth;
      const eleH = ele.offsetHeight;
      const eleOffsetTop = ele.offsetTop;
      const eleOffsetLeft = ele.offsetLeft;

      const canvas = document.createElement('canvas');
      let abs = 0;

      const win_in = document.documentElement.clientWidth || document.body.clientWidth;
      const win_out = window.innerWidth;

      if (win_out > win_in) {
        abs = (win_out - win_in) / 2;
      }

      canvas.width = eleW * 2;
      canvas.height = eleH * 2;

      const context = canvas.getContext('2d');
      if (!context) return;

      context.scale(2, 2);
      context.translate(-eleOffsetLeft - abs, -eleOffsetTop);

      const pdf = new jsPDF('', 'pt', 'a4');

      const childrenBox = Array.from(ele.children);
      const pdfItem = []
      for (let i = 0; i < childrenBox.length; i++) {
        pdfItem.push(...Array.from(childrenBox[i].children))
      }

      for (let i = 0; i < pdfItem.length; i++) {
        const res = await html2canvas(pdfItem[i] as HTMLElement, {
          dpi: 300,
          useCORS: true,
          scale: 4,
        });

        const pageData = res.toDataURL('image/jpeg', 1.0);
        const contentWidth = res.width;
        const contentHeight = res.height;
        const imgWidth = 555.28;
        const imgHeight = (552.28 / contentWidth) * contentHeight;

        pdf.addImage(pageData, 'JPEG', 20, 20, imgWidth, imgHeight);

        // 添加页码(居中底部)
        const pageNum = i + 1;
        const pageCount = pdfItem.length;
        pdf.setFontSize(8);
        pdf.setTextColor(96, 98, 102); // 设置字体颜色为 #606266 (RGB: 96, 98, 102)
        pdf.text(`- ${pageNum} / ${pageCount} -`, pdf.internal.pageSize.getWidth() / 2, pdf.internal.pageSize.getHeight() - 10, {
          align: 'center'
        });

        if (i < pdfItem.length - 1) {
          pdf.addPage();
        }
      }

      pdf.save(`${title}.pdf`);
    };
    app.config.globalProperties.htmlToPdfPrint = async function (name: string, title: string): Promise<void> {

      const ele = document.querySelector(`#${name}`) as HTMLElement;
      if (!ele) {
        console.error(`Element with id '${name}' not found.`);
        return;
      }

      const eleW = ele.offsetWidth;
      const eleH = ele.offsetHeight;
      const eleOffsetTop = ele.offsetTop;
      const eleOffsetLeft = ele.offsetLeft;

      const canvas = document.createElement('canvas');
      let abs = 0;

      const win_in = document.documentElement.clientWidth || document.body.clientWidth;
      const win_out = window.innerWidth;

      if (win_out > win_in) {
        abs = (win_out - win_in) / 2;
      }

      canvas.width = eleW * 2;
      canvas.height = eleH * 2;

      const context = canvas.getContext('2d');
      if (!context) return;

      context.scale(2, 2);
      context.translate(-eleOffsetLeft - abs, -eleOffsetTop);

      const pdf = new jsPDF('', 'pt', 'a4');

      const childrenBox = Array.from(ele.children);
      const pdfItem = [];
      for (let i = 0; i < childrenBox.length; i++) {
        pdfItem.push(...Array.from(childrenBox[i].children));
      }

      for (let i = 0; i < pdfItem.length; i++) {
        const res = await html2canvas(pdfItem[i] as HTMLElement, {
          dpi: 300,
          useCORS: true,
          scale: 4,
        });

        const pageData = res.toDataURL('image/jpeg', 1.0);
        const contentWidth = res.width;
        const contentHeight = res.height;
        const imgWidth = 555.28;
        const imgHeight = (552.28 / contentWidth) * contentHeight;

        pdf.addImage(pageData, 'JPEG', 20, 20, imgWidth, imgHeight);

        // 添加页码(居中底部)
        const pageNum = i + 1;
        const pageCount = pdfItem.length;
        pdf.setFontSize(8);
        pdf.setTextColor(96, 98, 102); // 设置字体颜色为 #606266 (RGB: 96, 98, 102)
        pdf.text(`- ${pageNum} / ${pageCount} -`, pdf.internal.pageSize.getWidth() / 2, pdf.internal.pageSize.getHeight() - 10, {
          align: 'center'
        });

        if (i < pdfItem.length - 1) {
          pdf.addPage();
        }
      }

      // 👉 自动打开打印预览并调用 print()
      const blob = pdf.output('blob');
      const blobUrl = URL.createObjectURL(blob);

      // 👉 创建隐藏 iframe
      const iframe = document.createElement('iframe');
      iframe.style.display = 'none';
      iframe.src = blobUrl;

      document.body.appendChild(iframe);

      iframe.onload = function () {
        setTimeout(() => {
          iframe.contentWindow?.focus();
          iframe.contentWindow?.print();
        }, 300);
      }
    }
  }
}

上面工具类中有两个方法:htmlToPdfInstall 是将html转化成PDF并在浏览器下载;htmlToPdfPrint 是将html转化成PDF然后实现打印预览,这两个方法的转化PDF都是 html2canvas 进行转化,区别是一个是浏览器下载,一个是预览打印,并且两个都添加了分页效果。

全局注册htmlToPdf工具

main.ts中注册函数

// html转PDF
import htmlToPdf from '@/utils/htmlToPdf';
const app = createApp(App)
app.use(htmlToPdf)
app.mount('#app1')

global.d.ts中添加如下代码:

import { ComponentCustomProperties } from 'vue';

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    htmlToPdfInstall: (name: string, title: string) => Promise<void>;
    htmlToPdfPrint: (name: string, title: string) => Promise<void>;
  }
}

编写页面样式

html代码

<!--  报名信息导出预览  -->
    <div v-if="isRegisterInfo" class="register_pdf_view_warp">
      <div class="register_pdf_view_title">报名信息预览</div>
      <div class="register_pdf_view_btn">
        <el-button
          type="warning"
          plain
          size="small"
          @click="printPDF"
          :loading="exportLoading"
        >
          <Icon icon="ep:view" class="mr-5px"/>
          打印预览
        </el-button>
        <el-button
          type="primary"
          plain
          size="small"
          @click="exportPDF"
          :loading="exportLoading"
        >
          <Icon icon="ep:download" class="mr-5px"/>
          下载
        </el-button>
        <Icon icon="ep:circle-close" color="#303133" @click="closeRegisterInfo" size="20" class="preview_close_icon"/>
      </div>
      <div ref="registerPdfView" class="register_pdf_view_con">
        <div id="contentToExport">
          <div v-for="(item,index) in teamAndMemberInfo" :key="index" class="register_pdf_view_item">
            <div v-for="(member, i) in item.memberTotalPages" :key="i" class="register_pdf_view_item_table">
              <div class="register_pdf_view_item_title">
                <el-row :gutter="10">
                  <el-col :span="12">
                    <span class="register_pdf_team_info_label">&nbsp;&nbsp;队:</span>
                    <span>{{item.teamName}}</span>
                  </el-col>
                </el-row>
                <el-row :gutter="10">
                  <el-col :span="12">
                    <span class="register_pdf_team_info_label">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;队:</span>
                    <span>{{item.leader}}</span>
                  </el-col>
                  <el-col :span="12">
                    <span class="register_pdf_team_info_label">联系电话:</span>
                    <span>{{item.leaderPhone}}</span>
                  </el-col>
                </el-row>
                <el-row :gutter="10">
                  <el-col :span="12">
                    <span class="register_pdf_team_info_label">&nbsp;&nbsp;练:</span>
                    <span>{{item.coach}}</span>
                  </el-col>
                  <el-col :span="12">
                    <span class="register_pdf_team_info_label">联系电话:</span>
                    <span>{{item.coachPhone}}</span>
                  </el-col>
                </el-row>
                <el-row :gutter="10">
                  <el-col :span="12">
                    <span class="register_pdf_team_info_label">助理教练:</span>
                    <span>{{item.assistantCoach}}</span>
                  </el-col>
                  <el-col :span="12">

                    <span class="register_pdf_team_info_label">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;医:</span>
                    <span>{{item.teamDoctor}}</span>
                  </el-col>
                </el-row>
              </div>
              <el-table border
                        :cell-style="{ textAlign: 'center' }"
                        :header-cell-style="{
                  textAlign: 'center',
                  background: 'var(--el-table-row-hover-bg-color)',
                  color: 'var(--el-text-color-primary)'
                }"
                        :data="item['memberList'+ i]"
                        :stripe="true"
                        :show-overflow-tooltip="true">
                <el-table-column type="index" label="序号" width="60"/>
                <el-table-column prop="role" label="身份"/>
                <el-table-column prop="name" label="姓名" width="100"/>
                <el-table-column prop="sex" label="性别" width="60"/>
                <el-table-column prop="hbw" label="身高/体重" width="120"/>
                <el-table-column prop="raceNum" label="比赛号码" width="100"/>
                <el-table-column prop="idCard" label="身份证号" width="180"/>
                <el-table-column prop="clothNum" label="服装号码" width="90"/>
              </el-table>
            </div>
            <div v-for="(photo, i) in item.photoTotalPages" :key="i" class="register_pdf_view_item_list_warp">
              <div class="register_pdf_view_item_list_title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;名:{{item.teamName}}</div>
              <div class="register_pdf_view_item_list">
                <div v-for="item1 in item['photoInfoList' + i]" :key="item1" class="register_pdf_view_item_list_item_warp">
                  <el-image class="list_item_img" fit="contain"
                            :src="item1.imagePath" :preview-src-list="[item1.imagePath]" />
                  <div class="list_item_info_warp">
                    <span class="list_item_role">{{item1.role}}:</span>
                    <span class="list_item_name">{{item1.name}}</span>
                  </div>
                  <div class="list_item_info_id_card">{{item1.idCard}}</div>
                  <div v-if="item1.raceNumber" class="list_item_info_race_number">号码:{{item1.raceNumber}}</div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

ts代码

// 报名信息预览
const closeRegisterInfo = () => {
  isRegisterInfo.value = false
}

// 预览pdf
const printPDF = () => {
  message.warning("报名表打印预览生成中,请耐心等待...")
  htmlToPdfPrint?.('contentToExport', '【'+ currentCompetition.value.matchName +'】报名信息表');
}

// 导出pdf
const exportPDF = () => {
  message.warning("报名表导出中,请耐心等待...")
  htmlToPdfInstall?.('contentToExport', '【'+ currentCompetition.value.matchName +'】报名信息表');
}

const pageSize = 24; // 每页大小
// 预览报名信息
const viewRegisterInfo = async () => {
  try {
    if (currentEvent.value.id){
      teamAndMemberInfo.value = await SmallEventRegisterTeamApi.getRegisterTeamAndMemberInfo(currentEvent.value.id);
      // 分页
      teamAndMemberInfo.value.forEach(item => {
        // 人员信息分页
        const memberTotalPages = Math.ceil(item.memberList.length / pageSize);
        item['memberTotalPages'] = memberTotalPages
        for (let i = 0; i < memberTotalPages; i++) {
          const start = i * pageSize;
          const end = start + pageSize;
          item[`memberList${i}`] = item.memberList.slice(start, end);
        }
        // 头像信息分页
        const photoTotalPages = Math.ceil(item.photoInfoList.length / pageSize);
        item['photoTotalPages'] = photoTotalPages
        for (let i = 0; i < photoTotalPages; i++) {
          const start = i * pageSize;
          const end = start + pageSize;
          item[`photoInfoList${i}`] = item.photoInfoList.slice(start, end);
        }
      })
    }else {
      message.error("请先选中项目,然后进行导出操作!!!")
    }
  } finally {
    isRegisterInfo.value = !isRegisterInfo.value
  }
}

提示:上面获取预览信息的数据来自于后端,这里进行数据处理,默认每页是24条数据,如果超出24条就直接换到下一页,这个大家可以根据自己的数据大小和多少动态的计算

CSS代码

这里其实对专业前端来说,小菜一碟。

.register_pdf_view_warp {
  position: absolute;
  top: 10px;
  left: 10px;
  width: 880px;
  padding: 15px;
  background-color: #FAFAFA;
  box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
  z-index: 999;
  height: 80vh;
}

.register_pdf_view_title {
  font-size: 18px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 15px;
}

.register_pdf_view_con {
  overflow-x: auto;
  height: 90%;
}

.register_pdf_view_item_table, .register_pdf_view_item_list_warp {
  border: 1px solid #f2f2f2;
  padding: 10px;
  margin-bottom: 10px;
  background-color: #fff;
  position: relative;
  height: 1210px;
}

.register_pdf_view_item_title {
  width: 90%;
  color: #303133;
  font-size: 16px;
  margin: 20px 0 20px;
  padding: 0 20px;
}

.register_pdf_view_item_list {
  display: grid;
  justify-content: center;
  grid-template-columns: repeat(4, 1fr);
  align-items: center;
  column-gap: 10px;
  row-gap: 10px;
}

.list_item_img {
  width: 80px;
  height: 110px;
}

.register_pdf_view_btn {
  position: absolute;
  top: 10px;
  right: 10px;
  display: flex;
  gap: 20px;
}

.register_pdf_view_item_list_item_warp {
  text-align: center;
  color: #303133;
  font-size: 14px;
}

.register_pdf_team_info_label {
  display: inline-block;
  width: 94px;
  text-align: justify;
  text-align-last: justify;
  line-height: 35px;
}

.register_pdf_view_item_list_title {
  font-size: 16px;
  margin: 20px 0;
  padding-left: 20px;
}

/* Webkit 浏览器:Chrome / Edge / Safari */
.register_pdf_view_con::-webkit-scrollbar {
  width: 8px; /* 垂直滚动条宽度 */
  height: 8px; /* 水平滚动条高度 */
}

.register_pdf_view_con::-webkit-scrollbar-thumb {
  background-color: rgba(0, 0, 0, 0.2); /* 滚动条拖动块颜色 */
  border-radius: 4px;
}

.register_pdf_view_con::-webkit-scrollbar-track {
  background-color: transparent; /* 滚动条轨道颜色 */
}

/* 鼠标悬浮时滚动条更明显 */
.register_pdf_view_con::-webkit-scrollbar-thumb:hover {
  background-color: rgba(0, 0, 0, 0.3);
}

最终页面效果如下

页面预览

在这里插入图片描述

打印预览

在这里插入图片描述

下载效果

在这里插入图片描述

有疑问的话可以留言,大家一起交流讨论,如有问题的话,可以指出,看到后修改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值