axios Vue 3当数据更改时,获取v-for以更新循环中的项目时遇到问题

wfypjpf4  于 2023-01-25  发布在  iOS
关注(0)|答案(1)|浏览(199)

我在Vue 3中的for循环中遇到了一些问题。
我有一个文件数组,每个文件都有进度和状态等属性。
当我用axios上传文件时,我还用状态和进度更新了当前上传的文件。
v-for循环没有捕捉到这些变化,并且仍然向我显示旧的状态值,比如“pending”,即使当axios正在进行/完成上传时,属性被更改为“uploading”和“uploaded”。
我试着用reactive() Package 数据,但运气不好。我也不能使用Vue.$set,因为它从Vue 3中删除了。有人知道我该如何解决这个问题吗?
我使用以下方法更新当前上传的文件:updateFileStatus()
它是在uploadFile方法中完成的axios请求内调用的。
整个组件在下面。
如果有人能帮我的话,先谢谢了。

<template>
  <div class="flex items-center">
    <div id="dropzone-wrapper" class="flex flex-col w-full">
      <input
        type="file"
        ref="legacyFileSelect"
        class="hidden"
        @change="filesSelected"
        :multiple="multiple"
        :accept="extensions.join(', ')" />

      <div
        ref="dropzone"
        class="flex items-center justify-center py-8 border-2 rounded border-dotted border-nord-frost-300 dark:border-nord-frost-300 w-full"
        @dragover="dragOver"
        @dragleave="dragLeave"
        @drop="drop"
        @click="$refs.legacyFileSelect.click()">
        <span
          class="text-nord-300/50 dark:text-nord-snow-storm-300/25 text-sm italic">
          {{ placeholder }}
        </span>
      </div>

      <div id="uploadResults" class="flex flex-col space-y-1 mt-2">
        <template v-for="(file, fileIndex) in uploadResults" :key="fileIndex">
          <div class="flex flex-col rounded border border-nord-snow-storm-300 dark:border-nord-snow-storm-300/25">
            <div class="flex space-x-1 items-center p-1.5 px-2 pr-3">
              <span v-if="!uploadOnSelect" class="mt-1">
                <VButton
                  @click="removeFile(fileIndex)"
                  icon="delete"
                  color="red"
                  :noBackground="true"
                  size="lg"
                  :class="{ 'hidden': file.status !== 'pending' }"
                />
              </span>

              <span class="text-nord-300/50 dark:text-nord-snow-storm-300/50 text-xs mt-1">
                {{ formatBytes(file.size) }}
              </span>

              <span class="text-nord-300 dark:text-nord-snow-storm-300 text-sm mt-0.5">
                {{ file.name }}
              </span>

              <div class="flex grow justify-end">
                <span
                  v-if="file.status === 'uploaded'"
                  class="text-nord-aurora-1100 dark:text-nord-aurora-1100 text-sm mt-0.5">
                  Uploaded
                </span>

                <span
                  v-if="file.status === 'uploading'"
                  class="text-nord-aurora-1100 dark:text-nord-aurora-1100 text-sm mt-0.5">
                  Uploading
                </span>

                <span
                  v-if="file.status === 'failed'"
                  class="text-nord-aurora-200 dark:text-nord-aurora-200 text-sm mt-0.5">
                  Failed
                </span>

                <span
                  v-if="file.status === 'pending'"
                  class="text-nord-300/50 dark:text-nord-snow-storm-300/50 text-sm mt-0.5">
                  Pending
                </span>
              </div>
            </div>

            <div class="flex-grow">
              <div class="h-1">
                <div
                  class="h-1 rounded-b"
                  :class="{
                    'bg-nord-snow-storm-300/25': file.status === 'pending',
                    'bg-nord-aurora-1100': file.status === 'uploading',
                    'bg-nord-aurora-1100': file.status === 'uploaded',
                    'bg-nord-aurora-200': file.status === 'failed',
                  }"
                  :style="{ width: `${file.progress}%` }">
                </div>
              </div>
            </div>
          </div>
        </template>
      </div>

      <VButton v-if="!uploadOnSelect" @click="upload" color="blue" size="sm" class="mt-2">
          Upload
      </VButton>
    </div>
  </div>
</template>

