Explorar el Código

离线上传:上传图片新接口

Michael Wang hace 4 años
padre
commit
ea9228bb81

+ 1 - 1
src/features/Login/HiddenRequest.js

@@ -1,7 +1,7 @@
 import { TK_SERVER_HTML_URL } from "@/constants/constants.js";
 
 export default function (uuid) {
-  const url = TK_SERVER_HTML_URL + "/resource.js?u=" + uuid;
+  const url = process.env.VUE_APP_CORE_HOST_URL + "/resource.js?u=" + uuid;
   // TK_SERVER_HTML_URL.replace("http", "http") + ":8000/resource.js?u=" + uuid;
 
   // if(process.env.NODE_ENV !== 'production')

+ 1 - 1
src/features/OfflineExam/OfflineExamHome.vue

@@ -74,7 +74,7 @@ export default {
         examStudentId: c.examStudentId,
         startTime: c.startTime,
         endTime: c.endTime,
-        offlineFileUrl: c.offlineFileUrl,
+        offlineFiles: c.offlineFiles,
         paperId: c.paperId,
       }));
     },

+ 38 - 31
src/features/OfflineExam/OfflineExamList.vue

@@ -10,7 +10,7 @@
           <td style="max-width: 200px;">操作</td>
         </tr>
 
-        <tr v-for="course in courses" :key="course.examId">
+        <tr v-for="(course, courseIndex) in courses" :key="courseIndex">
           <td>{{ course.courseName }}</td>
           <td>{{ course.specialtyName }}</td>
           <td>
@@ -19,15 +19,18 @@
             {{ course.endTime }}
           </td>
           <td>
-            <div v-if="course.offlineFileUrl">
-              <a
-                :href="course.offlineFileUrl"
-                download
-                ondragstart="return false;"
-                @click="() => downloadOfflineFile(course.offlineFileUrl)"
-              >
-                <i-icon type="ios-cloud-download"></i-icon>下载作答
-              </a>
+            <div v-if="course.offlineFiles">
+              <div v-for="(file, index) in course.offlineFiles" :key="index">
+                <a
+                  :href="file.offlineFileUrl"
+                  download
+                  :title="file.originalFileName"
+                  ondragstart="return false;"
+                  @click="() => downloadOfflineFile(file.offlineFileUrl)"
+                >
+                  <i-icon type="ios-cloud-download"></i-icon>下载作答
+                </a>
+              </div>
             </div>
             <div v-else>未上传</div>
           </td>
@@ -56,23 +59,12 @@
                 >
                   下载中
                 </i-button>
-                <!-- <i-button class="qm-primary-button">
-                  <a
-                    class="qm-primary-button"
-                    href="https://ecs-static.qmth.com.cn/offline-exam/答题卡.zip"
-                    download
-                  >下载答题卡</a>
-                </i-button> -->
-                <ecs-offline-exam-upload-cug
-                  v-if="isCug"
-                  :course="course"
-                  @reloadList="$emit('reloadList')"
-                ></ecs-offline-exam-upload-cug>
-                <ecs-offline-exam-upload
-                  v-else
-                  :course="course"
-                  @reloadList="$emit('reloadList')"
-                ></ecs-offline-exam-upload>
+                <i-button
+                  class="qm-primary-button"
+                  @click="uploadHandler(course)"
+                >
+                  上传答案
+                </i-button>
               </div>
 
               <div v-else style="display: grid; grid-gap: 10px;">
@@ -104,6 +96,12 @@
         </tr>
       </tbody>
     </table>
+    <ecs-offline-exam-modal
+      v-if="selectedCourse"
+      ref="uploadModal"
+      :course="selectedCourse"
+      @reloadList="$emit('reloadList')"
+    ></ecs-offline-exam-modal>
   </div>
 </template>
 
@@ -113,14 +111,12 @@ import {
   TK_SERVER_HTML_URL,
   TK_SERVER_API_URL,
 } from "@/constants/constants.js";
