wangxy 1 månad sedan
förälder
incheckning
7866573f1a

+ 67 - 0
components/dialog/commonDialog.vue

@@ -0,0 +1,67 @@
+<template>
+	<uni-popup ref="commonPopup" :animation="false" :is-mask-click="false"
+	 mask-background-color="rgba(0, 0, 0, 0.4)">
+	 <view class="phone-common-dialog">
+		<view class="common-body-box">
+			<view class="common-title">{{title}}</view>
+			<view class="common-content" :class="dialogContentClass">{{content}}</view>
+			<view class="common-btn-box">
+				<view class="not-confirm-btn" @click="handleClose">{{notBtn}}</view>
+				<view class="confirm-btn" @click="confirmBtn">{{okBtn}}</view>
+			</view>
+		</view>
+	 </view>
+	</uni-popup>
+</template>
+
+<script setup>
+	import { ref } from 'vue';
+	const props = defineProps({
+	  title: {
+	    type: String,
+	    default: ''
+	  },
+	  content: {
+	    type: String,
+		require: true,
+	    default: ''
+	  },
+	  dialogContentClass: {
+	    type: String,
+	  	require: true,
+	    default: 'content-center-class'
+	  },
+	  notBtn: {
+	    type: String,
+	  	require: true,
+	    default: '取消'
+	  },
+	  okBtn: {
+	    type: String,
+	  	require: true,
+	    default: '确认'
+	  },
+	});
+	const commonPopup = ref(null); // 索引
+	const $emit = defineEmits(['confirm-btn'])
+	// 打开弹窗
+	function handleShow() {
+		commonPopup.value.open();
+	}
+	// 取消
+	function handleClose() {
+		commonPopup.value.close();
+	}
+	// 确认
+	function confirmBtn(){
+		$emit('confirm-btn');
+		commonPopup.value.close();
+	}
+	defineExpose({
+			handleShow,
+			handleClose
+		})
+</script>
+
+<style>
+</style>

+ 66 - 0
components/dialog/qiepingDl.vue

@@ -0,0 +1,66 @@
+<template>
+	<uni-popup ref="commonPopup" :animation="false" :is-mask-click="false"
+	 mask-background-color="rgba(0, 0, 0, 0.4)">
+	 <view class="phone-common-dialog">
+		<view class="common-body-box">
+			<view class="common-title">{{title}}</view>
+			<view class="common-content" :class="dialogContentClass">{{content}}</view>
+			<view class="common-btn-box">
+				<view class="confirm-btn" @click="confirmBtn">{{okBtn}}</view>
+			</view>
+		</view>
+	 </view>
+	</uni-popup>
+</template>
+
+<script setup>
+	import { ref } from 'vue';
+	const props = defineProps({
+	  title: {
+	    type: String,
+	    default: ''
+	  },
+	  content: {
+	    type: String,
+		require: true,
+	    default: ''
+	  },
+	  dialogContentClass: {
+	    type: String,
+	  	require: true,
+	    default: 'content-center-class'
+	  },
+	  notBtn: {
+	    type: String,
+	  	require: true,
+	    default: '取消'
+	  },
+	  okBtn: {
+	    type: String,
+	  	require: true,
+	    default: '确认'
+	  },
+	});
+	const commonPopup = ref(null); // 索引
+	const $emit = defineEmits(['confirm-btn'])
+	// 打开弹窗
+	function handleShow() {
+		commonPopup.value.open();
+	}
+	// 取消
+	function handleClose() {
+		commonPopup.value.close();
+	}
+	// 确认
+	function confirmBtn(){
+		$emit('confirm-btn');
+		commonPopup.value.close();
+	}
+	defineExpose({
+			handleShow,
+			handleClose
+		})
+</script>
+
+<style>
+</style>

+ 83 - 0
components/dialog/shexiangDialog.vue

@@ -0,0 +1,83 @@
+<template>
+	<view>
+		<uni-popup ref="commonPopup" :animation="false" :is-mask-click="false"
+		 mask-background-color="rgba(0, 0, 0, 0.4)">
+		 <view class="phone-common-dialog">
+			<view class="common-body-box">
+				<view class="common-title">{{title}}</view>
+				<view class="common-content" :class="dialogContentClass">
+				  <view>
+						<!-- IOS-->
+						<view v-if="platformType === 'ios'" class="content-margin-bottom">推荐使用微信或safari浏览器,使用其他浏览器可能会在考试过程中出现摄像头问题,影响考试结果,导致重考,不建议使用其他浏览器。
+						</view>
+						<!-- 安卓-->
+						<view v-if="platformType === 'Android'" class="content-margin-bottom">
+							推荐使用微信或火狐浏览器、谷歌浏览器,使用其他浏览器可能会在考试过程中出现摄像头问题,影响考试结果,导致重考,不建议使用其他浏览器。
+						</view>
+						<view class="content-margin-bottom">请在考试前使用摄像头测试功能,测试摄像头是否可以正常工作,在测试前请先确保摄像头设备可以正常使用,并且使用推荐浏览器并赋予了浏览器摄像头权限。</view>
+						<view>若摄像头测试中图像显示异常,请及时更换浏览器或手机,以免影响考试结果。</view>
+					</view>
+				</view>
+				<view class="common-btn-box">
+					<view class="confirm-btn" @click="confirmBtn">{{okBtn}}</view>
+				</view>
+			</view>
+		 </view>
+		</uni-popup>
+	</view>
+</template>
+
+<script setup>
+	import { ref } from 'vue';
+	const props = defineProps({
+	  title: {
+	    type: String,
+	    default: ''
+	  },
+	  content: {
+	    type: String,
+		require: true,
+	    default: ''
+	  },
+	  dialogContentClass: {
+	    type: String,
+	  	require: true,
+	    default: ''
+	  },
+	  notBtn: {
+	    type: String,
+	  	require: true,
+	    default: '取消'
+	  },
+	  okBtn: {
+	    type: String,
+	  	require: true,
+	    default: '确认'
+	  },
+	});
+	const commonPopup = ref(null); // 索引
+	const $emit = defineEmits(['confirm-btn'])
+	
+	const platformType = uni.getSystemInfoSync().platform;
+	
+	// 打开弹窗
+	function handleShow() {
+		commonPopup.value.open();
+	}
+	// 取消
+	function handleClose() {
+		commonPopup.value.close();
+	}
+	// 确认
+	function confirmBtn(){
+		$emit('confirm-btn');
+		commonPopup.value.close();
+	}
+	defineExpose({
+			handleShow,
+			handleClose
+		})
+</script>
+
+<style>
+</style>

+ 85 - 0
components/questions/danxuan.vue

@@ -0,0 +1,85 @@
+<template>
+	<view v-if="question" class="phone-danxuan-box">
+		<view class="phone-shiti-question">
+			<view class="question-num">{{question.onlyNum}}、</view>
+			<!-- 题干区域 -->
+			<rich-text :nodes="data.name"></rich-text>
+		</view>
+		
+		<!-- 选项区域 -->
+		<view v-for="(item,index) in data.contents" class="danxuan-option-box" :class="formatClass(index)" :key="index">
+			<text class="option-change"  @click="onSelect(index)">{{item.number}}</text>
+			<rich-text :nodes="item.label" class="option-question"></rich-text>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref,
+		reactive,
+		watch
+	} from 'vue';
+	import {
+		useQuestionTools
+	} from "./useQuestionTools"
+	const {
+		getLetterByIndex
+	} = useQuestionTools();
+
+	const props = defineProps({
+		question: {
+			type: Object,
+		},
+		showError: {
+			type: Boolean,
+			default: false
+		}
+	})
+
+	const data = reactive({
+		name: '', //题干数据
+		contents: [], // 选项数据
+	})
+
+	watch(() => props.question, (val) => formatData(val), {
+		immediate: true
+	})
+
+	function formatClass(index) {
+		if (props.showError) {
+			return {
+				active_right: props.question.result == index,
+				showError: props.question.reply == index && props.question.result != index
+			}
+		} else {
+			  if (props.question.reply === ''||props.question.reply === null ) {
+				return {
+				  active: false
+				}
+			  }
+			return {
+				active: props.question.reply == index
+			}
+		}
+	}
+
+	function formatData(val) {
+		if (val) {
+			data.name = val.name;
+			data.contents = val.content.map((item, index) => {
+				return {
+					label: item,
+					number: getLetterByIndex(index)
+				}
+			})
+		}
+	}
+
+	function onSelect(index) {
+		if (props.showError) {
+			return;
+		}
+		props.question.reply = index;
+	}
+</script>

+ 87 - 0
components/questions/duoxuan.vue

@@ -0,0 +1,87 @@
+<template>
+	<view v-if="question" class="phone-duoxuan-box">
+		<view class="phone-shiti-question">
+			<view class="question-num">{{question.onlyNum}}、</view>
+			<!-- 题干区域 -->
+			<rich-text :nodes="data.name"></rich-text>
+		</view>
+		<!-- 选项区域 -->
+		<view v-for="(item,index) in data.contents" class="duoxuan-option-box" :class="formatClass(index)" :key="index">
+			<text class="option-change" @click="onSelect(index)">{{item.number}}</text>
+			<rich-text :nodes="item.label" class="option-question"></rich-text>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref,
+		reactive,
+		watch
+	} from 'vue';
+	import {
+		useQuestionTools
+	} from "./useQuestionTools"
+	const {
+		getLetterByIndex,
+		haveSameElements
+	} = useQuestionTools();
+
+	const props = defineProps({
+		question: {
+			type: Object,
+		},
+		showError: {
+			type: Boolean,
+			default: false
+		}
+	})
+
+	const data = reactive({
+		name: '', //题干数据
+		contents: [], // 选项数据
+	})
+
+	watch(() => props.question, (val) => formatData(val), {
+		immediate: true
+	})
+
+
+	function formatClass(index) {
+		if (props.showError) {
+			return {
+				active_right: props.question.result.some(item => item == index),
+				showError: !props.question.result.some(item => item == index)
+			}
+		} else {
+			return {
+				active: props.question.reply.some(item => item == index)
+			}
+		}
+	}
+
+	function formatData(val) {
+		if (val) {
+			data.name = val.name;
+			data.contents = val.content.map((item, index) => {
+				return {
+					label: item,
+					number: getLetterByIndex(index)
+				}
+			})
+		}
+	}
+
+	function onSelect(index) {
+		if (props.showError) {
+			return;
+		}
+		if (props.question.reply) {
+			if (props.question.reply.some(item => item == index)) {
+				props.question.reply = props.question.reply.filter(item => item != index);
+			} else {
+				props.question.reply.push(index);
+			}
+		}
+	}
+</script>

