springboot+vue自定义上传图片及视频

说明:本例采用的是将图片视频全都先传到前端,再一次性提交给后端的思路,另外还做了一个上传前的预览

效果图

在这里插入图片描述

vue页面代码

<el-drawer
  size="40%"
  :visible.sync="collect_drawer"
  :append-to-body="true"
  :wrapperClosable="false"
>
  <template slot="title">
    <span class="drawer-title">{{ collect_drawer_title }}</span>
  </template>
  <el-form
    ref="collect_form"
    :rules="collectRules"
    :model="collect"
    label-width="138px"
    style="width: 100%; padding: 0 20px 0 0; height: 85%"
  >
    <el-form-item>
      <span slot="label"
        ><span style="color: #f56c6c; margin-right: 4px">*</span
        >产品图片/视频</span
      >
      <div style="display: flex; flex-wrap: wrap">
        <!-- 循环遍历 要不就是video 要不就是img -->
        <div
          v-for="(item, index) in pro_files"
          :key="index + 'proimg'"
          class="video_border"
        >
        <!-- 视频块 -->
          <div
            style="width: 130px; height: 130px; position: relative"
            v-if="item.type == 'video'"
          >
            <i
              class="iconfont icon-al-fill-right play-icon"
              @click="getPlayer(index + 1)"
            />
            <div class="crop">
              <video
                :ref="'video' + (index + 1)"
                class="video_li"
                style="
                  width: 130px;
                  height: 130px;
                  margin-right: 10px;
                  object-fit: cover;
                "
              ></video>
              <div class="delete-div">
                <i
                  class="el-icon-delete delete_icon"
                  @click="deleteFile(index)"
                ></i>
              </div>
            </div>
          </div>
           <!-- 图片块 -->
          <div
            style="width: 130px; height: 130px; position: relative"
            v-else-if="item.type == 'image'"
          >
            <div class="crop">
              <img
                @click="getImage(index + 1)"
                :ref="'image' + (index + 1)"
                class="rect_img"
              />
              <div class="delete-div">
                <i
                  class="el-icon-delete delete_icon"
                  @click="deleteFile(index)"
                ></i>
              </div>
            </div>
          </div>
          <!-- <el-progress
            class="video-progress"
            :percentage="item.loaded"
          ></el-progress> -->
        </div>
        <input
          type="file"
          style="position: absolute"
          accept="image/jpg,image/jpeg,image/bmp,image/png,image/gif,video/mp4,video/webm,video/ogg"
          ref="myfile"
          name="file"
          id="file"
          @change="uploadMyFile"
          hidden
        />
        <label for="file" style="id">
          <div id="file_up"><i class="el-icon-plus"></i></div>
        </label>
      </div>
    </el-form-item>

    <el-form-item label="产品报价" prop="price">
      <el-input
        v-model="collect.price"
        size="medium"
        placeholder="请输入产品报价(元)"
      ></el-input>
    </el-form-item>
    <el-form-item label="网址" prop="url">
      <el-input
        v-model="collect.url"
        size="medium"
        placeholder="请输入网址"
      ></el-input>
    </el-form-item>
    <el-form-item label="来源" prop="source">
      <el-input
        v-model="collect.source"
        size="medium"
        placeholder="请输入来源"
      ></el-input>
    </el-form-item>
    <el-form-item label="卖家" prop="seller">
      <el-input
        v-model="collect.seller"
        size="medium"
        placeholder="请输入卖家"
      ></el-input>
    </el-form-item>
    <el-form-item label="规格" prop="spec">
      <el-input
        v-model="collect.spec"
        size="medium"
        placeholder="请输入规格(cm)"
      ></el-input>
    </el-form-item>
    <el-form-item label="说明" prop="note">
      <el-input
        type="textarea"
        rows="3"
        resize="none"
        v-model="collect.note"
        size="medium"
        placeholder="请输入说明"
      ></el-input>
    </el-form-item>
  </el-form>
  <div style="text-align: center; height: 10%; padding: 10px">
    <el-button type="primary" @click="saveOrUpdate">保存</el-button>
    <el-button type="primary" @click="closeDrawer">取消</el-button>
  </div>
</el-drawer>
<!-- 视频展示框 -->
<el-dialog
	:visible.sync="videoVisible"
	width="80%"
	top="3vh"
	:before-close="resetVideo"
>
 <div style="height: 80vh; text-align: center">
   <video
     src=""
     style="width: 100%; height: 100%; object-fit: contain"
     ref="showVideo"
     controls="controls"
   ></video>
 </div>
</el-dialog>
<!-- 图片展示框 -->
<el-dialog
	 :visible.sync="dialogVisible"
	 append-to-body
	 width="80%"
	 top="3vh"
>
 <div style="height: 80vh; text-align: center">
   <img
     :src="dialogImageUrl"
     alt=""
     style="height: 100%; width: 100%; object-fit: contain"
   />
 </div>
</el-dialog>

data数据定义

data() {
//这里存放数据
 return {
  //视频展示框是否显示
  videoVisible: false,
  //图片展示框是否显示
  dialogVisible: false,
  //图片展示框对应url
  dialogImageUrl: undefined,
  //图片视频文件实际存放数组
  pro_files: [],
  //记录删除前的文件队列
  update_src: [],
  //控制抽屉
  collect_drawer: false,
  //抽屉标题
  collect_drawer_title: undefined,
  //实体类
  collect: {
    collect_id: undefined,
    note: undefined,
    price: undefined,
    url: undefined,
    source: undefined,
    seller: undefined,
    spec: undefined,
  },
  //表单校验
  collectRules: {},
},

methods方法定义

methods: {
    bindBeforeunLoad() {
      window.onbeforeunload = this.perforresult;
    },
    unBindBeforeunLoad() {
      window.onbeforeunload = null;
    },
    perforresult() {
      return "";
    },
    deleteFile(index) {
      //由于删除的时候
      this.update_src = [];
      for (var i = index + 1; i < this.pro_files.length; i++) {
        if (this.pro_files[i].type === "video") {
          this.update_src.push(this.$refs["video" + (i + 1)][0].src);
        } else {
          this.update_src.push(this.$refs["image" + (i + 1)][0].src);
        }
      }

      this.pro_files.splice(index, 1);

      this.$nextTick(() => {
        for (var j = 0; j < this.update_src.length; j++) {
          for (var i = index++; i < this.pro_files.length; i++) {
            if (this.pro_files[i].type === "video") {
              this.$refs["video" + (i + 1)][0].src = this.update_src[j];
            } else {
              this.$refs["image" + (i + 1)][0].src = this.update_src[j];
            }
            break;
          }
        }
      });
      this.$refs.myfile.value = null;
    },
    /**开启全屏loading */
    startLoading() {
      this.loading = this.$loading({
        lock: true,
        text: "加载中...",
        spinner: "el-icon-loading",
        background: "rgba(0, 0, 0, 0.8)",
      });
    },
    /**关闭loading**/
    closeLoading() {
      this.loading.close();
    },

    //用于关闭之后重置视频
    resetVideo() {
      this.$refs.showVideo.pause();
      this.videoVisible = false;
    },
    //上传视频方法
    uploadMyFile() {
      const that = this;
      //获取到 input file
      let myfile = that.$refs.myfile;
      //获取到上传的file文件
      let file = myfile.files[0];
      if (myfile.files[0]) {
        //判断如果是视频
        if (file.type.indexOf("video") != -1) {
          //先将视频文件放到视频文件总数组
          that.$set(that.pro_files, that.pro_files.length, {
            file: file,
            type: "video",
          });
          //因为我根据v-for总数组来渲染的,所以当我去设置数组,会有延迟渲染,取不到ref,用nextTick,等渲染结束再操作
          that.$nextTick(() => {
            //获取到当前上传的文件的虚拟dom,用filReader读取文件,并且把视频对应的file信息装填上去。
            let video = that.$refs["video" + that.pro_files.length][0];
            if (window.FileReader) {
              var fr = new FileReader();
              // fr.onprogress = function (e) {
              //   that.$set(
              //     that.pro_files[that.pro_files.length - 1],
              //     "loaded",
              //     (e.loaded / file.size).toFixed(2) * 100
              //   );
              // };
              fr.onloadend = function (e) {
                //上传完成之后
                video.src = e.target.result;
                video.title = myfile.files[0].name;
              };
              fr.readAsDataURL(myfile.files[0]);
            }
          });
        } else {
          //与上传视频几乎同理,不再赘述
          that.$set(that.pro_files, that.pro_files.length, {
            file: file,
            type: "image",
          });
          that.$nextTick(() => {
            let image = that.$refs["image" + that.pro_files.length][0];
            if (window.FileReader) {
              var fr = new FileReader();
              // fr.onprogress = function (e) {
              //   that.$set(
              //     that.pro_files[that.pro_files.length - 1],
              //     "loaded",
              //     (e.loaded / file.size).toFixed(2) * 100
              //   );
              // };
              fr.onloadend = function (e) {
                image.src = e.target.result;
                image.title = myfile.files[0].name;
              };
              fr.readAsDataURL(myfile.files[0]);
            }
          });
        }
      }
    },
    //预览视频
    getPlayer(index) {
      this.videoVisible = true;
      this.$nextTick(() => {
        console.log(window.height);
        let source = this.$refs["video" + index][0].src;
        this.$refs.showVideo.src = source;
        this.$refs.showVideo.play();
      });
    },
    //预览图片
    getImage(index) {
      this.dialogVisible = true;
      this.$nextTick(() => {
        let source = this.$refs["image" + index][0].src;
        this.dialogImageUrl = source;
      });
    },
    //打开抽屉
    toAddCollect() {
      this.collect_drawer = true;
      this.collect_drawer_title = "新增款式";
    },
    //关闭抽屉
    closeDrawer() {
      this.collect_drawer = false;
    },
    /**
     * 总体上传方法,思路是使用FormData来封装
     * 文件:都在一个数组里,所以循环设置即可,用同一个名字后台好接收
     * 表单:使用new Blob将json转成二进制文件
     */
    saveOrUpdate() {
      //我这里是因为图片或视频必填
      if (this.pro_files.length == 0) {
        this.$message.error({ message: "产品图片/视频文件不能为空" });
        return;
      }

      var addData = new FormData();
      this.pro_files.forEach((file) => {
        addData.append("files", file.file);
      });

      addData.append(
        "productCollect",
        new Blob([JSON.stringify(this.collect)], { type: "application/json" })
      );
      //点上传之后打开全屏loading 不让操作
      this.startLoading();
      //防止用户关闭当前窗口,弹窗防止,如果你想改弹出来的文字说明...(oh,我也想改呢)
      this.bindBeforeunLoad();
      this.postRequest("/api/proCollect", addData, {
        "Content-Type": "multipart/form-data",
      }).then((res) => {
        if (res.success) {
          this.$message.success({ message: res.message });
          this.collect_drawer = false;
        }
        this.closeLoading();
        this.unBindBeforeunLoad();
      });
    },
}

css样式

<style scoped>
	::v-deep .el-upload-list--picture-card .el-upload-list__item {
	  width: 100px;
	  height: 100px;
	}
	::v-deep .el-upload--picture-card {
	  width: 100px;
	  height: 100px;
	  line-height: 110px;
	}
	#file_up {
	  background-color: #fbfdff;
	  border: 1px dashed #c0ccda;
	  text-align: center;
	  border-radius: 6px;
	  box-sizing: border-box;
	  width: 130px;
	  height: 130px;
	  line-height: 130px;
	  cursor: pointer;
	  vertical-align: top;
	  display: inline-block;
	}
	#file_up:hover,
	#file_up:focus {
	  border-color: #409eff;
	  color: #409eff;
	}
	#file_up i {
	  font-size: 28px;
	  color: #8c939d;
	}
	.play-icon {
	  background: grey;
	  font-size: 20px;
	  cursor: pointer;
	  z-index: 100;
	  border-radius: 50%;
	  position: absolute;
	  left: 50%;
	  top: 50%;
	  margin-left: -16px;
	  margin-top: -16px;
	  color: #fff;
	  border: 6px solid grey;
	}
	
	.play-icon:hover {
	  opacity: 0.8;
	}
	.video_border {
	  margin-right: 10px;
	  margin-bottom: 10px;
	}
	
	.delete_icon {
	  color: #fff;
	  line-height: inherit;
	  font-size: 17px;
	  cursor: pointer;
	}
	.delete_icon:hover {
	  color: #f56c6c;
	  line-height: inherit;
	  font-size: 17px;
	  cursor: pointer;
	}
	.delete-div {
	  height: 0px;
	  position: absolute;
	  width: 100%;
	  background: #000;
	  opacity: 0.7;
	  bottom: 0;
	  text-align: center;
	  transition: height 0.2s ease 0s;
	}
	.crop:hover .delete-div {
	  height: 30%;
	}
	.rect_img {
	  position: absolute;
	  clip: rect(0px 130px 130px 0px);
	  width: 130px;
	  height: 130px;
	  margin-right: 10px;
	  cursor: pointer;
	  object-fit: cover;
	}
</style>

后台接口

    @PostMapping
    @ApiOperation("新增款式收集")
    public Result addCollect(
    	@RequestPart(value = "productCollect") ProductCollect productCollect,
    	@RequestPart(value = "files", required = true) MultipartFile[] files) 
    {
        return cProductCollectService.addCollectFlow(productCollect,files);
    }

记录学习日常,欢迎交流 ~

补充说明

其实我还做了一个进度条,就是你看到注释掉的那些,进度通过 fileReader 类的 onProgress 可以获取到,但效果并不是很理想(而且怪难看的
这个例子里我没有做任何的文件处理(压缩、分片),可以自行完善
顺便一提,如果有很大的文件,这样上传要卡挺久的,所以我要改成一个个往后端传了,告辞

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值