# 匹配题连线功能实现文档 ## 概述 本文档详细介绍了基于React + TypeScript + SVG实现的交互式匹配题连线功能。该功能允许用户通过点击和拖拽的方式,将左侧选项与右侧选项进行连线匹配,并提供实时视觉反馈和答案记录功能。 ## 技术架构 ### 核心技术栈 - **React 18** - 组件化开发和状态管理 - **TypeScript** - 类型安全和代码可维护性 - **SVG** - 矢量图形绘制连线 - **CSS3** - 样式和动画效果 - **Framer Motion** - 高级动画库(可选) ### 系统架构图 ``` 用户交互层 ↓ 事件处理层 (鼠标点击/移动) ↓ 状态管理层 (React Hooks) ↓ SVG渲染层 (连线可视化) ↓ 数据持久层 (答案存储) ``` ## 核心数据结构 ### 1. 连线接口定义 ```typescript // 完成的连线 interface Line { start: { x: number; y: number }; // 起点坐标 end: { x: number; y: number }; // 终点坐标 leftId: string; // 左侧选项ID rightId: string; // 右侧选项ID } // 正在绘制的连线 interface ActiveLine { start: { x: number; y: number }; // 起点坐标 end?: { x: number; y: number }; // 终点坐标(可选,跟随鼠标) leftId: string; // 左侧选项ID } // 匹配题数据结构 interface MatchingQuestion { id: string; leftItems: Array<{ id: string; text: string }>; rightItems: Array<{ id: string; text: string }>; correctMatches: { [leftId: string]: string }; } ``` ### 2. 状态管理 ```typescript const [lines, setLines] = useState([]); // 已完成连线 const [activeLine, setActiveLine] = useState(null); // 当前绘制连线 const [answers, setAnswers] = useState<{ [key: string]: string }>({}); // 用户答案 const itemRefs = useRef<{ [key: string]: HTMLDivElement }>({}); // DOM元素引用 const svgRef = useRef(null); // SVG容器引用 ``` ## 核心功能实现 ### 1. 坐标计算系统 ```typescript /** * 计算元素中心点坐标(相对于SVG容器) * @param element - 目标DOM元素 * @returns 中心点坐标 */ const getItemCenter = (element: HTMLElement) => { const rect = element.getBoundingClientRect(); const svgRect = svgRef.current?.getBoundingClientRect() || { left: 0, top: 0 }; return { x: rect.left + rect.width / 2 - svgRect.left, y: rect.top + rect.height / 2 - svgRect.top }; }; ``` ### 2. 连线开始处理 ```typescript /** * 处理左侧选项点击事件 * @param leftId - 左侧选项ID * @param e - 鼠标事件 */ const handleLeftItemClick = (leftId: string, e: React.MouseEvent) => { const element = itemRefs.current[leftId]; if (element) { // 如果该项已经连线,先移除现有连线 if (lines.some(line => line.leftId === leftId)) { setLines(prev => prev.filter(line => line.leftId !== leftId)); setAnswers(prev => { const newAnswers = { ...prev }; delete newAnswers[`${leftId}_match`]; return newAnswers; }); } // 创建新的活动连线 const center = getItemCenter(element); setActiveLine({ start: center, leftId }); } }; ``` ### 3. 实时连线跟踪 ```typescript /** * 处理鼠标移动事件(连线跟随鼠标) * @param e - 鼠标事件 */ const handleMouseMove = (e: React.MouseEvent) => { if (activeLine) { const svgRect = svgRef.current?.getBoundingClientRect(); if (svgRect) { setActiveLine({ ...activeLine, end: { x: e.clientX - svgRect.left, y: e.clientY - svgRect.top } }); } } }; ``` ### 4. 连线完成处理 ```typescript /** * 处理右侧选项点击事件 * @param rightId - 右侧选项ID * @param e - 鼠标事件 */ const handleRightItemClick = (rightId: string, e: React.MouseEvent) => { if (activeLine) { const element = itemRefs.current[rightId]; if (element) { // 防止重复连接 if (lines.some(line => line.rightId === rightId)) { return; } // 如果左侧项已经有其他连线,先移除 const existingLine = lines.find(line => line.leftId === activeLine.leftId); if (existingLine) { setLines(prev => prev.filter(line => line.leftId !== activeLine.leftId)); setAnswers(prev => { const newAnswers = { ...prev }; delete newAnswers[`${activeLine.leftId}_match`]; return newAnswers; }); } // 创建新连线 const center = getItemCenter(element); setLines(prev => [...prev, { start: activeLine.start, end: center, leftId: activeLine.leftId, rightId }]); setActiveLine(null); // 更新答案 setAnswers(prev => ({ ...prev, [`${activeLine.leftId}_match`]: rightId })); } } }; ``` ## SVG可视化系统 ### 1. SVG容器结构 ```jsx
setActiveLine(null)} > {/* 已完成的连线 */} {lines.map((line, i) => ( ))} {/* 正在绘制的连线 */} {activeLine && ( )} {/* 左侧选项 */}
{/* 选项内容 */}
{/* 右侧选项 */}
{/* 选项内容 */}
``` ### 2. 选项组件结构 ```jsx {question.leftItems.map(item => (
el && (itemRefs.current[item.id] = el)} onClick={(e) => handleLeftItemClick(item.id, e)} className={`bg-blue-900/20 p-4 rounded-lg border border-blue-500/30 cursor-pointer transition-all duration-300 ${ lines.some(line => line.leftId === item.id) ? 'bg-blue-500/30' : 'hover:bg-blue-900/40' }`} > {item.text}
))} ``` ## 辅助功能实现 ### 1. 重置功能 ```typescript /** * 重置匹配题连线 * @param question - 题目数据 */ const handleReset = (question: MatchingQuestion) => { setLines([]); setAnswers(prev => { const newAnswers = { ...prev }; question.leftItems.forEach((item) => { delete newAnswers[`${item.id}_match`]; }); return newAnswers; }); // 重新打乱选项顺序 setShuffledQuestions(prev => prev.map(q => q.id === question.id ? { ...q, leftItems: shuffleArray(q.leftItems), rightItems: shuffleArray(q.rightItems) } : q ) ); }; ``` ### 2. 答案评分系统 ```typescript /** * 计算匹配题得分 */ const calculateMatchingScore = () => { let correctCount = 0; let totalCount = 0; matchingQuestions.forEach(question => { Object.keys(question.correctMatches).forEach(leftId => { totalCount++; const userAnswer = answers[`${leftId}_match`]; const correctAnswer = question.correctMatches[leftId]; if (userAnswer === correctAnswer) { correctCount++; } }); }); return { correct: correctCount, total: totalCount, percentage: Math.round((correctCount / totalCount) * 100) }; }; ``` ### 3. 数组打乱算法 ```typescript /** * Fisher-Yates洗牌算法 * @param array - 待打乱的数组 * @returns 打乱后的新数组 */ const shuffleArray = (array: T[]): T[] => { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; }; ``` ## 特性与优势 ### 1. 用户体验特性 - **实时反馈**:连线过程中提供实时视觉反馈 - **防误操作**:不允许重复连接和无效操作 - **连线替换**:支持重新连接和修改答案 - **视觉提示**:已连接选项有明显的视觉标识 ### 2. 技术特性 - **响应式设计**:支持不同屏幕尺寸 - **类型安全**:TypeScript提供完整的类型检查 - **性能优化**:使用React.memo和useMemo优化渲染 - **事件优化**:合理的事件绑定和清理机制 ### 3. 可维护性 - **模块化设计**:功能拆分清晰,易于维护 - **可配置性**:题目数据和样式可灵活配置 - **可扩展性**:易于添加新功能和动画效果 ## 使用示例 ### 1. 基本使用 ```jsx import React from 'react'; import MatchingQuiz from './MatchingQuiz'; const App = () => { const questionData = { id: 'demo', leftItems: [ { id: 'l1', text: 'React' }, { id: 'l2', text: 'Vue' } ], rightItems: [ { id: 'r1', text: '前端框架' }, { id: 'r2', text: 'JavaScript库' } ], correctMatches: { 'l1': 'r2', 'l2': 'r1' } }; return ; }; ``` ### 2. 自定义样式 ```css /* 自定义连线颜色 */ .matching-line { stroke: #10B981; stroke-width: 3; } /* 自定义选项样式 */ .matching-item { background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); transition: all 0.3s ease; } .matching-item:hover { background: rgba(59, 130, 246, 0.2); transform: translateY(-2px); } .matching-item.connected { background: rgba(59, 130, 246, 0.3); border-color: rgba(59, 130, 246, 0.6); } ``` ## 性能优化建议 ### 1. 渲染优化 ```typescript // 使用React.memo避免不必要的重渲染 const MatchingItem = React.memo<{ item: { id: string; text: string }; isConnected: boolean; onClick: (id: string) => void; }>(({ item, isConnected, onClick }) => { return (
onClick(item.id)} className={`matching-item ${isConnected ? 'connected' : ''}`} > {item.text}
); }); ``` ### 2. 事件优化 ```typescript // 使用useCallback缓存事件处理函数 const handleLeftClick = useCallback((leftId: string) => { // 处理逻辑 }, [/* 依赖项 */]); ``` ## 故障排除 ### 常见问题 1. **连线位置不准确** - 检查`getBoundingClientRect()`计算 - 确保SVG容器尺寸正确 2. **连线无法绘制** - 检查SVG的z-index设置 - 确保pointer-events设置正确 3. **性能问题** - 减少不必要的状态更新 - 使用React.memo优化组件 ## 扩展功能 ### 1. 添加音效反馈 ```typescript const playConnectionSound = () => { const audio = new Audio('/sounds/connect.mp3'); audio.play(); }; ``` ### 2. 添加动画效果 ```typescript // 使用Framer Motion添加连线动画 ``` ### 3. 支持触摸设备 ```typescript const handleTouchStart = (e: React.TouchEvent) => { // 触摸事件处理 }; const handleTouchMove = (e: React.TouchEvent) => { // 触摸移动处理 }; ``` ## 总结 本匹配题连线功能实现了一个完整的交互式学习组件,结合了现代前端技术的最佳实践。通过React的状态管理、SVG的图形能力和TypeScript的类型安全,创造了一个高质量、易维护的教育应用功能模块。 该实现不仅提供了良好的用户体验,还具备了良好的可扩展性和维护性,可以作为类似教育应用的参考实现。