+ 52 - 0
components/questions/panduan.vue

@@ -0,0 +1,52 @@
+<template>
+	<view class="phone-panduan-box">
+		<view class="phone-shiti-question">
+			<view class="question-num">{{question.onlyNum}}、</view>
+			<!-- 题干区域 -->
+			<rich-text :nodes="question.name"></rich-text>
+		</view>
+		<!-- 选项区域 -->
+		<radio-group @change="radioChange" class="panduan-option-box">
+			<label class="option-question" :class="formatClass('1')">
+				<radio value="1" :disabled="showError" :checked="question.reply == '1'"/>
+				<view>正确</view>
+			</label>
+			<label class="option-question" :class="formatClass('0')"> 
+				<radio value="0" :disabled="showError" :checked="question.reply == '0'"/>
+				<view>错误</view>
+			</label>
+		</radio-group>
+	</view>
+</template>
+
+<script setup>
+	const props = defineProps({
+		question: {
+			type: Object,
+		},
+		showError: {
+			type: Boolean,
+			default: false
+		}
+	})
+
+	function radioChange(e) {
+		if (props.showError) {
+			return;
+		}
+		props.question.reply = e.detail.value;
+	}
+	
+	function formatClass(index) {
+		if (props.showError) {
+			return {
+				active_right: props.question.result == index,
+				showError: props.question.reply == index && props.question.result != index
+			}
+		} else {
+			return {
+				active: props.question.reply == index
+			}
+		}
+	}
+</script>

+ 35 - 0
components/questions/tiankong.vue

@@ -0,0 +1,35 @@
+<template>
+	<view v-if="question" class="phone-tiankong-box">
+		<view class="phone-shiti-question">
+			<view class="question-num">{{question.onlyNum}}、</view>
+			<!-- 题干区域 -->
+			<rich-text :nodes="question.name"></rich-text>
+		</view>
+		<!-- 选项区域 -->
+		<view v-for="(item,index) in question.reply" class="tiankong-option-box" :key="index" :class="formatClass(index)">
+			<text class="option-question">填空{{index+1}}:</text>
+			<input type="text" v-model="question.reply[index]" class="option-question-text" :placeholder="`请输入填空${index+1}答案`">
+		</view>
+	</view>
+</template>
+
+<script setup>
+	const props = defineProps({
+		question: {
+			type: Object,
+		},
+		showError: {
+			type: Boolean,
+			default: false
+		}
+	})
+
+	function formatClass(index) {
+		if (props.showError) {
+			return {
+				active_right: props.question.result[index].some(item => item == props.question.reply[index]?props.question.reply[index].trim(): ''),
+				showError: !props.question.result[index].some(item => item == props.question.reply[index]?props.question.reply[index].trim(): '')
+			}
+		}
+	}
+</script>

+ 85 - 0
components/questions/useQuestionTools.js

@@ -0,0 +1,85 @@
+export function useQuestionTools() {
+	function getLetterByIndex(index) {
+		let letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+		if (index < 0 || index > 26) {
+			return '?';
+		}
+		return letters.charAt(index);
+	}
+
+	// 判断两个数组是否具有相同元素
+	function haveSameElements(arr1, arr2) {
+		// 如果两个数组的长度不同,它们不可能包含相同的元素
+		if (arr1.length !== arr2.length) {
+			return false;
+		}
+
+		// 对两个数组进行排序
+		arr1.sort((a, b) => a - b);
+		arr2.sort((a, b) => a - b);
+
+		// 比较排序后的数组是否相同
+		for (let i = 0; i < arr1.length; i++) {
+			if (arr1[i] != arr2[i]) {
+				return false;
+			}
+		}
+
+		// 如果所有元素都相同,返回 true
+		return true;
+	}
+
+	function checkDanxuanReply(item) {
+		if (item.reply === 0 || item.reply === '0') {
+			return true
+		}
+		if (!item.reply || item.reply === '' || item.reply === [] || item.reply.length === 0) {
+			return false;
+		} else {
+			return true;
+		}
+	}
+
+	function checkDuoxuanReply(item) {
+		if (!item.reply || item.reply === '' || item.reply === [] || item.reply.length === 0) {
+			return false;
+		} else {
+			for (const _item of item.reply) {
+				if (_item === '') {
+					return false;
+				}
+			}
+			return true;
+		}
+	}
+
+	function checkPanduanReply(item) {
+		if (!item.reply || item.reply === '' || item.reply === [] || item.reply.length === 0) {
+			return false;
+		} else {
+			return true;
+		}
+	}
+
+	function checkTiankongReply(item) {
+		if (!item.reply || item.reply === '' || item.reply === [] || item.reply.length === 0) {
+			return false;
+		} else {
+			for (const _item of item.reply) {
+				if (_item === '') {
+					return false;
+				}
+			}
+			return true;
+		}
+	}
+
+	return {
+		getLetterByIndex,
+		haveSameElements,
+		checkDanxuanReply,
+		checkDuoxuanReply,
+		checkPanduanReply,
+		checkTiankongReply
+	}
+}

+ 6 - 0
components/scroll-list-card/scroll-list-card.vue

@@ -103,6 +103,12 @@ const btns = computed(() => [
     show: data.value.status === 8,
   },
 ]);
+
+const Emits = defineEmits(['btnClick'])
+
+function btnClick(data) {
+	Emits('btnClick',data)
+}
 </script>
 
 <style lang="scss" scoped>

+ 57 - 0
components/zhuapaiConfirm/answerQueren.vue

@@ -0,0 +1,57 @@
+<template>
+	<view>
+		<uni-popup ref="popupRef" type="dialog" :animation="false" :is-mask-click="false"
+		mask-background-color="rgba(0, 0, 0, 0.4);">
+			<uni-popup-dialog mode="input"
+			class="phone-ksxz-dialog"
+				title="提示"
+				:duration="2000" 
+				:before-close="true"
+				:showClose="false"
+				@close="handleClose"
+				@confirm="handleConfirm">
+				<text>
+					您已经回答了{{data.answercartsCount}}题(共{{data.answercartsTotal}}题),确认交卷?
+				</text>
+			</uni-popup-dialog>
+		</uni-popup>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref,reactive
+	} from "vue";
+	const popupRef = ref(null)
+	const data = reactive({
+		answercartsCount: '',
+		answercartsTotal: '',
+
+	})
+	
+	const emits = defineEmits(['confirm', 'cancel'])
+	
+	function showDialog(options) {
+		data.answercartsCount = options.answercartsCount;
+		data.answercartsTotal = options.answercartsTotal;
+		popupRef.value.open()
+	}
+	
+	function handleClose() {
+		emits('cancel');
+		popupRef.value.close()
+	}
+	
+	function handleConfirm() {
+		emits('confirm', data);
+		popupRef.value.close()
+	}
+	
+	defineExpose({
+		showDialog
+	})
+	
+</script>
+
+<style>
+</style>

+ 137 - 0
components/zhuapaiConfirm/index.vue

@@ -0,0 +1,137 @@
+<template>
+	<uni-popup ref="popupRef" type="dialog" :animation="false" :is-mask-click="false"
+		mask-background-color="rgba(0, 0, 0, 0.4);">
+		<uni-popup-dialog mode="input" class="phone-camera my-dialog-cc" :title="title" :duration="2000" :before-close="true"
+			@close="handleClose" @confirm="handleConfirm">
+			<view class="phone-camera-box" style="overflow: hidden;">
+				<view v-show="!showConfirmBtn" class="sxt-tip-box" style="text-align: center;">{{textMess}}</view>
+				<video v-show="showConfirmBtn" ref="videoRef" style="width:100%; height: 320rpx;" id="videoConfirm"
+					:controls="false" :enable-progress-gesture="false"></video>
+					
+				<!-- 隐藏抓拍绘制图片 -->
+				<canvas id="canvasConfirm" style="width: auto; height: 320rpx;visibility: hidden;position: absolute;"></canvas>
+				<!-- <canvas id="canvasConfirm" style="width: auto; height: 320rpx;"></canvas> -->
+				<!-- 测试抓拍使用 -->
+				<!-- <button @click="handleZhua">抓拍</button> -->
+			</view>
+		</uni-popup-dialog>
+	</uni-popup>
+</template>
+
+<script setup>
+	import {
+		ref,
+		onUnmounted,
+		onMounted,
+		nextTick
+	} from "vue"
+	import {
+		useH5Camera
+	} from "./useCamera.js"
+	
+	defineProps({
+		title: {
+			type: String,
+			default: '摄像头确认'
+		}
+	})
+	
+	const emits = defineEmits(['success', 'error', 'cancel'])
+	
+	const popupRef = ref(null)
+	const showConfirmBtn = ref(false);
+	
+	let zhuapaiFun = null;
+	let stopCamera = null;
+	let errorFunCompleteFn = null;
+	const textMess = ref('摄像头正在初始化...')
+
+	function startCamera() {
+		// 请求摄像头权限并获取流
+		// #ifdef H5
+		console.log('navigator', navigator)
+		const {
+			startH5Camera,
+			handlePaiZhao,
+			stopH5Camera,
+			errorFunComplete
+		} = useH5Camera({
+			elVideoId: '#videoConfirm',
+			elCanvasId: '#canvasConfirm',
+			onVideoSuccess,
+			onVideoError
+		})
+		startH5Camera();
+		zhuapaiFun = handlePaiZhao;
+		stopCamera = stopH5Camera;
+		errorFunCompleteFn = errorFunComplete;
+		// #endif
+
+		// #ifdef APP-PLUS
+		console.log('App端暂时不支持摄像头功能')
+		// #endif
+	}
+
+	function handleZhua() {
+		zhuapaiFun()
+	}
+
+
+	function onVideoSuccess() {
+		showConfirmBtn.value = true;
+	}
+
+	function onVideoError(e) {
+		showConfirmBtn.false = true;
+		textMess.value = '摄像头初始化失败!'
+		emits('error')
+		errorFunCompleteFn && errorFunCompleteFn(e)
+	}
+
+	function showDialog() {
+		textMess.value = '摄像头正在初始化...'
+		popupRef.value.open();
+		nextTick(() => {
+			startCamera()
+		})
+	}
+
+	function handleClose() {
+		emits('cancel')
+		popupRef.value.close();
+		stopCamera && stopCamera()
+	}
+
+	function handleConfirm() {
+		emits('success')
+		popupRef.value.close();
+		stopCamera && stopCamera()
+	}
+
+	defineExpose({
+		showDialog
+	})
+
+	onUnmounted(() => {
+		// 组件销毁时停止摄像头
+		stopCamera && stopCamera();
+	})
+</script>
+
+<style lang="scss">
+	.phone-camera-box {
+		width: 100%;
+	}
+
+	.uni-video-container {
+		width: 100%;
+	}
+
+	.uni-video-video {
+		width: 100%;
+	}
+
+  .my-dialog-cc .uni-dialog-button-group .uni-border-left .uni-button-color{
+      color: #3fd2a1;
+  }
+</style>

