vue+el-upload组件封装(图片,文件上传至oss阿里云)

1、安装ali-oss

npm install ali-oss --save

2、oss方法封装

新建utils/ali-oss-upload.js文件(代码如下)

const OSS = require('ali-oss')

// 文档链接:https://help.aliyun.com/document_detail/32068.html?spm=a2c4g.11186623.6.1291.86b3c107IHjkTR
/**
 * 阿里 OSS 服务 OSS 对象
 * [accessKeyId] {String}:通过阿里云控制台创建的AccessKey。
 * [accessKeySecret] {String}:通过阿里云控制台创建的AccessSecret。
 * [stsToken] {String}:使用临时授权方式,详情请参见使用 STS 进行临时授权。
 * [bucket] {String}:通过控制台或PutBucket创建的bucket。
 * [endpoint] {String}:OSS域名。
 * [region] {String}:bucket所在的区域, 默认oss-cn-hangzhou。
 * [internal] {Boolean}:是否使用阿里云内网访问,默认false。比如通过ECS访问OSS,则设置为true,采用internal的endpoint可节约费用。
 * [cname] {Boolean}:是否支持上传自定义域名,默认false。如果cname为true,endpoint传入自定义域名时,自定义域名需要先同bucket进行绑定。
 * [isRequestPay] {Boolean}:bucket是否开启请求者付费模式,默认false。具体可查看请求者付费模式。
 * [secure] {Boolean}:(secure: true)则使用HTTPS,(secure: false)则使用HTTP,详情请查看常见问题。
 * [timeout] {String|Number}:超时时间,默认60s。
 */
const client = new OSS({
  region: '',
  accessKeyId: '',
  accessKeySecret: '',
  bucket: ''
})

/**
 * OSS 服务文件上传单个文件
 * 同步方式:基于 async 和 await 方式,异步编程同步化。
 * @param {String} objectName 可以自定义为文件名(例如file.txt)或目录(例如abc/test/file.txt)的形式,实现将文件上传至当前Bucket或Bucket下的指定目录。
 * @param {file} file 本地文件或者文件路径
 */
export async function ossPut(objectName, file) {
  try {
    const result = await client.put(objectName, file)
    // console.log(result)
    return result
  } catch (error) {
    // console.log(error)
    return error
  }
}

/**
 * OSS 服务文件上传单个文件
 * 异步方式:API 接口返回 Promise
 * @param {String} objectName 可以自定义为文件名(例如file.txt)或目录(例如abc/test/file.txt)的形式,实现将文件上传至当前Bucket或Bucket下的指定目录。
 * @param {file} file 本地文件或者文件路径
 */
export async function ossAsyncPut(objectName, file) {
  return new Promise((resolve, reject) => {
    client.put(objectName, file)
      .then(res => {
        resolve(res)
      })
      .catch(error => {
        resolve(error)
      })
  })
}

/**
 * OSS 服务文件下载单个文件
 * 同步方式:基于 async 和 await 方式,异步编程同步化。
 * @param {String} objectName
 */
export async function ossGet(objectName) {
  try {
    const result = await client.get(objectName)
    // console.log(result)
    return result
  } catch (error) {
    // console.log(error)
    return error
  }
}

/**
 * OSS 服务文件下载单个文件
 * 异步方式:API 接口返回 Promise
 * @param {String} objectName
 */
export async function ossAsyncGet(objectName) {
  return new Promise((resolve, reject) => {
    client.get(objectName)
      .then(res => {
        resolve(res)
      })
      .catch(error => {
        reject(error)
      })
  })
}

/**
 * OSS 服务文件删除单个文件
 * @param {String} objectName
 */
export async function ossDelete(objectName) {
  try {
    const result = await client.delete(objectName)
    return result
  } catch (error) {
    // console.log(error)
    return error
  }
}

3、组件封装

  1. 图片上传
    1) 涉及到图片转换的方法如下(utils/index.js)
