123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- import { ActionSheet, Button, Image, Popup, Swipe, SwipeItem } from 'vant';
- import {
- defineComponent,
- nextTick,
- onMounted,
- onUnmounted,
- reactive,
- ref
- } from 'vue';
- import { useRoute, useRouter } from 'vue-router';
- import styles from './index.module.less';
- import iconButtonList from '../images/icon-button-list.png';
- import MSticky from '@/components/m-sticky';
- import ChoiceQuestion from '../model/choice-question';
- import AnswerList from '../model/answer-list';
- import DragQuestion from '../model/drag-question';
- import KeepLookQuestion from '../model/keep-look-question';
- import PlayQuestion from '../model/play-question';
- import ResultFinish from '../model/result-finish';
- import { eventUnit, QuestionType } from '../unit';
- import request from '@/helpers/request';
- import { CurrentTime, useCountDown, useRect } from '@vant/use';
- import MHeader from '@/components/m-header';
- import { useEventListener, useWindowScroll } from '@vueuse/core';
- export default defineComponent({
- name: 'unit-detail',
- setup() {
- const route = useRoute();
- const router = useRouter();
- const swipeRef = ref();
- const state = reactive({
- type: route.query.type, // 类型
- knowledgePointIds: route.query.knowledgePointIds, // 智能组卷 多个编号
- lessonCoursewareId: route.query.lessonCoursewareId, // 教材编号
- studentUnitExaminationId: '', // 测验编号
- background: 'transparent',
- color: '#fff',
- visiableAnswer: false,
- examDetail: {} as any,
- currentIndex: 0,
- time: 0,
- questionList: [],
- visiableInfo: {
- show: false,
- operationType: 'RESULT' as 'RESULT' | 'BACK' | 'CONTINUE' | 'TIME',
- type: 'DEFAULT' as 'DEFAULT' | 'FAIL' | 'PASS' | 'GOOD' | 'COUNTDOWN',
- content: '',
- showCancelButton: false,
- confirmButtonText: '',
- cancelButtonText: '',
- title: ''
- },
- nextStatus: false,
- swipeHeight: 'auto' as any,
- countDownOver: false // 是否已显示时间倒计时
- });
- // 计时
- const countDownRef = useCountDown({
- // 倒计时 60 秒
- time: state.time,
- onChange(current: CurrentTime) {
- const diffTime = 5 * 60 * 1000;
- if (diffTime >= current.total && !state.countDownOver) {
- state.visiableInfo.show = true;
- state.visiableInfo.title = '倒计时5分钟';
- state.visiableInfo.showCancelButton = false;
- state.visiableInfo.operationType = 'TIME';
- state.visiableInfo.type = 'COUNTDOWN';
- state.visiableInfo.confirmButtonText = '确认';
- state.visiableInfo.content = `距离交卷时间还剩五分钟哦,请尽快答题~`;
- state.countDownOver = true;
- }
- },
- onFinish: async () => {
- eventUnit.emit('unitAudioStop');
- await onResultPopup();
- }
- });
- const getExamDetails = async () => {
- try {
- let temp: any = {};
- if (state.type === 'ai') {
- const { data } = await request.post(
- '/edu-app/studentUnitExamination/pointRandomSave',
- {
- data: {
- knowledgePointIds: state.knowledgePointIds
- }
- }
- );
- temp = data || {};
- } else {
- const { data } = await request.post(
- '/edu-app/studentUnitExamination/mockExamination',
- {
- data: {
- lessonCoursewareId: state.lessonCoursewareId
- }
- }
- );
- temp = data || {};
- }
- temp.examinationQuestionAdds.forEach((item: any) => {
- item.showAnalysis = false; // 默认不显示解析
- item.analysis = {
- message: item.answerAnalysis,
- topic: true, // 是否显示结果
- userResult: false // 用户答题对错
- };
- item.userAnswer = []; // 用户答题
- });
- state.questionList = temp.examinationQuestionAdds || [];
- state.studentUnitExaminationId = temp.unitExaminationId;
- state.examDetail = temp || {};
- calcTime();
- } catch {
- //
- }
- };
- /**
- * @description 计算考试时间剩余时间
- */
- const calcTime = async () => {
- const examDetail = state.examDetail || {};
- const timeMinutes = examDetail.timeMinutes || 0; // 测验时间
- state.time = Math.ceil(timeMinutes * 60 * 1000);
- setTimeout(() => {
- countDownRef.reset(timeMinutes * 60 * 1000);
- countDownRef.start();
- }, 10);
- };
- /**
- * @description 下一题 | 测试完成
- */
- const onNextQuestion = async () => {
- try {
- const questionList = state.questionList || [];
- const userAnswerList: any = []; // 所有题目的答案
- questionList.forEach((question: any, index: number) => {
- // 格式化所有题目的答案
- if (question.userAnswer && question.userAnswer.length > 0) {
- userAnswerList.push({
- questionId: question.id,
- details: question.userAnswer
- });
- }
- });
- // 判断是否是最后一题
- if (state.questionList.length === state.currentIndex + 1) {
- eventUnit.emit('unitAudioStop');
- state.visiableInfo.show = true;
- state.visiableInfo.title = '测验完成';
- state.visiableInfo.showCancelButton = true;
- state.visiableInfo.operationType = 'CONTINUE';
- state.visiableInfo.type = 'DEFAULT';
- state.visiableInfo.cancelButtonText = '再等等';
- state.visiableInfo.confirmButtonText = '确认完成';
- state.visiableInfo.content = `确认本次测验的题目都完成了吗?`;
- return;
- }
- state.nextStatus = true;
- await request.post('/edu-app/studentUnitExamination/submitAnswer', {
- hideLoading: true,
- data: {
- answers: userAnswerList,
- studentUnitExaminationId: state.studentUnitExaminationId
- }
- });
- swipeRef.value?.next();
- state.nextStatus = false;
- } catch {
- //
- }
- };
- /**
- * @description 重置当前的题目高度
- * @param {any} scroll 是否滚动到顶部
- */
- let size = 0;
- const resizeSwipeItemHeight = (scroll = true) => {
- nextTick(() => {
- scroll && window.scrollTo(0, 0);
- setTimeout(() => {
- const currentItemDom: any = document
- .querySelectorAll('.van-swipe-item')
- [state.currentIndex]?.querySelector('.swipe-item-question');
- const allImg = currentItemDom?.querySelectorAll(
- '.answerTitleImg img'
- );
- let status = true;
- // console.log(allImg)
- allImg?.forEach((img: any) => {
- console.log(img.complete);
- if (!img.complete) {
- status = false;
- }
- });
- // 判断图片是否加载完了
- if (!status && size < 3) {
- setTimeout(() => {
- size += 1;
- resizeSwipeItemHeight(scroll);
- }, 300);
- }
- if (status) {
- size = 0;
- }
- const rect = useRect(currentItemDom);
- state.swipeHeight = rect.height;
- }, 100);
- });
- };
- const onConfirmResult = () => {
- if (state.visiableInfo.operationType === 'RESULT') {
- state.visiableInfo.show = false;
- router.back();
- onAfter();
- } else if (state.visiableInfo.operationType === 'BACK') {
- onResultPopup();
- } else if (state.visiableInfo.operationType === 'CONTINUE') {
- onResultPopup();
- } else if (state.visiableInfo.operationType === 'TIME') {
- state.visiableInfo.show = false;
- }
- };
- const onCloseResult = async (status: boolean) => {
- if (state.visiableInfo.operationType === 'BACK') {
- if (status) {
- state.visiableInfo.show = false;
- window.history.pushState(null, '', document.URL);
- window.addEventListener('popstate', onBack, false);
- return;
- }
- try {
- await request.get('/edu-app/studentUnitExamination/dropExamination', {
- params: {
- studentUnitExaminationId: state.studentUnitExaminationId
- }
- });
- state.visiableInfo.show = false;
- onAfter();
- } catch {
- //
- }
- } else if (state.visiableInfo.operationType === 'CONTINUE') {
- state.visiableInfo.show = false;
- }
- };
- /** 结果页面弹窗 */
- const onResultPopup = async () => {
- try {
- const questionList = state.questionList || [];
- const userAnswerList: any = []; // 所有题目的答案
- questionList.forEach((question: any) => {
- // 格式化所有题目的答案
- if (question.userAnswer && question.userAnswer.length > 0) {
- userAnswerList.push({
- questionId: question.id,
- details: question.userAnswer
- });
- }
- });
- const { data } = await request.post(
- '/edu-app/studentUnitExamination/completionExamination',
- {
- hideLoading: false,
- data: {
- answers: userAnswerList,
- studentUnitExaminationId: state.studentUnitExaminationId
- }
- }
- );
- // 60 及格
- // 85 及以上优秀
- state.visiableInfo.show = true;
- state.visiableInfo.title = data.score + '分';
- state.visiableInfo.showCancelButton = false;
- state.visiableInfo.operationType = 'RESULT';
- state.visiableInfo.confirmButtonText = '确认';
- if (data.status === 'A_EXCELLENT') {
- state.visiableInfo.type = 'GOOD';
- state.visiableInfo.content = '<div>你很棒,题目掌握的非常不错,';
- } else if (data.status === 'B_PASS') {
- state.visiableInfo.type = 'PASS';
- state.visiableInfo.content = '<div>还需要加油哦,';
- } else {
- state.visiableInfo.type = 'FAIL';
- state.visiableInfo.content = '<div>别气馁,继续努力,';
- }
- state.visiableInfo.content += `您本次获得了<span class='${
- styles.right
- }'>${data.score}分</span>,正确率<span class='${styles.error}'>${
- data.rightRate
- }%</span>,实际用时<span class='${styles.minutes}'>${Math.ceil(
- data.answerTime / 60
- )}</span>分钟~</div>`;
- } catch {
- //
- }
- };
- // 拦截
- const onBack = () => {
- state.visiableInfo.show = true;
- state.visiableInfo.title = '确认要离开吗?';
- state.visiableInfo.showCancelButton = true;
- state.visiableInfo.operationType = 'BACK';
- state.visiableInfo.type = 'DEFAULT';
- state.visiableInfo.cancelButtonText = '弃考';
- state.visiableInfo.confirmButtonText = '确定';
- state.visiableInfo.content = `还有题目未完成哦,是否要提前交卷?`;
- eventUnit.emit('unitAudioStop');
- };
- const onAfter = () => {
- window.removeEventListener('popstate', onBack, false);
- router.back();
- };
- onMounted(async () => {
- useEventListener(document, 'scroll', () => {
- const { y } = useWindowScroll();
- if (y.value > 52) {
- state.background = '#fff';
- state.color = '#323333';
- } else {
- state.background = 'transparent';
- state.color = '#fff';
- }
- });
- await getExamDetails();
- resizeSwipeItemHeight();
- window.history.pushState(null, '', document.URL);
- window.addEventListener('popstate', onBack, false);
- });
- onUnmounted(() => {
- // 关闭所有音频
- eventUnit.emit('unitAudioStop');
- });
- return () => (
- <div class={styles.unitDetail}>
- <MSticky position="top">
- <MHeader
- border={false}
- background={state.background}
- color={state.color}
- />
- </MSticky>
- <Swipe
- loop={false}
- showIndicators={false}
- ref={swipeRef}
- duration={300}
- touchable={false}
- class={styles.unitSwipe}
- style={{ paddingBottom: '12px' }}
- lazyRender
- height={state.swipeHeight}
- onChange={(index: number) => {
- eventUnit.emit('unitAudioStop');
- state.currentIndex = index;
- resizeSwipeItemHeight();
- }}>
- {state.questionList.map((item: any, index: number) => (
- <SwipeItem>
- <div class="swipe-item-question">
- {item.questionTypeCode === QuestionType.RADIO && (
- <ChoiceQuestion
- v-model:value={item.userAnswer}
- index={index + 1}
- data={item}
- type="radio"
- showAnalysis={item.showAnalysis}
- analysis={item.analysis}>
- {{
- title: () => (
- <div class={styles.questionTitle}>
- <div class={styles.questionNum}>
- <span>{state.currentIndex + 1}</span>/
- {state.questionList.length}
- </div>
- <div class={styles.questionType}>
- {countDownRef.current.value.minutes +
- countDownRef.current.value.hours * 60}
- :{countDownRef.current.value.seconds}
- </div>
- </div>
- )
- }}
- </ChoiceQuestion>
- )}
- {item.questionTypeCode === QuestionType.CHECKBOX && (
- <ChoiceQuestion
- v-model:value={item.userAnswer}
- index={index + 1}
- data={item}
- type="checkbox"
- showAnalysis={item.showAnalysis}
- analysis={item.analysis}>
- {{
- title: () => (
- <div class={styles.questionTitle}>
- <div class={styles.questionNum}>
- <span>{state.currentIndex + 1}</span>/
- {state.questionList.length}
- </div>
- <div class={styles.questionType}>
- {countDownRef.current.value.minutes +
- countDownRef.current.value.hours * 60}
- :{countDownRef.current.value.seconds}
- </div>
- </div>
- )
- }}
- </ChoiceQuestion>
- )}
- {item.questionTypeCode === QuestionType.SORT && (
- <DragQuestion
- v-model:value={item.userAnswer}
- onUpdate:value={() => {
- // 如果是空则滑动到顶部
- const status =
- item.userAnswer && item.userAnswer.length > 0
- ? false
- : true;
- resizeSwipeItemHeight(status);
- }}
- data={item}
- index={index + 1}
- showAnalysis={item.showAnalysis}
- analysis={item.analysis}>
- {{
- title: () => (
- <div class={styles.questionTitle}>
- <div class={styles.questionNum}>
- <span>{state.currentIndex + 1}</span>/
- {state.questionList.length}
- </div>
- <div class={styles.questionType}>
- {countDownRef.current.value.minutes +
- countDownRef.current.value.hours * 60}
- :{countDownRef.current.value.seconds}
- </div>
- </div>
- )
- }}
- </DragQuestion>
- )}
- {item.questionTypeCode === QuestionType.LINK && (
- <KeepLookQuestion
- v-model:value={item.userAnswer}
- data={item}
- index={index + 1}
- showAnalysis={item.showAnalysis}
- analysis={item.analysis}>
- {{
- title: () => (
- <div class={styles.questionTitle}>
- <div class={styles.questionNum}>
- <span>{state.currentIndex + 1}</span>/
- {state.questionList.length}
- </div>
- <div class={styles.questionType}>
- {countDownRef.current.value.minutes +
- countDownRef.current.value.hours * 60}
- :{countDownRef.current.value.seconds}
- </div>
- </div>
- )
- }}
- </KeepLookQuestion>
- )}
- {item.questionTypeCode === QuestionType.PLAY && (
- <PlayQuestion
- v-model:value={item.userAnswer}
- data={item}
- index={index + 1}
- unitId={state.studentUnitExaminationId as any}
- showAnalysis={item.showAnalysis}
- analysis={item.analysis}>
- {{
- title: () => (
- <div class={styles.questionTitle}>
- <div class={styles.questionNum}>
- <span>{state.currentIndex + 1}</span>/
- {state.questionList.length}
- </div>
- <div class={styles.questionType}>
- {countDownRef.current.value.minutes +
- countDownRef.current.value.hours * 60}
- :{countDownRef.current.value.seconds}
- </div>
- </div>
- )
- }}
- </PlayQuestion>
- )}
- </div>
- </SwipeItem>
- ))}
- </Swipe>
- <MSticky position="bottom">
- <div class={['btnGroup btnMore', styles.btnSection]}>
- <Button
- round
- block
- class={
- state.currentIndex > 0 ? styles.activePrevBtn : styles.prevBtn
- }
- disabled={state.currentIndex > 0 ? false : true}
- onClick={() => {
- swipeRef.value?.prev();
- }}>
- 上一题
- </Button>
- <Button
- block
- round
- class={styles.nextBtn}
- onClick={onNextQuestion}
- loading={state.nextStatus}
- disabled={state.nextStatus}>
- {state.questionList.length === state.currentIndex + 1
- ? '提交'
- : '下一题'}
- </Button>
- <Image
- src={iconButtonList}
- class={[styles.wapList, 'van-haptics-feedback']}
- onClick={() => (state.visiableAnswer = true)}
- />
- </div>
- </MSticky>
- {/* 题目集合 */}
- <ActionSheet
- v-model:show={state.visiableAnswer}
- title="题目列表"
- safeAreaInsetBottom>
- <AnswerList
- value={state.questionList}
- onSelect={(item: any) => {
- // 跳转,并且跳过动画
- swipeRef.value?.swipeTo(item, {
- immediate: true
- });
- state.visiableAnswer = false;
- }}
- />
- </ActionSheet>
- <Popup
- v-model:show={state.visiableInfo.show}
- closeOnClickOverlay={false}
- style={{
- background: 'transparent',
- width: '100%',
- maxWidth: '100%',
- transform: 'translateY(-55%)'
- }}>
- <ResultFinish
- title={state.visiableInfo.title}
- showCancelButton={state.visiableInfo.showCancelButton}
- cancelButtonText={state.visiableInfo.cancelButtonText}
- confirmButtonText={state.visiableInfo.confirmButtonText}
- status={state.visiableInfo.type}
- content={state.visiableInfo.content}
- closeable={
- state.visiableInfo.operationType === 'BACK' ? true : false
- }
- contentHtml
- onConform={onConfirmResult}
- onClose={onCloseResult}
- />
- </Popup>
- </div>
- );
- }
- });
|