element-ui上传多张图片到腾讯云COS

302 阅读2分钟

最近做 vue2 项目,用到了 element-ui,其中有个需求是上传多张图片到腾讯云 COS,这里记录一下实现过程。

大致效果如下:

upload_img1.gif

使用方式

这里外层是一个el-form,增删行是其中一个el-form-item,对应formData的一个字段,代码如下所示:

<el-form :model="formData">
  <el-form-item label="最低版本号">
    <UploadImage v-model="formData.urls" :limit="3"/>
  </el-form-item>
  <!-- 其他el-form-item -->
</el-form>

UploadImage的组件实现

UploadImage的组件逻辑:

  • 使用的是el-upload组件,核心的数据是fileList,其数据结构最重要的是[{url:'https://xxx.png, status:'success'}],status 的状态需要手动更换。设置自定义上传,新增、删除、上传都会触发fileList的变化,然后通过$emit('input', this.fileList.map(item=>item.url))fileList传递给父组件。
  • value:父组件传入的值,即formData.urls,需要在created生命周期中初始化根据 value 来初始化fileList,其实就是value.map(url=>({url,status:'success'}))
  • 这里因为需要额外实现图片预览,所以还有一个el-dialog,点击图片预览,关闭图片预览,不需要的话可以去掉,如果
  • 属性解读,v-bind="this.$attrs"v-on="this.$listeners",这个是为了将父组件传递的属性和事件传递给el-upload,这样就可以使用el-upload的所有属性和事件了。
  • beforeUpload:上传前的校验,这里只是校验是否是图片,可以根据实际情况来校验。
  • upload:上传文件,这里是上传到腾讯云 COS,需要调用腾讯云 COS 的接口,这里只是简单的实现,具体的上传逻辑可以根据实际情况来实现。
  • onAddonRemove:新增、删除文件,这里是同步fileList,然后通过$emit('input', this.fileList.map(item=>item.url))fileList传递给父组件。
  • handlePictureCardPreview:图片预览,点击图片预览,打开el-dialog,关闭图片预览,关闭el-dialog
  • onExceed:超出文件个数限制,这里只是简单的提示,可以根据实际情况来实现。

正常情况,其实内部用 innerValue,然后 computed 计算属性,get 和 set,这样就实现了双向绑定。但是因为 fileList 一旦引用发生变化,el-upload 会重新渲染,所以这里直接用 value,然后在 created 中初始化 fileList。innerValue 的用法可以参考这里的组件封装

以下是UploadImage的组件实现:

<template>
  <div>
    <el-upload
      multiple
      accept="image/*"
      class="upload-img"
      action="#"
      :http-request="upload"
      :show-file-list="true"
      list-type="picture-card"
      :file-list="fileList"
      :on-change="onAdd"
      :on-remove="onRemove"
      :on-exceed="onExceed"
      :before-upload="beforeUpload"
      :limit="limit"
      v-bind="this.$attrs"
      v-on="this.$listeners"
    >
      <i slot="default" class="el-icon-plus"></i>
    </el-upload>
  </div>
</template>
<script>
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';

export default {
  name: 'UploadImg',
  model: {
    prop: 'value', // 默认为 'value'
    event: 'input', // 默认为 'input'
  },
  props: {
    value: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  data() {
    return {
      fileList: (this.value || []).map((url) => ({ status: 'success', url })),
      // 这个太常用了,直接写在data里面
      limit: this.$attrs.limit || 1,
    };
  },
  methods: {
    /** 新增同步fileList */
    onAdd(file, fileList) {
      this.fileList = fileList;
      this.$emit(
        'input',
        this.fileList.map((item) => item.url)
      );
    },
    /** 删除同步fileList */
    onRemove(file, fileList) {
      this.fileList = fileList.filter((item) => item.url !== file.url);
      this.$emit(
        'input',
        this.fileList.map((item) => item.url)
      );
    },
    /** 上传同步fileList */
    async upload(res) {
      if (res.file) {
        // 这里声明状态,fileList能同步更新
        res.file.status = 'loading';
        const url = await uploadFileToCos(res.file, getCosConfig);
        res.file.status = 'success';
        res.file.url = url;
        // 更新fileList的url
        this.fileList.find((item) => item.uid === res.file.uid).url = url;
        // 需要向外
        this.$emit(
          'input',
          this.fileList.map((item) => item.url)
        );
      }
    },
    onExceed(files, fileList) {
      this.$message.error(
        `限制 ${this.limit} 个文件,本次最多能选择 ${
          this.limit - fileList.length
        } 个文件`
      );
    },

    beforeUpload(file) {
      const isImg = file.type.startsWith('image/');
      // const isLt = file.size / 1024 / 1024 < 2

      if (!isImg) {
        this.$message.error('上传只能是 图片格式!');
      }
      // if (!isLt) {
      //     this.$message.error("上传头像图片大小不能超过 2MB!")
      // }
      return isImg;
    },
  },
};
</script>
<style scoped lang="less">
/** 调整添加卡片大小   item是展示卡片大小*/
.upload-img /deep/.el-upload--picture-card,
.upload-img /deep/.el-upload-list--picture-card .el-upload-list__item {
  width: 100px;
  height: 100px;
}
/** line-height是调整加号位置 */
.upload-img /deep/.el-upload--picture-card {
  line-height: 100px;
}
</style>

utils.js

import COS from 'cos-js-sdk-v5';
import uuid from 'react-uuid';

/**
 * 上传文件到腾讯云COS
 * @param {File} file 文件
 * @param {Function} getCosConfig 获取cos配置,大部分时候,上传文件需要一些配置,这里使用了一个异步函数获取cos配置,这边使用临时秘钥,所以每次请求,如果不是临时秘钥,可以直接传入配置.这里是一个异步函数返回Promise对象: bucketName, region, TmpSecretId, TmpSecretKey, XCosSecurityToken, StartTime, ExpiredTime,
 * @param {String} path 上传路径
 * @returns {String} 文件url
 * @example
 * const url = await uploadFileToCos(file, getCosConfig, path)
 * console.log(url)
 */

export async function uploadFileToCos(file, getCosConfig, path = '/App') {
  //
  // 获取cos配置
  const cosConfig = await getCosConfig();
  const cos = new COS({
    getAuthorization: (options, callback) => {
      callback(cosConfig);
    },
  });

  const params = {
    Bucket: cosConfig.bucketName,
    Region: cosConfig.region,
    // Key 可以理解是文件的路径,这里我使用了一个带有uuid的文件名
    Key: `${path}/${addUuidToName(file.name)}`,
    Body: file,
    onProgress: function (progressData) {
      const { loaded, total } = progressData;
      console.log('上传进度', { loaded, total });
    },
  };
  const url = await putObject(cos, params);
  return url;
}

export function putObject(cos, params) {
  return new Promise((resolve, reject) => {
    cos.putObject(params, function (err, data) {
      if (err) {
        console.error('上传失败', err);
        reject(err);
      }
      console.log('上传成功', data.Location);
      const url = `https://${data.Location}`;
      resolve(url);
    });
  });
}

/**
 *
 * @param {*} filename
 * @returns string
 * @example
 * addUuidToName('test.png') // test_6e1fa314-96ef-d686-104c-4b6a67649483.png
 */
function addUuidToName(filename) {
  const dotIndex = filename.lastIndexOf('.');
  if (dotIndex === -1) {
    return `${filename}_${uuid()}`;
  }
  const base = filename.slice(0, dotIndex);
  const suffix = filename.slice(dotIndex);
  return `${base}_${uuid()}${suffix}`;
}

图片超过限制不显示加号

如果图片超过限制,不显示加号,那么需要在el-upload加上类名判断:

<el-upload
  :class="{ 'upload-img': true, 'is-enough': fileList.length >= limit }"
>
  <!-- css部分
 .is-enough /deep/.el-upload--picture-card {
  display: none;
}
 --></el-upload
>

想要加预览功能的话

默认的缩略图不支持预览,一方面需要 slot 重新设计操作图标,一方面需要加预览弹框,这里使用el-dialog,点击图片预览,关闭图片预览,代码如下所示:

<template>
  <div>
    <el-upload ...>
      <i slot="default" class="el-icon-plus"></i>
      <div slot="file" slot-scope="{ file }">
        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
        <span class="el-upload-list__item-actions">
          <span
            class="el-upload-list__item-preview"
            @click="handlePictureCardPreview(file)"
          >
            <i class="el-icon-zoom-in"></i>
          </span>
          <span
            class="el-upload-list__item-delete"
            @click="onRemove(file, fileList)"
          >
            <i class="el-icon-delete"></i>
          </span>
        </span>
      </div>
    </el-upload>

    <el-dialog :visible.sync="dialogVisible" append-to-body>
      <img width="100%" :src="dialogImageUrl" alt="" />
    </el-dialog>
  </div>
</template>
<script>
/**
 * 目前最大的问题是 重新赋值数组 会一抖一抖的  所以并没有检测外围的value变化,只是内部值发生变化,同步外围数据
 */
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';

export default {
  name: 'UploadImg',
  // ...
  data() {
    return {
      dialogImageUrl: '',
      dialogVisible: false,
      // ...
    };
  },
  methods: {
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    },
    //  ...
  },
};
</script>
<style scoped lang="less"></style>

上面所有功能的的代码

<template>
  <div>
    <el-upload
      multiple
      accept="image/*"
      :class="{ 'upload-img': true, 'is-enough': fileList.length >= limit }"
      action="#"
      :http-request="upload"
      :show-file-list="true"
      list-type="picture-card"
      :file-list="fileList"
      :on-change="onAdd"
      :on-remove="onRemove"
      :on-exceed="onExceed"
      :limit="limit"
      :before-upload="beforeUpload"
      v-bind="this.$attrs"
      v-on="this.$listeners"
    >
      <i slot="default" class="el-icon-plus"></i>
      <div slot="file" slot-scope="{ file }">
        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
        <span class="el-upload-list__item-actions">
          <span
            class="el-upload-list__item-preview"
            @click="handlePictureCardPreview(file)"
          >
            <i class="el-icon-zoom-in"></i>
          </span>
          <span
            v-if="!disabled"
            class="el-upload-list__item-delete"
            @click="onRemove(file, fileList)"
          >
            <i class="el-icon-delete"></i>
          </span>
        </span>
      </div>
    </el-upload>

    <el-dialog :visible.sync="dialogVisible" append-to-body>
      <img width="100%" :src="dialogImageUrl" alt="" />
    </el-dialog>
  </div>
</template>
<script>
/**
 * 目前最大的问题是 重新赋值数组 会一抖一抖的  所以并没有检测外围的value变化,只是内部值发生变化,同步外围数据
 */
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';

function formatFileListToUrlList(fileList) {
  return fileList.map((item) => item.url);
}
export default {
  name: 'UploadImg',
  model: {
    prop: 'value', // 默认为 'value'
    event: 'input', // 默认为 'input'
  },
  props: {
    value: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  data() {
    return {
      dialogImageUrl: '',
      dialogVisible: false,
      disabled: false,
      fileList: (this.value || []).map((url) => ({ status: 'success', url })),
      limit: this.$attrs.limit || 1,
    };
  },
  methods: {
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    },
    /** 新增同步fileList */
    onAdd(file, fileList) {
      console.log(file, fileList);
      this.fileList = fileList;
      this.$emit('input', formatFileListToUrlList(fileList));
    },
    /** 删除同步fileList */
    onRemove(file, fileList) {
      console.log('remove', file, fileList);
      this.fileList = fileList.filter((item) => item.url !== file.url);
      this.$emit('input', formatFileListToUrlList(this.fileList));
    },
    async upload(res) {
      if (res.file) {
        // 这里声明状态,fileList能同步更新
        res.file.status = 'loading';
        const url = await uploadFileToCos(res.file, getCosConfig);
        res.file.status = 'success';
        res.file.url = url;
        this.fileList.find((item) => item.uid === res.file.uid).url = url;
        // 需要向外
        this.$emit('input', formatFileListToUrlList(this.fileList));
      }
    },
    onExceed(files, fileList) {
      this.$message.error(
        `限制 ${this.limit} 个文件,本次最多能选择 ${
          this.limit - fileList.length
        } 个文件`
      );
    },

    beforeUpload(file) {
      const isImg = file.type.startsWith('image/');
      // const isLt = file.size / 1024 / 1024 < 2

      if (!isImg) {
        this.$message.error('上传只能是 图片格式!');
      }
      // if (!isLt) {
      //     this.$message.error("上传头像图片大小不能超过 2MB!")
      // }
      return isImg;
    },
  },
};
</script>
<style scoped lang="less">
/** 调整添加卡片大小   item是展示卡片大小*/
.upload-img {
  &.is-enough {
    /deep/.el-upload--picture-card {
      display: none;
    }
  }
  /deep/.el-upload--picture-card,
  /deep/.el-upload-list--picture-card .el-upload-list__item {
    width: 100px;
    height: 100px;
  }
  /** line-height是调整加号位置 */
  /deep/.el-upload--picture-card {
    line-height: 100px;
  }
}
</style>

我这边限制10张图片,效果如下 upload_img2.gif