Tang 2 years ago
parent
commit
b2e178ddc2

+ 3 - 2
src/common/uploadFile.js

@@ -3,14 +3,15 @@ import { readImg, compressImg } from "./compress"
 export default {
     // 上传图片标识 0头像 1身份证 2题库 3指南指引图片 4广告图片 5身份证或学信网图片 6文件excel,word,zip等
     //file: 类似this.$refs.file.files[0]
-    upload: function (file, int) {
+    upload: function (file, int, data) {
         return new Promise(async (resolve, reject) => {
             if (typeof file != 'object') {
                 resolve(file)
                 return;
             }
             var datas = {
-                imageStatus: int || 0
+                imageStatus: int || 0,
+                ...data
             }
             //图片压缩
             if (file.type.indexOf("image") !== -1) {

+ 352 - 0
src/components/takePicture/index.vue

@@ -0,0 +1,352 @@
+<template>
+  <div id="">
+    <el-dialog
+      width="800px"
+      class="take-photo"
+      :visible.sync="takePhotoModal"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :show-close="false"
+    >
+      <div class="take-photo__content">
+        <!-- <div class="take-photo__close" @click="takePhotoModal = false">X</div> -->
+        <div class="take-photo__header">人脸验证</div>
+        <div class="take-photo__body clearfix">
+          <div class="left-box">
+            <div class="title">重要提示:</div>
+            <div class="content">
+              <p>1、请保证摄像头正对自己,避免头像偏左或者偏右。</p>
+              <p>
+                2、请保证拍照环境光线充足(照片太暗或曝光会降低验证通过率)。
+              </p>
+              <p>
+                3、请保证整个头像在人脸识别区域内,脸部无遮挡装饰物(佩戴眼镜会降低通过率)。
+              </p>
+              <p>
+                4、如果下面视频中出现黑屏,摄像头可能被其他进程占用,请关闭其他调用摄像头的程序,重新刷新当前页面重新拍照识别。
+              </p>
+            </div>
+          </div>
+          <div class="right-box">
+            <img v-show="!isTaking" :src="faceUrl" alt="" />
+            <video v-show="isTaking" id="video"></video>
+            <div v-show="isTaking" class="mask"></div>
+          </div>
+        </div>
+        <div class="take-photo__footer">
+          <el-button
+            type="primary"
+            v-if="isTaking"
+            class="take"
+            @click="onPhoto"
+            >拍照</el-button
+          >
+          <el-button
+            type="primary"
+            v-if="!isTaking"
+            class="take"
+            :loading="loading"
+            @click="reTake"
+            >重拍</el-button
+          >
+          <el-button
+            type="primary"
+            v-if="!isTaking"
+            :loading="loading"
+            class="take"
+            @click="takeOk"
+            >确认</el-button
+          >
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      takePhotoModal: false,
+      isTaking: true,
+      loading: false,
+      faceUrl: ""
+    };
+  },
+  methods: {
+    //拍照
+    openPhoto() {
+      var polyvPlayerContext = this.player;
+      if (polyvPlayerContext) {
+        this.isFullScreen();
+      }
+      this.$nextTick(() => {
+        if (
+          (window.navigator.mediaDevices &&
+            window.navigator.mediaDevices.getUserMedia) ||
+          window.navigator.getUserMedia ||
+          window.navigator.webkitGetUserMedia ||
+          window.navigator.mozGetUserMedia
+        ) {
+          // 调用用户媒体设备, 访问摄像头
+          this.getUserMedia(
+            {
+              video: {
+                width: 400,
+                height: 400
+              }
+            },
+            this.photographSuccess,
+            this.photographError
+          );
+        } else {
+          this.photographError();
+        }
+      });
+    },
+    //成功读取摄像头
+    photographSuccess(stream) {
+      // 兼容webkit核心浏览器
+      if (this.isVirtualCamera(stream)) {
+        return;
+      }
+      this.faceUrl = "";
+      this.loading = false;
+      this.isTaking = true;
+      this.takePhotoModal = true;
+
+      this.$nextTick(() => {
+        const video = document.getElementById("video");
+        // 将视频流设置为video元素的源
+        video.srcObject = stream;
+        video.play().catch(() => {});
+      });
+    },
+    //未读取到摄像头 兼容webkit核心浏览器
+    isVirtualCamera(stream) {
+      const list = [
+        "VCam",
+        "ManyCam",
+        "OBS",
+        "ClassInCam",
+        "Ev",
+        "Video2Webcam"
+      ];
+      let isT = list.some(e => {
+        return stream.getTracks()[0].label.indexOf(e) != -1;
+      });
+      if (isT) {
+        this.$confirm("检测到你使用虚拟摄像头,无法继续学习。", "提示", {
+          confirmButtonText: "返回",
+          showConfirmButton: true,
+          closeOnClickModal: false,
+          showCancelButton: false,
+          closeOnPressEscape: false,
+          distinguishCancelAndClose: false,
+          showClose: false
+        }).then(() => {
+          this.$router.go(-1);
+        });
+      }
+      return isT;
+    },
+    //未读取到摄像头
+    photographError(err) {
+      this.$confirm(
+        "课程学习需要开启摄像头进行拍照,经检测您的设备无摄像头可使用,请检测环境是否支持。",
+        "提示",
+        {
+          confirmButtonText: "返回",
+          showConfirmButton: true,
+          closeOnClickModal: false,
+          showCancelButton: false,
+          closeOnPressEscape: false,
+          distinguishCancelAndClose: false,
+          showClose: false
+        }
+      ).then(() => {
+        this.$router.back(-1);
+      });
+    },
+    // 调用用户媒体设备, 访问摄像头
+    getUserMedia(constraints, success, error) {
+      if (window.navigator.mediaDevices.getUserMedia) {
+        // 最新的标准API
+        window.navigator.mediaDevices
+          .getUserMedia(constraints)
+          .then(success)
+          .catch(error);
+      } else if (window.navigator.webkitGetUserMedia) {
+        // webkit核心浏览器
+        window.navigator.webkitGetUserMedia(constraints, success, error);
+      } else if (window.navigator.mozGetUserMedia) {
+        // firfox浏览器
+        window.navigator.mozGetUserMedia(constraints, success, error);
+      } else if (window.navigator.getUserMedia) {
+        // 旧版API
+        window.navigator.getUserMedia(constraints, success, error);
+      }
+    },
+
+    //判断是全屏则退出全屏
+    isFullScreen() {
+      if (!!(document.webkitIsFullScreen || this.fullele())) {
+        try {
+          var de = document;
+          if (de.exitFullscreen) {
+            de.exitFullscreen();
+          } else if (de.mozCancelFullScreen) {
+            de.mozCancelFullScreen();
+          } else if (de.webkitCancelFullScreen) {
+            de.webkitCancelFullScreen();
+          }
+        } catch (err) {}
+      }
+    },
+    fullele() {
+      return (
+        document.fullscreenElement ||
+        document.webkitFullscreenElement ||
+        document.msFullscreenElement ||
+        document.mozFullScreenElement ||
+        null
+      );
+    },
+    //拍照
+    onPhoto() {
+      const canvas = document.createElement("canvas");
+      canvas.width = 400;
+      canvas.height = 400;
+      const context = canvas.getContext("2d");
+      const video = document.getElementById("video");
+      context.drawImage(video, 0, 0, 400, 400);
+      this.faceUrl = canvas.toDataURL("image/png");
+      this.isTaking = false;
+    },
+    //重拍-重置拍照
+    reTake() {
+      this.faceUrl = "";
+      this.isTaking = true;
+      this.getUserMedia({
+        video: {
+          width: 400,
+          height: 400
+        }
+      });
+    },
+    //确认拍照
+    async takeOk() {
+      this.loading = true;
+      this.$emit("returnParameter", this.faceUrl);
+      this.takePhotoModal = false;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.take-photo {
+  /deep/ .el-dialog__header {
+    display: none;
+  }
+
+  /deep/ .el-dialog__body {
+    padding: 0;
+    overflow: unset;
+  }
+
+  &__close {
+    cursor: pointer;
+    position: absolute;
+    right: 0;
+    top: -28px;
+    width: 24px;
+    height: 24px;
+    line-height: 24px;
+    text-align: center;
+    color: #eee;
+    border: 1px solid #eee;
+    border-radius: 50%;
+  }
+
+  &__header {
+    height: 40px;
+    border-bottom: 1px solid #eee;
+    line-height: 40px;
+    font-size: 16px;
+    font-family: Microsoft YaHei;
+    font-weight: bold;
+    color: #333333;
+    padding-left: 24px;
+  }
+
+  &__body {
+    // height: 400px;
+    padding: 40px 24px;
+
+    .left-box {
+      width: 336px;
+      float: left;
+
+      .title {
+        font-size: 16px;
+        font-family: Microsoft YaHei;
+        font-weight: bold;
+        color: #ff3b30;
+        line-height: 24px;
+      }
+
+      .content {
+        font-size: 14px;
+        font-family: Microsoft YaHei;
+        font-weight: 400;
+        color: #333333;
+        line-height: 28px;
+        margin-top: 32px;
+      }
+    }
+
+    .right-box {
+      float: right;
+      width: 400px;
+      height: 400px;
+      position: relative;
+      overflow: hidden;
+
+      video {
+        width: 100%;
+        height: 100%;
+      }
+
+      .mask {
+        width: 55%;
+        height: 200px;
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        margin: 30px auto 0;
+        box-shadow: 0 0 0 2000px rgba(0, 0, 0, 0.4);
+      }
+    }
+  }
+
+  &__footer {
+    height: 90px;
+    border-top: 1px solid #eee;
+    text-align: center;
+
+    .take {
+      display: inline-block;
+      width: 200px;
+      height: 40px;
+      padding: 0;
+      border-radius: 20px;
+      text-align: center;
+      line-height: 40px;
+      margin: 24px auto;
+    }
+  }
+}
+</style>

+ 202 - 5
src/components/videoCy/index.vue

@@ -13,11 +13,17 @@
       <span class="videoCurrentTime_style">|</span>
       <span class="btn_sty" @click="seekVideo0">从头播放</span>
     </div>
+    <takePicture
+      ref="takePicture"
+      @returnParameter="returnParameter"
+    ></takePicture>
   </div>
 </template>
 
 <script>
+import takePicture from "@/components/takePicture/index.vue";
 export default {
+  components: { takePicture },
   inject: ["getGoodsData"],
   data() {
     return {
@@ -28,7 +34,8 @@ export default {
       photoHistoryList: [], //历史和已拍照数据
       photoIndex: 0, //当前拍照对应索引
       showRecordStatus: false, //是否显示从头播放提示
-      showRecordSetTimeOut: null //从头播放提示计时器函数
+      showRecordSetTimeOut: null, //从头播放提示计时器函数
+      openPhotoStatus: 0 //暂存学习状态
     };
   },
   computed: {
@@ -229,7 +236,7 @@ export default {
       return photoList;
     },
     //postTime = true 只提交随机时间 false 提交拍照
-    postCoursePhotoRecord(postTime = false) {
+    postCoursePhotoRecord(postTime = false, photoUrl) {
       return new Promise((resolve, reject) => {
         let currentTime = this.player.j2s_getCurrentTime();
         let data = {
@@ -239,7 +246,7 @@ export default {
           moduleId: this.activeSection.moduleId,
           chapterId: this.activeSection.chapterId,
           sectionId: this.activeSection.sectionId,
-          photo: this.ossAvatarUrl,
+          photo: postTime ? "" : photoUrl,
           photoTime: parseInt(currentTime > 0 ? currentTime : 0),
           photoIndex: postTime ? -2 : parseInt(this.photoIndex), //从0算起,-2只提交随机时间
           photoNum: parseInt(this.goodsData.goodsPhotographConfig.photoNum),
@@ -248,7 +255,7 @@ export default {
         this.$request
           .coursePhotoRecord(data)
           .then(res => {
-            resolve();
+            resolve(res);
           })
           .catch(err => {
             reject();
@@ -426,8 +433,39 @@ export default {
     },
     //当前视频播放完毕时触发
     onPlayOver() {
+      this.$message({
+        type: "success",
+        message: "播放完毕"
+      });
+      this.isFullScreen();
+      this.postStudyRecord(1);
       console.log("当前视频播放完毕时触发");
     },
+
+    //判断是全屏则退出全屏
+    isFullScreen() {
+      if (!!(document.webkitIsFullScreen || this.fullele())) {
+        try {
+          var de = document;
+          if (de.exitFullscreen) {
+            de.exitFullscreen();
+          } else if (de.mozCancelFullScreen) {
+            de.mozCancelFullScreen();
+          } else if (de.webkitCancelFullScreen) {
+            de.webkitCancelFullScreen();
+          }
+        } catch (err) {}
+      }
+    },
+    fullele() {
+      return (
+        document.fullscreenElement ||
+        document.webkitFullscreenElement ||
+        document.msFullscreenElement ||
+        document.mozFullScreenElement ||
+        null
+      );
+    },
     //播放出现错误时触发
     onPlayerError() {
       this.$message.error("播放出现错误时触发");
@@ -437,7 +475,166 @@ export default {
       this.$message.error("发生业务逻辑错误");
     },
     //启动拍照
-    openPhoto() {}
+    openPhoto() {
+      this.$refs.takePicture.openPhoto();
+    },
+    //拍照成功回显 url
+    async returnParameter(url) {
+      let compareFaceData = await this.faceRecognition(url);
+      if (compareFaceData >= 80) {
+        let file = this.$tools.convertBase64UrlToBlob(url);
+        const photoUrl = await this.$upload.upload(file, 0, {
+          gradeId: this.goodsData.gradeId,
+          orderGoodsId: this.goodsData.orderGoodsId
+        });
+        this.postCoursePhotoRecord(false, photoUrl)
+          .then(async res => {
+            this.photoHistoryList.push(this.photoIndex);
+            const STATUS = await this.postStudyRecord(
+              0,
+              photoUrl,
+              compareFaceData
+            ); //提交记录
+            //恢复播放
+            if (STATUS) {
+              var polyvPlayerContext = this.player;
+              if (polyvPlayerContext && this.openPhotoStatus !== 1) {
+                polyvPlayerContext.j2s_resumeVideo();
+              }
+            }
+          })
+          .catch(err => {
+            this.$message({
+              type: "warning",
+              message: "上传接口报错,请重新拍照上传"
+            });
+            setTimeout(() => {
+              this.openPhoto();
+            }, 1500);
+          });
+      } else {
+        this.$message({
+          type: "warning",
+          message: "人脸匹配不通过,请重新拍照上传"
+        });
+        setTimeout(() => {
+          this.openPhoto();
+        }, 1500);
+      }
+    },
+
+    /**
+     * 提交观看记录
+     * status 1 学完 0未学完
+     */
+    postStudyRecord(status = 0, imgUrl, compareFaceData) {
+      return new Promise((resolve, reject) => {
+        let currentTime = 0;
+        let PlayDuration = 0;
+        var polyvPlayerContext = this.player;
+        if (polyvPlayerContext) {
+          currentTime = polyvPlayerContext.j2s_getCurrentTime(); //当前视频播放时刻
+          PlayDuration = polyvPlayerContext.j2s_realPlayVideoTime(); //本次看的时长
+        }
+        let data = {
+          orderGoodsId: parseInt(this.goodsData.orderGoodsId),
+          goodsId: parseInt(this.goodsData.goodsId),
+          gradeId: parseInt(this.goodsData.gradeId),
+          courseId: this.activeSection.courseId,
+          moduleId: this.activeSection.moduleId,
+          chapterId: this.activeSection.chapterId,
+          sectionId: this.activeSection.sectionId,
+          fromPlat: 2, //来源平台 1小程序 2网站
+          photo: imgUrl || "",
+          studyDuration: parseInt(PlayDuration > 0 ? PlayDuration : 0),
+          videoCurrentTime: parseInt(currentTime > 0 ? currentTime : 0),
+          erJianErZao: this.goodsData.erJianErZao
+        };
+
+        if (imgUrl) {
+          data.similarity = compareFaceData; // 相似度
+        }
+        if (status > 0) {
+          data.status = status;
+        }
+        // /study/record 学习记录
+        this.$request
+          .studyRecord(data)
+          .then(res => {
+            if (status > 0) {
+              this.openPhotoStatus = 0;
+              this.$message.success("学习完成");
+              resolve(false);
+            }
+            if (this.openPhotoStatus === 1) {
+              this.postStudyRecord(1);
+            }
+            if (status == 0 && this.openPhotoStatus !== 1) {
+              resolve(true);
+            }
+          })
+          .catch(err => {
+            if (err.code === 600) {
+              polyvPlayerContext.j2s_pauseVideo();
+              this.$confirm(`开通信息推送不成功,无法学习!`, "提示", {
+                confirmButtonText: "确定",
+                closeOnClickModal: false,
+                closeOnPressEscape: false,
+                distinguishCancelAndClose: false,
+                showCancelButton: false,
+                showClose: false
+              })
+                .then(_ => {
+                  //停止执行-退出页面
+                  this.$router.back(-1);
+                })
+                .catch(_ => {
+                  //停止执行-退出页面
+                  this.$router.back(-1);
+                });
+            }
+            if (err.code === 559) {
+              console.log("拍照不够触发");
+              this.$message.error(err.msg);
+              this.openPhotoStatus = 1;
+              setTimeout(() => {
+                this.openPhoto();
+              }, 1500);
+            }
+            if (err.code === 558) {
+              this.$message.error(err.msg);
+            }
+          });
+      });
+    },
+    //人脸校验
+    faceRecognition(url) {
+      return new Promise(resolve => {
+        this.$request
+          .faceCertificationCompareFace({
+            imageA: url,
+            orderGoodsId: this.goodsData.orderGoodsId,
+            gradeId: this.goodsData.gradeId
+          })
+          .then(res => {
+            resolve(res.data);
+          })
+          .catch(err => {
+            if (err.toString().indexOf("timeout") != -1) {
+              err = {
+                msg: "拍照超时,请重新拍照"
+              };
+            }
+            this.$message({
+              type: "warning",
+              message: err.msg
+            });
+            setTimeout(() => {
+              this.openPhoto();
+            }, 1500);
+          });
+      });
+    }
   }
 };
 </script>

+ 9 - 3
src/pages/course-detail/components/CourseTree.vue

@@ -203,10 +203,15 @@ export default {
         const examRes = await this.$request.reSectionExamList({
           chapterId: 0,
           courseId: id,
-          gradeId: this.goodsData.gradeId
+          gradeId: this.goodsData.gradeId,
+          orderGoodsId: this.goodsData.orderGoodsId
         }); //获取节关联练习试卷
         this.$request
-          .reMenuList({ courseId: id, gradeId: this.goodsData.gradeId })
+          .reMenuList({
+            courseId: id,
+            gradeId: this.goodsData.gradeId,
+            orderGoodsId: this.goodsData.orderGoodsId
+          })
           .then(res => {
             this.courseDataList = res.rows.map(i => {
               return {
@@ -287,7 +292,8 @@ export default {
           moduleId: item.moduleId,
           chapterId: item.chapterId,
           courseId: item.courseId,
-          gradeId: this.goodsData.gradeId
+          gradeId: this.goodsData.gradeId,
+          orderGoodsId: this.goodsData.orderGoodsId
         }); //获取节关联练习试卷
         this.$request
           .reSectionList({