📋 项目背景
作为Java开发者,我们经常需要理解JVM的执行过程,但传统的学习方式存在以下问题:
- 抽象概念难理解 - 堆、栈、方法区等概念很抽象
- 执行过程不直观 - 无法直观看到字节码如何一步步执行
- 内存变化看不到 - 对象创建、栈帧变化等过程是黑盒
- 理论与实践脱节 - 书本知识与实际运行过程难以对应
🎯 解决的痛点
痛点1:JVM执行过程黑盒化
问题 : 学生只能通过文字和静态图表学习JVM原理 解决: 提供25步详细的JVM执行演示,从类加载到程序结束
痛点2:内存状态变化不可见
问题 : 无法直观看到堆内存、JVM栈的实时变化 解决: 实时显示内存区域状态,每一步都有对应的内存快照
痛点3:字节码与源码难对应
问题 : 不知道Java代码对应什么字节码指令 解决: 同时展示源码和字节码,当前执行指令高亮显示
痛点4:学习体验枯燥
问题 : 传统学习方式单调,难以保持学习兴趣 解决: 交互式操作,用户可以控制执行节奏,支持前进后退
🛠️ 解决方案
核心功能
- 分步式执行 - 将JVM执行分解为25个清晰步骤
- 多维度展示 - 源码、字节码、内存状态三位一体
- 实时交互 - 支持步骤跳转、键盘控制
- 可视化效果 - 现代化UI设计,直观易懂
技术实现
- 前端技术: 纯HTML/CSS/JavaScript,无框架依赖
- 布局设计: CSS Grid三层布局,响应式设计
- 交互方式: 点击、键盘、胶囊式步骤导航
- 视觉效果: 橘白配色,毛玻璃效果,平滑动画
示例程序
选择了包含对象创建、方法调用、算术运算的典型Java程序:
java
public class JvmTest {
public int add(){
int a=1;
int b=2;
int c=(a+b)*10;
return c;
}
public static void main(String[] args) {
JvmTest jvmTest =new JvmTest();
jvmTest.add();
}
}
✨ 最终效果

