142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
"use client"
|
|
|
|
import React from "react"
|
|
import { ScrollArea } from "@/shared/components/ui/scroll-area"
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/shared/components/ui/dialog"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Eye, Printer } from "lucide-react"
|
|
import type { ExamNode } from "./selected-question-list"
|
|
|
|
type ChoiceOption = {
|
|
id: string
|
|
text: string
|
|
}
|
|
|
|
type QuestionContent = {
|
|
text?: string
|
|
options?: ChoiceOption[]
|
|
}
|
|
|
|
type ExamPaperPreviewProps = {
|
|
title: string
|
|
subject: string
|
|
grade: string
|
|
durationMin: number
|
|
totalScore: number
|
|
nodes: ExamNode[]
|
|
}
|
|
|
|
export function ExamPaperPreview({ title, subject, grade, durationMin, totalScore, nodes }: ExamPaperPreviewProps) {
|
|
// Helper to flatten questions for continuous numbering
|
|
let questionCounter = 0
|
|
|
|
const renderNode = (node: ExamNode, depth: number = 0) => {
|
|
if (node.type === 'group') {
|
|
return (
|
|
<div key={node.id} className="space-y-4 mb-6">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className={`font-bold ${depth === 0 ? 'text-lg' : 'text-md'} text-foreground/90`}>
|
|
{node.title || "Section"}
|
|
</h3>
|
|
{/* Optional: Show section score if needed */}
|
|
</div>
|
|
<div className="pl-0">
|
|
{node.children?.map(child => renderNode(child, depth + 1))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (node.type === 'question' && node.question) {
|
|
questionCounter++
|
|
const q = node.question
|
|
const content = q.content as QuestionContent
|
|
|
|
return (
|
|
<div key={node.id} className="mb-6 break-inside-avoid">
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold text-foreground min-w-[24px]">{questionCounter}.</span>
|
|
<div className="flex-1 space-y-2">
|
|
<div className="text-foreground/90 leading-relaxed whitespace-pre-wrap">
|
|
{content.text ?? ""}
|
|
<span className="text-muted-foreground text-sm ml-2">({node.score}分)</span>
|
|
</div>
|
|
|
|
{/* Options for Choice Questions */}
|
|
{(q.type === 'single_choice' || q.type === 'multiple_choice') && content.options && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-2 gap-x-4 mt-2 pl-2">
|
|
{content.options.map((opt) => (
|
|
<div key={opt.id} className="flex gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors">
|
|
<span className="font-medium">{opt.id}.</span>
|
|
<span>{opt.text}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Space for written answers */}
|
|
{q.type === 'text' && (
|
|
<div className="mt-4 h-24 border-b border-dashed border-muted-foreground/30 w-full"></div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button variant="secondary" size="sm" className="gap-2">
|
|
<Eye className="h-4 w-4" />
|
|
Preview Exam
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-4xl h-[90vh] flex flex-col p-0 gap-0">
|
|
<DialogHeader className="p-6 pb-2 border-b shrink-0">
|
|
<div className="flex items-center justify-between">
|
|
<DialogTitle>Exam Preview</DialogTitle>
|
|
<Button variant="outline" size="sm" onClick={() => window.print()} className="hidden">
|
|
<Printer className="h-4 w-4 mr-2" />
|
|
Print
|
|
</Button>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<ScrollArea className="flex-1 p-8 bg-white/50 dark:bg-zinc-950/50">
|
|
<div className="max-w-3xl mx-auto bg-card shadow-sm border p-12 min-h-[1000px] print:shadow-none print:border-none">
|
|
{/* Header */}
|
|
<div className="text-center space-y-4 mb-12 border-b-2 border-primary/20 pb-8">
|
|
<h1 className="text-3xl font-black tracking-tight text-foreground">{title}</h1>
|
|
<div className="flex justify-center gap-8 text-sm text-muted-foreground font-medium uppercase tracking-wide">
|
|
<span>Subject: {subject}</span>
|
|
<span>Grade: {grade}</span>
|
|
<span>Time: {durationMin} mins</span>
|
|
<span>Total: {totalScore} pts</span>
|
|
</div>
|
|
<div className="flex justify-center gap-12 text-sm pt-4">
|
|
<div className="w-32 border-b border-foreground/20 text-left px-1">Class:</div>
|
|
<div className="w-32 border-b border-foreground/20 text-left px-1">Name:</div>
|
|
<div className="w-32 border-b border-foreground/20 text-left px-1">No.:</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="space-y-2">
|
|
{nodes.length === 0 ? (
|
|
<div className="text-center py-20 text-muted-foreground">
|
|
Empty Exam Paper
|
|
</div>
|
|
) : (
|
|
nodes.map(node => renderNode(node))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|