ElUpload不好用?一文教你实现一个简易图片上传预览组件

简介: ElUpload不好用?一文教你实现一个简易图片上传预览组件

前言


嗯,,,跟之前封装“全局 Loading”的出发点基本一样,因为产品觉得 ElementUI 提供的默认上传组件,使用“照片墙”或者“缩略图”模式都需要去改动原本的组件样式,并且缩略图尺寸也不能调整,预览模式也会对原始图片进行缩放和处理,不符合系统本身的样式规范。


最离谱的是,预览模式居然有背景色,但是背景色又没有填满整个 model 的背景区域,,,甚至还出现了滚动条!!!


所以,为了更好的配合产品和UI,特地重新编写了一个图片上传组件。


1. 功能设计


嗯,既然是图片上传,那么肯定只支持图片文件了。因为是内部项目,所以也保留了 http 上传部分,大家可以参照 ElementUI 适当修改。


修改后的上传组件支持以下功能:


  1. 上传(基础中的基础)


  1. 实现 v-model 语法糖绑定上传数据列表(嗯,,,也很基础)


  1. 需要支持大图预览


  1. 不能换行,超出宽度显示滚动条且支持鼠标控制(不用 shift 的那种)


功能设计完成之后,大致的页面样式就是这样的:


网络异常,图片无法展示
|


后面会补上[码上掘金]的地址


2. 实现


为了能够适应更多的场景,我决定把预览部分也直接提取出来。


2.1 图片预览 PicturePreviewer


这里的图片预览是基于 ElDialog 开发的,支持翻页、循环等。


本身不依赖外部的图片元素,可以和 ElDialog 一样直接使用 visible 属性来控制显示和隐藏。


<template>
   <el-dialog
     :title="title"
     :visible="visible"
     :close-on-click-modal="false"
     width="1000px"
     destroy-on-close
     append-to-body
     @close="closeDialog"
   >
     <div class="q-picture__img-box">
       <div
         class="q-picture__prev-btn"
         @mouseover="leftBtnStatus = true"
         @mouseleave="leftBtnStatus = false"
       >
         <transition name="btn-fade">
           <el-button
             v-show="leftBtnStatus && isNeeding"
             circle
             icon="el-icon-arrow-left"
             @click="lastImage()"
           />
         </transition>
       </div>
       <img v-show="visible" :src="currentSrc" alt="" v-loading="loading" @load="loading = false" />
       <div
         class="q-picture__next-btn"
         @mouseover="rightBtnStatus = true"
         @mouseleave="rightBtnStatus = false"
       >
         <transition name="btn-fade">
           <el-button
             v-show="rightBtnStatus && isNeeding"
             circle
             icon="el-icon-arrow-right"
             @click="nextImage()"
           />
         </transition>
       </div>
     </div>
   </el-dialog>
 </template>
 <script>
 export default {
   name: "PicturePreviewer",
   props: {
     visible: { type: Boolean, default: false },
     pageable: { type: Boolean, default: true },
     recyclable: { type: Boolean, default: true },
     src: { type: [String, Array], required: true },
     title: { type: String, default: "图片预览" },
     current: { type: Number, default: 0 }
   },
   data() {
     return {
       currentKey: -1,
       leftBtnStatus: false,
       rightBtnStatus: false,
       loading: false
     };
   },
   computed: {
     isNeeding: function () {
       return typeof this.src === "object" && this.pageable && this.src && this.src.length > 1;
     },
     currentSrc: function () {
       if (typeof this.src === "string") return this.src;
       if (this.src && this.src.length) {
         return this.src[this.currentKey] || "";
       }
       return "";
     }
   },
   methods: {
     closeDialog() {
       this.$emit("update:visible", false);
     },
     lastImage() {
       if (this.currentKey - 1 === -1) {
         if (this.recyclable) this.currentKey = this.src.length - 1;
         else this.$message.info("当前已经是第一张图片");
       } else {
         this.currentKey = this.currentKey - 1;
       }
     },
     nextImage() {
       if (this.currentKey + 1 === this.src.length) {
         if (this.recyclable) this.currentKey = 0;
         else this.$message.info("当前已经是最后一张图片");
       } else {
         this.currentKey = this.currentKey + 1;
       }
     }
   },
   watch: {
     current: {
       handler: function (val) {
         if (val) this.currentKey = val;
         else this.currentKey = 0;
       },
       immediate: true
     }
   }
 };
 </script>