+ 138 - 0
components/zhuapaiConfirm/qieping.vue

@@ -0,0 +1,138 @@
+<template>
+	<view></view>
+</template>
+
+<script setup>
+	import {
+		ref,
+		onUnmounted,
+		nextTick,
+		computed
+	} from "vue";
+	import {
+		getClientQiepingCheat,getClientQiepingTimes
+	} from "@/api/kaoshi.js"
+	import {
+		useZhuapaiStore
+	} from "@/store/zhuapai.js"
+
+	const zhuapaiStore = useZhuapaiStore();
+
+	const emits = defineEmits(['zhuapai', 'forceSubmit'])
+	const zhuapaiFlag = ref(false);
+	const zhuapaixiaoshi = ref(0);
+	const leaveTime = ref('');
+	const toggleScreenSecond = ref(0);
+	const toggleScreenFlag = ref(0);
+	const ksId = ref('')
+
+	function cheatingFun() {
+		// 用户离开了当前页面
+		if (document.visibilityState === 'hidden') {
+			console.log('页面不可见');
+			if (zhuapaiFlag.value) {
+				// 此时 切出后台 发生抓拍情况下 要传递固定图片
+				console.log('切出去zhuapaixiaoshi 恢复正常');
+				zhuapaixiaoshi.value = 1
+				zhuapaiStore.setStatus(1)
+			}
+			//  计时
+			leaveTime.value = new Date().getTime();
+		} else if (document.visibilityState === 'visible') {
+			console.log('切回来 恢复正常');
+			// 用户打开或回到页面
+			if (zhuapaiFlag.value) {
+				zhuapaixiaoshi.value = 0
+				zhuapaiStore.setStatus(0)
+				emits('zhuapai') // 重置抓拍
+			}
+			zhuapaixiaoshi.value = 0
+			zhuapaiStore.setStatus(0)
+			console.log('页面可见');
+			let nowTime = new Date().getTime();
+			if (Number(nowTime) - Number(leaveTime.value) > toggleScreenSecond.value + '000') {
+				let req = {
+					ksId: ksId.value,
+				};
+				getClientQiepingCheat(req).then(res => {
+					//cutScreenDialog   是否超限 true:超过限制 false:未超过限制 ,
+					if (res.code === 0 && res.data.flag) {
+						emits('forceSubmit') // 强制交卷
+					} else {
+						emits('qiepingToast', res.data.times) // 提示警告
+					}
+				});
+			}
+		}
+	}
+
+	function zhuapaiFun() {
+		if (document.visibilityState === 'hidden') {
+			console.log('页面不可见');
+			zhuapaixiaoshi.value = 1
+			zhuapaiStore.setStatus(1)
+		} else if (document.visibilityState === 'visible') {
+			zhuapaixiaoshi.value = 0
+			zhuapaiStore.setStatus(0)
+			emits('zhuapai')
+		}
+	}
+
+	function init(options) {
+		console.log('init', options)
+		toggleScreenFlag.value = options.toggleScreenFlag;
+		toggleScreenSecond.value = options.toggleScreenSecond;
+		zhuapaiFlag.value = options.zhuapaiFlag;
+		ksId.value = options.ksId;
+		// #ifdef H5
+		if (toggleScreenFlag.value !== 0) {
+			console.log("有切屏");
+			document.addEventListener('visibilitychange', cheatingFun, false);
+			cheatingNumberSearch();
+		}
+		if (zhuapaiFlag.value && toggleScreenFlag.value == 0) {
+			console.log("有抓拍 无切屏");
+			// 有抓拍 没有切屏  此方法是 解决切到后台,抓拍停留一帧的问题
+			document.addEventListener('visibilitychange', zhuapaiFun, false);
+		}
+		// #endif
+	}
+
+	function cheatingNumberSearch() {
+		let req = {
+			ksId: ksId.value,
+		};
+		getClientQiepingTimes(req).then(res => {
+			if (res.code === 0) {
+				if (res.data.times > 0 && res.data.times <= res.data.toggleScreenFlag) {
+					emits('qiepingToast', res.data.times) // 提示警告
+				} else if (res.data.times > 0 && res.data.times >= res.data.toggleScreenFlag) {
+					emits('forceSubmit') // 强制交卷
+				} 
+			}
+		});
+	}
+
+	function stopListen() {
+		if (toggleScreenFlag.value !== 0) {
+			console.log("有切屏 销毁");
+			document.removeEventListener('visibilitychange', cheatingFun, false);
+		}
+		if (zhuapaiFlag.value && toggleScreenFlag.value == 0) {
+			console.log("有抓拍 无切屏 销毁");
+			document.removeEventListener('visibilitychange', zhuapaiFun, false);
+		}
+	}
+
+	onUnmounted(() => {
+		// 组件销毁时移除监听
+		stopListen()
+	})
+
+	defineExpose({
+		init
+	})
+</script>
+
+<style>
+</style>

+ 107 - 0
components/zhuapaiConfirm/submitScore.vue

@@ -0,0 +1,107 @@
+<template>
+	<uni-popup ref="popupRef" :animation="false" :is-mask-click="false" type="bottom"
+		mask-background-color="rgba(0, 0, 0, 0.4);">
+		<view class="exam-score-dialog">
+			<view class="icon-title-bjcolor-navBar-box">
+				<view @click="handleClose" class="nav-bar-icon"></view>
+				<text class="nav-bar-title">{{title}}</text>
+			</view>
+
+			<view class="score-content-box">
+          <view class="score-content-name">{{data[nameKey]}}</view>
+          <view class="content-score-box">
+					<view>
+						<view class="score-content-text">正确数量:{{data.rightCount}}</view>
+						<view class="score-content-text">试题总数:{{data.shitiTotal}}</view>
+						<view class="score-content-text">及格分数:{{data.okScore}}</view>
+						<view class="score-content-text">{{labelName}}:{{data.ksScore}}</view>
+					</view>
+					<view class="score-content-text"><text>{{data.userScore}}</text>分</view>
+				</view>
+			</view>
+
+			<view class="progress-text-btn-box">
+				<text class="progress-text">正确率</text>
+				<c-progress-circle :progress='data.userAccuracy/100' color='#3fd2a1' size='240rpx'
+					boderWidth="240rpx" class="progress-circle-box"></c-progress-circle>
+				<button type="default" class="phone-green-btn score-dialog-btn" @click="handleCheckSj">查看成绩</button>
+			</view>
+		</view>
+	</uni-popup>
+</template>
+
+<script setup>
+	import {
+		ref,
+		reactive
+	} from "vue";
+	const popupRef = ref(null)
+	const data = reactive({
+		ksName: '',
+    lxName: '',
+		ksScore: '',
+		okScore: '',
+		rightCount: '',
+		shitiTotal: '',
+		userScore: '',
+	})
+
+	defineProps({
+		title: {
+			type: String,
+			default: '考试得分'
+		},
+		labelName: {
+			type: String,
+			default: '考试总分'
+		},
+    nameKey: {
+      type: String,
+      default: 'ksName'
+    }
+	})
+
+	const emits = defineEmits(['confirm', 'close'])
+
+	function handleClose() {
+		emits('close');
+		popupRef.value.close()
+	}
+
+	function handleCheckSj() {
+		emits('confirm', data);
+		popupRef.value.close()
+	}
+
+	function showDialog(options) {
+		data.ksName = options.ksName;
+    data.lxName = options.lxName;
+		data.ksScore = options.ksScore;
+		data.okScore = options.okScore;
+		data.rightCount = options.rightCount;
+		data.shitiTotal = options.shitiTotal;
+		data.userScore = options.userScore;
+		data.userAccuracy = options.userAccuracy;
+		popupRef.value.open()
+	}
+
+	defineExpose({
+		showDialog
+	})
+</script>
+
+<style lang="scss">
+	 .content{
+	        display: flex;
+	        flex-direction: column;
+	        align-items: center;
+	        padding-top: 20rpx;
+	        font-size: 28rpx;
+	    }
+	    .btnBox{
+	        width: 100%;
+	        display: flex;
+	        align-items: center;
+	        margin-top:30rpx;
+	    }
+</style>

