vue3+vant4實(shí)現(xiàn)pdf文件上傳與預(yù)覽組件
注意下載的插件的版本"pdfjs-dist": "^2.2.228",
npm i pdfjs-dist@2.2.228
然后封裝一個pdf的遮罩。因?yàn)閜df文件有多頁,所以我用了swiper輪播的形式展示。因?yàn)橛玫揭苿佣耍謩踊瑒禹撁孢@樣比點(diǎn)下一頁下一頁的方便多了。
直接貼代碼了
PdfPreview/index.vue
<!--預(yù)覽pdf文件的組件-->
<template>
<van-overlay :show="show" @click="close()">
<div class="pdf-viewer" >
<van-swipe class="my-swipe" indicator-color="red" @click.stop>
<van-swipe-item v-for="item in pageNum" :key="item">
<canvas :id="`pdf-canvas-${item}`" class="pdf-page"/>
</van-swipe-item>
<template #indicator="{ active, total }">
<div class="custom-indicator">{{ active + 1 }}/{{ total }}</div>
</template>
</van-swipe>
<van-empty
v-if="loadError"
image="error"
description="PDF加載出錯了..."
/>
</div>
<van-icon name="close" color="#fff" size="0.3rem"/>
</van-overlay>
</template>
<script setup lang="tsx">
import {ref, nextTick, watch} from 'vue';
import {closeToast, showLoadingToast, showSuccessToast} from "vant";
// 引入pdf預(yù)覽插件相關(guān)的參數(shù),注意這塊開始試了很多網(wǎng)上方法都不好用
import * as pdfjs from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?url';
// 設(shè)置 worker 路徑
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
const show = ref(true);
// html部分涉及的參數(shù)
const loadError = ref(false);
const detail = ref({});
let pdfDoc = null; // 一定不能使用響應(yīng)式的數(shù)據(jù),會報(bào)錯Cannot read from private field---pdf.js
const pageNum = ref(0);
const props = defineProps({
pdfUrl: {
type: String,
default: ""
},
})
const emit= defineEmits(['close'])
watch(() => props.pdfUrl, (newVal) => {
// console.log("監(jiān)聽", newVal, props.pdfUrl)
showLoadingToast('加載中');
nextTick(() => {
loadingPdf(props.pdfUrl);
})
}, {immediate: true,deep:true})
// 防抖 debounce 函數(shù)的實(shí)現(xiàn)正確。
const debounce(func, wait, options = {}) {
let timeout;
const { leading = false, trailing = true } = options;
return function(...args) {
const later = () => {
timeout = null;
if (!leading) func.apply(this, args);
};
const callNow = leading && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
};
}
// 使用防抖函數(shù),300ms內(nèi)只執(zhí)行一次,避免多次點(diǎn)擊立刻打開又關(guān)閉的情況
const close = debounce(() => {
show.value = false;
emit('close')
}, 300, { leading: true, trailing: false });
//加載pdf
const loadingPdf = (url) => {
const afterUrl = {
url,
httpHeaders: {
token: `Bearer-${localStorage.getItem('token')}`,//微信小程序里面打開這個模塊,發(fā)現(xiàn)請求401,報(bào)錯信息是登陸訪問超時,發(fā)現(xiàn)pdfjs加載pdf時沒有攜帶token,于是在加載url時添加token即可
},
};
const loadingTask = pdfjs.getDocument(afterUrl);
loadingTask.promise
.then((pdf) => {
pdfDoc = pdf;
pageNum.value = pdf.numPages;
nextTick(() => {
renderPage();
});
})
.catch(() => {
loadError.value = true;
});
}
// 渲染pdf
const renderPage = (num = 1) => {
pdfDoc.getPage(num).then((page) => {
const canvas = document.getElementById(`pdf-canvas-${num}`);
if(!canvas){return}
const ctx = canvas.getContext('2d');
const scale = 1.5;
const viewport = page.getViewport({scale});
// 畫布大小,默認(rèn)值是width:300px,height:150px
canvas.height = viewport.height;
canvas.width = viewport.width;
// 畫布的dom大小, 設(shè)置移動端,寬度設(shè)置鋪滿整個屏幕
const {clientWidth} = document.body;
// 減去2rem使用因?yàn)槲业捻撁孀笥壹恿藀adding
canvas.style.width = `calc(${clientWidth}px - 2rem)`;
// 根據(jù)pdf每頁的寬高比例設(shè)置canvas的高度
canvas.style.height = `${
clientWidth * (viewport.height / viewport.width)
}px`;
canvas.height = viewport.height;
canvas.width = viewport.width;
page.render({
canvasContext: ctx,
viewport,
});
//隱藏渲染所有的頁面
if (num < pageNum.value) {
renderPage(num + 1);
} else {
closeToast();
}
});
}
</script>
<style scoped>
.pdf-viewer{
display: flex;
justify-content: center;
align-items: center;
height:100vh;
width:100vw;
text-align: center;
}
.custom-indicator {
position: absolute;
left: 50%;
bottom: 15px;
transform: translateX(-50%);
padding: 2px 5px;
font-size: 18px;
color: #fff;
background: rgba(0, 0, 0, 0.1);
}
</style>上傳的頁面可以參考文末補(bǔ)充內(nèi)容,稍微改動一下就可以了。
然后給組件添加一個點(diǎn)擊預(yù)覽的事件 。并把上面寫好的預(yù)覽組件引入

