@@ -11,7 +11,7 @@
|
||||
</BoxedIconTitleBlock>
|
||||
|
||||
<SkeletonLoaderBlock v-if="isLoading && !exam" :rows="2" :cols-per-row="1" />
|
||||
<StudentExamSummary v-else-if="exam" :exam="exam" @start="onStart" />
|
||||
<StudentExamSummary v-else-if="exam" :exam="examSummary" @start="onStart" />
|
||||
|
||||
<SimpleTitleIconBlock title="تعداد تلاش ها برای این آزمون" class="se__list-title">
|
||||
<template #header-icon>
|
||||
@@ -32,14 +32,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import { useStudentExamQuery } from '@/services/query/student-exams'
|
||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
|
||||
import StudentExamSummary from '@/features/student/exams/components/StudentExamSummary.vue'
|
||||
import { useStudentExamQuery, useStudentExamResultsQuery } from '@/services/query/student-exams'
|
||||
import StudentExamAttemptItem from '@/features/student/exams/components/StudentExamAttemptItem.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -51,7 +51,40 @@ const { data: exam, isLoading } = useStudentExamQuery(examId, {
|
||||
enabled: () => !!examId.value,
|
||||
})
|
||||
|
||||
const attempts = computed(() => exam.value?.attempts ?? [])
|
||||
const resultsFilters = ref({})
|
||||
const resultsPagination = ref({ page: 1, perPage: 50 })
|
||||
const { data: results } = useStudentExamResultsQuery(resultsFilters, resultsPagination)
|
||||
|
||||
// Attempts on this exam, newest first, shaped for StudentExamAttemptItem.
|
||||
const attempts = computed(() =>
|
||||
(results.value?.data ?? [])
|
||||
.filter((a) => String(a.examId) === String(examId.value))
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
title: exam.value?.title || 'آزمون',
|
||||
score: a.score,
|
||||
date: a.submittedAt,
|
||||
status: a.isPassed ? 'passed' : 'failed',
|
||||
statusLabel: a.isPassed ? 'قبول شده' : 'مردود',
|
||||
}))
|
||||
)
|
||||
|
||||
const examSummary = computed(() => {
|
||||
const passed = attempts.value.some((a) => a.status === 'passed')
|
||||
const status = passed ? 'passed' : attempts.value.length > 0 ? 'failed' : 'not_started'
|
||||
const statusLabel = passed
|
||||
? 'قبول شده اید'
|
||||
: attempts.value.length > 0
|
||||
? 'قبول نشده اید'
|
||||
: 'شروع نشده'
|
||||
return {
|
||||
...exam.value,
|
||||
attempts: attempts.value,
|
||||
attemptsCount: attempts.value.length,
|
||||
status,
|
||||
statusLabel,
|
||||
}
|
||||
})
|
||||
|
||||
const onStart = () => {
|
||||
router.push({ name: 'student-take-exam', params: { id: examId.value } }).catch(() => {})
|
||||
|
||||
@@ -52,11 +52,12 @@ import { toast } from 'vue3-toastify'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import StudentExamQuestion from '@/features/student/exams/components/StudentExamQuestion.vue'
|
||||
import {
|
||||
useStartStudentExamMutation,
|
||||
useStudentExamQuery,
|
||||
useSubmitStudentExamAttemptMutation,
|
||||
} from '@/services/query/student-exams'
|
||||
@@ -79,12 +80,13 @@ const onAnswer = (questionId, value) => {
|
||||
else answers[questionId] = value
|
||||
}
|
||||
|
||||
const startMutation = useStartStudentExamMutation()
|
||||
const remaining = ref(0)
|
||||
let intervalId = null
|
||||
|
||||
const startTimer = (minutes) => {
|
||||
const tickFrom = (seconds) => {
|
||||
if (intervalId) clearInterval(intervalId)
|
||||
remaining.value = Math.max(0, Math.floor(minutes * 60))
|
||||
remaining.value = Math.max(0, Math.floor(seconds))
|
||||
intervalId = setInterval(() => {
|
||||
if (remaining.value <= 0) {
|
||||
clearInterval(intervalId)
|
||||
@@ -96,13 +98,20 @@ const startTimer = (minutes) => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => exam.value?.durationMinutes,
|
||||
(minutes) => {
|
||||
if (minutes != null) startTimer(minutes)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
// Begin (or resume) the attempt; the server returns deadlineAt which drives the
|
||||
// countdown. Falls back to the exam duration if no deadline comes back.
|
||||
const beginAttempt = async () => {
|
||||
try {
|
||||
const res = await startMutation.mutateAsync(examId.value)
|
||||
const attempt = res?.data ?? res
|
||||
const deadlineMs = attempt?.deadlineAt ? new Date(attempt.deadlineAt).getTime() : null
|
||||
if (deadlineMs) tickFrom((deadlineMs - Date.now()) / 1000)
|
||||
else if (exam.value?.durationMinutes != null) tickFrom(exam.value.durationMinutes * 60)
|
||||
} catch (error) {
|
||||
toast.error(error?.response?.data?.message || 'امکان شروع آزمون وجود ندارد.')
|
||||
router.push({ name: 'student-exam', params: { id: examId.value } }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (intervalId) clearInterval(intervalId)
|
||||
@@ -149,7 +158,11 @@ const onTimeout = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!examId.value) router.push({ name: 'student-dashboard' })
|
||||
if (!examId.value) {
|
||||
router.push({ name: 'student-dashboard' })
|
||||
return
|
||||
}
|
||||
beginAttempt()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
<div class="ssd__prompt">
|
||||
<SvgIcon name="warning" :size="22" color="#b8b8b8" class="ssd__prompt-icon" />
|
||||
<p class="ssd__prompt-text">
|
||||
{{ session?.homeworkPrompt || 'متنی برای این جلسه ثبت نشده است.' }}
|
||||
{{
|
||||
session?.homeworkPrompt || homework?.description || 'متنی برای این جلسه ثبت نشده است.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +71,7 @@
|
||||
<div class="ssd__hint">
|
||||
<SvgIcon name="warning" :size="22" color="#b8b8b8" />
|
||||
<p class="ssd__hint-text">
|
||||
{{ session?.homeworkHint || '' }}
|
||||
{{ session?.homeworkHint || homework?.title || '' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +105,7 @@ import ImageUploader from '@/components/form/ImageUploader.vue'
|
||||
import VideoPlayerBlock from '@/components/blocks/VideoPlayerBlock.vue'
|
||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import { useStudentHomeworksListQuery } from '@/services/query/student-homeworks'
|
||||
import {
|
||||
useStudentSessionQuery,
|
||||
useSubmitStudentHomeworkMutation,
|
||||
@@ -116,6 +119,15 @@ const { data: session, isLoading } = useStudentSessionQuery(sessionId, {
|
||||
enabled: () => !!sessionId.value,
|
||||
})
|
||||
|
||||
// The homework for this session comes from the dedicated endpoint (the session
|
||||
// payload itself doesn't embed homeworks).
|
||||
const homeworkFilters = computed(() => ({ sessionId: sessionId.value }))
|
||||
const homeworkPagination = ref({ page: 1, perPage: 20 })
|
||||
const { data: homeworksData } = useStudentHomeworksListQuery(homeworkFilters, homeworkPagination, {
|
||||
enabled: () => !!sessionId.value,
|
||||
})
|
||||
const homework = computed(() => (homeworksData.value?.data ?? [])[0] ?? null)
|
||||
|
||||
const contentKind = computed(() => {
|
||||
const s = session.value
|
||||
if (!s) return ''
|
||||
@@ -135,7 +147,9 @@ const canSubmit = computed(() => !!(homeworkAudio.value || homeworkImage.value))
|
||||
const submitMutation = useSubmitStudentHomeworkMutation()
|
||||
const uploadMediaMutation = useUploadMediaMutation()
|
||||
|
||||
const homeworkId = computed(() => session.value?.homeworkId ?? session.value?.homework?.id ?? null)
|
||||
const homeworkId = computed(
|
||||
() => homework.value?.id ?? session.value?.homeworkId ?? session.value?.homework?.id ?? null
|
||||
)
|
||||
|
||||
const uploadHomeworkFile = async (file) => {
|
||||
const fd = objectToFormData({ file, purpose: 'homework_file', context: 'homework' })
|
||||
|
||||
Reference in New Issue
Block a user