+ 512 - 0
components/zhuapaiConfirm/useCamera.js

@@ -0,0 +1,512 @@
+const errorMessage = {
+	ms1: '请使用微信、Chrome、Firefox或Safari浏览器,如果浏览器没有问题,请联系管理员',
+	ms2: '请使用微信、Chrome、Firefox或Safari',
+	ms3: 'navigator对象无媒体属性', // 错误提示同ms2
+	ms4: '未找到摄像头,请确认摄像头是否正常以及当前摄像头是否被禁用',
+	ms5: '检测到当前摄像头已被占用,请关闭摄像头后重新尝试',
+	ms6: '摄像头硬件无法满足使用要求,请更换摄像头后重新尝试',
+	ms7: '请开启浏览器摄像头权限',
+	ms8: '未获取摄像头数据,请检测摄像头是否正常',
+	ms9: '当前浏览器不支持,请更换浏览器后尝试',
+	ms10: '当前Android系统版本低于9!请更新操作系统',
+	ms11: '当前IOS系统版本低于14.3!请更新操作系统',
+	ms12: '推荐使用safari浏览器或微信,使用其他浏览器可能会在考试过程中出现摄像头问题,影响考试结果,导致重考,不建议使用其他浏览器。',
+	ms13: '推荐使用火狐浏览器、谷歌浏览器或微信,使用其他浏览器可能会在考试过程中出现摄像头问题,影响考试结果,导致重考,不建议使用其他浏览器',
+};
+
+export {
+	errorMessage
+};
+
+
+function checkPlatform2() {
+	const ua = navigator.userAgent.toLowerCase();
+	// 安卓系统
+	if (/android/i.test(navigator.userAgent)) {
+		let test = /android\s([\w.]+)/;
+		let match = test.exec(ua);
+		let version = match[1].split('.')[0];
+		if (version < 9) {
+			uni.showToast({
+				icon: 'none',
+				title: errorMessage.ms10
+			})
+			
+			return {
+				data: false,
+				waitCode: false
+			};
+		}
+		// 判断 浏览器 Android
+		if (!checkAndroidForBrowser()) {
+			// 提示信息
+			uni.showToast({
+				icon: 'none',
+				title: errorMessage.ms13
+			})
+			
+			return {
+				data: true,
+				waitCode: true
+			};
+		}
+		return {
+			data: true,
+			waitCode: false
+		};
+	}
+	// ios 系统
+	if (/(iphone | ipad | ipod | iOS)/i.test(navigator.userAgent)) {
+		let test = /os\s([\w]+)/;
+		let match = test.exec(ua);
+		let vs = match[1].split('_');
+		let version = '';
+
+		if (vs.length > 2) {
+			version = `${vs[0]}.${vs[1]}`;
+		} else if (vs.length == 2) {
+			version = `${vs[0]}.${vs[1]}`;
+		} else {
+			version = `${vs[0]}.0`;
+		}
+
+		if (version < 14.3) {
+			uni.showToast({
+				icon: 'none',
+				title: errorMessage.ms11
+			})
+			return {
+				data: false,
+				waitCode: false,
+				duration: 3000
+			};
+		}
+
+		// 判断浏览器兼容 判断 ios 浏览器提示信息
+		// 判断 浏览器 Android
+		if (!checkIosForBrowser()) {
+			// 提示信息
+			uni.showToast({
+				icon: 'none',
+				title: errorMessage.ms12,
+				duration: 5000
+			})
+			return {
+				data: true,
+				waitCode: true
+			};
+		}
+
+		return {
+			data: true,
+			waitCode: false
+		};
+	}
+}
+
+function checkIosForBrowser() {
+	let u = navigator.userAgent;
+	let result = false;
+	let curname = getBrowser();
+	// 如果是 苹果
+	if (curname === 'safari') {
+		result = true;
+	}
+	// 如果是 微信
+	if (u.indexOf('MicroMessenger') > -1) {
+		result = true;
+	}
+	return result;
+}
+
+function checkAndroidForBrowser() {
+	let curname = getBrowser();
+	let result = false;
+	// 如果是 谷歌
+	if (curname === 'chrome') {
+		result = true;
+	}
+	if (curname === 'firefox') {
+		result = true;
+	}
+	// 如果是 微信
+	if (curname === 'wechat') {
+		result = true;
+	}
+	return result;
+}
+
+function getBrowser() {
+	var u = navigator.userAgent;
+
+	var bws = [{
+		name: 'sgssapp',
+		it: /sogousearch/i.test(u),
+	}, {
+		name: 'wechat',
+		it: /MicroMessenger/i.test(u),
+	}, {
+		name: 'weibo',
+		it: !!u.match(/Weibo/i),
+	}, {
+		name: 'uc',
+		it: !!u.match(/UCBrowser/i) || u.indexOf(' UBrowser') > -1,
+	}, {
+		name: 'sogou',
+		it: u.indexOf('MetaSr') > -1 || u.indexOf('Sogou') > -1,
+	}, {
+		name: 'xiaomi',
+		it: u.indexOf('MiuiBrowser') > -1,
+	}, {
+		name: 'baidu',
+		it: u.indexOf('Baidu') > -1 || u.indexOf('BIDUBrowser') > -1,
+	}, {
+		name: '360',
+		it: u.indexOf('360EE') > -1 || u.indexOf('360SE') > -1,
+	}, {
+		name: '2345',
+		it: u.indexOf('2345Explorer') > -1,
+	}, {
+		name: 'edge',
+		it: u.indexOf('Edge') > -1,
+	}, {
+		name: 'ie11',
+		it: u.indexOf('Trident') > -1 && u.indexOf('rv:11.0') > -1,
+	}, {
+		name: 'ie',
+		it: u.indexOf('compatible') > -1 && u.indexOf('MSIE') > -1,
+	}, {
+		name: 'firefox',
+		it: u.indexOf('Firefox') > -1,
+	}, {
+		name: 'safari',
+		it: u.indexOf('Safari') > -1 && u.indexOf('Chrome') === -1 && u.indexOf('(KHTML, like Gecko) Version') >
+			-1 && u.indexOf('MQQBrowser') === -1 && u.indexOf('FingerBrowser') === -1,
+	}, {
+		name: 'qqbrowser',
+		it: u.indexOf('MQQBrowser') > -1 && u.indexOf(' QQ') === -1,
+	}, {
+		name: 'qq',
+		it: u.indexOf('QQ') > -1,
+	}, {
+		name: 'chrome',
+		it: u.indexOf('(KHTML, like Gecko) Chrome') > -1 && u.indexOf('MiuiBrowser') === -1 && u.indexOf(
+			'UCBrowser') === -1 && u.indexOf('HarmonyOS') === -1 && u.indexOf('HuaweiBrowser') === -1,
+	}, {
+		name: 'opera',
+		it: u.indexOf('Opera') > -1 || u.indexOf('OPR') > -1,
+	}, {
+		name: 'wechat',
+		it: /MicroMessenger/i.test(u)
+	}, ];
+
+	for (var i = 0; i < bws.length; i++) {
+		if (bws[i].it) {
+			return bws[i].name;
+		}
+	}
+
+	return 'other';
+}
+
+export function errorFunComplete(e) {
+	const name = e.name;
+	if (name === 'NotFoundError' || name === 'DevicesNotFoundError') {
+		uni.showToast({
+			icon: 'none',
+			title: errorMessage.ms4,
+		})
+	}
+	if (name === 'NotReadableError' || name === 'TrackStartError') {
+		uni.showToast({
+			icon: 'none',
+			title: errorMessage.ms5,
+		})
+	} else if (name === 'OverconstrainedError' || name === 'ConstraintNotSatisfiedError') {
+		uni.showToast({
+			icon: 'none',
+			title: errorMessage.ms6,
+		})
+	} else if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {
+		uni.showToast({
+			icon: 'none',
+			title: errorMessage.ms7,
+		})
+	} else if (name === 'TypeError' || name === 'TypeError') {
+		uni.showToast({
+			icon: 'none',
+			title: errorMessage.ms8,
+		})
+	} else {
+		uni.showToast({
+			icon: 'none',
+			title: errorMessage.ms9,
+		})
+	}
+}
+
+let constraints = {
+	audio: false,
+	video: {
+		width: 480,
+		height: 320,
+		sourceId: 'default',
+		deviceId: 'default',
+		transform: 'rotate(180deg)',
+		facingMode: {
+			exact: 'user'
+		},
+	},
+};
+
+// 校验权限
+export function check(success, error, backFun) {
+
+	if (!navigator) {
+		uni.showToast({
+			icon: 'none',
+			title: errorMessage.ms1,
+			duration: 5000
+		})
+		// 当前浏览器版本 无媒体对象
+		backFun && backFun();
+		return false;
+	}
+
+	const resoutData = checkPlatform2();
+	const timeD = resoutData.waitCode ? 5000 : 0;
+	if (!resoutData.data) {
+		backFun && backFun();
+		return false;
+	}
+
+	setTimeout(() => {
+		if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+			//最新的标准API 返回promise
+			navigator.mediaDevices.getUserMedia(constraints).then(success).catch(error);
+		} else if (navigator.webkitGetUserMedia) {
+			//webkit核心浏览器
+			navigator.webkitGetUserMedia(constraints, success, error);
+
+		} else if (navigator.mozGetUserMedia) {
+			//firfox浏览器
+			navigator.mozGetUserMedia(constraints, success, error);
+
+		} else if (navigator.getUserMedia) {
+			//旧版API
+			navigator.getUserMedia(constraints, success, error);
+		} else {
+			uni.showToast({
+				icon: 'none',
+				title: errorMessage.ms2,
+			})
+			backFun && backFun();
+		}
+	}, timeD);
+	// 没有媒体对象
+	// Toast.fail(errorMessage.ms2);
+	// 1. ios 14.3版本一下
+	// 2. 当前浏览器非完整版
+}
+
+// 抓拍确认
+export function check2(success, error) {
+
+
+	if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+		//最新的标准API 返回promise
+		navigator.mediaDevices.getUserMedia(constraints).then(success).catch(error);
+	} else if (navigator.webkitGetUserMedia) {
+		//webkit核心浏览器
+		navigator.webkitGetUserMedia(constraints, success, error);
+
+	} else if (navigator.mozGetUserMedia) {
+		//firfox浏览器
+		navigator.mozGetUserMedia(constraints, success, error);
+	} else if (navigator.getUserMedia) {
+		//旧版API
+		navigator.getUserMedia(constraints, success, error);
+	}
+}
+
+import {
+	ref,
+	nextTick
+} from "vue"
+import {useZhuapaiStore} from "@/store/zhuapai.js"
+import {getFileUpload} from "@/api/kaoshi.js"
+
+// H5 播放抓拍功能
+export function useH5Camera({
+	elVideoId,
+	elCanvasId,
+	onVideoSuccess, // 成功播放回调
+	onVideoError, // 失败回调
+	zhuapaiHttp, // 抓拍接口将base64 上传
+	operId
+}) {
+
+	const zhuapaiStore = useZhuapaiStore();
+
+
+	const videoRef = ref('');
+	videoRef.value = document.querySelector(`${elVideoId} .uni-video-video`);
+
+	function videoSuccessFun(MediaStream) {
+		// 赋值流
+		videoRef.value.srcObject = MediaStream;
+		// 设置video监听 为了确实获取视频播放判断有视频数据流
+		addVideoListener();
+		// 播放video 执行播放操作
+		playVideo();
+	}
+
+	function addVideoListener() {
+		videoRef && videoRef.value.addEventListener('play', onVideoPlay);
+	}
+
+	function removeVideoListener() {
+		videoRef && videoRef.value.removeEventListener('play', onVideoPlay);
+	}
+
+	function playVideo() {
+		if (videoRef.value) {
+			videoRef.value.play();
+		}
+	}
+
+	function onVideoPlay() {
+		onVideoSuccess && onVideoSuccess();
+	}
+
+	function videoErrorFun(e) {
+		console.log('错误', e)
+		removeVideoListener();
+		onVideoError && onVideoError(e)
+	}
+
+	// 主动开启
+	function startH5Camera() {
+		check2(videoSuccessFun, videoErrorFun);
+	}
+
+	// 主动关闭
+	function stopH5Camera() {
+		// 重置虚拟流
+		const stream = videoRef && videoRef.value.srcObject;
+		if (!stream) {
+			return;
+		}
+
+		const tracks = stream.getTracks();
+		tracks.forEach(function(track) {
+			track.stop();
+		});
+		// 销毁视频资源
+		videoRef.value.srcObject = null;
+		// 移除监听
+		removeVideoListener && removeVideoListener();
+	}
+
+	function handlePaiZhao() {
+		try {
+			const streamActive = videoRef.value.srcObject.active;
+			// 判断视频流 是否运行
+			if (!streamActive) {
+				onVideoError && onVideoError(new Error('摄像头抓拍异常'))
+				return;
+			}
+		} catch (e) {
+			onVideoError && onVideoError(new Error('摄像头抓拍异常'))
+			return;
+		}
+
+		try {
+			if (zhuapaiStore.status == 0) {
+				// status 为 如果抓拍过程中 应用进入后台 传递固定图片 0为正常 1为进入后台
+				let canvas = document.querySelector(`${elCanvasId} .uni-canvas-canvas`);
+				let context = canvas.getContext('2d');
+				context.drawImage(videoRef.value, 0, 0, videoRef.value.clientWidth, videoRef.value.clientHeight);
+				const ImageFile = context.canvas.toDataURL('image/png');
+				getSnapShotImage(ImageFile);
+			} else {
+				 const ImageFile = getBase64Image()
+				getSnapShotImage(ImageFile);
+			}
+		} catch (err) {
+			console.error('源 :绘图失败', err);
+		}
+	}
+
+	function getBase64Image() {
+		var canvas = document.createElement("canvas");
+		var ctx = canvas.getContext("2d");
+		canvas.width = 480;
+		canvas.height = 320;
+		ctx.drawImage(document.querySelector('#gdImg'), 0, 0, 480, 320);
+		let image = new Image();
+		image.src = canvas.toDataURL('image/png', 1)
+		setTimeout(res => {
+			ctx.clearRect(0, 0, 480, 320)
+		}, 500)
+		return image.src
+	}
+
+	function getSnapShotImage(data) {
+		// console.log('base64', data)
+		const imgData = data.split(';base64,');
+		if (!imgData.length) {
+			console.error('【源 :拍照数据异常,未找到图片二进制数据分割节点: `;base64,`】');
+			return;
+		}
+		const opt = {
+			data: imgData[1],
+			prefix: 'kaoshi/zhuapai',
+			suffix: 'png',
+			
+		};
+
+		console.log('optoptopt', opt)
+		
+		getFileUpload(opt).then(res => {
+			const dOption = {
+				operId,
+				url: res.data
+			}
+			zhuapaiHttp && zhuapaiHttp(dOption)
+				.then(res => {
+					console.log('【源 : 获取抓拍数据】');
+				})
+				.catch(err => {
+					console.error('源 :抓拍接口异常', err);
+					uni.showToast({
+						icon: 'none',
+						title: '抓拍图片异常!'
+					})
+					uni.redirectTo({
+						url: "/pages/client/Kaoshi/list"
+					})
+				});
+		}).catch(err => {
+			uni.showToast({
+				icon: 'none',
+				title: '当前网络可能存在异常,请稍后重试,如持续异常,请联系管理员。注:若异常未联系管理员,可能会影响考试结果。'
+			})
+			uni.redirectTo({
+				url: "/pages/client/Kaoshi/list"
+			})
+		})
+		
+		
+	}
+
+	return {
+		startH5Camera,
+		stopH5Camera,
+		handlePaiZhao,
+		errorFunComplete,
+		playVideo
+	}
+}
+
+

