上传标签 限制最大数量之后 多选图片还是会出现上传情况

问题描述:ant desgin vue 的上传组件,限制最大上传五个,一开始选择七张图片还是会上传成功,如果在上传之前函数拦截,会出现七个白框占位。

解决方案:用input 代替上传组件 

<template>
  <div class="upload-container">
    <input
      ref="fileInput"
      type="file"
      class="hidden-input"
      :multiple="multiple"
      accept="image/*, video/*"
      @change="handleFileSelect"
    >

    <AUpload
      v-model:fileList="fileList"
      listType="picture-card"
      :multiple="multiple"
      :beforeUpload="beforeUpload"
      :showUploadList="{ showPreviewIcon: false, showRemoveIcon: false }"
      :customRequest="customRequest"
      accept="image/*, video/*"
      @preview="handlePreview"
    >
      <div v-if="fileList.length < maxCount">
        <PlusOutlined style="font-size: 28px;" />
        <div class="ant-upload-text">
          <!-- 上传文件 ({{ fileList.length }}/{{ maxCount }}) -->
          上传文件
        </div>
      </div>
      <template #itemRender="{file}">
        <div
          :key="file.uid"
          class="ant-upload-list-item"
        >
          <div class="ant-upload-list-item-info">
            <div
              class="ant-upload-list-item-thumbnail"
              style="display: flex;"
            >
              <LoadingOutlined
                v-if="file.status === 'uploading'"
                style="font-size: 40px; color: #1890ff;"
              />
              <img
                v-if="file.status === 'done' && file.url"
                :src="file.cover"
                alt="图片缩略图"
              >
            </div>
            <img
              v-if="file.status === 'done' && file.type?.startsWith('video/')"
              class="video-thumbnail-container"
              src="../../assets/play.png"
              alt=""
            >
            <div
              v-if="file.status === 'uploading'"
              class="upload-progress"
            >
              <div
                class="progress-bar"
                :style="{ width: `${file.progress || 0}%` }"
              />
            </div>
          </div>
          <div
            v-if="!isAtlas"
            class="ant-upload-list-item-actions"
          >
            <span
              class="ant-upload-list-item-action"
              @click="handlePreview(file)"
            >
              <EyeOutlined style="font-size: 20px;width: 20px;" />
            </span>
            <span
              class="ant-upload-list-item-action"
              @click="removeFile(file)"
            >
              <DeleteOutlined style="font-size: 20px;width: 20px;" />
            </span>
          </div>
          <div
            v-if="isAtlas"
            class="ant-upload-list-item-actions"
          >
            <div>
              <AButton
                type="primary"
                shape="round"
                size="small"
                @click="handleTitle(file)"
              >
                标题
              </AButton>
            </div>
            <div style="margin-top: 8px;">
              <AButton
                type="primary"
                shape="round"
                size="small"
                danger
                @click="removeFile(file)"
              >
                删除
              </AButton>
            </div>
          </div>
        </div>
        <span style="line-height: 20px;">{{ file.title }}</span>
      </template>
    </AUpload>
    <slot name="tip" />

    <AModal
      :open="previewVisible"
      title="预览"
      @cancel="previewVisible = false"
    >
      <div v-if="previewType == 'image'">
        <img
          :src="previewUrl"
          alt="预览图片"
          class="preview-image"
        >
      </div>
      <div v-else-if="previewType == 'video'">
        <video
          controls
          class="preview-video"
        >
          <source
            :src="previewUrl"
            type="video/mp4"
          >
          您的浏览器不支持视频播放
        </video>
      </div>
      <template #footer>
        <AButton @click="previewVisible = false">
          关闭
        </AButton>
      </template>
    </AModal>

    <AModal
      :open="titleModal"
      title="标题"
      @cancel="closeTitle"
      @ok="saveTitle"
    >
      <AInput
        v-model:value="picTitle"
        placeholder="请输入标题"
      />
    </AModal>
  </div>
</template>

<script lang="ts" setup>
import { ref, defineProps, defineEmits, getCurrentInstance, watch, onMounted } from 'vue'
import type { UploadFile } from 'ant-design-vue/es/upload/interface'
import { message } from 'ant-design-vue'

const { proxy } = getCurrentInstance()

interface CustomFile extends UploadFile {
  uid: string;
  name: string;
  thumbnail?: string;
  type?: string;
  progress?: number;
  cover?: string;
  title?: string;
  id?: string;
  status?: string;
  url?: string;
}

const props = defineProps({
  multiple: {
    type: Boolean,
    default: true,
  },
  maxCount: {
    type: Number,
    default: 5,
  },
  isAtlas: {
    type: Boolean,
    default: false,
  },
  modelValue: { type: [String, Array, Object], default: () => {} },
})

const emit = defineEmits(['update:modelValue', 'update:fileList'])

const fileList = ref<CustomFile[]>([])
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref('image')
const titleModal = ref(false)
const picTitle = ref('')
const selectedFile = ref<CustomFile | null>(null)
const pendingFiles = ref<File[]>([])
const maxCount = ref(props.maxCount)
const fileInput = ref<HTMLInputElement | null>(null)

// 初始化文件列表
if (Array.isArray(props.modelValue)) {
  fileList.value = props.modelValue.map((item: any) => ({
    ...item,
    status: 'done',
  }))
} else if (props.modelValue && typeof props.modelValue === 'object') {
  fileList.value = [{
    ...props.modelValue,
    status: 'done',
    uid: (props.modelValue.uid || Date.now().toString()),
    name: (props.modelValue.name || '文件'),
  }]
}

