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);// 👉 創建隱藏 iframeconst 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-buttontype="warning"plainsize="small"@click="printPDF":loading="exportLoading"><Icon icon="ep:view" class="mr-5px"/>打印預覽</el-button><el-buttontype="primary"plainsize="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'] = memberTotalPagesfor (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'] = photoTotalPagesfor (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);
}

最終頁面效果如下

頁面預覽

在這里插入圖片描述

打印預覽

在這里插入圖片描述

下載效果

在這里插入圖片描述

有疑問的話可以留言,大家一起交流討論,如有問題的話,可以指出,看到后修改。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/79907.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/79907.shtml
英文地址,請注明出處:http://en.pswp.cn/web/79907.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

SpringAI框架中的RAG模塊詳解及應用示例

SpringAI框架中的RAG模塊詳解及應用示例 RAG&#xff08;Retrieval-Augmented Generation&#xff09;可以通過檢索知識庫&#xff0c;克服大模型訓練完成后參數凍結的局限性&#xff0c;攜帶知識讓大模型根據知識進行回答。SpringAI框架提供了模塊化的API來支持RAG&#xff0…

MySQL-數據查詢(測試)-05-(12-1)

1-數據準備&#xff1a; CREATE TABLE 員工信息表 (員工編號 VARCHAR(10) PRIMARY KEY,姓名 VARCHAR(20),學歷 VARCHAR(20),出生日期 DATE,性別 INT,工作年限 INT,地址 VARCHAR(100),電話號碼 VARCHAR(20),員工部門號 INT ); INSERT INTO 員工信息表 (員工編號, 姓名, 學歷, 出…

5G網絡:能源管理的“智能電網“革命,Python如何成為關鍵推手?

5G網絡:能源管理的"智能電網"革命,Python如何成為關鍵推手? 大家好,我是Echo_Wish。今天咱們聊一個既硬核又接地氣的話題——5G網絡如何用Python代碼重構全球能源管理。 不知道你們有沒有注意過: ? 家里裝了智能電表后,電費突然變"聰明"了,谷時充…

AI背景下,如何重構你的產品?

當AI敲門時&#xff0c;你的產品準備好開門了嗎&#xff1f; 最近和做產品的老張聊天&#xff0c;他愁眉苦臉地說&#xff1a;"現在AI這么火&#xff0c;我們的產品就像個老古董&#xff0c;用戶都跑隔壁用AI產品去了。“這話讓我想起三年前另一個朋友&#xff0c;當時區…

互聯網大廠Java面試實戰:從Spring Boot到微服務的技術問答與解析

&#x1f4aa;&#x1f3fb; 1. Python基礎專欄&#xff0c;基礎知識一網打盡&#xff0c;9.9元買不了吃虧&#xff0c;買不了上當。 Python從入門到精通 &#x1f601; 2. 畢業設計專欄&#xff0c;畢業季咱們不慌忙&#xff0c;幾百款畢業設計等你選。 ?? 3. Python爬蟲專欄…

Apollo學習——aem問題

執行aem指令出現一下問題 lxflxf:~/MYFile/apollo_v10.0 $aem enter permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.49/containers/json?filters%7B%22name%22%…

數字IC后端零基礎入門基礎理論(Day2)

數字IC后端零基礎入門基礎理論&#xff08;Day1&#xff09; Placement Blockage: cell擺放阻擋層。它是用來引導工具做placement的一種物理約束或手段&#xff0c;目的是希望工具按照我們的要求來做標準單元的擺放。 它主要有三種類型&#xff0c;分別是hard placement bloc…

如何遠程執行腳本不留痕跡

通常我們在做遠程維護的時候&#xff0c;會有這么一個需求&#xff0c;就是我想在遠程主機執行一個腳本&#xff0c;但是這個腳本我又不想保留在遠程主機上&#xff0c;那么有人就說了&#xff0c;那就復制過去再登錄遠程執行不就行了嗎&#xff1f;嗯嗯&#xff0c;但是這還不…

【Lua】java 調用redis執行 lua腳本

