前端文件上传组件最全封装+删除+下载+预览

前言:使用的是若依的框架+element ui+vue2封装的。如果有不对的地方欢迎指出。后台管理使用,文件需要上传。回显列表,详情也需要回显+预览

javascript 复制代码
// 开始封装组件:封装在 src/components/FileUpload/index.vue中
<template>
  <div class="upload-file">
    <el-upload
      multiple
      name="multipartFile"
      :action="uploadFileUrl"
      :data="{ 上传时附带的额外参数 }"
      :limit="limit"
      :file-list="fileList"
      :before-upload="handleBeforeUpload"
      :on-exceed="handleExceed"
      :on-error="handleUploadError"
      :on-success="handleUploadSuccess"
      :show-file-list="false"
      :headers="headers"
      class="upload-file-uploader"
      ref="fileUpload"
    >
      <!-- 上传按钮 -->
      <el-button size="mini" type="primary" plain v-if="!disabled">选取文件</el-button>
      <!-- 上传提示 -->
      <div class="el-upload__tip" slot="tip" v-if="showTip && !disabled">
        请上传
        <template v-if="fileSize">
          大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
        </template>
        <template v-if="fileType">
          格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
        </template>
        的文件
      </div>
    </el-upload>

    <!-- 文件列表,我们功能需要 删除文件、下载文件、预览文件功能 -->
    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul" style="min-width: 300px">
      <li class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList" :key="file.fileId">
        <el-link class="link" :href="`${baseUrl}${file.filePath}`" :underline="false" target="_blank">
          <span class="el-icon-document" style="padidng-left: 10px">
            {{ getFileName(file.fileName) }}
          </span>
        </el-link>
        <div class="controls">
          <div class="ele-upload-list__item-content-action" style="width: 50px; text-align: center">
            <el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled">删除</el-link>
          </div>
          <div class="ele-upload-list__item-content-action" style="width: 50px; text-align: center">
            <el-link :underline="false" @click="handlePreview(file)" type="danger">预览</el-link>
          </div>
          <div class="ele-upload-list__item-content-action" style="width: 50px; text-align: center">
            <el-link :underline="false" @click="handleDownload(file)" type="danger">下载</el-link>
          </div>
        </div>
      </li>
    </transition-group>

	<!-- 文件预览功能,点击文件列表后的预览按钮,需要预览文档,pdf,excel,照片,视频,音频。其他没有封装,所以就不支持 -->
    <el-dialog title="文件预览" :visible.sync="preview.open" append-to-body :before-close="previewCancel">
      <vue-office-docx v-if="fileSuffix == 'docx'" :src="preview.url" style="height: 100vh" />
      <vue-office-excel v-else-if="fileSuffix == 'xlsx'" :src="preview.url" style="width: auto; height: 100vh" />
      <vue-office-pdf v-else-if="fileSuffix == 'pdf'" :src="preview.url" style="height: 100vh" />
      <div style="text-align: center" v-else-if="fileSuffix == 'img'">
        <el-image style="width: 500px" :src="preview.url" fit="fill" :preview-src-list="[preview.url]"></el-image>
      </div>
      <div style="text-align: center" v-else-if="fileSuffix == 'mp3'">
        <audio controls loop ref="myAudio" autoplay class="my-audio">
          <source :src="preview.url" />
        </audio>
      </div>
      <div style="text-align: center" v-else-if="fileSuffix == 'mp4'">
        <video-app :src="preview.url" :second="1"></video-app>
      </div>
      <div style="text-align: center" v-else>暂不支持该文件预览,请下载预览</div>
    </el-dialog>
  </div>
</template>

<script>
import { getToken } from "@/utils/auth";
import { 查看和下载的接口,后端给的 } from "";

//引入VueOfficeDocx组件,需要npm安装
import VueOfficeDocx from "@vue-office/docx";
import "@vue-office/docx/lib/index.css";

//引入VueOfficeExcel组件,需要npm安装
import VueOfficeExcel from "@vue-office/excel";
import "@vue-office/excel/lib/index.css";

//引入VueOfficePdf组件,需要npm安装
import VueOfficePdf from "@vue-office/pdf";

import VideoApp from "./video.vue"; // 这个是我封装的视频预览的组件

