vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue3 vant4 pdf文件上传与预览

vue3+vant4实现pdf文件上传与预览组件

作者:肖肖肖丽珠

这篇文章主要介绍了vue3如何结合vant4实现简单的pdf文件上传与预览组件,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

注意下载的插件的版本"pdfjs-dist": "^2.2.228",

npm i  pdfjs-dist@2.2.228

然后封装一个pdf的遮罩。因为pdf文件有多页,所以我用了swiper轮播的形式展示。因为用到移动端,手动滑动页面这样比点下一页下一页的方便多了。

直接贴代码了

PdfPreview/index.vue

<!--预览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预览插件相关的参数,注意这块开始试了很多网上方法都不好用
import * as pdfjs from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?url';
// 设置 worker 路径
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;


const show = ref(true);
// html部分涉及的参数
const loadError = ref(false);
const detail = ref({});
let pdfDoc = null; // 一定不能使用响应式的数据,会报错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("监听", newVal, props.pdfUrl)
  showLoadingToast('加载中');
  nextTick(() => {
    loadingPdf(props.pdfUrl);
  })

}, {immediate: true,deep:true})
// 防抖 debounce 函数的实现正确。
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);
    };
}
// 使用防抖函数,300ms内只执行一次,避免多次点击立刻打开又关闭的情况
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')}`,//微信小程序里面打开这个模块,发现请求401,报错信息是登陆访问超时,发现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大小, 设置移动端,宽度设置铺满整个屏幕
    const {clientWidth} = document.body;
    // 减去2rem使用因为我的页面左右加了padding
    canvas.style.width = `calc(${clientWidth}px - 2rem)`;
    // 根据pdf每页的宽高比例设置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>

上传的页面可以参考文末补充内容,稍微改动一下就可以了。

然后给组件添加一个点击预览的事件 。并把上面写好的预览组件引入

import PdfPreview from "@/components/PdfPreview/index.vue";
// 点击预览文件
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后面判断了一下找到了再继续 ,注意上面棕色加粗的地方。

如果报错

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';

问题:如果你点击第一次弹窗展示了,但是再点击就没有弹出。

原因是预览的组件渲染是监听的pdf的url的地址。如果你第一个打开没有把组件销毁。那么再次显示的时候没有走监听。就不会显示。所以要在每次关闭弹窗是组件也销毁。这就是上面我要在子组件中用@close给组件通知让他不显示也就是销毁子组件的原因。

问题:无意间双击了文件导致遮罩马上显示又隐藏。页面效果就是黑色遮罩闪了一下。

可以使用防抖的方式。延迟关闭。参考上面紫色的关闭函数

知识补充

vant4+vue3封装一个上传公共组件.有上传和删除访问接口的过程。限制上传的格式和上传文件大小

效果图

我的上传接口需要参数和返回的参数

<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';

//上传的内容参数
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; // 最大图片上传数 ==> 非必传(默认为 5张)
  nowLimit?:number;//当前页面只有一个图片的地方。图片上传的数量限制
  fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
  fileType?:string; // 类型限制 ==> 非必传(默认为 "image/jpeg", "image/png", "image/gif","application/pdf")
  height?: string; // 组件高度 ==> 非必传(默认为 60px)
  width?: string; // 组件宽度 ==> 非必传(默认为 60px)
  borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
  uploadParams?: any; //上传带的参数==>必填
  tips?:string;
  deletable?:boolean;
  reupload?:boolean;//是否可重复上传==>非必填默认false。不可重复上传。点击图片是预览效果
}

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(); //上传的文件

// 监听 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)
//二进制内容上传参数
  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 删除的文件
 * */
// 提取过滤逻辑为独立函数,增强可读性和复用性
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)
  // 应用过滤逻辑,过滤掉已经删除的内容
  _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 文件数超出
 * */
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="上传内容">
  </Upload>
</template>

​​​​​​​<script lang="ts" setup>
import Upload from "@/components/Upload/index.vue";
const img3=ref([])
//定义要传递的参数
const serviceOtherParams = reactive({
  sysCode: "aged",
  businessCode: "serviceOrderRecord",
  businessSubCode: "serviceOther",
});
</script>

img3就是最后上传的内容。不需要再写函数接收上传的返回值了。

到此这篇关于vue3+vant4实现pdf文件上传与预览组件的文章就介绍到这了,更多相关vue3 vant4 pdf文件上传与预览内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文