Aupload + vuedraggable实现 上传的文件可以拖拽排序

一、问题

1.以前的需求已经通过Aupload封装了 文件上传组件,现在希望添加 文件拖拽排序功能

2.首先想到可以使用 vuedraggable来实现。但是 vuedraggable需要传递数组并且在插槽里面指定单个元素的渲染方式,但是原本的Aupload组件是直接渲染数组的,要怎么兼容呢?

二、解决方法

1.方法一:不用Aupload了,自己实现单个文件的渲染fileCard以及文件上传逻辑,在vueDraggable中复用fileCard组件。之前的很多逻辑都不能用了需要重写,工作量比较大,还有可能有性能问题

2.方法二:在当前的基础上直接添加拖拽功能。本文采用方案二,具体实现如下

  1. 实现拖拽的功能:拿到上传的文件数据就行=> 拖拽区域的数据和上传区域的数据共用,拖拽结束后同步修改父组件传入的数据
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>
  1. 现在上传图片后会显示两份数据:Draggable和Aupload各一份,如何只显示拖拽区域的数据呢?

AUpload添加配置不显示上传数据 : :showUploadList="false"

  1. 当前的样式是:拖拽区域和上传区域 上下布局,如何让上传区域紧跟在 Draggable渲染的列表后面呢?

把Aupload那部分放在Draggable组件的 footer插槽中

  1. 对于图片希望可以直接预览,不打开新窗口

使用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.其实实现的过程并不复杂,但是一开始没有思路想得脑子疼。还是要先行动,一步一步的想办法接近最终结果,很有可能就完美实现了!

/*

希望对你有帮助!

如有错误,欢迎指正,谢谢!

*/

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax