成绩复盘独立导航页,柱状图点击展示各科考试明细。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 17:26:53 +08:00
parent b4df6e5e18
commit 5f00f07dbe
9 changed files with 816 additions and 616 deletions
+99 -75
View File
@@ -1,9 +1,10 @@
import { Button, Checkbox, Collapse, Select, Space, Table, Typography, message } from 'antd'
import { Button, Checkbox, Divider, Select, Space, Table, Typography, message } from 'antd'
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 ReviewBarChart from './ReviewBarChart'
import ReviewSubjectDetail from './ReviewSubjectDetail'
function apiErrorMessage(err: unknown, fallback: string): string {
if (err && typeof err === 'object' && 'response' in err) {
@@ -16,16 +17,29 @@ function apiErrorMessage(err: unknown, fallback: string): string {
return fallback
}
function firstSubjectWithReview(exams: Exam[]): string | null {
const names = new Set<string>()
for (const exam of exams) {
for (const score of exam.scores) {
if (score.review_statuses?.length) {
names.add(score.subject_name || `科目${score.subject_id}`)
}
}
}
const sorted = Array.from(names).sort((a, b) => a.localeCompare(b, 'zh-CN'))
return sorted[0] || null
}
interface Props {
exams: Exam[]
onRefresh: () => void
}
export default function ExamReviewPanel({ exams, onRefresh }: Props) {
const [open, setOpen] = useState(false)
const [examId, setExamId] = useState<string>()
const [statusMap, setStatusMap] = useState<Record<number, ReviewStatus[]>>({})
const [saving, setSaving] = useState(false)
const [selectedSubject, setSelectedSubject] = useState<string | null>(null)
const examOptions = useMemo(
() =>
@@ -39,11 +53,17 @@ export default function ExamReviewPanel({ exams, onRefresh }: Props) {
const selectedExam = exams.find((e) => e.id === examId)
useEffect(() => {
if (!open) return
if (!examId && exams.length) {
setExamId(exams[0].id)
}
}, [open, examId, exams])
}, [examId, exams])
useEffect(() => {
const defaultSubject = firstSubjectWithReview(exams)
if (defaultSubject) {
setSelectedSubject((prev) => prev ?? defaultSubject)
}
}, [exams])
useEffect(() => {
if (!selectedExam) {
@@ -81,77 +101,81 @@ export default function ExamReviewPanel({ exams, onRefresh }: Props) {
}
}
if (exams.length === 0) {
return (
<Typography.Text type="secondary"></Typography.Text>
)
}
return (
<Collapse
style={{ marginTop: 16 }}
activeKey={open ? ['review'] : []}
onChange={(keys) => setOpen(keys.includes('review'))}
items={[
{
key: 'review',
label: '复盘',
children: (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{exams.length === 0 ? (
<Typography.Text type="secondary"></Typography.Text>
) : (
<>
<Space wrap style={{ width: '100%' }}>
<Select
style={{ minWidth: 260, flex: 1 }}
placeholder="选择考试"
value={examId}
onChange={setExamId}
options={examOptions}
/>
<Button type="primary" loading={saving} onClick={handleSave}>
</Button>
</Space>
{selectedExam && (
<Table
size="small"
pagination={false}
rowKey="subject_id"
dataSource={selectedExam.scores}
scroll={{ x: 480 }}
columns={[
{
title: '科目',
dataIndex: 'subject_name',
width: 80,
},
{
title: '得分',
width: 100,
render: (_, row) => `${row.obtained_score}/${row.total_score}`,
},
{
title: '考试状态(可多选)',
render: (_, row) => (
<Checkbox.Group
options={REVIEW_STATUS_OPTIONS}
value={statusMap[row.subject_id] || []}
onChange={(values) =>
setStatusMap((prev) => ({
...prev,
[row.subject_id]: values as ReviewStatus[],
}))
}
/>
),
},
]}
/>
)}
</>
)}
<Typography.Text strong></Typography.Text>
<ReviewBarChart exams={exams} />
</Space>
),
},
]}
/>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Typography.Title level={5} style={{ marginTop: 0 }}>
</Typography.Title>
<Space wrap style={{ width: '100%', marginBottom: 12 }}>
<Select
style={{ minWidth: 280 }}
placeholder="选择考试"
value={examId}
onChange={setExamId}
options={examOptions}
/>
<Button type="primary" loading={saving} onClick={handleSave}>
</Button>
</Space>
{selectedExam && (
<Table
size="small"
pagination={false}
rowKey="subject_id"
dataSource={selectedExam.scores}
scroll={{ x: 480 }}
columns={[
{
title: '科目',
dataIndex: 'subject_name',
width: 80,
},
{
title: '得分',
width: 100,
render: (_, row) => `${row.obtained_score}/${row.total_score}`,
},
{
title: '考试状态(可多选)',
render: (_, row) => (
<Checkbox.Group
options={REVIEW_STATUS_OPTIONS}
value={statusMap[row.subject_id] || []}
onChange={(values) =>
setStatusMap((prev) => ({
...prev,
[row.subject_id]: values as ReviewStatus[],
}))
}
/>
),
},
]}
/>
)}
</div>
<Divider style={{ margin: '8px 0' }} />
<div>
<Typography.Title level={5} style={{ marginTop: 0 }}>
</Typography.Title>
<ReviewBarChart
exams={exams}
selectedSubject={selectedSubject}
onSubjectSelect={setSelectedSubject}
/>
<ReviewSubjectDetail exams={exams} subjectName={selectedSubject} />
</div>
</Space>
)
}