feat: comprehensive documentation and demo updates
- Update READMEs and docs across multiple languages - Enhance interactive demos for Agent, LLM, VLM, Audio, Image Gen, Terminal, and Web Basics - Add new appendix sections for Database and IDE intros - Update VitePress config, theme, and utility scripts - Clean up unused assets and components
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
<div class="tab">Logging</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tui-body">
|
||||
<div class="sidebar" :style="{ width: sidebarWidth + '%' }">
|
||||
<div class="list-item success">
|
||||
@@ -39,13 +39,15 @@
|
||||
<span class="icon">○</span> ci-db-migration
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="main-content">
|
||||
<div class="content-header">
|
||||
<h3>ci-email-service</h3>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<div class="status-row">Status: <span class="text-success">success</span></div>
|
||||
<div class="status-row">
|
||||
Status: <span class="text-success">success</span>
|
||||
</div>
|
||||
<div class="info-row">Updated: 2024-01-15 14:32:00 UTC</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,12 +84,12 @@ const simulateResize = () => {
|
||||
const originalWidth = sidebarWidth.value
|
||||
sidebarWidth.value = 20
|
||||
sizeDisplay.value = '40x20'
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
sidebarWidth.value = 40
|
||||
sizeDisplay.value = '80x20'
|
||||
}, 500)
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
sidebarWidth.value = originalWidth
|
||||
sizeDisplay.value = '60x20'
|
||||
@@ -169,10 +171,18 @@ const simulateResize = () => {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.success .icon { color: #22c55e; }
|
||||
.warning .icon { color: #eab308; }
|
||||
.error .icon { color: #ef4444; }
|
||||
.pending .icon { color: #666; }
|
||||
.success .icon {
|
||||
color: #22c55e;
|
||||
}
|
||||
.warning .icon {
|
||||
color: #eab308;
|
||||
}
|
||||
.error .icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
.pending .icon {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
@@ -190,7 +200,9 @@ const simulateResize = () => {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.text-success { color: #22c55e; }
|
||||
.text-success {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
color: #666;
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container" @click="nextStep" :class="{ 'clickable': currentStep < totalSteps }">
|
||||
<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">
|
||||
@@ -53,7 +57,7 @@
|
||||
<span class="text">演示结束,点击重置 / Finished (Reset)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Spaces Background -->
|
||||
<div class="spaces-bg">
|
||||
<div class="space user-space">
|
||||
@@ -71,7 +75,9 @@
|
||||
<div class="node terminal" :class="{ active: activeNode === 'terminal' }">
|
||||
<div class="node-title">TERMINAL (终端)</div>
|
||||
<div class="screen">
|
||||
<div v-for="(line, i) in terminalLines" :key="i" class="line">{{ line }}</div>
|
||||
<div v-for="(line, i) in terminalLines" :key="i" class="line">
|
||||
{{ line }}
|
||||
</div>
|
||||
<div class="line input-line">
|
||||
<span class="prompt">$</span>
|
||||
<span class="typing">{{ currentInput }}</span>
|
||||
@@ -82,7 +88,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Connections -->
|
||||
<div class="connection t-s" :class="{ active: packetState === 't-to-s' || packetState === 's-to-t' }">
|
||||
<div
|
||||
class="connection t-s"
|
||||
:class="{
|
||||
active: packetState === 't-to-s' || packetState === 's-to-t'
|
||||
}"
|
||||
>
|
||||
<div class="line-path"></div>
|
||||
<div class="data-label" v-if="packetState === 't-to-s'">
|
||||
<span class="icon">➡️</span> Bytes
|
||||
@@ -104,7 +115,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Connections -->
|
||||
<div class="connection s-k" :class="{ active: packetState === 's-to-k' || packetState === 'k-to-s' }">
|
||||
<div
|
||||
class="connection s-k"
|
||||
:class="{
|
||||
active: packetState === 's-to-k' || packetState === 'k-to-s'
|
||||
}"
|
||||
>
|
||||
<div class="line-path"></div>
|
||||
<div class="data-label" v-if="packetState === 's-to-k'">
|
||||
<span class="icon">➡️</span> Syscall
|
||||
@@ -128,20 +144,26 @@
|
||||
|
||||
<div class="controls">
|
||||
<div class="btn-group">
|
||||
<button class="btn primary" @click="nextStep" :disabled="currentStep >= totalSteps">
|
||||
<button
|
||||
class="btn primary"
|
||||
@click="nextStep"
|
||||
:disabled="currentStep >= totalSteps"
|
||||
>
|
||||
<span v-if="currentStep === 0">▶️ Start Simulation / 开始演示</span>
|
||||
<span v-else-if="currentStep < totalSteps">Next Step / 下一步 ({{ currentStep }}/{{ totalSteps }}) ➡️</span>
|
||||
<span v-else-if="currentStep < totalSteps"
|
||||
>Next Step / 下一步 ({{ currentStep }}/{{ totalSteps }}) ➡️</span
|
||||
>
|
||||
<span v-else>✅ Done / 完成 (Reset)</span>
|
||||
</button>
|
||||
<button class="btn secondary" @click="reset" v-if="currentStep > 0">
|
||||
Reset / 重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="step-info" v-if="currentStep > 0">
|
||||
<div class="step-title">
|
||||
{{ steps[currentStep - 1].titleEn }}
|
||||
<span class="divider">|</span>
|
||||
{{ steps[currentStep - 1].titleEn }}
|
||||
<span class="divider">|</span>
|
||||
{{ steps[currentStep - 1].titleZh }}
|
||||
</div>
|
||||
<div class="step-desc">
|
||||
@@ -158,7 +180,10 @@
|
||||
</div>
|
||||
<div class="step-info placeholder" v-else>
|
||||
<div class="step-desc">
|
||||
<div class="en">Click "Start Simulation" to see how the command 'ls' travels through the system.</div>
|
||||
<div class="en">
|
||||
Click "Start Simulation" to see how the command 'ls' travels through
|
||||
the system.
|
||||
</div>
|
||||
<div class="zh">点击“开始演示”查看 'ls' 命令如何在系统中流转。</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,8 +195,8 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const currentStep = ref(0)
|
||||
const activeNode = ref('terminal')
|
||||
const packetState = ref(null)
|
||||
const activeNode = ref('terminal')
|
||||
const packetState = ref(null)
|
||||
const terminalLines = ref([])
|
||||
const currentInput = ref('')
|
||||
const shellStatus = ref('Idle')
|
||||
@@ -181,12 +206,13 @@ const kernelIcon = ref('💤')
|
||||
|
||||
const steps = [
|
||||
{
|
||||
titleEn: "1. User Input",
|
||||
titleZh: "1. 用户输入",
|
||||
descEn: "You type 'ls' in the terminal window. The terminal captures your keystrokes.",
|
||||
titleEn: '1. User Input',
|
||||
titleZh: '1. 用户输入',
|
||||
descEn:
|
||||
"You type 'ls' in the terminal window. The terminal captures your keystrokes.",
|
||||
descZh: "你在终端窗口输入 'ls'。终端会捕获你的按键操作。",
|
||||
techEn: "Terminal buffers input in 'Cooked Mode' until you press Enter.",
|
||||
techZh: "终端在“加工模式 (Cooked Mode)”下缓冲输入,直到你按下回车键。",
|
||||
techZh: '终端在“加工模式 (Cooked Mode)”下缓冲输入,直到你按下回车键。',
|
||||
action: async () => {
|
||||
activeNode.value = 'terminal'
|
||||
currentInput.value = 'l'
|
||||
@@ -195,12 +221,13 @@ const steps = [
|
||||
}
|
||||
},
|
||||
{
|
||||
titleEn: "2. Transmission",
|
||||
titleZh: "2. 传输",
|
||||
descEn: "The Terminal sends the characters 'l', 's', and 'Enter' to the Shell.",
|
||||
titleEn: '2. Transmission',
|
||||
titleZh: '2. 传输',
|
||||
descEn:
|
||||
"The Terminal sends the characters 'l', 's', and 'Enter' to the Shell.",
|
||||
descZh: "终端将字符 'l'、's' 和 '回车' 发送给 Shell。",
|
||||
techEn: "Data travels via standard input (stdin) as a byte stream.",
|
||||
techZh: "数据通过标准输入 (stdin) 以字节流的形式传输。",
|
||||
techEn: 'Data travels via standard input (stdin) as a byte stream.',
|
||||
techZh: '数据通过标准输入 (stdin) 以字节流的形式传输。',
|
||||
action: async () => {
|
||||
packetState.value = 't-to-s'
|
||||
await wait(1000)
|
||||
@@ -208,10 +235,10 @@ const steps = [
|
||||
}
|
||||
},
|
||||
{
|
||||
titleEn: "3. Shell Parsing",
|
||||
titleZh: "3. Shell 解析",
|
||||
descEn: "The Shell (Waiter) translates your command for the Kernel.",
|
||||
descZh: "Shell(服务员)接收指令,并将其翻译成内核能听懂的请求。",
|
||||
titleEn: '3. Shell Parsing',
|
||||
titleZh: '3. 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 () => {
|
||||
@@ -221,12 +248,12 @@ const steps = [
|
||||
}
|
||||
},
|
||||
{
|
||||
titleEn: "4. System Call",
|
||||
titleZh: "4. 系统调用",
|
||||
descEn: "The Shell asks the Kernel to read the file list from the disk.",
|
||||
descZh: "Shell 请求内核从磁盘读取文件列表。",
|
||||
techEn: "Shell invokes `execve()` and `getdents()` system calls.",
|
||||
techZh: "Shell 调用 `execve()` 和 `getdents()` 等系统调用。",
|
||||
titleEn: '4. System Call',
|
||||
titleZh: '4. 系统调用',
|
||||
descEn: 'The Shell asks the Kernel to read the file list from the disk.',
|
||||
descZh: 'Shell 请求内核从磁盘读取文件列表。',
|
||||
techEn: 'Shell invokes `execve()` and `getdents()` system calls.',
|
||||
techZh: 'Shell 调用 `execve()` 和 `getdents()` 等系统调用。',
|
||||
action: async () => {
|
||||
packetState.value = 's-to-k'
|
||||
await wait(1000)
|
||||
@@ -234,12 +261,12 @@ const steps = [
|
||||
}
|
||||
},
|
||||
{
|
||||
titleEn: "5. Kernel Execution",
|
||||
titleZh: "5. 内核执行",
|
||||
descEn: "The Kernel (Kitchen) executes the request by accessing hardware.",
|
||||
descZh: "内核(后厨)直接操作硬件(如磁盘)来执行实际任务。",
|
||||
techEn: "Kernel driver accesses the file system (APFS/ext4).",
|
||||
techZh: "内核驱动程序访问文件系统 (APFS/ext4)。",
|
||||
titleEn: '5. Kernel Execution',
|
||||
titleZh: '5. 内核执行',
|
||||
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 () => {
|
||||
activeNode.value = 'kernel'
|
||||
kernelIcon.value = '💾'
|
||||
@@ -249,12 +276,12 @@ const steps = [
|
||||
}
|
||||
},
|
||||
{
|
||||
titleEn: "6. Returning Data",
|
||||
titleZh: "6. 返回数据",
|
||||
descEn: "The Kernel gives the raw file list back to the Shell.",
|
||||
descZh: "内核将原始文件列表数据返回给 Shell。",
|
||||
techEn: "System call returns with file descriptors/structs.",
|
||||
techZh: "系统调用返回文件描述符或结构体数据。",
|
||||
titleEn: '6. Returning Data',
|
||||
titleZh: '6. 返回数据',
|
||||
descEn: 'The Kernel gives the raw file list back to the Shell.',
|
||||
descZh: '内核将原始文件列表数据返回给 Shell。',
|
||||
techEn: 'System call returns with file descriptors/structs.',
|
||||
techZh: '系统调用返回文件描述符或结构体数据。',
|
||||
action: async () => {
|
||||
kernelStatus.value = 'Idle'
|
||||
kernelIcon.value = '💤'
|
||||
@@ -264,12 +291,13 @@ const steps = [
|
||||
}
|
||||
},
|
||||
{
|
||||
titleEn: "7. Formatting",
|
||||
titleZh: "7. 格式化",
|
||||
descEn: "The Shell formats the raw list into text, adding colors if needed.",
|
||||
descZh: "Shell 将原始列表格式化为文本,并根据需要添加颜色。",
|
||||
techEn: "Shell formats output buffer, adding ANSI color codes.",
|
||||
techZh: "Shell 格式化输出缓冲区,并添加 ANSI 颜色代码。",
|
||||
titleEn: '7. Formatting',
|
||||
titleZh: '7. 格式化',
|
||||
descEn:
|
||||
'The Shell formats the raw list into text, adding colors if needed.',
|
||||
descZh: 'Shell 将原始列表格式化为文本,并根据需要添加颜色。',
|
||||
techEn: 'Shell formats output buffer, adding ANSI color codes.',
|
||||
techZh: 'Shell 格式化输出缓冲区,并添加 ANSI 颜色代码。',
|
||||
action: async () => {
|
||||
activeNode.value = 'shell'
|
||||
shellIcon.value = '🎨'
|
||||
@@ -278,12 +306,12 @@ const steps = [
|
||||
}
|
||||
},
|
||||
{
|
||||
titleEn: "8. Display Output",
|
||||
titleZh: "8. 显示输出",
|
||||
descEn: "The Shell sends the final text back to the Terminal to show you.",
|
||||
descZh: "Shell 将最终文本发送回终端以供显示。",
|
||||
techEn: "Data travels via standard output (stdout) to the TTY.",
|
||||
techZh: "数据通过标准输出 (stdout) 传输到 TTY。",
|
||||
titleEn: '8. Display Output',
|
||||
titleZh: '8. 显示输出',
|
||||
descEn: 'The Shell sends the final text back to the Terminal to show you.',
|
||||
descZh: 'Shell 将最终文本发送回终端以供显示。',
|
||||
techEn: 'Data travels via standard output (stdout) to the TTY.',
|
||||
techZh: '数据通过标准输出 (stdout) 传输到 TTY。',
|
||||
action: async () => {
|
||||
shellStatus.value = 'Idle'
|
||||
shellIcon.value = '💤'
|
||||
@@ -293,12 +321,12 @@ const steps = [
|
||||
}
|
||||
},
|
||||
{
|
||||
titleEn: "9. Render",
|
||||
titleZh: "9. 渲染",
|
||||
descEn: "The Terminal draws the text on the screen grid.",
|
||||
descZh: "终端在屏幕网格上绘制文本。",
|
||||
techEn: "Terminal emulator renders glyphs into the frame buffer.",
|
||||
techZh: "终端模拟器将字形渲染到帧缓冲区中。",
|
||||
titleEn: '9. Render',
|
||||
titleZh: '9. 渲染',
|
||||
descEn: 'The Terminal draws the text on the screen grid.',
|
||||
descZh: '终端在屏幕网格上绘制文本。',
|
||||
techEn: 'Terminal emulator renders glyphs into the frame buffer.',
|
||||
techZh: '终端模拟器将字形渲染到帧缓冲区中。',
|
||||
action: async () => {
|
||||
activeNode.value = 'terminal'
|
||||
terminalLines.value = ['file1.txt photo.jpg', 'notes.md']
|
||||
@@ -307,17 +335,16 @@ const steps = [
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const totalSteps = steps.length
|
||||
|
||||
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const nextStep = async () => {
|
||||
if (currentStep.value >= totalSteps) {
|
||||
reset()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const step = steps[currentStep.value]
|
||||
currentStep.value++
|
||||
await step.action()
|
||||
@@ -446,9 +473,15 @@ const reset = () => {
|
||||
}
|
||||
|
||||
@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); }
|
||||
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 {
|
||||
@@ -512,16 +545,18 @@ const reset = () => {
|
||||
}
|
||||
|
||||
.user-space {
|
||||
flex: 2;
|
||||
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;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.user-space .space-header { color: #22d3ee; }
|
||||
.user-space .space-header {
|
||||
color: #22d3ee;
|
||||
}
|
||||
|
||||
.kernel-space {
|
||||
flex: 1;
|
||||
@@ -531,7 +566,9 @@ const reset = () => {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.kernel-space .space-header { color: #ef4444; }
|
||||
.kernel-space .space-header {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.barrier {
|
||||
width: 2px;
|
||||
@@ -556,8 +593,6 @@ const reset = () => {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.node {
|
||||
background: #18181b;
|
||||
border: 2px solid #27272a;
|
||||
@@ -573,7 +608,7 @@ const reset = () => {
|
||||
|
||||
/* Specific z-index for Shell to prevent it from covering barrier label */
|
||||
.node.shell {
|
||||
z-index: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.node.active {
|
||||
@@ -603,7 +638,8 @@ const reset = () => {
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.screen, .process-box {
|
||||
.screen,
|
||||
.process-box {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
@@ -693,18 +729,26 @@ const reset = () => {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10;
|
||||
animation: pop-in 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes pop-in {
|
||||
from { opacity: 0; transform: translate(-50%, 5px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
@@ -795,27 +839,33 @@ const reset = () => {
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.analogy-header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
.diagram-container {
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
|
||||
.connection {
|
||||
width: 2px;
|
||||
height: 50px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.conn-label {
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
@@ -824,25 +874,37 @@ const reset = () => {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.packet {
|
||||
top: 0;
|
||||
left: 10px;
|
||||
animation: travel-vertical 1s linear forwards;
|
||||
}
|
||||
|
||||
|
||||
.packet.reverse {
|
||||
animation: travel-vertical-back 1s linear forwards;
|
||||
}
|
||||
|
||||
|
||||
@keyframes travel-vertical {
|
||||
0% { top: 0; transform: translateY(0); }
|
||||
100% { top: 100%; transform: translateY(-100%); }
|
||||
0% {
|
||||
top: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
top: 100%;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes travel-vertical-back {
|
||||
0% { top: 100%; transform: translateY(-100%); }
|
||||
100% { top: 0; transform: translateY(0); }
|
||||
0% {
|
||||
top: 100%;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
100% {
|
||||
top: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -7,18 +7,31 @@
|
||||
<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">
|
||||
<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">
|
||||
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>
|
||||
<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>
|
||||
|
||||
@@ -30,12 +43,21 @@
|
||||
<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">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>
|
||||
@@ -55,14 +77,20 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -101,7 +129,7 @@ const quitVim = () => {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
@@ -118,9 +146,15 @@ const quitVim = () => {
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.red { background: #ff5f56; }
|
||||
.yellow { background: #ffbd2e; }
|
||||
.green { background: #27c93f; }
|
||||
.red {
|
||||
background: #ff5f56;
|
||||
}
|
||||
.yellow {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
.green {
|
||||
background: #27c93f;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
@@ -281,4 +315,4 @@ p {
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -17,26 +17,28 @@
|
||||
{{ char }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="controls-area">
|
||||
<div class="control-group">
|
||||
<label>CHARACTER</label>
|
||||
<div class="char-buttons">
|
||||
<button
|
||||
v-for="c in chars"
|
||||
:key="c"
|
||||
<button
|
||||
v-for="c in chars"
|
||||
:key="c"
|
||||
:class="{ active: char === c }"
|
||||
@click="char = c"
|
||||
>{{ c }}</button>
|
||||
>
|
||||
{{ c }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>FOREGROUND</label>
|
||||
<div class="color-palette">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
class="color-swatch"
|
||||
:style="{ backgroundColor: color }"
|
||||
:class="{ active: fgColor === color }"
|
||||
@@ -48,15 +50,23 @@
|
||||
<div class="control-group">
|
||||
<label>BACKGROUND</label>
|
||||
<div class="color-palette">
|
||||
<div
|
||||
<div
|
||||
class="color-swatch"
|
||||
:class="{ active: bgColor === 'transparent' }"
|
||||
style="background: linear-gradient(45deg, #222 25%, transparent 25%), linear-gradient(-45deg, #222 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #222 75%), linear-gradient(-45deg, transparent 75%, #222 75%); background-size: 10px 10px; background-color: #111;"
|
||||
style="
|
||||
background:
|
||||
linear-gradient(45deg, #222 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #222 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #222 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #222 75%);
|
||||
background-size: 10px 10px;
|
||||
background-color: #111;
|
||||
"
|
||||
@click="bgColor = 'transparent'"
|
||||
></div>
|
||||
<div
|
||||
v-for="color in bgColors"
|
||||
:key="color"
|
||||
<div
|
||||
v-for="color in bgColors"
|
||||
:key="color"
|
||||
class="color-swatch"
|
||||
:style="{ backgroundColor: color }"
|
||||
:class="{ active: bgColor === color }"
|
||||
@@ -69,11 +79,11 @@
|
||||
<label>ATTRIBUTES</label>
|
||||
<div class="toggles">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="isBold">
|
||||
<input type="checkbox" v-model="isBold" />
|
||||
<span>Bold</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="isUnderline">
|
||||
<input type="checkbox" v-model="isUnderline" />
|
||||
<span>Underline</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -85,13 +95,49 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const chars = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P']
|
||||
const chars = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P'
|
||||
]
|
||||
const colors = [
|
||||
'#ef4444', '#22c55e', '#eab308', '#3b82f6', '#a855f7', '#06b6d4', '#f3f4f6', '#6b7280',
|
||||
'#f87171', '#4ade80', '#facc15', '#60a5fa', '#c084fc', '#22d3ee', '#ffffff'
|
||||
'#ef4444',
|
||||
'#22c55e',
|
||||
'#eab308',
|
||||
'#3b82f6',
|
||||
'#a855f7',
|
||||
'#06b6d4',
|
||||
'#f3f4f6',
|
||||
'#6b7280',
|
||||
'#f87171',
|
||||
'#4ade80',
|
||||
'#facc15',
|
||||
'#60a5fa',
|
||||
'#c084fc',
|
||||
'#22d3ee',
|
||||
'#ffffff'
|
||||
]
|
||||
const bgColors = [
|
||||
'#000000', '#1f2937', '#111827', '#374151', '#1e3a8a', '#3f2c08', '#310b0b'
|
||||
'#000000',
|
||||
'#1f2937',
|
||||
'#111827',
|
||||
'#374151',
|
||||
'#1e3a8a',
|
||||
'#3f2c08',
|
||||
'#310b0b'
|
||||
]
|
||||
|
||||
const char = ref('A')
|
||||
@@ -208,7 +254,7 @@ const cellStyle = computed(() => ({
|
||||
.color-swatch.active {
|
||||
border-color: #fff;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.toggles {
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
<template>
|
||||
<div class="cooked-raw-demo">
|
||||
<div class="mode-switch">
|
||||
<button
|
||||
:class="{ active: mode === 'cooked' }"
|
||||
@click="setMode('cooked')"
|
||||
>
|
||||
<button :class="{ active: mode === 'cooked' }" @click="setMode('cooked')">
|
||||
🥘 Cooked Mode (Standard)
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: mode === 'raw' }"
|
||||
@click="setMode('raw')"
|
||||
>
|
||||
<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
|
||||
<input
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
type="text"
|
||||
class="hidden-input"
|
||||
@keydown="handleKey"
|
||||
@blur="isFocused = false"
|
||||
@@ -28,12 +22,13 @@
|
||||
|
||||
<!-- 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-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>
|
||||
@@ -42,7 +37,10 @@
|
||||
<div class="arrow">⬇</div>
|
||||
|
||||
<!-- 2. OS Buffer (Only for Cooked) -->
|
||||
<div class="stage buffer" :class="{ disabled: mode === 'raw', active: mode === '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>
|
||||
@@ -50,7 +48,9 @@
|
||||
</div>
|
||||
<div class="buffer-content">
|
||||
<template v-if="mode === 'cooked'">
|
||||
<span v-for="(char, i) in buffer" :key="i" class="char">{{ char }}</span>
|
||||
<span v-for="(char, i) in buffer" :key="i" class="char">{{
|
||||
char
|
||||
}}</span>
|
||||
<span class="cursor">_</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -58,7 +58,9 @@
|
||||
</template>
|
||||
</div>
|
||||
<div class="explanation">
|
||||
<span v-if="mode === 'cooked'">Waiting for Enter... (Backspace works)</span>
|
||||
<span v-if="mode === 'cooked'"
|
||||
>Waiting for Enter... (Backspace works)</span
|
||||
>
|
||||
<span v-else>No buffering. Every key is sent immediately.</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,7 +79,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,9 +111,9 @@ const focusInput = () => {
|
||||
|
||||
const handleKey = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
|
||||
const key = e.key
|
||||
|
||||
|
||||
// Visual feedback
|
||||
if (key === ' ') lastPressedKey.value = 'Space'
|
||||
else if (key === 'Enter') lastPressedKey.value = 'Enter'
|
||||
@@ -122,7 +123,16 @@ const handleKey = (e) => {
|
||||
|
||||
// Clear visual feedback after delay
|
||||
setTimeout(() => {
|
||||
if (lastPressedKey.value === (key === ' ' ? 'Space' : (key === 'Enter' ? 'Enter' : (key === 'Backspace' ? '⌫' : key)))) {
|
||||
if (
|
||||
lastPressedKey.value ===
|
||||
(key === ' '
|
||||
? 'Space'
|
||||
: key === 'Enter'
|
||||
? 'Enter'
|
||||
: key === 'Backspace'
|
||||
? '⌫'
|
||||
: key)
|
||||
) {
|
||||
// lastPressedKey.value = '' // Optional: keep last key visible
|
||||
}
|
||||
}, 500)
|
||||
@@ -345,7 +355,8 @@ const handleRawMode = (e) => {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.cursor, .app-cursor {
|
||||
.cursor,
|
||||
.app-cursor {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
background: currentColor;
|
||||
@@ -353,10 +364,20 @@ const handleRawMode = (e) => {
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% { transform: scale(0.9); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
</style>
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<div class="controls">
|
||||
<button @click="reset" :disabled="isPlaying">Reset</button>
|
||||
<button @click="togglePlay" class="play-btn">
|
||||
{{ isPlaying ? '⏸ Pause' : (isFinished ? '↺ Replay' : '▶ Play Animation') }}
|
||||
{{
|
||||
isPlaying ? '⏸ Pause' : isFinished ? '↺ Replay' : '▶ Play Animation'
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,16 +17,19 @@
|
||||
<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"
|
||||
<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
|
||||
:class="{
|
||||
active: index === currentIndex,
|
||||
processed: index < currentIndex,
|
||||
special: char.isSpecial,
|
||||
arg: char.isArg
|
||||
}"
|
||||
>
|
||||
<span class="char-val">{{ char.display }}</span>
|
||||
@@ -47,11 +52,14 @@
|
||||
<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-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>
|
||||
@@ -63,18 +71,22 @@
|
||||
<div class="terminal-screen">
|
||||
<div class="label">Terminal Screen / 屏幕显示</div>
|
||||
<div class="screen-content">
|
||||
<span
|
||||
v-for="(char, index) in outputBuffer"
|
||||
<span
|
||||
v-for="(char, index) in outputBuffer"
|
||||
:key="index"
|
||||
:style="char.style"
|
||||
>{{ char.val }}</span><span class="cursor">_</span>
|
||||
>{{ 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>)并执行。
|
||||
<span class="badge escape">Escape</span> 模式下(遇到
|
||||
<code>ESC</code>
|
||||
后),终端<strong>停止输出</strong>,开始收集字符作为指令,直到指令结束(如
|
||||
<code>m</code>)并执行。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,7 +147,7 @@ const togglePlay = () => {
|
||||
const play = async () => {
|
||||
if (isPlaying.value) return
|
||||
isPlaying.value = true
|
||||
|
||||
|
||||
// If finished, reset first
|
||||
if (isFinished.value) {
|
||||
reset()
|
||||
@@ -146,7 +158,7 @@ const play = async () => {
|
||||
if (!isPlaying.value) break
|
||||
|
||||
const char = charStream.value[currentIndex.value]
|
||||
|
||||
|
||||
// Processing Logic
|
||||
if (parserState.value === 'NORMAL') {
|
||||
if (char.val === '\x1B') {
|
||||
@@ -171,23 +183,23 @@ const play = async () => {
|
||||
currentStyle.value = {}
|
||||
lastAction.value = 'Execute: Reset Style'
|
||||
}
|
||||
|
||||
|
||||
// Small delay to show "Executing" state
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
await new Promise((r) => setTimeout(r, 200))
|
||||
parserState.value = 'NORMAL'
|
||||
} else {
|
||||
lastAction.value = 'Buffering...'
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 600)) // Animation speed
|
||||
|
||||
await new Promise((r) => setTimeout(r, 600)) // Animation speed
|
||||
|
||||
// Check playing again after wait
|
||||
if (!isPlaying.value) break
|
||||
|
||||
|
||||
currentIndex.value++
|
||||
}
|
||||
|
||||
|
||||
if (currentIndex.value >= charStream.value.length) {
|
||||
isPlaying.value = false
|
||||
isFinished.value = true
|
||||
@@ -272,8 +284,20 @@ const play = async () => {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
/* 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);
|
||||
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 {
|
||||
@@ -290,7 +314,7 @@ const play = async () => {
|
||||
- Item[0] center is at: 18px (half width)
|
||||
- So we need to shift left by 18px initially.
|
||||
*/
|
||||
margin-left: -18px;
|
||||
margin-left: -18px;
|
||||
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
@@ -312,7 +336,7 @@ const play = async () => {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 10px rgba(255,255,255,0.5);
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
z-index: 10;
|
||||
border-color: #fff;
|
||||
}
|
||||
@@ -339,8 +363,15 @@ const play = async () => {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.char-val { font-size: 14px; font-weight: bold; }
|
||||
.char-code { font-size: 9px; opacity: 0.7; margin-top: 2px; }
|
||||
.char-val {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.char-code {
|
||||
font-size: 9px;
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
position: absolute;
|
||||
@@ -350,9 +381,14 @@ const play = async () => {
|
||||
text-align: center;
|
||||
color: #0dbc79;
|
||||
}
|
||||
.arrow { font-size: 20px; line-height: 1; }
|
||||
.pointer-label { font-size: 10px; white-space: nowrap; }
|
||||
|
||||
.arrow {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
.pointer-label {
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* State Machine */
|
||||
.parser-state-machine {
|
||||
@@ -389,10 +425,19 @@ const play = async () => {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.state-name { font-weight: bold; font-size: 12px; }
|
||||
.state-desc { font-size: 10px; opacity: 0.8; }
|
||||
.state-name {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
.state-desc {
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.arrow-right { color: #555; font-size: 18px; }
|
||||
.arrow-right {
|
||||
color: #555;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.action-log {
|
||||
margin-left: 20px;
|
||||
@@ -440,10 +485,24 @@ const play = async () => {
|
||||
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; } }
|
||||
.badge.normal {
|
||||
background: #0dbc79;
|
||||
}
|
||||
.badge.escape {
|
||||
background: #e5e510;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes flash {
|
||||
0% {
|
||||
background: #333;
|
||||
}
|
||||
100% {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<span class="zh">16 色调色板</span>
|
||||
</div>
|
||||
<div class="palette-grid">
|
||||
<div
|
||||
v-for="(color, index) in palette"
|
||||
<div
|
||||
v-for="(color, index) in palette"
|
||||
:key="index"
|
||||
class="swatch"
|
||||
:style="{ backgroundColor: color }"
|
||||
@@ -31,7 +31,8 @@
|
||||
></div>
|
||||
</div>
|
||||
<div class="hint-text" v-if="activeColor">
|
||||
Sequence: <span class="code">^[[38;5;{{ palette.indexOf(activeColor) }}m</span>
|
||||
Sequence:
|
||||
<span class="code">^[[38;5;{{ palette.indexOf(activeColor) }}m</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,25 +86,32 @@
|
||||
<div class="preview">
|
||||
<div class="terminal-window">
|
||||
<div class="window-header">
|
||||
<div class="dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="dots"><span></span><span></span><span></span></div>
|
||||
<div class="window-title">Terminal Preview</div>
|
||||
</div>
|
||||
<div class="window-content">
|
||||
<div class="sequence-display-area">
|
||||
<span class="label">Last Sequence:</span>
|
||||
<span v-if="lastSequence" class="sequence-code">{{ lastSequence }}</span>
|
||||
<span v-if="lastSequence" class="sequence-code">{{
|
||||
lastSequence
|
||||
}}</span>
|
||||
<span v-else class="placeholder">Waiting for input...</span>
|
||||
</div>
|
||||
|
||||
<div class="main-display" :style="currentStyle" v-if="isContentVisible">
|
||||
|
||||
<div
|
||||
class="main-display"
|
||||
:style="currentStyle"
|
||||
v-if="isContentVisible"
|
||||
>
|
||||
Hello World
|
||||
</div>
|
||||
|
||||
|
||||
<div class="cursor-line">
|
||||
<span class="prompt">$</span>
|
||||
<span class="cursor-placeholder" v-if="cursorMode === 'absolute'"></span>
|
||||
<span
|
||||
class="cursor-placeholder"
|
||||
v-if="cursorMode === 'absolute'"
|
||||
></span>
|
||||
<span class="cursor-block" :style="cursorStyle"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,8 +124,22 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const palette = [
|
||||
'#000000', '#cd3131', '#0dbc79', '#e5e510', '#2472c8', '#bc3fbc', '#11a8cd', '#e5e5e5',
|
||||
'#666666', '#f14c4c', '#23d18b', '#f5f543', '#3b8eea', '#d670d6', '#29b8db', '#ffffff'
|
||||
'#000000',
|
||||
'#cd3131',
|
||||
'#0dbc79',
|
||||
'#e5e510',
|
||||
'#2472c8',
|
||||
'#bc3fbc',
|
||||
'#11a8cd',
|
||||
'#e5e5e5',
|
||||
'#666666',
|
||||
'#f14c4c',
|
||||
'#23d18b',
|
||||
'#f5f543',
|
||||
'#3b8eea',
|
||||
'#d670d6',
|
||||
'#29b8db',
|
||||
'#ffffff'
|
||||
]
|
||||
|
||||
const activeColor = ref(null)
|
||||
@@ -303,8 +325,14 @@ button.active {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.code { color: #facc15; font-weight: bold; }
|
||||
.desc { color: #a1a1aa; font-size: 12px; }
|
||||
.code {
|
||||
color: #facc15;
|
||||
font-weight: bold;
|
||||
}
|
||||
.desc {
|
||||
color: #a1a1aa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
background: #000;
|
||||
@@ -407,7 +435,9 @@ button.active {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="flow-diagram">
|
||||
<div class="stack-col">
|
||||
<div class="stack-label">TERMINAL STACK</div>
|
||||
|
||||
|
||||
<div class="stack-box kbd" :class="{ active: activeStage === 'kbd' }">
|
||||
<div class="box-header">
|
||||
<span class="box-icon">[kbd]</span>
|
||||
@@ -56,11 +56,9 @@
|
||||
|
||||
<div class="output-col">
|
||||
<div class="output-label">OUTPUT</div>
|
||||
|
||||
|
||||
<div class="terminal-preview">
|
||||
<div class="term-header">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="term-header"><span></span><span></span><span></span></div>
|
||||
<div class="term-body">
|
||||
<span class="prompt">$ </span>
|
||||
<span class="typed-text">{{ displayText }}</span>
|
||||
@@ -74,7 +72,11 @@
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="play-btn" @click="startAnimation" :disabled="isAnimating">
|
||||
<button
|
||||
class="play-btn"
|
||||
@click="startAnimation"
|
||||
:disabled="isAnimating"
|
||||
>
|
||||
{{ isAnimating ? 'Simulating...' : 'Simulate Keystroke' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -96,13 +98,13 @@ const statusColor = computed(() => {
|
||||
return 'text-green'
|
||||
})
|
||||
|
||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const startAnimation = async () => {
|
||||
if (isAnimating.value) return
|
||||
isAnimating.value = true
|
||||
displayText.value = ''
|
||||
|
||||
|
||||
// Stage 1: Keyboard
|
||||
activeStage.value = 'kbd'
|
||||
statusTitle.value = 'Input'
|
||||
@@ -159,12 +161,14 @@ const startAnimation = async () => {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.stack-col, .output-col {
|
||||
.stack-col,
|
||||
.output-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stack-label, .output-label {
|
||||
.stack-label,
|
||||
.output-label {
|
||||
color: #eab308;
|
||||
font-size: 12px;
|
||||
margin-bottom: 20px;
|
||||
@@ -245,8 +249,12 @@ const startAnimation = async () => {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.prompt { color: #888; }
|
||||
.typed-text { color: #22c55e; }
|
||||
.prompt {
|
||||
color: #888;
|
||||
}
|
||||
.typed-text {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
@@ -262,7 +270,9 @@ const startAnimation = async () => {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-box {
|
||||
@@ -285,8 +295,12 @@ const startAnimation = async () => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.text-red { color: #ef4444; }
|
||||
.text-green { color: #22c55e; }
|
||||
.text-red {
|
||||
color: #ef4444;
|
||||
}
|
||||
.text-green {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: 100%;
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
- 历史记录:记录最近几次按键的编码流。
|
||||
-->
|
||||
<template>
|
||||
<div class="input-visualizer" tabindex="0" @keydown="handleKeydown" @blur="handleBlur">
|
||||
<div
|
||||
class="input-visualizer"
|
||||
tabindex="0"
|
||||
@keydown="handleKeydown"
|
||||
@blur="handleBlur"
|
||||
>
|
||||
<div class="focus-overlay" v-if="!isFocused" @click="focus">
|
||||
<div class="focus-btn">
|
||||
<span class="icon">⌨️</span>
|
||||
@@ -22,7 +27,7 @@
|
||||
|
||||
<div class="main-display" :class="{ 'blur-content': !isFocused }">
|
||||
<div class="key-name">{{ currentKey.name || 'Press any key' }}</div>
|
||||
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-box">
|
||||
<div class="label">BYTES (HEX)</div>
|
||||
@@ -33,9 +38,10 @@
|
||||
<div class="value code">{{ currentKey.sequence || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="char-display">
|
||||
Character: <span class="char-val">{{ currentKey.charDisplay || '-' }}</span>
|
||||
Character:
|
||||
<span class="char-val">{{ currentKey.charDisplay || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +77,7 @@ const handleBlur = () => {
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
|
||||
let name = e.key
|
||||
let bytes = ''
|
||||
let sequence = ''
|
||||
@@ -80,15 +86,15 @@ const handleKeydown = (e) => {
|
||||
// Map special keys
|
||||
const keyMap = {
|
||||
' ': { name: 'Space', bytes: '20', char: ' ' },
|
||||
'Enter': { name: 'Enter', bytes: '0a', char: '\\n' },
|
||||
'Tab': { name: 'Tab', bytes: '09', char: '\\t' },
|
||||
'Escape': { name: 'Esc', bytes: '1b', char: '\\e' },
|
||||
'Backspace': { name: 'Backspace', bytes: '7f', char: '\\b' },
|
||||
'Delete': { name: 'Del', bytes: '1b 5b 33 7e', sequence: '^[[3~' },
|
||||
'ArrowUp': { name: 'Arrow Up', bytes: '1b 5b 41', sequence: '^[[A' },
|
||||
'ArrowDown': { name: 'Arrow Down', bytes: '1b 5b 42', sequence: '^[[B' },
|
||||
'ArrowRight': { name: 'Arrow Right', bytes: '1b 5b 43', sequence: '^[[C' },
|
||||
'ArrowLeft': { name: 'Arrow Left', bytes: '1b 5b 44', sequence: '^[[D' },
|
||||
Enter: { name: 'Enter', bytes: '0a', char: '\\n' },
|
||||
Tab: { name: 'Tab', bytes: '09', char: '\\t' },
|
||||
Escape: { name: 'Esc', bytes: '1b', char: '\\e' },
|
||||
Backspace: { name: 'Backspace', bytes: '7f', char: '\\b' },
|
||||
Delete: { name: 'Del', bytes: '1b 5b 33 7e', sequence: '^[[3~' },
|
||||
ArrowUp: { name: 'Arrow Up', bytes: '1b 5b 41', sequence: '^[[A' },
|
||||
ArrowDown: { name: 'Arrow Down', bytes: '1b 5b 42', sequence: '^[[B' },
|
||||
ArrowRight: { name: 'Arrow Right', bytes: '1b 5b 43', sequence: '^[[C' },
|
||||
ArrowLeft: { name: 'Arrow Left', bytes: '1b 5b 44', sequence: '^[[D' }
|
||||
}
|
||||
|
||||
if (keyMap[e.key]) {
|
||||
@@ -117,7 +123,7 @@ const handleKeydown = (e) => {
|
||||
|
||||
const keyData = { name, bytes, sequence, charDisplay }
|
||||
currentKey.value = keyData
|
||||
|
||||
|
||||
history.value.unshift(keyData)
|
||||
if (history.value.length > 5) history.value.pop()
|
||||
}
|
||||
@@ -138,7 +144,9 @@ const handleKeydown = (e) => {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.input-visualizer:focus {
|
||||
@@ -190,7 +198,9 @@ const handleKeydown = (e) => {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: opacity 0.2s, filter 0.2s;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
filter 0.2s;
|
||||
}
|
||||
|
||||
.blur-content {
|
||||
@@ -237,8 +247,12 @@ const handleKeydown = (e) => {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.highlight { color: #facc15; /* Yellow 400 */ }
|
||||
.code { color: #22d3ee; /* Cyan 400 */ }
|
||||
.highlight {
|
||||
color: #facc15; /* Yellow 400 */
|
||||
}
|
||||
.code {
|
||||
color: #22d3ee; /* Cyan 400 */
|
||||
}
|
||||
|
||||
.char-display {
|
||||
color: #a1a1aa; /* Zinc 400 */
|
||||
@@ -275,9 +289,9 @@ const handleKeydown = (e) => {
|
||||
border: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
.arrow {
|
||||
color: #71717a; /* Lighter grey for better visibility */
|
||||
margin: 0 8px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.h-name {
|
||||
@@ -285,8 +299,8 @@ const handleKeydown = (e) => {
|
||||
color: #e4e4e7;
|
||||
}
|
||||
|
||||
.h-bytes {
|
||||
color: #facc15;
|
||||
.h-bytes {
|
||||
color: #facc15;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,37 +6,41 @@
|
||||
|
||||
## 组件列表
|
||||
|
||||
| 组件名 | 描述 | 对应文档章节 |
|
||||
| :--- | :--- | :--- |
|
||||
| **TerminalDefinition.vue** | 可视化终端作为“字符流输入/输出环境”的定义。展示键盘输入 -> 字符流 -> 屏幕输出的过程。 | 1. 概念界定 |
|
||||
| **ArchitectureDemo.vue** | 演示终端(前端)与 Shell(后端)的分离架构。模拟点餐流程类比。 | 2. 核心架构 |
|
||||
| **TerminalGrid.vue** | 展示终端的字符网格系统,演示行、列和单元格的概念。 | 3. 视觉模型 |
|
||||
| **CellInspector.vue** | 单元格检查器,展示每个字符单元格背后的属性(字符、前景色、背景色等)。 | 3.2 样式检查 |
|
||||
| **EscapeSequences.vue** | 演示 ANSI 转义序列如何控制颜色、样式、光标移动和清屏。 | 4. 通信协议 |
|
||||
| **InputVisualizer.vue** | 可视化键盘按键如何转换为字节序列发送给 Shell。 | 5. 输入机制 |
|
||||
| **WebTerminal.vue** | 一个功能较完整的模拟终端,支持 `ls`, `cd`, `cat`, `apt` 等命令,包含虚拟文件系统。 | 附录/综合演示 |
|
||||
| **SignalsDemo.vue** | 演示终端信号(如 Ctrl+C SIGINT)的工作机制。 | (文档中可能引用) |
|
||||
| **FlowDiagram.vue** | 展示标准输入/输出/错误流 (stdin/stdout/stderr) 的流向图。 | (文档中可能引用) |
|
||||
| **AdvancedTUIDemo.vue** | 展示基于文本的用户界面 (TUI) 的高级布局能力(如面板、进度条)。 | (文档中可能引用) |
|
||||
| 组件名 | 描述 | 对应文档章节 |
|
||||
| :------------------------- | :------------------------------------------------------------------------------------ | :--------------- |
|
||||
| **TerminalDefinition.vue** | 可视化终端作为“字符流输入/输出环境”的定义。展示键盘输入 -> 字符流 -> 屏幕输出的过程。 | 1. 概念界定 |
|
||||
| **ArchitectureDemo.vue** | 演示终端(前端)与 Shell(后端)的分离架构。模拟点餐流程类比。 | 2. 核心架构 |
|
||||
| **TerminalGrid.vue** | 展示终端的字符网格系统,演示行、列和单元格的概念。 | 3. 视觉模型 |
|
||||
| **CellInspector.vue** | 单元格检查器,展示每个字符单元格背后的属性(字符、前景色、背景色等)。 | 3.2 样式检查 |
|
||||
| **EscapeSequences.vue** | 演示 ANSI 转义序列如何控制颜色、样式、光标移动和清屏。 | 4. 通信协议 |
|
||||
| **InputVisualizer.vue** | 可视化键盘按键如何转换为字节序列发送给 Shell。 | 5. 输入机制 |
|
||||
| **WebTerminal.vue** | 一个功能较完整的模拟终端,支持 `ls`, `cd`, `cat`, `apt` 等命令,包含虚拟文件系统。 | 附录/综合演示 |
|
||||
| **SignalsDemo.vue** | 演示终端信号(如 Ctrl+C SIGINT)的工作机制。 | (文档中可能引用) |
|
||||
| **FlowDiagram.vue** | 展示标准输入/输出/错误流 (stdin/stdout/stderr) 的流向图。 | (文档中可能引用) |
|
||||
| **AdvancedTUIDemo.vue** | 展示基于文本的用户界面 (TUI) 的高级布局能力(如面板、进度条)。 | (文档中可能引用) |
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 技术栈
|
||||
|
||||
- **Vue 3**: 使用 `<script setup>` 语法。
|
||||
- **Styling**: Scoped CSS,主要使用 Flexbox 和 Grid 布局。
|
||||
- **Theme**: 统一使用黑色系背景 (`#09090b`, `#18181b`) 和 JetBrains Mono 字体,保持类似终端的视觉风格。
|
||||
|
||||
### 维护注意事项
|
||||
|
||||
1. **双语支持**: 组件内部文本尽量支持中英双语,或通过 Props 传入文本。目前部分组件已硬编码双语标签。
|
||||
2. **自包含**: 组件应尽量自包含,不依赖外部复杂的 Store 或 Context,以便于在 Markdown 中直接使用。
|
||||
3. **响应式**: 考虑移动端适配,通常使用 `@media (max-width: 768px)` 进行布局调整。
|
||||
|
||||
### 常用颜色变量 (参考)
|
||||
|
||||
- 背景: `#09090b` (Main), `#18181b` (Panel)
|
||||
- 边框: `#27272a`
|
||||
- 文本: `#e4e4e7` (Primary), `#a1a1aa` (Secondary)
|
||||
- 强调色: `#22c55e` (Green/Success), `#facc15` (Yellow/Warning), `#22d3ee` (Cyan/Info)
|
||||
|
||||
## 目录结构
|
||||
|
||||
所有组件均位于 `docs/.vitepress/theme/components/appendix/terminal-intro/` 下。
|
||||
注册逻辑位于 `docs/.vitepress/theme/index.js`。
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<div class="signals-demo">
|
||||
<div class="left-panel">
|
||||
<div class="signal-list">
|
||||
<div
|
||||
class="signal-item"
|
||||
<div
|
||||
class="signal-item"
|
||||
:class="{ active: activeSignal === 'SIGINT' }"
|
||||
@click="sendSignal('SIGINT')"
|
||||
>
|
||||
@@ -27,8 +27,8 @@
|
||||
<div class="signal-name">SIGINT</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="signal-item"
|
||||
<div
|
||||
class="signal-item"
|
||||
:class="{ active: activeSignal === 'SIGTSTP' }"
|
||||
@click="sendSignal('SIGTSTP')"
|
||||
>
|
||||
@@ -43,22 +43,33 @@
|
||||
<div class="info-box">
|
||||
<div v-if="activeSignal === 'SIGINT'">
|
||||
<div class="info-header">
|
||||
<span class="highlight">Ctrl+C</span> → <span class="signal-green">SIGINT</span>
|
||||
<span class="highlight">Ctrl+C</span> →
|
||||
<span class="signal-green">SIGINT</span>
|
||||
</div>
|
||||
<div class="info-desc">Stop the running program</div>
|
||||
<p>Sends SIGINT (signal interrupt) to the foreground process. Most programs respond by stopping immediately. It's how you cancel a long-running command or exit a program that's stuck.</p>
|
||||
<p>
|
||||
Sends SIGINT (signal interrupt) to the foreground process. Most
|
||||
programs respond by stopping immediately. It's how you cancel a
|
||||
long-running command or exit a program that's stuck.
|
||||
</p>
|
||||
<div class="example-box">
|
||||
Example: Running `sleep 100` and pressing Ctrl+C stops it immediately.
|
||||
Example: Running `sleep 100` and pressing Ctrl+C stops it
|
||||
immediately.
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="activeSignal === 'SIGTSTP'">
|
||||
<div class="info-header">
|
||||
<span class="highlight">Ctrl+Z</span> → <span class="signal-blue">SIGTSTP</span>
|
||||
<span class="highlight">Ctrl+Z</span> →
|
||||
<span class="signal-blue">SIGTSTP</span>
|
||||
</div>
|
||||
<div class="info-desc">Suspend the running program</div>
|
||||
<p>Sends SIGTSTP (signal terminal stop). The process is paused and put in the background. You can resume it later with `fg` command.</p>
|
||||
<p>
|
||||
Sends SIGTSTP (signal terminal stop). The process is paused and put
|
||||
in the background. You can resume it later with `fg` command.
|
||||
</p>
|
||||
<div class="example-box">
|
||||
Example: Pressing Ctrl+Z pauses a running editor like vim, returning you to the shell.
|
||||
Example: Pressing Ctrl+Z pauses a running editor like vim, returning
|
||||
you to the shell.
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -74,12 +85,18 @@
|
||||
<div class="dots"><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
<div class="window-content">
|
||||
<div v-for="(line, i) in lines" :key="i" class="term-line" :class="line.type">
|
||||
<div
|
||||
v-for="(line, i) in lines"
|
||||
:key="i"
|
||||
class="term-line"
|
||||
:class="line.type"
|
||||
>
|
||||
{{ line.text }}
|
||||
</div>
|
||||
<div v-if="isRunning" class="term-line output">sleeping...</div>
|
||||
<div v-if="inputBuffer" class="term-line input">
|
||||
<span class="prompt">$</span> {{ inputBuffer }}<span class="cursor"></span>
|
||||
<span class="prompt">$</span> {{ inputBuffer
|
||||
}}<span class="cursor"></span>
|
||||
</div>
|
||||
<div v-else class="term-line input">
|
||||
<span class="prompt">$</span> <span class="cursor"></span>
|
||||
@@ -88,7 +105,9 @@
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn" @click="runCommand" :disabled="isRunning">Run Command</button>
|
||||
<button class="btn" @click="runCommand" :disabled="isRunning">
|
||||
Run Command
|
||||
</button>
|
||||
<button class="btn" @click="sendSignal('SIGINT')">Ctrl+C</button>
|
||||
<button class="btn" @click="sendSignal('SIGTSTP')">Ctrl+Z</button>
|
||||
<button class="btn secondary" @click="reset">Reset</button>
|
||||
@@ -97,9 +116,10 @@
|
||||
<div class="state-display">
|
||||
State: <span :class="stateClass">{{ processState }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<p class="instruction">
|
||||
Click "Run Command" to start a simulated process, then try sending different signals.
|
||||
Click "Run Command" to start a simulated process, then try sending
|
||||
different signals.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,9 +130,7 @@ import { ref, computed } from 'vue'
|
||||
|
||||
const activeSignal = ref('SIGINT')
|
||||
const isRunning = ref(false)
|
||||
const lines = ref([
|
||||
{ type: 'input', text: '$ sleep 100' }
|
||||
])
|
||||
const lines = ref([{ type: 'input', text: '$ sleep 100' }])
|
||||
const processState = ref('Running')
|
||||
const inputBuffer = ref('')
|
||||
|
||||
@@ -131,7 +149,7 @@ const runCommand = () => {
|
||||
|
||||
const sendSignal = (sig) => {
|
||||
activeSignal.value = sig
|
||||
|
||||
|
||||
if (!isRunning.value && sig === 'SIGINT') return
|
||||
|
||||
if (sig === 'SIGINT') {
|
||||
@@ -142,7 +160,10 @@ const sendSignal = (sig) => {
|
||||
} else if (sig === 'SIGTSTP') {
|
||||
lines.value.push({ type: 'output', text: 'sleeping...' })
|
||||
lines.value.push({ type: 'control', text: '^Z' })
|
||||
lines.value.push({ type: 'output', text: '[1]+ Stopped sleep 100' })
|
||||
lines.value.push({
|
||||
type: 'output',
|
||||
text: '[1]+ Stopped sleep 100'
|
||||
})
|
||||
isRunning.value = false
|
||||
processState.value = 'Process suspended (stopped)'
|
||||
}
|
||||
@@ -161,7 +182,10 @@ reset()
|
||||
<style scoped>
|
||||
.signals-demo {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); /* 自动适应宽度,不够时换行 */
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(280px, 1fr)
|
||||
); /* 自动适应宽度,不够时换行 */
|
||||
gap: 30px;
|
||||
background: #09090b;
|
||||
padding: 30px;
|
||||
@@ -237,9 +261,15 @@ reset()
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight { color: #facc15; }
|
||||
.signal-green { color: #22c55e; }
|
||||
.signal-blue { color: #3b82f6; }
|
||||
.highlight {
|
||||
color: #facc15;
|
||||
}
|
||||
.signal-green {
|
||||
color: #22c55e;
|
||||
}
|
||||
.signal-blue {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.info-desc {
|
||||
color: #a1a1aa;
|
||||
@@ -271,7 +301,9 @@ reset()
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.window-header {
|
||||
@@ -300,10 +332,19 @@ reset()
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.control { color: #ef4444; }
|
||||
.output { color: #d4d4d8; }
|
||||
.input { color: #fff; }
|
||||
.prompt { color: #71717a; margin-right: 8px; }
|
||||
.control {
|
||||
color: #ef4444;
|
||||
}
|
||||
.output {
|
||||
color: #d4d4d8;
|
||||
}
|
||||
.input {
|
||||
color: #fff;
|
||||
}
|
||||
.prompt {
|
||||
color: #71717a;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
@@ -348,9 +389,15 @@ reset()
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.state-green { color: #22c55e; }
|
||||
.state-red { color: #ef4444; }
|
||||
.state-blue { color: #3b82f6; }
|
||||
.state-green {
|
||||
color: #22c55e;
|
||||
}
|
||||
.state-red {
|
||||
color: #ef4444;
|
||||
}
|
||||
.state-blue {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
color: #a1a1aa;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<template>
|
||||
<div class="terminal-definition">
|
||||
<div class="mode-switch">
|
||||
<button
|
||||
:class="{ active: mode === 'cli' }"
|
||||
@click="mode = 'cli'"
|
||||
>
|
||||
<button :class="{ active: mode === 'cli' }" @click="mode = 'cli'">
|
||||
🖥️ CLI (命令行界面)
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: mode === 'gui' }"
|
||||
@click="mode = 'gui'"
|
||||
>
|
||||
<button :class="{ active: mode === 'gui' }" @click="mode = 'gui'">
|
||||
🖱️ GUI (图形用户界面)
|
||||
</button>
|
||||
</div>
|
||||
@@ -21,7 +15,28 @@
|
||||
<!-- Input Side -->
|
||||
<div class="stage input-stage">
|
||||
<div class="icon-box">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><path d="M6 8h.001"></path><path d="M10 8h.001"></path><path d="M14 8h.001"></path><path d="M18 8h.001"></path><path d="M6 12h.001"></path><path d="M10 12h.001"></path><path d="M14 12h.001"></path><path d="M18 12h.001"></path><path d="M7 16h10"></path></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect>
|
||||
<path d="M6 8h.001"></path>
|
||||
<path d="M10 8h.001"></path>
|
||||
<path d="M14 8h.001"></path>
|
||||
<path d="M18 8h.001"></path>
|
||||
<path d="M6 12h.001"></path>
|
||||
<path d="M10 12h.001"></path>
|
||||
<path d="M14 12h.001"></path>
|
||||
<path d="M18 12h.001"></path>
|
||||
<path d="M7 16h10"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="label">Input (Keyboard)</div>
|
||||
<div class="sub-label">发送指令 (字符信号)</div>
|
||||
@@ -31,8 +46,8 @@
|
||||
<div class="stream-path">
|
||||
<div class="stream-line"></div>
|
||||
<div class="stream-label">Character Stream / 字符流</div>
|
||||
<div
|
||||
v-for="char in activeChars"
|
||||
<div
|
||||
v-for="char in activeChars"
|
||||
:key="char.id"
|
||||
class="stream-char"
|
||||
:style="{ left: char.progress + '%' }"
|
||||
@@ -45,16 +60,20 @@
|
||||
<div class="stage output-stage">
|
||||
<div class="terminal-screen">
|
||||
<div class="screen-content">
|
||||
<span class="prompt">$</span> {{ typedContent }}<span class="cursor">_</span>
|
||||
<span class="prompt">$</span> {{ typedContent
|
||||
}}<span class="cursor">_</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label">Output (Text Grid)</div>
|
||||
<div class="sub-label">文本网格反馈</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="desc-box">
|
||||
<p><strong>CLI (Command Line Interface)</strong>: 这种模式下,计算机只认识字符。你的每一次按键都会被转换成编码发送给系统,系统处理后返回文字结果。它不关心你在哪里点击,只关心你输入了什么。</p>
|
||||
<p>
|
||||
<strong>CLI (Command Line Interface)</strong>:
|
||||
这种模式下,计算机只认识字符。你的每一次按键都会被转换成编码发送给系统,系统处理后返回文字结果。它不关心你在哪里点击,只关心你输入了什么。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="control-bar">
|
||||
@@ -71,7 +90,20 @@
|
||||
<!-- Input Side -->
|
||||
<div class="stage input-stage">
|
||||
<div class="icon-box gui-input" :class="{ clicking: isGuiClicking }">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path><path d="M13 13l6 6"></path></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path>
|
||||
<path d="M13 13l6 6"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="label">Input (Mouse)</div>
|
||||
<div class="sub-label">发送事件 (坐标/点击)</div>
|
||||
@@ -81,7 +113,7 @@
|
||||
<div class="stream-path">
|
||||
<div class="stream-line dashed"></div>
|
||||
<div class="stream-label">Event Loop / 事件循环</div>
|
||||
<div
|
||||
<div
|
||||
v-for="ev in guiEvents"
|
||||
:key="ev.id"
|
||||
class="gui-event-packet"
|
||||
@@ -98,11 +130,22 @@
|
||||
<div class="win-header"></div>
|
||||
<div class="win-body">
|
||||
<div class="icon-grid">
|
||||
<div class="desktop-icon" :class="{ selected: iconSelected }">📁</div>
|
||||
<div class="desktop-icon" :class="{ selected: iconSelected }">
|
||||
📁
|
||||
</div>
|
||||
<div class="desktop-icon">📄</div>
|
||||
</div>
|
||||
<div class="gui-cursor" :style="cursorStyle">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="white" stroke="black" stroke-width="2"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path></svg>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
stroke="black"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +156,11 @@
|
||||
</div>
|
||||
|
||||
<div class="desc-box">
|
||||
<p><strong>GUI (Graphical User Interface)</strong>: 这种模式下,计算机实时追踪鼠标坐标和点击事件,并每秒刷新 60 次屏幕像素。它更直观,但需要消耗大量资源来处理图形渲染。</p>
|
||||
<p>
|
||||
<strong>GUI (Graphical User Interface)</strong>:
|
||||
这种模式下,计算机实时追踪鼠标坐标和点击事件,并每秒刷新 60
|
||||
次屏幕像素。它更直观,但需要消耗大量资源来处理图形渲染。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="control-bar">
|
||||
@@ -123,7 +170,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -143,9 +189,9 @@ const startSimulation = () => {
|
||||
isAnimating.value = true
|
||||
typedContent.value = ''
|
||||
activeChars.value = []
|
||||
|
||||
|
||||
let index = 0
|
||||
|
||||
|
||||
const processNextChar = () => {
|
||||
if (index >= demoText.length) {
|
||||
setTimeout(() => {
|
||||
@@ -156,36 +202,36 @@ const startSimulation = () => {
|
||||
|
||||
const char = demoText[index]
|
||||
const charId = Date.now() + index
|
||||
|
||||
|
||||
// Create new flying char
|
||||
const newChar = {
|
||||
id: charId,
|
||||
val: char,
|
||||
progress: 10 // start position
|
||||
}
|
||||
|
||||
|
||||
activeChars.value.push(newChar)
|
||||
|
||||
|
||||
// Animate this char
|
||||
let progress = 10
|
||||
const interval = setInterval(() => {
|
||||
progress += 4 // Faster speed
|
||||
const charObj = activeChars.value.find(c => c.id === charId)
|
||||
const charObj = activeChars.value.find((c) => c.id === charId)
|
||||
if (charObj) charObj.progress = progress
|
||||
|
||||
|
||||
if (progress >= 90) {
|
||||
clearInterval(interval)
|
||||
// Remove from stream and add to screen
|
||||
activeChars.value = activeChars.value.filter(c => c.id !== charId)
|
||||
activeChars.value = activeChars.value.filter((c) => c.id !== charId)
|
||||
typedContent.value += char
|
||||
|
||||
|
||||
// Next char
|
||||
index++
|
||||
setTimeout(processNextChar, 100) // Faster typing
|
||||
}
|
||||
}, 20)
|
||||
}
|
||||
|
||||
|
||||
processNextChar()
|
||||
}
|
||||
|
||||
@@ -208,26 +254,29 @@ const startGuiSimulation = () => {
|
||||
inputMousePosition.value = { x: 80, y: 60 }
|
||||
screenCursorPosition.value = { x: 80, y: 60 }
|
||||
guiEvents.value = []
|
||||
|
||||
|
||||
// 1. Move Cursor (Physical Mouse Movement)
|
||||
let step = 0
|
||||
const moveInterval = setInterval(() => {
|
||||
step++
|
||||
inputMousePosition.value = {
|
||||
x: 80 - step * 2,
|
||||
y: 60 - step * 1.5
|
||||
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 }
|
||||
})
|
||||
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
|
||||
@@ -244,17 +293,17 @@ const emitGuiEvent = (type, onArrive) => {
|
||||
progress: 10
|
||||
}
|
||||
guiEvents.value.push(newEvent)
|
||||
|
||||
|
||||
let progress = 10
|
||||
const packetInterval = setInterval(() => {
|
||||
progress += 2
|
||||
const ev = guiEvents.value.find(e => e.id === eventId)
|
||||
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)
|
||||
|
||||
guiEvents.value = guiEvents.value.filter((e) => e.id !== eventId)
|
||||
|
||||
// Execute callback when packet arrives at Output
|
||||
if (onArrive) onArrive()
|
||||
}
|
||||
@@ -264,21 +313,20 @@ const emitGuiEvent = (type, onArrive) => {
|
||||
const performClick = () => {
|
||||
setTimeout(() => {
|
||||
isGuiClicking.value = true
|
||||
|
||||
|
||||
// Send Click Event
|
||||
emitGuiEvent('Click(40,30)', () => {
|
||||
// When packet arrives: Select icon
|
||||
iconSelected.value = true
|
||||
|
||||
iconSelected.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
isGuiAnimating.value = false
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
isGuiClicking.value = false
|
||||
isGuiClicking.value = false
|
||||
}, 200) // Input click feedback is fast
|
||||
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -380,7 +428,7 @@ const performClick = () => {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -439,7 +487,7 @@ const performClick = () => {
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
transition: transform 0.1s linear; /* Smooth interpolation */
|
||||
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.5));
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
@@ -481,7 +529,13 @@ const performClick = () => {
|
||||
}
|
||||
|
||||
.stream-line.dashed {
|
||||
background: repeating-linear-gradient(90deg, #27272a 0, #27272a 6px, transparent 6px, transparent 10px);
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#27272a 0,
|
||||
#27272a 6px,
|
||||
transparent 6px,
|
||||
transparent 10px
|
||||
);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
@@ -539,7 +593,9 @@ const performClick = () => {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.desc-box {
|
||||
@@ -592,13 +648,13 @@ button:disabled {
|
||||
height: auto;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
|
||||
.stream-path {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
|
||||
.stream-line {
|
||||
transform: rotate(90deg);
|
||||
width: 40px;
|
||||
@@ -606,4 +662,4 @@ button:disabled {
|
||||
margin-left: -20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
<div class="grid-demo">
|
||||
<div class="terminal-screen">
|
||||
<div class="grid-row" v-for="(row, rIndex) in rows" :key="rIndex">
|
||||
<div
|
||||
class="grid-cell"
|
||||
v-for="(cell, cIndex) in row"
|
||||
<div
|
||||
class="grid-cell"
|
||||
v-for="(cell, cIndex) in row"
|
||||
:key="cIndex"
|
||||
:class="{
|
||||
:class="{
|
||||
'active-cursor': cursor.r === rIndex && cursor.c === cIndex,
|
||||
'drawn': cell.drawn
|
||||
drawn: cell.drawn
|
||||
}"
|
||||
@mousedown.prevent="handleCellMouseDown(rIndex, cIndex)"
|
||||
@mouseover="handleCellHover(rIndex, cIndex)"
|
||||
@@ -30,12 +30,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="controls">
|
||||
<input
|
||||
<input
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
v-model="inputText"
|
||||
type="text"
|
||||
v-model="inputText"
|
||||
placeholder="Type here..."
|
||||
class="text-input"
|
||||
@keydown="handleKeydown"
|
||||
@@ -52,9 +52,10 @@ import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||
const ROW_COUNT = 10
|
||||
const COL_COUNT = 40
|
||||
|
||||
const createGrid = () => Array.from({ length: ROW_COUNT }, () =>
|
||||
Array.from({ length: COL_COUNT }, () => ({ char: '', drawn: false }))
|
||||
)
|
||||
const createGrid = () =>
|
||||
Array.from({ length: ROW_COUNT }, () =>
|
||||
Array.from({ length: COL_COUNT }, () => ({ char: '', drawn: false }))
|
||||
)
|
||||
|
||||
const rows = reactive(createGrid())
|
||||
const cursor = reactive({ r: 0, c: 0 })
|
||||
@@ -76,12 +77,12 @@ const handleKeydown = (e) => {
|
||||
rows[cursor.r][cursor.c].char = ''
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (e.key.length === 1) {
|
||||
rows[cursor.r][cursor.c].char = e.key
|
||||
advanceCursor()
|
||||
}
|
||||
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
cursor.r = Math.min(cursor.r + 1, ROW_COUNT - 1)
|
||||
cursor.c = 0
|
||||
@@ -116,8 +117,8 @@ const handleCellHover = (r, c) => {
|
||||
}
|
||||
|
||||
const clearGrid = () => {
|
||||
for(let r=0; r<ROW_COUNT; r++) {
|
||||
for(let c=0; c<COL_COUNT; c++) {
|
||||
for (let r = 0; r < ROW_COUNT; r++) {
|
||||
for (let c = 0; c < COL_COUNT; c++) {
|
||||
rows[r][c].char = ''
|
||||
rows[r][c].drawn = false
|
||||
}
|
||||
@@ -181,66 +182,68 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
width: 16px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid #27272a;
|
||||
border-bottom: 1px solid #27272a;
|
||||
color: #e4e4e7;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
width: 16px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid #27272a;
|
||||
border-bottom: 1px solid #27272a;
|
||||
color: #e4e4e7;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.grid-cell.drawn {
|
||||
background-color: #3f3f46;
|
||||
}
|
||||
|
||||
.grid-cell.active-cursor {
|
||||
background-color: #e4e4e7;
|
||||
color: #000;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.grid-cell.drawn {
|
||||
background-color: #3f3f46;
|
||||
}
|
||||
|
||||
.grid-cell.active-cursor {
|
||||
background-color: #e4e4e7;
|
||||
color: #000;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
background: #18181b;
|
||||
border: 1px solid #3f3f46;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #27272a;
|
||||
border: 1px solid #3f3f46;
|
||||
color: #e4e4e7;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #3f3f46;
|
||||
border-color: #52525b;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #a1a1aa; /* Zinc 400 */
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
background: #18181b;
|
||||
border: 1px solid #3f3f46;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #27272a;
|
||||
border: 1px solid #3f3f46;
|
||||
color: #e4e4e7;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #3f3f46;
|
||||
border-color: #52525b;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #a1a1aa; /* Zinc 400 */
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<!-- Left Panel: Task Guide -->
|
||||
<div class="task-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">🎯 实操任务 ({{ currentTaskIndex + 1 }}/{{ tasks.length }})</span>
|
||||
<span class="panel-title"
|
||||
>🎯 实操任务 ({{ currentTaskIndex + 1 }}/{{ tasks.length }})</span
|
||||
>
|
||||
<div class="os-selector">
|
||||
<select v-model="currentOS" @change="resetCurrentTask">
|
||||
<option value="mac">macOS</option>
|
||||
@@ -18,7 +20,7 @@
|
||||
<div class="task-content">
|
||||
<h3>{{ currentTask.title }}</h3>
|
||||
<p class="task-desc">{{ currentTask.description }}</p>
|
||||
|
||||
|
||||
<div class="ai-helper">
|
||||
<div class="ai-header">
|
||||
<span class="ai-icon">🤖</span>
|
||||
@@ -29,23 +31,36 @@
|
||||
{{ currentTask.aiQuery }}
|
||||
</div>
|
||||
<div class="chat-bubble ai">
|
||||
<p>{{ currentTask.aiResponse[currentOS] || currentTask.aiResponse.common }}</p>
|
||||
<p>
|
||||
{{
|
||||
currentTask.aiResponse[currentOS] ||
|
||||
currentTask.aiResponse.common
|
||||
}}
|
||||
</p>
|
||||
<!-- Multiple Commands Support -->
|
||||
<div v-if="currentTask.commands && currentTask.commands[currentOS]" class="cmd-buttons">
|
||||
<button
|
||||
v-for="(cmdItem, idx) in currentTask.commands[currentOS]"
|
||||
<div
|
||||
v-if="currentTask.commands && currentTask.commands[currentOS]"
|
||||
class="cmd-buttons"
|
||||
>
|
||||
<button
|
||||
v-for="(cmdItem, idx) in currentTask.commands[currentOS]"
|
||||
:key="idx"
|
||||
class="copy-btn"
|
||||
class="copy-btn"
|
||||
@click="copyCommand(cmdItem.cmd)"
|
||||
>
|
||||
{{ cmdItem.label || '复制命令' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Fallback for Single Command -->
|
||||
<button
|
||||
<button
|
||||
v-else-if="currentTask.expectedCmd"
|
||||
class="copy-btn"
|
||||
@click="copyCommand(currentTask.expectedCmd[currentOS] || currentTask.expectedCmd.common)"
|
||||
class="copy-btn"
|
||||
@click="
|
||||
copyCommand(
|
||||
currentTask.expectedCmd[currentOS] ||
|
||||
currentTask.expectedCmd.common
|
||||
)
|
||||
"
|
||||
>
|
||||
复制命令
|
||||
</button>
|
||||
@@ -57,11 +72,17 @@
|
||||
<span class="label">预期目标:</span>
|
||||
<span class="value">{{ currentTask.goal }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="success-message" v-if="isTaskCompleted">
|
||||
<span class="icon">🎉</span>
|
||||
<span>太棒了!任务完成!</span>
|
||||
<button class="next-btn" @click="nextTask" v-if="currentTaskIndex < tasks.length - 1">下一关</button>
|
||||
<button
|
||||
class="next-btn"
|
||||
@click="nextTask"
|
||||
v-if="currentTaskIndex < tasks.length - 1"
|
||||
>
|
||||
下一关
|
||||
</button>
|
||||
<button class="reset-btn" @click="resetAll" v-else>重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,22 +100,29 @@
|
||||
</div>
|
||||
<div class="terminal-body" ref="terminalBody" @click="focusInput">
|
||||
<div v-for="(line, index) in history" :key="index" class="line">
|
||||
<span v-if="line.type === 'input'" class="prompt">{{ line.prompt }}</span>
|
||||
<span v-if="line.type === 'input'" class="prompt">{{
|
||||
line.prompt
|
||||
}}</span>
|
||||
<span :class="line.type">{{ line.content }}</span>
|
||||
</div>
|
||||
|
||||
<div class="line input-line" v-if="!isTaskCompleted || currentTaskIndex < tasks.length - 1">
|
||||
|
||||
<div
|
||||
class="line input-line"
|
||||
v-if="!isTaskCompleted || currentTaskIndex < tasks.length - 1"
|
||||
>
|
||||
<span class="prompt">{{ prompt }}</span>
|
||||
<input
|
||||
<input
|
||||
ref="cmdInput"
|
||||
v-model="inputCmd"
|
||||
v-model="inputCmd"
|
||||
@keydown.enter="executeCommand"
|
||||
@keydown.tab.prevent
|
||||
type="text"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<span v-if="inputCmd.length > 0" class="enter-hint">⏎ 按回车执行</span>
|
||||
<span v-if="inputCmd.length > 0" class="enter-hint"
|
||||
>⏎ 按回车执行</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,10 +143,10 @@ const terminalBody = ref(null)
|
||||
|
||||
// System Configurations
|
||||
const osConfig = {
|
||||
'mac': { prompt: 'user@MacBook ~ % ', title: 'user — -zsh' },
|
||||
mac: { prompt: 'user@MacBook ~ % ', title: 'user — -zsh' },
|
||||
'win-ps': { prompt: 'PS C:\\Users\\User> ', title: 'Windows PowerShell' },
|
||||
'win-cmd': { prompt: 'C:\\Users\\User> ', title: 'Command Prompt' },
|
||||
'linux': { prompt: 'user@localhost:~$ ', title: 'user@localhost: ~' }
|
||||
linux: { prompt: 'user@localhost:~$ ', title: 'user@localhost: ~' }
|
||||
}
|
||||
|
||||
const prompt = computed(() => osConfig[currentOS.value].prompt)
|
||||
@@ -132,14 +160,17 @@ const tasks = [
|
||||
goal: '列出当前目录下的所有文件。',
|
||||
aiQuery: '我想查看当前目录下的文件,应该用什么命令?',
|
||||
aiResponse: {
|
||||
'mac': '在 macOS 和 Linux 中,查看文件列表使用 `ls` 命令 (List)。',
|
||||
'linux': '在 macOS 和 Linux 中,查看文件列表使用 `ls` 命令 (List)。',
|
||||
mac: '在 macOS 和 Linux 中,查看文件列表使用 `ls` 命令 (List)。',
|
||||
linux: '在 macOS 和 Linux 中,查看文件列表使用 `ls` 命令 (List)。',
|
||||
'win-ps': '在 PowerShell 中,你可以使用 `ls` 或 `dir` 命令。',
|
||||
'win-cmd': '在 Windows CMD 中,查看文件列表使用 `dir` 命令 (Directory)。',
|
||||
'common': '通常使用 ls 或 dir。'
|
||||
common: '通常使用 ls 或 dir。'
|
||||
},
|
||||
expectedCmd: {
|
||||
'mac': 'ls', 'linux': 'ls', 'win-ps': 'ls', 'win-cmd': 'dir'
|
||||
mac: 'ls',
|
||||
linux: 'ls',
|
||||
'win-ps': 'ls',
|
||||
'win-cmd': 'dir'
|
||||
},
|
||||
validate: (cmd, os) => {
|
||||
const valid = os === 'win-cmd' ? ['dir'] : ['ls', 'dir', 'll']
|
||||
@@ -161,14 +192,16 @@ d---- 1/15/2026 9:00 AM Downloads
|
||||
},
|
||||
{
|
||||
title: '第二步:创建一个新家',
|
||||
description: '文件太多会很乱,我们创建一个专门的文件夹来存放今天的练习文件。',
|
||||
description:
|
||||
'文件太多会很乱,我们创建一个专门的文件夹来存放今天的练习文件。',
|
||||
goal: '创建一个名为 "demo" 的文件夹。',
|
||||
aiQuery: '怎么创建一个新的文件夹?名字叫 demo。',
|
||||
aiResponse: {
|
||||
'common': '创建文件夹(目录)的命令是 `mkdir` (Make Directory)。你可以输入 `mkdir demo`。'
|
||||
common:
|
||||
'创建文件夹(目录)的命令是 `mkdir` (Make Directory)。你可以输入 `mkdir demo`。'
|
||||
},
|
||||
expectedCmd: {
|
||||
'common': 'mkdir demo'
|
||||
common: 'mkdir demo'
|
||||
},
|
||||
validate: (cmd) => cmd.trim() === 'mkdir demo',
|
||||
output: () => '' // mkdir usually has no output on success
|
||||
@@ -179,10 +212,10 @@ d---- 1/15/2026 9:00 AM Downloads
|
||||
goal: '进入 "demo" 文件夹。',
|
||||
aiQuery: '怎么进入刚才建好的 demo 文件夹?',
|
||||
aiResponse: {
|
||||
'common': '切换目录使用 `cd` 命令 (Change Directory)。输入 `cd demo` 即可。'
|
||||
common: '切换目录使用 `cd` 命令 (Change Directory)。输入 `cd demo` 即可。'
|
||||
},
|
||||
expectedCmd: {
|
||||
'common': 'cd demo'
|
||||
common: 'cd demo'
|
||||
},
|
||||
validate: (cmd) => cmd.trim() === 'cd demo',
|
||||
output: () => '' // cd usually has no output, but prompt changes
|
||||
@@ -193,19 +226,26 @@ d---- 1/15/2026 9:00 AM Downloads
|
||||
goal: '创建一个名为 "hello.txt" 的文件。',
|
||||
aiQuery: '我想新建一个空文件叫 hello.txt,怎么做?',
|
||||
aiResponse: {
|
||||
'mac': '在 Mac/Linux 上,使用 `touch hello.txt` 可以快速创建一个空文件。',
|
||||
'linux': '在 Mac/Linux 上,使用 `touch hello.txt` 可以快速创建一个空文件。',
|
||||
'win-ps': '在 PowerShell 中,可以使用 `ni hello.txt` 或 `echo "" > hello.txt`。',
|
||||
'win-cmd': '在 CMD 中,可以使用 `type nul > hello.txt` 或 `echo. > hello.txt`。',
|
||||
mac: '在 Mac/Linux 上,使用 `touch hello.txt` 可以快速创建一个空文件。',
|
||||
linux: '在 Mac/Linux 上,使用 `touch hello.txt` 可以快速创建一个空文件。',
|
||||
'win-ps':
|
||||
'在 PowerShell 中,可以使用 `ni hello.txt` 或 `echo "" > hello.txt`。',
|
||||
'win-cmd':
|
||||
'在 CMD 中,可以使用 `type nul > hello.txt` 或 `echo. > hello.txt`。'
|
||||
},
|
||||
expectedCmd: {
|
||||
'mac': 'touch hello.txt',
|
||||
'linux': 'touch hello.txt',
|
||||
mac: 'touch hello.txt',
|
||||
linux: 'touch hello.txt',
|
||||
'win-ps': 'ni hello.txt',
|
||||
'win-cmd': 'type nul > hello.txt'
|
||||
},
|
||||
validate: (cmd, os) => {
|
||||
if (cmd.includes('touch') || cmd.includes('echo') || cmd.includes('ni') || cmd.includes('type')) {
|
||||
if (
|
||||
cmd.includes('touch') ||
|
||||
cmd.includes('echo') ||
|
||||
cmd.includes('ni') ||
|
||||
cmd.includes('type')
|
||||
) {
|
||||
return cmd.includes('hello.txt')
|
||||
}
|
||||
return false
|
||||
@@ -214,22 +254,26 @@ d---- 1/15/2026 9:00 AM Downloads
|
||||
},
|
||||
{
|
||||
title: '第五步:安装程序 (系统软件 & Python库)',
|
||||
description: '终端不仅能管理文件,还能安装软件。我们来尝试两种常见的安装场景:安装系统工具(如 wget/git)和安装 Python 库(如 requests)。',
|
||||
description:
|
||||
'终端不仅能管理文件,还能安装软件。我们来尝试两种常见的安装场景:安装系统工具(如 wget/git)和安装 Python 库(如 requests)。',
|
||||
goal: '任选其一:安装系统工具或 Python 库。',
|
||||
aiQuery: '怎么用命令行安装软件?我想装 git 或者 python 的 requests 库。',
|
||||
aiResponse: {
|
||||
'mac': 'macOS 推荐使用 Homebrew 安装系统软件,使用 pip 安装 Python 库。',
|
||||
'linux': 'Linux (Ubuntu/Debian) 使用 apt 安装系统软件,使用 pip 安装 Python 库。',
|
||||
'win-ps': 'Windows PowerShell 推荐使用 winget 安装系统软件,使用 pip 安装 Python 库。',
|
||||
'win-cmd': 'Windows CMD 推荐使用 winget 安装系统软件,使用 pip 安装 Python 库。',
|
||||
'common': '不同系统有不同的包管理器。'
|
||||
mac: 'macOS 推荐使用 Homebrew 安装系统软件,使用 pip 安装 Python 库。',
|
||||
linux:
|
||||
'Linux (Ubuntu/Debian) 使用 apt 安装系统软件,使用 pip 安装 Python 库。',
|
||||
'win-ps':
|
||||
'Windows PowerShell 推荐使用 winget 安装系统软件,使用 pip 安装 Python 库。',
|
||||
'win-cmd':
|
||||
'Windows CMD 推荐使用 winget 安装系统软件,使用 pip 安装 Python 库。',
|
||||
common: '不同系统有不同的包管理器。'
|
||||
},
|
||||
commands: {
|
||||
'mac': [
|
||||
mac: [
|
||||
{ label: '安装 wget (系统)', cmd: 'brew install wget' },
|
||||
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
|
||||
],
|
||||
'linux': [
|
||||
linux: [
|
||||
{ label: '安装 git (系统)', cmd: 'sudo apt install git' },
|
||||
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
|
||||
],
|
||||
@@ -244,34 +288,46 @@ d---- 1/15/2026 9:00 AM Downloads
|
||||
},
|
||||
expectedCmd: {
|
||||
// Fallback/Legacy
|
||||
'mac': 'brew install wget',
|
||||
'linux': 'sudo apt install git',
|
||||
mac: 'brew install wget',
|
||||
linux: 'sudo apt install git',
|
||||
'win-ps': 'pip install requests',
|
||||
'win-cmd': 'pip install requests'
|
||||
},
|
||||
validate: (cmd, os) => {
|
||||
const c = cmd.trim()
|
||||
if (os === 'mac') return c === 'brew install wget' || c === 'pip install requests'
|
||||
if (os === 'linux') return c === 'sudo apt install git' || c === 'apt install git' || c === 'pip install requests'
|
||||
if (os === 'win-ps' || os === 'win-cmd') return c === 'winget install git.git' || c === 'winget install git' || c === 'pip install requests'
|
||||
if (os === 'mac')
|
||||
return c === 'brew install wget' || c === 'pip install requests'
|
||||
if (os === 'linux')
|
||||
return (
|
||||
c === 'sudo apt install git' ||
|
||||
c === 'apt install git' ||
|
||||
c === 'pip install requests'
|
||||
)
|
||||
if (os === 'win-ps' || os === 'win-cmd')
|
||||
return (
|
||||
c === 'winget install git.git' ||
|
||||
c === 'winget install git' ||
|
||||
c === 'pip install requests'
|
||||
)
|
||||
return c === 'pip install requests'
|
||||
},
|
||||
output: (os, cmd) => { // Modified to accept cmd
|
||||
output: (os, cmd) => {
|
||||
// Modified to accept cmd
|
||||
const c = cmd ? cmd.trim() : ''
|
||||
|
||||
|
||||
// Python requests output
|
||||
if (c.includes('pip install requests')) {
|
||||
return `
|
||||
return `
|
||||
Downloading/unpacking requests
|
||||
Downloading requests-2.31.0-py3-none-any.whl (62kB): 62kB downloaded
|
||||
Installing collected packages: requests
|
||||
Successfully installed requests
|
||||
Cleaning up...`
|
||||
}
|
||||
|
||||
|
||||
// Windows winget output
|
||||
if (c.includes('winget install')) {
|
||||
return `
|
||||
return `
|
||||
Found Git [Git.Git] Version 2.43.0
|
||||
This application is licensed to you by its owner.
|
||||
Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.
|
||||
@@ -310,14 +366,15 @@ Setting up git (1:2.34.1-1ubuntu1.9) ...`
|
||||
goal: '删除 "hello.txt" 文件。',
|
||||
aiQuery: '我不想要 hello.txt 了,怎么删除它?',
|
||||
aiResponse: {
|
||||
'mac': '删除文件使用 `rm` 命令 (Remove)。小心,这个操作通常不可撤销!输入 `rm hello.txt`。',
|
||||
'linux': '删除文件使用 `rm` 命令 (Remove)。小心,这个操作通常不可撤销!输入 `rm hello.txt`。',
|
||||
mac: '删除文件使用 `rm` 命令 (Remove)。小心,这个操作通常不可撤销!输入 `rm hello.txt`。',
|
||||
linux:
|
||||
'删除文件使用 `rm` 命令 (Remove)。小心,这个操作通常不可撤销!输入 `rm hello.txt`。',
|
||||
'win-ps': '在 PowerShell 中使用 `rm` 或 `del`。输入 `rm hello.txt`。',
|
||||
'win-cmd': '在 CMD 中使用 `del` 命令 (Delete)。输入 `del hello.txt`。',
|
||||
'win-cmd': '在 CMD 中使用 `del` 命令 (Delete)。输入 `del hello.txt`。'
|
||||
},
|
||||
expectedCmd: {
|
||||
'mac': 'rm hello.txt',
|
||||
'linux': 'rm hello.txt',
|
||||
mac: 'rm hello.txt',
|
||||
linux: 'rm hello.txt',
|
||||
'win-ps': 'rm hello.txt',
|
||||
'win-cmd': 'del hello.txt'
|
||||
},
|
||||
@@ -362,12 +419,18 @@ const executeCommand = () => {
|
||||
// 1. Add to history
|
||||
let currentPrompt = prompt.value
|
||||
// Special handling for prompt update simulation (hacky way)
|
||||
if (currentTaskIndex.value >= 2 && currentTaskIndex.value < 6 && history.value.length > 0) {
|
||||
// If we are inside demo folder
|
||||
if (currentOS.value === 'mac') currentPrompt = 'user@MacBook demo % '
|
||||
else if (currentOS.value === 'linux') currentPrompt = 'user@localhost:~/demo$ '
|
||||
else if (currentOS.value === 'win-ps') currentPrompt = 'PS C:\\Users\\User\\demo> '
|
||||
else currentPrompt = 'C:\\Users\\User\\demo> '
|
||||
if (
|
||||
currentTaskIndex.value >= 2 &&
|
||||
currentTaskIndex.value < 6 &&
|
||||
history.value.length > 0
|
||||
) {
|
||||
// If we are inside demo folder
|
||||
if (currentOS.value === 'mac') currentPrompt = 'user@MacBook demo % '
|
||||
else if (currentOS.value === 'linux')
|
||||
currentPrompt = 'user@localhost:~/demo$ '
|
||||
else if (currentOS.value === 'win-ps')
|
||||
currentPrompt = 'PS C:\\Users\\User\\demo> '
|
||||
else currentPrompt = 'C:\\Users\\User\\demo> '
|
||||
}
|
||||
|
||||
history.value.push({ type: 'input', prompt: currentPrompt, content: cmd })
|
||||
@@ -375,7 +438,10 @@ const executeCommand = () => {
|
||||
|
||||
// 2. Process Command
|
||||
// Check if it matches current task requirement
|
||||
if (!isTaskCompleted.value && currentTask.value.validate(cmd, currentOS.value)) {
|
||||
if (
|
||||
!isTaskCompleted.value &&
|
||||
currentTask.value.validate(cmd, currentOS.value)
|
||||
) {
|
||||
// Success
|
||||
const out = currentTask.value.output(currentOS.value, cmd) // Pass cmd to output
|
||||
if (out) {
|
||||
@@ -386,19 +452,29 @@ const executeCommand = () => {
|
||||
// Failure or just random command
|
||||
// Simple mock responses for common commands if not matching task
|
||||
if (cmd.trim() === 'ls' || cmd.trim() === 'dir') {
|
||||
if (currentTaskIndex.value < 2) {
|
||||
// Initial state
|
||||
history.value.push({ type: 'output', content: tasks[0].output(currentOS.value) })
|
||||
} else if (currentTaskIndex.value >= 2) {
|
||||
// Inside demo
|
||||
if (currentTaskIndex.value === 3) history.value.push({ type: 'output', content: '' }) // empty
|
||||
else history.value.push({ type: 'output', content: 'hello.txt' })
|
||||
}
|
||||
if (currentTaskIndex.value < 2) {
|
||||
// Initial state
|
||||
history.value.push({
|
||||
type: 'output',
|
||||
content: tasks[0].output(currentOS.value)
|
||||
})
|
||||
} else if (currentTaskIndex.value >= 2) {
|
||||
// Inside demo
|
||||
if (currentTaskIndex.value === 3)
|
||||
history.value.push({ type: 'output', content: '' }) // empty
|
||||
else history.value.push({ type: 'output', content: 'hello.txt' })
|
||||
}
|
||||
} else if (cmd.trim() === 'clear' || cmd.trim() === 'cls') {
|
||||
history.value = []
|
||||
} else if (!isTaskCompleted.value) {
|
||||
history.value.push({ type: 'error', content: `Command not found or not matching task: ${cmd}` })
|
||||
history.value.push({ type: 'info', content: `💡 提示:试试点击左侧的“问问 AI”?` })
|
||||
history.value.push({
|
||||
type: 'error',
|
||||
content: `Command not found or not matching task: ${cmd}`
|
||||
})
|
||||
history.value.push({
|
||||
type: 'info',
|
||||
content: `💡 提示:试试点击左侧的“问问 AI”?`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +486,10 @@ const nextTask = () => {
|
||||
currentTaskIndex.value++
|
||||
isTaskCompleted.value = false
|
||||
// Clear history to keep it clean? Or keep it? Let's keep it but maybe add a separator
|
||||
history.value.push({ type: 'info', content: `--- 进入下一关: ${currentTask.value.title} ---` })
|
||||
history.value.push({
|
||||
type: 'info',
|
||||
content: `--- 进入下一关: ${currentTask.value.title} ---`
|
||||
})
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
@@ -446,7 +525,7 @@ watch(currentOS, () => {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -606,11 +685,18 @@ watch(currentOS, () => {
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.next-btn, .reset-btn {
|
||||
.next-btn,
|
||||
.reset-btn {
|
||||
margin-left: auto;
|
||||
padding: 6px 16px;
|
||||
background: #10b981;
|
||||
@@ -622,7 +708,8 @@ watch(currentOS, () => {
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.next-btn:hover, .reset-btn:hover {
|
||||
.next-btn:hover,
|
||||
.reset-btn:hover {
|
||||
transform: scale(1.05);
|
||||
background: #059669;
|
||||
}
|
||||
@@ -636,9 +723,21 @@ watch(currentOS, () => {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.terminal-panel.win-cmd { background: #0c0c0c; color: #cccccc; font-family: 'Consolas', monospace; }
|
||||
.terminal-panel.win-ps { background: #012456; color: #ffffff; font-family: 'Consolas', monospace; }
|
||||
.terminal-panel.mac, .terminal-panel.linux { background: #2b2b2b; color: #f0f0f0; }
|
||||
.terminal-panel.win-cmd {
|
||||
background: #0c0c0c;
|
||||
color: #cccccc;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
.terminal-panel.win-ps {
|
||||
background: #012456;
|
||||
color: #ffffff;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
.terminal-panel.mac,
|
||||
.terminal-panel.linux {
|
||||
background: #2b2b2b;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
padding: 8px 12px;
|
||||
@@ -658,11 +757,21 @@ watch(currentOS, () => {
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.dot.red { background: #ff5f56; }
|
||||
.dot.yellow { background: #ffbd2e; }
|
||||
.dot.green { background: #27c93f; }
|
||||
.dot.red {
|
||||
background: #ff5f56;
|
||||
}
|
||||
.dot.yellow {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
.dot.green {
|
||||
background: #27c93f;
|
||||
}
|
||||
|
||||
.terminal-panel.win-cmd .dot, .terminal-panel.win-ps .dot { border-radius: 0; background: #ccc; }
|
||||
.terminal-panel.win-cmd .dot,
|
||||
.terminal-panel.win-ps .dot {
|
||||
border-radius: 0;
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.terminal-header .title {
|
||||
position: absolute;
|
||||
@@ -696,8 +805,12 @@ watch(currentOS, () => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-panel.win-cmd .prompt { color: #cccccc; }
|
||||
.terminal-panel.win-ps .prompt { color: #ffffff; }
|
||||
.terminal-panel.win-cmd .prompt {
|
||||
color: #cccccc;
|
||||
}
|
||||
.terminal-panel.win-ps .prompt {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
@@ -725,8 +838,13 @@ watch(currentOS, () => {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.line.output {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="terminal-os-demo">
|
||||
<div class="os-switch">
|
||||
<button
|
||||
v-for="os in osList"
|
||||
<button
|
||||
v-for="os in osList"
|
||||
:key="os.id"
|
||||
:class="{ active: currentOS === os.id }"
|
||||
:class="{ active: currentOS === os.id }"
|
||||
@click="switchOS(os.id)"
|
||||
>
|
||||
<span class="os-icon">{{ os.icon }}</span>
|
||||
@@ -21,12 +21,26 @@
|
||||
</div>
|
||||
<div class="window-title">{{ currentOSConfig.title }}</div>
|
||||
<div class="window-controls">
|
||||
<button class="control-btn" @click="resetDemo" title="Reset">↺</button>
|
||||
<button class="control-btn" @click="resetDemo" title="Reset">
|
||||
↺
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-content" @click="nextStep" :class="{ 'clickable': !isTyping && !isFinished }">
|
||||
<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-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>
|
||||
@@ -43,22 +57,33 @@
|
||||
|
||||
<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>
|
||||
<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
|
||||
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>
|
||||
@@ -90,17 +115,54 @@ const configs = {
|
||||
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: '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: '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 }
|
||||
]
|
||||
},
|
||||
@@ -109,46 +171,137 @@ const configs = {
|
||||
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: '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"。' }
|
||||
{
|
||||
type: 'command',
|
||||
content: 'echo "Hello World"',
|
||||
delay: 400,
|
||||
explanation:
|
||||
'输入 `echo`。这是让计算机**复读**你说的话,常用于测试或打印信息。'
|
||||
},
|
||||
{
|
||||
type: 'output',
|
||||
content: 'Hello World',
|
||||
delay: 100,
|
||||
explanation: '计算机乖乖地输出了 "Hello World"。'
|
||||
}
|
||||
]
|
||||
},
|
||||
'mac': {
|
||||
mac: {
|
||||
title: 'user — -zsh — 80x24',
|
||||
prompt: 'user@MacBook-Pro ~ % ',
|
||||
demo: [
|
||||
{ type: 'explanation', content: '准备输入命令...' },
|
||||
{ type: 'command', content: 'ls -G', delay: 400, explanation: '输入 `ls` (List)。这是 Mac/Linux 系统用来**列出文件**的命令。`-G` 参数让输出带颜色。' },
|
||||
{ type: 'output', content: 'Desktop Downloads Movies Music', delay: 100 },
|
||||
{ type: 'output', content: 'Documents Library Pictures Public', delay: 100, explanation: '系统列出了你的主目录下的文件夹。' },
|
||||
{ type: 'command', content: 'sw_vers', delay: 400, explanation: '输入 `sw_vers` (Software Version)。这是 macOS 特有的命令,查看**系统版本**。' },
|
||||
{
|
||||
type: 'command',
|
||||
content: 'ls -G',
|
||||
delay: 400,
|
||||
explanation:
|
||||
'输入 `ls` (List)。这是 Mac/Linux 系统用来**列出文件**的命令。`-G` 参数让输出带颜色。'
|
||||
},
|
||||
{
|
||||
type: 'output',
|
||||
content: 'Desktop Downloads Movies Music',
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
type: 'output',
|
||||
content: 'Documents Library Pictures Public',
|
||||
delay: 100,
|
||||
explanation: '系统列出了你的主目录下的文件夹。'
|
||||
},
|
||||
{
|
||||
type: 'command',
|
||||
content: 'sw_vers',
|
||||
delay: 400,
|
||||
explanation:
|
||||
'输入 `sw_vers` (Software Version)。这是 macOS 特有的命令,查看**系统版本**。'
|
||||
},
|
||||
{ type: 'output', content: 'ProductName: macOS', delay: 50 },
|
||||
{ type: 'output', content: 'ProductVersion: 15.1', delay: 50 },
|
||||
{ type: 'output', content: 'BuildVersion: 24B83', delay: 50, explanation: '系统返回了当前的 macOS 版本信息。' }
|
||||
{
|
||||
type: 'output',
|
||||
content: 'BuildVersion: 24B83',
|
||||
delay: 50,
|
||||
explanation: '系统返回了当前的 macOS 版本信息。'
|
||||
}
|
||||
]
|
||||
},
|
||||
'linux': {
|
||||
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"。' }
|
||||
{
|
||||
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 isFinished = computed(
|
||||
() =>
|
||||
currentOSConfig.value &&
|
||||
currentStepIndex.value >= currentOSConfig.value.demo.length - 1
|
||||
)
|
||||
|
||||
const switchOS = (id) => {
|
||||
currentOS.value = id
|
||||
@@ -161,7 +314,11 @@ const resetDemo = () => {
|
||||
currentStepIndex.value = -1
|
||||
isTyping.value = false
|
||||
// Add initial prompt
|
||||
lines.value.push({ type: 'input', prompt: currentOSConfig.value.prompt, content: '' })
|
||||
lines.value.push({
|
||||
type: 'input',
|
||||
prompt: currentOSConfig.value.prompt,
|
||||
content: ''
|
||||
})
|
||||
}
|
||||
|
||||
// Initial reset
|
||||
@@ -169,77 +326,80 @@ 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: '' })
|
||||
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))
|
||||
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.
|
||||
// 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))
|
||||
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: '' })
|
||||
lines.value.push({ type: 'input', prompt: promptText, content: '' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -345,9 +505,15 @@ const nextStep = async () => {
|
||||
.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; }
|
||||
.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 */
|
||||
.terminal-window.mac {
|
||||
@@ -363,9 +529,15 @@ const nextStep = async () => {
|
||||
.terminal-window.mac .window-buttons .btn {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.terminal-window.mac .window-buttons .close { background: #ff5f56; }
|
||||
.terminal-window.mac .window-buttons .minimize { background: #ffbd2e; }
|
||||
.terminal-window.mac .window-buttons .maximize { background: #27c93f; }
|
||||
.terminal-window.mac .window-buttons .close {
|
||||
background: #ff5f56;
|
||||
}
|
||||
.terminal-window.mac .window-buttons .minimize {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
.terminal-window.mac .window-buttons .maximize {
|
||||
background: #27c93f;
|
||||
}
|
||||
|
||||
.window-bar {
|
||||
padding: 8px 12px;
|
||||
@@ -478,14 +650,20 @@ const nextStep = async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
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); }
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.completed-overlay {
|
||||
@@ -513,7 +691,7 @@ const nextStep = async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
@@ -595,8 +773,13 @@ const nextStep = async () => {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
<div class="terminal-title">Terminal - zsh</div>
|
||||
</div>
|
||||
<div class="terminal-body" ref="terminalBody" @click="focusInput">
|
||||
<div v-for="(line, index) in history" :key="index" class="terminal-line">
|
||||
<div
|
||||
v-for="(line, index) in history"
|
||||
:key="index"
|
||||
class="terminal-line"
|
||||
>
|
||||
<span class="prompt" v-if="line.type === 'input'">
|
||||
<span class="path">{{ currentPath }}</span>
|
||||
<span class="arrow">$ </span>
|
||||
@@ -35,9 +39,9 @@
|
||||
<span class="path">{{ currentPath }}</span>
|
||||
<span class="arrow">$ </span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="currentInput"
|
||||
<input
|
||||
type="text"
|
||||
v-model="currentInput"
|
||||
@keyup.enter="executeCommand"
|
||||
@keydown.up.prevent="navigateHistory(-1)"
|
||||
@keydown.down.prevent="navigateHistory(1)"
|
||||
@@ -52,16 +56,26 @@
|
||||
|
||||
<div class="cheat-sheet">
|
||||
<div class="sheet-title">
|
||||
<span class="icon">📖</span>
|
||||
<span class="icon">📖</span>
|
||||
<span class="en">Command Cheat Sheet</span>
|
||||
<span class="divider">|</span>
|
||||
<span class="zh">命令速查表</span>
|
||||
</div>
|
||||
<div class="sheet-content">
|
||||
<div class="cmd-group" v-for="(group, gIndex) in cheatSheet" :key="gIndex">
|
||||
<div
|
||||
class="cmd-group"
|
||||
v-for="(group, gIndex) in cheatSheet"
|
||||
:key="gIndex"
|
||||
>
|
||||
<div class="group-title">{{ group.category }}</div>
|
||||
<div class="cmd-item" v-for="(cmd, cIndex) in group.commands" :key="cIndex">
|
||||
<div class="cmd-name" @click="fillCommand(cmd.name)">{{ cmd.name }}</div>
|
||||
<div
|
||||
class="cmd-item"
|
||||
v-for="(cmd, cIndex) in group.commands"
|
||||
:key="cIndex"
|
||||
>
|
||||
<div class="cmd-name" @click="fillCommand(cmd.name)">
|
||||
{{ cmd.name }}
|
||||
</div>
|
||||
<div class="cmd-desc">
|
||||
<div class="en">{{ cmd.descEn }}</div>
|
||||
<div class="zh">{{ cmd.descZh }}</div>
|
||||
@@ -77,8 +91,16 @@
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
|
||||
const history = ref([
|
||||
{ type: 'output', content: 'Welcome to the interactive terminal simulator! / 欢迎使用交互式终端模拟器!' },
|
||||
{ type: 'output', content: 'Type "help" to see available commands. / 输入 "help" 查看可用命令。' }
|
||||
{
|
||||
type: 'output',
|
||||
content:
|
||||
'Welcome to the interactive terminal simulator! / 欢迎使用交互式终端模拟器!'
|
||||
},
|
||||
{
|
||||
type: 'output',
|
||||
content:
|
||||
'Type "help" to see available commands. / 输入 "help" 查看可用命令。'
|
||||
}
|
||||
])
|
||||
const currentInput = ref('')
|
||||
const inputField = ref(null)
|
||||
@@ -91,41 +113,60 @@ const fileSystem = {
|
||||
name: '/',
|
||||
type: 'dir',
|
||||
children: {
|
||||
'home': {
|
||||
home: {
|
||||
name: 'home',
|
||||
type: 'dir',
|
||||
children: {
|
||||
'user': {
|
||||
user: {
|
||||
name: 'user',
|
||||
type: 'dir',
|
||||
children: {
|
||||
'hello.txt': { name: 'hello.txt', type: 'file', content: 'Hello World! This is a mock file.\n你好!这是一个模拟文件。' },
|
||||
'notes.md': { name: 'notes.md', type: 'file', content: '# My Notes\n- Learn Terminal\n- Learn Shell\n- Learn Kernel' },
|
||||
'projects': {
|
||||
name: 'projects',
|
||||
type: 'dir',
|
||||
children: {
|
||||
'app.js': { name: 'app.js', type: 'file', content: 'console.log("Hello");' }
|
||||
}
|
||||
'hello.txt': {
|
||||
name: 'hello.txt',
|
||||
type: 'file',
|
||||
content:
|
||||
'Hello World! This is a mock file.\n你好!这是一个模拟文件。'
|
||||
},
|
||||
'Downloads': { name: 'Downloads', type: 'dir', children: {} }
|
||||
'notes.md': {
|
||||
name: 'notes.md',
|
||||
type: 'file',
|
||||
content:
|
||||
'# My Notes\n- Learn Terminal\n- Learn Shell\n- Learn Kernel'
|
||||
},
|
||||
projects: {
|
||||
name: 'projects',
|
||||
type: 'dir',
|
||||
children: {
|
||||
'app.js': {
|
||||
name: 'app.js',
|
||||
type: 'file',
|
||||
content: 'console.log("Hello");'
|
||||
}
|
||||
}
|
||||
},
|
||||
Downloads: { name: 'Downloads', type: 'dir', children: {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'etc': {
|
||||
etc: {
|
||||
name: 'etc',
|
||||
type: 'dir',
|
||||
children: {
|
||||
'passwd': { name: 'passwd', type: 'file', content: 'root:x:0:0:root:/root:/bin/bash\nuser:x:1000:1000:user:/home/user:/bin/zsh' }
|
||||
passwd: {
|
||||
name: 'passwd',
|
||||
type: 'file',
|
||||
content:
|
||||
'root:x:0:0:root:/root:/bin/bash\nuser:x:1000:1000:user:/home/user:/bin/zsh'
|
||||
}
|
||||
}
|
||||
},
|
||||
'bin': {
|
||||
bin: {
|
||||
name: 'bin',
|
||||
type: 'dir',
|
||||
children: {
|
||||
'ls': { name: 'ls', type: 'file', content: 'Binary file' },
|
||||
'cat': { name: 'cat', type: 'file', content: 'Binary file' }
|
||||
ls: { name: 'ls', type: 'file', content: 'Binary file' },
|
||||
cat: { name: 'cat', type: 'file', content: 'Binary file' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,12 +176,13 @@ let currentPath = '~'
|
||||
let currentDirObj = fileSystem.children['home'].children['user']
|
||||
|
||||
const resolvePath = (path) => {
|
||||
if (path === '~' || path === '') return fileSystem.children['home'].children['user']
|
||||
if (path === '~' || path === '')
|
||||
return fileSystem.children['home'].children['user']
|
||||
if (path === '/') return fileSystem
|
||||
|
||||
let parts = path.split('/').filter(p => p)
|
||||
|
||||
let parts = path.split('/').filter((p) => p)
|
||||
let current = path.startsWith('/') ? fileSystem : currentDirObj
|
||||
|
||||
|
||||
for (const part of parts) {
|
||||
if (part === '.') continue
|
||||
if (part === '..') {
|
||||
@@ -177,23 +219,25 @@ const navigateTo = (target) => {
|
||||
newDir = fileSystem.children['home'].children['user']
|
||||
} else if (target === '..') {
|
||||
if (currentPath === '/') return { path: '/', dir: fileSystem }
|
||||
if (currentPath === '~') { // ~ is /home/user
|
||||
newPath = '/home'
|
||||
newDir = fileSystem.children['home']
|
||||
if (currentPath === '~') {
|
||||
// ~ is /home/user
|
||||
newPath = '/home'
|
||||
newDir = fileSystem.children['home']
|
||||
} else {
|
||||
// Simple string manipulation for path
|
||||
const parts = currentPath.split('/')
|
||||
parts.pop()
|
||||
newPath = parts.join('/') || '/'
|
||||
|
||||
// Re-resolve dir from root for safety
|
||||
if (newPath === '/') newDir = fileSystem
|
||||
else if (newPath === '/home') newDir = fileSystem.children['home']
|
||||
else if (newPath === '/home/user') newDir = fileSystem.children['home'].children['user']
|
||||
else {
|
||||
// Fallback for deeper paths if we supported them
|
||||
// For now, let's keep it simple
|
||||
}
|
||||
// Simple string manipulation for path
|
||||
const parts = currentPath.split('/')
|
||||
parts.pop()
|
||||
newPath = parts.join('/') || '/'
|
||||
|
||||
// Re-resolve dir from root for safety
|
||||
if (newPath === '/') newDir = fileSystem
|
||||
else if (newPath === '/home') newDir = fileSystem.children['home']
|
||||
else if (newPath === '/home/user')
|
||||
newDir = fileSystem.children['home'].children['user']
|
||||
else {
|
||||
// Fallback for deeper paths if we supported them
|
||||
// For now, let's keep it simple
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Relative path
|
||||
@@ -201,7 +245,8 @@ const navigateTo = (target) => {
|
||||
const targetObj = currentDirObj.children[target]
|
||||
if (targetObj.type === 'dir') {
|
||||
newDir = targetObj
|
||||
newPath = currentPath === '/' ? `/${target}` : `${currentPath}/${target}`
|
||||
newPath =
|
||||
currentPath === '/' ? `/${target}` : `${currentPath}/${target}`
|
||||
} else {
|
||||
return { error: `cd: not a directory: ${target}` }
|
||||
}
|
||||
@@ -216,24 +261,52 @@ const cheatSheet = [
|
||||
{
|
||||
category: 'Navigation / 导航',
|
||||
commands: [
|
||||
{ name: 'ls', descEn: 'List directory contents', descZh: '列出当前目录下的文件和文件夹' },
|
||||
{ name: 'cd <dir>', descEn: 'Change directory', descZh: '进入指定目录 (例如: cd projects)' },
|
||||
{ name: 'pwd', descEn: 'Print working directory', descZh: '显示当前所在的完整路径' }
|
||||
{
|
||||
name: 'ls',
|
||||
descEn: 'List directory contents',
|
||||
descZh: '列出当前目录下的文件和文件夹'
|
||||
},
|
||||
{
|
||||
name: 'cd <dir>',
|
||||
descEn: 'Change directory',
|
||||
descZh: '进入指定目录 (例如: cd projects)'
|
||||
},
|
||||
{
|
||||
name: 'pwd',
|
||||
descEn: 'Print working directory',
|
||||
descZh: '显示当前所在的完整路径'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'File Operations / 文件操作',
|
||||
commands: [
|
||||
{ name: 'cat <file>', descEn: 'Show file contents', descZh: '查看文件内容 (例如: cat hello.txt)' },
|
||||
{ name: 'touch <file>', descEn: 'Create empty file', descZh: '创建一个新文件' },
|
||||
{ name: 'mkdir <dir>', descEn: 'Make directory', descZh: '创建一个新文件夹' },
|
||||
{
|
||||
name: 'cat <file>',
|
||||
descEn: 'Show file contents',
|
||||
descZh: '查看文件内容 (例如: cat hello.txt)'
|
||||
},
|
||||
{
|
||||
name: 'touch <file>',
|
||||
descEn: 'Create empty file',
|
||||
descZh: '创建一个新文件'
|
||||
},
|
||||
{
|
||||
name: 'mkdir <dir>',
|
||||
descEn: 'Make directory',
|
||||
descZh: '创建一个新文件夹'
|
||||
},
|
||||
{ name: 'rm <file>', descEn: 'Remove file', descZh: '删除文件' }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'System / 系统',
|
||||
commands: [
|
||||
{ name: 'echo <text>', descEn: 'Print text', descZh: '在屏幕上打印一段文字' },
|
||||
{
|
||||
name: 'echo <text>',
|
||||
descEn: 'Print text',
|
||||
descZh: '在屏幕上打印一段文字'
|
||||
},
|
||||
{ name: 'whoami', descEn: 'Current user', descZh: '显示当前用户名' },
|
||||
{ name: 'date', descEn: 'Show date/time', descZh: '显示当前日期和时间' },
|
||||
{ name: 'clear', descEn: 'Clear screen', descZh: '清空屏幕内容' }
|
||||
@@ -242,8 +315,16 @@ const cheatSheet = [
|
||||
{
|
||||
category: 'Package Manager / 软件包 (Mock)',
|
||||
commands: [
|
||||
{ name: 'apt update', descEn: 'Update package list', descZh: '更新软件包列表' },
|
||||
{ name: 'apt install <pkg>', descEn: 'Install package', descZh: '安装软件 (例如: apt install git)' }
|
||||
{
|
||||
name: 'apt update',
|
||||
descEn: 'Update package list',
|
||||
descZh: '更新软件包列表'
|
||||
},
|
||||
{
|
||||
name: 'apt install <pkg>',
|
||||
descEn: 'Install package',
|
||||
descZh: '安装软件 (例如: apt install git)'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -253,14 +334,14 @@ const commands = {
|
||||
return `Available commands:
|
||||
ls, cd, pwd, cat, touch, mkdir, rm, echo, whoami, date, clear, apt`
|
||||
},
|
||||
|
||||
|
||||
ls: (args) => {
|
||||
if (!currentDirObj.children) return ''
|
||||
const items = Object.values(currentDirObj.children)
|
||||
if (items.length === 0) return ''
|
||||
|
||||
|
||||
// Simple column formatting
|
||||
const names = items.map(item => {
|
||||
const names = items.map((item) => {
|
||||
return item.type === 'dir' ? `\x1b[1;34m${item.name}/\x1b[0m` : item.name
|
||||
})
|
||||
return names.join(' ')
|
||||
@@ -292,7 +373,7 @@ const commands = {
|
||||
cat: (args) => {
|
||||
const file = args[0]
|
||||
if (!file) return 'usage: cat <file>'
|
||||
|
||||
|
||||
if (currentDirObj.children && currentDirObj.children[file]) {
|
||||
const target = currentDirObj.children[file]
|
||||
if (target.type === 'dir') return `cat: ${file}: Is a directory`
|
||||
@@ -312,7 +393,8 @@ const commands = {
|
||||
mkdir: (args) => {
|
||||
const name = args[0]
|
||||
if (!name) return 'usage: mkdir <dir>'
|
||||
if (currentDirObj.children[name]) return `mkdir: cannot create directory '${name}': File exists`
|
||||
if (currentDirObj.children[name])
|
||||
return `mkdir: cannot create directory '${name}': File exists`
|
||||
currentDirObj.children[name] = { name, type: 'dir', children: {} }
|
||||
return null
|
||||
},
|
||||
@@ -322,7 +404,8 @@ const commands = {
|
||||
if (!name) return 'usage: rm <file>'
|
||||
// Mock: -r not supported for simplicity
|
||||
if (currentDirObj.children[name]) {
|
||||
if (currentDirObj.children[name].type === 'dir') return `rm: cannot remove '${name}': Is a directory`
|
||||
if (currentDirObj.children[name].type === 'dir')
|
||||
return `rm: cannot remove '${name}': Is a directory`
|
||||
delete currentDirObj.children[name]
|
||||
return null
|
||||
}
|
||||
@@ -330,7 +413,7 @@ const commands = {
|
||||
},
|
||||
|
||||
whoami: () => 'user',
|
||||
|
||||
|
||||
date: () => new Date().toString(),
|
||||
|
||||
apt: (args) => {
|
||||
@@ -363,7 +446,7 @@ Setting up ${pkg} (1.0.0) ...`
|
||||
|
||||
const executeCommand = () => {
|
||||
const input = currentInput.value.trim()
|
||||
|
||||
|
||||
if (!input) {
|
||||
history.value.push({ type: 'input', content: '' })
|
||||
currentInput.value = ''
|
||||
@@ -378,32 +461,41 @@ const executeCommand = () => {
|
||||
history.value.push({ type: 'input', content: input })
|
||||
|
||||
const [cmd, ...args] = input.split(/\s+/)
|
||||
|
||||
|
||||
if (commands[cmd]) {
|
||||
try {
|
||||
const output = commands[cmd](args)
|
||||
if (output !== null) {
|
||||
// Handle colored output simply by not escaping HTML if we trust it (Vue escapes by default)
|
||||
// For simple color simulation, we can strip codes or use a span.
|
||||
// For simple color simulation, we can strip codes or use a span.
|
||||
// Here we just keep simple text, but `ls` returns ANSI codes which we might want to handle or strip.
|
||||
// For this demo, let's strip ANSI codes for safety/simplicity in Vue or parse them.
|
||||
// Let's simple string replace for blue color
|
||||
|
||||
|
||||
let safeOutput = output
|
||||
|
||||
|
||||
const lines = safeOutput.split('\n')
|
||||
lines.forEach(line => {
|
||||
// Basic ANSI parser for ls colors
|
||||
const isDir = line.includes('\x1b[1;34m')
|
||||
const cleanContent = line.replace(/\x1b\[[0-9;]*m/g, '')
|
||||
history.value.push({ type: isDir ? 'output-dir' : 'output', content: cleanContent })
|
||||
lines.forEach((line) => {
|
||||
// Basic ANSI parser for ls colors
|
||||
const isDir = line.includes('\x1b[1;34m')
|
||||
const cleanContent = line.replace(/\x1b\[[0-9;]*m/g, '')
|
||||
history.value.push({
|
||||
type: isDir ? 'output-dir' : 'output',
|
||||
content: cleanContent
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
history.value.push({ type: 'error', content: `Error executing command: ${e.message}` })
|
||||
history.value.push({
|
||||
type: 'error',
|
||||
content: `Error executing command: ${e.message}`
|
||||
})
|
||||
}
|
||||
} else {
|
||||
history.value.push({ type: 'error', content: `zsh: command not found: ${cmd}` })
|
||||
history.value.push({
|
||||
type: 'error',
|
||||
content: `zsh: command not found: ${cmd}`
|
||||
})
|
||||
}
|
||||
|
||||
currentInput.value = ''
|
||||
@@ -412,12 +504,13 @@ const executeCommand = () => {
|
||||
|
||||
const navigateHistory = (direction) => {
|
||||
if (commandHistory.value.length === 0) return
|
||||
|
||||
|
||||
historyIndex.value += direction
|
||||
|
||||
|
||||
if (historyIndex.value < 0) historyIndex.value = 0
|
||||
if (historyIndex.value > commandHistory.value.length) historyIndex.value = commandHistory.value.length
|
||||
|
||||
if (historyIndex.value > commandHistory.value.length)
|
||||
historyIndex.value = commandHistory.value.length
|
||||
|
||||
if (historyIndex.value === commandHistory.value.length) {
|
||||
currentInput.value = ''
|
||||
} else {
|
||||
@@ -430,9 +523,11 @@ const handleTabCompletion = () => {
|
||||
const input = currentInput.value
|
||||
const [cmd, ...args] = input.split(/\s+/)
|
||||
const partial = args[args.length - 1] || ''
|
||||
|
||||
|
||||
if (cmd && currentDirObj.children) {
|
||||
const matches = Object.keys(currentDirObj.children).filter(name => name.startsWith(partial))
|
||||
const matches = Object.keys(currentDirObj.children).filter((name) =>
|
||||
name.startsWith(partial)
|
||||
)
|
||||
if (matches.length === 1) {
|
||||
const completed = matches[0]
|
||||
// Replace last arg with completed
|
||||
@@ -506,9 +601,15 @@ onMounted(() => {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.red { background-color: #ef4444; }
|
||||
.yellow { background-color: #facc15; }
|
||||
.green { background-color: #22c55e; }
|
||||
.red {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.yellow {
|
||||
background-color: #facc15;
|
||||
}
|
||||
.green {
|
||||
background-color: #22c55e;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
flex: 1;
|
||||
@@ -652,8 +753,9 @@ input {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.terminal-container, .cheat-sheet {
|
||||
|
||||
.terminal-container,
|
||||
.cheat-sheet {
|
||||
height: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user