基于vue+ts 的上传组件封装(上传+预览<包括图片+PDF>) 功能超全hh

一、依赖包

  • vue-pdf "^4.0.12"
  • v-viewer "^1.5.1"
  • jquery "^3.4.1"
  • element-ui "^2.13.0"

二、初衷、思路、想法

绝大数后台管理项目和少数C端项目都离不开文件的上传及预览,所以我就做了一个这个自认为适用于绝大多数项目的组件~
用户上传图片orPDF,预览上传的文件无论图片orPDF,由于在现在的公司做的主要项目针对的是后台管理,且有的页面上传的文件较多,有的文件必传、有的文件需要显示示例图、有的文件需要对文件类型及文件大小做限制、有的场景希望上传的区域较大或较小、直接拖拽文件到某一特定区域上传、在文件较多的页面希望用户“直接”定位到有哪一个或哪一些文件上传的校验没通过
对于开发者,希望在用户上传特定的一些文件后立即触发回调事件对用户上传的文件及时做处理。

三、组件预览图

  • 总揽


  • pdf预览


  • 图片预览


  • gif


    QQ20200826-174338-HD.gif

四、使用

<uploadFileNew
      :fileImgList="fileNameList"
      :fileSizeByte="10240"
      ref="uploadFiles"
      type="orange"
      size="mini"
    />

fileNameList: Array<object> = [
    {
      id: "idCardFront",
      fileName: "身份证正面",
      required: true, // 必上传
      fileSizeByte: 15360, // 自定义当前文件最大15兆
      fileType: ["pdf"],
      callback: (file: any) => {
        console.log("回调成功", file);
      },
    },
    {
      id: "idCardBack",
      fileName: "身份证反面",
    },
    {
      id: "vin",
      fileName: "vim码",
    },
    {
      id: "businessLicense",
      fileName: "营业执照",
    },
    {
      id: "engine",
      fileName: "车辆发动机舱全景",
      initImg: require("@/assets/img/11592459733_.pic.jpg"),
    },
  ];

// 获取上传的所有文件
(this as any).$refs.filePdf.getFiles("object");

五、源码

基本上全都写了注释

  • 文件上传组件
