| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- <template>
- <ie-page :fix-height="true" :safe-area-inset-bottom="false">
- <ie-navbar :title="pageTitle" custom-back @left-click="handleLeftClick">
- <template v-if="isReady" #headerRight>
- <view v-if="isExamMode" class="countdown-text" :class="{ 'text-red-500': examDuration < 30 }">
- {{ formatExamDuration }}
- </view>
- <view v-else class="">{{ formatPracticeDuration }}</view>
- </template>
- </ie-navbar>
- <view v-if="isReady" class="px-20 py-14 bg-back flex justify-between items-center gap-x-20">
- <text class="flex-1 min-w-1 text-26 ellipsis-1">{{ pageSubtitle }}</text>
- <view class="flex items-baseline">
- <text class="text-34 text-primary font-bold">{{ currentIndex + 1 }}</text>/
- <text class="text-28 text-fore-subtitle">{{ totalCount }}</text>
- </view>
- </view>
- <view class="flex-1 min-h-1 relative">
- <view class="absolute inset-0 ">
- <swiper class="h-full" :disable-touch="false" :current="currentIndex" :duration="swiperDuration"
- @change="handleSwiperChange" @transition="handleSwiperTransition"
- @animationfinish="handleSwiperAnimationFinish">
- <swiper-item class="h-full" v-for="(item, index) in questionList" :key="index">
- <question-wrap :question="item" @update:question="handleUpdateQuestion" />
- </swiper-item>
- </swiper>
- </view>
- </view>
- <ie-safe-toolbar v-if="isReady" :height="64" :shadow="false">
- <view class="px-18 h-full flex items-center justify-around border-0 border-t border-solid border-[#EFEFEF]">
- <view class="w-48 h-48 flex items-center justify-center" @click="handleFavorite">
- <uv-icon v-if="currentQuestion.isFavorite" name="star-fill" color="#FF9A18" size="27" />
- <uv-icon v-else name="star" size="27" />
- </view>
- <view class="w-48 h-48 flex items-center justify-center" @click="handleMark">
- <ie-image
- :src="currentQuestion.isMark ? '/pagesStudy/static/image/icon-mark-active.png' : '/pagesStudy/static/image/icon-mark.png'"
- custom-class="w-38 h-38" mode="aspectFill" />
- </view>
- <view class="w-48 h-48 flex items-center justify-center" @click="handleCalendar">
- <uv-icon name="calendar" size="28" />
- </view>
- </view>
- </ie-safe-toolbar>
- </ie-page>
- <question-stats-popup ref="questionStatsPopupRef">
- <template #title>
- <view class="ml-20">
- <text class="text-30 text-primary">{{ doneCount }}</text>
- <text>/</text>
- <text class="text-30 text-fore-light">{{ totalCount }}</text>
- </view>
- </template>
- <view class="popup-content">
- <view class="flex-1 min-h-1">
- <scroll-view class="h-full" scroll-y>
- <view v-for="(item, i) in groupedQuestionList" :key="i" class="">
- <template v-if="item.list.length > 0">
- <view class="h-70 bg-back px-20 leading-70 text-fore-subcontent">{{ questionTypeDesc[item.type] }}</view>
- <view class="grid grid-cols-5 place-items-center gap-x-20 gap-y-20 p-30">
- <view v-for="(qs, j) in item.list" :key="j" class="aspect-square flex items-center justify-center"
- @click="hanadleNavigate(qs.index)">
- <view
- class="w-74 h-74 rounded-full flex items-center justify-center bg-white border border-solid border-border relative"
- :class="{
- 'is-done': qs.question.isDone,
- 'is-not-know': qs.question.isNotKnow,
- 'is-mark': qs.question.isMark
- }">
- <text class="z-1">{{ qs.index + 1 }}</text>
- <ie-image v-if="qs.question.isMark" src="/pagesStudy/static/image/icon-mark-active.png"
- custom-class="absolute -top-12 left-14 w-28 h-28 z-1" mode="aspectFill" />
- <question-progress :progress="qs.question.progress || 0" />
- </view>
- </view>
- </view>
- </template>
- </view>
- </scroll-view>
- </view>
- <view class="h-150 bg-white flex items-center gap-x-120 px-40">
- <view class="flex flex-col items-center gap-x-10" @click="handleReset">
- <uv-icon name="reload" size="20" :color="doneCount > 0 ? '#999' : '#cccccc'" />
- <text class="mt-4 text-20 text-subcontent" :class="{ 'text-fore-light': doneCount <= 0 }">重新作答</text>
- </view>
- <view class="flex-1 py-20 text-center rounded-full bg-primary text-white" @click="beforeSubmit">交卷</view>
- </view>
- </view>
- </question-stats-popup>
- </template>
- <script lang="ts" setup>
- import QuestionItem from './components/question-item.vue';
- import QuestionWrap from './components/question-wrap.vue';
- import QuestionStatsPopup from './components/question-stats-popup.vue';
- import QuestionProgress from './components/question-progress.vue';
- import { useTransferPage } from '@/hooks/useTransferPage';
- import { EnumExamMode } from '@/common/enum';
- import { getOpenExaminee, getPaper, commitExamineePaper, collectQuestion, cancelCollectQuestion } from '@/api/modules/study';
- import { useExam } from '@/composables/useExam';
- import { Study } from '@/types';
- import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUICKLY } from '@/types/injectionSymbols';
- // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
- const { prevData, transferBack } = useTransferPage();
- const { setQuestionList, questionList, groupedQuestionList, stateQuestionList, questionTypeDesc, favoriteList, notKnowList, markList, currentIndex,
- totalCount, doneCount, notDoneCount, isAllDone, nextQuestion, prevQuestion, nextQuestionQuickly, prevQuestionQuickly, swiperDuration,
- formatPracticeDuration, formatExamDuration, practiceDuration, startPracticeDuration, stopPracticeDuration,
- examDuration, startExamDuration, stopExamDuration, setExamDuration, setCountDownCallback, changeIndex, setDuration, reset } = useExam();
- provide(NEXT_QUESTION, nextQuestion);
- provide(PREV_QUESTION, prevQuestion);
- provide(NEXT_QUESTION_QUICKLY, nextQuestionQuickly);
- provide(PREV_QUESTION_QUICKLY, prevQuestionQuickly);
- const isAnimationFinish = ref(false);
- const transitionStartX = ref(null);
- const transitionEndX = ref(null);
- const isReady = ref(false);
- // 自动提交只提醒1次
- const hasShowSubmitConfirm = ref(false);
- const examineerData = ref<Study.Examinee>({} as Study.Examinee);
- const paperData = ref<Study.ExamPaper>({} as Study.ExamPaper);
- const pageTitle = computed(() => {
- const { mode } = prevData.value;
- return mode === EnumExamMode.PRACTICE ? '练习' : '考试';
- });
- const pageSubtitle = computed(() => {
- const { mode, name } = prevData.value;
- return (mode === EnumExamMode.PRACTICE ? '知识点练习' : '考试') + '-' + name;
- });
- const isExamMode = computed(() => {
- return prevData.value.mode === EnumExamMode.EXAM;
- });
- const handleLeftClick = () => {
- if (!isReady.value) {
- transferBack();
- return;
- }
- beforeQuit();
- };
- const handleSwiperChange = (e: any) => {
- currentIndex.value = e.detail.current;
- };
- const beforeQuit = () => {
- const { mode } = prevData.value;
- if (!isReady.value) {
- return;
- }
- stopTime();
- const msg = mode === EnumExamMode.PRACTICE ? '当前练习未完成,确认退出?' : '当前考试未完成,确认退出?';
- uni.$ie.showModal({
- title: '提示',
- content: msg,
- }).then(confirm => {
- if (confirm) {
- handleSubmit(true);
- } else {
- startTime();
- }
- });
- };
- const currentQuestion = computed(() => {
- return questionList.value[currentIndex.value] || {};
- });
- const hanadleNavigate = (index: number) => {
- changeIndex(index);
- }
- const handleFavorite = async () => {
- if (!currentQuestion.value.isFavorite) {
- await collectQuestion(currentQuestion.value.id);
- currentQuestion.value.isFavorite = true;
- uni.$ie.showToast('收藏成功');
- } else {
- await cancelCollectQuestion(currentQuestion.value.id);
- currentQuestion.value.isFavorite = false;
- uni.$ie.showToast('取消收藏成功');
- }
- };
- const handleMark = () => {
- currentQuestion.value.isMark = !currentQuestion.value.isMark;
- };
- const questionStatsPopupRef = ref();
- const handleCalendar = () => {
- questionStatsPopupRef.value.open();
- };
- const handleSwiperTransition = (e: any) => {
- if (currentIndex.value === questionList.value.length - 1) {
- if (!transitionStartX.value) {
- transitionStartX.value = e.detail.dx;
- } else {
- transitionEndX.value = e.detail.dx;
- }
- return;
- }
- };
- const startTime = () => {
- if (isExamMode.value) {
- startExamDuration();
- } else {
- startPracticeDuration();
- }
- }
- const stopTime = () => {
- if (isExamMode.value) {
- stopExamDuration();
- } else {
- stopPracticeDuration();
- }
- }
- const handleSwiperAnimationFinish = (e: any) => {
- if (transitionStartX.value == null || transitionEndX.value == null || currentIndex.value !== questionList.value.length - 1) {
- isAnimationFinish.value = true;
- transitionStartX.value = null;
- transitionEndX.value = null;
- return;
- }
- const offsetX = transitionEndX.value - transitionStartX.value;
- if (offsetX < 0 && offsetX > -150) {
- beforeSubmit();
- }
- isAnimationFinish.value = true;
- transitionStartX.value = null;
- transitionEndX.value = null;
- };
- const beforeSubmit = () => {
- const text = notDoneCount.value > 0 ? `还有${notDoneCount.value}题未做,确认交卷?` : '是否确认交卷?';
- stopTime();
- uni.$ie.showModal({
- title: '提示',
- content: text,
- }).then(confirm => {
- if (confirm) {
- handleSubmit(false);
- } else {
- startTime();
- }
- });
- }
- const handleReset = () => {
- if (doneCount.value <= 0) {
- return;
- }
- uni.$ie.showModal({
- title: '重新作答',
- content: '是否确认清空全部作答数据?',
- }).then(confirm => {
- if (confirm) {
- questionStatsPopupRef.value.close();
- reset();
- setTimeout(() => {
- startTime();
- }, 300);
- }
- });
- }
- const autoSubmit = () => {
- if (isAllDone.value) {
- if (hasShowSubmitConfirm.value) {
- return;
- }
- hasShowSubmitConfirm.value = true;
- beforeSubmit();
- }
- }
- const handleSubmit = (tempSave: boolean = false) => {
- console.log('handleSubmit', questionList.value)
- const msg = tempSave ? '保存中...' : '提交中...';
- uni.$ie.showLoading(msg);
- setTimeout(() => {
- uni.$ie.hideLoading();
- const params = {
- ...paperData.value,
- questions: questionList.value.map(item => {
- return {
- ...item,
- isDone: tempSave ? item.isDone : true
- };
- }),
- examineeId: examineerData.value.examineeId,
- isDone: isAllDone.value,
- duration: isExamMode.value ? examDuration.value : practiceDuration.value
- } as Study.ExamPaperSubmit;
- if (!isExamMode.value) {
- params.duration = practiceDuration.value;
- }
- commitExamineePaper(params);
- uni.navigateBack();
- }, 1000);
- }
- const handleUpdateQuestion = (question: Study.Question) => {
- if (currentIndex.value === questionList.value.length - 1) {
- autoSubmit();
- }
- }
- /**
- * 恢复上次做题历史数据
- * @param savedQuestion 上次做题历史数据
- * @param fullQuestion 当前试卷数据
- */
- const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion: Study.ExamineeQuestion[]) => {
- if (!savedQuestion) {
- return fullQuestion;
- }
- console.log(savedQuestion, fullQuestion)
- for (const item of fullQuestion) {
- const savedQs = savedQuestion.find(q => q.id === item.id);
- if (savedQs) {
- if (savedQs.answers) {
- item.answers = savedQs.answers.filter(ans => ans.trim());
- }
- item.isMark = savedQs.isMark;
- item.isFavorite = savedQs.isFavorite;
- item.isNotKnow = savedQs.isNotKnow;
- if (item.subQuestions) {
- restoreQuestion(savedQs.subQuestions, item.subQuestions);
- }
- }
- }
- return fullQuestion;
- }
- const loadData = async () => {
- uni.$ie.showLoading();
- const { data } = await getOpenExaminee({
- paperType: prevData.value.paperType,
- relateId: prevData.value.relateId,
- directed: prevData.value.directed
- });
- if (!data) {
- uni.$ie.hideLoading();
- transferBack();
- return;
- }
- examineerData.value = data;
- if (data.paperId) {
- const res = await getPaper({
- type: prevData.value.paperType,
- id: data.paperId
- });
- uni.$ie.hideLoading();
- paperData.value = res.data;
- paperData.value.questions = restoreQuestion(data.questions, res.data.questions);
- setQuestionList(paperData.value.questions);
- setDuration(data.duration || 0);
- }
- isReady.value = true;
- setCountDownCallback(() => {
- handleSubmit(false);
- });
- startTime();
- };
- onLoad(() => {
- loadData();
- });
- </script>
- <style lang="scss" scoped>
- .countdown-text {
- height: 100%;
- display: flex;
- align-items: center;
- font-weight: bold;
- font-family: 'Courier New', Courier, monospace;
- }
- .popup-content {
- @apply h-[42vh] flex flex-col;
- }
- .scroll-view {
- @apply h-full;
- }
- .is-done {
- @apply text-primary border-[#EBF9FF] bg-[#EBF9FF];
- }
- .is-not-know {
- @apply text-fore-title border-[#F2F2F2] bg-[#F2F2F2];
- }
- </style>
|