html2canvas依赖
npm install html2canvas
jspdf依赖
npm install jspdf
以导出横向,A4大小的pdf为例
规律:1. html2canvas 中,在保持jsPDF中的宽高不变的情况下,设置html2canvas中的 width 和 height 值越小,导出的pdf越显示不全(会被放大,只能看到局部),反之值越大,导出的pdf越显示完整(值也不能过大,过大在pdf中就显示的越小)。
2. jsPDF 中,在保持html2canvas中的宽高不变的情况下,pdf.addImage(pageData, ‘JPEG’, 5, yPosition, width, height) width 和 height 值越小,导出的pdf越显示完整,反之导出的pdf越显示不全。
总结:html2canvas 与 jsPDF 设置刚好相反,合理设置大小,才能使数据撑满整个pdf。
index.vue执行导出pdf页面
<a-spin :spinning="pdfConfirmLoading">
<div ref="pdfDiv">
<pdf-template :ref="'pdfTemplate' + i" v-for="i in arrNum"></pdf-template>
</div>
</a-spin>
<script>
import {printPdf} from './utils/index'
import pdfTemplate from './template/pdfTemplate'
export default {
name: 'courseTableList',
components: {
pdfTemplate
},
data() {
return {
pdfVisible: false,
disableSubmit: false,
arrNum: 1,
pdfConfirmLoading: false,
}
},
created() {
},
methods: {
//导出pdf
async handleExportPdf() {
this.handleElement()
},
handleElement() {
if(this.selectionRows.length > 0) {
this.pdfConfirmLoading = true
const groupedData = this.selectionRows.reduce((result, item) => {
// 检查是否已存在以该teacher为键的分组
if (!result[item.instructor]) {
result[item.instructor] = [];
}
// 将当前项添加到对应的分组中
result[item.instructor].push(item);
return result;
}, {});
// 转换成数组形式
const groupedArray = Object.values(groupedData);
let len = groupedArray.length
this.arrNum = len
let that = this
this.pdfVisible = true
this.$nextTick(() => {
for (let index = 0; index < groupedArray.length; index++) {
const item = groupedArray[index];
let num = index + 1
let pageFooter = num + ' of ' + len;
that.$refs['pdfTemplate' + num][0].setDataList(item, pageFooter);
}
this.pdfConfirmLoading = false
})
} else {
this.$message.warning("请勾选数据")
}
},
//确认导出pdf
async confirmExportPdf() {
this.pdfConfirmLoading = true
await printPdf(this.$refs.pdfDiv, "courseSchedule")
this.pdfConfirmLoading = false
this.pdfVisible = false
},
}
}
</script>
pdfTemplate.vue模板,根据需求自定义创建
<template>
<div class="content" ref="pdfContent" :key="JSON.stringify(datasource)">
<!-- 头部 -->
<div class="header">
<div class="header_row1">
<div class="header_row1_v">
<div class="header_row1_left">
<span>Regd. User: YiZhong College</span>
</div>
<div class="header_row1_right">
<span>Christine Xia</span>
</div>
</div>
<div class="header_row1_middle">
<span>Timetables</span>
</div>
</div>
<div class="header_row2">
<div class="header_row2_v">
<div class="header_row2_left">
<span>School: FD - YiZhong Cambridge International School</span>
</div>
<div class="header_row2_right">
<span>FDCC1</span>
</div>
</div>
<div class="header_row2_middle">
<span>FD 2022-23 Teacher's Timetables</span>
</div>
</div>
<div class="header_line"></div>
</div>
<!-- 表格 -->
<div class="table_middle">
<a-table :columns="columns" :data-source="datasource" bordered :pagination="false">
<!-- 时间段 -->
<tamplate slot="timePeriodSlot" slot-scope="text, record">
<span>{{record.period}}<br> {{text}} ~ {{record.endTime}}</span>
</tamplate>
</a-table>
</div>
<!-- 页脚 -->
<div class="footer">
<div class="footer_line"></div>
<div class="footer_end">
<div class="footer_end_datetime">{{this.currentTime}}</div>
<div class="footer_end_pages">{{this.pageNum}}</div>
</div>
</div>
</div>
</template>
<script>
let columns = [
{
title: '教师',
dataIndex: 'startTime',
scopedSlots: { customRender: 'timePeriodSlot' },
customCell: () => {
return {
style: {
'min-width': '120px',
},
};
},
},
{
title: "Monday",
children: [
{
title: '',
dataIndex: 'monday',
key: 'monday',
customCell: () => {
return {
style: {
'min-width': '180px',
},
};
},
},
],
},
{
title: "Tues",
children: [
{
title: '',
dataIndex: 'tuesday',
key: 'tuesday',
scopedSlots: { customRender: 'childrenRender' },
customCell: () => {
return {
style: {
'min-width': '180px',
},
};
},
},
],
},
{
title: "Wed",
children: [
{
title: '',
dataIndex: 'wednesday',
key: 'wednesday',
scopedSlots: { customRender: 'childrenRender' },
customCell: () => {
return {
style: {
'min-width': '180px',
},
};
},
},
],
},
{
title: "Thurs",
children: [
{
title: '',
dataIndex: 'thursday',
key: 'thursday',
scopedSlots: { customRender: 'childrenRender' },
customCell: () => {
return {
style: {
'min-width': '180px',
},
};
},
},
],
},
{
title: "Friday",
children: [
{
title: '',
dataIndex: 'friday',
key: 'friday',
scopedSlots: { customRender: 'childrenRender' },
customCell: () => {
return {
style: {
'min-width': '180px',
},
};
},
},
],
},
];
export default {
name: "pdfExport",
data() {
return {
currentTime: '',
pageNum: 'Page 1 of 1',
datasource: [],
}
},
computed: {
columns() {
return columns
}
},
created() {
this.getCurrentTime();
},
methods: {
setDataList(list, pageFooter) {
this.datasource = list
if(this.datasource.length > 0) {
this.columns[0].title = this.datasource[0].instructor
}
this.sortByStartTime()
if(pageFooter) {
this.pageNum = pageFooter
}
},
sortByStartTime() {
this.datasource.sort((a, b) => {
const timeA = new Date(`2023/01/01 ${a.startTime}`);
const timeB = new Date(`2023/01/01 ${b.startTime}`);
return timeA - timeB;
});
},
getCurrentTime() {
const date = new Date();
const month = date.getMonth() + 1;
const day = date.getDate();
const year = date.getFullYear();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
// 格式化为指定格式的字符串
const dateString = `${this.formatNumber(month)}/${this.formatNumber(day)}/${year}`;
const timeString = `${this.formatNumber(hours)}:${this.formatNumber(minutes)}:${this.formatNumber(seconds)}`;
this.currentTime = `${dateString} ${timeString}`;
},
formatNumber(number) {
return number < 10 ? `0${number}` : number;
}
}
}
</script>
<style scoped lang="less">
.content {
padding: 15px;
color: black;
}
/**头部属性样式设置*/
.header_row1 {
display: flex;
justify-content: center;
font-size: 16px;
margin-bottom: 3px;
position: relative;
}
.header_row1 .header_row1_middle {
font-weight: 700;
position: absolute;
}
.header_row2 {
display: flex;
justify-content: center;
font-size: 16px;
margin-bottom: 15px;
position: relative;
}
.header_row2 .header_row2_middle {
font-size: 22px;
font-weight: 700;
position: absolute;
}
.header_row1_left,
.header_row1_right,
.header_row2_left,
.header_row2_right {
display: inline-block;
}
.header_row1_v,
.header_row2_v {
width: 100%;
}
.header_row1_left,
.header_row2_left {
float: left;
}
.header_row1_right,
.header_row2_right {
float: right;
}
.header_line {
border: 1px solid black;
margin-bottom: 5px;
}
/**中间属性样式设置*/
/* 将表格的标题行背景设置为白色 */
.ant-table-thead {
::v-deep & > tr > th {
background: #fff;
}
}
/* 将所有边框设置为黑色 */
.table_middle {
/deep/ .ant-table {
color: black;
font-size: 16px;
}
/deep/ .ant-table-bordered .ant-table-thead > tr:first-child > th:first-child {
border-left: none;
border-top: none;
font-weight: 700;
text-align: left;
}
/deep/ .ant-table-bordered .ant-table-thead > tr:not(:last-child) > th {
border-bottom: 2px solid black;
border-top: 2px solid black;
font-weight: normal;
text-align: center;
}
/deep/ .ant-table-bordered .ant-table-thead > tr > th{
border-right: 2px solid black;
border-bottom: 2px solid black;
}
/deep/.ant-table-bordered .ant-table-tbody > tr > td {
border-right: 2px solid black;
border-bottom: 2px solid black;
}
/deep/ .ant-table-bordered .ant-table-tbody > tr > td:first-child {
border-left: 2px solid black;
}
/deep/ .ant-table-bordered.ant-table-empty .ant-table-placeholder {
border: 2px solid black;
border-top: 1px solid black;
}
}
/**页脚属性样式设置*/
.footer .footer_line {
border: 1px solid black;
}
.footer {
font-size: 16px;
}
.footer_line {
margin-top: 75px;
margin-bottom: 3px;
}
.footer_end {
display: flex;
justify-content: space-between;
}
</style>
index.js pdf导出单页方法
export const printPdf = (dom, name = '文件') => {
const printEle = dom
let width = printEle.scrollWidth;
let height = printEle.scrollHeight;
html2canvas(printEle, {
allowTaint: true, //允许跨域
useCORS: true,
width: width,
height: height,
background: '#FFFFFF', //如果指定的div没有设置背景色会默认成黑色
scale: 2 // 按比例增加分辨率
}).then(canvas => {
let contentWidth = canvas.width
let contentHeight = canvas.height
//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
let pdf = new jsPDF('l', 'pt', 'a4')
let imgWidth = pdf.internal.pageSize.getWidth()
let imgHeight = (imgWidth / contentWidth) * contentHeight
let pageData = canvas.toDataURL('image/jpeg', 1.0)
//有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
//当内容未超过pdf一页显示的范围,无需分页
pdf.addImage(pageData, 'JPEG', 5, 5, imgWidth, imgHeight)
pdf.save(`${name}.pdf`)
})
}
index.js pdf导出分页方法
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
export const printPdf = (dom, name = '文件') => {
let contents = dom.getElementsByClassName("content")
return exportToPDF(contents, name)
}
//导出pdf
async function exportToPDF(contents, name = 'exportPdf') {
// 创建一个空的横向A4大小的PDF文档对象
const pdf = new jsPDF('l', 'pt', 'a4');
let yPosition = 5; // 当前y坐标位置
for (let i = 0; i < contents.length; i++) {
const content = contents[i];
let width = content.scrollWidth;
let height = content.scrollHeight;
// 使用html2canvas将content转换为canvas
const canvas = await html2canvas(content, {
allowTaint: true, //允许跨域
useCORS: true,
width: width * 1.02,
height: height,
background: '#FFFFFF', //如果指定的div没有设置背景色会默认成黑色
scale: 2 // 按比例增加分辨率
})
// 如果是下一个content的内容,则创建新的页码
if (i > 0) {
pdf.addPage();
yPosition = 5;
}
let pageData = canvas.toDataURL('image/jpeg', 1.0)
//宽度使用pdf的宽度
let imgWidth = pdf.internal.pageSize.getWidth()
//高度根据宽度的比列计算
let imgHeight = (imgWidth / canvas.width) * canvas.height
// 将canvas添加到PDF中
pdf.addImage(pageData, 'JPEG', 5, yPosition, imgWidth, imgHeight)
yPosition += pdf.internal.pageSize.getHeight();
}
// 输出PDF文件
pdf.save(`${name}.pdf`)
return new Promise((resolve, reject) => {
resolve()
})
}