<template>
  <el-row class="clearfix upload-files">
    <el-col
      :xs="24"
      :sm="12"
      :md="12"
      :lg="6"
      :xl="6"
      v-for="(item, index) in fileImgListDrawing"
      tag="div"
      :key="index"
      :id="item.id"
      class="upload-show-box"
      :class="{'upload-show-box-mini':size=='mini'}"
    >
      <!-- 目标文件名称 -->
      <span class="file-info-top">{{item.fileName}}</span>
      <main class="file-container" ref="fileBox" :data-index="index">
        <!-- 文件展示区域 -->
        <div
          class="file-image-area"
          :class="{'file-image-area-orange':type=='orange','no-border':!item.border}"
          @click.stop="uploadImg(index)"
        >
          <!-- 初始化展示 -->
          <div class="file-image-init-display" v-show="!item.data&&!item.pdfData">
            <img class="initial-img-url" :src="initialImgUrl" />
            <!-- 蒙板示例图片 -->
            <img class="mask-sample-img" :src="item.initImg" v-if="item.initImg" />
            <p class="initial-title">
              <span>将图片拖放到这里&nbsp或&nbsp</span>
              <span class="underline">点击选择图片</span>
            </p>
          </div>
          <!-- 图片展示 -->
          <img class="file-img" v-show="item.data" :src="item.data" alt />
          <!-- pdf展示 -->
          <pdf v-show="item.pdfData" :src="item.pdfData" :page="1" />
          <input type="file" id="upLoad" ref="fileImage" @change.stop="uploadFile($event,index)" />
        </div>
        <!-- 文件名及文件操作区域 -->
        <div class="file-text-bottom">
          <!-- 文件名 -->
          <el-tooltip
            class="item"
            :disabled="item.fileData.name?false:true"
            effect="light"
            :content="item.fileData.name"
            placement="top-start"
          >
            <span class="file-name">{{item.fileData.name}}</span>
          </el-tooltip>
          <!-- 操作区域 -->
          <div class="operating-area">
            <i class="el-icon-folder-opened" @click.stop="uploadImg(index)"></i>
            <i
              class="el-icon-zoom-in"
              v-show="item.data||item.pdfData"
              @click="filePreview(item,index)"
            ></i>
            <el-popconfirm
              confirmButtonText="确定"
              cancelButtonText="取消"
              icon="el-icon-info"
              title="您是否删除该已上传图片?"
              popper-class="deleteUpload"
              @onConfirm="deleteFile(index,item.id)"
              v-show="item.data||item.pdfData"
            >
              <i class="el-icon-delete" slot="reference"></i>
            </el-popconfirm>
          </div>
        </div>
      </main>
      <!-- 文件验证错误 -->
      <span class="file-info-err" v-show="item.errInfo">{{item.errInfo}}</span>
    </el-col>
    <!-- 图片预览 -->
    <img-viewer ref="viewer" />
    <!-- pdf预览 -->
    <div class="pdf-preview" v-show="pdf.pdfPreview" @click.self="pdf.pdfPreview=false">
      <div class="pdf-preview-box">
        <div class="pdf-preview-box-top">
          <span class="pdf-title">{{pdf.name}}</span>
          <i class="el-icon-circle-close" @click="pdf.pdfPreview=false"></i>
        </div>
        <div class="pdf-preview-box-main">
          <pdf
            v-for="i in 1"
            :key="i"
            :src="pdf.pdf"
            :page="pdf.page"
            style="display: inline-block; width: 100%"
          ></pdf>
        </div>
        <div class="pdf-preview-box-paging">
          <i class="el-icon-arrow-left" @click="pdfPaging(false)"></i>
          <span>{{pdf.page}}</span>
          <span style="margin:0px">/</span>
          <span>{{pdf.size}}</span>
          <i class="el-icon-arrow-right" @click="pdfPaging(true)"></i>
        </div>
      </div>
    </div>
  </el-row>
</template>

<script lang="ts">
// 引入vue-pdf
import pdf from "vue-pdf";
import CMapReaderFactory from "vue-pdf/src/CMapReaderFactory.js";
// 图片查看
import imgViewer from "components/imgViewer/index.vue";

import { Component, Vue, Prop, Watch } from "vue-property-decorator";
import type from "@/utils/type";
@Component({
  components: {
    pdf: Vue.extend(pdf),
    imgViewer,
  },
})
export default class UploadFiles extends Vue {
  // pdf预览
  pdf = {
    pdfPreview: false,
    pdf: "",
    page: 1,
    size: 1,
    name: "",
  };

  // 组件大小
  @Prop({
    type: String,
  })
  private size?: string;

  // 组件风格
  @Prop({
    type: String,
    default: "blue",
  })
  private type?: string;