样式部分就放到 马上掘金 了。


Markup:

<div id="app">
  <div class="img-box">
    <img v-for="(img, key) in images" :key="img" :src="img" @click="openPreviewer(key)" />
  </div>
  <el-button size="small" type="primary" @click="openPreviewer(0)">查看大图</el-button>
  <picture-previewer :visible.sync="dialogImageVisible" :src="images" :current="current" />
</div>


Style:

.img-box {
  display: inline-flex;
  width: 100%;
  img {
    width: 100px;
    max-width: 100px;
    height: 64px;
    max-height: 64px;
    &:hover {
      cursor: pointer;
    }
    & + img {
      margin-left: 16px;
    }
  }
}
.q-picture__img-box {
  display: inline-flex;
  width: 100%;
  height: 552px; // 600px - 48px
  overflow: hidden;
  justify-content: center;
  align-items: center;
  position: relative;
  img {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
    max-height: 100%;
    max-width: 100%;
  }
  .q-picture__prev-btn,
  .q-picture__next-btn {
    position: absolute;
    top: 0;
    height: 100%;
    width: 240px;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    color: #909399;
    box-sizing: border-box;
    .control-btn {
      box-sizing: border-box;
      border-radius: 50%;
      width: 36px;
      height: 36px;
      border: 1px solid #909399;
    }
  }
  .q-picture__prev-btn {
    left: 0;
    padding: 0 120px 0 0;
  }
  .q-picture__next-btn {
    right: 0;
    padding: 0 0 0 120px;
  }
}
.btn-fade-enter,
.btn-fade-leave-to {
  opacity: 0;
}
.btn-fade-enter-to,
.btn-fade-leave {
  opacity: 1;
}
.btn-fade-enter-active,
.btn-fade-leave-active {
  transition: opacity 0.5s;
}


Script:

