vue elementui 实现图片上传后拖拽排序功能示例代码
醉逍遥neo 人气:0很久没写技术博客了,今天是春节假期前最后一天上班,没什么事情,就随便写写吧,这次就分享一下之前封装的一个图片上传组件的实现过程,这里主要分享下拖拽排序功能的一种实现方式。
2021-11-17 更新:
文末附完整源码及使用示例代码
2021-08-30 更新:
新增vue3支持组件,采用script setup语法+ts语言,并拓展了一些功能,代码见文章末尾。
1、主要技术栈
vue2、elementui2、vuedraggable2
2、需求分析
产品的要求就是多图上传完后,可以对图片列表进行拖拽排序。本身elementui的el-upload组件已经支持很多功能,但是唯独没有拖拽排序,它的多图上传是按你上传时选择的文件的顺序来展示。
一般产品提出的复杂功能咱开发人员都是尽量简化或者砍掉哈哈,不过呢这个功能还是对用户非常友好的,秉承着用户至上的原则那就接下吧。
3、思路
要是用原生js手写一个拖拽功能做起来会很复杂,h5的draggable原生api有点鸡肋,很难实现一个比较好的交互效果,用js的mouseover不错,但是要实现最终完整的交互效果还是很费时间精力,所以还是找插件吧,通过拖拽插件和el-upload结合实现。
然后网上找到一个vuedraggable插件,github传送门,看demo效果还可以,那么问题来了,vuedraggable的使用方式是这样的:
<draggable v-model="myArray" group="people" @start="drag=true" @end="drag=false"> <div v-for="element in myArray" :key="element.id">{{element.name}}</div> </draggable>
就是通过slot插槽的方式传递列表,只对插槽内第一层级的元素起作用。而el-upload使用上传后生成的列表是动态生成的且无法手动控制,也就是说vuedraggable无法作用到el-upload生成的图片列表,那就放弃使用el-upload的图片列表,自己手动写个列表盒子来展示图片,并把这个列表放在vuedraggable组件插槽中,上传完后把获取的url地址赋值过去。
DOM结构大概如下:
<vuedraggable tag="ul" draggable=".draggable-item"> <!-- 拖拽元素 --> <li v-for="(item, index) in imgList" :key="item + index" class="draggable-item" > <el-image :src="item" :preview-src-list="[item]"></el-image> </li> <!-- 上传按钮 --> <el-upload slot="footer"> <i class="el-icon-plus uploadIcon"></i> </el-upload> </vuedraggable>
4、其他一些处理点
(1)图片删除
我的效果是鼠标悬浮在图片上时图片右上角展示删除按钮,鼠标移下时消失,这样会有一个问题就是用鼠标拖拽完图片后可能出现拖拽之前的位置换了新图片但删除按钮还在,处理方式就是给vuedraggable绑定拖拽开始和拖拽结束事件,拖拽开始时添加隐藏删除按钮的类名,使拖拽过程中都不显示删除按钮,拖拽结束再移除这个类名恢复正常。
onDragStart (e) { e.target.classList.add('hideShadow') }, onDragEnd (e) { e.target.classList.remove('hideShadow') }
(2)图片预览
单击图片即可预览,我这里使用了el-image的组件,设置preview-src-list属性就可以实现预览,但是它的预览会保留预览时的状态,包括图片翻页的位置,所以这里就不要它的图片翻页功能了,直接通过数组[]包裹下该图片的地址字符串。
<el-image :src="item" :preview-src-list="[item]"></el-image>
(3)上传数量限制
由于图片上传仍然使用的el-upload组件,而图片上传是接口请求异步的,所以无法通过判断图片展示列表数量来控制图片超限,那还是继续使用el-upload自带的上传限制功能吧,也就是绑定on-exceed属性。
需要处理的一点是在图片展示列表删除单张图片后要同步下el-upload组件里上传完的图片数据,这样它才能正确判断数量是否超限,而它的图片数据都存储在el-upload元素的uploadFiles属性里,这个属性在elementui官方文档里没有说明,可以通过给el-upload绑定一个ref属性,通过this.$refs.uploadRef.uploadFiles获取,里面是一个数组,数组项是对象格式,有name、url、status、uid四个属性,uid需要保证值的唯一性,
5、使用方式示例
引入封装好的组件imgUpload,可以配合element的表单组件el-form:
<el-form label-position="right" label-width="120px" :model="formData" ref="formRef" > <el-form-item label="文章图片:" prop="imgList"> <imgUpload v-model="formData.imgList"/> </el-form-item> </el-form> ...... <script> import imgUpload from '@/components/imgUpload' export default { components: { imgUpload } data () { return { formData: { imgList: [] // formData初始值要带上imgList属性,以保证数据响应式 } } } ...... } </script>
- 使用封装好的组件时通过 v-model=“formData.imgList” 绑定了formData对象的imgList属性,那么在vue的data给formData设置初始值时就必须添加imgList属性,给一个默认值空数组即可。(因为vue2.0默认无法监听对象新增的属性,所以初始结构里就要包含这个属性才能有响应式,或者通过this.$set()手动添加响应式也可以。)
6、其他说明
组件本身对外暴露了很多配置项,包括一些配置项都依赖el-upload组件,这也是没有自己手写上传功能的主要原因。
el-upload标签的action属性是图片上传接口地址,记得换成你们公司自己的稳定接口,然后根据你们接口返回的数据格式修改onSuccessUpload方法里的处理。
7、vue3语法封装
(1)前置条件
- vue版本:v3.2.0以上(因为用到了script setup语法,该语法在v3.2.0才转为正式api,script setup语法使用参考)
- vuedraggable版本:v4.1.0以上
- element-plus版本:v1.0.2-beta.70,因为element-plus一直在不断更新,最新版会有一些不兼容问题,比如新版移除了自带字体图标,只能手动导入。
(2)imgUpload.vue组件完整代码:
传送门
8、常见问题
题外话:
- 这个组件封装已经在实际项目中经受过考验,不要怀疑。
- 对于评论区和私信的问题,基本都是vue的使用问题,对vue的双向数据绑定理解不到位,或者使用经验不足导致的,而非本文组件的封装问题。
(1)删除或拖拽操作后数据没有变化
这种大多是数据响应式的问题,常见于vue2.x版本,vue2.x里数据字段需要在data里提前定义好初始值或者后面通过this.$set手动设置,才能有数据响应式,vue3.x里得益于proxy api的优势不会有这个问题。
特别是数据回显的时候,容易出现问题,列举以下几个场景解决方式:
直接在一个页面里:
方式1:先在data里定义好字段,获取详情接口拿到回显数据后再赋值数据。
data () { return { formData: { imgList: [], } } }, methods: { getDetail () { setTimeout(() => { const res = { imgList: ['https://abc.png'] } this.formData = res }, 1000) } }
方式2:data里未定义字段,获取详情接口拿到回显数据后通过this.$set赋值。
data () { return { formData: {} } }, methods: { getDetail () { setTimeout(() => { const res = { imgList: ['https://abc.png'] } this.$set(this.formData, 'imgList', res.imgList) }, 1000) } }
组件里props接收回显数据:
方式1:先在data里定义好字段,watch监听props接收的回显数据再赋值。
props: { detailData: { type: Object, default () { return {} } } }, data () { return { formData: { imgList: [], } } }, watch: { detailData: { deep: true, handler (val) { this.formData = val } } },
方式2:通过computed get set 配合 this.$set 添加响应式
props: { detailData: { type: Object, default () { return {} } } }, computed: { formData: { get () { const obj = this.detailData !obj.imgList && this.$set(obj, 'imgList', []) return obj }, set (val) { this.$emit('update:formData', val) } }, }
9、完整源码
为了更方便阅读以及代码示例,已经把源码和使用示例上传到了github(传送门),欢迎star。
加载全部内容