  // 初始化默认图片橙色
  initialImgUrlConfigOrange =
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAAA0CAYAAAAzMZ5zAAAFeElEQVRoQ+1aa2wUVRg9Z7oWaH1AAsluWx6+wEcUBDEN8sNHIgkqMb4iRqpYOtMfPlA0oCZKTCSBIAY1sdMiaDRBMT5iTCSKwUeiQSGAJkABBQ3dXREloqgt7Ryz5SG73e3c2W3TGeL+nfN93zln7zf33rmX+P+X5QBz/ZBEuDW1sLwrAAwHcAAWN6Le/opc6J3q/mUZoubELZAWSzg3VziBvYC1gI3JN09lU04YIrdqqeTN8xNL4kU66fv9cFF93m2I3Pg8CUtNRZB8gk5qkSk+SjiqpboGnrdL0mBT4iQ7YJVfwIYf95jGRAVHNSUWCXosKGFaXEY75dtiQfMONJ5yE5slTQhKhOQOOqkLg8aFHU81xX8XcGZQogTb2ZgybrOg+QcKnxkhhyVVBCVAoBNOqpykgsaGGZ95h+wQNC4oycy6hI3ps4PGhR1PufEXJNwXlCjJFXRSDUHjwo6n3JpLoM4tAixTsiQEi5PZkNpkGhMV3NGFWXP8eXkwXn0SbGFjyo6KyCA8j61UJ50Gtb0j4Aa/YJIfYWTlDE7f3e6HjeLz//Yya24rw8EvHgcwX0JlrhiCf4NcirFjn+bVn3ZGUawJ557bf7dqOKCbAEzu3v4Lv4LYiCEV77Hu+/0mSfsDo5erxrE+2dofuU/O2cOQ/i5YTH6tqDkPnZ2bwbIZdNrWF5PDNCb0hkgLLTS7n0maSiIJWOPpJA+YCgyKC78hbmKupOeOCyPwARvTNwYVaooPtSFaVXM+2ru2ChqS1ecW59JOLTcVGQQXWkNObpU8M147LKuWdtuWIGJNsOE1JKdV8pjSiooRk1j37WEToaaYUBpSqFV6moJX2JiebSrWBBc6Q3prlXyCWGbdyYbkahOxJpg+M0RufCWIl2invzEpXAgjN/GQpGWmOQgcQnn5Zbz3px9MY3rD9Ykhaq66Q563msQRwHoStr2kmEMt01bJ0zpfg9VT6Ww6UqopJRsit6oCUKukmhNrBWI9Bsdm8e59baYEu1vFdT8XdKVpTNZUTCyhk55fTGxWnlITyE08IymzKcz6kfwNxBzaqXdNagRtlZ71Mt9orGlsSH5sUq8QpqQRopWjzkHHkW2CBhUsQLoAH6aT/Kvge6PAAiyoMAJpVFSOL2UTWpohbvx9Cb7LaJLbEbNmsr5ta67IUlslz8hcCzs5vdiP30UboubENHlaa/ovZo4tQCyAnVx+MtlSWyXvVEw8Qif9rCm3kt8hcjNf2JLfFfW1nlwLa/A9bNjzc7Gzip/Q7tlOnMLG1EY/bJ4ZK2hI5nC86lHJWxI88mgEgf2wyurheQuKnVX8apPYjdhZE1nf+ocftqQRolVj4uj4Z6eEM4IUGggsydfppGYFqR34HaKmxKuC6oIUGUgsLdbRTr1myiGQIWqproXX9aWUGfXR+BH4E4NiEzl73y4TxsbCjm26Nki63CRxmDAkN2HYsCm8fVuHHy9zQ9zEHEktfgnD+tz0PouRIVo1Zig62ndKGhFWwX68uo9fietppz/sDWtmiJtYLukBv6Jhf07yF5QPupSz96YLcfU1RM2jLobXkTkMj4VdsAk/kutgJ68rtLT3N6Qp/omAa0yKRQXDo/dtF+fj26shaonfqi68FRWhpjy7bz8xNpXOvg25MQUN0ZqaITjYtV3SaNNCUcKR2IPK0yfwrt2HjJbucqsWSt5TURIZlCuJN+ikZ/oaohWJ0ejE9twTs6AFo4CnZdXTTq48zjVvy8hNvC3p5igIKpUjicOIWZOOX7XoeT+kqfpaoWtdqYWiFE9yC0ZW1mZuRWUZovVXxdDamjlcvihKgvqCK8nn6aQezDbE5zy1LwqHOQdhzcg1xAY4NMyk+5UbceBfXqQOZiFnaPcAAAAASUVORK5CYII=";
  // 初始化默认图片蓝色
  initialImgUrlConfigBlue =
    "data:image/png;base64,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";
  // 初始化展示默认图片
  initialImgUrl =
    this.type === "blue"
      ? this.initialImgUrlConfigBlue
      : this.initialImgUrlConfigOrange;

  // 上传文件的列表
  @Prop({
    type: Array,
    required: true,
  })
  private fileImgList?: Array<any>;

  // 上传的文件限制大小
  @Prop({
    type: Number,
    default: 5120, // 默认5兆
  })
  private fileSizeByte?: number;

