vue3 naive 上传组件封装

2,615 阅读3分钟

背景

项目基于 vue3 naive 搭建的后台项目,使用 naiveupload 不能满足一些合理的特殊需求(拖拽、多张上传的处理),故封装了自定义上传组件

目标

  1. 基础上传功能
  2. 图片上的功能:删除 查看
  3. 拖拽排序
  4. 多张图片上传 & 限制上传张数
  5. 限制上传图片的大小
  6. 列表中展示缩略图,预览展示原图
  7. 支持查看模式

基础的上传功能

设置 inputtype="file",即可使用上传功能。当选择完上传文件之后便会触发 change 的回调 handlerFileChange

<input
    type="file"
    @change="handlerFileChange"
    multiple
    style="display: none"
    ref="fileInputRef"
    accept="image/*"
/>

handlerFileChange 将用户选择的文件(们)进行进一步处理

满足了图片数量和图片大小的要求之后将文件流传给上传函数 upload

这里需要注意一点是要将 target 对象清空,可以解决同一个文件二次上传失败的问题

/**
 * 用户点击上传按钮
 */
async function handlerFileChange(e: Event) {
  const target = e.target as HTMLInputElement
  const files = target.files as FileList

  // 允许上传的长度
  const canUploadMax = props.max - fileList.value.length

  // 触发上传,剔除允许上传文件数量之外的文件
  for (let i = 0; i < Math.min(files.length, canUploadMax); i++) {
    const file = files[i]
    if (file.size > UploadConfig.maxSize * 1024 * 1024) {
      // 限制图片大小
      message.warning(`请选择小于${UploadConfig.maxSize}的文件`)
      break
    }
    await upload(file)
  }

  // 手动置空,解决同个图片不能上传的问题
  target.value = ''

  nextTick(() => {
    sortFun()
  })

  update()
}

upload 上传函数

formatStr 表示图片的格式

Key 作为即将放置的图片的路径

这里用的是腾讯云的 cos 上传 sdk

上传成功之后,将图片链接存入组件的图片列表的变量中

function upload(file: File) {
  return new Promise((resolve, reject) => {
    const formatStr = file.type?.replace('image/', '') // 图片格式
    const Key = `/test/static/pic/cat/oai${new Date()
      .getTime()
      .toString()}.${formatStr}`

    imgSTSStore.cos.putObject(
      {
        Bucket: imgSTSStore.Bucket,
        Region: imgSTSStore.Region,
        Key,
        Body: file as File,
        onProgress: function (progressData) {},
      },
      function (err, data) {
        const url = `https://res.mdaren.cn${Key}`

        // 将文件传入文件变量
        fileList.value.push({
          id: new Date().getTime().toString(),
          url: url,
          file: file,
        })

        resolve(1)
      }
    )
  })
}

图片上的功能:删除 查看

删除

点击删除后利用id唯一性在回调函数中找到需要删除的项,splice 删除数据,上报数据更新

<n-button
    text
    style="font-size: 16px"
    type="error"
    @click="deleteImg(item.id)"
    v-if="!viewFlag"
>
    <n-icon>
        <SvgIcon name="delete"></SvgIcon>
    </n-icon>
</n-button>
/**
 * 删除图片
 * @param id 被删除项ID
 */
function deleteImg(id: string) {
  const findIndex = fileList.value.findIndex((v) => v.id === id)

  fileList.value.splice(findIndex, 1)

  update()
}

查看

<n-button
    text
    style="font-size: 16px"
    type="primary"
    @click="previewImg(item.url)"
>
    <n-icon>
        <SvgIcon name="eye"></SvgIcon>
    </n-icon>
</n-button>

点击预览图片

监听到点击预览事件之后,将所要预览的图片链接赋值给变量 previewUrl,同时触发图片组件的预览点击,唤起预览弹窗

const previewUrl: Ref<string> = ref('')
const previewRef: Ref<any> = ref(null)

/**
 * 预览图片
 * @param url 将要预览的图片的url
 */
function previewImg(url: string) {
  previewUrl.value = url
  nextTick(() => {
    previewRef.value.click()
  })
}

图片预览,工具组件

这里利用 naive 的图片预览功能作为本文组件的预览方案,下面是 n-image 的组件写法

<n-image
    ref="previewRef"
    width="100"
    :src="previewUrl"
    style="display: none"
/>

拖拽排序

拖拽用的是 sortablejs

拖拽成功之后的回调会得到原位置和新位置两个的索引,这里利用这两个索引对图片列表进行了插队排序,得到拖拽成功后的数据


import Sortable from 'sortablejs'

const sortable: Ref<Sortable | null> = ref(null)

const draggableContainer: Ref<any> = ref(null)
function sortFun() {
  const el = draggableContainer.value.$el.querySelectorAll(
    'div.imgList'
  )[0] as HTMLElement

  sortable.value = Sortable.create(el, {
    ghostClass: 'sortable-ghost', // Class name for the drop placeholder
    onEnd: (evt: any) => {
      if (
        typeof evt.oldIndex !== 'undefined' &&
        typeof evt.newIndex !== 'undefined'
      ) {
        const { oldIndex, newIndex } = evt
        const idArr: string[] = []
        fileList.value.forEach((v) => {
          idArr.push(v.id)
        })

        // 切割的位置
        const sliceIndex = oldIndex > newIndex ? newIndex : newIndex + 1
        // 原位置
        const spliceIndex = oldIndex > newIndex ? oldIndex + 1 : oldIndex

        const newArr = [
          ...fileList.value.slice(0, sliceIndex),
          fileList.value[oldIndex],
          ...fileList.value.slice(sliceIndex, idArr.length),
        ]
        newArr.splice(spliceIndex, 1)
        fileList.value = newArr

        update()
      }
    },
  })
}

多张图片上传 & 限制上传张数

input multiple 属性设置之后就能允许多张

限制张数从 handlerFileChange 限制

// 允许上传的长度
const canUploadMax = props.max - fileList.value.length

// 触发上传,剔除允许上传文件数量之外的文件
for (let i = 0; i < Math.min(files.length, canUploadMax); i++) {
    const file = files[i]
    if (file.size > UploadConfig.maxSize * 1024 * 1024) {
        // 限制图片大小
        message.warning(`请选择小于${UploadConfig.maxSize}的文件`)
        break
    }
    await upload(file)
}

canUploadMax 表示允许上传的文件数量 = 允许上传总数 - 当前已经拥有的数量。多余的文件将不会被处理

限制上传图片大小

触发上传回调函数 handlerFileChange中进行文件大小判断,将不满足要求的过滤掉。

for (let i = 0; i < Math.min(files.length, canUploadMax); i++) {
    const file = files[i]
    if (file.size > UploadConfig.maxSize * 1024 * 1024) {
        // 限制图片大小
        message.warning(`请选择小于${UploadConfig.maxSize}的文件`)
        break
    }
    await upload(file)
}

UploadConfig.maxSize 表示允许上传的文件大小,大于该数值的文件将不会被处理,而且会有一个警告

列表中展示缩略图,预览展示原图

展示图片的时候 src 使用全局压缩方法 compressImage。这里处理的目的是快速展示组件需要展示的图片和节流

<!-- 展示的图片 -->
<img class="img" :src="compressImage(item.url)" />

支持查看模式

对于详情和编辑共用一个页面的情况下,查看模式就很合适

通过父组件传入的 viewFlag 判断是否使用查看模式,默认不使用查看模式。如果是查看模式的话就隐藏上传按钮和删除按钮

const props = defineProps({
  // 查看模式
  viewFlag: {
    type: Boolean,
    default: false,
  },
})