Source: packages/PreviewShow/index.vue

<template>
  <div :class="['ps-con',model].join(' ')">
    <template v-if="model=='page'">
      <div class="ps-page-con">
        <!-- <div  class="p-title-con">
          <div class="title" :title="winConfig.title"></div>
          <div class="btn-group">
            <a-button v-if="vhas" v-has="`${vhas}`" type="dashed" icon="download" @click="uploads(1)" style="margin: 0 5px">下载无水印</a-button>
            <a-button type="dashed" icon="download" @click="uploads" style="margin: 0 5px" >下 载</a-button >
          </div>
        </div> -->
        <div
          class="p-data-con">
          <component
            v-if="!showCarousel"
            :is="componentNameList[mark + '0']"
            :url="url"
            :value="url"
            :order="getKey(url)"
            :setEvent="setWatermarkFile"
            :isShowWatermark="isShowWatermark"
          ></component>

          <SwiperCon
            ref="previewShowcarousel"
            v-if="showCarousel"
            :onChange="onChange"
          >
            <div
              class="item-con swiper-slide"
              :key="'carousel-item-con-' + index"
              v-for="(item, index) in url"
            >
              <component
                :is="componentNameList[mark + index]"
                :url="item"
                :value="item"
                :setEvent="setWatermarkFile"
                :order="getKey(item) + index"
                :isShowWatermark="isShowWatermark"
              ></component>
            </div>
          </SwiperCon>
        </div>
      </div>
    </template>
    <template v-if="model=='modal'">
      <a-modal
        :dialogClass="madalClass"
        :visible="winConfig.visible"
        :width="winConfig.width"
        :confirm-loading="winConfig.loading"
        :zIndex="10000000"
        @ok="
          () => {
            modalEvent('ok');
          }
        "
        @cancel="
          () => {
            modalEvent('cancel');
          }
        "
        :footer="null"
      >
        <div slot="title" class="p-title-con">
          <div class="title" :title="winConfig.title">{{ winConfig.title }}</div>
          <div class="btn-group">
            <a-button v-if="vhas" v-has="`${vhas}`" type="dashed" icon="download" @click="uploads(1)" style="margin: 0 5px">下载无水印</a-button>
            <a-button type="dashed" icon="download" @click="uploads" style="margin: 0 5px" >下 载</a-button >
            <a-button type="dashed" @click="enlarge" style="margin: 0 5px">{{ enlargeLabel }}</a-button>
          </div>
        </div>
        <div
          class="p-data-con"
          :style="{ height: `calc(${winConfig.height} - 140px)` }"
        >
          <component
            v-if="!showCarousel"
            :is="componentNameList[mark + '0']"
            :url="url"
            :value="url"
            :order="getKey(url)"
            :setEvent="setWatermarkFile"
            :isShowWatermark="isShowWatermark"
          ></component>

          <SwiperCon
            ref="previewShowcarousel"
            v-if="showCarousel"
            :onChange="onChange"
          >
            <div
              class="item-con swiper-slide"
              :key="'carousel-item-con-' + index"
              v-for="(item, index) in url"
            >
              <component
                :is="componentNameList[mark + index]"
                :url="item"
                :value="item"
                :setEvent="setWatermarkFile"
                :order="getKey(item) + index"
                :isShowWatermark="isShowWatermark"
              ></component>
            </div>
          </SwiperCon>
        </div>
      </a-modal>
    </template>
  </div>
</template>

<script>
import { base64Img2Blob } from "./../utils/Watermark.js";
import {addWatermark} from './../utils/addWatermark'
import { getCaptionFileName } from "./../utils/util";
import SwiperCon from "./../SwiperCon/index";
import * as IS from "./../utils/is";
import DownLondBox from './component/downLondBox.vue'
import TextBox from './component/textBox.vue'
import PdfBox from './component/pdfBox.vue'
import IframeBox from './component/iframeBox.vue'
import ImgBox from './component/imgBox.vue'
import odfBox from './component/odfBox.vue'
import UUID from "./../utils/uuid";

