<script lang="ts" setup>
|
import type { PersonTaskItem } from './types';
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useMessage } from '@jnpf/hooks';
|
|
import { startOnlineExam } from '#/api/x/tms/onlineExam';
|
import { getPersonTaskInfo, markPersonTaskLearned } from '#/api/x/tms/personTask';
|
import { TmsAttachmentPreview } from '#/views/x/tms/shared/attachmentPreview';
|
import { TMS_BTN } from '#/views/x/tms/shared/ui';
|
|
import '#/views/x/tms/shared/page.css';
|
|
defineOptions({ name: 'TmsPersonTaskLearn' });
|
|
const route = useRoute();
|
const router = useRouter();
|
const { createMessage } = useMessage();
|
|
const loading = ref(false);
|
const acting = ref(false);
|
const detail = ref<PersonTaskItem | null>(null);
|
const activeFileId = ref<string>();
|
const sessionSeconds = ref(0);
|
const notifiedFull = ref(false);
|
|
let learnTimer: ReturnType<typeof setInterval> | null = null;
|
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
let pendingSeconds = 0;
|
|
const files = computed(() => detail.value?.files || []);
|
const progressText = computed(() => {
|
const learned = detail.value?.learnedSeconds || 0;
|
const required = detail.value?.requiredSeconds || 0;
|
return `${formatDuration(learned)} / ${required > 0 ? formatDuration(required) : '未配置'}`;
|
});
|
const progressPercent = computed(() => {
|
const required = detail.value?.requiredSeconds || 0;
|
if (required <= 0) return 0;
|
return Math.min(100, Math.round(((detail.value?.learnedSeconds || 0) / required) * 100));
|
});
|
const signStatusText = computed(() => {
|
if (detail.value?.signed || detail.value?.signTime) {
|
return `已签到${detail.value.signTime ? `(${detail.value.signTime})` : ''}`;
|
}
|
return '未签到';
|
});
|
|
onMounted(async () => {
|
const ok = await loadData();
|
if (ok) {
|
startTimer();
|
}
|
});
|
|
onBeforeUnmount(() => {
|
stopTimer(true);
|
});
|
|
/** @returns 是否允许开始学习计时 */
|
async function loadData(): Promise<boolean> {
|
const id = String(route.params.id || '');
|
if (!id) {
|
router.replace('/tms/personTask');
|
return false;
|
}
|
loading.value = true;
|
try {
|
detail.value = await getPersonTaskInfo(id);
|
const signed = Boolean(detail.value.signed || detail.value.signTime);
|
if (!signed) {
|
router.replace('/tms/personTask');
|
return false;
|
}
|
if ((detail.value.learnedSeconds || 0) >= (detail.value.requiredSeconds || 0) && (detail.value.requiredSeconds || 0) > 0) {
|
notifiedFull.value = true;
|
}
|
return true;
|
} catch (e: any) {
|
createMessage.error(e?.message || '加载失败');
|
router.replace('/tms/personTask');
|
return false;
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function goBack() {
|
stopTimer(true).finally(() => {
|
router.push('/tms/personTask');
|
});
|
}
|
|
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}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
}
|
|
function startTimer() {
|
if (learnTimer) return;
|
learnTimer = setInterval(() => {
|
sessionSeconds.value += 1;
|
pendingSeconds += 1;
|
if (!detail.value) return;
|
detail.value.learnedSeconds = (detail.value.learnedSeconds || 0) + 1;
|
const required = detail.value.requiredSeconds || 0;
|
const learned = detail.value.learnedSeconds || 0;
|
detail.value.learnStatus = required > 0 && learned >= required ? '已学满' : learned > 0 ? '学习中' : '未学习';
|
}, 1000);
|
flushTimer = setInterval(() => {
|
flushLearn();
|
}, 8000);
|
}
|
|
async function flushLearn() {
|
if (!detail.value?.id || pendingSeconds <= 0) return;
|
const seconds = pendingSeconds;
|
pendingSeconds = 0;
|
try {
|
const result = await markPersonTaskLearned(detail.value.id, seconds);
|
detail.value.learnedSeconds = result.learnedSeconds ?? detail.value.learnedSeconds;
|
detail.value.requiredSeconds = result.requiredSeconds ?? detail.value.requiredSeconds;
|
detail.value.learnStatus = result.learnStatus || detail.value.learnStatus;
|
detail.value.bizStatus = result.bizStatus || detail.value.bizStatus;
|
detail.value.canExam = result.canExam;
|
detail.value.examTip = result.examTip;
|
if (result.message && (!notifiedFull.value || result.autoCompleted)) {
|
notifiedFull.value = true;
|
if (result.autoCompleted) {
|
createMessage.success(result.message);
|
} else {
|
createMessage.info(result.message);
|
}
|
}
|
} catch {
|
pendingSeconds += seconds;
|
}
|
}
|
|
async function stopTimer(flush = false) {
|
if (learnTimer) {
|
clearInterval(learnTimer);
|
learnTimer = null;
|
}
|
if (flushTimer) {
|
clearInterval(flushTimer);
|
flushTimer = null;
|
}
|
if (flush) {
|
await flushLearn();
|
}
|
}
|
|
async function handleExam() {
|
if (!detail.value?.paperId || !detail.value.canExam || acting.value) return;
|
await stopTimer(true);
|
acting.value = true;
|
try {
|
const paper = await startOnlineExam(detail.value.paperId);
|
if (paper?.autoSubmitted) {
|
createMessage.warning('考试时间已到,已自动交卷');
|
await loadData();
|
startTimer();
|
return;
|
}
|
sessionStorage.setItem('tms_online_exam_paper', JSON.stringify(paper));
|
sessionStorage.setItem('tms_online_exam_myPaperId', detail.value.paperId);
|
router.push('/tms/onlineExam/exam');
|
} catch (e: any) {
|
const msg = e?.message || '进入考试失败';
|
if (String(msg).includes('自动交卷')) {
|
createMessage.warning(msg);
|
await loadData();
|
} else {
|
createMessage.error(msg);
|
}
|
startTimer();
|
} finally {
|
acting.value = false;
|
}
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper tms-learn-page" v-loading="loading">
|
<div class="jnpf-content-wrapper-center">
|
<div class="jnpf-content-wrapper-content tms-learn-wrap" v-if="detail">
|
<div class="learn-toolbar">
|
<div class="learn-toolbar__main">
|
<div class="learn-toolbar__title" :title="detail.subject">{{ detail.subject || '学习' }}</div>
|
<div class="learn-toolbar__meta">
|
<span>本场 {{ formatDuration(sessionSeconds) }}</span>
|
<span>累计 {{ progressText }}</span>
|
<span>{{ signStatusText }}</span>
|
</div>
|
<a-progress
|
:percent="progressPercent"
|
:show-info="false"
|
size="small"
|
class="learn-progress"
|
/>
|
</div>
|
<div class="learn-toolbar__actions">
|
<a-button size="small" @click="goBack">{{ TMS_BTN.back }}</a-button>
|
<a-button v-if="detail.canExam" size="small" type="primary" :loading="acting" @click="handleExam">
|
{{ detail.passFlag === '0' || detail.passFlag === 'fail' ? TMS_BTN.retake : TMS_BTN.enterExam }}
|
</a-button>
|
</div>
|
</div>
|
|
<div class="learn-preview">
|
<TmsAttachmentPreview
|
v-model="activeFileId"
|
:files="files"
|
list-title="教材列表"
|
empty-list-text="暂无教材"
|
empty-preview-text="请选择左侧教材开始学习"
|
/>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.tms-learn-page,
|
.tms-learn-wrap {
|
height: 100%;
|
min-height: 0;
|
}
|
|
.tms-learn-wrap {
|
display: flex;
|
flex-direction: column;
|
overflow: hidden;
|
background: #fff;
|
padding: 8px 12px 10px;
|
box-sizing: border-box;
|
}
|
|
.learn-toolbar {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 12px;
|
flex-shrink: 0;
|
margin-bottom: 6px;
|
}
|
|
.learn-toolbar__main {
|
min-width: 0;
|
flex: 1;
|
}
|
|
.learn-toolbar__title {
|
font-size: 15px;
|
font-weight: 600;
|
line-height: 1.3;
|
color: rgba(0, 0, 0, 0.88);
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.learn-toolbar__meta {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px 14px;
|
margin-top: 2px;
|
font-size: 12px;
|
color: rgba(0, 0, 0, 0.45);
|
line-height: 1.4;
|
}
|
|
.learn-toolbar__actions {
|
display: flex;
|
flex-wrap: wrap;
|
justify-content: flex-end;
|
gap: 6px;
|
flex-shrink: 0;
|
}
|
|
.learn-progress {
|
margin-top: 4px;
|
margin-bottom: 0;
|
max-width: 420px;
|
}
|
|
.learn-preview {
|
flex: 1;
|
min-height: 0;
|
}
|
|
@media (max-width: 900px) {
|
.learn-toolbar {
|
flex-direction: column;
|
}
|
}
|
</style>
|