vue3+vant4實(shí)現(xiàn)pdf文件上傳與預(yù)覽組件
注意下載的插件的版本"pdfjs-dist": "^2.2.228",
npm i pdfjs-dist@2.2.228
然后封裝一個(gè)pdf的遮罩。因?yàn)閜df文件有多頁(yè),所以我用了swiper輪播的形式展示。因?yàn)橛玫揭苿?dòng)端,手動(dòng)滑動(dòng)頁(yè)面這樣比點(diǎn)下一頁(yè)下一頁(yè)的方便多了。
直接貼代碼了
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加載出錯(cuò)了..."
/>
</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ù),注意這塊開(kāi)始試了很多網(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ù),會(huì)報(bào)錯(cuò)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)聽(tīng)", 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)擊立刻打開(kāi)又關(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')}`,//微信小程序里面打開(kāi)這個(gè)模塊,發(fā)現(xiàn)請(qǐng)求401,報(bào)錯(cuò)信息是登陸訪問(wèn)超時(shí),發(fā)現(xiàn)pdfjs加載pdf時(shí)沒(méi)有攜帶token,于是在加載url時(shí)添加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});
// 畫(huà)布大小,默認(rèn)值是width:300px,height:150px
canvas.height = viewport.height;
canvas.width = viewport.width;
// 畫(huà)布的dom大小, 設(shè)置移動(dòng)端,寬度設(shè)置鋪滿整個(gè)屏幕
const {clientWidth} = document.body;
// 減去2rem使用因?yàn)槲业捻?yè)面左右加了padding
canvas.style.width = `calc(${clientWidth}px - 2rem)`;
// 根據(jù)pdf每頁(yè)的寬高比例設(shè)置canvas的高度
canvas.style.height = `${
clientWidth * (viewport.height / viewport.width)
}px`;
canvas.height = viewport.height;
canvas.width = viewport.width;
page.render({
canvasContext: ctx,
viewport,
});
//隱藏渲染所有的頁(yè)面
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>上傳的頁(yè)面可以參考文末補(bǔ)充內(nèi)容,稍微改動(dòng)一下就可以了。
然后給組件添加一個(gè)點(diǎn)擊預(yù)覽的事件 。并把上面寫(xiě)好的預(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;
}
}遇到的問(wèn)題:
如果報(bào)錯(cuò)
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'getContext')
可能是canvas要找的那個(gè)id在頁(yè)面還沒(méi)有渲染出來(lái)。所以我用的nextTick,還在獲取canvas后面判斷了一下找到了再繼續(xù) ,注意上面棕色加粗的地方。
如果報(bào)錯(cuò)
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';
這樣寫(xiě)的就是錯(cuò)的,改成
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?url';
問(wèn)題:如果你點(diǎn)擊第一次彈窗展示了,但是再點(diǎn)擊就沒(méi)有彈出。
原因是預(yù)覽的組件渲染是監(jiān)聽(tīng)的pdf的url的地址。如果你第一個(gè)打開(kāi)沒(méi)有把組件銷毀。那么再次顯示的時(shí)候沒(méi)有走監(jiān)聽(tīng)。就不會(huì)顯示。所以要在每次關(guān)閉彈窗是組件也銷毀。這就是上面我要在子組件中用@close給組件通知讓他不顯示也就是銷毀子組件的原因。
問(wèn)題:無(wú)意間雙擊了文件導(dǎo)致遮罩馬上顯示又隱藏。頁(yè)面效果就是黑色遮罩閃了一下。
可以使用防抖的方式。延遲關(guān)閉。參考上面紫色的關(guān)閉函數(shù)
知識(shí)補(bǔ)充
vant4+vue3封裝一個(gè)上傳公共組件.有上傳和刪除訪問(wèn)接口的過(guò)程。限制上傳的格式和上傳文件大小

效果圖
我的上傳接口需要參數(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>
//我的展示效果里面有個(gè)文字。所以在下面加了這個(gè).
<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)前頁(yè)面只有一個(gè)圖片的地方。圖片上傳的數(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)聽(tīng) props.fileList 列表默認(rèn)值改變
watch(
() => props.fileList,
(n) => {
_fileList.value = n.map(res=>{
return {
url:res.absoluteUrl||'',//展示的時(shí)候需要url但是接口給我回傳的里面沒(méi)有。所以這里自己拼接一下
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( `上傳文件大小不能超過(guò) ${props.fileSize}M!`);
}, 0);
return imgType && imgSize;
};
const onOversize=()=>{
showFailToast( `上傳文件大小不能超過(guò) ${props.fileSize}M!`);
}
/**
* @description 圖片上傳,請(qǐng)求接口
* @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;//上傳接口全局一樣,可以直接寫(xiě)死
const { data,code } = await api(formData);
if(code==200){
//把接口返回的值給到操作的這個(gè)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 刪除的文件
* */
// 提取過(guò)濾邏輯為獨(dú)立函數(shù),增強(qiáng)可讀性和復(fù)用性
function shouldRemoveItem(item, file) {
// 檢查每個(gè)屬性是否完全匹配,避免邏輯錯(cuò)誤
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)用過(guò)濾邏輯,過(guò)濾掉已經(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 圖片上傳錯(cuò)誤
* */
const uploadError = () => {
showFailToast( "上傳失敗,請(qǐng)您重新上傳!");
};
/**
* @description 文件數(shù)超出
* */
const handleExceed = () => {
showFailToast("當(dāng)前最多只能上傳"+(props.nowLimit||8)+"份,請(qǐng)移除后上傳!");
};
</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)容。不需要再寫(xiě)函數(shù)接收上傳的返回值了。
到此這篇關(guān)于vue3+vant4實(shí)現(xiàn)pdf文件上傳與預(yù)覽組件的文章就介紹到這了,更多相關(guān)vue3 vant4 pdf文件上傳與預(yù)覽內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
webpack dev-server代理websocket問(wèn)題
這篇文章主要介紹了webpack dev-server代理websocket問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
Vue.js組件tree實(shí)現(xiàn)省市多級(jí)聯(lián)動(dòng)
這篇文章主要為大家詳細(xì)介紹了Vue.js組件tree實(shí)現(xiàn)省市多級(jí)聯(lián)動(dòng)的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
vue中v-model、v-bind和v-on三大指令的區(qū)別詳解
v-model和v-bind都是數(shù)據(jù)綁定的方式,下面這篇文章主要給大家介紹了關(guān)于vue中v-model、v-bind和v-on三大指令的區(qū)別,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11
詳解Vue調(diào)用手機(jī)相機(jī)和相冊(cè)以及上傳
這篇文章主要介紹了Vue調(diào)用手機(jī)相機(jī)及上傳,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
Vue 無(wú)限滾動(dòng)加載指令實(shí)現(xiàn)方法
這篇文章主要介紹了Vue 無(wú)限滾動(dòng)加載指令的實(shí)現(xiàn)代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-05-05
Vue前端利用slice()方法實(shí)現(xiàn)分頁(yè)器
分頁(yè)功能是常見(jiàn)的需求之一,本文主要介紹了Vue前端利用slice()方法實(shí)現(xiàn)分頁(yè)器,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
基于vue.js組件實(shí)現(xiàn)分頁(yè)效果
這篇文章主要為大家詳細(xì)介紹了基于vue.js組件實(shí)現(xiàn)分頁(yè)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12

