import { useState, useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; import type { MatchQuizPanelProps } from "./types"; /** * 连线题面板组件 * 支持移动端触摸操作 * * @example * ```tsx * { * console.log("匹配结果:", matches, "是否正确:", isCorrect); * }} * /> * ``` */ const MatchQuizPanel = ({ visible, zIndex = 10000, title, leftItems, rightItems, correctMatches, errorMessage, onSubmit, onError, disabled = false, }: MatchQuizPanelProps) => { const [matches, setMatches] = useState>([]); const [selectedLeftId, setSelectedLeftId] = useState(null); const [error, setError] = useState(""); const containerRef = useRef(null); // 当窗口关闭时,重置状态 useEffect(() => { if (!visible) { setMatches([]); setSelectedLeftId(null); setError(""); } }, [visible]); // 处理左列项点击 const handleLeftItemClick = (leftId: string) => { if (disabled) return; // 如果已经匹配过,先取消匹配 const existingMatch = matches.find((m) => m.leftId === leftId); if (existingMatch) { setMatches(matches.filter((m) => m.leftId !== leftId)); setSelectedLeftId(null); setError(""); return; } // 如果已经选中了另一个左项,取消之前的选中 if (selectedLeftId && selectedLeftId !== leftId) { setSelectedLeftId(leftId); setError(""); return; } // 切换选中状态 setSelectedLeftId(selectedLeftId === leftId ? null : leftId); setError(""); }; // 处理右列项点击 const handleRightItemClick = (rightId: string) => { if (disabled) return; // 如果没有选中左项,直接返回 if (!selectedLeftId) return; // 如果已经匹配过,先取消匹配 const existingMatch = matches.find((m) => m.rightId === rightId); if (existingMatch) { setMatches(matches.filter((m) => m.rightId !== rightId)); setSelectedLeftId(null); setError(""); return; } // 创建新的匹配 const newMatch = { leftId: selectedLeftId, rightId }; const newMatches = [...matches, newMatch]; setMatches(newMatches); setSelectedLeftId(null); setError(""); // 检查是否所有项都已匹配 if (newMatches.length === leftItems.length && newMatches.length === rightItems.length) { // 验证答案 const isCorrect = validateMatches(newMatches); if (isCorrect) { onSubmit?.(newMatches, true); } else { const errorMsg = errorMessage || "连线不正确,请重新尝试!"; setError(errorMsg); onError?.(errorMsg); // 清空匹配,允许重新尝试 setTimeout(() => { setMatches([]); }, 2000); } } }; // 验证匹配是否正确 const validateMatches = (matchesToValidate: Array<{ leftId: string; rightId: string }>) => { if (!correctMatches || matchesToValidate.length !== correctMatches.length) { return false; } // 检查每个匹配是否正确 for (const match of matchesToValidate) { const isCorrect = correctMatches.some( (cm) => cm.leftId === match.leftId && cm.rightId === match.rightId ); if (!isCorrect) { return false; } } return true; }; // 获取左项是否已匹配 const isLeftItemMatched = (leftId: string) => { return matches.some((m) => m.leftId === leftId); }; // 获取右项是否已匹配 const isRightItemMatched = (rightId: string) => { return matches.some((m) => m.rightId === rightId); }; // 获取左项对应的右项ID const getMatchedRightId = (leftId: string) => { const match = matches.find((m) => m.leftId === leftId); return match?.rightId || null; }; // 获取右项对应的左项ID const getMatchedLeftId = (rightId: string) => { const match = matches.find((m) => m.rightId === rightId); return match?.leftId || null; }; if (!visible) return null; return (
{/* 题干卡片 */}
{title}
{/* 答案反馈 */} {error && (
{error}
)}
{/* 连线区域 */}
{/* 左列:日期 */}
{leftItems.map((item) => { const isMatched = isLeftItemMatched(item.id); const isSelected = selectedLeftId === item.id; const matchedRightId = getMatchedRightId(item.id); return (
handleLeftItemClick(item.id)} onTouchStart={(e) => { // 移动端触摸优化 e.currentTarget.classList.add("scale-105"); }} onTouchEnd={(e) => { e.currentTarget.classList.remove("scale-105"); }} > {item.label} {isMatched && (
✓
)}
); })}
{/* 中间连线区域(移动端隐藏,桌面端显示) */}
{/* 绘制连线 */} {matches.map((match, index) => { const leftItem = leftItems.find((item) => item.id === match.leftId); const rightItem = rightItems.find((item) => item.id === match.rightId); if (!leftItem || !rightItem) return null; const leftIndex = leftItems.findIndex((item) => item.id === match.leftId); const rightIndex = rightItems.findIndex((item) => item.id === match.rightId); // 计算连线起点和终点(基于项目高度和间距) const itemHeight = 80; // 每个项目的大概高度 const gap = 16; // 项目之间的间距 const startY = leftIndex * (itemHeight + gap) + itemHeight / 2; const endY = rightIndex * (itemHeight + gap) + itemHeight / 2; return ( ); })} {/* 箭头标记定义 */}
{/* 右列:事件 */}
{rightItems.map((item) => { const isMatched = isRightItemMatched(item.id); const matchedLeftId = getMatchedLeftId(item.id); return (
handleRightItemClick(item.id)} onTouchStart={(e) => { if (selectedLeftId) { e.currentTarget.classList.add("scale-105"); } }} onTouchEnd={(e) => { e.currentTarget.classList.remove("scale-105"); }} > {item.label} {isMatched && (
✓
)}
); })}
{/* 移动端连线提示 */}
请先点击左侧日期,再点击右侧对应的事件进行连线
); }; export default MatchQuizPanel;