Files
NextEdu/src/modules/diagnostic/export.ts
SpecialX 138b6f1b00 feat(dashboard,diagnostic,elective): add widgets, layout, parent dashboard, role-config, services, elective components
dashboard:

- Add comparison-badge, dashboard-notification-widget, dashboard-responsive-layout, dashboard-time-range-filter

- Add parent-dashboard components directory

- Add config, hooks, and services directories

diagnostic:

- Add role-config and services directory

elective:

- Add elective-course-detail, elective-stats-cards, parent-selection-view components

- Add data-access-settings and data-access-stats
2026-07-03 10:25:46 +08:00

188 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "server-only"
import { getTranslations } from "next-intl/server"
import { exportToExcel } from "@/shared/lib/excel"
import { formatDateForFile } from "@/shared/lib/utils"
import { BusinessError } from "@/shared/lib/action-utils"
import { getDiagnosticReportById } from "./data-access-reports"
import { getStudentMasterySummary, getClassMasterySummary } from "./data-access"
/**
* v2-P2-3: 导出报告不存在的结构化错误。
*/
export class DiagnosticExportError extends BusinessError {
constructor(code: "REPORT_NOT_FOUND", message: string) {
super(message, code)
this.name = "DiagnosticExportError"
}
}
/**
* v3-P2-4: 导出诊断报告为 Excel。
*
* 个人报告 Sheet
* - 概览(学生、周期、综合得分、摘要、强项、弱项、建议)
* - 知识点掌握度明细
*
* 班级报告 Sheet
* - 概览(班级、周期、综合得分、摘要、强项、弱项、建议)
* - 知识点统计(平均掌握度、掌握人数、未掌握人数)
* - 需关注学生列表
*/
export async function exportDiagnosticReportToExcel(params: {
reportId: string
}): Promise<Buffer> {
const report = await getDiagnosticReportById(params.reportId)
if (!report) {
// v2-P2-3: 使用结构化错误码,由调用方 i18n 化
throw new DiagnosticExportError("REPORT_NOT_FOUND", "Report not found")
}
const t = await getTranslations("diagnostic")
const periodLabel = report.period ?? t("parent.selectChild")
const overallScore = report.overallScore ?? "-"
const strengths = (report.strengths ?? []).join("\n") || "-"
const weaknesses = (report.weaknesses ?? []).join("\n") || "-"
const recommendations = (report.recommendations ?? []).join("\n") || "-"
const summary = report.summary ?? "-"
if (report.reportType === "individual" && report.studentId) {
// 个人报告
const mastery = await getStudentMasterySummary(report.studentId)
const overviewRows = [
{ metric: t("exportContent.metricStudent"), value: report.studentName ?? "-" },
{ metric: t("exportContent.metricPeriod"), value: periodLabel },
{ metric: t("exportContent.metricScore"), value: overallScore },
{ metric: t("exportContent.metricStatus"), value: report.status },
{ metric: t("exportContent.metricGeneratedBy"), value: report.generatedByName ?? "-" },
{ metric: t("exportContent.metricCreatedAt"), value: report.createdAt.split("T")[0] },
{ metric: t("exportContent.metricSummary"), value: summary },
{ metric: t("exportContent.metricStrengths"), value: strengths },
{ metric: t("exportContent.metricWeaknesses"), value: weaknesses },
{ metric: t("exportContent.metricRecommendations"), value: recommendations },
]
const masteryRows = (mastery?.allMastery ?? []).map((m) => ({
knowledgePoint: m.knowledgePointName,
masteryLevel: m.masteryLevel,
totalQuestions: m.totalQuestions,
correctQuestions: m.correctQuestions,
lastAssessedAt: m.lastAssessedAt.split("T")[0],
}))
return exportToExcel({
sheets: [
{
name: t("exportContent.sheetOverview"),
columns: [
{ header: t("exportContent.metricStudent"), key: "metric", width: 20 },
{ header: "", key: "value", width: 60 },
],
rows: overviewRows,
},
{
name: t("exportContent.sheetMastery"),
columns: [
{ header: t("exportContent.colKnowledgePoint"), key: "knowledgePoint", width: 28 },
{ header: t("exportContent.colMasteryLevel"), key: "masteryLevel", width: 12 },
{ header: t("exportContent.colTotalQuestions"), key: "totalQuestions", width: 10 },
{ header: t("exportContent.colCorrectQuestions"), key: "correctQuestions", width: 10 },
{ header: t("exportContent.colLastAssessed"), key: "lastAssessedAt", width: 14 },
],
rows: masteryRows,
},
],
})
}
// 班级报告reportType === "class"
// v4-P2-1: 利用 classId 字段查询班级掌握度,导出知识点统计+需关注学生明细
const classSummary = report.classId ? await getClassMasterySummary(report.classId) : null
const overviewRows = [
{ metric: t("exportContent.metricReportType"), value: t("type.class") },
...(classSummary ? [{ metric: t("exportContent.metricClass"), value: classSummary.className }] : []),
{ metric: t("exportContent.metricPeriod"), value: periodLabel },
{ metric: t("exportContent.metricScore"), value: overallScore },
...(classSummary ? [{ metric: t("exportContent.metricStudentCount"), value: classSummary.studentCount }] : []),
...(classSummary ? [{ metric: t("exportContent.metricAttentionCount"), value: classSummary.studentsNeedingAttention.length }] : []),
{ metric: t("exportContent.metricStatus"), value: report.status },
{ metric: t("exportContent.metricGeneratedBy"), value: report.generatedByName ?? "-" },
{ metric: t("exportContent.metricCreatedAt"), value: report.createdAt.split("T")[0] },
{ metric: t("exportContent.metricSummary"), value: summary },
{ metric: t("exportContent.metricStrengths"), value: strengths },
{ metric: t("exportContent.metricWeaknesses"), value: weaknesses },
{ metric: t("exportContent.metricRecommendations"), value: recommendations },
]
const sheets: Array<{
name: string
columns: Array<{ header: string; key: string; width: number }>
rows: Array<Record<string, string | number>>
}> = [
{
name: t("exportContent.sheetOverview"),
columns: [
{ header: t("exportContent.metricStudent"), key: "metric", width: 20 },
{ header: "", key: "value", width: 60 },
],
rows: overviewRows,
},
]
// v4-P2-1: 知识点统计 Sheet
if (classSummary && classSummary.knowledgePointStats.length > 0) {
const classStatsRows = classSummary.knowledgePointStats.map((kp) => ({
knowledgePoint: kp.knowledgePointName,
averageMastery: kp.averageMastery.toFixed(1),
masteredCount: kp.masteredCount,
notMasteredCount: kp.notMasteredCount,
totalStudents: kp.totalStudents,
}))
sheets.push({
name: t("exportContent.sheetClassStats"),
columns: [
{ header: t("exportContent.colKnowledgePoint"), key: "knowledgePoint", width: 28 },
{ header: t("exportContent.colAverageMastery"), key: "averageMastery", width: 14 },
{ header: t("exportContent.colMasteredCount"), key: "masteredCount", width: 16 },
{ header: t("exportContent.colNotMasteredCount"), key: "notMasteredCount", width: 16 },
{ header: t("exportContent.colTotalStudents"), key: "totalStudents", width: 12 },
],
rows: classStatsRows,
})
}
// v4-P2-1: 需关注学生 Sheet
if (classSummary && classSummary.studentsNeedingAttention.length > 0) {
const attentionRows = classSummary.studentsNeedingAttention.map((s) => ({
studentName: s.studentName,
averageMastery: s.averageMastery.toFixed(1),
weakCount: s.weakCount,
}))
sheets.push({
name: t("exportContent.sheetAttentionStudents"),
columns: [
{ header: t("exportContent.colStudentName"), key: "studentName", width: 24 },
{ header: t("exportContent.colAverageMastery"), key: "averageMastery", width: 14 },
{ header: t("exportContent.colWeakCount"), key: "weakCount", width: 12 },
],
rows: attentionRows,
})
}
return exportToExcel({ sheets })
}
/**
* 生成诊断报告导出文件名。
*/
export async function buildDiagnosticReportFilename(period: string | null): Promise<string> {
const t = await getTranslations("diagnostic.exportContent")
const safePeriod = (period ?? "report").replace(/[\\/:*?"<>|]/g, "_")
const date = formatDateForFile()
return t("filename", { period: safePeriod, date })
}