亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Vue前端導(dǎo)出頁面為PDF文件的最佳方案

 更新時(shí)間:2025年07月23日 10:07:34   作者:就胡編亂碼  
這篇文章主要介紹了前端導(dǎo)出PDF方案,通過html2canvas和jsPDF實(shí)現(xiàn)單頁導(dǎo)出,利用iframe分批處理列表頁數(shù)據(jù)并打包ZIP,兼顧性能與樣式還原,有效減輕服務(wù)端壓力,需要的朋友可以參考下

前言

小編最近遇到一個(gè)需求,要把前端渲染出來的頁面完整的導(dǎo)出為PDF格式,最開始的方案是想在服務(wù)端導(dǎo)出,使用Freemarker或者Thymeleaf模板引擎,但是頁面實(shí)在是有點(diǎn)復(fù)雜,開發(fā)起來比較費(fèi)勁,最終還是尋找前端導(dǎo)出PDF的方案。其實(shí)前端導(dǎo)出反而更好,可以減輕服務(wù)器端的壓力,導(dǎo)出來的樣式也更好看,給各位看下,筆者要導(dǎo)出的頁面,內(nèi)容還是挺多的吧。

一、導(dǎo)出工具類

下面直接展示PDF導(dǎo)出工具類

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

export default {
	/**
	 * 將HTML元素導(dǎo)出為PDF
	 * @param element 需要導(dǎo)出的DOM元素
	 * @param fileName 導(dǎo)出的文件名
	 */
	async exportElementToPdf(element: HTMLElement, fileName: string = 'document'): Promise<void> {
		if (!element) {
			console.error('導(dǎo)出元素不能為空');
			return;
		}

		try {
			// 處理textarea元素,臨時(shí)替換為div以確保內(nèi)容完整顯示
			const textareas = Array.from(element.querySelectorAll('textarea'));
			const originalStyles: { [key: string]: string } = {};
			const replacedElements: HTMLElement[] = [];

			// 處理滾動(dòng)區(qū)域
			const scrollElements = element.querySelectorAll('[style*="overflow"],[style*="height"]');
			const originalScrollStyles: { [key: string]: string } = {};

			scrollElements.forEach((el, index) => {
				const computedStyle = window.getComputedStyle(el);
				if (computedStyle.overflow === 'auto' || computedStyle.overflow === 'scroll' ||
					computedStyle.overflowY === 'auto' || computedStyle.overflowY === 'scroll') {
					originalScrollStyles[index] = (el as HTMLElement).style.cssText;
					(el as HTMLElement).style.overflow = 'visible';
					(el as HTMLElement).style.maxHeight = 'none';
					(el as HTMLElement).style.height = 'auto';
				}
			});

			// 替換所有textarea為div,保留內(nèi)容和樣式
			textareas.forEach((textarea, index) => {
				// 保存原始樣式
				originalStyles[index] = textarea.style.cssText;

				// 創(chuàng)建替代元素
				const replacementDiv = document.createElement('div');
				replacementDiv.innerHTML = textarea.value.replace(/\n/g, '
');
				replacementDiv.style.cssText = textarea.style.cssText;
				replacementDiv.style.height = 'auto'; // 確保高度自適應(yīng)內(nèi)容
				replacementDiv.style.minHeight = window.getComputedStyle(textarea).height;
				replacementDiv.style.border = window.getComputedStyle(textarea).border;
				replacementDiv.style.padding = window.getComputedStyle(textarea).padding;
				replacementDiv.style.boxSizing = 'border-box';
				replacementDiv.style.whiteSpace = 'pre-wrap';
				replacementDiv.style.overflowY = 'visible';

				// 替換元素
				textarea.parentNode?.insertBefore(replacementDiv, textarea);
				textarea.style.display = 'none';
				replacedElements.push(replacementDiv);
			});

			// 預(yù)加載所有圖片的增強(qiáng)方法
			const preloadImages = async () => {
				// 查找所有圖片元素
				const images = Array.from(element.querySelectorAll('img'));

				// 記錄原始的src屬性
				const originalSrcs = images.map(img => img.src);

				// 確保所有圖片都完全加載
				await Promise.all(
					images.map((img, index) => {
						return new Promise<void>((resolve) => {
							// 如果圖片已經(jīng)完成加載,直接解析
							if (img.complete && img.naturalHeight !== 0) {
								resolve();
								return;
							}

							// 為每個(gè)圖片添加加載和錯(cuò)誤事件監(jiān)聽器
							const onLoad = () => {
								img.removeEventListener('load', onLoad);
								img.removeEventListener('error', onError);
								resolve();
							};

							const onError = () => {
								console.warn(`無法加載圖片: ${img.src}`);
								img.removeEventListener('load', onLoad);
								img.removeEventListener('error', onError);

								// 嘗試重新加載圖片
								const newImg = new Image();
								newImg.crossOrigin = "Anonymous";
								newImg.onload = () => {
									img.src = originalSrcs[index]; // 恢復(fù)原始src
									resolve();
								};
								newImg.onerror = () => {
									img.src = originalSrcs[index]; // 恢復(fù)原始src
									resolve(); // 即使失敗也繼續(xù)執(zhí)行
								};

								// 強(qiáng)制重新加載
								const src = img.src;
								img.src = '';
								setTimeout(() => {
									newImg.src = src;
								}, 100);
							};

							img.addEventListener('load', onLoad);
							img.addEventListener('error', onError);

							// 如果圖片沒有src或src是數(shù)據(jù)URL,直接解析
							if (!img.src || img.src.startsWith('data:')) {
								resolve();
							}
						});
					})
				);
			};

			// 預(yù)加載所有圖片
			await preloadImages();

			// 使用html2canvas將整個(gè)元素轉(zhuǎn)為單個(gè)canvas
			const canvas = await html2canvas(element, {
				scale: 2, // 提高清晰度
				useCORS: true, // 允許加載跨域圖片
				logging: false,
				allowTaint: true, // 允許污染畫布
				backgroundColor: '#ffffff', // 設(shè)置背景色為白色
				imageTimeout: 15000, // 增加圖片加載超時(shí)時(shí)間到15秒
				onclone: (documentClone) => {
					// 在克隆的文檔中查找所有圖片
					const clonedImages = documentClone.querySelectorAll('img');

					// 確保所有圖片都設(shè)置了crossOrigin屬性
					clonedImages.forEach(img => {
						img.crossOrigin = "Anonymous";

						// 對(duì)于數(shù)據(jù)URL的圖片跳過
						if (img.src && !img.src.startsWith('data:')) {
							// 添加時(shí)間戳以避免緩存問題
							if (img.src.indexOf('?') === -1) {
								img.src = `${img.src}?t=${new Date().getTime()}`;
							} else {
								img.src = `${img.src}&t=${new Date().getTime()}`;
							}
						}
					});

					return documentClone;
				}
			});

			// 恢復(fù)原始DOM,移除臨時(shí)添加的元素
			textareas.forEach((textarea, index) => {
				textarea.style.cssText = originalStyles[index];
				textarea.style.display = '';
				if (replacedElements[index] && replacedElements[index].parentNode) {
					replacedElements[index].parentNode.removeChild(replacedElements[index]);
				}
			});

			// 恢復(fù)滾動(dòng)區(qū)域的樣式
			scrollElements.forEach((el, index) => {
				if (originalScrollStyles[index]) {
					(el as HTMLElement).style.cssText = originalScrollStyles[index];
				}
			});

			// 創(chuàng)建PDF(使用適合內(nèi)容的尺寸)
			// 如果內(nèi)容寬高比接近A4,使用A4;否則使用自定義尺寸
			const imgWidth = 210; // A4寬度(mm)
			const imgHeight = (canvas.height * imgWidth) / canvas.width;

			// 使用一頁完整顯示內(nèi)容,不強(qiáng)制分頁
			const pdf = new jsPDF({
				orientation: imgHeight > 297 ? 'p' : 'p', // 如果內(nèi)容高度超過A4高度,使用縱向
				unit: 'mm',
				format: imgHeight > 297 ? [imgWidth, imgHeight] : 'a4' // 如果內(nèi)容高度超過A4高度,使用自定義尺寸
			});

			// 添加圖像到PDF,確保填滿頁面但保持比例
			pdf.addImage(
				canvas.toDataURL('image/jpeg', 1.0), // 使用高質(zhì)量
				'JPEG',
				0,
				0,
				imgWidth,
				imgHeight
			);

			// 保存PDF
			pdf.save(`${fileName}.pdf`);
		} catch (error) {
			console.error('導(dǎo)出PDF時(shí)發(fā)生錯(cuò)誤:', error);
		}
	}
};

這個(gè) 工具類考慮了導(dǎo)出的html頁面中的圖片和text滾動(dòng)文本框,使得導(dǎo)出來的PDF文件能夠完整展示原HTML頁面內(nèi)容,基本能做到95%以上的還原吧,導(dǎo)出的格式是A4紙張大小,方便打印出來。

二、單頁面詳情導(dǎo)出

比如說我現(xiàn)在有個(gè)頁面叫detail.vue,頁面模板部分如下

<template >
	<div
		class="reports-detail-page"
		v-if="reportDetail"
		ref="weekReportRef"

	>
		<img
			src="/icon/read.png"
			class="read-mark"
			:class="{
				'read-mark-mobile': mainStates.isMobile,
			}"
			alt="已審批"
			v-if="reportDetail.weekReports.status === 1"
		/>
		<el-button  class="export-pdf" type="primary" v-if="!isImporting" size="small" @click="downloadPdf">導(dǎo)出PDF</el-button>
		<week-report
			:is-plan="false"
			v-if="reportDetail.lastWeekReports"
			:week-report="reportDetail.lastWeekReports"
			:self-comments="reportDetail.weekReportsSelfCommentsList"
		/>
		<week-report
			:is-plan="true"
			:week-report="reportDetail.weekReports"
			:self-comments="reportDetail.weekReportsSelfCommentsList"
		/>
		<comment-area :is-importing="isImporting" :report-detail="reportDetail" />
	</div>