  // 上传的文件类型限制
  @Prop({
    type: Array,
    default: () => ["jpg", "jpeg", "pdf", "png", "txt", "webp"],
  })
  private fileType?: Array<string>;

  // 错误滚动盒子
  @Prop({
    type: String,
    default: ".el-scrollbar__wrap",
  })
  private errScrollBox?: string;

  // 渲染列表
  fileImgListDrawing: Array<any> = [];
  // 向外暴露出去的总文件数组
  exposedFileArr: Array<any> = [];

  created() {
    // 初始化上传文件列表
    this.fileImgListDrawing = (this as any).fileImgList.map((v: any) => {
      // 文件错误提示
      v.errInfo = "";
      // 文件
      v.fileData = {};
      // border虚线
      v.border = true;
      return v;
    });
  }

  mounted() {
    // 拖拽文件上传
    const that = this;

    document.addEventListener(
      "drop",
      function (e) {
        e.preventDefault();
      },
      false
    );
    document.addEventListener(
      "dragover",
      function (e) {
        e.preventDefault();
      },
      false
    );

    (this.$refs.fileBox as any).forEach((el: any) => {
      el.ondragleave = (e: any) => {
        e.preventDefault(); //阻止离开时的浏览器默认行为
      };
      el.ondrop = (e: any) => {
        //阻止拖放后的浏览器默认行为
        e.preventDefault();

        const data = e.dataTransfer.files;

        // 检测是否有文件拖拽到页面
        if (data.length < 1) {
          return;
        }
        that.uploadFile(e, $(el).data("index"));
      };
      el.ondragenter = (e: any) => {
        //阻止拖入时的浏览器默认行为
        e.preventDefault();
      };
      el.ondragover = (e: any) => {
        //阻止拖来拖去的浏览器默认行为
        e.preventDefault();
      };
    });
  }

