mutiple.vue 6.24 KB
<template>
  <view class="file-upload-wrapper">
    <view class="btn" @click="selectFile" v-if="fileList.length < limit">
      <image class="icon" src="/static/images/add.png" mode="widthFix" />
      <text>上传附件</text>
    </view>
    <view v-if="fileList.length > 0" class="file-list">
      <view v-for="(item, index) in fileList" :key="index" class="file-item">
        <text class="file-name">{{ item.fileName || item.originalFileName || item.name }}</text>
        <text class="delete-btn" @click.stop="deleteFile(index)">×</text>
      </view>
    </view>
  </view>
</template>

<script>
import { uploadFileApi } from '@/api/base.js'
import upload from '@/utils/upload'

export default {
  name: 'FileUpload',
  props: {
    value: { type: [Array, String], default: () => [] },
    limit: { type: Number, default: 1 },
    fileSize: { type: Number, default: 5 }, // MB
    fileType: { type: Array, default: () => [] } // e.g. ['png', 'jpg', 'pdf']
  },
  data() {
    return {
      fileList: []
    }
  },
  watch: {
    value: {
      handler(val) {
        if (Array.isArray(val)) {
          this.fileList = val
        } else if (val) {
          try {
            this.fileList = JSON.parse(val)
          } catch (e) {
            this.fileList = []
          }
        } else {
          this.fileList = []
        }
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    extractId(res) {
      const data = res && res.data ? res.data : res
      const id = (data && (data.id || data.fileId || (data.data && (data.data.id || data.data.fileId))))
        ? String(data.id || data.fileId || data.data.id || data.data.fileId)
        : ''
      return id
    },
    // 校验文件
    validateFile(file) {
      // 校验类型
      if (this.fileType && this.fileType.length > 0) {
        const name = file.name || ''
        const ext = name.substring(name.lastIndexOf('.') + 1).toLowerCase()
        if (!this.fileType.includes(ext)) {
          uni.showToast({ title: `仅支持 ${this.fileType.join(',')} 格式`, icon: 'none' })
          return false
        }
      }
      // 校验大小
      if (this.fileSize && file.size > this.fileSize * 1024 * 1024) {
        uni.showToast({ title: `文件大小不能超过${this.fileSize}MB`, icon: 'none' })
        return false
      }
      return true
    },

    async uploadFileByPath(filePath, name) {
      const r = await upload({ url: '/sw/filebox/uploadFile', filePath, name: 'file' })
      const id = this.extractId(r)
      return { id, name }
    },

    async uploadFileByFormData(fd, name) {
      const r = await uploadFileApi(fd)
      const id = this.extractId(r)
      return { id, name }
    },

    selectFile() {
      const count = this.limit - this.fileList.length
      if (count <= 0) return
      if (typeof uni !== 'undefined' && typeof uni.chooseMessageFile === 'function') {
        uni.chooseMessageFile({
          count,
          type: 'all',
          success: (res) => {
            const tempFiles = (res && res.tempFiles) ? res.tempFiles : []
            this.processFiles(tempFiles)
          },
          fail: () => {
            uni.showToast({ title: '未选择文件', icon: 'none' })
          }
        })
        return
      }
      // #ifdef H5
      this.selectFileH5()
      // #endif
    },

    selectFileH5() {
      const input = document.createElement('input')
      input.type = 'file'
      input.multiple = this.limit > 1
      input.style.display = 'none'
      document.body.appendChild(input)
      input.addEventListener('change', (e) => {
        const files = Array.from(e.target.files)
        const processedFiles = files.map(f => ({
          path: null,
          file: f,
          name: f.name,
          size: f.size
        }))
        this.processFiles(processedFiles)
        document.body.removeChild(input)
      })
      input.click()
    },

    async processFiles(files) {
      if (!files || files.length === 0) return

      const validFiles = []
      for (let i = 0; i < files.length; i++) {
        const file = files[i]
        if (this.validateFile(file)) {
          validFiles.push(file)
        }
      }

      if (validFiles.length === 0) return

      if (this.fileList.length + validFiles.length > this.limit) {
        uni.showToast({ title: `最多只能上传${this.limit}个文件`, icon: 'none' })
        return
      }

      uni.showLoading({ title: '上传中...' })

      try {
        const uploadPromises = validFiles.map((f) => {
          const name = f.name || 'file'
          if (f.file) {
            const fd = new FormData()
            fd.append('file', f.file, name)
            return this.uploadFileByFormData(fd, name)
          }
          if (f.path) {
            return this.uploadFileByPath(f.path, name)
          }
          return Promise.resolve({ id: '', name })
        })

        const results = await Promise.all(uploadPromises)

        const newFiles = results
          .filter(r => r && r.id)
          .map(r => ({ id: r.id, name: r.name, fileId: r.id, fileName: r.name }))

        this.fileList = [...this.fileList, ...newFiles]
        this.emitChange()

      } catch (e) {
        console.error(e)
        uni.showToast({ title: '上传失败', icon: 'none' })
      } finally {
        uni.hideLoading()
      }
    },

    deleteFile(index) {
      this.fileList.splice(index, 1)
      this.emitChange()
    },

    emitChange() {
      this.$emit('input', this.fileList)
      this.$emit('change', this.fileList)
      this.$emit('update:value', this.fileList)
    }
  }
}
</script>

<style scoped lang="scss">
.file-upload-wrapper {
  width: 100%;
}

.btn {
  display: inline-flex;
  align-items: center;
  color: #3D48A3;
  font-size: 28rpx;
  cursor: pointer;
  height: 48rpx;

  .icon {
    width: 32rpx;
    height: 32rpx;
    margin-right: 8rpx;
  }
}

.file-list {
  display: flex;
  flex-direction: column;
  gap: 16rpx;
}

.file-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background-color: #f5f7fa;
  padding: 12rpx 20rpx;
  border-radius: 8rpx;

  .file-name {
    font-size: 26rpx;
    color: #333;
    flex: 1;
    margin-right: 20rpx;
    white-space: pre-wrap;
    word-break: break-all;
  }

  .delete-btn {
    color: #999;
    font-size: 32rpx;
    padding: 0 10rpx;
  }
}
</style>