docs: update Chinese documentation and add Vue components

- Update AI capability dictionary by removing redundant mention of Baidu's model
- Add new Vue components for context engineering visualization (IntroProblemReasonSolution, MemoryPalaceDemo, MemoryPalaceActionDemo, KVCacheDemo, LostInMiddleDemo)
- Register new components in theme index.js
- Enhance audio introduction with new interactive demos (AudioQuickStartDemo, MelSpectrogramDemo, TTSPipelineDemo, VoiceCloningDemo, ASRvsTTSDemo, AudioTokenizationDemo, EmotionControlDemo)
- Improve existing context engineering demos with Chinese localization and better tokenization
- Fix Japanese documentation layout by properly closing NavGrid components
This commit is contained in:
sanbuphy
2026-02-03 19:41:14 +08:00
parent e5b1c6cc88
commit 084ebed417
30 changed files with 11563 additions and 2126 deletions
@@ -0,0 +1,506 @@
<!--
BrowserRenderingDemo.vue
浏览器渲染演示 - 拆开包裹/装修房子类比
压缩版横向延展减少纵向高度
-->
<template>
<div class="unboxing-demo">
<!-- 紧凑头部标题+场景+步骤导航合并 -->
<div class="compact-header">
<div class="header-left">
<span class="title-icon">[包裹]</span>
<span class="header-title">拆开包裹代码如何变成画面</span>
</div>
<div class="header-steps">
<button
v-for="(step, index) in steps"
:key="step.id"
@click="goToStep(index)"
class="step-chip"
:class="{ active: currentStep === index, completed: currentStep > index }"
>
<span class="chip-num">{{ index + 1 }}</span>
<span class="chip-name">{{ step.shortName }}</span>
</button>
</div>
</div>
<!-- 横向主区域代码输入 | 处理可视化 | 屏幕输出 -->
<div class="main-stage">
<!-- 左侧代码输入 -->
<div class="stage-box input-box">
<div class="box-label">[代码包] 收到的代码</div>
<pre class="code-mini">&lt;div class="box"&gt;
&lt;h1&gt;Hello&lt;/h1&gt;
&lt;/div&gt;
&lt;style&gt;
.box { background: blue; }
&lt;/style&gt;</pre>
</div>
<!-- 中间处理过程 -->
<div class="stage-box process-box">
<div class="box-label">[{{ stepIcons[currentStep] }}] {{ steps[currentStep]?.name }}</div>
<!-- 步骤1: 解析HTML -->
<div v-if="currentStep === 0" class="process-content">
<div class="mini-tree">
<span class="tree-line">html</span>
<span class="tree-line indent"> body</span>
<span class="tree-line indent2 highlight"> div.box</span>
</div>
</div>
<!-- 步骤2: 解析CSS -->
<div v-else-if="currentStep === 1" class="process-content">
<div class="mini-css">
<span class="css-sel">.box</span> {<br/>
&nbsp;&nbsp;background: <span class="css-val">blue</span>;<br/>
}
</div>
</div>
<!-- 步骤3: 合并渲染树 -->
<div v-else-if="currentStep === 2" class="process-content">
<div class="mini-render">
<span class="render-tag">div.box</span>
<span class="render-arrow"></span>
<span class="render-style">蓝色背景</span>
</div>
</div>
<!-- 步骤4: 计算布局 -->
<div v-else-if="currentStep === 3" class="process-content">
<div class="mini-layout" :style="{ width: boxSize + 'px', height: boxSize + 'px' }">
{{ boxSize }}×{{ boxSize }}
</div>
</div>
<!-- 步骤5: 绘制像素 -->
<div v-else-if="currentStep === 4" class="process-content">
<div class="mini-layers">
<div class="layer-bar" :style="{ opacity: layerOp }">背景</div>
<div class="layer-bar" :style="{ opacity: layerOp }">文字</div>
</div>
</div>
<!-- 步骤6: 合成显示 -->
<div v-else class="process-content">
<div class="mini-final">
<strong>Hello</strong>
</div>
</div>
</div>
<!-- 右侧屏幕输出 -->
<div class="stage-box output-box">
<div class="box-label">[屏幕] 显示结果</div>
<div class="screen-mini">
<div class="browser-bar-mini">
<span></span><span></span><span></span>
</div>
<div class="screen-content" :class="{ styled: currentStep >= 4, final: currentStep >= 5 }">
<template v-if="currentStep >= 5">
<strong>Hello</strong>
</template>
<template v-else-if="currentStep >= 3">
内容
</template>
<template v-else>
...
</template>
</div>
</div>
</div>
</div>
<!-- 双栏对照说明 - 横向排列 -->
<div class="dual-bar">
<div class="bar-side">
<span class="bar-label">[生活] {{ steps[currentStep]?.analogy }}</span>
</div>
<div class="bar-divider"></div>
<div class="bar-side">
<span class="bar-label">[技术] {{ steps[currentStep]?.desc }}</span>
</div>
</div>
<!-- 控制按钮 -->
<div class="control-bar">
<button class="bar-btn" @click="prevStep" :disabled="currentStep <= 0">[上一步]</button>
<button class="bar-btn primary" @click="nextStep" :disabled="currentStep >= steps.length - 1">
{{ currentStep >= steps.length - 1 ? '[完成]' : '[下一步]' }}
</button>
<button class="bar-btn" @click="currentStep = 0">[重置]</button>
</div>
<!-- 完整流程 - 横向紧凑 -->
<div class="flow-bar" v-if="currentStep >= 5">
<div class="flow-items">
<span v-for="(step, i) in steps" :key="i" class="flow-item-mini" :class="{ highlight: i === 5 }">
[{{ stepIcons[i] }}] {{ step.shortName }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const currentStep = ref(0)
const boxSize = ref(0)
const layerOp = ref(0)
const stepIcons = ['清单', '图纸', '组装', '测量', '上色', '完成']
const steps = [
{
id: 'parse',
name: '解析 HTML',
shortName: '解析',
desc: '浏览器读取HTML代码,理解页面结构。',
analogy: '拆开包裹,先看清单。'
},
{
id: 'cssom',
name: '解析 CSS',
shortName: '样式',
desc: '浏览器读取CSS样式,知道每个元素的样子。',
analogy: '看装修设计图。'
},
{
id: 'render',
name: '合并渲染树',
shortName: '合并',
desc: '把HTML结构和CSS样式结合。',
analogy: '把结构图和设计图结合。'
},
{
id: 'layout',
name: '计算布局',
shortName: '布局',
desc: '计算每个元素在屏幕上的位置和大小。',
analogy: '丈量房间尺寸。'
},
{
id: 'paint',
name: '绘制像素',
shortName: '绘制',
desc: '把颜色、文字绘制到屏幕上。',
analogy: '刷漆、贴壁纸。'
},
{
id: 'composite',
name: '合成显示',
shortName: '显示',
desc: '把所有图层合成,最终显示。',
analogy: '装修完成!'
}
]
const goToStep = (step) => { currentStep.value = step }
const nextStep = () => { if (currentStep.value < steps.length - 1) currentStep.value++ }
const prevStep = () => { if (currentStep.value > 0) currentStep.value-- }
watch(currentStep, (newStep) => {
if (newStep === 3) { boxSize.value = 0; setTimeout(() => boxSize.value = 60, 100) }
if (newStep === 4) { layerOp.value = 0; setTimeout(() => layerOp.value = 1, 100) }
})
</script>
<style scoped>
.unboxing-demo {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
margin: 16px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 紧凑头部 */
.compact-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
flex-wrap: wrap;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.title-icon {
font-size: 12px;
color: var(--vp-c-brand);
font-weight: 600;
}
.header-title {
font-size: 15px;
font-weight: 700;
color: var(--vp-c-text-1);
}
.header-steps {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.step-chip {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
cursor: pointer;
font-size: 11px;
transition: all 0.2s;
}
.step-chip:hover { border-color: var(--vp-c-brand); }
.step-chip.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.step-chip.completed { border-color: #67c23a; color: #67c23a; }
.chip-num {
width: 14px;
height: 14px;
background: var(--vp-c-divider);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
}
.step-chip.active .chip-num { background: rgba(255,255,255,0.3); }
/* 横向主区域 */
.main-stage {
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
gap: 12px;
margin-bottom: 12px;
}
.stage-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 10px;
min-height: 100px;
}
.box-label {
font-size: 11px;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid var(--vp-c-divider);
}
/* 代码输入 */
.code-mini {
font-family: monospace;
font-size: 10px;
line-height: 1.4;
color: var(--vp-c-text-2);
margin: 0;
white-space: pre-wrap;
}
/* 处理过程 */
.process-content {
display: flex;
align-items: center;
justify-content: center;
min-height: 70px;
}
.mini-tree {
font-family: monospace;
font-size: 10px;
line-height: 1.5;
}
.tree-line { display: block; }
.indent { margin-left: 12px; }
.indent2 { margin-left: 24px; }
.highlight { color: var(--vp-c-brand); font-weight: 600; }
.mini-css {
font-family: monospace;
font-size: 10px;
background: var(--vp-c-bg-soft);
padding: 8px 12px;
border-radius: 4px;
}
.css-sel { color: #e6a23c; }
.css-val { color: #409eff; }
.mini-render {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
}
.render-tag {
padding: 3px 8px;
background: var(--vp-c-brand);
color: white;
border-radius: 3px;
font-size: 10px;
}
.render-arrow { color: var(--vp-c-text-3); }
.render-style { color: var(--vp-c-text-2); }
.mini-layout {
background: var(--vp-c-brand);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 10px;
font-weight: 600;
transition: all 0.4s;
}
.mini-layers {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
.layer-bar {
padding: 4px 8px;
background: var(--vp-c-bg-soft);
border-radius: 3px;
font-size: 10px;
text-align: center;
transition: opacity 0.4s;
}
.mini-final {
padding: 12px 20px;
background: linear-gradient(135deg, #409eff, #67c23a);
color: white;
border-radius: 4px;
font-size: 14px;
}
/* 屏幕输出 */
.screen-mini {
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
}
.browser-bar-mini {
display: flex;
gap: 3px;
padding: 4px 6px;
background: var(--vp-c-divider);
}
.browser-bar-mini span {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ccc;
}
.browser-bar-mini span:nth-child(1) { background: #ff5f57; }
.browser-bar-mini span:nth-child(2) { background: #febc2e; }
.browser-bar-mini span:nth-child(3) { background: #28c840; }
.screen-content {
padding: 16px;
text-align: center;
font-size: 11px;
color: var(--vp-c-text-3);
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.screen-content.styled {
background: #409eff;
color: white;
}
.screen-content.final {
background: linear-gradient(135deg, #409eff, #67c23a);
}
/* 双栏对照 */
.dual-bar {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 8px;
align-items: center;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 10px;
}
.bar-side { text-align: center; }
.bar-label {
font-size: 11px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.bar-divider {
font-size: 12px;
color: var(--vp-c-brand);
font-weight: 700;
}
/* 控制按钮 */
.control-bar {
display: flex;
justify-content: center;
gap: 10px;
}
.bar-btn {
padding: 6px 16px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 14px;
cursor: pointer;
font-size: 11px;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.bar-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.bar-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.bar-btn.primary { background: var(--vp-c-brand); border-color: var(--vp-c-brand); color: white; }
/* 流程条 */
.flow-bar {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--vp-c-divider);
}
.flow-items {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.flow-item-mini {
padding: 4px 10px;
background: var(--vp-c-bg);
border-radius: 12px;
font-size: 10px;
color: var(--vp-c-text-2);
}
.flow-item-mini.highlight {
background: linear-gradient(135deg, #67c23a, #85ce61);
color: white;
}
@media (max-width: 768px) {
.main-stage { grid-template-columns: 1fr; }
.compact-header { flex-direction: column; align-items: flex-start; }
.dual-bar { grid-template-columns: 1fr; gap: 4px; }
.bar-divider { display: none; }
}
</style>
@@ -0,0 +1,724 @@
<!--
DnsLookupDemo.vue
DNS查询演示 - 查地址簿 vs 真实DNS查询 双栏对照
用途
"查地址簿"的生活化比喻配合真实DNS查询过程
让0基础用户理解域名如何转换成IP地址
-->
<template>
<div class="dns-lookup-demo">
<!-- 标题区 -->
<div class="demo-header">
<div class="header-title">
<span class="title-icon">[地址簿]</span>
<span>查地址簿 vs DNS查询</span>
</div>
<div class="header-subtitle">生活比喻 技术实现 双栏对照</div>
</div>
<!-- 场景设置 -->
<div class="scenario-setup">
<div class="setup-text">
快递员要送包裹给 <strong>"{{ currentTarget.name }}"</strong>{{ currentTarget.domain }}
但他只知道名字不知道具体门牌号...
</div>
<div class="target-selector">
<span class="selector-label">换个目标</span>
<button
v-for="target in targets"
:key="target.name"
@click="selectTarget(target)"
class="target-chip"
:class="{ active: currentTarget.name === target.name }"
:disabled="isSearching"
>
{{ target.name }}
</button>
</div>
</div>
<!-- 开始查询按钮 -->
<div class="start-action" v-if="!isSearching && !showResult">
<button class="start-btn" @click="startSearch">
[查询] 开始查询地址
</button>
</div>
<!-- 双栏对照展示 -->
<div class="comparison-container" v-if="isSearching || showResult">
<!-- 左侧生活比喻查地址簿 -->
<div class="comparison-side analogy-side">
<div class="side-header">
<span class="side-icon">[生活]</span>
<span class="side-title">查地址簿流程</span>
</div>
<div class="analogy-flow">
<div
v-for="(level, index) in queryLevels"
:key="level.id"
class="flow-level"
:class="{
passed: currentStep > index,
current: currentStep === index,
pending: currentStep < index
}"
>
<div class="level-icon">{{ level.analogyIcon }}</div>
<div class="level-content">
<div class="level-name">{{ level.analogyName }}</div>
<div class="level-role">{{ level.analogyRole }}</div>
<div class="level-action" v-if="currentStep === index">
<span class="action-text">{{ level.analogyAction }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 中间连接指示 -->
<div class="connection-indicator">
<div class="indicator-line" v-for="i in 5" :key="i">
<span class="indicator-arrow"></span>
<span class="indicator-text">对应</span>
</div>
</div>
<!-- 右侧技术实现真实DNS -->
<div class="comparison-side tech-side">
<div class="side-header">
<span class="side-icon">[技术]</span>
<span class="side-title">DNS查询流程</span>
</div>
<div class="tech-flow">
<div
v-for="(level, index) in queryLevels"
:key="level.id"
class="flow-level"
:class="{
passed: currentStep > index,
current: currentStep === index,
pending: currentStep < index
}"
>
<div class="level-icon" :style="{ background: level.techColor }">{{ level.techIcon }}</div>
<div class="level-content">
<div class="level-name">{{ level.techName }}</div>
<div class="level-role">{{ level.techRole }}</div>
<div class="level-action" v-if="currentStep === index">
<code class="action-code">{{ level.techAction }}</code>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 查询结果 -->
<div class="result-section" v-if="showResult">
<div class="result-card">
<div class="result-header">[成功] 查询成功</div>
<div class="result-body">
<div class="result-row">
<span class="result-label">域名名字</span>
<code class="result-value">{{ currentTarget.domain }}</code>
</div>
<div class="result-row">
<span class="result-label">IP地址门牌号</span>
<code class="result-value highlight">{{ currentTarget.ip }}</code>
</div>
</div>
</div>
<!-- 技术说明卡片 -->
<div class="tech-explanation">
<div class="explanation-header">
<span class="explanation-icon">[详解]</span>
<span>DNS查询技术详解</span>
</div>
<div class="explanation-body">
<div class="explanation-item">
<strong>[查询] 查询类型</strong>
<p><strong>递归查询</strong>浏览器只发一次请求本地DNS负责层层查询后返回结果像委托代理</p>
<p><strong>迭代查询</strong>每层只告诉下一层去哪查浏览器需要多次查询像自己跑腿</p>
</div>
<div class="explanation-item">
<strong>[缓存] 缓存机制</strong>
<p>查询结果会被缓存在浏览器操作系统路由器本地DNS服务器等多个层级下次直接返回大大加速访问</p>
</div>
<div class="explanation-item">
<strong>[] 根域名服务器</strong>
<p>全球只有13组根服务器字母A-M命名管理所有顶级域.com/.org/.cn等它们知道每个顶级域由谁管理</p>
</div>
</div>
</div>
<button class="reset-btn" @click="reset">[重置] 再查一次</button>
</div>
<!-- 层级说明未开始查询时显示 -->
<div class="levels-info" v-if="!isSearching && !showResult">
<div class="info-title">[对照] DNS查询层级对照表</div>
<div class="info-grid">
<div class="info-card" v-for="level in queryLevels" :key="level.id">
<div class="info-analogy">
<span class="info-icon">{{ level.analogyIcon }}</span>
<span class="info-name">{{ level.analogyName }}</span>
</div>
<div class="info-arrow"></div>
<div class="info-tech">
<span class="info-icon" :style="{ background: level.techColor }">{{ level.techIcon }}</span>
<span class="info-name">{{ level.techName }}</span>
</div>
<div class="info-desc">{{ level.description }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const targets = [
{ name: '百度', domain: 'baidu.com', ip: '110.242.68.66' },
{ name: '谷歌', domain: 'google.com', ip: '142.250.80.46' },
{ name: 'GitHub', domain: 'github.com', ip: '140.82.114.4' },
{ name: 'B站', domain: 'bilibili.com', ip: '120.92.78.57' }
]
const currentTarget = ref(targets[0])
const isSearching = ref(false)
const showResult = ref(false)
const currentStep = ref(0)
const queryLevels = [
{
id: 'browser',
analogyIcon: '自',
analogyName: '翻通讯录',
analogyRole: '快递员自己',
analogyAction: '先看看自己记没记过这个地址',
techIcon: '浏',
techName: '浏览器缓存',
techRole: '本地缓存',
techAction: '检查 DNS cache',
techColor: '#67c23a',
description: '浏览器会缓存最近访问过的域名,避免重复查询'
},
{
id: 'os',
analogyIcon: '本',
analogyName: '查记事本',
analogyRole: '自己的记录',
analogyAction: '看看之前有没有记过',
techIcon: '系',
techName: '操作系统缓存',
techRole: 'OS DNS Cache',
techAction: '检查 hosts 文件',
techColor: '#95d475',
description: '操作系统也有DNS缓存,hosts文件可手动指定域名映射'
},
{
id: 'recursive',
analogyIcon: '服',
analogyName: '社区服务中心',
analogyRole: '帮跑腿的人',
analogyAction: '帮用户查询,自己跑遍各部门',
techIcon: '递',
techName: '本地DNS服务器',
techRole: 'Recursive Resolver',
techAction: 'ISP DNS 查询',
techColor: '#409eff',
description: '通常由网络运营商提供,负责递归查询并缓存结果'
},
{
id: 'root',
analogyIcon: '根',
analogyName: '国务院',
analogyRole: '最高管理机构',
analogyAction: '.com 归谁管?去问它!',
techIcon: '根',
techName: '根域名服务器',
techRole: 'Root Server',
techAction: '返回 TLD 服务器地址',
techColor: '#e6a23c',
description: '全球13组,管理所有顶级域,知道.com/.cn等归谁管'
},
{
id: 'tld',
analogyIcon: '省',
analogyName: '省政府',
analogyRole: '省级管理机构',
analogyAction: 'baidu.com 归谁管?',
techIcon: '顶',
techName: '顶级域服务器',
techRole: 'TLD Server',
techAction: '返回权威DNS地址',
techColor: '#f56c6c',
description: '管理特定顶级域(如Verisign管理.com),知道具体域名归谁管'
},
{
id: 'auth',
analogyIcon: '户',
analogyName: '户籍系统',
analogyRole: '最终档案',
analogyAction: '查到具体门牌号了!',
techIcon: '权',
techName: '权威DNS服务器',
techRole: 'Authoritative DNS',
techAction: '返回 A 记录',
techColor: '#b37feb',
description: '域名所有者设置的DNS服务器,保存着域名到IP的最终映射'
}
]
const selectTarget = (target) => {
currentTarget.value = target
reset()
}
const startSearch = () => {
isSearching.value = true
showResult.value = false
currentStep.value = 0
// 模拟查询过程
const steps = [0, 1, 2, 3, 4, 5]
let i = 0
const nextStep = () => {
if (i < steps.length) {
currentStep.value = steps[i]
i++
setTimeout(nextStep, 800)
} else {
setTimeout(() => {
showResult.value = true
isSearching.value = false
}, 500)
}
}
nextStep()
}
const reset = () => {
isSearching.value = false
showResult.value = false
currentStep.value = 0
}
</script>
<style scoped>
.dns-lookup-demo {
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
}
/* 头部 */
.demo-header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.header-title {
font-size: 18px;
font-weight: 700;
color: var(--vp-c-text-1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.title-icon {
font-size: 14px;
color: var(--vp-c-brand);
font-weight: 600;
}
.header-subtitle {
font-size: 13px;
color: var(--vp-c-text-3);
margin-top: 4px;
}
/* 场景设置 */
.scenario-setup {
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(103, 194, 58, 0.1));
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.setup-text {
font-size: 14px;
color: var(--vp-c-text-1);
line-height: 1.6;
margin-bottom: 12px;
}
.setup-text strong {
color: var(--vp-c-brand);
}
.target-selector {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.selector-label {
font-size: 12px;
color: var(--vp-c-text-3);
}
.target-chip {
padding: 6px 12px;
background: white;
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
cursor: pointer;
font-size: 12px;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.target-chip:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.target-chip.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.target-chip:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 开始按钮 */
.start-action {
text-align: center;
margin-bottom: 20px;
}
.start-btn {
padding: 12px 32px;
background: linear-gradient(135deg, var(--vp-c-brand), #67c23a);
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: all 0.3s;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(64, 158, 255, 0.3);
}
/* 双栏对照容器 */
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
margin-bottom: 20px;
}
/* 侧边栏 */
.comparison-side {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
}
.side-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
font-weight: 600;
font-size: 14px;
color: white;
}
.analogy-side .side-header {
background: linear-gradient(90deg, #67c23a, #95d475);
}
.tech-side .side-header {
background: linear-gradient(90deg, #409eff, #79bbff);
}
.side-icon {
font-size: 12px;
font-weight: 600;
}
/* 流程展示 */
.analogy-flow, .tech-flow {
padding: 12px;
}
.flow-level {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
margin-bottom: 8px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
transition: all 0.3s;
opacity: 0.4;
}
.flow-level.passed {
opacity: 0.7;
border-color: #67c23a;
background: rgba(103, 194, 58, 0.1);
}
.flow-level.current {
opacity: 1;
border-color: var(--vp-c-brand);
background: rgba(64, 158, 255, 0.1);
transform: scale(1.02);
}
.level-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.analogy-flow .level-icon {
background: #fff3e0;
color: #666;
}
.level-content {
flex: 1;
}
.level-name {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.level-role {
font-size: 11px;
color: var(--vp-c-text-3);
}
.level-action {
margin-top: 4px;
}
.action-text {
font-size: 11px;
color: var(--vp-c-brand);
background: white;
padding: 2px 8px;
border-radius: 10px;
}
.action-code {
font-size: 10px;
color: var(--vp-c-brand);
background: white;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
/* 连接指示器 */
.connection-indicator {
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 40px 0;
}
.indicator-line {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.indicator-arrow {
font-size: 18px;
color: var(--vp-c-brand);
font-weight: bold;
}
.indicator-text {
font-size: 10px;
color: var(--vp-c-text-3);
writing-mode: vertical-rl;
}
/* 结果区域 */
.result-section {
animation: fadeIn 0.5s ease;
}
.result-card {
background: linear-gradient(135deg, #67c23a, #85ce61);
border-radius: 12px;
padding: 20px;
color: white;
margin-bottom: 16px;
}
.result-header {
font-size: 16px;
font-weight: 700;
margin-bottom: 12px;
}
.result-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 14px;
}
.result-label {
opacity: 0.9;
}
.result-value {
font-family: monospace;
background: rgba(255,255,255,0.2);
padding: 4px 8px;
border-radius: 4px;
}
.result-value.highlight {
background: rgba(255,255,255,0.3);
font-weight: 600;
}
/* 技术说明 */
.tech-explanation {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
}
.explanation-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
}
.explanation-icon {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-brand);
}
.explanation-item {
margin-bottom: 12px;
}
.explanation-item strong {
display: block;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.explanation-item p {
margin: 4px 0;
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
/* 重置按钮 */
.reset-btn {
display: block;
width: 100%;
padding: 10px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.reset-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
/* 层级信息 */
.levels-info {
margin-top: 20px;
}
.info-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.info-card {
background: white;
border-radius: 10px;
padding: 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.info-analogy, .info-tech {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.info-icon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.info-analogy .info-icon {
background: #fff3e0;
color: #666;
}
.info-name {
font-size: 12px;
font-weight: 600;
}
.info-arrow {
font-size: 16px;
color: var(--vp-c-brand);
margin: 4px 0;
}
.info-desc {
font-size: 11px;
color: var(--vp-c-text-3);
margin-top: 8px;
line-height: 1.4;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 响应式 */
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
.connection-indicator {
flex-direction: row;
padding: 8px 0;
justify-content: center;
gap: 16px;
}
.indicator-text {
writing-mode: horizontal-tb;
}
}
</style>
@@ -0,0 +1,533 @@
<!--
HttpExchangeDemo.vue
HTTP请求响应演示 - 快递员送达对话类比
用途
"快递员和收件人对话"的生活化比喻让用户理解HTTP请求和响应的过程
把枯燥的HTTP协议变成直观的对话场景
-->
<template>
<div class="delivery-dialog-demo">
<!-- 标题 -->
<div class="dialog-header">
<span class="dialog-icon">[送达]</span>
<span class="dialog-title">送达对话请求与响应</span>
</div>
<!-- 场景选择 -->
<div class="scenario-selector">
<div class="selector-label">选择送达场景</div>
<div class="scenario-buttons">
<button
v-for="scene in scenarios"
:key="scene.id"
@click="selectScenario(scene)"
class="scenario-btn"
:class="{ active: currentScenario?.id === scene.id }"
:disabled="isDelivering"
>
<span class="btn-text">{{ scene.name }}</span>
</button>
</div>
</div>
<!-- 对话场景 -->
<div class="dialog-scene" v-if="currentScenario">
<div class="scene-background">
<!-- 快递员请求方 -->
<div class="character courier">
<div class="char-avatar"></div>
<div class="char-name">快递员浏览器</div>
</div>
<!-- 对话区域 -->
<div class="conversation-area">
<!-- 请求消息 -->
<div class="message request" :class="{ sent: step >= 1 }">
<div class="message-bubble">
<div class="bubble-header">
<span class="method-badge" :class="currentScenario.method.toLowerCase()">
{{ currentScenario.method }}
</span>
<span class="path-text">{{ currentScenario.path }}</span>
</div>
<div class="bubble-body">{{ currentScenario.requestText }}</div>
</div>
<div class="message-meta">请求</div>
</div>
<!-- 传输动画 -->
<div class="transit-animation" v-if="step === 2">
<div class="transit-line"></div>
<div class="transit-package"></div>
</div>
<!-- 响应消息 -->
<div class="message response" :class="{ sent: step >= 3 }">
<div class="message-meta">响应</div>
<div class="message-bubble" :class="currentScenario.statusType">
<div class="bubble-header">
<span class="status-badge" :class="currentScenario.statusType">
{{ currentScenario.status }}
</span>
<span class="status-text">{{ currentScenario.statusText }}</span>
</div>
<div class="bubble-body">{{ currentScenario.responseText }}</div>
</div>
</div>
</div>
<!-- 收件人响应方 -->
<div class="character recipient">
<div class="char-avatar"></div>
<div class="char-name">收件人服务器</div>
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="dialog-controls">
<button
class="control-btn primary"
@click="nextStep"
:disabled="isDelivering || step >= 3"
>
{{ step === 0 ? '[开始]' : step === 3 ? '对话完成' : '[下一步]' }}
</button>
<button class="control-btn" @click="reset" v-if="step > 0">
[重置]
</button>
</div>
<!-- 状态码说明 -->
<div class="status-legend">
<div class="legend-title">[对照] HTTP状态码速查</div>
<div class="legend-grid">
<div class="legend-item success">
<span class="status-dot"></span>
<span class="status-code">2xx</span>
<span class="status-meaning">成功送达</span>
</div>
<div class="legend-item redirect">
<span class="status-dot"></span>
<span class="status-code">3xx</span>
<span class="status-meaning">地址变更</span>
</div>
<div class="legend-item client-error">
<span class="status-dot"></span>
<span class="status-code">4xx</span>
<span class="status-meaning">请求有误</span>
</div>
<div class="legend-item server-error">
<span class="status-dot"></span>
<span class="status-code">5xx</span>
<span class="status-meaning">服务器问题</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const scenarios = [
{
id: 'success',
name: '正常送达',
method: 'GET',
path: '/index.html',
requestText: '您好,这是您的包裹,请签收!',
status: '200',
statusText: 'OK',
statusType: 'success',
responseText: '好的,收到了,谢谢!'
},
{
id: 'notfound',
name: '地址错误',
method: 'GET',
path: '/nopage',
requestText: '您好,送包裹到这个地方。',
status: '404',
statusText: 'Not Found',
statusType: 'client-error',
responseText: '这里没有这个人,您送错地方了。'
},
{
id: 'redirect',
name: '地址变更',
method: 'GET',
path: '/old-address',
requestText: '您好,送包裹到这个地址。',
status: '301',
statusText: 'Moved',
statusType: 'redirect',
responseText: '这里搬走了,请送到新地址。'
},
{
id: 'error',
name: '家中故障',
method: 'POST',
path: '/api/order',
requestText: '您好,我来送您订购的商品。',
status: '500',
statusText: 'Error',
statusType: 'server-error',
responseText: '抱歉,我们家系统出问题了,暂时无法接收。'
}
]
const currentScenario = ref(scenarios[0])
const step = ref(0)
const isDelivering = ref(false)
const selectScenario = (scenario) => {
currentScenario.value = scenario
reset()
}
const nextStep = () => {
if (step.value < 3) {
isDelivering.value = true
step.value++
if (step.value === 2) {
setTimeout(() => {
step.value++
isDelivering.value = false
}, 1000)
} else {
isDelivering.value = false
}
}
}
const reset = () => {
step.value = 0
isDelivering.value = false
}
</script>
<style scoped>
.delivery-dialog-demo {
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 头部 */
.dialog-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.dialog-icon {
font-size: 14px;
color: var(--vp-c-brand);
font-weight: 600;
}
.dialog-title {
font-size: 17px;
font-weight: 700;
color: var(--vp-c-text-1);
}
/* 场景选择 */
.scenario-selector {
margin-bottom: 20px;
}
.selector-label {
font-size: 13px;
color: var(--vp-c-text-3);
margin-bottom: 10px;
}
.scenario-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.scenario-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
}
.scenario-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.scenario-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.scenario-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-text { font-size: 13px; }
/* 对话场景 */
.dialog-scene {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.scene-background {
display: flex;
align-items: center;
gap: 20px;
}
/* 角色 */
.character {
text-align: center;
flex-shrink: 0;
}
.char-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 600;
color: white;
margin-bottom: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.courier .char-avatar {
background: linear-gradient(135deg, #409eff, #67c23a);
}
.recipient .char-avatar {
background: linear-gradient(135deg, #e6a23c, #f56c6c);
}
.char-name {
font-size: 12px;
color: var(--vp-c-text-2);
font-weight: 500;
}
/* 对话区域 */
.conversation-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
align-items: center;
gap: 12px;
opacity: 0.3;
transform: translateY(10px);
transition: all 0.4s;
}
.message.sent {
opacity: 1;
transform: translateY(0);
}
.message.request {
justify-content: flex-end;
}
.message.response {
justify-content: flex-start;
}
.message-bubble {
max-width: 280px;
padding: 14px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
}
.message.request .message-bubble {
background: #409eff;
border-color: #409eff;
color: white;
}
.message.response .message-bubble.success {
background: #67c23a;
border-color: #67c23a;
color: white;
}
.message.response .message-bubble.redirect {
background: #e6a23c;
border-color: #e6a23c;
color: white;
}
.message.response .message-bubble.client-error {
background: #f56c6c;
border-color: #f56c6c;
color: white;
}
.message.response .message-bubble.server-error {
background: #909399;
border-color: #909399;
color: white;
}
.bubble-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.method-badge, .status-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
font-family: monospace;
background: rgba(255,255,255,0.2);
}
.path-text, .status-text {
font-size: 12px;
opacity: 0.9;
}
.bubble-body {
font-size: 13px;
line-height: 1.5;
}
.message-meta {
font-size: 11px;
color: var(--vp-c-text-3);
font-weight: 500;
}
/* 传输动画 */
.transit-animation {
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: 40px;
}
.transit-line {
width: 100%;
height: 2px;
background: var(--vp-c-divider);
}
.transit-package {
position: absolute;
font-size: 20px;
font-weight: 600;
color: var(--vp-c-brand);
animation: deliver 1s ease-in-out;
}
@keyframes deliver {
0% { transform: translateX(-100px); }
50% { transform: translateX(0) scale(1.2); }
100% { transform: translateX(100px); }
}
/* 控制按钮 */
.dialog-controls {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
}
.control-btn {
padding: 12px 24px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 24px;
cursor: pointer;
font-size: 14px;
color: var(--vp-c-text-2);
transition: all 0.3s;
}
.control-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.control-btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.control-btn.primary:hover:not(:disabled) {
background: #66b1ff;
}
/* 状态码说明 */
.status-legend {
padding: 16px;
background: var(--vp-c-bg);
border-radius: 12px;
}
.legend-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.legend-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.legend-item.success .status-dot { background: #67c23a; }
.legend-item.redirect .status-dot { background: #e6a23c; }
.legend-item.client-error .status-dot { background: #f56c6c; }
.legend-item.server-error .status-dot { background: #909399; }
.status-code {
font-family: monospace;
font-size: 12px;
font-weight: 700;
color: var(--vp-c-text-1);
}
.status-meaning {
font-size: 12px;
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.scene-background {
flex-direction: column;
}
.character {
order: -1;
}
.legend-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,551 @@
<!--
TcpHandshakeDemo.vue
TCP三次握手演示 - 打电话确认类比
用途
"打电话确认对方是否在家"的生活化比喻让用户理解TCP三次握手的必要性
把枯燥的技术概念变成直观的对话过程
-->
<template>
<div class="phone-call-demo">
<!-- 标题 -->
<div class="call-header">
<span class="call-icon">[电话]</span>
<span class="call-title">打电话确认建立可靠连接</span>
</div>
<!-- 场景说明 -->
<div class="scenario-box">
<div class="scenario-text">
快递员到了<strong>"{{ targetAddress }}"</strong>附近但他需要确认收件人是否在家才能确保包裹能成功送达
</div>
</div>
<!-- 模式切换 -->
<div class="mode-toggle">
<button
v-for="mode in modes"
:key="mode.id"
@click="currentMode = mode.id"
class="mode-btn"
:class="{ active: currentMode === mode.id }"
>
{{ mode.label }}
</button>
</div>
<!-- 通话可视化 -->
<div class="call-visualization">
<!-- 左侧快递员客户端 -->
<div class="caller-side">
<div class="avatar-box client">
<div class="avatar"></div>
<div class="avatar-label">快递员你的电脑</div>
</div>
<div class="speech-bubble client" v-if="currentStep >= 1">
<div class="bubble-content">
{{ currentMode === 'simple' ? '喂,在家吗?我是快递员!' : 'SYN: 请求连接' }}
</div>
<div class="bubble-arrow"></div>
</div>
</div>
<!-- 中间信号传输 -->
<div class="signal-area">
<div class="signal-line">
<div
v-for="(signal, i) in signals"
:key="i"
class="signal-dot"
:class="[signal.type, { active: currentStep >= signal.step }]"
>
{{ signal.icon }}
</div>
</div>
<div class="connection-status" v-if="currentStep > 0">
<span class="status-text" :class="{ connected: currentStep >= 3 }">
{{ currentStep >= 3 ? '[已接通]' : '[连接中...]' }}
</span>
</div>
</div>
<!-- 右侧收件人服务器 -->
<div class="receiver-side">
<div class="speech-bubble server" v-if="currentStep >= 2">
<div class="bubble-arrow"></div>
<div class="bubble-content">
{{ currentMode === 'simple' ? '在的!我听到了,请说!' : 'SYN-ACK: 确认收到' }}
</div>
</div>
<div class="avatar-box server">
<div class="avatar"></div>
<div class="avatar-label">收件人服务器</div>
</div>
<div class="speech-bubble server final" v-if="currentStep >= 3">
<div class="bubble-arrow"></div>
<div class="bubble-content">
{{ currentMode === 'simple' ? '好的,开始说吧!' : 'ACK: 确认连接' }}
</div>
</div>
</div>
</div>
<!-- 步骤说明 -->
<div class="steps-explanation">
<div
v-for="(step, index) in steps"
:key="index"
class="step-card"
:class="{ active: currentStep === index + 1, completed: currentStep > index + 1 }"
@click="goToStep(index + 1)"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ currentMode === 'simple' ? step.simpleTitle : step.techTitle }}</div>
<div class="step-desc">{{ currentMode === 'simple' ? step.simpleDesc : step.techDesc }}</div>
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="control-panel">
<button class="ctrl-btn" @click="prevStep" :disabled="currentStep <= 0">
[上一步]
</button>
<button
class="ctrl-btn primary"
@click="nextStep"
:disabled="currentStep >= 3"
>
{{ currentStep >= 3 ? '已接通' : '[下一步]' }}
</button>
<button class="ctrl-btn" @click="reset">
[重置]
</button>
</div>
<!-- 为什么是三次 -->
<div class="why-three-box" v-if="currentStep >= 3">
<div class="why-icon">[提示]</div>
<div class="why-content">
<strong>为什么是三次不是两次</strong>
<p>两次对话只能确认"你能发、对方能收"但对方不知道他的回复你有没有收到三次对话确保<strong>双方都能发双方都能收</strong>通信才是可靠的</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentMode = ref('simple')
const currentStep = ref(0)
const targetAddress = ref('北京市朝阳区XX小区')
const modes = [
{ id: 'simple', label: '生活语言' },
{ id: 'tech', label: '技术术语' }
]
const steps = [
{
simpleTitle: '快递员:喂,在家吗?',
simpleDesc: '快递员拨通电话,确认对方是否在家',
techTitle: '客户端发送 SYN',
techDesc: 'Synchronize: 请求建立连接,携带初始序列号'
},
{
simpleTitle: '收件人:在的,请说!',
simpleDesc: '收件人确认在家,也表示可以接收包裹',
techTitle: '服务器回复 SYN-ACK',
techDesc: 'Synchronize-Acknowledge: 确认收到,也请求连接'
},
{
simpleTitle: '快递员:好的,开始送!',
simpleDesc: '快递员确认对方已准备好,开始运送',
techTitle: '客户端回复 ACK',
techDesc: 'Acknowledge: 确认收到,连接建立成功'
}
]
const signals = [
{ type: 'syn', step: 1, icon: '发' },
{ type: 'synack', step: 2, icon: '收' },
{ type: 'ack', step: 3, icon: '通' }
]
const nextStep = () => {
if (currentStep.value < 3) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
const goToStep = (step) => {
currentStep.value = step
}
const reset = () => {
currentStep.value = 0
}
</script>
<style scoped>
.phone-call-demo {
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 头部 */
.call-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.call-icon {
font-size: 14px;
color: var(--vp-c-brand);
font-weight: 600;
}
.call-title {
font-size: 17px;
font-weight: 700;
color: var(--vp-c-text-1);
}
/* 场景 */
.scenario-box {
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(103, 194, 58, 0.1));
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.scenario-text {
font-size: 14px;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.scenario-text strong {
color: var(--vp-c-brand);
}
/* 模式切换 */
.mode-toggle {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
}
.mode-btn {
padding: 8px 20px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-2);
transition: all 0.3s;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.mode-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
/* 通话可视化 */
.call-visualization {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 12px;
margin-bottom: 20px;
}
.caller-side, .receiver-side {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
flex: 1;
}
.avatar-box {
text-align: center;
}
.avatar-box.client .avatar {
background: linear-gradient(135deg, #409eff, #67c23a);
}
.avatar-box.server .avatar {
background: linear-gradient(135deg, #e6a23c, #f56c6c);
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 600;
color: white;
margin-bottom: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.avatar-label {
font-size: 12px;
color: var(--vp-c-text-2);
font-weight: 500;
}
.speech-bubble {
position: relative;
max-width: 160px;
}
.speech-bubble.client {
align-self: flex-end;
}
.speech-bubble.server {
align-self: flex-start;
}
.bubble-content {
padding: 10px 14px;
border-radius: 12px;
font-size: 12px;
line-height: 1.5;
}
.speech-bubble.client .bubble-content {
background: #409eff;
color: white;
border-bottom-right-radius: 4px;
}
.speech-bubble.server .bubble-content {
background: #67c23a;
color: white;
border-bottom-left-radius: 4px;
}
/* 信号区域 */
.signal-area {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.signal-line {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.signal-dot {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: white;
opacity: 0.2;
transform: scale(0.8);
transition: all 0.4s;
}
.signal-dot.active {
opacity: 1;
transform: scale(1);
animation: pulseSignal 1s infinite;
}
.signal-dot.syn { background: #409eff; }
.signal-dot.synack { background: #67c23a; }
.signal-dot.ack { background: #e6a23c; }
@keyframes pulseSignal {
0%, 100% { box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.4); }
50% { box-shadow: 0 0 0 10px rgba(64, 158, 255, 0); }
}
.connection-status {
padding: 6px 12px;
background: var(--vp-c-bg-soft);
border-radius: 16px;
}
.status-text {
font-size: 12px;
color: var(--vp-c-text-3);
}
.status-text.connected {
color: #67c23a;
font-weight: 600;
}
/* 步骤说明 */
.steps-explanation {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.step-card {
flex: 1;
padding: 14px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
opacity: 0.5;
}
.step-card:hover {
opacity: 0.8;
transform: translateY(-2px);
}
.step-card.active {
opacity: 1;
border-color: var(--vp-c-brand);
background: rgba(64, 158, 255, 0.05);
}
.step-card.completed {
opacity: 0.8;
border-color: #67c23a;
}
.step-number {
width: 28px;
height: 28px;
background: var(--vp-c-divider);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
margin-bottom: 10px;
}
.step-card.active .step-number {
background: var(--vp-c-brand);
color: white;
}
.step-card.completed .step-number {
background: #67c23a;
color: white;
}
.step-title {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.step-desc {
font-size: 11px;
color: var(--vp-c-text-3);
line-height: 1.4;
}
/* 控制面板 */
.control-panel {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 16px;
}
.ctrl-btn {
padding: 10px 20px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-2);
transition: all 0.3s;
}
.ctrl-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.ctrl-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.ctrl-btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.ctrl-btn.primary:hover:not(:disabled) {
background: #66b1ff;
}
/* 为什么是三次 */
.why-three-box {
display: flex;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, rgba(103, 194, 58, 0.1), rgba(64, 158, 255, 0.1));
border-radius: 12px;
border-left: 4px solid #67c23a;
animation: slideIn 0.4s ease;
}
.why-icon {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-brand);
}
.why-content {
flex: 1;
}
.why-content strong {
font-size: 14px;
color: var(--vp-c-text-1);
}
.why-content p {
margin: 8px 0 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 640px) {
.call-visualization {
flex-direction: column;
}
.signal-area {
flex-direction: row;
order: -1;
}
.signal-line {
flex-direction: row;
}
.steps-explanation {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,517 @@
<!--
UrlParserDemo.vue
URL解析演示 - 交互式可视化组件
用途
URL 解析过程可视化通过颜色编码和分块展示
直观地展示 URL 的各个组成部分及其对应的技术含义和生活比喻
-->
<template>
<div class="url-parser-demo">
<!-- 头部控制区 -->
<div class="control-panel">
<div class="header-section">
<span class="icon">🔍</span>
<span class="title">URL 解析器</span>
</div>
<div class="examples-section">
<span class="label">试一试</span>
<div class="button-group">
<button
v-for="ex in examples"
:key="ex.name"
@click="useExample(ex)"
class="action-btn outline small"
:class="{ active: currentExample === ex.name }"
>
{{ ex.name }}
</button>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-section">
<div class="input-wrapper">
<input
v-model="urlInput"
type="text"
placeholder="输入或粘贴一个网址..."
@input="parseUrl"
class="url-input"
/>
<button class="clear-btn" @click="clear" v-if="urlInput"></button>
</div>
</div>
<!-- 可视化展示区 -->
<div class="visualization-area" v-if="parsed.protocol">
<div class="url-blocks">
<div
v-for="(field, key) in formFields"
:key="key"
v-show="shouldShowField(key)"
class="url-block"
:class="[key, { active: hovered === key }]"
:style="{ '--block-color': field.color, '--block-bg': hexToRgba(field.color, 0.15) }"
@mouseenter="hovered = key"
@mouseleave="hovered = null"
>
<span class="block-value">{{ getDisplayValue(key) }}</span>
<span class="block-label">{{ field.techName }}</span>
</div>
</div>
</div>
<!-- 详情说明卡片 -->
<div class="info-card" v-if="hovered && formFields[hovered]">
<div class="info-header" :style="{ borderLeftColor: formFields[hovered].color }">
<span class="info-title">{{ formFields[hovered].techLabel }} ({{ formFields[hovered].techName }})</span>
<span class="info-badge" :style="{ backgroundColor: formFields[hovered].color }">
{{ formFields[hovered].icon }}
</span>
</div>
<div class="info-content">
<div class="info-row">
<div class="info-label">技术含义</div>
<div class="info-value">
<strong>{{ formFields[hovered].techDesc }}</strong>
<div class="info-detail">{{ formFields[hovered].techDetail }}</div>
</div>
</div>
<div class="info-divider"></div>
<div class="info-row">
<div class="info-label">生活比喻</div>
<div class="info-value">
<strong>{{ formFields[hovered].analogyLabel }}</strong>
<div class="info-detail">{{ formFields[hovered].analogyDesc }}</div>
</div>
</div>
</div>
</div>
<!-- 空状态提示 -->
<div class="empty-state" v-else-if="!urlInput">
<p>👆 在上方输入网址查看它是由哪些部分组成的</p>
</div>
<div class="default-info" v-else>
<p>👆 鼠标悬停在上方色块上查看详细解释</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const urlInput = ref('')
const parsed = ref({})
const hovered = ref(null)
const currentExample = ref('')
const examples = [
{ name: '百度搜索', url: 'https://www.baidu.com/s?wd=hello' },
{ name: 'GitHub项目', url: 'https://github.com/vuejs/core' },
{ name: '带端口', url: 'http://localhost:8080/api/users' },
{ name: '带锚点', url: 'https://vuejs.org/guide/introduction.html#what-is-vue' }
]
const formFields = {
protocol: {
techName: 'Protocol',
techLabel: '协议',
color: '#f43f5e', // Red
icon: '规',
analogyLabel: '快递公司',
techDesc: '通信规则',
analogyDesc: '决定是用"顺丰"(HTTPS)还是"平邮"(HTTP)传输',
techDetail: 'https (加密) 或 http (明文)'
},
hostname: {
techName: 'Hostname',
techLabel: '域名',
color: '#3b82f6', // Blue
icon: '名',
analogyLabel: '收件人',
techDesc: '服务器地址',
analogyDesc: '如 "google.com",方便人类记忆的名字',
techDetail: '最终需要通过 DNS 解析为 IP 地址'
},
port: {
techName: 'Port',
techLabel: '端口',
color: '#f59e0b', // Amber
icon: '门',
analogyLabel: '门牌号',
techDesc: '服务入口',
analogyDesc: '如 ":8080",大楼里的具体房间号',
techDetail: '默认端口(80/443)通常会被浏览器省略'
},
pathname: {
techName: 'Path',
techLabel: '路径',
color: '#10b981', // Emerald
icon: '径',
analogyLabel: '具体位置',
techDesc: '资源路径',
analogyDesc: '如 "/files/doc.txt",文件柜的位置',
techDetail: '指向服务器上的具体资源'
},
search: {
techName: 'Query',
techLabel: '参数',
color: '#8b5cf6', // Violet
icon: '参',
analogyLabel: '备注',
techDesc: '查询参数',
analogyDesc: '如 "?q=hello",告诉对方具体要求',
techDetail: '键值对形式的附加数据'
},
hash: {
techName: 'Hash',
techLabel: '锚点',
color: '#ec4899', // Pink
icon: '锚',
analogyLabel: '页码',
techDesc: '页内定位',
analogyDesc: '如 "#section1",书的某一页',
techDetail: '浏览器滚动到指定位置,不会发送给服务器'
}
}
const hexToRgba = (hex, alpha) => {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
const shouldShowField = (key) => {
const val = parsed.value[key]
if (!val) return false
if (val === '无') return false
if (key === 'search' && (val === '' || val === '?')) return false
if (key === 'hash' && (val === '' || val === '#')) return false
return true
}
const getDisplayValue = (key) => {
let val = parsed.value[key]
if (key === 'protocol') return val + '://'
if (key === 'port') return ':' + val.replace('(默认)', '')
// simple formatting
return val
}
const useExample = (ex) => {
urlInput.value = ex.url
currentExample.value = ex.name
parseUrl()
}
const clear = () => {
urlInput.value = ''
parsed.value = {}
currentExample.value = ''
}
const parseUrl = () => {
if (!urlInput.value) {
parsed.value = {}
return
}
try {
let urlStr = urlInput.value.trim()
// Auto-add protocol if missing for better UX
if (!urlStr.match(/^https?:\/\//)) {
if (urlStr.startsWith('localhost')) {
urlStr = 'http://' + urlStr
} else {
urlStr = 'https://' + urlStr
}
}
const u = new URL(urlStr)
// Determine if port is explicit or default
let portDisplay = u.port
if (!portDisplay) {
// Just for display logic in the parser, we might not show it if it's default/hidden
// But let's show it if we want to be educational
// Actually, let's only show if it's in the string or we want to be explicit
// For visualizer, maybe better to show what's THERE.
// But to be educational, maybe show implied?
// Let's stick to what's in the URL object but handle defaults
// If the user typed it, u.port is set. If implied, it's empty string.
}
parsed.value = {
protocol: u.protocol.replace(':', ''),
hostname: u.hostname,
port: u.port, // Only show if explicit
pathname: u.pathname === '/' ? '/' : u.pathname,
search: u.search,
hash: u.hash
}
} catch (e) {
// simplistic fallback or error state could go here
// parsed.value = {}
}
}
</script>
<style scoped>
.url-parser-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.header-section {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.examples-section {
display: flex;
align-items: center;
gap: 0.5rem;
}
.label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.button-group {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.action-btn {
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
cursor: pointer;
border: 1px solid var(--vp-c-divider);
background: transparent;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.action-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.action-btn.active {
background-color: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
.input-section {
margin-bottom: 1.5rem;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.url-input {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: inherit;
font-size: 1rem;
transition: border-color 0.2s;
}
.url-input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.clear-btn {
position: absolute;
right: 0.75rem;
background: none;
border: none;
color: var(--vp-c-text-3);
cursor: pointer;
font-size: 1rem;
}
.clear-btn:hover {
color: var(--vp-c-text-1);
}
.visualization-area {
margin-bottom: 1rem;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.url-blocks {
display: flex;
gap: 4px;
align-items: flex-start;
}
.url-block {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: 6px;
background-color: var(--block-bg);
border: 1px solid var(--block-color);
cursor: help;
transition: all 0.2s;
min-width: fit-content;
}
.url-block:hover, .url-block.active {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
filter: brightness(1.05);
}
.block-value {
font-weight: bold;
color: var(--block-color);
font-size: 1rem;
white-space: nowrap;
}
.block-label {
font-size: 0.7rem;
text-transform: uppercase;
margin-top: 0.25rem;
opacity: 0.7;
color: var(--block-color);
}
.info-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
animation: slide-up 0.3s ease;
}
.info-header {
padding: 0.75rem 1rem;
background: var(--vp-c-bg-alt);
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid transparent;
}
.info-title {
font-weight: bold;
font-size: 0.95rem;
}
.info-badge {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: bold;
}
.info-content {
padding: 1rem;
display: grid;
grid-template-columns: 1fr 1px 1fr;
gap: 1rem;
}
.info-divider {
background: var(--vp-c-divider);
height: 100%;
}
.info-row {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.info-value {
font-size: 0.9rem;
}
.info-detail {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.empty-state, .default-info {
text-align: center;
padding: 2rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
border: 1px dashed var(--vp-c-divider);
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 640px) {
.info-content {
grid-template-columns: 1fr;
}
.info-divider {
height: 1px;
width: 100%;
}
}
</style>
@@ -0,0 +1,914 @@
<!--
UrlToBrowserQuickStart.vue
网络快递之旅 - 全流程快速体验组件
用途
"寄快递"的故事主线让0基础用户在30秒内体验从输入URL到页面显示的完整过程
设计原则故事化可视化即时反馈
-->
<template>
<div class="delivery-journey">
<!-- 故事标题 -->
<div class="journey-header">
<span class="journey-icon">[包裹]</span>
<span class="journey-title">体验一次"网络快递"之旅</span>
</div>
<!-- 快递单填写 -->
<div class="delivery-form">
<div class="form-label">填写快递单输入网址</div>
<div class="address-input">
<span class="protocol-badge">https://</span>
<input
v-model="url"
type="text"
placeholder="比如:baidu.com"
@keyup.enter="startJourney"
:disabled="isRunning"
/>
<button class="send-btn" @click="startJourney" :disabled="!url || isRunning">
{{ isRunning ? '运送中...' : '寄出' }}
</button>
</div>
</div>
<!-- 快速选择 -->
<div class="quick-select" v-if="!isRunning && !showResult">
<span class="quick-hint">快速体验</span>
<button
v-for="u in quickUrls"
:key="u.domain"
@click="url = u.domain; startJourney()"
class="quick-btn"
:title="u.desc"
>
{{ u.domain }}
</button>
</div>
<!-- 双栏对照展示 -->
<div class="comparison-view" v-if="isRunning || showResult">
<div class="comparison-header">
<div class="side-label delivery-side">寄快递流程</div>
<div class="connection-hint">对应关系</div>
<div class="side-label network-side">网络访问流程</div>
</div>
<div class="comparison-steps">
<div
v-for="(step, index) in steps"
:key="step.id"
class="step-row"
:class="{
passed: currentStep > index,
current: currentStep === index,
waiting: currentStep < index
}"
>
<!-- 左侧快递流程 -->
<div class="step-delivery">
<div class="step-icon">{{ step.deliveryIcon }}</div>
<div class="step-content">
<div class="step-title">{{ step.deliveryTitle }}</div>
<div class="step-desc">{{ step.deliveryDesc }}</div>
</div>
</div>
<!-- 中间对应指示 -->
<div class="step-connector">
<div class="connector-line"></div>
<div class="connector-arrow"></div>
<div class="connector-label">{{ step.mappingLabel }}</div>
</div>
<!-- 右侧网络流程 -->
<div class="step-network">
<div class="step-icon">{{ step.networkIcon }}</div>
<div class="step-content">
<div class="step-title">{{ step.networkTitle }}</div>
<div class="step-desc">{{ step.networkDesc }}</div>
<div class="step-tech" v-if="currentStep >= index">{{ step.techDetail }}</div>
</div>
</div>
</div>
</div>
<!-- 当前步骤高亮提示 -->
<div class="current-step-hint" v-if="isRunning && steps[currentStep]">
<div class="hint-label">当前阶段</div>
<div class="hint-content">
<span class="hint-delivery">{{ steps[currentStep].deliveryTitle }}</span>
<span class="hint-equals">=</span>
<span class="hint-network">{{ steps[currentStep].networkTitle }}</span>
</div>
</div>
<!-- 进度条 -->
<div class="progress-track" v-if="isRunning">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
</div>
<!-- 送达结果 -->
<div class="delivery-result" v-if="showResult">
<div class="success-banner">
包裹送达耗时 {{ time }}ms
</div>
<!-- 网页预览 -->
<div class="page-preview">
<div class="browser-chrome">
<div class="chrome-dots">
<span></span><span></span><span></span>
</div>
<div class="chrome-address">{{ url }}</div>
</div>
<div class="page-content">
<div class="skeleton-line" style="width: 70%"></div>
<div class="skeleton-line" style="width: 50%"></div>
<div class="skeleton-img"></div>
<div class="skeleton-line" style="width: 80%"></div>
</div>
</div>
<button class="retry-btn" @click="reset">再寄一次</button>
</div>
<!-- 名词对照卡片默认显示 -->
<div class="glossary-cards" v-if="!isRunning && !showResult">
<div class="glossary-title">快递 vs 网络 名词对照</div>
<div class="cards-grid">
<div
v-for="item in glossary"
:key="item.delivery"
class="glossary-card"
@mouseenter="hoveredCard = item"
@mouseleave="hoveredCard = null"
>
<div class="card-delivery">
<span class="card-label">快递</span>
<span class="card-value">{{ item.delivery }}</span>
</div>
<div class="card-arrow"></div>
<div class="card-network">
<span class="card-label">网络</span>
<span class="card-value">{{ item.network }}</span>
</div>
<div class="card-explanation" v-if="hoveredCard === item">
{{ item.explanation }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const url = ref('')
const isRunning = ref(false)
const showResult = ref(false)
const currentStep = ref(0)
const progress = ref(0)
const time = ref(0)
const hoveredCard = ref(null)
const quickUrls = [
{ domain: 'baidu.com', desc: '百度搜索引擎' },
{ domain: 'github.com', desc: '代码托管平台' },
{ domain: 'vuejs.org', desc: 'Vue.js 官网' }
]
// 步骤定义
const steps = [
{
id: 'parse',
deliveryIcon: '写',
deliveryTitle: '填写快递单',
deliveryDesc: '写明收件人、地址、电话',
mappingLabel: '对应',
networkIcon: '输',
networkTitle: '输入网址',
networkDesc: '浏览器解析 URL',
techDetail: '协议 + 域名 + 路径 + 参数'
},
{
id: 'dns',
deliveryIcon: '查',
deliveryTitle: '查地址簿',
deliveryDesc: '姓名 → 门牌号',
mappingLabel: '对应',
networkIcon: 'DNS',
networkTitle: 'DNS 查询',
networkDesc: '域名 → IP 地址',
techDetail: 'google.com → 142.250.80.46'
},
{
id: 'tcp',
deliveryIcon: '电',
deliveryTitle: '打电话确认',
deliveryDesc: '"在家吗?能收件吗?"',
mappingLabel: '对应',
networkIcon: 'TCP',
networkTitle: 'TCP 三次握手',
networkDesc: '建立可靠连接',
techDetail: 'SYN → SYN-ACK → ACK'
},
{
id: 'http',
deliveryIcon: '送',
deliveryTitle: '快递员送货',
deliveryDesc: '把包裹送到对方手中',
mappingLabel: '对应',
networkIcon: 'HTTP',
networkTitle: 'HTTP 传输',
networkDesc: '请求网页数据',
techDetail: 'GET /index.html → 200 OK'
},
{
id: 'render',
deliveryIcon: '拆',
deliveryTitle: '拆开包裹',
deliveryDesc: '看到礼物内容',
mappingLabel: '对应',
networkIcon: '渲染',
networkTitle: '浏览器渲染',
networkDesc: '显示网页内容',
techDetail: 'HTML + CSS + JS → 像素'
}
]
// 名词对照表
const glossary = [
{
delivery: '快递单',
network: 'URL',
explanation: '网页的完整地址,包含去哪里找、找什么资源'
},
{
delivery: '收件人姓名',
network: '域名',
explanation: '服务器的名字,如 google.com,方便人类记忆'
},
{
delivery: '门牌号',
network: 'IP 地址',
explanation: '服务器的数字地址,如 142.250.80.46,计算机使用'
},
{
delivery: '查地址簿',
network: 'DNS 查询',
explanation: '把域名转换成 IP 地址的查询系统'
},
{
delivery: '打电话确认',
network: 'TCP 握手',
explanation: '确保双方在线且能正常通信的确认过程'
},
{
delivery: '快递员送货',
network: 'HTTP 请求',
explanation: '浏览器向服务器请求数据,服务器返回响应'
},
{
delivery: '拆开包裹',
network: '浏览器渲染',
explanation: '把代码转换成屏幕上看到的图文页面'
}
]
const startJourney = () => {
if (!url.value) return
isRunning.value = true
showResult.value = false
currentStep.value = 0
progress.value = 0
const startTime = Date.now()
let step = 0
const runStep = () => {
if (step >= steps.length) {
isRunning.value = false
showResult.value = true
time.value = Date.now() - startTime
return
}
currentStep.value = step
// 进度动画
let p = step * 20
const interval = setInterval(() => {
p += 1
progress.value = p
if (p >= (step + 1) * 20) {
clearInterval(interval)
step++
setTimeout(runStep, 600)
}
}, 80)
}
runStep()
}
const reset = () => {
url.value = ''
isRunning.value = false
showResult.value = false
currentStep.value = 0
progress.value = 0
}
</script>
<style scoped>
.delivery-journey {
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 头部 */
.journey-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.journey-icon {
font-size: 20px;
font-weight: bold;
color: var(--vp-c-brand);
}
.journey-title {
font-size: 18px;
font-weight: 700;
color: var(--vp-c-text-1);
}
/* 快递单 */
.delivery-form {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
color: var(--vp-c-text-2);
margin-bottom: 10px;
font-weight: 500;
}
.address-input {
display: flex;
align-items: center;
gap: 8px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 4px;
transition: border-color 0.3s;
}
.address-input:focus-within {
border-color: var(--vp-c-brand);
}
.protocol-badge {
padding: 8px 12px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
font-size: 13px;
color: var(--vp-c-text-3);
font-family: monospace;
}
.address-input input {
flex: 1;
border: none;
background: transparent;
font-size: 15px;
padding: 8px;
outline: none;
color: var(--vp-c-text-1);
}
.send-btn {
padding: 10px 20px;
background: linear-gradient(135deg, var(--vp-c-brand), #67c23a);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.send-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 快速选择 */
.quick-select {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.quick-hint {
font-size: 13px;
color: var(--vp-c-text-3);
}
.quick-btn {
padding: 6px 14px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.quick-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-1px);
}
/* 双栏对照视图 */
.comparison-view {
margin-top: 20px;
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
}
.comparison-header {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--vp-c-divider);
}
.side-label {
font-size: 14px;
font-weight: 600;
padding: 8px 16px;
border-radius: 8px;
text-align: center;
}
.delivery-side {
background: linear-gradient(135deg, #e6f7ff, #bae7ff);
color: #096dd9;
}
.network-side {
background: linear-gradient(135deg, #f6ffed, #d9f7be);
color: #389e0d;
}
.connection-hint {
font-size: 12px;
color: var(--vp-c-text-3);
padding: 4px 12px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
}
/* 步骤行 */
.comparison-steps {
display: flex;
flex-direction: column;
gap: 12px;
}
.step-row {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 12px;
align-items: stretch;
padding: 12px;
border-radius: 10px;
transition: all 0.3s;
opacity: 0.4;
}
.step-row.passed {
opacity: 0.7;
background: rgba(103, 194, 58, 0.05);
}
.step-row.current {
opacity: 1;
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(103, 194, 58, 0.1));
border: 2px solid var(--vp-c-brand);
transform: scale(1.02);
}
/* 左侧:快递流程 */
.step-delivery {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: linear-gradient(135deg, #e6f7ff, #f0f5ff);
border-radius: 10px;
border: 1px solid #91d5ff;
}
.step-delivery .step-icon {
width: 40px;
height: 40px;
background: #1890ff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
flex-shrink: 0;
}
.step-delivery .step-title {
font-size: 14px;
font-weight: 600;
color: #096dd9;
}
.step-delivery .step-desc {
font-size: 12px;
color: var(--vp-c-text-2);
margin-top: 4px;
}
/* 中间:连接器 */
.step-connector {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
min-width: 60px;
}
.connector-line {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: var(--vp-c-divider);
}
.connector-arrow {
width: 28px;
height: 28px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
z-index: 1;
}
.connector-label {
font-size: 10px;
color: var(--vp-c-text-3);
margin-top: 4px;
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
}
/* 右侧:网络流程 */
.step-network {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: linear-gradient(135deg, #f6ffed, #f0f9ff);
border-radius: 10px;
border: 1px solid #b7eb8f;
}
.step-network .step-icon {
width: 40px;
height: 40px;
background: #52c41a;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
flex-shrink: 0;
}
.step-network .step-title {
font-size: 14px;
font-weight: 600;
color: #389e0d;
}
.step-network .step-desc {
font-size: 12px;
color: var(--vp-c-text-2);
margin-top: 4px;
}
.step-network .step-tech {
font-size: 11px;
color: var(--vp-c-brand);
margin-top: 6px;
padding: 4px 8px;
background: rgba(64, 158, 255, 0.1);
border-radius: 4px;
font-family: monospace;
}
/* 当前步骤提示 */
.current-step-hint {
margin-top: 20px;
padding: 16px;
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(103, 194, 58, 0.1));
border-radius: 10px;
text-align: center;
}
.hint-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.hint-content {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.hint-delivery {
font-size: 14px;
font-weight: 600;
color: #096dd9;
padding: 6px 12px;
background: #e6f7ff;
border-radius: 6px;
}
.hint-equals {
font-size: 16px;
font-weight: bold;
color: var(--vp-c-brand);
}
.hint-network {
font-size: 14px;
font-weight: 600;
color: #389e0d;
padding: 6px 12px;
background: #f6ffed;
border-radius: 6px;
}
/* 进度条 */
.progress-track {
height: 8px;
background: var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
margin-top: 16px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), #67c23a);
border-radius: 4px;
transition: width 0.1s linear;
}
/* 送达结果 */
.delivery-result {
margin-top: 20px;
text-align: center;
}
.success-banner {
font-size: 16px;
font-weight: 600;
color: #67c23a;
margin-bottom: 20px;
padding: 12px;
background: rgba(103, 194, 58, 0.1);
border-radius: 10px;
}
.page-preview {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
margin-bottom: 20px;
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
.browser-chrome {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.chrome-dots {
display: flex;
gap: 6px;
}
.chrome-dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vp-c-divider);
}
.chrome-dots span:nth-child(1) { background: #ff5f57; }
.chrome-dots span:nth-child(2) { background: #febc2e; }
.chrome-dots span:nth-child(3) { background: #28c840; }
.chrome-address {
flex: 1;
padding: 4px 10px;
background: var(--vp-c-bg);
border-radius: 4px;
font-size: 12px;
color: var(--vp-c-text-2);
text-align: left;
}
.page-content {
padding: 20px;
}
.skeleton-line {
height: 12px;
background: var(--vp-c-divider);
border-radius: 6px;
margin-bottom: 12px;
}
.skeleton-img {
width: 80px;
height: 60px;
background: linear-gradient(135deg, var(--vp-c-divider), var(--vp-c-bg-soft));
border-radius: 8px;
margin: 16px auto;
}
.retry-btn {
padding: 10px 24px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 24px;
cursor: pointer;
font-size: 14px;
color: var(--vp-c-text-2);
font-weight: 500;
transition: all 0.3s;
}
.retry-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
/* 名词对照卡片 */
.glossary-cards {
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--vp-c-divider);
}
.glossary-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 16px;
text-align: center;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.glossary-card {
position: relative;
padding: 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.glossary-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-delivery,
.card-network {
display: flex;
align-items: center;
gap: 8px;
}
.card-network {
margin-top: 8px;
}
.card-label {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
}
.card-delivery .card-label {
background: #e6f7ff;
color: #096dd9;
}
.card-network .card-label {
background: #f6ffed;
color: #389e0d;
}
.card-value {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.card-arrow {
text-align: center;
font-size: 14px;
color: var(--vp-c-text-3);
margin: 4px 0;
}
.card-explanation {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--vp-c-divider);
font-size: 11px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
/* 响应式 */
@media (max-width: 768px) {
.comparison-header {
grid-template-columns: 1fr;
gap: 8px;
}
.connection-hint {
display: none;
}
.step-row {
grid-template-columns: 1fr;
gap: 8px;
}
.step-connector {
flex-direction: row;
min-height: 40px;
}
.connector-line {
position: static;
width: 100%;
height: 2px;
}
.cards-grid {
grid-template-columns: 1fr;
}
.address-input {
flex-wrap: wrap;
}
.protocol-badge {
display: none;
}
}
</style>