赞
踩
记录下两年前开发小程序答题监控功能的思路。因时间久远,同时删除了部分业务代码,没讲清楚的请多多包涵。
开始采用的方案是分别开发“考试须知”和“考试”两个独立页面,后因“考试”页面采用了tensorflow人脸识别,初始相机和加载模型耗时过长,遂将两个页面合并,让用户提前加载模型,同时两个页面是考试时长控制也统一起来了。其中最关键的视图容器就是page-container。
功能描述:
页面容器。
小程序如果在页面内进行复杂的界面设计(如在页面内弹出半屏的弹窗、在页面内加载一个全屏的子页面等),用户进行返回操作会直接离开当前页面,不符合用户预期,预期应为关闭当前弹出的组件。 为此提供“假页”容器组件,效果类似于
popup
弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用navigateBack
接口。
根据官方文档描述,我们使用page-container来构建一个类似弹出层的假的“子页面”。它的特性可以用于禁止用户在考试过程中返回上一页,如果是两个独立页面,很难实现这一点。
功能描述:
系统相机。扫码二维码功能,需升级微信客户端至6.7.3。需要用户授权
scope.camera
。 2.10.0起 initdone 事件返回 maxZoom,最大变焦范围,相关接口 CameraContext.setZoom。请注意原生组件使用限制
- <page-meta page-style="{{ popShow||containerShow ? 'overflow: hidden;' : '' }}">
- <pageload id="pageload" bind:initPage="initPage" ...>
- <view>
- 考试须知页面
- </view>
- </pageload>
-
- <page-container show="{{containerShow}}" z-index="100" id="container"
- duration="{{duration}}"
- position="right"
- bind:enter="pageenter"
- bind:beforeleave="beforeleave"
- bind:afterleave="afterleave"
- >
-
- <pageload bind:initPage="initExamPage" requesting="true" id="examPage">
- <block wx:if="{{cameraShow}}">
- <view class="camera-container-out">
- <view class="camera-container-in">
- <camera
- device-position="front"
- flash="off"
- frame-size="small"
- binderror="cameraError"
- class="camera"
- ></camera>
- <canvas id="mark-canvas" class="mark-canvas" type="2d"></canvas>
- </view>
- </view>
- </block>
-
- <view class="titlebar">
- <view>
- 剩余答题时间:
- </view>
- </view>
- <view class="pageBody pageBody_sty">
- <view>
- 考试题目
- </view>
- <scroll-view scroll-y="true" scroll-top="{{top}}" style="height:{{viewHeight}}rpx;" show-scrollbar="false">
- <view class="item_con" wx:for="{{curr_question.trunk.options}}">
- 试题选项
- </view>
- </scroll-view>
- </view>
-
- <view class="foot-nav-panel">
- <view class="foot-nav-ul">
- <view class="li">
- <van-button type="primary" block custom-class="foot-btn"
- bind:click="showPop">
- 答题板
- </van-button>
- </view>
- <view class="li leftBorder">
- <van-button type="primary" block custom-class="foot-btn"
- data-id="{{curr_question.nextId}}"
- bind:click="nextQuestion">
- {{!curr_question.nextId?'交卷':'下一题'}}
- </van-button>
- </view>
- </view>
- </view>
-
- <van-popup show="{{popShow}}" closeable position="bottom" custom-style="height: 60%" bind:close="hidPop">
- <view class="serialNum">
- 答题板详情
- </view>
- </van-popup>
- </pageload>
-
- </page-container>
- </page-meta>
页面属性配置节点,用于指定页面的一些属性、监听页面事件。只能是页面内的第一个节点。page-style属性:页面根节点样式,页面根节点是所有页面节点的祖先节点,相当于 HTML 中的 body 节点。
<page-meta page-style="{{ popShow||containerShow ? 'overflow: hidden;' : '' }}">
当弹出层(答题板)或者子页面(答题页)显示时,父页面溢出隐藏,防止滑动操作时父页面滚动。
功能描述
页面初始化。打开页面后执行initPage方法,通过回调来判断展示正确或错误的页面信息
- <view>
- <view wx:if="{{requesting===false}}">
- <view wx:if="{{status===false}}" class="error">
- <image class="error_image" src="{{errImg}}"></image>
- <view class="error_text">{{errText?errText:'未知错误'}}</view>
- <view class="error_btn">
- <!-- <view bindtap="refresh" class="refresh">
- <image class="icon" src="/assets/image/replay.png"/>
- </view> -->
- <button bindtap="refresh" class="btnDefault">刷新</button>
- <button bindtap="goback" class="btnBack marginTop2">返回</button>
- </view>
- </view>
- <view wx:if="{{status===true}}">
- <slot>
- </slot>
- </view>
- </view>
- </view>
- Component({
- options: {
- addGlobalClass: true
- },
- properties: {
- requesting: {
- type: Boolean,
- value: false,
- observer: 'requestingEnd',
- },
- status: {
- type: Boolean,
- value: false
- },
- errText: {
- type: String,
- value: ''
- },
- errImg: {
- type: String,
- value: '/assets/image/empty-image-error.png'
- },
- cancelLoading:{
- type: Boolean,
- value: false
- }
- },
- data: {
-
- },
- methods: {
- /**
- * 监听 requesting 字段变化
- */
- requestingEnd(newVal, oldVal) {
- if(!this.data.cancelLoading){
- if (oldVal === true && newVal === false) {
- if(this.data.status===false&&this.data.errText==''){
- //兼容request.js中拦截500和404响应,toast提示不能立马关闭
- setTimeout(() => {
- wx.hideLoading()
- }, 2000);
- }else{
- wx.hideLoading()
- }
- } else {
- wx.showLoading({
- title: '加载中',
- mask: true
- })
- }
- }
- },
- init: function(){
- //防止重复调用
- if(this.data.requesting){
- return;
- }
- //插件初始化
- this.setData({
- requesting : true,
- status: false,
- errText: ''
- });
- this.network().then(() => {
- this.triggerEvent('initPage',{
- callback: res=>{
- this.setData(res);
- }
- });
- }).catch(res => {
- this.setData({
- requesting : false,
- status : false,
- errText : res
- });
- })
-
- },
- refresh: function(){
- this.init();
- },
- network: function() {
- return new Promise((resolve, reject) => {
- wx.getNetworkType({
- success: res => {
- if (res.networkType != 'none') {
- resolve();
- } else {
- reject("网络异常");
- }
- },
- })
- })
- },
- goback: function(){
- wx.navigateBack();
- }
- },
- pageLifetimes: {
- show: function() {
- // 页面被展示
- },
- hide: function() {
- // 页面被隐藏
- }
- },
- lifetimes: {
- attached: function() {
- // 在组件实例进入页面节点树时执行
- if(!this.data.requesting){
- this.init();
- }
- },
- }
- })
-
- /*
- 使用该组件的页面js中添加如下方法:
- initPage(e){
- let that = this;
- let callback = e.detail.callback;
- let url = app.globalData.url + "/...";
- request.requestPostApi(url,{},this,succRes=>{
- callback({
- status : succRes.status,
- errText : succRes.message
- });
- if(succRes.status){
- doSuccess...
- }
- },failRes=>{
- callback({
- status : false,
- errText : '未知错误'
- });
- },completeRes=>{
- callback({requesting:false});
- });
- }
- */
本文使用的UI组件为Vant Weapp - 轻量、可靠的小程序 UI 组件库
- <view>剩余时间:
- <van-count-down use-slot
- class="control-count-down"
- time="{{ examInfo.remainingTime }}"
- bind:change="remainingTimeChange"
- bind:finish="remainingTimeFinish">
- <text class="time" wx:if="{{vantRemainingTime.days>0}}">{{ vantRemainingTime.days }}天</text>
- <text class="time" >{{ vantRemainingTime.hours>9?vantRemainingTime.hours:'0'+vantRemainingTime.hours }}时</text>
- <text class="time" >{{ vantRemainingTime.minutes>9?vantRemainingTime.minutes:'0'+vantRemainingTime.minutes }}分</text>
- <text class="time" >{{vantRemainingTime.seconds>9?vantRemainingTime.seconds:'0'+vantRemainingTime.seconds}}秒</text>
- </van-count-down>
- </view>
通过组件绑定事件remainingTimeChange来处理倒计时问题。除了计算整场考试剩余时间,可调用自定义函数去控制单个题目显示时长。
- remainingTimeChange(e) {
- this.setData({
- 'vantRemainingTime':e.detail
- });
- //自定义函数执行业务逻辑
- this.timeGo(e.detail);
- }
当小程序切换到后台,js定时器会停止,考试剩余时间就需要重新从服务端获取,可以在onShow中来触发。
- onHide(){
- this.setData({pageHide:true})
- },
- onShow(){
- this.setData({pageHide:false});
- //请求服务端更新考试剩余时间
- this.updateRemainingTime();
- }
在onLoad中初始化子页面的滚动区域大小,在onReady中初始化tfjs插件(注:20230828,鸿蒙系统无法使用,暂未解决。该插件主要为在小程序端追踪人脸,实时提示用户保持头像居中,可整体移除。具体的考试监控由相机拍照上传至服务端后,异步执行)。tfjs插件使用教程可自行搜索。
- const app = getApp();
- const faces = require("../../../utils/face-storage.js");
- const request = require("../../../utils/request.js");
- const util = require("../../../utils/util.js");
- const examApi = require("../../../utils/exam.js");
- var fetchWechat = require('fetch-wechat');
- var tf = require('@tensorflow/tfjs-core');
- var webgl = require('@tensorflow/tfjs-backend-webgl');
- var plugin = requirePlugin('tfjsPlugin');
- const faceDetection = require('@tensorflow-models/face-detection');
- const log = require('../../../utils/log.js')
-
- onLoad(options) {
- util.triggerEvent('resetIsClick');
- this._cup2model = app.globalData.cup2model;
- var that = this;
- that.initViewHeight();
- //防止 setTimeout 和 setInterval 在页面返回后继续执行
- var taskId = setInterval(() => {
- let timeout;
- if(!util.isEmpty(that.data.examInfo)){
- timeout = that.data.examInfo.timeout;
- }
- if(timeout!=1){
- that.updateRemainingTime();
- }else{
- clearInterval(taskId);
- }
- }, 1000*60*10)
- that.setData({taskId,taskId});
- },
- initViewHeight: function(){
- var that = this;
- wx.getSystemInfoAsync({
- success: function(res) {
- let {windowHeight,windowWidth} = res;
- //页面高度减头部底部按钮高度,结合自身页面情况计算
- let rpxHeight = Math.floor(750*(windowHeight-105)/(windowWidth || 375))-280;
- that.setData({viewHeight:rpxHeight});
- }
- });
- },
- async onReady(){
- if(this.data.useTfjs){
- this.initTfjsPlugin();
- this.loadmodel();
- }
- },
- initTfjsPlugin(){
- let config = {
- // polyfill fetch function
- fetchFunc: fetchWechat.fetchFunc(),
- // inject tfjs runtime
- tf,
- // inject webgl backend
- webgl,
- // provide webgl canvas
- canvas: wx.createOffscreenCanvas()
- }
- plugin.configPlugin(config);
- },
- async loadmodel(){
- try {
- let start = new Date();
- const FILE_STORAGE_PATH = 'zjk_face_model';
- const fileStorageHandler = plugin.fileStorageIO(FILE_STORAGE_PATH,wx.getFileSystemManager());
- const model = faceDetection.SupportedModels.MediaPipeFaceDetector;
- const detectorConfig = {
- maxFaces: 2,
- runtime: 'tfjs'
- };
- detectorConfig.detectorModelUrl = this._modelUrl;
- this._model = await faceDetection.createDetector(model, detectorConfig);
- let end = new Date();
- log.info("下载模型成功,耗时:"+ (end-start)+" ms");
- if(!this._cup2model){
- this._model.estimateFaces(
- {
- data:new Uint8Array(),
- width:1,
- height:1
- },
- {flipHorizontal: false}
- ).then(e=>{
- log.info("模型运行成功,耗时:"+ (new Date()-end)+" ms")
- if(this._modelLoad){
- log.info("成功后关闭等待框")
- wx.hideLoading();
- }
- this._modelLoad = true;
- this._cup2model = true;
- app.globalData.cup2model = true;
- console.log("第一次模型运行成功")
- }).catch(e=>{
- log.error("模型运行失败,耗时:"+ (new Date()-end)+" ms")
- if(this._modelLoad){
- log.error("失败后关闭等待框")
- wx.hideLoading();
- }
- this._modelLoad = true;
- console.log("模型运行失败",e)
- })
- }
- } catch (error) {
- log.error("捕捉到异常信息"+JSON.stringify(error))
- if(this._modelLoad){
- log.error("捕捉到异常信息后关闭等待框")
- wx.hideLoading();
- }
- this._modelLoad = true;
- }
- //代码运行失败
- /* try{
- console.log("加载本地模型")
- detectorConfig.detectorModelUrl = fileStorageHandler;
- this._model = await faceDetection.createDetector(model, detectorConfig);
- }catch(e){
- console.log("加载本地模型失败",e)
- detectorConfig.detectorModelUrl = this._modelUrl;
- this._model = await faceDetection.createDetector(model, detectorConfig);
- console.log("加载网络模型成功")
- try{
- this._model.detectorModel.save(fileStorageHandler);
- }catch(e){
- console.log("保存本地模型失败",e)
- }
-
- } */
- }
- onShow(){
- this.setData({pageHide:false});
- this.updateRemainingTime();
- },
- onHide(){
- this.setData({pageHide:true})
- },
- onUnload(){
- if(this.data.taskId){
- clearInterval(this.data.taskId);
- }
- let countdown = this.selectComponent('.control-count-down');
- if(countdown){
- //返回上一页必须关闭定时器,否则会一直执行
- countdown.pause();
- }
- },
- /** 页面初始化,获取考试信息*/
- initPage(e){
- let that = this;
- let callback = e.detail.callback;
- let url = app.globalData.url + "/.../getExamInfo";
- request.requestPostApi(url,{},this,succRes=>{
- callback({
- status : succRes.status,
- errText : succRes.message
- });
- if(succRes.status){
- that.setData({
- examInfo : succRes.result
- });
- if(!that.data.examInfo.examing){//如果后台已经提交或者无考试,重置本地考试完成状态
- examApi.finish(false);
- }
- that.setData({localFinish:examApi.finish()})
- }
- },failRes=>{
- callback({
- status : false,
- errText : '未知错误'
- });
- },completeRes=>{
- callback({requesting:false});
- if(!this._cup2model&&!this._modelLoad){
- this._modelLoad = true;
- wx.showLoading({
- title: '加载中',
- mask: true
- })
- }
- });
- },
- timeBeforeChange(e) {
- this.setData({
- timeBefore: e.detail
- });
- },
- timeBeforeFinish(){
- this.setData({
- 'examInfo.timeout':0
- })
- },
- remainingTimeChange(e) {
- this.setData({
- 'vantRemainingTime':e.detail
- });
- this.timeGo(e.detail);
- },
- remainingTimeFinish(){
- this.setData({
- 'examInfo.timeout':1
- })
- if(this.data.examInfo.examing){
- this.submitExam();
- }
- },
- updateRemainingTime(){
- var that = this;
- if(!util.isEmpty(that.data.examInfo)&&that.data.examInfo.timeout!=1&&!(that.data.examInfo.answerCountCurrent==that.data.examInfo.answerCountMax&&!that.data.examInfo.examing&&that.data.examInfo.isSubmit)){
- request.requestPostApi(app.globalData.url + "/.../validateRemainingTime",{},this,succRes=>{
- if(succRes.status){
- that.updateExamInfo(succRes.result);
- }else{
- console.log("刷新时间失败:"+succRes.message);
- }
- },failRes=>{
- console.log("刷新时间失败");
- });
- }
- }
- pageenter(){
- //打开子页面,执行initPage绑定方法
- let examPage = this.selectComponent("#examPage");
- examPage.setData({requesting:false});
- examPage.init();//执行的是initExamPage
- },
- beforeleave(){
- //隐藏子页面,停止摄像头
- this._listener&&this._listener.stop();
- this.setData({
- curr_question: {},
- containerShow:false
- })
- },
- afterleave(){
- // 可在该事件内阻止用户返回
- // if(!this.data.localFinish&&this.data.examInfo.timeout==0){
- // this.setData({containerShow:true});
- // setTimeout(() => {
- // request.toast("请保持答题页面至提交试卷")
- // }, 300);
- // }
- },
- initExamPage(e){
- let that = this;
- that.setData({isLoadingExam:false});
- let callback = e.detail.callback;
- let{questions,answers,currQuestion} = examApi.getAll();
- //中途退出,直接从本地缓存取题目
- if(!util.isEmpty(questions)&&!util.isEmpty(currQuestion)){
- console.log("从缓存中获取试题数据")
- var count = 0;
- for(let key in questions){
- count++;
- }
- that.setData({
- questions:questions,
- listCount: count,
- userAnswers: answers,
- curr_question:questions[currQuestion.id]
- })
- //作用:显示剩余时间后再callback
- if(!util.isEmpty(that.data.vantRemainingTime)){
- that.timeGo(that.data.vantRemainingTime);
- }
- callback({
- status : true,
- errText : '',
- requesting : false
- });
- //初始化相机
- that.initCamera();
- return;
- }
-
- let url = app.globalData.url + "/.../getExamQuestions";
- request.requestPostApi(url,{},this,succRes=>{
- callback({
- status : succRes.status,
- errText : succRes.message
- });
- if(succRes.status){
- let qs = succRes.result;
- let temp_questions = {};
- for(var i=0;i<qs.length;i++){
- temp_questions[qs[i].id]=qs[i];
- }
- that.setData({
- questions:temp_questions,
- listCount: qs.length
- })
- that.showQuestion(qs[0]["id"]);//显示第一题,在本地保存questions和curr_question
- that.updateRemainingTime();
- //初始化相机
- that.initCamera();
- }
- },failRes=>{
- callback({
- status : false,
- errText : '未知错误'
- });
- },completeRes=>{
- callback({requesting:false});
- });
- }
- initCamera(){
- console.log("显示相机和画布");
- this.setData({cameraShow:true});
- setTimeout(() => {//必须setTimeout,等待页面渲染完成
- try {
- if(!this._camera){
- console.log("创建相机")
- this._camera = wx.createCameraContext();
- this._camera.setZoom({
- zoom:1
- })
- }else{
- this._camera.setZoom({
- zoom:1
- })
- }
- if(this.data.useTfjs){//判断是否使用tfjs
- this.initCanvas();//初始画布,用于显示人脸追踪框
- }
- this.addCameraListener();
- this._listener.start();
- } catch (error) {
- console.log("exception:",error)
- }
- }, 1500);
- },
- initCanvas(){
- if(!this._canvas){
- console.log("创建画布")
- const query = wx.createSelectorQuery();
- query.select('#mark-canvas')
- .fields({ node: true, size: true })
- .exec((res) => {
- this._canvas = res[0].node;
- const canvas = res[0].node;
- const canvasContext = canvas.getContext('2d')
- const systemInfo = wx.getSystemInfoSync()
- //设备像素比
- const dpr = systemInfo.pixelRatio
- //画布像素
- canvas.width = res[0].width * dpr
- this._canvasWidthPix = canvas.width;
- canvas.height = res[0].height * dpr
- this._canvasHeightPix = canvas.height
- this._dpr = dpr;
- canvasContext.lineWidth = 3
- canvasContext.strokeStyle = 'red'
- canvasContext.fillStyle = 'yellow'
- this._canvasContext = canvasContext;
- })
- }
- },
- addCameraListener(){
- if(!this._listener){
- console.log("创建相机监听")
- this._listener = this._camera.onCameraFrame(async frame=> {
- //当使用tfjs时,每隔1秒(60帧)判断一次人脸位置
- if(this.data.useTfjs&&this.data.containerShow){
- if(this._frameWidth!=frame.width || this._frameHeight != frame.height){
- this._frameWidth = frame.width;
- this._frameHeight = frame.height;
- this._xRatio = Math.round(this._canvasWidthPix*1000 / frame.width)/1000;
- //等比例缩放后画布高的像素值
- let tempCanvasnHeightPix = Math.floor(frame.height*this._xRatio);
- //画布沿y轴偏移量
- this._yoffset = Math.floor((this._canvasHeightPix-tempCanvasnHeightPix)/2);
- console.log("初始化缩放倍数和偏移量",this._frameWidth,this._frameHeight,this._xRatio,this._yoffset)
- }
- this._count++;
- if (this._count === 60) {
- this.clearMarkCanvas();
- if(this._model){
- const res = await this.detectFace(frame);
- this.validateFace(res);
- }
- this._count = 0;
- }else if(this._count>60){
- this._count = 0;
- }
- }
- });
- }
- },
- async detectFace(frame) {
- const image = {
- data: new Uint8Array(frame.data),
- width: frame.width,
- height: frame.height
- }
- const estimationConfig = {flipHorizontal: false};
- return await this._model.estimateFaces(image, estimationConfig);
- },
- validateFace(res){
- //this._canvasContext.strokeRect(0,0,this._canvasWidthPix,this._canvasHeightPix);
- let msg = "";
- if(res.length<1){
- msg = '请保持头像居中';
- this.setData({faceError:msg});
- return;
- }
- /* if(res.length>1){
- msg = '检测到多人';
- this.setData({faceError:msg});
- return;
- } */
-
- const face = res[0];
- //画关键点
- // const keypoints = face.keypoints
- // for(let i=0; i<6; ++i){
- // const point = this.transformPoint([keypoints[i].x,keypoints[i].y])
- // this._canvasContext.fillRect(point[0],point[1],6,6)
- // }
- const box = face.box;
- const start = this.transformPoint([box.xMin,box.yMin]);
- const end = this.transformPoint([box.xMax,box.yMax]);
- let size = [end[0] - start[0], end[1] - start[1]]
- //画人脸预测框
- //this._canvasContext.strokeRect(start[0], start[1], size[0], size[1]);
- /* if (size[0] < 0.2 * this._canvasWidthPix){
- msg = '距离太远';
- this.setData({faceError:msg});
- return;
- } */
- if (size[0] > this._canvasWidthPix*1.05){
- msg = '距离太近';
- this.setData({faceError:msg});
- return;
- }
- if(start[0] < -0.06 * this._canvasWidthPix){
- msg = "头像偏左,";
- }else if(end[0] > 1.06 * this._canvasWidthPix){
- msg = "头像偏右,";
- }else if(start[1] < 0.14 * this._canvasHeightPix){
- msg = "头像偏上,";
- }else if(end[1] > 1.06 * this._canvasHeightPix){
- msg = "头像偏下,";
- }
- if(msg){
- this.setData({faceError:msg+'请保持头像居中'});
- return;
- }
- this.setData({faceError:""});
- },
- transformPoint(point){
- const x = Math.floor(point[0] * this._xRatio);
- const y = Math.floor(point[1] * this._xRatio)+this._yoffset;
- return [x,y]
- },
- clearMarkCanvas(){
- this._canvasContext&&this._canvasContext.clearRect(0,0,this._canvasWidthPix,this._canvasHeightPix)
- },
- cameraError(e){
- request.toast("摄像头打开失败,请退出重试或更换手机!")
- }
如果要使用tfjs,请参考下面摄像头和画布的关系。画布透明地覆盖在摄像头上。如果要求隐形监控的话,可以将相机组件长宽设置为1px,同时,在onCameraFrame中截取视频帧图片进行上传,以达到隐形效果。
因本案例无特殊要求,所以采用了最简单的方式实现监控:每间隔一道题拍一次照片上传至服务端。这样做的弊端是,苹果用户拍照会有系统提示声(甲方没说就懒得改了...)
- <block wx:if="{{cameraShow}}">
- <view class="camera-container-out">
- <view class="camera-container-in">
- <camera
- device-position="front"
- flash="off"
- frame-size="small"
- binderror="cameraError"
- class="camera"
- ></camera>
- <canvas id="mark-canvas" class="mark-canvas" type="2d"></canvas>
- </view>
- </view>
- </block>
- .camera-container-out {
- position: absolute;
- top:100rpx;
- right: 30rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- z-index: 101;
- }
- .camera-container-in {
- display: flex;
- justify-content: flex-start;
- }
- .camera{
- width: 50px;
- height: 70px;
- z-index: 101;
- }
- .mark-canvas{
- position: absolute;
- width: 50px;
- height: 70px;
- z-index: 102;
- }
图片保存使用阿里云oss服务。人脸比对也用的是阿里云服务。
- takePhoto() {
- const that = this;
- if(!this._camera){
- setTimeout(() => {
- that.takePhoto();
- }, 1100);
- return;
- }
- this._camera.takePhoto({
- quality: 'high',
- success: (res) => {
- let localSrc = res.tempImagePath;
- let type = localSrc.substring(localSrc.lastIndexOf("."));
- console.log("开始上传");
- let policy = this._policy;
- let signature = this._signature;
- let oSSAccessKeyId = this._OSSAccessKeyId;
- let date = new Date();
- let filePath = that.data.examInfo.themeId+"/"+that.data.examInfo.examUser+"/"+date.Format('yyyyMMddhhmmssS')+type;
- let uploadUrl = "https://域名.oss-cn-shanghai.aliyuncs.com";
- wx.uploadFile({
- url: uploadUrl, // 开发者服务器的URL。
- filePath: localSrc,
- name: 'file', // 必须填file。
- formData: {
- key: filePath,
- policy: policy,
- OSSAccessKeyId: oSSAccessKeyId,
- signature: signature
- },success: (res) => {
- if (res.statusCode === 204||res.statusCode === 200) {
- let photos = examApi.photos();
- photos.push("/"+filePath);
- let photoTimes = examApi.photoTimes();
- photoTimes.push(date.getTime());
- examApi.updatePhotoDatas(photos,photoTimes);
- }
- },
- fail: err => {
- console.log(err);
- }
- });
- },fail: err =>{
- console.log(err);
- }
- })
- }
因tfjs依赖包比较大,考试模块建议采用分包开发。本案例中tfjs依赖包如下:
"dependencies": {
"node-fetch": "2.6.1",
"@mediapipe/face_detection": "0.4.1646425229",
"@tensorflow-models/face-detection": "1.0.1",
"@tensorflow/tfjs-backend-webgl": "3.20.0",
"@tensorflow/tfjs-converter": "3.20.0",
"@tensorflow/tfjs-core": "3.20.0",
"fetch-wechat": "0.0.3"
}
其他注意事项:
防止页面重复打开;防止页面返回后定时任务和相机监控继续执行;防止tfjs初始化失败对业务的影响;相机组件要在页面渲染完成后初始化,否则会错位。
自认为这边文章写得很水,感谢阅读x3。如有问题请留意。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。