/**
 * base64ToFileObject base64 码转 blob 二进制,再转 file 对象
 * @param  {[type]} base64 [base64码]
 * @param  {string} fileName [转码后 file 对象的名称]
 * @return {[type]} newFile[返回新的file对象]
 */
export function base64ToFileObject(base64, fileName = 'file') {
  const mime = base64.split(',')[0].match(/:(.*?);/)[1]
  base64 = base64.split(',')[1]
  base64 = window.atob(base64)
  const u8arr = new Uint8Array(base64.length)
  for (let i = 0; i < base64.length; i++) {
    u8arr[i] = base64.charCodeAt(i)
  }
  const blob = new Blob([u8arr], { type: mime })
  const suffix = mime.split('/')[1]
  return new File([blob], `${fileName}.${suffix}`, { type: mime })
}

/**
 * imgToBase64 压缩图片
 * @param  {[type]} file      [file对象 event.target.files[0]]
 * @param  {[type]} maxWidth  [最大宽度]
 * @param  {[type]} maxHeight [最大高度]
 * @return {[type]}           [description]
 */

export function imgToBase64(file, maxWidth, maxHeight) {
  return new Promise((resolve, reject) => {
    // 压缩图片需要的一些元素和对象
    const reader = new FileReader()
    const img = new Image()
    img.onload = function() {
      // 图片原始尺寸
      const originWidth = this.width
      const originHeight = this.height
      // 目标尺寸
      let targetWidth = originWidth
      let targetHeight = originHeight
      // 图片尺寸超过400x400的限制
      if (originWidth > maxWidth || originHeight > maxHeight) {
        if (originWidth / originHeight > maxWidth / maxHeight) {
          // 更宽,按照宽度限定尺寸
          targetWidth = maxWidth
          targetHeight = Math.round(maxWidth * (originHeight / originWidth))
        } else {
          targetHeight = maxHeight
          targetWidth = Math.round(maxHeight * (originWidth / originHeight))
        }
      }
      // 缩放图片需要的canvas
      const canvas = document.createElement('canvas')
      const context = canvas.getContext('2d')
      // canvas对图片进行缩放
      canvas.width = targetWidth
      canvas.height = targetHeight
      // 清除画布
      context.clearRect(0, 0, targetWidth, targetHeight)
      // 图片压缩
      context.drawImage(img, 0, 0, targetWidth, targetHeight)
      const base64 = canvas.toDataURL('image/jpeg', 0.8) // 压缩后质量
      // const base64 = canvas.toDataURL()
      // console.log(canvas.toDataURL());
      resolve(base64)
    }
    reader.onload = function(e) {
      img.src = e.target.result
    }
    reader.readAsDataURL(file)
  })
}

2)组件封装,新建uploadImg/index.vue文件,代码如下

<!--
 * @Description:图片上传
 * @Author: duyan
 * @Date: 2021-10-27 19:44:02
 * @LastEditTime: 2021-11-19 16:05:57
 * @LastEditors: duyan
-->
<template>
  <div class="uploadImg">
    <el-upload
      :class="(maxCount>imgLength)?'uploadImgContent':'uploadImgContentNone'"
      :file-list="value"
      :limit="maxCount"
      :drag="isDrag"
      :http-request="onUploadFile"
      :before-upload="handleBeforeUpload"
      :on-preview="handlePictureCardPreview"
      :on-remove="handleRemove"
      :on-error="handleError"
      v-bind="$attrs"
      action=""
      accept="image/png,image/jpg,image/jpeg"
      list-type="picture-card"
      v-on="$listeners"
    >
      <i slot="default" class="el-icon-plus"/>
      <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过2M</div>
    </el-upload>
    <el-dialog :visible.sync="dialogVisible" width="100%" append-to-body>
      <img :src="dialogImageUrl" height="100%" alt="">
    </el-dialog>
  </div>
</template>