【Lua】java 調用redis執行 lua腳本 public Object executeLuaScript(String script, List<String> keys, Object... args) {// 注意: 這里 Long.class 是返回值類型, 一定要指定清楚 不然會報錯return this.redisTemplate.execute(RedisScript.of(j腳本, Long.class), k…

利用混合磁共振成像 - 顯微鏡纖維束成像技術描繪結構連接組|文獻速遞-深度學習醫療AI最新文獻

Title 題目 Imaging the structural connectome with hybrid MRI-microscopy tractography 利用混合磁共振成像 - 顯微鏡纖維束成像技術描繪結構連接組 01 文獻速遞介紹 通過多種模態繪制大腦結構能夠增進我們對大腦功能、發育、衰老以及疾病的理解&#xff08;漢森等人&am…

Shell腳本實踐(修改文件,修改配置文件,執行jar包)

1、前言 需要編寫一個shell腳本支持 1、修改.so文件名 2、修改配置文件 3、執行jar包 2、代碼解析 2.1、修改.so文件名 so_file_dir="/opt/casb/xxx/lib" # 處理.so文件 cd "$so_file_dir" || { echo "錯誤: 無法進入目錄 $so_file_dir"; exit …

基于GPUGEEK 平臺進行深度學習

一、平臺簡介 GPUGEEK 是一個專注于提供 GPU 算力租賃服務的平臺&#xff0c;在人工智能與深度學習領域為用戶搭建起便捷的算力橋梁。它整合了豐富多樣的 GPU 資源&#xff0c;涵蓋 RTX - 4090、RTX - 3090、A100 - PCIE 等多種型號&#xff0c;滿足不同用戶在模型訓練、數據處…

Android Framework學習五:APP啟動過程原理及速度優化

文章目錄 APP啟動優化概述APP啟動流程點擊圖片啟動APP的過程啟動觸發Zygote 與應用進程創建Zygote進程的創建應用進程初始化 ApplicationActivity 啟動與顯示 優化啟動時黑白屏現象可優化的階段Application階段相關優化 Activity階段數據加載階段 Framework學習系列文章 APP啟動…

Web 實時通信技術:WebSocket 與 Server-Sent Events (SSE) 深入解析

一、WebSocket&#xff1a; &#xff08;一&#xff09;WebSocket 是什么&#xff1f; WebSocket 是一種網絡通信協議&#xff0c;它提供了一種在單個 TCP 連接上進行全雙工通信的方式。與傳統的 HTTP 請求 - 響應模型不同&#xff0c;WebSocket 允許服務器和客戶端在連接建立…

MySQL(8)什么是主鍵和外鍵?

主鍵&#xff08;Primary Key&#xff09;和外鍵&#xff08;Foreign Key&#xff09;是關系數據庫中用于定義和維護表之間關系的重要概念。以下是詳細的解釋、示例代碼和操作步驟。 主鍵&#xff08;Primary Key&#xff09; 定義 主鍵是表中的一個或多個字段&#xff0c;其…

任意復雜度的 JSON 數據轉換為多個結構化的 Pandas DataFrame 表格

以下是一個 完整、結構清晰、可運行的 Python 工具&#xff0c;用于將任意復雜度的 JSON 數據轉換為多個結構化的 Pandas DataFrame 表格。該工具支持嵌套對象、嵌套數組&#xff0c;并通過主鍵和外鍵建立表之間的關聯關系。 if __name__ "__main__":# 示例 JSON 數…

【SSL部署與優化?】??HTTP/2與HTTPS的協同效應

HTTP/2與HTTPS的協同效應&#xff1a;為何HTTP/2強制要求TLS 1.2&#xff1f; HTTP/2是HTTP協議的現代升級版&#xff0c;旨在通過多路復用、頭部壓縮等技術提升性能。然而&#xff0c;HTTP/2的設計與部署與HTTPS&#xff08;TLS加密&#xff09;緊密相關&#xff0c;甚至強制…

爬蟲請求頻率應控制在多少合適?

爬蟲請求頻率的控制是一個非常重要的問題&#xff0c;它不僅關系到爬蟲的效率&#xff0c;還涉及到對目標網站服務器的影響以及避免被封禁的風險。合理的請求頻率需要根據多個因素來綜合考慮&#xff0c;以下是一些具體的指導原則和建議&#xff1a; 一、目標網站的政策 查看網…

使用Visual Studio將C#程序發布為.exe文件

說明 .exe 是可執行文件&#xff08;Executable File&#xff09;的擴展名。這類文件包含計算機可以直接運行的機器代碼指令&#xff0c;通常由編程語言&#xff08;如 C、C、C#、Python 等&#xff09;編譯或打包生成。可以用于執行自動化操作&#xff08;執行腳本或批處理操…

分布式1(cap base理論 鎖 事務 冪等性 rpc)

目錄 分布式系統介紹 一、定義與概念 二、分布式系統的特點 三、分布式系統面臨的挑戰 四、分布式系統的常見應用場景 CAP 定理 BASE 理論 BASE理論是如何保證最終一致性的 分布式鎖的常見使用場景有哪些&#xff1f; 1. 防止多節點重復操作 2. 資源互斥訪問 3. 分…