成绩复盘独立导航页,柱状图点击展示各科考试明细。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user