<script>
import { imgToBase64, base64ToFileObject } from '@/utils'
import { ossAsyncPut, ossDelete } from '@/utils/ali-oss-upload.js'
import { v4 as uuidv4 } from 'uuid'
export default {
  // 名字
  name: 'UploadImg',
  // 部件
  components: {
  },
  model: {
    prop: 'fileList'
  },
  // 静态
  props: {
    fileList: {
      type: Array,
      default: () => []
    },
    // 是否直接上传到 OSS 服务
    isPutOss: {
      type: Boolean,
      default: false
    },

    // 图片上传到 OSS 服务上的存储目录
    ossPathPrefix: {
      type: String,
      default: ''
    },
    // 图片上传数
    maxCount: {
      type: Number,
      default: 1
    },
    isDrag: {
      type: Boolean,
      default: false
    },
    // eslint-disable-next-line vue/require-default-prop
    uploadSuccess: Function,
    // eslint-disable-next-line vue/require-default-prop
    afterRead: Function
  },
  // 数据
  data() {
    return {
      dialogImageUrl: '',
      dialogVisible: false,
      imgLength: 0
    }
  },
  // 属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。主要当作属性来使用;
  computed: {
    value: {
      get(val) {
        // eslint-disable-next-line vue/no-side-effects-in-computed-properties
        this.imgLength = this.fileList.length
        return this.fileList
      },
      set(val) {
        this.imgLength = val.length
        this.$emit('input', val)
      }
    }
  },
  // 对象内部的属性监听,也叫深度监听
  watch: {
  },
  // 请求数据
  created() {
  },
  mounted() {
  },
  // 方法表示一个具体的操作,主要书写业务逻辑;
  methods: {
    // 图片上传
    onUploadFile(file) {
      // 上传完成
      this.afterRead && this.afterRead(file)
      // 上传oss
      this.isPutOss && this.onUpload(file)
    },
    onUpload(file) {
      file.status = 'uploading'
      file.message = '上传中...'
      this.uploaded(file.file)
        .then(response => {
          const { url } = response
          file.url = url
          file.status = 'done'
          file.message = '上传成功'
          this.value.push(file)
          this.$emit('input', this.value)
          // this.uploadSuccess(file)
          this.$forceUpdate()// 强制渲染
        })
        .catch(() => {
          file.status = 'failed'
          file.message = '上传失败'
          file.url = null
          this.$forceUpdate()
        })
    },
    /**
     * 文件上传至阿里 OSS 服务
     */
    uploaded(file) {
      return new Promise((resolve, reject) => {
        const suffix = file.type.split('/')[1]
        const fileName = `${this.ossPathPrefix}/${uuidv4()}.${suffix}`
        ossAsyncPut(fileName, file)
          .then(({ res, url }) => {
            // console.log(res)
            // console.log(url)
            if (res && res.status === 200) {
              // console.log('上传成功')
              resolve({ res, url })
            } else {
              // console.log('上传失败')
              reject(new Error('图片上传 OSS 服务失败!'))
            }
          })
      })
    },
    // 图片从oss上删除
    _delete(file) {
      console.log(file)
      if (file.url) {
        const startIndex = file.url.indexOf(this.ossPathPrefix)
        const objectName = file.url.substr(startIndex)
        ossDelete(objectName).then(res => {
        })
      }
    },
    handleBeforeUpload(file) {
      return new Promise((resolve, reject) => {
        const fileType = ['image/png', 'image/jpg', 'image/jpeg']
        if (!fileType.includes(file.type)) {
          this.$notify.warning({
            title: '警告',
            message: '请上传格式为image/png, image/jpg, image/jpeg的图片'
          })
          reject()
        }
        // 图片压缩后校验图片大小
        imgToBase64(file, 800, 800).then(base64 => {
          const resultFile = base64ToFileObject(base64)
          const isNeed = resultFile.size / 1024 < 200
          // console.log(resultFile)
          // console.log(resultFile.type, isNeed)
          if (!isNeed) {
            this.$notify.warning({
              title: '警告',
              message: '图片过大'
            })
            reject()
          }
          resolve()
        })
      })
    },
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url
      this.dialogVisible = true
    },
    handleRemove(file, fileList) {
      this.value = fileList
      this._delete(file)
      this.$forceUpdate()
    },
    handleError(err, file, fileList) {
      console.log('error:', err, file, fileList)
    }
  }
}
</script>

