很久沒寫技術博客了,今天是春節假期前最後一天上班,沒什麼事情,就隨便寫寫吧,這次就分享一下之前封裝的一個圖片上傳組件的實現過程,所以主要分享下拖拽排序功能的一種實現方式。
1、主要技術棧
vue、elementui、vuedraggable
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、組件完整功能
組件本身對外暴露了很多配置項,包括一些配置項都依賴el-upload組件,所以這也是沒有自己手寫上傳功能的主要原因。
完整代碼:
/**
* 圖片上傳 公共組件
*/
<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) {
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'
}
}
}
/**
* 圖片上傳
* @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
}