2026-02-23 01:40:56 +08:00
|
|
|
|
<template>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<div class="cpu-demo">
|
|
|
|
|
|
<div class="demo-title">CPU 指令执行周期详细演示</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="main-layout">
|
|
|
|
|
|
<!-- LEFT: CPU internals -->
|
|
|
|
|
|
<div class="cpu-box">
|
|
|
|
|
|
<div class="cpu-label">CPU</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Control Unit -->
|
|
|
|
|
|
<div class="unit cu-unit" :class="{ active: isActive('CU') }">
|
|
|
|
|
|
<div class="unit-title">控制单元 CU</div>
|
|
|
|
|
|
<div class="regs-row">
|
|
|
|
|
|
<div class="reg-cell" :class="{ highlight: isHighlight('PC') }">
|
|
|
|
|
|
<span class="reg-name">PC</span>
|
|
|
|
|
|
<span class="reg-val">{{ fmt(regs.PC) }}</span>
|
|
|
|
|
|
<span class="reg-hint">程序计数器</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="reg-cell" :class="{ highlight: isHighlight('IR') }">
|
|
|
|
|
|
<span class="reg-name">IR</span>
|
|
|
|
|
|
<span class="reg-val ir-val">{{ regs.IR || '—' }}</span>
|
|
|
|
|
|
<span class="reg-hint">指令寄存器</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<!-- MAR / MDR -->
|
|
|
|
|
|
<div class="unit bus-unit">
|
|
|
|
|
|
<div class="regs-row">
|
|
|
|
|
|
<div class="reg-cell" :class="{ highlight: isHighlight('MAR') }">
|
|
|
|
|
|
<span class="reg-name">MAR</span>
|
|
|
|
|
|
<span class="reg-val">{{ fmt(regs.MAR) }}</span>
|
|
|
|
|
|
<span class="reg-hint">内存地址寄存器</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="reg-cell" :class="{ highlight: isHighlight('MDR') }">
|
|
|
|
|
|
<span class="reg-name">MDR</span>
|
|
|
|
|
|
<span class="reg-val">{{ regs.MDR !== null ? regs.MDR : '—' }}</span>
|
|
|
|
|
|
<span class="reg-hint">内存数据寄存器</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<!-- ALU -->
|
|
|
|
|
|
<div class="unit alu-unit" :class="{ active: isActive('ALU') }">
|
|
|
|
|
|
<div class="unit-title">算术逻辑单元 ALU</div>
|
|
|
|
|
|
<div class="regs-row">
|
|
|
|
|
|
<div class="reg-cell" :class="{ highlight: isHighlight('ACC') }">
|
|
|
|
|
|
<span class="reg-name">ACC</span>
|
|
|
|
|
|
<span class="reg-val">{{ fmt(regs.ACC) }}</span>
|
|
|
|
|
|
<span class="reg-hint">累加器</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="alu-op" :class="{ running: isActive('ALU') }">
|
|
|
|
|
|
{{ aluOp }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- General Registers -->
|
|
|
|
|
|
<div class="unit reg-unit">
|
|
|
|
|
|
<div class="unit-title">通用寄存器组</div>
|
|
|
|
|
|
<div class="regs-row">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="r in ['R0','R1','R2','R3']"
|
|
|
|
|
|
:key="r"
|
|
|
|
|
|
class="reg-cell"
|
|
|
|
|
|
:class="{ highlight: isHighlight(r) }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="reg-name">{{ r }}</span>
|
|
|
|
|
|
<span class="reg-val">{{ fmt(regs[r]) }}</span>
|
|
|
|
|
|
</div>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</div>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<!-- CENTER: Buses -->
|
|
|
|
|
|
<div class="bus-col">
|
|
|
|
|
|
<div class="bus addr-bus" :class="{ active: busActive === 'addr' }">
|
|
|
|
|
|
<span class="bus-label">地址总线</span>
|
|
|
|
|
|
<span class="bus-val" v-if="busActive === 'addr'">{{ fmt(regs.MAR) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="bus data-bus" :class="{ active: busActive === 'data' }">
|
|
|
|
|
|
<span class="bus-label">数据总线</span>
|
|
|
|
|
|
<span class="bus-val" v-if="busActive === 'data'">{{ regs.MDR !== null ? regs.MDR : '' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="bus ctrl-bus" :class="{ active: busActive === 'ctrl' }">
|
|
|
|
|
|
<span class="bus-label">控制总线</span>
|
|
|
|
|
|
<span class="bus-val" v-if="busActive === 'ctrl'">{{ ctrlSignal }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- arrows -->
|
|
|
|
|
|
<div class="arrow-row">
|
|
|
|
|
|
<div class="arrow" :class="{ lit: busActive === 'addr' }">→</div>
|
|
|
|
|
|
<div class="arrow" :class="{ lit: busActive === 'data' }">↔</div>
|
|
|
|
|
|
<div class="arrow" :class="{ lit: busActive === 'ctrl' }">→</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- RIGHT: Memory -->
|
|
|
|
|
|
<div class="mem-box">
|
|
|
|
|
|
<div class="mem-label">主存 Memory</div>
|
|
|
|
|
|
<div class="mem-rows">
|
2026-02-23 01:40:56 +08:00
|
|
|
|
<div
|
2026-02-24 18:22:58 +08:00
|
|
|
|
v-for="(inst, i) in program"
|
|
|
|
|
|
:key="i"
|
|
|
|
|
|
class="mem-row"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
'pc-row': regs.PC === BASE_ADDR + i,
|
|
|
|
|
|
'mar-row': regs.MAR === BASE_ADDR + i && busActive === 'addr',
|
|
|
|
|
|
'fetched': fetchedAddr === BASE_ADDR + i
|
|
|
|
|
|
}"
|
2026-02-23 01:40:56 +08:00
|
|
|
|
>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<span class="pc-arrow">{{ regs.PC === BASE_ADDR + i ? '▶' : '\u00a0' }}</span>
|
|
|
|
|
|
<span class="mem-addr">{{ hex(BASE_ADDR + i) }}</span>
|
|
|
|
|
|
<span class="mem-inst">{{ inst.asm }}</span>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</div>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mem-data-area">
|
|
|
|
|
|
<div class="mem-label-sm">数据区</div>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
<div
|
2026-02-24 18:22:58 +08:00
|
|
|
|
v-for="(val, addr) in dataMemory"
|
|
|
|
|
|
:key="addr"
|
|
|
|
|
|
class="mem-row data-row"
|
|
|
|
|
|
:class="{ 'mar-row': regs.MAR === addr && busActive === 'addr' }"
|
2026-02-23 01:40:56 +08:00
|
|
|
|
>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<span class="pc-arrow"> </span>
|
|
|
|
|
|
<span class="mem-addr">{{ hex(addr) }}</span>
|
|
|
|
|
|
<span class="mem-inst">{{ val }}</span>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
</div>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<!-- Pipeline bar -->
|
|
|
|
|
|
<div class="pipeline-bar">
|
2026-02-23 01:40:56 +08:00
|
|
|
|
<div
|
2026-02-24 18:22:58 +08:00
|
|
|
|
v-for="(ph, i) in phases"
|
|
|
|
|
|
:key="i"
|
|
|
|
|
|
class="ph-cell"
|
|
|
|
|
|
:class="{ 'ph-active': currentPhase === i, 'ph-done': currentPhase > i }"
|
2026-02-23 01:40:56 +08:00
|
|
|
|
>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<span class="ph-en">{{ ph.en }}</span>
|
|
|
|
|
|
<span class="ph-zh">{{ ph.zh }}</span>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<!-- Step detail -->
|
|
|
|
|
|
<div class="step-detail">
|
|
|
|
|
|
<div class="step-badge">步骤 {{ stepIndex }} / {{ totalSteps }}</div>
|
|
|
|
|
|
<div class="step-msg">{{ currentStep.msg }}</div>
|
|
|
|
|
|
<div class="step-signal" v-if="currentStep.signal">
|
|
|
|
|
|
信号:<code>{{ currentStep.signal }}</code>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<!-- Controls -->
|
2026-02-23 01:40:56 +08:00
|
|
|
|
<div class="controls">
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<button class="btn-clock" @click="advance" :disabled="done">
|
|
|
|
|
|
⟳ 时钟脉冲 (下一步)
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn-auto" @click="toggleAuto" :disabled="done">
|
|
|
|
|
|
{{ autoRunning ? '⏸ 暂停' : '▶ 自动运行' }}
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</button>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<button class="btn-reset" @click="reset">↺ 重置</button>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
<div class="done-msg" v-if="done">
|
|
|
|
|
|
✅ 程序执行完毕!共执行 {{ program.length }} 条指令,{{ stepIndex }} 个时钟步骤。
|
|
|
|
|
|
<button class="btn-reset inline" @click="reset">重新开始</button>
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
import { ref, computed, onUnmounted } from 'vue'
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
const BASE_ADDR = 0x100
|
|
|
|
|
|
const DATA_BASE = 0x200
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
// Program: 4 instructions
|
|
|
|
|
|
const program = [
|
|
|
|
|
|
{ asm: 'LOAD R0, [0x200]', op: 'LOAD', dst: 'R0', src: DATA_BASE },
|
|
|
|
|
|
{ asm: 'LOAD R1, #7', op: 'LOADI', dst: 'R1', imm: 7 },
|
|
|
|
|
|
{ asm: 'ADD R0, R1', op: 'ADD', dst: 'R0', src: 'R1' },
|
|
|
|
|
|
{ asm: 'STORE [0x201], R0', op: 'STORE', addr: DATA_BASE + 1, src: 'R0' },
|
|
|
|
|
|
]
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
const phases = [
|
|
|
|
|
|
{ en: 'Fetch', zh: '取指' },
|
|
|
|
|
|
{ en: 'Decode', zh: '译码' },
|
|
|
|
|
|
{ en: 'Execute', zh: '执行' },
|
|
|
|
|
|
{ en: 'Write Back', zh: '写回' },
|
2026-02-23 01:40:56 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
function hex(n) { return n != null ? '0x' + n.toString(16).toUpperCase().padStart(3, '0') : '—' }
|
|
|
|
|
|
function fmt(v) { return v != null ? v : '—' }
|
|
|
|
|
|
|
|
|
|
|
|
// Build step sequence for all instructions
|
|
|
|
|
|
function buildSteps() {
|
|
|
|
|
|
const steps = []
|
|
|
|
|
|
for (let i = 0; i < program.length; i++) {
|
|
|
|
|
|
const inst = program[i]
|
|
|
|
|
|
const pc = BASE_ADDR + i
|
|
|
|
|
|
|
|
|
|
|
|
// ── FETCH (3 sub-steps) ──────────────────────────────────────────
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 0,
|
|
|
|
|
|
highlights: ['PC'],
|
|
|
|
|
|
bus: 'ctrl',
|
|
|
|
|
|
ctrlSignal: 'READ',
|
|
|
|
|
|
aluOp: '—',
|
|
|
|
|
|
regUpdates: { MAR: pc },
|
|
|
|
|
|
msg: `[取指 1/3] PC=${hex(pc)},控制单元发出读信号,将 PC 值送入 MAR(内存地址寄存器)`,
|
|
|
|
|
|
signal: `MAR ← PC (${hex(pc)})`,
|
|
|
|
|
|
})
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 0,
|
|
|
|
|
|
highlights: ['MAR'],
|
|
|
|
|
|
bus: 'addr',
|
|
|
|
|
|
ctrlSignal: 'READ',
|
|
|
|
|
|
aluOp: '—',
|
|
|
|
|
|
regUpdates: {},
|
|
|
|
|
|
msg: `[取指 2/3] MAR=${hex(pc)} 通过地址总线送到主存,主存定位该地址`,
|
|
|
|
|
|
signal: `地址总线: ${hex(pc)}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 0,
|
|
|
|
|
|
highlights: ['MDR', 'IR'],
|
|
|
|
|
|
bus: 'data',
|
|
|
|
|
|
ctrlSignal: 'READ',
|
|
|
|
|
|
aluOp: '—',
|
|
|
|
|
|
regUpdates: { MDR: inst.asm, IR: inst.asm, PC: pc + 1 },
|
|
|
|
|
|
fetchedAddr: pc,
|
|
|
|
|
|
msg: `[取指 3/3] 主存将指令 "${inst.asm}" 经数据总线送入 MDR,再转存到 IR;PC 自增 → ${hex(pc + 1)}`,
|
|
|
|
|
|
signal: `MDR ← MEM[${hex(pc)}];IR ← MDR;PC++`,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ── DECODE (2 sub-steps) ─────────────────────────────────────────
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 1,
|
|
|
|
|
|
highlights: ['IR'],
|
|
|
|
|
|
bus: null,
|
|
|
|
|
|
ctrlSignal: '',
|
|
|
|
|
|
aluOp: '译码',
|
|
|
|
|
|
regUpdates: {},
|
|
|
|
|
|
msg: `[译码 1/2] 控制单元解析 IR 中的指令 "${inst.asm}",识别操作码与操作数`,
|
|
|
|
|
|
signal: `IR → 操作码: ${inst.op}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 1,
|
|
|
|
|
|
highlights: ['CU'],
|
|
|
|
|
|
bus: 'ctrl',
|
|
|
|
|
|
ctrlSignal: inst.op,
|
|
|
|
|
|
aluOp: '准备',
|
|
|
|
|
|
regUpdates: {},
|
|
|
|
|
|
msg: `[译码 2/2] 控制单元生成控制信号 "${inst.op}",激活对应功能部件,准备操作数路径`,
|
|
|
|
|
|
signal: `控制信号: ${inst.op}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ── EXECUTE ──────────────────────────────────────────────────────
|
|
|
|
|
|
if (inst.op === 'LOAD') {
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 2,
|
|
|
|
|
|
highlights: ['MAR'],
|
|
|
|
|
|
bus: 'addr',
|
|
|
|
|
|
ctrlSignal: 'READ',
|
|
|
|
|
|
aluOp: '读内存',
|
|
|
|
|
|
regUpdates: { MAR: inst.src },
|
|
|
|
|
|
msg: `[执行 1/2] 将操作数地址 ${hex(inst.src)} 送入 MAR,通过地址总线访问主存`,
|
|
|
|
|
|
signal: `MAR ← ${hex(inst.src)}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 2,
|
|
|
|
|
|
highlights: ['MDR', 'R0'],
|
|
|
|
|
|
bus: 'data',
|
|
|
|
|
|
ctrlSignal: 'READ',
|
|
|
|
|
|
aluOp: '读内存',
|
|
|
|
|
|
regUpdates: { MDR: 42, [inst.dst]: 42 },
|
|
|
|
|
|
msg: `[执行 2/2] 主存数据 42 经数据总线送入 MDR,再写入目标寄存器 ${inst.dst}`,
|
|
|
|
|
|
signal: `MDR ← MEM[${hex(inst.src)}];${inst.dst} ← MDR`,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else if (inst.op === 'LOADI') {
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 2,
|
|
|
|
|
|
highlights: ['IR', inst.dst],
|
|
|
|
|
|
bus: null,
|
|
|
|
|
|
ctrlSignal: 'LOADI',
|
|
|
|
|
|
aluOp: '立即数',
|
|
|
|
|
|
regUpdates: { [inst.dst]: inst.imm },
|
|
|
|
|
|
msg: `[执行] 立即数 #${inst.imm} 直接从 IR 中提取,写入寄存器 ${inst.dst}`,
|
|
|
|
|
|
signal: `${inst.dst} ← #${inst.imm}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else if (inst.op === 'ADD') {
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 2,
|
|
|
|
|
|
highlights: ['R0', 'R1', 'ACC'],
|
|
|
|
|
|
bus: null,
|
|
|
|
|
|
ctrlSignal: 'ADD',
|
|
|
|
|
|
aluOp: 'R0 + R1',
|
|
|
|
|
|
regUpdates: { ACC: null }, // computed at runtime
|
|
|
|
|
|
msg: `[执行 1/2] ALU 读取 R0 和 R1 的值,开始加法运算`,
|
|
|
|
|
|
signal: `ALU: R0 + R1`,
|
|
|
|
|
|
})
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 2,
|
|
|
|
|
|
highlights: ['ACC'],
|
|
|
|
|
|
bus: null,
|
|
|
|
|
|
ctrlSignal: 'ADD',
|
|
|
|
|
|
aluOp: '= 结果',
|
|
|
|
|
|
regUpdates: { ACC: '__ADD_RESULT__' },
|
|
|
|
|
|
msg: `[执行 2/2] ALU 完成加法,结果暂存到累加器 ACC`,
|
|
|
|
|
|
signal: `ACC ← R0 + R1`,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else if (inst.op === 'STORE') {
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 2,
|
|
|
|
|
|
highlights: ['MAR', 'MDR'],
|
|
|
|
|
|
bus: 'addr',
|
|
|
|
|
|
ctrlSignal: 'WRITE',
|
|
|
|
|
|
aluOp: '写内存',
|
|
|
|
|
|
regUpdates: { MAR: inst.addr, MDR: '__FROM_R0__' },
|
|
|
|
|
|
msg: `[执行 1/2] 将目标地址 ${hex(inst.addr)} 送入 MAR,将 ${inst.src} 的值送入 MDR,准备写入主存`,
|
|
|
|
|
|
signal: `MAR ← ${hex(inst.addr)};MDR ← ${inst.src}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 2,
|
|
|
|
|
|
highlights: ['MDR'],
|
|
|
|
|
|
bus: 'data',
|
|
|
|
|
|
ctrlSignal: 'WRITE',
|
|
|
|
|
|
aluOp: '写内存',
|
|
|
|
|
|
regUpdates: { '__MEM__': inst.addr },
|
|
|
|
|
|
msg: `[执行 2/2] MDR 的值经数据总线写入主存地址 ${hex(inst.addr)}`,
|
|
|
|
|
|
signal: `MEM[${hex(inst.addr)}] ← MDR`,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
// ── WRITE BACK ───────────────────────────────────────────────────
|
|
|
|
|
|
if (inst.op === 'ADD') {
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 3,
|
|
|
|
|
|
highlights: ['ACC', 'R0'],
|
|
|
|
|
|
bus: null,
|
|
|
|
|
|
ctrlSignal: 'WB',
|
|
|
|
|
|
aluOp: '写回',
|
|
|
|
|
|
regUpdates: { R0: '__ACC__' },
|
|
|
|
|
|
msg: `[写回 1/2] 将 ACC 中的运算结果写回目标寄存器 R0`,
|
|
|
|
|
|
signal: `R0 ← ACC`,
|
|
|
|
|
|
})
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 3,
|
|
|
|
|
|
highlights: ['PC'],
|
|
|
|
|
|
bus: null,
|
|
|
|
|
|
ctrlSignal: 'WB',
|
|
|
|
|
|
aluOp: '—',
|
|
|
|
|
|
regUpdates: {},
|
|
|
|
|
|
msg: `[写回 2/2] 写回完成,PC 已在取指阶段自增,指向下一条指令 ${hex(pc + 1)}`,
|
|
|
|
|
|
signal: `PC = ${hex(pc + 1)}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else if (inst.op === 'STORE') {
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 3,
|
|
|
|
|
|
highlights: ['PC'],
|
|
|
|
|
|
bus: null,
|
|
|
|
|
|
ctrlSignal: 'WB',
|
|
|
|
|
|
aluOp: '—',
|
|
|
|
|
|
regUpdates: {},
|
|
|
|
|
|
msg: `[写回] STORE 指令结果已在执行阶段写入主存,写回阶段确认完成,PC=${hex(pc + 1)}`,
|
|
|
|
|
|
signal: `完成`,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
steps.push({
|
|
|
|
|
|
phase: 3,
|
|
|
|
|
|
highlights: ['PC'],
|
|
|
|
|
|
bus: null,
|
|
|
|
|
|
ctrlSignal: 'WB',
|
|
|
|
|
|
aluOp: '—',
|
|
|
|
|
|
regUpdates: {},
|
|
|
|
|
|
msg: `[写回] 结果已写入目标寄存器,PC 已自增至 ${hex(pc + 1)},准备执行下一条指令`,
|
|
|
|
|
|
signal: `PC = ${hex(pc + 1)}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return steps
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const allSteps = buildSteps()
|
|
|
|
|
|
const totalSteps = allSteps.length
|
|
|
|
|
|
|
|
|
|
|
|
const stepIndex = ref(0)
|
|
|
|
|
|
const done = ref(false)
|
|
|
|
|
|
const autoRunning = ref(false)
|
|
|
|
|
|
let autoTimer = null
|
|
|
|
|
|
|
|
|
|
|
|
// CPU state
|
|
|
|
|
|
const regs = ref({ PC: BASE_ADDR, IR: '', MAR: null, MDR: null, ACC: 0, R0: 0, R1: 0, R2: 0, R3: 0 })
|
|
|
|
|
|
const busActive = ref(null)
|
|
|
|
|
|
const ctrlSignal = ref('')
|
|
|
|
|
|
const aluOp = ref('—')
|
|
|
|
|
|
const fetchedAddr = ref(null)
|
|
|
|
|
|
const dataMemory = ref({ [DATA_BASE]: 42, [DATA_BASE + 1]: 0 })
|
|
|
|
|
|
const activeHighlights = ref([])
|
|
|
|
|
|
const currentPhase = ref(-1)
|
|
|
|
|
|
|
|
|
|
|
|
const currentStep = computed(() => {
|
|
|
|
|
|
if (stepIndex.value === 0) return { msg: '点击"时钟脉冲"开始逐步执行,或点击"自动运行"连续播放。', signal: null }
|
|
|
|
|
|
return allSteps[Math.min(stepIndex.value - 1, totalSteps - 1)]
|
|
|
|
|
|
})
|
|
|
|
|
|
function isHighlight(name) { return activeHighlights.value.includes(name) }
|
|
|
|
|
|
function isActive(unit) {
|
|
|
|
|
|
if (unit === 'CU') return currentPhase.value === 0 || currentPhase.value === 1
|
|
|
|
|
|
if (unit === 'ALU') return currentPhase.value === 2 && aluOp.value !== '读内存' && aluOp.value !== '写内存'
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyStep(step) {
|
|
|
|
|
|
currentPhase.value = step.phase
|
|
|
|
|
|
busActive.value = step.bus
|
|
|
|
|
|
ctrlSignal.value = step.ctrlSignal || ''
|
|
|
|
|
|
aluOp.value = step.aluOp || '—'
|
|
|
|
|
|
activeHighlights.value = step.highlights || []
|
|
|
|
|
|
if (step.fetchedAddr != null) fetchedAddr.value = step.fetchedAddr
|
|
|
|
|
|
|
|
|
|
|
|
for (const [k, v] of Object.entries(step.regUpdates || {})) {
|
|
|
|
|
|
if (k === '__MEM__') {
|
|
|
|
|
|
dataMemory.value = { ...dataMemory.value, [v]: regs.value.MDR }
|
|
|
|
|
|
} else if (v === '__ADD_RESULT__') {
|
|
|
|
|
|
regs.value = { ...regs.value, ACC: regs.value.R0 + regs.value.R1 }
|
|
|
|
|
|
} else if (v === '__ACC__') {
|
|
|
|
|
|
regs.value = { ...regs.value, R0: regs.value.ACC }
|
|
|
|
|
|
} else if (v === '__FROM_R0__') {
|
|
|
|
|
|
regs.value = { ...regs.value, MDR: regs.value.R0 }
|
|
|
|
|
|
} else if (v === null) {
|
|
|
|
|
|
// no-op placeholder
|
|
|
|
|
|
} else {
|
|
|
|
|
|
regs.value = { ...regs.value, [k]: v }
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
function advance() {
|
|
|
|
|
|
if (done.value) return
|
|
|
|
|
|
applyStep(allSteps[stepIndex.value])
|
|
|
|
|
|
stepIndex.value++
|
|
|
|
|
|
if (stepIndex.value >= totalSteps) {
|
|
|
|
|
|
done.value = true
|
|
|
|
|
|
stopAuto()
|
|
|
|
|
|
}
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
|
|
|
|
|
|
function toggleAuto() {
|
|
|
|
|
|
if (autoRunning.value) {
|
|
|
|
|
|
stopAuto()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
autoRunning.value = true
|
|
|
|
|
|
autoTimer = setInterval(() => {
|
2026-02-26 04:35:28 +08:00
|
|
|
|
if (done.value) {
|
|
|
|
|
|
stopAuto()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
advance()
|
|
|
|
|
|
}, 900)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopAuto() {
|
|
|
|
|
|
autoRunning.value = false
|
2026-02-26 04:35:28 +08:00
|
|
|
|
if (autoTimer) {
|
|
|
|
|
|
clearInterval(autoTimer)
|
|
|
|
|
|
autoTimer = null
|
|
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function reset() {
|
|
|
|
|
|
stopAuto()
|
|
|
|
|
|
stepIndex.value = 0
|
|
|
|
|
|
done.value = false
|
|
|
|
|
|
regs.value = { PC: BASE_ADDR, IR: '', MAR: null, MDR: null, ACC: 0, R0: 0, R1: 0, R2: 0, R3: 0 }
|
|
|
|
|
|
busActive.value = null
|
|
|
|
|
|
ctrlSignal.value = ''
|
|
|
|
|
|
aluOp.value = '—'
|
|
|
|
|
|
fetchedAddr.value = null
|
|
|
|
|
|
dataMemory.value = { [DATA_BASE]: 42, [DATA_BASE + 1]: 0 }
|
|
|
|
|
|
activeHighlights.value = []
|
|
|
|
|
|
currentPhase.value = -1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 04:35:28 +08:00
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
stopAuto()
|
|
|
|
|
|
})
|
2026-02-23 01:40:56 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.cpu-demo {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
2026-02-24 18:22:58 +08:00
|
|
|
|
border-radius: 10px;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
padding: 1rem 1.2rem;
|
|
|
|
|
|
margin: 1rem 0;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
font-size: 0.82rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.demo-title {
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
color: var(--vp-c-brand-1);
|
|
|
|
|
|
margin-bottom: 0.9rem;
|
|
|
|
|
|
text-align: center;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
/* ── Main layout ── */
|
|
|
|
|
|
.main-layout {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 80px 1fr;
|
|
|
|
|
|
gap: 0.6rem;
|
|
|
|
|
|
margin-bottom: 0.8rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
/* ── CPU box ── */
|
|
|
|
|
|
.cpu-box {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
border: 2px dashed var(--vp-c-brand-1);
|
|
|
|
|
|
border-radius: 8px;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
padding: 0.6rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
background: var(--vp-c-bg);
|
2026-02-24 18:22:58 +08:00
|
|
|
|
position: relative;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.cpu-label {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -0.6rem;
|
|
|
|
|
|
left: 0.8rem;
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
padding: 0 0.4rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 0.75rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
color: var(--vp-c-brand-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.unit {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 6px;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
padding: 0.45rem 0.5rem;
|
|
|
|
|
|
margin-bottom: 0.45rem;
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
transition: background 0.25s, border-color 0.25s;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.unit:last-child { margin-bottom: 0; }
|
|
|
|
|
|
.unit.active {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
background: var(--vp-c-brand-soft);
|
|
|
|
|
|
border-color: var(--vp-c-brand-1);
|
|
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.unit-title {
|
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
|
font-weight: 700;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
color: var(--vp-c-text-2);
|
2026-02-24 18:22:58 +08:00
|
|
|
|
margin-bottom: 0.35rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.regs-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.reg-cell {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
display: flex;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 0.25rem 0.4rem;
|
|
|
|
|
|
min-width: 52px;
|
|
|
|
|
|
transition: background 0.2s, border-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.reg-cell.highlight {
|
|
|
|
|
|
background: #fef08a;
|
|
|
|
|
|
border-color: #ca8a04;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.dark .reg-cell.highlight {
|
|
|
|
|
|
background: #713f12;
|
|
|
|
|
|
border-color: #fbbf24;
|
|
|
|
|
|
}
|
|
|
|
|
|
.reg-name {
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--vp-c-brand-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.reg-val {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
font-family: monospace;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.ir-val {
|
|
|
|
|
|
font-size: 0.6rem;
|
|
|
|
|
|
max-width: 90px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.reg-hint {
|
|
|
|
|
|
font-size: 0.55rem;
|
|
|
|
|
|
color: var(--vp-c-text-3);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.alu-op {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
2026-02-23 01:40:56 +08:00
|
|
|
|
background: var(--vp-c-bg);
|
2026-02-24 18:22:58 +08:00
|
|
|
|
border: 1px dashed var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
|
|
min-width: 60px;
|
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.alu-op.running {
|
|
|
|
|
|
color: var(--vp-c-brand-1);
|
|
|
|
|
|
border-color: var(--vp-c-brand-1);
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
/* ── Bus column ── */
|
|
|
|
|
|
.bus-col {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
justify-content: center;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.bus {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
border-radius: 4px;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
padding: 0.3rem 0.4rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
2026-02-24 18:22:58 +08:00
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
transition: background 0.25s, border-color 0.25s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.bus.active { border-color: var(--vp-c-brand-1); background: var(--vp-c-brand-soft); }
|
|
|
|
|
|
.addr-bus.active { border-color: #3b82f6; background: #eff6ff; }
|
|
|
|
|
|
.data-bus.active { border-color: #10b981; background: #ecfdf5; }
|
|
|
|
|
|
.ctrl-bus.active { border-color: #f59e0b; background: #fffbeb; }
|
|
|
|
|
|
.dark .addr-bus.active { background: #1e3a5f; }
|
|
|
|
|
|
.dark .data-bus.active { background: #064e3b; }
|
|
|
|
|
|
.dark .ctrl-bus.active { background: #451a03; }
|
|
|
|
|
|
|
|
|
|
|
|
.bus-label {
|
|
|
|
|
|
font-size: 0.6rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--vp-c-text-3);
|
|
|
|
|
|
writing-mode: vertical-rl;
|
|
|
|
|
|
text-orientation: mixed;
|
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.bus-val {
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 0.6rem;
|
|
|
|
|
|
color: var(--vp-c-brand-1);
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-top: 0.2rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.arrow-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.4rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.arrow {
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
color: var(--vp-c-text-3);
|
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.arrow.lit { color: var(--vp-c-brand-1); }
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
/* ── Memory box ── */
|
|
|
|
|
|
.mem-box {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 0.6rem;
|
|
|
|
|
|
background: var(--vp-c-bg-alt);
|
|
|
|
|
|
}
|
|
|
|
|
|
.mem-label {
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
margin-bottom: 0.4rem;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mem-label-sm {
|
|
|
|
|
|
font-size: 0.65rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
color: var(--vp-c-text-3);
|
2026-02-24 18:22:58 +08:00
|
|
|
|
margin: 0.4rem 0 0.2rem;
|
|
|
|
|
|
border-top: 1px dashed var(--vp-c-divider);
|
|
|
|
|
|
padding-top: 0.3rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mem-rows { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
|
|
|
|
.mem-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.3rem;
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
padding: 0.2rem 0.3rem;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
transition: background 0.2s, border-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mem-row.pc-row {
|
|
|
|
|
|
background: var(--vp-c-brand-soft);
|
|
|
|
|
|
border-color: var(--vp-c-brand-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.mem-row.mar-row {
|
|
|
|
|
|
background: #eff6ff;
|
|
|
|
|
|
border-color: #3b82f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dark .mem-row.mar-row { background: #1e3a5f; }
|
|
|
|
|
|
.mem-row.fetched {
|
|
|
|
|
|
background: #f0fdf4;
|
|
|
|
|
|
border-color: #10b981;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.dark .mem-row.fetched { background: #064e3b; }
|
|
|
|
|
|
.pc-arrow { color: var(--vp-c-brand-1); font-weight: 700; width: 10px; }
|
|
|
|
|
|
.mem-addr { color: var(--vp-c-text-3); min-width: 42px; }
|
|
|
|
|
|
.mem-inst { color: var(--vp-c-text-1); }
|
|
|
|
|
|
.data-row .mem-inst { color: var(--vp-c-text-2); }
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
/* ── Pipeline bar ── */
|
|
|
|
|
|
.pipeline-bar {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
2026-02-24 18:22:58 +08:00
|
|
|
|
border-radius: 6px;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
overflow: hidden;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
margin-bottom: 0.7rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.ph-cell {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
text-align: center;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
padding: 0.35rem 0;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
border-right: 1px solid var(--vp-c-divider);
|
2026-02-24 18:22:58 +08:00
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
transition: background 0.2s;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.ph-cell:last-child { border-right: none; }
|
|
|
|
|
|
.ph-cell.ph-done { background: var(--vp-c-bg-alt); }
|
|
|
|
|
|
.ph-cell.ph-active { background: var(--vp-c-brand-1); color: white; }
|
|
|
|
|
|
.ph-en { display: block; font-size: 0.65rem; font-weight: 700; }
|
|
|
|
|
|
.ph-zh { display: block; font-size: 0.72rem; }
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
/* ── Step detail ── */
|
|
|
|
|
|
.step-detail {
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 0.6rem 0.8rem;
|
|
|
|
|
|
margin-bottom: 0.7rem;
|
|
|
|
|
|
min-height: 60px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.step-badge {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
background: var(--vp-c-brand-soft);
|
|
|
|
|
|
color: var(--vp-c-brand-1);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
padding: 0.1rem 0.4rem;
|
|
|
|
|
|
margin-bottom: 0.3rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.step-msg {
|
2026-02-23 01:40:56 +08:00
|
|
|
|
font-size: 0.8rem;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
color: var(--vp-c-text-1);
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.step-signal {
|
|
|
|
|
|
margin-top: 0.3rem;
|
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
}
|
|
|
|
|
|
.step-signal code {
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
padding: 0.1rem 0.3rem;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
font-family: monospace;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
/* ── Controls ── */
|
2026-02-23 01:40:56 +08:00
|
|
|
|
.controls {
|
|
|
|
|
|
display: flex;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
gap: 0.6rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
justify-content: center;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
flex-wrap: wrap;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.btn-clock, .btn-auto, .btn-reset {
|
|
|
|
|
|
padding: 0.45rem 1rem;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
font-weight: 600;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border: none;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
transition: opacity 0.2s;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.btn-clock { background: var(--vp-c-brand-1); color: white; }
|
|
|
|
|
|
.btn-auto { background: #10b981; color: white; }
|
|
|
|
|
|
.btn-reset { background: transparent; border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-2); }
|
|
|
|
|
|
.btn-clock:disabled, .btn-auto:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
|
|
.btn-clock:not(:disabled):hover { opacity: 0.85; }
|
|
|
|
|
|
.btn-auto:not(:disabled):hover { opacity: 0.85; }
|
2026-02-23 01:40:56 +08:00
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.done-msg {
|
|
|
|
|
|
margin-top: 0.7rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
text-align: center;
|
2026-02-24 18:22:58 +08:00
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
color: #10b981;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-reset.inline {
|
|
|
|
|
|
margin-left: 0.5rem;
|
|
|
|
|
|
padding: 0.2rem 0.6rem;
|
|
|
|
|
|
font-size: 0.75rem;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 18:22:58 +08:00
|
|
|
|
@media (max-width: 680px) {
|
|
|
|
|
|
.main-layout {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
.bus-col {
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
justify-content: space-around;
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
2026-02-24 18:22:58 +08:00
|
|
|
|
.bus-label { writing-mode: horizontal-tb; }
|
|
|
|
|
|
.arrow-row { flex-direction: row; }
|
2026-02-23 01:40:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|