<style scoped lang="scss">
.uploadImg{
  .uploadImgContent{
    ::v-deep .el-upload{
      display: inline-block !important ;
    }
  }
  .uploadImgContentNone{
    ::v-deep .el-upload{
      display: none !important;
    }
  }
}
  ::v-deep .el-dialog{
    margin: 5vh 0 !important;
    background: none;
    .el-dialog__headerbtn .el-dialog__close {
      color: #dddddd;
      border: 3px solid #dddddd;
      border-radius: 50%;
      font-size: 28px;
    }
    .el-dialog__body{
      height: calc( 100vh - 20vh );
      overflow: hidden;
      padding:0px 10px 10px 10px;
      text-align: center;
    }
  }
</style>
  1. 文件上传封装
    1) 文件类型使用及判断见此处
    2) 新建uploadFile/index.vue文件,代码如下
<!--
 * @Description:文件上传
 * @Author: duyan
 * @Date: 2021-10-27 19:44:02
 * @LastEditTime: 2021-12-21 11:29:30
 * @LastEditors: duyan
-->
<template>
  <div class="uploadImg">
    <el-upload
      :class="(maxCount>imgLength)?'uploadImgContent':'uploadImgContentNone'"
      :file-list="value"
      :limit="maxCount"
      :drag="isDrag"
      :http-request="onUploadFile"
      :before-upload="handleBeforeUpload"
      :on-preview="handlePictureCardPreview"
      :on-remove="handleRemove"
      :on-error="handleError"
      v-bind="$attrs"
      :accept="fileType"
      :show-file-list="isShowFileList"
      action=""
      v-on="$listeners"
    >
      <div v-show="value.length>
        <!-- 模板下载 -->
        <div v-if="templateDownload">
          <el-dropdown split-button type="success">
            <i class="el-icon-upload2"/>
            {{ btnLabel }}
            <el-dropdown-menu slot="dropdown">
              <el-dropdown-item icon="el-icon-download">
                <!-- <div style="display: inline-block; height:32px;width:100px;background-color:#11b95c;border:transparent;text-align:center;line-height:32px;border-radius:3px;"> -->
                <a :href="downloadUrl">下载导入模板</a>
                <!-- </div> -->
              </el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
        </div>
        <div v-else>
          <el-button size="small" icon="el-icon-plus" type="primary">{{ btnLabel }}</el-button>
          <div slot="tip" class="el-upload__tip">只能上传{{ fileType }}文件</div>
        </div>
      </div>
    </el-upload>
  </div>
</template>

