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
188 lines
7.7 KiB
TypeScript
188 lines
7.7 KiB
TypeScript
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 })
|
||
}
|