docs: update terminal intro content and components
This commit is contained in:
@@ -15,29 +15,58 @@
|
||||
<div class="arch-demo">
|
||||
<div class="analogy-header">
|
||||
<div class="analogy-item">
|
||||
<div class="icon">🍽️</div>
|
||||
<div class="icon">🖥️</div>
|
||||
<div class="text">
|
||||
<div class="role">Terminal = Table (餐桌)</div>
|
||||
<div class="desc">UI & Input/Output<br>交互界面与输入输出</div>
|
||||
<div class="role">Terminal (终端)</div>
|
||||
<div class="desc">传声筒 / 窗口</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="analogy-item">
|
||||
<div class="icon">💁♂️</div>
|
||||
<div class="icon">🗣️</div>
|
||||
<div class="text">
|
||||
<div class="role">Shell = Waiter (服务员)</div>
|
||||
<div class="desc">Interpreter & Logic<br>解释器与逻辑处理</div>
|
||||
<div class="role">Shell (壳)</div>
|
||||
<div class="desc">翻译官 / 助手</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="analogy-item">
|
||||
<div class="icon">👨🍳</div>
|
||||
<div class="icon">⚙️</div>
|
||||
<div class="text">
|
||||
<div class="role">Kernel = Kitchen (后厨)</div>
|
||||
<div class="desc">System Execution<br>系统执行与硬件调度</div>
|
||||
<div class="role">Kernel (内核)</div>
|
||||
<div class="desc">大管家 / 芯片</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<div class="diagram-container" @click="nextStep" :class="{ 'clickable': currentStep < totalSteps }">
|
||||
<!-- Click Overlay Hint -->
|
||||
<div class="click-overlay" v-if="currentStep === 0">
|
||||
<div class="click-hint">
|
||||
<span class="icon">👆</span>
|
||||
<span class="text">不断点击屏幕演示 / Keep Clicking</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Overlay -->
|
||||
<div class="completed-overlay" v-if="currentStep >= totalSteps">
|
||||
<div class="completed-hint" @click.stop="reset">
|
||||
<span class="icon">✅</span>
|
||||
<span class="text">演示结束,点击重置 / Finished (Reset)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spaces Background -->
|
||||
<div class="spaces-bg">
|
||||
<div class="space user-space">
|
||||
<div class="space-header">User Space (用户空间)</div>
|
||||
</div>
|
||||
<div class="barrier">
|
||||
<div class="barrier-line"></div>
|
||||
</div>
|
||||
<div class="space kernel-space">
|
||||
<div class="space-header">Kernel Space (内核空间)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Node -->
|
||||
<div class="node terminal" :class="{ active: activeNode === 'terminal' }">
|
||||
<div class="node-title">TERMINAL (终端)</div>
|
||||
@@ -181,8 +210,8 @@ const steps = [
|
||||
{
|
||||
titleEn: "3. Shell Parsing",
|
||||
titleZh: "3. Shell 解析",
|
||||
descEn: "The Shell receives the characters and figures out what you want.",
|
||||
descZh: "Shell 接收到字符,并解析你的意图。",
|
||||
descEn: "The Shell (Waiter) translates your command for the Kernel.",
|
||||
descZh: "Shell(服务员)接收指令,并将其翻译成内核能听懂的请求。",
|
||||
techEn: "Shell tokenizes input, finds the 'ls' executable in $PATH.",
|
||||
techZh: "Shell 对输入进行分词,并在 $PATH 环境变量中查找 'ls' 可执行文件。",
|
||||
action: async () => {
|
||||
@@ -207,8 +236,8 @@ const steps = [
|
||||
{
|
||||
titleEn: "5. Kernel Execution",
|
||||
titleZh: "5. 内核执行",
|
||||
descEn: "The Kernel (the boss) talks to the hardware to get the actual data.",
|
||||
descZh: "内核(大管家)与硬件通信以获取实际数据。",
|
||||
descEn: "The Kernel (Kitchen) executes the request by accessing hardware.",
|
||||
descZh: "内核(后厨)直接操作硬件(如磁盘)来执行实际任务。",
|
||||
techEn: "Kernel driver accesses the file system (APFS/ext4).",
|
||||
techZh: "内核驱动程序访问文件系统 (APFS/ext4)。",
|
||||
action: async () => {
|
||||
@@ -366,9 +395,169 @@ const reset = () => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
/* Increase padding to accommodate labels */
|
||||
padding: 40px 10px 20px 10px;
|
||||
z-index: 1;
|
||||
cursor: default;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.diagram-container.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.diagram-container.clickable:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.click-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50; /* Topmost */
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: 12px;
|
||||
animation: pulse-bg 2s infinite;
|
||||
}
|
||||
|
||||
.click-hint {
|
||||
background: #22c55e;
|
||||
color: #000;
|
||||
padding: 10px 20px;
|
||||
border-radius: 30px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 15px rgba(34, 197, 94, 0.4);
|
||||
transform: scale(1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.diagram-container:hover .click-hint {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@keyframes pulse-bg {
|
||||
0% { background: rgba(0, 0, 0, 0.4); }
|
||||
50% { background: rgba(0, 0, 0, 0.2); }
|
||||
100% { background: rgba(0, 0, 0, 0.4); }
|
||||
}
|
||||
|
||||
.completed-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50; /* Same as click overlay */
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(2px);
|
||||
animation: fade-in 0.5s;
|
||||
}
|
||||
|
||||
.completed-hint {
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 30px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.completed-hint:hover {
|
||||
transform: scale(1.05);
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.spaces-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.space {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.space-header {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
padding: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.user-space {
|
||||
flex: 2;
|
||||
background: rgba(34, 211, 238, 0.03);
|
||||
border-right: 1px dashed #3f3f46;
|
||||
border-radius: 8px 0 0 8px;
|
||||
align-items: flex-start;
|
||||
/* Ensure User Space (containing Shell) is below the Barrier Label */
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.user-space .space-header { color: #22d3ee; }
|
||||
|
||||
.kernel-space {
|
||||
flex: 1;
|
||||
background: rgba(239, 68, 68, 0.03);
|
||||
border-radius: 0 8px 8px 0;
|
||||
align-items: flex-end;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.kernel-space .space-header { color: #ef4444; }
|
||||
|
||||
.barrier {
|
||||
width: 2px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 10; /* Bring Barrier to front */
|
||||
}
|
||||
|
||||
.barrier-line {
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
#facc15 0,
|
||||
#facc15 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.node {
|
||||
background: #18181b;
|
||||
border: 2px solid #27272a;
|
||||
@@ -378,10 +567,15 @@ const reset = () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s;
|
||||
z-index: 2;
|
||||
z-index: 5; /* Nodes should be above spaces but below barrier label if overlapping */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Specific z-index for Shell to prevent it from covering barrier label */
|
||||
.node.shell {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.node.active {
|
||||
border-color: #22c55e;
|
||||
box-shadow: 0 0 15px rgba(34, 197, 94, 0.2);
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="buffer-demo">
|
||||
<div class="terminal-frame">
|
||||
<div class="window-bar">
|
||||
<span class="dot red"></span>
|
||||
<span class="dot yellow"></span>
|
||||
<span class="dot green"></span>
|
||||
<span class="title">Terminal - Buffer Switching Demo</span>
|
||||
</div>
|
||||
|
||||
<div class="screen-container">
|
||||
<!-- Main Buffer (Layer 0) -->
|
||||
<div class="buffer main-buffer">
|
||||
<div class="line"><span class="prompt">➜</span> <span class="cmd">ls -la</span></div>
|
||||
<div class="line output">total 16</div>
|
||||
<div class="line output">drwxr-xr-x 2 user staff 64 Jan 15 10:00 .</div>
|
||||
<div class="line output">drwxr-xr-x 4 user staff 128 Jan 15 09:55 ..</div>
|
||||
<div class="line output">-rw-r--r-- 1 user staff 1024 Jan 15 10:00 notes.txt</div>
|
||||
<div class="line"><span class="prompt">➜</span> <span class="cmd">echo "Hello World"</span></div>
|
||||
<div class="line output">Hello World</div>
|
||||
<div class="line"><span class="prompt">➜</span> <span class="cmd">vim notes.txt</span></div>
|
||||
<!-- The cursor would be here if not in vim -->
|
||||
</div>
|
||||
|
||||
<!-- Alternate Buffer (Layer 1) -->
|
||||
<transition name="slide-up">
|
||||
<div v-if="isAltBufferActive" class="buffer alt-buffer">
|
||||
<div class="vim-header">
|
||||
<span class="filename">notes.txt</span>
|
||||
<span class="modified">[+]</span>
|
||||
</div>
|
||||
<div class="vim-body">
|
||||
<div class="line-num">1</div><div class="code">This is a text file opened in Vim.</div>
|
||||
<div class="line-num">2</div><div class="code"></div>
|
||||
<div class="line-num">3</div><div class="code">Notice how this interface takes up</div>
|
||||
<div class="line-num">4</div><div class="code">the entire screen?</div>
|
||||
<div class="line-num">5</div><div class="code"></div>
|
||||
<div class="line-num">6</div><div class="code">It is running in the <span class="highlight">Alternate Buffer</span>.</div>
|
||||
<div class="line-num">~</div>
|
||||
<div class="line-num">~</div>
|
||||
</div>
|
||||
<div class="vim-status-bar">
|
||||
<span class="mode">NORMAL</span>
|
||||
<span class="file-info">notes.txt [unix] (10:24)</span>
|
||||
</div>
|
||||
<div class="vim-cmd-line">
|
||||
<span v-if="showQuitCmd">:q</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="description">
|
||||
<div v-if="!isAltBufferActive">
|
||||
<p><strong>Current: Primary Buffer (主缓冲区)</strong></p>
|
||||
<p>This is the standard scrolling log. Commands are executed line by line.</p>
|
||||
<button class="action-btn" @click="openVim">
|
||||
Execute `vim notes.txt`
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p><strong>Current: Alternate Buffer (备用缓冲区)</strong></p>
|
||||
<p>A separate "canvas" for full-screen apps. It hides the history but doesn't delete it.</p>
|
||||
<button class="action-btn red" @click="quitVim">
|
||||
Execute `:q` (Quit)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isAltBufferActive = ref(false)
|
||||
const showQuitCmd = ref(false)
|
||||
|
||||
const openVim = () => {
|
||||
isAltBufferActive.value = true
|
||||
showQuitCmd.value = false
|
||||
}
|
||||
|
||||
const quitVim = () => {
|
||||
showQuitCmd.value = true
|
||||
setTimeout(() => {
|
||||
isAltBufferActive.value = false
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.buffer-demo {
|
||||
margin: 20px 0;
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.terminal-frame {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.window-bar {
|
||||
background: #2d2d2d;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.red { background: #ff5f56; }
|
||||
.yellow { background: #ffbd2e; }
|
||||
.green { background: #27c93f; }
|
||||
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.screen-container {
|
||||
position: relative;
|
||||
height: 240px;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.buffer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Main Buffer Styles */
|
||||
.main-buffer {
|
||||
color: #ccc;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #27c93f;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.cmd {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.output {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Alt Buffer Styles (Vim Look) */
|
||||
.alt-buffer {
|
||||
background: #282c34;
|
||||
color: #abb2bf;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.vim-header {
|
||||
display: none; /* Vim doesn't usually have a top header like this, but helpful for context? Maybe skip */
|
||||
}
|
||||
|
||||
.vim-body {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.line-num {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
color: #5c6370;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.code {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #e5c07b;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vim-status-bar {
|
||||
background: #3e4452;
|
||||
color: #ccc;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mode {
|
||||
font-weight: bold;
|
||||
background: #98c379;
|
||||
color: #282c34;
|
||||
padding: 0 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.vim-cmd-line {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 15px;
|
||||
background: #f6f6f7;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dark .controls {
|
||||
background: #252529;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: #27c93f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #22b036;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn.red {
|
||||
background: #ff5f56;
|
||||
}
|
||||
|
||||
.action-btn.red:hover {
|
||||
background: #e0483e;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<div class="cooked-raw-demo">
|
||||
<div class="mode-switch">
|
||||
<button
|
||||
:class="{ active: mode === 'cooked' }"
|
||||
@click="setMode('cooked')"
|
||||
>
|
||||
🥘 Cooked Mode (Standard)
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: mode === 'raw' }"
|
||||
@click="setMode('raw')"
|
||||
>
|
||||
🥩 Raw Mode (Vim/Games)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-container" @click="focusInput">
|
||||
<!-- Hidden Input for capturing keystrokes -->
|
||||
<input
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
class="hidden-input"
|
||||
@keydown="handleKey"
|
||||
@blur="isFocused = false"
|
||||
@focus="isFocused = true"
|
||||
/>
|
||||
|
||||
<!-- Visualization -->
|
||||
<div class="flow-diagram">
|
||||
|
||||
<!-- 1. User Input -->
|
||||
<div class="stage user-input" :class="{ focused: isFocused }">
|
||||
<div class="stage-title">1. Keyboard Input</div>
|
||||
<div class="key-visual">
|
||||
<span v-if="lastPressedKey" class="key-cap">{{ lastPressedKey }}</span>
|
||||
<span v-else class="placeholder">Type here...</span>
|
||||
</div>
|
||||
<div class="status-text" v-if="!isFocused">Click to focus</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">⬇</div>
|
||||
|
||||
<!-- 2. OS Buffer (Only for Cooked) -->
|
||||
<div class="stage buffer" :class="{ disabled: mode === 'raw', active: mode === 'cooked' }">
|
||||
<div class="stage-title">
|
||||
2. Line Buffer (Kernel)
|
||||
<span class="badge" v-if="mode === 'cooked'">Active</span>
|
||||
<span class="badge disabled" v-else>Bypassed</span>
|
||||
</div>
|
||||
<div class="buffer-content">
|
||||
<template v-if="mode === 'cooked'">
|
||||
<span v-for="(char, i) in buffer" :key="i" class="char">{{ char }}</span>
|
||||
<span class="cursor">_</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="bypass-text">⚡ Direct Pass-through ⚡</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="explanation">
|
||||
<span v-if="mode === 'cooked'">Waiting for Enter... (Backspace works)</span>
|
||||
<span v-else>No buffering. Every key is sent immediately.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">⬇</div>
|
||||
|
||||
<!-- 3. Application -->
|
||||
<div class="stage app-input">
|
||||
<div class="stage-title">3. Application Receives</div>
|
||||
<div class="app-content">
|
||||
<div v-for="(line, i) in appLines" :key="i" class="app-line">
|
||||
{{ line }}
|
||||
</div>
|
||||
<div class="app-line current">
|
||||
{{ appCurrentLine }}<span class="app-cursor">_</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mode = ref('cooked')
|
||||
const buffer = ref([])
|
||||
const appLines = ref([])
|
||||
const appCurrentLine = ref('')
|
||||
const lastPressedKey = ref('')
|
||||
const inputRef = ref(null)
|
||||
const isFocused = ref(false)
|
||||
|
||||
const setMode = (m) => {
|
||||
mode.value = m
|
||||
buffer.value = []
|
||||
appLines.value = []
|
||||
appCurrentLine.value = ''
|
||||
lastPressedKey.value = ''
|
||||
// Focus input
|
||||
setTimeout(() => inputRef.value?.focus(), 50)
|
||||
}
|
||||
|
||||
const focusInput = () => {
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
|
||||
const handleKey = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const key = e.key
|
||||
|
||||
// Visual feedback
|
||||
if (key === ' ') lastPressedKey.value = 'Space'
|
||||
else if (key === 'Enter') lastPressedKey.value = 'Enter'
|
||||
else if (key === 'Backspace') lastPressedKey.value = '⌫'
|
||||
else if (key.length === 1) lastPressedKey.value = key
|
||||
else lastPressedKey.value = key
|
||||
|
||||
// Clear visual feedback after delay
|
||||
setTimeout(() => {
|
||||
if (lastPressedKey.value === (key === ' ' ? 'Space' : (key === 'Enter' ? 'Enter' : (key === 'Backspace' ? '⌫' : key)))) {
|
||||
// lastPressedKey.value = '' // Optional: keep last key visible
|
||||
}
|
||||
}, 500)
|
||||
|
||||
if (mode.value === 'cooked') {
|
||||
handleCookedMode(e)
|
||||
} else {
|
||||
handleRawMode(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCookedMode = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Flush buffer to app
|
||||
const text = buffer.value.join('')
|
||||
appLines.value.push(text)
|
||||
buffer.value = []
|
||||
} else if (e.key === 'Backspace') {
|
||||
buffer.value.pop()
|
||||
} else if (e.key.length === 1) {
|
||||
buffer.value.push(e.key)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRawMode = (e) => {
|
||||
// Immediate send
|
||||
if (e.key === 'Enter') {
|
||||
appLines.value.push(appCurrentLine.value)
|
||||
appCurrentLine.value = ''
|
||||
} else if (e.key === 'Backspace') {
|
||||
// In raw mode, Backspace is just a control code sent to app
|
||||
// But for demo visualization, let's show it effectively deletes in app if app handles it
|
||||
// Or strictly show control code? Let's simulate app handling it immediately.
|
||||
appCurrentLine.value = appCurrentLine.value.slice(0, -1)
|
||||
} else if (e.key.length === 1) {
|
||||
appCurrentLine.value += e.key
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cooked-raw-demo {
|
||||
margin: 24px 0;
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mode-switch button {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mode-switch button.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border: 1px solid #333;
|
||||
position: relative;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.flow-diagram {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stage {
|
||||
width: 100%;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stage-title {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stage.user-input.focused {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 10px rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
}
|
||||
|
||||
.key-visual {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.key-cap {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 0 #ccc;
|
||||
font-size: 16px;
|
||||
animation: pop 0.1s;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #555;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Buffer */
|
||||
.stage.buffer.active {
|
||||
background: #25332e;
|
||||
border-color: #0dbc79;
|
||||
}
|
||||
|
||||
.stage.buffer.disabled {
|
||||
opacity: 0.5;
|
||||
background: #222;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.buffer-content {
|
||||
background: #000;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
min-height: 40px;
|
||||
color: #0dbc79;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bypass-text {
|
||||
color: #e5e510;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* App */
|
||||
.stage.app-input {
|
||||
background: #252526;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
background: #000;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
min-height: 80px;
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.app-line {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: #0dbc79;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge.disabled {
|
||||
background: #555;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.cursor, .app-cursor {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
background: currentColor;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% { transform: scale(0.9); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
</style>
|
||||
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div class="parser-demo">
|
||||
<div class="demo-header">
|
||||
<div class="title">转义序列解析原理 (Parser Mechanism)</div>
|
||||
<div class="controls">
|
||||
<button @click="reset" :disabled="isPlaying">Reset</button>
|
||||
<button @click="togglePlay" :disabled="isFinished" class="play-btn">
|
||||
{{ isPlaying ? '⏸ Pause' : '▶ Play Animation' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. 字节流传送带 -->
|
||||
<div class="stream-container">
|
||||
<div class="label">Input Byte Stream / 输入字节流</div>
|
||||
<div class="stream-track">
|
||||
<div class="stream-window-mask">
|
||||
<div class="stream-content" :style="{ transform: `translateX(-${currentIndex * 40}px)` }">
|
||||
<div
|
||||
v-for="(char, index) in charStream"
|
||||
:key="index"
|
||||
class="char-box"
|
||||
:class="{
|
||||
'active': index === currentIndex,
|
||||
'processed': index < currentIndex,
|
||||
'special': char.isSpecial,
|
||||
'arg': char.isArg
|
||||
}"
|
||||
>
|
||||
<span class="char-val">{{ char.display }}</span>
|
||||
<span class="char-code">{{ char.hex }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 指针 -->
|
||||
<div class="pointer">
|
||||
<div class="arrow">⬆</div>
|
||||
<div class="pointer-label">Current Byte</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 解析器状态机 -->
|
||||
<div class="parser-state-machine">
|
||||
<div class="state-box" :class="{ active: parserState === 'NORMAL' }">
|
||||
<div class="state-name">NORMAL</div>
|
||||
<div class="state-desc">Print Characters</div>
|
||||
</div>
|
||||
<div class="arrow-right">→</div>
|
||||
<div class="state-box warning" :class="{ active: parserState === 'ESCAPE' }">
|
||||
<div class="state-name">ESCAPE MODE</div>
|
||||
<div class="state-desc">Buffer Command...</div>
|
||||
</div>
|
||||
|
||||
<!-- 指令说明框 -->
|
||||
<div class="action-log" v-if="lastAction">
|
||||
<span class="action-icon">⚡</span>
|
||||
{{ lastAction }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 终端屏幕 -->
|
||||
<div class="terminal-screen">
|
||||
<div class="label">Terminal Screen / 屏幕显示</div>
|
||||
<div class="screen-content">
|
||||
<span
|
||||
v-for="(char, index) in outputBuffer"
|
||||
:key="index"
|
||||
:style="char.style"
|
||||
>{{ char.val }}</span><span class="cursor">_</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="explanation">
|
||||
<p>
|
||||
<span class="badge normal">Normal</span> 模式下,字符直接上屏。
|
||||
<span class="badge escape">Escape</span> 模式下(遇到 <code>ESC</code> 后),终端<strong>停止输出</strong>,开始收集字符作为指令,直到指令结束(如 <code>m</code>)并执行。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 原始字符串: Hello [RED]World[RESET]!
|
||||
// \x1B [ 3 1 m
|
||||
const RAW_DATA = [
|
||||
{ val: 'H', display: 'H', hex: '48' },
|
||||
{ val: 'i', display: 'i', hex: '69' },
|
||||
{ val: ' ', display: ' ', hex: '20' },
|
||||
{ val: '\x1B', display: 'ESC', hex: '1B', isSpecial: true },
|
||||
{ val: '[', display: '[', hex: '5B', isSpecial: true },
|
||||
{ val: '3', display: '3', hex: '33', isArg: true },
|
||||
{ val: '1', display: '1', hex: '31', isArg: true },
|
||||
{ val: 'm', display: 'm', hex: '6D', isSpecial: true }, // End of seq
|
||||
{ val: 'V', display: 'V', hex: '56' },
|
||||
{ val: 'i', display: 'i', hex: '69' },
|
||||
{ val: 'b', display: 'b', hex: '62' },
|
||||
{ val: 'e', display: 'e', hex: '65' },
|
||||
{ val: '\x1B', display: 'ESC', hex: '1B', isSpecial: true },
|
||||
{ val: '[', display: '[', hex: '5B', isSpecial: true },
|
||||
{ val: '0', display: '0', hex: '30', isArg: true },
|
||||
{ val: 'm', display: 'm', hex: '6D', isSpecial: true },
|
||||
{ val: '!', display: '!', hex: '21' }
|
||||
]
|
||||
|
||||
const charStream = ref(RAW_DATA)
|
||||
const currentIndex = ref(0)
|
||||
const outputBuffer = ref([])
|
||||
const parserState = ref('NORMAL') // NORMAL, ESCAPE
|
||||
const currentStyle = ref({})
|
||||
const isPlaying = ref(false)
|
||||
const isFinished = ref(false)
|
||||
const lastAction = ref('')
|
||||
|
||||
const reset = () => {
|
||||
currentIndex.value = 0
|
||||
outputBuffer.value = []
|
||||
parserState.value = 'NORMAL'
|
||||
currentStyle.value = {}
|
||||
isPlaying.value = false
|
||||
isFinished.value = false
|
||||
lastAction.value = ''
|
||||
}
|
||||
|
||||
const play = async () => {
|
||||
if (isPlaying.value) return
|
||||
isPlaying.value = true
|
||||
|
||||
while (currentIndex.value < charStream.value.length) {
|
||||
const char = charStream.value[currentIndex.value]
|
||||
|
||||
// Processing Logic
|
||||
if (parserState.value === 'NORMAL') {
|
||||
if (char.val === '\x1B') {
|
||||
parserState.value = 'ESCAPE'
|
||||
lastAction.value = 'Start Sequence'
|
||||
} else {
|
||||
outputBuffer.value.push({
|
||||
val: char.val,
|
||||
style: { ...currentStyle.value }
|
||||
})
|
||||
lastAction.value = 'Print Char'
|
||||
}
|
||||
} else if (parserState.value === 'ESCAPE') {
|
||||
// 简单模拟:遇到 'm' 结束
|
||||
if (char.val === 'm') {
|
||||
// 解析指令 (Hardcoded for demo)
|
||||
const prevChar = charStream.value[currentIndex.value - 1]
|
||||
if (prevChar.val === '1') {
|
||||
currentStyle.value = { color: '#ff5f56', fontWeight: 'bold' }
|
||||
lastAction.value = 'Execute: Set Color Red'
|
||||
} else if (prevChar.val === '0') {
|
||||
currentStyle.value = {}
|
||||
lastAction.value = 'Execute: Reset Style'
|
||||
}
|
||||
|
||||
// Small delay to show "Executing" state
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
parserState.value = 'NORMAL'
|
||||
} else {
|
||||
lastAction.value = 'Buffering...'
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 600)) // Animation speed
|
||||
currentIndex.value++
|
||||
}
|
||||
|
||||
isPlaying.value = false
|
||||
isFinished.value = true
|
||||
lastAction.value = 'Done'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.parser-demo {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
color: #fff;
|
||||
font-family: 'Menlo', monospace;
|
||||
margin: 20px 0;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
background: #333;
|
||||
border: 1px solid #555;
|
||||
color: white;
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.controls button.play-btn {
|
||||
background: #0dbc79;
|
||||
border-color: #0dbc79;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.controls button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Stream Track */
|
||||
.stream-container {
|
||||
background: #111;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stream-track {
|
||||
position: relative;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: center; /* Center the focus area */
|
||||
}
|
||||
|
||||
.stream-window-mask {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
/* Mask gradient to fade edges */
|
||||
mask-image: linear-gradient(to right, transparent, black 40%, black 60%, transparent);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, black 40%, black 60%, transparent);
|
||||
}
|
||||
|
||||
.stream-content {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
position: absolute;
|
||||
left: 50%; /* Start from center */
|
||||
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
padding-left: 20px; /* Offset for first item */
|
||||
}
|
||||
|
||||
.char-box {
|
||||
width: 36px;
|
||||
height: 48px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #444;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.char-box.active {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 10px rgba(255,255,255,0.5);
|
||||
z-index: 10;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.char-box.processed {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.char-box.special {
|
||||
border-color: #e5e510;
|
||||
color: #e5e510;
|
||||
}
|
||||
.char-box.active.special {
|
||||
background: #e5e510;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.char-box.arg {
|
||||
border-color: #11a8cd;
|
||||
color: #11a8cd;
|
||||
}
|
||||
.char-box.active.arg {
|
||||
background: #11a8cd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.char-val { font-size: 14px; font-weight: bold; }
|
||||
.char-code { font-size: 9px; opacity: 0.7; margin-top: 2px; }
|
||||
|
||||
.pointer {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
color: #0dbc79;
|
||||
}
|
||||
.arrow { font-size: 20px; line-height: 1; }
|
||||
.pointer-label { font-size: 10px; white-space: nowrap; }
|
||||
|
||||
|
||||
/* State Machine */
|
||||
.parser-state-machine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
background: #252526;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.state-box {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background: #333;
|
||||
opacity: 0.3;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.state-box.active {
|
||||
opacity: 1;
|
||||
background: #0dbc79;
|
||||
color: #000;
|
||||
box-shadow: 0 0 15px rgba(13, 188, 121, 0.2);
|
||||
}
|
||||
|
||||
.state-box.warning.active {
|
||||
background: #e5e510;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.state-name { font-weight: bold; font-size: 12px; }
|
||||
.state-desc { font-size: 10px; opacity: 0.8; }
|
||||
|
||||
.arrow-right { color: #555; font-size: 18px; }
|
||||
|
||||
.action-log {
|
||||
margin-left: 20px;
|
||||
padding: 4px 12px;
|
||||
background: #000;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #444;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
animation: flash 0.5s;
|
||||
}
|
||||
|
||||
/* Screen */
|
||||
.terminal-screen {
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
animation: blink 1s infinite;
|
||||
color: #0dbc79;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
margin-top: 15px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
color: #000;
|
||||
}
|
||||
.badge.normal { background: #0dbc79; }
|
||||
.badge.escape { background: #e5e510; }
|
||||
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
@keyframes flash { 0% { background: #333; } 100% { background: #000; } }
|
||||
|
||||
</style>
|
||||
@@ -82,11 +82,12 @@
|
||||
<div class="stream-line dashed"></div>
|
||||
<div class="stream-label">Event Loop / 事件循环</div>
|
||||
<div
|
||||
v-if="guiEvent"
|
||||
v-for="ev in guiEvents"
|
||||
:key="ev.id"
|
||||
class="gui-event-packet"
|
||||
:style="{ left: guiEvent.progress + '%' }"
|
||||
:style="{ left: ev.progress + '%' }"
|
||||
>
|
||||
{{ guiEvent.type }}
|
||||
{{ ev.type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +136,7 @@ const mode = ref('cli') // 'cli' | 'gui'
|
||||
const isAnimating = ref(false)
|
||||
const activeChars = ref([])
|
||||
const typedContent = ref('')
|
||||
const demoText = 'ls -la'
|
||||
const demoText = 'echo "hello world"'
|
||||
|
||||
const startSimulation = () => {
|
||||
if (isAnimating.value) return
|
||||
@@ -168,7 +169,7 @@ const startSimulation = () => {
|
||||
// Animate this char
|
||||
let progress = 10
|
||||
const interval = setInterval(() => {
|
||||
progress += 2
|
||||
progress += 4 // Faster speed
|
||||
const charObj = activeChars.value.find(c => c.id === charId)
|
||||
if (charObj) charObj.progress = progress
|
||||
|
||||
@@ -180,7 +181,7 @@ const startSimulation = () => {
|
||||
|
||||
// Next char
|
||||
index++
|
||||
setTimeout(processNextChar, 300)
|
||||
setTimeout(processNextChar, 100) // Faster typing
|
||||
}
|
||||
}, 20)
|
||||
}
|
||||
@@ -191,60 +192,92 @@ const startSimulation = () => {
|
||||
// GUI Logic
|
||||
const isGuiAnimating = ref(false)
|
||||
const isGuiClicking = ref(false)
|
||||
const guiEvent = ref(null)
|
||||
const guiEvents = ref([]) // Array of events
|
||||
const iconSelected = ref(false)
|
||||
const cursorPosition = ref({ x: 50, y: 50 })
|
||||
const inputMousePosition = ref({ x: 80, y: 60 }) // Input side (Invisible physical mouse)
|
||||
const screenCursorPosition = ref({ x: 80, y: 60 }) // Output side (Visible screen cursor)
|
||||
|
||||
const cursorStyle = computed(() => ({
|
||||
transform: `translate(${cursorPosition.value.x}px, ${cursorPosition.value.y}px)`
|
||||
transform: `translate(${screenCursorPosition.value.x}px, ${screenCursorPosition.value.y}px)`
|
||||
}))
|
||||
|
||||
const startGuiSimulation = () => {
|
||||
if (isGuiAnimating.value) return
|
||||
isGuiAnimating.value = true
|
||||
iconSelected.value = false
|
||||
cursorPosition.value = { x: 80, y: 60 } // Reset pos
|
||||
inputMousePosition.value = { x: 80, y: 60 }
|
||||
screenCursorPosition.value = { x: 80, y: 60 }
|
||||
guiEvents.value = []
|
||||
|
||||
// 1. Move Cursor
|
||||
// 1. Move Cursor (Physical Mouse Movement)
|
||||
let step = 0
|
||||
const moveInterval = setInterval(() => {
|
||||
step++
|
||||
cursorPosition.value = {
|
||||
inputMousePosition.value = {
|
||||
x: 80 - step * 2,
|
||||
y: 60 - step * 1.5
|
||||
}
|
||||
|
||||
// Emit Move Event frequently (Simulate high polling rate)
|
||||
if (step % 2 === 0) {
|
||||
const targetX = inputMousePosition.value.x
|
||||
const targetY = inputMousePosition.value.y
|
||||
emitGuiEvent(`Move(${Math.round(targetX)},${Math.round(targetY)})`, () => {
|
||||
// When packet arrives: Update screen cursor
|
||||
screenCursorPosition.value = { x: targetX, y: targetY }
|
||||
})
|
||||
}
|
||||
|
||||
if (step >= 20) {
|
||||
clearInterval(moveInterval)
|
||||
// 2. Click
|
||||
performClick()
|
||||
}
|
||||
}, 20)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const emitGuiEvent = (type, onArrive) => {
|
||||
const eventId = Date.now() + Math.random()
|
||||
const newEvent = {
|
||||
id: eventId,
|
||||
type: type,
|
||||
progress: 10
|
||||
}
|
||||
guiEvents.value.push(newEvent)
|
||||
|
||||
let progress = 10
|
||||
const packetInterval = setInterval(() => {
|
||||
progress += 2
|
||||
const ev = guiEvents.value.find(e => e.id === eventId)
|
||||
if (ev) ev.progress = progress
|
||||
|
||||
if (progress >= 90) {
|
||||
clearInterval(packetInterval)
|
||||
guiEvents.value = guiEvents.value.filter(e => e.id !== eventId)
|
||||
|
||||
// Execute callback when packet arrives at Output
|
||||
if (onArrive) onArrive()
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const performClick = () => {
|
||||
setTimeout(() => {
|
||||
isGuiClicking.value = true
|
||||
|
||||
// Send Event Packet
|
||||
guiEvent.value = { type: 'Click(40,30)', progress: 10 }
|
||||
|
||||
let progress = 10
|
||||
const packetInterval = setInterval(() => {
|
||||
progress += 2
|
||||
if (guiEvent.value) guiEvent.value.progress = progress
|
||||
// Send Click Event
|
||||
emitGuiEvent('Click(40,30)', () => {
|
||||
// When packet arrives: Select icon
|
||||
iconSelected.value = true
|
||||
|
||||
if (progress >= 90) {
|
||||
clearInterval(packetInterval)
|
||||
guiEvent.value = null
|
||||
setTimeout(() => {
|
||||
isGuiAnimating.value = false
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
isGuiClicking.value = false
|
||||
iconSelected.value = true // Effect
|
||||
|
||||
setTimeout(() => {
|
||||
isGuiAnimating.value = false
|
||||
}, 1000)
|
||||
}
|
||||
}, 10)
|
||||
}, 200) // Input click feedback is fast
|
||||
|
||||
}, 300)
|
||||
}
|
||||
@@ -405,7 +438,7 @@ const performClick = () => {
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
transition: transform 0.05s linear;
|
||||
transition: transform 0.1s linear; /* Smooth interpolation */
|
||||
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.5));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,618 @@
|
||||
<template>
|
||||
<div class="terminal-os-demo">
|
||||
<div class="os-switch">
|
||||
<button
|
||||
v-for="os in osList"
|
||||
:key="os.id"
|
||||
:class="{ active: currentOS === os.id }"
|
||||
@click="switchOS(os.id)"
|
||||
>
|
||||
<span class="os-icon">{{ os.icon }}</span>
|
||||
{{ os.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="terminal-window" :class="currentOS">
|
||||
<div class="window-bar">
|
||||
<div class="window-buttons">
|
||||
<span class="btn close"></span>
|
||||
<span class="btn minimize"></span>
|
||||
<span class="btn maximize"></span>
|
||||
</div>
|
||||
<div class="window-title">{{ currentOSConfig.title }}</div>
|
||||
<div class="window-controls">
|
||||
<button class="control-btn" @click="resetDemo" title="Reset">↺</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-content" @click="nextStep" :class="{ 'clickable': !isTyping && !isFinished }">
|
||||
<!-- Start Overlay -->
|
||||
<div class="start-overlay" v-if="lines.length === 0 || (lines.length === 1 && lines[0].content === '' && currentStepIndex === -1)">
|
||||
<div class="start-hint">
|
||||
<span class="icon">👆</span>
|
||||
<span class="text">不断点击屏幕演示 / Keep Clicking</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Overlay -->
|
||||
<div class="completed-overlay" v-if="isFinished">
|
||||
<div class="completed-hint" @click.stop="resetDemo">
|
||||
<span class="icon">✅</span>
|
||||
<span class="text">演示结束,点击重置 / Finished (Reset)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(line, index) in lines" :key="index" class="line">
|
||||
<template v-if="line.type === 'input'">
|
||||
<span class="prompt">{{ line.prompt }}</span><span class="cmd-text">{{ line.content }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="output-text">{{ line.content }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Active Input Line (when not animating or just waiting) -->
|
||||
<div class="line input-line" v-if="lines.length === 0 || (!isTyping && lines[lines.length-1].type !== 'input' && !isFinished)">
|
||||
<span class="prompt">{{ currentOSConfig.prompt }}</span>
|
||||
<span class="cursor">_</span>
|
||||
<span v-if="lines.length === 0" class="hint"> (点击屏幕继续 / Click screen to continue)</span>
|
||||
<span v-else class="hint blink-hint"> ⏎ </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Explanation Bar -->
|
||||
<div class="explanation-bar" :class="{ visible: currentExplanation }">
|
||||
<span class="icon">💡</span>
|
||||
<span class="text">{{ currentExplanation }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const currentOS = ref('win-cmd')
|
||||
const isTyping = ref(false)
|
||||
const lines = ref([])
|
||||
const currentExplanation = ref('')
|
||||
const currentStepIndex = ref(-1)
|
||||
|
||||
const osList = [
|
||||
{ id: 'win-cmd', name: 'Windows CMD', icon: '🪟' },
|
||||
{ id: 'win-ps', name: 'Windows PowerShell', icon: '⚡' },
|
||||
{ id: 'linux', name: 'Linux Terminal', icon: '🐧' }
|
||||
]
|
||||
|
||||
const configs = {
|
||||
'win-cmd': {
|
||||
title: 'Command Prompt',
|
||||
prompt: 'C:\\Users\\User>',
|
||||
demo: [
|
||||
{ type: 'explanation', content: '准备输入命令...' },
|
||||
{ type: 'command', content: 'dir', delay: 400, explanation: '输入 `dir` (Directory)。这是 Windows 系统用来**列出当前文件夹内容**的命令。' },
|
||||
{ type: 'output', content: ' Volume in drive C has no label.', delay: 100, explanation: '系统正在执行命令...' },
|
||||
{ type: 'output', content: ' Volume Serial Number is A1B2-C3D4', delay: 50 },
|
||||
{ type: 'output', content: '', delay: 50 },
|
||||
{ type: 'output', content: ' Directory of C:\\Users\\User', delay: 50 },
|
||||
{ type: 'output', content: '', delay: 50 },
|
||||
{ type: 'output', content: '01/15/2026 10:00 AM <DIR> .', delay: 50 },
|
||||
{ type: 'output', content: '01/15/2026 10:00 AM <DIR> ..', delay: 50 },
|
||||
{ type: 'output', content: '01/15/2026 10:00 AM 128 demo.txt', delay: 50 },
|
||||
{ type: 'output', content: ' 1 File(s) 128 bytes', delay: 50 },
|
||||
{ type: 'output', content: ' 2 Dir(s) 50,000,000,000 bytes free', delay: 50, explanation: '系统返回了文件列表。`<DIR>` 表示这是一个文件夹,数字表示文件大小。' },
|
||||
{ type: 'output', content: '', delay: 100 }
|
||||
]
|
||||
},
|
||||
'win-ps': {
|
||||
title: 'Windows PowerShell',
|
||||
prompt: 'PS C:\\Users\\User>',
|
||||
demo: [
|
||||
{ type: 'explanation', content: '准备输入命令...' },
|
||||
{ type: 'command', content: 'Get-Date', delay: 400, explanation: '输入 `Get-Date`。PowerShell 使用动词-名词的命名方式,这里是**获取当前时间**。' },
|
||||
{ type: 'output', content: '', delay: 100, explanation: '系统返回了当前的日期和时间。' },
|
||||
{ type: 'output', content: 'Thursday, January 15, 2026 10:00:00 AM', delay: 100 },
|
||||
{ type: 'output', content: '', delay: 100 },
|
||||
{ type: 'command', content: 'echo "Hello World"', delay: 400, explanation: '输入 `echo`。这是让计算机**复读**你说的话,常用于测试或打印信息。' },
|
||||
{ type: 'output', content: 'Hello World', delay: 100, explanation: '计算机乖乖地输出了 "Hello World"。' }
|
||||
]
|
||||
},
|
||||
'linux': {
|
||||
title: 'user@hostname: ~',
|
||||
prompt: 'user@hostname:~$ ',
|
||||
demo: [
|
||||
{ type: 'explanation', content: '准备输入命令...' },
|
||||
{ type: 'command', content: 'ls -la', delay: 400, explanation: '输入 `ls` (List)。这是 Linux/Mac 系统用来**列出文件**的命令。`-la` 是参数,表示“列出所有文件(all)的详细信息(long)”。' },
|
||||
{ type: 'output', content: 'total 8', delay: 100, explanation: '系统返回了文件列表。左边的 `drwxr-xr-x` 看起来像乱码,其实是**权限描述**(谁能读、谁能写)。' },
|
||||
{ type: 'output', content: 'drwxr-xr-x 2 user user 4096 Jan 15 10:00 .', delay: 50 },
|
||||
{ type: 'output', content: 'drwxr-xr-x 3 user user 4096 Jan 15 10:00 ..', delay: 50 },
|
||||
{ type: 'output', content: '-rw-r--r-- 1 user user 128 Jan 15 10:00 demo.txt', delay: 50 },
|
||||
{ type: 'command', content: 'whoami', delay: 400, explanation: '输入 `whoami` (Who am I)。这是一个经典的哲学命令(笑),告诉计算机:**我是谁?**(当前登录用户)。' },
|
||||
{ type: 'output', content: 'user', delay: 100, explanation: '系统回答:你是 "user"。' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const currentOSConfig = computed(() => configs[currentOS.value])
|
||||
const isFinished = computed(() => currentOSConfig.value && currentStepIndex.value >= currentOSConfig.value.demo.length - 1)
|
||||
|
||||
const switchOS = (id) => {
|
||||
currentOS.value = id
|
||||
resetDemo()
|
||||
}
|
||||
|
||||
const resetDemo = () => {
|
||||
lines.value = []
|
||||
currentExplanation.value = ''
|
||||
currentStepIndex.value = -1
|
||||
isTyping.value = false
|
||||
// Add initial prompt
|
||||
lines.value.push({ type: 'input', prompt: currentOSConfig.value.prompt, content: '' })
|
||||
}
|
||||
|
||||
// Initial reset
|
||||
watch(currentOSConfig, resetDemo, { immediate: true })
|
||||
|
||||
const nextStep = async () => {
|
||||
if (isTyping.value || isFinished.value) return
|
||||
|
||||
const demoLines = currentOSConfig.value.demo
|
||||
const promptText = currentOSConfig.value.prompt
|
||||
|
||||
// Loop to process consecutive output lines or until a pause point
|
||||
while (currentStepIndex.value < demoLines.length - 1) {
|
||||
currentStepIndex.value++
|
||||
const step = demoLines[currentStepIndex.value]
|
||||
|
||||
// 1. Update Explanation if exists
|
||||
if (step.explanation) {
|
||||
currentExplanation.value = step.explanation
|
||||
}
|
||||
|
||||
// 2. Handle specific types
|
||||
if (step.type === 'explanation') {
|
||||
// Just show explanation and pause
|
||||
break
|
||||
}
|
||||
|
||||
if (step.type === 'command') {
|
||||
// Ensure input line exists
|
||||
if (lines.value.length === 0 || lines.value[lines.value.length - 1].type !== 'input') {
|
||||
lines.value.push({ type: 'input', prompt: promptText, content: '' })
|
||||
}
|
||||
|
||||
// Type effect
|
||||
isTyping.value = true
|
||||
const text = step.content
|
||||
const targetLine = lines.value[lines.value.length - 1]
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
targetLine.content += text[i]
|
||||
await new Promise(r => setTimeout(r, 30 + Math.random() * 40))
|
||||
}
|
||||
isTyping.value = false
|
||||
|
||||
// Pause after typing command
|
||||
break
|
||||
}
|
||||
|
||||
if (step.type === 'output') {
|
||||
lines.value.push({ type: 'output', content: step.content })
|
||||
|
||||
// Logic to continue or pause:
|
||||
// Pause if:
|
||||
// - This output has an explanation (user needs to read)
|
||||
// - Next step is NOT output (it's a command or explanation block)
|
||||
// - Next step is output BUT has an explanation
|
||||
|
||||
if (step.explanation) {
|
||||
break
|
||||
}
|
||||
|
||||
const nextStep = demoLines[currentStepIndex.value + 1]
|
||||
if (!nextStep || nextStep.type !== 'output' || nextStep.explanation) {
|
||||
// If next is command, we might want to show a prompt before pausing?
|
||||
// But the command step logic adds prompt.
|
||||
// If we pause here, the user sees output. Next click -> types command.
|
||||
// Seems correct.
|
||||
break
|
||||
}
|
||||
|
||||
// Small delay between batched outputs for visual smoothness
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
}
|
||||
}
|
||||
|
||||
// If we finished everything, add a final prompt
|
||||
if (isFinished.value) {
|
||||
lines.value.push({ type: 'input', prompt: promptText, content: '' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terminal-os-demo {
|
||||
margin: 24px 0;
|
||||
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.os-switch {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.os-switch button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.os-switch button:hover {
|
||||
background: var(--vp-c-bg-mute);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.os-switch button.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.3);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
/* Windows CMD Style */
|
||||
.terminal-window.win-cmd {
|
||||
background: #0c0c0c;
|
||||
color: #cccccc;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
.terminal-window.win-cmd .window-bar {
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
color: #000;
|
||||
}
|
||||
.terminal-window.win-cmd .window-title {
|
||||
color: #000;
|
||||
font-weight: normal;
|
||||
}
|
||||
.terminal-window.win-cmd .window-buttons .btn {
|
||||
background: #ccc;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* PowerShell Style */
|
||||
.terminal-window.win-ps {
|
||||
background: #012456;
|
||||
color: #ffffff;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
.terminal-window.win-ps .window-bar {
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
color: #000;
|
||||
}
|
||||
.terminal-window.win-ps .window-title {
|
||||
color: #000;
|
||||
}
|
||||
.terminal-window.win-ps .window-buttons .btn {
|
||||
background: #ccc;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Linux Style */
|
||||
.terminal-window.linux {
|
||||
background: #2b2b2b;
|
||||
color: #f0f0f0;
|
||||
font-family: 'Ubuntu Mono', monospace;
|
||||
}
|
||||
.terminal-window.linux .window-bar {
|
||||
background: #3e3e3e;
|
||||
border-bottom: 1px solid #222;
|
||||
color: #ccc;
|
||||
}
|
||||
.terminal-window.linux .window-buttons .btn {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.terminal-window.linux .window-buttons .close { background: #ff5f56; }
|
||||
.terminal-window.linux .window-buttons .minimize { background: #ffbd2e; }
|
||||
.terminal-window.linux .window-buttons .maximize { background: #27c93f; }
|
||||
|
||||
/* Common Layout */
|
||||
.window-bar {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.window-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: transparent;
|
||||
border: 1px solid currentColor;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
height: 22px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.control-btn:hover:not(:disabled) {
|
||||
opacity: 1;
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.control-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.control-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.control-btn.primary:hover:not(:disabled) {
|
||||
background: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
line-height: 36px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.terminal-content {
|
||||
padding: 16px;
|
||||
min-height: 240px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
text-align: left;
|
||||
transition: background-color 0.2s;
|
||||
position: relative; /* For overlay */
|
||||
}
|
||||
|
||||
.terminal-content.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.start-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.start-hint {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.completed-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
animation: fade-in 0.5s;
|
||||
}
|
||||
|
||||
.completed-hint {
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.completed-hint:hover {
|
||||
transform: scale(1.05);
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.terminal-content.clickable:hover {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.blink-hint {
|
||||
animation: blink 1s step-end infinite;
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.line {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Linux Prompt Colors */
|
||||
.terminal-window.linux .prompt {
|
||||
color: #87d700;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 1.2em;
|
||||
background-color: currentColor;
|
||||
vertical-align: text-bottom;
|
||||
animation: blink 1s step-end infinite;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* If last line input, show cursor there */
|
||||
.line:last-child .cmd-text::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 1.2em;
|
||||
background-color: currentColor;
|
||||
vertical-align: text-bottom;
|
||||
animation: blink 1s step-end infinite;
|
||||
opacity: 0.7;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* Only show cursor on the very last line if it is input type and we are animating OR we are idle (lines=0) */
|
||||
/* Actually, simpler: */
|
||||
.input-line .cursor {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Hide the pseudo-element cursor if we are not on the last line or if it is output */
|
||||
.line:not(:last-child) .cmd-text::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Also if the last line is output, no cursor */
|
||||
.line:last-child:not(:has(.prompt)) .cmd-text::after {
|
||||
/* This selector is tricky. Let's rely on v-if logic in template if possible,
|
||||
but since we iterate lines, I added a cursor logic in CSS.
|
||||
Let's adjust template to be explicit about cursor.
|
||||
*/
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.hint {
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.explanation-bar {
|
||||
background: #fff;
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: all 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.explanation-bar.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.explanation-bar .icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.explanation-bar .text {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.terminal-window.win-cmd .explanation-bar,
|
||||
.terminal-window.win-ps .explanation-bar {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
border-top-color: #ccc;
|
||||
}
|
||||
|
||||
.terminal-window.linux .explanation-bar {
|
||||
background: #222;
|
||||
color: #ccc;
|
||||
border-top-color: #444;
|
||||
}
|
||||
</style>
|
||||
@@ -17,9 +17,14 @@ import EscapeSequences from './components/appendix/terminal-intro/EscapeSequence
|
||||
import InputVisualizer from './components/appendix/terminal-intro/InputVisualizer.vue'
|
||||
import SignalsDemo from './components/appendix/terminal-intro/SignalsDemo.vue'
|
||||
import FlowDiagram from './components/appendix/terminal-intro/FlowDiagram.vue'
|
||||
import BufferSwitchDemo from './components/appendix/terminal-intro/BufferSwitchDemo.vue'
|
||||
import AdvancedTUIDemo from './components/appendix/terminal-intro/AdvancedTUIDemo.vue'
|
||||
import ArchitectureDemo from './components/appendix/terminal-intro/ArchitectureDemo.vue'
|
||||
import TerminalDefinition from './components/appendix/terminal-intro/TerminalDefinition.vue'
|
||||
import TerminalOSDemo from './components/appendix/terminal-intro/TerminalOSDemo.vue'
|
||||
|
||||
import EscapeParserDemo from './components/appendix/terminal-intro/EscapeParserDemo.vue'
|
||||
import CookedRawDemo from './components/appendix/terminal-intro/CookedRawDemo.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
@@ -32,12 +37,16 @@ export default {
|
||||
app.component('TerminalGrid', TerminalGrid)
|
||||
app.component('CellInspector', CellInspector)
|
||||
app.component('EscapeSequences', EscapeSequences)
|
||||
app.component('EscapeParserDemo', EscapeParserDemo)
|
||||
app.component('CookedRawDemo', CookedRawDemo)
|
||||
app.component('InputVisualizer', InputVisualizer)
|
||||
app.component('SignalsDemo', SignalsDemo)
|
||||
app.component('FlowDiagram', FlowDiagram)
|
||||
app.component('BufferSwitchDemo', BufferSwitchDemo)
|
||||
app.component('AdvancedTUIDemo', AdvancedTUIDemo)
|
||||
app.component('ArchitectureDemo', ArchitectureDemo)
|
||||
app.component('TerminalDefinition', TerminalDefinition)
|
||||
app.component('TerminalOSDemo', TerminalOSDemo)
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
## 1. 概念界定:终端是什么? (Definition)
|
||||
|
||||
*不同操作系统下的终端长相不同,**命令方式也不同**。点击下方按钮切换查看,注意观察 Windows 和 Linux 是如何用不同的命令(如 `dir` vs `ls`)做同一件事的:*
|
||||
|
||||
<TerminalOSDemo />
|
||||
|
||||
在图形用户界面(GUI)普及之前,终端是人类与计算机交互的主要方式。即便在今天,它依然是开发者控制计算机最精确、最高效的工具。
|
||||
|
||||
<TerminalDefinition />
|
||||
@@ -31,25 +35,28 @@
|
||||
|
||||
它不处理复杂的图形、图片或视频,而是专注于**文本信息的交互**。
|
||||
|
||||
## 2. 核心架构:终端与 Shell (The Architecture)
|
||||
## 2. 核心架构:三者关系大白话 (The Big Picture)
|
||||
|
||||
初学者常混淆“终端”与“Shell”,理解两者的区别是掌握命令行的关键。它们在计算机历史中演化为两个独立的组件:
|
||||
别被专业术语吓跑,其实它们就是三个分工明确的“打工人”:
|
||||
|
||||
- **终端 (Terminal)**:负责“交互”。
|
||||
它是一个负责显示字符、接收键盘输入的**窗口程序**。它本身不懂任何逻辑,只负责传输数据。
|
||||
*常见的终端软件:macOS Terminal, iTerm2, Windows Terminal, Hyper.*
|
||||
- **终端 (Terminal) —— 只是个“传声筒”**
|
||||
* 它只负责**显示画面**和**接收按键**。
|
||||
* 它本身**没有任何智能**,就像一个显示器或键盘。
|
||||
* *它不管你输入的是命令还是乱码,只管把字显示出来。*
|
||||
|
||||
- **Shell (壳 / 命令解释器)**:负责“逻辑”。
|
||||
它是运行在终端内部的**后端程序**,负责接收你的指令、解析语义、调用系统内核并返回结果。
|
||||
*常见的 Shell:bash, zsh, fish, sh.*
|
||||
- **Shell (壳) —— 真正的“翻译官”**
|
||||
* 它才是有逻辑的程序。
|
||||
* 它负责**听懂**你的命令(比如 `ls`),把它**翻译**成电脑能听懂的指令,然后指挥内核去干活。
|
||||
* *就像 Siri 或小爱同学,听懂你的话,然后去调动手机功能。*
|
||||
|
||||
**类比理解**:
|
||||
如果把计算机操作比作去餐厅点餐:
|
||||
- **终端**是**餐桌和菜单**(负责展示信息、提供输入界面,但它自己不会做菜)。
|
||||
- **Shell**是**服务员**(负责听取你的点单,交给后厨处理,并把结果端回来)。
|
||||
- **内核**是**后厨**(负责真正的执行和运算)。
|
||||
- **内核 (Kernel) —— 幕后的“大管家”**
|
||||
* 它是操作系统的核心,只有它能直接控制硬件(硬盘、CPU)。
|
||||
* **Shell 不包含内核**,Shell 只是站在门口喊话的人,内核才是屋里干活的人。
|
||||
|
||||
*下面的演示直观展示了从“点餐”到“上菜”的全过程:*
|
||||
**一句话总结流程**:
|
||||
你在**终端**(窗口)打字 ➡️ **Shell**(翻译官)听懂并指挥 ➡️ **内核**(大管家)去硬件里干活。
|
||||
|
||||
*下面的演示展示了这个过程,注意看 Shell 和内核之间那道“墙”:*
|
||||
|
||||
<ArchitectureDemo />
|
||||
|
||||
@@ -88,9 +95,13 @@
|
||||
- 序列 `\033[31m` → **指令**:将后续文字颜色设为红色。
|
||||
- 序列 `\033[2J` → **指令**:清空屏幕。
|
||||
|
||||
这就是为什么有时候在日志文件中会看到乱码(如 `^[[31m`),那其实是未被正确解析的颜色指令。
|
||||
这就好比你和朋友约定:如果我正常说话,你就记录下来;如果我举起左手(相当于 `ESC`),接下来的那句话就是命令而不是内容。
|
||||
|
||||
*下方组件展示了转义序列如何实时改变终端的渲染状态:*
|
||||
*点击下方的“播放”按钮,观察终端是如何逐个处理字符流,并识别出隐藏的指令:*
|
||||
|
||||
<EscapeParserDemo />
|
||||
|
||||
*下方组件则展示了更多种类的转义序列及其渲染效果:*
|
||||
|
||||
<EscapeSequences />
|
||||
|
||||
@@ -110,21 +121,25 @@
|
||||
|
||||
<InputVisualizer />
|
||||
|
||||
## 6. 运行模式:加工与原始 (Cooked vs. Raw Mode)
|
||||
## 6. 运行模式:打字机 vs 游戏机 (Cooked vs. Raw Mode)
|
||||
|
||||
终端有两种主要的工作模式,决定了输入数据如何被处理。理解这一点能帮你明白为什么 `ls` 命令和 `vim` 编辑器的操作体验完全不同。
|
||||
终端有两种截然不同的性格。理解这一点,你就能明白为什么在终端里**打命令**和**玩贪吃蛇**是完全不同的体验。
|
||||
|
||||
- **加工模式 (Cooked Mode / Canonical Mode)**:
|
||||
这是标准 Shell 的默认模式。
|
||||
- **行为**:终端会**缓存**你的输入,允许你使用退格键修改。只有当你按下回车键(Enter)后,整行数据才会一次性发送给程序。
|
||||
- **类比**:像在写信,写完一整句确认无误后才寄出。
|
||||
- *适用场景:日常命令输入。*
|
||||
- **加工模式 (Cooked Mode) —— 像打字机**
|
||||
* 这是默认模式。
|
||||
* **行为**:你输入的字符会被终端**暂时扣留**,直到你按下回车键(Enter)。
|
||||
* **好处**:这给了你修改的机会。打错了?按退格键(Backspace)删掉重写,程序根本不知道你之前打错过。
|
||||
* *适用场景:平时敲命令(如 `ls`, `cd`)。*
|
||||
|
||||
- **原始模式 (Raw Mode)**:
|
||||
这是高级交互程序的模式。
|
||||
- **行为**:终端不进行任何缓冲或处理,将每一个按键(包括修饰键)**即时**发送给程序。
|
||||
- **类比**:像打电玩,按下一个键,角色立刻做出反应。
|
||||
- *适用场景:Vim 编辑器、终端游戏、交互式菜单。*
|
||||
- **原始模式 (Raw Mode) —— 像游戏手柄**
|
||||
* 这是“高手”模式。
|
||||
* **行为**:你按下的每一个键(包括方向键、Ctrl组合键),都会**瞬间**发送给程序,没有任何缓冲。
|
||||
* **好处**:程序能实时响应你的操作。
|
||||
* *适用场景:玩终端游戏(如贪吃蛇)、使用 Vim 编辑器(一种纯键盘操作的编辑器)。*
|
||||
|
||||
*点击下方按钮切换模式,体验“写信”与“打游戏”的不同手感:*
|
||||
|
||||
<CookedRawDemo />
|
||||
|
||||
## 7. 进程控制:信号 (Signals)
|
||||
|
||||
@@ -140,15 +155,24 @@
|
||||
|
||||
## 8. 高级应用:全屏界面与缓冲区 (Buffers & TUI)
|
||||
|
||||
现代终端应用(TUI, Text User Interface)如 `vim`、`htop` 或 `tmux`,能够像图形软件一样利用整个屏幕,这得益于**备用屏幕缓冲区 (Alternate Screen Buffer)**。
|
||||
你有没有发现,当你用 `vim` 编辑文件或者用 `htop` 看系统状态时,它们会占满整个屏幕?而当你退出它们时,屏幕瞬间变回了原来的样子,之前的命令记录完全没变。
|
||||
|
||||
终端通常维护两块“画布”:
|
||||
1. **主缓冲区 (Primary Buffer)**:保存命令历史,即我们日常滚动查看的流式界面。
|
||||
2. **备用缓冲区 (Alternate Buffer)**:一块独立的、不保存历史的画布,供全屏应用使用。
|
||||
这是因为终端有两块“画布”在来回切换:
|
||||
|
||||
当你打开 `vim` 时,终端切换到备用缓冲区;当你退出 `vim` 时,终端切回主缓冲区。这就是为什么退出编辑器后,之前的命令历史依然完好无损地留在屏幕上。
|
||||
* **主缓冲区 (Primary Buffer)**:就像**草稿本**。
|
||||
* 你写一行,系统回一行。
|
||||
* 写满了就翻页(滚动),以前写的东西都在上面。
|
||||
* *用于:日常敲命令。*
|
||||
|
||||
<AdvancedTUIDemo />
|
||||
* **备用缓冲区 (Alternate Buffer)**:就像**黑板**。
|
||||
* 程序把黑板擦干净,在上面画画(全屏显示)。
|
||||
* 不管怎么画,都不会影响你桌子上的草稿本。
|
||||
* 当你退出程序时,就像把黑板收起来,你又回到了草稿本面前。
|
||||
* *用于:Vim, Nano, 游戏等全屏软件。*
|
||||
|
||||
*点击下方按钮,体验“草稿本”和“黑板”是如何瞬间切换的:*
|
||||
|
||||
<BufferSwitchDemo />
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user