import PdfPreview from "@/components/PdfPreview/index.vue";
// 點(diǎn)擊預(yù)覽文件
const showPreview=(file)=>{
if(file.absoluteUrl.endsWith('.pdf')){
pdfUrl.value=file.absoluteUrl;
preview.value=true;
}
}遇到的問題:
如果報(bào)錯
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'getContext')
可能是canvas要找的那個id在頁面還沒有渲染出來。所以我用的nextTick,還在獲取canvas后面判斷了一下找到了再繼續(xù) ,注意上面棕色加粗的地方。
如果報(bào)錯
vue-router.mjs:3518SyntaxError: The requested module '/node_modules/.vite/deps/pdfjs-dist_build_pdf__worker__entry.js?v=8ae4d11f' does not provide an export named 'default'
檢查一下你引入插件的地方。如果是
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
這樣寫的就是錯的,改成
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?url';
問題:如果你點(diǎn)擊第一次彈窗展示了,但是再點(diǎn)擊就沒有彈出。
原因是預(yù)覽的組件渲染是監(jiān)聽的pdf的url的地址。如果你第一個打開沒有把組件銷毀。那么再次顯示的時候沒有走監(jiān)聽。就不會顯示。所以要在每次關(guān)閉彈窗是組件也銷毀。這就是上面我要在子組件中用@close給組件通知讓他不顯示也就是銷毀子組件的原因。
問題:無意間雙擊了文件導(dǎo)致遮罩馬上顯示又隱藏。頁面效果就是黑色遮罩閃了一下。
可以使用防抖的方式。延遲關(guān)閉。參考上面紫色的關(guān)閉函數(shù)
知識補(bǔ)充
vant4+vue3封裝一個上傳公共組件.有上傳和刪除訪問接口的過程。限制上傳的格式和上傳文件大小

效果圖
我的上傳接口需要參數(shù)和返回的參數(shù)