const PicturePreviewer = Vue.component("picture-previewer", {
  template: `
    <el-dialog
    :title="title"
    :visible="visible"
    :close-on-click-modal="false"
    width="1000px"
    destroy-on-close
    append-to-body
    @close="closeDialog"
  >
    <div class="q-picture__img-box">
      <div
        class="q-picture__prev-btn"
        @mouseover="leftBtnStatus = true"
        @mouseleave="leftBtnStatus = false"
      >
        <transition name="btn-fade">
          <el-button
            v-show="leftBtnStatus && isNeeding"
            circle
            icon="el-icon-arrow-left"
            @click="lastImage()"
          />
        </transition>
      </div>
      <img v-show="visible" :src="currentSrc" alt="" v-loading="loading" @load="loading = false" />
      <div
        class="q-picture__next-btn"
        @mouseover="rightBtnStatus = true"
        @mouseleave="rightBtnStatus = false"
      >
        <transition name="btn-fade">
          <el-button
            v-show="rightBtnStatus && isNeeding"
            circle
            icon="el-icon-arrow-right"
            @click="nextImage()"
          />
        </transition>
      </div>
    </div>
  </el-dialog>`,
  name: "PicturePreviewer",
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    pageable: {
      type: Boolean,
      default: true
    },
    recyclable: {
      type: Boolean,
      default: true
    },
    src: {
      type: [String, Array],
      required: true
    },
    title: {
      type: String,
      default: "图片预览"
    },
    current: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      currentKey: -1,
      leftBtnStatus: false,
      rightBtnStatus: false,
      loading: false
    };
  },
  computed: {
    isNeeding: function () {
      return typeof this.src === "object" && this.pageable && this.src && this.src.length > 1;
    },
    currentSrc: function () {
      if (typeof this.src === "string") return this.src;
      if (this.src && this.src.length) {
        return this.src[this.currentKey] || "";
      }
      return "";
    }
  },
  methods: {
    closeDialog() {
      this.$emit("update:visible", false);
    },
    lastImage() {
      if (this.currentKey - 1 === -1) {
        if (this.recyclable) this.currentKey = this.src.length - 1;
        else this.$message.info("当前已经是第一张图片");
      } else {
        this.currentKey = this.currentKey - 1;
      }
    },
    nextImage() {
      if (this.currentKey + 1 === this.src.length) {
        if (this.recyclable) this.currentKey = 0;
        else this.$message.info("当前已经是最后一张图片");
      } else {
        this.currentKey = this.currentKey + 1;
      }
    }
  },
  watch: {
    current: {
      handler: function (val) {
        if (val) this.currentKey = val;
        else this.currentKey = 0;
      },
      immediate: true
    }
  }
})
const app = new Vue({
  el: "#app",
  name: "App",
  components: { "picture-previewer": PicturePreviewer },
  data() {
    return {
      dialogImageVisible: false,
      current: 0,
      images: [
        "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1fe3be07700d4377ba581bd9aa8b59b7~tplv-k3u1fbpfcp-zoom-1.image",
        "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/453ff3d20fac45abae53d03973e12e4a~tplv-k3u1fbpfcp-zoom-1.image",
        "https://www.logosc.cn/uploads/resources/2018/11/28/1543389900_thumb.jpg",
        "https://www.logosc.cn/uploads/resources/2018/11/24/1543048917.jpg",
        "https://www.logosc.cn/uploads/resources/2018/11/06/1541472520.jpg",
        "https://www.logosc.cn/uploads/resources/2018/11/28/1543377592.jpg"
      ],
    }
  },
  methods: {
    openPreviewer(key) {
      this.dialogImageVisible = true;
      this.current = key;
    }
  }
})


运行:


image.png


2.2 图片上传 ImageUpload


图片预览处理完成够,就可以处理图片上传了。


<template>
   <div
     class="q-upload__preview"
     ref="pictures"
     :title="messageInfo"
     @mouseenter="horizontalRolling"
   >
     <slot name="preSlot"></slot>
     <input
       class="q-upload__file-input"
       type="file"
       ref="fileInput"
       name="fileInput"
       @change="fileChange"
       :accept="accept"
     />
     <div
       class="q-upload__file-label"
       v-loading="fileLoading"
       @click="selectFile"
       v-show="fileLists.length < limitNum && !disabled"
     >
       <i class="el-icon-plus"></i>
     </div>
     <slot name="middle"></slot>
     <div class="q-upload__pre-img" v-for="(i, k) in fileLists" :key="i.smallUrl">
       <img class="q-upload__img" :src="i.smallUrl ? i.smallUrl : i.url" />
       <div class="q-upload__mask">
         <i v-if="prev" class="el-icon-zoom-in" @click="imgPreview(i, k)"></i>
         <i class="el-icon-delete" v-if="!disabled" @click="imgRemove(k)"></i>
       </div>
     </div>
     <slot name="endSlot"></slot>
     <picture-previewer
       :visible.sync="dialogImageVisible"
       :src="imageUrls"
       :current="currentImage"
     />
   </div>
 </template>
 <script>
 import Utils from "../../src/utils/commonUtils";
 export default {
   name: "ImageUpload",
   props: {
     active: { type: String, default: "/api/file/upload" },
     accept: { type: String, default: "" },
     limitNum: { type: Number, default: 9 },
     size: { type: Number, default: 10 },
     prev: { type: Boolean, default: true },
     disabled: { type: Boolean, default: false },
     value: { type: Array, default: () => [] }
   },
   data() {
     return {
       fileLoading: false,
       dialogImageVisible: false,
       dialogImageUrl: "",
       currentImage: 0,
       fileLists: [],
       messageInfo: ""
     };
   },
   computed: {
     imageUrls: function () {
       return this.fileLists.map(o => o.url);
     }
   },
   methods: {
     async validateImage(file) {
       const isJPEG = file.type === "image/jpeg";
       const isJPG = file.type === "image/jpg";
       const isPNG = file.type === "image/png";
       const isBMP = file.type === "image/bmp";
       const isLtSize = file.size / 1024 / 1024 < this.size;
       if (!(isJPEG || isJPG || isPNG || isBMP)) {
         return {
           status: false,
           message: `上传图片必须是${this.accept}格式!`
         };
       }
       if (!isLtSize) {
         return {
           status: false,
           message: "上传图片大小不能超过 " + this.size + " MB!"
         };
       }
       return { status: true, message: "" };
     },
     // 选择文件
     selectFile() {
       this.$refs.fileInput.value = null; // 置空,防止删除后无法再次选择
       this.$refs.fileInput.click();
       return true;
     },
     // 文件选取之后·
     async fileChange(el) {
       const file = [...el.target.files][0];
       let { status, message } = await this.validateImage(file);
       if (status) {
         this.fileLoading = true;
         await this.customHttpRequest(file);
       } else {
         this.$message.error(message);
         return false;
       }
     },
     // 上传
     async customHttpRequest(file) {
       try {
         let fData = Utils.createUploadForm(file);
         let {
           data: { data, status, message }
         } = await this.$http.post(this.active, fData.formData, fData.config);
         if (status) {
           this.fileLists.unshift(data);
           this.$emit("success", data);
         } else {
           this.$message.error(message);
           this.$emit("error");
         }
       } finally {
         this.fileLoading = false;
       }
     },
     imgPreview(file, k) {
       this.dialogImageUrl = file.url;
       this.dialogImageVisible = true;
       this.currentImage = k;
     },
     imgRemove(index) {
       this.fileLists.splice(index, 1);
       this.$emit("input", this.fileLists);
       this.$emit("change", this.fileLists);
       this.$emit("blur", this.fileLists);
     },
     horizontalRolling() {
       if (this.$refs["pictures"].clientWidth < this.$refs["pictures"].scrollWidth) {
         this.messageInfo = "滚动滚轮查看所有信息";
       } else {
         this.messageInfo = "";
       }
       this.$refs["pictures"].addEventListener("mousewheel", this.$_scrollEvent);
       this.$once("hook:beforeDestroy", () => {
         this.$refs["pictures"].removeEventListener("mousewheel", this.$_scrollEvent);
       });
     },
     $_scrollEvent(e) {
       let left = this.$refs["pictures"].scrollLeft;
       this.$refs["pictures"].scrollLeft = e.deltaY > 0 ? left + 40 : left - 40;
     }
   },
   watch: {
     value: {
       deep: true,
       immediate: true,
       handler: function () {
         this.fileLists = typeof this.value === "string" ? JSON.parse(this.value) : this.value;
         if (!this.fileLists) this.fileLists = [];
       }
     },
     fileLists: {
       deep: true,
       immediate: false,
       handler: function () {
         if (this.value && this.value.length > this.limitNum) {
           this.$message.warning(`最多可以上传【 ${this.limitNum} 】张图片!!`);
         }
         this.$emit("input", this.fileLists);
         this.$emit("change", this.fileLists);
       }
     }
   }
 };
 </script>


Markup:

<div id="app">
  <h2>图片上传</h2>
  <image-upload v-model="imgList" />
</div>


Style:

