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
This commit is contained in:
SpecialX
2026-07-03 10:25:46 +08:00
parent dfffb61e94
commit 138b6f1b00
58 changed files with 3313 additions and 695 deletions

View File

@@ -1,11 +1,24 @@
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。
*
@@ -23,10 +36,13 @@ export async function exportDiagnosticReportToExcel(params: {
}): Promise<Buffer> {
const report = await getDiagnosticReportById(params.reportId)
if (!report) {
throw new Error("Report not found")
// v2-P2-3: 使用结构化错误码,由调用方 i18n 化
throw new DiagnosticExportError("REPORT_NOT_FOUND", "Report not found")
}
const periodLabel = report.period ?? "本期"
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") || "-"
@@ -37,16 +53,16 @@ export async function exportDiagnosticReportToExcel(params: {
// 个人报告
const mastery = await getStudentMasterySummary(report.studentId)
const overviewRows = [
{ metric: "学生姓名", value: report.studentName ?? "-" },
{ metric: "报告周期", value: periodLabel },
{ metric: "综合得分", value: overallScore },
{ metric: "报告状态", value: report.status },
{ metric: "生成人", value: report.generatedByName ?? "-" },
{ metric: "生成时间", value: report.createdAt.split("T")[0] },
{ metric: "摘要", value: summary },
{ metric: "强项", value: strengths },
{ metric: "弱项", value: weaknesses },
{ metric: "建议", value: recommendations },
{ 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) => ({
@@ -60,21 +76,21 @@ export async function exportDiagnosticReportToExcel(params: {
return exportToExcel({
sheets: [
{
name: "报告概览",
name: t("exportContent.sheetOverview"),
columns: [
{ header: "指标", key: "metric", width: 20 },
{ header: "数值", key: "value", width: 60 },
{ header: t("exportContent.metricStudent"), key: "metric", width: 20 },
{ header: "", key: "value", width: 60 },
],
rows: overviewRows,
},
{
name: "知识点掌握度",
name: t("exportContent.sheetMastery"),
columns: [
{ header: "知识点", key: "knowledgePoint", width: 28 },
{ header: "掌握度", key: "masteryLevel", width: 12 },
{ header: "总题数", key: "totalQuestions", width: 10 },
{ header: "正确数", key: "correctQuestions", width: 10 },
{ header: "最近评估", key: "lastAssessedAt", width: 14 },
{ 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,
},
@@ -83,40 +99,89 @@ export async function exportDiagnosticReportToExcel(params: {
}
// 班级报告reportType === "class"
// 班级报告的 studentId 为 null需要从 period 反查 classId 不现实,
// 这里仅导出报告概览(知识点统计需要 classId但报告本身未存储 classId
// 如需导出班级明细,应通过 generateClassDiagnosticReport 时记录 classId。
// v4-P2-1: 利用 classId 字段查询班级掌握度,导出知识点统计+需关注学生明细
const classSummary = report.classId ? await getClassMasterySummary(report.classId) : null
const overviewRows = [
{ metric: "报告类型", value: "班级报告" },
{ metric: "报告周期", value: periodLabel },
{ metric: "综合得分", value: overallScore },
{ metric: "报告状态", value: report.status },
{ metric: "生成人", value: report.generatedByName ?? "-" },
{ metric: "生成时间", value: report.createdAt.split("T")[0] },
{ metric: "摘要", value: summary },
{ metric: "强项", value: strengths },
{ metric: "弱项", value: weaknesses },
{ metric: "建议", value: recommendations },
{ 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 },
]
return exportToExcel({
sheets: [
{
name: "报告概览",
columns: [
{ header: "指标", key: "metric", width: 20 },
{ header: "数值", key: "value", width: 60 },
],
rows: overviewRows,
},
],
})
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 function buildDiagnosticReportFilename(period: string | null): string {
export async function buildDiagnosticReportFilename(period: string | null): Promise<string> {
const t = await getTranslations("diagnostic.exportContent")
const safePeriod = (period ?? "report").replace(/[\\/:*?"<>|]/g, "_")
return `诊断报告_${safePeriod}_${formatDateForFile()}.xlsx`
const date = formatDateForFile()
return t("filename", { period: safePeriod, date })
}