展示效果
需求
需求想要一个可拖拽排序的图片列表,但是发现el-upload虽然可以实现照片墙,但是没办法拖拽
实现思路
使用
vue-draggable-plus
拖拽插件,隐藏Upload原有的已上传文件列表,自定义上传后文件列表的样式并绑定预览和删除功能
安装VueDraggablePlus库 (尤雨溪推荐)
功能齐全
:全面继承 Sortable.js 的所有功能
无缝迁移
:适用于 Vue 3 和 Vue2
灵活使用
:支持组件、指令、函数式调用
类型强
:用 TypeScript 编写,带有完整的 TS 文档
数据绑定
:支持 v-model 双向绑定,不需要单独维护排序数据
自定义容器
:可以自定某个容器作为拖拽容器,比 Sortable.js 更灵活
安装指令
npm install vue-draggable-plus
yarn add vue-draggable-plus
pnpm add vue-draggable-plus
使用(组件方式)
封装成公共组件 /src/components/DraggableImageUpload/index.vue
HTML代码
<template>
<div class="draggable_image_upload">
<VueDraggable class="box-uploader" ref="draggableRef" v-model="curList" :animation="600" easing="ease-out"
ghostClass="ghost" draggable="ul" @start="onStart" @update="onUpdate">
<!-- 使用element-ui el-upload自带样式 -->
<ul v-for="(item, index) in curList" :key="index" class="el-upload-list el-upload-list--picture-card">
<li class="el-upload-list__item is-success animated">
<el-image class="originalImg" :src="item.url" :preview-src-list="[item.url]" />
<!-- <el-icon>
<Close />
</el-icon> -->
<label class="el-upload-list__item-status-label">
<el-icon class="el-icon--upload-success el-icon--check">
<Check />
</el-icon>
</label>
<span class="el-upload-list__item-actions">
<!-- 预览 -->
<span class="el-upload-list__item-preview" @click="handleImgPreview('originalImg', index)">
<el-icon>
<ZoomIn />
</el-icon>
</span>
<!-- 删除 -->
<span class="el-upload-list__item-delete" @click="onImageRemove(item.url)">
<el-icon>
<Delete />
</el-icon>
</span>
</span>
</li>
</ul>
<!-- 上传组件 -->
<el-upload ref="uploadImages" :show-file-list="false" :multiple="multiple" :auto-upload="autoUpload" action="#"
:on-change="onSkuImgSelect" :on-exceed="(file: any, fileList: any) => ElMessage.warning($t('reg.tip.imgLimit'))"
:file-list="curList" list-type="picture-card" accept=".jpg,.jpeg,.png,.gif" :limit="limit"
style="display: inline;">
<ElImage class="circle-plus" :src="$getIconUrl('circle-plus', 'svg')" fit="scale-down" />
</el-upload>
</VueDraggable>
</div>
</template>
JavaScript代码
<script name="DraggableImageUpload" setup lang="ts">
import { Upload } from '@/api/business/product';
import utils from '@/utils/utils';
import { ComponentInternalInstance, getCurrentInstance } from 'vue';
import { type UseDraggableReturn, VueDraggable } from 'vue-draggable-plus'
const draggableRef = ref<UseDraggableReturn>()
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({
//接受上传的文件类型(thumbnail-mode 模式下此参数无效)
accept: {
type: String,
default: ".jpg,.jpeg,.png"
},
//允许上传文件的最大数量
limit: {
type: Number,
default: 9999
},
//是否支持多选文件
multiple: {
type: Boolean,
default: true
},
//是否自动上传文件
autoUpload: {
type: Boolean,
default: false
},
modelValue: {
type: Array,
default: () => []
},
});
const data = reactive<any>({
maxImgsLen: 0,
imgList: [],
});
const { maxImgsLen, imgList} = toRefs(data)
const emit = defineEmits(["update:modelValue"]);
const curList: any = computed({
get() {
return props.modelValue;
},
set(newValue) {
emit("update:modelValue", newValue);
}
});
//元素开始拖拽
const onStart = () => {
console.log('start')
}
//元素顺序更新时触发
const onUpdate = () => {
console.log(curList.value, 'update++++++++++++++')
}
// 图片预览
const handleImgPreview = (domClass: string, index: number) => {
/* showViewer.value = true
previewList.value = [url] */
const dom = document.getElementsByClassName(domClass);
(dom[index].firstElementChild as any).click(); //调用 el-image 的预览方法
}
// 删除图片
const onImageRemove = (url: string) => {
let list = utils.deepClone(curList.value)
list.forEach((item: any, index: number) => {
if (url === item.url) {
list.splice(index, 1)
}
})
curList.value = list
}
// 监听图片选择后回调
const onSkuImgSelect = (file: any, fileList: any[]) => {
let curLen = fileList.length
maxImgsLen.value = Math.max(curLen, maxImgsLen.value)
setTimeout(() => {
if (curLen !== maxImgsLen.value) return
validatePic(fileList)
});
}
// 校验批量上传图片集合规格合法性
const validatePic = async (fileList: any[]) => {
let tip = ''
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i]
if (!file.id && file.raw) {
const type = file.raw.type,
size = file.raw.size
const isImg =
type === 'image/jpeg' ||
type === 'image/jpg' ||
type === 'image/png' ||
//type === 'image/bmp' ||
type === 'image/gif',
isLt5M = size / 1024 / 1024 < 5
//console.log('是否为图片', type, isImg)
isImg
? isLt5M
? null
: (tip = '上传图片大小不能超过5MB')
: (tip = '上传图片格式不正确')
if (!isImg || !isLt5M) {
setTimeout(() => {
ElMessage.error(tip);
})
fileList.splice(i, 1)
i--
continue
}
}
}
// console.log('处理结果列表------list', fileList)
let defaultCount = 0
fileList.forEach(item => {
item.id ? defaultCount++ : null
})
// console.log('已有图片数量', defaultCount)
if (fileList.length > defaultCount) {
imgList.value.length = 0;
imgList.value.push(...fileList);
uploadImg()
}
maxImgsLen.value = 0
}
// 图集上传方法
const uploadImg = async () => {
const fd = new FormData()
//console.log('二级制对象长度', fd.forEach)
imgList.value.forEach((file: any) => {
if (file.raw) {
console.log('文件后缀', file.raw.type)
fd.append('file', file.raw)
}
})
if (!fd.has('file')) return;
proxy?.$loading({
lock: true,
text: '正在上传,请稍候...',
background: 'rgba(0, 0, 0, 0.7)'
})
//将图片上传至服务器
const { data } = await Upload(fd).finally(() => proxy?.$loading.closeLoading());
if (!data || JSON.stringify(data) === '{}') return;
let curListNew = utils.deepClone(curList.value) //深拷贝
data.forEach((file: any) => {
const { ossId, fileName, savePath, url } = file,
imgObj = {
ossId,
fileName,
savePath,
url
}
curListNew.push(imgObj)
})
curList.value = curListNew
}
</script>
Style
<style lang="scss" scoped>
.draggable_image_upload {
.box-uploader {
display: inline-block;
vertical-align: middle;
:deep(.el-upload) {
border-radius: 4px;
/* display flex
align-items center
justify-content center */
.circle-plus {
width: 24px;
height: 24px;
}
&.el-upload-list {
&.el-upload__item {
width: 100px;
height: 100px;
margin: 0 17px 17px 0;
border-color: #e7e7e7;
padding: 3px;
}
}
&.el-upload--picture-card {
width: 100px;
height: 100px;
line-height: 100px;
border-style: dashed;
margin-right: 17px;
}
}
.el-upload-list__item {
width: 100px;
height: 100px;
margin: 0 17px 17px 0;
border-color: #e7e7e7;
padding: 3px;
.originalImg {
:deep(.el-image__preview) {
width: 100%;
height: 100%;
-o-object-fit: contain;
object-fit: contain;
}
}
}
ul:nth-child(6n+6) {
li {
margin-right: 0;
}
}
}
}
</style>
在 xxx.vue 文件中使用
<template>
<el-dialog title="上传商品图片" v-model="infoVisible" :close-on-click-modal="false">
<!-- 图片拖拽组件 -->
<DraggableImageUpload accept=".jpg,.jpeg,.png,.gif" v-model="curList" />
</el-dialog>
</template>
<script name="" setup lang="ts">
import DraggableImageUpload from '@/components/DraggableImageUpload/index.vue'
import { ComponentInternalInstance, getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const data = reactive<any>({
curList: [], // 需亚预览的数组
infoVisible: false, //预览组件可见性
});
const { infoVisible, curList } = toRefs(data)
</script>
图片预览第二种实现方式
将 li 中 el-image 组件更改为 img标签,引入 el-image-viewer组件,并修改图片预览方法,下面只给出要调整的部分代码
<template>
<li class="el-upload-list__item is-success animated">
<!-- class里的类名不能遗弃,否则将无法使用el-upload样式 -->
<img :src="item.url" alt="Upload Image" class="el-upload-list__item-thumbnail">
<!-- <el-image class="originalImg" :src="item.url" :preview-src-list="[item.url]" /> -->
</li>
<!-- 预览 -->
<span class="el-upload-list__item-preview" @click="handleImgPreview(item.url, index)">
<el-icon>
<ZoomIn />
</el-icon>
</span>
<!-- element-plus图片预览组件 -->
<el-image-viewer @close="()=>{showViewer = false}" v-if="showViewer" :url-list="previewList" />
</template>
<script name="" setup lang="ts">
import { ComponentInternalInstance, getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const data = reactive<any>({
previewList: [],
showViewer: false,
});
const { previewList, showViewer } = toRefs(data)
// 图片预览
const handleImgPreview = (url: string, index: number) => {
showViewer.value = true
previewList.value = [url]
}
</script>