Files
NextEdu/src/shared/components/charts/trend-line-chart.tsx
SpecialX a60105455e feat(exams,homework,parent): V3 审计深度修复 — 批量批改/考试分析/提交反馈/家长视图/移动端优化
V3-5: exam-actions.tsx 集成 useExamHomeworkFeatures hook,按角色控制菜单项可见性
V3-7: 批量批改 — 新增 batchAutoGradeSubmissions data-access + Server Action + HomeworkBatchGradingView 组件
V3-8: 考试分析仪表盘 — 新增 getExamAnalytics stats-service + ExamAnalyticsDashboard 组件 + /teacher/exams/[id]/analytics 路由
V3-9: 提交后即时反馈页 — 新增 HomeworkSubmissionResult 组件 + /student/learning/assignments/[id]/result 路由
V3-11: 家长考试详情 — 新增 ChildExamDetail 组件 + getStudentExamResults data-access + child-detail-panel exams Tab
V3-12: 移动端触控优化 — 题目导航与考试操作按钮 44px 最小触控目标

修复: instrumentation.ts 适配器补全 questionCount/averageScore/overdueCount 字段
修复: exam-homework-port.ts 类型导入对齐 ExamWithQuestionsForHomework
修复: trend-line-chart.tsx 数据类型允许 undefined(classAverage 可选场景)

同步更新 004/005 架构文档
2026-06-23 01:06:27 +08:00

154 lines
4.8 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.
"use client"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/shared/components/ui/chart"
import { cn } from "@/shared/lib/utils"
/**
* 趋势折线图:统一的 LineChart 配置CartesianGrid + XAxis + YAxis + ChartTooltip + Line
*
* 覆盖以下重复模式4 个文件几乎逐行相同):
* - GradeTrendChartdataKey=normalizedScore, height=h-[280px]
* - TeacherGradeTrendsdataKey=score, height=h-[200px]
* - StudentGradesCarddataKey=score, height=h-[200px]
* - ChildGradeSummarydataKey=score, xKey=date, height=h-[160px]
*
* 默认配置:
* - CartesianGrid: vertical=false, strokeDasharray="4 4", strokeOpacity=0.4
* - XAxis: tickLine=false, axisLine=false, tickMargin=8, 默认截断到 10 字符
* - YAxis: domain=[0,100], tickLine=false, axisLine=false, 默认百分比格式化, width=36
* - ChartTooltip: cursor 虚线, content=ChartTooltipContent indicator="line" labelKey="fullTitle"
* - Line: type="monotone", strokeWidth=2
*/
export interface TrendLineSeries {
/** 数据字段名(对应 data 中的 key */
dataKey: string
/** 图例名称 */
name: string
/** 颜色CSS 变量或 hsl 值,如 "hsl(var(--primary))" */
color: string
/** 数据点半径(默认 3 */
dotRadius?: number
/** 激活数据点半径(默认 5 */
activeDotRadius?: number
}
interface TrendLineChartProps {
/** 图表数据 */
data: Array<Record<string, string | number | undefined>>
/** 折线系列配置(支持单条或多条) */
series: TrendLineSeries[]
/** X 轴数据字段名(默认 "title" */
xKey?: string
/** Y 轴定义域(默认 [0, 100] */
yDomain?: [number, number]
/** Y 轴刻度格式化(默认百分比 `${value}%` */
yTickFormatter?: (value: number) => string
/** X 轴刻度格式化(默认截断到 10 字符;设为 null 则不格式化) */
xTickFormatter?: ((value: string) => string) | null
/** 图表高度类名(默认 "h-[280px]" */
heightClassName?: string
/** 图表 margin默认 { left: 8, right: 8, top: 8, bottom: 8 } */
margin?: { left: number; right: number; top: number; bottom: number }
/** Y 轴宽度(默认 36 */
yWidth?: number
/** Tooltip 内容宽度类名(默认 "w-[220px]" */
tooltipClassName?: string
/** Tooltip labelKey默认 "fullTitle" */
tooltipLabelKey?: string
/** 容器额外类名 */
className?: string
}
const DEFAULT_X_TICK_TRUNCATE_LENGTH = 10
function defaultXTickFormatter(value: string): string {
return value.length > DEFAULT_X_TICK_TRUNCATE_LENGTH
? `${value.slice(0, DEFAULT_X_TICK_TRUNCATE_LENGTH)}...`
: value
}
function defaultYTickFormatter(value: number): string {
return `${value}%`
}
export function TrendLineChart({
data,
series,
xKey = "title",
yDomain = [0, 100],
yTickFormatter = defaultYTickFormatter,
xTickFormatter = defaultXTickFormatter,
heightClassName = "h-[280px]",
margin = { left: 8, right: 8, top: 8, bottom: 8 },
yWidth = 36,
tooltipClassName = "w-[220px]",
tooltipLabelKey = "fullTitle",
className,
}: TrendLineChartProps) {
const chartConfig: ChartConfig = {}
for (const s of series) {
chartConfig[s.dataKey] = {
label: s.name,
color: s.color,
}
}
return (
<ChartContainer config={chartConfig} className={cn(heightClassName, "w-full", className)}>
<LineChart data={data} margin={margin}>
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
<XAxis
dataKey={xKey}
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={xTickFormatter ?? undefined}
/>
<YAxis
domain={yDomain}
tickLine={false}
axisLine={false}
tickFormatter={yTickFormatter}
width={yWidth}
/>
<ChartTooltip
cursor={{
stroke: "hsl(var(--muted-foreground))",
strokeWidth: 1,
strokeDasharray: "4 4",
}}
content={
<ChartTooltipContent
indicator="line"
labelKey={tooltipLabelKey}
className={tooltipClassName}
/>
}
/>
{series.map((s) => (
<Line
key={s.dataKey}
dataKey={s.dataKey}
type="monotone"
stroke={`var(--color-${s.dataKey})`}
strokeWidth={2}
dot={{
fill: `var(--color-${s.dataKey})`,
r: s.dotRadius ?? 3,
strokeWidth: 2,
}}
activeDot={{ r: s.activeDotRadius ?? 5, strokeWidth: 0 }}
/>
))}
</LineChart>
</ChartContainer>
)
}