<script>
import { ossAsyncPut, ossDelete } from '@/utils/ali-oss-upload.js'
import { v4 as uuidv4 } from 'uuid'
export default {
  // 名字
  name: 'UploadImg',
  // 部件
  components: {
  },
  model: {
    prop: 'fileList'
  },
  // 静态
  props: {
    // 文件list
    fileList: {
      type: Array,
      default: () => []
    },
    // 是否直接上传到 OSS 服务
    isPutOss: {
      type: Boolean,
      default: false
    },

    // 文件上传到 OSS 服务上的存储目录
    ossPathPrefix: {
      type: String,
      default: ''
    },
    // 文件上传数
    maxCount: {
      type: Number,
      default: 1
    },
    isDrag: {
      type: Boolean,
      default: false
    },
    // 文件类型
    fileType: {
      type: String,
      default: () => '.pdf'
    },
    fileTypeList: {
      type: Array,
      default: () => {
        return ['application/pdf', 'application/doc', 'application/docx']
      }
    },
    // 按钮文字
    btnLabel: {
      type: String,
      default: '点击上传'
    },
    // 是否显示已上传文件列表
    isShowFileList: {
      type: Boolean,
      default: true
    },
    // 是否进行模板下载
    templateDownload: {
      type: Boolean,
      default: false
    },
    // 模板下载链接
    downloadUrl: {
      type: String,
      default: 'https://yzwy1-app.oss-cn-shenzhen.aliyuncs.com/communityModel.xlsx'
    },
    // eslint-disable-next-line vue/require-default-prop
    uploadSuccess: Function,
    // eslint-disable-next-line vue/require-default-prop
    afterRead: Function
  },
  // 数据
  data() {
    return {
      dialogImageUrl: '',
      dialogVisible: false,
      imgLength: 0
    }
  },
  // 属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。主要当作属性来使用;
  computed: {
    value: {
      get(val) {
        // eslint-disable-next-line vue/no-side-effects-in-computed-properties
        this.imgLength = this.fileList.length
        return this.fileList
      },
      set(val) {
        this.imgLength = val.length
        this.$emit('input', val)
      }
    }
  },
  // 对象内部的属性监听,也叫深度监听
  watch: {
  },
  // 请求数据
  created() {
  },
  mounted() {
  },
  // 方法表示一个具体的操作,主要书写业务逻辑;
  methods: {
    // 文件上传
    onUploadFile(file) {
      // 上传完成
      this.afterRead && this.afterRead(file)
      // 上传oss
      this.isPutOss && this.onUpload(file)
    },
    onUpload(file) {
      file.status = 'uploading'
      file.message = '上传中...'
      this.uploaded(file.file)
        .then(response => {
          const { url } = response
          file.url = url
          file.status = 'done'
          file.message = '上传成功'
          file.name = file.file.name
          this.value.push(file)
          this.$emit('input', this.value)
          // this.uploadSuccess(file)
          this.$forceUpdate()// 强制渲染
        })
        .catch(() => {
          file.status = 'failed'
          file.message = '上传失败'
          file.url = null
          this.$forceUpdate()
        })
    },
    /**
     * 文件上传至阿里 OSS 服务
     */
    uploaded(file) {
      return new Promise((resolve, reject) => {
        const suffix = file.type.split('/')[1]
        const fileName = `${this.ossPathPrefix}/${uuidv4()}.${suffix}`
        ossAsyncPut(fileName, file)
          .then(({ res, url }) => {
            // console.log(res)
            // console.log(url)
            if (res && res.status === 200) {
              // console.log('上传成功')
              resolve({ res, url })
            } else {
              // console.log('上传失败')
              reject(new Error('文件上传 OSS 服务失败!'))
            }
          })
      })
    },
    // 文件从oss上删除
    _delete(file) {
      console.log(file)
      if (file.url) {
        const startIndex = file.url.indexOf(this.ossPathPrefix)
        const objectName = file.url.substr(startIndex)
        ossDelete(objectName).then(res => {
        })
      }
    },
    handleBeforeUpload(file) {
      return new Promise((resolve, reject) => {
        const fileType = this.fileTypeList
        // console.log('fileType----', fileType)
        // console.log('file----', file)
        if (!fileType.includes(file.type)) {
          this.$notify.warning({
            title: '警告',
            message: '请上传格式为' + this.fileType + '的文件'
          })
          reject()
        }
        resolve()
      })
    },
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url
      this.dialogVisible = true
    },
    handleRemove(file, fileList) {
      this.value = fileList
      this._delete(file)
      this.$forceUpdate()
    },
    handleError(err, file, fileList) {
      console.log('error:', err, file, fileList)
    }
  }
}
</script>