export default {
  name: "FileUpload",
  props: {
    // 数量限制
    limit: {
      type: Number,
      default: 5,
    },
    // 大小限制(MB)
    fileSize: {
      type: Number,
      default: 20,
    },
    // 文件类型, 例如['png', 'jpg', 'jpeg']
    fileType: {
      type: Array,
      default: () => ["docx", "pptx", "pdf"],
    },
    // 是否显示提示
    isShowTip: {
      type: Boolean,
      default: true,
    },
    formFileList: {
      type: Array,
      default: () => [],
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    // 培训记录附件的类型
    busiType: {
      type: String,
    },
  },
  components: {
    VueOfficeDocx,
    VueOfficeExcel,
    VueOfficePdf,
    VideoApp,
  },
  data() {
    return {
      number: 0,
      uploadList: [],
      baseUrl: // 地址 ,
      uploadFileUrl: , // 上传文件服务器地址
      headers: {
        Authorization: "Bearer " + getToken(),
      },
      fileList: [],

      lookFile: false,
      url: "",
      // 文件预览
      preview: {
        open: false,
        url: "",
      },
      fileSuffix: "",
    };
  },
  watch: {
  	// 编辑和详情的回显fileList
    formFileList: {
      handler(val) {
        if (val !== undefined) {
          this.fileList = val;
        }
        if (val == null) {
          this.fileList = [];
          return;
        }
      },
      deep: true,
      immediate: true,
    },
  },
  computed: {
    // 是否显示提示
    showTip() {
      return this.isShowTip && (this.fileType || this.fileSize);
    },
  },
  methods: {
    // 上传前校检格式和大小
    handleBeforeUpload(file) {
      // 校检文件类型
      if (this.fileType) {
        const fileName = file.name.split(".");
        const fileExt = fileName[fileName.length - 1];
        const isTypeOk = this.fileType.length ? this.fileType.indexOf(fileExt) >= 0 : [];
        if (!isTypeOk) {
          this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`);
          return false;
        }
      }
      // 校检文件大小
      if (this.fileSize) {
        const isLt = file.size / 1024 / 1024 < this.fileSize;
        if (!isLt) {
          this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`);
          return false;
        }
      }
      this.$modal.loading("正在上传文件,请稍候...");
      this.number++;
      return true;
    },
    // 文件个数超出
    handleExceed() {
      this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
    },
    // 上传失败
    handleUploadError(err) {
      this.number--;
      this.$modal.msgError("上传文件失败,请重试");
      this.$modal.closeLoading();
    },
    // 上传成功回调
    handleUploadSuccess(res, file) {
      if (res.code === 200) {
        this.uploadList.push(res.data);
        this.$modal.closeLoading();
        this.uploadedSuccessfully();
      } else {
        this.number--;
        this.$modal.closeLoading();
        this.$modal.msgError(res.msg);
        this.$refs.fileUpload.handleRemove(file);
        this.uploadedSuccessfully();
      }
    },
    // 删除文件
    handleDelete(index) {
      this.fileList.splice(index, 1);
      this.$emit("input", this.listToString(this.fileList));
    },
    // 上传结束处理
    uploadedSuccessfully() {
      if (this.number > 0 && this.uploadList.length === this.number) {
        this.fileList = this.fileList.concat(this.uploadList);
        this.uploadList = [];
        this.number = 0;
        this.$emit("input", this.listToString(this.fileList));
        this.$modal.closeLoading();
        this.$emit("fileUploadSuccess", this.fileList);
      }
    },
    // 获取文件名称
    getFileName(name) {
      if (name?.lastIndex/Of("/") > -1) {
        return name.slice(name.lastIndexOf("/") + 1);
      } else {
        return name;
      }
    },
    // 对象转成指定字符串分隔
    listToString(list, separator) {
      let strs = "";
      separator = separator || ",";
      for (let i in list) {
        strs += list[i].url + separator;
      }
      return strs != "" ? strs.substr(0, strs.length - 1) : "";
    },
    resetFileList() {
      this.fileList = [];
    },
    // 下载
    handleDownload(file) {
    	
      downloadFile(file.fileId).then((res) => {
        this.exportFunction(res, file.fileName, file.fileType);
      });
    },
    exportFunction(response, name, type) {
      const link = document.createElement("a");
      const blob = new Blob([response], { type });
      link.style.display = "none";
      link.href = URL.createObjectURL(blob);
      link.setAttribute("download", name, type);
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    },
    // 预览
    handlePreview(file) {
      this.preview.url = "";
      if (file.fileType.indexOf("image") !== -1) {
        this.fileSuffix = "img";
      } else {
        this.fileSuffix = file.suffix;
      }
      
      showFileURL(file.fileId).then((res) => {
        this.preview.url = defaultSettings.minioUrl + res;
        if (file.suffix == "mp3") {
          this.$nextTick((res) => {
            this.$refs.myAudio.load();
            this.$refs.myAudio.play();
          });
        }
        this.preview.open = true;
      });
    },
    previewCancel() {
      this.preview.open = false;
      this.preview.url = "";
      if (this.fileSuffix == "mp3") {
        this.$refs.myAudio.pause();
      }
    },
  },
};
</script>

