vue3+vant4實現(xiàn)pdf文件上傳與預(yù)覽組件
注意下載的插件的版本"pdfjs-dist": "^2.2.228",
npm i pdfjs-dist@2.2.228
然后封裝一個pdf的遮罩。因為pdf文件有多頁,所以我用了swiper輪播的形式展示。因為用到移動端,手動滑動頁面這樣比點下一頁下一頁的方便多了。
直接貼代碼了
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ù),會報錯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ù)的實現(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í)行一次,避免多次點擊立刻打開又關(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,報錯信息是登陸訪問超時,發(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}); // 畫布大小,默認值是width:300px,height:150px canvas.height = viewport.height; canvas.width = viewport.width; // 畫布的dom大小, 設(shè)置移動端,寬度設(shè)置鋪滿整個屏幕 const {clientWidth} = document.body; // 減去2rem使用因為我的頁面左右加了padding 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>
上傳的頁面可以參考文末補充內(nèi)容,稍微改動一下就可以了。
然后給組件添加一個點擊預(yù)覽的事件 。并把上面寫好的預(yù)覽組件引入
import PdfPreview from "@/components/PdfPreview/index.vue"; // 點擊預(yù)覽文件 const showPreview=(file)=>{ if(file.absoluteUrl.endsWith('.pdf')){ pdfUrl.value=file.absoluteUrl; preview.value=true; } }
遇到的問題:
如果報錯
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'getContext')
可能是canvas要找的那個id在頁面還沒有渲染出來。所以我用的nextTick,還在獲取canvas后面判斷了一下找到了再繼續(xù) ,注意上面棕色加粗的地方。
如果報錯
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';
問題:如果你點擊第一次彈窗展示了,但是再點擊就沒有彈出。
原因是預(yù)覽的組件渲染是監(jiān)聽的pdf的url的地址。如果你第一個打開沒有把組件銷毀。那么再次顯示的時候沒有走監(jiān)聽。就不會顯示。所以要在每次關(guān)閉彈窗是組件也銷毀。這就是上面我要在子組件中用@close給組件通知讓他不顯示也就是銷毀子組件的原因。
問題:無意間雙擊了文件導致遮罩馬上顯示又隱藏。頁面效果就是黑色遮罩閃了一下。
可以使用防抖的方式。延遲關(guān)閉。參考上面紫色的關(guān)閉函數(shù)
知識補充
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; // 是否禁用上傳組件 ==> 非必傳(默認為 false) limit?: number; // 最大圖片上傳數(shù) ==> 非必傳(默認為 5張) nowLimit?:number;//當前頁面只有一個圖片的地方。圖片上傳的數(shù)量限制 fileSize?: number; // 圖片大小限制 ==> 非必傳(默認為 5M) fileType?:string; // 類型限制 ==> 非必傳(默認為 "image/jpeg", "image/png", "image/gif","application/pdf") height?: string; // 組件高度 ==> 非必傳(默認為 60px) width?: string; // 組件寬度 ==> 非必傳(默認為 60px) borderRadius?: string; // 組件邊框圓角 ==> 非必傳(默認為 8px) uploadParams?: any; //上傳帶的參數(shù)==>必填 tips?:string; deletable?:boolean; reupload?:boolean;//是否可重復上傳==>非必填默認false。不可重復上傳。點擊圖片是預(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 列表默認值改變 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 所有配置項 * */ const handleHttpUpload = async (options: any) => { // console.log("handleHttpUpload", options.file) //二進制內(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,相當于更新了_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 刪除的文件 * */ // 提取過濾邏輯為獨立函數(shù),增強可讀性和復用性 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("當前最多只能上傳"+(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實現(xiàn)pdf文件上傳與預(yù)覽組件的文章就介紹到這了,更多相關(guān)vue3 vant4 pdf文件上傳與預(yù)覽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
webpack dev-server代理websocket問題
這篇文章主要介紹了webpack dev-server代理websocket問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08Vue.js組件tree實現(xiàn)省市多級聯(lián)動
這篇文章主要為大家詳細介紹了Vue.js組件tree實現(xiàn)省市多級聯(lián)動的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12vue中v-model、v-bind和v-on三大指令的區(qū)別詳解
v-model和v-bind都是數(shù)據(jù)綁定的方式,下面這篇文章主要給大家介紹了關(guān)于vue中v-model、v-bind和v-on三大指令的區(qū)別,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-11-11