<script lang="ts" setup>
|
/**
|
* TMS 首页 · 学员视图
|
*/
|
import type { MyPaperListItem } from '#/views/x/tms/onlineExam/types';
|
import type { PersonTaskItem } from '#/views/x/tms/personTask/types';
|
import type { SelfTestRecordItem } from '#/views/x/tms/selfTest/types';
|
import type { TrainArchiveCatalogItem, TrainArchiveProfile } from '#/views/x/tms/trainArchive/types';
|
|
import { computed, onMounted, ref } from 'vue';
|
import { useRouter } from 'vue-router';
|
|
import { useUserStore } from '@vben/stores';
|
|
import { getMyPaperList } from '#/api/x/tms/onlineExam';
|
import { getMyPersonTaskList } from '#/api/x/tms/personTask';
|
import { getSelfTestList } from '#/api/x/tms/selfTest';
|
import { getMyTrainArchive } from '#/api/x/tms/trainArchive';
|
import { labelOfPersonStatus, loadPersonTaskDics } from '#/views/x/tms/personTask/constants';
|
import { isPassNo, isPassYes } from '#/views/x/tms/shared/dic';
|
|
defineOptions({ name: 'TmsTraineeHome' });
|
|
defineProps<{
|
displayName: string;
|
todayText: string;
|
}>();
|
|
const router = useRouter();
|
const userStore = useUserStore();
|
|
const loading = ref(false);
|
const allTasks = ref<PersonTaskItem[]>([]);
|
const allPapers = ref<MyPaperListItem[]>([]);
|
const selfTests = ref<SelfTestRecordItem[]>([]);
|
const catalogList = ref<TrainArchiveCatalogItem[]>([]);
|
const archiveProfile = ref<TrainArchiveProfile | null>(null);
|
|
const deptText = computed(() => {
|
const info: any = userStore.getUserInfo || {};
|
return (
|
archiveProfile.value?.deptName ||
|
info.organizeName ||
|
info.departmentName ||
|
info.deptName ||
|
''
|
);
|
});
|
|
const recentTasks = computed(() => allTasks.value.slice(0, 5));
|
const recentPapers = computed(() => allPapers.value.slice(0, 5));
|
const recentSelfTests = computed(() => selfTests.value.slice(0, 4));
|
const recentCatalog = computed(() => catalogList.value.slice(0, 4));
|
|
const heroSummary = computed(() => {
|
const activeTasks = allTasks.value.filter((t) => !isDoneStatus(t.bizStatus)).length;
|
const examTodo = allPapers.value.filter((p) => p.status === 'notStarted' || p.status === 'doing').length;
|
return [
|
{ label: '进行中培训', value: activeTasks, path: '/tms/personTask', tone: 'blue' },
|
{ label: '待考 / 考试中', value: examTodo, path: '/tms/onlineExam', tone: 'cyan' },
|
{ label: '档案条目', value: catalogList.value.length, path: '/tms/trainArchive', tone: 'amber' },
|
{ label: '自测记录', value: selfTests.value.length, path: '/tms/selfTest', tone: 'violet' },
|
];
|
});
|
|
const flowSteps = [
|
{ step: '01', title: '签到', desc: '到达培训时间后完成签到', path: '/tms/personTask', tone: 'blue' },
|
{ step: '02', title: '学习', desc: '按要求完成教材学习时长', path: '/tms/personTask', tone: 'teal' },
|
{ step: '03', title: '考试', desc: '进入在线考试并提交答卷', path: '/tms/onlineExam', tone: 'violet' },
|
{ step: '04', title: '归档', desc: '培训记录归档后进入个人档案', path: '/tms/trainArchive', tone: 'amber' },
|
];
|
|
const tips = [
|
{
|
title: '如何开始培训?',
|
text: '打开「我的培训」,对未签到任务先签到,再进入学习页计时学习。',
|
path: '/tms/personTask',
|
},
|
{
|
title: '考试入口在哪?',
|
text: '学满或满足开考条件后,可在「我的培训」或「在线考试」进入试卷。',
|
path: '/tms/onlineExam',
|
},
|
{
|
title: '成绩何时进档案?',
|
text: '个人培训档案目录在培训记录归档后写入,学习完成不会立刻出现。',
|
path: '/tms/trainArchive',
|
},
|
{
|
title: '想练习怎么办?',
|
text: '可用「自我测试」按题库抽题练习,不影响正式考试成绩。',
|
path: '/tms/selfTest',
|
},
|
];
|
|
const moreLinks = [
|
{ title: '试卷管理', desc: '组卷与发布', path: '/tms/paper', tone: 'blue' },
|
{ title: '题库管理', desc: '试题维护', path: '/tms/question', tone: 'teal' },
|
{ title: '阅卷管理', desc: '主观题阅卷', path: '/tms/examGrade', tone: 'violet' },
|
{ title: '考试成绩', desc: '成绩汇总查询', path: '/tms/examScore', tone: 'amber' },
|
{ title: '自我测试', desc: '题库练习入口', path: '/tms/selfTest', tone: 'rose' },
|
{ title: '培训记录', desc: '签到归档维护', path: '/tms/record', tone: 'slate' },
|
];
|
|
const shortcuts = [
|
{ key: 'personTask', mark: '培', title: '我的培训', desc: '签到、学习、进入考试', path: '/tms/personTask', tone: 'blue' },
|
{ key: 'onlineExam', mark: '考', title: '在线考试', desc: '我的试卷与成绩', path: '/tms/onlineExam', tone: 'teal' },
|
{ key: 'trainArchive', mark: '档', title: '个人档案', desc: '已归档培训目录', path: '/tms/trainArchive', tone: 'amber' },
|
{ key: 'selfTest', mark: '测', title: '自我测试', desc: '题库练习与自测', path: '/tms/selfTest', tone: 'violet' },
|
{ key: 'task', mark: '任', title: '培训任务', desc: '任务维护与发布', path: '/tms/task', tone: 'slate' },
|
{ key: 'record', mark: '录', title: '培训记录', desc: '签到、归档与名单', path: '/tms/record', tone: 'rose' },
|
];
|
|
onMounted(() => {
|
void loadHome();
|
});
|
|
async function loadHome() {
|
loading.value = true;
|
try {
|
await loadPersonTaskDics().catch(() => undefined);
|
const [taskRes, paperRes, selfRes, archive] = await Promise.all([
|
getMyPersonTaskList({ currentPage: 1, pageSize: 10 }).catch(() => null),
|
getMyPaperList({ currentPage: 1, pageSize: 10 }).catch(() => null),
|
getSelfTestList({ currentPage: 1, pageSize: 8 }).catch(() => null),
|
getMyTrainArchive().catch(() => null),
|
]);
|
allTasks.value = taskRes?.list || [];
|
allPapers.value = paperRes?.list || [];
|
selfTests.value = selfRes?.list || [];
|
catalogList.value = archive?.catalogList || [];
|
archiveProfile.value = archive?.profile || null;
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function go(path: string) {
|
router.push(path);
|
}
|
|
function openTask(row: PersonTaskItem) {
|
if (!row?.id) return;
|
router.push(`/tms/personTask/detail/${row.id}`);
|
}
|
|
function openPaper(row: MyPaperListItem) {
|
if (!row?.id) return;
|
router.push(`/tms/onlineExam/detail/${row.id}`);
|
}
|
|
function openSelfTest(row: SelfTestRecordItem) {
|
if (!row?.id) return;
|
router.push(`/tms/selfTest/detail/${row.id}`);
|
}
|
|
function paperStatusText(status?: string) {
|
if (status === 'doing') return '考试中';
|
if (status === 'submitted') return '已交卷';
|
return '未开始';
|
}
|
|
function paperStatusTone(status?: string) {
|
if (status === 'doing') return 'blue';
|
if (status === 'submitted') return 'violet';
|
return 'slate';
|
}
|
|
function selfTestStatusText(row: SelfTestRecordItem) {
|
const s = String(row.testStatus || '');
|
if (s === 'submitted' || s === 'done' || s === '已交卷') return '已交卷';
|
if (s === 'doing' || s === '进行中') return '进行中';
|
return s || '记录';
|
}
|
|
function selfTestStatusTone(row: SelfTestRecordItem) {
|
const s = String(row.testStatus || '');
|
if (s === 'submitted' || s === 'done' || s === '已交卷') return 'violet';
|
if (s === 'doing' || s === '进行中') return 'blue';
|
return 'slate';
|
}
|
|
function passTone(flag?: string) {
|
if (isPassYes(flag)) return 'green';
|
if (isPassNo(flag)) return 'red';
|
return 'slate';
|
}
|
|
function passText(flag?: string) {
|
if (isPassYes(flag)) return '合格';
|
if (isPassNo(flag)) return '不合格';
|
return '未出结果';
|
}
|
|
function isDoneStatus(s?: string) {
|
const v = String(s || '').toLowerCase();
|
return v === 'completed' || v === 'done' || v === 'cancelled' || v === 'cancel';
|
}
|
|
function taskStatusTone(bizStatus?: string) {
|
const s = String(bizStatus || '').toLowerCase();
|
if (s.includes('sign') || s === 'signed') return 'green';
|
if (s.includes('learn') || s.includes('学习')) return 'violet';
|
if (s.includes('exam') || s.includes('考')) return 'cyan';
|
if (isDoneStatus(s)) return 'slate';
|
if (s.includes('cancel')) return 'red';
|
return 'blue';
|
}
|
</script>
|
|
<template>
|
<div>
|
<section class="home-hero">
|
<div class="home-hero__main">
|
<div class="home-hero__hello">你好,{{ displayName }}</div>
|
<div class="home-hero__sub">
|
<span>TMS · 我的培训</span>
|
<span class="home-hero__dot">·</span>
|
<span>{{ todayText }}</span>
|
<template v-if="deptText">
|
<span class="home-hero__dot">·</span>
|
<span>{{ deptText }}</span>
|
</template>
|
</div>
|
<div class="home-hero__actions">
|
<button type="button" class="hero-btn hero-btn--solid" @click="go('/tms/personTask')">
|
进入我的培训
|
</button>
|
<button type="button" class="hero-btn hero-btn--ghost" @click="go('/tms/onlineExam')">
|
进入在线考试
|
</button>
|
</div>
|
</div>
|
<div class="home-hero__side">
|
<button
|
v-for="item in heroSummary"
|
:key="item.label"
|
type="button"
|
class="hero-chip"
|
:class="`hero-chip--${item.tone}`"
|
@click="go(item.path)"
|
>
|
<div class="hero-chip__value">{{ item.value }}</div>
|
<div class="hero-chip__label">{{ item.label }}</div>
|
</button>
|
</div>
|
</section>
|
|
<div v-loading="loading" class="home-below">
|
<section class="home-block">
|
<div class="home-section__title">快捷入口</div>
|
<div class="home-shortcuts">
|
<button
|
v-for="item in shortcuts"
|
:key="item.key"
|
type="button"
|
class="home-shortcut"
|
:class="`home-shortcut--${item.tone}`"
|
@click="go(item.path)"
|
>
|
<span class="home-shortcut__mark">{{ item.mark }}</span>
|
<span class="home-shortcut__text">
|
<span class="home-shortcut__title">{{ item.title }}</span>
|
<span class="home-shortcut__desc">{{ item.desc }}</span>
|
</span>
|
</button>
|
</div>
|
<div class="flow-row">
|
<button
|
v-for="(item, idx) in flowSteps"
|
:key="item.step"
|
type="button"
|
class="flow-card"
|
:class="`flow-card--${item.tone}`"
|
@click="go(item.path)"
|
>
|
<div class="flow-card__step">{{ item.step }}</div>
|
<div class="flow-card__body">
|
<div class="flow-card__title">{{ item.title }}</div>
|
<div class="flow-card__desc">{{ item.desc }}</div>
|
</div>
|
<div v-if="idx < flowSteps.length - 1" class="flow-card__arrow" aria-hidden="true">›</div>
|
</button>
|
</div>
|
</section>
|
|
<div class="home-panels">
|
<section class="home-panel">
|
<div class="home-panel__head">
|
<span class="home-panel__title">我的培训任务</span>
|
<a class="home-panel__more" @click.prevent="go('/tms/personTask')">全部</a>
|
</div>
|
<div v-if="recentTasks.length" class="home-list">
|
<div
|
v-for="row in recentTasks"
|
:key="row.id"
|
class="home-list__row"
|
@click="openTask(row)"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name" :title="row.subject">{{ row.subject || '-' }}</div>
|
<div class="home-list__meta">
|
<span>{{ row.taskNo || '-' }}</span>
|
<span>{{ row.startTime || '-' }}</span>
|
</div>
|
</div>
|
<div class="home-list__tag" :class="`tone-tag--${taskStatusTone(row.bizStatus)}`">
|
{{ labelOfPersonStatus(row.bizStatus) }}
|
</div>
|
</div>
|
</div>
|
<div v-else class="home-empty">暂无培训任务</div>
|
</section>
|
|
<section class="home-panel">
|
<div class="home-panel__head">
|
<span class="home-panel__title">我的考试</span>
|
<a class="home-panel__more" @click.prevent="go('/tms/onlineExam')">全部</a>
|
</div>
|
<div v-if="recentPapers.length" class="home-list">
|
<div
|
v-for="row in recentPapers"
|
:key="row.id"
|
class="home-list__row"
|
@click="openPaper(row)"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name" :title="row.paperName">{{ row.paperName || '-' }}</div>
|
<div class="home-list__meta">
|
<span>{{ row.examTimeText || row.examStart || '-' }}</span>
|
<span v-if="row.gotScore != null">得分 {{ row.gotScore }}</span>
|
</div>
|
</div>
|
<div class="home-list__tag" :class="`tone-tag--${paperStatusTone(row.status)}`">
|
{{ paperStatusText(row.status) }}
|
</div>
|
</div>
|
</div>
|
<div v-else class="home-empty">暂无考试试卷</div>
|
</section>
|
|
<section class="home-panel">
|
<div class="home-panel__head">
|
<span class="home-panel__title">自我测试记录</span>
|
<a class="home-panel__more" @click.prevent="go('/tms/selfTest/records')">全部</a>
|
</div>
|
<div v-if="recentSelfTests.length" class="home-list">
|
<div
|
v-for="row in recentSelfTests"
|
:key="row.id"
|
class="home-list__row"
|
@click="openSelfTest(row)"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name" :title="row.bankName">
|
{{ row.bankName || '自我测试' }}
|
</div>
|
<div class="home-list__meta">
|
<span>{{ row.startTime || row.submitTime || '-' }}</span>
|
<span v-if="row.scoreRate != null">正确率 {{ row.scoreRate }}%</span>
|
<span v-else-if="row.correctCount != null">
|
对 {{ row.correctCount }}/{{ row.totalCount }}
|
</span>
|
</div>
|
</div>
|
<div class="home-list__tag" :class="`tone-tag--${selfTestStatusTone(row)}`">
|
{{ selfTestStatusText(row) }}
|
</div>
|
</div>
|
</div>
|
<div v-else class="home-empty">暂无自测记录,去题库练一练</div>
|
</section>
|
|
<section class="home-panel">
|
<div class="home-panel__head">
|
<span class="home-panel__title">个人培训档案</span>
|
<a class="home-panel__more" @click.prevent="go('/tms/trainArchive')">全部</a>
|
</div>
|
<div v-if="archiveProfile" class="archive-profile">
|
<div class="archive-profile__row">
|
<span>档案编号</span>
|
<b>{{ archiveProfile.archiveNo || '-' }}</b>
|
</div>
|
<div class="archive-profile__row">
|
<span>岗位</span>
|
<b>{{ archiveProfile.mainPostName || '-' }}</b>
|
</div>
|
</div>
|
<div v-if="recentCatalog.length" class="home-list">
|
<div
|
v-for="row in recentCatalog"
|
:key="row.id"
|
class="home-list__row home-list__row--static"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name" :title="row.trainContent">
|
{{ row.trainContent || '-' }}
|
</div>
|
<div class="home-list__meta">
|
<span>{{ row.recordNo || '-' }}</span>
|
<span>{{ row.trainStart || '-' }}</span>
|
</div>
|
</div>
|
<div class="home-list__tag" :class="`tone-tag--${passTone(row.passFlag)}`">
|
{{ passText(row.passFlag) }}
|
</div>
|
</div>
|
</div>
|
<div v-else class="home-empty">暂无已归档培训目录</div>
|
</section>
|
</div>
|
|
<section class="home-block">
|
<div class="home-section__title">使用说明</div>
|
<div class="tips-grid">
|
<button
|
v-for="tip in tips"
|
:key="tip.title"
|
type="button"
|
class="tip-card"
|
@click="go(tip.path)"
|
>
|
<div class="tip-card__title">{{ tip.title }}</div>
|
<div class="tip-card__text">{{ tip.text }}</div>
|
</button>
|
</div>
|
</section>
|
|
<section class="home-block">
|
<div class="home-section__title">更多功能</div>
|
<div class="more-grid">
|
<button
|
v-for="item in moreLinks"
|
:key="item.title"
|
type="button"
|
class="more-card"
|
:class="`more-card--${item.tone}`"
|
@click="go(item.path)"
|
>
|
<div class="more-card__title">{{ item.title }}</div>
|
<div class="more-card__desc">{{ item.desc }}</div>
|
</button>
|
</div>
|
</section>
|
</div>
|
</div>
|
</template>
|