<script lang="ts" setup>
|
import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
|
|
import type { TrainStatsQuery, TrainStatsRow } from '#/api/x/tms/trainStats';
|
|
import { nextTick, onMounted, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { useModal } from '@jnpf/ui/modal';
|
import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
|
import dayjs from 'dayjs';
|
|
import { exportTrainStats, getTrainStatsList } from '#/api/x/tms/trainStats';
|
import { TMS_DIC_FIELD_NAMES } from '#/views/x/tms/shared/dic';
|
import { saveExportBlob } from '#/views/x/tms/shared/exportDownload';
|
import { TMS_BTN } from '#/views/x/tms/shared/ui';
|
import {
|
EVAL_MODE_OPTIONS,
|
TASK_CATEGORY_OPTIONS,
|
labelOfCategory,
|
labelOfEvalMode,
|
loadRecordDics,
|
} from '#/views/x/tms/record/constants';
|
|
import DetailModal from './DetailModal.vue';
|
|
import '#/views/x/tms/shared/page.css';
|
|
defineOptions({ name: 'TmsTrainStats' });
|
|
const { createMessage } = useMessage();
|
const exporting = ref(false);
|
const groupByRef = ref('category');
|
const [registerDetailModal, { openModal: openDetailModal }] = useModal();
|
|
/** 与字典 Select 一致:value=enCode,避免回显英文编码 */
|
const groupOptions = [
|
{ id: 'category', enCode: 'category', fullName: '按培训分类' },
|
{ id: 'dept', enCode: 'dept', fullName: '按部门' },
|
{ id: 'evalMode', enCode: 'evalMode', fullName: '按考核方式' },
|
];
|
|
type StatsRowView = TrainStatsRow & { _rowKey?: string; groupBy?: string };
|
|
const columns: BasicColumn[] = [
|
{
|
title: '统计维度',
|
dataIndex: 'dimName',
|
minWidth: 180,
|
customRender: ({ record }) => dimText(record as StatsRowView),
|
},
|
{ title: '培训场次', dataIndex: 'sessionCount', width: 100, align: 'center' },
|
{ title: '应到人次', dataIndex: 'personCount', width: 100, align: 'center' },
|
{ title: '签到人次', dataIndex: 'signedCount', width: 100, align: 'center' },
|
{ title: '合格人次', dataIndex: 'passedCount', width: 100, align: 'center' },
|
{
|
title: '签到率',
|
dataIndex: 'signRate',
|
width: 100,
|
align: 'center',
|
customRender: ({ record }) => rateText((record as TrainStatsRow).signRate),
|
},
|
{
|
title: '合格率',
|
dataIndex: 'passRate',
|
width: 100,
|
align: 'center',
|
customRender: ({ record }) => rateText((record as TrainStatsRow).passRate),
|
},
|
];
|
|
const [registerTable, { reload, getForm }] = useVxeTable({
|
api: fetchList,
|
columns,
|
immediate: false,
|
// 维度切换后 dimCode 可能相同,复合 key 避免 VXE 复用旧行导致「列不更新」
|
rowKey: '_rowKey',
|
useSearchForm: true,
|
pagination: false,
|
formConfig: {
|
baseColProps: { span: 6 },
|
compact: true,
|
showAdvancedButton: true,
|
alwaysShowLines: 1,
|
autoAdvancedLine: 4,
|
schemas: [
|
{
|
field: 'groupBy',
|
label: '聚合维度',
|
component: 'Select',
|
defaultValue: 'category',
|
componentProps: {
|
options: groupOptions,
|
fieldNames: TMS_DIC_FIELD_NAMES,
|
placeholder: '请选择',
|
onChange: (v: string) => {
|
// BasicForm 先调 schema.onChange,再 setFormModel;此处立刻 reload 会读到旧 groupBy
|
const gb = v || 'category';
|
groupByRef.value = gb;
|
nextTick(() => {
|
reload({ searchInfo: { groupBy: gb } });
|
});
|
},
|
},
|
},
|
{
|
field: 'category',
|
label: '培训分类',
|
component: 'Select',
|
componentProps: {
|
allowClear: true,
|
placeholder: '全部',
|
options: TASK_CATEGORY_OPTIONS.value,
|
fieldNames: TMS_DIC_FIELD_NAMES,
|
},
|
},
|
{
|
field: 'evalMode',
|
label: '考核方式',
|
component: 'Select',
|
componentProps: {
|
allowClear: true,
|
placeholder: '全部',
|
options: EVAL_MODE_OPTIONS.value,
|
fieldNames: TMS_DIC_FIELD_NAMES,
|
},
|
},
|
{
|
field: 'deptId',
|
label: '部门',
|
component: 'OrganizeSelect',
|
componentProps: {
|
placeholder: '请选择部门',
|
multiple: false,
|
allowClear: true,
|
},
|
},
|
{
|
field: 'startTimeFrom',
|
label: '开始日期',
|
component: 'DatePicker',
|
defaultValue: dayjs().startOf('year').format('YYYY-MM-DD'),
|
componentProps: {
|
format: 'YYYY-MM-DD',
|
valueFormat: 'YYYY-MM-DD',
|
placeholder: '开始日期',
|
style: { width: '100%' },
|
},
|
},
|
{
|
field: 'startTimeTo',
|
label: '结束日期',
|
component: 'DatePicker',
|
defaultValue: dayjs().format('YYYY-MM-DD'),
|
componentProps: {
|
format: 'YYYY-MM-DD',
|
valueFormat: 'YYYY-MM-DD',
|
placeholder: '结束日期',
|
style: { width: '100%' },
|
},
|
},
|
],
|
},
|
actionColumn: {
|
width: 110,
|
title: '操作',
|
dataIndex: 'action',
|
fixed: 'right',
|
},
|
});
|
|
onMounted(async () => {
|
await loadRecordDics();
|
getForm()?.updateSchema?.([
|
{
|
field: 'category',
|
componentProps: {
|
allowClear: true,
|
placeholder: '全部',
|
options: TASK_CATEGORY_OPTIONS.value,
|
fieldNames: TMS_DIC_FIELD_NAMES,
|
},
|
},
|
{
|
field: 'evalMode',
|
componentProps: {
|
allowClear: true,
|
placeholder: '全部',
|
options: EVAL_MODE_OPTIONS.value,
|
fieldNames: TMS_DIC_FIELD_NAMES,
|
},
|
},
|
]);
|
reload();
|
});
|
|
function dimText(row: StatsRowView) {
|
// 优先用后端已解析的中文名;仅在仍是编码时再按行内 groupBy 兜底翻字典
|
const name = (row.dimName || '').trim();
|
const code = (row.dimCode || '').trim();
|
if (name && name !== code && name !== '-') {
|
return name;
|
}
|
const gb = row.groupBy || groupByRef.value;
|
if (gb === 'category') {
|
return labelOfCategory(code) || name || code || '-';
|
}
|
if (gb === 'evalMode') {
|
return labelOfEvalMode(code) || name || code || '-';
|
}
|
return name || code || '-';
|
}
|
|
function rateText(v?: number | null) {
|
if (v == null || Number.isNaN(Number(v))) return '-';
|
return `${Number(v)}%`;
|
}
|
|
/** 组织选择可能返回 string / string[] / 对象 */
|
function normalizeDeptId(raw: any): string | undefined {
|
if (raw == null || raw === '') return undefined;
|
if (Array.isArray(raw)) {
|
const first = raw[0];
|
if (first == null || first === '') return undefined;
|
if (typeof first === 'object') return String(first.id || first.organizeId || '').trim() || undefined;
|
return String(first).trim() || undefined;
|
}
|
if (typeof raw === 'object') {
|
return String(raw.id || raw.organizeId || '').trim() || undefined;
|
}
|
return String(raw).trim() || undefined;
|
}
|
|
function formatDayStart(v: any): string | undefined {
|
if (v == null || v === '') return undefined;
|
const d = dayjs(v);
|
if (!d.isValid()) return undefined;
|
return `${d.format('YYYY-MM-DD')} 00:00:00`;
|
}
|
|
function formatDayEnd(v: any): string | undefined {
|
if (v == null || v === '') return undefined;
|
const d = dayjs(v);
|
if (!d.isValid()) return undefined;
|
return `${d.format('YYYY-MM-DD')} 23:59:59`;
|
}
|
|
function buildQuery(params: Record<string, any> = {}): TrainStatsQuery {
|
const form = getForm?.()?.getFieldsValue?.() || {};
|
const merged = { ...form, ...params };
|
groupByRef.value = merged.groupBy || 'category';
|
return {
|
groupBy: merged.groupBy || 'category',
|
category: merged.category || undefined,
|
evalMode: merged.evalMode || undefined,
|
deptId: normalizeDeptId(merged.deptId),
|
startTimeFrom: formatDayStart(merged.startTimeFrom),
|
startTimeTo: formatDayEnd(merged.startTimeTo),
|
};
|
}
|
|
async function fetchList(params: Record<string, any>) {
|
const query = buildQuery(params);
|
const gb = query.groupBy || 'category';
|
const list = ((await getTrainStatsList(query)) || []).map((row) => ({
|
...row,
|
groupBy: gb,
|
_rowKey: `${gb}:${row.dimCode ?? ''}`,
|
}));
|
return {
|
data: {
|
list,
|
pagination: { total: list.length },
|
},
|
};
|
}
|
|
async function handleExport() {
|
if (exporting.value) return;
|
exporting.value = true;
|
try {
|
const res = await exportTrainStats(buildQuery());
|
await saveExportBlob(res, '培训效果统计.xls');
|
createMessage.success('导出成功');
|
} catch (e: any) {
|
createMessage.error(e?.message || '导出失败');
|
} finally {
|
exporting.value = false;
|
}
|
}
|
|
/** 本页 BasicModal 展示该维度下的培训场次明细 */
|
function handleViewDetail(row: StatsRowView) {
|
const q = buildQuery();
|
const gb = row.groupBy || q.groupBy || 'category';
|
const code = (row.dimCode || '').trim() || '-';
|
openDetailModal(true, {
|
title: `${dimText(row)} · 场次明细`,
|
query: {
|
...q,
|
groupBy: gb,
|
dimCode: code,
|
} as TrainStatsQuery,
|
});
|
}
|
|
function getTableActions(row: StatsRowView): ActionItem[] {
|
return [{ label: TMS_BTN.viewDetail, onClick: handleViewDetail.bind(null, row) }];
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper">
|
<div class="jnpf-content-wrapper-center">
|
<div class="jnpf-content-wrapper-content">
|
<BasicVxeTable @register="registerTable">
|
<template #tableTitle>
|
<a-space wrap>
|
<a-button type="primary" :loading="exporting" @click="handleExport">
|
{{ TMS_BTN.export }}
|
</a-button>
|
<span class="tms-toolbar-hint">按部门 / 培训分类 / 考核方式统计签到率与合格率;操作列查看本维度场次明细</span>
|
</a-space>
|
</template>
|
<template #action="{ record }">
|
<TableAction :actions="getTableActions(record)" />
|
</template>
|
</BasicVxeTable>
|
<DetailModal @register="registerDetailModal" />
|
</div>
|
</div>
|
</div>
|
</template>
|