
本文详解如何在 react 中为维吉尼亚密码(vigenère cipher)加密过程中的每一对明文-密钥字母,动态高亮其在加法表中对应的行与列,实现「逐字符、分步着色」的可视化效果。
在维吉尼亚密码的 React 实现中,仅静态展示加密结果远远不够——真正的教学价值和用户体验提升,来自于可感知的算法执行过程。用户希望看到:当输入 password="cat" 和 text="rat" 时,界面依次高亮 (c,r) → (a,a) → (t,t) 在密码表中对应的行与列(即:第 r 行 + 第 c 列 → 第 a 行 + 第 a 列 → 第 t 行 + 第 t 列),形成清晰的「步骤式引导动画」。这要求状态管理必须精确到当前处理索引,而非仅记录最终位置。
✅ 核心思路:分离关注点 + 步进式状态驱动
我们不直接在 handleCipherButtonClick 中一次性计算全部结果并设置最终 row/col,而是将加密逻辑拆解为可暂停、可步进、可渲染的流程:
- 使用 useState 管理当前处理的字符索引 stepIndex
- 使用 useEffect 配合 setInterval 或 setTimeout 控制步进节奏
- 每次更新 stepIndex,自动触发表格重新渲染,高亮对应行列
以下为优化后的完整实现(已适配原 Georgian 字母表):
import React, { useState, useEffect, useCallback } from 'react';
function VigenereCipher() {
const georgianLetters = 'აბგდევზთიკლმნოპჟრსტუფქღყშჩცძწჭხჯჰ';
const table = Array.from({ length: 33 }, (_, i) =>
Array.from({ length: 33 }, (_, j) => georgianLetters[(i + j) % 33])
);
const [password, setPassword] = useState('');
const [text, setText] = useState('');
const [ciphered, setCiphered] = useState('');
const [stepIndex, setStepIndex] = useState(-1); // -1: 未开始;0 ~ n-1: 当前步;n: 完成
const [isRunning, setIsRunning] = useState(false);
// ✅ 提取加密逻辑为纯函数,便于复用与测试
const getStepData = useCallback(() => {
if (!password || !text || stepIndex < 0) return null;
const cleanText = text.split('').filter(c => c !== ' ');
const cleanPassword = password.split('').filter(c => c !== ' ');
if (stepIndex >= cleanText.length) return null;
const char = cleanText[stepIndex];
const keyChar = cleanPassword[stepIndex % cleanPassword.length];
const rowIndex = georgianLetters.indexOf(char);
const colIndex = georgianLetters.indexOf(keyChar);
return { char, keyChar, rowIndex, colIndex, isSpace: text[stepIndex] === ' ' };
}, [password, text, stepIndex]);
// ✅ 启动/重置动画
const startAnimation = () => {
if (!password || !text) {
alert('შეავსეთ პაროლის და ტექსტის ველები');
return;
}
setStepIndex(0);
setIsRunning(true);
setCiphered('');
};
// ✅ 停止动画(可选)
const stopAnimation = () => {
setIsRunning(false);
};
// ✅ 步进逻辑:每次只推进 1 步
useEffect(() => {
if (!isRunning || stepIndex < 0) return;
const timer = setTimeout(() => {
const next = stepIndex + 1;
const cleanText = text.split('').filter(c => c !== ' ');
if (next > cleanText.length) {
setIsRunning(false);
} else {
setStepIndex(next);
}
}, 1200); // 每步间隔 1.2 秒,可调
return () => clearTimeout(timer);
}, [isRunning, stepIndex, text]);
// ✅ 实时生成高亮后的表格
const renderTable = () => {
const stepData = getStepData();
const highlightRow = stepData?.rowIndex ?? -1;
const highlightCol = stepData?.colIndex ?? -1;
return (
<table className="vigenere-table">
<tbody>
{georgianLetters.split('').map((_, i) => (
<tr key={i}>
{georgianLetters.split('').map((_, j) => {
let bgColor = 'transparent';
if (i === highlightRow && j === highlightCol) {
bgColor = '#8b5cf6'; // 紫色:交汇点(核心匹配)
} else if (i === highlightRow) {
bgColor = '#fbbf24'; // 黄色:当前明文字母所在行
} else if (j === highlightCol) {
bgColor = '#3b82f6'; // 蓝色:当前密钥字母所在列
}
return (
<td key={j} style={{ backgroundColor: bgColor }}>
{table[i][j]}
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
};
// ✅ 动态生成加密结果(仅用于显示,非实时渲染)
useEffect(() => {
if (stepIndex < 0 || !password || !text) return;
const cleanText = text.split('').filter(c => c !== ' ');
const cleanPassword = password.split('').filter(c => c !== ' ');
if (stepIndex > cleanText.length) {
// 加密完成
let k = 0;
let result = '';
for (let i = 0; i < text.length; i++) {
if (text[i] === ' ') {
result += ' ';
} else {
const rIdx = georgianLetters.indexOf(text[i]);
const cIdx = georgianLetters.indexOf(password[k % password.length]);
if (rIdx !== -1 && cIdx !== -1) {
result += table[rIdx][cIdx];
k++;
}
}
}
setCiphered(result);
}
}, [stepIndex, password, text, table]);
return (
<div className="main">
<h2>ვიჟინერის ცხრილი — ნაბიჯ-ნაბიჯ გამოსახულება</h2>
{renderTable()}
<div className="right-side">
<div className="pass-text">
<label>პაროლი</label>
<input type="text" value={password} onChange={(e) => setPassword(e.target.value)} />
<label>ტექსტი</label>
<textarea value={text} onChange={(e) => setText(e.target.value)} rows="4" />
</div>
<div className="controls">
<button onClick={startAnimation} disabled={isRunning}>
{isRunning ? 'გაშვებული...' : 'დაწყება'}
</button>
<button onClick={stopAnimation} disabled={!isRunning}>
შეწყვეტა
</button>
</div>
<div className="ciphered">
<label>შედეგი</label>
<textarea readOnly value={ciphered} rows="4" />
</div>
<div className="step-info">
<p>
ნაბიჯი: <strong>{stepIndex < 0 ? '—' : stepIndex === 0 ? '0 (საწყისი)' : `${stepIndex}/${text.replace(/\s/g, '').length}`}</strong>
{getStepData() && (
<span> | მიმდინარე წყვილი: <code>{getStepData().char}</code> × <code>{getStepData().keyChar}</code></span>
)}
</p>
</div>
</div>
</div>
);
}
export default VigenereCipher;⚠️ 关键注意事项与最佳实践
- 避免阻塞主线程:切勿在 useEffect 中使用 while 循环或同步密集计算。务必用 setTimeout/setInterval 分帧执行,保障 UI 流畅。
- 状态原子性:stepIndex 是唯一驱动高亮的单一数据源,所有视觉状态(行/列索引、颜色、文本提示)均由此派生,杜绝多状态不同步风险。
- 空格与边界处理:明文中的空格不参与加密,但需保留在输出中;stepIndex 应跳过空格计数,确保「第 N 个有效字符」逻辑准确。
- 可访问性增强:为高亮单元格添加 aria-label(如 aria-label="რიგი ა, სვეტი კ — შედეგი: ლ"),提升屏幕阅读器支持。
- 性能优化(大型表):若字母表扩展至 100+ 字符,建议改用 CSS Grid + grid-area 定位 + className 切换,避免内联样式大量重绘。
✅ 总结
通过将「算法执行」与「UI 渲染」解耦,并以 stepIndex 作为唯一真值来源,我们实现了真正符合教学逻辑的维吉尼亚密码可视化:每一步都明确、可控、可逆、可调试。这种模式不仅适用于密码学演示,还可迁移至 DFS/BFS 路径高亮、排序算法动画、状态机流转等任何需要「过程可见性」的交互场景。记住:好的动画不是炫技,而是让抽象逻辑变得可触摸、可理解。