-import OfflineExamUpload from "./OfflineExamUpload.vue";
-import OfflineExamUploadCug from "./OfflineExamUploadCug.vue";
+import OfflineExamModal from "./OfflineExamModal.vue";
 
 export default {
   name: "EcsOfflineList",
   components: {
-    "ecs-offline-exam-upload": OfflineExamUpload,
-    "ecs-offline-exam-upload-cug": OfflineExamUploadCug,
+    "ecs-offline-exam-modal": OfflineExamModal,
   },
   props: {
     courses: {
@@ -133,6 +129,7 @@ export default {
   data() {
     return {
       disableDownloadPaperBtn: false,
+      selectedCourse: null,
     };
   },
   computed: {
@@ -186,6 +183,16 @@ export default {
         "&$token=" +
         this.user.token;
     },
+    uploadHandler(course) {
+      this.selectedCourse = course;
+      // setTimeout(() => {
+      //   console.log(this.$refs);
+      // }, 1000);
+      this.$nextTick(() => {
+        // console.log(this.$refs.uploadModal.$refs.uploadModal);
+        this.$refs.uploadModal.$refs.uploadModal.visible = true;
+      });
+    },
   },
 };
 </script>

+ 180 - 0
src/features/OfflineExam/OfflineExamModal.vue

@@ -0,0 +1,180 @@
+<template>
+  <Modal
+    ref="uploadModal"
+    v-model="uploadModalVisible"
+    title="上传文件"
+    mask
+    footer-hide
+    :mask-closable="false"
+    :closable="true"
+    width="660"
+    @close="modalClose"
+  >
+    <div style="font-size: 16px; line-height: 20px;">
+      <span style="padding-right: 10px;">请选择上传文件类型:</span>
+      <span
+        v-if="serverFormat.includes('ZIP')"
+        @click="selectedFileType = 'ZIP'"
+      >
+        <input
+          type="radio"
+          name="filetype"
+          value="ZIP"
+          :checked="selectedFileType == 'ZIP'"
+          style="display: inline-block;"
+        />
+        <span class="right-margin">ZIP</span>
+      </span>
+      <span
+        v-if="serverFormat.includes('PDF')"
+        @click="selectedFileType = 'PDF'"
+      >
+        <input
+          type="radio"
+          name="filetype"
+          value="PDF"
+          :checked="selectedFileType == 'PDF'"
+          style="display: inline-block;"
+        />
+        <span class="right-margin">PDF</span>
+      </span>
+      <span
+        v-if="serverFormat.includes('IMAGE')"
+        @click="selectedFileType = 'IMAGE'"
+      >
+        <input
+          type="radio"
+          name="filetype"
+          value="IMAGE"
+          :checked="selectedFileType == 'IMAGE'"
+          style="display: inline-block;"
+        />
+        <span class="right-margin">图片</span>
+      </span>
+    </div>
+    <div
+      style="font-size: 16px; line-height: 20px; margin: 20px 0; display: flex;"
+    >
+      <div style="padding-right: 10px;">请选择文件:</div>
+      <OfflineExamUpload
+        :course="course"
+        :selected-file-type="selectedFileType"
+        :upload-file-format="uploadFileFormat"
+        :upload-file-accept="uploadFileAccept"
+        @reloadList="$emit('reloadList')"
+        @closeModal="closeModal"
+      />
+    </div>
+  </Modal>
+</template>
+
+<script>
+import OfflineExamUpload from "./OfflineExamUpload.vue";
+
+export default {
+  name: "OfflineExamModal",
+  components: {
+    OfflineExamUpload: OfflineExamUpload,
+  },
+  props: {
+    course: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      uploadModalVisible: true,
+      serverFormat: "",
+      selectedFileType: "",
+      // uploadFileFormat: [],
+      // uploadFileAccept: "",
+    };
+  },
+  computed: {
+    uploadFileFormat() {
+      let res;
+      switch (this.selectedFileType) {
+        case "":
+          res = [];
+          break;
+        case "ZIP":
+          res = ["zip"];
+          break;
+        case "PDF":
+          res = ["pdf"];
+          break;
+        case "IMAGE":
+          res = ["jpeg", "jpg", "png"];
+          break;
+      }
+      return res;
+    },
+    uploadFileAccept() {
+      let res;
+      switch (this.selectedFileType) {
+        case "":
+          res = "";
+          break;
+        case "ZIP":
+          res = ".zip,zip";
+          break;
+        case "PDF":
+          res = ".pdf";
+          break;
+        case "IMAGE":
+          res = ".jpeg,.jpg,.png";
+          break;
+      }
+      return res;
+    },
+  },
+  watch: {
+    course() {
+      this.updateServerFormat();
+    },
+  },
+  async created() {
+    this.updateServerFormat();
+    // this.uploadFileFormat =
+    //   (res.data.OFFLINE_UPLOAD_FILE_TYPE &&
+    //     JSON.parse(res.data.OFFLINE_UPLOAD_FILE_TYPE)) ||
+    //   [];
+    // this.uploadFileFormat = this.uploadFileFormat.map((v) => v.toLowerCase());
+    // this.uploadFileAccept = this.uploadFileFormat
+    //   .map((v) => "application/" + v)
+    //   .join();
+    // if (this.uploadFileAccept.includes("zip")) {
+    //   this.uploadFileAccept = ".zip," + this.uploadFileAccept;
+    // }
+  },
+  methods: {
+    async updateServerFormat() {
+      const res = await this.$http.get(
+        "/api/ecs_exam_work/exam/getExamPropertyFromCacheByStudentSession/" +
+          this.course.examId +
+          `/OFFLINE_UPLOAD_FILE_TYPE`
+      );
+      this.serverFormat =
+        (res.data.OFFLINE_UPLOAD_FILE_TYPE &&
+          JSON.parse(res.data.OFFLINE_UPLOAD_FILE_TYPE)) ||
+        [];
+    },
+    modalClose() {
+      this.selectedFileType = "";
+    },
+    closeModal() {
+      this.selectedFileType = "";
+      this.$refs.uploadModal.visible = false;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.right-margin {
+  padding: 0 15px 0 5px;
+}
+</style>

+ 368 - 59
src/features/OfflineExam/OfflineExamUpload.vue

@@ -1,14 +1,37 @@
 <template>
-  <div>
+  <div style="width: 350px;">
+    <div v-if="selectedFileType === 'IMAGE'" class="total-images">
+      <div
+        v-for="(item, index) in uploadFileList"
+        :key="index"
+        class="demo-upload-list"
+      >
+        <img
+          :ref="'image' + index"
+          class="image-small"
+          :data-src="blobToSrc(item, index)"
+        />
+        <div class="demo-upload-list-cover">
+          <Icon
+            type="ios-eye-outline"
+            size="30"
+            @click.native="handleView('.total-images', index)"
+          ></Icon>
+          <Icon
+            type="ios-trash-outline"
+            size="30"
+            style="position: absolute; top: 0px;"
+            @click.native="handleRemoveTotal(index)"
+          ></Icon>
+        </div>
+      </div>
+    </div>
     <Upload
       ref="uploadComp"
+      action
       :headers="headers"
       :data="{ fileType: fileType }"
       :before-upload="handleBeforeUpload"
-      :action="
-        '/api/ecs_oe_admin/offlineExam/submitPaper?examRecordDataId=' +
-        course.examRecordDataId
-      "
       :max-size="1024 * 30"
       :format="uploadFileFormat"
       :accept="uploadFileAccept"
@@ -16,26 +39,58 @@
       :on-exceeded-size="handleMaxSize"
       :on-success="handleSuccess"
       :on-error="handleError"
-      :show-upload-list="false"
+      :multiple="selectedFileType === 'IMAGE'"
     >
+      <div v-if="selectedFileType !== 'IMAGE'">
+        <i-button
+          icon="ios-cloud-upload-outline"
+          class="qm-primary-button"
+          style=""
+          @click="clickUpload"
+        >
+          选择文件
+        </i-button>
+        <span v-if="uploadFileList.length > 0">{{
+          uploadFileList[0].name
+        }}</span>
+      </div>
+      <div v-else>
+        <div
+          v-if="uploadFileList.length < 6"
+          class="demo-upload-list plus"
+          @click="clickUpload"
+        >
+          +
+        </div>
+      </div>
+    </Upload>
+
+    <div>
       <i-button
         icon="ios-cloud-upload-outline"
         class="qm-primary-button"
-        style="width: 100%;"
+        style="width: 40%; margin-right: 20px;"
+        :disabled="uploadFileList.length === 0"
+        @click="uploadFiles"
       >
-        上传作答
+        确认上传
       </i-button>
-    </Upload>
-    <div v-if="file !== null && loadingStatus">
-      待上传文件: {{ file.name }}
-      <i-button :loading="loadingStatus">
-        {{ loadingStatus ? "上传中..." : "上传" }}
+      <i-button
+        class="qm-primary-button"
+        style="width: 40%;"
+        @click="$emit('closeModal')"
+      >
+        取消上传
       </i-button>
     </div>
   </div>
 </template>
 
 <script>
+import MD5 from "js-md5";
+import "viewerjs/dist/viewer.css";
+import Viewer from "viewerjs";
+
 export default {
   name: "EcsOfflineUpload",
   props: {
@@ -45,6 +100,18 @@ export default {
         return {};
       },
     },
+    selectedFileType: {
+      type: String,
+      default: "",
+    },
+    uploadFileFormat: {
+      type: Array,
+      default: () => [],
+    },
+    uploadFileAccept: {
+      type: String,
+      default: "",
+    },
   },
   data() {
     return {
@@ -55,30 +122,48 @@ export default {
       file: null,
       fileType: null,
       loadingStatus: false,
-      uploadFileFormat: [],
-      uploadFileAccept: "",
+      uploadFileList: [],
+      // uploadFileFormat: [],
+      // uploadFileAccept: "",
     };
   },
+  watch: {
+    selectedFileType() {
+      this.uploadFileList = [];
+    },
+  },
   async created() {
-    const res = await this.$http.get(
-      "/api/ecs_exam_work/exam/getExamPropertyFromCacheByStudentSession/" +
-        this.course.examId +
-        `/OFFLINE_UPLOAD_FILE_TYPE`
-    );
-
-    this.uploadFileFormat =
-      (res.data.OFFLINE_UPLOAD_FILE_TYPE &&
-        JSON.parse(res.data.OFFLINE_UPLOAD_FILE_TYPE)) ||
-      [];
-    this.uploadFileFormat = this.uploadFileFormat.map((v) => v.toLowerCase());
-    this.uploadFileAccept = this.uploadFileFormat
-      .map((v) => "application/" + v)
-      .join();
-    if (this.uploadFileAccept.includes("zip")) {
-      this.uploadFileAccept = ".zip," + this.uploadFileAccept;
-    }
+    // const res = await this.$http.get(
+    //   "/api/ecs_exam_work/exam/getExamPropertyFromCacheByStudentSession/" +
+    //     this.course.examId +
+    //     `/OFFLINE_UPLOAD_FILE_TYPE`
+    // );
+    // this.uploadFileFormat =
+    //   (res.data.OFFLINE_UPLOAD_FILE_TYPE &&
+    //     JSON.parse(res.data.OFFLINE_UPLOAD_FILE_TYPE)) ||
+    //   [];
+    // this.uploadFileFormat = this.uploadFileFormat.map((v) => v.toLowerCase());
+    // this.uploadFileAccept = this.uploadFileFormat
+    //   .map((v) => "application/" + v)
+    //   .join();
+    // if (this.uploadFileAccept.includes("zip")) {
+    //   this.uploadFileAccept = ".zip," + this.uploadFileAccept;
+    // }
   },
   methods: {
+    clickUpload(e) {
+      // console.log(e);
+      e.preventDefault();
+      if (!this.selectedFileType) {
+        this.$Notice.warning({
+          title: "请先选择上传文件类型",
+          // desc: file.name + " 文件是pdf文档,但文件名后缀不是.pdf!"
+        });
+        e.stopPropagation();
+        return false;
+      }
+      if (this.selectedFileType !== "IMAGE") this.uploadFileList = [];
+    },
     fileFormatCheck(file, resolve, reject) {
       function getMimetype(signature) {
         switch (signature) {
@@ -128,7 +213,7 @@ export default {
               this.file = null;
               reject("文件内容与文件的后缀不一致,请确认文件是pdf文档!");
             } else {
-              resolve();
+              resolve(true);
             }
           } else if (["application/zip"].includes(getMimetype(hex))) {
             if (!file.name.endsWith(".zip")) {
@@ -142,7 +227,35 @@ export default {
               this.file = null;
               reject("文件内容与文件的后缀不一致,请确认文件是zip压缩包!");
             } else {
-              resolve();
+              resolve(true);
+            }
+          } else if (["image/jpeg"].includes(getMimetype(hex))) {
+            if (file.name.endsWith(".jpeg") || file.name.endsWith(".jpg")) {
+              resolve(true);
+            } else {
+              this.loadingStatus = false;
+              // this.$refs.uploadComp.fileList.splice(0);
+              // this.$refs.uploadComp.fileList = [];
+              this.$Notice.warning({
+                title: "文件内容与文件的后缀不一致",
+                // desc: file.name + " 文件是zip压缩包,但文件名后缀不是.zip!"
+              });
+              this.file = null;
+              reject("文件内容与文件的后缀不一致,请确认文件是jpeg文件!");
+            }
+          } else if (["image/png"].includes(getMimetype(hex))) {
+            if (!file.name.endsWith(".png")) {
+              this.loadingStatus = false;
+              // this.$refs.uploadComp.fileList.splice(0);
+              // this.$refs.uploadComp.fileList = [];
+              this.$Notice.warning({
+                title: "文件内容与文件的后缀不一致",
+                // desc: file.name + " 文件是zip压缩包,但文件名后缀不是.zip!"
+              });
+              this.file = null;
+              reject("文件内容与文件的后缀不一致,请确认文件是png文件!");
+            } else {
+              resolve(true);
             }
           } else {
             console.log("binary file type check: not zip or pdf");
@@ -170,15 +283,17 @@ export default {
       filereader.readAsArrayBuffer(blob);
     },
     handleSuccess() {
-      window._hmt.push(["_trackEvent", "离线考试页面", "上传作答", "上传成功"]);
-      this.file = null;
-      this.loadingStatus = false;
-      this.$Message.success({
-        content: "上传成功",
-        duration: 5,
-        closable: true,
-      });
-      this.$emit("reloadList");
+      //   window._hmt.push(["_trackEvent", "离线考试页面", "上传作答", "上传成功"]);
+      //   this.file = null;
+      //   this.loadingStatus = false;
+      //   this.$Message.success({
+      //     content: "上传成功",
+      //     duration: 5,
+      //     closable: true,
+      //   });
+      //   this.uploadFileList = [];
+      //   this.$emit("closeModal");
+      //   this.$emit("reloadList");
     },
     handleError(error, file) {
       window._hmt.push(["_trackEvent", "离线考试页面", "上传作答", "上传失败"]);
@@ -211,7 +326,7 @@ export default {
         desc: file.name + " 太大,作答文件不能超过30M.",
       });
     },
-    handleBeforeUpload(file) {
+    async handleBeforeUpload(file) {
       const suffix = file.name.split(".").pop();
       if (suffix.toLowerCase() !== suffix) {
         this.$Notice.error({
@@ -221,28 +336,172 @@ export default {
         return Promise.reject("file suffix should be lower case");
       }
 
-      return new Promise((resolve, reject) => {
-        if (this.course.offlineFileUrl) {
+      // this.uploadFileList.push(file);
+      // const oldLength = this.uploadFileList.length;
+
+      // await new Promise((resolve) => setTimeout(() => resolve(), 1000));
+
+      // const newLength = this.uploadFileList.length;
+
+      // if (oldLength !== newLength) {
+      //   console.log("还有新的文件要上传,取消本次上传");
+      //   throw "取消";
+      // }
+      // console.log(this.uploadFileList);
+
+      if (this.uploadFileList.length > 1) {
+        if (
+          this.uploadFileList.map((v) => v.type).includes("application/pdf")
+        ) {
+          console.log("PDF文件只允许单独上传");
+          throw "取消";
+        }
+        if (
+          this.uploadFileList.map((v) => v.type).includes("application/zip")
+        ) {
+          console.log("ZIP文件只允许单独上传");
+          throw "取消";
+        }
+      }
+
+      console.log("上传");
+      // return;
+
+      // if (file.type.includes("/pdf")) {
+      //   this.fileType = "pdf";
+      // } else if (file.type.includes("/zip")) {
+      //   this.fileType = "zip";
+      // }
+      this.file = file;
+      this.loadingStatus = true;
+      const format = await new Promise((resolve, reject) =>
+        this.fileFormatCheck(file, resolve, reject)
+      );
+
+      if (format !== true) {
+        console.log(format);
+        return;
+      }
+
+      this.uploadFileList.push(file);
+
+      throw "stop uploading by <Upload>";
+    },
+    async uploadFiles() {
+      if (this.uploadFileList.length === 0) {
+        return;
+      }
+      if (this.course.offlineFiles) {
+        const res = await new Promise((resolve, reject) => {
           this.$Modal.confirm({
             title: "已有作答附件,是否覆盖?",
             onCancel: () => reject(-1),
             onOk: () => resolve(),
           });
-        } else {
-          resolve();
-        }
-      }).then(() => {
-        if (file.type.includes("/pdf")) {
-          this.fileType = "pdf";
-        } else if (file.type.includes("/zip")) {
-          this.fileType = "zip";
+        });
+        if (res === -1) {
+          return false;
         }
-        this.file = file;
-        this.loadingStatus = true;
-        return new Promise((resolve, reject) =>
-          this.fileFormatCheck(file, resolve, reject)
-        );
+      }
+
+      let params = new FormData();
+      params.append("examRecordDataId", this.course.examRecordDataId);
+      params.append("fileType", this.selectedFileType);
+
+      async function blobToArray(blob) {
+        return new Promise((resolve) => {
+          var reader = new FileReader();
+          reader.addEventListener("loadend", function () {
+            // reader.result contains the contents of blob as a typed array
+            resolve(reader.result);
+          });
+          reader.readAsArrayBuffer(blob);
+        });
+      }
+
+      for (const file of this.uploadFileList) {
+        const ab = await blobToArray(file);
+        params.append("fileArray", file);
+        params.append("fileMd5Array", MD5(ab));
+      }
+      // for (let f of this.fileList) {
+      //   params.append("fileArray", f.raw);
+      // }
+
+      // //先对文件md5进行排序(按索引正序排列)
+      // this.summaryList.sort((a, b) => a.index - b.index);
+      // let summaries = [];
+      // for (let s of this.summaryList) {
+      //   summaries.push(s.summary);
+      // }
+      // params.append("fileMd5Array", summaries);
+
+      this.$Message.info({
+        content: "上传中...",
+        duration: 5,
+        closable: true,
       });
+      const res = await this.$http.post(
+        "/api/ecs_oe_admin/offlineExam/batchSubmitPaper",
+        params,
+        { headers: { "Content-Type": "multipart/form-data" } }
+      );
+      // console.log(res);
+      this.$Message.destroy();
+      this.$Message.success({
+        content: "上传成功",
+        duration: 5,
+        closable: true,
+      });
+      this.uploadFileList = [];
+      this.$emit("closeModal");
+      this.$emit("reloadList");
+    },
+
+    async blobToSrc(item, index) {
+      var fr = new FileReader();
+      fr.onload = (eve) => {
+        const e = this.$refs["image" + index];
+        e[0].src = eve.target.result;
+      };
+
+      fr.readAsDataURL(item);
+    },
+
+    handleView(imagesClass, index) {
+      const viewer = new Viewer(document.querySelector(imagesClass), {
+        container: "#app",
+        zIndex: 99999,
+        title: false,
+        toolbar: {
+          zoomIn: 1,
+          zoomOut: 1,
+          oneToOne: 1,
+          reset: 1,
+          prev: 1,
+          play: {
+            show: 0,
+            size: "large",
+          },
+          next: 1,
+          rotateLeft: 1,
+          rotateRight: 1,
+          flipHorizontal: 1,
+          flipVertical: 1,
+        },
+        ready() {
+          // viewer.zoomTo(1);
+          viewer.view(index);
+          // console.log("once");
+        },
+        hidden() {
+          viewer.destroy();
+        },
+      });
+      viewer.show();
+    },
+    handleRemoveTotal(index) {
+      this.uploadFileList.splice(index, 1);
     },
   },
 };
@@ -252,4 +511,54 @@ export default {
 .list .ivu-upload-select {
   width: 100%;
 }
+/* .image-small {
+  width: 100px;
+  height: 100px;
+} */
+
+.demo-upload-list {
+  display: inline-block;
+  width: 100px;
+  height: 100px;
+  text-align: center;
+  line-height: 100px;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  overflow: hidden;
+  background: #fff;
+  position: relative;
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+  margin-right: 4px;
+
+  /* cursor: move; */
+}
+.demo-upload-list img {
+  width: 100%;
+  height: 100%;
+}
+.plus {
+  font-size: 48px;
+}
+.plus:hover {
+  cursor: pointer;
+  color: blueviolet;
+}
+.demo-upload-list-cover {
+  display: none;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: rgba(0, 0, 0, 0.6);
+}
+.demo-upload-list:hover .demo-upload-list-cover {
+  display: block;
+}
+.demo-upload-list-cover i {
+  color: #fff;
+  font-size: 20px;
+  cursor: pointer;
+  margin: 0 2px;
+}
 </style>