Initial commit: secondary school grade archive system.

Add FastAPI/React app with Docker deployment, Ubuntu one-click install, and docs for junior/senior high score tracking and mistake bank.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 11:18:58 +08:00
commit e329d3398a
76 changed files with 8506 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
import { Alert, Button, Col, Input, Modal, Row, Space, Spin, Typography, message } from 'antd'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { wrongQuestionApi } from '../api/client'
import type { WrongQuestion } from '../types'
import { STATUS_LABELS } from '../types'
interface Props {
questionId: string
open: boolean
onClose: () => void
onUpdated: () => void
}
export default function WrongQuestionDetail({ questionId, open, onClose, onUpdated }: Props) {
const [wq, setWq] = useState<WrongQuestion | null>(null)
const [loading, setLoading] = useState(false)
const [questionText, setQuestionText] = useState('')
const [solutionText, setSolutionText] = useState('')
const [saving, setSaving] = useState(false)
const [regenerating, setRegenerating] = useState(false)
const load = async () => {
setLoading(true)
try {
const { data } = await wrongQuestionApi.get(questionId)
setWq(data)
setQuestionText(data.question_text || '')
setSolutionText(data.solution_text || '')
} finally {
setLoading(false)
}
}
useEffect(() => {
if (open && questionId) load()
}, [open, questionId])
const handleSave = async () => {
setSaving(true)
try {
await wrongQuestionApi.update(questionId, {
question_text: questionText,
solution_text: solutionText,
})
message.success('已保存')
onUpdated()
} finally {
setSaving(false)
}
}
const handleRegenerate = async () => {
setRegenerating(true)
try {
const { data } = await wrongQuestionApi.regenerate(questionId)
setWq(data)
setQuestionText(data.question_text || '')
setSolutionText(data.solution_text || '')
message.success('解法已重新生成')
onUpdated()
} catch {
message.error('生成失败,请确认 Ollama 已启动')
} finally {
setRegenerating(false)
}
}
const handleRetryOcr = async () => {
await wrongQuestionApi.retryOcr(questionId)
message.info('已重新识别,请稍后刷新')
onUpdated()
onClose()
}
return (
<Modal
title={wq ? `${wq.subject_name} · 错题详情` : '错题详情'}
open={open}
onCancel={onClose}
width="90%"
style={{ maxWidth: 960 }}
footer={
<Space wrap>
<Button onClick={handleRetryOcr}> OCR</Button>
<Button loading={regenerating} onClick={handleRegenerate}>
</Button>
<Button type="primary" loading={saving} onClick={handleSave}>
</Button>
</Space>
}
>
<Spin spinning={loading}>
{wq && (
<>
<Typography.Text type="secondary">{STATUS_LABELS[wq.status]}</Typography.Text>
{wq.solution_text && (
<Alert
message="AI 生成内容,请核对后再使用"
type="warning"
showIcon
style={{ margin: '12px 0' }}
/>
)}
<Row gutter={16} style={{ marginTop: 12 }}>
<Col xs={24} md={10}>
<img
src={wrongQuestionApi.imageUrl(wq.id)}
alt="原题"
style={{ width: '100%', borderRadius: 8, border: '1px solid #f0f0f0' }}
/>
{wq.ocr_raw_text && (
<div style={{ marginTop: 12 }}>
<Typography.Text strong>OCR </Typography.Text>
<pre
style={{
background: '#fafafa',
padding: 8,
fontSize: 12,
maxHeight: 150,
overflow: 'auto',
whiteSpace: 'pre-wrap',
}}
>
{wq.ocr_raw_text}
</pre>
</div>
)}
</Col>
<Col xs={24} md={14}>
<Typography.Text strong></Typography.Text>
<Input.TextArea
rows={6}
value={questionText}
onChange={(e) => setQuestionText(e.target.value)}
style={{ marginTop: 8, marginBottom: 16 }}
/>
<Typography.Text strong></Typography.Text>
<Input.TextArea
rows={8}
value={solutionText}
onChange={(e) => setSolutionText(e.target.value)}
style={{ marginTop: 8, marginBottom: 12 }}
/>
{solutionText && (
<div style={{ background: '#fafafa', padding: 12, borderRadius: 8 }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
<ReactMarkdown>{solutionText}</ReactMarkdown>
</div>
)}
</Col>
</Row>
</>
)}
</Spin>
</Modal>
)
}