<script lang="ts" setup>
|
import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
|
|
import type { ExamScoreDetailRow, ExamScoreSummaryItem } from './types';
|
|
import { ref } from 'vue';
|
import { useRouter } from 'vue-router';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
|
|
import { exportExamScores, getExamScoreList } from '#/api/x/tms/examScore';
|
import { TMS_BTN } from '#/views/x/tms/shared/ui';
|
|
import '#/views/x/tms/shared/page.css';
|
|
defineOptions({ name: 'TmsExamScore' });
|
|
const router = useRouter();
|
const { createMessage } = useMessage();
|
const exporting = ref(false);
|
|
const columns: BasicColumn[] = [
|
{ title: '试卷名称', dataIndex: 'taskSubject', minWidth: 240 },
|
{ title: '试卷编号', dataIndex: 'taskNo', width: 180 },
|
{
|
title: '试卷名称',
|
dataIndex: 'paperName',
|
minWidth: 220,
|
slots: { default: 'paperName' },
|
},
|
{
|
title: '参考人数',
|
dataIndex: 'participantCount',
|
width: 90,
|
align: 'center',
|
},
|
{
|
title: '不及格数',
|
dataIndex: 'failCount',
|
width: 90,
|
align: 'center',
|
slots: { default: 'failCount' },
|
},
|
{ title: '最高分', dataIndex: 'maxScore', width: 80, align: 'center' },
|
{ title: '最低分', dataIndex: 'minScore', width: 80, align: 'center' },
|
{ title: '平均分', dataIndex: 'avgScore', width: 80, align: 'center' },
|
{ title: '及格分', dataIndex: 'passScore', width: 80, align: 'center' },
|
{ title: '总分', dataIndex: 'totalScore', width: 80, align: 'center' },
|
];
|
|
const [registerTable, { getSelectRows }] = useVxeTable({
|
api: fetchList,
|
columns,
|
immediate: true,
|
rowKey: 'id',
|
rowSelection: { type: 'checkbox' },
|
useSearchForm: true,
|
formConfig: {
|
baseColProps: { span: 6 },
|
compact: true,
|
schemas: [
|
{
|
field: 'keyword',
|
label: '关键词',
|
component: 'Input',
|
componentProps: { placeholder: '请输入检索关键字', submitOnPressEnter: true },
|
},
|
{
|
field: 'taskNo',
|
label: '试卷编号',
|
component: 'Input',
|
componentProps: { placeholder: '请输入试卷编号', submitOnPressEnter: true },
|
},
|
],
|
},
|
actionColumn: {
|
width: 110,
|
title: '操作',
|
dataIndex: 'action',
|
fixed: 'right',
|
},
|
});
|
|
async function fetchList(params: Record<string, any>) {
|
const page = await getExamScoreList(params);
|
return {
|
data: {
|
list: Array.isArray(page?.list) ? page.list : [],
|
pagination: page?.pagination || { total: 0 },
|
},
|
};
|
}
|
|
function handleDetail(record: ExamScoreSummaryItem) {
|
router.push(`/tms/examScore/detail/${record.id}`);
|
}
|
|
async function handleExport() {
|
const selected = (getSelectRows?.() || []) as ExamScoreSummaryItem[];
|
if (!selected.length) {
|
createMessage.warning('请先勾选要导出的考试记录');
|
return;
|
}
|
if (exporting.value) return;
|
exporting.value = true;
|
try {
|
const rows = await exportExamScores(selected.map((item) => item.id));
|
if (!rows?.length) {
|
createMessage.warning('所选试卷暂无已交卷成绩');
|
return;
|
}
|
downloadScoreFile(rows, selected);
|
createMessage.success(`已导出 ${rows.length} 条考生成绩`);
|
} catch (e: any) {
|
createMessage.error(e?.message || '导出失败');
|
} finally {
|
exporting.value = false;
|
}
|
}
|
|
function passText(flag?: string) {
|
if (flag === '1') return '合格';
|
if (flag === '0') return '不合格';
|
return '-';
|
}
|
|
function xmlCell(value: unknown) {
|
const text = value == null ? '' : String(value);
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
}
|
|
function scoreFileName(selected: ExamScoreSummaryItem[]) {
|
const names = [...new Set(selected.map((item) => (item.taskSubject || '').trim()).filter(Boolean))];
|
const raw = names.length === 1 ? `考试成绩_${names[0]}` : '考试成绩';
|
return `${raw.replace(/[\\/:*?"<>|\r\n]/g, '_')}.xls`;
|
}
|
|
function downloadScoreFile(rows: ExamScoreDetailRow[], selected: ExamScoreSummaryItem[]) {
|
const header = ['试卷名称', '试卷编号', '考生', '考试开始时间', '考试结束时间', '成绩', '及格分', '是否合格'];
|
const body = rows.map((row) => [
|
row.taskSubject,
|
row.taskNo,
|
row.userName || row.userId,
|
row.examStart || '',
|
row.examEnd || '',
|
row.score,
|
row.passScore,
|
passText(row.passFlag),
|
]);
|
const widths = [180, 160, 100, 150, 150, 60, 60, 80];
|
const cols = widths.map((width) => `<Column ss:Width="${width}"/>`).join('');
|
const toRow = (cells: unknown[]) =>
|
`<Row>${cells.map((cell) => `<Cell><Data ss:Type="String">${xmlCell(cell)}</Data></Cell>`).join('')}</Row>`;
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
<?mso-application progid="Excel.Sheet"?>
|
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
|
<Worksheet ss:Name="考试成绩"><Table>${cols}${toRow(header)}${body.map(toRow).join('')}</Table></Worksheet>
|
</Workbook>`;
|
const blob = new Blob([xml], { type: 'application/vnd.ms-excel;charset=utf-8' });
|
const link = document.createElement('a');
|
link.href = URL.createObjectURL(blob);
|
link.download = scoreFileName(selected);
|
link.click();
|
URL.revokeObjectURL(link.href);
|
}
|
|
function getTableActions(record: ExamScoreSummaryItem): ActionItem[] {
|
return [{ label: TMS_BTN.detail, onClick: handleDetail.bind(null, record) }];
|
}
|
</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>
|
<a-button type="primary" :loading="exporting" @click="handleExport">{{ TMS_BTN.export }}</a-button>
|
<span class="tms-toolbar-hint">按场次汇总成绩,可导出或进入明细</span>
|
</a-space>
|
</template>
|
<template #paperName="{ record }">
|
<span :class="record.paperInvalid ? 'text-gray-400' : ''">{{ record.paperName }}</span>
|
</template>
|
<template #failCount="{ record }">
|
<span :class="record.failCount > 0 ? 'text-red-500' : ''">{{ record.failCount }}</span>
|
</template>
|
<template #action="{ record }">
|
<TableAction :actions="getTableActions(record)" />
|
</template>
|
</BasicVxeTable>
|
</div>
|
</div>
|
</div>
|
</template>
|