<script>
  import axios from 'axios'
  import VButton from './V-Button.vue'
  export default {
    components: {
      VButton,
    },

    props: {
      multiple: {
        type: Boolean,
        required: false,
        default: false,
      },

      url: {
        type: String,
        required: true,
        default: null,
      },

      uploadOnSelect: {
        type: Boolean,
        required: false,
        default: false,
      },

      extensions: {
        type: Array,
        required: false,
        default: ['jpg', 'png', 'jpeg'],
      },

      maxFiles: {
        type: Number,
        required: false,
        default: 5,
      },

      maxFileSize: {
        type: Number,
        required: false,
        default: 5 * 1024 * 1024,
      },

      placeholder: {
        type: String,
        required: false,
        default: 'Drag and drop file(s) here or click to select files',
      },

      headers: {
        type: Object,
        required: false,
        default: {
          'Content-Type': 'multipart/form-data' 
        }
      }
    },

    data() {
      return {
        uploadResults: [],
      }
    },

    methods: {
      /**
       * Remove a file from the uploadResults array
       * 
       * @param {Number} fileIndex
       * @return {void}
       */
      removeFile(fileIndex) {
        this.uploadResults.splice(fileIndex, 1)
      },

      /**
       * Update the progress and status of a given
       * file in the uploadResults array
       * 
       * @param {String} status 
       * @param {Number} progress 
       * @param {Number} fileIndex
       * @return {void} 
       */
      updateFileStatus(status, progress, fileIndex) {
        this.uploadResults[fileIndex].status = status
        this.uploadResults[fileIndex].progress = progress
      },

      upload() {
        this.uploadResults.forEach((file, index) => {
          this.uploadFile(file, index)
        })
      },

      /**
       * Upload a file to the server using axios
       * 
       * @param {File} file 
       * @param {Number} fileIndex 
       * @return {void}
       */
      uploadFile(file, fileIndex) {
        let formData = new FormData()
        formData.append('file', file)

        axios.post(this.url, formData, {
          headers: this.headers,
          
          onUploadProgress: (progressEvent) => {
            let percentCompleted = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total
            )

            this.updateFileStatus('uploading', percentCompleted, fileIndex)
          },

        }).then((response) => {

          this.updateFileStatus('uploaded', 100, fileIndex)
          this.$emit('success', response.data)

        }).catch((error) => {

          this.updateFileStatus('failed', 0, fileIndex)
          this.$emit('error', error.response.data.message)

        })

        return
      },

      /**
       * When files are dragged over dropzone area this method is called
       * 
       * @param {*} event
       * @returns {void}
       */
      dragOver(event) {
        event.preventDefault()
        event.stopPropagation()

        this.$refs.dropzone.classList.add('border-nord-frost-400')
        this.$refs.dropzone.classList.remove('border-nord-frost-300')
      },

      /**
       * When files are dragged out of dropzone area this method is called
       * 
       * @param {*} event 
       * @returns {void}
       */
      dragLeave(event) {
        event.preventDefault()
        event.stopPropagation()

        this.$refs.dropzone.classList.add('border-nord-frost-300')
        this.$refs.dropzone.classList.remove('border-nord-frost-400')
      },

      /**
       * When files are dropped in dropzone area this method is called
       * 
       * @param {*} event 
       * @returns {void}
       */
      drop(event) {
        event.preventDefault()
        event.stopPropagation()

        this.$refs.dropzone.classList.add('border-nord-frost-300')
        this.$refs.dropzone.classList.remove('border-nord-frost-400')

        this.prepareValidate(event.dataTransfer.files)
      },

      /**
       * When files are selected from input file or
       * dropped in dropzone area this method is called
       * 
       * @param {*} event 
       * @returns {void}
       */
      filesSelected(event) {
        this.prepareValidate(event.target.files)
      },

      /**
       * Prepare and validate files before upload or send to
       * upload method if uploadOnSelect is set to true
       * 
       * @param {FileList} files 
       * @returns {void}
       */
      prepareValidate(files) {
        if (!this.multiple && files.length > 1) {
          this.$emit('error', 'Only one file can be uploaded')
          return
        }

        if (!this.multiple && this.uploadResults.length > 0) {
          this.uploadResults = []
        }

        if (files.length > this.maxFiles && this.multiple) {
          this.$emit('error', `Max files allowed is ${this.maxFiles}`)
          return
        }

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

          file.status = 'pending'
          file.progress = 0

          this.uploadResults.push(file)
        }

        if (this.uploadOnSelect) {
          this.upload()
        }

        return
      },

      /**
       * Validate single file
       * 
       * @param {File} file 
       * @returns {Boolean}
       */
      validateFile(file) {
        if (!this.extensions.includes(file.name.split('.').pop())) {
          this.$emit('error', 'File type not allowed')
          return false
        }
        
        if (file.size > this.maxFileSize) {
          this.$emit('error', `File size too large. Max file size is ${this.formatBytes(this.maxFileSize)}`)
          return false
        }

        return true
      },

      /**
       * Format bytes to human readable format
       * 
       * @param {Number} bytes 
       * @param {Number} decimals
       * @returns {String}
       */
      formatBytes(bytes, decimals = 2) {
        if (bytes === 0) return '0 Bytes';

        const k = 1024;
        const dm = decimals < 0 ? 0 : decimals;
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        const units = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
        const unit = units[i] || 'Bytes';
        return `${(bytes / Math.pow(k, i)).toFixed(dm)} ${unit}`;
      },
    },

    emits: ['error', 'success']
  }
</script>
webghufk

webghufk1#

通过用一个新对象替换整个文件对象来解决它。这似乎出于某种原因起作用了。

updateFileStatus(status, progress, fileIndex) {
        let file = this.uploadResults[fileIndex]
        file = {
          ...file,
          name: file.name,
          size: file.size,
          status: status,
          progress: progress,
        }

        this.uploadResults.splice(fileIndex, 1, file)
      },

相关问题