SOURCE

console 命令行工具 X clear

                    
>
console
<template>
  <div class="file-upload-container">
    <!-- 拖拽上传区域 -->
    <div class="drop-zone" @dragover.prevent @drop.prevent="onDrop" :class="{ 'disabled': disabled }">
      <input type="file" multiple :accept="getAcceptedFileTypes()" @change="onFileSelect" ref="fileInput" style="display: none" :disabled="disabled">
      <button @click="openFileDialog" :disabled="disabled || readonly">
        <span>选择文件</span>
      </button>
    </div>
    <!-- 文件展示列表区域 -->
    <div class="file-list-container">
      <ul class="file-list">
        <li v-for="file in selectedFiles" :key="file.id">
          <div class="file-info">
            <span class="file-name">{{ file.name }}</span>
            <button @click="confirmRemoveFile(file.id)" class="delete-icon" :disabled="disabled || readonly">
              <i class="el-icon-delete"></i>
            </button>
          </div>
          <!-- 只有在文件有进度且未上传完成时才展示进度条 -->
          <div class="progress-container" v-if="file.progress!== undefined && file.progress < 100 &&!file.uploaded">
            <div class="progress-bar">
              <div class="progress" :style="{ width: file.progress + '%' }"></div>
            </div>
            <span class="progress-text">{{ file.progress }}%</span>
          </div>
        </li>
      </ul>
      <div v-if="invalidFiles.length > 0" class="invalid-files">
        <p v-if="invalidFiles.some((file) => file.invalidType)">以下文件类型不被支持:</p>
        <ul>
          <li v-for="file in invalidFiles.filter((file) => file.invalidType)" :key="file.id">
            {{ file.name }}
            <button @click="confirmRemoveInvalidFile(file.id, 'invalidType')" class="delete-icon" :disabled="disabled || readonly">
              <i class="el-icon-delete"></i>
            </button>
          </li>
        </ul>
        <p v-if="invalidFiles.some((file) => file.invalidSize)">以下文件大小超出限制:</p>
        <ul>
          <li v-for="file in invalidFiles.filter((file) => file.invalidSize)" :key="file.id">
            {{ file.name }}
            <button @click="confirmRemoveInvalidFile(file.id, 'invalidSize')" class="delete-icon" :disabled="disabled || readonly">
              <i class="el-icon-delete"></i>
            </button>
          </li>
        </ul>
        <p v-if="invalidFiles.some((file) => file.invalidCount)">以下文件因数量限制无法添加:</p>
        <ul>
          <li v-for="file in invalidFiles.filter((file) => file.invalidCount)" :key="file.id">
            {{ file.name }}
            <button @click="confirmRemoveInvalidFile(file.id, 'invalidCount')" class="delete-icon" :disabled="disabled || readonly">
              <i class="el-icon-delete"></i>
            </button>
          </li>
        </ul>
      </div>
      <button @click="clearFiles" v-if="selectedFiles.length > 0" :disabled="disabled || readonly">清空已选文件</button>
      <button @click="startUpload" v-if="selectedFiles.some(file => file.progress === undefined || (file.progress < 100 &&!file.uploaded))" :disabled="disabled || readonly">开始上传</button>
    </div>
  </div>
</template>

<script>
import { MessageBox, Message } from 'element-ui';

