@@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user