</template>

這里的關(guān)鍵屬性是ref=“weekReportRef”,其聲明定義如下:

const weekReportRef = ref<HTMLElement | null>(null);

在Vue 3中,ref是一個(gè)非常重要的響應(yīng)式API,它有兩種主要用途:

  1. 在腳本中創(chuàng)建響應(yīng)式變量:通過ref()函數(shù)創(chuàng)建一個(gè)響應(yīng)式引用
  2. 在模板中引用DOM元素或組件實(shí)例:通過在模板元素上添加ref屬性

這里主要是利用了第二點(diǎn),代表了當(dāng)前組件的渲染實(shí)例,導(dǎo)出PDF按鈕對(duì)應(yīng)的方法如下:

// 下載PDF
const downloadPdf = async () => {
	if (!weekReportRef.value) return;

	isImporting.value = true;

	// 創(chuàng)建文件名,例如:張三_2025年第28周_總結(jié)
	const fileName = `${reportDetail.value?.weekReports.userName}${weekDesc.value}周報(bào)`;

	ElLoading.service({
		lock: true,
		text: '正在導(dǎo)出PDF,請(qǐng)稍后...',
		spinner: 'el-icon-loading',
		background: 'rgba(0, 0, 0, 0.7)',
	});
	try {
		// 使用nextTick等待DOM更新完成
		await nextTick();
		await PdfExportUtils.exportElementToPdf(weekReportRef.value, fileName).then(()=>{
			isImporting.value = false;
			ElLoading.service().close();
		});
	} catch (error) {
		console.error('導(dǎo)出PDF失敗', error);
	}
};