export default {
  model: {
    prop: 'value',
    event: 'input'
  },
  props: {
    value: {
      type: Array,
      default: () => []
    },
    acceptedFileTypes: {
      type: String,
      default: ''
    },
    maxFileSize: {
      type: Number,
      default: Infinity
    },
    maxFileCount: {
      type: Number,
      default: Infinity
    },
    disabled: {
      type: Boolean,
      default: false
    },
    readonly: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      isDragging: false,
      selectedFiles: this.value.map(file => ({...file, uploaded: file.uploaded || false })),
      invalidFiles: [],
      uploadInProgress: false,
      initialFiles: this.value.map(file => file.id)
    };
  },
  watch: {
    value: {
      handler(newValue) {
        this.selectedFiles = newValue.map(file => ({...file, uploaded: file.uploaded || false }));
        this.initialFiles = newValue.map(file => file.id);
      },
      deep: true
    }
  },
  methods: {
    getAcceptedFileTypes() {
      if (this.acceptedFileTypes) {
        return this.acceptedFileTypes;
      } else {
        return 'application/*,text/*';
      }
    },
    openFileDialog() {
      if (!this.disabled &&!this.readonly) {
        this.$refs.fileInput.click();
      }
    },
    onFileSelect(event) {
      console.log('原生的 onFileSelect', event.target.files);
      if (!this.disabled &&!this.readonly) {
        const files = Array.from(event.target.files);
        this.processFiles(files);
      }
    },
    onDrop(event) {
      if (!this.disabled &&!this.readonly) {
        this.isDragging = false;
        const files = Array.from(event.dataTransfer.files);
        this.processFiles(files);
      }
    },
    onDragOver() {
      this.isDragging = true;
    },
    onDragLeave() {
      this.isDragging = false;
    },
    processFiles(files) {
      const validFiles = [];
      const newInvalidFiles = [];
      const allowedTypes = this.getAcceptedFileTypes().split(',').map((type) => type.trim());
      const availableCount = this.maxFileCount - this.selectedFiles.length;

      files.forEach((file) => {
        const invalidType =!allowedTypes.includes('*') &&!allowedTypes.some((type) => file.type === type);
        const invalidSize = file.size > this.maxFileSize;
        const invalidCount = availableCount <= 0;

        if (!invalidType &&!invalidSize &&!invalidCount) {
          const newFile = {
            id: Date.now() + Math.random(),
            name: file.name,
            progress: 0,
            uploaded: false,
          };
          validFiles.push(newFile);
        } else {
          const newInvalidFile = {
            id: Date.now() + Math.random(),
            name: file.name,
            invalidType,
            invalidSize,
            invalidCount,
          };
          newInvalidFiles.push(newInvalidFile);
        }
      });

      this.selectedFiles = this.selectedFiles.concat(validFiles);
      this.invalidFiles = this.invalidFiles.concat(newInvalidFiles);
      this.$emit('input', this.selectedFiles);
      this.$emit('files-changed', { validFiles, invalidFiles: newInvalidFiles });
    },
    resInputValue(refKey) {
      this.$refs[refKey].value = '';
    },
    clearFiles() {
      if (!this.disabled &&!this.readonly) {
        this.selectedFiles = [];
        this.invalidFiles = [];
        this.resInputValue('fileInput');
        this.$emit('input', []);
        this.$emit('files-cleared');
      }
    },
    startUpload() {
      console.log('startUpload');
      if (!this.disabled &&!this.readonly) {
        this.resInputValue('fileInput');
        this.uploadInProgress = true;
        this.selectedFiles.forEach((file) => {
          if (!this.initialFiles.includes(file.id) &&!file.uploaded) {
            file.progress = 0;
            this.getUploadCredential()
              .then((credentialId) => {
                this.uploadToTerr(file, credentialId, {
                  onProgress: (credentialId, progress) => {
                    const updatedFiles = this.selectedFiles.map((f) =>
                      f.id === file.id? { ...f, progress } : f
                    );
                    this.selectedFiles = updatedFiles;
                    this.$emit('input', this.selectedFiles);
                  },
                  onSuccess: (credentialId, status) => {
                    console.log(`文件 ${file.name} 上传成功,凭证 ID: ${credentialId},状态: ${status}`);
                    const updatedFiles = this.selectedFiles.map((f) =>
                      f.id === file.id? { ...f, progress: 100, uploaded: true } : f
                    );
                    this.selectedFiles = updatedFiles;
                    this.$emit('input', this.selectedFiles);
                  },
                  onFailure: (credentialId, status) => {
                    console.error(`文件 ${file.name} 上传失败,凭证 ID: ${credentialId},状态: ${status}`);
                    this.removeFile(file.id);
                    Message.error(`文件 ${file.name} 上传失败,已删除`);
                  },
                });
              })
              .catch((error) => {
                console.error(`获取上传凭证失败: ${error}`);
              });
          }
        });
      }
    },
    getUploadCredential() {
      return new Promise((resolve) => {
        setTimeout(() => {
          const credentialId = `credential_${Date.now()}`;
          resolve(credentialId);
        }, 500);
      });
    },
    uploadToTerr(file, credentialId, callbacks) {
      let progress = 0;
      const interval = setInterval(() => {
        if (Math.random() < 0.05) {
          clearInterval(interval);
          callbacks.onFailure(credentialId, '上传失败');
        } else {
          progress += 10;
          if (progress >= 100) {
            clearInterval(interval);
            callbacks.onSuccess(credentialId, '上传成功');
          } else {
            callbacks.onProgress(credentialId, progress);
          }
        }
      }, 500);
    },
    confirmRemoveFile(id) {
      if (!this.disabled &&!this.readonly) {
        MessageBox.confirm('确定要删除这个文件吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning',
        })
          .then(() => {
            this.removeFile(id);
          })
          .catch(() => {
            // 用户取消操作,不做处理
          });
      }
    },
    confirmRemoveInvalidFile(id, reason) {
      if (!this.disabled &&!this.readonly) {
        MessageBox.confirm('确定要删除这个无效文件吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning',
        })
          .then(() => {
            this.removeInvalidFile(id, reason);
          })
          .catch(() => {
            // 用户取消操作,不做处理
          });
      }
    },
    removeFile(id) {
      this.selectedFiles = this.selectedFiles.filter((file) => file.id!== id);
      this.$emit('input', this.selectedFiles);
      this.$emit('file-removed', id);
    },
    removeInvalidFile(id, reason) {
      this.invalidFiles = this.invalidFiles.filter((file) => file.id!== id);
      this.$emit('invalid-file-removed', id, reason);
    },
  },
  mounted() {
    const dropArea = this.$el.querySelector('.drop-zone');
    dropArea.addEventListener('dragover', this.onDragOver);
    dropArea.addEventListener('dragleave', this.onDragLeave);
  },
  beforeDestroy() {
    const dropArea = this.$el.querySelector('.drop-zone');
    dropArea.removeEventListener('dragover', this.onDragOver);
    dropArea.removeEventListener('dragleave', this.onDragLeave);
  },
};
</script>

