作业帮式错题标注:OCR 定位错误红框 + 解题思路。

- PaddleOCR 行级坐标 + AI 识别错答区域,生成标注图

- 解法拆分为「解题思路」与「详细解答」

- 详情页标注图/原图切换,列表显示标注缩略图
This commit is contained in:
dekun
2026-06-28 13:50:20 +08:00
parent c30e21b51e
commit a2a6d59f7c
16 changed files with 852 additions and 507 deletions
+36 -12
View File
@@ -3,6 +3,7 @@ import api from '../api/client'
interface Props {
questionId: string
variant?: 'original' | 'annotated'
className?: string
alt?: string
style?: React.CSSProperties
@@ -10,6 +11,7 @@ interface Props {
export default function AuthenticatedImage({
questionId,
variant = 'original',
className,
alt = '题目',
style,
@@ -21,36 +23,58 @@ export default function AuthenticatedImage({
let objectUrl: string | null = null
let cancelled = false
api
.get(`/wrong-questions/${questionId}/image`, { responseType: 'blob' })
.then((res) => {
const load = async (path: string, fallback?: string) => {
try {
const res = await api.get(path, { responseType: 'blob' })
if (cancelled) return
objectUrl = URL.createObjectURL(res.data)
setSrc(objectUrl)
setFailed(false)
})
.catch(() => {
if (!cancelled) setFailed(true)
})
} catch {
if (fallback && !cancelled) {
await load(fallback)
} else if (!cancelled) {
setFailed(true)
}
}
}
const annotatedPath = `/wrong-questions/${questionId}/annotated-image`
const originalPath = `/wrong-questions/${questionId}/image`
if (variant === 'annotated') {
load(annotatedPath, originalPath)
} else {
load(originalPath)
}
return () => {
cancelled = true
if (objectUrl) URL.revokeObjectURL(objectUrl)
}
}, [questionId])
}, [questionId, variant])
if (failed) {
return (
<div className={className} style={{ ...style, background: '#fafafa', color: '#999', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12 }}>
<div
className={className}
style={{
...style,
background: '#fafafa',
color: '#999',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
}}
>
</div>
)
}
if (!src) {
return (
<div className={className} style={{ ...style, background: '#fafafa' }} />
)
return <div className={className} style={{ ...style, background: '#fafafa' }} />
}
return <img src={src} alt={alt} className={className} style={style} />
@@ -38,7 +38,7 @@ export default function WrongQuestionList({
{items.map((wq) => (
<div key={wq.id} className="wq-card">
<div className="wq-card-click" onClick={() => onSelect(wq.id)}>
<AuthenticatedImage questionId={wq.id} alt="题目" className="wq-card-img" />
<AuthenticatedImage questionId={wq.id} variant="annotated" alt="题目" className="wq-card-img" />
<div className="wq-card-body">
<Typography.Text strong>{wq.subject_name}</Typography.Text>
{wq.category === 'olympiad' && (