.img-box {
  display: inline-flex;
  width: 100%;
  img {
    width: 100px;
    max-width: 100px;
    height: 64px;
    max-height: 64px;
    &:hover {
      cursor: pointer;
    }
    & + img {
      margin-left: 16px;
    }
  }
}
.q-picture__img-box {
  display: inline-flex;
  width: 100%;
  height: 552px; // 600px - 48px
  overflow: hidden;
  justify-content: center;
  align-items: center;
  position: relative;
  img {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
    max-height: 100%;
    max-width: 100%;
  }
  .q-picture__prev-btn,
  .q-picture__next-btn {
    position: absolute;
    top: 0;
    height: 100%;
    width: 240px;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    color: #909399;
    box-sizing: border-box;
    .control-btn {
      box-sizing: border-box;
      border-radius: 50%;
      width: 36px;
      height: 36px;
      border: 1px solid #909399;
    }
  }
  .q-picture__prev-btn {
    left: 0;
    padding: 0 120px 0 0;
  }
  .q-picture__next-btn {
    right: 0;
    padding: 0 0 0 120px;
  }
}
.btn-fade-enter,
.btn-fade-leave-to {
  opacity: 0;
}
.btn-fade-enter-to,
.btn-fade-leave {
  opacity: 1;
}
.btn-fade-enter-active,
.btn-fade-leave-active {
  transition: opacity 0.5s;
}
.q-upload__preview {
  display: flex;
  flex-direction: row;
  padding-bottom: 6px;
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  .el-loading-spinner {
    margin-top: -26px !important;
  }
  .q-upload__pre-img {
    width: 70px;
    min-width: 70px;
    height: 70px;
    margin-right: 12px;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all ease-in-out 0.2s;
    border: 1px dashed #c0ccda;
    border-radius: 4px;
    /*overflow: hidden;*/
    img {
      max-width: 68px !important;
      max-height: 68px !important;
      border-radius: 4px;
    }
    .q-upload__mask {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      border-radius: 4px;
      z-index: -1;
      display: flex;
      justify-content: center;
      transition: all ease-in-out 0.2s;
      i {
        transition: all ease-in-out 0.2s;
        font-size: 20px;
        color: #ffffff;
        padding: 0 6px;
        line-height: 70px;
        &:hover {
          cursor: pointer;
        }
      }
    }
    &:hover {
      .q-upload__mask {
        z-index: 2;
        background: rgba(0, 0, 0, 0.4);
      }
    }
  }
}
.q-upload__file-input {
  display: none !important;
  width: 0;
  height: 0;
  margin: 0;
  padding: 0;
}
.q-upload__file-label {
  width: 70px;
  min-width: 70px !important;
  height: 70px;
  margin-right: 12px;
  border-radius: 4px;
  border: 1px dashed lightgray;
  line-height: 70px;
  font-size: 20px;
  color: lightgray;
  transition: all ease-in-out 0.2s;
  text-align: center;
  position: relative;
  &:hover {
    border-color: #2d8cf0;
    color: #2d8cf0;
    cursor: pointer;
  }
}


Script:

