feat: improve terminal intro components and content
- Fix EscapeParserDemo play button and pointer alignment - Improve TerminalHandsOn AI helper visibility and apt/pip support - Update terminal intro markdown content
This commit is contained in:
@@ -4,8 +4,8 @@
|
|||||||
<div class="title">转义序列解析原理 (Parser Mechanism)</div>
|
<div class="title">转义序列解析原理 (Parser Mechanism)</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button @click="reset" :disabled="isPlaying">Reset</button>
|
<button @click="reset" :disabled="isPlaying">Reset</button>
|
||||||
<button @click="togglePlay" :disabled="isFinished" class="play-btn">
|
<button @click="togglePlay" class="play-btn">
|
||||||
{{ isPlaying ? '⏸ Pause' : '▶ Play Animation' }}
|
{{ isPlaying ? '⏸ Pause' : (isFinished ? '↺ Replay' : '▶ Play Animation') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
// 原始字符串: Hello [RED]World[RESET]!
|
// 原始字符串: Hello [RED]World[RESET]!
|
||||||
// \x1B [ 3 1 m
|
// \x1B [ 3 1 m
|
||||||
@@ -115,20 +115,36 @@ const isFinished = ref(false)
|
|||||||
const lastAction = ref('')
|
const lastAction = ref('')
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
isPlaying.value = false // Stop first
|
||||||
currentIndex.value = 0
|
currentIndex.value = 0
|
||||||
outputBuffer.value = []
|
outputBuffer.value = []
|
||||||
parserState.value = 'NORMAL'
|
parserState.value = 'NORMAL'
|
||||||
currentStyle.value = {}
|
currentStyle.value = {}
|
||||||
isPlaying.value = false
|
|
||||||
isFinished.value = false
|
isFinished.value = false
|
||||||
lastAction.value = ''
|
lastAction.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (isPlaying.value) {
|
||||||
|
isPlaying.value = false
|
||||||
|
} else {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const play = async () => {
|
const play = async () => {
|
||||||
if (isPlaying.value) return
|
if (isPlaying.value) return
|
||||||
isPlaying.value = true
|
isPlaying.value = true
|
||||||
|
|
||||||
|
// If finished, reset first
|
||||||
|
if (isFinished.value) {
|
||||||
|
reset()
|
||||||
|
isPlaying.value = true
|
||||||
|
}
|
||||||
|
|
||||||
while (currentIndex.value < charStream.value.length) {
|
while (currentIndex.value < charStream.value.length) {
|
||||||
|
if (!isPlaying.value) break
|
||||||
|
|
||||||
const char = charStream.value[currentIndex.value]
|
const char = charStream.value[currentIndex.value]
|
||||||
|
|
||||||
// Processing Logic
|
// Processing Logic
|
||||||
@@ -165,12 +181,18 @@ const play = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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++
|
currentIndex.value++
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlaying.value = false
|
if (currentIndex.value >= charStream.value.length) {
|
||||||
isFinished.value = true
|
isPlaying.value = false
|
||||||
lastAction.value = 'Done'
|
isFinished.value = true
|
||||||
|
lastAction.value = 'Done'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -241,14 +263,14 @@ const play = async () => {
|
|||||||
.stream-track {
|
.stream-track {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
display: flex;
|
/* Use a fixed height to contain the items */
|
||||||
justify-content: center; /* Center the focus area */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-window-mask {
|
.stream-window-mask {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
/* Mask gradient to fade edges */
|
/* Mask gradient to fade edges */
|
||||||
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);
|
-webkit-mask-image: linear-gradient(to right, transparent, black 40%, black 60%, transparent);
|
||||||
@@ -258,9 +280,18 @@ const play = async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%; /* Start from center */
|
left: 50%; /* Center the container start */
|
||||||
|
/*
|
||||||
|
Correct centering logic:
|
||||||
|
- Item width: 36px
|
||||||
|
- Gap: 4px
|
||||||
|
- Total unit: 40px
|
||||||
|
- We want Item[0] center to be at left:0 (relative to left:50%)
|
||||||
|
- Item[0] center is at: 18px (half width)
|
||||||
|
- So we need to shift left by 18px initially.
|
||||||
|
*/
|
||||||
|
margin-left: -18px;
|
||||||
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
padding-left: 20px; /* Offset for first item */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.char-box {
|
.char-box {
|
||||||
|
|||||||
@@ -20,18 +20,33 @@
|
|||||||
<p class="task-desc">{{ currentTask.description }}</p>
|
<p class="task-desc">{{ currentTask.description }}</p>
|
||||||
|
|
||||||
<div class="ai-helper">
|
<div class="ai-helper">
|
||||||
<div class="ai-header" @click="toggleAi" :class="{ active: isAiOpen }">
|
<div class="ai-header">
|
||||||
<span class="ai-icon">🤖</span>
|
<span class="ai-icon">🤖</span>
|
||||||
<span class="ai-title">不知道怎么写?问问 AI</span>
|
<span class="ai-title">不知道怎么写?问问 AI</span>
|
||||||
<span class="ai-arrow">▼</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-chat" v-if="isAiOpen">
|
<div class="ai-chat" v-show="isAiOpen">
|
||||||
<div class="chat-bubble user">
|
<div class="chat-bubble user">
|
||||||
{{ currentTask.aiQuery }}
|
{{ currentTask.aiQuery }}
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-bubble ai">
|
<div class="chat-bubble ai">
|
||||||
<p>{{ currentTask.aiResponse[currentOS] || currentTask.aiResponse.common }}</p>
|
<p>{{ currentTask.aiResponse[currentOS] || currentTask.aiResponse.common }}</p>
|
||||||
<button class="copy-btn" @click="copyCommand(currentTask.expectedCmd[currentOS] || currentTask.expectedCmd.common)">
|
<!-- Multiple Commands Support -->
|
||||||
|
<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"
|
||||||
|
@click="copyCommand(cmdItem.cmd)"
|
||||||
|
>
|
||||||
|
{{ cmdItem.label || '复制命令' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Fallback for Single Command -->
|
||||||
|
<button
|
||||||
|
v-else-if="currentTask.expectedCmd"
|
||||||
|
class="copy-btn"
|
||||||
|
@click="copyCommand(currentTask.expectedCmd[currentOS] || currentTask.expectedCmd.common)"
|
||||||
|
>
|
||||||
复制命令
|
复制命令
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +94,7 @@
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
<span v-if="inputCmd.length > 0" class="enter-hint">⏎ 按回车执行</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +107,7 @@ import { ref, computed, nextTick, watch } from 'vue'
|
|||||||
|
|
||||||
const currentOS = ref('win-cmd')
|
const currentOS = ref('win-cmd')
|
||||||
const currentTaskIndex = ref(0)
|
const currentTaskIndex = ref(0)
|
||||||
const isAiOpen = ref(false)
|
const isAiOpen = ref(true)
|
||||||
const inputCmd = ref('')
|
const inputCmd = ref('')
|
||||||
const history = ref([])
|
const history = ref([])
|
||||||
const cmdInput = ref(null)
|
const cmdInput = ref(null)
|
||||||
@@ -197,23 +213,80 @@ d---- 1/15/2026 9:00 AM Downloads
|
|||||||
output: () => ''
|
output: () => ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '第五步:安装程序',
|
title: '第五步:安装程序 (系统软件 & Python库)',
|
||||||
description: '终端不仅能管理文件,还能安装软件。比如我们想安装一个 Python 库 "requests"。',
|
description: '终端不仅能管理文件,还能安装软件。我们来尝试两种常见的安装场景:安装系统工具(如 wget/git)和安装 Python 库(如 requests)。',
|
||||||
goal: '使用 pip 安装 requests 库。',
|
goal: '任选其一:安装系统工具或 Python 库。',
|
||||||
aiQuery: '怎么用命令行安装 python 的 requests 库?',
|
aiQuery: '怎么用命令行安装软件?我想装 git 或者 python 的 requests 库。',
|
||||||
aiResponse: {
|
aiResponse: {
|
||||||
'common': '安装 Python 库通常使用 `pip` (Python Package Installer)。命令是 `pip install requests`。'
|
'mac': 'macOS 推荐使用 Homebrew 安装系统软件,使用 pip 安装 Python 库。',
|
||||||
|
'linux': 'Linux (Ubuntu/Debian) 使用 apt 安装系统软件,使用 pip 安装 Python 库。',
|
||||||
|
'win-ps': 'Windows PowerShell 可以使用 pip 安装 Python 库。系统软件通常用 winget (这里暂只演示 pip)。',
|
||||||
|
'win-cmd': 'CMD 也可以使用 pip 安装 Python 库。',
|
||||||
|
'common': '不同系统有不同的包管理器。'
|
||||||
|
},
|
||||||
|
commands: {
|
||||||
|
'mac': [
|
||||||
|
{ label: '安装 wget (系统)', cmd: 'brew install wget' },
|
||||||
|
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
|
||||||
|
],
|
||||||
|
'linux': [
|
||||||
|
{ label: '安装 git (系统)', cmd: 'sudo apt install git' },
|
||||||
|
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
|
||||||
|
],
|
||||||
|
'win-ps': [
|
||||||
|
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
|
||||||
|
],
|
||||||
|
'win-cmd': [
|
||||||
|
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
expectedCmd: {
|
expectedCmd: {
|
||||||
'common': 'pip install requests'
|
// Fallback/Legacy
|
||||||
|
'mac': 'brew install wget',
|
||||||
|
'linux': 'sudo apt install git',
|
||||||
|
'win-ps': 'pip install requests',
|
||||||
|
'win-cmd': 'pip install requests'
|
||||||
},
|
},
|
||||||
validate: (cmd) => cmd.trim() === 'pip install requests',
|
validate: (cmd, os) => {
|
||||||
output: () => `
|
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'
|
||||||
|
return c === 'pip install requests'
|
||||||
|
},
|
||||||
|
output: (os, cmd) => { // Modified to accept cmd
|
||||||
|
const c = cmd ? cmd.trim() : ''
|
||||||
|
|
||||||
|
// Python requests output
|
||||||
|
if (c.includes('pip install requests')) {
|
||||||
|
return `
|
||||||
Downloading/unpacking requests
|
Downloading/unpacking requests
|
||||||
Downloading requests-2.31.0-py3-none-any.whl (62kB): 62kB downloaded
|
Downloading requests-2.31.0-py3-none-any.whl (62kB): 62kB downloaded
|
||||||
Installing collected packages: requests
|
Installing collected packages: requests
|
||||||
Successfully installed requests
|
Successfully installed requests
|
||||||
Cleaning up...`
|
Cleaning up...`
|
||||||
|
}
|
||||||
|
|
||||||
|
// System tools output
|
||||||
|
if (os === 'mac') {
|
||||||
|
return `
|
||||||
|
==> Downloading https://ghcr.io/v2/homebrew/core/wget/manifests/1.21.4
|
||||||
|
######################################################################## 100.0%
|
||||||
|
==> Installing wget
|
||||||
|
🍺 /usr/local/Cellar/wget/1.21.4: 90 files, 4.2MB`
|
||||||
|
}
|
||||||
|
if (os === 'linux') {
|
||||||
|
return `
|
||||||
|
Reading package lists... Done
|
||||||
|
Building dependency tree... Done
|
||||||
|
The following NEW packages will be installed:
|
||||||
|
git
|
||||||
|
0 upgraded, 1 newly installed, 0 to remove.
|
||||||
|
Get:1 http://archive.ubuntu.com/ubuntu jammy/main amd64 git amd64 1:2.34.1 [3MB]
|
||||||
|
Fetched 3MB in 1s (2560 kB/s)
|
||||||
|
Setting up git (1:2.34.1-1ubuntu1.9) ...`
|
||||||
|
}
|
||||||
|
return `Successfully installed.`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '第六步:打扫战场',
|
title: '第六步:打扫战场',
|
||||||
@@ -288,7 +361,7 @@ const executeCommand = () => {
|
|||||||
// Check if it matches current task requirement
|
// 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
|
// Success
|
||||||
const out = currentTask.value.output(currentOS.value)
|
const out = currentTask.value.output(currentOS.value, cmd) // Pass cmd to output
|
||||||
if (out) {
|
if (out) {
|
||||||
history.value.push({ type: 'output', content: out })
|
history.value.push({ type: 'output', content: out })
|
||||||
}
|
}
|
||||||
@@ -320,7 +393,6 @@ const nextTask = () => {
|
|||||||
if (currentTaskIndex.value < tasks.length - 1) {
|
if (currentTaskIndex.value < tasks.length - 1) {
|
||||||
currentTaskIndex.value++
|
currentTaskIndex.value++
|
||||||
isTaskCompleted.value = false
|
isTaskCompleted.value = false
|
||||||
isAiOpen.value = false
|
|
||||||
// Clear history to keep it clean? Or keep it? Let's keep it but maybe add a separator
|
// 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()
|
scrollToBottom()
|
||||||
@@ -330,7 +402,6 @@ const nextTask = () => {
|
|||||||
const resetCurrentTask = () => {
|
const resetCurrentTask = () => {
|
||||||
isTaskCompleted.value = false
|
isTaskCompleted.value = false
|
||||||
inputCmd.value = ''
|
inputCmd.value = ''
|
||||||
isAiOpen.value = false
|
|
||||||
history.value = []
|
history.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +497,6 @@ watch(currentOS, () => {
|
|||||||
.ai-header {
|
.ai-header {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background: linear-gradient(to right, rgba(16, 185, 129, 0.1), transparent);
|
background: linear-gradient(to right, rgba(16, 185, 129, 0.1), transparent);
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -440,16 +510,6 @@ watch(currentOS, () => {
|
|||||||
background: linear-gradient(to right, rgba(16, 185, 129, 0.2), transparent);
|
background: linear-gradient(to right, rgba(16, 185, 129, 0.2), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-header.active .ai-arrow {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-arrow {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat {
|
.ai-chat {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-top: 1px solid var(--vp-c-divider);
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
@@ -478,15 +538,22 @@ watch(currentOS, () => {
|
|||||||
border-bottom-left-radius: 2px;
|
border-bottom-left-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cmd-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.copy-btn {
|
.copy-btn {
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
padding: 2px 8px;
|
padding: 4px 10px;
|
||||||
border: 1px solid var(--vp-c-brand);
|
border: 1px solid var(--vp-c-brand);
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-btn:hover {
|
.copy-btn:hover {
|
||||||
@@ -633,6 +700,19 @@ watch(currentOS, () => {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enter-hint {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
animation: blink 1.5s infinite;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
.line.output {
|
.line.output {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
|
|||||||
@@ -31,10 +31,12 @@
|
|||||||
> 💡 **提示**:为了安全和方便,推荐你在下方的**网页模拟器**中操作。如果你有信心,也可以按照第 0 章的方法打开你电脑上真实的终端,跟随步骤一起练习(效果是一样的)。
|
> 💡 **提示**:为了安全和方便,推荐你在下方的**网页模拟器**中操作。如果你有信心,也可以按照第 0 章的方法打开你电脑上真实的终端,跟随步骤一起练习(效果是一样的)。
|
||||||
|
|
||||||
在这个练习中,你将学会:
|
在这个练习中,你将学会:
|
||||||
1. 查看当前有什么文件。
|
1. **查看文件**:学会用 `ls` 或 `dir` 看看当前目录下有什么。
|
||||||
2. 创建文件夹和文件。
|
2. **创建与进入**:学会用 `mkdir` 创建新文件夹,用 `cd` 像传送门一样进入它。
|
||||||
3. 删除它们。
|
3. **新建文件**:学会用命令快速创建一个新文件。
|
||||||
4. **学会向 AI 提问**:当你忘记命令时,如何让 AI 告诉你答案。
|
4. **安装软件**:体验一行代码安装 Python 库或系统软件的快感。
|
||||||
|
5. **删除清理**:学会如何删除不需要的文件(慎用!)。
|
||||||
|
6. **求助 AI**:这是最重要的!当你忘记命令时,学会问 AI:“在 Mac 上怎么删除文件?”,它会直接告诉你答案。
|
||||||
|
|
||||||
*请在下方选择你常用的操作系统,然后跟随引导开始操作:*
|
*请在下方选择你常用的操作系统,然后跟随引导开始操作:*
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user