html2canvas、jspdf实现Vue2项目纯前端导出DOM为pdf
安装插件
sh
npm install --save html2canvas
npm install jspdf --savenpm install --save html2canvas
npm install jspdf --save在src/untils目录下新建htmlToPdf.js文件,以下代码实现了多种功能,包括前端导出DOM为pdf和上传到服务器函数
js
// 页面导出为pdf格式
import html2Canvas from 'html2canvas'
import jsPDF from 'jspdf'
const htmlToPdf = {
getPdf(title, url) {
html2Canvas(document.querySelector('#pdfDom'), {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4, //将分辨率提高到特定的DPI 提高四倍
scale: 4, //按比例增加分辨率
}).then((canvas) => {
var pdf = new jsPDF('p', 'mm', 'a4') //A4纸,纵向
var ctx = canvas.getContext('2d'),
// a4w = 190, a4h = 277, //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
a4w = 210,
a4h = 297, //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
imgHeight = Math.floor((a4h * canvas.width) / a4w), //按A4显示比例换算一页图像的像素高度
renderedHeight = 0
while (renderedHeight < canvas.height) {
var page = document.createElement('canvas')
page.width = canvas.width
page.height = Math.min(imgHeight, canvas.height - renderedHeight) //可能内容不足一页
//用getImageData剪裁指定区域,并画到前面创建的canvas对象中
page
.getContext('2d')
.putImageData(
ctx.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
),
0,
0
)
pdf.addImage(
page.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4w,
Math.min(a4h, (a4w * page.height) / page.width)
) //添加图像到页面,保留10mm边距
renderedHeight += imgHeight
if (renderedHeight < canvas.height) {
pdf.addPage() //如果后面还有内容,添加一个空页
}
// delete page;
}
//保存文件
pdf.save(title + '.pdf')
})
},
// 返回 PDF 文件对象,可用于上传
async getPdfFile(title = '文件') {
const canvas = await html2Canvas(document.querySelector('#pdfDom'), {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4,
scale: 4,
})
const pdf = createPdfFromCanvas(canvas)
const blob = pdf.output('blob') // 返回Blob对象
const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
return file
},
}
// 用来输出文件上传到服务器
function createPdfFromCanvas(canvas) {
const pdf = new jsPDF('p', 'mm', 'a4')
const ctx = canvas.getContext('2d')
const a4w = 190,
a4h = 277
const imgHeight = Math.floor((a4h * canvas.width) / a4w)
let renderedHeight = 0
while (renderedHeight < canvas.height) {
const page = document.createElement('canvas')
page.width = canvas.width
page.height = Math.min(imgHeight, canvas.height - renderedHeight)
page
.getContext('2d')
.putImageData(
ctx.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
),
0,
0
)
pdf.addImage(
page.toDataURL('image/jpeg', 1.0),
'JPEG',
10,
10,
a4w,
Math.min(a4h, (a4w * page.height) / page.width)
)
renderedHeight += imgHeight
if (renderedHeight < canvas.height) {
pdf.addPage()
}
}
return pdf
}
export default htmlToPdf// 页面导出为pdf格式
import html2Canvas from 'html2canvas'
import jsPDF from 'jspdf'
const htmlToPdf = {
getPdf(title, url) {
html2Canvas(document.querySelector('#pdfDom'), {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4, //将分辨率提高到特定的DPI 提高四倍
scale: 4, //按比例增加分辨率
}).then((canvas) => {
var pdf = new jsPDF('p', 'mm', 'a4') //A4纸,纵向
var ctx = canvas.getContext('2d'),
// a4w = 190, a4h = 277, //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
a4w = 210,
a4h = 297, //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
imgHeight = Math.floor((a4h * canvas.width) / a4w), //按A4显示比例换算一页图像的像素高度
renderedHeight = 0
while (renderedHeight < canvas.height) {
var page = document.createElement('canvas')
page.width = canvas.width
page.height = Math.min(imgHeight, canvas.height - renderedHeight) //可能内容不足一页
//用getImageData剪裁指定区域,并画到前面创建的canvas对象中
page
.getContext('2d')
.putImageData(
ctx.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
),
0,
0
)
pdf.addImage(
page.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4w,
Math.min(a4h, (a4w * page.height) / page.width)
) //添加图像到页面,保留10mm边距
renderedHeight += imgHeight
if (renderedHeight < canvas.height) {
pdf.addPage() //如果后面还有内容,添加一个空页
}
// delete page;
}
//保存文件
pdf.save(title + '.pdf')
})
},
// 返回 PDF 文件对象,可用于上传
async getPdfFile(title = '文件') {
const canvas = await html2Canvas(document.querySelector('#pdfDom'), {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4,
scale: 4,
})
const pdf = createPdfFromCanvas(canvas)
const blob = pdf.output('blob') // 返回Blob对象
const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
return file
},
}
// 用来输出文件上传到服务器
function createPdfFromCanvas(canvas) {
const pdf = new jsPDF('p', 'mm', 'a4')
const ctx = canvas.getContext('2d')
const a4w = 190,
a4h = 277
const imgHeight = Math.floor((a4h * canvas.width) / a4w)
let renderedHeight = 0
while (renderedHeight < canvas.height) {
const page = document.createElement('canvas')
page.width = canvas.width
page.height = Math.min(imgHeight, canvas.height - renderedHeight)
page
.getContext('2d')
.putImageData(
ctx.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
),
0,
0
)
pdf.addImage(
page.toDataURL('image/jpeg', 1.0),
'JPEG',
10,
10,
a4w,
Math.min(a4h, (a4w * page.height) / page.width)
)
renderedHeight += imgHeight
if (renderedHeight < canvas.height) {
pdf.addPage()
}
}
return pdf
}
export default htmlToPdf页面代码
html
<template>
<div style="width: 100%">
<div class="preview-content-operateBtn" style="padding: 8px 16px">
<!-- <el-button @click="uploadToService()" type="primary">上传至服务器</el-button>
<el-input style="width: 200px; margin: 0 8px" v-model="sceneId"></el-input>
<el-button @click="downLoad_api()" type="primary">下载</el-button> -->
<!-- <el-button class="previewBtn" type="warning" @click="onClickDownLoad">DOM在前端直接转转换为PDF并下载</el-button> -->
</div>
<div id="pdfDom1">
<pdf_page01 class="pdf-page"></pdf_page01>
<pdf_page02 class="pdf-page"></pdf_page02>
<pdf_page03 class="pdf-page"></pdf_page03>
<pdf_page04 class="pdf-page"></pdf_page04>
<pdf_page05 class="pdf-page"></pdf_page05>
<pdf_page06 class="pdf-page"></pdf_page06>
<pdf_page07 class="pdf-page"></pdf_page07>
</div>
<div id="pdfDom2">
<pdf_page08 class="pdf-page"></pdf_page08>
<pdf_page09 class="pdf-page"></pdf_page09>
<pdf_page10 class="pdf-page"></pdf_page10>
<pdf_page11 class="pdf-page"></pdf_page11>
<pdf_page12 class="pdf-page"></pdf_page12>
<pdf_page13 class="pdf-page"></pdf_page13>
<pdf_page14 class="pdf-page"></pdf_page14>
</div>
<el-button class="back-to-top" type="primary" style="position: fixed; bottom: 60px; right: 10px; z-index: 9999"
@click="onClickDownLoad()">导出PDF</el-button>
<el-button class="back-to-top" style="position: fixed; bottom: 10px; right: 10px; z-index: 9999"
@click="backToTop()">返回顶部</el-button>
</div>
</template>
<script>
// import {
// upload_file2, // 上传
// } from "@/api/system/SystemManagementApi";
// import {
// download_file, // 下载
// } from "@/api/system/SystemManagementApi";
import pdf_page01 from '../components/pdf_page01.vue'
import pdf_page02 from '../components/pdf_page02.vue'
import pdf_page03 from '../components/pdf_page03.vue'
import pdf_page04 from '../components/pdf_page04.vue'
import pdf_page05 from '../components/pdf_page05.vue'
import pdf_page06 from '../components/pdf_page06.vue'
import pdf_page07 from '../components/pdf_page07.vue'
import pdf_page08 from '../components/pdf_page08.vue'
import pdf_page09 from '../components/pdf_page09.vue'
import pdf_page10 from '../components/pdf_page10.vue'
import pdf_page11 from '../components/pdf_page11.vue'
import pdf_page12 from '../components/pdf_page12.vue'
import pdf_page13 from '../components/pdf_page13.vue'
import pdf_page14 from '../components/pdf_page14.vue'
import htmlToPdf from "../untils/htmlToPdf.js";
export default {
components: {
pdf_page01,
pdf_page02,
pdf_page03,
pdf_page04,
pdf_page05,
pdf_page06,
pdf_page07,
pdf_page08,
pdf_page09,
pdf_page10,
pdf_page11,
pdf_page12,
pdf_page13,
pdf_page14,
},
props: {},
created () { },
mounted () { },
data () {
return {
sceneCode: "institutionLaw",
sceneId: "",
uploadFileUrl:
process.env.VUE_APP_BASE_API +
"/admin-api/infra/file/upload?sceneCode=", // 请求地址
};
},
methods: {
backToTop () { // 滚动回到页面顶部
window.scrollTo(0, 0);
},
downLoad_api () {
download_file({
sceneId: this.sceneId,
sceneCode: "initVisit",
})
.then((res) => {
this.$download.excel(res, "test.pdf");
})
.finally(() => { });
},
async uploadToService () {
const file = await htmlToPdf.getPdfFile("测试文件名");
const formData = new FormData();
formData.append("file", file);
const url = this.uploadFileUrl + this.sceneCode; // 拼接 sceneCode 参数
upload_file2(formData, url).then((res) => {
this.$message.success("上传成功");
this.sceneId = res.data.fileId; // 上传成功后返回 sceneId
});
},
onClickDownLoad () {
htmlToPdf.getPdf("智家国内二季度税务风险报告");
},
},
};
</script>
<style scoped>
.pdf-page {
width: 840px;
height: 1188px;
border: 1px solid #cccccc;
box-sizing: border-box;
}
</style><template>
<div style="width: 100%">
<div class="preview-content-operateBtn" style="padding: 8px 16px">
<!-- <el-button @click="uploadToService()" type="primary">上传至服务器</el-button>
<el-input style="width: 200px; margin: 0 8px" v-model="sceneId"></el-input>
<el-button @click="downLoad_api()" type="primary">下载</el-button> -->
<!-- <el-button class="previewBtn" type="warning" @click="onClickDownLoad">DOM在前端直接转转换为PDF并下载</el-button> -->
</div>
<div id="pdfDom1">
<pdf_page01 class="pdf-page"></pdf_page01>
<pdf_page02 class="pdf-page"></pdf_page02>
<pdf_page03 class="pdf-page"></pdf_page03>
<pdf_page04 class="pdf-page"></pdf_page04>
<pdf_page05 class="pdf-page"></pdf_page05>
<pdf_page06 class="pdf-page"></pdf_page06>
<pdf_page07 class="pdf-page"></pdf_page07>
</div>
<div id="pdfDom2">
<pdf_page08 class="pdf-page"></pdf_page08>
<pdf_page09 class="pdf-page"></pdf_page09>
<pdf_page10 class="pdf-page"></pdf_page10>
<pdf_page11 class="pdf-page"></pdf_page11>
<pdf_page12 class="pdf-page"></pdf_page12>
<pdf_page13 class="pdf-page"></pdf_page13>
<pdf_page14 class="pdf-page"></pdf_page14>
</div>
<el-button class="back-to-top" type="primary" style="position: fixed; bottom: 60px; right: 10px; z-index: 9999"
@click="onClickDownLoad()">导出PDF</el-button>
<el-button class="back-to-top" style="position: fixed; bottom: 10px; right: 10px; z-index: 9999"
@click="backToTop()">返回顶部</el-button>
</div>
</template>
<script>
// import {
// upload_file2, // 上传
// } from "@/api/system/SystemManagementApi";
// import {
// download_file, // 下载
// } from "@/api/system/SystemManagementApi";
import pdf_page01 from '../components/pdf_page01.vue'
import pdf_page02 from '../components/pdf_page02.vue'
import pdf_page03 from '../components/pdf_page03.vue'
import pdf_page04 from '../components/pdf_page04.vue'
import pdf_page05 from '../components/pdf_page05.vue'
import pdf_page06 from '../components/pdf_page06.vue'
import pdf_page07 from '../components/pdf_page07.vue'
import pdf_page08 from '../components/pdf_page08.vue'
import pdf_page09 from '../components/pdf_page09.vue'
import pdf_page10 from '../components/pdf_page10.vue'
import pdf_page11 from '../components/pdf_page11.vue'
import pdf_page12 from '../components/pdf_page12.vue'
import pdf_page13 from '../components/pdf_page13.vue'
import pdf_page14 from '../components/pdf_page14.vue'
import htmlToPdf from "../untils/htmlToPdf.js";
export default {
components: {
pdf_page01,
pdf_page02,
pdf_page03,
pdf_page04,
pdf_page05,
pdf_page06,
pdf_page07,
pdf_page08,
pdf_page09,
pdf_page10,
pdf_page11,
pdf_page12,
pdf_page13,
pdf_page14,
},
props: {},
created () { },
mounted () { },
data () {
return {
sceneCode: "institutionLaw",
sceneId: "",
uploadFileUrl:
process.env.VUE_APP_BASE_API +
"/admin-api/infra/file/upload?sceneCode=", // 请求地址
};
},
methods: {
backToTop () { // 滚动回到页面顶部
window.scrollTo(0, 0);
},
downLoad_api () {
download_file({
sceneId: this.sceneId,
sceneCode: "initVisit",
})
.then((res) => {
this.$download.excel(res, "test.pdf");
})
.finally(() => { });
},
async uploadToService () {
const file = await htmlToPdf.getPdfFile("测试文件名");
const formData = new FormData();
formData.append("file", file);
const url = this.uploadFileUrl + this.sceneCode; // 拼接 sceneCode 参数
upload_file2(formData, url).then((res) => {
this.$message.success("上传成功");
this.sceneId = res.data.fileId; // 上传成功后返回 sceneId
});
},
onClickDownLoad () {
htmlToPdf.getPdf("智家国内二季度税务风险报告");
},
},
};
</script>
<style scoped>
.pdf-page {
width: 840px;
height: 1188px;
border: 1px solid #cccccc;
box-sizing: border-box;
}
</style>导出超出一定页面导致pdf黑屏,拆分为多个canvas再导出
调整htmlToPdf.js文件
js
import html2Canvas from 'html2canvas'
import jsPDF from 'jspdf'
const htmlToPdf = {
// 下载 PDF 文件
async getPdf(title = '文件') {
const pdf = new jsPDF('p', 'mm', 'a4')
const selectors = ['#pdfDom1', '#pdfDom2']
for (let i = 0; i < selectors.length; i++) {
const canvas = await html2Canvas(document.querySelector(selectors[i]), {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4,
scale: 4,
})
await appendCanvasToPdf(canvas, pdf, i > 0) // 第一个不要 addPage,后面都要
}
pdf.save(`${title}.pdf`)
},
// 返回 PDF 文件对象
async getPdfFile(title = '文件') {
const pdf = new jsPDF('p', 'mm', 'a4')
const selectors = ['#pdfDom1', '#pdfDom2']
for (let i = 0; i < selectors.length; i++) {
const canvas = await html2Canvas(document.querySelector(selectors[i]), {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4,
scale: 4,
})
await appendCanvasToPdf(canvas, pdf, i > 0)
}
const blob = pdf.output('blob')
const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
return file
},
}
// 通用函数:将 canvas 分页加入 pdf
async function appendCanvasToPdf(canvas, pdf, addPageFirst = false) {
const ctx = canvas.getContext('2d')
const a4w = 210,
a4h = 297
const imgHeight = Math.floor((a4h * canvas.width) / a4w)
let renderedHeight = 0
while (renderedHeight < canvas.height) {
const pageCanvas = document.createElement('canvas')
pageCanvas.width = canvas.width
pageCanvas.height = Math.min(imgHeight, canvas.height - renderedHeight)
pageCanvas
.getContext('2d')
.putImageData(
ctx.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
),
0,
0
)
if (addPageFirst || renderedHeight > 0) {
pdf.addPage()
}
pdf.addImage(
pageCanvas.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4w,
Math.min(a4h, (a4w * pageCanvas.height) / pageCanvas.width)
)
renderedHeight += imgHeight
}
}
export default htmlToPdfimport html2Canvas from 'html2canvas'
import jsPDF from 'jspdf'
const htmlToPdf = {
// 下载 PDF 文件
async getPdf(title = '文件') {
const pdf = new jsPDF('p', 'mm', 'a4')
const selectors = ['#pdfDom1', '#pdfDom2']
for (let i = 0; i < selectors.length; i++) {
const canvas = await html2Canvas(document.querySelector(selectors[i]), {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4,
scale: 4,
})
await appendCanvasToPdf(canvas, pdf, i > 0) // 第一个不要 addPage,后面都要
}
pdf.save(`${title}.pdf`)
},
// 返回 PDF 文件对象
async getPdfFile(title = '文件') {
const pdf = new jsPDF('p', 'mm', 'a4')
const selectors = ['#pdfDom1', '#pdfDom2']
for (let i = 0; i < selectors.length; i++) {
const canvas = await html2Canvas(document.querySelector(selectors[i]), {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4,
scale: 4,
})
await appendCanvasToPdf(canvas, pdf, i > 0)
}
const blob = pdf.output('blob')
const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
return file
},
}
// 通用函数:将 canvas 分页加入 pdf
async function appendCanvasToPdf(canvas, pdf, addPageFirst = false) {
const ctx = canvas.getContext('2d')
const a4w = 210,
a4h = 297
const imgHeight = Math.floor((a4h * canvas.width) / a4w)
let renderedHeight = 0
while (renderedHeight < canvas.height) {
const pageCanvas = document.createElement('canvas')
pageCanvas.width = canvas.width
pageCanvas.height = Math.min(imgHeight, canvas.height - renderedHeight)
pageCanvas
.getContext('2d')
.putImageData(
ctx.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
),
0,
0
)
if (addPageFirst || renderedHeight > 0) {
pdf.addPage()
}
pdf.addImage(
pageCanvas.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4w,
Math.min(a4h, (a4w * pageCanvas.height) / pageCanvas.width)
)
renderedHeight += imgHeight
}
}
export default htmlToPdf解决pdf分页导致截断页面连续性问题
- 使用autoTable
- 不分页导出一整个长pdf
- 扫描空白自动算法
- 自动计算元素高度堆叠marginTop算法
开发过程中常见bug及其解决方案
- 部分文字多次重叠问题
- 浏览器缩放导致文字左右重叠问题
- 导出文件体积过大,缩小ppi
- 单元格合并导致内容丢失问题
- 监听页面缩放百分比,解决导出pdf页面大小不一致问题
- import更换为cdn引入方式获取依赖
解决方案及源代码
html
<template>
<div>
<!-- <el-button :loading="loading_index" type="primary" @click="test">测试</el-button> -->
<div style="width: 100%; display: flex; justify-content: center">
<div style="" class="pdf-card">
当前缩放倍率:{{ Number(devicePixelRatio).toFixed(1) }}
</div>
<!-- <el-loading :fullscreen="true" lock text="加载中..." v-if="loading_index"></el-loading> -->
<div ref="pdfPrintIndex" id="pdfPrintIndex">
<pdf_page01 @getTitle="getTitle" id="pdfDom0" :props_obj="props_obj" class="pdf-print-pdf-page">
</pdf_page01>
<pdf_page02 id="pdfDom1" :props_obj="props_obj" class="pdf-print-pdf-page"></pdf_page02>
<pdfPageCom01 class="pdf-print-pdf-page-com" id="pdfDom2" :props_obj="props_obj"
@childReady="onChildReady"></pdfPageCom01>
<pdfPageCom02 class="pdf-print-pdf-page-com" id="pdfDom3" :props_obj="props_obj"
@childReady="onChildReady"></pdfPageCom02>
<pdfPageCom03 class="pdf-print-pdf-page-com" style="margin-bottom: 20px;" id="pdfDom4"
:props_obj="props_obj" @childReady="onChildReady"></pdfPageCom03>
</div>
<el-button icon="el-icon-position" :loading="loading_btn" class="back-to-top" plain type="primary"
style="position: fixed; bottom: 100px; right: 10px" @click="sendEmail()">发送邮件</el-button>
<el-button icon="el-icon-upload2" :loading="loading_btn" class="back-to-top" plain type="primary"
style="position: fixed; bottom: 60px; right: 10px" @click="uploadToService()">附件上传</el-button>
<el-button icon="el-icon-document" :loading="loading_btn" class="back-to-top" type="primary"
style="position: fixed; bottom: 20px; right: 10px" @click="onClickDownLoad()">导出PDF</el-button>
</div>
</div>
</template>
<script>
// import {
// download_file, // 下载
// } from "@/api/system/SystemManagementApi";
import pdf_page01 from './componentsPdf/pdf_page01.vue'
import pdf_page02 from './componentsPdf/pdf_page02.vue'
import pdfPageCom01 from './componentsPdf/pdfPageCom01.vue'
import pdfPageCom02 from './componentsPdf/pdfPageCom02.vue'
import pdfPageCom03 from './componentsPdf/pdfPageCom03.vue'
// import { PdfLoader } from "../../utils/outputPDF";
import htmlToPdf2 from '../../utils/htmlToPdf2.js'
import {
uploadFxbgToFtp, // 上传
sendFxbgFromFtp
} from '@/api/fxgli/pdfPrint'
export default {
components: {
pdf_page01,
pdf_page02,
pdfPageCom01,
pdfPageCom02,
pdfPageCom03
},
props: {},
created () {
this.loading_index = this.$loading({
lock: true,
text: '数据加载中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.9)'
})
this.props_obj = JSON.parse(this.$route.query.test || '{}')
console.warn(this.props_obj)
},
mounted () {
// 实时监听 devicePixelRatio
this.updatePixelRatio = () => {
this.devicePixelRatio = window.devicePixelRatio
}
window.addEventListener('resize', this.updatePixelRatio)
window.addEventListener('zoom', this.updatePixelRatio)
// 兼容部分浏览器缩放
setInterval(this.updatePixelRatio, 500)
},
beforeDestroy () {
window.removeEventListener('resize', this.updatePixelRatio)
window.removeEventListener('zoom', this.updatePixelRatio)
},
data () {
return {
devicePixelRatio: window.devicePixelRatio,
loading_index: true,
loading_btn: false,
props_obj: {},
totalChildren: 3,
finishedChildren: 0,
pageTitle: ''
}
},
methods: {
getTitle (val) {
this.pageTitle = val
console.warn('标题:', this.pageTitle)
},
onChildReady () {
this.finishedChildren++
console.warn(
'%cfinishedChildren➜:',
'background:green;color:#fff;padding:4px;',
this.finishedChildren
)
if (this.finishedChildren >= this.totalChildren) {
this.loading_index.close()
console.log('%c所有子组件加载完成,loading 关闭', 'color: green')
}
},
async sendEmail () {
this.loading_btn = true
sendFxbgFromFtp({
ssny: this.props_obj.ny,
ssly: this.props_obj.lycy
}).then((res) => {
if (res.code == 1) {
this.$message.error(res.code.message)
} else {
this.$message.success('邮件发送成功')
}
}).catch((error) => {
}).finally(() => {
this.loading_btn = false
})
},
async uploadToService () {
let loading = null
loading = this.$loading({
lock: true,
text: '上传中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.9)'
})
const file = await htmlToPdf2.getPdfFile(this.pageTitle)
const formData = new FormData()
formData.append('file', file)
formData.append('ssny', this.props_obj.ny)
formData.append('ssly', this.props_obj.lycy)
uploadFxbgToFtp(formData).then((res) => {
loading.close()
this.$message.success(this.pageTitle, '上传至服务器成功')
})
},
async onClickDownLoad () {
console.log('缩放倍率1', window.devicePixelRatio)
console.log('缩放倍率2', window.innerWidth)
console.log('缩放倍率3', window.outerWidth)
// 如果window.devicePixelRatio约等于1,继续,否则return
function isApproximatelyEqual (a, b, tolerance = 0.1) {
return Math.abs(a - b) > tolerance;
}
if (isApproximatelyEqual(window.devicePixelRatio, 1)) {
this.$message.warning('请使用CTRL+滚轮缩放页面至100%后再导出PDF')
return
}
let loading = null
loading = this.$loading({
lock: true,
text: '导出中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.9)'
})
await this.$nextTick()
setTimeout(() => {
htmlToPdf2
.getPdf('税务风险报告')
.then(async (res) => {
this.$message.success('导出成功!')
loading.close()
})
.catch((error) => {
this.$message.warning('导出失败,请重试!')
})
}, 1000)
}
// test () {
// // 获得当前组件的DOM元素
// const element = document.getElementById('pdfPrintIndex');
// const pdfLoader = new PdfLoader(element, {
// contentWidth: 550,
// fileName: '技术报告.pdf',
// baseY: 15,
// isPageMessage: true,
// direction: 'p', // 竖向
// scale: window.devicePixelRatio * 2
// });
// // 生成PDF
// pdfLoader.getPdf().then(result => {
// console.log('PDF生成成功', result);
// }).catch(error => {
// console.error('PDF生成失败', error);
// });
// },
}
}
</script>
<style>
.pdf-print-pdf-page {
width: 840px;
height: 1188px;
background-color: white;
}
.pdf-print-pdf-page-com {
width: 840px;
background-color: white;
padding: 20px 0;
/* margin-top: 20px; */
}
.pdf-print-title-text {
color: white;
font-size: 15px;
font-weight: bold;
position: absolute;
}
.pdf-print-gray-box {
background-color: #eef0f7;
margin: 5px 20px;
display: flex;
}
.pdf-print-gray-box>div:first-child {
border-left: 6px solid #e76869;
padding: 8px 16px;
color: black;
font-weight: 600;
width: 130px;
height: 40px;
line-height: 22px;
}
.pdf-print-gray-box>div:nth-child(2) {
font-size: 13px;
padding-top: 10px;
margin-right: 25px;
width: 650px;
line-height: 22px;
}
.pdf-print-gray-box-2 {
background-color: #eef0f7;
margin: 5px 20px;
display: flex;
}
.pdf-print-gray-box-2>div:first-child {
border-left: 6px solid #66bd99;
padding: 8px 16px;
color: black;
font-weight: 600;
width: 130px;
height: 40px;
line-height: 22px;
}
.pdf-print-gray-box-2>div:nth-child(2) {
font-size: 13px;
padding-bottom: 10px;
padding-top: 10px;
margin-right: 25px;
width: 650px;
line-height: 22px;
}
.pdf-print-gradient-box {
height: 35px;
background: linear-gradient(to right, #3f8d69 0%, #84c9ac 50%, #3f8d69 100%);
margin: 20px;
margin-bottom: 5px;
/* 居中对齐 */
display: flex;
align-items: center;
justify-content: center;
/* 文字样式 */
color: black;
font-weight: bold;
font-size: 17px;
}
.pdf-print-gradient-box-origin {
height: 35px;
background: linear-gradient(to right, #f97319 0%, #ffba8b 50%, #f97319 100%);
margin: 20px;
margin-bottom: 5px;
display: flex;
align-items: center;
justify-content: center;
color: black;
font-weight: bold;
font-size: 17px;
}
.pdf-print-custom-legend {
display: flex;
justify-content: center;
margin-top: 10px;
font-size: 12px;
}
.pdf-print-legend-item {
display: flex;
align-items: center;
margin: 0 12px;
}
.pdf-print-legend-color {
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 2px;
}
.pdf-print-legend-green {
background-color: #86caad;
/* 已关差:绿色 */
}
.pdf-print-legend-orange {
background-color: #feb77c;
/* 未关差:橙色 */
}
.pdf-card {
height: 100px;
width: 200px;
position: fixed;
top: 120px;
right: 15px;
z-index: 50;
background: #fff;
border: 1px solid #cacaca;
padding: 6px 12px;
border-radius: 7px;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
font-weight: 600;
}
</style><template>
<div>
<!-- <el-button :loading="loading_index" type="primary" @click="test">测试</el-button> -->
<div style="width: 100%; display: flex; justify-content: center">
<div style="" class="pdf-card">
当前缩放倍率:{{ Number(devicePixelRatio).toFixed(1) }}
</div>
<!-- <el-loading :fullscreen="true" lock text="加载中..." v-if="loading_index"></el-loading> -->
<div ref="pdfPrintIndex" id="pdfPrintIndex">
<pdf_page01 @getTitle="getTitle" id="pdfDom0" :props_obj="props_obj" class="pdf-print-pdf-page">
</pdf_page01>
<pdf_page02 id="pdfDom1" :props_obj="props_obj" class="pdf-print-pdf-page"></pdf_page02>
<pdfPageCom01 class="pdf-print-pdf-page-com" id="pdfDom2" :props_obj="props_obj"
@childReady="onChildReady"></pdfPageCom01>
<pdfPageCom02 class="pdf-print-pdf-page-com" id="pdfDom3" :props_obj="props_obj"
@childReady="onChildReady"></pdfPageCom02>
<pdfPageCom03 class="pdf-print-pdf-page-com" style="margin-bottom: 20px;" id="pdfDom4"
:props_obj="props_obj" @childReady="onChildReady"></pdfPageCom03>
</div>
<el-button icon="el-icon-position" :loading="loading_btn" class="back-to-top" plain type="primary"
style="position: fixed; bottom: 100px; right: 10px" @click="sendEmail()">发送邮件</el-button>
<el-button icon="el-icon-upload2" :loading="loading_btn" class="back-to-top" plain type="primary"
style="position: fixed; bottom: 60px; right: 10px" @click="uploadToService()">附件上传</el-button>
<el-button icon="el-icon-document" :loading="loading_btn" class="back-to-top" type="primary"
style="position: fixed; bottom: 20px; right: 10px" @click="onClickDownLoad()">导出PDF</el-button>
</div>
</div>
</template>
<script>
// import {
// download_file, // 下载
// } from "@/api/system/SystemManagementApi";
import pdf_page01 from './componentsPdf/pdf_page01.vue'
import pdf_page02 from './componentsPdf/pdf_page02.vue'
import pdfPageCom01 from './componentsPdf/pdfPageCom01.vue'
import pdfPageCom02 from './componentsPdf/pdfPageCom02.vue'
import pdfPageCom03 from './componentsPdf/pdfPageCom03.vue'
// import { PdfLoader } from "../../utils/outputPDF";
import htmlToPdf2 from '../../utils/htmlToPdf2.js'
import {
uploadFxbgToFtp, // 上传
sendFxbgFromFtp
} from '@/api/fxgli/pdfPrint'
export default {
components: {
pdf_page01,
pdf_page02,
pdfPageCom01,
pdfPageCom02,
pdfPageCom03
},
props: {},
created () {
this.loading_index = this.$loading({
lock: true,
text: '数据加载中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.9)'
})
this.props_obj = JSON.parse(this.$route.query.test || '{}')
console.warn(this.props_obj)
},
mounted () {
// 实时监听 devicePixelRatio
this.updatePixelRatio = () => {
this.devicePixelRatio = window.devicePixelRatio
}
window.addEventListener('resize', this.updatePixelRatio)
window.addEventListener('zoom', this.updatePixelRatio)
// 兼容部分浏览器缩放
setInterval(this.updatePixelRatio, 500)
},
beforeDestroy () {
window.removeEventListener('resize', this.updatePixelRatio)
window.removeEventListener('zoom', this.updatePixelRatio)
},
data () {
return {
devicePixelRatio: window.devicePixelRatio,
loading_index: true,
loading_btn: false,
props_obj: {},
totalChildren: 3,
finishedChildren: 0,
pageTitle: ''
}
},
methods: {
getTitle (val) {
this.pageTitle = val
console.warn('标题:', this.pageTitle)
},
onChildReady () {
this.finishedChildren++
console.warn(
'%cfinishedChildren➜:',
'background:green;color:#fff;padding:4px;',
this.finishedChildren
)
if (this.finishedChildren >= this.totalChildren) {
this.loading_index.close()
console.log('%c所有子组件加载完成,loading 关闭', 'color: green')
}
},
async sendEmail () {
this.loading_btn = true
sendFxbgFromFtp({
ssny: this.props_obj.ny,
ssly: this.props_obj.lycy
}).then((res) => {
if (res.code == 1) {
this.$message.error(res.code.message)
} else {
this.$message.success('邮件发送成功')
}
}).catch((error) => {
}).finally(() => {
this.loading_btn = false
})
},
async uploadToService () {
let loading = null
loading = this.$loading({
lock: true,
text: '上传中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.9)'
})
const file = await htmlToPdf2.getPdfFile(this.pageTitle)
const formData = new FormData()
formData.append('file', file)
formData.append('ssny', this.props_obj.ny)
formData.append('ssly', this.props_obj.lycy)
uploadFxbgToFtp(formData).then((res) => {
loading.close()
this.$message.success(this.pageTitle, '上传至服务器成功')
})
},
async onClickDownLoad () {
console.log('缩放倍率1', window.devicePixelRatio)
console.log('缩放倍率2', window.innerWidth)
console.log('缩放倍率3', window.outerWidth)
// 如果window.devicePixelRatio约等于1,继续,否则return
function isApproximatelyEqual (a, b, tolerance = 0.1) {
return Math.abs(a - b) > tolerance;
}
if (isApproximatelyEqual(window.devicePixelRatio, 1)) {
this.$message.warning('请使用CTRL+滚轮缩放页面至100%后再导出PDF')
return
}
let loading = null
loading = this.$loading({
lock: true,
text: '导出中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.9)'
})
await this.$nextTick()
setTimeout(() => {
htmlToPdf2
.getPdf('税务风险报告')
.then(async (res) => {
this.$message.success('导出成功!')
loading.close()
})
.catch((error) => {
this.$message.warning('导出失败,请重试!')
})
}, 1000)
}
// test () {
// // 获得当前组件的DOM元素
// const element = document.getElementById('pdfPrintIndex');
// const pdfLoader = new PdfLoader(element, {
// contentWidth: 550,
// fileName: '技术报告.pdf',
// baseY: 15,
// isPageMessage: true,
// direction: 'p', // 竖向
// scale: window.devicePixelRatio * 2
// });
// // 生成PDF
// pdfLoader.getPdf().then(result => {
// console.log('PDF生成成功', result);
// }).catch(error => {
// console.error('PDF生成失败', error);
// });
// },
}
}
</script>
<style>
.pdf-print-pdf-page {
width: 840px;
height: 1188px;
background-color: white;
}
.pdf-print-pdf-page-com {
width: 840px;
background-color: white;
padding: 20px 0;
/* margin-top: 20px; */
}
.pdf-print-title-text {
color: white;
font-size: 15px;
font-weight: bold;
position: absolute;
}
.pdf-print-gray-box {
background-color: #eef0f7;
margin: 5px 20px;
display: flex;
}
.pdf-print-gray-box>div:first-child {
border-left: 6px solid #e76869;
padding: 8px 16px;
color: black;
font-weight: 600;
width: 130px;
height: 40px;
line-height: 22px;
}
.pdf-print-gray-box>div:nth-child(2) {
font-size: 13px;
padding-top: 10px;
margin-right: 25px;
width: 650px;
line-height: 22px;
}
.pdf-print-gray-box-2 {
background-color: #eef0f7;
margin: 5px 20px;
display: flex;
}
.pdf-print-gray-box-2>div:first-child {
border-left: 6px solid #66bd99;
padding: 8px 16px;
color: black;
font-weight: 600;
width: 130px;
height: 40px;
line-height: 22px;
}
.pdf-print-gray-box-2>div:nth-child(2) {
font-size: 13px;
padding-bottom: 10px;
padding-top: 10px;
margin-right: 25px;
width: 650px;
line-height: 22px;
}
.pdf-print-gradient-box {
height: 35px;
background: linear-gradient(to right, #3f8d69 0%, #84c9ac 50%, #3f8d69 100%);
margin: 20px;
margin-bottom: 5px;
/* 居中对齐 */
display: flex;
align-items: center;
justify-content: center;
/* 文字样式 */
color: black;
font-weight: bold;
font-size: 17px;
}
.pdf-print-gradient-box-origin {
height: 35px;
background: linear-gradient(to right, #f97319 0%, #ffba8b 50%, #f97319 100%);
margin: 20px;
margin-bottom: 5px;
display: flex;
align-items: center;
justify-content: center;
color: black;
font-weight: bold;
font-size: 17px;
}
.pdf-print-custom-legend {
display: flex;
justify-content: center;
margin-top: 10px;
font-size: 12px;
}
.pdf-print-legend-item {
display: flex;
align-items: center;
margin: 0 12px;
}
.pdf-print-legend-color {
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 2px;
}
.pdf-print-legend-green {
background-color: #86caad;
/* 已关差:绿色 */
}
.pdf-print-legend-orange {
background-color: #feb77c;
/* 未关差:橙色 */
}
.pdf-card {
height: 100px;
width: 200px;
position: fixed;
top: 120px;
right: 15px;
z-index: 50;
background: #fff;
border: 1px solid #cacaca;
padding: 6px 12px;
border-radius: 7px;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
font-weight: 600;
}
</style>js
import html2Canvas from 'html2canvas'
// import jsPDF from 'jspdf'
var script0 = document.createElement('script')
script0.type = 'text/javascript'
script0.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.2/jspdf.umd.min.js'
document.head.appendChild(script0)
const htmlToPdf = {
// 下载 PDF 文件
async getPdf(title = '文件') {
const pdf = new jsPDF('p', 'mm', 'a4') // 初始化,但后续每页尺寸自动调整
const selectors = ['#pdfPrintIndex']
let isFirst = true
for (let i = 0; i < selectors.length; i++) {
const el = document.querySelector(selectors[i])
if (!el) continue
const canvas = await html2Canvas(el, {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 1,
scale: 1,
letterRendering: true
})
await appendFullCanvasToPdf(canvas, pdf, !isFirst)
isFirst = false
}
pdf.save(`${title}.pdf`)
},
// 返回 PDF 文件对象
async getPdfFile(title = '文件') {
const pdf = new jsPDF('p', 'mm', 'a4')
const selectors = ['#pdfPrintIndex']
let isFirst = true
for (let i = 0; i < selectors.length; i++) {
const el = document.querySelector(selectors[i])
if (!el) continue
const canvas = await html2Canvas(el, {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 1,
scale: 1,
letterRendering: true
})
await appendFullCanvasToPdf(canvas, pdf, !isFirst)
isFirst = false
}
const blob = pdf.output('blob')
const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
return file
},
}
// ✅ 新的:不分页,整图插入
async function appendFullCanvasToPdf(canvas, pdf, addPage = false) {
const imgData = canvas.toDataURL('image/jpeg', 1.0)
// 像素转 mm:1 px ≈ 0.2646 mm
const pxToMm = 0.2646
const imgWidthMm = canvas.width * pxToMm
const imgHeightMm = canvas.height * pxToMm
// 如果不是第一页就添加新页面
if (addPage) {
pdf.addPage([imgWidthMm, imgHeightMm])
} else {
// 修改首页尺寸为当前图尺寸(首次 addPage 无法修改第一页尺寸)
pdf.internal.pageSize.width = imgWidthMm
pdf.internal.pageSize.height = imgHeightMm
}
// 插入图片
pdf.addImage(
imgData,
'JPEG',
0,
0,
imgWidthMm,
imgHeightMm
)
}
export default htmlToPdfimport html2Canvas from 'html2canvas'
// import jsPDF from 'jspdf'
var script0 = document.createElement('script')
script0.type = 'text/javascript'
script0.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.2/jspdf.umd.min.js'
document.head.appendChild(script0)
const htmlToPdf = {
// 下载 PDF 文件
async getPdf(title = '文件') {
const pdf = new jsPDF('p', 'mm', 'a4') // 初始化,但后续每页尺寸自动调整
const selectors = ['#pdfPrintIndex']
let isFirst = true
for (let i = 0; i < selectors.length; i++) {
const el = document.querySelector(selectors[i])
if (!el) continue
const canvas = await html2Canvas(el, {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 1,
scale: 1,
letterRendering: true
})
await appendFullCanvasToPdf(canvas, pdf, !isFirst)
isFirst = false
}
pdf.save(`${title}.pdf`)
},
// 返回 PDF 文件对象
async getPdfFile(title = '文件') {
const pdf = new jsPDF('p', 'mm', 'a4')
const selectors = ['#pdfPrintIndex']
let isFirst = true
for (let i = 0; i < selectors.length; i++) {
const el = document.querySelector(selectors[i])
if (!el) continue
const canvas = await html2Canvas(el, {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 1,
scale: 1,
letterRendering: true
})
await appendFullCanvasToPdf(canvas, pdf, !isFirst)
isFirst = false
}
const blob = pdf.output('blob')
const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
return file
},
}
// ✅ 新的:不分页,整图插入
async function appendFullCanvasToPdf(canvas, pdf, addPage = false) {
const imgData = canvas.toDataURL('image/jpeg', 1.0)
// 像素转 mm:1 px ≈ 0.2646 mm
const pxToMm = 0.2646
const imgWidthMm = canvas.width * pxToMm
const imgHeightMm = canvas.height * pxToMm
// 如果不是第一页就添加新页面
if (addPage) {
pdf.addPage([imgWidthMm, imgHeightMm])
} else {
// 修改首页尺寸为当前图尺寸(首次 addPage 无法修改第一页尺寸)
pdf.internal.pageSize.width = imgWidthMm
pdf.internal.pageSize.height = imgHeightMm
}
// 插入图片
pdf.addImage(
imgData,
'JPEG',
0,
0,
imgWidthMm,
imgHeightMm
)
}
export default htmlToPdf
liang14658fox