c30e21b51e
- 图片通过带 Token 的 blob 请求加载,修复不显示 - URL ?tab= 保持当前标签,刷新列表不再重置到成绩录入 - 奥数区固定数学科目;错题卡片与详情增加删除
195 lines
6.0 KiB
TypeScript
195 lines
6.0 KiB
TypeScript
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>
|
||
)
|
||
}
|