feat(docs): add terminal introduction appendix with interactive components

Add a comprehensive terminal introduction guide with interactive Vue components demonstrating terminal concepts. Includes:
- Terminal definition and architecture visualization
- Character grid and cell inspector
- ANSI escape sequences demo
- Input visualization and signal mechanisms
- Flow diagrams and TUI examples

The components are registered in the VitePress theme and linked from the appendix section. Each component includes detailed documentation and interactive elements to help users understand terminal principles.
This commit is contained in:
sanbuphy
2026-01-14 19:04:09 +08:00
parent a9f4071308
commit c238f07e0d
14 changed files with 4248 additions and 1 deletions
@@ -0,0 +1,246 @@
<!--
TerminalGrid.vue
终端网格模型演示组件
用途
展示终端屏幕本质上是由字符网格构成的
帮助用户理解终端不是像素画板而是由一个个固定大小的单元格Cell组成的矩阵
交互功能
- 点击/拖拽可以在网格上出字符
- 键盘输入可以直接在网格中打字观察光标移动和字符填充
- 响应式布局支持横向滚动适应不同屏幕宽度
-->
<template>
<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"
:key="cIndex"
:class="{
'active-cursor': cursor.r === rIndex && cursor.c === cIndex,
'drawn': cell.drawn
}"
@mousedown.prevent="handleCellMouseDown(rIndex, cIndex)"
@mouseover="handleCellHover(rIndex, cIndex)"
>
{{ cell.char || ' ' }}
</div>
</div>
</div>
<div class="controls">
<input
ref="inputRef"
type="text"
v-model="inputText"
placeholder="Type here..."
class="text-input"
@keydown="handleKeydown"
/>
<button class="btn" @click="clearGrid">Clear</button>
<span class="hint">Click/Drag cells to draw, Type to insert text</span>
</div>
</div>
</template>
<script setup>
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 rows = reactive(createGrid())
const cursor = reactive({ r: 0, c: 0 })
const inputText = ref('')
const isDrawing = ref(false)
const inputRef = ref(null)
const drawingListener = () => {
isDrawing.value = false
}
const handleKeydown = (e) => {
if (e.key === 'Backspace') {
if (cursor.c > 0) {
cursor.c--
} else if (cursor.r > 0) {
cursor.r--
cursor.c = COL_COUNT - 1
}
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
}
}
const advanceCursor = () => {
cursor.c++
if (cursor.c >= COL_COUNT) {
cursor.c = 0
cursor.r++
if (cursor.r >= ROW_COUNT) {
cursor.r = ROW_COUNT - 1 // Stop at bottom
}
}
}
const handleCellMouseDown = (r, c) => {
isDrawing.value = true
rows[r][c].drawn = !rows[r][c].drawn
cursor.r = r
cursor.c = c
if (inputRef.value) {
inputRef.value.focus()
}
}
const handleCellHover = (r, c) => {
if (isDrawing.value) {
rows[r][c].drawn = true
}
}
const clearGrid = () => {
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
}
}
cursor.r = 0
cursor.c = 0
inputText.value = ''
if (inputRef.value) {
inputRef.value.focus()
}
}
onMounted(() => {
window.addEventListener('mouseup', drawingListener)
})
onBeforeUnmount(() => {
window.removeEventListener('mouseup', drawingListener)
})
</script>
<style scoped>
.grid-demo {
background: #09090b;
padding: 20px;
border-radius: 12px;
border: 1px solid #27272a;
font-family: 'JetBrains Mono', 'Menlo', 'Monaco', monospace;
overflow: hidden; /* 防止内容溢出圆角 */
}
.terminal-screen {
border: 1px solid #27272a;
background: #000;
cursor: text;
display: block;
overflow-x: auto; /* 允许横向滚动 */
max-width: 100%;
border-radius: 6px;
scrollbar-width: thin; /* Firefox */
scrollbar-color: #3f3f46 #18181b;
}
/* Webkit scrollbar styles */
.terminal-screen::-webkit-scrollbar {
height: 8px;
}
.terminal-screen::-webkit-scrollbar-track {
background: #18181b;
}
.terminal-screen::-webkit-scrollbar-thumb {
background-color: #3f3f46;
border-radius: 4px;
}
.grid-row {
display: flex;
width: max-content; /* 确保内容撑开宽度 */
}
.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;
}
.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>