Spaces:
Build error
Build error
| "use client"; | |
| import { useState } from 'react'; | |
| import { CheckCircle, XCircle, AlertCircle, Share2, Copy, Check, ChevronDown, ChevronUp } from 'lucide-react'; | |
| export interface QuizQuestion { | |
| id: number; | |
| question: string; | |
| options: string[]; | |
| correctAnswer: number; // 0-based index | |
| explanation: string; | |
| } | |
| interface InteractiveQuizProps { | |
| questions: QuizQuestion[]; | |
| allowSharing?: boolean; | |
| } | |
| export function InteractiveQuiz({ questions, allowSharing = false }: InteractiveQuizProps) { | |
| const [userAnswers, setUserAnswers] = useState<Record<number, number>>({}); | |
| const [isSubmitted, setIsSubmitted] = useState(false); | |
| const [shareUrl, setShareUrl] = useState<string | null>(null); | |
| const [isGeneratingLink, setIsGeneratingLink] = useState(false); | |
| const [isCopied, setIsCopied] = useState(false); | |
| const handleSelect = (questionId: number, optionIndex: number) => { | |
| if (isSubmitted) return; | |
| setUserAnswers(prev => ({ | |
| ...prev, | |
| [questionId]: optionIndex | |
| })); | |
| }; | |
| const calculateScore = () => { | |
| let correct = 0; | |
| questions.forEach(q => { | |
| if (userAnswers[q.id] === q.correctAnswer) { | |
| correct++; | |
| } | |
| }); | |
| return correct; | |
| }; | |
| const handleSubmit = () => { | |
| if (Object.keys(userAnswers).length < questions.length) { | |
| if (!confirm("还有题目未完成,确定要提交吗?")) return; | |
| } | |
| setIsSubmitted(true); | |
| }; | |
| const handleGenerateShareLink = async () => { | |
| if (shareUrl) return; | |
| setIsGeneratingLink(true); | |
| try { | |
| const response = await fetch('/api/quiz', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ questions }), | |
| }); | |
| const data = await response.json(); | |
| if (data.id) { | |
| const url = `${window.location.origin}/quiz/${data.id}`; | |
| setShareUrl(url); | |
| } | |
| } catch (error) { | |
| alert('生成链接失败,请稍后重试'); | |
| console.error(error); | |
| } finally { | |
| setIsGeneratingLink(false); | |
| } | |
| }; | |
| const copyToClipboard = () => { | |
| if (!shareUrl) return; | |
| navigator.clipboard.writeText(shareUrl); | |
| setIsCopied(true); | |
| setTimeout(() => setIsCopied(false), 2000); | |
| }; | |
| const score = calculateScore(); | |
| return ( | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-200 my-4 w-full max-w-2xl mx-auto overflow-hidden"> | |
| <div className="p-6 border-b border-gray-100 flex items-center justify-between bg-white"> | |
| <h2 className="text-xl font-bold text-gray-800 flex items-center gap-2"> | |
| <span>📝</span> 知识库测试 | |
| <span className="text-sm font-normal text-gray-500 ml-2">({questions.length} 题)</span> | |
| </h2> | |
| <div className="flex items-center gap-3"> | |
| {allowSharing && !shareUrl && ( | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleGenerateShareLink(); | |
| }} | |
| disabled={isGeneratingLink} | |
| className="text-sm px-3 py-1.5 bg-primary-50 text-primary-600 rounded-lg hover:bg-primary-100 transition-colors flex items-center gap-2" | |
| > | |
| {isGeneratingLink ? ( | |
| <span>生成中...</span> | |
| ) : ( | |
| <> | |
| <Share2 className="w-4 h-4" /> | |
| <span>分享</span> | |
| </> | |
| )} | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <div className="p-6 animate-in slide-in-from-top-2 duration-200"> | |
| {allowSharing && shareUrl && ( | |
| <div className="mb-6 bg-primary-50 border border-primary-200 rounded-lg p-4"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-sm font-bold text-primary-800">✅ 试卷链接已生成</span> | |
| <a | |
| href={shareUrl} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-xs flex items-center gap-1 text-primary-700 hover:text-primary-900 font-medium bg-white px-2 py-1 rounded border border-primary-200 hover:border-primary-300" | |
| > | |
| <Share2 className="w-3 h-3" /> | |
| 直接打开 | |
| </a> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex-1 text-xs text-gray-500 break-all bg-white p-2 rounded border border-primary-100 select-all"> | |
| {shareUrl} | |
| </div> | |
| <button | |
| onClick={copyToClipboard} | |
| className="shrink-0 text-xs flex items-center gap-1 text-primary-700 hover:text-primary-900 font-medium bg-white px-2 py-2 rounded border border-primary-200 hover:border-primary-300" | |
| > | |
| {isCopied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />} | |
| {isCopied ? "已复制" : "复制"} | |
| </button> | |
| </div> | |
| <p className="text-xs text-primary-700 mt-2"> | |
| 将此链接发送给员工,他们可以直接在线答题。 | |
| </p> | |
| </div> | |
| )} | |
| <div className="space-y-8"> | |
| {questions.map((q, index) => ( | |
| <div key={q.id} className="border-b border-gray-100 pb-6 last:border-0 last:pb-0"> | |
| <h3 className="text-lg font-medium text-gray-900 mb-4 leading-relaxed"> | |
| {index + 1}. {q.question} | |
| </h3> | |
| <div className="space-y-3"> | |
| {q.options.map((option, optIndex) => { | |
| const isSelected = userAnswers[q.id] === optIndex; | |
| const isCorrect = q.correctAnswer === optIndex; | |
| const isWrong = isSelected && !isCorrect; | |
| let containerClass = "p-3 rounded-lg border cursor-pointer transition-all flex items-center justify-between group "; | |
| if (isSubmitted) { | |
| if (isCorrect) containerClass += "bg-green-50 border-green-200 text-green-900"; | |
| else if (isWrong) containerClass += "bg-red-50 border-red-200 text-red-900"; | |
| else containerClass += "bg-gray-50 border-gray-200 text-gray-400 opacity-70"; | |
| } else { | |
| if (isSelected) containerClass += "bg-primary-50 border-primary-500 text-primary-700 shadow-sm"; | |
| else containerClass += "bg-white border-gray-200 hover:bg-gray-50 hover:border-gray-300 text-gray-700"; | |
| } | |
| return ( | |
| <div | |
| key={optIndex} | |
| onClick={() => handleSelect(q.id, optIndex)} | |
| className={containerClass} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <div className={`w-6 h-6 rounded-full border flex items-center justify-center shrink-0 transition-colors ${ | |
| isSelected || (isSubmitted && isCorrect) | |
| ? "border-current" | |
| : "border-gray-300 group-hover:border-gray-400" | |
| }`}> | |
| {(isSelected || (isSubmitted && isCorrect)) && ( | |
| <div className="w-3 h-3 rounded-full bg-current" /> | |
| )} | |
| </div> | |
| <span className="text-sm font-medium">{String.fromCharCode(65 + optIndex)}. {option.replace(/^[A-Z][.、]\s*/, '')}</span> | |
| </div> | |
| {isSubmitted && ( | |
| <div> | |
| {isCorrect && <CheckCircle className="w-5 h-5 text-green-600" />} | |
| {isWrong && <XCircle className="w-5 h-5 text-red-600" />} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {isSubmitted && ( | |
| <div className="mt-4 bg-blue-50 p-4 rounded-lg text-sm text-blue-800 flex gap-2 items-start animate-in fade-in slide-in-from-top-2"> | |
| <AlertCircle className="w-5 h-5 shrink-0 mt-0.5" /> | |
| <div> | |
| <span className="font-bold block mb-1">解析:</span> | |
| {q.explanation} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| {!isSubmitted ? ( | |
| <button | |
| onClick={handleSubmit} | |
| className="w-full mt-8 py-3 px-6 bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white font-bold rounded-lg shadow-md transition-all transform active:scale-[0.98]" | |
| > | |
| 提交答案 | |
| </button> | |
| ) : ( | |
| <div className="mt-8 pt-6 border-t border-gray-200 text-center animate-in zoom-in-95 duration-300"> | |
| <div className="text-3xl font-bold text-gray-900 mb-2"> | |
| 得分: <span className={` | |
| ${score === questions.length ? 'text-green-600' : ''} | |
| ${score < questions.length && score > 0 ? 'text-blue-600' : ''} | |
| ${score === 0 ? 'text-red-600' : ''} | |
| `}>{score}</span> / {questions.length} | |
| </div> | |
| <p className="text-gray-500 font-medium"> | |
| {score === questions.length ? "太棒了!全对!🎉" : | |
| score >= questions.length * 0.6 ? "成绩不错,继续加油!💪" : "再接再厉,多复习一下知识库哦!📚"} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |