什么?封装一个上传视频组件,还要看上传进度?

老规矩,使用简单,同样只需要传递对应的接口数据及参数,其他就不用你操心了。

支持切片上传,大小配置,基础样式以及文件类型限制等

index.vue

<template>
  <div
    :class="['uploader', { 'single-file': !multiple }]"
    @drop.prevent="onDrop"
    @dragover.prevent
    @click="onClick"
    :style="{
      width: containerWidth,
      height: containerHeight ? containerHeight + 'px' : '220px'
    }"
  >
    <input
      type="file"
      :multiple="multiple"
      ref="fileInput"
      @change="onFileChange"
      style="display: none;"
    />
    <div class="upload-progress" v-if="isUploading">
      <p class="current-file-name">{
  
  { currentFileName }}</p>
      <div class="progress-bar">
        <div
          v-show="overallProgress > 1"
          :style="{ width: overallProgress + '%' }"
        ></div>
      </div>
      <p class="progress-text">{
  
  { overallProgress }}%</p>
    </div>

    <div class="upload-preview" v-if="!isUploading && previewItems.length">
      <ul style="width: 100%;">
        <li
          v-for="(item, index) in previewItems"
          :key="index"
          class="preview-item"
          :style="{
            height: containerHeight ? containerHeight - 30 + 'px' : '190px'
          }"
        >
          <img
            v-if="item.type.startsWith('image/')"
            :src="item.url"
            alt="File Preview"
            class="imgStyle"
          />
          <video
            v-else-if="item.type.startsWith('video/')"
            controls
            class="videoStyle"
          >
            <source :src="item.url" :type="item.type" />
          </video>
          <div v-else class="file-preview">
            <slot name="file-preview" :file="item">
              <span class="file-icon">📄</span>
              <span class="file-name">{
  
  { item.name }}</span>
            </slot>
          </div>
          <button
            class="delete-button"
            @click.stop="deleteFile(item, index)"
            type="button"
          >
            ❌
          </button>
        </li>
      </ul>
    </div>

    <span v-if="!files.length && !isUploading && !previewItems.length">
      <p class="drop-text">点击或将文件拖拽</p>
      <p class="drop-text">到这里来上传</p>
    </span>
  </div>
</template>
<script>
import axios from "axios";

export default {
  props: {
    multiple: {
      type: Boolean,
      default: false
    },
    maxSize: {
      type: Number,
      default: 1024 * 1024 * 1024 // 1G
    },
    acceptedTypes: {
      type: Array,
      default: () => [
        "image/jpeg",
        "image/png",
        // "application/msword",
        // "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        // "application/vnd.ms-excel",
        // "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "video/mp4",
        "video/x-m4v",
        "video/x-matroska",
        "video/quicktime",
        "video/x-ms-wmv",
        "video/x-flv"
      ]
    },
    chunkSize: {
      type: Number,
      default: 500 * 1024 * 1024 // 500MB
    },
    containerHeight: {
      type: Number,
      default: 220 // 默认高度为220
    },
    containerWidth: {
      type: String,
      default: "" // 默认宽度为100%
    },
    errorMessage: {
      type: String,
      default: ""
    },
    deleteEvent: {
      type: Object,
      default: function() {
        return {
          api: null, // function
          params: null
        };
      }
    },
    customInterface: {
      type: Object,
      default: function() {
        return {
          api: "adminFile/upload",
          params: {
            isPublic: 1,
            type: "img"
          }
        };
      }
    }
  },
  data() {
    return {
      BASE_API: process.env.BASE_API,
      typeData: ["图片", "视频"],
      files: [],
      previewItems: [],
      isUploading: false,
      uploadedData: [],
      overallProgress: 0,
      totalSize: 0,
      uploadedSize: 0,
      currentFileName: ""
    };
  },
  mounted() {},
  methods: {
    onDrop(event) {
      const newFiles = Array.from(event.dataTransfer.files).filter(
        file => !this.files.includes(file)
      );

      if (!this.multiple && newFiles.length > 1) {
        this.showError("只能选择一个文件");
        return;
      }

      this.handleNewFiles(newFiles);
    },
    onClick() {
      this.$refs.fileInput.click();
    },
    onFileChange(event) {
      const newFiles = Array.from(event.target.files).filter(
        file => !this.files.includes(file)
      );
      this.handleNewFiles(newFiles);
    },
    handleNewFiles(newFiles) {
      if (!this.multiple && newFiles.length > 0) {
        this.reset(); // 重置所有状态
        this.files = [...newFiles]; // 替换文件
      } else {
        this.files = [...this.files, ...newFiles]; // 追加文件
      }
      this.handleFiles();
    },
    handleFiles() {
      if (this.files.length) {
        this.totalSize = this.files.reduce(
          (total, file) => total + file.size,
          0
        );
        this.uploadedSize = 0;
        this.overallProgress = 0;
        this.isUploading = true;
        this.uploadNextFile(0);
      }
    },
    createPreviewItem(file) {
      if (file.type.startsWith("image/") || file.type.startsWith("video/")) {
        return {
          url: URL.createObjectURL(file),
          name: file.name,
          type: file.type
        };
      } else {
        return { name: file.name, type: file.type };
      }
    },
    showError(message) {
      this.$message.error(message);
    },
    removeFile(index) {
      this.files.splice(index, 1);
    },
    deleteFile(item, index) {
      this.previewItems.splice(index, 1);
      this.uploadedData.splice(index, 1); // 同步删除
      if (this.previewItems.length === 0) {
        this.reset();
      }
      // 抛出事件
      this.$emit("change", this.uploadedData, this.previewItems);
      // 如果存在自定义删除方法 则调用
      if (this.deleteEvent.api) {
        let apiFun = this.deleteEvent.api
        let data = this.deleteEvent.params
        apiFun({
          ...data
        }).then((res)=>{
          if(res.code!=0) return this.$message.error(res.msg)
        })
      }
    },
    uploadNextFile(index) {
      if (index >= this.files.length) {
        this.isUploading = false;
        this.$emit("all-files-uploaded", this.uploadedData, this.previewItems);
        return;
      }

      const file = this.files[index];

      if (file.size > this.maxSize) {
        this.showError(`文件 ${file.name} 太大,超过 ${this.maxSize / (1024 * 1024)} MB`);
        this.removeFile(index);
        this.uploadNextFile(index); // 继续下一个文件
        return;
      }

      if (!this.acceptedTypes.includes(file.type)) {
        this.showError(`文件 ${file.name} 类型不正确,仅支持${this.errorMessage ? this.errorMessage : this.typeData.join(", ")}`);
        this.removeFile(index);
        this.uploadNextFile(index); // 继续下一个文件
        return;
      }

      this.currentFileName = file.name;
      this.uploadFile(file, index);
    },
    uploadFile(file, index) {
      this.overallProgress = 0; // 重置进度条
      let completedChunks = 0; // 已上传分块数量
      const totalChunks = Math.ceil(file.size / this.chunkSize);
      const timestamp = new Date().getTime();// 生成当前时间戳
      this.customInterface.params.size = totalChunks // 分块数量
      const uploadChunk = chunkIndex => {
        this.customInterface.params.chunkId = `${timestamp}-${totalChunks}` // 生成唯一标识符
        this.customInterface.params.index = chunkIndex + 1 // 分块索引
        console.log(this.customInterface,'<===uploadChunk');
        const start = chunkIndex * this.chunkSize;
        const end = Math.min(file.size, start + this.chunkSize);
        const chunk = file.slice(start, end);
        let formData = new FormData();
        formData.append("file", chunk, file.name);
        let data = Object.keys(this.customInterface.params);
        data && data.forEach(item => {
          formData.append(item, this.customInterface.params[item] || "");
        });
        //
        axios.post(this.BASE_API + this.customInterface.api, formData, {
            headers: { "Content-Type": "multipart/form-data" },
            onUploadProgress: progressEvent => {
              if (progressEvent.lengthComputable) {
                // 计算当前分块的进度
                let currentProgress = (progressEvent.loaded / progressEvent.total) * 100;
                // 根据已上传的分块数量加上当前分块的进度比例
                this.overallProgress = Math.floor(((completedChunks + currentProgress / 100) / totalChunks) * 100);
                // 确保进度不超过100%
                this.overallProgress = Math.min(this.overallProgress, 100);
              }
            }
          })
          .then(response => {
            completedChunks++;
            // 更新总进度(考虑到可能的浮点数运算误差)
            this.overallProgress = Math.floor((completedChunks / totalChunks) * 100);
            if (completedChunks < totalChunks) {
              uploadChunk(chunkIndex + 1);
            } else {
              let fileData = response.data;
              if (fileData.code != 0) {
                this.showError(`文件 ${file.name} 上传失败!${fileData.msg}`);
              }
              this.uploadedData.push(fileData);
              this.previewItems.push(this.createPreviewItem(file));
              this.removeFile(index);
              this.uploadNextFile(index); // 递归调用,传递当前索引
              this.resetFileInput();
              // 抛出事件
              this.$emit("change", this.uploadedData, this.previewItems);
            }
          })
          .catch(error => {
            this.showError(`文件 ${file.name} 上传失败`);
            this.removeFile(index);
            this.uploadNextFile(index); // 递归调用,传递当前索引
            this.resetFileInput();
            // 抛出错误事件
            this.$emit("error", error, this.uploadedData, this.previewItems);
          });
      };
      uploadChunk(0);
    },
    resetFileInput() {
      if (this.$refs.fileInput) {
        this.$refs.fileInput.value = "";
      }
    },
    reset() {
      this.files = [];
      this.previewItems = [];
      this.isUploading = false;
      this.uploadedData = [];
      this.overallProgress = 0;
      this.totalSize = 0;
      this.uploadedSize = 0;
      this.currentFileName = "";
      this.resetFileInput();
    }
  }
};
</script>
<style scoped>
.uploader {
  border: 2px dashed #ccc;
  padding: 10px;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.3s ease;
  position: relative;
  /* height: 220px; */
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  overflow-y: auto;
}
.uploader:hover {
  border-color: #888;
}
.uploader p {
  margin: 0;
  font-size: 18px;
  color: #c3c7cf;
}

.upload-progress {
  width: 100%;
  margin-bottom: 10px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.current-file-name {
  font-size: 16px;
  color: #666;
  margin-bottom: 10px;
  /* 最多显示2行 超出省略 */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.progress-bar {
  height: 20px;
  background-color: #f3f3f3;
  border-radius: 5px;
  overflow: hidden;
  margin: 10px 0;
  width: 100%;
}

.progress-bar > div {
  height: 100%;
  background-color: #7ac943;
  transition: width 1s ease;
}

.progress-text {
  font-size: 16px;
  color: #7ac943;
  font-weight: bold;
  margin-top: 10px;
}

.upload-preview {
  width: 100%;
  overflow: auto;
}

.preview-item {
  height: 190px;
  overflow: hidden;
  position: relative;
}

.file-preview {
  display: flex;
  align-items: center;
  gap: 10px;
}

.file-icon,
.success-icon {
  font-size: 24px;
}

.file-name {
  font-size: 16px;
  color: #666;
  /* 最多显示2行 超出省略 */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.delete-button {
  position: absolute;
  top: 0;
  right: 0;
  background: none;
  border: none;
  font-size: 12px;
  cursor: pointer;
  color: red;
}

.imgStyle {
  width: 100%;
  height: 100%;
}

.videoStyle {
  width: 100%;
  height: 100%;
  object-fit: cover; /* 保证图片或视频填充整个容器,不留白 */
}

.upload-preview > ul > li + li {
  margin-top: 10px;
}

.single-file .upload-preview {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}
.drop-text {
  line-height: 40px;
}
</style>

使用方法 

html

<MyUpload
  :acceptedTypes="acceptedTypes"
  :containerHeight="220"
  :containerWidth="220"
  errorMessage="请上传图片或视频"
>
</MyUpload>

js

 import MyUpload from "@/components/MyUpload";
    export default {
        components: {
            MyUpload
        },
        data(){
            return{
                multiple: false,
                acceptedTypes:["image/jpeg","image/png","video/mp4","video/x-m4v","video/x-matroska","video/quicktime","video/x-ms-wmv","video/x-flv"],
            }
        },
        
    }
## 属性



| 属性名          | 类型    | 默认值                 | 说明           |

| --------------- | ------- | ---------------------- | -------------- |

| maxSize         | Number  | 1024 _1024_ 1024       | 最大文件大小   |

| chunkSize       | Number  | 500 _1024_ 1024        | 切片大小       |

| deleteEvent     | Object  | {api:"",params:object} | 自定义删除接口 |

| customInterface | Object  | {api:"",params:object} | 自定义上传接口 |

| multiple        | Boolean | false                  | 是否多选上传   |

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值