基于element 图片上传封装

我们用过element ui文件上传的朋友都知道,它属性齐全,api强大,但是我们在好多后台系统上传的时候发现,每一个上传我们都需要重新配置一遍,action,headers,data,name,以及上传前校验,上传个数等等。烦不胜烦。有没有办法就写一个图片上传,一个上传方法,其他的通过简单地配置就能实现上传呢,答案当然是有的。今天就带大家来实现一个单图,多图上传封装,其中还包含了图片预览,格式校验,以及图片删除,数据返回类型等操作,满足你的基本要求。本次封装不包含裁剪以及视频和文件上传。这些功能在接下来会逐步完成,希望大家喜欢,先上图!


QQ截图20201112112951.png

我们成功的上传了一张图片,我们再来看一下上传成功后的控制台显示的源文件内容


QQ截图20201112113151.png

我们发现,先是第一行和最后一行的WebKitFormBoundary 码,第二行的ContentDisposition,该行包含一些文件基本信息,还有第三行文件内容类型,后端的同志通过我们传递的文件数据规则,来进行文件解析,
传递的数据规则里包含所传递文件的基本信息 ,如文件名与文件类型,以便后端写出正确格式的文件。

到这里,我们明白了大致的上传原理。接下来,我们来订制一下专属我们的上传组件流程。

首先,我们使用的是axios,不需要像ajax一样自己去通过FormData实例化一个文件fd插入到文件内容中去,我们只需要在element提供的文件上传headers属性中添加进去'ContentType': 'multipart/form-data'就可以实现上传formdata编码类型的传输了。至于为什么要用formdata类型,涉及到二进制文件编码云云,我也是半知半解,想详细了解的同学可以自行查阅,这里不多说。
然后我们就开始写组件的属性,把常用的upload属性都拿过来

props:{
    value:{},
    //action是上传的地址
    action:{
        type:String,
        default: process.env.VUE_APP_BASE_API + process.env.VUE_APP_MY_UPLOAD_URL  || ''
    },
    //最大允许上传个数
    limit: {
        type: Number,
        default: 20
    },
    //是否采用头像上传模式
    avatar:{
        type:Boolean,
        default:false,
    },
    //根据业务需要,设置是否返回给父组件字符串类型
    //或者直接返回数组
    isString:{
        type:Boolean,
        default:false,
    },
    //是否显示已上传文件列表,如果要换成头像上传则设置成false
    //并且把avatar属性设置成true
    showFileList: {
        type: Boolean,
        default: true
    },
    //设置限制上传图片的大小
    size: {
        type: Number,
        default: 2
    },
    //上传时附带的额外字段,看你们后端是否要求,没有则不设置
    dataObj: {
        type: Object,
        default:()=> {
            return {type:5}//这个没有就默认空
        }
    },
    //上传的文件字段名,后端要求的字段名
    name: {
        type: String,
        default: 'files'
    },
    //文件列表的类型,text/picture/picture-card
    listType: {
        type: String,
        default: 'picture-card'
    },
},

这里属性定义完了,我们来重点说一下这个element上传的一个坑点。他的file-list也就是文件上传列表,将来我们用作多图照片墙回显的地方,需要按照它的格式来才行,否则你会发现,渲染不出来。好多的ui组件都是这个模式,移动端的vant组件,需要一个叫image:true的属性,一个叫url的属性。我们得element则是需要[{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}]这样的属性。
所以我们在检测父组件传递过来的value时候,要把原来的['1','2','3']字符串list变成对象的list,即示例要求的图片数组形式。
还有就是我们在监测父组件传递过来的图片数据时,要把它传递过来的数据添加到上传图片成功后的通知变量-------emitList里面,这样我们才能在有父组件传递变量的情况下,我们再接着上传时不会清空或者覆盖父组件传递过来的数据。