  // 代理打开文件上传
  uploadImg(i: number) {
    (this.$refs as any).fileImage[i].dispatchEvent(new MouseEvent("click"));
  }
  // 文件上传-预览文件至“文件展示区域”
  uploadFile(el: any, i: number) {
    // 将读出的文件保存
    const elFile = el.dataTransfer
      ? el.dataTransfer.files[0]
      : el.target.files[0];

    if (!elFile) return;

    // 文件验证不通过
    if (!this.fileLimit(elFile, i)) {
      return this.deleteFile(i, this.fileImgListDrawing[i].id);
    }

    const reader = new FileReader();

    this.$set(this.fileImgListDrawing[i], "fileData", elFile);

    reader.readAsDataURL(elFile);
    reader.onloadstart = () => {
      const that = this;
      // 判断是否为pdf文件
      if (elFile.type == "application/pdf") {
        reader.onload = function () {
          that.fileImgListDrawing[i].pdfData = (pdf as any).createLoadingTask({
            url: this.result,
            CMapReaderFactory,
          });
          that.fileImgListDrawing[i].data = "";
          that.fileImgListDrawing[i].border = false;
          that.$set(that.fileImgListDrawing, i, that.fileImgListDrawing[i]);
        };
      } else {
        reader.onload = function () {
          that.fileImgListDrawing[i].data = this.result;
          that.fileImgListDrawing[i].pdfData = "";
          that.fileImgListDrawing[i].border = false;
          that.$set(that.fileImgListDrawing, i, that.fileImgListDrawing[i]);
        };
      }

      // 触发回调
      this.fileImgListDrawing[i].callback &&
        this.fileImgListDrawing[i].callback(elFile);
    };

    elFile.typefileId = this.fileImgListDrawing[i].id;
    this.integrationFileArr(elFile);
  }
  // 文件大小限制及类型限制
  fileLimit(file: any, i: number): boolean {
    const arrFile = this.fileImgListDrawing[i];
    // 获取文件大小
    const fileSize = file.size / 1024,
      // 单个文件最大大小
      onesBiggestFileSize = arrFile.fileSizeByte,
      // 最终文件最大大小
      biggestFileSize = onesBiggestFileSize
        ? onesBiggestFileSize
        : this.fileSizeByte,
      // 验证文件大小是否符合
      fileSizeValidation = fileSize > biggestFileSize;

    if (fileSizeValidation) {
      arrFile.errInfo = `文件不能超过${biggestFileSize / 1024}兆`;
      return false;
    }

    // 获取文件类型
    const fileType = file.type
        .substring(file.type.lastIndexOf("/") + 1)
        .toLowerCase(),
      // 单个文件最终验证类型
      onesUltimatelyFileType = arrFile.fileType,
      // 文件最终验证类型
      ultimatelyFileType = onesUltimatelyFileType
        ? onesUltimatelyFileType
        : this.fileType,
      // 验证文件类型是否符合
      fileTypeValidation = ultimatelyFileType.some(
        (v: string) => v == fileType
      );

    if (!fileTypeValidation) {
      arrFile.errInfo = `只能上传${ultimatelyFileType.join("/")}文件`;
      return false;
    }

    arrFile.errInfo = "";

    return true;
  }
  // 文件必传拦截
  fileErrorToIntercept() {
    const errArr: any = [];

    this.fileImgListDrawing = this.fileImgListDrawing.map((v, i) => {
      if (v.required && !v.fileData.size) {
        v.errInfo = `${v.fileName}必须上传`;
        errArr.push(v.id);
      }
      return v;
    });

    const noRulesLength = errArr.length;
    if (!noRulesLength) {
      return {
        isValidation: true,
        // 必传但未传的文件数量
        noRulesLength,
      };
    }

    // 锚点滚动
    $((this as any).errScrollBox).animate(
      { scrollTop: $("#" + errArr[0])[0].offsetTop },
      300
    );

    return {
      isValidation: false,
      noRulesLength,
    };
  }
  // 文件整合数组 去重
  integrationFileArr(val: any) {
    if (this.exposedFileArr.length === 0) {
      this.exposedFileArr.push(val);
    }

    const index = this.exposedFileArr.findIndex(
      (e) => e.typefileId === val.typefileId
    );

    if (
      this.exposedFileArr.findIndex(
        (element) => element.typefileId == val.typefileId
      ) === -1
    ) {
      this.exposedFileArr.push(val);
    } else {
      this.exposedFileArr.splice(index, 1, val);
    }
  }
  // 获取向外暴露出去的文件集合
  getFiles(type?: string) {
    let newList: any = this.exposedFileArr;
    if (type === "object") {
      newList = {};
      this.exposedFileArr.forEach((v) => {
        newList[v.typefileId] = v;
      });
    }
    const { isValidation, noRulesLength } = this.fileErrorToIntercept();
    return {
      fileList: newList,
      // 总共上传的文件数量
      length: this.exposedFileArr.length,
      // 需上传的文件是否全部验证成功
      isValidation,
      // 必传但未传的文件数量
      noRulesLength,
    };
  }
  // 文件删除
  deleteFile(i: number, id: string) {
    const fileObj = this.fileImgListDrawing[i];
    fileObj.data = fileObj.pdfData = "";
    fileObj.fileData = {};
    fileObj.border = true;

    this.$set(this.fileImgListDrawing, i, fileObj);

    // 清空数据
    (this.$refs as any).fileImage[i].value = "";
    // 删除文件
    this.exposedFileArr.forEach((v, i) => {
      if (v.typefileId === id) return this.exposedFileArr.splice(i, 1);
    });
  }
  // 文件预览
  filePreview(val: any, index: number) {
    const file = this.fileImgListDrawing[index];
    // pdf预览
    if (file.pdfData) {
      this.pdfScrollBarTop();
      this.pdf.pdfPreview = true;
      this.pdf.pdf = file.pdfData;
      this.pdf.page = 1;
      this.pdf.name = file.fileName;
      file.pdfData.promise.then((pdf: any) => {
        this.pdf.size = pdf.numPages ? pdf.numPages : 1;
      });
    }
    // 图片预览
    if (file.data) {
      let imgArr: any = [];
      imgArr.push({
        thumbnail: file.data,
        source: file.data,
      });
      (this.$refs as any).viewer.show(imgArr, 0);
    }
  }
  // pdf滚动条置顶
  pdfScrollBarTop() {
    $(".pdf-preview-box-main").animate({ scrollTop: 0 }, 300);
  }
  // pdf分页
  pdfPaging(isAdd: boolean) {
    this.pdfScrollBarTop();
    this.pdf.page =
      isAdd && this.pdf.page < this.pdf.size ? ++this.pdf.page : this.pdf.page;
    this.pdf.page =
      !isAdd && this.pdf.page > 1 ? --this.pdf.page : this.pdf.page;
  }
}
</script>

