Преглед изворни кода

小程序提交答案(图片和音频)

Michael Wang пре 5 година
родитељ
комит
f13954311c

+ 2 - 1
package.json

@@ -30,6 +30,7 @@
     "register-service-worker": "^1.6.2",
     "vue": "^2.6.10",
     "vue-router": "^3.1.2",
+    "vuedraggable": "^2.23.0",
     "vuex": "^3.1.1"
   },
   "devDependencies": {
@@ -47,9 +48,9 @@
     "eslint": "^5.16.0",
     "eslint-plugin-prettier": "^3.1.0",
     "eslint-plugin-vue": "^5.2.2",
-    "prettier": "^1.18.2",
     "iview-loader": "^1.2.2",
     "json-server": "^0.14.0",
+    "prettier": "^1.18.2",
     "vue-cli-plugin-iview": "^1.0.6",
     "vue-template-compiler": "^2.6.10"
   }

+ 2 - 1
src/features/OnlineExam/Examing/ExamingEnd.vue

@@ -163,7 +163,8 @@ export default {
       exam: null,
       paperStruct: null,
       examQuestionList: null,
-      questionAudioFileUrl: [],
+      questionAnswerFileUrl: [],
+      pictureAnswer: {},
     });
   },
   computed: {

+ 9 - 9
src/features/OnlineExam/Examing/ExamingHome.vue

@@ -257,7 +257,8 @@ export default {
       exam: null,
       paperStruct: null,
       examQuestionList: null,
-      questionAudioFileUrl: [],
+      questionAnswerFileUrl: [],
+      pictureAnswer: {},
     });
     this.$Modal.remove();
   },
@@ -389,7 +390,8 @@ export default {
         paperStruct: paperStruct,
         examQuestionList: examQuestionList,
         allAudioPlayTimes: JSON.parse(examQuestionList[0].audioPlayTimes) || [],
-        questionAudioFileUrl: [],
+        questionAnswerFileUrl: [],
+        pictureAnswer: {},
       });
       // console.log(examQuestionList);
       // console.log(examQuestionList.find(v => v.answerType === "SINGLE_AUDIO"));