//文件上传成功时的钩子
handleSuccess(file,fileInfo){
    //定义一个上传成功的变量,用来暂停监听父组件的数据,
    //因为我们还没有通知父组件接受参数
    this.changeFlag = false;
    this.loading = true;
    let item = file.data[0];
    //头像上传(不带预览)
    if(this.avatar){
        this.imageUrl = item;
        this.$emit('input', item);
    }else{
        //多图上传(带预览)
        this.emitList.push(item);
        let val = [...this.emitList];
        //如果需要回传的是字符串类型,则转字符串回传父组件
        if (this.isString) {
            val = val.join(',')
        }
        this.$emit('input', val);
    }
    this.loading = false;
},

//对父组件传递过来的数据进行监听
watch:{
    value:{
        immediate:true,   
        handler:function(val){
            //this.changeFlag利用这个变量,让我们在上传的时候暂停掉对父组件传递数据的监听
            //因为这个时候我们还没有通知父组件去接受我们上传成功后的图片路径
            if(this.changeFlag){
                //判断是否是头像上传,
                if(!this.avatar && val){
                    //判断是否传递过来的数据是字符串,是字符串转为数组;有的业务需要如此
                    //如果是字符串有可能为空
                    if (this.isString) {
                        val = val.split(',')
                    }
                    //把父组件传递过来的图片数组添加到上传成功后的暂存变量中,
                    //再次上传后重新给父组件,更新图片绑定数据
                    this.emitList = [...val];
                    //循环遍历父组件数据,重新赋值成element组件所需要的回显数据
                    val.forEach(item=>{
                        let obj = {
                            uid: item.uid,
                            name: item.name,
                            url: item
                        }
                        this.fileList.push(obj)
                    })
                    //如果是头像上传则直接赋值给图片路径即可
                }else{
                    this.imageUrl = val;
                }
                
            }
            
        }
    },
},

基本核心代码全部写完,我们的上传回显就成功了。接下来我们再来完成删除和大图预览,我们的整个上传组件就完美写出来了

//我们先来写删除
//文件列表移除文件时的钩子
handleRemove(file, fileList) {
    //同样的暂停掉监听父组件数据,以免产生图片展示bug
    this.changeFlag = false;
     //获取要删除的图片路径。判断是父组件传递的图片图片路径还是刚刚上传产生的图片路径
    const url = file.response ? file.response.data[0] : file.url;
    //遍历当前图片路径在通知给父组件的数据中是哪一个。然后删除掉。再通知父组件更新数据
    this.emitList.forEach((item, index) => {
        if (item.indexOf(url) > -1) {
            this.emitList.splice(index, 1)
            let val = [...this.emitList];
            //判断回传父组件是否是字符串类型
            if (this.isString) {
                val = val.join(',')
            }
            this.$emit('input', val);
        }
    })
},

我们打印一下删除时候的钩子参数,file和fileList,发现删除的时候,我们的父组件传递过来的图片儿数据和上传之后的图片数据,返回值是不太一致的
删除原有的父组件传递过来的图片时,file参数为:


QQ截图20201113084139.png

如果是刚上传后再删除当前上传的图片,file参数为:


QQ截图20201113085543.png

我们发现多了response参数,在这里我们才能取到图片的url,而不是下面最外层的blob本地url。
我们删除的基本思路是找到我们当前要删除的图片路径去对比我们要通知父组件更新的路径数据,两者的交集则是我们要在emitList中删除的。这样一个删除就完成了。接下来我们再来完成预览缩略图大图:
//点击文件列表中已上传的文件时的钩子
handlePreview(file) {
    if (this.showFileList && this.listType == 'picture-card') {
        this.imgSpreadUrl = file.url
        this.imgSpreadVisible = true
    }
},
//预览没什么好说的,打开dialog,然后给弹出层图片一个路径就好了,这个大家都懂,哈哈。

然后我们加上两个小判断,一个是应用头像模式,一个是回传字符串来满足服务端可能需要你的多图上传数据是字符串类型的要求。
最后给大家看一下效果图


