feat: 更新附录交互组件和文档
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user