<style scoped lang="scss">
.uploadImg{
  .uploadImgContent{
    ::v-deep .el-upload{
      display: inline-block !important ;
    }
  }
  .uploadImgContentNone{
    ::v-deep .el-upload{
      display: none !important;
    }
  }
}
  ::v-deep .el-dialog{
    margin: 5vh 0 !important;
    background: none;
    .el-dialog__headerbtn .el-dialog__close {
      color: #dddddd;
      border: 3px solid #dddddd;
      border-radius: 50%;
      font-size: 28px;
    }
    .el-dialog__body{
      height: calc( 100vh - 20vh );
      overflow: hidden;
      padding:0px 10px 10px 10px;
      text-align: center;
    }
  }
  .el-dropdown {
    vertical-align: top;
  }
  .el-dropdown + .el-dropdown {
    margin-left: 15px;
  }
  .el-icon-arrow-down {
    font-size: 12px;
  }
</style>

4.引入使用

代码如下:

<!--
 * @Description:组件使用
 * @Author: duyan
 * @Date: 2021-12-20 10:37:07
 * @LastEditTime: 2021-12-31 15:30:11
 * @LastEditors: duyan
-->
<template>
  <div class="community-administrative">
    <!-- 上传本地返回 -->
    <div style="line-height:35px;">上传表格文件本地返回:</div>
    <upload-file
      :file-type="'.xls,.xlsx'"
      :file-type-list="['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/vnd.ms-excel']"
      v-model="fileListFile"
      :is-put-oss="false"
      :btn-label="downloadLabel"
      :max-count="1"
      :template-download="true"
      :is-show-file-list="false"
      :download-url="'模板下载链接'"
      :after-read="afterRead"/>
    <!-- 上传至oss返回 -->
    <div style="line-height:35px;">上传pdf文件至oss:</div>
    <upload-file
      :file-type="'.pdf'"
      :file-type-list="['application/pdf']"
      v-model="fileListPdf"
      :is-put-oss="true"
      :max-count="1"
      @input="uploadSuccess"/>
    <div style="line-height:35px;">上传图片文件至oss:</div>
    <upload-img v-model="fileList" :is-put-oss="true" :max-count="1" :oss-path-prefix="'yzsurvey_file/PC'" @input="uploadSuccess"/>
  </div>
</template>

<script>
import uploadFile from '@/views/components/uploadFile/index'
import uploadImg from '@/views/components/uploadImg/index'

export default {
  // 名字
  name: 'UploadFile',
  // 部件
  components: {
    uploadFile,
    uploadImg
  },
  // 静态
  props: {
  },
  // 数据
  data() {
    return {

      // word文件
      fileListFile: [],
      // word文件文本
      downloadLabel: '导入至本地文件及模板下载',
      // Pdf文件
      fileListPdf: [],
      // 图片文件文件
      fileList: []
    }
  },
  // 属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。主要当作属性来使用;
  computed: {
  },
  // 对象内部的属性监听,也叫深度监听
  watch: {
  },
  // 请求数据
  created() {
  },
  mounted() {
  },
  // 方法表示一个具体的操作,主要书写业务逻辑;
  methods: {
    /**
     * @description:文件上传成功返回
     * @param  {*}
     * @return {*}
     * @param {*} file 本地文件信息
     */
    afterRead(file) {
      console.log('file-----', file)
    },
    /**
     * @description:图片/文件上传线上成功返回
     * @param  {*}
     * @return {*}
     * @param {*} data 文件信息及线上链接
     */
    uploadSuccess(data) {
      console.log('imageData-----', data)
    }
  }
}
</script>

<style scoped lang="scss">
.community-administrative{
   padding:10px 10px 0 10px;
}
</style>

效果展示如下:
vue+el-upload组件封装(图片,文件上传至oss阿里云)_第1张图片

文见传入格式点此处查看

使用以上组件
存在问题:上传会存在上下闪动问题
出现原因:fileList双向绑定会导致value赋值两次
点此处进行办法解决>>

你可能感兴趣的:(vue.js,阿里云,前端)