QQ截图20201113102512.png

好了,到这里我们的图片上传功能就做好了。最后给大家贴一组件代码和使用方式,希望对你有帮助的朋友能给点个赞,十分感谢!!!!!!!

//上传图片组件
<template>
  <div>
        <el-upload
            v-loading="loading"
          class="upload-demo"
          :action="action"
            :limit="limit"
            :show-file-list="showFileList"
            :file-list="fileList"
            :data="dataObj"
            :name="name"
            :headers="headers"
            :list-type="listType"
            :on-success="handleSuccess"
            :on-change="handleChange"
            :on-exceed="handleExceed"
          :on-preview="handlePreview"
          :on-remove="handleRemove"
            :before-upload="beforeUpload">
          <div v-if="!avatar">
                <i slot="default" class="el-icon-plus"></i>
            </div>
            <div v-else>
                <img v-if="imageUrl" :src="imageUrl" class="avatar">
                <i v-else class="el-icon-plus avatar-uploader-icon"></i>
            </div>
        </el-upload>
        <el-dialog width="800px" :visible.sync="imgSpreadVisible" append-to-body>
          <img class="spread-image" :src="imgSpreadUrl">
        </el-dialog>
    </div>
</template>

<script>
//引入token,一般上传图片接口可能需要token
import { getToken } from '@/utils/auth'
export default {
props:{
    value:{},
    //action是上传的地址
    action:{
        type:String,
        default: process.env.VUE_APP_BASE_API + process.env.VUE_APP_MY_UPLOAD_URL  || ''
    },
    //最大允许上传个数
    limit: {
        type: Number,
        default: 20
    },
    //是否采用头像上传模式
    avatar:{
        type:Boolean,
        default:false,
    },
    //根据业务需要,设置是否返回给父组件字符串类型
    //或者直接返回数组
    isString:{
        type:Boolean,
        default:false,
    },
    //是否显示已上传文件列表,如果要换成头像上传则设置成false
    //并且把avatar属性设置成true
    showFileList: {
        type: Boolean,
        default: true
    },
    //设置限制上传图片的大小
    size: {
        type: Number,
        default: 2
    },
    //上传时附带的额外字段,看你们后端是否要求,没有则不设置
    dataObj: {
        type: Object,
        default:()=> {
            return {type:5}//这个没有就默认空
        }
    },
    //上传的文件字段名,后端要求的字段名
    name: {
        type: String,
        default: 'files'
    },
    //文件列表的类型,text/picture/picture-card
    listType: {
        type: String,
        default: 'picture-card'
    },
},
    watch:{
        value:{
            immediate:true,   
            handler:function(val){
                //this.changeFlag利用这个变量,让我们在上传的时候暂停掉对父组件传递数据的监听
                //因为这个时候我们还没有通知父组件去接受我们上传成功后的图片路径
                if(this.changeFlag){
                    //判断是否是头像上传,
                    if(!this.avatar && val){
                        //判断是否传递过来的数据是字符串,是字符串转为数组;有的业务需要如此
                        //如果是字符串有可能为空
                        if (this.isString) {
                          val = val.split(',')
                        }
                        //把父组件传递过来的图片数组添加到上传成功后的暂存变量中,
                        //再次上传后重新给父组件,更新图片绑定数据
                        this.emitList = [...val];
                        //循环遍历父组件数据,重新赋值成element组件所需要的回显数据
                        val.forEach(item=>{
                            let obj = {
                                uid: item.uid,
                                name: item.name,
                                url: item
                            }
                            this.fileList.push(obj)
                        })
                        //如果是头像上传则直接赋值给图片路径即可
                    }else{
                        this.imageUrl = val;
                    }
                    
                }
                
            }
        },
    },
    computed: {
      // 设置上传的请求头部
      headers() {
        return {
          'ContentType': 'multipart/form-data',// 设置Content-Type类型为multipart/form-data
          'token': getToken()// 设置token
        }
      }
    },
  data() {
    return {
            fileList:[],//element自带的图片list
            emitList:[],//回显的图片list
            imgSpreadVisible:false,//查看缩略图的弹窗状态
            imgSpreadUrl:'',//缩略图大图图片路径
            loading:false,//加载状态
            changeFlag:true,
            imageUrl:''
    }
  },
  methods: {
        //上传前校验一下图片大小
        beforeUpload(file){
            const isLtSize = file.size / 1024 / 1024 < this.size
            if (!isLtSize) {
              this.$message.error('文件大小不能超过 ' + this.size + 'M')
            }
            return isLtSize
        },
        //文件上传成功时的钩子
        handleSuccess(file,fileInfo){
            //定义一个上传成功的变量,用来暂停监听父组件的数据,
            //因为我们还没有通知父组件接受参数
            this.changeFlag = false;
            this.loading = true;
            let item = file.data[0];
            //头像上传(不带预览)
            if(this.avatar){
                this.imageUrl = item;
                this.$emit('input', item);
            }else{
                //多图上传(带预览)
                this.emitList.push(item);
                let val = [...this.emitList];
                //如果需要回传的是字符串类型,则转字符串回传父组件
                if (this.isString) {
                  val = val.join(',')
                }
                this.$emit('input', val);
            }
            this.loading = false;
        },
        //文件状态改变时的钩子
        handleChange(){
            if (this.loading) {
                //用于图片校验
              this.$emit('change', true)
            }
        },
        //文件列表移除文件时的钩子
        handleRemove(file, fileList) {
            //同样的暂停掉监听父组件数据,以免产生图片展示bug
            this.changeFlag = false;
             //获取要删除的图片路径。判断是父组件传递的图片图片路径还是刚刚上传产生的图片路径
            const url = file.response ? file.response.data[0] : file.url;
            //遍历当前图片路径在通知给父组件的数据中是哪一个。然后删除掉。再通知父组件更新数据
            this.emitList.forEach((item, index) => {
              if (item.indexOf(url) > -1) {
                this.emitList.splice(index, 1)
                let val = [...this.emitList];
                    //判断回传父组件是否是字符串类型
                    if (this.isString) {
                      val = val.join(',')
                    }
                this.$emit('input', val);
              }
            })
        },
        //点击文件列表中已上传的文件时的钩子
        handlePreview(file) {
            if (this.showFileList && this.listType == 'picture-card') {
                this.imgSpreadUrl = file.url
                this.imgSpreadVisible = true
            }
        },
        //文件超出个数限制时的钩子
        handleExceed(files, fileList) {
            this.$message.error('最多上传' + this.limit + '个文件')
        },
  }
}
</script>

<style scoped>
    .spread-image{display: block;max-width: 100%;max-height: 500px;margin: auto;}
    .avatar{width: 100%;height: auto;}
</style>

//页面使用
<template>
  <div class="container">
        <!-- 数组类型的多图上传 -->
        <upload-image v-model="arrayImage"></upload-image>
        <!-- 字符串类型的多图上传 -->
        <upload-image v-model="stringImage" :isString="true" ></upload-image>
        <!-- 头像类型的上传 -->
        <upload-image v-model="avatarValue":avatar="true" :showFileList="false"></upload-image>
    </div>
</template>

<script>
import uploadImage from './components/upload.vue'
export default {
    components:{
        uploadImage
    },
  data() {
    return {
      arrayImage:[],//数组类型的多图上传
      stringImage:'https://gcjf.guochengjinfu.cn/20201112/textProPic/1e6a8d9a94504af2b2f0d0136987de31.png',
      //字符串类型多图上传
      avatarValue:'',//头像上传
    }
  }
}
</script>

希望对朋友们有帮助,最后的最后~~~~~~~~~给点个赞吧!谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345