通過以上代碼,可以看到在調(diào)用導(dǎo)出PDF時(shí),傳入了當(dāng)前的組件的實(shí)例,其中isImporting這個(gè)屬性,是筆者為了限制某些按鈕什么的控件不要在導(dǎo)出后的PDF文件中顯示而添加的臨時(shí)屬性。

三、列表頁批量壓縮導(dǎo)出

上面說的是單頁面導(dǎo)出PDF,那如果有個(gè)列表頁,需要批量選擇然后導(dǎo)出怎么辦?導(dǎo)出過程中,又沒辦法一個(gè)個(gè)點(diǎn)進(jìn)去等待數(shù)據(jù)渲染。前輩大佬早就想到了這個(gè)場(chǎng)景,我們可以利用html中的標(biāo)簽iframe,在批量選擇導(dǎo)出時(shí),為每一個(gè)列表數(shù)據(jù)臨時(shí)創(chuàng)建一個(gè)渲染后的詳情頁面數(shù)據(jù),即Dom中的Dom,然后對(duì)嵌套頁面導(dǎo)出壓縮,當(dāng)然我們用戶自己是感知不到的。比如下面的列表:

以下代碼是針對(duì)勾選數(shù)據(jù)的定義和響應(yīng)式綁定

const selectedRows = ref<WeekReportsDetail[]>([]);
// 處理表格選擇變化
const handleSelectionChange = (selection: WeekReportsDetail[]) => {
	selectedRows.value = selection;
};

