首发于 前端学习

基于el-upload前端图片上传组件二次封装设计思路

需求如下:

  • 图片上传需要限制格式
  • 图片上传限制图片的张数
  • 单张图片的大小
  • 图片上传后可以进行拖拽排序
  • 可以多张图片同时上传(发送多个请求即可)
  • 如果是单张图片上传那么上传成功后就只显示图片即可 不必再显示上传按钮组件

设计如下:

定义变量:

// 限制上传的图片数量
    limit: {
      type: Number,
      default: 99,
    // 限制上传图片的文件大小(kb)
    size: {
      type: Number,
      default: 5000,
    },

还需要考虑到单个图片的情况 已经上传的url路径 由于业务的需要可能需要单独的文件夹命名等等

    bizPath: {
      type: String,
      required: false,
      default: 'temp',
    postAction: {
      type: String,
      default: window._CONFIG['domianURL'] + '/sys/common/upload', // 默认的action url数据
// 是否是单图上传(单图上传就是已传图片和上传按钮重叠)
    isSingle: {
      type: Boolean,
      default: false,
    // 图片显示的宽度(px)
    width: {
      type: Number,
      default: 100,
    // 图片显示的高度(px)
    height: {
      type: Number,
      default: 100,
    },

最后设置一个绑定的值

 // 图片数据(图片url组成的数组) 通过v-model传递
    value: {
      type: String,
      default() {
        return ''
    },

计算属性

这里我们在外面传递了这个值后 需要利用计算的属性来绑定这值 由于是个字符串利用','号来拼接的 所以需要分割成数组 这样就可以用v-for 来循环展示即可

computed: {
    // 图片数组数据
    imgList: {
      get() {
          // 以逗号开始显示为逗号即可
          if(this.value == '') {
              return []
          }else {
            const urls = (this.value + '').split(',')
            return urls                     
      set(val) {
        if (val.length < this.imgList.length) {
          // 判断是删除图片时同步el-upload数据
          this.syncElUpload(val)
        let urls = ''
        if (Array.isArray(val)) {
            urls = val.join(',')
        // 同步v-model
        this.$emit('input', urls)
    // 控制达到最大限制时隐藏上传按钮
    isMaxHidden() {
      return this.imgList.length >= this.limit
  },

html的模板分析:

我们需要用到一个拖拽组件 选用vuedraggable 同时需要一个上传组件 选用el-upload即可

上传后需要点击可以显示这个图片的具体详情(原图片的大小和我们上传后的显示不一定是一个大小)选用el-image即可 对于el-image 这个组件 由于点击图片会出现一个弹出框来显示这个图片 所以为了限制z-index 的层级 最好是大于2999 由于el-dialog的显示层级问题

  <div class="uploadWrapper">
    <!--  图片可以拖拽进行排序 -->
    <vuedraggable
      class="vue-draggable"
      :class="{ single: isSingle, maxHidden: isMaxHidden }"
      v-model="imgList"
      tag="ul"
      :data="{ biz: bizPath }"
      draggable=".draggable-item"
      @start="onDragStart"
      @end="onDragEnd"
      <!-- 拖拽元素 -->
        v-for="(item, index) in imgList"
        :key="item + index"
        class="draggable-item"
        :style="{ width: width + 'px', height: height + 'px' }"
        <el-image :z-index="3000" :src="item" :preview-src-list="[item]"></el-image>
        <div class="shadow" @click="onRemoveHandler(index)">
          <i class="el-icon-delete"></i>
      <!-- 上传按钮 -->
      <el-upload
        slot="footer"
        ref="uploadRef"
        class="uploadBox"
        :style="{ width: width + 'px', height: height + 'px' }"
        :action="postAction"
        :headers="headers"
        accept=".jpg,.jpeg,.png,.gif"
        :show-file-list="false"
        :multiple="!isSingle"
        :limit="limit"
        :before-upload="beforeUpload"
        :on-success="onSuccessUpload"
        :on-exceed="onExceed"
        <i class="el-icon-plus uploadIcon">
          <span class="uploading" v-show="isUploading">正在上传...</span>
          <span v-if="!isUploading && limit && limit !== 99 && !isSingle" class="limitTxt">最多{{ limit }}张</span>
      </el-upload>
    </vuedraggable>
</template>

初始化

在一开始的组件加载的过程中 一般的上传需要在data里面传递token的值 所以不妨在初始化阶段获取即可

  created() {
    // 上传的token
    const token = Vue.ls.get(ACCESS_TOKEN)
    this.headers = { 'X-Access-Token': token }
  },

watch属性

在图片的上传过程或者是上传后的图片删除了 由于图片是一个数组 我们需要对这个imageList 要做增加或者删除的操作 所以需要对这个数组进行深度检测

watch: {
    value: {
      handler(val) {
        if (this.isFirstMount && this.value.length > 0) {
          this.syncElUpload()
      deep: true,
  },

方法体之删除

的图片操作需要确定操作即可

// 移除单张图片
    onRemoveHandler(index) {
      this.$confirm('确定删除该图片?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
        .then(() => {
          this.imgList = this.imgList.filter((v, i) => {
            return i !== index
        .catch(() => {})

上传的图片之前的操作

 // 上传图片之前
    beforeUpload(file) {
      this.isFirstMount = false
      if (validImgUpload(file, this.size)) {
        this.isUploading = true
        return true
      } else {
        return false
    },

上传图片成功后

const imgList = [].concat(this.imgList)
          this.imgList = imgList

以上这个代码 不能直接写this.imgList = imgList 考虑到一个情况就是多个图片上传后 那么会导致视图不会更新这个图片 这个是因为第一张图片上传后就直接更新了数组 但是没有更新到视图里面 所以这个时候 我们不妨先手动的用一个空数组来生成一个新的数组 然后在赋值 这样vue就能检测到这个数组变化 然后更新到视图了

 // 上传完单张图片
    onSuccessUpload(res, file, fileList) {
         console.log('onSuccessUpload', res)
      // 这里需要根据你自己的接口返回数据格式和层级来自行修改
      if (res.success) {
        // 判断接口上传成功
        if (this.imgList.length < this.limit) {
          // 未超限时,把接口返回的图片url地址添加到imgList
          this.imgList.push(res.result.filePath)
          const imgList = [].concat(this.imgList)
          this.imgList = imgList
          // Vue.$set(this.imgList, this.imgList)
      } else {
        // 判断接口上传失败
        this.syncElUpload()
        this.$message({ type: 'error', message: res.msg })
      this.isUploading = false
    },

如果超出限制

 // 超限
    onExceed() {
      this.$refs.uploadRef.abort() // 取消剩余接口请求
      this.syncElUpload()
      this.$message({
        type: 'warning',
        message: `图片超限,最多可上传${this.limit}张图片`,
    },

拖拽的过程

在移动开始的时候对item图片增加一个外阴影 这样交互更友好一点 同时如果是结束了 那么就需要消除这个阴影

onDragStart(e) {
      e.target.classList.add('hideShadow')
    onDragEnd(e) {
      e.target.classList.remove('hideShadow')
    },

整个代码如下

<template>
  <div class="uploadWrapper">
    <!--  图片可以拖拽进行排序 -->
    <vuedraggable
      class="vue-draggable"
      :class="{ single: isSingle, maxHidden: isMaxHidden }"
      v-model="imgList"
      tag="ul"
      :data="{ biz: bizPath }"
      draggable=".draggable-item"
      @start="onDragStart"
      @end="onDragEnd"
      <!-- 拖拽元素 -->
        v-for="(item, index) in imgList"
        :key="item + index"
        class="draggable-item"
        :style="{ width: width + 'px', height: height + 'px' }"
        <el-image :z-index="3000" :src="item" :preview-src-list="[item]"></el-image>
        <div class="shadow" @click="onRemoveHandler(index)">
          <i class="el-icon-delete"></i>
      <!-- 上传按钮 -->
      <el-upload
        slot="footer"
        ref="uploadRef"
        class="uploadBox"
        :style="{ width: width + 'px', height: height + 'px' }"
        :action="postAction"
        :headers="headers"
        accept=".jpg,.jpeg,.png,.gif"
        :show-file-list="false"
        :multiple="!isSingle"
        :limit="limit"
        :before-upload="beforeUpload"
        :on-success="onSuccessUpload"
        :on-exceed="onExceed"
        <i class="el-icon-plus uploadIcon">
          <span class="uploading" v-show="isUploading">正在上传...</span>
          <span v-if="!isUploading && limit && limit !== 99 && !isSingle" class="limitTxt">最多{{ limit }}张</span>
      </el-upload>
    </vuedraggable>
</template>
<script>
import vuedraggable from 'vuedraggable'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import { validImgUpload } from './utils'
import Vue from 'vue'
export default {
  name: 'ImgUpload',
  props: {
    bizPath: {
      type: String,
      required: false,
      default: 'temp',
    postAction: {
      type: String,
      default: window._CONFIG['domianURL'] + '/sys/common/upload', // 默认的action url数据
    // 图片数据(图片url组成的数组) 通过v-model传递
    value: {
      type: String,
      default() {
        return ''
    // 限制上传的图片数量
    limit: {
      type: Number,
      default: 99,
    // 限制上传图片的文件大小(kb)
    size: {
      type: Number,
      default: 5000,
    // 是否是单图上传(单图上传就是已传图片和上传按钮重叠)
    isSingle: {
      type: Boolean,
      default: false,
    // 图片显示的宽度(px)
    width: {
      type: Number,
      default: 100,
    // 图片显示的高度(px)
    height: {
      type: Number,
      default: 100,
  data() {
    return {
      headers: {},
      isUploading: false, // 正在上传状态
      isFirstMount: true, // 控制防止重复回显
  created() {
    // 上传的token
    const token = Vue.ls.get(ACCESS_TOKEN)
    this.headers = { 'X-Access-Token': token }
  computed: {
    // 图片数组数据
    imgList: {
      get() {
          // 以逗号开始显示为逗号即可
          if(this.value == '') {
              return []
          }else {
            const urls = (this.value + '').split(',')
            return urls                     
      set(val) {
        if (val.length < this.imgList.length) {
          // 判断是删除图片时同步el-upload数据
          this.syncElUpload(val)
        let urls = ''
        if (Array.isArray(val)) {
            urls = val.join(',')
        // 同步v-model
        this.$emit('input', urls)
    // 控制达到最大限制时隐藏上传按钮
    isMaxHidden() {
      return this.imgList.length >= this.limit
  watch: {
    value: {
      handler(val) {
        if (this.isFirstMount && this.value.length > 0) {
          this.syncElUpload()
      deep: true,
  mounted() {
    if (this.value.length > 0) {
      this.syncElUpload()
  methods: {
    createUniqueString() {
      const timestamp = +new Date() + ''
      const randomNum = parseInt((1 + Math.random()) * 65536) + ''
      return (+(randomNum + timestamp)).toString(32)
    // 同步el-upload数据
    syncElUpload(val) {
      const imgList = val || this.imgList
      this.$refs.uploadRef.uploadFiles = imgList.map((v, i) => {
        return {
          name: 'pic' + i,
          url: v,
          status: 'success',
          uid: this.createUniqueString(),
      this.isFirstMount = false
    // 上传图片之前
    beforeUpload(file) {
      this.isFirstMount = false
      if (validImgUpload(file, this.size)) {
        this.isUploading = true
        return true
      } else {
        return false
    // 上传完单张图片
    onSuccessUpload(res, file, fileList) {
         console.log('onSuccessUpload', res)
      // 这里需要根据你自己的接口返回数据格式和层级来自行修改
      if (res.success) {
        // 判断接口上传成功
        if (this.imgList.length < this.limit) {
          // 未超限时,把接口返回的图片url地址添加到imgList
          this.imgList.push(res.result.filePath)
          const imgList = [].concat(this.imgList)
          this.imgList = imgList
          // Vue.$set(this.imgList, this.imgList)
      } else {
        // 判断接口上传失败
        this.syncElUpload()
        this.$message({ type: 'error', message: res.msg })
      this.isUploading = false
    // 移除单张图片
    onRemoveHandler(index) {
      this.$confirm('确定删除该图片?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
        .then(() => {
          this.imgList = this.imgList.filter((v, i) => {
            return i !== index
        .catch(() => {})
    // 超限
    onExceed() {
      this.$refs.uploadRef.abort() // 取消剩余接口请求
      this.syncElUpload()
      this.$message({
        type: 'warning',
        message: `图片超限,最多可上传${this.limit}张图片`,
    onDragStart(e) {
      e.target.classList.add('hideShadow')
    onDragEnd(e) {
      e.target.classList.remove('hideShadow')
  components: { vuedraggable },
</script>
<style lang="less" scoped>
/deep/ .el-upload {
  width: 100%;
  height: 100%;
// 上传按钮
.uploadIcon {
  width: 100%;
  height: 100%;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed #c0ccda;
  background-color: #fbfdff;
  border-radius: 6px;
  font-size: 20px;
  color: #999;
  .limitTxt,
  .uploading {
    position: absolute;
    bottom: 10%;
    left: 0;
    width: 100%;
    font-size: 14px;
    text-align: center;
// 拖拽
.vue-draggable {
  display: flex;
  flex-wrap: wrap;
  padding-left: 0px;
  padding-right: 15px;
  .draggable-item {
    margin-right: 5px;
    margin-bottom: 5px;
    border: 1px solid #ddd;
    border-radius: 6px;
    position: relative;
    overflow: hidden;
    .el-image {
      width: 100%;
      height: 100%;
    .shadow {
      position: absolute;
      top: 0;
      right: 0;
      background-color: rgba(0, 0, 0, 0.5);
      opacity: 0;
      transition: opacity 0.3s;
      color: #fff;
      font-size: 20px;
      line-height: 20px;
      padding: 2px;
      cursor: pointer;
    &:hover {
      .shadow {
        opacity: 1;
  &.hideShadow {
    .shadow {
      display: none;
  &.single {
    overflow: hidden;
    position: relative;
    .draggable-item {
      position: absolute;
      left: 0;
      top: 0;
      z-index: 1;
  &.maxHidden {
    .uploadBox {
      display: none;
// el-image
.el-image-viewer__wrapper {
  .el-image-viewer__mask {
    opacity: 0.8;