<style scoped lang="scss">
$gobal-color-big: rgba(75, 108, 246, 1);
$gobal-color-orange: #fe9818;
.upload-files {
  .upload-show-box {
    margin-bottom: 20px;
    position: relative;
  }
  .upload-show-box-mini {
    .file-container {
      width: 174px;
      height: 160px;
      box-shadow: 0px 0px 8px 0px rgba(39, 59, 100, 0.19);
      .file-image-area {
        width: 162px;
        height: 120px;
        margin: 6px auto;
        .file-image-init-display {
          .initial-img-url {
            margin: 28px auto 10px;
          }
        }
      }
      .file-text-bottom {
        width: 162px;
        height: 20px;
        .file-name {
          width: 95px;
          line-height: 20px;
        }
        .operating-area {
          i,
          span {
            height: 20px;
            line-height: 20px;
            font-size: 16px;
            &:hover {
              color: $gobal-color-orange;
            }
          }
        }
      }
    }
  }
  .file-info-top {
    font-size: 12px;
    font-weight: 400;
    color: rgba(81, 81, 81, 1);
    margin-bottom: 6px;
    display: inline-block;
  }
  .file-info-err {
    left: 0;
    bottom: -20px;
    position: absolute;
    font-size: 12px;
    font-weight: 400;
    color: red;
    display: inline-block;
  }
  .file-container {
    width: 230px;
    box-sizing: border-box;
    height: 210px;
    background: rgba(255, 255, 255, 1);
    box-shadow: 0px 0px 8px 0px rgba(39, 59, 100, 0.08);
    border-radius: 1px;
    overflow: hidden;
    .no-border {
      border: none !important;
    }
    .file-image-area {
      width: 214px;
      height: 156px;
      margin: 8px auto;
      background: rgba(252, 254, 255, 1);
      border-radius: 1px;
      overflow: auto;
      border: 1px dashed $gobal-color-big;
      #upLoad {
        display: none;
      }
      .file-image-init-display {
        width: 100%;
        height: 100%;
        cursor: pointer;
        display: inline-block;
        position: relative;
        .initial-img-url {
          margin: 48px auto 20px;
          width: 32px;
          display: block;
          position: relative;
          z-index: 1;
        }
        .mask-sample-img {
          width: 100%;
          height: 100%;
          position: absolute;
          left: 0;
          top: 0;
          right: 0;
          bottom: 0;
          opacity: 0.2;
          -webkit-mask: -webkit-linear-gradient(
            rgba(0, 0, 0, 0.5),
            rgba(0, 0, 0, 1),
            rgba(0, 0, 0, 1),
            rgba(0, 0, 0, 0.5)
          );
        }
        .initial-title {
          font-size: 12px;
          font-weight: 400;
          color: rgba(81, 81, 81, 1);
          text-align: center;
          .underline {
            text-decoration: underline;
          }
        }
      }
      .file-img {
        cursor: pointer;
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    }
    .file-image-area-orange {
      border: 1px dashed $gobal-color-orange;
    }
    .file-text-bottom {
      width: 214px;
      height: 30px;
      margin: 0 auto;
      background: rgba(255, 255, 255, 1);
      box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.08);
      display: flex;
      .file-name {
        width: 135px;
        height: 100%;
        line-height: 30px;
        font-size: 12px;
        color: #414141;
        font-weight: 600;
        padding-left: 8px;
        box-sizing: border-box;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        cursor: pointer;
      }
      .operating-area {
        flex: 1;
        display: flex;
        margin-right: 4px;
        i,
        span {
          flex: 1;
          font-size: 18px;
          color: #bbbbbb;
          cursor: pointer;
          text-align: right;
          height: 30px;
          line-height: 30px;
          &:hover {
            color: $gobal-color-big;
          }
        }
      }
    }
  }
  .pdf-preview {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background-color: #0000004d;
    z-index: 999;
    overflow: auto;
    .pdf-preview-box {
      width: 600px;
      height: 450px;
      border-radius: 10px;
      overflow: hidden;
      background-color: #fff;
      margin: 0 auto;
      position: relative;
      top: 50%;
      transform: translateY(-50%);
      display: flex;
      flex-direction: column;
      .pdf-preview-box-top {
        width: 100%;
        height: 24px;
        background-color: #233e48;
        text-align: center;
        line-height: 24px;
        position: relative;
        .pdf-title {
          vertical-align: super;
          font-size: 12px;
          font-weight: 400;
          color: rgba(255, 255, 255, 1);
        }
        .el-icon-circle-close {
          position: absolute;
          font-size: 16px;
          color: #fff;
          right: 10px;
          cursor: pointer;
          top: 50%;
          transform: translateY(-50%);
        }
      }
      .pdf-preview-box-main {
        flex: 1;
        overflow: auto;
      }
      .pdf-preview-box-paging {
        width: 100%;
        height: 24px;
        background-color: #233e484d;
        text-align: center;
        font-size: 14px;
        font-weight: 400;
        line-height: 24px;
        i {
          color: #000;
          cursor: pointer;
          margin: 0 10px;
        }
        span {
          margin: 0 10px;
          user-select: none;
        }
      }
    }
  }
}
</style>
  • 图片预览组件