批量導(dǎo)出壓縮PDF文件的代碼如下,比較復(fù)雜,僅供參考:

// 導(dǎo)出選中項(xiàng)到PDF并壓縮
const exportSelectedToPdf = async () => {
	if (selectedRows.value.length === 0) {
		ElNotification({
			title: '提示',
			message: '請(qǐng)先選擇要導(dǎo)出的周報(bào)',
			type: 'warning',
		});
		return;
	}

	// 顯示加載中提示
	const loading = ElLoading.service({
		lock: true,
		text: `正在準(zhǔn)備導(dǎo)出...`,
		spinner: 'el-icon-loading',
		background: 'rgba(0, 0, 0, 0.7)',
	});

	try {
		// 創(chuàng)建ZIP實(shí)例
		const zip = new JSZip();
		const allPdfResults: { fileName: string, pdfBlob: Blob }[] = [];

		// 定義批處理大小和函數(shù)
		const batchSize = 5; // 每批處理的數(shù)量,可以根據(jù)實(shí)際情況調(diào)整

		// 批量處理函數(shù)
		const processBatch = async (batchReports: WeekReportsDetail[]) => {
			const batchPromises = batchReports.map((report) => {
				return new Promise<{fileName: string, pdfBlob: Blob}>(async (resolve, reject) => {
					try {
						const overall = selectedRows.value.indexOf(report) + 1;
						loading.setText(`正在導(dǎo)出第 ${overall}/${selectedRows.value.length} 個(gè)周報(bào)...`);

						const iframe = document.createElement('iframe');
						iframe.style.position = 'fixed';
						iframe.style.left = '0';
						iframe.style.top = '0';
						iframe.style.width = '1024px';
						iframe.style.height = '768px';
						iframe.style.border = 'none';
						iframe.style.zIndex = '-1';
						iframe.style.opacity = '0.01'; // 幾乎不可見但會(huì)渲染

						// 加載詳情頁面的URL
						iframe.src = `${window.location.origin}/center/detail/${report.id}?corpId=${mainStates.corpId}&isImporting=true`;
						document.body.appendChild(iframe);

						// 使用Promise包裝iframe加載和處理
						let retryCount = 0;
						const maxRetries = 2;

						while (retryCount <= maxRetries) {
							try {
								await new Promise<void>((resolveIframe, rejectIframe) => {
									// 設(shè)置超時(shí)
									const timeoutId = setTimeout(() => {
										rejectIframe(new Error('加載超時(shí)'));
									}, 15000); // 15秒超時(shí)

									iframe.onload = async () => {
										clearTimeout(timeoutId);

										try {
											// 給頁面充分的時(shí)間加載數(shù)據(jù)和渲染
											await new Promise(r => setTimeout(r, 3000));
											const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
											if (!iframeDocument) {
												resolveIframe();
												return;
											}
											const reportElement = iframeDocument.querySelector('.reports-detail-page');
											if (!reportElement) {
												resolveIframe();
												return;
											}

											// 處理iframe中的所有textarea和滾動(dòng)區(qū)域
											const iframeTextareas = Array.from(reportElement.querySelectorAll('textarea'));
											const replacedElements: HTMLElement[] = [];

											// 替換所有textarea為div
											iframeTextareas.forEach((textarea) => {
												const replacementDiv = document.createElement('div');
												replacementDiv.innerHTML = textarea.value.replace(/\n/g, '
');
												replacementDiv.style.cssText = textarea.style.cssText;
												replacementDiv.style.height = 'auto';
												replacementDiv.style.minHeight = window.getComputedStyle(textarea).height;
												replacementDiv.style.boxSizing = 'border-box';
												replacementDiv.style.whiteSpace = 'pre-wrap';
												replacementDiv.style.overflowY = 'visible';

												textarea.parentNode?.insertBefore(replacementDiv, textarea);
												textarea.style.display = 'none';
												replacedElements.push(replacementDiv);
											});

											// 處理滾動(dòng)區(qū)域
											const scrollElements = reportElement.querySelectorAll('[style*="overflow"],[style*="height"]');
											scrollElements.forEach((el) => {
												const computedStyle = window.getComputedStyle(el);
												if (computedStyle.overflow === 'auto' || computedStyle.overflow === 'scroll' ||
													computedStyle.overflowY === 'auto' || computedStyle.overflowY === 'scroll') {
													(el as HTMLElement).style.overflow = 'visible';
													(el as HTMLElement).style.maxHeight = 'none';
													(el as HTMLElement).style.height = 'auto';
												}
											});

											// 預(yù)加載所有圖片
											const images = Array.from(reportElement.querySelectorAll('img'));
											await Promise.all(
												images.map(img => {
													return new Promise<void>((resolveImg) => {
														if (img.complete && img.naturalHeight !== 0) {
															resolveImg();
															return;
														}

														const onLoad = () => {
															img.removeEventListener('load', onLoad);
															img.removeEventListener('error', onError);
															resolveImg();
														};

														const onError = () => {
															console.warn(`無法加載圖片: ${img.src}`);
															img.removeEventListener('load', onLoad);
															img.removeEventListener('error', onError);
															resolveImg();
														};

														img.addEventListener('load', onLoad);
														img.addEventListener('error', onError);

														// 如果圖片沒有src或src是數(shù)據(jù)URL,直接解析
														if (!img.src || img.src.startsWith('data:')) {
															resolveImg();
														} else {
															// 添加時(shí)間戳以避免緩存問題
															const currentSrc = img.src;
															img.src = '';
															setTimeout(() => {
																if (currentSrc.indexOf('?') === -1) {
																	img.src = `${currentSrc}?t=${new Date().getTime()}`;
																} else {
																	img.src = `${currentSrc}&t=${new Date().getTime()}`;
																}
															}, 50);
														}
													});
												})
											);

											// 等待額外時(shí)間確保渲染完成
											await new Promise(r => setTimeout(r, 1000));

											// 創(chuàng)建周報(bào)文件名
											const weekDesc = DateTimeUtils.getWeekDescByYearAndWeek({
												weekIndex: report.weekIndex,
												yearIndex: report.year,
											});
											const fileName = `${report.userName}_${weekDesc}周報(bào).pdf`;

											// 使用html2canvas轉(zhuǎn)換為canvas
											const canvas = await html2canvas(reportElement as HTMLElement, {
												scale: 2,
												useCORS: true,
												logging: false,
												allowTaint: true,
												backgroundColor: '#ffffff',
												imageTimeout: 15000, // 增加超時(shí)時(shí)間
											});

											// 從canvas創(chuàng)建PDF
											const imgWidth = 210; // A4寬度(mm)
											const imgHeight = (canvas.height * imgWidth) / canvas.width;
											const pdf = new jsPDF({
												orientation: imgHeight > 297 ? 'p' : 'p',
												unit: 'mm',
												format: imgHeight > 297 ? [imgWidth, imgHeight] : 'a4',
											});

											pdf.addImage(
												canvas.toDataURL('image/jpeg', 1.0),
												'JPEG',
												0,
												0,
												imgWidth,
												imgHeight,
											);

											// 獲取PDF的Blob
											const pdfBlob = pdf.output('blob');

											// 恢復(fù)iframe中的DOM
											iframeTextareas.forEach((textarea, index) => {
												textarea.style.display = '';
												if (replacedElements[index] && replacedElements[index].parentNode) {
													replacedElements[index].parentNode.removeChild(replacedElements[index]);
												}
											});

											// 解析PDF處理結(jié)果
											resolveIframe();

											// 直接添加到ZIP
											zip.file(fileName, pdfBlob);
											resolve({ fileName, pdfBlob });

										} catch (error) {
											console.error('處理PDF時(shí)出錯(cuò):', error);
											rejectIframe(error);
										}
									};

									iframe.onerror = () => {
										clearTimeout(timeoutId);
										rejectIframe(new Error('iframe加載失敗'));
									};
								});

								// 如果成功處理了,跳出重試循環(huán)
								break;

							} catch (error) {
								retryCount++;
								console.warn(`處理PDF失敗,正在重試(${retryCount}/${maxRetries})...`, error);

								// 如果已經(jīng)達(dá)到最大重試次數(shù),則放棄這個(gè)報(bào)告
								if (retryCount > maxRetries) {
									console.error(`無法處理周報(bào) ${report.id},已達(dá)到最大重試次數(shù)`);
									// 創(chuàng)建一個(gè)空白PDF表示失敗
									const weekDesc = DateTimeUtils.getWeekDescByYearAndWeek({
										weekIndex: report.weekIndex,
										yearIndex: report.year,
									});
									const fileName = `${report.userName}_${weekDesc}周報(bào)(處理失敗).pdf`;

									// 創(chuàng)建一個(gè)簡(jiǎn)單的錯(cuò)誤PDF
									const pdf = new jsPDF();
									pdf.setFontSize(16);
									pdf.text('處理此周報(bào)時(shí)出錯(cuò)', 20, 20);
									pdf.setFontSize(12);
									pdf.text(`用戶: ${report.userName}`, 20, 40);
									pdf.text(`周報(bào)ID: ${report.id}`, 20, 50);
									pdf.text(`時(shí)間: ${weekDesc}`, 20, 60);
									pdf.text(`錯(cuò)誤信息: ${error || '未知錯(cuò)誤'}`, 20, 70);

									const errorPdfBlob = pdf.output('blob');
									zip.file(fileName, errorPdfBlob);
									resolve({ fileName, pdfBlob: errorPdfBlob });
									break;
								}

								// 等待一段時(shí)間再重試
								await new Promise(r => setTimeout(r, 2000));
							}
						}

						// 移除iframe
						if (document.body.contains(iframe)) {
							document.body.removeChild(iframe);
						}

					} catch (error) {
						console.error('PDF生成失敗:', error);
						reject(error);
					}
				});
			});

			// 處理當(dāng)前批次
			return await Promise.allSettled(batchPromises);
		};

		// 將報(bào)告分成多個(gè)批次
		const reportBatches: WeekReportsDetail[][] = [];
		for (let i = 0; i < selectedRows.value.length; i += batchSize) {
			reportBatches.push(selectedRows.value.slice(i, i + batchSize));
		}

		// 逐批處理
		for (let i = 0; i < reportBatches.length; i++) {
			loading.setText(`正在處理第 ${i+1}/${reportBatches.length} 批周報(bào)...`);
			const batchResults = await processBatch(reportBatches[i]);

			// 將結(jié)果添加到總結(jié)果中
			batchResults.forEach(result => {
				if (result.status === 'fulfilled') {
					allPdfResults.push(result.value);
				}
			});

			// 釋放一些內(nèi)存
			await new Promise(r => setTimeout(r, 500));
		}

		// 生成ZIP文件
		loading.setText('正在生成ZIP文件...');

		// 生成并下載ZIP文件
		const zipBlob = await zip.generateAsync({type: 'blob'});
		const zipUrl = URL.createObjectURL(zipBlob);
		const link = document.createElement('a');
		link.href = zipUrl;
		link.download = `周報(bào)匯總_${new Date().getTime()}.zip`;
		link.click();
		URL.revokeObjectURL(zipUrl);

		ElNotification({
			title: '導(dǎo)出成功',
			message: `已將${allPdfResults.length}個(gè)周報(bào)導(dǎo)出為ZIP壓縮文件`,
			type: 'success',
		});
	} catch (error) {
		console.error('導(dǎo)出PDF時(shí)發(fā)生錯(cuò)誤:', error);
		ElNotification({
			title: '導(dǎo)出失敗',
			message: '導(dǎo)出PDF時(shí)發(fā)生錯(cuò)誤,請(qǐng)稍后再試',
			type: 'error',
		});
	} finally {
		loading.close();
	}
};

執(zhí)行流程與關(guān)鍵步驟

  1. 前置校驗(yàn)與初始化
  • 選中項(xiàng)校驗(yàn):首先檢查 selectedRows(選中的周報(bào)數(shù)組)是否為空,若為空則通過 ElNotification 顯示警告提示(“請(qǐng)先選擇要導(dǎo)出的周報(bào)”),直接終止流程。
  • 加載提示初始化:通過 ElLoading.service 創(chuàng)建全屏加載提示,顯示 “正在準(zhǔn)備導(dǎo)出…”,鎖定頁面交互以避免重復(fù)操作。
  1. 批量處理機(jī)制
    為避免一次性處理過多數(shù)據(jù)導(dǎo)致瀏覽器性能問題,采用分批處理策略:
  • 批處理配置:定義 batchSize = 5(每批處理 5 個(gè)周報(bào),可按需調(diào)整),將選中的周報(bào)數(shù)組拆分為多個(gè)批次(reportBatches)。
  • 逐批處理:通過循環(huán)逐個(gè)處理每個(gè)批次,每批處理完成后等待 500ms 釋放內(nèi)存,降低瀏覽器資源占用。
  1. 單批周報(bào)處理(核心邏輯)
    每批周報(bào)通過 processBatch 函數(shù)處理,單個(gè)周報(bào)的轉(zhuǎn)換流程如下:
  • 創(chuàng)建隱藏 iframe:動(dòng)態(tài)生成一個(gè)不可見的 iframe(定位在頁面外,透明度 0.01),用于加載周報(bào)詳情頁(/center/detail/${report.id})。iframe 的作用是隔離詳情頁環(huán)境,避免直接操作當(dāng)前頁面 DOM 導(dǎo)致沖突。
  • iframe 加載與重試機(jī)制:
    • 為 iframe 設(shè)置 15 秒超時(shí)時(shí)間,若加載失敗則重試(最多重試 2 次),避免因網(wǎng)絡(luò)或資源加載問題導(dǎo)致單個(gè)周報(bào)處理失敗。
    • 加載完成后等待 3 秒,確保詳情頁數(shù)據(jù)和樣式完全渲染。
  • DOM 預(yù)處理(確保 PDF 內(nèi)容完整):
    • 替換 textarea:將詳情頁中的 textarea 替換為 div(保留原樣式),因?yàn)?textarea 的滾動(dòng)特性可能導(dǎo)致內(nèi)容截?cái)?,替換后可完整顯示所有文本。
    • 處理滾動(dòng)區(qū)域:將帶有 overflow: auto/scroll 或固定高度的元素改為 overflow: visible 且 maxHeight: none,確保內(nèi)容不被容器截?cái)唷?/li>
  • 圖片預(yù)加載:遍歷詳情頁中的所有圖片,等待圖片加載完成(或超時(shí) / 錯(cuò)誤)后再繼續(xù),避免 PDF 中出現(xiàn)圖片缺失。通過添加時(shí)間戳(?t=${time})避免緩存影響。
  • 轉(zhuǎn)換為 PDF:
    • 用 html2canvas 將預(yù)處理后的詳情頁元素(.reports-detail-page)轉(zhuǎn)換為 canvas(scale: 2 提高清晰度)。
    • 用 jsPDF 將 canvas 轉(zhuǎn)為 PDF,設(shè)置 A4 尺寸(或自適應(yīng)內(nèi)容高度),輸出為 Blob 格式。
      異常處理:若多次重試后仍失敗,生成一個(gè) “錯(cuò)誤 PDF”(包含失敗原因、周報(bào) ID 等信息),避免單個(gè)失敗阻斷整個(gè)批次。
  1. 壓縮與下載
  • ZIP 打包:所有 PDF 處理完成后,通過 JSZip 將所有 PDF Blob 打包為一個(gè) ZIP 文件,文件名格式為 “周報(bào)匯總_時(shí)間戳.zip”。
  • 觸發(fā)下載:將 ZIP 文件轉(zhuǎn)換為 Blob URL,通過動(dòng)態(tài)創(chuàng)建 標(biāo)簽觸發(fā)瀏覽器下載,下載完成后釋放 URL 資源。
  1. 結(jié)果反饋與資源清理
  • 成功反饋:若全部處理完成,通過 ElNotification 顯示成功提示(“已將 X 個(gè)周報(bào)導(dǎo)出為 ZIP 壓縮文件”)。
  • 異常反饋:若過程中出現(xiàn)未捕獲的錯(cuò)誤,顯示錯(cuò)誤提示(“導(dǎo)出失敗,請(qǐng)稍后再試”)。
  • 資源清理:無論成功或失敗,最終通過 loading.close() 關(guān)閉加載提示,釋放頁面鎖定。

