<script lang="ts" setup>
|
/**
|
* TMS 首页 · 管理视图
|
*/
|
import type { EChartsOption } from 'echarts';
|
|
import type { TrainStatsRow } from '#/api/x/tms/trainStats';
|
import type { GradeSessionListItem } from '#/views/x/tms/examGrade/types';
|
import type { TmsRecordListItem } from '#/views/x/tms/record/types';
|
import type { TaskEntity } from '#/views/x/tms/task/types';
|
|
import { computed, onMounted, ref } from 'vue';
|
import { useRouter } from 'vue-router';
|
|
import { useUserStore } from '@vben/stores';
|
import dayjs from 'dayjs';
|
|
import { getGradeSessionList } from '#/api/x/tms/examGrade';
|
import { getTmsRecordList } from '#/api/x/tms/record';
|
import { getTaskList } from '#/api/x/tms/task';
|
import { getTrainStatsList } from '#/api/x/tms/trainStats';
|
import { getFlowTodoCount } from '#/api/workFlow/flowMonitor';
|
import { getFlowBeforeList, getFlowLaunchList } from '#/api/workFlow/task';
|
import { useFlowState } from '#/hooks/flow/useFlowStatus';
|
import {
|
labelOfArchiveStatus,
|
loadRecordDics,
|
} from '#/views/x/tms/record/constants';
|
import { labelOfPublishStatus, loadTaskDics } from '#/views/x/tms/task/constants';
|
|
import HomeChart from './HomeChart.vue';
|
|
defineOptions({ name: 'TmsAdminHome' });
|
|
defineProps<{
|
displayName: string;
|
todayText: string;
|
}>();
|
|
interface FlowTodoItem {
|
id?: string;
|
taskId?: string;
|
flowId?: string;
|
fullName?: string;
|
flowName?: string;
|
startTime?: string;
|
status?: number;
|
/** 待签 / 待办 */
|
flowKind?: 'toSign' | 'todo';
|
}
|
|
interface FlowHubItem {
|
id: string;
|
title: string;
|
path: string;
|
mark: string;
|
tone: string;
|
count: number;
|
}
|
|
const router = useRouter();
|
const userStore = useUserStore();
|
const { getFlowStatusContent } = useFlowState();
|
|
const loading = ref(false);
|
const flowHub = ref<FlowHubItem[]>([
|
{ id: 'flowLaunch', title: '我发起的', path: '/workFlow/flowLaunch', mark: '发', tone: 'blue', count: 0 },
|
{ id: 'flowToSign', title: '我的待签', path: '/workFlow/flowToSign', mark: '签', tone: 'teal', count: 0 },
|
{ id: 'flowTodo', title: '我的待办', path: '/workFlow/flowTodo', mark: '办', tone: 'amber', count: 0 },
|
{ id: 'flowDoing', title: '我的在办', path: '/workFlow/flowDoing', mark: '在', tone: 'violet', count: 0 },
|
{ id: 'flowDone', title: '我的已办', path: '/workFlow/flowDone', mark: '完', tone: 'slate', count: 0 },
|
{ id: 'flowCirculate', title: '抄送我的', path: '/workFlow/flowCirculate', mark: '抄', tone: 'rose', count: 0 },
|
]);
|
const flowActionItems = ref<FlowTodoItem[]>([]);
|
const flowLaunchItems = ref<FlowTodoItem[]>([]);
|
const gradeSessions = ref<GradeSessionListItem[]>([]);
|
const archiveRecords = ref<TmsRecordListItem[]>([]);
|
const pendingArchiveCount = ref(0);
|
const readyArchiveCount = ref(0);
|
const tasks = ref<TaskEntity[]>([]);
|
const taskTotal = ref(0);
|
const statsRows = ref<TrainStatsRow[]>([]);
|
|
const deptText = computed(() => {
|
const info: any = userStore.getUserInfo || {};
|
return info.organizeName || info.departmentName || info.deptName || '';
|
});
|
|
const flowPendingCount = computed(() => {
|
const toSign = flowHub.value.find((x) => x.id === 'flowToSign')?.count || 0;
|
const todo = flowHub.value.find((x) => x.id === 'flowTodo')?.count || 0;
|
return toSign + todo;
|
});
|
const recentFlows = computed(() => flowActionItems.value.slice(0, 5));
|
const recentLaunches = computed(() => flowLaunchItems.value.slice(0, 5));
|
const recentGrades = computed(() =>
|
[...gradeSessions.value]
|
.filter((x) => (x.pendingCount || 0) > 0)
|
.sort((a, b) => (b.pendingCount || 0) - (a.pendingCount || 0))
|
.slice(0, 5),
|
);
|
const pendingGradeTotal = computed(() =>
|
gradeSessions.value.reduce((sum, x) => sum + (x.pendingCount || 0), 0),
|
);
|
const recentArchives = computed(() => archiveRecords.value.slice(0, 5));
|
const recentTasks = computed(() => tasks.value.slice(0, 5));
|
const statsPreview = computed(() => statsRows.value.slice(0, 8));
|
|
const statsSummary = computed(() => {
|
const rows = statsRows.value;
|
const sessionCount = rows.reduce((s, r) => s + (r.sessionCount || 0), 0);
|
const personCount = rows.reduce((s, r) => s + (r.personCount || 0), 0);
|
const signedCount = rows.reduce((s, r) => s + (r.signedCount || 0), 0);
|
const passedCount = rows.reduce((s, r) => s + (r.passedCount || 0), 0);
|
const signRate = personCount > 0 ? Math.round((signedCount / personCount) * 1000) / 10 : 0;
|
const passRate = personCount > 0 ? Math.round((passedCount / personCount) * 1000) / 10 : 0;
|
return { sessionCount, personCount, signedCount, passedCount, signRate, passRate };
|
});
|
|
const chartRows = computed(() => statsPreview.value);
|
|
const axisLabelStyle = {
|
interval: 0,
|
fontSize: 11,
|
lineHeight: 14,
|
width: 64,
|
overflow: 'break' as const,
|
};
|
|
/** 各部门场次 / 人次柱状图 */
|
const barOption = computed<EChartsOption>(() => {
|
const list = chartRows.value;
|
const labels = list.map((r, i) => r.dimName || r.dimCode || `维度${i + 1}`);
|
return {
|
color: ['#1677ff', '#13a8a8', '#52c41a'],
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
legend: { data: ['场次', '应培', '签到'], top: 0 },
|
grid: { left: 16, right: 16, top: 36, bottom: 8, containLabel: true },
|
xAxis: {
|
type: 'category',
|
data: labels,
|
axisLabel: axisLabelStyle,
|
},
|
yAxis: {
|
type: 'value',
|
minInterval: 1,
|
splitLine: { lineStyle: { type: 'dashed' } },
|
},
|
series: [
|
{
|
name: '场次',
|
type: 'bar',
|
barMaxWidth: 18,
|
data: list.map((r) => Number(r.sessionCount) || 0),
|
},
|
{
|
name: '应培',
|
type: 'bar',
|
barMaxWidth: 18,
|
data: list.map((r) => Number(r.personCount) || 0),
|
},
|
{
|
name: '签到',
|
type: 'bar',
|
barMaxWidth: 18,
|
data: list.map((r) => Number(r.signedCount) || 0),
|
},
|
],
|
};
|
});
|
|
/** 签到率 / 合格率折线 */
|
const rateOption = computed<EChartsOption>(() => {
|
const list = chartRows.value;
|
const labels = list.map((r, i) => r.dimName || r.dimCode || `维度${i + 1}`);
|
return {
|
color: ['#1677ff', '#eb2f96'],
|
tooltip: {
|
trigger: 'axis',
|
valueFormatter: (v: any) => `${v}%`,
|
},
|
legend: { data: ['签到率', '合格率'], top: 0 },
|
grid: { left: 16, right: 24, top: 36, bottom: 8, containLabel: true },
|
xAxis: {
|
type: 'category',
|
data: labels,
|
boundaryGap: true,
|
axisLabel: axisLabelStyle,
|
},
|
yAxis: {
|
type: 'value',
|
min: 0,
|
max: 100,
|
axisLabel: { formatter: '{value}%' },
|
splitLine: { lineStyle: { type: 'dashed' } },
|
},
|
series: [
|
{
|
name: '签到率',
|
type: 'line',
|
smooth: true,
|
symbolSize: 6,
|
data: list.map((r) => toPercent(r.signRate)),
|
},
|
{
|
name: '合格率',
|
type: 'line',
|
smooth: true,
|
symbolSize: 6,
|
data: list.map((r) => toPercent(r.passRate)),
|
},
|
],
|
};
|
});
|
|
/** 签到结构饼图 */
|
const pieOption = computed<EChartsOption>(() => {
|
const s = statsSummary.value;
|
return {
|
color: ['#1677ff', '#d9d9d9'],
|
tooltip: { trigger: 'item', formatter: '{b}: {c}({d}%)' },
|
legend: { bottom: 0, left: 'center' },
|
series: [
|
{
|
name: '签到结构',
|
type: 'pie',
|
radius: ['42%', '68%'],
|
center: ['50%', '46%'],
|
avoidLabelOverlap: true,
|
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
label: { formatter: '{b}\n{d}%', fontSize: 11 },
|
data: [
|
{ name: '已签到', value: s.signedCount },
|
{ name: '未签到', value: Math.max(s.personCount - s.signedCount, 0) },
|
].filter((x) => x.value > 0),
|
},
|
],
|
};
|
});
|
|
const heroSummary = computed(() => [
|
{
|
label: '待签 / 待办',
|
value: flowPendingCount.value,
|
path: '/workFlow/flowTodo',
|
tone: 'blue',
|
},
|
{
|
label: '待阅卷',
|
value: pendingGradeTotal.value,
|
path: '/tms/examGrade',
|
tone: 'violet',
|
},
|
{
|
label: '待/可归档',
|
value: pendingArchiveCount.value + readyArchiveCount.value,
|
path: '/tms/record',
|
tone: 'amber',
|
},
|
{
|
label: '培训任务',
|
value: taskTotal.value || tasks.value.length,
|
path: '/tms/task',
|
tone: 'cyan',
|
},
|
]);
|
|
const shortcuts = [
|
{ key: 'task', mark: '任', title: '培训任务', desc: '任务维护与发布', path: '/tms/task', tone: 'blue' },
|
{ key: 'record', mark: '录', title: '培训记录', desc: '签到、归档与名单', path: '/tms/record', tone: 'amber' },
|
{ key: 'examGrade', mark: '阅', title: '阅卷管理', desc: '主观题待批改', path: '/tms/examGrade', tone: 'violet' },
|
{ key: 'examScore', mark: '绩', title: '考试成绩', desc: '成绩汇总查询', path: '/tms/examScore', tone: 'teal' },
|
{ key: 'paper', mark: '卷', title: '试卷管理', desc: '组卷与发布', path: '/tms/paper', tone: 'slate' },
|
{ key: 'question', mark: '题', title: '题库管理', desc: '试题维护', path: '/tms/question', tone: 'rose' },
|
{ key: 'trainStats', mark: '统', title: '培训统计', desc: '签到率 / 合格率', path: '/tms/trainStats', tone: 'cyan' },
|
];
|
|
onMounted(() => {
|
void loadHome();
|
});
|
|
async function loadHome() {
|
loading.value = true;
|
try {
|
await Promise.all([loadTaskDics().catch(() => undefined), loadRecordDics().catch(() => undefined)]);
|
|
const [
|
flowCountRes,
|
launchRes,
|
toSignRes,
|
todoRes,
|
gradeRes,
|
pendingRes,
|
readyRes,
|
taskRes,
|
statsRes,
|
] = await Promise.all([
|
getFlowTodoCount({
|
flowToSignType: [],
|
flowTodoType: [],
|
flowDoingType: [],
|
flowDoneType: [],
|
flowCirculateType: [],
|
}).catch(() => null),
|
getFlowLaunchList({ currentPage: 1, pageSize: 5 }).catch(() => null),
|
getFlowBeforeList({ category: 0, currentPage: 1, pageSize: 5 }).catch(() => null),
|
getFlowBeforeList({ category: 1, currentPage: 1, pageSize: 5 }).catch(() => null),
|
getGradeSessionList({ currentPage: 1, pageSize: 20 }).catch(() => null),
|
getTmsRecordList({ currentPage: 1, pageSize: 10, archiveStatus: 'pending' }).catch(() => null),
|
getTmsRecordList({ currentPage: 1, pageSize: 10, archiveStatus: 'ready', listMode: 'ready' }).catch(
|
() => null,
|
),
|
getTaskList({ currentPage: 1, pageSize: 10 }).catch(() => null),
|
getTrainStatsList({ groupBy: 'dept' }).catch(() => null),
|
]);
|
|
applyFlowHubCounts(unwrapData(flowCountRes), launchRes);
|
flowLaunchItems.value = unwrapList<FlowTodoItem>(launchRes);
|
|
const toSignList = unwrapList<FlowTodoItem>(toSignRes).map((x) => ({ ...x, flowKind: 'toSign' as const }));
|
const todoList = unwrapList<FlowTodoItem>(todoRes).map((x) => ({ ...x, flowKind: 'todo' as const }));
|
flowActionItems.value = [...toSignList, ...todoList];
|
|
gradeSessions.value = unwrapList<GradeSessionListItem>(gradeRes);
|
|
const pendingList = unwrapList<TmsRecordListItem>(pendingRes);
|
const readyList = unwrapList<TmsRecordListItem>(readyRes);
|
pendingArchiveCount.value = Number(
|
(pendingRes as any)?.pagination?.total || pendingList.length || 0,
|
);
|
readyArchiveCount.value = Number((readyRes as any)?.pagination?.total || readyList.length || 0);
|
archiveRecords.value = [
|
...readyList.map((r) => ({ ...r, archiveStatus: r.archiveStatus || ('ready' as const) })),
|
...pendingList.map((r) => ({ ...r, archiveStatus: r.archiveStatus || ('pending' as const) })),
|
];
|
|
const taskList = unwrapList<TaskEntity>(taskRes);
|
tasks.value = taskList;
|
taskTotal.value = Number(
|
(taskRes as any)?.pagination?.total || (taskRes as any)?.total || taskList.length || 0,
|
);
|
statsRows.value = Array.isArray(statsRes) ? statsRes : unwrapList<TrainStatsRow>(statsRes);
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function unwrapData(res: any) {
|
if (!res) return {};
|
if (res.data && typeof res.data === 'object' && !Array.isArray(res.data)) return res.data;
|
return res;
|
}
|
|
function unwrapList<T>(res: any): T[] {
|
if (!res) return [];
|
if (Array.isArray(res)) return res as T[];
|
if (Array.isArray(res.list)) return res.list as T[];
|
if (Array.isArray(res.data?.list)) return res.data.list as T[];
|
return [];
|
}
|
|
function applyFlowHubCounts(countData: Record<string, any>, launchRes: any) {
|
const launchTotal = Number(
|
(launchRes as any)?.pagination?.total ||
|
(launchRes as any)?.total ||
|
unwrapList(launchRes).length ||
|
0,
|
);
|
flowHub.value = flowHub.value.map((item) => {
|
if (item.id === 'flowLaunch') return { ...item, count: launchTotal };
|
if (item.id === 'flowToSign') return { ...item, count: Number(countData.flowToSign) || 0 };
|
if (item.id === 'flowTodo') return { ...item, count: Number(countData.flowTodo) || 0 };
|
if (item.id === 'flowDoing') return { ...item, count: Number(countData.flowDoing) || 0 };
|
if (item.id === 'flowDone') return { ...item, count: Number(countData.flowDone) || 0 };
|
if (item.id === 'flowCirculate') return { ...item, count: Number(countData.flowCirculate) || 0 };
|
return item;
|
});
|
}
|
|
function go(path: string) {
|
router.push(path);
|
}
|
|
function openFlow() {
|
router.push('/workFlow/flowTodo');
|
}
|
|
function openFlowHub(item: FlowHubItem) {
|
router.push(item.path);
|
}
|
|
function flowKindText(kind?: string) {
|
if (kind === 'toSign') return '待签';
|
if (kind === 'todo') return '待办';
|
return '流程';
|
}
|
|
function flowKindTone(kind?: string) {
|
if (kind === 'toSign') return 'teal';
|
if (kind === 'todo') return 'orange';
|
return 'blue';
|
}
|
|
function launchStatusTone(status?: number) {
|
if (status === 1) return 'blue';
|
if (status === 2) return 'green';
|
if (status === 0 || status === 8 || status === 9) return 'orange';
|
return 'slate';
|
}
|
|
function formatDateTime(v?: string | null) {
|
if (!v) return '-';
|
const d = dayjs(v);
|
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : String(v);
|
}
|
|
function openGrade(row: GradeSessionListItem) {
|
if (!row?.id) return;
|
router.push(`/tms/examGrade/session/${row.id}`);
|
}
|
|
function openRecord(row: TmsRecordListItem) {
|
if (!row?.id) return;
|
router.push(`/tms/record/detail/${row.id}`);
|
}
|
|
function openTask(row: TaskEntity) {
|
if (!row?.id) return;
|
router.push(`/tms/task/detail/${row.id}`);
|
}
|
|
function archiveTone(status?: string) {
|
if (status === 'ready') return 'green';
|
if (status === 'pending') return 'orange';
|
return 'slate';
|
}
|
|
function publishTone(status?: string) {
|
if (status === 'published') return 'blue';
|
if (status === 'done') return 'green';
|
if (status === 'cancelled') return 'red';
|
return 'slate';
|
}
|
|
function formatRate(v?: number) {
|
if (v == null || Number.isNaN(Number(v))) return '-';
|
return `${toPercent(v)}%`;
|
}
|
|
/** 兼容 0~1 与 0~100 两种比率口径 */
|
function toPercent(v?: number | null) {
|
if (v == null || Number.isNaN(Number(v))) return 0;
|
const n = Number(v);
|
return n <= 1 ? Math.round(n * 1000) / 10 : Math.round(n * 10) / 10;
|
}
|
</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="openFlow">
|
进入审批中心
|
</button>
|
<button type="button" class="hero-btn hero-btn--ghost" @click="go('/tms/task')">
|
进入培训任务
|
</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 home-shortcuts--admin">
|
<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>
|
</section>
|
|
<section class="home-block">
|
<div class="home-section__title">
|
审批中心
|
<a class="home-panel__more" style="margin-left: auto; font-weight: 400" @click.prevent="openFlow">
|
更多
|
</a>
|
</div>
|
<div class="flow-hub">
|
<button
|
v-for="item in flowHub"
|
:key="item.id"
|
type="button"
|
class="flow-hub__item"
|
:class="`flow-hub__item--${item.tone}`"
|
@click="openFlowHub(item)"
|
>
|
<span class="flow-hub__mark">{{ item.mark }}</span>
|
<span class="flow-hub__title">{{ item.title }}</span>
|
<span v-if="item.count > 0" class="flow-hub__badge">{{ item.count > 99 ? '99+' : item.count }}</span>
|
</button>
|
<button type="button" class="flow-hub__item flow-hub__item--launch" @click="go('/workFlow/flowQuickLaunch')">
|
<span class="flow-hub__mark">+</span>
|
<span class="flow-hub__title">发起流程</span>
|
</button>
|
</div>
|
<div v-if="recentFlows.length" class="home-list" style="margin-top: 8px">
|
<div
|
v-for="row in recentFlows"
|
:key="`${row.flowKind}-${row.id || row.taskId}`"
|
class="home-list__row"
|
@click="go(row.flowKind === 'toSign' ? '/workFlow/flowToSign' : '/workFlow/flowTodo')"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name" :title="row.fullName">{{ row.fullName || '-' }}</div>
|
<div class="home-list__meta">
|
<span>{{ row.flowName || '-' }}</span>
|
<span>{{ formatDateTime(row.startTime) }}</span>
|
</div>
|
</div>
|
<div class="home-list__tag" :class="`tone-tag--${flowKindTone(row.flowKind)}`">
|
{{ flowKindText(row.flowKind) }}
|
</div>
|
</div>
|
</div>
|
<div v-else class="home-empty">暂无待签 / 待办事项</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/examGrade')">全部</a>
|
</div>
|
<div v-if="recentGrades.length" class="home-list">
|
<div
|
v-for="row in recentGrades"
|
:key="row.id"
|
class="home-list__row"
|
@click="openGrade(row)"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name" :title="row.taskSubject || row.paperName">
|
{{ row.taskSubject || row.paperName || '-' }}
|
</div>
|
<div class="home-list__meta">
|
<span>{{ row.taskNo || '-' }}</span>
|
<span>{{ row.paperName || '-' }}</span>
|
</div>
|
</div>
|
<div class="home-list__tag tone-tag--violet">待批 {{ row.pendingCount || 0 }}</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/record/ready')">可归档列表</a>
|
</div>
|
<div v-if="recentArchives.length" class="home-list">
|
<div
|
v-for="row in recentArchives"
|
:key="row.id"
|
class="home-list__row"
|
@click="openRecord(row)"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name" :title="row.subject">{{ row.subject || '-' }}</div>
|
<div class="home-list__meta">
|
<span>{{ row.recordNo || '-' }}</span>
|
<span>{{ row.endTime || row.closeTime || row.startTime || '-' }}</span>
|
</div>
|
</div>
|
<div class="home-list__tag" :class="`tone-tag--${archiveTone(row.archiveStatus)}`">
|
{{ labelOfArchiveStatus(row.archiveStatus) }}
|
</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>
|
<div class="home-panel__actions">
|
<a class="home-panel__more" @click.prevent="go('/tms/task/create')">新建</a>
|
<a class="home-panel__more" @click.prevent="go('/tms/task')">全部</a>
|
</div>
|
</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--${publishTone(row.publishStatus)}`">
|
{{ labelOfPublishStatus(row.publishStatus) }}
|
</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('/workFlow/flowLaunch')">全部</a>
|
</div>
|
<div v-if="recentLaunches.length" class="home-list">
|
<div
|
v-for="row in recentLaunches"
|
:key="row.id || row.taskId"
|
class="home-list__row"
|
@click="go('/workFlow/flowLaunch')"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name" :title="row.fullName">{{ row.fullName || '-' }}</div>
|
<div class="home-list__meta">
|
<span>{{ row.flowName || '-' }}</span>
|
<span>{{ formatDateTime(row.startTime) }}</span>
|
</div>
|
</div>
|
<div class="home-list__tag" :class="`tone-tag--${launchStatusTone(row.status)}`">
|
{{ getFlowStatusContent(row.status) || '-' }}
|
</div>
|
</div>
|
</div>
|
<div v-else class="home-empty">暂无我发起的流程</div>
|
</section>
|
</div>
|
|
<section class="home-block">
|
<div class="home-section__title">
|
培训统计摘要
|
<a class="home-panel__more" style="margin-left: auto; font-weight: 400" @click.prevent="go('/tms/trainStats')">
|
查看详情
|
</a>
|
</div>
|
<div class="stats-summary">
|
<div class="stats-summary__item">
|
<div class="stats-summary__value">{{ statsSummary.sessionCount }}</div>
|
<div class="stats-summary__label">培训场次</div>
|
</div>
|
<div class="stats-summary__item">
|
<div class="stats-summary__value">{{ statsSummary.personCount }}</div>
|
<div class="stats-summary__label">应培人次</div>
|
</div>
|
<div class="stats-summary__item">
|
<div class="stats-summary__value">{{ statsSummary.signRate }}%</div>
|
<div class="stats-summary__label">签到率</div>
|
</div>
|
<div class="stats-summary__item">
|
<div class="stats-summary__value">{{ statsSummary.passRate }}%</div>
|
<div class="stats-summary__label">合格率</div>
|
</div>
|
</div>
|
|
<div v-if="chartRows.length" class="stats-charts">
|
<div class="stats-chart-card">
|
<div class="stats-chart-card__title">场次 / 人次(按部门)</div>
|
<HomeChart :option="barOption" height="240px" />
|
</div>
|
<div class="stats-chart-card">
|
<div class="stats-chart-card__title">签到率 / 合格率</div>
|
<HomeChart :option="rateOption" height="240px" />
|
</div>
|
<div class="stats-chart-card stats-chart-card--pie">
|
<div class="stats-chart-card__title">签到结构</div>
|
<HomeChart :option="pieOption" height="240px" />
|
</div>
|
</div>
|
|
<div v-if="statsPreview.length" class="home-list" style="margin-top: 8px">
|
<div
|
v-for="row in statsPreview"
|
:key="row.dimCode || row.dimName"
|
class="home-list__row home-list__row--static"
|
>
|
<div class="home-list__main">
|
<div class="home-list__name">{{ row.dimName || row.dimCode || '-' }}</div>
|
<div class="home-list__meta">
|
<span>场次 {{ row.sessionCount ?? 0 }}</span>
|
<span>人次 {{ row.personCount ?? 0 }}</span>
|
<span>签到率 {{ formatRate(row.signRate) }}</span>
|
<span>合格率 {{ formatRate(row.passRate) }}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div v-else class="home-empty">暂无统计数据</div>
|
</section>
|
</div>
|
</div>
|
</template>
|