欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue前端導出頁面為PDF文件的最佳方案

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

前言

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

一、導出工具類

下面直接展示PDF導出工具類

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

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

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

			// 處理滾動區(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,保留內容和樣式
			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'; // 確保高度自適應內容
				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);
			});

			// 預加載所有圖片的增強方法
			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) => {
							// 如果圖片已經完成加載,直接解析
							if (img.complete && img.naturalHeight !== 0) {
								resolve();
								return;
							}

							// 為每個圖片添加加載和錯誤事件監(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]; // 恢復原始src
									resolve();
								};
								newImg.onerror = () => {
									img.src = originalSrcs[index]; // 恢復原始src
									resolve(); // 即使失敗也繼續(xù)執(zhí)行
								};

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

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

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

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

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

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

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

					return documentClone;
				}
			});

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

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

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

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

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

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

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

二、單頁面詳情導出

比如說我現在有個頁面叫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">導出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>

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

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

在Vue 3中,ref是一個非常重要的響應式API,它有兩種主要用途:

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

這里主要是利用了第二點,代表了當前組件的渲染實例,導出PDF按鈕對應的方法如下:

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

	isImporting.value = true;

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

	ElLoading.service({
		lock: true,
		text: '正在導出PDF,請稍后...',
		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('導出PDF失敗', error);
	}
};

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

三、列表頁批量壓縮導出

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

以下代碼是針對勾選數據的定義和響應式綁定

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

批量導出壓縮PDF文件的代碼如下,比較復雜,僅供參考:

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

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

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

		// 定義批處理大小和函數
		const batchSize = 5; // 每批處理的數量,可以根據實際情況調整

		// 批量處理函數
		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(`正在導出第 ${overall}/${selectedRows.value.length} 個周報...`);

						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'; // 幾乎不可見但會渲染

						// 加載詳情頁面的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) => {
									// 設置超時
									const timeoutId = setTimeout(() => {
										rejectIframe(new Error('加載超時'));
									}, 15000); // 15秒超時

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

										try {
											// 給頁面充分的時間加載數據和渲染
											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和滾動區(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);
											});

											// 處理滾動區(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';
												}
											});

											// 預加載所有圖片
											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是數據URL,直接解析
														if (!img.src || img.src.startsWith('data:')) {
															resolveImg();
														} else {
															// 添加時間戳以避免緩存問題
															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);
														}
													});
												})
											);

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

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

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

											// 從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');

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

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

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

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

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

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

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

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

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

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

								// 等待一段時間再重試
								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);
					}
				});
			});

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

		// 將報告分成多個批次
		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} 批周報...`);
			const batchResults = await processBatch(reportBatches[i]);

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

			// 釋放一些內存
			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 = `周報匯總_${new Date().getTime()}.zip`;
		link.click();
		URL.revokeObjectURL(zipUrl);

		ElNotification({
			title: '導出成功',
			message: `已將${allPdfResults.length}個周報導出為ZIP壓縮文件`,
			type: 'success',
		});
	} catch (error) {
		console.error('導出PDF時發(fā)生錯誤:', error);
		ElNotification({
			title: '導出失敗',
			message: '導出PDF時發(fā)生錯誤,請稍后再試',
			type: 'error',
		});
	} finally {
		loading.close();
	}
};

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

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

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

四、總結

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

以上就是Vue前端導出頁面為PDF文件的最佳方案的詳細內容,更多關于Vue導出頁面為PDF的資料請關注腳本之家其它相關文章!

相關文章

  • Vue3在jsx下父子組件實現使用插槽方式

    Vue3在jsx下父子組件實現使用插槽方式

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

    教你三分鐘掌握Vue過濾器filters及時間戳轉換

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

    vue獲取DOM元素并設置屬性的兩種實現方法

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

    Vue3報錯Cannot convert undefined or null&n

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

    vue-router中scrollBehavior的巧妙用法

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

    Vue和Flask通信的實現

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

    vue+element+springboot實現文件下載進度條展現功能示例

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

    Vue動態(tài)組件實例解析

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

    Vue父子組件傳值的一些坑

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

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

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

最新評論