UploadStep.vue 7.46 KB
<template>
  <el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
    <el-form-item class="mb-20px">
      <div class="w-full">
        <div
          class="w-full border-2 border-dashed border-[#dcdfe6] rounded-md p-20px text-center hover:border-[#409eff]"
        >
          <el-upload
            ref="uploadRef"
            class="upload-demo"
            drag
            :action="uploadUrl"
            :auto-upload="true"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            :on-change="handleFileChange"
            :on-remove="handleFileRemove"
            :before-upload="beforeUpload"
            :http-request="httpRequest"
            :file-list="fileList"
            :multiple="true"
            :show-file-list="false"
            :accept="acceptedFileTypes"
          >
            <div class="flex flex-col items-center justify-center py-20px">
              <Icon icon="ep:upload-filled" class="text-[48px] text-[#c0c4cc] mb-10px" />
              <div class="el-upload__text text-[16px] text-[#606266]">
                拖拽文件至此,或者
                <em class="text-[#409eff] not-italic cursor-pointer">选择文件</em>
              </div>
              <div class="el-upload__tip mt-10px text-[#909399] text-[12px]">
                已支持 {{ supportedFileTypes.join('、') }},每个文件不超过 {{ maxFileSize }} MB。
              </div>
            </div>
          </el-upload>
        </div>

        <div
          v-if="modelData.list && modelData.list.length > 0"
          class="mt-15px grid grid-cols-1 gap-2"
        >
          <div
            v-for="(file, index) in modelData.list"
            :key="index"
            class="flex justify-between items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
          >
            <div class="flex items-center">
              <Icon icon="ep:document" class="mr-8px text-[#409eff]" />
              <span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
            </div>
            <el-button type="danger" link @click="removeFile(index)" class="ml-2">
              <Icon icon="ep:delete" />
            </el-button>
          </div>
        </div>
      </div>
    </el-form-item>

    <!-- 添加下一步按钮 -->
    <el-form-item>
      <div class="flex justify-end w-full">
        <el-button type="primary" @click="handleNextStep" :disabled="!isAllUploaded">
          下一步
        </el-button>
      </div>
    </el-form-item>
  </el-form>
</template>

<script lang="ts" setup>
import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { generateAcceptedFileTypes } from '@/utils'
import { Icon } from '@/components/Icon'

const props = defineProps({
  modelValue: {
    type: Object as PropType<any>,
    required: true
  }
})

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

const formRef = ref() // 表单引用
const uploadRef = ref() // 上传组件引用
const parent = inject('parent', null) // 获取父组件实例
const { uploadUrl, httpRequest } = useUpload() // 使用上传组件的钩子
const message = useMessage() // 消息弹窗
const fileList = ref([]) // 文件列表
const uploadingCount = ref(0) // 上传中的文件数量

// 支持的文件类型和大小限制
const supportedFileTypes = [
  'TXT',
  'MARKDOWN',
  'MDX',
  'PDF',
  'HTML',
  'XLSX',
  'XLS',
  'DOC',
  'DOCX',
  'CSV',
  'EML',
  'MSG',
  'PPTX',
  'XML',
  'EPUB',
  'PPT',
  'MD',
  'HTM'
]
const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase()) // 小写的扩展名列表
const maxFileSize = 15 // 最大文件大小(MB)

// 构建 accept 属性值,用于限制文件选择对话框中可见的文件类型
const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))

/** 表单数据 */
const modelData = computed({
  get: () => {
    return props.modelValue
  },
  set: (val) => emit('update:modelValue', val)
})

/** 确保 list 属性存在 */
const ensureListExists = () => {
  if (!props.modelValue.list) {
    emit('update:modelValue', {
      ...props.modelValue,
      list: []
    })
  }
}

/** 是否所有文件都已上传完成 */
const isAllUploaded = computed(() => {
  return modelData.value.list && modelData.value.list.length > 0 && uploadingCount.value === 0
})

/**
 * 上传前检查文件类型和大小
 *
 * @param file 待上传的文件
 * @returns 是否允许上传
 */
const beforeUpload = (file) => {
  // 1.1 检查文件扩展名
  const fileName = file.name.toLowerCase()
  const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1)
  if (!allowedExtensions.includes(fileExtension)) {
    message.error('不支持的文件类型!')
    return false
  }
  // 1.2 检查文件大小
  if (!(file.size / 1024 / 1024 < maxFileSize)) {
    message.error(`文件大小不能超过 ${maxFileSize} MB!`)
    return false
  }

  // 2. 增加上传中的文件计数
  uploadingCount.value++
  return true
}

/**
 * 文件上传成功处理
 *
 * @param response 上传响应
 * @param file 上传的文件
 */
const handleUploadSuccess = (response, file) => {
  // 添加到文件列表
  if (response && response.data) {
    ensureListExists()
    emit('update:modelValue', {
      ...props.modelValue,
      list: [
        ...props.modelValue.list,
        {
          name: file.name,
          url: response.data
        }
      ]
    })
  } else {
    message.error(`文件 ${file.name} 上传失败`)
  }

  // 减少上传中的文件计数
  uploadingCount.value = Math.max(0, uploadingCount.value - 1)
}

/**
 * 文件上传失败处理
 *
 * @param error 错误信息
 * @param file 上传的文件
 */
const handleUploadError = (error, file) => {
  message.error(`文件 ${file.name} 上传失败: ${error}`)
  // 减少上传中的文件计数
  uploadingCount.value = Math.max(0, uploadingCount.value - 1)
}

/**
 * 文件变更处理
 *
 * @param file 变更的文件
 */
const handleFileChange = (file) => {
  if (file.status === 'success' || file.status === 'fail') {
    uploadingCount.value = Math.max(0, uploadingCount.value - 1)
  }
}

/**
 * 文件移除处理
 *
 * @param file 被移除的文件
 */
const handleFileRemove = (file) => {
  if (file.status === 'uploading') {
    uploadingCount.value = Math.max(0, uploadingCount.value - 1)
  }
}

/**
 * 从列表中移除文件
 *
 * @param index 要移除的文件索引
 */
const removeFile = (index: number) => {
  // 从列表中移除文件
  const newList = [...props.modelValue.list]
  newList.splice(index, 1)
  // 更新表单数据
  emit('update:modelValue', {
    ...props.modelValue,
    list: newList
  })
}

/** 下一步按钮处理 */
const handleNextStep = () => {
  // 1.1 检查是否有文件上传
  if (!modelData.value.list || modelData.value.list.length === 0) {
    message.warning('请上传至少一个文件')
    return
  }
  // 1.2 检查是否有文件正在上传
  if (uploadingCount.value > 0) {
    message.warning('请等待所有文件上传完成')
    return
  }

  // 2. 获取父组件的goToNextStep方法
  const parentEl = parent || getCurrentInstance()?.parent
  if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
    parentEl.exposed.goToNextStep()
  }
}

/** 初始化 */
onMounted(() => {
  ensureListExists()
})
</script>

<style lang="scss" scoped></style>