const PicturePreviewer = Vue.component("picture-previewer", {
  template: `
    <el-dialog
    :title="title"
    :visible="visible"
    :close-on-click-modal="false"
    width="1000px"
    destroy-on-close
    append-to-body
    @close="closeDialog"
  >
    <div class="q-picture__img-box">
      <div
        class="q-picture__prev-btn"
        @mouseover="leftBtnStatus = true"
        @mouseleave="leftBtnStatus = false"
      >
        <transition name="btn-fade">
          <el-button
            v-show="leftBtnStatus && isNeeding"
            circle
            icon="el-icon-arrow-left"
            @click="lastImage()"
          />
        </transition>
      </div>
      <img v-show="visible" :src="currentSrc" alt="" v-loading="loading" @load="loading = false" />
      <div
        class="q-picture__next-btn"
        @mouseover="rightBtnStatus = true"
        @mouseleave="rightBtnStatus = false"
      >
        <transition name="btn-fade">
          <el-button
            v-show="rightBtnStatus && isNeeding"
            circle
            icon="el-icon-arrow-right"
            @click="nextImage()"
          />
        </transition>
      </div>
    </div>
  </el-dialog>`,
  name: "PicturePreviewer",
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    pageable: {
      type: Boolean,
      default: true
    },
    recyclable: {
      type: Boolean,
      default: true
    },
    src: {
      type: [String, Array],
      required: true
    },
    title: {
      type: String,
      default: "图片预览"
    },
    current: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      currentKey: -1,
      leftBtnStatus: false,
      rightBtnStatus: false,
      loading: false
    };
  },
  computed: {
    isNeeding: function () {
      return typeof this.src === "object" && this.pageable && this.src && this.src.length > 1;
    },
    currentSrc: function () {
      if (typeof this.src === "string") return this.src;
      if (this.src && this.src.length) {
        return this.src[this.currentKey] || "";
      }
      return "";
    }
  },
  methods: {
    closeDialog() {
      this.$emit("update:visible", false);
    },
    lastImage() {
      if (this.currentKey - 1 === -1) {
        if (this.recyclable) this.currentKey = this.src.length - 1;
        else this.$message.info("当前已经是第一张图片");
      } else {
        this.currentKey = this.currentKey - 1;
      }
    },
    nextImage() {
      if (this.currentKey + 1 === this.src.length) {
        if (this.recyclable) this.currentKey = 0;
        else this.$message.info("当前已经是最后一张图片");
      } else {
        this.currentKey = this.currentKey + 1;
      }
    }
  },
  watch: {
    current: {
      handler: function (val) {
        if (val) this.currentKey = val;
        else this.currentKey = 0;
      },
      immediate: true
    }
  }
})
function createUploadForm(file, name = "file", scale = "0.5", width = "", height = "") {
  let res = {
    config: {
      headers: {
        "Content-Type": "multipart/form-data"
      }
    },
    formData: {}
  }; //添加请求头
  let formData = new FormData();
  formData.append(name, file);
  formData.append("scale", scale);
  formData.append("width", width);
  formData.append("height", height);
  res.formData = formData;
  return res;
}
const ImageUpload = Vue.component("image-upload", {
  template: `
    <div
    class="q-upload__preview"
    ref="pictures"
    @mouseenter="horizontalRolling"
    :title="messageInfo"
  >
    <slot name="preSlot"></slot>
    <input
      class="q-upload__file-input"
      type="file"
      ref="fileInput"
      name="fileInput"
      @change="fileChange"
      :accept="accept"
    />
    <div
      class="q-upload__file-label"
      v-loading="fileLoading"
      @click="selectFile"
      v-show="fileLists.length < limitNum && !disabled"
    >
      <i class="el-icon-plus"></i>
    </div>
    <slot name="middle"></slot>
    <div class="q-upload__pre-img" v-for="(i, k) in fileLists" :key="i.smallUrl">
      <img class="q-upload__img" :src="i.smallUrl ? i.smallUrl : i.url" />
      <div class="q-upload__mask">
        <i v-if="prev" class="el-icon-zoom-in" @click="imgPreview(i, k)"></i>
        <i class="el-icon-delete" v-if="!disabled" @click="imgRemove(k)"></i>
      </div>
    </div>
    <slot name="endSlot"></slot>
    <picture-previewer
      :visible.sync="dialogImageVisible"
      :src="imageUrls"
      :current="currentImage"
    />
  </div>`,
  name: "ImageUpload",
  components: { "picture-previewer": PicturePreviewer },
  props: {
    active: {
      type: String,
      default: "/api/file/upload/thumbnail"
    },
    accept: {
      type: String,
      default: ""
    },
    limitNum: {
      type: Number,
      default: 9
    },
    size: {
      type: Number,
      default: 10
    },
    prev: {
      type: Boolean,
      default: true
    },
    disabled: {
      type: Boolean,
      default: false
    },
    value: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      fileLoading: false,
      dialogImageVisible: false,
      dialogImageUrl: "",
      currentImage: 0,
      fileLists: [],
      messageInfo: ""
    };
  },
  computed: {
    imageUrls: function () {
      return this.fileLists.map(o => o.url);
    }
  },
  methods: {
    async validateImage(file) {
      const isJPEG = file.type === "image/jpeg";
      const isJPG = file.type === "image/jpg";
      const isPNG = file.type === "image/png";
      const isBMP = file.type === "image/bmp";
      const isLtSize = file.size / 1024 / 1024 < this.size;
      if (!(isJPEG || isJPG || isPNG || isBMP)) {
        return {
          status: false,
          message: `上传图片必须是${this.accept}格式!`
        };
      }
      if (!isLtSize) {
        return {
          status: false,
          message: "上传图片大小不能超过 " + this.size + " MB!"
        };
      }
      return {
        status: true,
        message: ""
      };
    },
    // 选择文件
    selectFile() {
      this.$refs.fileInput.value = null; // 置空,防止删除后无法再次选择
      this.$refs.fileInput.click();
      return true;
    },
    // 文件选取之后·
    async fileChange(el) {
      const file = [...el.target.files][0];
      let { status, message } = await this.validateImage(file);
      if (status) {
        this.fileLoading = true;
        await this.customHttpRequest(file);
      } else {
        this.$message.error(message);
        return false;
      }
    },
    // 上传
    async customHttpRequest(file) {
      try {
        let fData = createUploadForm(file);
        let {
          data: { data, status, message }
        } = await this.$http.post(this.active, fData.formData, fData.config);
        if (status) {
          this.fileLists.unshift(data);
          this.$emit("success", data);
        } else {
          this.$message.error(message);
          this.$emit("error");
        }
      } finally {
        this.fileLoading = false;
      }
    },
    imgPreview(file, k) {
      this.dialogImageUrl = file.url;
      this.dialogImageVisible = true;
      this.currentImage = k;
    },
    imgRemove(index) {
      this.fileLists.splice(index, 1);
      this.$emit("input", this.fileLists);
      this.$emit("change", this.fileLists);
      this.$emit("blur", this.fileLists);
    },
    horizontalRolling() {
      if (this.$refs["pictures"].clientWidth < this.$refs["pictures"].scrollWidth) {
        this.messageInfo = "滚动滚轮查看所有信息";
      } else {
        this.messageInfo = "";
      }
      this.$refs["pictures"].addEventListener("mousewheel", this.$_scrollEvent);
      this.$once("hook:beforeDestroy", () => {
        window.removeEventListener("resize", this.$_scrollEvent);
      });
    },
    $_scrollEvent(e) {
      let left = this.$refs["pictures"].scrollLeft;
      this.$refs["pictures"].scrollLeft = e.deltaY > 0 ? left + 40 : left - 40;
    }
  },
  watch: {
    value: {
      deep: true,
      immediate: true,
      handler: function () {
        this.fileLists = typeof this.value === "string" ? JSON.parse(this.value) : this.value;
        if (!this.fileLists) this.fileLists = [];
      }
    },
    fileLists: {
      deep: true,
      immediate: false,
      handler: function () {
        if (this.value && this.value.length > this.limitNum) {
          this.$message.warning(`最多可以上传【 ${this.limitNum} 】张图片!!`);
        }
        this.$emit("input", this.fileLists);
        this.$emit("change", this.fileLists);
        this.$emit("blur", this.fileLists);
      }
    }
  }
})
const app = new Vue({
  el: "#app",
  name: "App",
  components: { "picture-previewer": PicturePreviewer },
  data() {
    return {
      list: [
        "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1fe3be07700d4377ba581bd9aa8b59b7~tplv-k3u1fbpfcp-zoom-1.image",
        "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/453ff3d20fac45abae53d03973e12e4a~tplv-k3u1fbpfcp-zoom-1.image",
        "https://www.logosc.cn/uploads/resources/2018/11/28/1543389900_thumb.jpg",
        "https://www.logosc.cn/uploads/resources/2018/11/24/1543048917.jpg",
        "https://www.logosc.cn/uploads/resources/2018/11/06/1541472520.jpg",
        "https://www.logosc.cn/uploads/resources/2018/11/28/1543377592.jpg"
      ],
      imgList: [],
    }
  },
  created() {
    this.imgList = this.list.map(i => ({ smallUrl: i, url: i }));
  }
})


