| | |
| | | <script lang="ts" setup> |
| | | import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable'; |
| | | |
| | | import type { ExamScoreSummaryItem } from './types'; |
| | | 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 { getExamScoreList } from '#/api/x/tms/examScore'; |
| | | 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: 140 }, |
| | | { title: '试卷名称', dataIndex: 'taskSubject', minWidth: 240 }, |
| | | { title: '试卷编号', dataIndex: 'taskNo', width: 180 }, |
| | | { |
| | | title: '试卷名称', |
| | | dataIndex: 'paperName', |
| | |
| | | }, |
| | | { |
| | | field: 'taskNo', |
| | | label: '培训编号', |
| | | label: '试卷编号', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请输入培训编号', submitOnPressEnter: true }, |
| | | componentProps: { placeholder: '请输入试卷编号', submitOnPressEnter: true }, |
| | | }, |
| | | ], |
| | | }, |
| | |
| | | }); |
| | | |
| | | async function fetchList(params: Record<string, any>) { |
| | | return { data: await getExamScoreList(params) }; |
| | | 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}`); |
| | | } |
| | | |
| | | function handleExport() { |
| | | const rows = getSelectRows?.() || []; |
| | | if (!rows.length) { |
| | | async function handleExport() { |
| | | const selected = (getSelectRows?.() || []) as ExamScoreSummaryItem[]; |
| | | if (!selected.length) { |
| | | createMessage.warning('请先勾选要导出的考试记录'); |
| | | return; |
| | | } |
| | | createMessage.success(`已选择 ${rows.length} 条(导出接口联调后生效)`); |
| | | 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: '考试详情', onClick: handleDetail.bind(null, record) }]; |
| | | return [{ label: TMS_BTN.detail, onClick: handleDetail.bind(null, record) }]; |
| | | } |
| | | </script> |
| | | |
| | |
| | | <BasicVxeTable @register="registerTable"> |
| | | <template #tableTitle> |
| | | <a-space> |
| | | <a-button type="primary" @click="handleExport">导出考试</a-button> |
| | | <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 }"> |