+ 296 - 0
components/zhuapaiConfirm/zhuapai.vue

@@ -0,0 +1,296 @@
+<template>
+	<view class="zhuapai-drop-container" id="Drop" ref="DropRef" :style="style" @touchmove="touchmove($event)"
+		@touchstart="touchstart($event)">
+		<view class="phone-camera-box-zhuapai">
+			<video ref="videoRef" class="video-view-box" :class="myClass" id="videoZhaPai" :controls="false"></video>
+			<!-- 隐藏抓拍绘制图片 -->
+			<canvas id="canvasZhuaPai" class="video-view-box canvas-view-box" :class="myClass"></canvas>
+			<!-- 用于抓拍切出去传递固定img-->
+			<!-- #ifdef H5 -->
+			<img :src="imgUrl" alt="" id="gdImg" v-show="false">
+			<!-- #endif -->
+			<!-- 测试抓拍使用 -->
+			<!-- <button @click="handleZhua">抓拍</button> -->
+		</view>
+		<span v-show="showVideo" @click="noShowVideoBtn" class="shiti-video-hidden-btn"><i></i></span>
+		<span v-show="!showVideo" @click="showVideoBtn" class="shiti-video-show-btn"><i></i></span>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref,
+		onUnmounted,
+		nextTick,
+		computed
+	} from "vue";
+	import {
+		useH5Camera
+	} from "@/components/zhuapaiConfirm/useCamera";
+	import * as ksApi from "@/api/kaoshi.js"
+	import {
+		getStaticUrl
+	} from "@/utils/common.js"
+
+	const imgUrl = getStaticUrl('static/images/exam/nokaoshi.png')
+
+	let zhuapaiFun = null;
+	let stopCamera = null;
+	let playVideoFun = null;
+
+	const DropRef = ref(null);
+	const DropContainerRef = ref(null);
+	const zhuapai = ref(0); // 单位分
+	const operId = ref(null); // 单位分
+	const disX = ref(0); // 移动x
+	const disY = ref(0); // 移动y
+	const showVideo = ref(true);
+	const isBuffer = ref(false);
+
+	const stopTimer = ref(null);
+	const style = ref({
+		top: "10vh",
+		right: "0",
+	});
+
+	const myClass = computed(() => {
+		return {
+			'show-video': showVideo.value,
+			'hidden-video': !showVideo.value
+		}
+	})
+
+	const emits = defineEmits(['init', 'success', 'error', 'cancel', 'progress'])
+
+	function noShowVideoBtn() {
+		showVideo.value = false
+	}
+
+	function showVideoBtn() {
+		showVideo.value = true;
+		playVideoFun && playVideoFun();
+	}
+
+	function touchmove(event) {
+		// 2,获取手指移动的实时位置  需要减去位置差
+		let moveX = event.touches[0].pageX - disX.value;
+		let moveY = event.touches[0].pageY - disY.value;
+
+		const systemInfo = uni.getSystemInfoSync();
+		const windowHeight = systemInfo.windowHeight; // 可视区域高度 ‌:ml-citation{ref="1,3" data="citationList"}  
+		const windowWidth = systemInfo.windowWidth; // 可视区域高度 ‌:ml-citation{ref="1,3" data="citationList"}  
+
+
+		// 3,获取容器的宽高和拖动元素的宽高  每一次移动都会获取一次 ,建议放在外面进行获取
+		let dragHeight = DropRef.value.$el.offsetHeight;
+		let dragWidth = DropRef.value.$el.offsetWidth;
+
+		// 4,控制范围:在元素 被拖拽的过程中 判断 元素的定位值 是否到达边界 如果到了 就不能在走了
+		if (moveX <= 0) {
+			moveX = 0;
+		}
+		// 上边界
+		if (moveY <= 0) {
+			moveY = 0;
+		}
+		//下边界  容器高度 - 拖动元素高度
+		if (moveY >= windowHeight - dragHeight - 150) {
+			moveY = windowHeight - dragHeight - 150;
+		}
+		//右边界   容器宽度 - 拖动元素宽度
+		if (moveX >= windowWidth - dragWidth) {
+			moveX = 0;
+		}
+
+		// 5,开始移动
+		style.value.top = moveY + "px";
+
+	}
+
+	function touchstart(event) {
+		disX.value = event.touches[0].pageX - DropRef.value.$el.offsetLeft;
+		disY.value = event.touches[0].pageY - DropRef.value.$el.offsetTop;
+	}
+
+
+	function init(options) {
+		zhuapai.value = options.zhuapai;
+		operId.value = options.operId;
+		if (zhuapai.value > 0) {
+			// 启动摄像头
+			nextTick(() => {
+				startCamera()
+				// 设定计时器
+				setInterval(() => handleZhua(), zhuapai.value * 60 * 1000)
+			})
+
+		}
+	}
+
+	function startCamera() {
+		// 请求摄像头权限并获取流
+		// #ifdef H5
+		console.log('navigator', navigator)
+		const {
+			startH5Camera,
+			handlePaiZhao,
+			stopH5Camera,
+			playVideo
+		} = useH5Camera({
+			elVideoId: '#videoZhaPai',
+			elCanvasId: '#canvasZhuaPai',
+			onVideoSuccess,
+			onVideoError,
+			zhuapaiHttp: ksApi.getClientZhuaPaiUpdate,
+			operId: operId.value
+		})
+		startH5Camera();
+		zhuapaiFun = handlePaiZhao;
+		stopCamera = stopH5Camera;
+		playVideoFun = playVideo;
+		// #endif
+
+	}
+
+	function handleZhua() {
+		zhuapaiFun && zhuapaiFun()
+	}
+
+	function onVideoSuccess() {
+		setTimeout(() => {
+			// 首次运行进行抓拍一次
+			handleZhua();
+		}, 3000);
+		addVideoListener();
+	}
+
+	function onVideoError() {
+		emits('error')
+		removeVideoListener()
+	}
+
+	// 针对视频通话的监听处理
+
+	function onTimeupdate() {
+		if (isBuffer.value) {
+			console.log('buffer')
+			return;
+		}
+		if (!stopTimer.value) {
+			return;
+		}
+		console.log('onTimeupdate')
+		clearTimeout(stopTimer.value);
+		stopTimer.value = null;
+	}
+
+	function onProgress() {
+		if (stopTimer.value) {
+			return;
+		}
+		isBuffer.value = true;
+		console.log('onProgress')
+		// buffer时间增大到3秒 过滤掉后续的onTimeupdate
+		setTimeout(() => {isBuffer.value = false}, 3000)
+		// 视频中途暂停被占用
+		stopTimer.value = setTimeout(() => {
+			emits('progress', false);
+			console.log('结束')
+		}, 10 * 1000)
+	}
+   function addVideoListener() {
+		let video = document.querySelector(`#videoZhaPai .uni-video-video`)
+		// 判定有流
+		video.addEventListener('progress', onProgress);
+		video.addEventListener('timeupdate', onTimeupdate);
+	}
+     function removeVideoListener() {
+		let video =document.querySelector(`#videoZhaPai .uni-video-video`)
+		video && video.removeEventListener('progress', onProgress);
+		video && video.removeEventListener('timeupdate', onTimeupdate);
+	}
+
+
+	onUnmounted(() => {
+		// 组件销毁时停止摄像头
+		stopCamera && stopCamera();
+		removeVideoListener();
+	})
+
+	defineExpose({
+		init,
+		showVideoBtn
+	})
+</script>
+
+<style lang="scss">
+	.zhuapai-drop-container {
+		width: 180rpx;
+		height: 400rpx;
+		margin: 0;
+		padding: 0;
+		z-index: 10;
+		position: absolute;
+		overflow: hidden;
+
+		.phone-camera-box-zhuapai {
+			width: 100%;
+			height: 240rpx;
+			position: absolute;
+			overflow: hidden;
+
+			.uni-video-container {
+				background-color: transparent;
+				pointer-events: none;
+			}
+
+			.canvas-view-box,
+			.hidden-video {
+				transform: translateY(10000rpx);
+			}
+		}
+
+		.video-view-box {
+			width: 100%;
+			height: 240rpx;
+			position: absolute;
+		}
+
+		.shiti-video-hidden-btn,
+		.shiti-video-show-btn {
+			position: absolute;
+			top: 0;
+
+			i {
+				width: 32rpx;
+				height: 32rpx;
+				display: block;
+				background-size: cover;
+				background-repeat: no-repeat;
+				background-position: center;
+			}
+		}
+
+		.shiti-video-hidden-btn {
+			width: 60rpx;
+			height: 60rpx;
+			left: 0;
+
+			i {
+				background-image: url("@/static/images/exam/video-close-icon.svg");
+				margin: 6rpx auto 6rpx 6rpx;
+			}
+		}
+
+		.shiti-video-show-btn {
+			background-color: #dcfbf1;
+			padding: 20rpx;
+			border-radius: 8rpx;
+			right: 0;
+
+			i {
+				background-image: url("@/static/images/exam/video-play-icon.svg");
+			}
+		}
+	}
+</style>

