feat(exams,homework): add rich text exam editor and scan-based grading
- Add Tiptap-based rich text editor with custom extensions (dotted-mark, blank-node, image-node, group-block, question-block) for exam creation - Add AI auto-marking action to convert pasted exam text to structured editor doc - Add resizable split-panel layout for editor + live preview - Add student scan upload (photo of paper answers) with drag-drop and reorder - Add scan image viewer with zoom/rotate/fullscreen for teachers - Add scan grading view with side-by-side questions and scan images - Add /teacher/exams/new and /teacher/homework/submissions/[id]/scan-grading routes - Fix getScansAction to support both teacher (HOMEWORK_GRADE) and student (HOMEWORK_SUBMIT) permission scopes - Add i18n keys for rich editor, scan upload, and scan grading (zh-CN/en) - Sync architecture diagrams (004/005) with new modules, routes, and deps
This commit is contained in:
@@ -21,12 +21,13 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/shared/components/ui/alert-dialog"
|
||||
import { Clock, CheckCircle2, Save, FileText, ChevronLeft, TriangleAlert, CloudUpload, CloudOff, Check, Loader2, Timer } from "lucide-react"
|
||||
import { Clock, CheckCircle2, Save, FileText, ChevronLeft, TriangleAlert, CloudUpload, CloudOff, Check, Loader2, Timer, Camera } from "lucide-react"
|
||||
import { formatDate, cn } from "@/shared/lib/utils"
|
||||
|
||||
import type { StudentHomeworkTakeData } from "../types"
|
||||
import { saveHomeworkAnswerAction, startHomeworkSubmissionAction, submitHomeworkAction } from "../actions"
|
||||
import { saveHomeworkAnswerAction, startHomeworkSubmissionAction, submitHomeworkAction, getScansAction, deleteScanAction } from "../actions"
|
||||
import { QuestionRenderer } from "./question-renderer"
|
||||
import { ScanUploader, type ScanImage } from "./scan-uploader"
|
||||
import { parseSavedAnswer } from "../lib/question-content-utils"
|
||||
import { useDebouncedAutoSave, loadOfflineCache, clearOfflineCache } from "../hooks/use-debounced-auto-save"
|
||||
import { useExamCountdown } from "../hooks/use-exam-countdown"
|
||||
@@ -43,6 +44,26 @@ export function HomeworkTakeView({ assignmentId, initialData }: HomeworkTakeView
|
||||
const [submissionStatus, setSubmissionStatus] = useState<string>(initialData.submission?.status ?? "not_started")
|
||||
const [isBusy, setIsBusy] = useState(false)
|
||||
const [showSubmitConfirm, setShowSubmitConfirm] = useState(false)
|
||||
const [scanImages, setScanImages] = useState<ScanImage[]>([])
|
||||
|
||||
// 加载已有答题扫描图(拍照上传)
|
||||
useEffect(() => {
|
||||
if (!submissionId) return
|
||||
void (async () => {
|
||||
const result = await getScansAction(submissionId)
|
||||
if (result.success && result.data) {
|
||||
setScanImages(result.data)
|
||||
}
|
||||
})()
|
||||
}, [submissionId])
|
||||
|
||||
const handleDeleteScan = async (fileId: string) => {
|
||||
if (!submissionId) return
|
||||
const result = await deleteScanAction(submissionId, fileId)
|
||||
if (!result.success) {
|
||||
toast.error(result.message || t("homework.take.saveFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
const initialAnswersByQuestionId = useMemo(() => {
|
||||
const map = new Map<string, { answer: unknown }>()
|
||||
@@ -357,6 +378,29 @@ export function HomeworkTakeView({ assignmentId, initialData }: HomeworkTakeView
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
|
||||
{showQuestions && submissionId && (
|
||||
<Card className="border-l-4 border-l-blue-500 shadow-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Camera className="h-4 w-4 text-blue-500" />
|
||||
<h3 className="font-semibold text-sm">{t("homework.take.scanTitle")}</h3>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t("homework.take.scanDescription")}
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<ScanUploader
|
||||
images={scanImages}
|
||||
onChange={setScanImages}
|
||||
onDeleteScan={handleDeleteScan}
|
||||
submissionId={submissionId}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user