一、问题
1.以前的需求已经通过Aupload封装了 文件上传组件,现在希望添加 文件拖拽排序功能。
2.首先想到可以使用 vuedraggable来实现。但是 vuedraggable需要传递数组并且在插槽里面指定单个元素的渲染方式,但是原本的Aupload组件是直接渲染数组的,要怎么兼容呢?
二、解决方法
1.方法一:不用Aupload了,自己实现单个文件的渲染fileCard以及文件上传逻辑,在vueDraggable中复用fileCard组件。之前的很多逻辑都不能用了需要重写,工作量比较大,还有可能有性能问题
2.方法二:在当前的基础上直接添加拖拽功能。本文采用方案二,具体实现如下
- 实现拖拽的功能:拿到上传的文件数据就行=> 拖拽区域的数据和上传区域的数据共用,拖拽结束后同步修改父组件传入的数据
html
<Draggable
v-model="innerFileList"
item-key="uid"
:animation="animation"
tag="div"
class="drag-list"
@end="handleDragEnd"
>
<!-- 拖拽区域 -->
<template #item="{ element }">
<div class="drag-item">
<AUpload
:file-list="[element]"
:list-type="listType"
@remove="handleRemove"
@preview="handlePreview"
>
</AUpload>
</div>
</template>
</Draggable>
<div>
<template v-if="!maxCount || innerFileList.length < maxCount">
<AUpload
v-bind="bind"
v-model:file-list="innerFileList"
:beforeUpload="handleBeforeUpload"
:showUploadList="false"
@remove="handleRemove"
@change="handleChange"
>
<!-- 添加图片 -->
<PlusOutlined />
<div class="ml-1">{{ uploadText }}</div>
</AUpload>
</template>
</div>
- 现在上传图片后会显示两份数据:Draggable和Aupload各一份,如何只显示拖拽区域的数据呢?
AUpload添加配置不显示上传数据 : :showUploadList="false"
- 当前的样式是:拖拽区域和上传区域 上下布局,如何让上传区域紧跟在 Draggable渲染的列表后面呢?
把Aupload那部分放在Draggable组件的 footer插槽中
- 对于图片希望可以直接预览,不打开新窗口
使用AImage组件自定义预览渲染时机,默认样式设置为不显示
7.支持批量上传,但是超过最大上传数量时,本次上传失败
用当前所有的文件数量和最大上传数量比较。
注意:beforeUpload中取到的只是本次上传的数量,需要加上 之前的数量,才是总的上传数量
javascript
async function handleBeforeUpload(file: UploadFile, fileList: UploadFile[]) {
if (!checkMaxCount(fileList)) {
return false
}
}
// 检查文件数量是否超过最大限制
function checkMaxCount(fileList: UploadFile[]) {
const totalCount = fileList.length + innerFileList.value.length
if (props.maxCount && totalCount > props.maxCount) {
message.destroy()
message.warning(props.checkTips?.maxCount || `最多上传${props.maxCount}张图片`)
return false
}
return true
}
8.完整代码
1)DragSort.vue
javascript
<template>
<div class="flex flex-wrap gap-4">
<!-- Aupload上传支持拖拽排序 -->
<Draggable
v-model="innerFileList"
item-key="uid"
:animation="animation"
tag="div"
class="drag-list"
@end="handleDragEnd"
>
<template #item="{ element }">
<div class="inline-flex">
<div class="drag-item">
<AUpload :file-list="[element]" :list-type="listType" @remove="handleRemove"> </AUpload>
</div>
</div>
</template>
<template #footer>
<slot name="footer">
<ImgUpload
v-if="innerFileList.length < maxCount"
v-bind="bind"
:show-upload-list="false"
@update:file-list="handleUpdateFileList"
/>
</slot>
</template>
</Draggable>
</div>
</template>
<script setup lang="ts">
import ImgUpload from '@/components/ImgUpload.vue'
import type { UploadFile } from 'ant-design-vue'
import type { UploadListType } from 'ant-design-vue/es/upload/interface'
import Draggable from 'vuedraggable'
const props = withDefaults(
defineProps<{
fileList?: UploadFile[]
urlList?: string[]
maxCount?: number
[key: string]: any
size?: number
listType?: UploadListType
animation?: number
}>(),
{
size: 100,
animation: 200,
listType: 'picture-card',
maxCount: 0
}
)
const emit = defineEmits(['update:fileList', 'update:urlList'])
const attrs: any = useAttrs()
const bind = computed(() => {
return {
...attrs,
...props
}
})
const innerFileList = ref<UploadFile[]>([])
function handleUpdateFileList(fileList: UploadFile[]) {
innerFileList.value = fileList
}
// 删除文件
function handleRemove(file: UploadFile) {
innerFileList.value = innerFileList.value.filter((item) => item.uid !== file.uid)
emit('update:fileList', innerFileList.value)
emit(
'update:urlList',
innerFileList.value.map((item) => item.thumbUrl)
)
}
// 拖拽结束------更新文件顺序
function handleDragEnd() {
emit('update:fileList', innerFileList.value)
emit(
'update:urlList',
innerFileList.value.map((item) => item.thumbUrl)
)
}
// 外部修改urlList------更新文件列表
watch(
() => props.urlList,
(newVal) => {
if (newVal && newVal.length > 0) {
innerFileList.value = newVal?.map((item: string) => {
let itemInfo = item.split('.')
return {
uid: item,
name: itemInfo[0] || item,
url: item,
thumbUrl: item,
type: itemInfo[1]
}
})
} else {
innerFileList.value = []
}
},
{
immediate: true,
deep: true
}
)
</script>
<style lang="less" scoped>
.drag-list {
@apply flex flex-wrap gap-4;
.drag-item {
width: v-bind('`${$props.size}px`');
height: v-bind('`${$props.size}px`');
cursor: move;
@apply rounded-lg;
}
}
</style>
2)useFileDeal.ts
javascript
export function useFileDeal() {
function getBase64(file: any) {
return new Promise<string>((resolve) => {
const reader = new FileReader()
reader.addEventListener('load', () => resolve(reader.result as string))
reader.readAsDataURL(file)
})
}
// 将 base64 数据转换为 Blob
function dataURLtoBlob(dataURL: string): Blob {
const arr = dataURL.split(',')
const mime = arr[0].match(/:(.*?);/)?.[1] || 'application/pdf'
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], { type: mime })
}
function previewFile(fileUrl: string) {
if (fileUrl.startsWith('data:')) {
// 如果是 base64 数据,创建 blob URL
const blob = dataURLtoBlob(fileUrl)
const blobUrl = URL.createObjectURL(blob)
const newWindow = window.open(blobUrl, '_blank')
if (newWindow) {
// 清理 blob URL 当窗口关闭时
newWindow.addEventListener('beforeunload', () => {
URL.revokeObjectURL(blobUrl)
})
}
} else {
// 如果是普通 URL,直接打开
window.open(fileUrl, '_blank')
}
}
// 获取文件类型
enum FileType {
DOCUMENT = 'DOCUMENT',
SPREADSHEET = 'SPREADSHEET',
IMAGE = 'IMAGE',
PDF = 'PDF',
PPT = 'PPT',
UNKNOWN = 'UNKNOWN',
OTHER = 'OTHER'
}
function getFileExt(file: string) {
const start = file.lastIndexOf('.')
const fileExt = file.slice(start + 1).toLowerCase()
if (['pdf', 'ofd'].includes(fileExt)) {
return FileType.PDF
} else if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExt)) {
return FileType.IMAGE
} else if (['xls', 'xlsx'].includes(fileExt)) {
return FileType.SPREADSHEET
} else if (['ppt', 'pptx'].includes(fileExt)) {
return FileType.PPT
} else if (['doc', 'docx'].includes(fileExt)) {
return FileType.DOCUMENT
} else {
return FileType.OTHER
}
}
return {
getBase64,
dataURLtoBlob,
previewFile,
getFileExt,
FileType
}
}
三、总结
1.通过AUpload和vuedraggable可以实现 上传文件拖拽功能,但是需要注意了解第三库对应组件的详细属性
2.其实实现的过程并不复杂,但是一开始没有思路想得脑子疼。还是要先行动,一步一步的想办法接近最终结果,很有可能就完美实现了!
/*
希望对你有帮助!
如有错误,欢迎指正,谢谢!
*/