<template>
<div class="upload-box1">
<van-uploader
v-model="_fileList"
list-type="picture-card"
:class="['upload', self_disabled ? 'disabled' : '']"
:multiple="false"
:disabled="self_disabled"
:max-count="props.limit"
:after-read="handleHttpUpload"
:before-read="beforeUpload"
:before-delete="handleRemove"
:accept="props.fileType"
:deletable="props.deletable"
upload-icon="plus"
:max-size="500 * 1024* 1000"
:preview-image="true"
:preview-size="props.width"
:reupload="props.reupload"
@oversize="onOversize"
>
</van-uploader>
//我的展示效果里面有個文字。所以在下面加了這個.
<div v-for="item in _fileList" :key="item.url" v-if="_fileList.length>0" >
<div name="tips" class="tips" style="width:70px">{{props.tips}}</div>
</div>
<div class="tips" :style="{'width':props.width}" v-else>
<div name="tips">{{props.tips}}</div>
</div>
</div>
</template>
<script setup lang="tsx" name="UploadImgs">
import { ref, computed, inject, watch } from "vue";
import { removeImg, uploadFile } from "@/api/modules/upload";//封裝的接口
import {showFailToast,showSuccessToast } from 'vant';
import type { UploaderFileListItem, ImagePreviewOptions } from 'vant';
//上傳的內(nèi)容參數(shù)
interface UploaderFileList {
url:string,
name:string,
absoluteUrl:string,
businessCode:string,
businessSubCode:string,
businessId:string,
resourceId:string,
}
interface UploadFileProps {
fileList: UploaderFileList[];
disabled?: boolean; // 是否禁用上傳組件 ==> 非必傳(默認(rèn)為 false)
limit?: number; // 最大圖片上傳數(shù) ==> 非必傳(默認(rèn)為 5張)
nowLimit?:number;//當(dāng)前頁面只有一個圖片的地方。圖片上傳的數(shù)量限制
fileSize?: number; // 圖片大小限制 ==> 非必傳(默認(rèn)為 5M)
fileType?:string; // 類型限制 ==> 非必傳(默認(rèn)為 "image/jpeg", "image/png", "image/gif","application/pdf")
height?: string; // 組件高度 ==> 非必傳(默認(rèn)為 60px)
width?: string; // 組件寬度 ==> 非必傳(默認(rèn)為 60px)
borderRadius?: string; // 組件邊框圓角 ==> 非必傳(默認(rèn)為 8px)
uploadParams?: any; //上傳帶的參數(shù)==>必填
tips?:string;
deletable?:boolean;
reupload?:boolean;//是否可重復(fù)上傳==>非必填默認(rèn)false。不可重復(fù)上傳。點(diǎn)擊圖片是預(yù)覽效果
}
const props = withDefaults(defineProps<UploadFileProps>(), {
fileList:()=> [],
disabled: false,
limit: 5,
nowLimit:1,
fileSize: 5,
fileType: "image/jpeg, image/png, image/gif, application/pdf",
height: "60px",
width: "60px",
borderRadius: "8px",
uploadParams: {},
tips:"上傳",
deletable:true,
reupload:false
});
const emit = defineEmits(["update:fileList", "update"]);
// 判斷是否禁用上傳和刪除
const self_disabled = computed(() => {
return props.disabled
});
const _fileList = ref<UploaderFileList[]>(props.fileList)
const uploadFileData = ref(); //上傳的文件
// 監(jiān)聽 props.fileList 列表默認(rèn)值改變
watch(
() => props.fileList,
(n) => {
_fileList.value = n.map(res=>{
return {
url:res.absoluteUrl||'',//展示的時候需要url但是接口給我回傳的里面沒有。所以這里自己拼接一下
name:res.name||'',
absoluteUrl:res.absoluteUrl||'',
businessCode:res.businessCode||'',
businessSubCode:res.businessSubCode||'',
businessId:res.businessId||'',
resourceId:res.resourceId||'',
}
});
}
);
/**
* @description 文件上傳之前判斷
* @param rawFile 選擇的文件
* */
const beforeUpload= (rawFile) => {
console.log(rawFile)
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
const imgType = props.fileType.includes(rawFile.type);
if (!imgType)
showFailToast( "上傳文件不符合所需的格式!");
if (!imgSize)
setTimeout(() => {
showFailToast( `上傳文件大小不能超過 ${props.fileSize}M!`);
}, 0);
return imgType && imgSize;
};
const onOversize=()=>{
showFailToast( `上傳文件大小不能超過 ${props.fileSize}M!`);
}
/**
* @description 圖片上傳,請求接口
* @param options upload 所有配置項(xiàng)
* */
const handleHttpUpload = async (options: any) => {
// console.log("handleHttpUpload", options.file)
//二進(jìn)制內(nèi)容上傳參數(shù)
let formData = new FormData();
formData.append("uploadFile", options.file);
for (let key in props.uploadParams) {
formData.append(key, props.uploadParams[key]);
}
try {
const api = uploadFile;//上傳接口全局一樣,可以直接寫死
const { data,code } = await api(formData);
if(code==200){
//把接口返回的值給到操作的這個file,相當(dāng)于更新了_fileList.value
options.businessCode = data.businessCode;
options.businessSubCode = data.businessSubCode;
options.resourceId = data.resourceId;
options.businessId = data.businessId;
options.absoluteUrl = data.absoluteUrl;
options.url = data.absoluteUrl;
emit("update", { data: data });
showSuccessToast( "上傳成功!");
console.log("上傳成功fileList:",_fileList.value)
emit("update:fileList",_fileList.value);
}else{
uploadError();
}
} catch (error) {
console.log(error as any);
}
};
/**
* @description 刪除圖片
* @param file 刪除的文件
* */
// 提取過濾邏輯為獨(dú)立函數(shù),增強(qiáng)可讀性和復(fù)用性
function shouldRemoveItem(item, file) {
// 檢查每個屬性是否完全匹配,避免邏輯錯誤
return (
item.url === file.url &&
item.name === file.name &&
item.absoluteUrl === file.absoluteUrl
);
}
const handleRemove = async (file: UploaderFileListItem, detail: { index: number }) => {
// console.log("刪除",file,detail)
// 應(yīng)用過濾邏輯,過濾掉已經(jīng)刪除的內(nèi)容
_fileList.value = _fileList.value.filter((item) => !shouldRemoveItem(item, file));
console.log("刪除",_fileList.value)
let deleteParam = {
sysCode: "aged",
businessCode: file.businessCode,
businessSubCode: file.businessSubCode,
businessId: file.businessId,
resourceIds: [file.resourceId],
};
let [code]= await removeImg(deleteParam);
if(code==200){
showSuccessToast( "刪除成功!");
}
emit("update:fileList", _fileList.value);
};
/**
* @description 圖片上傳錯誤
* */
const uploadError = () => {
showFailToast( "上傳失敗,請您重新上傳!");
};
/**
* @description 文件數(shù)超出
* */
const handleExceed = () => {
showFailToast("當(dāng)前最多只能上傳"+(props.nowLimit||8)+"份,請移除后上傳!");
};
</script>
<style scoped lang="scss">
</style>
使用方法
<template>
<Upload :upload-params="serviceOtherParams" v-model:file-list="img3" :limit="6" tips="上傳內(nèi)容">
</Upload>
</template>
???????<script lang="ts" setup>
import Upload from "@/components/Upload/index.vue";
const img3=ref([])
//定義要傳遞的參數(shù)
const serviceOtherParams = reactive({
sysCode: "aged",
businessCode: "serviceOrderRecord",
businessSubCode: "serviceOther",
});
</script>img3就是最后上傳的內(nèi)容。不需要再寫函數(shù)接收上傳的返回值了。
到此這篇關(guān)于vue3+vant4實(shí)現(xiàn)pdf文件上傳與預(yù)覽組件的文章就介紹到這了,更多相關(guān)vue3 vant4 pdf文件上傳與預(yù)覽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
VueJs與ReactJS和AngularJS的異同點(diǎn)
這篇文章主要為大家詳細(xì)介紹了VueJs與ReactJS和AngularJS的異同點(diǎn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
vue+vant-UI框架實(shí)現(xiàn)購物車的復(fù)選框全選和反選功能
這篇文章主要介紹了vue+vant-UI框架實(shí)現(xiàn)購物車的復(fù)選框全選和反選功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11
Vue導(dǎo)出Excel,后端返回的文件流,前端接收后無法打開文件的解決
在使用Vue導(dǎo)出Excel時,如果后端返回文件流,前端接收后無法打開文件,通常是由于響應(yīng)類型設(shè)置不正確導(dǎo)致的,通過設(shè)置axios請求接口時的響應(yīng)類型為`blob`,并使用blob轉(zhuǎn)換文件流,可以解決文件損壞無法打開的問題2025-12-12
uniapp-vue3-彈出選擇組件wo-pop-selector使用示例
wo-pop-selector彈出選擇組件采用uniapp-vue3實(shí)現(xiàn), 支持H5、微信小程序,本文給大家介紹uniapp-vue3-彈出選擇組件wo-pop-selector及使用示例,感興趣的朋友一起看看吧2023-10-10

