Element UI el-upload 二次封装图片上传组件(可预览、可批量,含手动上传)

先说明几点:1. 使用Vue 2.x。2. 这几个例子是比较适合我自己项目场景的方案,主要为了记录下,仅供参考。样式有引入部分覆盖element-ui的公共样式,因此光用我组件里scoped的样式显示效果不会完全一样。然后单图上传是模拟了element-ui 两种文件列表的样子,实际上属性设置为 :show-file-list="false" ,移除功能也是额外定义而非传值实现的。需要留下心。

一、单图上传(父子组件图片地址双向绑定)

我们先看功能和效果。大致分点击 按钮上传 拖拽上传 ,上传后都可以预览(预览弹窗宽度可传参 dialogWidth: String 自定义)

1. 点击按钮上传 (不传 drag 参数)
:before-upload="beforeUpload" :on-success="handleUploadSuccess" :on-error="handleUploadError" :drag="drag" with-credentials :class="uploadClass" <div v-if="!drag"> <el-button v-if="imageUrl" size="small" type="success">已上传,可点击修改</el-button> <el-button size="small" type="primary" v-else><i class="el-icon-upload el-icon--left"></i>点击上传</el-button> <div v-if="drag"> <i class="el-icon-upload" /> <div class="el-upload__text">将单张图片拖到<br />此处,或<em>点击上传</em></div> </el-upload> <el-dialog :visible.sync="dialogVisible" :append-to-body="true" :modal-append-to-body="false" :width="dialogWidth" class="preview-dialog"> <img width="100%" :src="imageUrl" alt="" /> </el-dialog> <div v-if="!drag && imageUrl.length > 0" class="el-upload-list el-upload-list--text"> <div class="el-upload-list__item is-success"> <a class="el-upload-list__item-name" @click="handlePreview"> <i class="el-icon-picture"></i>点此处预览</a> <label class="el-upload-list__item-status-label"> <i class="el-icon-upload-success el-icon-circle-check"></i> </label> <i class="el-icon-close" @click="removeImage"></i> <div v-if="drag && imageUrl.length > 0" class="el-upload-list el-upload-list--picture-card"> <div class="el-upload-list__item is-success"> <img :src="imageUrl" alt="" class="el-upload-list__item-thumbnail" /> <label class="el-upload-list__item-status-label"> <i class="el-icon-upload-success el-icon-check"></i> </label> <i class="el-icon-close"></i> <i class="el-icon-close-tip">按 delete 键可删除</i> <span class="el-upload-list__item-actions"> <span class="el-upload-list__item-preview"> <i class="el-icon-zoom-in" @click="handlePreview"></i> </span> <span class="el-upload-list__item-delete"> <i class="el-icon-delete" @click="removeImage"></i> </span> </span> <div class="el-upload__tip" slot="tip">只能上传一张图片,格式限jpg/png/gif,大小不超过2M</div> </template> <script> export default { name: 'SingleUpload', props: { value: { type: String, default: '' drag: { type: Boolean, default: false dialogWidth: { type: String, default: '35%' data() { return { dialogVisible: false computed: { uploadUrl() { const url = process.env.VUE_APP_BASE_API + '/admin/single/upload' return url imageUrl() { return this.value ? process.env.VUE_APP_BASE_API + this.value : '' uploadClass() { return this.drag ? 'image-uploader' : '' methods: { removeImage() { this.$confirm(`确定移除已上传的图片?`, '提示').then(() => { this.emitInput('') emitInput(val) { this.$emit('input', val) handleUploadSuccess(response) { this.emitInput(this.commonApi.uploadSuccess(response)) handleUploadError(err) { this.$message.error(err.message) handleUploadRemove() { this.emitInput('') beforeUpload(file) { const typeList = ['image/jpeg', 'image/png', 'image/gif'] const isTypeValid = typeList.includes(file.type) const isLt2M = file.size / 1024 / 1024 < 2 if (!isTypeValid) { this.$message.error('图片格式只能是 JPG/PNG/GIF!') if (!isLt2M) { this.$message.error('图片大小不能超过 2MB!') return isTypeValid && isLt2M handlePreview() { this.dialogVisible = true </script> <style lang="scss" scoped> @import '~@/styles/mixin.scss'; .upload-container { width: 100%; position: relative; display: flex; flex-direction: row; @include clearfix; .image-uploader { width: 150px; margin-right: 20px; .el-icon-upload { margin: 20px 0 16px; font-size: 60px; .el-upload__text { line-height: 20px; font-size: 13px; .el-upload-list--text { margin: 6px 0 0 10px; .image-uploader /deep/ .el-upload-dragger { width: 150px; height: 150px; .el-upload__tip { margin: 0 0 0 15px; .el-upload-list__item:first-child { margin: 0; .preview-dialog /deep/ .el-dialog__header { padding: 0; .preview-dialog /deep/ .el-dialog__body { padding: 20px; </style>
4. 使用

前提是我已经 全局注册 upload-single 组件,若单独引用别忘了在当前组件导入和注册。然后一般情况同个项目上传地址都是一致的,就写死在上传组件里了。用起来基本 只需关注数据字段 ,代码非常简洁。

<template>
  <div class="app-container">
    <!---- * 简单示例,省略其余外层代码 * ---->
    <!---- 1. 点击上传 ---->
    <el-col :span="24">
      <el-form-item label="照片" prop="photoSrc">
        <upload-single v-model="newPlant.photoSrc" />
      </el-form-item>
    </el-col>
    <!---- 2. 拖拽上传,同时定义了预览弹窗宽度,和element-ui官方api一致值是百分比 ---->
    <el-col :span="24">
      <el-form-item label="照片" prop="photoSrc">
        <upload-single v-model="newPlant.photoSrc" dialogWidth="30%" drag />
      </el-form-item>
    </el-col>
</template>
export default {
  name: 'Plant',
  data() {
    return {
      newPlant: {
        // ...
        photoSrc: ''    

一、批量上传

设置 multiple: true 可以让我们同时选取多个文件,然而上传到action地址的时候,则是选取几个文件便调用几次接口,on-success也会触发相应的次数。处理上传成功返回数据的时候要特别注意。

除此之外,el-upload也支持手动上传。(这在多图的时候很有必要,对用户来说可以在上传前确认一遍选择的资源是否正确,对服务端来说可以节省不必要的开销)
在组件设置:auto-upload="false"ref="upload":http-request="handleUpload"之后,执行this.$refs.upload.submit(),便会触发http-request钩子。如果我们在http-request函数中发起上传请求,则仍是fileList有几个文件就调用几次接口,依旧无法实现在一次请求中上传多个文件。例子如下:

<template>
  <el-upload
    ref="upload"
    name="photoSrc"
    :file-list="uploadList"
    :auto-upload="false"
    :http-request="handleUpload"
    multiple
    <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
    <el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>
  </el-upload>
</template>
export default {
  name: 'MultiUpload',
  methods: {
    submitUpload() {
      this.$refs.upload.submit()
    handleUpload(params) {
      // 如果在这里写上传业务,有多少张图片就会触发多少次上传请求
      console.log(params)
选取两张图片,再点击上传到服务器后http-request钩子打印的参数

如果我们想在选取图片后手动上传,并且提交请求时一次性上传多个文件,就选择放弃http-request钩子,也不要再调用this.$refs.upload.submit()。同时on-successon-error也都无用武之地了。
文件状态改变时的钩子on-change也可以帮助我们方便地获取文件数据。on-change事件触发的次数也是根据选取图片的张数来的,我们可以在此处理验证文件格式等,代替自动上传时before-upload进行的逻辑。
action是el-upload的必选属性,但手动上传的情况这个值其实无意义了,我们可以设置为空字符串或者任意字符 :action="''"
再然后,让用户自己选取图片再确认上传这个功能似乎不太合理(头像上传这种单图的情况相对适用)。一方面直观简洁的操作体验更好,还有可能造成部分场景逻辑不连贯:比如图片上传只是表单的一项而已,最终提交表单的时候,用户选取了文件但没有点击上传,是他/她放弃传还是忘记了呢?所以这种情况我们把上传事件放在提交表单里。

说了这么多,你是不是想说,don't bb,show me the code
:class="uploadClass" <el-button v-if="!drag" slot="trigger" size="small" type="primary">选取文件</el-button> <div v-if="drag"> <i class="el-icon-upload" /> <div class="el-upload__text">将单张图片拖到<br />此处,或<em>点击上传</em></div> <!-- <el-button style="margin-left: 10px;" size="small" type="success" @click="handleUpload">上传到服务器</el-button> --> <div slot="tip" class="el-upload__tip"> <span style="color: #f56c6c;">只能上传jpg/png/gif文件,且不超过2M。</span><br /> 图片列表中已经上传成功的图片(<span style="color: #67c23a;">有绿色✓</span>),点击图片名称可以预览大图 </el-upload> <el-dialog :visible.sync="dialogVisible" :append-to-body="true" :modal-append-to-body="false" :width="dialogWidth" class="preview-dialog"> <img width="100%" :src="previewUrl" alt="" /> </el-dialog> </template> <script> import { uploadMult } from '@/api/upload' export default { name: 'MultiUpload', props: { list: { type: Array, default: [] drag: { type: Boolean, default: false dialogWidth: { type: String, default: '35%' data() { return { value: '', fileList: [], // 由于不是简单数据,不能直接用props.list值作为初始值 dialogVisible: false computed: { previewUrl() { return this.value ? process.env.VUE_APP_BASE_API + this.value : '' uploadClass() { return this.drag ? 'image-uploader' : '' watch: { list: { handler(newVal) { // 深拷贝,项目里推荐lodash // this.fileList = _.cloneDeep(this.list) this.fileList = this.list.map(obj => ({ ...obj })) deep: true methods: { fileListChange(file, fileList) { // 添加文件、上传成功和上传失败时都会被调用 const typeList = ['image/jpeg', 'image/png', 'image/gif'] const isTypeValid = typeList.includes(file.raw.type) const isLt2M = file.size / 1024 / 1024 < 2 if (!isTypeValid) return this.$message.error('图片格式只能是 JPG/PNG/GIF!') if (!isLt2M) return this.$message.error('图片大小不能超过 2MB!') this.fileList.push(file) // 能响应式更改数组 // this.$set(this.fileList, this.fileList.length, file) // 能响应式更改数组 filterFileList(uploaded) { // 传 uploaded 筛选已上传的图片列表,不传筛选未上传的 return uploaded ? this.fileList.filter(item => !item.hasOwnProperty('raw')) : this.fileList.filter(item => item.hasOwnProperty('raw')) uploadMultiSuccess(files) { let photoList = [] files.forEach((pic, i) => { const src = pic.path.replace('/public', '') const uid = Date.parse(new Date()) / 1000 + i photoList.push({ name: pic.name, src, uid, status: 'success' }) return photoList async getUploadedList() { /* 逻辑梳理: 1. 点击父组件表单提交按钮触发,这时说明组件已无其他数据操作 2. 父组件需要的数据是fileList中所有不含raw字段的数据,不含的只有list传过来的新成功上传后经过处理的两种可嫩 3. 首先判断有没有含有raw的,如果没有,直接return fileList, 因为fileList的值就等于传过来的list 4. 如果有去批量上传,请求的返回值追加到this.list中,再返给父组件 const toUploadList = this.filterFileList() if (toUploadList.length == 0) return this.fileList let formData = new FormData() toUploadList.forEach(file => formData.append('photoSrc', file.raw, file.name)) const { data } = await uploadMult(formData) return this.filterFileList(true).concat(this.uploadMultiSuccess(data)) handlePreview(file) { if (file.src) { this.value = file.src this.dialogVisible = true handleRemove(file, fileList) { this.fileList = fileList.map(obj => ({ ...obj })) </script> <style lang="scss" scoped> @import '~@/styles/mixin.scss'; .upload-container { width: 100%; position: relative; display: flex; flex-direction: row; flex-flow: wrap; .image-uploader { width: 100%; display: flex; flex-direction: row; .el-icon-upload { margin: 20px 0 16px; font-size: 60px; .el-upload__text { line-height: 20px; font-size: 13px; .el-upload__tip { width: 300px; margin: 0 20px; line-height: 20px; .image-uploader /deep/ .el-upload-list__item:first-child { margin-top: 0px; .image-uploader /deep/ .el-upload-dragger { width: 150px; height: 150px; </style>

父组件使用

<template>
  <div class="app-container">
    <!---- * 简单示例,省略其余外层代码 * ---->
    <!---- 1. 点击上传 ---->
    <el-col :span="24">
      <el-form-item label="照片" prop="photoSrc">
        <upload-multi ref="upload" :list="newClothing.photoSrc" />
      </el-form-item>
    </el-col>
    <!---- 2. 拖拽上传,同时定义了预览弹窗宽度,和element-ui官方api一致值是百分比 ---->
    <el-col :span="24">
      <el-form-item label="照片" prop="photoSrc">
        <upload-multi v-model="newClothing.photoSrc" dialogWidth="40%" drag />
      </el-form-item>
    </el-col>
</template>
export default {
  name: 'Clothing',
  data() {
    return {
     newClothing: {
        // ...
        photoSrc: ''    
  method: