Files
secondary-school-grade-archive/frontend/src/pages/WrongQuestionDetail.tsx
T
dekun c30e21b51e 修复错题图片显示、Tab 刷新跳转,奥数仅数学并支持删除。
- 图片通过带 Token 的 blob 请求加载,修复不显示

- URL ?tab= 保持当前标签,刷新列表不再重置到成绩录入

- 奥数区固定数学科目;错题卡片与详情增加删除
2026-06-28 13:47:53 +08:00

195 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Alert, Button, Col, Input, Modal, Popconfirm, Row, Space, Spin, Typography, message } from 'antd'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import AuthenticatedImage from '../components/AuthenticatedImage'
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
onDeleted?: () => void
}
export default function WrongQuestionDetail({
questionId,
open,
onClose,
onUpdated,
onDeleted,
}: 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 [deleting, setDeleting] = 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('生成失败,请检查 AI 模型配置')
} finally {
setRegenerating(false)
}
}
const handleRetryOcr = async () => {
await wrongQuestionApi.retryOcr(questionId)
message.info('已重新识别,请稍后刷新')
onUpdated()
onClose()
}
const handleDelete = async () => {
setDeleting(true)
try {
await wrongQuestionApi.remove(questionId)
message.success('已删除')
onDeleted?.()
onClose()
} catch {
message.error('删除失败')
} finally {
setDeleting(false)
}
}
return (
<Modal
title={
wq
? `${wq.subject_name}${wq.category === 'olympiad' ? ' · 奥数' : ''} · 详情`
: '详情'
}
open={open}
onCancel={onClose}
width="90%"
style={{ maxWidth: 960 }}
footer={
<Space wrap>
<Popconfirm title="确定删除该题?" onConfirm={handleDelete}>
<Button danger loading={deleting}>
</Button>
</Popconfirm>
<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}>
<AuthenticatedImage
questionId={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>
)
}