核心步驟就是iframe,動(dòng)態(tài)生成一個(gè)不可見的 iframe(定位在頁面外,透明度 0.01),用于加載周報(bào)詳情頁(/center/detail/${report.id}),另外為什么采用批處理,不一次并發(fā)執(zhí)行呢?因?yàn)橐淮螆?zhí)行過多,渲染太多子頁面,超出瀏覽器承受范圍會(huì)報(bào)錯(cuò)。

四、總結(jié)

綜上,前端導(dǎo)出 PDF 方案通過 html2canvas 與 jsPDF 組合,結(jié)合 DOM 預(yù)處理解決了復(fù)雜頁面的完整還原問題。單頁導(dǎo)出利用 Vue 的 ref 獲取 DOM 元素直接轉(zhuǎn)換,批量導(dǎo)出則借助 iframe 隔離渲染環(huán)境并配合 JSZip 壓縮,既減輕了服務(wù)端壓力,又保證了導(dǎo)出效果。實(shí)際應(yīng)用中可根據(jù)頁面復(fù)雜度調(diào)整預(yù)處理邏輯與批處理參數(shù),平衡導(dǎo)出效率與準(zhǔn)確性。

以上就是Vue前端導(dǎo)出頁面為PDF文件的最佳方案的詳細(xì)內(nèi)容,更多關(guān)于Vue導(dǎo)出頁面為PDF的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Vue3在jsx下父子組件實(shí)現(xiàn)使用插槽方式

    Vue3在jsx下父子組件實(shí)現(xiàn)使用插槽方式

    這篇文章主要介紹了Vue3在jsx下父子組件實(shí)現(xiàn)使用插槽方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • 教你三分鐘掌握Vue過濾器filters及時(shí)間戳轉(zhuǎn)換

    教你三分鐘掌握Vue過濾器filters及時(shí)間戳轉(zhuǎn)換

    這篇文章教你三分鐘掌握Vue過濾器filters及時(shí)間戳轉(zhuǎn)換,本文將結(jié)合時(shí)間戳轉(zhuǎn)換的例子帶你快速了解filters的用法,需要的朋友可以參考下
    2023-03-03
  • vue獲取DOM元素并設(shè)置屬性的兩種實(shí)現(xiàn)方法

    vue獲取DOM元素并設(shè)置屬性的兩種實(shí)現(xiàn)方法

    下面小編就為大家?guī)硪黄獀ue獲取DOM元素并設(shè)置屬性的兩種實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-09-09
  • Vue3報(bào)錯(cuò)Cannot convert undefined or null to object問題及解決

    Vue3報(bào)錯(cuò)Cannot convert undefined or null&n

    Vue3與Vue-cli5中出現(xiàn)“Cannot convert undefined or null to object”錯(cuò)誤,因基類組件data()未返回默認(rèn)空值對(duì)象導(dǎo)致屬性合并異常,解決方法是確保data()返回對(duì)象,避免繼承組件屬性沖突
    2025-08-08
  • vue-router中scrollBehavior的巧妙用法

    vue-router中scrollBehavior的巧妙用法

    本文給大家介紹vue-router中scrollBehavior的妙用,文中給大家提到了兩種解決方案,需要的朋友可以參考下
    2018-07-07
  • Vue和Flask通信的實(shí)現(xiàn)

    Vue和Flask通信的實(shí)現(xiàn)

    最近新做了個(gè)項(xiàng)目,前端使用的是目前很流行的前端框架,對(duì)于后端,本項(xiàng)目選擇的是比較好上手、輕量級(jí)的python后臺(tái)框架:Flask。感興趣的可以了解一下
    2021-05-05
  • vue+element+springboot實(shí)現(xiàn)文件下載進(jìn)度條展現(xiàn)功能示例

    vue+element+springboot實(shí)現(xiàn)文件下載進(jìn)度條展現(xiàn)功能示例

    本文主要介紹了vue + element-ui + springboot 實(shí)現(xiàn)文件下載進(jìn)度條展現(xiàn)功能,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • Vue動(dòng)態(tài)組件實(shí)例解析

    Vue動(dòng)態(tài)組件實(shí)例解析

    讓多個(gè)組件使用同一個(gè)掛載點(diǎn),并動(dòng)態(tài)切換,這就是動(dòng)態(tài)組件。這篇文章主要介紹了Vue動(dòng)態(tài)組件 ,需要的朋友可以參考下
    2017-08-08
  • Vue父子組件傳值的一些坑

    Vue父子組件傳值的一些坑

    這篇文章主要介紹了Vue父子組件傳值的一些坑,幫助大家更好的理解和使用vue父子組件,感興趣的朋友可以了解下
    2020-09-09
  • vue中data和props的區(qū)別詳解

    vue中data和props的區(qū)別詳解

    這篇文章主要介紹了vue中data和props的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編來一起學(xué)習(xí)吧
    2024-01-01

最新評(píng)論