- 添加导航栏组件及直播回放按钮 - 实现视频播放模态框 - 配置赛博朋克风格主题 - 添加课程首页和课程页面 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
12 KiB
匹配题连线功能实现文档
概述
本文档详细介绍了基于React + TypeScript + SVG实现的交互式匹配题连线功能。该功能允许用户通过点击和拖拽的方式,将左侧选项与右侧选项进行连线匹配,并提供实时视觉反馈和答案记录功能。
技术架构
核心技术栈
- React 18 - 组件化开发和状态管理
- TypeScript - 类型安全和代码可维护性
- SVG - 矢量图形绘制连线
- CSS3 - 样式和动画效果
- Framer Motion - 高级动画库(可选)
系统架构图
用户交互层
↓
事件处理层 (鼠标点击/移动)
↓
状态管理层 (React Hooks)
↓
SVG渲染层 (连线可视化)
↓
数据持久层 (答案存储)
核心数据结构
1. 连线接口定义
// 完成的连线
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. 状态管理
const [lines, setLines] = useState<Line[]>([]); // 已完成连线
const [activeLine, setActiveLine] = useState<ActiveLine | null>(null); // 当前绘制连线
const [answers, setAnswers] = useState<{ [key: string]: string }>({}); // 用户答案
const itemRefs = useRef<{ [key: string]: HTMLDivElement }>({}); // DOM元素引用
const svgRef = useRef<SVGSVGElement>(null); // SVG容器引用
核心功能实现
1. 坐标计算系统
/**
* 计算元素中心点坐标(相对于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. 连线开始处理
/**
* 处理左侧选项点击事件
* @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. 实时连线跟踪
/**
* 处理鼠标移动事件(连线跟随鼠标)
* @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. 连线完成处理
/**
* 处理右侧选项点击事件
* @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容器结构
<div
className="relative grid md:grid-cols-2 gap-8 min-h-[400px]"
onMouseMove={handleMouseMove}
onMouseLeave={() => setActiveLine(null)}
>
<svg
ref={svgRef}
className="absolute inset-0 pointer-events-none"
style={{ zIndex: 1, width: '100%', height: '100%' }}
>
{/* 已完成的连线 */}
{lines.map((line, i) => (
<g key={i}>
<line
x1={line.start.x}
y1={line.start.y}
x2={line.end.x}
y2={line.end.y}
stroke="#60A5FA"
strokeWidth="2"
className="transition-all duration-300"
/>
<circle cx={line.start.x} cy={line.start.y} r="4" fill="#60A5FA" />
<circle cx={line.end.x} cy={line.end.y} r="4" fill="#60A5FA" />
</g>
))}
{/* 正在绘制的连线 */}
{activeLine && (
<g>
<line
x1={activeLine.start.x}
y1={activeLine.start.y}
x2={activeLine.end?.x || activeLine.start.x}
y2={activeLine.end?.y || activeLine.start.y}
stroke="#60A5FA"
strokeWidth="2"
strokeDasharray="5,5"
className="animate-pulse"
/>
<circle cx={activeLine.start.x} cy={activeLine.start.y} r="4" fill="#60A5FA" />
</g>
)}
</svg>
{/* 左侧选项 */}
<div className="relative z-10">
{/* 选项内容 */}
</div>
{/* 右侧选项 */}
<div className="relative z-10">
{/* 选项内容 */}
</div>
</div>
2. 选项组件结构
{question.leftItems.map(item => (
<div
key={item.id}
ref={el => 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'
}`}
>
<span className="text-white font-medium">{item.text}</span>
</div>
))}
辅助功能实现
1. 重置功能
/**
* 重置匹配题连线
* @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. 答案评分系统
/**
* 计算匹配题得分
*/
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. 数组打乱算法
/**
* Fisher-Yates洗牌算法
* @param array - 待打乱的数组
* @returns 打乱后的新数组
*/
const shuffleArray = <T>(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. 基本使用
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 <MatchingQuiz question={questionData} />;
};
2. 自定义样式
/* 自定义连线颜色 */
.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. 渲染优化
// 使用React.memo避免不必要的重渲染
const MatchingItem = React.memo<{
item: { id: string; text: string };
isConnected: boolean;
onClick: (id: string) => void;
}>(({ item, isConnected, onClick }) => {
return (
<div
onClick={() => onClick(item.id)}
className={`matching-item ${isConnected ? 'connected' : ''}`}
>
{item.text}
</div>
);
});
2. 事件优化
// 使用useCallback缓存事件处理函数
const handleLeftClick = useCallback((leftId: string) => {
// 处理逻辑
}, [/* 依赖项 */]);
故障排除
常见问题
-
连线位置不准确
- 检查
getBoundingClientRect()计算 - 确保SVG容器尺寸正确
- 检查
-
连线无法绘制
- 检查SVG的z-index设置
- 确保pointer-events设置正确
-
性能问题
- 减少不必要的状态更新
- 使用React.memo优化组件
扩展功能
1. 添加音效反馈
const playConnectionSound = () => {
const audio = new Audio('/sounds/connect.mp3');
audio.play();
};
2. 添加动画效果
// 使用Framer Motion添加连线动画
<motion.line
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.5 }}
// ...其他属性
/>
3. 支持触摸设备
const handleTouchStart = (e: React.TouchEvent) => {
// 触摸事件处理
};
const handleTouchMove = (e: React.TouchEvent) => {
// 触摸移动处理
};
总结
本匹配题连线功能实现了一个完整的交互式学习组件,结合了现代前端技术的最佳实践。通过React的状态管理、SVG的图形能力和TypeScript的类型安全,创造了一个高质量、易维护的教育应用功能模块。
该实现不仅提供了良好的用户体验,还具备了良好的可扩展性和维护性,可以作为类似教育应用的参考实现。