const getFileSuffix = (filePath) => {
  const res = { suffix: "", name: "" };
  let tempUrl = filePath
  if(filePath.indexOf('?')>=0){
    tempUrl = tempUrl.split('?')[0]
  }
  var startIndex = tempUrl.lastIndexOf(".");
  const name = tempUrl.match(/[^//]+$/)[0];
  res.name = name;
  if (startIndex != -1) {
    res.suffix = tempUrl.match(/[^.]+$/)[0];
  } // filePath.substring(startIndex + 1, filePath.length).toLowerCase();
  
  return res;
};
const downloadFile = (url,fileName) => {
  fetch(url)
    .then(response => response.blob())
    .then(data => {
       const fileURL = window.URL.createObjectURL(data); // 创建了一个URL,该URL表示了Blob对象的内容
       const link = document.createElement('a'); // 创建一个链接元素并设置其属性
       link.href = fileURL;
       link.download = fileName;
       link.click(); // 模拟点击下载链接
       window.URL.revokeObjectURL(fileURL); // 释放创建的URL对象
     })
     .catch(error => {
      // 处理错误
     });
}

 /**
   * PPreviewShow 附件展示组件 可展示 图片  文档 pdf odf 等
   *  使用方法  <p-PreviewShow  :config="{}" :url="''"  :event="()=>{}"></p-PreviewShow>
   * 
   * @vue-prop {String} [model='modal'] - 当前组件以什么方式展示。默认值是:modal;   modal 表示弹框模式;page 表示页面嵌入模式; 
   * @vue-prop {Number} [step=1] - Step
   * @vue-data {Number} counter - Current counter's value
   * @vue-computed {String} message
   * @vue-event {Number} increment - Emit counter's value after increment
   * @vue-event {Number} decrement - Emit counter's value after decrement
   */
export default {
  name: "PPreviewShow",
  components: {
    imgBox:ImgBox,
    iframeBox:IframeBox,
    pdfBox:PdfBox,
    downLondBox:DownLondBox,
    textBox:TextBox,
    odfBox,
    SwiperCon,
  },
  props: {
    // modal  弹框模式    page 非弹框模式
    model:{type:String,default:()=>{return 'modal'}},
    vhas:{type:String},
    /**
     * 用于设置弹窗的
     * @prop {Object} config - 用于设置弹窗的  {title:'',visible: false, loading: false, width: '50vw',height: '60vh' }
     */
    config: { type: Object,required: true, }, // 用于设置弹窗的  {title:'',visible: false, loading: false, width: '50vw',height: '60vh' }
    url: { type: [String, Array],required: true, }, // 用于显示的文件的地址
    name: { type: [String, Array] }, // 文件名称
    suffix: { type: [String, Array] }, // 文件后缀
    event: { type: Function,required: true, }, // 执行的事件 关闭
    isShowWatermark:{type:Boolean, default:true}, // 是否显示水印 默认:true
    showHelp:{type:Boolean, default: () => { return false }},
    // 下载按钮是否是批量下载  默认:false  表示单个下载;  true 表示批量下载
    isBatchDownload:{type:Boolean, default: () => { return false }},
  },
  data() {
    return {
      isSingleFile: true,
      mark: "item",
      currentIndex: 0,
      componentConfig: {
        componentName: "",
      },
      componentNameList: {},
      // 是否启用多文件预览 false 表示单位件预览  true 表示多文件预览
      showCarousel: false,
      previewUrlList: [],
      madalClass: "",
      isEnlarge: false,
      enlargeLabel: "全 屏",
      winConfig: {
        title: "",
        visible: false,
        loading: false,
        width: "50vw",
        height: "60vh",
      },
      dataUrl: {},
      // 用于下载带有水印的文件
      downLoadWatermarkFile: null,
      fileName: {},
    };
  },

  watch: {
    config: {
      handler(val_) {
        const val = val_
        if (Object.prototype.toString.call(val) === "[object Object]") {
          this.winConfig = Object.assign(
            {},
            JSON.parse(JSON.stringify(this.winConfig)),
            JSON.parse(JSON.stringify(val))
          );
        }
      },
      deep: true,
      immediate: true,
    },
    url: {
      handler(val_) {
        const val = val_
        // console.log('2222222222222', val, this.suffix, this.name)
        if (IS.isEmpty(val)) {
          return false;
        }
        this.dataUrl = {};
        this.componentNameList = {};
        this.currentIndex = 0;
        let tempArr = [];
        if (IS.isArray(val)) {
          this.showCarousel = true;
          this.isSingleFile = false;
          tempArr = [...val];
        }
        if (IS.isString(val)) {
          this.isSingleFile = true; // true
          this.showCarousel = false;
          tempArr = [val];
        }

        tempArr.forEach((item, index) => {
          let tempUrl = item
          if(tempUrl.indexOf('?')>=0){
            tempUrl = tempUrl.split('?')[0]
          }
          this.dataHandling(tempUrl, this.mark + index, index);
        });
        this.$nextTick(() => {
          tempArr.length>1 && this.$refs.previewShowcarousel && this.$refs.previewShowcarousel.goTo && this.$refs.previewShowcarousel.goTo(this.currentIndex);
          this.setWinTitle();
        });
      },
      deep: true,
      immediate: true,
    },
  },
  mounted(){
      if(this.$isShowHelp == true &&this.showHelp === true){
        console.log(`
        组件名称:
            PPreviewShow
        使用方法:
            <p-PreviewShow  :config="{}" :url="''"  :event="()=>{}"></p-PreviewShow>
        props参数说明:
            props: {
              // 此参数必填 用于设置弹窗的  {title:'',visible: false, loading: false, width: '50vw',height: '60vh' }
              config: { type: Object },
              // 此参数必填 用于显示的文件的地址
              url: { type: [String, Array] },
              // 此参数可选 文件名称
              name: { type: [String, Array] },
              // 此参数可选 文件后缀
              suffix: { type: [String, Array] },
              // 此参数必填 执行的事件 关闭
              event: { type: Function },
              // 此参数可选 用于控制无水印下载按钮的权限标识
              vhas:{type:String},
              // 是否显示水印 默认:true  表示显示水印(下载时有水印)  false 表示不显示水印(下载时无水印)
              isShowWatermark:{type:Boolean, default:true},
              // 下载按钮是否是批量下载  默认:false  表示单个下载;  true 表示批量下载
              isBatchDownload:{type:Boolean, default: () => { return false }},
            },
        `);
      }
    },
  methods: {
    onChange(a) {
      // console.log(a)
      this.currentIndex = a;
      this.setWinTitle();
    },
    getKey(str){
      return  UUID('MDUUID',str+'')
    },
    setWinTitle() {
      let currentTitle = "";
      const key = this.mark + this.currentIndex;
      const currentUrl = this.dataUrl[key]
      if(!IS.isNullOrUnDef(currentUrl) && !IS.isEmpty(currentUrl)){
        if (IS.isArray(this.name)) {
          currentTitle =this.name[this.currentIndex] || getCaptionFileName(currentUrl);
        }
        if (IS.isString(this.name)) {
          currentTitle = this.name || getCaptionFileName(currentUrl);
        }
        if (IS.isNullOrUnDef(currentTitle) || IS.isEmpty(currentTitle)) {
          currentTitle = getCaptionFileName(currentUrl);
        }
      }
      // console.log('title = ====>', currentTitle, key, this.name, this.dataUrl)
      this.winConfig.title = currentTitle;
    },
    setWatermarkFile(data) {
      // suffix url
      const res = base64Img2Blob(data, window);
      // console.log('setWatermarkFile====>', res)
      this.downLoadWatermarkFile = res;
    },
    // 放大
    enlarge() {
      // madalClass
      if (this.isEnlarge) {
        // 还原
        this.madalClass = "";
        this.enlargeLabel = "全 屏";
        this.winConfig.width = "50vw";
        this.winConfig.height = "50vh";
      } else {
        // 放大
        this.enlargeLabel = "还 原";
        this.madalClass = "ps-con-modal";
        this.winConfig.width = "100vw";
        this.winConfig.height = "100vh";
      }
      this.isEnlarge = !this.isEnlarge;
    },
    modalEvent(type) {
      // 关闭
      this.winConfig.visible = false;
      this.winConfig.width = "50vw";
      this.winConfig.height = "60vh";
      this.componentConfig.componentName = "";
      this.dataUrl = "";
      this.isEnlarge = false;
      this.madalClass = "";
      this.enlargeLabel = "全 屏";
      this.event && this.event("close");
    },
    dataHandling(val, key, index) {
      this.downLoadWatermarkFile = null;
      const suffix_ = getFileSuffix(val);
      this.fileName[key] = this.name || suffix_.name;
      // console.log('suffix_===>',suffix_,this.suffix);
      let currentSuffix = "";
      let currentTitle = "";
      if(IS.isUnDef(this.suffix) || IS.isEmpty(this.suffix)){
        currentSuffix = suffix_.suffix;
      }
      if (IS.isArray(this.suffix)) {
        currentSuffix = this.suffix[index] || suffix_.suffix;
      }
      if (IS.isString(this.suffix)) {
        currentSuffix = this.suffix || suffix_.suffix;
      }

      // console.log('后缀========》',currentSuffix,this.suffix,index,suffix_.suffix);
      if (IS.isArray(this.name)) {
        currentTitle = this.name[index] || getCaptionFileName(val);
      }
      if (IS.isString(this.name)) {
        currentTitle = this.name || getCaptionFileName(val);
      }

      // this.name || getCaptionFileName(url)
      // console.log('@@@@@@@@@@@@',currentSuffix, val, key, currentTitle);
      this.getComponentInfo(currentSuffix, val, key, currentTitle);
    },
    getComponentInfo(fileType, url, index, title_) {
      // console.log('getComponentInfo====>', fileType, url, index)
      const documentFun = (self) => {
        self.componentNameList[index] = "iframeBox";
        // `https://view.officeapps.live.com/op/view.aspx?src=${url}`
        // self.dataUrl[index] = `https://view.officeapps.live.com/op/view.aspx?src=${url}`;
        self.dataUrl[index] = url;
      };

      const imgFun = (self) => {
        // console.log('@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@2222',this.componentNameList,this.dataUrl);
        self.componentNameList[index] = "imgBox";
        self.dataUrl[index] = url;
      };
      const textFun = (self) => {
        self.componentNameList[index] = "textBox";
        self.dataUrl[index] = url;
        fetch(self.dataUrl[index]).then((res) => {
          res.blob().then((blob) => {
            var reader = new FileReader(); //这是核心,读取操作就是由它完成.
            reader.readAsText(blob, "utf8"); //读取文件的内容,也可以读取文件的URL
            reader.onload = function () {
              console.log("this.result===>", this.result);
              //当读取完成后回调这个函数,然后此时文件的内容存储到了result中,直接操作即可
              document.getElementById("txtBox").innerHTML = this.result;
            };
          });
        });
      };

      const getInfo = {
        xlsx: documentFun,
        xls: documentFun,
        pptx: documentFun,
        ppt: documentFun,
        doc: documentFun,
        docx: documentFun,
        jpg: imgFun,
        png: imgFun,
        jpeg: imgFun,
        pdf: (self) => {
          self.componentNameList[index] = "pdfBox";
          // // this.dataUrl = 'https://zxcloud.oss-cn-hangzhou.aliyuncs.com/user/task/deliverables/1679882880287_%E6%9D%9C%E5%90%AF%E5%AE%97-%E6%84%8F%E5%A4%96%E9%99%A9%E7%94%B5%E5%AD%90%E4%BF%9D%E5%8D%9520221126-20231125-53.04.pdf' // url
          // this.dataUrl = 'https://zxcloud-test.oss-cn-hangzhou.aliyuncs.com/qsb/businessLicense/1679968661043_%E5%B0%8F%E7%BA%A2%E4%B9%A6(%E7%AC%AC%E5%9B%9B%E7%89%88).pdf' // url
          // this.AddWatermark.addWatermark(this.dataUrl).then(res=>{
          //     PDFObject.embed(res.url, "#pdfBox");
          //   })
          self.dataUrl[index] = url;
          // this.$nextTick(() => {
          //   PDFObject.embed(this.dataUrl[index], '#pdfBox')
          // })
          // console.log('@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@3333',index,this.componentNameList,this.dataUrl);
        },
        txt: textFun,
        ofd: (self) => {
          self.dataUrl[index] = url;
          self.componentNameList[index] = "odfBox";
        },
      };
      // if (this.config && !this.config.title && this.config.title != '--') {
      //   this.winConfig.title = this.name || getCaptionFileName(url)
      // }
      let fileType_ = fileType;
      const tempArr = ["xlsx", "xls"];
      if (tempArr.indexOf(fileType_) >= 0) {
        if (this.isSingleFile === true) {
          getInfo[fileType_] && getInfo[fileType_](this);
        } else {
          fileType_ = "";
          // getInfo[''] && getInfo[''](this) 
        }
      } else {
        getInfo[fileType_] && getInfo[fileType_](this);
      }
      this.setWinTitle();
      // console.log('文件类型====》',fileType_,this.isSingleFile, this.componentNameList[index],index,this.componentNameList, getInfo[fileType_],getInfo);
      if (!getInfo[fileType_]) {
        // 下载 downLondBox
        this.componentNameList[index] = "downLondBox";
        this.dataUrl[index] = url;
      }
    },

    getDownFileName(itemIndex, url){
      let fileName = getCaptionFileName(url);
      if(!IS.isNullOrUnDef(this.name)&&!IS.isEmpty(this.name)){
        if(IS.isArray(this.name)){
          fileName = this.name[itemIndex]
        }
        if(IS.isString(this.name)){
          fileName = this.name
        }
      }
      return fileName
    },

    async downItemFile(type,itemIndex,url){
       //从链接上截取文件名
       let fileName = this.getDownFileName(itemIndex,url)
      if(type===1){
        // 无水印下载
        downloadFile(url,fileName)
        return false
      }else{
        // 有水印下载
        if(this.isShowWatermark == false){
          // url = this.dataUrl[key] //this.downLoadWatermarkFile || this.url
          downloadFile(url,fileName)
        }else{
          const res = await addWatermark(url,this,this.$watermarkConfig)
          downloadFile(res.url,fileName)
        }
      }
    },

    //下载
    async uploads(type) {
      const key = this.mark + this.currentIndex;
      let url = this.dataUrl[key]; //this.downLoadWatermarkFile || this.url
      if(this.isBatchDownload == false){
        this.downItemFile(type,this.currentIndex,url)
      }
      if(this.isBatchDownload == true){
        const tempARr = Object.keys(this.dataUrl)
        tempARr.forEach((key_,index)=>{
          const url_ = this.dataUrl[key_];
          this.downItemFile(type,index,url_)
        })
      }
    },
  },
};
</script>

<style lang="less">
.img-box {
  width: auto;
  height: 100%;
}

.p-data-con {
  height: 60vh;
  display: flex;
  justify-content: center;
  align-items: center;
  box-sizing: border-box;
  .carousel-con {
    .slick-slider {
      width: 100%;
      height: 100%;
      .slick-list {
        width: 100%;
        height: 100%;
      }
    }
  }
  .dots-item {
    li button {
      background: #000000;
    }
    li {
      &.slick-active button {
        background: #3205f7;
      }
    }
    .item-con {
      height: 100%;
    }
  }
  .custom-slick-arrow {
    width: 25px;
    height: 25px;
    font-size: 25px;
    // color: #f0f1f5;
    // background-color: rgb(10, 10, 10);
    opacity: 1;
    z-index: 100;
  }
  // .custom-slick-arrow:before {
  //   display: none;
  // }
  // .custom-slick-arrow:hover {
  //   opacity: 1;
  // }
  .slick-track {
    height: 100%;
    .slick-slide div {
      height: 100%;
      display: flex !important;
      justify-content: center;
      align-items: center;
    }
  }
}

.p-title-con {
  width: calc(100% - 50px);
  display: flex;
  justify-content: space-between;
  align-items: center;
  z-index: 100;

  .title {
    flex: 1;
    display: flex;
    overflow: hidden;
    /*文本不会换行*/
    white-space: nowrap;
    /*当文本溢出包含元素时,以省略号表示超出的文本*/
    text-overflow: ellipsis;
    padding: 5px 0;
  }

  .btn-group {
    // width: 200px;
  }
}

.ps-con-modal {
  top: 0px !important;
}

.ps-con{
  &.page{
    width: 100%;
    height: 100%;
  }
  &.modal{}
  .ps-page-con{
    width: 100%;
    height: 100%;
    .p-title-con{
      width: calc(100% - 50px);
      display: flex;
      justify-content: space-between;
      align-items: center;
      z-index: 100;

      .title {
        flex: 1;
        display: flex;
        overflow: hidden;
        /*文本不会换行*/
        white-space: nowrap;
        /*当文本溢出包含元素时,以省略号表示超出的文本*/
        text-overflow: ellipsis;
        padding: 5px 0;
      }

      .btn-group {
        // width: 200px;
      }
    }
    .p-data-con{
      width: 100%;
      height: 100%;
    }
  }
}
</style>