Vue前端導出頁面為PDF文件的最佳方案
前言
小編最近遇到一個需求,要把前端渲染出來的頁面完整的導出為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,它有兩種主要用途:
- 在腳本中創(chuàng)建響應式變量:通過
ref()函數創(chuàng)建一個響應式引用 - 在模板中引用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í)行流程與關鍵步驟
- 前置校驗與初始化
- 選中項校驗:首先檢查 selectedRows(選中的周報數組)是否為空,若為空則通過 ElNotification 顯示警告提示(“請先選擇要導出的周報”),直接終止流程。
- 加載提示初始化:通過 ElLoading.service 創(chuàng)建全屏加載提示,顯示 “正在準備導出…”,鎖定頁面交互以避免重復操作。
- 批量處理機制
為避免一次性處理過多數據導致瀏覽器性能問題,采用分批處理策略:
- 批處理配置:定義 batchSize = 5(每批處理 5 個周報,可按需調整),將選中的周報數組拆分為多個批次(reportBatches)。
- 逐批處理:通過循環(huán)逐個處理每個批次,每批處理完成后等待 500ms 釋放內存,降低瀏覽器資源占用。
- 單批周報處理(核心邏輯)
每批周報通過 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 等信息),避免單個失敗阻斷整個批次。
- 壓縮與下載
- ZIP 打包:所有 PDF 處理完成后,通過 JSZip 將所有 PDF Blob 打包為一個 ZIP 文件,文件名格式為 “周報匯總_時間戳.zip”。
- 觸發(fā)下載:將 ZIP 文件轉換為 Blob URL,通過動態(tài)創(chuàng)建 標簽觸發(fā)瀏覽器下載,下載完成后釋放 URL 資源。
- 結果反饋與資源清理
- 成功反饋:若全部處理完成,通過 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報錯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的妙用,文中給大家提到了兩種解決方案,需要的朋友可以參考下2018-07-07
vue+element+springboot實現文件下載進度條展現功能示例
本文主要介紹了vue + element-ui + springboot 實現文件下載進度條展現功能,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11

