Skip to content

Commit 307f464

Browse files
committed
feat: 添加UI组件和状态管理
1 parent 4932b29 commit 307f464

File tree

7 files changed

+766
-1
lines changed

7 files changed

+766
-1
lines changed

src/components/BackToHot100.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import React from 'react';
21
import styled from 'styled-components';
32

43
const BackLink = styled.a`

src/components/CanvasComponent.tsx

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import React, { useEffect, useRef } from 'react';
2+
import * as d3 from 'd3';
3+
import { AnimationState } from '../state/animationSlice';
4+
5+
interface LinkData {
6+
source: number;
7+
target: number;
8+
}
9+
10+
interface NodeData {
11+
x: number;
12+
y: number;
13+
value: number;
14+
id: number;
15+
}
16+
17+
interface CanvasComponentProps {
18+
state: AnimationState;
19+
width: number;
20+
height: number;
21+
}
22+
23+
const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height }) => {
24+
const svgRef = useRef<SVGSVGElement>(null);
25+
26+
// 渲染楼梯节点图
27+
useEffect(() => {
28+
if (!svgRef.current || !state.staircase.nodes.length) return;
29+
30+
const svg = d3.select(svgRef.current);
31+
svg.selectAll('*').remove(); // 清空画布
32+
33+
// 创建图层
34+
const nodeGroup = svg.append('g').attr('class', 'nodes');
35+
const linkGroup = svg.append('g').attr('class', 'links');
36+
const textGroup = svg.append('g').attr('class', 'texts');
37+
const formulaGroup = svg.append('g').attr('class', 'formula');
38+
39+
// 绘制连接线
40+
linkGroup.selectAll('line')
41+
.data(state.staircase.links)
42+
.enter()
43+
.append('line')
44+
.attr('x1', (d: LinkData) => state.staircase.nodes[d.source].x)
45+
.attr('y1', (d: LinkData) => state.staircase.nodes[d.source].y)
46+
.attr('x2', (d: LinkData) => state.staircase.nodes[d.target].x)
47+
.attr('y2', (d: LinkData) => state.staircase.nodes[d.target].y)
48+
.attr('stroke', '#666')
49+
.attr('stroke-width', 2);
50+
51+
// 绘制节点
52+
const nodeColor = getColorByAlgorithm(state.currentAlgorithm);
53+
54+
nodeGroup.selectAll('circle')
55+
.data(state.staircase.nodes)
56+
.enter()
57+
.append('circle')
58+
.attr('cx', (d: NodeData) => d.x)
59+
.attr('cy', (d: NodeData) => d.y)
60+
.attr('r', 20)
61+
.attr('fill', nodeColor)
62+
.attr('stroke', '#333')
63+
.attr('stroke-width', 2);
64+
65+
// 添加节点值文本
66+
textGroup.selectAll('text')
67+
.data(state.staircase.nodes)
68+
.enter()
69+
.append('text')
70+
.attr('x', (d: NodeData) => d.x)
71+
.attr('y', (d: NodeData) => d.y + 5)
72+
.attr('text-anchor', 'middle')
73+
.attr('fill', '#fff')
74+
.attr('font-weight', 'bold')
75+
.text((d: NodeData) => d.value);
76+
77+
// 渲染公式
78+
if (state.formula) {
79+
formulaGroup.append('text')
80+
.attr('x', width - 50)
81+
.attr('y', 30)
82+
.attr('text-anchor', 'end')
83+
.attr('font-family', 'serif')
84+
.attr('font-size', '14px')
85+
.text(state.formula);
86+
}
87+
88+
// 渲染矩阵(仅当使用矩阵算法时)
89+
if (state.currentAlgorithm === 'matrix' && state.matrix.length > 0) {
90+
renderMatrix(svg, state.matrix);
91+
}
92+
93+
}, [state.staircase, state.currentAlgorithm, state.formula, state.matrix, width, height]);
94+
95+
// 根据算法类型获取颜色
96+
const getColorByAlgorithm = (algorithm: AnimationState['currentAlgorithm']): string => {
97+
switch (algorithm) {
98+
case 'dp':
99+
return '#4CAF50'; // 绿色
100+
case 'matrix':
101+
return '#2196F3'; // 蓝色
102+
case 'formula':
103+
return '#9C27B0'; // 紫色
104+
default:
105+
return '#4CAF50';
106+
}
107+
};
108+
109+
// 渲染矩阵
110+
const renderMatrix = (svg: d3.Selection<SVGSVGElement, unknown, null, undefined>, matrix: number[][]) => {
111+
const matrixGroup = svg.append('g')
112+
.attr('class', 'matrix')
113+
.attr('transform', `translate(${width - 100}, 50)`);
114+
115+
const cellSize = 30;
116+
117+
// 绘制矩阵单元格
118+
matrix.forEach((row, i) => {
119+
row.forEach((value, j) => {
120+
matrixGroup.append('rect')
121+
.attr('x', j * cellSize)
122+
.attr('y', i * cellSize)
123+
.attr('width', cellSize)
124+
.attr('height', cellSize)
125+
.attr('fill', '#f5f5f5')
126+
.attr('stroke', '#333');
127+
128+
matrixGroup.append('text')
129+
.attr('x', j * cellSize + cellSize / 2)
130+
.attr('y', i * cellSize + cellSize / 2 + 5)
131+
.attr('text-anchor', 'middle')
132+
.text(value);
133+
});
134+
});
135+
136+
// 添加矩阵括号
137+
matrixGroup.append('path')
138+
.attr('d', `M0,0 L-5,0 L-5,${matrix.length * cellSize} L0,${matrix.length * cellSize}`)
139+
.attr('stroke', '#333')
140+
.attr('fill', 'none');
141+
142+
matrixGroup.append('path')
143+
.attr('d', `M${matrix[0].length * cellSize},0 L${matrix[0].length * cellSize + 5},0 L${matrix[0].length * cellSize + 5},${matrix.length * cellSize} L${matrix[0].length * cellSize},${matrix.length * cellSize}`)
144+
.attr('stroke', '#333')
145+
.attr('fill', 'none');
146+
};
147+
148+
return (
149+
<svg
150+
ref={svgRef}
151+
width={width}
152+
height={height}
153+
style={{ border: '1px solid #ccc', borderRadius: '4px' }}
154+
/>
155+
);
156+
};
157+
158+
export default CanvasComponent;

src/components/ControlPanel.tsx

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import React from 'react';
2+
import {
3+
playPause,
4+
resetAnimation,
5+
setAlgorithm,
6+
setCurrentStep,
7+
setPlaybackSpeed,
8+
AnimationState
9+
} from '../state/animationSlice';
10+
11+
interface ControlPanelProps {
12+
state: AnimationState;
13+
dispatch: (action: any) => void; // Redux dispatch 函数
14+
}
15+
16+
const ControlPanel: React.FC<ControlPanelProps> = ({ state, dispatch }) => {
17+
// 切换算法
18+
const handleAlgorithmChange = (algorithm: AnimationState['currentAlgorithm']) => {
19+
dispatch(setAlgorithm(algorithm));
20+
dispatch(resetAnimation());
21+
};
22+
23+
// 播放/暂停动画
24+
const handlePlayPause = () => {
25+
dispatch(playPause());
26+
};
27+
28+
// 重置动画
29+
const handleReset = () => {
30+
dispatch(resetAnimation());
31+
};
32+
33+
// 步进控制
34+
const handleStepChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35+
const step = parseInt(e.target.value, 10);
36+
dispatch(setCurrentStep(step));
37+
};
38+
39+
// 上一步
40+
const handlePreviousStep = () => {
41+
if (state.currentStep > 0) {
42+
dispatch(setCurrentStep(state.currentStep - 1));
43+
// 如果正在播放,则暂停
44+
if (state.isPlaying) {
45+
dispatch(playPause());
46+
}
47+
}
48+
};
49+
50+
// 下一步
51+
const handleNextStep = () => {
52+
if (state.currentStep < state.totalSteps - 1) {
53+
dispatch(setCurrentStep(state.currentStep + 1));
54+
// 如果正在播放,则暂停
55+
if (state.isPlaying) {
56+
dispatch(playPause());
57+
}
58+
}
59+
};
60+
61+
// 处理播放速度变化
62+
const handleSpeedChange = (e: React.ChangeEvent<HTMLInputElement>) => {
63+
const speed = parseFloat(e.target.value);
64+
dispatch(setPlaybackSpeed(speed));
65+
};
66+
67+
return (
68+
<div className="control-panel" style={styles.container}>
69+
<div style={styles.section}>
70+
<h3 style={styles.heading}>爬楼梯算法</h3>
71+
<div style={styles.buttonGroup}>
72+
<button
73+
style={{
74+
...styles.algorithmButton,
75+
backgroundColor: state.currentAlgorithm === 'dp' ? '#4CAF50' : '#e0e0e0',
76+
color: state.currentAlgorithm === 'dp' ? 'white' : 'black'
77+
}}
78+
onClick={() => handleAlgorithmChange('dp')}
79+
>
80+
动态规划
81+
</button>
82+
<button
83+
style={{
84+
...styles.algorithmButton,
85+
backgroundColor: state.currentAlgorithm === 'matrix' ? '#2196F3' : '#e0e0e0',
86+
color: state.currentAlgorithm === 'matrix' ? 'white' : 'black'
87+
}}
88+
onClick={() => handleAlgorithmChange('matrix')}
89+
>
90+
矩阵快速幂
91+
</button>
92+
<button
93+
style={{
94+
...styles.algorithmButton,
95+
backgroundColor: state.currentAlgorithm === 'formula' ? '#9C27B0' : '#e0e0e0',
96+
color: state.currentAlgorithm === 'formula' ? 'white' : 'black'
97+
}}
98+
onClick={() => handleAlgorithmChange('formula')}
99+
>
100+
通项公式
101+
</button>
102+
</div>
103+
</div>
104+
105+
<div style={styles.section}>
106+
<h3 style={styles.heading}>动画控制</h3>
107+
<div style={styles.buttonGroup}>
108+
<button
109+
style={styles.controlButton}
110+
onClick={handlePreviousStep}
111+
disabled={state.currentStep === 0}
112+
>
113+
上一步
114+
</button>
115+
<button style={styles.controlButton} onClick={handlePlayPause}>
116+
{state.isPlaying ? '暂停' : '播放'}
117+
</button>
118+
<button
119+
style={styles.controlButton}
120+
onClick={handleNextStep}
121+
disabled={state.currentStep >= state.totalSteps - 1}
122+
>
123+
下一步
124+
</button>
125+
<button style={styles.controlButton} onClick={handleReset}>
126+
重置
127+
</button>
128+
</div>
129+
130+
<div style={styles.sliderContainer}>
131+
<span>步骤: {state.currentStep} / {state.totalSteps > 0 ? state.totalSteps -1 : 0}</span>
132+
<input
133+
type="range"
134+
min="0"
135+
max={state.totalSteps > 0 ? state.totalSteps - 1 : 0}
136+
value={state.currentStep}
137+
onChange={handleStepChange}
138+
style={styles.slider}
139+
disabled={state.totalSteps === 0}
140+
/>
141+
</div>
142+
143+
<div style={styles.speedControlContainer}>
144+
<label htmlFor="speedControl">播放速度: {state.playbackSpeed.toFixed(1)}x</label>
145+
<input
146+
id="speedControl"
147+
type="range"
148+
min="0.1"
149+
max="4"
150+
step="0.1"
151+
value={state.playbackSpeed}
152+
onChange={handleSpeedChange}
153+
style={styles.slider}
154+
/>
155+
</div>
156+
</div>
157+
158+
<div style={styles.description}>
159+
{state.timeline[state.currentStep]?.description || '准备开始动画演示'}
160+
</div>
161+
</div>
162+
);
163+
};
164+
165+
// 组件样式
166+
const styles = {
167+
container: {
168+
display: 'flex',
169+
flexDirection: 'column' as const,
170+
padding: '10px',
171+
backgroundColor: '#f5f5f5',
172+
borderRadius: '4px',
173+
marginBottom: '10px',
174+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
175+
},
176+
section: {
177+
marginBottom: '15px'
178+
},
179+
heading: {
180+
fontSize: '16px',
181+
marginBottom: '8px'
182+
},
183+
buttonGroup: {
184+
display: 'flex',
185+
gap: '8px',
186+
flexWrap: 'wrap' as const
187+
},
188+
algorithmButton: {
189+
padding: '6px 12px',
190+
border: 'none',
191+
borderRadius: '4px',
192+
cursor: 'pointer',
193+
transition: 'background-color 0.3s'
194+
},
195+
controlButton: {
196+
padding: '6px 12px',
197+
backgroundColor: '#555',
198+
color: 'white',
199+
border: 'none',
200+
borderRadius: '4px',
201+
cursor: 'pointer'
202+
},
203+
sliderContainer: {
204+
marginTop: '15px',
205+
display: 'flex',
206+
flexDirection: 'column' as const,
207+
gap: '5px'
208+
},
209+
speedControlContainer: {
210+
marginTop: '15px',
211+
display: 'flex',
212+
flexDirection: 'column' as const,
213+
gap: '5px'
214+
},
215+
slider: {
216+
width: '100%'
217+
},
218+
description: {
219+
marginTop: '15px',
220+
padding: '10px',
221+
backgroundColor: '#fff',
222+
borderRadius: '4px',
223+
border: '1px solid #ddd',
224+
minHeight: '20px',
225+
fontSize: '14px'
226+
}
227+
};
228+
229+
export default ControlPanel;

0 commit comments

Comments
 (0)