<style scoped lang="scss">
::v-deep .el-button--primary.is-plain {
  color: var(--select-selected-color);
  background: var(--table-content-bColor);
  border-color: var(--search-back);
}
::v-deep .el-button--primary.is-plain:hover,
::v-deep .el-button--primary.is-plain:focus {
  border-color: var(--select-selected-color);
  background-color: var(--select-selected-color);
  color: #fff !important;
}

.upload-file-uploader {
  margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
  border: 1px solid #e4e7ed;
  line-height: 2;
  margin-bottom: 10px;
  position: relative;
}
.upload-file-list .ele-upload-list__item-content {
  display: flex;
  color: inherit;
  .link {
    flex: 1;
    display: block;
    margin-left: 2px;
    ::v-deep .el-icon-document {
      width: 310px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  }
  .controls {
    display: flex;
  }
}
.ele-upload-list__item-content-action .el-link {
  margin-right: 10px;
}
::v-deep {
  .x-spreadsheet-table {
    width: auto !important;
  }
}
.my-audio {
  width: 100%;
}
</style>

video组件封装:

javascript 复制代码
<template>
  <div class="m-video" :class="{'u-video-hover': !hidden}" :style="`width: ${width}px; height: ${height}px;`">
    <video
      ref="veo"
      :style="`object-fit: ${zoom};`"
      :src="src"
      :poster="veoPoster"
      :width="width"
      :height="height"
      :autoplay="autoplay"
      :controls="!originPlay&&controls"
      :loop="loop"
      :muted="autoplay || muted"
      :preload="preload"
      crossorigin="anonymous"
      @loadeddata="poster ? () => false : getPoster()"
      @pause="showPlay ? onPause() : () => false"
      @playing="showPlay ? onPlaying() : () => false"
      @click.prevent.once="onPlay"
      v-bind="$attrs">
      您的浏览器不支持video标签。
    </video>
    <svg v-show="originPlay || showPlay" class="u-play" :class="{'hidden': hidden}" :style="`width: ${playWidth}px; height: ${playWidth}px;`" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.75 6.75C4.75 5.64543 5.64543 4.75 6.75 4.75H17.25C18.3546 4.75 19.25 5.64543 19.25 6.75V17.25C19.25 18.3546 18.3546 19.25 17.25 19.25H6.75C5.64543 19.25 4.75 18.3546 4.75 17.25V6.75Z"></path>
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.25 12L9.75 8.75V15.25L15.25 12Z"></path>
    </svg>
  </div>
</template>
<script>
export default {
  name: 'Video',
  props: {
    src: { // 视频文件url,必传,支持网络地址 https 和相对地址 require('@/assets/files/Bao.mp4')
      type: String,
      required: true,
      default: ''
    },
    poster: { // 视频封面url,支持网络地址 https 和相对地址 require('@/assets/images/Bao.jpg')
      type: String,
      default: ''
    },
    second: { // 在未设置封面时,自动截取视频第 second 秒对应帧作为视频封面
      type: Number,
      default: 0.5
    },
    width: { // 视频播放器宽度
      type: Number,
      default: 800
    },
    height: { // 视频播放器高度
      type: Number,
      default: 450
    },
    autoplay: { // 视频就绪后是否马上播放
      type: Boolean,
      default: false
    },
    controls: { // 是否向用户显示控件,比如进度条,全屏
      type: Boolean,
      default: true
    },
    loop: { // 视频播放完成后,是否循环播放
      type: Boolean,
      default: false
    },
    muted: { // 是否静音
      type: Boolean,
      default: false
    },
    preload: { // 是否在页面加载后载入视频,如果设置了autoplay属性,则preload将被忽略;
      type: String,
      default: 'auto' // auto:一旦页面加载,则开始加载视频; metadata:当页面加载后仅加载视频的元数据 none:页面加载后不应加载视频
    },
    showPlay: { // 播放暂停时是否显示播放器中间的暂停图标
      type: Boolean,
      default: true
    },
    playWidth: { // 中间播放暂停按钮的边长
      type: Number,
      default: 96
    },
    zoom: { // video的poster默认图片和视频内容缩放规则
      type: String,
      default: 'contain' // none:(默认)保存原有内容,不进行缩放; fill:不保持原有比例,内容拉伸填充整个内容容器; contain:保存原有比例,内容以包含方式缩放; cover:保存原有比例,内容以覆盖方式缩放
    }
  },
  data () {
    return {
      veoPoster: this.poster,
      originPlay: true,
      hidden: false
    }
  },
  mounted () {
    if (this.autoplay) {
      this.hidden = true
      this.originPlay = false
    }
    /*
      自定义设置播放速度,经测试:
      在vue2中需设置:this.$refs.veo.playbackRate = 2
      在vue3中需设置:veo.value.defaultPlaybackRate = 2
    */
    // this.$refs.veo.playbackRate = 2
  },
  methods: {
    /*
      loadeddata 事件在媒体当前播放位置的视频帧(通常是第一帧)加载完成后触发
      preload为none时不会触发
    */
    getPoster () { // 在未设置封面时,自动获取视频0.5s对应帧作为视频封面
      // 由于不少视频第一帧为黑屏,故设置视频开始播放时间为0.5s,即取该时刻帧作为封面图
      this.$refs.veo.currentTime = this.second
      // 创建canvas元素
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      // canvas画图
      canvas.width = this.$refs.veo.videoWidth
      canvas.height = this.$refs.veo.videoHeight
      ctx.drawImage(this.$refs.veo, 0, 0, canvas.width, canvas.height)
      // 把canvas转成base64编码格式
      this.veoPoster = canvas.toDataURL('image/png')
    },
    onPlay () {
      if (this.originPlay) {
        this.$refs.veo.currentTime = 0
        this.originPlay = false
      }
      if (this.autoplay) {
        this.$refs.veo.pause()
      } else {
        this.hidden = true
        this.$refs.veo.play()
      }
    },
    onPause () {
      this.hidden = false
    },
    onPlaying () {
      this.hidden = true
    }
  }
}
</script>
<style lang="scss" scoped>
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
.m-video {
  display: inline-block;
  position: relative;
  background: #000;
  cursor: pointer;
  .u-play {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    margin: auto;
    fill: none;
    color: #FFF;
    pointer-events: none;
    opacity: 0.7;
    transition: opacity .3s;
    path {
      stroke: #FFF;
    }
  }
  .hidden {
    opacity: 0;
  }
}
.u-video-hover {
  &:hover {
    .u-play {
      opacity: 0.9;
    }
  }
}
</style>

使用组件:

javascript 复制代码
import FileUpload= from "@/components/FileUpload=";
<el-form-item label="文件上传">
  <file-upload
    ref="fileResetRef"
    @fileUploadSuccess="fileUploadSuccessHandle"
    :formFileList="form.files" // 回显的数据文件列表
    :disabled="isReadonly" // 区分编辑还是查看
    :file-type="[ // 支持的类型
      'png',
      'jpg',
      'docx',
      'xlsx',
      'pptx',
      'pdf',
      'mp3',
      'mp4',
      'zip',
    ]"
  >
  </file-upload>
</el-form-item>
// 文件上传成功的展示
fileUploadSuccessHandle1(fileList) {
  this.form.files = fileList;
},

上传组件效果图:

上传的文件列表:

相关推荐
Martin -Tang18 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发19 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端