Phase 1 配置基线:next.config.ts 新增 experimental.optimizePackageImports(lucide-react/recharts/@xyflow/react/@tiptap/* /@radix-ui/*/date-fns)+ serverExternalPackages 追加 tencentcloud-sdk-nodejs + exceljs;providers.tsx 注入 WebVitalsReporter 闭环 RUM 上报链路(之前组件存在但未注入导致生产环境零性能数据)。 Phase 2 Bundle 预算优化补漏:TipTap 三实例(shared/ui/rich-text-editor + exams/editor/exam-rich-editor + lesson-preparation/blocks/rich-text-block)之前仅文件拆分未真正 next/dynamic ssr:false,本次补齐 lazy wrapper + xxx-inner.tsx 拆分模式;ReactFlow knowledge-graph.tsx 改用 next/dynamic 加载 inner;chart.tsx 改 recharts 具名导入替代 import * as RechartsPrimitive barrel。 Phase 4 组件渲染优化补漏:12 个 recharts 图表组件(parent/grades/dashboard/homework 模块下)margin props 提取到模块级 CHART_MARGIN 常量避免 inline 重建;layout.tsx metadata 增强(metadataBase/openGraph/twitter/robots/authors/creator)+ getLocale/getMessages/auth 串行 await 改 Promise.all 并行。 同步架构文档 004/005 + known-issues.md:新增 Phase 1 配置基线规则章节 + Phase 2 文件清单修正(之前引用不存在的 question-rich-editor.tsx 和 paper-rich-editor.tsx,实际是 rich-text-editor.tsx 和 rich-text-block.tsx)+ Phase 4.9 metadata + Promise.all 规则补充。架构图 1.1.5 章节新增未完成项(React Compiler/force-dynamic 评估/Playwright 性能断言)。 验证:npx tsc --noEmit 零错误;npm run lint 零新增错误(3 errors + 12 warnings 均位于未修改的 pre-existing 文件)。
97 lines
3.9 KiB
TypeScript
97 lines
3.9 KiB
TypeScript
"use client"
|
|
|
|
import type { HomeworkAssignmentQuestionAnalytics } from "@/modules/homework/types"
|
|
import { useTranslations } from "next-intl"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
|
|
|
/** recharts 图表 margin 常量(避免每次渲染创建新对象引用) */
|
|
const CHART_MARGIN = { top: 10, right: 10, left: -20, bottom: 0 } as const
|
|
|
|
export function HomeworkAssignmentQuestionErrorOverviewCard({
|
|
questions,
|
|
gradedSampleCount,
|
|
}: {
|
|
questions: HomeworkAssignmentQuestionAnalytics[]
|
|
gradedSampleCount: number
|
|
}) {
|
|
const t = useTranslations("examHomework")
|
|
const data = questions.map((q, index) => ({
|
|
name: `Q${index + 1}`,
|
|
errorRate: q.errorRate * 100,
|
|
errorCount: q.errorCount,
|
|
total: gradedSampleCount,
|
|
}))
|
|
|
|
return (
|
|
<Card className="md:col-span-1">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">{t("homework.analytics.errorRateOverview")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="h-72">
|
|
{questions.length === 0 || gradedSampleCount === 0 ? (
|
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
|
{t("homework.analytics.noGradedSubmissions")}
|
|
</div>
|
|
) : (
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={data} margin={CHART_MARGIN}>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
|
<XAxis
|
|
dataKey="name"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
|
interval={0}
|
|
/>
|
|
<YAxis
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
|
tickFormatter={(value) => `${value}%`}
|
|
domain={[0, 100]}
|
|
/>
|
|
<Tooltip
|
|
cursor={{ fill: "hsl(var(--muted)/0.2)" }}
|
|
content={({ active, payload }) => {
|
|
if (active && payload && payload.length) {
|
|
const d = payload[0].payload
|
|
return (
|
|
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="flex flex-col">
|
|
<span className="text-[0.70rem] uppercase text-muted-foreground">{t("homework.analytics.question")}</span>
|
|
<span className="font-bold text-muted-foreground">{d.name}</span>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-[0.70rem] uppercase text-muted-foreground">{t("homework.analytics.errorRateLabel")}</span>
|
|
<span className="font-bold">{d.errorRate.toFixed(1)}%</span>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-[0.70rem] uppercase text-muted-foreground">{t("homework.analytics.errors")}</span>
|
|
<span className="font-bold">
|
|
{d.errorCount} / {d.total}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
return null
|
|
}}
|
|
/>
|
|
<Bar
|
|
dataKey="errorRate"
|
|
fill="hsl(var(--primary))"
|
|
radius={[4, 4, 0, 0]}
|
|
maxBarSize={40}
|
|
/>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|