feat: 更新附录交互组件和文档

This commit is contained in:
sanbuphy
2026-02-24 00:18:09 +08:00
parent d45df3cda5
commit 94f9db0834
88 changed files with 11797 additions and 7634 deletions
@@ -1,810 +1,172 @@
<!--
TcpHandshakeDemo.vue
TCP三次握手演示 - 紧凑交互版
设计理念
1. 循循善诱"打电话"的生活场景类比 TCP 连接建立过程
2. 紧凑布局保留核心可视化区使用固定底部详情板代替长列表
-->
<template>
<div class="tcp-compact">
<!-- 顶部标题与控制 -->
<div class="top-bar">
<div class="title-section">
<span class="app-icon">📞</span>
<span class="app-title">TCP 三次握手</span>
</div>
<div class="actions">
<button
v-if="currentStep < 3"
class="action-btn primary"
:disabled="currentStep >= 3"
@click="nextStep"
>
{{ currentStep === 0 ? '▶ 开始拨号' : '下一步 ➔' }}
</button>
<button
class="action-btn outline"
@click="reset"
>
重置
</button>
</div>
<div class="tcp-handshake-demo">
<div class="demo-header">
<span class="title">TCP 三次握手</span>
<span class="subtitle">建立可靠连接的过程</span>
</div>
<!-- 核心可视化舞台 -->
<div class="stage-area">
<!-- 左侧客户端/快递员 -->
<div class="handshake-flow">
<div class="actor client">
<div class="avatar-box">
<span class="avatar-icon">🧑💻</span>
<span class="avatar-label">客户端 ()</span>
</div>
<transition name="pop">
<div
v-if="currentStep >= 1"
class="bubble client"
>
{{ getBubbleText(1) }}
</div>
</transition>
<span class="actor-icon">🧑💻</span>
<span class="actor-name">客户端</span>
</div>
<!-- 中间连接状态线 -->
<div class="connection-line">
<div class="line-bg" />
<div
class="signal-packet"
:class="getSignalClass()"
>
<span
v-if="currentStep > 0"
class="packet-icon"
>{{ getSignalIcon() }}</span>
<div class="messages">
<div class="message-row">
<span class="msg-label">SYN</span>
<span class="msg-arrow"></span>
<span class="msg-desc">"我能连你吗?"</span>
</div>
<div
class="status-badge"
:class="{ connected: currentStep === 3 }"
>
{{ currentStep === 3 ? '✅ 连接建立' : '⏳ 连接中...' }}
<div class="message-row">
<span class="msg-desc">"能,你也能收到我吗?"</span>
<span class="msg-arrow"></span>
<span class="msg-label">SYN-ACK</span>
</div>
<div class="message-row">
<span class="msg-label">ACK</span>
<span class="msg-arrow"></span>
<span class="msg-desc">"能,开始吧!"</span>
</div>
</div>
<!-- 右侧服务器/收件人 -->
<div class="actor server">
<div class="avatar-box">
<span class="avatar-icon">🖥</span>
<span class="avatar-label">服务器</span>
</div>
<transition name="pop">
<div
v-if="currentStep >= 2"
class="bubble server"
>
{{ getBubbleText(2) }}
</div>
</transition>
<span class="actor-icon">🖥</span>
<span class="actor-name">服务器</span>
</div>
</div>
<!-- 步骤进度条 (可点击跳转) -->
<div class="step-indicator">
<div
v-for="(step, index) in steps"
:key="index"
class="step-dot"
:class="{ active: currentStep === index + 1, passed: currentStep > index + 1 }"
:title="step.techTitle"
@click="goToStep(index + 1)"
>
<span class="dot-num">{{ index + 1 }}</span>
<span
v-if="index < steps.length - 1"
class="dot-line"
/>
</div>
<div class="status-bar">
<span class="status-badge success"> 连接已建立</span>
</div>
<!-- 底部详情面板 (固定高度) -->
<div class="detail-panel">
<transition
name="fade"
mode="out-in"
>
<div
v-if="currentStep > 0"
:key="currentStep"
class="detail-content"
>
<div
class="detail-left"
:style="{ borderColor: getCurrentStepColor() }"
>
<div
class="step-badge"
:style="{ background: getCurrentStepColor() }"
>
步骤 {{ currentStep }}
</div>
</div>
<div class="detail-divider" />
<div class="detail-right">
<div class="info-row">
<span class="tag life">生活对话</span>
<span class="text highlight">{{ steps[currentStep-1].simpleTitle }}</span>
</div>
<div class="info-row">
<span class="tag tech">技术原理</span>
<div class="tech-content">
<div class="tech-desc">
{{ steps[currentStep-1].techDesc }}
</div>
<!-- 动态名词解码卡片 -->
<div class="term-glossary">
<div
v-for="term in steps[currentStep-1].terms"
:key="term.key"
class="term-item"
>
<span class="term-key">{{ term.key }}</span>
<span class="term-val">{{ term.val }}</span>
</div>
</div>
<!-- 代码实现细节 (折叠) -->
<details
v-if="steps[currentStep-1].codeImpl"
class="code-details"
>
<summary class="code-summary">
<span class="summary-icon">🛠</span>
<span class="summary-text">技术深究底层代码如何实现</span>
</summary>
<div class="code-block-wrapper">
<div class="code-title">
{{ steps[currentStep-1].codeImpl.title }}
</div>
<pre class="code-block"><code v-html="steps[currentStep-1].codeImpl.code" /></pre>
</div>
</details>
<!-- 技术问答 (折叠) - 仅在有问答时显示 -->
<details
v-if="steps[currentStep-1].qa"
class="code-details qa-details"
>
<summary class="code-summary qa-summary">
<span class="summary-icon">🎓</span>
<span class="summary-text">{{ steps[currentStep-1].qa.title }}</span>
</summary>
<div class="code-block-wrapper qa-content">
<div
v-for="(item, idx) in steps[currentStep-1].qa.content"
:key="idx"
class="qa-item"
>
<div class="qa-q">
Q: {{ item.q }}
</div>
<div
class="qa-a"
v-html="item.a"
/>
</div>
</div>
</details>
</div>
</div>
</div>
<!-- 下一步按钮 -->
<button
v-if="currentStep < 3"
class="next-btn"
@click="nextStep"
>
下一步
</button>
</div>
<div
v-else
class="detail-placeholder"
>
<span class="guide-bounce">📞</span>
<span>点击"开始拨号"或步骤圆点开始拨打电话</span>
</div>
</transition>
<div class="info-box">
<strong>核心思想</strong>
三次握手确保双方都能收发数据就像打电话时互相确认"能听到吗"
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{
simpleTitle: '喂,在家吗?我是快递员!',
techTitle: 'SYN',
techDesc: '客户端发送 SYN 包 (Seq=x),请求建立连接。',
color: '#3b82f6',
terms: [
{ key: 'SYN', val: '是单词 Synchronize (同步) 的缩写。💡 为什么叫"同步"?因为建立连接的第一步,就是双方要"对表",把暗号(序号)对齐,确保后续对话在同一个频道上。' },
{ key: 'Seq=x', val: '意思是:"我的数据计数器,从 x 开始"。💡 为什么要特意告诉对方?这就像是两个人约定"暗号"。我告诉你:"我的暗号是从 x 开始算的"。以后我每发给你一个字,暗号就加 1。如果你不知道我的起始暗号是 x,以后收到 x+100 你就不知道它是第几个字,也没法判断中间有没有丢字。' }
],
codeImpl: {
title: '💻 真实 TCP 报文构建 (伪代码)',
code: `// 1. 设置标志位:只置 SYN
<span class="kw">tcph->syn</span> = 1;
<span class="kw">tcph->ack</span> = 0;
// 2. 生成随机序号 (Seq)
// 操作系统内核会维护一个计数器
// 这里的 htonl 是为了处理网络字节序
<span class="kw">tcph->seq</span> = htonl(<span class="var">random_x</span>);
// 3. 发送数据包
send_packet(client_socket, tcph);`
}
},
{
simpleTitle: '在的!我听到了,请说!',
techTitle: 'SYN-ACK',
techDesc: '服务器回复 SYN-ACK 包 (Seq=y, Ack=x+1),确认并请求连接。',
color: '#10b981',
terms: [
{ key: 'ACK', val: '是单词 Acknowledgment (确认) 的缩写。💡 就像快递签收单,表示"我收到你的请求了"。' },
{ key: 'Ack=x+1', val: '确认号。💡 为什么要 +1?这是一种期待机制。意思是:"x 号那一页我已经收好了,请你下次从 x+1 页开始讲"。' },
{ key: 'Seq=y', val: '服务器也生成自己的随机序号 y,方便客户端确认服务器发来的话。' }
],
codeImpl: {
title: '💻 服务器内核响应逻辑 (伪代码)',
code: `// 1. 检查收到的是否是 SYN
if (<span class="kw">recv_tcph->syn</span> == 1) {
// 2. 准备回复包
<span class="kw">reply_tcph->syn</span> = 1; // 同步
<span class="kw">reply_tcph->ack</span> = 1; // 确认
// 3. 确认号 = 对方 Seq + 1
// 表示期待对方下一次发在这个序号之后的数据
<span class="kw">reply_tcph->ack_seq</span> = htonl(ntohl(<span class="var">recv_tcph->seq</span>) + 1);
// 4. 生成服务器自己的序号
<span class="kw">reply_tcph->seq</span> = htonl(<span class="var">random_y</span>);
send_packet(server_socket, reply_tcph);
}`
}
},
{
simpleTitle: '好的,那我开始说了!',
techTitle: 'ACK',
techDesc: '客户端发送 ACK 包 (Ack=y+1),连接建立成功,可以传输数据。',
color: '#8b5cf6',
terms: [
{ key: 'Ack=y+1', val: '客户端确认收到。意思是:"服务器你的 y 号信我也收到了,我们正式开始聊天吧!"' },
{ key: '连接建立', val: '双方都确认了对方"能听能说",通道正式打通。' }
],
codeImpl: {
title: '💻 客户端最终确认 (伪代码)',
code: `// 1. 检查收到的包
if (<span class="kw">recv_tcph->syn</span> == 1 && <span class="kw">recv_tcph->ack</span> == 1) {
// 2. 准备 ACK 包
<span class="kw">ack_tcph->syn</span> = 0; // 第三次握手不需要 SYN 了
<span class="kw">ack_tcph->ack</span> = 1;
// 3. 确认号 = 服务器 Seq + 1
<span class="kw">ack_tcph->ack_seq</span> = htonl(ntohl(<span class="var">recv_tcph->seq</span>) + 1);
// 4. 序号 = 自己的 Seq + 1
<span class="kw">ack_tcph->seq</span> = htonl(<span class="var">my_seq</span> + 1);
// 5. 连接状态变为 ESTABLISHED
<span class="hl">socket->state = TCP_ESTABLISHED;</span>
send_packet(client_socket, ack_tcph);
}`
},
qa: {
title: '🤔 为什么必须是三次?(核心逻辑)',
content: [
{
q: '为什么一定要三次?(双工确认原理)',
a: `这其实是在验证<strong>双方的"听说能力"</strong>是否正常。TCP 是全双工的(双方都能同时发和收),所以必须双方都确认对方能发能收:<br>
1️⃣ <strong>第一次 (Client -> Server)</strong>Server 收到,证明 <strong>Client 能发</strong><strong>Server 能收</strong>。<br>
2️⃣ <strong>第二次 (Server -> Client)</strong>Client 收到,证明 <strong>Server 能发</strong><strong>Client 能收</strong>。同时 Client 知道 Server 收到了自己的第一次请求。<br>
3️⃣ <strong>第三次 (Client -> Server)</strong>Server 收到,证明 <strong>Client 能收</strong>(因为 Client 回复了 Server 的消息)。<br>
<br>
<strong>结论:</strong> 只有经过这三次,双方都明确知道"自己"和"对方"的发送、接收功能全是好的。少一次都不行(Server 不知道 Client 能不能收),多一次没必要。`
},
{
q: '为什么这就算"连上"了?',
a: `所谓的"连接建立",在计算机里并不是真的拉了一根线。它的本质是:<strong>双方内存里都保存好了对方的"状态信息"</strong>。<br>
通过这三次握手,双方主要完成了两件事:<br>
1. <strong>确认通道畅通</strong>:就是上面说的双工能力确认。<br>
2. <strong>同步初始序号 (ISN)</strong>:双方交换了 Seq (x 和 y)。<br>
<br>
只要双方都记住了对方的 Seq,并且确认了对方在线,操作系统就会把 Socket 状态标记为 <code style="color:#10b981">ESTABLISHED</code> (已建立),这就叫"连上了"。`
}
]
}
}
]
const nextStep = () => {
if (currentStep.value < 3) currentStep.value++
}
const goToStep = (step) => {
currentStep.value = step
}
const reset = () => {
currentStep.value = 0
}
const getBubbleText = (stepIndex) => {
// stepIndex 1: Client speaks (Step 1)
// stepIndex 2: Server speaks (Step 2)
if (stepIndex === 1) return steps[0].simpleTitle
if (stepIndex === 2) return steps[1].simpleTitle
return ''
}
const getSignalClass = () => {
if (currentStep.value === 1) return 'sending' // Left to Right
if (currentStep.value === 2) return 'receiving' // Right to Left
if (currentStep.value === 3) return 'sending-final' // Left to Right
return ''
}
const getSignalIcon = () => {
return '🔔'
}
const getCurrentStepColor = () => {
return steps[currentStep.value - 1]?.color || '#ccc'
}
</script>
<style scoped>
.tcp-compact {
.tcp-handshake-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
padding: 16px;
margin: 16px 0;
font-size: 14px;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title-section {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn.primary {
background: var(--vp-c-brand);
color: white;
border: 1px solid var(--vp-c-brand);
}
.action-btn.outline {
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
/* 舞台区 */
.stage-area {
display: flex;
justify-content: space-between;
align-items: center;
height: 140px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0 30px;
position: relative;
margin-bottom: 20px;
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.demo-header {
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
display: block;
}
.demo-header .subtitle {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.handshake-flow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem 0;
margin-bottom: 0.75rem;
}
.actor {
display: flex;
flex-direction: column;
align-items: center;
width: 120px;
position: relative;
gap: 0.25rem;
}
.avatar-box {
.actor-icon {
font-size: 1.5rem;
}
.actor-name {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.messages {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
z-index: 2;
gap: 0.4rem;
}
.avatar-icon { font-size: 32px; }
.avatar-label { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
/* 气泡 */
.bubble {
position: absolute;
top: -40px;
background: white;
padding: 6px 10px;
border-radius: 12px;
font-size: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
white-space: nowrap;
border: 1px solid var(--vp-c-divider);
color: #333;
}
.bubble.client { left: 50%; transform: translateX(-50%); }
.bubble.server { left: 50%; transform: translateX(-50%); }
.pop-enter-active, .pop-leave-active { transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.pop-enter-from, .pop-leave-to { opacity: 0; transform: translateX(-50%) scale(0.8); }
/* 连接线 */
.connection-line {
flex: 1;
height: 2px;
background: var(--vp-c-divider);
margin: 0 20px;
position: relative;
.message-row {
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
gap: 0.4rem;
padding: 0.35rem 0.5rem;
background: var(--vp-c-bg);
border-radius: 4px;
}
.msg-label {
font-size: 0.7rem;
font-weight: bold;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
min-width: 50px;
text-align: center;
}
.msg-arrow {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.msg-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
flex: 1;
text-align: center;
}
.status-bar {
text-align: center;
margin-bottom: 0.75rem;
}
.status-badge {
position: absolute;
top: 15px;
font-size: 12px;
color: var(--vp-c-text-3);
transition: all 0.3s;
}
.status-badge.connected { color: var(--vp-c-brand); font-weight: bold; }
.signal-packet {
position: absolute;
width: 24px;
height: 24px;
background: var(--vp-c-brand);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
opacity: 0;
top: -11px;
}
.signal-packet.sending {
animation: moveRight 1.5s forwards;
opacity: 1;
}
.signal-packet.receiving {
animation: moveLeft 1.5s forwards;
opacity: 1;
background: #10b981;
}
.signal-packet.sending-final {
animation: moveRight 1.5s forwards;
opacity: 1;
background: #8b5cf6;
}
@keyframes moveRight {
0% { left: 0; }
100% { left: 100%; }
}
@keyframes moveLeft {
0% { left: 100%; }
100% { left: 0; }
}
/* 步骤指示器 */
.step-indicator {
display: flex;
justify-content: center;
gap: 40px;
margin-bottom: 20px;
}
.step-dot {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: var(--vp-c-text-3);
cursor: pointer;
position: relative;
transition: all 0.3s;
}
.step-dot.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
transform: scale(1.1);
}
.step-dot.passed {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.dot-line {
position: absolute;
left: 24px;
width: 40px;
height: 2px;
background: var(--vp-c-divider);
}
/* 详情面板 */
.detail-panel {
min-height: 80px; /* 改为最小高度 */
background: var(--vp-c-bg-alt);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
padding: 12px 16px;
display: flex;
align-items: flex-start; /* 顶部对齐 */
/* overflow: hidden; 移除隐藏 */
}
.detail-content {
display: flex;
width: 100%;
align-items: flex-start; /* 顶部对齐 */
}
.detail-left {
padding-right: 16px;
border-right: 2px solid transparent;
margin-top: 4px; /* 微调对齐 */
}
.step-badge {
padding: 4px 8px;
border-radius: 4px;
color: white;
font-size: 12px;
font-weight: bold;
white-space: nowrap;
}
.detail-divider {
width: 1px;
align-self: stretch; /* 拉伸高度 */
background: var(--vp-c-divider);
margin: 0 16px;
}
.detail-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px; /* 增加间距 */
padding-bottom: 4px;
}
.info-row {
display: flex;
align-items: flex-start; /* 顶部对齐 */
gap: 8px;
}
.tag {
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
margin-top: 2px;
}
.tag.life { background: #e6f7ff; color: #1890ff; }
.tag.tech { background: #f6ffed; color: #52c41a; }
.text {
font-size: 13px;
color: var(--vp-c-text-1);
line-height: 1.5;
}
.text.highlight {
display: inline-block;
padding: 0.3rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
color: var(--vp-c-brand);
}
/* 新增:术语解释样式 */
.term-glossary {
margin-top: 8px;
.status-badge.success {
background: #d1fae5;
color: #059669;
}
.info-box {
display: flex;
flex-direction: column;
gap: 6px;
background: rgba(0,0,0,0.03);
padding: 8px;
gap: 0.25rem;
background-color: var(--vp-c-bg-alt);
padding: 0.5rem 0.75rem;
border-radius: 6px;
}
.term-item {
font-size: 12px;
line-height: 1.5;
font-size: 0.8rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}
.term-key {
font-weight: bold;
color: var(--vp-c-brand-dark);
margin-right: 6px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
padding: 0 4px;
border-radius: 3px;
}
.next-btn {
padding: 6px 16px;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin-left: 12px;
margin-top: 4px;
.info-box strong {
white-space: nowrap;
align-self: flex-start; /* 按钮顶部对齐 */
}
.detail-placeholder {
display: flex;
align-items: center;
gap: 8px;
color: var(--vp-c-text-3);
width: 100%;
justify-content: center;
}
.guide-bounce {
animation: bounce 1.5s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
/* 代码实现折叠块 */
.code-details {
margin-top: 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
background: var(--vp-c-bg-alt);
}
.code-summary {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
user-select: none;
background: rgba(0,0,0,0.02);
transition: background 0.2s;
}
.code-summary:hover {
background: rgba(0,0,0,0.05);
color: var(--vp-c-brand);
}
.code-block-wrapper {
padding: 12px;
border-top: 1px solid var(--vp-c-divider);
background: #282c34; /* 深色背景适合代码 */
color: #abb2bf;
}
.code-title {
font-size: 11px;
color: #61afef;
margin-bottom: 6px;
font-family: monospace;
font-weight: bold;
}
.code-block {
margin: 0;
font-family: var(--vp-font-family-mono);
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
/* 语法高亮类 (深色模式) */
:deep(.kw) { color: #c678dd; } /* 紫色 - 关键字/字段 */
:deep(.var) { color: #d19a66; } /* 橙色 - 变量 */
:deep(.hl) { color: #98c379; font-weight: bold; } /* 绿色 - 高亮行 */
/* 问答折叠块 */
.qa-details {
background: rgba(255, 165, 0, 0.05); /* 淡淡的橙色背景 */
border-color: rgba(255, 165, 0, 0.2);
}
.qa-summary {
color: #d46b08;
}
.qa-summary:hover {
color: #ff7a45;
background: rgba(255, 165, 0, 0.1);
}
.qa-content {
background: var(--vp-c-bg); /* 恢复浅色/深色背景 */
flex-shrink: 0;
color: var(--vp-c-text-1);
padding: 16px;
}
.qa-item {
margin-bottom: 12px;
}
.qa-item:last-child { margin-bottom: 0; }
.qa-q {
font-weight: bold;
font-size: 13px;
color: var(--vp-c-brand-dark);
margin-bottom: 4px;
}
.qa-a {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
</style>