运行:


image.png


因为是内部项目,所以上传方法还是使用的实例上的 axios 方法来发送上传请求的;在独立组件库中依然应该通过 props 的方式传递项目中定义的 http 请求方法。


组件接收一个最大张数限制 limitNum 和文件大小限制 size,以及预览控制 prev 和禁用状态 disabled


在选择文件之后会立即上传、点击已上传文件则是预览当前文件;当前内部也依赖了 ElementUI 的 Message 组件,用来显示提示信息。


在预览区域前后也增加了一个插槽,用来插入开发者需要的其他信息。


在整个组件的 Dom 节点上,会添加一个鼠标的 mouseenter 事件,当鼠标在组件内部的时候,则计算内部的缩略图区域与外层节点的大小进行比较,如果大于外层父节点的宽度的话,则提示用户通过鼠标滚轮来控制缩略图区域的滚动。


3. 后记


整个组件虽然可以满足当时的系统的一个需求,但是仔细研究代码的话会发现依然有很多细节的地方需要修复。例如:


  • 组件的 mouseenter 事件,每次被触发时都会给 dom 添加一个鼠标监听事件,而没有在鼠标移出时及时销毁监听


  • 没有增加自定义 http 配置


  • 没有控制预览组件的配置项


  • 缩略图区域没有尺寸控制


