• 手摸手系列之前端Vue实现PDF预览及打印的终极解决方案


    前言

    近期我正在开发一个前后端分离项目,使用了Spring Boot 和 Vue2,借助了国内优秀的框架 jeecg,前端UI库则选择了 ant-design-vue。在项目中,需要实现文件上传功能,同时还要能够在线预览和下载图片和PDF文件,甚至需要在页面上直接打印PDF文件。尽管框架自带了 vue-print-nb-jeecg 组件,但它相对较为简陋,只支持单页打印,无法实现多页打印。经过仔细的权衡和比较后,最终决定采用 vue-pdf print-js 组件来满足需求。

    一、先来展示一下最终效果

    前端上传文件列表:
    在这里插入图片描述

    点击PDF文件后展示预览:

    在这里插入图片描述

    点击打印按钮后效果:

    在这里插入图片描述

    二、实现步骤及代码

    vue-pdf 可以用于在线预览,而 print-js 则提供了更强大的打印功能,支持多种文档类型,包括PDF、HTML、IMAGE和JSON,而且默认情况下是PDF。其实vue-pdf 也可以实现打印功能,但是跟前述的vue-print-nb一样,只能打印页面显示的第一页内容(预览展示没问题)。
    Print.js官网👉点我直达

    1. 在vue中安装vue-pdfPrint.js
    yarn add vue-pdf
    ...
    yarn add print-js
    
    • 1
    • 2
    • 3
    2. 可以全局引入,也可以在需要的文件中引入
     import pdf from 'vue-pdf'
     import printJS from 'print-js'
    
    • 1
    • 2
    3.主要代码
    <a-modal :visible="previewVisibleForAll" :footer="null" @cancel="handleCancelAll" :width="800" :maskClosable="maskClosable" :keyboard="keyboard">
         <img alt="example" style="width: 100%;margin-top:20px" :src="previewFileSrc" v-if="isImage"/>
    
         <div v-if="isPdf" style="overflow-y: auto;overflow-x: hidden;">
           <a-button shape="round" icon="file-pdf" @click="handlePrint(printData)" size="small">打印</a-button>
           <div id="printFrom">
             <pdf ref="pdf" v-for="item in pageTotal"
                  :src="previewFileSrc"
                  :key="item"
                  :page="item"></pdf>
           </div>
         </div>
    
     </a-modal>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    打印按钮执行的方法

    // data参数
    printData: {
      printable: 'printFrom',
      header: '',
      ignore: ['no-print']
    },
    
    // 执行方法
    handlePrint(params) {
      printJS({
        printable: params.printable, // 'printFrom', // 标签元素id
        type: params.type || 'html',
        header: params.header, // '表单',
        targetStyles: ['*'],
        style: '@page {margin:0 10mm};', // 可选-打印时去掉眉页眉尾
        ignoreElements: params.ignore || [], // ['no-print']
        properties: params.properties || null
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    不同组件,如果文件是图片就预览图片,PDF就预览PDF。

    4. 全部代码:
    <template>
      <div :id="containerId" style="position: relative">
    
        <!--  ---------------------------- begin 图片左右换位置 ------------------------------------- -->
        <div class="movety-container" :style="{top:top+'px',left:left+'px',display:moveDisplay}" style="padding:0 8px;position: absolute;z-index: 91;height: 32px;width: 104px;text-align: center;">
          <div :id="containerId+'-mover'" :class="showMoverTask?'uploadty-mover-mask':'movety-opt'" style="margin-top: 12px">
            <a @click="moveLast" style="margin: 0 5px;"><a-icon type="arrow-left" style="color: #fff;font-size: 16px"/></a>
            <a @click="moveNext" style="margin: 0 5px;"><a-icon type="arrow-right" style="color: #fff;font-size: 16px"/></a>
          </div>
        </div>
        <!--  ---------------------------- end 图片左右换位置 ------------------------------------- -->
    
        <a-upload
          name="file"
          :multiple="multiple"
          :action="uploadAction"
          :headers="headers"
          :data="{'biz':bizPath}"
          :fileList="fileList"
          :beforeUpload="doBeforeUpload"
          @change="handleChange"
          :disabled="disabled"
          :returnUrl="returnUrl"
          :listType="complistType"
          @preview="handlePreview1"
          :showUploadList="{
                    showRemoveIcon: true,
                    showDownloadIcon: true
                  }"
          :class="{'uploadty-disabled':disabled}">
          <template>
            <div v-if="isImageComp">
              <a-icon type="plus" />
              <div class="ant-upload-text">{{ text }}</div>
            </div>
            <a-button v-else-if="buttonVisible">
             <a-icon type="upload" />{{ text }}
            </a-button>
          </template>
        </a-upload>
        <a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
          <img alt="example" style="width: 100%" :src="previewImage" />
        </a-modal>
        <a-modal :visible="previewVisibleForAll" :footer="null" @cancel="handleCancelAll" :width="800" :maskClosable="maskClosable" :keyboard="keyboard">
          <img alt="example" style="width: 100%;margin-top:20px" :src="previewFileSrc" v-if="isImage"/>
    
          <div v-if="isPdf" style="overflow-y: auto;overflow-x: hidden;">
            <a-button shape="round" icon="file-pdf" @click="handlePrint(printData)" size="small">打印</a-button>
            <div id="printFrom">
              <pdf ref="pdf" v-for="item in pageTotal"
                   :src="previewFileSrc"
                   :key="item"
                   :page="item"></pdf>
            </div>
          </div>
    
        </a-modal>
      </div>
    </template>
    
    <script>
    
      import Vue from 'vue'
      import { ACCESS_TOKEN } from "@/store/mutation-types"
      import { getFileAccessHttpUrl } from '@/api/manage';
      import pdf from 'vue-pdf'
      import printJS from 'print-js'
      const FILE_TYPE_ALL = "all"
      const FILE_TYPE_IMG = "image"
      const FILE_TYPE_TXT = "file"
      const uidGenerator=()=>{
        return '-'+parseInt(Math.random()*10000+1,10);
      }
      const getFileName=(path)=>{
        if(path.lastIndexOf("\\")>=0){
          let reg=new RegExp("\\\\","g");
          path = path.replace(reg,"/");
        }
        return path.substring(path.lastIndexOf("/")+1);
      }
      export default {
        name: 'JUpload',
        components: { pdf },
        data(){
          return {
            printData: {
              printable: 'printFrom',
              header: '',
              ignore: ['no-print']
            },
            uploadAction:window._CONFIG['domianURL']+"/sys/common/upload",
            headers:{},
            fileList: [],
            newFileList: [],
            uploadGoOn:true,
            previewVisible: false,
            //---------------------------- begin 图片左右换位置 -------------------------------------
            previewImage: '',
            containerId:'',
            top:'',
            left:'',
            moveDisplay:'none',
            showMoverTask:false,
            moverHold:false,
            currentImg:'',
            //---------------------------- end 图片左右换位置 -------------------------------------
            previewVisibleForAll:false,
            pageTotal: null,
            previewFileSrc:'',
            isImage:false,
            isExcel:false,
            isPdf:false,
          }
        },
        props:{
          text:{
            type:String,
            required:false,
            default:"点击上传"
          },
          fileType:{
            type:String,
            required:false,
            default:FILE_TYPE_ALL
          },
          /*这个属性用于控制文件上传的业务路径*/
          bizPath:{
            type:String,
            required:false,
            default:"temp"
          },
          value:{
            type:[String,Array],
            required:false
          },
          // update-begin- --- author:wangshuai ------ date:20190929 ---- for:Jupload组件增加是否能够点击
          disabled:{
            type:Boolean,
            required:false,
            default: false
          },
          // update-end- --- author:wangshuai ------ date:20190929 ---- for:Jupload组件增加是否能够点击
          //此属性被废弃了
          triggerChange:{
            type: Boolean,
            required: false,
            default: false
          },
          /**
           * update -- author:lvdandan -- date:20190219 -- for:Jupload组件增加是否返回url,
           * true:仅返回url
           * false:返回fileName filePath fileSize
           */
          returnUrl:{
            type:Boolean,
            required:false,
            default: true
          },
          number:{
            type:Number,
            required:false,
            default: 0
          },
          buttonVisible:{
            type:Boolean,
            required:false,
            default: true
          },
          multiple: {
            type: Boolean,
            default: true
          },
          beforeUpload: {
            type: Function
          },
          maskClosable: {
            type: Boolean,
            default:true,
          },
          keyboard: {
            type: Boolean,
            default:true,
          },
        },
        watch:{
          value:{
            immediate: true,
            handler() {
              let val = this.value
              if (val instanceof Array) {
                if(this.returnUrl){
                  this.initFileList(val.join(','))
                }else{
                  this.initFileListArr(val);
                }
              } else {
                this.initFileList(val)
              }
            }
          }
        },
        computed:{
          isImageComp(){
            return this.fileType === FILE_TYPE_IMG
          },
          complistType(){
            return this.fileType === FILE_TYPE_IMG?'picture-card':'text'
          }
        },
        created(){
          const token = Vue.ls.get(ACCESS_TOKEN);
          //---------------------------- begin 图片左右换位置 -------------------------------------
          this.headers = {"X-Access-Token":token};
          this.containerId = 'container-ty-'+new Date().getTime();
          //---------------------------- end 图片左右换位置 -------------------------------------
        },
    
        methods:{
          handlePrint(params) {
            printJS({
              printable: params.printable, // 'printFrom', // 标签元素id
              type: params.type || 'html',
              header: params.header, // '表单',
              targetStyles: ['*'],
              style: '@page {margin:0 10mm};', // 可选-打印时去掉眉页眉尾
              ignoreElements: params.ignore || [], // ['no-print']
              properties: params.properties || null
            })
          },
          printPdf() {
            this.$refs.pdf.print()
            // window.print()
          },
          initFileListArr(val){
            console.log(val)
            if(!val || val.length==0){
              this.fileList = [];
              return;
            }
            let fileList = [];
            for(var a=0;a<val.length;a++){
              let url = getFileAccessHttpUrl(val[a].filePath);
              fileList.push({
                uid:uidGenerator(),
                name:val[a].fileName,
                status: 'done',
                url: url,
                response:{
                  status:"history",
                  message:val[a].filePath
                }
              })
            }
            this.fileList = fileList
            console.log(this.fileList)
          },
          initFileList(paths){
            if(!paths || paths.length==0){
              //return [];
              // update-begin- --- author:os_chengtgen ------ date:20190729 ---- for:issues:326,Jupload组件初始化bug
              this.fileList = [];
              return;
              // update-end- --- author:os_chengtgen ------ date:20190729 ---- for:issues:326,Jupload组件初始化bug
            }
            let fileList = [];
            let arr = paths.split(",")
            for(var a=0;a<arr.length;a++){
              let url = getFileAccessHttpUrl(arr[a]);
              fileList.push({
                uid:uidGenerator(),
                name:getFileName(arr[a]),
                status: 'done',
                url: url,
                response:{
                  status:"history",
                  message:arr[a]
                }
              })
            }
            this.fileList = fileList
          },
          handlePathChange(){
            let uploadFiles = this.fileList
            let path = ''
            if(!uploadFiles || uploadFiles.length==0){
              path = ''
            }
            let arr = [];
    
            for(var a=0;a<uploadFiles.length;a++){
              // update-begin-author:lvdandan date:20200603 for:【TESTA-514】【开源issue】多个文件同时上传时,控制台报错
              if(uploadFiles[a].status === 'done' ) {
                arr.push(uploadFiles[a].response.message)
              }else{
                return;
              }
              // update-end-author:lvdandan date:20200603 for:【TESTA-514】【开源issue】多个文件同时上传时,控制台报错
            }
            if(arr.length>0){
              path = arr.join(",")
            }
            this.$emit('change', path);
          },
          doBeforeUpload(file){
            this.uploadGoOn=true
            var fileType = file.type;
            if(this.fileType===FILE_TYPE_IMG){
              if(fileType.indexOf('image')<0){
                this.$message.warning('请上传图片');
                this.uploadGoOn=false
                return false;
              }
            }
            // 文件大小限定在600K以下
            const isLt2M = file.size / 1024 / 1024 < 10;
            if (!isLt2M){
                this.$message.warning('请确保上传的文件小于10MB!');
              this.fileList = []
                this.uploadGoOn=false
                return false;
            }
            // 扩展 beforeUpload 验证
            if (typeof this.beforeUpload === 'function') {
              return this.beforeUpload(file)
            }
            return true
          },
          handleChange(info) {
            console.log("--文件列表改变--")
            if(!info.file.status && this.uploadGoOn === false){
              info.fileList.pop();
            }
            let fileList = info.fileList
            if(info.file.status==='done'){
              if(this.number>0){
                fileList = fileList.slice(-this.number);
              }
              if(info.file.response.success){
                fileList = fileList.map((file) => {
                  if (file.response) {
                    let reUrl = file.response.message;
                    file.url = getFileAccessHttpUrl(reUrl);
                  }
                  return file;
                });
              }
              //this.$message.success(`${info.file.name} 上传成功!`);
            }else if (info.file.status === 'error') {
              this.$message.error(`${info.file.name} 上传失败.`);
            }else if(info.file.status === 'removed'){
              this.handleDelete(info.file)
            }
            this.fileList = fileList
            if(info.file.status==='done' || info.file.status === 'removed'){
              //returnUrl为true时仅返回文件路径
              if(this.returnUrl){
                this.handlePathChange()
              }else{
                //returnUrl为false时返回文件名称、文件路径及文件大小
                this.newFileList = [];
                for(var a=0;a<fileList.length;a++){
                  // update-begin-author:lvdandan date:20200603 for:【TESTA-514】【开源issue】多个文件同时上传时,控制台报错
                  if(fileList[a].status === 'done' ) {
                    var fileJson = {
                      fileName:fileList[a].name,
                      filePath:fileList[a].response.message,
                      fileSize:fileList[a].size
                    };
                    this.newFileList.push(fileJson);
                  }else{
                    return;
                  }
                  // update-end-author:lvdandan date:20200603 for:【TESTA-514】【开源issue】多个文件同时上传时,控制台报错
                }
                this.$emit('change', this.newFileList);
              }
            }
          },
          handleDelete(file){
            //如有需要新增 删除逻辑
            console.log(file)
          },
          // handlePreview(file){
          //   console.log('file')
          //   console.log(file)
          //   if(this.fileType === FILE_TYPE_IMG){
          //     this.previewImage = file.url || file.thumbUrl;
          //     this.previewVisible = true;
          //   }else{
          //     if(file.name.endsWith('pdf') || file.name.endsWith('PDF')) {
          //       let viewPath = window._CONFIG['domianURL'].replace('9999', '15550').replace('/jeecg-boot', '') + '/' + (file.url.replace(window._CONFIG['staticDomainURL'] + "/", ''))
          //       console.log(viewPath)
          //       window.open(viewPath,"_blank")
          //     this.isPdf = true
          //       this.previewFileSrc = file.url
          //     }
          //     // else {//TODO:重新打开页面
          //     //   location.href=file.url
          //     // }
          //   }
          // },
          // 获取pdf总页数
          getTotal() {
            // 多页pdf的src中不能直接使用后端获取的pdf地址 否则会按页数请求多次数据
            // 需要使用下述方法的返回值作为url
            this.previewFileSrc = pdf.createLoadingTask(this.previewFileSrc)
            // 获取页码
            this.previewFileSrc.promise.then(pdf => this.pageTotal = pdf.numPages).catch(error => {
            })
          },
          handlePreview1(file){
            if(this.fileType === FILE_TYPE_IMG){
              this.previewImage = file.url || file.thumbUrl;
              this.previewVisible = true;
            }else{
              // 判断当前文件类型
              this.previewFileSrc = file.url || file.thumbUrl; // "http://localhost:9999/sys/common/static/orderPaymentInfo/网约区域复习题_1694585302732.pdf"
              let fileTypeLocal = this.matchFileType(file.name);
              this.isImage = false;
              this.isPdf = false;
              if(fileTypeLocal == 'image') {
                this.previewVisibleForAll = true;
                this.isImage = true;
              } else if(fileTypeLocal == 'pdf') {
                this.previewVisibleForAll = true;
                this.isPdf = true;
                this.getTotal()
              } else {
                location.href=file.url
              }
            }
          },
          matchFileType(fileName) {
            // 后缀获取
            let suffix = '';
            // 获取类型结果
            let result = '';
            if (!fileName) return false;
            try {
              // 截取文件后缀
              suffix = fileName.substr(fileName.lastIndexOf('.') + 1, fileName.length)
              // 文件后缀转小写,方便匹配
              suffix = suffix.toLowerCase()
            } catch (err) {
              suffix = '';
            }
            // fileName无后缀返回 false
            if (!suffix) {
              result = false;
              return result;
            }
    
            let fileTypeList = [
              // 图片类型
              {'typeName': 'image', 'types': ['png', 'jpg', 'jpeg', 'bmp', 'gif']},
              // 文本类型
              {'typeName': 'txt', 'types': ['txt']},
              // excel类型
              {'typeName': 'excel', 'types': ['xls', 'xlsx']},
              {'typeName': 'word', 'types': ['doc', 'docx']},
              {'typeName': 'pdf', 'types': ['pdf']},
              {'typeName': 'ppt', 'types': ['ppt']},
              // 视频类型
              {'typeName': 'video', 'types': ['mp4', 'm2v', 'mkv']},
              // 音频
              {'typeName': 'radio', 'types': ['mp3', 'wav', 'wmv']}
            ]
            // let fileTypeList = ['image', 'txt', 'excel', 'word', 'pdf', 'video', 'radio']
            for (let i = 0; i < fileTypeList.length; i++) {
              const fileTypeItem = fileTypeList[i]
              const typeName = fileTypeItem.typeName
              const types = fileTypeItem.types
              console.log(fileTypeItem);
              result = types.some(function (item) {
                return item === suffix;
              });
              if (result) {
                return typeName
              }
            }
            return 'other'
          },
          handleCancel(){
            this.previewVisible = false;
          },
          handleCancelAll(){
            this.previewVisibleForAll = false;
            this.isImage = false;
            this.isPdf = false;
          },
          //---------------------------- begin 图片左右换位置 -------------------------------------
          moveLast(){
            //console.log(ev)
            //console.log(this.fileList)
            //console.log(this.currentImg)
            let index = this.getIndexByUrl();
            if(index==0){
              this.$message.warn('未知的操作')
            }else{
              let curr = this.fileList[index].url;
              let last = this.fileList[index-1].url;
              let arr =[]
              for(let i=0;i<this.fileList.length;i++){
                if(i==index-1){
                  arr.push(curr)
                }else if(i==index){
                  arr.push(last)
                }else{
                  arr.push(this.fileList[i].url)
                }
              }
              this.currentImg = last
              this.$emit('change',arr.join(','))
            }
          },
          moveNext(){
            let index = this.getIndexByUrl();
            if(index==this.fileList.length-1){
              this.$message.warn('已到最后~')
            }else{
              let curr = this.fileList[index].url;
              let next = this.fileList[index+1].url;
              let arr =[]
              for(let i=0;i<this.fileList.length;i++){
                if(i==index+1){
                  arr.push(curr)
                }else if(i==index){
                  arr.push(next)
                }else{
                  arr.push(this.fileList[i].url)
                }
              }
              this.currentImg = next
              this.$emit('change',arr.join(','))
            }
          },
          getIndexByUrl(){
            for(let i=0;i<this.fileList.length;i++){
              if(this.fileList[i].url === this.currentImg || encodeURI(this.fileList[i].url) === this.currentImg){
                return i;
              }
            }
            return -1;
          }
        },
        mounted(){
          const moverObj = document.getElementById(this.containerId+'-mover');
          if(moverObj){
            moverObj.addEventListener('mouseover',()=>{
              this.moverHold = true
              this.moveDisplay = 'block';
            });
            moverObj.addEventListener('mouseout',()=>{
              this.moverHold = false
              this.moveDisplay = 'none';
            });
          }
        
          let picList = document.getElementById(this.containerId)?document.getElementById(this.containerId).getElementsByClassName('ant-upload-list-picture-card'):[];
          if(picList && picList.length>0){
            picList[0].addEventListener('mouseover',(ev)=>{
              ev = ev || window.event;
              let target = ev.target || ev.srcElement;
              if('ant-upload-list-item-info' == target.className){
                this.showMoverTask=false
                let item = target.parentElement
                this.left = item.offsetLeft
                this.top=item.offsetTop+item.offsetHeight-50;
                this.moveDisplay = 'block';
                this.currentImg = target.getElementsByTagName('img')[0].src
              }
    
            });
    
            picList[0].addEventListener('mouseout',(ev)=>{
              ev = ev || window.event;
              let target = ev.target || ev.srcElement;
              //console.log('移除',target)
              if('ant-upload-list-item-info' == target.className){
                this.showMoverTask=true
                setTimeout(()=>{
                  if(this.moverHold === false)
                    this.moveDisplay = 'none';
                },100)
              }
              if('ant-upload-list-item ant-upload-list-item-done' == target.className || 'ant-upload-list ant-upload-list-picture-card'== target.className){
                this.moveDisplay = 'none';
              }
            })
            //---------------------------- end 图片左右换位置 -------------------------------------
          }
        },
        model: {
          prop: 'value',
          event: 'change'
        }
      }
    </script>
    
    <style lang="less">
    .uploadty-disabled{
      .ant-upload-list-item {
        .anticon-close{
          display: none;
        }
        .anticon-delete{
          display: none;
        }
      }
    }
      //---------------------------- begin 图片左右换位置 -------------------------------------
      .uploadty-mover-mask{
        background-color: rgba(0, 0, 0, 0.5);
        opacity: .8;
        color: #fff;
        height: 28px;
        line-height: 28px;
      }
      //---------------------------- end 图片左右换位置 -------------------------------------
    </style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414
    • 415
    • 416
    • 417
    • 418
    • 419
    • 420
    • 421
    • 422
    • 423
    • 424
    • 425
    • 426
    • 427
    • 428
    • 429
    • 430
    • 431
    • 432
    • 433
    • 434
    • 435
    • 436
    • 437
    • 438
    • 439
    • 440
    • 441
    • 442
    • 443
    • 444
    • 445
    • 446
    • 447
    • 448
    • 449
    • 450
    • 451
    • 452
    • 453
    • 454
    • 455
    • 456
    • 457
    • 458
    • 459
    • 460
    • 461
    • 462
    • 463
    • 464
    • 465
    • 466
    • 467
    • 468
    • 469
    • 470
    • 471
    • 472
    • 473
    • 474
    • 475
    • 476
    • 477
    • 478
    • 479
    • 480
    • 481
    • 482
    • 483
    • 484
    • 485
    • 486
    • 487
    • 488
    • 489
    • 490
    • 491
    • 492
    • 493
    • 494
    • 495
    • 496
    • 497
    • 498
    • 499
    • 500
    • 501
    • 502
    • 503
    • 504
    • 505
    • 506
    • 507
    • 508
    • 509
    • 510
    • 511
    • 512
    • 513
    • 514
    • 515
    • 516
    • 517
    • 518
    • 519
    • 520
    • 521
    • 522
    • 523
    • 524
    • 525
    • 526
    • 527
    • 528
    • 529
    • 530
    • 531
    • 532
    • 533
    • 534
    • 535
    • 536
    • 537
    • 538
    • 539
    • 540
    • 541
    • 542
    • 543
    • 544
    • 545
    • 546
    • 547
    • 548
    • 549
    • 550
    • 551
    • 552
    • 553
    • 554
    • 555
    • 556
    • 557
    • 558
    • 559
    • 560
    • 561
    • 562
    • 563
    • 564
    • 565
    • 566
    • 567
    • 568
    • 569
    • 570
    • 571
    • 572
    • 573
    • 574
    • 575
    • 576
    • 577
    • 578
    • 579
    • 580
    • 581
    • 582
    • 583
    • 584
    • 585
    • 586
    • 587
    • 588
    • 589
    • 590
    • 591
    • 592
    • 593
    • 594
    • 595
    • 596
    • 597
    • 598
    • 599
    • 600
    • 601
    • 602
    • 603
    • 604
    • 605
    • 606
    • 607
    • 608
    • 609
    • 610
    • 611
    • 612
    • 613
    • 614
    • 615
    • 616
    • 617
    • 618
    • 619
    • 620
    总结

    除了以上两个组件库1+1的方式,还有百度前端大神开发的vue-office组件库,而且优点也很明显:

    • 使用简单,对新手友好,只传递一个文件地址,就可实现预览。
    • 提供多种文件的一站式预览解决方案,解决常见的docx、excel、pdf三种文件的预览。
    • 预览效果也好,不只是对内容预览,也要支持样式。

    预览的效果确实超级棒,可惜的是不支持打印功能,不能满足需求,可惜了。
    有需要的可以去看vue-office的演示效果:
    Vue框架演示效果
    非Vue框架文件预览

  • 相关阅读:
    Bored Ape Yacht Club 生态系统指南
    linux云服务器病毒处理
    Docker认识即安装
    pytorch常用知识记录
    vue 项目中,后端返回文件流,导出excel
    vue3.0运行npm run dev 报错Cannot find module node:url
    【全民编程】《软件编程-讲课视频》【零基础入门到实战应用】
    计组笔记(1)——校验码、原补码乘除计算、浮点数计算
    【低代码】ivx详解之系统架构
    SAPRouter Certificate即将过期更新证书
  • 原文地址:https://blog.csdn.net/qq_26030541/article/details/132859554