很久没写技术博客了,今天是春节假期前最后一天上班,没什么事情,就随便写写吧,这次就分享一下之前封装的一个图片上传组件的实现过程,这里主要分享下拖拽排序功能的一种实现方式。
vue、elementui、vuedraggable
产品的要求就是多图上传完后,可以对图片列表进行拖拽排序。本身elementui的el-upload组件已经支持很多功能,但是唯独没有拖拽排序,它的多图上传是按你上传时选择的文件的顺序来展示。
一般产品提出的复杂功能咱开发人员都是尽量简化或者怼回去哈哈,不过呢这个需求功能还是对用户非常友好的。。。
你要用原生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>
我做的效果是鼠标悬浮在图片上时图片右上角展示删除按钮,鼠标移下时消失,这样会有一个问题就是用鼠标拖拽完图片后可能出现拖拽之前的位置换了新图片但删除按钮还在,处理方式就是给vuedraggable绑定拖拽开始和拖拽结束事件,拖拽开始时添加隐藏删除按钮的类名,使拖拽过程中都不显示删除按钮,拖拽结束再移除这个类名恢复正常。
onDragStart (e) {
e.target.classList.add('hideShadow')
},
onDragEnd (e) {
e.target.classList.remove('hideShadow')
}
单击图片预览即可,我这里使用了el-image的组件,设置preview-src-list属性就可以实现预览,但是它的预览会保留预览时的状态,包括图片翻页的位置,所以这里就不要它的图片翻页功能了,直接通过数组[]包裹下该图片的地址字符串。
<el-image :src="item" :preview-src-list="[item]">el-image>
由于图片上传仍然使用的el-upload组件,而图片上传是接口请求异步的,所以无法通过判断图片展示列表数量来控制图片超限,那还是继续使用el-upload自带的上传限时功能吧,也就是绑定on-exceed属性。
需要处理的一点是在图片展示列表删除单张图片后要同步下el-upload组件里上传完的图片数据,这样它才能正确判断数量是否超限,而它的图片数据都存储在el-upload元素的uploadFiles属性里,这个属性在elementui官方文档里没有说明,可以通过给el-upload绑定一个ref属性,通过this.$refs.uploadRef.uploadFiles获取,里面是一个数组,数组里每一项是个对象,有name、url、status、uid四个属性,uid需要保证值的唯一性,
/**
* 图片上传 公共组件
*/
<template>
<div class="uploadWrapper">
<vuedraggable
class="vue-draggable"
:class="{ single: isSingle, maxHidden: isMaxHidden }"
v-model="imgList"
tag="ul"
draggable=".draggable-item"
@start="onDragStart"
@end="onDragEnd"
>
<li
v-for="(item, index) in imgList"
:key="item + index"
class="draggable-item"
:style="{
width: width + 'px', height: height + 'px' }"
>
<el-image :src="item" :preview-src-list="[item]">el-image>
<div class="shadow" @click="onRemoveHandler(index)">
<i class="el-icon-delete">i>
div>
li>
<el-upload
slot="footer"
ref="uploadRef"
class="uploadBox"
:style="{
width: width + 'px', height: height + 'px' }"
action="https://httpbin.org/post"
:headers="headers"
accept=".jpg,.jpeg,.png,.gif"
:show-file-list="false"
:multiple="!isSingle"
:limit="limit"
:before-upload="beforeUpload"
:on-success="onSuccessUpload"
:on-exceed="onExceed"
>
<i class="el-icon-plus uploadIcon">
<span class="uploading" v-show="isUploading">正在上传...span>
<span
v-if="!isUploading && limit && limit!==99 && !isSingle"
class="limitTxt"
>最多{
{ limit }}张span>
i>
el-upload>
vuedraggable>
div>
template>
<script>
import vuedraggable from 'vuedraggable'
import {
getToken } from '@/utils/auth' // 获取token,用于后端接口登录校验,根据公司的业务自行移除或替换就行
import {
validImgUpload } from '@/utils/validate'
import lrz from 'lrz' // 前端图片压缩插件
import tools from '@/utils/tools'
export default {
name: 'ImgUpload',
props: {
// 图片数据(图片url组成的数组) 通过v-model传递
value: {
type: Array,
default () {
return []
}
},
// 限制上传的图片数量
limit: {
type: Number,
default: 99
},
// 限制上传图片的文件大小(kb)
size: {
type: Number,
default: 500
},
// 是否是单图上传(单图上传就是已传图片和上传按钮重叠)
isSingle: {
type: Boolean,
default: false
},
// 是否使用图片压缩
useCompress: {
type: Boolean,
default: false
},
// 图片显示的宽度(px)
width: {
type: Number,
default: 100
},
// 图片显示的高度(px)
height: {
type: Number,
default: 100
}
},
data () {
return {
headers: {
token: getToken() },
isUploading: false, // 正在上传状态
isFirstMount: true // 控制防止重复回显
}
},
computed: {
// 图片数组数据
imgList: {
get () {
return this.value
},
set (val) {
if (val.length < this.imgList.length) {
// 判断是删除图片时同步el-upload数据
this.syncElUpload(val)
}
// 同步v-model
this.$emit('input', val)
}
},
// 控制达到最大限制时隐藏上传按钮
isMaxHidden () {
return this.imgList.length >= this.limit
}
},
watch: {
value: {
handler (val) {
if (this.isFirstMount && this.value.length > 0) {
this.syncElUpload()
}
},
deep: true
}
},
mounted () {
if (this.value.length > 0) {
this.syncElUpload()
}
},
methods: {
// 同步el-upload数据
syncElUpload (val) {
const imgList = val || this.imgList
this.$refs.uploadRef.uploadFiles = imgList.map((v, i) => {
return {
name: 'pic' + i,
url: v,
status: 'success',
uid: tools.createUniqueString()
}
})
this.isFirstMount = false
},
// 上传图片之前
beforeUpload (file) {
this.isFirstMount = false
if (this.useCompress) {
// 图片压缩
return new Promise((resolve, reject) => {
lrz(file, {
width: 1920 }).then((rst) => {
file = rst.file
}).always(() => {
if (validImgUpload(file, this.size)) {
this.isUploading = true
resolve()
} else {
reject(new Error())
}
})
})
} else {
if (validImgUpload(file, this.size)) {
this.isUploading = true
return true
} else {
return false
}
}
},
// 上传完单张图片
onSuccessUpload (res, file, fileList) {
// 这里需要根据你自己的接口返回数据格式和层级来自行修改
if (res.files) {
// 判断接口上传成功
if (this.imgList.length < this.limit) {
// 未超限时,把接口返回的图片url地址添加到imgList
this.imgList.push(res.files.file)
}
} else {
// 判断接口上传失败
this.syncElUpload()
this.$message({
type: 'error', message: res.msg })
}
this.isUploading = false
},
// 移除单张图片
onRemoveHandler (index) {
this.$confirm('确定删除该图片?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
this.imgList = this.imgList.filter((v, i) => {
return i !== index
})
})
.catch(() => {
})
},
// 超限
onExceed () {
this.$refs.uploadRef.abort() // 取消剩余接口请求
this.syncElUpload()
this.$message({
type: 'warning',
message: `图片超限,最多可上传${
this.limit}张图片`
})
},
onDragStart (e) {
e.target.classList.add('hideShadow')
},
onDragEnd (e) {
e.target.classList.remove('hideShadow')
}
},
components: {
vuedraggable }
}
script>
<style lang="less" scoped>
/deep/ .el-upload {
width: 100%;
height: 100%;
}
// 上传按钮
.uploadIcon {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #c0ccda;
background-color: #fbfdff;
border-radius: 6px;
font-size: 20px;
color: #999;
.limitTxt,
.uploading {
position: absolute;
bottom: 10%;
left: 0;
width: 100%;
font-size: 14px;
text-align: center;
}
}
// 拖拽
.vue-draggable {
display: flex;
flex-wrap: wrap;
.draggable-item {
margin-right: 5px;
margin-bottom: 5px;
border: 1px solid #ddd;
border-radius: 6px;
position: relative;
overflow: hidden;
.el-image {
width: 100%;
height: 100%;
}
.shadow {
position: absolute;
top: 0;
right: 0;
background-color: rgba(0,0,0,.5);
opacity: 0;
transition: opacity .3s;
color: #fff;
font-size: 20px;
line-height: 20px;
padding: 2px;
cursor: pointer;
}
&:hover {
.shadow {
opacity: 1;
}
}
}
&.hideShadow {
.shadow {
display: none;
}
}
&.single {
overflow: hidden;
position: relative;
.draggable-item {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
}
&.maxHidden {
.uploadBox {
display: none;
}
}
}
// el-image
.el-image-viewer__wrapper {
.el-image-viewer__mask {
opacity: .8;
}
.el-icon-circle-close {
color: #fff;
}
}
style>
<el-form
label-position="right"
label-width="120px"
:model="formData"
class="formBox"
:rules="rules"
ref="formRef"
>
<el-form-item label="文章图片:" prop="from">
<imgUpload v-model="formData.imgList"/>
el-form-item>
el-form>
/**
* 创建唯一的字符串
* @return {string} ojgdvbvaua40
*/
function createUniqueString () {
const timestamp = +new Date() + ''
const randomNum = parseInt((1 + Math.random()) * 65536) + ''
return (+(randomNum + timestamp)).toString(32)
}
/**
* 数字存储大小格式化
* @param {number} num 存储大小 单位:Byte
* @param {number} digits 保留几位小数
* @return {string} 2MB
*/
function toStorage (num, digits) {
digits = digits || 2
if (num < 1024) {
return num + 'B'
}
num = (num * 1000 / 1024)
const si = [
{
value: 1E18, symbol: 'E' },
{
value: 1E15, symbol: 'P' },
{
value: 1E12, symbol: 'T' },
{
value: 1E9, symbol: 'G' },
{
value: 1E6, symbol: 'M' },
{
value: 1E3, symbol: 'K' }
]
for (let i = 0; i < si.length; i++) {
if (num >= si[i].value) {
return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') +
si[i].symbol + 'B'
}
}
}
export default {
createUniqueString,
toStorage
}
import tools from '@/utils/tools'
/**
* 图片上传
* @param {file} file el-upload文件对象
* @param {number} size 限制的文件大小(kb) 默认10M
*/
export const validImgUpload = (file, size) => {
size = +size || 10240
const isSizeOut = file.size / 1024 > size
if (isSizeOut) {
Message.error('上传图片大小不能超过' + tools.toStorage(size * 1024))
}
return !isSizeOut
}