等等一系列的问题,所以我们在抽离组件、公共逻辑的时候,还是需要尽可能的保留以后扩展的可能性,减少与外界逻辑或者业务的关联。


目录
相关文章
|
6月前
|
前端开发
前端切图:自制简易音乐播放器
前端切图:自制简易音乐播放器
47 0
|
前端开发
前端学习案例-WangEdit富文本编辑器增加上传视频功能
前端学习案例-WangEdit富文本编辑器增加上传视频功能
367 0
|
2天前
|
小程序
微信小程序拖拽实现(真实测试管用)
微信小程序拖拽实现(真实测试管用)
|
2天前
小清新卡通人物404错误页面源码
小清新卡通人物404错误页面源码
20 0
小清新卡通人物404错误页面源码
|
9月前
|
前端开发 JavaScript
【项目笔记】:elementui下拉框数据太多造成页面卡顿怎么解决?
针对前端下拉框数据过多造成页面卡顿,元芳你怎么看?
121 2
|
6月前
|
JavaScript 前端开发
前端js上传照片实现可预览功能
前端js上传照片实现可预览功能
33 0
|
数据采集 运维 资源调度
|
前端开发
前端工作总结123-视频上传和图片编辑功能
前端工作总结123-视频上传和图片编辑功能
75 0
前端工作总结123-视频上传和图片编辑功能
|
前端开发
前端工作总结273-处理预览界面
前端工作总结273-处理预览界面
101 0
|
存储 移动开发 小程序
如何实现微信小程序图像剪切?代码拿去用,不谢!
我在早先发布的文章《如何实现微信小程序换头像?三步帮你搞定!》中,提到实现微信小程序换头像需要三步: 获取用户头像 图片模板 图片合成 前文已经就获取用户头像和图片模板两个步骤进行了讲解,本文就来详细说说如何合成图片。图片合成的过程中非常重要的一块功能对图片进行剪切。该功能点很固定,大都是对图片进行拖拽、缩放后,在一定区域内剪切出一个固定长宽的图片。这类功能在app端和H5中都有很多成熟的插件供使用,接下来就来看看我在海豚趣图小程序中的头像剪切插件是如何实现的,欢迎大家提意见。