Polish quiz navigation and submission flow
This commit is contained in:
parent
6ad79be274
commit
43ddf589a3
@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Grid } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, ArrowRight, Finished, Grid, House } from '@element-plus/icons-vue'
|
||||
import { useQuizStore } from '@/stores/quiz'
|
||||
|
||||
const route = useRoute()
|
||||
@ -17,6 +17,7 @@ const autoSubmitting = shallowRef(false)
|
||||
const loadingAttempt = shallowRef(true)
|
||||
const touchStartX = shallowRef(0)
|
||||
const touchStartY = shallowRef(0)
|
||||
const slideName = shallowRef('slide-next')
|
||||
let timer: number | undefined
|
||||
let positionTimer: number | undefined
|
||||
|
||||
@ -47,6 +48,8 @@ const displaySelected = computed(() => {
|
||||
const singleDisplaySelected = computed(() => displaySelected.value[0])
|
||||
const showConfirmAnswer = computed(() => quiz.attempt?.status === 'in_progress' && (isMultiple.value || isBlank.value) && !showAnswer.value)
|
||||
const optionsLocked = computed(() => quiz.attempt?.status !== 'in_progress' || showAnswer.value)
|
||||
const isFirstQuestion = computed(() => quiz.currentIndex === 0)
|
||||
const isLastQuestion = computed(() => !quiz.attempt || quiz.currentIndex >= quiz.attempt.items.length - 1)
|
||||
|
||||
function optionClass(option: { id: number; is_correct: boolean }) {
|
||||
if (!showAnswer.value) return ''
|
||||
@ -85,6 +88,7 @@ async function selectSingle(optionId: number) {
|
||||
|
||||
async function go(index: number) {
|
||||
if (!quiz.attempt || index < 0 || index >= quiz.attempt.items.length) return
|
||||
slideName.value = index > quiz.currentIndex ? 'slide-next' : 'slide-prev'
|
||||
quiz.setPosition(index)
|
||||
syncLocalAnswer()
|
||||
sheetOpen.value = false
|
||||
@ -113,6 +117,17 @@ function handleTouchEnd(event: TouchEvent) {
|
||||
|
||||
async function submitPaper(auto = false) {
|
||||
if (!quiz.attempt || quiz.attempt.status !== 'in_progress') return
|
||||
if (!auto) {
|
||||
try {
|
||||
await ElMessageBox.confirm('提交后将完成本次练习,确认提交吗?', '提交确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认提交',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
await quiz.savePosition(quiz.currentIndex, true).catch(() => undefined)
|
||||
await quiz.submit()
|
||||
ElMessage.success(auto ? '时间到,已自动完成' : '练习已完成')
|
||||
@ -170,6 +185,8 @@ watch(remainingSeconds, async (value) => {
|
||||
<template>
|
||||
<main class="quiz-page" v-if="!loadingAttempt && quiz.attempt && item">
|
||||
<section class="question-panel" @touchstart.passive="handleTouchStart" @touchend.passive="handleTouchEnd">
|
||||
<Transition :name="slideName" mode="out-in">
|
||||
<article :key="item.id" class="question-slide">
|
||||
<div class="question-meta">
|
||||
<div class="meta-left">
|
||||
<ElTag>{{ typeLabel }}</ElTag>
|
||||
@ -221,19 +238,40 @@ watch(remainingSeconds, async (value) => {
|
||||
<p v-if="item.question.type === 'blank'">正确答案:{{ item.question.answers?.join('、') || '暂无标准答案' }}</p>
|
||||
<p>{{ item.question.explanation || '暂无解析' }}</p>
|
||||
</section>
|
||||
</article>
|
||||
</Transition>
|
||||
</section>
|
||||
|
||||
<footer class="quiz-footer">
|
||||
<ElButton :icon="Grid" circle @click="sheetOpen = true" />
|
||||
<ElButton plain @click="router.push('/quiz')">返回</ElButton>
|
||||
<ElButton :disabled="quiz.currentIndex === 0" @click="go(quiz.currentIndex - 1)">上一题</ElButton>
|
||||
<ElButton v-if="showConfirmAnswer" type="primary" @click="submitAnswer">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-group footer-group--left">
|
||||
<ElButton class="footer-icon footer-home" :icon="House" circle @click="router.push('/quiz')" />
|
||||
<ElButton class="footer-nav footer-prev" :icon="ArrowLeft" :disabled="isFirstQuestion" @click="go(quiz.currentIndex - 1)">
|
||||
上一题
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="footer-group footer-group--center">
|
||||
<ElButton class="footer-icon footer-sheet" :icon="Grid" circle @click="sheetOpen = true" />
|
||||
</div>
|
||||
<div class="footer-group footer-group--right">
|
||||
<ElButton class="footer-nav footer-next" type="primary" plain :disabled="isLastQuestion" @click="go(quiz.currentIndex + 1)">
|
||||
下一题
|
||||
<ElIcon class="footer-nav-icon"><ArrowRight /></ElIcon>
|
||||
</ElButton>
|
||||
<ElButton v-if="showConfirmAnswer" class="footer-primary" type="primary" @click="submitAnswer">
|
||||
确认本题
|
||||
</ElButton>
|
||||
<ElButton :disabled="quiz.currentIndex >= quiz.attempt.items.length - 1" @click="go(quiz.currentIndex + 1)">下一题</ElButton>
|
||||
<ElButton plain :disabled="quiz.attempt.status !== 'in_progress'" @click="submitPaper(false)">
|
||||
{{ quiz.attempt.status === 'submitted' ? '已完成' : '完成练习' }}
|
||||
<ElButton
|
||||
class="footer-finish"
|
||||
:class="{ 'footer-finish--defer': showConfirmAnswer }"
|
||||
:icon="Finished"
|
||||
:disabled="quiz.attempt.status !== 'in_progress'"
|
||||
@click="submitPaper(false)"
|
||||
>
|
||||
{{ quiz.attempt.status === 'submitted' ? '已提交' : '提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<ElDrawer v-model="sheetOpen" title="答题卡" direction="btt" size="48%">
|
||||
@ -274,6 +312,11 @@ watch(remainingSeconds, async (value) => {
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px 120px;
|
||||
touch-action: pan-y;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.question-slide {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.question-meta {
|
||||
@ -393,15 +436,98 @@ watch(remainingSeconds, async (value) => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--qq-line);
|
||||
background: rgba(247, 248, 244, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
min-height: 52px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(223, 229, 221, 0.9);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.48);
|
||||
box-shadow: 0 10px 28px rgba(23, 33, 27, 0.08);
|
||||
}
|
||||
|
||||
.footer-inner :deep(.el-button),
|
||||
.footer-group :deep(.el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.footer-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.footer-nav,
|
||||
.footer-primary,
|
||||
.footer-finish {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
width: 138px;
|
||||
}
|
||||
|
||||
.footer-primary {
|
||||
width: 132px;
|
||||
}
|
||||
|
||||
.footer-finish {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
.footer-primary {
|
||||
box-shadow: 0 8px 18px rgba(31, 111, 91, 0.18);
|
||||
}
|
||||
|
||||
.footer-finish {
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.footer-nav-icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.slide-next-enter-active,
|
||||
.slide-next-leave-active,
|
||||
.slide-prev-enter-active,
|
||||
.slide-prev-leave-active {
|
||||
transition:
|
||||
transform 220ms ease,
|
||||
opacity 180ms ease;
|
||||
}
|
||||
|
||||
.slide-next-enter-from,
|
||||
.slide-prev-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(42px);
|
||||
}
|
||||
|
||||
.slide-next-leave-to,
|
||||
.slide-prev-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-42px);
|
||||
}
|
||||
|
||||
.answer-sheet {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
|
||||
@ -436,7 +562,7 @@ watch(remainingSeconds, async (value) => {
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.question-panel {
|
||||
padding: 18px 12px 118px;
|
||||
padding: 18px 12px 132px;
|
||||
}
|
||||
|
||||
.question-panel h1 {
|
||||
@ -444,8 +570,79 @@ watch(remainingSeconds, async (value) => {
|
||||
}
|
||||
|
||||
.quiz-footer {
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr) minmax(0, 1fr) 42px;
|
||||
grid-template-areas:
|
||||
"home prev next sheet"
|
||||
"finish finish finish finish";
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.footer-group {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.footer-home {
|
||||
grid-area: home;
|
||||
}
|
||||
|
||||
.footer-sheet {
|
||||
grid-area: sheet;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.footer-prev {
|
||||
grid-area: prev;
|
||||
}
|
||||
|
||||
.footer-next {
|
||||
grid-area: next;
|
||||
}
|
||||
|
||||
.footer-primary {
|
||||
grid-area: finish;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
min-width: 0;
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.footer-finish {
|
||||
grid-area: finish;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.footer-finish--defer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.slide-next-enter-active,
|
||||
.slide-next-leave-active,
|
||||
.slide-prev-enter-active,
|
||||
.slide-prev-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user