feat(appendix): 添加多个交互式演示组件,完善 AI/Infra 等章节内容

- 新增 Vibe Coding 全栈相关演示组件 (DeveloperSkillShift, FrontendTriad, BackendCore 等)
- 新增 RAG 相关组件 (RAGPipeline, ChunkingStrategy, Retrieval 等)
- 新增 Embedding & Vector 相关组件 (EmbeddingConcept, VectorSimilarity 等)
- 新增 AI Native App 设计组件 (AINativeArch, PromptDesign 等)
- 新增 Infrastructure as Code 组件 (IaCConcept, TerraformWorkflow 等)
- 新增 DNS & HTTPS 演示组件 (DnsResolution, HttpsHandshake 等)
- 新增 Model Finetuning 组件 (FinetuningPipeline 等)
- 更新多个章节的 markdown 内容,集成交互式演示
This commit is contained in:
sanbuphy
2026-02-24 18:22:58 +08:00
parent b5a55811cc
commit 3af119a598
86 changed files with 20311 additions and 340 deletions
@@ -0,0 +1,307 @@
<template>
<div class="cert-chain-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔗 证书信任链可视化
</h4>
<p class="intro-text">
点击每一层证书查看它的详细信息和在信任链中的角色
</p>
<div class="chain-container">
<div
v-for="(cert, idx) in certs"
:key="idx"
class="cert-node"
:class="{ selected: selectedIdx === idx }"
:style="{ '--level-color': cert.color }"
@click="selectedIdx = idx"
>
<div class="cert-icon">{{ cert.icon }}</div>
<div class="cert-title">{{ cert.title }}</div>
<div class="cert-subtitle">{{ cert.subtitle }}</div>
<div v-if="idx < certs.length - 1" class="chain-arrow">
<span class="arrow-text">签发</span>
<span class="arrow-symbol"></span>
</div>
</div>
</div>
<div v-if="selectedIdx >= 0" class="detail-panel">
<div
class="detail-header"
:style="{ borderColor: certs[selectedIdx].color }"
>
<span class="detail-icon">{{ certs[selectedIdx].icon }}</span>
<span class="detail-name">{{ certs[selectedIdx].title }}</span>
</div>
<div class="detail-body">
<div class="detail-row" v-for="(item, i) in certs[selectedIdx].details" :key="i">
<span class="detail-label">{{ item.label }}</span>
<span class="detail-value">{{ item.value }}</span>
</div>
</div>
<div class="detail-explain">
{{ certs[selectedIdx].explain }}
</div>
</div>
<div class="verify-box">
<div class="verify-title">🔍 浏览器验证流程</div>
<div class="verify-steps">
<div v-for="(s, i) in verifySteps" :key="i" class="verify-step">
<span class="verify-num">{{ i + 1 }}</span>
<span class="verify-text">{{ s }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedIdx = ref(0)
const certs = [
{
icon: '🏛️',
title: '根证书(Root CA',
subtitle: '信任的起点',
color: '#c62828',
explain:
'根证书是整个信任链的锚点。它由根证书颁发机构自签名,预装在操作系统和浏览器中。全球只有少数几十个根 CA,它们的安全性由严格的审计和物理安全措施保障。根 CA 的私钥通常存储在离线的硬件安全模块(HSM)中。',
details: [
{ label: '签发者', value: 'DigiCert Global Root G2(自签名)' },
{ label: '有效期', value: '25 年(2013 - 2038' },
{ label: '密钥长度', value: 'RSA 2048 位' },
{ label: '存储位置', value: '操作系统 / 浏览器内置信任库' },
{ label: '数量级', value: '全球约 150 个受信根证书' }
]
},
{
icon: '🏢',
title: '中间证书(Intermediate CA',
subtitle: '信任的桥梁',
color: '#e65100',
explain:
'中间证书由根 CA 签发,作为根证书和服务器证书之间的桥梁。这种分层设计的好处是:即使中间证书被泄露,也可以单独吊销它而不影响根证书。中间 CA 负责日常的证书签发工作,根 CA 的私钥因此可以保持离线状态。',
details: [
{ label: '签发者', value: 'DigiCert Global Root G2' },
{ label: '持有者', value: 'DigiCert SHA2 Extended Validation Server CA' },
{ label: '有效期', value: '10 年' },
{ label: '用途', value: '签发终端实体(服务器)证书' },
{ label: '可吊销', value: '是(通过 CRL 或 OCSP' }
]
},
{
icon: '🌐',
title: '服务器证书(Server Certificate',
subtitle: '网站的身份证',
color: '#1565c0',
explain:
'服务器证书是网站向浏览器证明自己身份的凭证。它由中间 CA 签发,包含网站的域名、公钥和有效期等信息。当浏览器收到这张证书后,会沿着信任链向上验证,直到找到一个已经信任的根证书为止。',
details: [
{ label: '签发者', value: 'DigiCert SHA2 Extended Validation Server CA' },
{ label: '持有者', value: 'www.example.com' },
{ label: '有效期', value: '1 年(行业标准)' },
{ label: '包含公钥', value: 'ECDSA P-256 公钥' },
{ label: '验证级别', value: 'EV(扩展验证)/ DV(域名验证)' }
]
}
]
const verifySteps = [
'浏览器收到服务器证书,读取其签发者信息',
'找到中间证书,用中间 CA 的公钥验证服务器证书的签名',
'再用根 CA 的公钥验证中间证书的签名',
'确认根证书在本地信任库中 → 整条链验证通过'
]
</script>
<style scoped>
.cert-chain-demo {
background: linear-gradient(135deg, #fce4ec 0%, #fff3e0 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.intro-text {
font-size: 13px;
color: #666;
margin: 0 0 16px 0;
}
.chain-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
margin-bottom: 18px;
}
.cert-node {
position: relative;
background: #fff;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 14px 24px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
width: 280px;
max-width: 100%;
}
.cert-node:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.cert-node.selected {
border-color: var(--level-color);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: scale(1.03);
}
.cert-icon {
font-size: 30px;
}
.cert-title {
font-weight: 700;
font-size: 14px;
color: #1a1a2e;
margin-top: 4px;
}
.cert-subtitle {
font-size: 12px;
color: #888;
}
.chain-arrow {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 0;
color: #999;
}
.arrow-text {
font-size: 11px;
}
.arrow-symbol {
font-size: 20px;
line-height: 1;
}
.detail-panel {
background: #fff;
border-radius: 10px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid #e0e0e0;
}
.detail-header {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 3px solid;
}
.detail-icon {
font-size: 24px;
}
.detail-name {
font-weight: 700;
font-size: 16px;
color: #1a1a2e;
}
.detail-body {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.detail-row {
display: flex;
gap: 8px;
font-size: 13px;
}
.detail-label {
color: #888;
min-width: 80px;
flex-shrink: 0;
}
.detail-value {
color: #333;
font-weight: 500;
word-break: break-all;
}
.detail-explain {
font-size: 13px;
color: #555;
line-height: 1.7;
background: #f5f5f5;
padding: 10px 14px;
border-radius: 8px;
}
.verify-box {
background: #e8f5e9;
border-radius: 10px;
padding: 14px 18px;
}
.verify-title {
font-weight: 700;
color: #2e7d32;
margin-bottom: 10px;
font-size: 14px;
}
.verify-steps {
display: flex;
flex-direction: column;
gap: 8px;
}
.verify-step {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: #333;
}
.verify-num {
background: #4caf50;
color: #fff;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.verify-text {
line-height: 1.5;
padding-top: 1px;
}
</style>
@@ -0,0 +1,391 @@
<template>
<div class="comparison-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔐 HTTP vs HTTPS 数据传输对比
</h4>
<div class="control-row">
<button
class="mode-btn"
:class="{ active: mode === 'http' }"
@click="mode = 'http'"
>
HTTP明文
</button>
<button
class="mode-btn https"
:class="{ active: mode === 'https' }"
@click="mode = 'https'"
>
HTTPS加密
</button>
<button class="send-btn" :disabled="isSending" @click="sendData">
{{ isSending ? '传输中...' : '发送数据' }}
</button>
</div>
<div class="flow-area">
<div class="endpoint">
<div class="ep-icon">💻</div>
<div class="ep-label">浏览器</div>
<div class="ep-data original">
<div class="data-title">原始数据</div>
<code>{{ originalData }}</code>
</div>
</div>
<div class="transmission">
<div class="wire" :class="mode">
<div class="wire-label">
{{ mode === 'http' ? '🔓 明文传输' : '🔒 加密传输' }}
</div>
<div
class="packet"
:class="{ moving: isSending, done: sendDone }"
>
<code class="packet-text">{{ transmittedData }}</code>
</div>
</div>
<div v-if="mode === 'http'" class="hacker-box">
<div class="hacker-icon">🕵</div>
<div class="hacker-label">中间人可窃听</div>
<div v-if="sendDone" class="hacker-sees">
<code>{{ originalData }}</code>
</div>
</div>
<div v-else class="hacker-box blocked">
<div class="hacker-icon">🕵</div>
<div class="hacker-label">中间人无法解密</div>
<div v-if="sendDone" class="hacker-sees encrypted">
<code>{{ encryptedData }}</code>
</div>
</div>
</div>
<div class="endpoint">
<div class="ep-icon">🖥</div>
<div class="ep-label">服务器</div>
<div v-if="sendDone" class="ep-data received">
<div class="data-title">收到数据</div>
<code>{{ originalData }}</code>
</div>
</div>
</div>
<div class="compare-table">
<table>
<thead>
<tr>
<th>对比项</th>
<th>HTTP</th>
<th>HTTPS</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in compareRows" :key="i">
<td class="row-label">{{ row.label }}</td>
<td class="http-cell">{{ row.http }}</td>
<td class="https-cell">{{ row.https }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('http')
const isSending = ref(false)
const sendDone = ref(false)
const originalData = 'password=MySecret123&user=zhangsan'
const encryptedData = 'a7f2c9...3b8e1d(密文)'
const transmittedData = ref('')
const compareRows = [
{ label: '端口', http: '80', https: '443' },
{ label: '数据加密', http: '无(明文传输)', https: 'TLS 对称加密' },
{ label: '身份验证', http: '无', https: 'CA 证书验证服务器身份' },
{ label: '数据完整性', http: '无保障', https: 'MAC 校验防篡改' },
{ label: 'SEO 影响', http: '搜索引擎降权', https: '搜索引擎优先收录' },
{ label: '性能开销', http: '无额外开销', https: 'TLS 握手增加约 1-2 RTT' }
]
async function sendData() {
if (isSending.value) return
isSending.value = true
sendDone.value = false
if (mode.value === 'http') {
transmittedData.value = originalData
} else {
transmittedData.value = encryptedData
}
await sleep(1500)
sendDone.value = true
isSending.value = false
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
</script>
<style scoped>
.comparison-demo {
background: linear-gradient(135deg, #e8eaf6 0%, #fce4ec 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.control-row {
display: flex;
gap: 8px;
margin-bottom: 18px;
flex-wrap: wrap;
}
.mode-btn {
padding: 7px 18px;
border: 2px solid #ef5350;
border-radius: 8px;
background: #fff;
color: #c62828;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.mode-btn.https {
border-color: #43a047;
color: #2e7d32;
}
.mode-btn.active {
background: #c62828;
color: #fff;
}
.mode-btn.https.active {
background: #2e7d32;
color: #fff;
}
.send-btn {
padding: 7px 18px;
background: #1565c0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.flow-area {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 18px;
flex-wrap: wrap;
justify-content: center;
}
.endpoint {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
}
.ep-icon {
font-size: 32px;
}
.ep-label {
font-size: 13px;
font-weight: 600;
color: #333;
margin: 4px 0 8px;
}
.ep-data {
background: #fff;
border-radius: 8px;
padding: 8px 12px;
font-size: 11px;
max-width: 160px;
word-break: break-all;
}
.ep-data.original {
border: 1px solid #90caf9;
}
.ep-data.received {
border: 1px solid #a5d6a7;
}
.data-title {
font-size: 10px;
color: #888;
margin-bottom: 4px;
font-weight: 600;
}
.transmission {
flex: 1;
min-width: 180px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.wire {
width: 100%;
padding: 10px;
border-radius: 8px;
text-align: center;
position: relative;
min-height: 60px;
}
.wire.http {
background: #ffebee;
border: 2px dashed #ef5350;
}
.wire.https {
background: #e8f5e9;
border: 2px solid #43a047;
}
.wire-label {
font-size: 12px;
font-weight: 600;
margin-bottom: 6px;
}
.packet {
font-size: 11px;
opacity: 0;
transition: opacity 0.5s;
}
.packet.moving {
opacity: 1;
animation: slide 1.2s ease-in-out;
}
.packet.done {
opacity: 1;
}
@keyframes slide {
0% {
transform: translateX(-30px);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.packet-text {
font-size: 10px;
word-break: break-all;
}
.hacker-box {
background: #fff3e0;
border: 1px solid #ffcc80;
border-radius: 8px;
padding: 8px 12px;
text-align: center;
width: 100%;
}
.hacker-box.blocked {
background: #f1f8e9;
border-color: #aed581;
}
.hacker-icon {
font-size: 24px;
}
.hacker-label {
font-size: 11px;
font-weight: 600;
color: #e65100;
}
.hacker-box.blocked .hacker-label {
color: #558b2f;
}
.hacker-sees {
margin-top: 4px;
font-size: 10px;
color: #c62828;
word-break: break-all;
}
.hacker-sees.encrypted {
color: #558b2f;
}
.compare-table {
overflow-x: auto;
}
.compare-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
background: #fff;
border-radius: 8px;
overflow: hidden;
}
.compare-table th {
background: #37474f;
color: #fff;
padding: 8px 12px;
text-align: left;
font-weight: 600;
}
.compare-table td {
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
.row-label {
font-weight: 600;
color: #333;
}
.http-cell {
color: #c62828;
}
.https-cell {
color: #2e7d32;
}
</style>
@@ -0,0 +1,249 @@
<template>
<div class="dns-record-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
📋 DNS 记录类型速查
</h4>
<div class="tab-row">
<button
v-for="rec in records"
:key="rec.type"
class="tab-btn"
:class="{ active: selected === rec.type }"
@click="selected = rec.type"
>
{{ rec.type }}
</button>
</div>
<div v-if="current" class="detail-card">
<div class="detail-header">
<span class="type-badge">{{ current.type }}</span>
<span class="type-name">{{ current.name }}</span>
</div>
<p class="type-desc">{{ current.desc }}</p>
<div class="example-block">
<div class="example-title">示例记录</div>
<code class="example-code">{{ current.example }}</code>
</div>
<div class="usage-block">
<div class="usage-title">常见用途</div>
<ul class="usage-list">
<li v-for="(u, i) in current.usages" :key="i">{{ u }}</li>
</ul>
</div>
</div>
<div class="info-box">
<strong>小贴士</strong>
DNS 不只是把域名翻译成 IP它还承载了邮件路由域名验证负载均衡等多种功能全靠不同的记录类型来实现
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selected = ref('A')
const records = [
{
type: 'A',
name: 'Address 记录',
desc: '将域名映射到一个 IPv4 地址。这是最常见的 DNS 记录类型,浏览器访问网站时最终需要的就是这条记录。',
example: 'example.com. IN A 93.184.216.34',
usages: [
'网站域名指向服务器 IP',
'子域名指向不同的服务器',
'配合负载均衡返回多个 IP'
]
},
{
type: 'AAAA',
name: 'IPv6 Address 记录',
desc: '将域名映射到一个 IPv6 地址。随着 IPv4 地址耗尽,AAAA 记录变得越来越重要。',
example: 'example.com. IN AAAA 2606:2800:220:1:248:1893:25c8:1946',
usages: [
'支持 IPv6 网络的设备访问',
'双栈部署(同时配置 A 和 AAAA',
'面向未来的网络架构'
]
},
{
type: 'CNAME',
name: 'Canonical Name 记录',
desc: '将一个域名指向另一个域名(别名)。浏览器会继续解析目标域名,直到找到 A 记录。',
example: 'www.example.com. IN CNAME example.com.',
usages: [
'www 子域名指向主域名',
'CDN 加速(指向 CDN 提供商域名)',
'多个域名指向同一服务'
]
},
{
type: 'MX',
name: 'Mail Exchange 记录',
desc: '指定负责接收该域名邮件的邮件服务器地址和优先级。数字越小优先级越高。',
example: 'example.com. IN MX 10 mail.example.com.',
usages: [
'配置企业邮箱(如 Gmail、Outlook',
'设置邮件服务器优先级',
'邮件备份和容灾'
]
},
{
type: 'TXT',
name: 'Text 记录',
desc: '存储任意文本信息。常用于域名所有权验证、邮件安全策略(SPF/DKIM/DMARC)等场景。',
example: 'example.com. IN TXT "v=spf1 include:_spf.google.com ~all"',
usages: [
'SPF 记录防止邮件伪造',
'SSL 证书申请时的域名验证',
'第三方服务的域名所有权确认'
]
},
{
type: 'NS',
name: 'Name Server 记录',
desc: '指定该域名由哪些 DNS 服务器负责解析。这是 DNS 委派机制的核心。',
example: 'example.com. IN NS ns1.exampledns.com.',
usages: [
'将域名托管到指定 DNS 服务商',
'子域名委派给不同团队管理',
'DNS 服务迁移'
]
}
]
const current = computed(() => records.find((r) => r.type === selected.value))
</script>
<style scoped>
.dns-record-demo {
background: linear-gradient(135deg, #f3e5f5 0%, #ede7f6 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.tab-row {
display: flex;
gap: 6px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.tab-btn {
padding: 6px 16px;
border: 2px solid #ce93d8;
border-radius: 20px;
background: #fff;
color: #7b1fa2;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn.active {
background: #7b1fa2;
color: #fff;
border-color: #7b1fa2;
}
.tab-btn:hover:not(.active) {
background: #f3e5f5;
}
.detail-card {
background: #fff;
border-radius: 10px;
padding: 18px;
margin-bottom: 14px;
border: 1px solid #e1bee7;
}
.detail-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.type-badge {
background: #7b1fa2;
color: #fff;
padding: 3px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
font-family: monospace;
}
.type-name {
font-size: 15px;
color: #555;
font-weight: 500;
}
.type-desc {
font-size: 14px;
color: #333;
line-height: 1.7;
margin: 0 0 14px 0;
}
.example-block {
background: #263238;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 14px;
}
.example-title {
font-size: 11px;
color: #80cbc4;
margin-bottom: 6px;
font-weight: 600;
}
.example-code {
color: #e0f7fa;
font-size: 13px;
font-family: 'Fira Code', monospace;
word-break: break-all;
}
.usage-block {
background: #f3e5f5;
border-radius: 8px;
padding: 12px 16px;
}
.usage-title {
font-size: 12px;
font-weight: 700;
color: #7b1fa2;
margin-bottom: 6px;
}
.usage-list {
margin: 0;
padding-left: 18px;
font-size: 13px;
color: #444;
line-height: 1.8;
}
.info-box {
margin-top: 14px;
padding: 10px 14px;
background: #fff3e0;
border-radius: 8px;
font-size: 13px;
color: #5d4037;
line-height: 1.6;
}
</style>
@@ -0,0 +1,357 @@
<template>
<div class="dns-resolution-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔍 DNS 解析过程模拟器
</h4>
<div class="input-row">
<input
v-model="domain"
type="text"
placeholder="输入域名,如 www.example.com"
class="domain-input"
@keyup.enter="startResolve"
/>
<button class="resolve-btn" :disabled="isResolving" @click="startResolve">
{{ isResolving ? '解析中...' : '开始解析' }}
</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="resolve-flow">
<div
v-for="(step, idx) in steps"
:key="idx"
class="step-card"
:class="{
active: currentStep === idx,
done: currentStep > idx,
pending: currentStep < idx
}"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-label">{{ step.label }}</div>
<div v-if="currentStep > idx" class="step-result">
{{ step.result }}
</div>
<div v-if="currentStep === idx && isResolving" class="step-spinner">
</div>
<div
v-if="idx < steps.length - 1"
class="arrow"
:class="{ 'arrow-active': currentStep > idx }"
>
</div>
</div>
</div>
<div v-if="resolved" class="result-box">
<div class="result-title"> 解析完成</div>
<div class="result-detail">
<span class="result-domain">{{ domain }}</span>
<span class="result-ip">{{ resolvedIp }}</span>
</div>
<div class="result-time">总耗时 {{ totalTime }}ms模拟</div>
</div>
<div class="info-box">
<strong>解析流程说明</strong>
浏览器访问网站时需要先将域名翻译成 IP
地址这个过程会依次查询多级缓存和服务器直到找到对应的 IP
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const domain = ref('www.example.com')
const isResolving = ref(false)
const resolved = ref(false)
const currentStep = ref(-1)
const resolvedIp = ref('')
const totalTime = ref(0)
const steps = reactive([
{
icon: '🌐',
label: '浏览器缓存',
result: '未命中,继续查询...'
},
{
icon: '💻',
label: '操作系统缓存',
result: '未命中,继续查询...'
},
{
icon: '🔄',
label: '递归解析器',
result: '向根服务器发起查询...'
},
{
icon: '🌍',
label: '根域名服务器',
result: '返回 .com TLD 服务器地址'
},
{
icon: '📂',
label: 'TLD 服务器',
result: '返回权威服务器地址'
},
{
icon: '🏠',
label: '权威 DNS 服务器',
result: ''
}
])
function generateIp() {
const a = 93 + Math.floor(Math.random() * 60)
const b = Math.floor(Math.random() * 256)
const c = Math.floor(Math.random() * 256)
const d = 1 + Math.floor(Math.random() * 254)
return `${a}.${b}.${c}.${d}`
}
async function startResolve() {
if (isResolving.value || !domain.value.trim()) return
isResolving.value = true
resolved.value = false
currentStep.value = -1
const ip = generateIp()
resolvedIp.value = ip
steps[5].result = `找到记录!IP = ${ip}`
const delays = [200, 300, 400, 500, 400, 300]
let total = 0
for (let i = 0; i < steps.length; i++) {
currentStep.value = i
await sleep(delays[i])
total += delays[i]
currentStep.value = i + 1
}
totalTime.value = total
resolved.value = true
isResolving.value = false
}
function reset() {
isResolving.value = false
resolved.value = false
currentStep.value = -1
resolvedIp.value = ''
totalTime.value = 0
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
</script>
<style scoped>
.dns-resolution-demo {
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.input-row {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.domain-input {
flex: 1;
min-width: 200px;
padding: 8px 14px;
border: 2px solid #c5cae9;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.domain-input:focus {
border-color: #5c6bc0;
}
.resolve-btn {
padding: 8px 20px;
background: #5c6bc0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.resolve-btn:hover:not(:disabled) {
background: #3f51b5;
}
.resolve-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
padding: 8px 16px;
background: #e0e0e0;
color: #333;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.reset-btn:hover {
background: #bdbdbd;
}
.resolve-flow {
display: flex;
align-items: flex-start;
gap: 4px;
overflow-x: auto;
padding: 10px 0;
flex-wrap: wrap;
justify-content: center;
}
.step-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 10px;
border-radius: 10px;
background: #fff;
min-width: 100px;
max-width: 120px;
text-align: center;
transition: all 0.3s;
border: 2px solid transparent;
position: relative;
opacity: 0.5;
}
.step-card.active {
border-color: #ff9800;
background: #fff8e1;
opacity: 1;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
}
.step-card.done {
border-color: #4caf50;
background: #e8f5e9;
opacity: 1;
}
.step-card.pending {
opacity: 0.4;
}
.step-icon {
font-size: 28px;
margin-bottom: 6px;
}
.step-label {
font-size: 12px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.step-result {
font-size: 10px;
color: #666;
line-height: 1.3;
}
.step-spinner {
font-size: 20px;
animation: pulse 0.8s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.arrow {
position: absolute;
right: -14px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
color: #ccc;
font-weight: bold;
z-index: 1;
}
.arrow-active {
color: #4caf50;
}
.result-box {
margin-top: 16px;
padding: 14px 18px;
background: #e8f5e9;
border-radius: 10px;
border: 1px solid #a5d6a7;
}
.result-title {
font-weight: 700;
color: #2e7d32;
margin-bottom: 6px;
}
.result-detail {
font-size: 15px;
color: #333;
}
.result-domain {
color: #5c6bc0;
font-weight: 600;
}
.result-ip {
color: #e65100;
font-weight: 600;
font-family: monospace;
}
.result-time {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.info-box {
margin-top: 14px;
padding: 10px 14px;
background: #fff3e0;
border-radius: 8px;
font-size: 13px;
color: #5d4037;
line-height: 1.6;
}
</style>
@@ -0,0 +1,298 @@
<template>
<div class="https-handshake-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🤝 TLS 握手过程演示
</h4>
<div class="control-row">
<button class="start-btn" :disabled="isRunning" @click="startHandshake">
{{ isRunning ? '握手进行中...' : '开始 TLS 握手' }}
</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="handshake-area">
<div class="side client-side">
<div class="side-icon">💻</div>
<div class="side-label">客户端浏览器</div>
</div>
<div class="message-lane">
<div
v-for="(msg, idx) in messages"
:key="idx"
class="msg-row"
:class="{
active: currentStep === idx,
done: currentStep > idx,
pending: currentStep < idx
}"
>
<div
class="msg-arrow"
:class="msg.direction === 'right' ? 'arrow-right' : 'arrow-left'"
>
<span class="arrow-line"></span>
<span class="arrow-head">{{ msg.direction === 'right' ? '→' : '←' }}</span>
</div>
<div class="msg-content">
<div class="msg-name">{{ msg.name }}</div>
<div class="msg-desc">{{ msg.desc }}</div>
</div>
</div>
</div>
<div class="side server-side">
<div class="side-icon">🖥</div>
<div class="side-label">服务器</div>
</div>
</div>
<div v-if="currentStep >= 0 && currentStep < messages.length" class="detail-box">
<div class="detail-title">
{{ messages[currentStep].name }}
</div>
<div class="detail-text">
{{ messages[currentStep].detail }}
</div>
</div>
<div v-if="handshakeDone" class="success-box">
TLS 握手完成后续所有 HTTP 数据都将通过对称加密传输第三方无法窃听
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isRunning = ref(false)
const currentStep = ref(-1)
const handshakeDone = ref(false)
const messages = [
{
name: 'Client Hello',
direction: 'right',
desc: '发送支持的 TLS 版本、加密套件列表、随机数',
detail:
'浏览器向服务器发起连接请求,告知自己支持的 TLS 版本(如 TLS 1.3)、可用的加密算法列表(如 AES-256-GCM)以及一个客户端随机数(Client Random)。这就像自我介绍:"我会这些加密方式,你选一个吧。"'
},
{
name: 'Server Hello',
direction: 'left',
desc: '选定 TLS 版本、加密套件、服务器随机数',
detail:
'服务器从客户端提供的列表中选择一个最优的加密套件,并返回自己的随机数(Server Random)。相当于回应:"好的,我们就用 TLS 1.3 + AES-256-GCM 来通信。"'
},
{
name: 'Certificate',
direction: 'left',
desc: '服务器发送数字证书(含公钥)',
detail:
'服务器将自己的数字证书发送给浏览器。证书中包含服务器的公钥、域名信息以及 CA 的签名。浏览器会验证证书是否由受信任的 CA 签发、是否过期、域名是否匹配。'
},
{
name: 'Key Exchange',
direction: 'right',
desc: '双方协商生成会话密钥',
detail:
'在 TLS 1.3 中,客户端和服务器通过 ECDHE(椭圆曲线 Diffie-Hellman)算法交换密钥材料。双方各自生成临时密钥对,交换公钥后独立计算出相同的"预主密钥",再结合之前的随机数推导出最终的对称会话密钥。'
},
{
name: 'Finished',
direction: 'right',
desc: '双方确认握手成功,开始加密通信',
detail:
'双方各自发送 Finished 消息,其中包含之前所有握手消息的摘要(用刚协商好的密钥加密)。如果对方能正确解密并验证,说明密钥协商成功,后续所有数据都将使用对称加密传输。'
}
]
async function startHandshake() {
if (isRunning.value) return
isRunning.value = true
handshakeDone.value = false
currentStep.value = -1
for (let i = 0; i < messages.length; i++) {
currentStep.value = i
await sleep(1200)
}
handshakeDone.value = true
isRunning.value = false
}
function reset() {
isRunning.value = false
currentStep.value = -1
handshakeDone.value = false
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
</script>
<style scoped>
.https-handshake-demo {
background: linear-gradient(135deg, #e3f2fd 0%, #e8eaf6 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.control-row {
display: flex;
gap: 8px;
margin-bottom: 18px;
}
.start-btn {
padding: 8px 20px;
background: #1565c0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.start-btn:hover:not(:disabled) {
background: #0d47a1;
}
.start-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
padding: 8px 16px;
background: #e0e0e0;
color: #333;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.handshake-area {
display: flex;
gap: 12px;
align-items: flex-start;
}
.side {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
padding-top: 10px;
}
.side-icon {
font-size: 36px;
}
.side-label {
font-size: 12px;
font-weight: 600;
color: #333;
margin-top: 4px;
text-align: center;
}
.message-lane {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.msg-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 8px;
background: #fff;
border: 2px solid transparent;
opacity: 0.35;
transition: all 0.4s;
}
.msg-row.active {
opacity: 1;
border-color: #ff9800;
background: #fff8e1;
transform: scale(1.02);
box-shadow: 0 3px 10px rgba(255, 152, 0, 0.2);
}
.msg-row.done {
opacity: 1;
border-color: #4caf50;
background: #e8f5e9;
}
.msg-arrow {
display: flex;
align-items: center;
min-width: 36px;
font-size: 18px;
font-weight: bold;
}
.arrow-right {
color: #1565c0;
}
.arrow-left {
color: #e65100;
}
.msg-name {
font-weight: 700;
font-size: 14px;
color: #1a1a2e;
}
.msg-desc {
font-size: 12px;
color: #666;
margin-top: 2px;
}
.detail-box {
margin-top: 14px;
padding: 14px 18px;
background: #fff;
border-radius: 10px;
border-left: 4px solid #1565c0;
}
.detail-title {
font-weight: 700;
color: #1565c0;
margin-bottom: 6px;
font-size: 15px;
}
.detail-text {
font-size: 13px;
color: #444;
line-height: 1.7;
}
.success-box {
margin-top: 14px;
padding: 12px 18px;
background: #e8f5e9;
border-radius: 10px;
border: 1px solid #a5d6a7;
color: #2e7d32;
font-weight: 600;
font-size: 14px;
}
</style>