源码信息
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JVM运行过程可视化 - JvmTest示例</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Monaco', 'Consolas', monospace;
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
color: #333;
min-height: 100vh;
}
.container {
display: grid;
grid-template-rows: auto auto 1fr;
grid-template-columns: 1fr;
height: 100vh;
gap: 20px;
padding: 20px;
}
.bottom-row {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
}
.top-bar {
background: rgba(255,255,255,0.9);
border: 1px solid rgba(255,140,0,0.2);
border-radius: 10px;
padding: 15px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(255,140,0,0.1);
}
.top-steps {
background: rgba(255,255,255,0.9);
border: 1px solid rgba(255,140,0,0.2);
border-radius: 10px;
padding: 15px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(255,140,0,0.1);
}
.steps-horizontal {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 15px;
max-height: 200px;
overflow-y: auto;
}
.step-horizontal {
padding: 8px 12px;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 12px;
white-space: nowrap;
min-width: 120px;
text-align: center;
}
.step-horizontal.pending {
background: rgba(200,200,200,0.4);
color: #666;
}
.step-horizontal.current {
background: linear-gradient(45deg, #ff8c00, #ff7043);
color: #fff;
font-weight: bold;
box-shadow: 0 4px 15px rgba(255,140,0,0.3);
}
.step-horizontal.completed {
background: linear-gradient(45deg, #4caf50, #66bb6a);
color: #fff;
}
.code-toggle {
background: none;
border: none;
color: #e65100;
font-size: 18px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
transition: all 0.3s ease;
}
.code-toggle:hover {
color: #ff8c00;
}
.code-toggle-icon {
transition: transform 0.3s ease;
}
.code-toggle.collapsed .code-toggle-icon {
transform: rotate(-90deg);
}
.code-content {
overflow: hidden;
transition: max-height 0.3s ease;
}
.code-content.collapsed {
max-height: 0;
}
.left-panel {
background: rgba(255,255,255,0.9);
border: 1px solid rgba(255,140,0,0.2);
border-radius: 10px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(255,140,0,0.1);
}
.right-panel {
background: rgba(255,255,255,0.9);
border: 1px solid rgba(255,140,0,0.2);
border-radius: 10px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(255,140,0,0.1);
}
.steps-compact {
max-height: 300px;
overflow-y: auto;
}
.steps-compact .step {
padding: 6px;
margin: 3px 0;
font-size: 13px;
}
.steps-compact .step > div:first-child {
font-size: 12px;
}
.steps-compact .step > div:last-child {
font-size: 10px;
}
.memory-panel {
background: rgba(255,255,255,0.95);
border: 2px solid rgba(255,140,0,0.3);
border-radius: 15px;
padding: 25px;
backdrop-filter: blur(10px);
box-shadow: 0 12px 40px rgba(255,140,0,0.15);
}
.memory-grid {
display: grid;
grid-template-rows: auto auto auto;
gap: 15px;
}
.memory-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.memory-row.single {
grid-template-columns: 1fr;
}
.code-section {
background: rgba(255,255,255,0.8);
border: 1px solid rgba(255,140,0,0.2);
border-radius: 10px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(255,140,0,0.08);
flex: 1;
}
.bytecode-section {
background: rgba(255,255,255,0.8);
border: 1px solid rgba(255,140,0,0.2);
border-radius: 10px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(255,140,0,0.08);
flex: 1;
}
.step-list {
list-style: none;
}
.step {
padding: 10px;
margin: 5px 0;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
border-left: 4px solid transparent;
}
.step.pending {
background: rgba(200,200,200,0.4);
color: #666;
}
.step.current {
background: linear-gradient(45deg, #ff8c00, #ff7043);
border-left-color: #ff6f00;
color: #fff;
box-shadow: 0 4px 15px rgba(255,140,0,0.3);
}
.step.completed {
background: linear-gradient(45deg, #4caf50, #66bb6a);
border-left-color: #388e3c;
color: #fff;
}
.controls {
text-align: center;
margin: 20px 0;
}
.btn {
background: linear-gradient(45deg, #ff8c00, #ff7043);
color: white;
border: none;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
margin: 0 10px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(255,140,0,0.2);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(255,140,0,0.4);
background: linear-gradient(45deg, #ff7043, #ff5722);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.code pre {
color: #e65100;
font-size: 13px;
line-height: 1.4;
background: rgba(255,245,238,0.6);
padding: 10px;
border-radius: 6px;
border-left: 3px solid #ff8c00;
margin: 0;
}
.bytecode pre {
color: #f57c00;
font-size: 12px;
line-height: 1.5;
background: rgba(255,243,224,0.6);
padding: 12px;
border-radius: 6px;
border-left: 3px solid #ffb74d;
}
.memory-area {
background: rgba(255,248,240,0.8);
border: 1px solid rgba(255,140,0,0.3);
border-radius: 8px;
margin: 15px 0;
padding: 18px;
box-shadow: 0 2px 10px rgba(255,140,0,0.1);
}
.memory-title {
color: #e65100;
font-weight: bold;
font-size: 16px;
margin-bottom: 12px;
border-bottom: 2px solid rgba(255,140,0,0.4);
padding-bottom: 6px;
}
.stack-frame {
background: rgba(76, 175, 80, 0.1);
border: 2px solid #4caf50;
border-radius: 8px;
margin: 10px 0;
padding: 12px;
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.2);
}
.local-vars, .operand-stack {
margin: 8px 0;
}
.highlight {
background: linear-gradient(45deg, #ffd54f, #ffcc02);
color: #e65100;
padding: 3px 6px;
border-radius: 4px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(255,193,7,0.3);
}
.heap-object {
background: rgba(156, 39, 176, 0.1);
border: 2px solid #9c27b0;
border-radius: 8px;
padding: 12px;
margin: 8px 0;
box-shadow: 0 2px 8px rgba(156, 39, 176, 0.2);
}
.step-info {
background: rgba(255,248,240,0.9);
border: 1px solid rgba(255,140,0,0.3);
border-radius: 10px;
padding: 18px;
margin-top: 20px;
box-shadow: 0 4px 15px rgba(255,140,0,0.1);
}
.step-info h4 {
color: #e65100;
margin-bottom: 10px;
}
.step-info p {
color: #424242;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="container">
<!-- 第一行:Java源代码区域 -->
<div class="top-bar">
<button class="code-toggle" onclick="toggleCode()">
<span class="code-toggle-icon">▼</span>
📝 Java源代码 (点击收起/展开)
</button>
<div class="code-content" id="codeContent">
<div class="code">
<pre id="javaCode">public class JvmTest {
public int add(){
int a=1;
int b=2;
int c=(a+b)*10;
return c;
}
public static void main(String[] args) {
JvmTest jvmTest =new JvmTest();
jvmTest.add();
}
}</pre>
</div>
</div>
</div>
<!-- 第二行:执行步骤区域 -->
<div class="top-steps">
<h3>🚀 JVM执行步骤</h3>
<div class="steps-horizontal" id="stepList">
<!-- 步骤将通过JavaScript动态生成 -->
</div>
<div class="controls">
<button class="btn" onclick="prevStep()" id="prevBtn">上一步</button>
<button class="btn" onclick="nextStep()" id="nextBtn">下一步</button>
</div>
</div>
<!-- 第三行:底部区域 -->
<div class="bottom-row">
<!-- 左侧 - 字节码区域 (1/3) -->
<div class="left-panel">
<h3>⚙️ 字节码指令</h3>
<div class="bytecode">
<div id="mainBytecode">
<h4>main方法字节码:</h4>
<pre id="mainBytecodeContent">0: new #2 // class JvmTest
3: dup // 复制栈顶引用
4: invokespecial #3 // 调用<init>构造器
7: astore_1 // 存储到局部变量1
8: aload_1 // 加载局部变量1
9: invokevirtual #4 // 调用add()方法
12: pop // 弹出返回值
13: return // 方法返回</pre>
</div>
<div id="addBytecode" style="margin-top: 20px;">
<h4>add方法字节码:</h4>
<pre id="addBytecodeContent">0: iconst_1 // 将常量1推入栈
1: istore_1 // 存储到局部变量1 (a=1)
2: iconst_2 // 将常量2推入栈
3: istore_2 // 存储到局部变量2 (b=2)
4: iload_1 // 加载变量a
5: iload_2 // 加载变量b
6: iadd // 执行加法 a+b
7: bipush 10 // 将常量10推入栈
9: imul // 执行乘法 (a+b)*10
10: istore_3 // 存储到局部变量3 (c)
11: iload_3 // 加载变量c
12: ireturn // 返回c的值</pre>
</div>
</div>
</div>
<!-- 右侧 - JVM内存状态区域 (2/3) -->
<div class="right-panel">
<h3>🧠 JVM内存状态</h3>
<div class="memory-grid">
<!-- 第一行:当前步骤说明 -->
<div class="memory-row single">
<div class="step-info" id="stepInfo">
<h4>当前步骤说明</h4>
<p id="stepDescription">点击"下一步"开始JVM执行演示</p>
</div>
</div>
<!-- 第二行:堆内存和JVM栈 -->
<div class="memory-row">
<div class="memory-area">
<div class="memory-title">堆内存 (Heap)</div>
<div id="heapMemory">
<div>暂无对象</div>
</div>
</div>
<div class="memory-area">
<div class="memory-title">JVM栈 (Stack)</div>
<div id="jvmStack">
<div>暂无栈帧</div>
</div>
</div>
</div>
<!-- 第三行:方法区和程序计数器 -->
<div class="memory-row">
<div class="memory-area">
<div class="memory-title">方法区 (Method Area)</div>
<div id="methodArea">
<div>JvmTest.class (待加载)</div>
</div>
</div>
<div class="memory-area">
<div class="memory-title">程序计数器 (PC Register)</div>
<div id="pcRegister">
<div>PC: 0</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 切换Java源代码显示/隐藏
function toggleCode() {
const codeContent = document.getElementById('codeContent');
const toggleBtn = document.querySelector('.code-toggle');
if (codeContent.classList.contains('collapsed')) {
codeContent.classList.remove('collapsed');
codeContent.style.maxHeight = codeContent.scrollHeight + 'px';
toggleBtn.classList.remove('collapsed');
} else {
codeContent.style.maxHeight = '0';
codeContent.classList.add('collapsed');
toggleBtn.classList.add('collapsed');
}
}
// 初始化代码区域高度
function initCodeArea() {
const codeContent = document.getElementById('codeContent');
codeContent.style.maxHeight = codeContent.scrollHeight + 'px';
}
// JVM执行步骤数据
const steps = [
{ id: 1, title: "JVM初始化", phase: "启动", description: "创建堆、栈、方法区等内存区域,初始化JVM运行环境" },
{ id: 2, title: "类加载器启动", phase: "启动", description: "初始化Bootstrap、Extension、Application类加载器" },
{ id: 3, title: "加载核心类", phase: "启动", description: "加载Object、String等Java核心类到方法区" },
{ id: 4, title: "加载JvmTest.class", phase: "类加载", description: "将JvmTest字节码文件读入方法区" },
{ id: 5, title: "验证字节码", phase: "类加载", description: "检查字节码格式和安全性,确保类文件正确" },
{ id: 6, title: "准备阶段", phase: "类加载", description: "为静态变量分配内存并设置默认值" },
{ id: 7, title: "解析阶段", phase: "类加载", description: "将符号引用转换为直接引用" },
{ id: 8, title: "初始化JvmTest类", phase: "类加载", description: "执行静态初始化块和静态变量赋值" },
{ id: 9, title: "创建main方法栈帧", phase: "main执行", description: "为main方法创建栈帧,设置局部变量表和操作数栈" },
{ id: 10, title: "执行new指令", phase: "main执行", description: "在堆中分配JvmTest对象内存空间" },
{ id: 11, title: "执行dup指令", phase: "main执行", description: "复制栈顶的对象引用" },
{ id: 12, title: "调用构造方法", phase: "main执行", description: "invokespecial调用<init>构造器初始化对象" },
{ id: 13, title: "存储对象引用", phase: "main执行", description: "astore_1将对象引用存储到局部变量表[1]" },
{ id: 14, title: "加载对象引用", phase: "main执行", description: "aload_1从局部变量表加载对象引用到栈顶" },
{ id: 15, title: "调用add方法", phase: "main执行", description: "invokevirtual调用JvmTest实例的add方法" },
{ id: 16, title: "弹出返回值", phase: "main执行", description: "pop指令弹出add方法的返回值" },
{ id: 17, title: "创建add方法栈帧", phase: "add执行", description: "为add方法创建新的栈帧并压入JVM栈顶" },
{ id: 18, title: "iconst_1", phase: "add执行", description: "将整型常量1推入操作数栈顶" },
{ id: 19, title: "istore_1", phase: "add执行", description: "将栈顶的1存储到局部变量表[1] (int a = 1)" },
{ id: 20, title: "iconst_2", phase: "add执行", description: "将整型常量2推入操作数栈顶" },
{ id: 21, title: "istore_2", phase: "add执行", description: "将栈顶的2存储到局部变量表[2] (int b = 2)" },
{ id: 22, title: "执行加法运算", phase: "add执行", description: "iload_1, iload_2加载a,b到栈,iadd执行a+b=3" },
{ id: 23, title: "执行乘法运算", phase: "add执行", description: "bipush 10推入常量10,imul执行(a+b)*10=30" },
{ id: 24, title: "存储结果", phase: "add执行", description: "istore_3将计算结果30存储到局部变量表[3] (int c = 30)" },
{ id: 25, title: "方法返回", phase: "add执行", description: "iload_3加载c,ireturn返回值30并销毁栈帧" }
];
let currentStep = 0;
// 初始化页面
function initPage() {
initCodeArea();
renderStepList();
updateMemoryState();
updateButtons();
}
// 渲染步骤列表(水平排列)
function renderStepList() {
const stepList = document.getElementById('stepList');
stepList.innerHTML = '';
steps.forEach((step, index) => {
const div = document.createElement('div');
div.className = 'step-horizontal ' + getStepStatus(index);
div.textContent = `${index + 1}. ${step.title}`;
div.onclick = () => goToStep(index);
stepList.appendChild(div);
});
}
// 获取步骤状态
function getStepStatus(index) {
if (index < currentStep) return 'completed';
if (index === currentStep) return 'current';
return 'pending';
}
// 下一步
function nextStep() {
if (currentStep < steps.length - 1) {
currentStep++;
updateDisplay();
}
}
// 上一步
function prevStep() {
if (currentStep > 0) {
currentStep--;
updateDisplay();
}
}
// 跳转到指定步骤
function goToStep(step) {
if (step >= 0 && step < steps.length) {
currentStep = step;
updateDisplay();
}
}
// 更新显示
function updateDisplay() {
renderStepList();
updateStepDescription();
updateMemoryState();
updateButtons();
highlightBytecode();
}
// 更新步骤描述
function updateStepDescription() {
const stepInfo = steps[currentStep];
document.getElementById('stepDescription').textContent = stepInfo.description;
}
// 更新按钮状态
function updateButtons() {
document.getElementById('prevBtn').disabled = currentStep === 0;
document.getElementById('nextBtn').disabled = currentStep === steps.length - 1;
}
// 高亮字节码
function highlightBytecode() {
// 移除之前的高亮
document.querySelectorAll('.highlight').forEach(el => {
el.classList.remove('highlight');
});
// 根据当前步骤高亮相应的字节码行
const step = steps[currentStep];
if (step.id >= 9 && step.id <= 16) {
// main方法执行阶段
highlightBytecodeLine('mainBytecodeContent', getBytecodeLineForMainStep(step.id));
} else if (step.id >= 17 && step.id <= 25) {
// add方法执行阶段
highlightBytecodeLineForAddStep(step.id);
}
}
// 高亮字节码行
function highlightBytecodeLine(elementId, lineIndex) {
const element = document.getElementById(elementId);
if (element && lineIndex >= 0) {
const lines = element.innerHTML.split('\n');
if (lines[lineIndex]) {
lines[lineIndex] = '<span class="highlight">' + lines[lineIndex] + '</span>';
element.innerHTML = lines.join('\n');
}
}
}
// 获取main方法字节码行号
function getBytecodeLineForMainStep(stepId) {
const mapping = {
10: 0, // new
11: 1, // dup
12: 2, // invokespecial
13: 3, // astore_1
14: 4, // aload_1
15: 5, // invokevirtual
16: 6 // pop
};
return mapping[stepId] || -1;
}
// 高亮add方法字节码
function highlightBytecodeLineForAddStep(stepId) {
const mapping = {
18: 0, // iconst_1
19: 1, // istore_1
20: 2, // iconst_2
21: 3, // istore_2
22: [4, 5, 6], // iload_1, iload_2, iadd
23: [7, 8], // bipush, imul
24: 9, // istore_3
25: [10, 11] // iload_3, ireturn
};
const lines = mapping[stepId];
if (Array.isArray(lines)) {
lines.forEach(line => highlightBytecodeLineInAdd(line));
} else if (lines !== undefined) {
highlightBytecodeLineInAdd(lines);
}
}
function highlightBytecodeLineInAdd(lineIndex) {
const element = document.getElementById('addBytecodeContent');
if (element && lineIndex >= 0) {
const lines = element.innerHTML.split('\n');
if (lines[lineIndex]) {
lines[lineIndex] = '<span class="highlight">' + lines[lineIndex] + '</span>';
element.innerHTML = lines.join('\n');
}
}
}
// 更新内存状态
function updateMemoryState() {
updateHeapMemory();
updateJvmStack();
updateMethodArea();
updatePCRegister();
}
// 更新堆内存
function updateHeapMemory() {
const heap = document.getElementById('heapMemory');
const step = steps[currentStep];
if (step.id >= 10 && step.id <= 25) {
heap.innerHTML = `
<div class="heap-object">
<div><strong>JvmTest@0x1001</strong></div>
<div>Class: JvmTest.class</div>
<div>实例变量: 无</div>
</div>
`;
} else {
heap.innerHTML = '<div>暂无对象</div>';
}
}
// 更新JVM栈
function updateJvmStack() {
const stack = document.getElementById('jvmStack');
const step = steps[currentStep];
if (step.id >= 9 && step.id < 17) {
// main方法栈帧
stack.innerHTML = getMainStackFrame(step.id);
} else if (step.id >= 17 && step.id <= 25) {
// add方法栈帧 + main方法栈帧
stack.innerHTML = getAddStackFrame(step.id) + getMainStackFrame(15);
} else {
stack.innerHTML = '<div>暂无栈帧</div>';
}
}
// 获取main方法栈帧HTML
function getMainStackFrame(stepId) {
let operandStack = '[ ]';
let localVars = `
[0]: args[]<br>
[1]: ${stepId >= 13 ? 'JvmTest@0x1001' : 'null'}
`;
if (stepId === 10 || stepId === 11) {
operandStack = '[JvmTest@0x1001]';
} else if (stepId === 14) {
operandStack = '[JvmTest@0x1001]';
}
return `
<div class="stack-frame">
<div><strong>main方法栈帧</strong></div>
<div class="operand-stack">操作数栈: ${operandStack}</div>
<div class="local-vars">局部变量表:<br>${localVars}</div>
</div>
`;
}
// 获取add方法栈帧HTML
function getAddStackFrame(stepId) {
let operandStack = '[ ]';
let localVars = `
[0]: JvmTest@0x1001<br>
[1]: ${stepId >= 19 ? '1' : '未初始化'}<br>
[2]: ${stepId >= 21 ? '2' : '未初始化'}<br>
[3]: ${stepId >= 24 ? '30' : '未初始化'}
`;
// 根据步骤设置操作数栈内容
switch(stepId) {
case 18: operandStack = '[1]'; break;
case 20: operandStack = '[2]'; break;
case 22: operandStack = '[3]'; break;
case 23: operandStack = '[30]'; break;
case 25: operandStack = '[30]'; break;
}
return `
<div class="stack-frame" style="border-color: #e74c3c;">
<div><strong>add方法栈帧</strong></div>
<div class="operand-stack">操作数栈: ${operandStack}</div>
<div class="local-vars">局部变量表:<br>${localVars}</div>
</div>
`;
}
// 更新方法区
function updateMethodArea() {
const methodArea = document.getElementById('methodArea');
const step = steps[currentStep];
if (step.id >= 4) {
methodArea.innerHTML = `
<div>✅ JvmTest.class (已加载)</div>
<div style="margin-top: 5px; font-size: 12px; color: #bdc3c7;">
- add()方法字节码<br>
- main()方法字节码<br>
- 常量池
</div>
`;
} else {
methodArea.innerHTML = '<div>JvmTest.class (待加载)</div>';
}
}
// 更新程序计数器
function updatePCRegister() {
const pcRegister = document.getElementById('pcRegister');
const step = steps[currentStep];
let pc = 'N/A';
if (step.id >= 9 && step.id < 17) {
const pcMapping = { 10: 0, 11: 3, 12: 4, 13: 7, 14: 8, 15: 9, 16: 12 };
pc = pcMapping[step.id] || 0;
} else if (step.id >= 17 && step.id <= 25) {
const pcMapping = {
18: 0, 19: 1, 20: 2, 21: 3, 22: 4, 23: 7, 24: 10, 25: 11
};
pc = pcMapping[step.id] || 0;
}
pcRegister.innerHTML = `<div>PC: ${pc}</div>`;
}
// 键盘事件
document.addEventListener('keydown', function(e) {
if (e.key === 'ArrowRight' || e.key === ' ') {
e.preventDefault();
nextStep();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
prevStep();
}
});
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', initPage);
</script>
</body>
</html>