watch(
  () => props.modelValue,
  (newVal: any) => {
    if (Array.isArray(newVal)) {
      fileList.value = newVal.map((item: any) => ({
        ...item,
        status: 'done',
      }))
    } else if (newVal && typeof newVal === 'object') {
      fileList.value = [{ ...newVal, status: 'done' }]
    }
  },
  { deep: true },
)

const updateModelValue = (newList: any) => {
  emit('update:modelValue', newList)
}

const beforeUpload = (file: File) => {
  const isImage = file.type.startsWith('image/')
  const isVideo = file.type.startsWith('video/')

  if (!isImage && !isVideo) {
    message.error('只能上传图片或视频文件!')
    return false
  }
  return true
}

const handleFileSelect = (e: Event) => {
  const input = e.target as HTMLInputElement
  if (!input.files) return

  const availableSlots = maxCount.value - fileList.value.length
  if (availableSlots <= 0) {
    message.error(`最多只能上传 ${maxCount.value} 个文件!`)
    input.value = ''
    return
  }

  const files = Array.from(input.files)
  const filesToUpload = files.slice(0, availableSlots)

  if (files.length > availableSlots) {
    message.error(`只能选择 ${availableSlots} 个文件,已自动筛选`)
  }

  pendingFiles.value = [...pendingFiles.value, ...filesToUpload]
  processPendingFiles()
  input.value = ''
}

const processPendingFiles = async() => {
  while (pendingFiles.value.length > 0 && fileList.value.length < maxCount.value) {
    const file = pendingFiles.value[0]

    const previewFile: CustomFile = {
      uid: Date.now().toString(),
      name: file.name,
      status: 'uploading',
      url: '',
      type: file.type,
      cover: '',
      title: '',
      progress: 0,
    }

    fileList.value = [...fileList.value, previewFile]
    updateModelValue(fileList.value)

    try {
      await new Promise<void>((resolve, reject) => {
        proxy.$ossApi.upload('1011', file.name.replace(/\s+/g, ''), file,
          (res: any) => {
            const data = res.result
            const index = fileList.value.findIndex((f: any) => f.uid === previewFile.uid)
            if (index !== -1) {
              fileList.value[index] = {
                ...previewFile,
                status: 'done',
                url: data.url,
                cover: file.type.indexOf('image') >= 0
                  ? data.url
                  : data.url + '?x-oss-process=video/snapshot,t_2000,m_fast',
                id: data.id,
              }
              updateModelValue(fileList.value)
            }
            resolve()
          },
          (err: any) => {
            fileList.value = fileList.value.filter((f: any) => f.uid !== previewFile.uid)
            updateModelValue(fileList.value)
            message.error(`${file.name} 上传失败`)
            reject(err)
          },
          (progress: number) => {
            const index = fileList.value.findIndex((f: any) => f.uid === previewFile.uid)
            if (index !== -1) {
              fileList.value[index].progress = progress
            }
          },
        )
      })
    } catch (error) {
      console.error('上传失败:', error)
    } finally {
      pendingFiles.value = pendingFiles.value.slice(1)
    }
  }
}

const customRequest = () => {}

const handlePreview = (file: CustomFile) => {
  previewUrl.value = file.url || ''
  previewType.value = file.type?.startsWith('video/') ? 'video' : 'image'
  previewVisible.value = true
}

const removeFile = (file: CustomFile) => {
  fileList.value = fileList.value.filter((item: any) => item.uid !== file.uid)
  updateModelValue(fileList.value)
}

const handleTitle = (file: CustomFile) => {
  selectedFile.value = file
  picTitle.value = file.title || ''
  titleModal.value = true
}

const saveTitle = () => {
  if (selectedFile.value) {
    selectedFile.value.title = picTitle.value
    fileList.value.forEach((file: any) => {
      if (file.uid === selectedFile.value!.uid) {
        file.title = picTitle.value
      }
    })
    updateModelValue(fileList.value)
    titleModal.value = false
  }
}

const closeTitle = () => {
  titleModal.value = false
  picTitle.value = ''
}
</script>

<style scoped lang="less">
.upload-container {
  position: relative;
  padding: 10px;
  border: 1px solid #d9d9d9;
  border-radius: 4px;

  .hidden-input {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
    z-index: 10;
  }
}

.ant-upload-list-item-info {
  width: 100%;
  height: 100%;
  position: relative;
}

:deep(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container) {
  margin-block: 0 24px !important;
}

:deep(.ant-upload-wrapper .ant-upload-list.ant-upload-list-picture .ant-upload-list-item),
:deep(.ant-upload-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item) {
  padding: 0 !important;
}

:deep(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item::before) {
  width: 100% !important;
  height: 100% !important;
}

.ant-upload-list-item-thumbnail {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;

  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 4px;
  }
}

.preview-image {
  max-width: 100%;
  max-height: 400px;
  object-fit: contain;
}

.preview-video {
  max-width: 100%;
  max-height: 400px;
}

.video-thumbnail-container {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 18px !important;
  height: 18px !important;
}

.upload-progress {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: rgba(0,0,0,0.1);

  .progress-bar {
    height: 100%;
    background: #1890ff;
    transition: width 0.3s;
  }
}
</style>

  

posted @ 2025-06-27 11:34  沁猿春  阅读(29)  评论(0)    收藏  举报