复盘统计改为分色堆叠柱状图,替代节点树图。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 17:16:44 +08:00
parent f7a761da33
commit 4b55eb54b0
4 changed files with 132 additions and 20 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -9,7 +9,7 @@
<meta name="author" content="马建军" />
<meta name="copyright" content="Copyright (c) 马建军. All rights reserved." />
<title>中学成绩档案</title>
<script type="module" crossorigin src="/assets/index-BlbLMdT7.js"></script>
<script type="module" crossorigin src="/assets/index-9cr1FyU2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BSTLtBBx.css">
</head>
<body>
+2 -2
View File
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react'
import { examApi } from '../api/client'
import type { Exam, ReviewStatus } from '../types'
import { EXAM_TYPE_LABELS, REVIEW_STATUS_OPTIONS } from '../types'
import ReviewTreeChart from './ReviewTreeChart'
import ReviewBarChart from './ReviewBarChart'
function apiErrorMessage(err: unknown, fallback: string): string {
if (err && typeof err === 'object' && 'response' in err) {
@@ -147,7 +147,7 @@ export default function ExamReviewPanel({ exams, onRefresh }: Props) {
</>
)}
<Typography.Text strong></Typography.Text>
<ReviewTreeChart exams={exams} />
<ReviewBarChart exams={exams} />
</Space>
),
},
+112
View File
@@ -0,0 +1,112 @@
import ReactECharts from 'echarts-for-react'
import type { Exam, ReviewStatus } from '../types'
import { REVIEW_STATUS_LABELS } from '../types'
interface Props {
exams: Exam[]
}
const STATUS_ORDER: ReviewStatus[] = ['careless', 'unknown', 'nervous', 'normal']
const STATUS_COLORS: Record<ReviewStatus, string> = {
careless: '#fa8c16',
unknown: '#ff4d4f',
nervous: '#722ed1',
normal: '#52c41a',
}
function buildChartData(exams: Exam[]) {
const subjectSet = new Set<string>()
const counts: Record<ReviewStatus, Record<string, number>> = {
careless: {},
unknown: {},
nervous: {},
normal: {},
}
for (const exam of exams) {
for (const score of exam.scores) {
const subjectName = score.subject_name || `科目${score.subject_id}`
subjectSet.add(subjectName)
for (const status of score.review_statuses || []) {
counts[status][subjectName] = (counts[status][subjectName] || 0) + 1
}
}
}
const subjects = Array.from(subjectSet).sort((a, b) => a.localeCompare(b, 'zh-CN'))
return { subjects, counts }
}
export default function ReviewBarChart({ exams }: Props) {
const hasData = STATUS_ORDER.some((status) =>
exams.some((exam) =>
exam.scores.some((score) => (score.review_statuses || []).includes(status)),
),
)
if (!hasData) {
return (
<div style={{ textAlign: 'center', padding: 32, color: '#999' }}>
</div>
)
}
const { subjects, counts } = buildChartData(exams)
const option = {
title: {
text: '各科考试状态统计',
left: 'center',
textStyle: { fontSize: 15, fontWeight: 500 },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: Array<{ seriesName: string; value: number; marker: string }>) => {
const rows = params.filter((p) => p.value > 0)
if (!rows.length) return ''
const total = rows.reduce((sum, p) => sum + p.value, 0)
return (
rows.map((p) => `${p.marker}${p.seriesName}: ${p.value}`).join('<br/>') +
`<br/><strong>合计: ${total} 次</strong>`
)
},
},
legend: {
bottom: 0,
data: STATUS_ORDER.map((s) => REVIEW_STATUS_LABELS[s]),
},
grid: { left: 48, right: 24, top: 48, bottom: 56 },
xAxis: {
type: 'category',
data: subjects,
axisLabel: { interval: 0, rotate: subjects.length > 6 ? 30 : 0 },
},
yAxis: {
type: 'value',
name: '次数',
minInterval: 1,
splitLine: { lineStyle: { type: 'dashed' } },
},
series: STATUS_ORDER.map((status) => ({
name: REVIEW_STATUS_LABELS[status],
type: 'bar',
stack: 'review',
barMaxWidth: 48,
emphasis: { focus: 'series' },
itemStyle: { color: STATUS_COLORS[status], borderRadius: [2, 2, 0, 0] },
data: subjects.map((subject) => counts[status][subject] || 0),
})),
}
return (
<div>
<ReactECharts option={option} style={{ height: 360, width: '100%' }} notMerge />
<p style={{ color: '#888', fontSize: 12, marginTop: 8 }}>
</p>
</div>
)
}