<script lang="ts" setup>
|
import type { PersonTaskItem } from './types';
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useMessage } from '@jnpf/hooks';
|
import {
|
Descriptions as ADescriptions,
|
DescriptionsItem as ADescriptionsItem,
|
Modal as AModal,
|
} from 'ant-design-vue';
|
|
import {
|
getPersonTaskInfo,
|
getPersonTaskQuizAnswers,
|
signPersonTask,
|
} from '#/api/x/tms/personTask';
|
import type { TmsQuizAnswerPaper } from '#/views/x/tms/record/types';
|
import QuizPaperView from '#/views/x/tms/shared/QuizPaperView.vue';
|
import QuizPopup from '#/views/x/tms/shared/QuizPopup.vue';
|
import { TMS_BTN } from '#/views/x/tms/shared/ui';
|
|
import {
|
labelOfCategory,
|
labelOfEvalMode,
|
labelOfPassFlag,
|
labelOfPersonStatus,
|
labelOfTrainMode,
|
labelOfYesNo,
|
loadPersonTaskDics,
|
} from './constants';
|
|
import '#/views/x/tms/shared/page.css';
|
|
defineOptions({ name: 'TmsPersonTaskDetail' });
|
|
const route = useRoute();
|
const router = useRouter();
|
const { createMessage } = useMessage();
|
|
const loading = ref(false);
|
const signing = ref(false);
|
const detail = ref<PersonTaskItem | null>(null);
|
const tipRemainSeconds = ref(0);
|
const quizOpen = ref(false);
|
const quizQuestions = ref<Array<{ id?: string; sortNo?: number; question?: string }>>([]);
|
const quizPaperOpen = ref(false);
|
const quizPaperLoading = ref(false);
|
const quizPaper = ref<TmsQuizAnswerPaper | null>(null);
|
const hasQuizPaper = ref(false);
|
let tipTimer: ReturnType<typeof setInterval> | null = null;
|
|
const progressText = computed(() => {
|
const learned = detail.value?.learnedSeconds || 0;
|
const required = detail.value?.requiredSeconds || 0;
|
return `${formatDuration(learned)} / ${required > 0 ? formatDuration(required) : '未配置'}`;
|
});
|
|
const isClosed = computed(() => detail.value?.bizStatus === 'cancelled' || detail.value?.bizStatus === 'expired');
|
const isSigned = computed(() => Boolean(detail.value?.signed || detail.value?.signTime));
|
const canLearn = computed(() => Boolean(detail.value?.id) && !isClosed.value && isSigned.value);
|
const showExam = computed(() => Boolean(detail.value?.canExam && detail.value?.paperId));
|
const tipReady = computed(() => tipRemainSeconds.value <= 0);
|
const statusAlert = computed(() => {
|
if (!detail.value) return '';
|
if (detail.value.bizStatus === 'cancelled') return '任务已取消,无法继续学习或考试。';
|
if (detail.value.bizStatus === 'expired') return '任务已过期,无法继续学习或考试。';
|
return '';
|
});
|
|
onMounted(async () => {
|
await loadPersonTaskDics();
|
await loadData();
|
});
|
|
onBeforeUnmount(() => {
|
stopTipTimer();
|
});
|
|
watch(
|
() => [detail.value?.canSign, detail.value?.tipSeconds, detail.value?.signed] as const,
|
() => {
|
startTipCountdown();
|
},
|
);
|
|
function startTipCountdown() {
|
stopTipTimer();
|
if (!detail.value?.canSign || detail.value?.signed) {
|
tipRemainSeconds.value = 0;
|
return;
|
}
|
const tip = Number(detail.value?.tipSeconds || 0);
|
if (tip <= 0) {
|
tipRemainSeconds.value = 0;
|
return;
|
}
|
tipRemainSeconds.value = tip;
|
tipTimer = setInterval(() => {
|
if (tipRemainSeconds.value <= 1) {
|
tipRemainSeconds.value = 0;
|
stopTipTimer();
|
return;
|
}
|
tipRemainSeconds.value -= 1;
|
}, 1000);
|
}
|
|
function stopTipTimer() {
|
if (tipTimer) {
|
clearInterval(tipTimer);
|
tipTimer = null;
|
}
|
}
|
|
async function loadData() {
|
const id = String(route.params.id || '');
|
if (!id) {
|
router.replace('/tms/personTask');
|
return;
|
}
|
loading.value = true;
|
try {
|
detail.value = await getPersonTaskInfo(id);
|
hasQuizPaper.value = false;
|
if (detail.value?.evalMode === 'quiz' && detail.value.passFlag) {
|
try {
|
await getPersonTaskQuizAnswers(id);
|
hasQuizPaper.value = true;
|
} catch {
|
hasQuizPaper.value = false;
|
}
|
}
|
} catch (e: any) {
|
createMessage.error(e?.message || '加载失败');
|
router.replace('/tms/personTask');
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function goBack() {
|
router.push('/tms/personTask');
|
}
|
|
function goLearn() {
|
if (!detail.value?.id || !canLearn.value) return;
|
router.push(`/tms/personTask/learn/${detail.value.id}`);
|
}
|
|
function goExam() {
|
if (!detail.value?.id || !showExam.value) return;
|
router.push(`/tms/personTask/learn/${detail.value.id}`);
|
}
|
|
async function handleSign() {
|
if (!detail.value?.id || !detail.value.canSign || signing.value || !tipReady.value) return;
|
signing.value = true;
|
try {
|
const result = await signPersonTask(detail.value.id);
|
detail.value.signTime = result.signTime;
|
detail.value.signed = result.signed ?? true;
|
detail.value.canSign = result.canSign ?? false;
|
detail.value.signTip = result.signTip || '已签到';
|
detail.value.bizStatus = result.bizStatus || detail.value.bizStatus;
|
detail.value.canExam = result.canExam;
|
detail.value.examTip = result.examTip;
|
tipRemainSeconds.value = 0;
|
stopTipTimer();
|
createMessage.success(result.message || '签到成功');
|
if (result.needQuiz && result.quizQuestions?.length) {
|
quizQuestions.value = result.quizQuestions;
|
quizOpen.value = true;
|
}
|
} catch (e: any) {
|
createMessage.error(e?.message || '签到失败');
|
} finally {
|
signing.value = false;
|
}
|
}
|
|
async function onQuizDone(result: { passFlag?: string }) {
|
if (detail.value) {
|
detail.value.passFlag = result.passFlag || detail.value.passFlag;
|
await loadData();
|
}
|
}
|
|
async function openMyQuizPaper() {
|
const id = String(detail.value?.id || route.params.id || '');
|
if (!id) return;
|
quizPaperLoading.value = true;
|
quizPaperOpen.value = true;
|
quizPaper.value = null;
|
try {
|
quizPaper.value = await getPersonTaskQuizAnswers(id);
|
hasQuizPaper.value = true;
|
} catch (e: any) {
|
quizPaperOpen.value = false;
|
createMessage.error(e?.message || '加载提问答卷失败');
|
} finally {
|
quizPaperLoading.value = false;
|
}
|
}
|
|
function formatDuration(seconds: number) {
|
const h = Math.floor(seconds / 3600);
|
const m = Math.floor((seconds % 3600) / 60);
|
const s = seconds % 60;
|
if (h > 0) return `${h}小时${m}分`;
|
if (m > 0) return `${m}分${s}秒`;
|
return `${s}秒`;
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper tms-person-task-page" v-loading="loading">
|
<div class="jnpf-content-wrapper-center">
|
<div class="jnpf-content-wrapper-content tms-person-task-wrap" v-if="detail">
|
<div class="tms-page-header">
|
<div>
|
<div class="tms-page-header__title">个人培训任务</div>
|
<div class="tms-page-header__sub">{{ detail.taskNo || '-' }} · {{ detail.subject || '-' }}</div>
|
</div>
|
<div class="tms-page-header__actions">
|
<a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
|
<a-button
|
v-if="detail.canSign"
|
type="primary"
|
:loading="signing"
|
:disabled="!tipReady"
|
@click="handleSign"
|
>
|
{{ tipReady ? TMS_BTN.sign : `签到(${tipRemainSeconds}s)` }}
|
</a-button>
|
<a-button v-if="canLearn" type="primary" :ghost="showExam" @click="goLearn">
|
{{ TMS_BTN.enterLearn }}
|
</a-button>
|
<a-button v-if="showExam" type="primary" @click="goExam">
|
{{ detail.passFlag === '0' || detail.passFlag === 'fail' ? TMS_BTN.retake : TMS_BTN.enterExam }}
|
</a-button>
|
<a-button v-if="hasQuizPaper" @click="openMyQuizPaper">查看提问答卷</a-button>
|
</div>
|
</div>
|
|
<a-alert v-if="statusAlert" class="tms-page-tip" type="warning" show-icon :message="statusAlert" />
|
|
<ADescriptions bordered :column="2" size="small">
|
<ADescriptionsItem label="培训编号">{{ detail.taskNo || '-' }}</ADescriptionsItem>
|
<ADescriptionsItem label="状态">{{ labelOfPersonStatus(detail.bizStatus) }}</ADescriptionsItem>
|
<ADescriptionsItem label="培训主题" :span="2">{{ detail.subject || '-' }}</ADescriptionsItem>
|
<ADescriptionsItem label="培训分类">{{ labelOfCategory(detail.category) }}</ADescriptionsItem>
|
<ADescriptionsItem label="培训方式">{{ labelOfTrainMode(detail.trainMode) }}</ADescriptionsItem>
|
<ADescriptionsItem label="考核方式">{{ labelOfEvalMode(detail.evalMode) }}</ADescriptionsItem>
|
<ADescriptionsItem label="签到状态">
|
{{
|
detail.signed || detail.signTime
|
? `已签到${detail.signTime ? `(${detail.signTime})` : ''}`
|
: '未签到'
|
}}
|
</ADescriptionsItem>
|
<ADescriptionsItem label="是否可考">
|
{{ labelOfYesNo(detail.canExam) }}
|
<span v-if="isSigned && !detail.canExam && detail.examTip" class="tip-inline">({{ detail.examTip }})</span>
|
</ADescriptionsItem>
|
<ADescriptionsItem label="学习进度">{{ detail.learnStatus || '-' }}</ADescriptionsItem>
|
<ADescriptionsItem label="要求课时">{{ detail.requiredHours ?? '-' }} 小时</ADescriptionsItem>
|
<ADescriptionsItem label="已学/需学">{{ progressText }}</ADescriptionsItem>
|
<ADescriptionsItem label="开始时间">{{ detail.startTime || '-' }}</ADescriptionsItem>
|
<ADescriptionsItem label="结束时间">{{ detail.endTime || '-' }}</ADescriptionsItem>
|
<ADescriptionsItem label="关闭时间">{{ detail.closeTime || '-' }}</ADescriptionsItem>
|
<ADescriptionsItem label="地点">{{ detail.placeName || '-' }}</ADescriptionsItem>
|
<ADescriptionsItem label="试卷">{{ detail.paperName || '-' }}</ADescriptionsItem>
|
<ADescriptionsItem label="成绩">{{ detail.examScore ?? '-' }}</ADescriptionsItem>
|
<ADescriptionsItem v-if="detail.evalMode === 'quiz'" label="是否合格">
|
{{ labelOfPassFlag(detail.passFlag) }}
|
</ADescriptionsItem>
|
<ADescriptionsItem label="培训要点" :span="2">{{ detail.keyPoints || '-' }}</ADescriptionsItem>
|
</ADescriptions>
|
</div>
|
</div>
|
|
<QuizPopup
|
v-model:open="quizOpen"
|
:person-task-id="detail?.id"
|
:questions="quizQuestions"
|
@done="onQuizDone"
|
/>
|
|
<AModal
|
v-model:open="quizPaperOpen"
|
title="我的提问答卷"
|
width="800px"
|
:footer="null"
|
:destroy-on-close="true"
|
wrap-class-name="tms-quiz-paper-modal"
|
>
|
<QuizPaperView :paper="quizPaper" :loading="quizPaperLoading" answer-label="我的答案" />
|
</AModal>
|
</div>
|
</template>
|
|
<style scoped>
|
.tms-person-task-page,
|
.tms-person-task-wrap {
|
height: 100%;
|
min-height: 0;
|
}
|
|
.tms-person-task-wrap {
|
overflow: auto;
|
background: #fff;
|
padding: 16px 20px 24px;
|
box-sizing: border-box;
|
}
|
|
.tip-inline {
|
color: #fa8c16;
|
margin-left: 4px;
|
}
|
</style>
|