import { ExamScore } from '../entities/exam-score.entity'; import { esc, sectionFrame, sectionHeader } from './archive-report.helpers'; export function buildExamOverview(exams: ExamScore[], now: string): string { if (exams.length === 0) return ''; const cultureExams = exams.filter( (e) => e.examType && e.examType.includes('文化'), ); const entranceExam = exams.find((e) => e.examType === '入学测试'); const highestExam = [...exams].sort((a, b) => (b.score ?? 0) - (a.score ?? 0))[0]; const entranceScore = entranceExam?.score?.toFixed(1) ?? '-'; const highestScore = highestExam?.score?.toFixed(1) ?? '-'; const highestName = highestExam?.examName ?? '-'; // Improvement: last exam score minus first exam score const sortedScores = cultureExams .map((exam) => exam.score) .filter((score): score is number => score !== null && score !== undefined); let improvement = '—'; if (sortedScores.length >= 2) { const first = sortedScores[0]; const last = sortedScores[sortedScores.length - 1]; improvement = (last - first).toFixed(1); } const avgScore = cultureExams.length > 0 ? ( cultureExams.reduce((sum, e) => sum + (e.score ?? 0), 0) / cultureExams.length ).toFixed(1) : '-'; const metricHtml = `
${esc(now)} · 系统生成
考试成绩总览
入学测试成绩
${esc(entranceScore)}

入学摸底测试

最高分
${esc(highestScore)}

${esc(highestName)}

进步幅度
${esc(improvement)}

首考 → 末考变化

平均分
${esc(avgScore)}

文化课考试均分

`; const scoreTable = renderScoreTable(cultureExams); const trendChart = renderScoreTrendChart(cultureExams); return sectionFrame(` ${sectionHeader('考试成绩总览')} ${metricHtml} ${scoreTable} ${trendChart} `); } export function renderScoreTable(exams: ExamScore[]): string { if (exams.length === 0) return ''; return `

文化课考试成绩

${exams .map( (e) => ``, ) .join('')}
类型名称科目 分数班均排名日期
${esc(e.examType || '-')} ${esc(e.examName || '-')} ${esc(e.subject || '-')} ${e.score != null ? e.score : '-'} ${e.classAvg != null ? e.classAvg : '-'} ${e.rank != null ? e.rank : '-'} ${esc(e.examDate || '-')}
`; } export function renderScoreTrendChart(exams: ExamScore[]): string { const cultureExams = exams.filter((e) => e.score != null); if (cultureExams.length === 0) return ''; const scores = cultureExams.map((e) => Number(e.score)); const labels = cultureExams.map((e) => { const d = e.examDate || '-'; return d.length > 7 ? d.slice(5) : d; }); const w = 600; const h = 180; const pad = { top: 20, right: 20, bottom: 30, left: 40 }; const plotW = w - pad.left - pad.right; const plotH = h - pad.top - pad.bottom; const minScore = Math.min(...scores); const maxScore = Math.max(...scores); const scoreRange = maxScore - minScore || 1; const scaleY = (s: number): number => pad.top + plotH - ((s - minScore) / scoreRange) * plotH; let points = ''; let lines = ''; for (let i = 0; i < scores.length; i++) { const x = pad.left + (i / Math.max(scores.length - 1, 1)) * plotW; const y = scaleY(scores[i]); points += ``; if (i > 0) { const px = pad.left + ((i - 1) / Math.max(scores.length - 1, 1)) * plotW; const py = scaleY(scores[i - 1]); lines += ``; } } // Y-axis labels const ySteps = 4; let yLabels = ''; for (let i = 0; i <= ySteps; i++) { const val = minScore + (scoreRange * i) / ySteps; const y = scaleY(val); yLabels += `${val.toFixed(0)}`; if (i > 0) { yLabels += ``; } } // X-axis labels let xLabels = ''; const labelStep = Math.max(1, Math.floor(labels.length / 6)); for (let i = 0; i < labels.length; i += labelStep) { const x = pad.left + (i / Math.max(scores.length - 1, 1)) * plotW; xLabels += `${esc(labels[i])}`; } return `

成绩趋势

${yLabels} ${xLabels} ${lines} ${points}
趋势图展示文化课考试成绩的变化轨迹,点数代每次考试的分数
`; } export function buildExamDetail(exams: ExamScore[], now: string): string { const cultureExams = exams.filter( (e) => e.examType && e.examType.includes('文化'), ); if (cultureExams.length === 0) return ''; // Group by subject const subjectMap = new Map(); for (const e of cultureExams) { const subject = e.subject || '其他'; const existing = subjectMap.get(subject) ?? []; existing.push(e); subjectMap.set(subject, existing); } let subjectCards = ''; for (const [subject, subExams] of subjectMap) { const best = Math.max(...subExams.map((e) => e.score ?? 0)); const avg = ( subExams.reduce((sum, e) => sum + (e.score ?? 0), 0) / subExams.length ).toFixed(1); let rows = ''; for (const e of subExams) { rows += ` ${esc(e.examName || '-')} ${e.score != null ? e.score : '-'} ${e.classAvg != null ? e.classAvg : '-'} ${e.rank != null ? e.rank : '-'} ${esc(e.examDate || '-')} `; } subjectCards += `

${esc(subject)} · 最佳 ${best} · 均分 ${esc(avg)}

${rows}
考试名称分数班均排名日期
`; } return sectionFrame(` ${sectionHeader('文化课考试成绩')}
${esc(now)} · 系统生成
文化课考试成绩
${subjectCards} `); }