<style scoped>
.file-upload-container {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.drop-zone {
  position: relative;
  margin-bottom: 20px;
}

.drop-zone button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  cursor: pointer;
  border-radius: 4px;
  transition: background-color 0.3s ease;
}

.drop-zone button:hover {
  background-color: #0056b3;
}

.drop-zone.disabled button {
  background-color: #ccc;
  cursor: not-allowed;
}

.file-list-container {
  width: 100%;
}

.file-list {
  list-style-type: none;
  padding: 0;
  margin-top: 20px;
  text-align: left;
}

.file-list li {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  padding: 10px;
  border-bottom: 1px solid #eee;
  transition: background-color 0.3s ease;
}

.file-list li:hover {
  background-color: #f9f9f9;
}

.file-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}

.file-name {
  font-size: 14px;
  color: #333;
}

.progress-container {
  display: flex;
  align-items: center;
  width: 100%;
  margin-top: 5px;
}

.progress-bar {
  flex: 1;
  height: 10px;
  background-color: #eee;
  border-radius: 5px;
  overflow: hidden;
  margin-right: 10px;
}

.progress {
  height: 100%;
  background-color: #007bff;
  transition: width 0.3s ease;
}

.progress-text {
  font-size: 12px;
  color: #007bff;
}

.invalid-files {
  color: red;
  margin-top: 10px;
  text-align: left;
}

.delete-icon {
  background: none;
  border: none;
  cursor: pointer;
  color: #dc3545;
  font-size: 18px;
}

.delete-icon:disabled {
  color: #ccc;
  cursor: not-allowed;
}

.delete-icon:hover {
  color: #c82333;
}
</style>