Paginate attempt history and refine wrong-review footer
This commit is contained in:
parent
2050406306
commit
4682048dc5
@ -65,15 +65,14 @@ final class QuizController extends Controller
|
|||||||
$user = $this->currentUser($request);
|
$user = $this->currentUser($request);
|
||||||
abort_if(! $access->canAccessBank($user, $bank), 403);
|
abort_if(! $access->canAccessBank($user, $bank), 403);
|
||||||
|
|
||||||
return ApiResponse::success(
|
return ApiResponse::page(
|
||||||
QuizAttempt::query()
|
QuizAttempt::query()
|
||||||
->where('user_id', $user->id)
|
->where('user_id', $user->id)
|
||||||
->where('question_bank_id', $bank->id)
|
->where('question_bank_id', $bank->id)
|
||||||
->whereIn('mode', ['sequence', 'random'])
|
->whereIn('mode', ['sequence', 'random'])
|
||||||
->where('status', 'submitted')
|
->where('status', 'submitted')
|
||||||
->latest('submitted_at')
|
->latest('submitted_at')
|
||||||
->limit(30)
|
->paginate((int) $request->query('per_page', 10), ['id', 'mode', 'status', 'total_questions', 'correct_count', 'score', 'started_at', 'submitted_at']),
|
||||||
->get(['id', 'mode', 'status', 'total_questions', 'correct_count', 'score', 'started_at', 'submitted_at']),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
frontend/dist (2).zip
Normal file
BIN
frontend/dist (2).zip
Normal file
Binary file not shown.
Binary file not shown.
@ -9,8 +9,8 @@ export function fetchBankTags(bankId: number) {
|
|||||||
return apiGet<Array<{ id: number; name: string }>>(`/api/app/banks/${bankId}/tags`)
|
return apiGet<Array<{ id: number; name: string }>>(`/api/app/banks/${bankId}/tags`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchBankAttemptHistory(bankId: number) {
|
export function fetchBankAttemptHistory(bankId: number, params?: Record<string, unknown>) {
|
||||||
return apiGet<QuizAttemptHistory[]>(`/api/app/banks/${bankId}/attempts/history`)
|
return apiGet<PageData<QuizAttemptHistory>>(`/api/app/banks/${bankId}/attempts/history`, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startBankAttempt(bankId: number, payload: Record<string, unknown>) {
|
export function startBankAttempt(bankId: number, payload: Record<string, unknown>) {
|
||||||
|
|||||||
@ -357,7 +357,7 @@ watch(remainingSeconds, async (value) => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="quiz-footer">
|
<footer class="quiz-footer">
|
||||||
<div class="footer-inner">
|
<div class="footer-inner" :class="{ 'footer-inner--wrong-review': isWrongReview }">
|
||||||
<div class="footer-group footer-group--left">
|
<div class="footer-group footer-group--left">
|
||||||
<ElButton class="footer-icon footer-home" :icon="House" circle @click="router.push('/quiz')" />
|
<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 class="footer-nav footer-prev" :icon="ArrowLeft" :disabled="isFirstQuestion" @click="go(quiz.currentIndex - 1)">
|
||||||
@ -365,7 +365,6 @@ watch(remainingSeconds, async (value) => {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-group footer-group--center">
|
<div class="footer-group footer-group--center">
|
||||||
<ElButton class="footer-icon footer-sheet" :icon="Grid" circle @click="sheetOpen = true" />
|
|
||||||
<ElButton v-if="isWrongReview" class="footer-mastered" type="warning" plain @click="markCurrentMastered">
|
<ElButton v-if="isWrongReview" class="footer-mastered" type="warning" plain @click="markCurrentMastered">
|
||||||
已掌握
|
已掌握
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@ -379,7 +378,7 @@ watch(remainingSeconds, async (value) => {
|
|||||||
确认本题
|
确认本题
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="!isReadonlyReview"
|
v-if="!isReadonlyReview && !isWrongReview"
|
||||||
class="footer-finish"
|
class="footer-finish"
|
||||||
:class="{ 'footer-finish--defer': showConfirmAnswer }"
|
:class="{ 'footer-finish--defer': showConfirmAnswer }"
|
||||||
:icon="Finished"
|
:icon="Finished"
|
||||||
@ -388,6 +387,7 @@ watch(remainingSeconds, async (value) => {
|
|||||||
>
|
>
|
||||||
{{ quiz.attempt.status === 'submitted' ? '已提交' : '提交' }}
|
{{ quiz.attempt.status === 'submitted' ? '已提交' : '提交' }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
<ElButton class="footer-icon footer-sheet" :icon="Grid" circle @click="sheetOpen = true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@ -766,6 +766,13 @@ watch(remainingSeconds, async (value) => {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-inner--wrong-review {
|
||||||
|
grid-template-columns: 42px minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) 42px;
|
||||||
|
grid-template-areas:
|
||||||
|
"home prev mastered next sheet"
|
||||||
|
"confirm confirm confirm confirm confirm";
|
||||||
|
}
|
||||||
|
|
||||||
.footer-group {
|
.footer-group {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
@ -804,6 +811,10 @@ watch(remainingSeconds, async (value) => {
|
|||||||
padding-inline: 10px;
|
padding-inline: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-inner--wrong-review .footer-primary {
|
||||||
|
grid-area: confirm;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-finish {
|
.footer-finish {
|
||||||
grid-area: finish;
|
grid-area: finish;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -811,7 +822,7 @@ watch(remainingSeconds, async (value) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-mastered {
|
.footer-mastered {
|
||||||
grid-area: finish;
|
grid-area: mastered;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { computed, onMounted, reactive, shallowRef } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { fetchBankAttemptHistory, fetchBankTags, startBankAttempt, startPaperAttempt } from '@/api/quiz'
|
import { fetchBankAttemptHistory, fetchBankTags, startBankAttempt, startPaperAttempt } from '@/api/quiz'
|
||||||
import type { Paper, QuestionBank, QuizAttemptHistory } from '@/types/api'
|
import type { PageMeta, Paper, QuestionBank, QuizAttemptHistory } from '@/types/api'
|
||||||
import { fetchResources } from '@/api/quiz'
|
import { fetchResources } from '@/api/quiz'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -15,6 +15,9 @@ const historyDialogVisible = shallowRef(false)
|
|||||||
const randomBank = shallowRef<QuestionBank | null>(null)
|
const randomBank = shallowRef<QuestionBank | null>(null)
|
||||||
const historyBank = shallowRef<QuestionBank | null>(null)
|
const historyBank = shallowRef<QuestionBank | null>(null)
|
||||||
const historyAttempts = shallowRef<QuizAttemptHistory[]>([])
|
const historyAttempts = shallowRef<QuizAttemptHistory[]>([])
|
||||||
|
const historyMeta = shallowRef<PageMeta>({ current_page: 1, per_page: 10, total: 0, last_page: 1 })
|
||||||
|
const historyPage = shallowRef(1)
|
||||||
|
const historyPageSize = shallowRef(10)
|
||||||
const historyLoading = shallowRef(false)
|
const historyLoading = shallowRef(false)
|
||||||
const tags = shallowRef<Array<{ id: number; name: string }>>([])
|
const tags = shallowRef<Array<{ id: number; name: string }>>([])
|
||||||
const randomForm = reactive({
|
const randomForm = reactive({
|
||||||
@ -76,15 +79,37 @@ async function reviewWrong(bank: QuestionBank) {
|
|||||||
async function openHistory(bank: QuestionBank) {
|
async function openHistory(bank: QuestionBank) {
|
||||||
historyBank.value = bank
|
historyBank.value = bank
|
||||||
historyAttempts.value = []
|
historyAttempts.value = []
|
||||||
|
historyPage.value = 1
|
||||||
historyDialogVisible.value = true
|
historyDialogVisible.value = true
|
||||||
|
await loadHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHistory() {
|
||||||
|
if (!historyBank.value) return
|
||||||
historyLoading.value = true
|
historyLoading.value = true
|
||||||
try {
|
try {
|
||||||
historyAttempts.value = (await fetchBankAttemptHistory(bank.id)).data
|
const response = await fetchBankAttemptHistory(historyBank.value.id, {
|
||||||
|
page: historyPage.value,
|
||||||
|
per_page: historyPageSize.value,
|
||||||
|
})
|
||||||
|
historyAttempts.value = response.data.items
|
||||||
|
historyMeta.value = response.data.meta
|
||||||
} finally {
|
} finally {
|
||||||
historyLoading.value = false
|
historyLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function changeHistoryPage(page: number) {
|
||||||
|
historyPage.value = page
|
||||||
|
await loadHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeHistoryPageSize(size: number) {
|
||||||
|
historyPageSize.value = size
|
||||||
|
historyPage.value = 1
|
||||||
|
await loadHistory()
|
||||||
|
}
|
||||||
|
|
||||||
async function reviewAttempt(attempt: QuizAttemptHistory) {
|
async function reviewAttempt(attempt: QuizAttemptHistory) {
|
||||||
if (attempt.status !== 'submitted') {
|
if (attempt.status !== 'submitted') {
|
||||||
ElMessage.warning('只能回顾已提交的记录')
|
ElMessage.warning('只能回顾已提交的记录')
|
||||||
@ -93,6 +118,16 @@ async function reviewAttempt(attempt: QuizAttemptHistory) {
|
|||||||
await router.push(`/quiz/${attempt.id}`)
|
await router.push(`/quiz/${attempt.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value?: string) {
|
||||||
|
if (!value) return '-'
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return value
|
||||||
|
|
||||||
|
const pad = (number: number) => String(number).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
async function startPaper(paper: Paper) {
|
async function startPaper(paper: Paper) {
|
||||||
const response = await startPaperAttempt(paper.id)
|
const response = await startPaperAttempt(paper.id)
|
||||||
await router.push(`/quiz/${response.data.id}`)
|
await router.push(`/quiz/${response.data.id}`)
|
||||||
@ -122,7 +157,8 @@ onMounted(loadResources)
|
|||||||
<ElButton @click="start(bank, 'memorize')">顺序背题</ElButton>
|
<ElButton @click="start(bank, 'memorize')">顺序背题</ElButton>
|
||||||
<ElButton type="primary" @click="start(bank, 'sequence')">顺序刷题</ElButton>
|
<ElButton type="primary" @click="start(bank, 'sequence')">顺序刷题</ElButton>
|
||||||
<ElButton plain @click="openRandom(bank)">随机刷题</ElButton>
|
<ElButton plain @click="openRandom(bank)">随机刷题</ElButton>
|
||||||
<ElButton plain type="warning" :disabled="!bank.wrong_questions_count" @click="reviewWrong(bank)">回顾错题</ElButton>
|
<ElButton plain type="warning" :disabled="!bank.wrong_questions_count" @click="router.push(`/quiz/wrong-questions?bank_id=${bank.id}`)">错题列表</ElButton>
|
||||||
|
<ElButton plain type="warning" :disabled="!bank.wrong_questions_count" @click="reviewWrong(bank)">重刷错题</ElButton>
|
||||||
<ElButton plain @click="openHistory(bank)">历史回顾</ElButton>
|
<ElButton plain @click="openHistory(bank)">历史回顾</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@ -186,13 +222,24 @@ onMounted(loadResources)
|
|||||||
<article v-for="attempt in historyAttempts" :key="attempt.id" class="history-item">
|
<article v-for="attempt in historyAttempts" :key="attempt.id" class="history-item">
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ attempt.mode === 'random' ? '随机刷题' : '顺序刷题' }}</strong>
|
<strong>{{ attempt.mode === 'random' ? '随机刷题' : '顺序刷题' }}</strong>
|
||||||
<p>{{ attempt.submitted_at || attempt.started_at || '-' }}</p>
|
<p>{{ formatDateTime(attempt.submitted_at || attempt.started_at) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-meta">
|
<div class="history-meta">
|
||||||
<ElTag effect="plain">{{ attempt.correct_count }} / {{ attempt.total_questions }}</ElTag>
|
<ElTag effect="plain">{{ attempt.correct_count }} / {{ attempt.total_questions }}</ElTag>
|
||||||
<ElButton type="primary" plain @click="reviewAttempt(attempt)">回顾</ElButton>
|
<ElButton type="primary" plain @click="reviewAttempt(attempt)">回顾</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
<ElPagination
|
||||||
|
v-if="historyMeta.total > 0"
|
||||||
|
class="history-pagination"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="historyMeta.total"
|
||||||
|
:page-size="historyPageSize"
|
||||||
|
:current-page="historyPage"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
@update:current-page="changeHistoryPage"
|
||||||
|
@update:page-size="changeHistoryPageSize"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
</template>
|
</template>
|
||||||
@ -304,6 +351,11 @@ onMounted(loadResources)
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-pagination {
|
||||||
|
margin-top: 14px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.resource-actions :deep(.el-button) {
|
.resource-actions :deep(.el-button) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -333,6 +385,12 @@ onMounted(loadResources)
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-pagination {
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.random-dialog) {
|
:global(.random-dialog) {
|
||||||
margin-top: 8vh;
|
margin-top: 8vh;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user