+ 7 - 0
pages.json

@@ -60,6 +60,13 @@
 			{
 				"navigationBarTitleText" : "成绩"
 			}
+		},
+		{
+			"path" : "pages/exam/exam",
+			"style" : 
+			{
+				"navigationBarTitleText" : ""
+			}
 		}
 	],
 	"tabBar": {

+ 721 - 0
pages/exam/exam.vue

@@ -0,0 +1,721 @@
+<template>
+	<view class="phone-kaoshi-page">
+		<!-- 导航区域 -->
+		<view class="icon-title-bjcolor-navBar-box">
+			<view @click="handleBack" class="nav-bar-icon"></view>
+			<text class="nav-bar-title">{{data.ksName}}</text>
+		</view>
+		<!-- 第一行 -->
+		<view class="kaoshi-page-title">
+			<view v-if="activeSt" class="title-types">{{dlName}}</view>
+			<!--  倒计时 -->
+			<view v-if="!!data.endSecond">
+				<uni-countdown :show-day="false" :showHour="true" :showMinute="true" :second="data.endSecond" @timeup="onTimeUp"
+					:start="startCountDown"></uni-countdown>
+			</view>
+		</view>
+
+
+		<view class="kaoshi-shiti-content">
+			<!-- 内容区域 -->
+			<!-- 试题区域 -->
+			<view v-if="activeSt">
+				<template v-if="activeSt.stTypeId == 1">
+					<!-- 单选 -->
+					<danxuan :question="activeSt" :key="activeSt.stId"></danxuan>
+				</template>
+				<template v-if="activeSt.stTypeId == 2">
+					<!-- 多选 -->
+					<duoxuan :question="activeSt" :key="activeSt.stId"></duoxuan>
+				</template>
+				<template v-if="activeSt.stTypeId == 3">
+					<!-- 判断 -->
+					<panduan :question="activeSt" :key="activeSt.stId"></panduan>
+				</template>
+				<template v-if="activeSt.stTypeId == 4">
+					<!-- 填空 -->
+					<tiankong :question="activeSt" :key="activeSt.stId"></tiankong>
+				</template>
+			</view>
+
+		</view>
+
+		<view class="kaoshi-bottom-box">
+			<button class="phone-green-btn bj-btn" hover-class="none" type="default" size="mini"
+				@click="handleBiaoji">{{activeSt && activeSt.marked ? '取标':'标记'}}</button>
+			<view @click="showAnswerCard" class="shiti-num-box">
+				<icon class="shiti-num-icon"></icon>
+				<text
+					class="active-num">{{activeSt ? activeSt.onlyNum: 0}}</text>/<text>{{data.StListForSearch.length}}</text>
+			</view>
+			<button class="phone-green-btn save-btn" hover-class="none" type="default" size="mini"
+				@click="handleSave(true)">保存</button>
+		</view>
+		<template v-if="activeSt">
+			<button type="default" size="mini" hover-class="none" class="phone-green-btn ks-btn-prev"
+				@click="handlePrev" v-if="!isFistStId">上一题</button>
+			<button type="default" size="mini" hover-class="none" class="phone-green-btn ks-btn-next"
+				@click="handleNext" v-if="!isLastStId">下一题</button>
+			<button type="default" size="mini" hover-class="none" class="phone-green-btn ks-btn-next"
+				@click="handleJiaojuan" v-if="isLastStId">交卷</button>
+		</template>
+
+		<!-- 答题卡 -->
+		<uni-popup ref="popupRef" background-color="#fff" :animation="false" :is-mask-click="false" :mask-click="false">
+			<view class="answer-card-popup">
+				<view class="icon-title-bjcolor-navBar-box">
+					<view @click="handlePopupBack" class="nav-bar-icon"> </view>
+					<text class="nav-bar-title">答题卡</text>
+				</view>
+				<view class="card-content-box">
+					<view class="answer-card-content" v-for="(paragraph,paragraphIndex) in questionData"
+						:key="paragraphIndex">
+						<view class="paragraph-title">
+							{{paragraph.name}}
+						</view>
+						<view class="paragraph-qa" v-for="(qa,qaIndex) in paragraph.qas" :key="qaIndex"
+							:class="getQaClass(qa)" @click="answerCardItemClick(qa)">{{qa.onlyNum}}
+						</view>
+					</view>
+				</view>
+			</view>
+		</uni-popup>
+		<!-- 摄像头确认 -->
+		<zhuapaiConfirm ref="zhuapaiConfirmRef" @success="zpConfirmSuccess" @error="zpConfirmError" 
+			@cancel="zpConfirmCancel" key="1"></zhuapaiConfirm>
+		<template v-if="data.zhuapai && data.zhuapai > 0">
+			<!-- 抓拍 -->
+			<zhuapaiVue ref="zhuapaiRef" @error="zpError" @success="zpSuccess" key="2" @progress="onProgress"></zhuapaiVue>
+		</template>
+	
+		<!-- 切屏 -->
+		<qiepingVue ref="qiepingRef" @zhuapai="qpZhuapai" @forceSubmit="forceSubmit" @qiepingToast="qiepingToast"
+			key="3"></qiepingVue>
+		<!-- 交卷确认 -->
+		<answerQueren ref="answerQrRef" @confirm="handleQuerenConfirm"></answerQueren>
+		<!-- 考试得分 -->
+		<submitScoreVue ref="subScoreRef" @confirm="handleScoreConfirm" @close="handleScoreClose"></submitScoreVue>
+		
+		<!-- 切屏确认弹窗 -->
+		<qiepingDlVue ref="qiepingDlRef" :content="messageContent" title="注意" okBtn="知道了"></qiepingDlVue>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref,
+		reactive,
+		computed,
+		watch,
+		nextTick
+	} from "vue";
+	import zhuapaiVue from "@/components/zhuapaiConfirm/zhuapai.vue";
+	import qiepingVue from "@/components/zhuapaiConfirm/qieping.vue";
+	import zhuapaiConfirm from "@/components/zhuapaiConfirm/index.vue";
+	import answerQueren from "@/components/zhuapaiConfirm/answerQueren.vue";
+	import submitScoreVue from "@/components/zhuapaiConfirm/submitScore.vue";
+	import {
+		onLoad
+	} from "@dcloudio/uni-app";
+	import * as ksApi from "@/api/exam.js"
+	import danxuan from "@/components/questions/danxuan.vue";
+	import duoxuan from "@/components/questions/duoxuan.vue";
+	import tiankong from "@/components/questions/tiankong.vue";
+	import panduan from "@/components/questions/panduan.vue";
+	import {
+		useQuestionTools
+	} from "@/components/questions/useQuestionTools.js";
+	import {
+		useKaoShiCache
+	} from "./examTools"
+	import qiepingDlVue from "@/components/dialog/qiepingDl.vue";
+
+	const {
+		checkDanxuanReply,
+		checkDuoxuanReply,
+		checkPanduanReply,
+		checkTiankongReply,
+		getLetterByIndex
+	} = useQuestionTools();
+	const {
+		saveCacheKs,
+		getCacheKs,
+		removeCacheKs
+	} = useKaoShiCache();
+
+	onLoad((option) => {
+		data.ksId = option.ksId;
+		data.zhuapai = option.zhuapai;
+		data.userKaozhengId = option.userKaozhengId;
+		data.from = option.from;
+		if (data.zhuapai && data.zhuapai != 0) {
+			// 考试前确认摄像头
+			nextTick(() => {
+				initBeforKaoshi();
+			})
+		} else {
+			initKaoshi();
+		}
+	})
+
+
+	const popupRef = ref(null)
+	const zhuapaiRef = ref(null)
+	const qiepingRef = ref(null)
+	const zhuapaiConfirmRef = ref(null)
+	const answerQrRef = ref(null);
+	const startCountDown = ref(false);
+	const subScoreRef = ref(null);
+	const messageContent = ref('');
+	const qiepingDlRef = ref(null);
+	
+	const timer1 = ref(null);
+
+	const data = reactive({
+		ksId: null,
+		operId: null,
+		ksName: '',
+		stTotal: 0,
+		stScore: 0,
+		biaoji: {},
+		endSecond: 0,
+		pageSize: 0,
+		toggleScreenFlag: 0,
+		toggleScreenSecond: 0,
+		zhuapai: 0,
+		duanluo: [],
+		StListForSearch: [],
+		from: '',
+		hisId: '',
+		userKaozhengId: ''
+	})
+	
+	const markDB = ref([]);
+
+	const questionData = ref([]);
+
+	const progress = reactive({
+		dlIndex: 0,
+		dtIndex: 0
+	})
+
+	const dlName = computed(() => {
+		if (data.StListForSearch && activeSt.value) {
+			return data.StListForSearch[activeSt.value.onlyNum - 1].paragraphName
+		} else {
+			return ''
+		}
+	})
+
+	watch(() => data.duanluo, (newVal) => {
+		// 计算已答试题数量
+	}, {
+		deep: true
+	})
+
+	const activeSt = computed(() => {
+		if (questionData.value.length) {
+			return questionData.value.length && questionData.value[progress.dlIndex].qas[progress.dtIndex];
+		} else {
+			return null
+		}
+	})
+
+	const isFistStId = computed(() => {
+		if (data.StListForSearch.length) {
+			return data.StListForSearch[0].stId == activeSt.value.stId
+		} else {
+			return false
+		}
+	});
+	const isLastStId = computed(() => {
+		if (data.StListForSearch.length) {
+			return data.StListForSearch[data.StListForSearch.length - 1].stId == activeSt.value.stId
+		} else {
+			return false
+		}
+	});
+	
+
+	function handleScoreClose() {
+		handleBack()
+	}
+	
+	// 考试得分相关 start
+	function handleScoreConfirm() {
+		uni.redirectTo({
+			url: `/pages/client/Chengji/ksScoreShijuan?hisId=${data.hisId}&from=kaoshi`
+		})
+	}
+	
+	// 考试得分相关 end
+	
+	// 保存相关
+	function handleSave(showToast) {
+		if (timer1.value) {
+			uni.showToast({
+				title: '请勿连续保存',
+				icon: 'none'
+			})
+			return 
+		}
+		timer1.value = setTimeout(() => {
+			clearTimeout(timer1.value);
+		},10*1000);
+		console.log(questionData.value)
+		
+		const result = []
+		const option = {
+			force: false,
+			operId: data.operId,
+			replyList: []
+		}
+		questionData.value.forEach(dl => {
+			dl.qas.forEach(st => {
+				const opt = {
+					stId: st.stId,
+					reply: st.reply,
+				}
+				result.push(opt)
+				option.replyList.push(opt)
+			})
+		})
+		// 保存试题答案
+		saveCacheKs(data.operId, {replyList:result, position: {dlIndex:progress.dlIndex, dtIndex: progress.dtIndex}})
+		// 保存答题进度
+		ksApi.getClientKsSave(option).then(res => {
+			if (res.data && showToast) {
+				uni.showToast({
+					title: '保存成功',
+				})
+			}
+		})
+	}
+
+
+	//  交卷相关功能 start
+	function checkJiaojuan() {
+		const result = []
+		let count = 0;
+		let total = 0;
+		questionData.value.forEach(dl => {
+			dl.qas.forEach(st => {
+				const opt = {
+					stId: st.stId,
+					reply: st.reply,
+					stTypeId: st.stTypeId
+				}
+				result.push(opt)
+			})
+		})
+
+		result.forEach(item => {
+			total++;
+			if (item.stTypeId == 1 && !checkDanxuanReply(item)) {
+				count++;
+			}
+			if (item.stTypeId == 2 && !checkDuoxuanReply(item)) {
+				count++;
+			}
+			if (item.stTypeId == 3 && !checkPanduanReply(item)) {
+				count++;
+			}
+			if (item.stTypeId == 4 && !checkTiankongReply(item)) {
+				count++;
+			}
+		})
+		return {
+			total,
+			count,
+		}
+	}
+
+	function handleJiaojuan() {
+		const result = checkJiaojuan()
+		console.log(result)
+		if (result.count) {
+			// 提示
+			answerQrRef.value.showDialog({
+				answercartsCount: result.total - result.count,
+				answercartsTotal: result.total,
+			})
+		} else {
+			handleSubmit()
+		}
+	}
+
+	function handleQuerenConfirm() {
+		handleSubmit()
+	}
+	
+	function handleSubmit(force = false) {
+		const result = {
+			force,
+			operId: data.operId,
+			replyList: []
+		};
+		questionData.value.forEach(dl => {
+			dl.qas.forEach(st => {
+				const opt = {
+					stId: st.stId,
+					reply: st.reply
+				}
+				result.replyList.push(opt)
+			})
+		})
+	
+		ksApi.getClientKsSubmit(result).then(res => {
+			if (res.code == 0) {
+				subScoreRef.value.showDialog(res.data);
+				data.hisId = res.data.hisId;
+				// 清空缓存
+				removeCacheKs(data.operId);
+			}
+		})
+	}
+	
+	function onTimeUp() {
+		handleSubmit();
+	}
+	//  交卷相关功能 end
+
+
+	// 切屏功能 start
+	function qiepingToast(count) {
+		messageContent.value = `已离开${count}次。${data.toggleScreenFlag}次将自动交卷!`
+		qiepingDlRef.value.handleShow()
+	}
+
+	function forceSubmit() {
+		// 强制交卷
+		console.log('强制交卷')
+		handleSubmit(true)
+	}
+
+	function qpZhuapai() {
+		// 重新开启抓拍
+		zhuapaiRef.value && zhuapaiRef.value.showVideoBtn();
+	}
+	// 切屏功能 end
+
+	// 摄像头抓拍相关功能 start
+
+	function zpSuccess() {}
+
+	function zpError() {
+		uni.showToast({
+			title: '抓拍图片异常',
+			icon: 'none'
+		})
+		handleBack()
+	}
+
+	// 摄像头抓拍相关功能 end
+	// 摄像头确认相关功能 start
+	function onProgress() {
+		uni.showToast({
+			icon: 'none',
+			title: '摄像头运行环境异常,请重新进入考试',
+			duration: 8000
+		})
+		handleBack();
+	}
+	function zpConfirmSuccess() {
+		initKaoshi();
+	}
+
+	function zpConfirmError() {
+		handleBack()
+	}
+
+	function zpConfirmCancel() {
+		handleBack()
+	}
+
+	// 摄像头确认相关功能 end
+
+	function getQaClass(qa) {
+		if (qa.marked && qa.marked === true) {
+			return 'paragraph-qa-block-mark';
+		} else {
+			if (qa.stTypeId == 1) {
+				if (checkDanxuanReply(qa)) {
+					return 'paragraph-qa-block-done';
+				} else {
+					return 'paragraph-qa-block-init';
+				}
+			} else if (qa.stTypeId == 2) {
+				if (checkDuoxuanReply(qa)) {
+					return 'paragraph-qa-block-done';
+				} else {
+					return 'paragraph-qa-block-init';
+				}
+			} else if (qa.stTypeId == 3) {
+				if (checkPanduanReply(qa)) {
+					return 'paragraph-qa-block-done';
+				} else {
+					return 'paragraph-qa-block-init';
+				}
+			} else if (qa.stTypeId == 4) {
+				if (checkTiankongReply(qa)) {
+					return 'paragraph-qa-block-done';
+				} else {
+					return 'paragraph-qa-block-init';
+				}
+			}
+		}
+	}
+
+	function skipQuestion(dlIndex, dtIndex) {
+		progress.dlIndex = dlIndex;
+		progress.dtIndex = dtIndex;
+		handlePopupBack()
+	}
+
+	function answerCardItemClick(qa) {
+		const actQa = data.StListForSearch.find(item => item.stId == qa.stId);
+		skipQuestion(actQa.dlIndex, actQa.dtIndex)
+
+	}
+
+	function handleBack() {
+		 const pages = getCurrentPages();
+		 if (pages.length>1) {
+			 uni.navigateBack()
+		 } else {
+			  history.back();
+		 }
+		
+		
+	}
+
+	function showAnswerCard() {
+		popupRef.value.open('top')
+	}
+
+	function handlePopupBack() {
+		popupRef.value.close()
+	}
+
+	function handlePrev() {
+		const qa = data.StListForSearch.find(item => item.stId == activeSt.value.stId);
+		const index = qa.onlyNum - 1;
+		if (index > 0) {
+			const result = data.StListForSearch[index - 1];
+			progress.dlIndex = result.dlIndex;
+			progress.dtIndex = result.dtIndex
+		}
+
+	}
+
+	function handleNext() {
+		const qa = data.StListForSearch.find(item => item.stId == activeSt.value.stId);
+		const index = qa.onlyNum - 1;
+		if (index < data.StListForSearch.length) {
+			const result = data.StListForSearch[index + 1];
+			progress.dlIndex = result.dlIndex;
+			progress.dtIndex = result.dtIndex
+		}
+	}
+
+	function formatDuanluoList(dlData) {
+		let uIndex = 0; // 试题onlyNum
+		let iDuanluo = 0; // 段落onlyNum
+		let result = [];
+		for (const duanluo of data.duanluo) {
+			let paragraph = {
+				qas: [],
+			};
+			paragraph.name = duanluo.name;
+
+			let iQa = 0; // 当前试题序号
+			let order = 0; // 当前题型中第几题
+			for (const iDanxuan of duanluo.danxuan) {
+				iDanxuan.type = 'danxuan';
+				iDanxuan.marked = data.biaoji[iDanxuan.stId] ? true: false;
+				iDanxuan.onlyNum = uIndex + 1;
+				iDanxuan.order = order;
+				iDanxuan.iQa = iQa;
+				paragraph.qas.push(iDanxuan);
+				uIndex++;
+				order++;
+				iQa++;
+
+				data.StListForSearch.push({
+					stId: iDanxuan.stId,
+					paragraphName: paragraph.name,
+					dlIndex: iDuanluo,
+					dtIndex: iDanxuan.iQa,
+					onlyNum: iDanxuan.onlyNum
+				})
+			}
+			order = 0;
+			for (const iDuoxuan of duanluo.duoxuan) {
+				iDuoxuan.type = 'duoxuan';
+				iDuoxuan.marked = data.biaoji[iDuoxuan.stId] ? true: false;
+				iDuoxuan.onlyNum = uIndex + 1;
+				iDuoxuan.order = order;
+				paragraph.qas.push(iDuoxuan);
+				iDuoxuan.iQa = iQa;
+				iDuoxuan.reply = [];
+				uIndex++;
+				order++;
+				iQa++;
+
+				data.StListForSearch.push({
+					stId: iDuoxuan.stId,
+					paragraphName: paragraph.name,
+					dlIndex: iDuanluo,
+					dtIndex: iDuoxuan.iQa,
+					onlyNum: iDuoxuan.onlyNum
+				})
+			}
+			order = 0;
+			for (const iPanduan of duanluo.panduan) {
+				iPanduan.type = 'panduan';
+				iPanduan.marked = data.biaoji[iPanduan.stId] ? true: false;
+				iPanduan.onlyNum = uIndex + 1;
+				iPanduan.order = order;
+				paragraph.qas.push(iPanduan);
+				iPanduan.iQa = iQa;
+				uIndex++;
+				order++;
+				iQa++;
+
+				data.StListForSearch.push({
+					stId: iPanduan.stId,
+					paragraphName: paragraph.name,
+					dlIndex: iDuanluo,
+					dtIndex: iPanduan.iQa,
+					onlyNum: iPanduan.onlyNum
+				})
+			}
+			order = 0;
+			for (const iTiankong of duanluo.tiankong) {
+				iTiankong.type = 'tiankong';
+				iTiankong.marked = data.biaoji[iTiankong.stId] ? true: false;
+				iTiankong.onlyNum = uIndex + 1;
+				iTiankong.order = order;
+				paragraph.qas.push(iTiankong);
+				iTiankong.iQa = iQa;
+				iTiankong.reply = new Array(iTiankong.count).fill('');;
+				uIndex++;
+				order++;
+				iQa++;
+
+				data.StListForSearch.push({
+					stId: iTiankong.stId,
+					paragraphName: paragraph.name,
+					dlIndex: iDuanluo,
+					dtIndex: iTiankong.iQa,
+					onlyNum: iTiankong.onlyNum
+				})
+			}
+			iDuanluo++;
+			questionData.value.push(paragraph)
+			console.log('1', questionData.value)
+			console.log('2', data.StListForSearch)
+		}
+	}
+
+	function handleBiaoji() {
+		activeSt.value.marked = !activeSt.value.marked;
+		data.biaoji[activeSt.value.stId] = activeSt.value.marked
+		ksApi.getClientKaoshiBiaoji({
+			operId: data.operId,
+			biaoji: JSON.stringify(data.biaoji)
+		}).catch(err => {
+			/* uni.redirectTo({
+				url: '/pages/client/Kaoshi/list'
+			}) */
+			handleBack()
+		})
+	}
+
+	function formatKaoshiData() {
+		const historyData = getCacheKs(data.operId);
+		if (historyData) {
+			const { replyList ,position } = historyData;
+			if (replyList) {
+				questionData.value.forEach(dl => {
+					dl.qas.forEach(st => {
+						st.reply = replyList.find(item => item.stId == st.stId).reply
+					})
+				})
+			}
+			if (position) {
+				progress.dlIndex = position.dlIndex;
+				progress.dtIndex = position.dtIndex;
+			}
+		
+		}
+	}
+
+	// 摄像头确认初始化
+	function initBeforKaoshi() {
+		console.log(zhuapaiConfirmRef.value)
+		zhuapaiConfirmRef.value.showDialog()
+	}
+
+
+	function initKaoshi() {
+		ksApi.getClientKsStart({
+			ksId: data.ksId,
+			userKaozhengId: data.userKaozhengId
+		}).then(res => {
+			const {
+				ksId,
+				operId,
+				ksName,
+				stTotal,
+				stScore,
+				biaoji,
+				endSecond,
+				pageSize,
+				toggleScreenFlag,
+				toggleScreenSecond,
+				zhuapai,
+				duanluoList
+			} = res.data;
+
+			data.ksId = ksId;
+			data.operId = operId;
+			data.ksName = ksName;
+			data.stTotal = stTotal;
+			data.stScore = stScore;
+			data.biaoji = biaoji ? JSON.parse(biaoji): {};
+			data.endSecond = endSecond;
+			data.pageSize = pageSize;
+			data.toggleScreenFlag = toggleScreenFlag;
+			data.toggleScreenSecond = toggleScreenSecond;
+			data.zhuapai = zhuapai;
+			data.duanluo = duanluoList;
+			formatDuanluoList(data.duanluo);
+			// 设置缓存
+			formatKaoshiData();
+			// 设置抓拍监听
+			
+			if (data.zhuapai && data.zhuapai > 0) {
+				zhuapaiRef.value.init({
+					zhuapai: zhuapai,
+					operId: operId
+				});
+			}
+			
+			// 设置切屏监听
+			qiepingRef.value.init({
+				zhuapaiFlag: true,
+				toggleScreenFlag: toggleScreenFlag,
+				toggleScreenSecond: toggleScreenSecond,
+				ksId: data.ksId
+			})
+
+			uni.setNavigationBarTitle({
+				title: data.ksName
+			});
+			startCountDown.value = true;
+		}).catch(err => {
+      handleBack()
+    })
+	}
+</script>

+ 5 - 1
pages/exam/index.vue

@@ -1,7 +1,7 @@
 <template>
 	<custom-scroll-list :refreshFn="getExamList" :tabList="tabData" :defaultTab="1">
 		<template #default="{list}">
-			<scroll-list-card  v-for="(item,index) in list" :key="item.ksId" :data="item" ></scroll-list-card>
+			<scroll-list-card  v-for="(item,index) in list" :key="item.ksId" :data="item" @btnClick="handleClick"></scroll-list-card>
 		</template>
 	</custom-scroll-list>
 </template>
@@ -29,6 +29,10 @@
 			value: 4,
 		}
 	]
+	
+	function handleClick(data) {
+		console.log('ddd',data)
+	}
 </script>
 
 <style lang="scss">

+ 1 - 0
pages/login.vue

@@ -58,6 +58,7 @@
 			//password: encrypt.encrypt(password.value),
 			userName: 'root'
 		}
+		console.log('3333', req)
 		login(req).then(res => {
 			let obj = JSON.stringify(res.data)
 			console.log(obj)