@@ -619,7 +621,7 @@ export default {
       "snapProcessingCount",
       "shouldSubmitPaper",
       "remainTime",
-      "questionAudioFileUrl",
+      "questionAnswerFileUrl",
     ]),
     previousQuestionOrder: vm => {
       if (vm.examQuestion().order > 1) {
@@ -643,7 +645,7 @@ export default {
     shouldSubmitPaper() {
       this.realSubmitPaper();
     },
-    questionAudioFileUrl(value) {
+    questionAnswerFileUrl(value) {
       // console.log(this.examQuestion.studentAnswer);
       // console.log("watch", value);
       const examRecordDataId = this.$route.params.examRecordDataId;
@@ -659,7 +661,7 @@ export default {
               "/api/ecs_oe_student/examControl/saveUploadedFileAcknowledgeStatus",
               {
                 examRecordDataId,
-                filePath: q.audioFileUrl,
+                filePath: q.fileUrl,
                 order: q.order,
                 acknowledgeStatus,
               }
@@ -668,12 +670,10 @@ export default {
               if (q.transferFileType === "AUDIO") {
                 that.updateExamQuestion({
                   order: q.order,
-                  studentAnswer: q.audioFileUrl,
+                  studentAnswer: q.fileUrl,
                 });
               } else if (q.transferFileType === "PIC") {
-                that.updatePicture({
-                  pictureAnswer: q,
-                });
+                that.updatePicture(q);
               }
               q.saved = true;
               this.$Message.info({

+ 18 - 10
src/features/OnlineExam/Examing/TextQuestionView.vue

@@ -68,7 +68,7 @@
           </div>
         </div>
       </div>
-      <div v-if="shouldFetchQrCode">
+      <div v-if="shouldFetchQrCode && isAudioAnswerType">
         <div>
           <div v-if="qrValue" style="display: flex">
             <qrcode
@@ -79,7 +79,9 @@
             <div style="margin-top: 10px;">
               <div>
                 请使用<span style="font-weight: 900; color: #1E90FF;">微信</span
-                >扫描二维码后,在微信小程序上录音/拍照,并上传文件。
+                >扫描二维码后,在微信小程序上{{
+                  isAudioAnswerType ? "录音" : "拍照"
+                }},并上传文件。
               </div>
               <div v-if="qrScanned" style="margin-top: 30px; font-size: 30px;">
                 {{ this.examQuestion.studentAnswer ? "已上传" : "已扫描" }}
@@ -106,15 +108,18 @@
         </div>
       </div>
 
-      <div v-if="canAttachPhotos">
+      <div v-if="canAttachPhotos" style="padding-top: 1px;">
         <UploadPhotos
           :defaultList="
             photoAnswers.map(v => {
-              return { url: v };
+              return v;
             })
           "
           @on-photo-added="photoAdded"
           @on-photo-removed="photoRemoved"
+          @on-photos-reseted="photosReseted"
+          :qrValue="qrValue"
+          :examQuestion="examQuestion"
           style="margin-top: 20px; width: 350px;"
         />
       </div>
@@ -263,6 +268,9 @@ export default {
       this.photoAnswers = this.photoAnswers.filter(v => v !== url);
       // console.log(this.photoAnswers);
     },
+    photosReseted(urls) {
+      this.photoAnswers = [...urls];
+    },
   },
   watch: {
     examQuestion() {
@@ -329,17 +337,17 @@ export default {
     shouldFetchQrCode() {
       const shouldFetch =
         this.examQuestion.answerType === "SINGLE_AUDIO" ||
-        this.$store.state.user.schoolDomain === "ecs-dev.qmth.com.cn";
-      this.$store.state.user.schoolDomain === "iepcc-ps.ecs.qmth.com.cn";
+        this.$store.state.user.schoolDomain === "ecs-dev.qmth.com.cn" ||
+        this.$store.state.user.schoolDomain === "iepcc-ps.ecs.qmth.com.cn";
 
       return shouldFetch;
     },
     canAttachPhotos() {
       return (
-        this.$store.state.user.schoolDomain === "csu.ecs.qmth.com.cn" ||
-        this.$store.state.user.schoolDomain === "ecs-dev.qmth.com.cn" ||
-        (this.$store.state.user.schoolDomain === "iepcc-ps.ecs.qmth.com.cn" &&
-          !this.isAudioAnswerType)
+        (this.$store.state.user.schoolDomain === "csu.ecs.qmth.com.cn" ||
+          this.$store.state.user.schoolDomain === "ecs-dev.qmth.com.cn" ||
+          this.$store.state.user.schoolDomain === "iepcc-ps.ecs.qmth.com.cn") &&
+        !this.isAudioAnswerType
       );
     },
     photoAnswers: {

+ 140 - 218
src/features/OnlineExam/Examing/UploadPhotos.vue

@@ -1,53 +1,25 @@
 <template>
   <div>
-    <div class="demo-upload-list" v-for="item in uploadList" :key="item.url">
-      <template v-if="item.status === 'finished'">
-        <img :src="item.url + '!/both/100x100'" />
-        <div class="demo-upload-list-cover">
-          <Icon
-            type="ios-eye-outline"
-            size="30"
-            @click.native="handleView(item.url)"
-          ></Icon>
-          <Icon
-            type="ios-trash-outline"
-            size="30"
-            @click.native="handleRemove(item)"
-          ></Icon>
-        </div>
-      </template>
-      <template v-else>
-        <Progress
-          v-if="item.showProgress"
-          :percent="item.percentage"
-          hide-info
-        ></Progress>
-      </template>
-    </div>
-    <Upload
-      ref="upload"
-      v-show="
-        this.uploadList.length < 6 &&
-          !(this.uploadList.filter(v => v.status === 'uploading').length > 0)
-      "
-      :accept="this.format.map(v => 'image/' + v).join()"
-      :data="headers"
-      :show-upload-list="false"
-      :default-file-list="defaultList2"
-      :on-success="handleSuccess"
-      :format="format"
-      :max-size="5 * 1024"
-      :on-format-error="handleFormatError"
-      :on-exceeded-size="handleMaxSize"
-      :before-upload="handleBeforeUpload"
-      :action="this.uploadUrl"
-      type="drag"
-      style="display: inline-block;width:100px;"
-    >
-      <div style="width: 100px;height:100px;line-height: 100px;">
-        <Icon type="ios-camera" size="40"></Icon>
+    <div class="demo-upload-list" v-for="item in uploadList" :key="item">
+      <img :src="item" />
+      <div class="demo-upload-list-cover">
+        <Icon
+          type="ios-eye-outline"
+          size="30"
+          @click.native="handleView(item)"
+        ></Icon>
+        <Icon
+          type="ios-trash-outline"
+          size="30"
+          @click.native="handleRemove(item)"
+        ></Icon>
       </div>
-    </Upload>
+    </div>
+
+    <div v-if="true" class="demo-upload-list plus" @click="prepareUpload">
+      +
+    </div>
+
     <Modal title="查看图片" v-model="visible" footer-hide>
       <img
         :src="imgUrl"
@@ -66,37 +38,87 @@
       </div>
     </Modal>
     <div>最多上传6张图片</div>
+
+    <Modal
+      title="上传"
+      v-model="uploadModalVisible"
+      mask
+      footer-hide
+      :mask-closable="false"
+      :closable="false"
+    >
+      <div>
+        <!-- TODO: 超过6张,不显示二维码 -->
+        <div v-if="qrValue" style="display: flex">
+          <qrcode
+            :value="qrValue"
+            :options="{ width: 200 }"
+            style="margin-left: -10px;"
+          ></qrcode>
+          <div style="margin-top: 10px;">
+            <div>
+              请使用<span style="font-weight: 900; color: #1E90FF;">微信</span
+              >扫描二维码后,在微信小程序上拍照,并上传文件。
+            </div>
+            <div v-if="qrScanned" style="margin-top: 30px; font-size: 30px;">
+              {{ uploaded ? "已上传" : "已扫描" }}
+              <Icon type="md-checkmark" />
+            </div>
+          </div>
+        </div>
+        <div v-else>正在获取二维码...</div>
+
+        <div class="demo-upload-list" v-for="item in totalList" :key="item">
+          <img :src="item" />
+          <div class="demo-upload-list-cover">
+            <Icon
+              type="ios-eye-outline"
+              size="30"
+              @click.native="handleView(item)"
+            ></Icon>
+            <Icon
+              type="ios-trash-outline"
+              size="30"
+              @click.native="handleRemoveTotal(item)"
+            ></Icon>
+          </div>
+        </div>
+
+        <div style="display: flex; justify-content: center">
+          <Button @click="modalCloseClicked">关闭</Button>
+        </div>
+      </div>
+    </Modal>
   </div>
 </template>
 
 <script>
-import MD5 from "js-md5";
+import VueQrcode from "@chenfengyuan/vue-qrcode";
+import { createNamespacedHelpers } from "vuex";
+const { mapState } = createNamespacedHelpers("examingHomeModule");
 
 export default {
   name: "UploadPhotos",
-  props: ["defaultList"],
+  // props: ["defaultList", "qrValue"],
+  props: {
+    examQuestion: Object,
+    defaultList: Array,
+    qrValue: String,
+  },
   data() {
     return {
-      // defaultList: [
-      //   {
-      //     name: "a42bdcc1178e62b4694c830f028db5c0",
-      //     url:
-      //       "https://o5wwk8baw.qnssl.com/a42bdcc1178e62b4694c830f028db5c0/avatar"
-      //   },
-      //   {
-      //     name: "bc7521e033abdd1e92222d733590f104",
-      //     url:
-      //       "https://o5wwk8baw.qnssl.com/bc7521e033abdd1e92222d733590f104/avatar"
-      //   }
-      // ],
       imgUrl: "",
       visible: false,
+      uploadModalVisible: false,
       rotate: 0,
-      defaultList2: [...this.defaultList],
+      // defaultList2: [...this.defaultList],
       uploadList: [],
+      totalList: [],
       uploadUrl: "",
       headers: {},
       format: ["jpg", "jpeg", "png"],
+      qrScanned: false,
+      uploaded: false,
     };
   },
   methods: {
@@ -106,175 +128,68 @@ export default {
       this.visible = true;
     },
     handleRemove(file) {
-      const fileList = this.$refs.upload.fileList;
-      this.$emit("on-photo-removed", file.url);
-      this.$refs.upload.fileList.splice(fileList.indexOf(file), 1);
-    },
-    handleSuccess(res, file) {
-      file.url = this.resultUrl;
-      // file.name = "7eb99afb9d5f317c912f08b5212fd69a";
-      this.$emit("on-photo-added", this.resultUrl);
-    },
-    handleFormatError(file) {
-      this.$Notice.warning({
-        title: "只接受jpg/jpeg/png图片文件",
-        desc: file.name,
-      });
+      // const fileList = this.$refs.upload.fileList;
+      this.$emit("on-photo-removed", file);
+      // this.$refs.upload.fileList.splice(fileList.indexOf(file), 1);
     },
-    handleMaxSize(file) {
-      const MAX_UPLOAD_SIZE = 5;
-      this.$Notice.warning({
-        title: "文件过大",
-        desc: file.name + `超过${MAX_UPLOAD_SIZE}M.`,
-      });
+    handleRemoveTotal(file) {
+      // const fileList = this.$refs.upload.fileList;
+      // this.$emit("on-photo-removed", file);
+      this.totalList.splice(this.totalList.indexOf(file), 1);
     },
-    fileFormatCheck(file, resolve, reject) {
-      function getMimetype(signature) {
-        switch (signature) {
-          case "89504E47":
-            return "image/png";
-          case "47494638":
-            return "image/gif";
-          case "25504446":
-            return "application/pdf";
-          case "FFD8FFDB":
-          case "FFD8FFE0":
-          case "FFD8FFE1":
-            return "image/jpeg";
-          case "504B0304":
-            return "application/zip";
-          case "504B34":
-            return "application/zip";
-          default:
-            return "Unknown filetype";
-        }
-      }
-
-      const filereader = new FileReader();
-      let uploads = [];
-      filereader.onloadend = evt => {
-        if (evt.target.readyState === FileReader.DONE) {
-          const uint = new Uint8Array(evt.target.result);
-          let bytes = [];
-          uint.forEach(byte => {
-            bytes.push(byte.toString(16));
-          });
-          const hex = bytes.join("").toUpperCase();
-          uploads.push({
-            filename: file.name,
-            filetype: file.type ? file.type : "Unknown/Extension missing",
-            binaryFileType: getMimetype(hex),
-            hex: hex,
-          });
-
-          if (
-            ["image/png", "image/gif", "image/jpeg"].includes(getMimetype(hex))
-          ) {
-            resolve();
-          } else {
-            console.log("binary file type check: not zip or pdf");
-            this.$Notice.warning({
-              title: "文件损坏",
-              desc: file.name + " 文件无法以 " + "png/jpg/jpeg" + " 格式读取。",
-            });
-            this.loadingStatus = false;
-            reject("作答文件损坏");
-          }
-        }
-      };
-      const blob = file.slice(0, 4);
-      filereader.readAsArrayBuffer(blob);
+    // handleSuccess(res, file) {
+    //   // file.url = this.resultUrl;
+    //   this.$emit("on-photo-added", this.resultUrl);
+    // },
+    prepareUpload() {
+      this.uploadModalVisible = true;
+      this.totalList = [...this.uploadList];
     },
-    async handleBeforeUpload(file) {
-      const result = await new Promise((resolve, reject) =>
-        this.fileFormatCheck(file, resolve, reject)
-      );
-      if (result) {
-        // this.$Notice.warning({
-        //   title: `最多上传${MAX_UPLOADS_NUM}张照片。`
-        // });
-        return Promise.resolve(false);
-      }
-      const MAX_UPLOADS_NUM = 6;
-      const check = this.uploadList.length < MAX_UPLOADS_NUM;
-      if (!check) {
-        this.$Notice.warning({
-          title: `最多上传${MAX_UPLOADS_NUM}张照片。`,
-        });
-        // return false;
-        return Promise.resolve(false);
-      }
-
-      function readAsArrayBuffer(file) {
-        return new Promise(function(resolve) {
-          var reader = new FileReader();
-          reader.readAsArrayBuffer(file);
-          reader.onload = function(e) {
-            resolve(e.target.result);
-          };
-        });
-      }
-
-      const buffer = await readAsArrayBuffer(file);
-
-      // console.log(buffer);
-      // var view1 = new Uint8Array(buffer);
-      // console.log(buffer[0], buffer[1], buffer[429721]);
-      const fileMd5 = MD5(buffer);
-      // console.log(fileMd5);
-
-      const examRecordDataId = this.$route.params.examRecordDataId - 0;
-      const order = this.$route.params.order - 0;
-      const fileSuffix = file.name.split(".").pop();
-      const params = new URLSearchParams();
-      params.append("examRecordDataId", examRecordDataId);
-      params.append("order", order);
-      params.append("fileSuffix", fileSuffix);
-      params.append("fileMd5", fileMd5);
-      const res = await this.$http.post(
-        "/api/ecs_oe_student/examControl/upyunSignature",
-        params,
-        {
-          examRecordDataId,
-          order,
-          fileSuffix,
-          fileMd5,
-        },
-        { headers: { "content-type": "application/x-www-form-urlencoded" } }
-      );
+    modalCloseClicked() {
+      this.uploadModalVisible = false;
+      this.$emit("on-photos-reseted", this.totalList);
 
-      // console.log(res);
-      this.headers = {
-        policy: res.data.policy,
-        authorization: res.data.signature,
-      };
-      this.uploadUrl = res.data.uploadUrl;
-      this.resultUrl = res.data.upyunFileDomain + res.data.filePath;
-      return check;
+      // TODO: 在二维码被扫描,文件没得到之前,提示是否关闭。
+      // TODO: 检查是否超过6张
     },
   },
   mounted() {
-    this.uploadList = this.$refs.upload.fileList;
+    this.uploadList = this.defaultList;
+    // this.uploadList.push(
+    //   ...[
+    //     "https://o5wwk8baw.qnssl.com/a42bdcc1178e62b4694c830f028db5c0/avatar",
+    //     "https://o5wwk8baw.qnssl.com/bc7521e033abdd1e92222d733590f104/avatar",
+    //   ]
+    // );
+  },
+  computed: {
+    ...mapState(["questionQrCodeScanned", "pictureAnswer"]),
   },
   watch: {
-    uploadList: {
+    defaultList: {
       handler: function update() {
-        // 禁止同时上传附件
-        // for (const t of this.uploadList) {
-        //   console.log(t.status);
-        // }
-        if (this.uploadList.filter(v => v.status === "uploading").length > 0) {
-          this.$nextTick(() => {
-            this.$Spin.show({});
-          });
-        } else {
-          this.$nextTick(() => {
-            this.$Spin.hide();
-          });
-        }
+        this.uploadList = this.defaultList;
       },
       deep: true,
     },
+    questionQrCodeScanned(value) {
+      // console.log(this.examQuestion.studentAnswer);
+      // console.log("watch", value);
+      if (value.order === this.examQuestion.order) {
+        this.qrScanned = true;
+      }
+    },
+    pictureAnswer(value) {
+      // console.log(this.examQuestion.studentAnswer);
+      console.log("watch", value);
+      this.uploaded = true;
+      if (value.order === this.examQuestion.order) {
+        this.totalList.push(...[...new Set(value.fileUrl.split(","))]);
+      }
+    },
+  },
+  components: {
+    qrcode: VueQrcode,
   },
 };
 </script>
@@ -297,6 +212,13 @@ export default {
   width: 100%;
   height: 100%;
 }
+.plus {
+  font-size: 48px;
+}
+.plus:hover {
+  cursor: pointer;
+  color: blueviolet;
+}
 .demo-upload-list-cover {
   display: none;
   position: absolute;

+ 1 - 0
src/features/OnlineExam/Examing/ws.js

@@ -170,6 +170,7 @@ function processWSMessage(event) {
       store.commit("examingHomeModule/setQuestionFileAnswerUrl", {
         order: res.data.order,
         fileUrl: res.data.fileUrl,
+        transferFileType: res.data.transferFileType,
       });
       break;
     case "SYSTEM_ERROR":

+ 9 - 7
src/store.js

@@ -33,8 +33,8 @@ const examingHomeModule = {
     allAudioPlayTimes: [],
     questionQrCode: null,
     questionQrCodeScanned: null,
-    questionAudioFileUrl: [],
-    pictureAnswer: null,
+    questionAnswerFileUrl: [],
+    pictureAnswer: {},
   },
   mutations: {
     updateRemainTime(state, remainTime) {
@@ -156,22 +156,24 @@ const examingHomeModule = {
       state.questionQrCodeScanned = payload;
     },
     setQuestionFileAnswerUrl(state, payload) {
-      state.questionAudioFileUrl = state.questionAudioFileUrl.filter(
+      // 先清理之前保存过的记录
+      state.questionAnswerFileUrl = state.questionAnswerFileUrl.filter(
         v => !v.saved
       );
-      let ary = state.questionAudioFileUrl;
+      let ary = state.questionAnswerFileUrl;
       let found = false;
       for (const i of ary) {
         if (i.order === payload.order) {
-          i.audioFileUrl = payload.audioFileUrl;
+          // 同一道题可能更新
+          i.fileUrl = payload.fileUrl;
           found = true;
           break;
         }
       }
       if (found) {
-        state.questionAudioFileUrl = [...ary];
+        state.questionAnswerFileUrl = [...ary];
       } else {
-        state.questionAudioFileUrl.push(payload);
+        state.questionAnswerFileUrl.push(payload);
       }
     },
     updatePicture(state, payload) {

+ 12 - 0
yarn.lock

@@ -9599,6 +9599,11 @@ sort-keys@^2.0.0:
   dependencies:
     is-plain-obj "^1.0.0"
 
+sortablejs@^1.9.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.9.0.tgz#2d1e74ae6bac2cb4ad0622908f340848969eb88d"
+  integrity sha512-Ot6bYJ6PoqPmpsqQYXjn1+RKrY2NWQvQt/o4jfd/UYwVWndyO5EPO8YHbnm5HIykf8ENsm4JUrdAvolPT86yYA==
+
 source-list-map@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@@ -10715,6 +10720,13 @@ vue@^2.6.10:
   resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
   integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==
 
+vuedraggable@^2.23.0:
+  version "2.23.0"
+  resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.23.0.tgz#1f4a5a601675a5dbf0d96ee61aebfffa43445262"
+  integrity sha512-RgdH16k43WNoxyRcv/OarB/DZh9SY5TYthk9TS4YiHXpelD1DytEG0phLAXiXx5EhsmdH8ltSWxklGa4g1WTCw==
+  dependencies:
+    sortablejs "^1.9.0"
+
 vuex@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.1.1.tgz#0c264bfe30cdbccf96ab9db3177d211828a5910e"