| | |
| | | <script lang="ts" setup> |
| | | import type { OnlineExamDetail } from './types'; |
| | | import type { OnlineExamAttempt, OnlineExamDetail } from './types'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { useRoute, useRouter } from 'vue-router'; |
| | | |
| | | import { useMessage } from '@jnpf/hooks'; |
| | |
| | | Descriptions as ADescriptions, |
| | | DescriptionsItem as ADescriptionsItem, |
| | | Spin, |
| | | Table as ATable, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { getMyPaperDetail, startOnlineExam } from '#/api/x/tms/onlineExam'; |
| | | import { TMS_BTN } from '#/views/x/tms/shared/ui'; |
| | | |
| | | import { colorOfMyPaperStatus, labelOfMyPaperStatus } from './constants'; |
| | | import { |
| | | colorOfMyPaperStatus, |
| | | colorOfPassFlag, |
| | | labelOfGradeStatus, |
| | | labelOfMyPaperStatus, |
| | | labelOfPassFlag, |
| | | loadOnlineExamDics, |
| | | } from './constants'; |
| | | |
| | | import '#/views/x/tms/shared/page.css'; |
| | | |
| | | defineOptions({ name: 'TmsOnlineExamDetail' }); |
| | | |
| | |
| | | const starting = ref(false); |
| | | const detail = ref<OnlineExamDetail | null>(null); |
| | | |
| | | onMounted(() => { |
| | | loadDetail(); |
| | | const attemptColumns = [ |
| | | { title: '考试类型', dataIndex: 'attemptLabel', key: 'attemptLabel', width: 130 }, |
| | | { title: '状态', dataIndex: 'status', key: 'status', width: 100 }, |
| | | { title: '开始时间', dataIndex: 'startTime', key: 'startTime', width: 170 }, |
| | | { title: '交卷时间', dataIndex: 'submitTime', key: 'submitTime', width: 170 }, |
| | | { title: '得分', dataIndex: 'gotScore', key: 'gotScore', width: 90, align: 'center' as const }, |
| | | { title: '是否合格', dataIndex: 'passFlag', key: 'passFlag', width: 100, align: 'center' as const }, |
| | | { title: '阅卷', dataIndex: 'gradeStatus', key: 'gradeStatus', width: 100 }, |
| | | { title: '操作', key: 'action', width: 100, align: 'center' as const }, |
| | | ]; |
| | | |
| | | const attempts = computed(() => detail.value?.attempts || []); |
| | | |
| | | onMounted(async () => { |
| | | await loadOnlineExamDics(); |
| | | await loadDetail(); |
| | | }); |
| | | |
| | | async function loadDetail() { |
| | |
| | | router.push('/tms/onlineExam'); |
| | | } |
| | | |
| | | function goReview(row?: OnlineExamAttempt) { |
| | | if (!detail.value) return; |
| | | const examId = row?.examId || detail.value.examId; |
| | | if (!examId) { |
| | | createMessage.warning('暂无已交卷答卷可回顾'); |
| | | return; |
| | | } |
| | | router.push({ |
| | | path: `/tms/onlineExam/review/${detail.value.id}`, |
| | | query: { examId }, |
| | | }); |
| | | } |
| | | |
| | | async function handleStart() { |
| | | if (!detail.value || starting.value) return; |
| | | starting.value = true; |
| | | try { |
| | | const paper = await startOnlineExam(detail.value.id); |
| | | if (paper?.autoSubmitted) { |
| | | createMessage.warning('考试时间已到,已自动交卷'); |
| | | await loadDetail(); |
| | | return; |
| | | } |
| | | sessionStorage.setItem('tms_online_exam_paper', JSON.stringify(paper)); |
| | | sessionStorage.setItem('tms_online_exam_myPaperId', detail.value.id); |
| | | router.push('/tms/onlineExam/exam'); |
| | | } catch (e: any) { |
| | | createMessage.error(e?.message || '开始考试失败'); |
| | | const msg = e?.message || '开始考试失败'; |
| | | if (String(msg).includes('自动交卷')) { |
| | | createMessage.warning(msg); |
| | | await loadDetail(); |
| | | } else { |
| | | createMessage.error(msg); |
| | | } |
| | | } finally { |
| | | starting.value = false; |
| | | } |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="jnpf-content-wrapper"> |
| | | <div class="jnpf-content-wrapper tms-exam-detail-root"> |
| | | <div class="jnpf-content-wrapper-center"> |
| | | <div class="jnpf-content-wrapper-content tms-exam-detail-page"> |
| | | <Spin :spinning="loading"> |
| | | <div class="tms-exam-detail-header"> |
| | | <div class="text-base font-medium">考试详情</div> |
| | | <a-space> |
| | | <a-button @click="goBack">返回列表</a-button> |
| | | <div class="tms-page-header"> |
| | | <div> |
| | | <div class="tms-page-header__title">考试详情</div> |
| | | <div v-if="detail?.status === 'submitted'" class="tms-page-header__sub"> |
| | | 最近得分 {{ detail.gotScore ?? '-' }} / {{ detail.totalScore }} |
| | | <span v-if="detail.submitTime" class="ml-3">交卷时间:{{ detail.submitTime }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="tms-page-header__actions"> |
| | | <a-button @click="goBack">{{ TMS_BTN.back }}</a-button> |
| | | <a-button |
| | | v-if="detail && detail.status !== 'submitted'" |
| | | type="primary" |
| | | :loading="starting" |
| | | @click="handleStart" |
| | | > |
| | | {{ detail?.status === 'doing' ? '继续考试' : '开始考试' }} |
| | | {{ detail?.status === 'doing' ? TMS_BTN.continueExam : TMS_BTN.startExam }} |
| | | </a-button> |
| | | </a-space> |
| | | <a-button |
| | | v-else-if="detail?.canRetake" |
| | | type="primary" |
| | | :loading="starting" |
| | | @click="handleStart" |
| | | > |
| | | {{ TMS_BTN.retake }} |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <ADescriptions v-if="detail" bordered :column="2" size="middle"> |
| | | <ADescriptionsItem label="试卷名称" :span="2">{{ detail.paperName }}</ADescriptionsItem> |
| | | <ADescriptionsItem label="最近考试类型">{{ detail.attemptLabel || '首考' }}</ADescriptionsItem> |
| | | <ADescriptionsItem label="状态"> |
| | | <span :style="{ color: colorOfMyPaperStatus(detail.status) }"> |
| | | {{ labelOfMyPaperStatus(detail.status) }} |
| | | </span> |
| | | </ADescriptionsItem> |
| | | <ADescriptionsItem label="卷面总分">{{ detail.totalScore }}</ADescriptionsItem> |
| | | <ADescriptionsItem label="补考次数"> |
| | | {{ |
| | | detail.retakeLimit == null |
| | | ? '-' |
| | | : `剩余 ${detail.remainingRetakes ?? 0} / 上限 ${detail.retakeLimit}` |
| | | }} |
| | | </ADescriptionsItem> |
| | | <ADescriptionsItem label="考试时间" :span="2">{{ detail.examTimeText || '-' }}</ADescriptionsItem> |
| | | <ADescriptionsItem label="考试时长"> |
| | | {{ detail.durationMin != null ? `${detail.durationMin} 分钟` : '-' }} |
| | | </ADescriptionsItem> |
| | | <ADescriptionsItem label="合格分数">{{ detail.passScore ?? '-' }}</ADescriptionsItem> |
| | | <ADescriptionsItem label="开始时间">{{ detail.startTime || '-' }}</ADescriptionsItem> |
| | | <ADescriptionsItem label="交卷时间">{{ detail.submitTime || '-' }}</ADescriptionsItem> |
| | | <ADescriptionsItem label="得分"> |
| | | {{ detail.gotScore != null ? detail.gotScore : '-' }} |
| | | </ADescriptionsItem> |
| | | <ADescriptionsItem label="是否合格"> |
| | | <template v-if="detail.passFlag === '1'">合格</template> |
| | | <template v-else-if="detail.passFlag === '0'">不合格</template> |
| | | <template v-else>-</template> |
| | | </ADescriptionsItem> |
| | | </ADescriptions> |
| | | |
| | | <div class="history-block"> |
| | | <div class="history-title">历史答卷</div> |
| | | <ATable |
| | | size="middle" |
| | | :columns="attemptColumns" |
| | | :data-source="attempts" |
| | | :pagination="false" |
| | | row-key="examId" |
| | | bordered |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'status'"> |
| | | <span :style="{ color: colorOfMyPaperStatus(record.status) }"> |
| | | {{ labelOfMyPaperStatus(record.status) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'gotScore'"> |
| | | {{ record.gotScore != null ? record.gotScore : '-' }} |
| | | </template> |
| | | <template v-else-if="column.key === 'passFlag'"> |
| | | <span :style="{ color: colorOfPassFlag(record.passFlag) }"> |
| | | {{ labelOfPassFlag(record.passFlag) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'gradeStatus'"> |
| | | {{ labelOfGradeStatus(record.gradeStatus) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <a |
| | | v-if="record.status === 'submitted'" |
| | | @click.prevent="goReview(record as OnlineExamAttempt)" |
| | | > |
| | | 回顾 |
| | | </a> |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | | </template> |
| | | </ATable> |
| | | <div v-if="!attempts.length" class="history-empty">暂无答卷记录</div> |
| | | </div> |
| | | |
| | | <a-alert |
| | | v-if="detail && detail.status !== 'submitted' && !attempts.some((a) => a.status === 'submitted')" |
| | | class="mt-4" |
| | | type="info" |
| | | show-icon |
| | | :message="detail.status === 'doing' ? '考试进行中,交卷后可在历史答卷中回顾。' : '尚未开考,暂无答题内容。'" |
| | | /> |
| | | </Spin> |
| | | </div> |
| | | </div> |
| | |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .tms-exam-detail-page { |
| | | background: #fff; |
| | | padding: 20px 24px; |
| | | .tms-exam-detail-root { |
| | | height: 100%; |
| | | overflow: auto; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .tms-exam-detail-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | .tms-exam-detail-page { |
| | | height: 100%; |
| | | min-height: 0; |
| | | overflow-x: hidden; |
| | | overflow-y: auto !important; |
| | | background: #fff; |
| | | padding: 20px 24px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .history-block { |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .history-title { |
| | | font-weight: 600; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .history-empty { |
| | | margin-top: 12px; |
| | | color: #8c8c8c; |
| | | text-align: center; |
| | | } |
| | | </style> |