<template>
  <div>
    <viewer
      :images="images"
      :options="options"
      class="viewer"
      ref="viewer"
      @inited="inited"
      v-if="images && images.length"
    >
      <img
        v-for="{source, thumbnail} in images"
        :src="thumbnail"
        :data-source="source"
        :key="source"
        class="image"
      />
    </viewer>
  </div>
</template>

<script  lang="ts">
import { Component, Vue } from "vue-property-decorator";
import vue from "vue";
import Viewer from "v-viewer";
import "viewerjs/dist/viewer.css";
vue.use(Viewer);

@Component
export default class ImgViewer extends Vue {
  options = {
    url: "data-source",
  };
  index = 0;
  images = [];
  inited(viewer: any) {
    (this as any).$viewer = viewer;
    (this as any).$viewer.view(this.index);
  }
  view(index: any) {
    this.index = index;
    (this as any).$viewer.view(this.index);
  }
  show(images: any, index = 0) {
    if (this.images === images) {
      this.view(index);
      return;
    }
    this.images = images;
    this.index = index;
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
.image {
  display: none;
}
</style>
<style>
.viewer-loading > img {
  display: none; /* hide big images when it is loading */
}
.viewer-list > li {
  opacity: 0.3;
}
</style>
  • 删除弹框样式
.deleteUpload {
  width: 160px;
  height: 68px;
  padding: 10px 5px 0 7px;
  margin-left: -120px;
  box-sizing: border-box;

  .el-popconfirm__main {
    font-size: 12px;
    color: #515151;
    margin-bottom: 8px;

    i {
      display: none;
    }
  }

  &[x-placement^=top] {
    // margin-bottom: 0px;

    .popper__arrow {
      left: 132px !important;
      right: 18px;
    }
  }

  &[x-placement^=bottom] {
    // margin-top: 1px;

    .popper__arrow {
      left: 132px !important;
      right: 18px;
    }
  }

  .el-popconfirm__action {
    text-align: center;

    .el-button {
      width: 60px;
      height: 20px;
      padding: 0;
    }

    .el-button--text {
      border: 1px solid rgba(239, 244, 255, 1);
      color: #AEAEAE;
    }

    .el-button--primary {
      background-color: #4b6cf6;
      border-color: #4b6cf6;
    }
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容