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

This commit is contained in:
sanbuphy
2026-02-24 00:18:09 +08:00
parent d45df3cda5
commit 94f9db0834
88 changed files with 11797 additions and 7634 deletions
-1
View File
@@ -797,7 +797,6 @@ export default defineConfig({
text: '五、数据', text: '五、数据',
collapsed: false, collapsed: false,
items: [ items: [
{ text: 'SQL', link: '/zh-cn/appendix/5-data/sql' },
{ {
text: '数据库原理(索引 / 事务 / 查询优化)', text: '数据库原理(索引 / 事务 / 查询优化)',
link: '/zh-cn/appendix/5-data/database-fundamentals' link: '/zh-cn/appendix/5-data/database-fundamentals'
@@ -110,13 +110,13 @@ const i18n = {
title: '真实项目', title: '真实项目',
headline: '拒绝玩具代码。', headline: '拒绝玩具代码。',
desc: '深入理解用户鉴权、数据存储、文件上传等核心业务逻辑。', desc: '深入理解用户鉴权、数据存储、文件上传等核心业务逻辑。',
link: '/zh-cn/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase' link: '/zh-cn/stage-2/backend/2.2-database-supabase/chapter5/'
}, },
{ {
title: '部署上线', title: '部署上线',
headline: '让世界看到你的作品。', headline: '让世界看到你的作品。',
desc: '学习服务器配置、域名解析和自动化部署,打通产品落地的最后一公里。', desc: '学习服务器配置、域名解析和自动化部署,打通产品落地的最后一公里。',
link: '/zh-cn/stage-2/backend/2.5-zeabur-deployment/extra6/extra6-zeabur-what-is-it-and-how-to-deploy-web-applications' link: '/zh-cn/stage-2/backend/2.5-zeabur-deployment/extra6/'
} }
] ]
}, },
@@ -133,7 +133,7 @@ const i18n = {
{ {
title: 'AI 智能体', title: 'AI 智能体',
desc: '构建具备记忆与规划能力的 Agent,实现自主任务执行。', desc: '构建具备记忆与规划能力的 Agent,实现自主任务执行。',
link: '/zh-cn/stage-3/ai-advanced/3.a1-rag-introduction/extra5-what-is-rag-and-how-does-it-work-and-future' link: '/zh-cn/stage-3/ai-advanced/3.a1-rag-introduction/'
}, },
{ {
title: '长效稳定', title: '长效稳定',
@@ -1502,7 +1502,7 @@ const stage2Cards = [
desc: '深入理解用户鉴权、数据存储、文件上传等核心业务逻辑。', desc: '深入理解用户鉴权、数据存储、文件上传等核心业务逻辑。',
imageColor: '#8EC5FC', imageColor: '#8EC5FC',
visualType: 'server', visualType: 'server',
link: '/zh-cn/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase' link: '/zh-cn/stage-2/backend/2.2-database-supabase/chapter5/'
}, },
{ {
title: '部署上线', title: '部署上线',
@@ -1510,7 +1510,7 @@ const stage2Cards = [
desc: '学习服务器配置、域名解析和自动化部署,打通产品落地的最后一公里。', desc: '学习服务器配置、域名解析和自动化部署,打通产品落地的最后一公里。',
imageColor: '#96E6A1', imageColor: '#96E6A1',
visualType: 'cloud', visualType: 'cloud',
link: '/zh-cn/stage-2/backend/2.5-zeabur-deployment/extra6/extra6-zeabur-what-is-it-and-how-to-deploy-web-applications' link: '/zh-cn/stage-2/backend/2.5-zeabur-deployment/extra6/'
} }
] ]
@@ -1528,7 +1528,7 @@ const stage3Cards = [
desc: 'RAG、Agent,探索 LLM 的无限可能。', desc: 'RAG、Agent,探索 LLM 的无限可能。',
tag: 'Advanced', tag: 'Advanced',
visualType: 'ai', visualType: 'ai',
link: '/zh-cn/stage-3/ai-advanced/3.a1-rag-introduction/extra5-what-is-rag-and-how-does-it-work-and-future' link: '/zh-cn/stage-3/ai-advanced/3.a1-rag-introduction/'
}, },
{ {
title: '复杂业务架构', title: '复杂业务架构',
@@ -5,7 +5,7 @@
🌟 AI 发展阶段与核心范式全景对比 🌟 AI 发展阶段与核心范式全景对比
</div> </div>
<div class="era-grid"> <div class="era-grid">
<div class="era-item" v-for="era in eras" :key="era.name" :style="{ borderTopColor: era.color }"> <div v-for="era in eras" :key="era.name" class="era-item" :style="{ borderTopColor: era.color }">
<div class="e-icon" :style="{ background: era.color }">{{ era.icon }}</div> <div class="e-icon" :style="{ background: era.color }">{{ era.icon }}</div>
<div class="e-name" :style="{ color: era.color }">{{ era.name }}</div> <div class="e-name" :style="{ color: era.color }">{{ era.name }}</div>
<div class="e-time">{{ era.time }}</div> <div class="e-time">{{ era.time }}</div>
@@ -25,7 +25,7 @@
<div class="e-section"> <div class="e-section">
<div class="e-label">典型代表</div> <div class="e-label">典型代表</div>
<div class="e-tags"> <div class="e-tags">
<span class="e-tag" v-for="tag in era.examples" :key="tag">{{ tag }}</span> <span v-for="tag in era.examples" :key="tag" class="e-tag">{{ tag }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -1,7 +1,7 @@
<template> <template>
<div class="demo-card"> <div class="demo-card">
<div class="timeline-visual"> <div class="timeline-visual">
<div class="era" v-for="era in eras" :key="era.label" :style="{ flex: era.flex, background: era.bg }"> <div v-for="era in eras" :key="era.label" class="era" :style="{ flex: era.flex, background: era.bg }">
<div class="era-label">{{ era.label }}</div> <div class="era-label">{{ era.label }}</div>
<div class="era-years">{{ era.years }}</div> <div class="era-years">{{ era.years }}</div>
</div> </div>
@@ -8,7 +8,7 @@
</div> </div>
</div> </div>
<div class="bars-col"> <div class="bars-col">
<div class="attention-item" v-for="(item, i) in weights" :key="i"> <div v-for="(item, i) in weights" :key="i" class="attention-item">
<span class="bar-word" :class="{ focus: i === focusIdx }">{{ item.word }}</span> <span class="bar-word" :class="{ focus: i === focusIdx }">{{ item.word }}</span>
<div class="bar-bg"> <div class="bar-bg">
<div class="bar-fill" :style="{ width: item.w * 100 + '%', background: barColor(item.w) }"></div> <div class="bar-fill" :style="{ width: item.w * 100 + '%', background: barColor(item.w) }"></div>
@@ -1,7 +1,7 @@
<template> <template>
<div class="demo-card"> <div class="demo-card">
<div class="bp-flow"> <div class="bp-flow">
<div class="step-block" v-for="(step, i) in steps" :key="i" :style="{ borderTopColor: step.color }"> <div v-for="(step, i) in steps" :key="i" class="step-block" :style="{ borderTopColor: step.color }">
<div class="step-num" :style="{ background: step.color }">{{ i + 1 }}</div> <div class="step-num" :style="{ background: step.color }">{{ i + 1 }}</div>
<div class="step-icon">{{ step.icon }}</div> <div class="step-icon">{{ step.icon }}</div>
<div class="step-name">{{ step.name }}</div> <div class="step-name">{{ step.name }}</div>
@@ -1,7 +1,7 @@
<template> <template>
<div class="demo-card"> <div class="demo-card">
<div class="schools-grid"> <div class="schools-grid">
<div class="school-card" v-for="s in schools" :key="s.name" :style="{ borderTopColor: s.color }"> <div v-for="s in schools" :key="s.name" class="school-card" :style="{ borderTopColor: s.color }">
<div class="card-head"> <div class="card-head">
<span class="school-icon">{{ s.icon }}</span> <span class="school-icon">{{ s.icon }}</span>
<span class="school-name" :style="{ color: s.color }">{{ s.name }}</span> <span class="school-name" :style="{ color: s.color }">{{ s.name }}</span>
@@ -1,7 +1,7 @@
<template> <template>
<div class="demo-card"> <div class="demo-card">
<div class="gpt-grid"> <div class="gpt-grid">
<div class="gpt-card" v-for="m in models" :key="m.name" :style="{ borderTopColor: m.color }"> <div v-for="m in models" :key="m.name" class="gpt-card" :style="{ borderTopColor: m.color }">
<div class="card-top"> <div class="card-top">
<span class="gpt-name" :style="{ color: m.color }">{{ m.name }}</span> <span class="gpt-name" :style="{ color: m.color }">{{ m.name }}</span>
<span class="gpt-year">{{ m.year }}</span> <span class="gpt-year">{{ m.year }}</span>
@@ -11,7 +11,7 @@
</svg> </svg>
</div> </div>
<div class="layer-cards"> <div class="layer-cards">
<div class="layer-card" v-for="info in layerInfo" :key="info.name" :style="{ borderLeftColor: info.color }"> <div v-for="info in layerInfo" :key="info.name" class="layer-card" :style="{ borderLeftColor: info.color }">
<div class="lc-title" :style="{ color: info.color }">{{ info.name }}</div> <div class="lc-title" :style="{ color: info.color }">{{ info.name }}</div>
<div class="lc-desc">{{ info.desc }}</div> <div class="lc-desc">{{ info.desc }}</div>
</div> </div>
@@ -2,13 +2,13 @@
<div class="demo-card"> <div class="demo-card">
<div class="perceptron-layout"> <div class="perceptron-layout">
<div class="inputs-col"> <div class="inputs-col">
<div class="input-node" v-for="inp in inputs" :key="inp.label"> <div v-for="inp in inputs" :key="inp.label" class="input-node">
<span class="node-circle">{{ inp.val }}</span> <span class="node-circle">{{ inp.val }}</span>
<span class="node-label">{{ inp.label }}</span> <span class="node-label">{{ inp.label }}</span>
</div> </div>
</div> </div>
<div class="weights-col"> <div class="weights-col">
<div class="weight-arrow" v-for="inp in inputs" :key="inp.label"> <div v-for="inp in inputs" :key="inp.label" class="weight-arrow">
<span class="arrow"></span> <span class="arrow"></span>
<span class="w-tag">×{{ inp.weight }}</span> <span class="w-tag">×{{ inp.weight }}</span>
</div> </div>
@@ -4,7 +4,7 @@
<span class="title">关键发展路径总结</span> <span class="title">关键发展路径总结</span>
</div> </div>
<div class="path-flow"> <div class="path-flow">
<div class="path-item" v-for="(item, i) in path" :key="i"> <div v-for="(item, i) in path" :key="i" class="path-item">
<div class="path-card" :style="{ borderLeftColor: item.color }"> <div class="path-card" :style="{ borderLeftColor: item.color }">
<div class="path-top"> <div class="path-top">
<span class="path-icon" :style="{ background: item.color }">{{ i + 1 }}</span> <span class="path-icon" :style="{ background: item.color }">{{ i + 1 }}</span>
@@ -14,9 +14,7 @@
</div> </div>
<div class="t-line"> <div class="t-line">
<span class="t-ps">&gt; </span> <span class="t-ps">&gt; </span>
<span class="t-typing" <span class="t-typing">{{ typing }}<span class="t-cur"></span></span>
>{{ typing }}<span class="t-cur"></span></span
>
</div> </div>
</div> </div>
</div> </div>
@@ -29,8 +29,7 @@
"id": 123, "id": 123,
"name": "张三" "name": "张三"
} }
}</pre }</pre>
>
</div> </div>
<div class="compare-col"> <div class="compare-col">
<div class="compare-title">列表</div> <div class="compare-title">列表</div>
@@ -44,8 +43,7 @@
"total": 100 "total": 100
} }
} }
}</pre }</pre>
>
</div> </div>
</div> </div>
<div class="note"> <div class="note">
@@ -79,8 +77,7 @@
"created_at": "2024-01-15T09:30:00.000Z", "created_at": "2024-01-15T09:30:00.000Z",
"updated_at": "2024-01-15T10:00:00.000Z", "updated_at": "2024-01-15T10:00:00.000Z",
"expired_at": "2025-01-15T00:00:00.000Z" "expired_at": "2025-01-15T00:00:00.000Z"
}</pre }</pre>
>
</div> </div>
<div class="time-rules"> <div class="time-rules">
<div class="time-rule"> <div class="time-rule">
@@ -97,9 +94,7 @@
</div> </div>
<div class="time-rule"> <div class="time-rule">
<span class="rule-label">命名</span> <span class="rule-label">命名</span>
<span class="rule-value" <span class="rule-value">xxx_at 表示时间点xxx_duration 表示时长</span>
>xxx_at 表示时间点xxx_duration 表示时长</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -114,8 +109,7 @@
"name": "张三", "name": "张三",
"nickname": null, "nickname": null,
"avatar": null "avatar": null
}</pre }</pre>
>
<div class="compare-desc">字段存在但无值时返回 null</div> <div class="compare-desc">字段存在但无值时返回 null</div>
</div> </div>
<div class="compare-col bad-col"> <div class="compare-col bad-col">
@@ -123,8 +117,7 @@
<pre class="code-sm"> <pre class="code-sm">
{ {
"name": "张三" "name": "张三"
}</pre }</pre>
>
<div class="compare-desc">省略字段前端需判断是否存在</div> <div class="compare-desc">省略字段前端需判断是否存在</div>
</div> </div>
</div> </div>
@@ -156,9 +149,7 @@
<div class="tips"> <div class="tips">
<span class="tips-icon">💡</span> <span class="tips-icon">💡</span>
<span class="tips-text" <span class="tips-text">参考 ISO 8601 时间标准字段命名保持 snake_case 风格</span>
>参考 ISO 8601 时间标准字段命名保持 snake_case 风格</span
>
</div> </div>
</div> </div>
</template> </template>
@@ -37,8 +37,7 @@
} }
] ]
} }
}</pre }</pre>
>
<div class="field-tips"> <div class="field-tips">
<div class="tip-row"> <div class="tip-row">
<code>field</code> <code>field</code>
@@ -67,8 +66,7 @@
"shortfall": 49.00, "shortfall": 49.00,
"suggestion": "请充值后重试" "suggestion": "请充值后重试"
} }
}</pre }</pre>
>
<div class="business-tips"> <div class="business-tips">
<div class="b-tip"> 返回当前状态数据便于前端展示</div> <div class="b-tip"> 返回当前状态数据便于前端展示</div>
<div class="b-tip"> 提供 suggestion 给出解决建议</div> <div class="b-tip"> 提供 suggestion 给出解决建议</div>
@@ -166,9 +164,7 @@
<div class="tips"> <div class="tips">
<span class="tips-icon">💡</span> <span class="tips-icon">💡</span>
<span class="tips-text" <span class="tips-text">错误信息要"机器可读 + 人类友好"便于前端统一处理</span>
>错误信息要"机器可读 + 人类友好"便于前端统一处理</span
>
</div> </div>
</div> </div>
</template> </template>
@@ -29,8 +29,7 @@
{ "result": { "user": {...} } } { "result": { "user": {...} } }
// C // C
{ "user": {...} }</pre { "user": {...} }</pre>
>
<div class="problem-desc"> <div class="problem-desc">
前端需要针对每个接口单独处理代码冗余容易出错 前端需要针对每个接口单独处理代码冗余容易出错
</div> </div>
@@ -43,8 +42,7 @@
"message": "success", "message": "success",
"data": { ... }, "data": { ... },
"request_id": "req-xxx" "request_id": "req-xxx"
}</pre }</pre>
>
</div> </div>
</div> </div>
@@ -141,8 +139,7 @@
"total": 156, "total": 156,
"total_pages": 8, "total_pages": 8,
"has_next": true "has_next": true
}</pre }</pre>
>
</div> </div>
</div> </div>
</div> </div>
@@ -150,9 +147,7 @@
<div class="tips"> <div class="tips">
<span class="tips-icon">💡</span> <span class="tips-icon">💡</span>
<span class="tips-text" <span class="tips-text">request_id 用于问题追踪建议使用 UUID v4 或雪花算法生成</span>
>request_id 用于问题追踪建议使用 UUID v4 或雪花算法生成</span
>
</div> </div>
</div> </div>
</template> </template>
@@ -14,8 +14,8 @@
v-for="s in scenarios" v-for="s in scenarios"
:key="s.id" :key="s.id"
:class="['raf-chip', { active: currentScenario.id === s.id }]" :class="['raf-chip', { active: currentScenario.id === s.id }]"
@click="selectScenario(s)"
:disabled="processing" :disabled="processing"
@click="selectScenario(s)"
> >
{{ s.label }} {{ s.label }}
</button> </button>
@@ -34,14 +34,14 @@
</div> </div>
<button <button
class="raf-send-btn" class="raf-send-btn"
@click="sendRequest"
:disabled="processing" :disabled="processing"
@click="sendRequest"
> >
{{ processing ? 'Sending...' : 'Send Request' }} {{ processing ? 'Sending...' : 'Send Request' }}
</button> </button>
</div> </div>
<div class="raf-response-box" v-if="response"> <div v-if="response" class="raf-response-box">
<div class="raf-status-line"> <div class="raf-status-line">
<span class="raf-label">Response Status:</span> <span class="raf-label">Response Status:</span>
<span <span
@@ -82,7 +82,7 @@
<!-- Logs --> <!-- Logs -->
<div class="raf-section"> <div class="raf-section">
<div class="raf-section-title">📜 Server Logs</div> <div class="raf-section-title">📜 Server Logs</div>
<div class="raf-logs" ref="logsRef"> <div ref="logsRef" class="raf-logs">
<div v-for="(log, i) in logs" :key="i" class="raf-log-line"> <div v-for="(log, i) in logs" :key="i" class="raf-log-line">
<span class="raf-log-time">[{{ log.time }}]</span> <span class="raf-log-time">[{{ log.time }}]</span>
<span :class="log.type">{{ log.msg }}</span> <span :class="log.type">{{ log.msg }}</span>
@@ -0,0 +1,744 @@
<template>
<div class="api-compare-root">
<div class="demo-header">
<span class="title">📚 函数 API vs HTTP API</span>
<span class="subtitle">本地调用 vs 网络请求文档怎么看</span>
</div>
<div class="control-panel">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
>
{{ tab.icon }} {{ tab.name }}
</button>
</div>
<div class="visualization-area">
<!-- 对比视图 -->
<div v-if="activeTab === 'compare'" class="compare-view">
<div class="compare-cards">
<div class="compare-card">
<div class="card-header function">
<span class="card-icon">📦</span>
<span class="card-title">函数 API</span>
</div>
<div class="card-body">
<div class="feature-list">
<div class="feature-item">
<span class="feature-label">调用方式</span>
<span class="feature-value">直接函数调用</span>
</div>
<div class="feature-item">
<span class="feature-label">参数传递</span>
<span class="feature-value">括号内传参</span>
</div>
<div class="feature-item">
<span class="feature-label">返回值</span>
<span class="feature-value">直接获得结果</span>
</div>
<div class="feature-item">
<span class="feature-label">错误处理</span>
<span class="feature-value">异常/返回值</span>
</div>
</div>
<div class="code-block">
<div class="code-label">Python 示例</div>
<pre><code># 调用内置函数
length = len("hello") # 返回 5
# 调用库函数
import math
result = math.sqrt(16) # 返回 4.0
# 调用自定义函数
def add(a, b):
return a + b
sum = add(3, 5) # 返回 8</code></pre>
</div>
</div>
</div>
<div class="vs-divider">
<span class="vs-text">VS</span>
</div>
<div class="compare-card">
<div class="card-header http">
<span class="card-icon">🌐</span>
<span class="card-title">HTTP API</span>
</div>
<div class="card-body">
<div class="feature-list">
<div class="feature-item">
<span class="feature-label">调用方式</span>
<span class="feature-value">网络请求</span>
</div>
<div class="feature-item">
<span class="feature-label">参数传递</span>
<span class="feature-value">URL/Body/Header</span>
</div>
<div class="feature-item">
<span class="feature-label">返回值</span>
<span class="feature-value">JSON/XML 响应</span>
</div>
<div class="feature-item">
<span class="feature-label">错误处理</span>
<span class="feature-value">状态码判断</span>
</div>
</div>
<div class="code-block">
<div class="code-label">HTTP 请求示例</div>
<pre><code>POST /v1/chat/completions HTTP/1.1
Host: api.deepseek.com
Authorization: Bearer sk-xxx
Content-Type: application/json
{
"model": "deepseek-chat",
"messages": [
{"role": "user", "content": "你好"}
]
}</code></pre>
</div>
</div>
</div>
</div>
</div>
<!-- 文档对比视图 -->
<div v-if="activeTab === 'docs'" class="docs-view">
<div class="docs-cards">
<div class="doc-card">
<div class="doc-header">
<span class="doc-icon">📖</span>
<span class="doc-title">函数文档怎么看</span>
</div>
<div class="doc-content">
<div class="doc-section">
<div class="doc-section-title">🔍 关注重点</div>
<ul class="doc-list">
<li><strong>函数签名</strong>函数名和参数列表</li>
<li><strong>参数类型</strong>每个参数要什么类型</li>
<li><strong>返回值</strong>函数返回什么</li>
<li><strong>异常说明</strong>可能抛出什么错误</li>
</ul>
</div>
<div class="doc-example">
<div class="doc-example-label">Python 文档示例</div>
<pre><code>def open(file: str, mode: str = 'r') -> TextIO:
"""
打开文件并返回文件对象
Args:
file: 文件路径
mode: 打开模式 ('r', 'w', 'a')
Returns:
文件对象
Raises:
FileNotFoundError: 文件不存在
"""</code></pre>
</div>
</div>
</div>
<div class="doc-card">
<div class="doc-header">
<span class="doc-icon">📡</span>
<span class="doc-title">HTTP API 文档怎么看</span>
</div>
<div class="doc-content">
<div class="doc-section">
<div class="doc-section-title">🔍 关注重点</div>
<ul class="doc-list">
<li><strong>Endpoint</strong>URL 路径和 HTTP 方法</li>
<li><strong>认证方式</strong>API Key / Token 怎么传</li>
<li><strong>请求参数</strong>Body / Query / Header</li>
<li><strong>响应格式</strong>成功和错误返回什么</li>
</ul>
</div>
<div class="doc-example">
<div class="doc-example-label">API 文档示例</div>
<pre><code>POST /v1/chat/completions
Headers:
Authorization: Bearer {api_key}
Content-Type: application/json
Body:
{
"model": "deepseek-chat",
"messages": [...],
"temperature": 0.7
}
Response:
{
"choices": [{
"message": {"content": "..."}
}]
}</code></pre>
</div>
</div>
</div>
</div>
</div>
<!-- 快速判断视图 -->
<div v-if="activeTab === 'quick'" class="quick-view">
<div class="quick-cards">
<div class="quick-card">
<div class="quick-header">
<span class="quick-icon"></span>
<span class="quick-title">快速判断指南</span>
</div>
<div class="quick-content">
<div class="decision-tree">
<div class="decision-item">
<div class="decision-question">看到代码里有 <code>()</code> 调用</div>
<div class="decision-answer"> 这是 <strong>函数 API</strong></div>
<div class="decision-example">len(), print(), requests.get()</div>
</div>
<div class="decision-arrow"></div>
<div class="decision-item">
<div class="decision-question">看到 URL HTTP 方法</div>
<div class="decision-answer"> 这是 <strong>HTTP API</strong></div>
<div class="decision-example">POST /api/users, GET https://...</div>
</div>
<div class="decision-arrow"></div>
<div class="decision-item">
<div class="decision-question">看到 SDK/Client 对象</div>
<div class="decision-answer"> 这是 <strong>封装后的 HTTP API</strong></div>
<div class="decision-example">client.chat.completions.create()</div>
</div>
</div>
</div>
</div>
<div class="quick-card">
<div class="quick-header">
<span class="quick-icon">🎯</span>
<span class="quick-title">使用场景对比</span>
</div>
<div class="quick-content">
<div class="scenario-table">
<div class="scenario-row header">
<div class="scenario-cell">场景</div>
<div class="scenario-cell">推荐方式</div>
<div class="scenario-cell">原因</div>
</div>
<div class="scenario-row">
<div class="scenario-cell">本地数据处理</div>
<div class="scenario-cell"><span class="badge function">函数 API</span></div>
<div class="scenario-cell">快速无需网络</div>
</div>
<div class="scenario-row">
<div class="scenario-cell">调用 AI 模型</div>
<div class="scenario-cell"><span class="badge http">HTTP API</span></div>
<div class="scenario-cell">模型在远程服务器</div>
</div>
<div class="scenario-row">
<div class="scenario-cell">获取天气数据</div>
<div class="scenario-cell"><span class="badge http">HTTP API</span></div>
<div class="scenario-cell">数据在服务商那里</div>
</div>
<div class="scenario-row">
<div class="scenario-cell">文件读写操作</div>
<div class="scenario-cell"><span class="badge function">函数 API</span></div>
<div class="scenario-cell">直接操作本地文件</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心要点</strong>函数 API "本地办事"HTTP API "远程通信"看文档时函数关注参数和返回值HTTP API 关注 Endpoint认证和请求/响应格式
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeTab = ref('compare')
const tabs = [
{ id: 'compare', name: '核心区别', icon: '🔍' },
{ id: 'docs', name: '文档对比', icon: '📚' },
{ id: 'quick', name: '快速判断', icon: '⚡' }
]
</script>
<style scoped>
.api-compare-root {
margin: 20px 0;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
overflow: hidden;
}
.demo-header {
padding: 16px 20px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
display: block;
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.control-panel {
display: flex;
gap: 0;
border-bottom: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
.tab-btn {
flex: 1;
padding: 12px 16px;
background: transparent;
border: none;
border-right: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
color: var(--vp-c-text-2);
}
.tab-btn:last-child {
border-right: none;
}
.tab-btn:hover {
background: var(--vp-c-bg-mute);
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
}
.visualization-area {
padding: 20px;
background: var(--vp-c-bg);
}
/* 对比视图 */
.compare-view {
width: 100%;
}
.compare-cards {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
align-items: stretch;
}
@media (max-width: 768px) {
.compare-cards {
grid-template-columns: 1fr;
}
.vs-divider {
display: none;
}
}
.compare-card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: var(--vp-c-bg-soft);
}
.card-header {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 0.95rem;
}
.card-header.function {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.card-header.http {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
}
.card-icon {
font-size: 1.2rem;
}
.card-body {
padding: 16px;
}
.feature-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
@media (max-width: 480px) {
.feature-list {
grid-template-columns: 1fr;
}
}
.feature-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.feature-label {
font-size: 0.7rem;
color: var(--vp-c-text-3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.feature-value {
font-size: 0.85rem;
color: var(--vp-c-text-1);
font-weight: 500;
}
.code-block {
background: #0a0a0a;
border-radius: 6px;
overflow: hidden;
}
.code-label {
padding: 8px 12px;
background: #18181b;
color: #71717a;
font-size: 0.75rem;
font-weight: 600;
border-bottom: 1px solid #27272a;
}
.code-block pre {
margin: 0;
padding: 12px;
color: #e4e4e7;
font-size: 0.8rem;
line-height: 1.6;
overflow-x: auto;
}
.code-block code {
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
}
.vs-text {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
/* 文档视图 */
.docs-view {
width: 100%;
}
.docs-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.docs-cards {
grid-template-columns: 1fr;
}
}
.doc-card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: var(--vp-c-bg-soft);
}
.doc-header {
padding: 14px 16px;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.doc-icon {
font-size: 1.2rem;
}
.doc-content {
padding: 16px;
}
.doc-section {
margin-bottom: 16px;
}
.doc-section-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.doc-list {
list-style: none;
padding: 0;
margin: 0;
}
.doc-list li {
padding: 6px 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
border-bottom: 1px dashed var(--vp-c-divider);
}
.doc-list li:last-child {
border-bottom: none;
}
.doc-list strong {
color: var(--vp-c-brand);
}
.doc-example {
background: #0a0a0a;
border-radius: 6px;
overflow: hidden;
margin-top: 12px;
}
.doc-example-label {
padding: 8px 12px;
background: #18181b;
color: #71717a;
font-size: 0.75rem;
font-weight: 600;
border-bottom: 1px solid #27272a;
}
.doc-example pre {
margin: 0;
padding: 12px;
color: #e4e4e7;
font-size: 0.75rem;
line-height: 1.6;
overflow-x: auto;
}
.doc-example code {
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
}
/* 快速判断视图 */
.quick-view {
width: 100%;
}
.quick-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.quick-cards {
grid-template-columns: 1fr;
}
}
.quick-card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: var(--vp-c-bg-soft);
}
.quick-header {
padding: 14px 16px;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.quick-icon {
font-size: 1.2rem;
}
.quick-content {
padding: 16px;
}
.decision-tree {
display: flex;
flex-direction: column;
gap: 8px;
}
.decision-item {
padding: 14px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.decision-question {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 6px;
}
.decision-question code {
background: var(--vp-c-bg-mute);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.8rem;
}
.decision-answer {
font-size: 0.9rem;
color: var(--vp-c-text-1);
font-weight: 600;
}
.decision-example {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-top: 6px;
padding-top: 6px;
border-top: 1px dashed var(--vp-c-divider);
}
.decision-arrow {
text-align: center;
color: var(--vp-c-text-3);
font-size: 1.2rem;
}
.scenario-table {
display: flex;
flex-direction: column;
gap: 1px;
background: var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.scenario-row {
display: grid;
grid-template-columns: 1.2fr 1fr 1.2fr;
gap: 12px;
padding: 12px;
background: var(--vp-c-bg);
align-items: center;
}
.scenario-row.header {
background: var(--vp-c-bg-alt);
font-weight: 600;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.scenario-cell {
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.badge.function {
background: rgba(16, 185, 129, 0.15);
color: #059669;
}
.badge.http {
background: rgba(59, 130, 246, 0.15);
color: #2563eb;
}
/* Info Box */
.info-box {
display: flex;
gap: 0.5rem;
padding: 14px 20px;
background: var(--vp-c-bg-soft);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,652 @@
<template>
<div class="doc-types-root">
<div class="demo-header">
<span class="title">📋 不同文档类型怎么看</span>
<span class="subtitle">函数文档REST API 文档SDK 文档各有侧重点</span>
</div>
<div class="control-panel">
<button
v-for="doc in docTypes"
:key="doc.id"
:class="['doc-tab', { active: activeDoc === doc.id }]"
@click="activeDoc = doc.id"
>
<span class="tab-icon">{{ doc.icon }}</span>
<span class="tab-name">{{ doc.name }}</span>
</button>
</div>
<div class="visualization-area">
<div class="doc-display">
<!-- 文档头部信息 -->
<div class="doc-info-bar">
<div class="info-item">
<span class="info-label">文档类型</span>
<span class="info-value">{{ currentDoc.name }}</span>
</div>
<div class="info-item">
<span class="info-label">适用场景</span>
<span class="info-value">{{ currentDoc.scenario }}</span>
</div>
<div class="info-item">
<span class="info-label">阅读难度</span>
<span class="info-value">
<span class="difficulty-stars">{{ currentDoc.difficulty }}</span>
</span>
</div>
</div>
<!-- 关键信息区 -->
<div class="key-points">
<div class="point-section">
<div class="point-title">🔍 看文档时重点关注</div>
<div class="point-tags">
<span v-for="(point, idx) in currentDoc.keyPoints" :key="idx" class="point-tag">
{{ point }}
</span>
</div>
</div>
</div>
<!-- 文档示例区 -->
<div class="doc-example-area">
<div class="example-header">
<span class="example-icon">📝</span>
<span class="example-title">文档示例</span>
</div>
<div class="example-content">
<pre><code>{{ currentDoc.example }}</code></pre>
</div>
</div>
<!-- 阅读技巧 -->
<div class="reading-tips">
<div class="tips-header">
<span class="tips-icon">💡</span>
<span class="tips-title">阅读技巧</span>
</div>
<ul class="tips-list">
<li v-for="(tip, idx) in currentDoc.tips" :key="idx">{{ tip }}</li>
</ul>
</div>
</div>
</div>
<!-- 对比总结 -->
<div class="comparison-summary">
<div class="summary-header">
<span class="summary-icon">📊</span>
<span class="summary-title">三种文档快速对比</span>
</div>
<div class="summary-table">
<div class="summary-row header">
<div class="summary-cell">对比项</div>
<div class="summary-cell">函数文档</div>
<div class="summary-cell">REST API 文档</div>
<div class="summary-cell">SDK 文档</div>
</div>
<div class="summary-row">
<div class="summary-cell label">核心关注</div>
<div class="summary-cell">参数返回值</div>
<div class="summary-cell">Endpoint请求体</div>
<div class="summary-cell">初始化方法链</div>
</div>
<div class="summary-row">
<div class="summary-cell label">代码示例</div>
<div class="summary-cell">函数调用</div>
<div class="summary-cell">HTTP 请求</div>
<div class="summary-cell">对象方法</div>
</div>
<div class="summary-row">
<div class="summary-cell label">错误处理</div>
<div class="summary-cell">异常/返回值</div>
<div class="summary-cell">状态码</div>
<div class="summary-cell">异常对象</div>
</div>
<div class="summary-row">
<div class="summary-cell label">先看什么</div>
<div class="summary-cell">函数签名</div>
<div class="summary-cell">Base URL + Auth</div>
<div class="summary-cell">Quick Start</div>
</div>
</div>
</div>
<div class="info-box">
<strong>阅读建议</strong>函数文档看签名API 文档看请求格式SDK 文档看示例遇到不会的先找Quick StartGetting Started章节
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeDoc = ref('function')
const docTypes = [
{
id: 'function',
icon: '📦',
name: '函数文档',
scenario: '使用标准库/第三方库函数',
difficulty: '⭐⭐',
keyPoints: ['函数签名', '参数类型', '返回值', '异常说明', '示例代码'],
example: `### json.loads(s, *, cls=None, object_hook=None...)
JSON 字符串解析为 Python 对象
**参数**
- s (str): 要解析的 JSON 字符串
- cls (JSONDecoder): 自定义解码器类
- object_hook (callable): 可选的转换函数
**返回值**
- dict | list: 解析后的 Python 对象
**异常**
- JSONDecodeError: 字符串格式非法
**示例**
>>> import json
>>> json.loads('{"name": "Alice"}')
{'name': 'Alice'}`,
tips: [
'先看函数签名,了解需要什么参数',
'注意参数的类型和是否必填',
'查看返回值类型,方便后续处理',
'关注可能抛出的异常,做好错误处理'
]
},
{
id: 'rest',
icon: '🌐',
name: 'REST API 文档',
scenario: '调用远程 HTTP 接口',
difficulty: '⭐⭐⭐',
keyPoints: ['Base URL', '认证方式', 'Endpoint', '请求参数', '响应格式', '错误码'],
example: `## POST /v1/chat/completions
创建聊天完成请求
### 认证
Authorization: Bearer {api_key}
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| model | string | | 模型名称 |
| messages | array | | 消息列表 |
| temperature | float | | 采样温度 (0-2) |
### 请求示例
{
"model": "deepseek-chat",
"messages": [
{"role": "user", "content": "Hello"}
],
"temperature": 0.7
}
### 响应示例
{
"choices": [{
"message": {
"role": "assistant",
"content": "Hello! How can I help you?"
}
}]
}`,
tips: [
'先找到 Base URL 和认证方式(通常是 API Key',
'确认 HTTP 方法(GET/POST/PUT/DELETE',
'看清参数是放在 URL、Header 还是 Body 里',
'注意必填参数和可选参数的区别',
'查看错误码列表,了解各种异常情况'
]
},
{
id: 'sdk',
icon: '📚',
name: 'SDK 文档',
scenario: '使用官方封装好的开发工具包',
difficulty: '⭐⭐',
keyPoints: ['安装方式', '初始化', '核心类/方法', '配置选项', '最佳实践'],
example: `## OpenAI Python SDK
### 安装
pip install openai
### 初始化客户端
from openai import OpenAI
client = OpenAI(
api_key="your-api-key",
base_url="https://api.deepseek.com/v1"
)
### 创建聊天完成
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "user", "content": "Hello!"}
],
temperature=0.7,
max_tokens=1000
)
print(response.choices[0].message.content)
### 流式响应
stream = client.chat.completions.create(
model="deepseek-chat",
messages=[...],
stream=True
)
for chunk in stream:
print(chunk.choices[0].delta.content, end="")`,
tips: [
'先看 Quick Start / Getting Started 章节',
'了解如何初始化和配置客户端',
'关注核心类和方法的使用方式',
'查看高级配置选项(如超时、重试)',
'参考官方示例代码,理解最佳实践'
]
},
{
id: 'websocket',
icon: '🔌',
name: 'WebSocket 文档',
scenario: '实时双向通信',
difficulty: '⭐⭐⭐⭐',
keyPoints: ['连接地址', '连接建立', '消息格式', '事件处理', '心跳机制', '断开重连'],
example: `## WebSocket API
### 连接地址
wss://api.example.com/v1/stream
### 连接流程
1. **建立连接**
- 发送握手请求
- 服务端返回连接确认
2. **发送消息**
{
"type": "subscribe",
"channel": "price_updates",
"symbol": "BTC-USD"
}
3. **接收推送**
{
"type": "update",
"data": {
"symbol": "BTC-USD",
"price": "45000.00",
"timestamp": 1703001600
}
}
### 心跳机制
客户端每 30 秒发送 ping
{"type": "ping"}
服务端返回 pong
{"type": "pong"}`,
tips: [
'注意 ws:// 和 wss:// 的区别(是否加密)',
'了解连接建立和关闭的时机',
'明确消息的数据格式和类型',
'实现心跳检测,保持连接活跃',
'处理好断线重连逻辑',
'关注并发连接数限制'
]
}
]
const currentDoc = computed(() => {
return docTypes.find(d => d.id === activeDoc.value) || docTypes[0]
})
</script>
<style scoped>
.doc-types-root {
margin: 20px 0;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
overflow: hidden;
}
.demo-header {
padding: 16px 20px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
display: block;
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.control-panel {
display: flex;
gap: 0;
border-bottom: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
.doc-tab {
flex: 1;
padding: 14px 12px;
background: transparent;
border: none;
border-right: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
color: var(--vp-c-text-2);
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.doc-tab:last-child {
border-right: none;
}
.doc-tab:hover {
background: var(--vp-c-bg-mute);
}
.doc-tab.active {
background: var(--vp-c-brand);
color: white;
}
.tab-icon {
font-size: 1.4rem;
}
.tab-name {
font-size: 0.8rem;
}
.visualization-area {
padding: 20px;
background: var(--vp-c-bg);
}
.doc-display {
display: flex;
flex-direction: column;
gap: 16px;
}
.doc-info-bar {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
@media (max-width: 600px) {
.doc-info-bar {
grid-template-columns: 1fr;
}
}
.info-item {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.info-label {
font-size: 0.7rem;
color: var(--vp-c-text-3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value {
font-size: 0.9rem;
color: var(--vp-c-text-1);
font-weight: 600;
}
.difficulty-stars {
color: #f59e0b;
}
.key-points {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--vp-c-divider);
}
.point-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.point-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.point-tag {
padding: 6px 12px;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.doc-example-area {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: #0a0a0a;
}
.example-header {
padding: 12px 16px;
background: #18181b;
border-bottom: 1px solid #27272a;
display: flex;
align-items: center;
gap: 8px;
}
.example-icon {
font-size: 1rem;
}
.example-title {
font-size: 0.85rem;
font-weight: 600;
color: #a1a1aa;
}
.example-content pre {
margin: 0;
padding: 16px;
color: #e4e4e7;
font-size: 0.8rem;
line-height: 1.7;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.example-content code {
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
}
.reading-tips {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.tips-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.tips-icon {
font-size: 1rem;
}
.tips-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.tips-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.tips-list li {
padding: 10px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand);
}
.comparison-summary {
margin: 0 20px 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: var(--vp-c-bg-soft);
}
.summary-header {
padding: 14px 16px;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.summary-icon {
font-size: 1.1rem;
}
.summary-table {
display: flex;
flex-direction: column;
}
.summary-row {
display: grid;
grid-template-columns: 1fr 1.2fr 1.2fr 1.2fr;
gap: 1px;
background: var(--vp-c-divider);
}
.summary-row:not(.header) {
background: var(--vp-c-divider);
}
.summary-row.header {
background: var(--vp-c-bg-alt);
}
.summary-row.header .summary-cell {
background: var(--vp-c-bg-alt);
font-weight: 600;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.summary-cell {
padding: 12px;
background: var(--vp-c-bg);
font-size: 0.8rem;
color: var(--vp-c-text-1);
display: flex;
align-items: center;
}
.summary-cell.label {
font-weight: 600;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
}
@media (max-width: 768px) {
.summary-row {
grid-template-columns: 1fr;
}
.summary-row.header {
display: none;
}
.summary-cell {
padding: 8px 12px;
}
.summary-cell::before {
content: attr(data-label);
font-weight: 600;
color: var(--vp-c-text-2);
margin-right: 8px;
}
}
.info-box {
display: flex;
gap: 0.5rem;
padding: 14px 20px;
background: var(--vp-c-bg-soft);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
</style>
@@ -166,9 +166,9 @@
<div class="calc-row"> <div class="calc-row">
<span class="calc-label">本位</span> <span class="calc-label">本位</span>
<span class="calc-formula"> <span class="calc-formula">
{{ stages[activeBit]?.a }} {{ stages[activeBit]?.b }} {{ stages[activeBit]?.a }} XOR {{ stages[activeBit]?.b }}
<span v-if="stages[activeBit]?.cin !== null"> <span v-if="stages[activeBit]?.cin !== null">
{{ stages[activeBit]?.cin }}</span> XOR {{ stages[activeBit]?.cin }}</span>
= <strong>{{ stages[activeBit]?.sum }}</strong> = <strong>{{ stages[activeBit]?.sum }}</strong>
</span> </span>
<span class="calc-reason">{{ getSumReason(stages[activeBit]) }}</span> <span class="calc-reason">{{ getSumReason(stages[activeBit]) }}</span>
@@ -39,8 +39,7 @@
:key="'a' + i" :key="'a' + i"
class="bit" class="bit"
:class="{ hl: activeBit === 3 - i }" :class="{ hl: activeBit === 3 - i }"
>{{ b }}</span >{{ b }}</span>
>
</span> </span>
<span class="binary-dec">= {{ clampedA }}</span> <span class="binary-dec">= {{ clampedA }}</span>
</div> </div>
@@ -52,8 +51,7 @@
:key="'b' + i" :key="'b' + i"
class="bit" class="bit"
:class="{ hl: activeBit === 3 - i }" :class="{ hl: activeBit === 3 - i }"
>{{ b }}</span >{{ b }}</span>
>
</span> </span>
<span class="binary-dec">= {{ clampedB }}</span> <span class="binary-dec">= {{ clampedB }}</span>
</div> </div>
@@ -65,8 +63,7 @@
:key="'s' + i" :key="'s' + i"
class="bit" class="bit"
:class="{ hl: activeBit === 3 - i }" :class="{ hl: activeBit === 3 - i }"
>{{ b }}</span >{{ b }}</span>
>
</span> </span>
<span class="binary-dec">= {{ fourBitResult }}</span> <span class="binary-dec">= {{ fourBitResult }}</span>
</div> </div>
@@ -94,25 +91,14 @@
</span> </span>
</div> </div>
<div class="stage-io"> <div class="stage-io">
<span class="io-item" <span class="io-item"><span class="io-tag a">A</span>{{ stage.a }}</span>
><span class="io-tag a">A</span>{{ stage.a }}</span <span class="io-item"><span class="io-tag b">B</span>{{ stage.b }}</span>
> <span v-if="stage.carryIn !== null" class="io-item"><span class="io-tag cin">Cin</span>{{ stage.carryIn }}</span>
<span class="io-item"
><span class="io-tag b">B</span>{{ stage.b }}</span
>
<span v-if="stage.carryIn !== null" class="io-item"
><span class="io-tag cin">Cin</span>{{ stage.carryIn }}</span
>
</div> </div>
<div class="stage-divider"></div> <div class="stage-divider"></div>
<div class="stage-io"> <div class="stage-io">
<span class="io-item" <span class="io-item"><span class="io-tag s">S</span><strong>{{ stage.sum }}</strong></span>
><span class="io-tag s">S</span <span class="io-item"><span class="io-tag cout">C</span>{{ stage.carryOut }}</span>
><strong>{{ stage.sum }}</strong></span
>
<span class="io-item"
><span class="io-tag cout">C</span>{{ stage.carryOut }}</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -132,9 +132,7 @@
<div v-if="coinResult.length" class="coin-result"> <div v-if="coinResult.length" class="coin-result">
<div class="result-title">找零方案</div> <div class="result-title">找零方案</div>
<div class="coin-list"> <div class="coin-list">
<span v-for="(c, i) in coinResult" :key="i" class="coin" <span v-for="(c, i) in coinResult" :key="i" class="coin">{{ c }}</span>
>{{ c }}</span
>
</div> </div>
<div class="result-summary"> <div class="result-summary">
{{ coinResult.length }} 枚硬币 {{ coinResult.length }} 枚硬币
@@ -157,8 +155,7 @@
</div> </div>
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong <strong>核心思想</strong>算法是解决问题的方法好的算法能让程序效率提升几个数量级理解算法思维比记住具体算法更重要
>算法是解决问题的方法好的算法能让程序效率提升几个数量级理解算法思维比记住具体算法更重要
</div> </div>
</div> </div>
</template> </template>
@@ -0,0 +1,322 @@
<template>
<div class="addition-rules">
<div class="demo-header">
<span class="title">从手算加法到逻辑门</span>
<span class="subtitle">计算机如何只用 0 1 做数学题看看这个规律</span>
</div>
<!-- 1. 十进制类比 -->
<div class="section">
<div class="section-title">第一步回顾十进制的"进位"</div>
<div class="decimal-analogy">
<div class="math-column">
<div class="math-row">
<span class="digit carry-mark">1</span> <!-- 进位标记 -->
</div>
<div class="math-row">
<span class="digit"></span>
<span class="digit">7</span>
</div>
<div class="math-row">
<span class="op">+</span>
<span class="digit">5</span>
</div>
<div class="math-line"></div>
<div class="math-row result-row">
<span class="digit c-color">1</span>
<span class="digit s-color">2</span>
</div>
</div>
<div class="analogy-text">
<p>
因为 7 + 5 = 12这个结果超出了个位能装下的最大数字 (9)
我们把 12 拆成"一个完整的 10""剩下的 2"
</p>
<ul>
<li>
留在当前位置的那个 <span class="badge s-badge">2</span> <strong>写在个位</strong>这叫 <strong class="s-color">本位 (Sum)</strong>
</li>
<li>
"完整的 10"向十位<strong>进了一个 1</strong> <strong class="c-color">进位 (Carry)</strong>
</li>
</ul>
</div>
</div>
</div>
<!-- 2. 二进制四种情况交互 -->
<div class="section">
<div class="section-title">第二步二进制加法的 4 种情况点点看</div>
<div class="binary-demo">
<div class="binary-calc">
<button class="bit-btn" :class="{ on: inputA }" @click="inputA = !inputA">{{ inputA ? '1' : '0' }}</button>
<span class="op">+</span>
<button class="bit-btn" :class="{ on: inputB }" @click="inputB = !inputB">{{ inputB ? '1' : '0' }}</button>
<span class="op">=</span>
<span class="res-box">
<span class="res-bit carry-bit" :class="{ lit: carry }">{{ carry ? '1' : '0' }}</span>
<span class="res-bit sum-bit" :class="{ lit: sum }">{{ sum ? '1' : '0' }}</span>
</span>
</div>
<div class="binary-explain">
<p v-if="!inputA && !inputB">
0 + 0 = 0<br>本位写 <strong>0</strong>不进位
</p>
<p v-if="(!inputA && inputB) || (inputA && !inputB)">
{{ inputA ? '1' : '0' }} + {{ inputB ? '1' : '0' }} = 1<br>本位写 <strong>1</strong>不进位
</p>
<p v-if="inputA && inputB">
1 + 1 = 10<br>
二进制"满 2 就进 1"所以本位写 <strong class="s-color">0</strong>向左进位 <strong class="c-color">1</strong>
</p>
</div>
</div>
</div>
<!-- 3. 找出规律并对应到逻辑门 -->
<div class="section mb-0">
<div class="section-title">第三步给规律起个名字电路化</div>
<div class="rules-container">
<!-- 所有的 4 种情况一览表 -->
<div class="rules-table">
<div class="rt-head">
<span>A</span><span>B</span><span class="c-color">进位</span><span class="s-color">本位</span>
</div>
<div class="rt-row" :class="{ active: !inputA && !inputB }"><span>0</span><span>0</span><span>0</span><span>0</span></div>
<div class="rt-row" :class="{ active: !inputA && inputB }"> <span>0</span><span>1</span><span>0</span><span>1</span></div>
<div class="rt-row" :class="{ active: inputA && !inputB }"> <span>1</span><span>0</span><span>0</span><span>1</span></div>
<div class="rt-row" :class="{ active: inputA && inputB }"> <span>1</span><span>1</span><span>1</span><span>0</span></div>
</div>
<div class="rules-text">
<div class="rule-card sum-rule" :class="{ active: sum }">
<div class="rc-title"><span class="badge s-badge">本位</span> 规律</div>
<div class="rc-desc">
只有当输入是 (0,1) (1,0) 本位才是 1<br>
<strong>总结</strong>只有两个输入<strong>不同</strong>时才为 1<br>
<div class="rc-gate">这个规律在电路中叫 <strong>XOR (异或门)</strong></div>
</div>
</div>
<div class="rule-card carry-rule" :class="{ active: carry }">
<div class="rc-title"><span class="badge c-badge">进位</span> 规律</div>
<div class="rc-desc">
只有当输入是 (1,1) 进位才是 1<br>
<strong>总结</strong>只有两个输入<strong>都是 1</strong> 时才为 1<br>
<div class="rc-gate">这个规律在电路中叫 <strong>AND (与门)</strong></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inputA = ref(false)
const inputB = ref(false)
const sum = computed(() => inputA.value !== inputB.value)
const carry = computed(() => inputA.value && inputB.value)
</script>
<style scoped>
.addition-rules {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
background: var(--vp-c-bg-soft);
padding: 1.2rem;
margin: 1.5rem 0;
}
.demo-header {
margin-bottom: 1.2rem;
}
.title {
display: block;
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.8rem;
color: var(--vp-c-text-3);
}
.section {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.mb-0 { margin-bottom: 0; }
.section-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-brand-1);
margin-bottom: 0.8rem;
padding-bottom: 0.4rem;
border-bottom: 1px dashed var(--vp-c-divider);
}
/* 颜色常量 */
.s-color { color: #16a34a; font-weight: bold; }
.c-color { color: #d97706; font-weight: bold; }
.badge { padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.75rem; font-family: monospace; }
.s-badge { background: #dcfce7; color: #166534; }
.c-badge { background: #fef3c7; color: #92400e; }
/* 1. 十进制类比 */
.decimal-analogy {
display: flex;
gap: 2rem;
align-items: center;
flex-wrap: wrap;
}
.math-column {
display: inline-flex;
flex-direction: column;
align-items: flex-end;
font-family: monospace;
font-size: 1.5rem;
background: var(--vp-c-bg-alt);
padding: 1rem 1.5rem;
border-radius: 6px;
position: relative;
}
.math-row {
display: flex;
gap: 0.5rem;
line-height: 1.2;
}
.digit { width: 1.2rem; text-align: center; }
.op { font-weight: bold; color: var(--vp-c-text-3); margin-right: 0.2rem; }
.math-line {
width: 100%;
height: 2px;
background: var(--vp-c-text-2);
margin: 0.2rem 0;
}
.carry-mark {
color: #d97706;
font-size: 0.8rem;
line-height: 1;
transform: translateY(10px);
}
.analogy-text {
flex: 1;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.analogy-text ul { padding-left: 1.2rem; margin-top: 0.5rem; }
/* 2. 二进制四种情况 */
.binary-demo {
display: flex;
gap: 2rem;
align-items: center;
flex-wrap: wrap;
}
.binary-calc {
display: flex;
align-items: center;
gap: 0.6rem;
background: var(--vp-c-bg-alt);
padding: 0.8rem 1.2rem;
border-radius: 6px;
}
.bit-btn {
width: 3rem; height: 3rem; font-size: 1.5rem; font-weight: bold; font-family: monospace;
border-radius: 6px; background: var(--vp-c-bg); border: 2px solid var(--vp-c-divider);
cursor: pointer; transition: all 0.2s;
display: flex; align-items: center; justify-content: center;
}
.bit-btn.on { background: #dbeafe; color: #1d4ed8; border-color: #3b82f6; }
.res-box { display: flex; gap: 0.2rem; margin-left: 0.5rem; }
.res-bit {
width: 3rem; height: 3rem; border-radius: 6px; border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg); font-size: 1.5rem; font-weight: bold; font-family: monospace;
display: flex; align-items: center; justify-content: center;
color: var(--vp-c-text-3); transition: all 0.2s;
}
.carry-bit.lit { background: #fef3c7; color: #d97706; border-color: #d97706; }
.sum-bit.lit { background: #dcfce7; color: #16a34a; border-color: #16a34a; }
.binary-explain {
flex: 1;
background: var(--vp-c-bg-alt);
padding: 0.8rem 1rem;
border-radius: 6px;
border-left: 3px solid #3b82f6;
color: var(--vp-c-text-2);
font-size: 0.85rem;
line-height: 1.5;
min-width: 200px;
}
.binary-explain p { margin: 0; }
/* 3. 找出规律 */
.rules-container {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
.rules-table {
flex: 0 0 auto;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
font-family: monospace;
font-size: 0.85rem;
background: var(--vp-c-bg-alt);
}
.rt-head, .rt-row {
display: grid;
grid-template-columns: 2rem 2rem 3rem 3rem;
text-align: center;
padding: 0.4rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.rt-row:last-child { border-bottom: none; }
.rt-head { font-weight: bold; font-family: system-ui; font-size: 0.75rem; background: var(--vp-c-bg); }
.rt-row.active { background: #dbeafe; font-weight: bold; }
.rules-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.8rem;
min-width: 250px;
}
.rule-card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.8rem;
transition: all 0.2s;
background: var(--vp-c-bg-alt);
}
.sum-rule.active { border-color: #16a34a; background: #f0fdf4; }
.carry-rule.active { border-color: #d97706; background: #fffbeb; }
.rc-title { font-size: 0.8rem; font-weight: bold; margin-bottom: 0.4rem; color: var(--vp-c-text-1); }
.rc-desc { font-size: 0.75rem; color: var(--vp-c-text-2); line-height: 1.5; }
.rc-gate {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px dashed var(--vp-c-divider);
color: var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.decimal-analogy, .binary-demo, .rules-container { flex-direction: column; align-items: stretch; }
.math-column, .rules-table { align-self: center; }
}
</style>
@@ -48,8 +48,7 @@
v-for="(task, j) in currentStage.tasks" v-for="(task, j) in currentStage.tasks"
:key="j" :key="j"
class="task-chip" class="task-chip"
>{{ task }}</span >{{ task }}</span>
>
</div> </div>
<div class="detail-example"> <div class="detail-example">
@@ -88,9 +87,7 @@
<div class="exec-flow"> <div class="exec-flow">
<span v-for="(step, i) in model.steps" :key="i" class="flow-tag"> <span v-for="(step, i) in model.steps" :key="i" class="flow-tag">
{{ step }} {{ step }}
<span v-if="i < model.steps.length - 1" class="flow-arrow" <span v-if="i < model.steps.length - 1" class="flow-arrow"></span>
></span
>
</span> </span>
</div> </div>
<div class="exec-traits"> <div class="exec-traits">
@@ -104,8 +101,7 @@
</div> </div>
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong <strong>核心思想</strong>编译器像翻译官把人类能读懂的代码逐步翻译成机器能执行的指令六个阶段各司其职识别单词
>编译器像翻译官把人类能读懂的代码逐步翻译成机器能执行的指令六个阶段各司其职识别单词
理解语法 检查语义 生成中间码 优化 生成机器码 理解语法 检查语义 生成中间码 优化 生成机器码
</div> </div>
</div> </div>
@@ -146,7 +146,7 @@
<span class="gate-name">XOR</span> <span class="gate-name">XOR</span>
<span class="gate-cn">异或门</span> <span class="gate-cn">异或门</span>
</div> </div>
<div class="gate-formula">A B</div> <div class="gate-formula">A XOR B</div>
<div class="gate-desc">不同为 1 本位</div> <div class="gate-desc">不同为 1 本位</div>
</div> </div>
<div class="gate-box" :class="{ active: haCarry }"> <div class="gate-box" :class="{ active: haCarry }">
@@ -154,7 +154,7 @@
<span class="gate-name">AND</span> <span class="gate-name">AND</span>
<span class="gate-cn">与门</span> <span class="gate-cn">与门</span>
</div> </div>
<div class="gate-formula">A B</div> <div class="gate-formula">A AND B</div>
<div class="gate-desc"> 1 1 进位</div> <div class="gate-desc"> 1 1 进位</div>
</div> </div>
</div> </div>
@@ -203,13 +203,13 @@
</div> </div>
<div class="calc-row"> <div class="calc-row">
<span class="calc-label">本位</span> <span class="calc-label">本位</span>
<span class="calc-formula">A B = {{ haA ? '1' : '0' }} {{ haB ? '1' : '0' }} = <span class="calc-formula">A XOR B = {{ haA ? '1' : '0' }} XOR {{ haB ? '1' : '0' }} =
<strong>{{ haSum ? '1' : '0' }}</strong></span> <strong>{{ haSum ? '1' : '0' }}</strong></span>
<span class="calc-reason">{{ haA !== haB ? '不同' : '相同' }}</span> <span class="calc-reason">{{ haA !== haB ? '不同' : '相同' }}</span>
</div> </div>
<div class="calc-row"> <div class="calc-row">
<span class="calc-label">进位</span> <span class="calc-label">进位</span>
<span class="calc-formula">A B = {{ haA ? '1' : '0' }} {{ haB ? '1' : '0' }} = <span class="calc-formula">A AND B = {{ haA ? '1' : '0' }} AND {{ haB ? '1' : '0' }} =
<strong>{{ haCarry ? '1' : '0' }}</strong></span> <strong>{{ haCarry ? '1' : '0' }}</strong></span>
<span class="calc-reason">{{ haA && haB ? '全为 1' : '不全为 1' }}</span> <span class="calc-reason">{{ haA && haB ? '全为 1' : '不全为 1' }}</span>
</div> </div>
@@ -384,15 +384,15 @@
</div> </div>
<div class="calc-row"> <div class="calc-row">
<span class="calc-label">中间</span> <span class="calc-label">中间</span>
<span class="calc-formula">xor1 = A B = <strong>{{ faXor1 ? '1' : '0' }}</strong></span> <span class="calc-formula">中间值 = A XOR B = <strong>{{ faXor1 ? '1' : '0' }}</strong></span>
</div> </div>
<div class="calc-row"> <div class="calc-row">
<span class="calc-label">本位</span> <span class="calc-label">本位</span>
<span class="calc-formula">Sum = xor1 Cin = <strong>{{ faSum ? '1' : '0' }}</strong></span> <span class="calc-formula">本位 = 中间值 XOR Cin = <strong>{{ faSum ? '1' : '0' }}</strong></span>
</div> </div>
<div class="calc-row"> <div class="calc-row">
<span class="calc-label">进位</span> <span class="calc-label">进位</span>
<span class="calc-formula">Cout = (AB) (xor1Cin) = <span class="calc-formula">进位 = (A AND B) OR (中间值 AND Cin) =
<strong>{{ faCarryOut ? '1' : '0' }}</strong></span> <strong>{{ faCarryOut ? '1' : '0' }}</strong></span>
</div> </div>
</div> </div>
@@ -544,21 +544,21 @@ const gates = [
name: 'AND', name: 'AND',
cn: '与门', cn: '与门',
symbol: '&', symbol: '&',
formula: 'A B', formula: 'A AND B',
truth: [0, 0, 0, 1] truth: [0, 0, 0, 1]
}, },
{ {
name: 'OR', name: 'OR',
cn: '或门', cn: '或门',
symbol: '≥1', symbol: '≥1',
formula: 'A B', formula: 'A OR B',
truth: [0, 1, 1, 1] truth: [0, 1, 1, 1]
}, },
{ {
name: 'XOR', name: 'XOR',
cn: '异或门', cn: '异或门',
symbol: '=1', symbol: '=1',
formula: 'A B', formula: 'A XOR B',
truth: [0, 1, 1, 0] truth: [0, 1, 1, 0]
}, },
{ name: 'NOT', cn: '非门', symbol: '1', formula: '¬A', truth: [1, 0] } { name: 'NOT', cn: '非门', symbol: '1', formula: '¬A', truth: [1, 0] }
@@ -6,7 +6,7 @@
</div> </div>
<div class="lifecycle-flow"> <div class="lifecycle-flow">
<div class="flow-stage" v-for="(stage, index) in stages" :key="stage.id"> <div v-for="(stage, index) in stages" :key="stage.id" class="flow-stage">
<div class="stage-header" @click="activeStage = index"> <div class="stage-header" @click="activeStage = index">
<span class="stage-number">{{ index + 1 }}</span> <span class="stage-number">{{ index + 1 }}</span>
<span class="stage-name">{{ stage.name }}</span> <span class="stage-name">{{ stage.name }}</span>
@@ -86,9 +86,7 @@
<div class="arp-arrow"> 广播到局域网</div> <div class="arp-arrow"> 广播到局域网</div>
<div class="arp-answer"> <div class="arp-answer">
<span class="answer-icon"></span> <span class="answer-icon"></span>
<span class="answer-text" <span class="answer-text">我是我的 MAC 地址是 00:11:22:33:44:66</span>
>我是我的 MAC 地址是 00:11:22:33:44:66</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -40,9 +40,7 @@
<div class="linked-container"> <div class="linked-container">
<div v-for="(item, i) in linkedData" :key="i" class="linked-node"> <div v-for="(item, i) in linkedData" :key="i" class="linked-node">
<span class="node-value">{{ item.value }}</span> <span class="node-value">{{ item.value }}</span>
<span v-if="i < linkedData.length - 1" class="node-arrow" <span v-if="i < linkedData.length - 1" class="node-arrow"></span>
></span
>
</div> </div>
</div> </div>
<div class="operation-hint"> <div class="operation-hint">
@@ -88,8 +86,7 @@
v-for="(item, j) in bucket" v-for="(item, j) in bucket"
:key="j" :key="j"
class="bucket-item" class="bucket-item"
>{{ item }}</span >{{ item }}</span>
>
</div> </div>
</div> </div>
</div> </div>
@@ -178,8 +175,7 @@
</div> </div>
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong <strong>核心思想</strong>数据结构是数据的"容器"不同的容器有不同的特点选择合适的数据结构能让程序效率提升几个数量级
>数据结构是数据的"容器"不同的容器有不同的特点选择合适的数据结构能让程序效率提升几个数量级
</div> </div>
</div> </div>
</template> </template>
@@ -24,9 +24,7 @@
<!-- 推荐结果 --> <!-- 推荐结果 -->
<div v-if="activeScenario" class="recommendation"> <div v-if="activeScenario" class="recommendation">
<div class="rec-header"> <div class="rec-header">
<span class="rec-title" <span class="rec-title">推荐使用{{ currentScenario.recommendation }}</span>
>推荐使用{{ currentScenario.recommendation }}</span
>
</div> </div>
<div class="rec-reason"> <div class="rec-reason">
@@ -50,11 +50,9 @@
class="char-item" class="char-item"
> >
<span class="char-display">{{ char }}</span> <span class="char-display">{{ char }}</span>
<span class="char-unicode" <span class="char-unicode">U+{{
>U+{{
char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0') char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')
}}</span }}</span>
>
<span class="char-binary">{{ <span class="char-binary">{{
char.charCodeAt(0).toString(2).padStart(8, '0') char.charCodeAt(0).toString(2).padStart(8, '0')
}}</span> }}</span>
@@ -90,10 +90,10 @@
<div class="packet-header">数据包</div> <div class="packet-header">数据包</div>
<div class="packet-body"> <div class="packet-body">
<div <div
class="packet-layer"
v-for="(layer, index) in currentScenario.transmission v-for="(layer, index) in currentScenario.transmission
.layers" .layers"
:key="index" :key="index"
class="packet-layer"
> >
<span class="layer-name">{{ layer.name }}:</span> <span class="layer-name">{{ layer.name }}:</span>
<span class="layer-value">{{ layer.value }}</span> <span class="layer-value">{{ layer.value }}</span>
@@ -1,333 +1,392 @@
<template> <template>
<div class="filesystem-demo"> <div class="demo">
<div class="demo-wrapper"> <div class="title">📁 你看到的文件 vs 硬盘上的碎片</div>
<!-- 文件树逻辑视角 -->
<div class="logical-view"> <div class="scene">
<div class="view-title"> <!-- 文件视图 -->
<span>📁 你的视角 (文件系统)</span> <div class="file-view">
<span class="subtitle">漂亮整洁的目录树</span> <div class="view-label">📂 你看到的文件夹</div>
</div> <div class="folder-tree">
<div class="folder">
<div class="file-tree"> <span class="folder-icon">📁</span>
<div class="tree-node folder expanded"> <span>照片</span>
<span class="icon">💾</span> D盘 (根目录)
</div> </div>
<div class="tree-children"> <div class="files">
<div class="tree-node folder expanded"> <div
<span class="icon">📂</span> 照片 class="file-item"
:class="{ active: currentFile === 'pet' }"
>
<span class="file-icon">🖼</span>
<span>宠物.jpg</span>
<span class="file-size">2.5MB</span>
</div> </div>
<div class="tree-children"> <div
<div class="file-item"
class="tree-node file" :class="{ active: currentFile === 'trip' }"
:class="{ active: activeFile === 'pet' }" >
@click="selectFile('pet')" <span class="file-icon">🖼</span>
> <span>旅游.png</span>
<span class="icon">🖼</span> 宠物.jpg <span class="file-size">1.8MB</span>
<span class="size-badge">3 </span>
</div>
<div
class="tree-node file"
:class="{ active: activeFile === 'vacation' }"
@click="selectFile('vacation')"
>
<span class="icon">🖼</span> 旅游.png
<span class="size-badge">2 </span>
</div>
</div>
<div class="tree-node folder expanded">
<span class="icon">📂</span> 工作
</div>
<div class="tree-children">
<div
class="tree-node file"
:class="{ active: activeFile === 'doc' }"
@click="selectFile('doc')"
>
<span class="icon">📄</span> 总结.docx
<span class="size-badge">4 </span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 翻译官动画 --> <!-- 读取动画 -->
<div class="translator"> <div class="read-animation" v-if="isReading">
<div class="arrow"></div> <div class="read-text">正在读取...</div>
<div class="badge">文件系统账本<br />(inode表)</div> <div class="read-blocks">
<div class="arrow"></div> <div
</div> v-for="(block, idx) in readingBlocks"
:key="idx"
<!-- 磁盘块物理视角 --> class="read-block"
<div class="physical-view"> :class="{ read: idx <= readProgress }"
<div class="view-title"> :style="{ animationDelay: idx * 0.1 + 's' }"
<span>🖨 硬盘的视角 (物理存储)</span>
<span class="subtitle">无序零散的数据块</span>
</div>
<div class="disk-grid">
<div
v-for="block in 24"
:key="block"
class="disk-block"
:class="[getBlockOwner(block), { active: isBlockActive(block) }]"
> >
{{ block }} {{ block }}
</div> </div>
</div> </div>
</div> </div>
<!-- 硬盘视图 -->
<div class="disk-view">
<div class="view-label">💾 硬盘实际存储数据块</div>
<div class="disk-grid">
<div
v-for="n in 12"
:key="n"
class="disk-block"
:class="[
getBlockType(n),
{
active: isReading && currentBlocks.includes(n),
reading: isReading && currentBlocks.indexOf(n) === readProgress
}
]"
>
<span class="block-num">{{ n }}</span>
<span class="block-content">{{ getBlockContent(n) }}</span>
</div>
</div>
</div>
</div> </div>
<div class="explanation-box" v-if="activeFile"> <div class="explain">
<span v-if="activeFile === 'pet'"> <strong>💡 原理</strong>文件系统把文件切成碎片存在硬盘各处如宠物.jpg存在第3711然后用"账本"记录位置你看到的整齐文件夹只是账本上的记录
💡 宠物.jpg 其实被切碎分别放在了第 3814
文件系统帮你做好了翻译你只需双击它
</span>
<span v-if="activeFile === 'vacation'">
💡 旅游.png 放在了第 56
</span>
<span v-if="activeFile === 'doc'">
💡 总结.docx 被分散存放在 10111822
如果没有文件系统你得自己背下这些数字才能打开文件
</span>
</div>
<div class="explanation-box default" v-else>
试着点击左侧的文件看看它们在硬盘里到底长什么样
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
const activeFile = ref(null) const currentFile = ref('')
const isReading = ref(false)
const readProgress = ref(-1)
const currentBlocks = ref([])
// //
const fileMap = { const fileLocations = {
pet: [3, 8, 14], pet: [3, 7, 11], // .jpg 3711
vacation: [5, 6], trip: [5, 6] // .png 56
doc: [10, 11, 18, 22]
} }
const selectFile = (file) => { //
activeFile.value = file const blockContents = {
3: '宠-1',
7: '宠-2',
11: '宠-3',
5: '旅-1',
6: '旅-2'
} }
const getBlockOwner = (block) => { let timer = null
for (const [key, blocks] of Object.entries(fileMap)) { let phase = 0
if (blocks.includes(block)) return `owner-${key}`
} const getBlockType = (n) => {
if (fileLocations.pet.includes(n)) return 'pet'
if (fileLocations.trip.includes(n)) return 'trip'
return 'empty' return 'empty'
} }
const isBlockActive = (block) => { const getBlockContent = (n) => {
if (!activeFile.value) return false return blockContents[n] || ''
return fileMap[activeFile.value].includes(block)
} }
const readingBlocks = computed(() => {
return currentBlocks.value.map(b => blockContents[b] || '')
})
const runDemo = () => {
switch(phase) {
case 0: // .jpg
currentFile.value = 'pet'
currentBlocks.value = fileLocations.pet
isReading.value = true
readProgress.value = -1
phase = 1
break
case 1: //
if (readProgress.value < currentBlocks.value.length - 1) {
readProgress.value++
} else {
phase = 2
}
break
case 2: //
isReading.value = false
phase = 3
break
case 3: // .png
currentFile.value = 'trip'
currentBlocks.value = fileLocations.trip
isReading.value = true
readProgress.value = -1
phase = 4
break
case 4: //
if (readProgress.value < currentBlocks.value.length - 1) {
readProgress.value++
} else {
phase = 5
}
break
case 5: //
isReading.value = false
currentFile.value = ''
currentBlocks.value = []
phase = 0
break
}
}
onMounted(() => {
timer = setInterval(runDemo, 800)
})
onUnmounted(() => {
clearInterval(timer)
})
</script> </script>
<style scoped> <style scoped>
.filesystem-demo { .demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.demo-wrapper {
display: flex;
align-items: stretch;
gap: 1rem;
margin-bottom: 1.5rem;
}
@media (max-width: 768px) {
.demo-wrapper {
flex-direction: column;
}
.translator {
transform: rotate(90deg);
margin: 1rem 0;
}
}
.logical-view,
.physical-view {
flex: 1;
background: var(--vp-c-bg-alt);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.view-title {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px dashed var(--vp-c-divider);
}
.view-title span {
font-weight: bold;
font-size: 0.95rem;
}
.view-title .subtitle {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-weight: normal;
margin-top: 0.2rem;
}
/* File Tree Styles */
.file-tree {
font-size: 0.9rem;
}
.tree-node {
padding: 0.4rem 0.5rem;
border-radius: 6px;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.tree-node:hover {
background: var(--vp-c-bg-mute);
}
.tree-node.file.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-weight: bold;
}
.tree-children {
padding-left: 1.5rem;
border-left: 1px dashed var(--vp-c-divider);
margin-left: 0.6rem;
}
.size-badge {
margin-left: auto;
font-size: 0.7rem;
background: var(--vp-c-bg-mute);
padding: 0.1rem 0.4rem;
border-radius: 4px;
color: var(--vp-c-text-2);
}
.tree-node.active .size-badge {
background: var(--vp-c-brand-1);
color: white;
}
/* Translator */
.translator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.translator .badge {
background: var(--vp-c-brand-1);
color: white;
padding: 0.5rem 1rem;
border-radius: 8px; border-radius: 8px;
font-size: 0.8rem; background: var(--vp-c-bg-soft);
font-weight: bold; padding: 16px;
text-align: center; margin: 1rem 0;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
}
.arrow {
width: 2px;
height: 20px;
background: var(--vp-c-divider);
position: relative;
}
.arrow::after {
content: '';
position: absolute;
bottom: -4px;
left: -4px;
border-width: 5px;
border-style: solid;
border-color: var(--vp-c-divider) transparent transparent transparent;
} }
/* Disk Grid */ .title {
.disk-grid { font-weight: 600;
display: grid; font-size: 14px;
grid-template-columns: repeat(4, 1fr); margin-bottom: 12px;
gap: 0.4rem; text-align: center;
} }
.disk-block {
aspect-ratio: 1; .scene {
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: center; gap: 12px;
font-size: 0.75rem; margin-bottom: 12px;
color: var(--vp-c-text-3); }
.file-view, .disk-view {
background: var(--vp-c-bg); background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 4px; border-radius: 6px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding: 10px;
} }
.disk-block.owner-pet { .view-label {
background: rgba(16, 185, 129, 0.1); font-size: 11px;
border-color: rgba(16, 185, 129, 0.3); font-weight: 600;
color: var(--vp-c-text-3);
margin-bottom: 8px;
} }
.disk-block.owner-vacation {
background: rgba(59, 130, 246, 0.1); .folder-tree {
border-color: rgba(59, 130, 246, 0.3); padding-left: 8px;
} }
.disk-block.owner-doc {
background: rgba(245, 158, 11, 0.1); .folder {
border-color: rgba(245, 158, 11, 0.3); display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
margin-bottom: 4px;
}
.folder-icon {
font-size: 16px;
}
.files {
padding-left: 20px;
border-left: 1px dashed var(--vp-c-divider);
margin-left: 8px;
}
.file-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border-radius: 4px;
font-size: 12px;
transition: all 0.3s;
}
.file-item.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
font-weight: 600;
}
.file-icon {
font-size: 14px;
}
.file-size {
margin-left: auto;
font-size: 10px;
color: var(--vp-c-text-3);
}
.file-item.active .file-size {
color: var(--vp-c-brand);
}
.read-animation {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-brand);
border-radius: 6px;
padding: 10px;
text-align: center;
}
.read-text {
font-size: 11px;
color: var(--vp-c-brand);
margin-bottom: 8px;
font-weight: 600;
}
.read-blocks {
display: flex;
justify-content: center;
gap: 4px;
}
.read-block {
width: 32px;
height: 24px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
color: var(--vp-c-text-3);
transition: all 0.2s;
}
.read-block.read {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
animation: pulse 0.3s ease;
}
.disk-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 4px;
}
.disk-block {
aspect-ratio: 1;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 9px;
transition: all 0.3s;
position: relative;
}
.disk-block.empty {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-3);
}
.disk-block.pet {
background: #16a34a22;
border-color: #16a34a55;
color: #16a34a;
}
.disk-block.trip {
background: #3b82f622;
border-color: #3b82f655;
color: #3b82f6;
} }
.disk-block.active { .disk-block.active {
box-shadow: 0 0 8px currentColor;
}
.disk-block.reading {
transform: scale(1.1); transform: scale(1.1);
font-weight: 600;
animation: glow 0.5s ease infinite alternate;
}
.disk-block.pet.reading {
background: #16a34a;
color: white; color: white;
font-weight: bold;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 2;
}
.disk-block.owner-pet.active {
background: var(--vp-c-success-1);
border-color: var(--vp-c-success-1);
}
.disk-block.owner-vacation.active {
background: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
}
.disk-block.owner-doc.active {
background: var(--vp-c-warning-1);
border-color: var(--vp-c-warning-1);
} }
.explanation-box { .disk-block.trip.reading {
padding: 1rem; background: #3b82f6;
background: rgba(16, 185, 129, 0.1); color: white;
border-left: 4px solid var(--vp-c-success-1);
border-radius: 0 8px 8px 0;
font-size: 0.95rem;
animation: fadeIn 0.3s;
} }
.explanation-box.default {
background: var(--vp-c-bg-alt); .block-num {
border-left-color: var(--vp-c-text-3); font-size: 8px;
opacity: 0.6;
position: absolute;
top: 2px;
left: 3px;
}
.block-content {
font-weight: 600;
}
.explain {
font-size: 12px;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
line-height: 1.5;
padding: 10px;
background: var(--vp-c-bg);
border-radius: 6px;
} }
@keyframes fadeIn { .explain strong { color: var(--vp-c-text-1); }
from {
opacity: 0; @keyframes pulse {
transform: translateX(-10px); 0% { transform: scale(1); }
} 50% { transform: scale(1.1); }
to { 100% { transform: scale(1); }
opacity: 1; }
transform: translateX(0);
} @keyframes glow {
from { box-shadow: 0 0 5px currentColor; }
to { box-shadow: 0 0 15px currentColor; }
} }
</style> </style>
@@ -0,0 +1,479 @@
<template>
<div class="flip-flop-wrapper">
<div class="header">
<div class="title">从触发器到寄存器记忆的闭环机制</div>
<div class="desc">试着改变数据并观察没有时钟信号的允许输出重新反馈回输入端的"闭环"长久保护了记忆</div>
</div>
<div class="interactive-panel">
<!-- Left side: Controllable Data inputs -->
<div class="data-input-sec">
<div class="sec-label">数据总线 (Data Input)</div>
<div class="bus-lines">
<div
v-for="(bit, idx) in inputBits" :key="'in'+idx"
class="input-node"
:class="{ active: bit === 1 }"
@click="toggleInput(idx)"
>
{{ bit }}
<span v-if="bit === 1" class="pulse-ring"></span>
</div>
</div>
</div>
<!-- Arrow indicating flow, blocked by a 'gate' if no clock -->
<div class="gate-sec">
<div class="sec-label transparent">大门</div>
<div class="gate-door-container">
<div class="flow-paths">
<div v-for="(bit, idx) in inputBits" :key="'path'+idx" class="flow-line" :class="{ active: bit === 1, open: isClockPulsing }">
<span v-if="bit === 1 && isClockPulsing" class="data-particle"></span>
</div>
</div>
<div class="gate-door" :class="{ open: isClockPulsing }">
<span v-if="!isClockPulsing" class="lock-icon">🔒</span>
<span v-else class="lock-icon">🔓</span>
</div>
</div>
</div>
<!-- Right side: The flip-flops (registers) -->
<div class="register-sec" :class="{ writing: isClockPulsing }">
<div class="sec-label">4位寄存器 (存储状态)</div>
<div class="stored-bits">
<div
v-for="(bit, idx) in storedBits" :key="'s'+idx"
class="store-node-wrapper"
>
<div class="store-node" :class="{ active: bit === 1 }">
{{ bit }}
</div>
<!-- Individual loop for each bit to vividly show Feedback -->
<svg class="node-loop" viewBox="0 0 50 50" aria-hidden="true">
<path d="M 40 25 C 50 25 50 45 25 45 C 0 45 0 25 10 25" fill="none" class="loop-stroke" :class="{ active: bit === 1 }" stroke-width="2.5" />
<polygon points="6,20 6,30 14,25" class="loop-arrow" :class="{ active: bit === 1 }" />
</svg>
</div>
</div>
</div>
</div>
<!-- Clock button at bottom -->
<div class="clock-sec">
<div class="sec-label">控制中心</div>
<button class="clock-btn" :class="{ active: isClockPulsing }" @click="triggerClock">
<span class="icon"></span> 发送时钟脉冲 (Clock)
</button>
<div class="status-msg">
<strong :class="{ 'warning-text': pendingChanges, 'success-text': !pendingChanges && !isClockPulsing, 'action-text': isClockPulsing }">
{{ statusMessage }}
</strong>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inputBits = ref([1, 0, 1, 0])
const storedBits = ref([0, 0, 0, 0])
const isClockPulsing = ref(false)
const manualStatus = ref('')
const pendingChanges = computed(() => {
return inputBits.value.join('') !== storedBits.value.join('')
})
const statusMessage = computed(() => {
if (isClockPulsing.value) {
return '脉冲到达!突破闭环防线,正并行读入新数据...'
}
if (manualStatus.value) return manualStatus.value;
return '尝试改变左侧输入,闭环保护期间寄存器值无法更改。'
})
const toggleInput = (idx) => {
inputBits.value[idx] = inputBits.value[idx] === 1 ? 0 : 1
if (pendingChanges.value) {
manualStatus.value = '准备写入新数据,请点击"发送时钟脉冲"打破锁死。'
} else {
manualStatus.value = '输入总线与当前存储状态相同。'
}
}
const triggerClock = () => {
if (isClockPulsing.value) return
isClockPulsing.value = true
manualStatus.value = ''
// lock in the data exactly halfway through animation
setTimeout(() => {
storedBits.value = [...inputBits.value]
}, 150)
setTimeout(() => {
isClockPulsing.value = false
if (pendingChanges.value) {
manualStatus.value = '闭环重新生效,但还有未写入的新数据?'
} else {
manualStatus.value = '脉冲消退。反馈闭环恢复,当前状态被长久稳固保存。'
}
}, 600)
}
</script>
<style scoped>
.flip-flop-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 1.5rem;
overflow: hidden;
}
.header .title {
font-size: 1.15rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.header .desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.4rem;
line-height: 1.4;
}
.sec-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-3);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.8rem;
text-align: center;
}
.transparent {
opacity: 0;
user-select: none;
}
.interactive-panel {
display: flex;
align-items: stretch;
justify-content: space-evenly;
gap: 1.5rem;
background: var(--vp-c-bg-alt);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider-light);
}
.data-input-sec, .register-sec {
display: flex;
flex-direction: column;
align-items: center;
}
.bus-lines, .stored-bits {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.input-node, .store-node {
width: 2.8rem;
height: 2.8rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 1.2rem;
font-weight: bold;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
position: relative;
z-index: 2;
}
/* Inputs */
.input-node {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.input-node:hover {
border-color: var(--vp-c-brand-1);
transform: translateX(2px);
}
.input-node.active {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.pulse-ring {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
border-radius: 8px;
box-shadow: 0 0 10px var(--vp-c-brand-1);
animation: static-pulse 2s infinite;
z-index: -1;
opacity: 0.5;
}
@keyframes static-pulse {
0% { transform: scale(1); opacity: 0.6; }
50% { transform: scale(1.1); opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
/* Stored */
.store-node-wrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.store-node {
background: var(--vp-c-bg);
border: 2px dashed var(--vp-c-divider);
color: var(--vp-c-text-2);
box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
}
.store-node.active {
border-style: solid;
background: rgba(16, 185, 129, 0.08);
border-color: #10b981;
color: #10b981;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.2);
}
/* Loop Animation */
.node-loop {
position: absolute;
bottom: -40px;
width: 45px;
height: 45px;
z-index: 1;
}
.loop-stroke {
stroke: var(--vp-c-divider);
stroke-dasharray: 4;
animation: loop-march 2s linear infinite;
transition: all 0.3s;
}
.loop-stroke.active {
stroke: #10b981;
}
.loop-arrow {
fill: var(--vp-c-divider);
transition: all 0.3s;
}
.loop-arrow.active {
fill: #10b981;
}
@keyframes loop-march {
from { stroke-dashoffset: 8; }
to { stroke-dashoffset: 0; }
}
.register-sec.writing .store-node {
transform: scale(1.1);
border-color: #eab308;
box-shadow: 0 0 20px rgba(234, 179, 8, 0.4);
}
/* Gate Section */
.gate-sec {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
min-width: 60px;
}
.gate-door-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
width: 100%;
height: 100%;
position: relative;
}
.flow-paths {
display: flex;
flex-direction: column;
gap: 1.2rem;
width: 100%;
justify-content: flex-start;
padding-top: 1.4rem;
height: 100%;
}
.flow-line {
height: 0;
width: 100%;
border-bottom: 2px solid var(--vp-c-divider);
opacity: 0.2;
position: relative;
transition: all 0.3s;
}
.flow-line.active {
border-bottom-color: var(--vp-c-brand-1);
opacity: 0.5;
}
.flow-line.open.active {
opacity: 1;
}
.data-particle {
position: absolute;
top: -4px;
left: 0;
width: 8px;
height: 8px;
background: var(--vp-c-brand-1);
border-radius: 50%;
box-shadow: 0 0 8px var(--vp-c-brand-1);
animation: slide-across 0.3s linear forwards;
}
@keyframes slide-across {
0% { left: 0; }
100% { left: 100%; }
}
.gate-door {
position: absolute;
top: 10px;
bottom: 10px;
width: 8px;
background: var(--vp-c-danger-1);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px rgba(220, 38, 38, 0.3);
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.gate-door.open {
transform: translateY(-80px);
opacity: 0;
background: #eab308;
}
.lock-icon {
position: absolute;
left: -9px;
font-size: 1.2rem;
background: var(--vp-c-bg-alt);
border-radius: 50%;
padding: 2px;
}
/* Clock Section */
.clock-sec {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(234, 179, 8, 0.05);
padding: 1.2rem;
border-radius: 10px;
border: 1px solid rgba(234, 179, 8, 0.2);
}
.clock-btn {
background: var(--vp-c-bg);
border: 2px solid #eab308;
color: #c2410c;
padding: 0.6rem 2rem;
border-radius: 8px;
font-weight: 700;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 0.6rem;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 10px rgba(234, 179, 8, 0.15);
}
.dark .clock-btn {
color: #fde047;
}
.clock-btn:hover {
background: #eab308;
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(234, 179, 8, 0.3);
}
.clock-btn.active {
background: #eab308;
color: white;
transform: scale(0.95);
}
.icon {
font-size: 1.2rem;
}
.status-msg {
margin-top: 1rem;
font-size: 0.85rem;
text-align: center;
}
.warning-text { color: var(--vp-c-warning-1); }
.success-text { color: var(--vp-c-success-1); transition: color 0.3s; }
.action-text { color: #eab308; }
@media (max-width: 600px) {
.interactive-panel {
flex-direction: column;
align-items: center;
}
.gate-sec {
height: 40px;
width: 60%;
}
.gate-door {
top: 50%; bottom: auto;
width: 100%; height: 8px;
left: 0; right: 0;
transform: translateY(-50%);
}
.gate-door.open {
transform: translateY(-50%) translateX(-100px);
}
.flow-paths {
flex-direction: row; height: 100%; width: 100%;
align-items: center; justify-content: space-evenly;
padding-top: 0;
}
.flow-line {
width: 0; height: 100%;
border-bottom: none; border-right: 2px solid var(--vp-c-divider);
}
.flow-line.active { border-right-color: var(--vp-c-brand-1); }
.data-particle {
top: 0; left: -4px;
animation: slide-down 0.3s linear forwards;
}
@keyframes slide-down {
0% { top: 0; left: -4px; }
100% { top: 100%; left: -4px; }
}
.bus-lines, .stored-bits {
flex-direction: row; justify-content: center; flex-wrap: wrap; gap: 0.8rem;
}
.node-loop { display: none; /* Hide loops on mobile to avoid layout breaking */ }
}
</style>
@@ -1,212 +1,131 @@
<template> <template>
<div class="full-adder-demo"> <div class="full-adder-demo">
<div class="demo-header"> <div class="demo-header">
<span class="title">全加器 (Full Adder)</span> <span class="title">全加器 (Full Adder) 交互演示</span>
<span class="subtitle">能处理进位输入的完整加法单元 三个输入两个输出</span> <span class="subtitle">比半加器多一个输入来自低位的进位 (Cin)点击三个输入试试</span>
</div> </div>
<div class="terms-box"> <!-- 主交互区 -->
<div class="term-item"> <div class="main-area">
<span class="term-name">Cin (进位输入)</span> <!-- 大号加法展示 -->
<span class="term-desc">来自低位的进位信号</span> <div class="left-panel">
</div> <div class="big-calc">
<div class="term-item"> <button class="big-bit" :class="{ on: inputA }" @click="inputA = !inputA">{{ inputA ? '1' : '0' }}</button>
<span class="term-name">Sum (本位)</span> <span class="op">+</span>
<span class="term-desc">三位异或的结果</span> <button class="big-bit" :class="{ on: inputB }" @click="inputB = !inputB">{{ inputB ? '1' : '0' }}</button>
</div> <span class="op">+</span>
<div class="term-item"> <button class="big-bit cin" :class="{ on: carryIn }" @click="carryIn = !carryIn">{{ carryIn ? '1' : '0' }}</button>
<span class="term-name">Cout (进位输出)</span> <span class="op">=</span>
<span class="term-desc">向高位的进位信号</span> <span class="result-display">
</div> <span class="result-bit carry-bit" :class="{ lit: carryOut }">{{ carryOut ? '1' : '0' }}</span>
</div> <span class="result-bit sum-bit" :class="{ lit: sumOut }">{{ sumOut ? '1' : '0' }}</span>
</span>
</div>
<div class="circuit-container"> <div class="input-labels">
<div class="inputs"> <span class="il">A</span>
<div class="input-line"> <span class="il spacer"></span>
<button <span class="il">B</span>
class="toggle-btn" <span class="il spacer"></span>
:class="{ on: inputA }" <span class="il cin-label">低位进位</span>
@click="inputA = !inputA" <span class="il spacer"></span>
<span class="result-labels">
<span class="rl" :class="{ lit: carryOut }">进位</span>
<span class="rl" :class="{ lit: sumOut }">本位</span>
</span>
</div>
<div class="explain-box">
<div class="explain-text">{{ explainText }}</div>
</div>
<div class="vs-half">
<strong>和半加器相比</strong>全加器多了第三个输入低位进位 (Cin)在多位加法中每一列不仅要加 A B还要加上右边那一列传来的进位
</div>
</div>
<!-- 真值表 -->
<div class="right-panel">
<div class="table-title">所有 8 种情况3个输入 2³ = 8</div>
<div class="truth-table">
<div class="tr header">
<span>A</span><span>B</span><span>Cin</span><span class="sum-col">本位</span><span class="carry-col">进位</span>
</div>
<div
v-for="row in cases"
:key="row.key"
class="tr"
:class="{ active: row.a === +inputA && row.b === +inputB && row.cin === +carryIn }"
> >
{{ inputA ? '1' : '0' }} <span>{{ row.a }}</span>
</button> <span>{{ row.b }}</span>
<span class="label">输入 A</span> <span>{{ row.cin }}</span>
</div> <span class="sum-col">{{ row.sum }}</span>
<div class="input-line"> <span class="carry-col">{{ row.carry }}</span>
<button
class="toggle-btn"
:class="{ on: inputB }"
@click="inputB = !inputB"
>
{{ inputB ? '1' : '0' }}
</button>
<span class="label">输入 B</span>
</div>
<div class="input-line">
<button
class="toggle-btn cin-btn"
:class="{ on: carryIn }"
@click="carryIn = !carryIn"
>
{{ carryIn ? '1' : '0' }}
</button>
<span class="label">Cin</span>
</div>
</div>
<div class="wires">
<svg class="wire-svg" viewBox="0 0 120 180" preserveAspectRatio="none">
<path
d="M 0,30 C 30,30 30,45 60,45"
fill="none"
:stroke="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<path
d="M 0,30 L 15,30 L 15,105 L 60,105"
fill="none"
:stroke="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<path
d="M 0,90 C 30,90 30,60 60,60"
fill="none"
:stroke="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<path
d="M 0,90 L 25,90 L 25,120 L 60,120"
fill="none"
:stroke="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<path
d="M 0,150 C 30,150 30,135 60,135"
fill="none"
:stroke="carryIn ? '#d97706' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<circle
cx="15"
cy="30"
r="3"
:fill="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
/>
<circle
cx="25"
cy="90"
r="3"
:fill="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
/>
</svg>
</div>
<div class="gates">
<div class="gate-box xor-gate" :class="{ active: xor1 }">
<div class="gate-header">
<span class="gate-name">XOR</span>
<span class="gate-cn">异或门</span>
</div> </div>
<div class="gate-formula">A B</div>
<div class="gate-desc">不同为 1 中间值</div>
</div>
<div class="gate-box and-gate" :class="{ active: carry1 }">
<div class="gate-header">
<span class="gate-name">AND</span>
<span class="gate-cn">与门</span>
</div>
<div class="gate-formula">A B</div>
<div class="gate-desc"> 1 1 进位1</div>
</div>
<div class="gate-box xor-gate" :class="{ active: sumOut }">
<div class="gate-header">
<span class="gate-name">XOR</span>
<span class="gate-cn">异或门</span>
</div>
<div class="gate-formula">xor1 Cin</div>
<div class="gate-desc">不同为 1 本位</div>
</div>
<div class="gate-box or-gate" :class="{ active: carryOut }">
<div class="gate-header">
<span class="gate-name">OR</span>
<span class="gate-cn">或门</span>
</div>
<div class="gate-formula">c1 c2</div>
<div class="gate-desc"> 1 1 进位输出</div>
</div>
</div>
<div class="wires outputs-wires">
<svg class="wire-svg" viewBox="0 0 50 180" preserveAspectRatio="none">
<line
x1="0"
y1="52"
x2="50"
y2="52"
:stroke="
sumOut ? 'var(--vp-c-green-1, #16a34a)' : 'var(--vp-c-text-3)'
"
stroke-width="2"
/>
<line
x1="0"
y1="135"
x2="50"
y2="135"
:stroke="carryOut ? '#d97706' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
</svg>
</div>
<div class="outputs">
<div class="output-line" :class="{ active: sumOut }">
<span class="label">本位 (Sum)</span>
<span class="out-val s-val">{{ sumOut ? '1' : '0' }}</span>
</div>
<div class="output-line" :class="{ active: carryOut }">
<span class="label">Cout (进位)</span>
<span class="out-val c-val">{{ carryOut ? '1' : '0' }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="calculation-box"> <!-- 内部结构用两个半加器来理解 -->
<div class="calc-title">计算过程</div> <div class="structure-section">
<div class="calc-content"> <div class="structure-label">全加器的内部 = 两个半加器串联</div>
<div class="calc-row"> <div class="structure-row">
<span class="calc-label">输入</span> <!-- 半加器 1 -->
<span class="calc-value">A = {{ inputA ? '1' : '0' }}B = {{ inputB ? '1' : '0' }}Cin = <div class="ha-block">
{{ carryIn ? '1' : '0' }}</span> <div class="ha-title">第一步半加器 </div>
<div class="ha-desc">先算 A + B</div>
<div class="ha-io">
<div class="ha-in">
<span class="io-tag a">A = {{ inputA ? '1' : '0' }}</span>
<span class="io-tag b">B = {{ inputB ? '1' : '0' }}</span>
</div>
<div class="ha-arrow"></div>
<div class="ha-out">
<span class="io-result" :class="{ lit: xor1 }">中间和: {{ xor1 ? '1' : '0' }}</span>
<span class="io-result carry" :class="{ lit: carry1 }">进位①: {{ carry1 ? '1' : '0' }}</span>
</div>
</div>
</div> </div>
<div class="calc-row">
<span class="calc-label">中间值</span> <div class="chain-arrow"></div>
<span class="calc-formula">xor1 = A B = {{ inputA ? '1' : '0' }}
{{ inputB ? '1' : '0' }} = <!-- 半加器 2 -->
<strong>{{ xor1 ? '1' : '0' }}</strong></span> <div class="ha-block">
<span class="calc-reason">{{ inputA !== inputB ? '不同' : '相同' }}</span> <div class="ha-title">第二步半加器 </div>
<div class="ha-desc">把中间和 + 低位进位</div>
<div class="ha-io">
<div class="ha-in">
<span class="io-tag mid">中间和 = {{ xor1 ? '1' : '0' }}</span>
<span class="io-tag cin">Cin = {{ carryIn ? '1' : '0' }}</span>
</div>
<div class="ha-arrow"></div>
<div class="ha-out">
<span class="io-result sum" :class="{ lit: sumOut }">本位: {{ sumOut ? '1' : '0' }}</span>
<span class="io-result carry" :class="{ lit: carry2 }">进位②: {{ carry2 ? '1' : '0' }}</span>
</div>
</div>
</div> </div>
<div class="calc-row">
<span class="calc-label">本位</span> <div class="chain-arrow"></div>
<span class="calc-formula">Sum = xor1 Cin = {{ xor1 ? '1' : '0' }}
{{ carryIn ? '1' : '0' }} = <!-- OR 合并 -->
<strong>{{ sumOut ? '1' : '0' }}</strong></span> <div class="or-block">
<span class="calc-reason">{{ xor1 !== carryIn ? '不同' : '相同' }}</span> <div class="ha-title">第三步合并进位</div>
</div> <div class="ha-desc">两路进位只要有一个是 1就向高位进 1</div>
<div class="calc-row"> <div class="ha-io">
<span class="calc-label">进位</span> <div class="ha-in">
<span class="calc-formula">Cout = (AB) (xor1Cin) = ({{ carry1 ? '1' : '0' }}) ({{ <span class="io-tag c1">进位① = {{ carry1 ? '1' : '0' }}</span>
carry2 ? '1' : '0' <span class="io-tag c2">进位② = {{ carry2 ? '1' : '0' }}</span>
}}) = <strong>{{ carryOut ? '1' : '0' }}</strong></span> </div>
<div class="ha-arrow"></div>
<div class="ha-out">
<span class="io-result cout" :class="{ lit: carryOut }">最终进位: {{ carryOut ? '1' : '0' }}</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="info-box">
<strong>核心思想</strong>
全加器 = 两个半加器 + 一个 OR 第一级半加器算
A+B第二级半加器把结果加上 CinOR 门合并两路进位信号
</div>
</div> </div>
</template> </template>
@@ -214,302 +133,168 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const inputA = ref(true) const inputA = ref(true)
const inputB = ref(true) const inputB = ref(false)
const carryIn = ref(false) const carryIn = ref(false)
// 1
const xor1 = computed(() => inputA.value !== inputB.value) const xor1 = computed(() => inputA.value !== inputB.value)
const carry1 = computed(() => inputA.value && inputB.value) const carry1 = computed(() => inputA.value && inputB.value)
const carry2 = computed(() => xor1.value && carryIn.value)
// 2
const sumOut = computed(() => xor1.value !== carryIn.value) const sumOut = computed(() => xor1.value !== carryIn.value)
const carry2 = computed(() => xor1.value && carryIn.value)
// OR
const carryOut = computed(() => carry1.value || carry2.value) const carryOut = computed(() => carry1.value || carry2.value)
const cases = [
{ a: 0, b: 0, cin: 0, sum: 0, carry: 0, key: '000' },
{ a: 0, b: 0, cin: 1, sum: 1, carry: 0, key: '001' },
{ a: 0, b: 1, cin: 0, sum: 1, carry: 0, key: '010' },
{ a: 0, b: 1, cin: 1, sum: 0, carry: 1, key: '011' },
{ a: 1, b: 0, cin: 0, sum: 1, carry: 0, key: '100' },
{ a: 1, b: 0, cin: 1, sum: 0, carry: 1, key: '101' },
{ a: 1, b: 1, cin: 0, sum: 0, carry: 1, key: '110' },
{ a: 1, b: 1, cin: 1, sum: 1, carry: 1, key: '111' },
]
const explainText = computed(() => {
const a = +inputA.value
const b = +inputB.value
const c = +carryIn.value
const total = a + b + c
if (total === 0) return '0 + 0 + 0 = 0。本位写 0,不进位。'
if (total === 1) return `${a} + ${b} + ${c} = 1。本位写 1,不进位。`
if (total === 2) return `${a} + ${b} + ${c} = 2。二进制里 2 就是 "10",所以本位写 0,向左进 1。`
return `${a} + ${b} + ${c} = 3。二进制里 3 就是 "11",所以本位写 1,向左进 1。`
})
</script> </script>
<style scoped> <style scoped>
.full-adder-demo { .full-adder-demo {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 10px;
background: var(--vp-c-bg-soft); background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem; padding: 1.2rem;
margin: 1rem 0; margin: 1rem 0;
} }
.demo-header { margin-bottom: 1rem; }
.title { display: block; font-size: 0.95rem; font-weight: bold; color: var(--vp-c-text-1); }
.subtitle { font-size: 0.75rem; color: var(--vp-c-text-3); }
.demo-header { /* main area */
display: flex; .main-area { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1.2rem; }
flex-direction: column; .left-panel { flex: 1; min-width: 220px; }
gap: 0.15rem; .right-panel { flex: 1; min-width: 220px; }
margin-bottom: 0.75rem;
/* big calc */
.big-calc { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.2rem; flex-wrap: wrap; }
.big-bit {
width: 2.8rem; height: 2.8rem; font-size: 1.3rem; font-weight: bold; font-family: monospace;
border-radius: 6px; background: var(--vp-c-bg); border: 2px solid var(--vp-c-divider);
cursor: pointer; transition: all 0.2s;
} }
.big-bit.on { background: #dbeafe; color: #1d4ed8; border-color: #3b82f6; }
.big-bit.cin.on { background: #fef3c7; color: #d97706; border-color: #d97706; }
.op { font-size: 1.2rem; color: var(--vp-c-text-3); font-weight: bold; }
.title { .result-display { display: flex; gap: 0.15rem; }
font-size: 0.9rem; .result-bit {
font-weight: bold; width: 2.8rem; height: 2.8rem; border-radius: 6px; border: 2px solid var(--vp-c-divider);
color: var(--vp-c-text-1); background: var(--vp-c-bg); font-size: 1.3rem; font-weight: bold; font-family: monospace;
display: flex; align-items: center; justify-content: center;
color: var(--vp-c-text-3); transition: all 0.2s;
} }
.carry-bit.lit { background: #fef3c7; color: #d97706; border-color: #d97706; }
.sum-bit.lit { background: #dcfce7; color: #16a34a; border-color: #16a34a; }
.subtitle { .input-labels { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
font-size: 0.75rem; .il { font-size: 0.65rem; color: var(--vp-c-text-3); text-align: center; width: 2.8rem; }
color: var(--vp-c-text-3); .il.spacer { width: 1rem; }
.cin-label { color: #d97706; font-weight: 600; }
.result-labels { display: flex; gap: 0.15rem; }
.rl { font-size: 0.6rem; color: var(--vp-c-text-3); text-align: center; width: 2.8rem; transition: all 0.2s; }
.rl.lit:first-child { color: #d97706; font-weight: bold; }
.rl.lit:last-child { color: #16a34a; font-weight: bold; }
.explain-box {
background: var(--vp-c-bg); border-radius: 6px; padding: 0.6rem 0.8rem;
border-left: 3px solid var(--vp-c-brand-1); margin-bottom: 0.6rem;
} }
.explain-text { font-size: 0.82rem; color: var(--vp-c-text-2); line-height: 1.5; }
.terms-box { .vs-half {
display: flex; font-size: 0.78rem; color: var(--vp-c-text-2); line-height: 1.4;
gap: 0.5rem; padding: 0.5rem 0.7rem; background: var(--vp-c-bg-alt); border-radius: 6px;
margin-bottom: 0.75rem;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
} }
.vs-half strong { color: var(--vp-c-text-1); }
.term-item { /* truth table */
flex: 1; .table-title { font-size: 0.75rem; font-weight: 600; color: var(--vp-c-text-2); margin-bottom: 0.4rem; }
display: flex; .truth-table { border-radius: 6px; overflow: hidden; border: 1px solid var(--vp-c-divider); }
flex-direction: column; .tr {
gap: 0.15rem; display: grid; grid-template-columns: 1fr 1fr 1fr 1.5fr 1.5fr;
padding: 0.3rem 0.5rem; border-bottom: 1px solid var(--vp-c-divider);
font-family: monospace; font-size: 0.78rem; transition: all 0.2s;
} }
.tr:last-child { border-bottom: none; }
.term-name { .tr.header {
font-size: 0.78rem; background: var(--vp-c-bg-alt); font-weight: bold; font-family: system-ui;
font-weight: 600; font-size: 0.7rem; color: var(--vp-c-text-2);
color: var(--vp-c-brand-1);
} }
.tr.active { background: var(--vp-c-brand-soft); font-weight: bold; }
.sum-col { color: #16a34a; }
.carry-col { color: #d97706; }
.term-desc { /* structure section */
font-size: 0.68rem; .structure-section { border-top: 1px solid var(--vp-c-divider); padding-top: 1rem; }
color: var(--vp-c-text-3); .structure-label { font-size: 0.8rem; font-weight: 600; color: var(--vp-c-text-1); margin-bottom: 0.6rem; }
line-height: 1.3;
.structure-row { display: flex; align-items: stretch; gap: 0.3rem; overflow-x: auto; }
.ha-block, .or-block {
flex: 1; min-width: 160px; padding: 0.6rem; border-radius: 8px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
} }
.or-block { border-color: #d97706; background: #fffbeb; }
.circuit-container { .ha-title { font-size: 0.72rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.15rem; }
display: flex; .ha-desc { font-size: 0.65rem; color: var(--vp-c-text-3); margin-bottom: 0.4rem; }
align-items: center;
justify-content: center; .ha-io { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }
gap: 0; .ha-in { display: flex; flex-direction: column; gap: 0.2rem; }
padding: 1rem; .ha-arrow { font-size: 0.8rem; color: var(--vp-c-text-3); padding: 0 0.15rem; }
overflow-x: auto; .ha-out { display: flex; flex-direction: column; gap: 0.2rem; }
.io-tag {
font-size: 0.65rem; font-family: monospace; padding: 0.15rem 0.4rem;
border-radius: 3px; background: var(--vp-c-bg-alt); color: var(--vp-c-text-2);
} }
.io-tag.a { border-left: 2px solid #3b82f6; }
.io-tag.b { border-left: 2px solid #8b5cf6; }
.io-tag.cin { border-left: 2px solid #d97706; }
.io-tag.mid { border-left: 2px solid #16a34a; }
.io-tag.c1 { border-left: 2px solid #d97706; }
.io-tag.c2 { border-left: 2px solid #d97706; }
.inputs, .io-result {
.outputs { font-size: 0.68rem; font-family: monospace; padding: 0.15rem 0.4rem;
display: flex; border-radius: 3px; background: var(--vp-c-bg-alt); color: var(--vp-c-text-3);
flex-direction: column;
gap: 2rem;
min-width: 6rem;
z-index: 2;
}
.outputs {
min-width: 8rem;
}
.input-line,
.output-line {
display: flex;
align-items: center;
gap: 0.5rem;
}
.label {
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.toggle-btn {
width: 2.2rem;
height: 2.2rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-weight: bold;
font-family: monospace;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.io-result.lit { background: #dcfce7; color: #16a34a; font-weight: bold; }
.io-result.carry.lit { background: #fef3c7; color: #d97706; }
.io-result.sum.lit { background: #dcfce7; color: #16a34a; }
.io-result.cout.lit { background: #fef3c7; color: #d97706; }
.toggle-btn.on { .chain-arrow {
background: var(--vp-c-brand-soft); display: flex; align-items: center; font-size: 1.2rem; color: var(--vp-c-text-3);
color: var(--vp-c-brand-1); flex-shrink: 0; padding: 0 0.1rem;
border-color: var(--vp-c-brand-1);
} }
.toggle-btn.cin-btn.on { @media (max-width: 640px) {
background: #fef3c7; .main-area { flex-direction: column; }
color: #d97706; .structure-row { flex-direction: column; }
border-color: #d97706; .chain-arrow { transform: rotate(90deg); justify-content: center; padding: 0.3rem 0; }
}
.out-val {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.2rem;
height: 2.2rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-weight: bold;
font-family: monospace;
font-size: 1rem;
transition: all 0.2s;
}
.output-line.active .s-val {
background: #dcfce7;
color: #16a34a;
border-color: #16a34a;
}
.output-line.active .c-val {
background: #fef3c7;
color: #d97706;
border-color: #d97706;
}
.wires {
width: 100px;
height: 180px;
position: relative;
}
.outputs-wires {
width: 40px;
}
.wire-svg {
width: 100%;
height: 100%;
}
.gates {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
z-index: 2;
}
.gate-box {
width: 6rem;
height: 3.5rem;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.gate-box.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 8px var(--vp-c-brand-soft);
}
.gate-header {
display: flex;
align-items: baseline;
gap: 0.25rem;
}
.gate-name {
font-weight: bold;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.gate-cn {
font-size: 0.65rem;
color: var(--vp-c-text-3);
}
.gate-formula {
font-size: 0.7rem;
color: var(--vp-c-brand-1);
font-family: 'JetBrains Mono', monospace;
}
.gate-desc {
font-size: 0.6rem;
color: var(--vp-c-text-3);
margin-top: 0.1rem;
}
.calculation-box {
margin-top: 1rem;
padding: 0.6rem 0.8rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.calc-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.4rem;
}
.calc-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.calc-row {
display: flex;
align-items: baseline;
gap: 0.3rem;
font-size: 0.78rem;
}
.calc-label {
color: var(--vp-c-text-3);
min-width: 4rem;
}
.calc-formula {
font-family: 'JetBrains Mono', monospace;
color: var(--vp-c-text-1);
}
.calc-formula strong {
color: var(--vp-c-brand-1);
}
.calc-reason {
color: var(--vp-c-text-3);
font-size: 0.72rem;
}
.info-box {
display: flex;
gap: 0.25rem;
margin-top: 0.75rem;
padding: 0.6rem 0.8rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 600px) {
.circuit-container {
transform: scale(0.75);
transform-origin: left top;
padding-bottom: 0;
}
.terms-box {
flex-direction: column;
}
.gates {
grid-template-columns: 1fr;
}
} }
</style> </style>
@@ -20,8 +20,7 @@
<!-- MUX Demo --> <!-- MUX Demo -->
<div v-if="currentTab === 'mux'" class="demo-panel"> <div v-if="currentTab === 'mux'" class="demo-panel">
<div class="panel-desc"> <div class="panel-desc">
<strong>多路选择器 (MUX)</strong <strong>多路选择器 (MUX)</strong>像铁路道岔一样根据"选择信号"决定让哪一路数据通过
>像铁路道岔一样根据"选择信号"决定让哪一路数据通过
</div> </div>
<div class="mux-container"> <div class="mux-container">
<div class="inputs"> <div class="inputs">
@@ -81,8 +80,7 @@
<!-- Decoder Demo --> <!-- Decoder Demo -->
<div v-if="currentTab === 'decoder'" class="demo-panel"> <div v-if="currentTab === 'decoder'" class="demo-panel">
<div class="panel-desc"> <div class="panel-desc">
<strong>译码器 (Decoder)</strong <strong>译码器 (Decoder)</strong>将二进制输入转换为特定输出线的激活信号例如 2位输入可以激活
>将二进制输入转换为特定输出线的激活信号例如 2位输入可以激活
4根输出线中的一根 4根输出线中的一根
</div> </div>
<div class="decoder-container"> <div class="decoder-container">
@@ -37,9 +37,9 @@
</div> </div>
<div class="change-process"> <div class="change-process">
<div <div
class="process-step"
v-for="(step, index) in changeSteps" v-for="(step, index) in changeSteps"
:key="index" :key="index"
class="process-step"
> >
<div class="step-coin">{{ step.coin }}</div> <div class="step-coin">{{ step.coin }}</div>
<div class="step-text">× {{ step.count }} = {{ step.value }}</div> <div class="step-text">× {{ step.count }} = {{ step.value }}</div>
@@ -1,167 +1,116 @@
<template> <template>
<div class="half-adder-demo"> <div class="half-adder-demo">
<div class="demo-header"> <div class="demo-header">
<span class="title">半加器 (Half Adder)</span> <span class="title">半加器 (Half Adder) 交互演示</span>
<span class="subtitle">最基础的二进制加法单元 只能处理两个 1 位输入</span> <span class="subtitle">点击输入 A / B看看这一位加法的结果</span>
</div> </div>
<div class="terms-box"> <!-- 主交互区 -->
<div class="term-item"> <div class="main-area">
<span class="term-name">本位 (Sum)</span> <!-- 输入和直观结果 -->
<span class="term-desc">当前位的计算结果不考虑外部进位</span> <div class="left-panel">
</div> <div class="big-calc">
<div class="term-item"> <button class="big-bit" :class="{ on: inputA }" @click="inputA = !inputA">
<span class="term-name">进位 (Carry)</span>
<span class="term-desc">当两位都是 1 向更高位"借位"</span>
</div>
</div>
<div class="circuit-container">
<div class="inputs">
<div class="input-line">
<button
class="toggle-btn"
:class="{ on: inputA }"
@click="inputA = !inputA"
>
{{ inputA ? '1' : '0' }} {{ inputA ? '1' : '0' }}
</button> </button>
<span class="label">输入 A</span> <span class="op">+</span>
</div> <button class="big-bit" :class="{ on: inputB }" @click="inputB = !inputB">
<div class="input-line">
<button
class="toggle-btn"
:class="{ on: inputB }"
@click="inputB = !inputB"
>
{{ inputB ? '1' : '0' }} {{ inputB ? '1' : '0' }}
</button> </button>
<span class="label">输入 B</span> <span class="op">=</span>
<span class="result-display">
<span class="result-bit carry-bit" :class="{ lit: carryOut }">{{ carryOut ? '1' : '0' }}</span>
<span class="result-bit sum-bit" :class="{ lit: sumOut }">{{ sumOut ? '1' : '0' }}</span>
</span>
</div>
<div class="result-labels">
<span class="rl carry-label" :class="{ lit: carryOut }"> 进位向左边那列借一个 1</span>
<span class="rl sum-label" :class="{ lit: sumOut }"> 本位这一列写下的数字</span>
</div>
<div class="explain-box">
<div class="explain-text">{{ explainText }}</div>
</div> </div>
</div> </div>
<div class="wires"> <!-- 四种情况对照表高亮当前行 -->
<svg class="wire-svg" viewBox="0 0 100 150" preserveAspectRatio="none"> <div class="right-panel">
<path <div class="table-title">所有可能的情况</div>
d="M 0,30 C 50,30 50,40 100,40" <div class="truth-table">
fill="none" <div class="tr header">
:stroke="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'" <span>A</span><span>B</span><span class="sum-col">写下本位</span><span class="carry-col">进位</span>
stroke-width="2"
/>
<path
d="M 0,120 C 50,120 50,60 100,60"
fill="none"
:stroke="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<path
d="M 20,30 L 20,90 C 20,90 50,90 100,90"
fill="none"
:stroke="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<path
d="M 40,120 L 40,110 C 40,110 50,110 100,110"
fill="none"
:stroke="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<circle
cx="20"
cy="30"
r="3"
:fill="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
/>
<circle
cx="40"
cy="120"
r="3"
:fill="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
/>
</svg>
</div>
<div class="gates">
<div class="gate-box xor-gate" :class="{ active: sumOut }">
<div class="gate-header">
<span class="gate-name">XOR</span>
<span class="gate-cn">异或门</span>
</div> </div>
<div class="gate-formula">A B</div> <div
<div class="gate-desc">不同为 1 本位</div> v-for="row in cases"
</div> :key="row.a + '' + row.b"
<div class="gate-box and-gate" :class="{ active: carryOut }"> class="tr"
<div class="gate-header"> :class="{ active: row.a === +inputA && row.b === +inputB }"
<span class="gate-name">AND</span> >
<span class="gate-cn">与门</span> <span>{{ row.a }}</span>
<span>{{ row.b }}</span>
<span class="sum-col">{{ row.sum }}</span>
<span class="carry-col">{{ row.carry }}</span>
</div> </div>
<div class="gate-formula">A B</div>
<div class="gate-desc"> 1 1 进位</div>
</div> </div>
</div> <div class="pattern-note">
<p>仔细看这张表你会发现两个规律</p>
<div class="wires outputs-wires"> <ul>
<svg class="wire-svg" viewBox="0 0 50 150" preserveAspectRatio="none"> <li>写下只有 A B <strong>不一样</strong>时才是 1 这个规律叫 <code>XOR异或</code></li>
<line <li>进位只有 A B <strong>都是 1</strong> 时才是 1 这个规律叫 <code>AND</code></li>
x1="0" </ul>
y1="50"
x2="50"
y2="50"
:stroke="
sumOut ? 'var(--vp-c-green-1, #16a34a)' : 'var(--vp-c-text-3)'
"
stroke-width="2"
/>
<line
x1="0"
y1="100"
x2="50"
y2="100"
:stroke="carryOut ? '#d97706' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
</svg>
</div>
<div class="outputs">
<div class="output-line" :class="{ active: sumOut }">
<span class="label">本位 (Sum)</span>
<span class="out-val s-val">{{ sumOut ? '1' : '0' }}</span>
</div>
<div class="output-line" :class="{ active: carryOut }">
<span class="label">进位 (Carry)</span>
<span class="out-val c-val">{{ carryOut ? '1' : '0' }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="calculation-box"> <!-- 电路连接图 -->
<div class="calc-title">计算过程</div> <div class="circuit-section">
<div class="calc-content"> <div class="circuit-label">电路是这样连的</div>
<div class="calc-row"> <div class="circuit-row">
<span class="calc-label">输入</span> <div class="wire-inputs">
<span class="calc-value">A = {{ inputA ? '1' : '0' }}B = {{ inputB ? '1' : '0' }}</span> <div class="wire-bit a-bit" :class="{ on: inputA }">A = {{ inputA ? '1' : '0' }}</div>
<div class="wire-bit b-bit" :class="{ on: inputB }">B = {{ inputB ? '1' : '0' }}</div>
</div> </div>
<div class="calc-row"> <svg class="split-svg" viewBox="0 0 60 80" preserveAspectRatio="none">
<span class="calc-label">本位</span> <!-- A XOR -->
<span class="calc-formula">A B = {{ inputA ? '1' : '0' }} {{ inputB ? '1' : '0' }} = <line x1="0" y1="20" x2="30" y2="20" :stroke="inputA ? '#3b82f6' : '#ccc'" stroke-width="2" />
<strong>{{ sumOut ? '1' : '0' }}</strong></span> <line x1="30" y1="20" x2="60" y2="15" :stroke="inputA ? '#3b82f6' : '#ccc'" stroke-width="2" />
<span class="calc-reason">{{ inputA !== inputB ? '不同' : '相同' }}</span> <!-- A AND -->
<line x1="30" y1="20" x2="60" y2="65" :stroke="inputA ? '#3b82f6' : '#ccc'" stroke-width="2" />
<!-- 分支点 -->
<circle cx="30" cy="20" r="3" :fill="inputA ? '#3b82f6' : '#ccc'" />
<!-- B XOR -->
<line x1="0" y1="60" x2="30" y2="60" :stroke="inputB ? '#8b5cf6' : '#ccc'" stroke-width="2" />
<line x1="30" y1="60" x2="60" y2="25" :stroke="inputB ? '#8b5cf6' : '#ccc'" stroke-width="2" />
<!-- B AND -->
<line x1="30" y1="60" x2="60" y2="75" :stroke="inputB ? '#8b5cf6' : '#ccc'" stroke-width="2" />
<circle cx="30" cy="60" r="3" :fill="inputB ? '#8b5cf6' : '#ccc'" />
</svg>
<div class="gates-col">
<div class="gate-chip xor" :class="{ active: sumOut }">
<div class="chip-name">XOR 异或门</div>
<div class="chip-rule">不同 1</div>
<div class="chip-out">输出: <strong>{{ sumOut ? '1' : '0' }}</strong></div>
</div>
<div class="gate-chip and" :class="{ active: carryOut }">
<div class="chip-name">AND 与门</div>
<div class="chip-rule">全1 1</div>
<div class="chip-out">输出: <strong>{{ carryOut ? '1' : '0' }}</strong></div>
</div>
</div> </div>
<div class="calc-row"> <svg class="out-svg" viewBox="0 0 40 80" preserveAspectRatio="none">
<span class="calc-label">进位</span> <line x1="0" y1="20" x2="40" y2="20" :stroke="sumOut ? '#16a34a' : '#ccc'" stroke-width="2" />
<span class="calc-formula">A B = {{ inputA ? '1' : '0' }} {{ inputB ? '1' : '0' }} = <line x1="0" y1="60" x2="40" y2="60" :stroke="carryOut ? '#d97706' : '#ccc'" stroke-width="2" />
<strong>{{ carryOut ? '1' : '0' }}</strong></span> </svg>
<span class="calc-reason">{{ inputA && inputB ? '全为 1' : '不全为 1' }}</span> <div class="output-col">
<div class="out-chip sum" :class="{ active: sumOut }">
本位 (Sum)<br><strong>{{ sumOut ? '1' : '0' }}</strong>
</div>
<div class="out-chip carry" :class="{ active: carryOut }">
进位 (Carry)<br><strong>{{ carryOut ? '1' : '0' }}</strong>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="info-box">
<strong>核心思想</strong>
半加器用 XOR "本位和" AND
"进位"它是最小的加法单元但无法处理来自低位的进位
</div>
</div> </div>
</template> </template>
@@ -169,288 +118,277 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const inputA = ref(false) const inputA = ref(false)
const inputB = ref(true) const inputB = ref(false)
const sumOut = computed(() => inputA.value !== inputB.value) const sumOut = computed(() => inputA.value !== inputB.value)
const carryOut = computed(() => inputA.value && inputB.value) const carryOut = computed(() => inputA.value && inputB.value)
const cases = [
{ a: 0, b: 0, sum: 0, carry: 0 },
{ a: 0, b: 1, sum: 1, carry: 0 },
{ a: 1, b: 0, sum: 1, carry: 0 },
{ a: 1, b: 1, sum: 0, carry: 1 },
]
const explainText = computed(() => {
const a = +inputA.value
const b = +inputB.value
if (a === 0 && b === 0) return '0 + 0 = 0。这一列写下 0,不需要进位。'
if (a === 0 && b === 1) return '0 + 1 = 1。这一列写下 1,不需要进位。'
if (a === 1 && b === 0) return '1 + 0 = 1。这一列写下 1,不需要进位。'
return '1 + 1 = 2。但二进制这一列最多写 1,所以写下 0,并且向左边那列"进一个 1"(进位)。就像十进制的 9+1=10,个位写 0、十位进 1。'
})
</script> </script>
<style scoped> <style scoped>
.half-adder-demo { .half-adder-demo {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 10px;
background: var(--vp-c-bg-soft); background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem; padding: 1.2rem;
margin: 1rem 0; margin: 1rem 0;
} }
.demo-header { .demo-header {
display: flex; margin-bottom: 1rem;
flex-direction: column;
gap: 0.15rem;
margin-bottom: 0.75rem;
} }
.title { .title {
font-size: 0.9rem; display: block;
font-size: 0.95rem;
font-weight: bold; font-weight: bold;
color: var(--vp-c-text-1); color: var(--vp-c-text-1);
} }
.subtitle { .subtitle {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
} }
.terms-box { /* ── main area ── */
.main-area {
display: flex; display: flex;
gap: 0.5rem; gap: 1.5rem;
margin-bottom: 0.75rem; flex-wrap: wrap;
padding: 0.5rem; margin-bottom: 1.2rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
} }
.term-item { /* left */
flex: 1; .left-panel { flex: 1; min-width: 200px; }
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.term-name { .big-calc {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-brand-1);
}
.term-desc {
font-size: 0.68rem;
color: var(--vp-c-text-3);
line-height: 1.3;
}
.circuit-container {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 1rem;
overflow-x: auto;
}
.inputs,
.outputs {
display: flex;
flex-direction: column;
gap: 3.5rem;
min-width: 6rem;
z-index: 2;
}
.outputs {
min-width: 8rem;
}
.input-line,
.output-line {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 0.3rem;
} }
.label { .big-bit {
font-size: 0.8rem; width: 3rem;
color: var(--vp-c-text-1); height: 3rem;
} font-size: 1.5rem;
.toggle-btn {
width: 2.2rem;
height: 2.2rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-weight: bold; font-weight: bold;
font-family: monospace; font-family: monospace;
font-size: 1rem; border-radius: 6px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.toggle-btn.on { .big-bit.on {
background: var(--vp-c-brand-soft); background: #dbeafe;
color: var(--vp-c-brand-1); color: #1d4ed8;
border-color: var(--vp-c-brand-1); border-color: #3b82f6;
} }
.out-val { .op {
display: inline-flex; font-size: 1.5rem;
align-items: center; color: var(--vp-c-text-3);
justify-content: center; font-weight: bold;
width: 2.2rem; }
height: 2.2rem;
border-radius: 4px; .result-display {
display: flex;
gap: 0.2rem;
}
.result-bit {
width: 3rem;
height: 3rem;
border-radius: 6px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg); background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider); font-size: 1.5rem;
font-weight: bold; font-weight: bold;
font-family: monospace; font-family: monospace;
font-size: 1rem;
transition: all 0.2s;
}
.output-line.active .s-val {
background: #dcfce7;
color: #16a34a;
border-color: #16a34a;
}
.output-line.active .c-val {
background: #fef3c7;
color: #d97706;
border-color: #d97706;
}
.wires {
width: 80px;
height: 150px;
position: relative;
}
.outputs-wires {
width: 40px;
}
.wire-svg {
width: 100%;
height: 100%;
}
.gates {
display: flex; display: flex;
flex-direction: column;
gap: 1.5rem;
z-index: 2;
margin-top: 5px;
}
.gate-box {
width: 7.5rem;
height: 4rem;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.gate-box.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 8px var(--vp-c-brand-soft);
}
.gate-header {
display: flex;
align-items: baseline;
gap: 0.25rem;
}
.gate-name {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.gate-cn {
font-size: 0.7rem;
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
transition: all 0.2s;
} }
.carry-bit.lit { background: #fef3c7; color: #d97706; border-color: #d97706; }
.sum-bit.lit { background: #dcfce7; color: #16a34a; border-color: #16a34a; }
.gate-formula { .result-labels {
font-size: 0.75rem; display: flex;
color: var(--vp-c-brand-1); justify-content: flex-end;
font-family: 'JetBrains Mono', monospace; gap: 1rem;
margin-top: 0.2rem;
margin-bottom: 0.8rem;
} }
.rl {
.gate-desc {
font-size: 0.65rem; font-size: 0.65rem;
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
margin-top: 0.15rem; transition: all 0.2s;
} }
.carry-label.lit { color: #d97706; font-weight: bold; }
.sum-label.lit { color: #16a34a; font-weight: bold; }
.calculation-box { .explain-box {
margin-top: 1rem;
padding: 0.6rem 0.8rem;
background: var(--vp-c-bg); background: var(--vp-c-bg);
border-radius: 6px; border-radius: 6px;
padding: 0.6rem 0.8rem;
border-left: 3px solid var(--vp-c-brand-1);
}
.explain-text {
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
} }
.calc-title { /* right */
.right-panel { flex: 1; min-width: 200px; }
.table-title {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
} }
.truth-table { border-radius: 6px; overflow: hidden; border: 1px solid var(--vp-c-divider); margin-bottom: 0.75rem; }
.calc-content { .tr {
display: flex; display: grid;
flex-direction: column; grid-template-columns: 1fr 1fr 2fr 1.5fr;
gap: 0.25rem; padding: 0.35rem 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
font-family: monospace;
font-size: 0.82rem;
transition: all 0.2s;
} }
.tr:last-child { border-bottom: none; }
.tr.header {
background: var(--vp-c-bg-alt);
font-weight: bold;
font-family: system-ui;
font-size: 0.72rem;
color: var(--vp-c-text-2);
}
.tr.active {
background: var(--vp-c-brand-soft);
font-weight: bold;
}
.sum-col { color: #16a34a; }
.carry-col { color: #d97706; }
.tr.active .sum-col { color: #16a34a; }
.tr.active .carry-col { color: #d97706; }
.calc-row { .pattern-note {
display: flex; background: var(--vp-c-bg);
align-items: baseline; border-radius: 6px;
gap: 0.3rem; padding: 0.6rem 0.8rem;
font-size: 0.78rem; font-size: 0.78rem;
color: var(--vp-c-text-2);
} }
.pattern-note p { margin: 0 0 0.4rem 0; }
.calc-label { .pattern-note ul { margin: 0; padding-left: 1.2rem; }
color: var(--vp-c-text-3); .pattern-note li { margin-bottom: 0.3rem; line-height: 1.4; }
min-width: 3.5rem; .pattern-note code {
} background: var(--vp-c-bg-alt);
padding: 0.05rem 0.3rem;
.calc-formula { border-radius: 3px;
font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;
color: var(--vp-c-text-1);
}
.calc-formula strong {
color: var(--vp-c-brand-1); color: var(--vp-c-brand-1);
} }
.calc-reason { /* ── circuit section ── */
color: var(--vp-c-text-3); .circuit-section {
font-size: 0.72rem; border-top: 1px solid var(--vp-c-divider);
padding-top: 1rem;
} }
.circuit-label {
.info-box { font-size: 0.75rem;
display: flex; font-weight: 600;
gap: 0.25rem;
margin-top: 0.75rem;
padding: 0.6rem 0.8rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.circuit-row {
display: flex;
align-items: center;
gap: 0;
}
.wire-inputs {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.wire-bit {
padding: 0.3rem 0.6rem;
font-family: monospace;
font-size: 0.8rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
transition: all 0.2s;
min-width: 4.5rem;
text-align: center;
}
.wire-bit.on { background: #dbeafe; color: #1d4ed8; border-color: #3b82f6; }
.split-svg { width: 50px; height: 80px; flex-shrink: 0; }
.gates-col {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.gate-chip {
width: 7rem;
padding: 0.4rem 0.5rem;
border-radius: 6px;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
text-align: center;
transition: all 0.2s;
}
.gate-chip.active { border-color: var(--vp-c-brand-1); background: var(--vp-c-brand-soft); }
.chip-name { font-size: 0.7rem; font-weight: bold; color: var(--vp-c-text-1); }
.chip-rule { font-size: 0.62rem; color: var(--vp-c-text-3); }
.chip-out { font-size: 0.7rem; font-family: monospace; margin-top: 0.15rem; }
.gate-chip.active .chip-out strong { color: var(--vp-c-brand-1); }
.out-svg { width: 35px; height: 80px; flex-shrink: 0; }
.output-col {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.out-chip {
padding: 0.3rem 0.5rem;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-size: 0.7rem;
text-align: center;
line-height: 1.4; line-height: 1.4;
min-width: 5rem;
transition: all 0.2s;
} }
.out-chip strong { font-size: 1rem; display: block; }
.out-chip.sum.active { background: #dcfce7; border-color: #16a34a; color: #166534; }
.out-chip.carry.active { background: #fef3c7; border-color: #d97706; color: #92400e; }
.info-box strong { @media (max-width: 640px) {
white-space: nowrap; .main-area { flex-direction: column; }
flex-shrink: 0; .circuit-row { overflow-x: auto; }
color: var(--vp-c-text-1);
}
@media (max-width: 600px) {
.circuit-container {
transform: scale(0.85);
transform-origin: left top;
padding-bottom: 0;
}
.terms-box {
flex-direction: column;
}
} }
</style> </style>
@@ -28,7 +28,7 @@
placeholder="值 (如: 苹果)" placeholder="值 (如: 苹果)"
class="hash-input" class="hash-input"
/> />
<button @click="addData" class="add-btn">添加</button> <button class="add-btn" @click="addData">添加</button>
</div> </div>
</div> </div>
@@ -35,8 +35,7 @@
v-for="lang in era.languages" v-for="lang in era.languages"
:key="lang" :key="lang"
class="lang-dot" class="lang-dot"
>{{ lang }}</span >{{ lang }}</span>
>
</div> </div>
</div> </div>
</div> </div>
@@ -91,8 +90,7 @@
v-for="lang in selectedParadigm.languages" v-for="lang in selectedParadigm.languages"
:key="lang" :key="lang"
class="lang-tag" class="lang-tag"
>{{ lang }}</span >{{ lang }}</span>
>
</div> </div>
<div class="paradigm-detail-example"> <div class="paradigm-detail-example">
<pre><code>{{ selectedParadigm.example }}</code></pre> <pre><code>{{ selectedParadigm.example }}</code></pre>
@@ -102,8 +100,7 @@
v-for="t in selectedParadigm.traits" v-for="t in selectedParadigm.traits"
:key="t" :key="t"
class="trait-chip" class="trait-chip"
>{{ t }}</span >{{ t }}</span>
>
</div> </div>
</div> </div>
</div> </div>
@@ -161,8 +158,7 @@
v-for="lang in rec.langs" v-for="lang in rec.langs"
:key="lang" :key="lang"
class="choose-lang-tag" class="choose-lang-tag"
>{{ lang }}</span >{{ lang }}</span>
>
</div> </div>
<div class="choose-reason">{{ rec.reason }}</div> <div class="choose-reason">{{ rec.reason }}</div>
</div> </div>
@@ -185,19 +181,11 @@
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong> <strong>核心思想</strong>
<span v-if="activeTab === 'timeline'" <span v-if="activeTab === 'timeline'">编程语言从机器语言到现代高级语言一直在朝着"更接近人类思维"的方向演化</span>
>编程语言从机器语言到现代高级语言一直在朝着"更接近人类思维"的方向演化</span <span v-else-if="activeTab === 'paradigms'">编程范式是思考问题的方式命令式关注"怎么做"声明式关注"做什么"选择范式比选语言更重要</span>
> <span v-else-if="activeTab === 'compare'">没有最好的语言只有最适合场景的语言类型系统运行方式生态都是选择时的关键考量</span>
<span v-else-if="activeTab === 'paradigms'" <span v-else>初学者先学 Python简单通用再学 JavaScriptWeb
>编程范式是思考问题的方式命令式关注"怎么做"声明式关注"做什么"选择范式比选语言更重要</span 必备最后选一门静态语言TypeScript/Go/Rust深入</span>
>
<span v-else-if="activeTab === 'compare'"
>没有最好的语言只有最适合场景的语言类型系统运行方式生态都是选择时的关键考量</span
>
<span v-else
>初学者先学 Python简单通用再学 JavaScriptWeb
必备最后选一门静态语言TypeScript/Go/Rust深入</span
>
</div> </div>
</div> </div>
</template> </template>
@@ -1,373 +1,349 @@
<template> <template>
<div class="memory-demo"> <div class="demo">
<div class="demo-controls"> <div class="title">🧠 操作系统给每个程序"画饼"</div>
<button
class="allocate-btn wechat" <div class="scene">
@click="allocate('wechat')" <!-- 程序视角 -->
:disabled="!hasFreeSpace" <div class="view-box">
> <div class="view-title">📱 程序以为的内存虚拟</div>
+ 给微信分配数据 <div class="virtual-mem">
</button> <div class="proc-mem wechat">
<button <div class="proc-label">💬 微信</div>
class="allocate-btn game" <div class="mem-blocks">
@click="allocate('game')" <div
:disabled="!hasFreeSpace" v-for="n in 4"
> :key="n"
+ 给游戏分配数据 class="v-block"
</button> :class="{ filled: wechatProgress >= n * 25 }"
<button class="reset-btn" @click="reset"> 重置</button> >{{ n }}</div>
</div>
<div class="system-view">
<!-- 虚拟内存试图 -->
<div class="virtual-cluster">
<div class="process-vm wechat">
<div class="title">💬 微信的虚拟内存<br />(它认为自己独占了空间)</div>
<div class="vm-blocks">
<div
v-for="i in 4"
:key="'w' + i"
class="block"
:class="{ filled: wechatBlocks >= i }"
>
{{ wechatBlocks >= i ? '数据 ' + i : '虚拟空闲' }}
</div> </div>
</div> </div>
</div> <div class="proc-mem game">
<div class="proc-label">🎮 游戏</div>
<div class="process-vm game"> <div class="mem-blocks">
<div class="title"> <div
🎮 游戏的虚拟内存<br />(它也认为自己独占了空间) v-for="n in 4"
</div> :key="n"
<div class="vm-blocks"> class="v-block game"
<div :class="{ filled: gameProgress >= n * 25 }"
v-for="i in 4" >{{ n }}</div>
:key="'g' + i"
class="block"
:class="{ filled: gameBlocks >= i }"
>
{{ gameBlocks >= i ? '数据 ' + i : '虚拟空闲' }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- OS 页表 (映射表) --> <!-- 映射箭头 -->
<div class="os-page-table"> <div class="mapping-arrow">
<div class="title">保安大叔 (OS 页表)</div> <div class="arrow-text">操作系统偷偷映射 </div>
<div class="table-info"> <div class="mapping-lines">
当程序存数据时<br />由我暗中转移到真正的物理缝隙里 <div
v-for="(map, idx) in visibleMappings"
:key="idx"
class="map-line"
:class="map.type"
:style="{ animationDelay: idx * 0.2 + 's' }"
>
<span class="from">{{ map.from }}</span>
<span class="line"></span>
<span class="to">{{ map.to }}</span>
</div>
</div> </div>
</div> </div>
<!-- 物理内存 --> <!-- 物理内存 -->
<div class="physical-memory"> <div class="view-box physical">
<div class="title">🗄 真实的物理内存条<br />(其实像个大杂烩一样乱)</div> <div class="view-title">💾 真实的内存条物理</div>
<div class="pm-blocks"> <div class="physical-mem">
<div <div
v-for="(block, idx) in physicalBlocks" v-for="(block, idx) in physicalBlocks"
:key="'p' + idx" :key="idx"
class="block" class="p-block"
:class="[block.type, { occupied: block.type !== 'empty' }]" :class="[block.type, { active: block.active }]"
> >
{{ block.label }} <span class="p-addr">{{ idx + 1 }}</span>
<span class="p-owner">{{ block.label }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="explanation-box" v-if="wechatBlocks > 0 || gameBlocks > 0"> <div class="explain">
💡 <strong>💡 原理</strong>每个程序以为自己独占连续的内存实际上操作系统把数据分散存到真实内存各处程序看到的地址都是"假"操作系统负责翻译
发现了没尽管右侧真正的物理内存已经被塞得像个狗皮膏药但在左侧的微信和游戏眼里自己的内存条永远是连续且干净的更重要的是微信绝对访问不到橘色的物理块保证了安全
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
const wechatBlocks = ref(0) const wechatProgress = ref(0)
const gameBlocks = ref(0) const gameProgress = ref(0)
const currentMapping = ref(0)
// //
// empty = , os = const physicalBlocks = ref([
const initialPhysicalBlocks = [ { type: 'os', label: '系统', active: false },
{ type: 'os', label: '系统核心占用' }, { type: 'empty', label: '', active: false },
{ type: 'empty', label: '空闲' }, { type: 'empty', label: '', active: false },
{ type: 'os', label: '系统保留' }, { type: 'os', label: '系统', active: false },
{ type: 'empty', label: '空闲' }, { type: 'empty', label: '', active: false },
{ type: 'empty', label: '空闲' }, { type: 'empty', label: '', active: false },
{ type: 'empty', label: '空闲' }, { type: 'empty', label: '', active: false },
{ type: 'os', label: '系统驱动' }, { type: 'os', label: '系统', active: false }
{ type: 'empty', label: '空闲' } ])
// ->
const mappings = [
{ from: '微信-1', to: '物理-2', type: 'wechat' },
{ from: '微信-2', to: '物理-3', type: 'wechat' },
{ from: '游戏-1', to: '物理-5', type: 'game' },
{ from: '游戏-2', to: '物理-6', type: 'game' }
] ]
const physicalBlocks = ref(JSON.parse(JSON.stringify(initialPhysicalBlocks))) const visibleMappings = computed(() => {
return mappings.slice(0, currentMapping.value)
const freeSpaceCount = computed(() => {
return physicalBlocks.value.filter((b) => b.type === 'empty').length
}) })
const hasFreeSpace = computed(() => freeSpaceCount.value > 0) let timer = null
let phase = 0
const allocate = (process) => { const runDemo = () => {
if (!hasFreeSpace.value) return switch(phase) {
case 0: //
// Find a process block logic wechatProgress.value = 50
if (process === 'wechat' && wechatBlocks.value < 4) { physicalBlocks.value[1] = { type: 'wechat', label: 'W1', active: true }
wechatBlocks.value++ physicalBlocks.value[2] = { type: 'wechat', label: 'W2', active: true }
fillRandomEmptyBlock('wechat', `微信数据 ${wechatBlocks.value}`) currentMapping.value = 2
} else if (process === 'game' && gameBlocks.value < 4) { phase = 1
gameBlocks.value++ break
fillRandomEmptyBlock('game', `游戏数据 ${gameBlocks.value}`) case 1: //
gameProgress.value = 50
physicalBlocks.value[4] = { type: 'game', label: 'G1', active: true }
physicalBlocks.value[5] = { type: 'game', label: 'G2', active: true }
currentMapping.value = 4
phase = 2
break
case 2: //
physicalBlocks.value.forEach(b => b.active = false)
phase = 3
break
case 3: //
wechatProgress.value = 0
gameProgress.value = 0
currentMapping.value = 0
physicalBlocks.value[1] = { type: 'empty', label: '', active: false }
physicalBlocks.value[2] = { type: 'empty', label: '', active: false }
physicalBlocks.value[4] = { type: 'empty', label: '', active: false }
physicalBlocks.value[5] = { type: 'empty', label: '', active: false }
phase = 0
break
} }
} }
const fillRandomEmptyBlock = (type, label) => { onMounted(() => {
const emptyIndices = [] timer = setInterval(runDemo, 2000)
physicalBlocks.value.forEach((b, i) => { })
if (b.type === 'empty') emptyIndices.push(i)
})
if (emptyIndices.length > 0) { onUnmounted(() => {
const randomIndex = clearInterval(timer)
emptyIndices[Math.floor(Math.random() * emptyIndices.length)] })
physicalBlocks.value[randomIndex] = { type, label }
}
}
const reset = () => {
wechatBlocks.value = 0
gameBlocks.value = 0
physicalBlocks.value = JSON.parse(JSON.stringify(initialPhysicalBlocks))
}
</script> </script>
<style scoped> <style scoped>
.memory-demo { .demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft); background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider); padding: 16px;
border-radius: 12px; margin: 1rem 0;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.demo-controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
justify-content: center;
flex-wrap: wrap;
}
.allocate-btn {
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.allocate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(1);
}
.allocate-btn.wechat {
background: var(--vp-c-success-1);
}
.allocate-btn.wechat:not(:disabled):hover {
filter: brightness(1.1);
}
.allocate-btn.game {
background: var(--vp-c-warning-1);
}
.allocate-btn.game:not(:disabled):hover {
filter: brightness(1.1);
}
.reset-btn {
background: transparent;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
padding: 0.6rem 1.2rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.reset-btn:hover {
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
}
.system-view {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 1.5rem;
}
@media (max-width: 768px) {
.system-view {
flex-direction: column;
}
} }
.title { .title {
font-size: 0.85rem; font-weight: 600;
font-weight: bold; font-size: 14px;
margin-bottom: 12px;
text-align: center; text-align: center;
margin-bottom: 1rem;
color: var(--vp-c-text-1);
min-height: 2.5rem;
} }
.virtual-cluster { .scene {
display: flex;
gap: 1rem;
flex: 2;
}
.process-vm {
flex: 1;
background: var(--vp-c-bg-alt);
border: 2px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
}
.vm-blocks {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 12px;
margin-bottom: 12px;
} }
.block { .view-box {
padding: 0.6rem; background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 6px;
padding: 10px;
}
.view-box.physical {
background: #1a1a2e11;
}
.view-title {
font-size: 11px;
font-weight: 600;
color: var(--vp-c-text-3);
margin-bottom: 8px;
text-align: center; text-align: center;
font-size: 0.8rem; }
font-weight: bold;
.virtual-mem {
display: flex;
flex-direction: column;
gap: 8px;
}
.proc-mem {
display: flex;
align-items: center;
gap: 8px;
}
.proc-label {
font-size: 11px;
width: 50px;
flex-shrink: 0;
}
.mem-blocks {
display: flex;
gap: 4px;
flex: 1;
}
.v-block {
flex: 1;
height: 28px;
border: 1px dashed var(--vp-c-divider);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: var(--vp-c-text-3);
transition: all 0.3s; transition: all 0.3s;
} }
.process-vm .block { .v-block.filled {
background: var(--vp-c-bg-mute); background: #16a34a33;
border: 1px solid var(--vp-c-divider); border: 1px solid #16a34a;
color: var(--vp-c-text-3); color: #16a34a;
opacity: 0.5; font-weight: 600;
}
.process-vm.wechat .block.filled {
background: rgba(16, 185, 129, 0.15);
border: 1px solid var(--vp-c-success-1);
color: var(--vp-c-success-1);
opacity: 1;
}
.process-vm.game .block.filled {
background: rgba(245, 158, 11, 0.15);
border: 1px solid var(--vp-c-warning-1);
color: var(--vp-c-warning-1);
opacity: 1;
} }
.os-page-table { .v-block.game.filled {
flex: 1; background: #d9770633;
border-color: #d97706;
color: #d97706;
}
.mapping-arrow {
text-align: center;
padding: 4px 0;
}
.arrow-text {
font-size: 10px;
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.mapping-lines {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.map-line {
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
animation: fade-in 0.3s ease;
}
.map-line.wechat {
color: #16a34a;
}
.map-line.game {
color: #d97706;
}
.map-line .line {
width: 20px;
height: 1px;
background: currentColor;
}
.physical-mem {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
}
.p-block {
height: 32px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--vp-c-bg-alt); font-size: 9px;
border-radius: 10px; transition: all 0.3s;
padding: 1rem;
position: relative;
border: 2px solid var(--vp-c-brand-1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
} }
.os-page-table .table-info { .p-block.os {
font-size: 0.8rem; background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
text-align: center;
background: var(--vp-c-bg);
padding: 0.8rem;
border-radius: 8px;
}
.physical-memory {
flex: 1;
background: var(--vp-c-bg-alt);
border-radius: 10px;
padding: 1rem;
border: 2px solid var(--vp-c-text-3);
}
.pm-blocks {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.pm-blocks .block {
padding: 0.5rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-3);
font-size: 0.75rem;
}
.pm-blocks .block.os {
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-2);
border-style: dashed; border-style: dashed;
} color: var(--vp-c-text-3);
.pm-blocks .block.wechat {
background: var(--vp-c-success-1);
color: white;
border-color: var(--vp-c-success-1);
animation: popIn 0.3s ease-out;
}
.pm-blocks .block.game {
background: var(--vp-c-warning-1);
color: white;
border-color: var(--vp-c-warning-1);
animation: popIn 0.3s ease-out;
} }
@keyframes popIn { .p-block.wechat {
0% { background: #16a34a22;
transform: scale(0.9); border-color: #16a34a;
opacity: 0; color: #16a34a;
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: 1;
}
} }
.explanation-box { .p-block.game {
margin-top: 1.5rem; background: #d9770622;
padding: 1rem; border-color: #d97706;
background: rgba(16, 185, 129, 0.1); color: #d97706;
border-left: 4px solid var(--vp-c-success-1);
border-radius: 0 8px 8px 0;
font-size: 0.95rem;
animation: fadeIn 0.5s;
} }
@keyframes fadeIn { .p-block.active {
from { box-shadow: 0 0 8px currentColor;
opacity: 0; transform: scale(1.05);
transform: translateY(10px); }
}
to { .p-addr {
opacity: 1; font-size: 8px;
transform: translateY(0); opacity: 0.7;
} }
.p-owner {
font-weight: 600;
}
.explain {
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.5;
padding: 10px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.explain strong { color: var(--vp-c-text-1); }
@keyframes fade-in {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
} }
</style> </style>
@@ -70,8 +70,7 @@
</div> </div>
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong <strong>核心思想</strong>分层设计让网络协议模块化每层只关心自己的职责数据从应用层向下传递时每层都会添加自己的"信封"(头部)接收时再逐层拆开
>分层设计让网络协议模块化每层只关心自己的职责数据从应用层向下传递时每层都会添加自己的"信封"(头部)接收时再逐层拆开
</div> </div>
</div> </div>
</template> </template>
@@ -0,0 +1,317 @@
<template>
<div class="demo">
<div class="scene">
<!-- 应用程序层 -->
<div class="layer-box app-layer" :class="{ active: currentStep >= 1 }">
<div class="layer-title">📱 应用程序</div>
<div class="apps">
<span class="app-icon" :class="{ pulse: currentStep === 1 }">🎵</span>
<span class="app-icon" :class="{ pulse: currentStep === 1 }">💬</span>
<span class="app-icon" :class="{ pulse: currentStep === 1 }">🎮</span>
</div>
</div>
<!-- 流动箭头 -->
<div class="flow-arrow" :class="{ flowing: currentStep === 2 }">
<div class="arrow-line"></div>
<div class="arrow-head"></div>
<div class="packet" v-if="currentStep === 2">📦 请求</div>
</div>
<!-- 操作系统层 -->
<div class="layer-box os-layer" :class="{ active: currentStep >= 2, processing: currentStep === 3 }">
<div class="layer-title">🖥 操作系统</div>
<div class="os-core">
<div class="core-item" :class="{ working: currentStep === 3 && subStep === 0 }">调度CPU</div>
<div class="core-item" :class="{ working: currentStep === 3 && subStep === 1 }">分配内存</div>
<div class="core-item" :class="{ working: currentStep === 3 && subStep === 2 }">管理文件</div>
</div>
</div>
<!-- 流动箭头 -->
<div class="flow-arrow" :class="{ flowing: currentStep === 4 }">
<div class="arrow-line"></div>
<div class="arrow-head"></div>
<div class="packet" v-if="currentStep === 4"> 指令</div>
</div>
<!-- 硬件层 -->
<div class="layer-box hw-layer" :class="{ active: currentStep >= 4, working: currentStep === 5 }">
<div class="layer-title">💾 硬件</div>
<div class="hw-items">
<span class="hw-icon" :class="{ spin: currentStep === 5 }">🧠 CPU</span>
<span class="hw-icon" :class="{ flash: currentStep === 5 }">💾 内存</span>
<span class="hw-icon" :class="{ flash: currentStep === 5 }">💿 硬盘</span>
</div>
</div>
</div>
<div class="status-bar">
<span class="status-text">{{ statusText }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const currentStep = ref(0)
const subStep = ref(0)
let timer = null
const statusTexts = [
'应用程序准备发起请求...',
'应用程序:我要播放音乐!',
'请求发送给操作系统...',
'操作系统正在协调资源...',
'指令下发到硬件...',
'硬件开始执行:音乐播放中 🎵'
]
const statusText = computed(() => statusTexts[currentStep.value] || '')
const nextStep = () => {
if (currentStep.value === 3) {
//
subStep.value = (subStep.value + 1) % 3
if (subStep.value === 0) {
currentStep.value = 4
}
} else {
currentStep.value = (currentStep.value + 1) % 6
if (currentStep.value === 3) {
subStep.value = 0
}
}
}
onMounted(() => {
timer = setInterval(nextStep, 1500)
})
onUnmounted(() => {
clearInterval(timer)
})
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 1rem 0;
}
.scene {
display: flex;
flex-direction: column;
gap: 8px;
}
.layer-box {
padding: 12px;
border-radius: 8px;
border: 2px solid transparent;
transition: all 0.3s;
opacity: 0.5;
}
.layer-box.active {
opacity: 1;
}
.app-layer {
background: linear-gradient(135deg, #667eea22, #764ba222);
border-color: #667eea55;
}
.app-layer.active {
border-color: #667eea;
box-shadow: 0 0 15px #667eea55;
}
.os-layer {
background: linear-gradient(135deg, #f093fb22, #f5576c22);
border-color: #f5576c55;
}
.os-layer.active {
border-color: #f5576c;
box-shadow: 0 0 15px #f5576c55;
}
.os-layer.processing {
animation: pulse-os 1s infinite;
}
.hw-layer {
background: linear-gradient(135deg, #4facfe22, #00f2fe22);
border-color: #4facfe55;
}
.hw-layer.active {
border-color: #4facfe;
box-shadow: 0 0 15px #4facfe55;
}
.hw-layer.working {
animation: pulse-hw 0.5s infinite;
}
.layer-title {
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
text-align: center;
}
.apps {
display: flex;
justify-content: center;
gap: 16px;
}
.app-icon {
font-size: 24px;
transition: transform 0.3s;
}
.app-icon.pulse {
animation: bounce 0.5s infinite;
}
.os-core {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.core-item {
padding: 6px 12px;
background: var(--vp-c-bg);
border-radius: 4px;
font-size: 11px;
transition: all 0.3s;
}
.core-item.working {
background: #f5576c;
color: white;
transform: scale(1.1);
}
.hw-items {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.hw-icon {
font-size: 12px;
padding: 4px 8px;
background: var(--vp-c-bg);
border-radius: 4px;
transition: all 0.3s;
}
.hw-icon.spin {
animation: spin 1s linear infinite;
}
.hw-icon.flash {
animation: flash 0.5s infinite;
}
.flow-arrow {
display: flex;
flex-direction: column;
align-items: center;
height: 30px;
position: relative;
}
.arrow-line {
width: 2px;
height: 20px;
background: var(--vp-c-divider);
transition: all 0.3s;
}
.flow-arrow.flowing .arrow-line {
background: linear-gradient(to bottom, #f5576c, #4facfe);
box-shadow: 0 0 5px #f5576c;
}
.arrow-head {
font-size: 10px;
color: var(--vp-c-divider);
line-height: 1;
}
.flow-arrow.flowing .arrow-head {
color: #4facfe;
}
.packet {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #f5576c;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
animation: flow-down 1s ease-in-out;
white-space: nowrap;
}
.status-bar {
margin-top: 12px;
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
text-align: center;
}
.status-text {
font-size: 12px;
color: var(--vp-c-text-2);
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes pulse-os {
0%, 100% { box-shadow: 0 0 5px #f5576c55; }
50% { box-shadow: 0 0 20px #f5576caa; }
}
@keyframes pulse-hw {
0%, 100% { box-shadow: 0 0 5px #4facfe55; }
50% { box-shadow: 0 0 20px #4facfeaa; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes flow-down {
0% { opacity: 0; transform: translate(-50%, -100%); }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; transform: translate(-50%, 0%); }
}
</style>
@@ -1,313 +0,0 @@
<template>
<div class="os-overview-demo">
<div class="demo-header">
<span class="title">操作系统计算机的"大管家"</span>
<span class="subtitle">让多个程序和谐共处的艺术</span>
</div>
<div class="demo-content">
<div class="os-layers">
<div class="layer user-apps">
<div class="layer-title">应用程序层</div>
<div class="layer-content">
<div
v-for="app in applications"
:key="app.id"
class="app-icon"
:class="{ active: activeApp === app.id }"
@click="activeApp = app.id"
:title="app.name"
>
{{ app.icon }}
</div>
</div>
<div class="layer-desc">
用户直接使用的程序浏览器IDE游戏等
</div>
</div>
<div class="layer kernel">
<div class="layer-title">操作系统内核</div>
<div class="layer-content">
<div class="kernel-components">
<div
v-for="component in kernelComponents"
:key="component.id"
class="kernel-component"
:class="{ active: activeComponent === component.id }"
@click="activeComponent = component.id"
>
{{ component.icon }} {{ component.name }}
</div>
</div>
</div>
<div class="layer-desc">进程管理内存管理文件系统设备管理</div>
</div>
<div class="layer hardware">
<div class="layer-title">硬件层</div>
<div class="layer-content">
<div class="hardware-icons">
<span>💻 CPU</span>
<span>🧠 RAM</span>
<span>💾 硬盘</span>
<span>🖥 GPU</span>
</div>
</div>
</div>
</div>
<div class="resource-flow">
<div class="flow-title">资源流向</div>
<div class="flow-content">
<div class="flow-item" :class="{ active: showFlow }">
<div class="flow-arrow"></div>
<div class="flow-desc">应用程序请求资源内存CPU文件</div>
</div>
<div class="flow-item" :class="{ active: showFlow }">
<div class="flow-arrow"></div>
<div class="flow-desc">操作系统内核统一分配和调度</div>
</div>
<div class="flow-item" :class="{ active: showFlow }">
<div class="flow-arrow"></div>
<div class="flow-desc">硬件执行实际操作</div>
</div>
</div>
<button class="flow-btn" @click="showFlow = !showFlow">
{{ showFlow ? '隐藏' : '显示' }}资源流
</button>
</div>
<div class="demo-details">
<div class="detail-item">
<div class="detail-title">
当前选中{{ activeAppName || '请选择应用程序' }}
</div>
<div class="detail-desc">
{{ getActiveAppDesc() }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeApp = ref('browser')
const activeComponent = ref('process')
const showFlow = ref(false)
const applications = [
{ id: 'browser', name: '浏览器', icon: '🌐' },
{ id: 'ide', name: '代码编辑器', icon: '💻' },
{ id: 'music', name: '音乐播放器', icon: '🎵' },
{ id: 'video', name: '视频播放器', icon: '🎬' },
{ id: 'game', name: '游戏', icon: '🎮' }
]
const kernelComponents = [
{ id: 'process', name: '进程管理', icon: '🔄' },
{ id: 'memory', name: '内存管理', icon: '🧠' },
{ id: 'filesystem', name: '文件系统', icon: '📁' },
{ id: 'device', name: '设备管理', icon: '🔧' }
]
const activeAppName = computed(() => {
const app = applications.find((a) => a.id === activeApp.value)
return app?.name || ''
})
const getActiveAppDesc = () => {
const component = kernelComponents.find((c) => c.id === activeComponent.value)
const app = applications.find((a) => a.id === activeApp.value)
if (!app || !component) return '请选择应用程序和内核组件'
return `${app.name} 需要使用 ${component.name} 的功能`
}
</script>
<style scoped>
.os-overview-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.os-layers {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.layer {
padding: 1rem;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.layer.user-apps {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.layer.kernel {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.layer.hardware {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.layer-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 1rem;
}
.layer-content {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.app-icon {
font-size: 1.8rem;
padding: 0.5rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.app-icon:hover,
.app-icon.active {
transform: scale(1.1);
background: rgba(255, 255, 255, 0.2);
}
.kernel-component {
padding: 0.4rem 0.6rem;
border-radius: 4px;
background: rgba(255, 255, 255, 0.15);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.3s;
}
.kernel-component:hover,
.kernel-component.active {
background: rgba(255, 255, 255, 0.3);
font-weight: 600;
}
.hardware-icons {
display: flex;
gap: 1rem;
font-size: 1.2rem;
flex-wrap: wrap;
}
.layer-desc {
font-size: 0.85rem;
margin-top: 0.5rem;
opacity: 0.9;
}
.resource-flow {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
}
.flow-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.flow-content {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.flow-item {
text-align: center;
font-size: 0.85rem;
padding: 0.5rem;
opacity: 0.6;
transition: all 0.3s;
}
.flow-item.active {
opacity: 1;
color: var(--vp-c-brand);
}
.flow-arrow {
font-size: 1.2rem;
}
.flow-btn {
width: 100%;
padding: 0.5rem;
margin-top: 0.75rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.3s;
}
.flow-btn:hover {
background: var(--vp-c-brand-dark);
}
.demo-details {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.detail-title {
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: var(--vp-c-brand);
}
.detail-desc {
font-size: 0.85rem;
color: var(--vP-c-text-2);
line-height: 1.5;
}
</style>
@@ -1,376 +1,259 @@
<template> <template>
<div class="process-demo"> <div class="demo">
<div class="controls-section"> <div class="title"> CPU 在疯狂切换你感觉不出来</div>
<button
class="action-btn" <div class="cpu-core">
:class="{ active: isRunning }" <div class="cpu-label">CPU</div>
@click="toggleSimulation" <div class="current-task" :class="{ switching: isSwitching }">
> <span class="task-icon">{{ currentTask.icon }}</span>
{{ isRunning ? '⏸ 暂停时间片轮转' : '▶️ 启动 CPU' }} <span class="task-name">{{ currentTask.name }}</span>
</button>
<div class="speed-control">
<label>时间流速:</label>
<button :class="{ active: speed === 'slow' }" @click="setSpeed('slow')">
极慢动作
</button>
<button :class="{ active: speed === 'fast' }" @click="setSpeed('fast')">
真实速度
</button>
</div> </div>
<div class="time-slice">时间片: {{ timeLeft }}ms</div>
</div> </div>
<div class="cpu-container"> <div class="process-queue">
<div class="cpu-core" :class="{ active: isRunning }">
<div class="cpu-title">单核 CPU</div>
<div class="current-task">
<span v-if="activeProcess" class="task-badge">
正在处理: {{ activeProcess.icon }} {{ activeProcess.name }}
</span>
<span v-else class="task-badge idle"> 空闲中... </span>
</div>
</div>
<!-- 连接线动画 -->
<div class="connector">
<div
class="data-flow"
:class="[`flow-${activeProcessId}`, { running: isRunning }]"
></div>
</div>
</div>
<div class="processes-grid">
<div <div
v-for="p in processes" v-for="(proc, idx) in processes"
:key="p.id" :key="proc.id"
class="process-card" class="process"
:class="{ active: p.id === activeProcessId }" :class="{
active: idx === currentIdx,
waiting: idx !== currentIdx,
done: proc.progress >= 100
}"
:style="{ '--progress': proc.progress + '%' }"
> >
<div class="p-header"> <span class="p-icon">{{ proc.icon }}</span>
<div class="p-title"> <div class="p-info">
<span class="icon">{{ p.icon }}</span> <span class="p-name">{{ proc.name }}</span>
<span class="name">{{ p.name }}</span> <div class="p-bar">
<div class="p-fill"></div>
</div> </div>
<span
class="status-badge"
:class="p.id === activeProcessId ? 'running' : 'waiting'"
>
{{ p.id === activeProcessId ? '独占 CPU' : '排队等待' }}
</span>
</div>
<div class="p-progress">
<div class="progress-track">
<div
class="progress-fill"
:style="{ width: p.progress + '%' }"
></div>
</div>
<div class="progress-text">{{ Math.floor(p.progress) }}% 完成</div>
</div> </div>
<span class="p-status">{{ idx === currentIdx ? '运行中' : (proc.progress >= 100 ? '完成' : '等待') }}</span>
</div> </div>
</div> </div>
<div <div class="explain">
class="explanation-box" <strong>💡 原理</strong>CPU {{ sliceTime }}ms 切换一次进程因为太快了你感觉是"同时运行"实际上每个进程都在断断续续地执行
:class="{ show: isRunning && speed === 'fast' }"
>
💡
**关键启示**当切换速度足够快时肉眼已经无法分辨谁在等待这也就是为什么只有一个
CPU 核心的电脑依然能让你一边听歌一边打字
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
const isRunning = ref(false)
const activeProcessId = ref(null)
const speed = ref('slow')
let interval = null
const processes = ref([ const processes = ref([
{ id: 1, name: '微信接收', icon: '💬', progress: 0 }, { id: 1, name: '微信', icon: '💬', progress: 0 },
{ id: 2, name: '音乐播放', icon: '🎵', progress: 0 }, { id: 2, name: '音乐', icon: '🎵', progress: 0 },
{ id: 3, name: '游戏渲染', icon: '🎮', progress: 0 } { id: 3, name: '浏览器', icon: '🌐', progress: 0 }
]) ])
const activeProcess = computed(() => const currentIdx = ref(0)
processes.value.find((p) => p.id === activeProcessId.value) const timeLeft = ref(0)
) const isSwitching = ref(false)
const sliceTime = 100 // 100ms10ms
const setSpeed = (s) => { let timer = null
speed.value = s let switchTimer = null
if (isRunning.value) {
clearInterval(interval) const switchTask = () => {
startLoop() isSwitching.value = true
setTimeout(() => {
currentIdx.value = (currentIdx.value + 1) % processes.value.length
timeLeft.value = sliceTime
isSwitching.value = false
}, 200)
}
const tick = () => {
const current = processes.value[currentIdx.value]
//
if (current.progress < 100) {
current.progress = Math.min(100, current.progress + 5)
}
//
timeLeft.value -= 10
//
if (timeLeft.value <= 0) {
switchTask()
}
//
if (processes.value.every(p => p.progress >= 100)) {
//
setTimeout(() => {
processes.value.forEach(p => p.progress = 0)
currentIdx.value = 0
timeLeft.value = sliceTime
}, 2000)
} }
} }
const startLoop = () => { onMounted(() => {
const switchTime = speed.value === 'slow' ? 1200 : 80 // 1.2s timeLeft.value = sliceTime
timer = setInterval(tick, 10) // 10ms
if (!activeProcessId.value) { })
activeProcessId.value = 1
}
interval = setInterval(() => {
//
const curr = processes.value.find((p) => p.id === activeProcessId.value)
if (curr) {
curr.progress += speed.value === 'slow' ? 15 : 4
if (curr.progress >= 100) curr.progress = 0
}
//
let nextId = activeProcessId.value + 1
if (nextId > 3) nextId = 1
activeProcessId.value = nextId
}, switchTime)
}
const toggleSimulation = () => {
if (isRunning.value) {
clearInterval(interval)
isRunning.value = false
activeProcessId.value = null
} else {
isRunning.value = true
startLoop()
}
}
onUnmounted(() => { onUnmounted(() => {
if (interval) clearInterval(interval) clearInterval(timer)
clearTimeout(switchTimer)
}) })
const currentTask = computed(() => processes.value[currentIdx.value])
</script> </script>
<style scoped> <style scoped>
.process-demo { .demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.controls-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.action-btn {
background: var(--vp-c-brand-1);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 8px; border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 1rem 0;
}
.title {
font-weight: 600; font-weight: 600;
cursor: pointer; font-size: 14px;
transition: all 0.3s ease; margin-bottom: 12px;
min-width: 160px; text-align: center;
}
.action-btn.active {
background: var(--vp-c-danger-1);
}
.action-btn:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.speed-control {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.speed-control button {
background: transparent;
border: 1px solid var(--vp-c-divider);
padding: 0.3rem 0.8rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.speed-control button.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
font-weight: bold;
}
.cpu-container {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 2rem;
} }
.cpu-core { .cpu-core {
width: 240px; background: linear-gradient(135deg, #667eea22, #764ba222);
height: 90px; border: 2px solid #667eea;
background: var(--vp-c-bg-alt); border-radius: 8px;
border: 2px solid var(--vp-c-divider); padding: 12px;
border-radius: 12px; text-align: center;
display: flex; margin-bottom: 12px;
flex-direction: column;
justify-content: center;
align-items: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
position: relative; position: relative;
} }
.cpu-core.active {
border-color: var(--vp-c-brand-1); .cpu-label {
box-shadow: 0 0 20px var(--vp-c-brand-soft); font-size: 10px;
} color: var(--vp-c-text-3);
.cpu-title { margin-bottom: 4px;
font-weight: 800;
font-size: 1.1rem;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
letter-spacing: 2px;
} }
.current-task { .current-task {
height: 28px;
display: flex; display: flex;
align-items: center; align-items: center;
} justify-content: center;
.task-badge { gap: 8px;
background: var(--vp-c-brand-1); font-size: 18px;
color: white;
padding: 0.2rem 0.8rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600; font-weight: 600;
} transition: all 0.2s;
.task-badge.idle {
background: var(--vp-c-text-3);
} }
/* 连接线动画占位,简化效果,用发亮的虚线替代 */ .current-task.switching {
.connector { opacity: 0.3;
width: 2px; transform: scale(0.9);
height: 30px;
background: var(--vp-c-divider);
margin-top: 5px;
position: relative;
} }
.processes-grid { .task-icon {
display: grid; font-size: 24px;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
} }
@media (max-width: 640px) { .time-slice {
.processes-grid {
grid-template-columns: 1fr;
}
}
.process-card {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.process-card.active {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
box-shadow: 0 6px 16px var(--vp-c-brand-soft);
}
.process-card.active::before {
content: '';
position: absolute; position: absolute;
top: 0; top: 8px;
left: 0; right: 12px;
right: 0; font-size: 10px;
height: 4px; color: var(--vp-c-text-3);
background: var(--vp-c-brand-1); background: var(--vp-c-bg);
} padding: 2px 6px;
.p-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.p-title {
display: flex;
align-items: center;
gap: 0.4rem;
font-weight: 600;
}
.status-badge {
font-size: 0.75rem;
padding: 0.1rem 0.5rem;
border-radius: 4px; border-radius: 4px;
font-weight: bold;
}
.status-badge.waiting {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.status-badge.running {
background: rgba(16, 185, 129, 0.15);
color: var(--vp-c-success-1);
} }
.p-progress { .process-queue {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 8px;
margin-bottom: 12px;
} }
.progress-track { .process {
width: 100%; display: flex;
height: 8px; align-items: center;
gap: 10px;
padding: 8px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
transition: all 0.3s;
}
.process.active {
border-color: #667eea;
background: #667eea11;
box-shadow: 0 0 10px #667eea33;
}
.process.done {
opacity: 0.6;
}
.process.done .p-fill {
background: #10b981;
}
.p-icon {
font-size: 20px;
width: 24px;
text-align: center;
}
.p-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.p-name {
font-size: 12px;
font-weight: 600;
}
.p-bar {
height: 4px;
background: var(--vp-c-bg-soft); background: var(--vp-c-bg-soft);
border-radius: 4px; border-radius: 2px;
overflow: hidden; overflow: hidden;
} }
.progress-fill {
.p-fill {
height: 100%; height: 100%;
background: var(--vp-c-brand-1); width: var(--progress);
background: #667eea;
border-radius: 2px;
transition: width 0.1s linear; transition: width 0.1s linear;
} }
.process-card.active .progress-fill {
background: var(--vp-c-success-1); .p-status {
font-size: 10px;
color: var(--vp-c-text-3);
padding: 2px 6px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
} }
.progress-text { .process.active .p-status {
font-size: 0.75rem; color: #667eea;
background: #667eea22;
}
.explain {
font-size: 12px;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
text-align: right; line-height: 1.5;
font-variant-numeric: tabular-nums; padding: 10px;
background: var(--vp-c-bg);
border-radius: 6px;
} }
.explanation-box { .explain strong { color: var(--vp-c-text-1); }
margin-top: 1.5rem;
padding: 1rem;
background: rgba(16, 185, 129, 0.1);
border-left: 4px solid var(--vp-c-success-1);
border-radius: 0 8px 8px 0;
font-size: 0.95rem;
opacity: 0;
transform: translateY(10px);
transition: all 0.5s ease;
}
.explanation-box.show {
opacity: 1;
transform: translateY(0);
}
</style> </style>
@@ -1,446 +0,0 @@
<template>
<div class="pmf-collab-demo">
<div class="demo-header">
<span class="title">进程内存文件系统的协作</span>
<span class="subtitle">三大管理模块如何协同工作</span>
</div>
<div class="demo-content">
<div class="collab-scene">
<div class="scene-title">场景选择</div>
<div class="scene-buttons">
<button
v-for="scene in scenes"
:key="scene.id"
:class="['scene-btn', { active: activeScene === scene.id }]"
@click="activeScene = scene.id"
>
{{ scene.icon }} {{ scene.name }}
</button>
</div>
</div>
<div class="collab-visualization">
<div class="vis-container">
<!-- 进程区域 -->
<div class="zone process-zone">
<div class="zone-header">
<span class="zone-icon">🔄</span>
<span class="zone-name">进程管理</span>
</div>
<div class="zone-content">
<div class="process-list">
<div
v-for="proc in processes"
:key="proc.id"
class="process-item"
:class="{ active: proc.id === currentProcessId }"
>
<span class="proc-name">{{ proc.name }}</span>
<span class="proc-pid">PID: {{ proc.pid }}</span>
<span class="proc-state">{{ proc.state }}</span>
</div>
</div>
</div>
</div>
<!-- 内存区域 -->
<div class="zone memory-zone">
<div class="zone-header">
<span class="zone-icon">🧠</span>
<span class="zone-name">内存管理</span>
</div>
<div class="zone-content">
<div class="memory-grid">
<div
v-for="(block, index) in memoryBlocks"
:key="index"
class="memory-block"
:class="{
allocated: block.allocated,
process: block.processId
}"
:title="`地址: ${block.address}, 大小: ${block.size}KB`"
>
<div v-if="block.allocated" class="block-info">
{{ getProcessName(block.processId) }}
</div>
</div>
</div>
</div>
</div>
<!-- 文件系统区域 -->
<div class="zone filesystem-zone">
<div class="zone-header">
<span class="zone-icon">📁</span>
<span class="zone-name">文件系统</span>
</div>
<div class="zone-content">
<div class="file-tree">
<div
v-for="file in files"
:key="file.id"
class="file-item"
:class="{ active: file.id === currentFileId }"
>
<span class="file-icon">{{ getIcon(file.type) }}</span>
<span class="file-name">{{ file.name }}</span>
<span v-if="file.size" class="file-size">{{
file.size
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="explanation">
<div class="exp-title">
{{ currentSceneData.title }}
</div>
<div class="exp-content">
<div
v-for="(step, index) in currentSceneData.steps"
:key="index"
class="exp-step"
>
<span class="step-number">{{ index + 1 }}</span>
<span class="step-text">{{ step }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeScene = ref('launch')
const currentProcessId = ref(null)
const currentFileId = ref(null)
const scenes = [
{
id: 'launch',
name: '启动程序',
icon: '🚀'
},
{
id: 'memory-access',
name: '内存访问',
icon: '💾'
},
{
id: 'file-access',
name: '文件读写',
icon: '📄'
},
{
id: 'context-switch',
name: '进程切换',
icon: '🔄'
}
]
const processes = ref([
{ id: 1, name: '浏览器', pid: 1001, state: '运行中' },
{ id: 2, name: '音乐播放器', pid: 1002, state: '等待中' },
{ id: 3, name: '代码编辑器', pid: 1003, state: '运行中' }
])
const memoryBlocks = ref([
{ address: '0x1000', size: 256, allocated: true, processId: 1 },
{ address: '0x2000', size: 128, allocated: true, processId: 2 },
{ address: '0x3000', size: 512, allocated: true, processId: 3 },
{ address: '0x4000', size: 1024, allocated: false, processId: null },
{ address: '0x5000', size: 512, allocated: false, processId: null },
{ address: '0x6000', size: 256, allocated: false, processId: null },
{ address: '0x7000', size: 128, allocated: false, processId: null }
])
const files = ref([
{ id: 1, name: 'config.json', type: 'json', size: '2KB' },
{ id: 2, name: 'user_data.db', type: 'db', size: '50MB' },
{ id: 3, name: 'cache', type: 'folder', size: '' },
{ id: 4, name: 'song.mp3', type: 'audio', size: '5MB' }
])
const sceneData = {
launch: {
title: '场景1:启动程序(浏览器)',
steps: [
'1. 用户双击浏览器图标',
'2. 进程管理创建新进程(PID: 1004',
'3. 内存管理分配内存空间(代码段、数据段、堆、栈)',
'4. 文件系统读取配置文件和缓存数据'
]
},
'memory-access': {
title: '场景2:程序运行时申请内存',
steps: [
'1. 浏览器加载大图片,需要更多内存',
'2. 进程通过系统调用请求内存(malloc)',
'3. 内存管理查找可用内存块(如:0x4000)',
'4. 将内存块标记为"已分配",返回地址给程序'
]
},
'file-access': {
title: '场景3:保存文件',
steps: [
'1. 用户在浏览器点击"保存图片"',
'2. 进程发起文件写入系统调用',
'3. 文件系统查找空闲磁盘空间',
'4. 将数据写入磁盘,更新文件分配表'
]
},
'context-switch': {
title: '场景4:切换到音乐播放器',
steps: [
'1. 用户点击音乐播放器窗口',
'2. 操作系统暂停浏览器进程',
'3. 调度器加载音乐播放器进程上下文',
'4. CPU开始执行音乐播放器代码'
]
}
}
const currentSceneData = computed(
() => sceneData[activeScene.value] || sceneData.launch
)
const getProcessName = (id) => {
const proc = processes.value.find((p) => p.id === id)
return proc?.name || '系统'
}
const getIcon = (type) => {
const icons = {
json: '📋',
db: '🗄️',
folder: '📁',
audio: '🎵'
}
return icons[type] || '📄'
}
</script>
<style scoped>
.pmf-collab-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.scene-buttons {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.scene-btn {
padding: 0.6rem 1rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
}
.scene-btn:hover {
border-color: var(--vp-c-brand);
}
.scene-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.collab-visualization {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
@media (max-width: 768px) {
.collab-visualization {
grid-template-columns: 1fr;
}
}
.zone {
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
}
.zone-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
font-size: 0.9rem;
}
.zone-icon {
font-size: 1.2rem;
}
.zone-name {
font-size: 0.85rem;
}
.process-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.process-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
}
.process-item.active {
border: 2px solid var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.proc-name,
.proc-pid,
.proc-state {
flex: 1;
}
.proc-state {
color: var(--vp-c-text-2);
font-size: 0.75rem;
}
.memory-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.memory-block {
aspect-ratio: 2;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
position: relative;
background: var(--vp-c-bg-soft);
transition: all 0.3s;
}
.memory-block.allocated {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand);
}
.memory-block.process {
border: 2px solid var(--vp-c-brand);
}
.block-info {
font-weight: 600;
color: var(--vp-c-brand);
font-size: 0.7rem;
text-align: center;
}
.file-tree {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.file-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
}
.file-item.active {
border: 2px solid var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.file-name {
flex: 1;
}
.file-size {
color: var(--vp-c-text-2);
font-size: 0.7rem;
}
.explanation {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.exp-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.95rem;
color: var(--vp-c-brand);
}
.exp-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.exp-step {
display: flex;
gap: 0.5rem;
font-size: 0.85rem;
line-height: 1.5;
}
.step-number {
color: var(--vp-c-brand);
font-weight: 600;
flex-shrink: 0;
}
</style>
@@ -0,0 +1,301 @@
<template>
<div class="demo">
<div class="title">🚀 双击图标后电脑在忙什么</div>
<div class="timeline">
<div
v-for="(step, idx) in steps"
:key="idx"
class="step"
:class="{
done: currentStep > idx,
active: currentStep === idx,
pending: currentStep < idx
}"
>
<div class="step-marker">
<span class="step-num">{{ idx + 1 }}</span>
<span class="step-icon">{{ step.icon }}</span>
</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc" v-if="currentStep === idx">{{ step.desc }}</div>
</div>
<div class="step-arrow" v-if="idx < steps.length - 1"></div>
</div>
</div>
<div class="visualization" v-if="currentStep >= 0">
<div class="viz-box" :class="vizClass">
<div class="viz-icon">{{ currentViz.icon }}</div>
<div class="viz-text">{{ currentViz.text }}</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const steps = [
{
icon: '👆',
title: '你双击图标',
desc: '操作系统收到"启动浏览器"的请求'
},
{
icon: '📋',
title: '创建进程',
desc: '建立"户口本",记录进程ID和状态'
},
{
icon: '🧠',
title: '分配内存',
desc: '划分虚拟内存空间,让程序以为独占内存'
},
{
icon: '📁',
title: '加载文件',
desc: '从硬盘读取程序代码到内存'
},
{
icon: '▶️',
title: '开始运行',
desc: 'CPU开始执行,窗口出现在屏幕上!'
}
]
const vizStates = [
{ icon: '🖱️', text: '点击中...' },
{ icon: '📋', text: '创建进程...' },
{ icon: '💾', text: '分配内存...' },
{ icon: '💿', text: '读取文件...' },
{ icon: '🖥️', text: '运行中!' }
]
const currentStep = ref(0)
let timer = null
const vizClass = computed(() => {
const classes = ['click', 'process', 'memory', 'file', 'run']
return classes[currentStep.value] || ''
})
const currentViz = computed(() => vizStates[currentStep.value] || vizStates[0])
const progressPercent = computed(() => {
return ((currentStep.value + 1) / steps.length) * 100
})
onMounted(() => {
timer = setInterval(() => {
currentStep.value = (currentStep.value + 1) % steps.length
}, 2000)
})
onUnmounted(() => {
clearInterval(timer)
})
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 1rem 0;
}
.title {
font-weight: 600;
font-size: 14px;
margin-bottom: 16px;
text-align: center;
}
.timeline {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
position: relative;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.step-marker {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6px;
position: relative;
transition: all 0.3s;
}
.step-num {
font-size: 10px;
font-weight: 600;
position: absolute;
top: -2px;
right: -2px;
width: 14px;
height: 14px;
background: var(--vp-c-bg);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.step-icon {
font-size: 16px;
}
.step.done .step-marker {
background: #10b981;
color: white;
}
.step.active .step-marker {
background: var(--vp-c-brand);
color: white;
animation: pulse 1s infinite;
transform: scale(1.1);
}
.step.pending .step-marker {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
color: var(--vp-c-text-3);
}
.step-content {
text-align: center;
min-height: 40px;
}
.step-title {
font-size: 10px;
font-weight: 600;
margin-bottom: 2px;
}
.step.done .step-title {
color: #10b981;
}
.step.active .step-title {
color: var(--vp-c-brand);
}
.step.pending .step-title {
color: var(--vp-c-text-3);
}
.step-desc {
font-size: 9px;
color: var(--vp-c-text-2);
line-height: 1.3;
max-width: 80px;
}
.step-arrow {
position: absolute;
right: -10px;
top: 10px;
font-size: 12px;
color: var(--vp-c-divider);
}
.visualization {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
text-align: center;
}
.viz-box {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 32px;
border-radius: 8px;
transition: all 0.3s;
}
.viz-box.click {
background: #667eea22;
border: 2px solid #667eea;
}
.viz-box.process {
background: #f093fb22;
border: 2px solid #f5576c;
}
.viz-box.memory {
background: #4facfe22;
border: 2px solid #4facfe;
}
.viz-box.file {
background: #fa709a22;
border: 2px solid #fa709a;
}
.viz-box.run {
background: #10b98122;
border: 2px solid #10b981;
animation: success 0.5s ease;
}
.viz-icon {
font-size: 32px;
}
.viz-text {
font-size: 12px;
font-weight: 600;
}
.progress-bar {
height: 4px;
background: var(--vp-c-bg);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), #10b981);
border-radius: 2px;
transition: width 0.5s ease;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 var(--vp-c-brand-soft); }
50% { box-shadow: 0 0 0 8px transparent; }
}
@keyframes success {
0% { transform: scale(0.9); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
</style>
@@ -110,8 +110,7 @@ function traverse(folder) {
traverse(item) // traverse(item) //
} }
} }
}</pre }</pre>
>
</div> </div>
</div> </div>
</div> </div>
@@ -30,8 +30,7 @@
<span <span
class="val-box" class="val-box"
:class="{ on: storedData === 1, flash: isWriting }" :class="{ on: storedData === 1, flash: isWriting }"
>{{ storedData }}</span >{{ storedData }}</span>
>
</div> </div>
<!-- Output --> <!-- Output -->
@@ -40,10 +40,10 @@
</div> </div>
</div> </div>
<div class="search-controls"> <div class="search-controls">
<button @click="startLinearSearch" class="search-btn"> <button class="search-btn" @click="startLinearSearch">
开始查找 开始查找
</button> </button>
<button @click="reset" class="reset-btn">重置</button> <button class="reset-btn" @click="reset">重置</button>
</div> </div>
<div class="search-info"> <div class="search-info">
目标数字<input 目标数字<input
@@ -89,8 +89,8 @@
</div> </div>
</div> </div>
<div class="search-controls"> <div class="search-controls">
<button @click="binaryStep" class="search-btn">下一步</button> <button class="search-btn" @click="binaryStep">下一步</button>
<button @click="resetBinary" class="reset-btn">重置</button> <button class="reset-btn" @click="resetBinary">重置</button>
</div> </div>
</div> </div>
<div class="algo-stats"> <div class="algo-stats">
@@ -22,9 +22,9 @@
</div> </div>
<div class="controls"> <div class="controls">
<button @click="generateArray" class="control-btn">生成新数组</button> <button class="control-btn" @click="generateArray">生成新数组</button>
<button @click="startBubbleSort" class="control-btn">冒泡排序</button> <button class="control-btn" @click="startBubbleSort">冒泡排序</button>
<button @click="startQuickSort" class="control-btn">快速排序</button> <button class="control-btn" @click="startQuickSort">快速排序</button>
</div> </div>
<div class="algorithm-info"> <div class="algorithm-info">
@@ -54,8 +54,7 @@
</div> </div>
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong <strong>核心思想</strong>存储遵循"金字塔"原则越快的存储越贵容量越小CPU
>存储遵循"金字塔"原则越快的存储越贵容量越小CPU
需要的数据放在最快的存储寄存器缓存暂时不用的放在慢速大容量存储磁盘云端 需要的数据放在最快的存储寄存器缓存暂时不用的放在慢速大容量存储磁盘云端
</div> </div>
</div> </div>
@@ -114,12 +114,8 @@
</div> </div>
</div> </div>
<div class="legend"> <div class="legend">
<span class="legend-item" <span class="legend-item"><span class="network-box" /> 网络位 ({{ cidr }})</span>
><span class="network-box" /> 网络 ({{ cidr }})</span <span class="legend-item"><span class="host-box" /> 主机 ({{ 32 - cidr }})</span>
>
<span class="legend-item"
><span class="host-box" /> 主机位 ({{ 32 - cidr }})</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -62,8 +62,7 @@
v-for="(use, i) in currentProtocol.useCases" v-for="(use, i) in currentProtocol.useCases"
:key="i" :key="i"
class="use-tag" class="use-tag"
>{{ use }}</span >{{ use }}</span>
>
</div> </div>
</div> </div>
</div> </div>
@@ -44,8 +44,7 @@
:class="{ :class="{
sending: sendingBit === i && activeType === 'serial' sending: sendingBit === i && activeType === 'serial'
}" }"
>{{ bit }}</span >{{ bit }}</span>
>
</div> </div>
</div> </div>
<div class="channels"> <div class="channels">
@@ -57,8 +56,7 @@
:key="i" :key="i"
class="flow-dot" class="flow-dot"
:class="{ active: sendingBit !== null }" :class="{ active: sendingBit !== null }"
></span ></span>
>
</div> </div>
</div> </div>
<div v-else class="channel parallel"> <div v-else class="channel parallel">
@@ -119,8 +117,7 @@
</div> </div>
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong <strong>核心思想</strong>现代高速传输多采用串行方式虽然并行"看起来"更快一次传多位但串行可以跑更高频率抗干扰更强实际速度反而更快
>现代高速传输多采用串行方式虽然并行"看起来"更快一次传多位但串行可以跑更高频率抗干扰更强实际速度反而更快
</div> </div>
</div> </div>
</template> </template>
@@ -64,7 +64,7 @@
<div class="comparison-side tcp-side"> <div class="comparison-side tcp-side">
<div class="side-header">TCP</div> <div class="side-header">TCP</div>
<div class="side-animation"> <div class="side-animation">
<div class="packet" v-for="i in 3" :key="'tcp-' + i"> <div v-for="i in 3" :key="'tcp-' + i" class="packet">
📦 {{ i }} 📦 {{ i }}
</div> </div>
</div> </div>
@@ -76,7 +76,7 @@
<div class="comparison-side udp-side"> <div class="comparison-side udp-side">
<div class="side-header">UDP</div> <div class="side-header">UDP</div>
<div class="side-animation"> <div class="side-animation">
<div class="packet fast" v-for="i in 5" :key="'udp-' + i"> <div v-for="i in 5" :key="'udp-' + i" class="packet fast">
{{ i }} {{ i }}
</div> </div>
</div> </div>
@@ -52,8 +52,7 @@
v-for="t in selectedQuadrant.traits" v-for="t in selectedQuadrant.traits"
:key="t" :key="t"
class="trait-tag" class="trait-tag"
>{{ t }}</span >{{ t }}</span>
>
</div> </div>
</div> </div>
</div> </div>
@@ -111,18 +110,10 @@
</div> </div>
</div> </div>
<div class="convert-summary"> <div class="convert-summary">
<span v-if="activeLang === 'JavaScript'" class="summary-tag weak" <span v-if="activeLang === 'JavaScript'" class="summary-tag weak">弱类型隐式转换结果常出人意料</span>
>类型隐式转换结果常出人意料</span <span v-else-if="activeLang === 'Python'" class="summary-tag strong">类型拒绝隐式转换必须显式指定</span>
> <span v-else-if="activeLang === 'Java'" class="summary-tag strong">强类型字符串拼接是特例其余严格</span>
<span v-else-if="activeLang === 'Python'" class="summary-tag strong" <span v-else class="summary-tag strong">强类型类型不匹配就报错零容忍</span>
>强类型拒绝隐式转换必须显式指定</span
>
<span v-else-if="activeLang === 'Java'" class="summary-tag strong"
>强类型字符串拼接是特例其余严格</span
>
<span v-else class="summary-tag strong"
>强类型类型不匹配就报错零容忍</span
>
</div> </div>
</div> </div>
@@ -146,7 +137,7 @@
</div> </div>
</div> </div>
<div class="infer-benefit"> <div class="infer-benefit">
<span class="benefit-item" v-for="b in inferBenefits" :key="b">{{ <span v-for="b in inferBenefits" :key="b" class="benefit-item">{{
b b
}}</span> }}</span>
</div> </div>
@@ -155,19 +146,11 @@
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong> <strong>核心思想</strong>
<span v-if="activeTab === 'quadrant'" <span v-if="activeTab === 'quadrant'">类型系统在两个维度上做选择何时检查静态/动态和是否允许隐式转换/没有最好的组合只有最适合的场景</span>
>类型系统在两个维度上做选择何时检查静态/动态和是否允许隐式转换/没有最好的组合只有最适合的场景</span <span v-else-if="activeTab === 'check'">静态类型在编译时就能发现错误动态类型要到运行时才知道越早发现
> bug修复成本越低</span>
<span v-else-if="activeTab === 'check'" <span v-else-if="activeTab === 'convert'">弱类型语言会""你的意思做隐式转换常出错强类型语言要求你明确表达意图更安全</span>
>静态类型在编译时就能发现错误动态类型要到运行时才知道越早发现 <span v-else>类型推断让你两全其美代码像动态语言一样简洁编译器像静态语言一样严格检查</span>
bug修复成本越低</span
>
<span v-else-if="activeTab === 'convert'"
>弱类型语言会"猜"你的意思做隐式转换常出错强类型语言要求你明确表达意图更安全</span
>
<span v-else
>类型推断让你两全其美代码像动态语言一样简洁编译器像静态语言一样严格检查</span
>
</div> </div>
</div> </div>
</template> </template>
@@ -9,8 +9,8 @@
<div class="slider-group"> <div class="slider-group">
<label>采样频率{{ sampleRate }} /</label> <label>采样频率{{ sampleRate }} /</label>
<input <input
type="range"
v-model="sliderValue" v-model="sliderValue"
type="range"
min="1" min="1"
max="50" max="50"
step="1" step="1"
@@ -31,7 +31,7 @@
</span> </span>
</div> </div>
<div class="inspection-box" v-if="hoveredPixel"> <div v-if="hoveredPixel" class="inspection-box">
<div class="preview-color" :style="{ backgroundColor: hoveredPixel.color }"></div> <div class="preview-color" :style="{ backgroundColor: hoveredPixel.color }"></div>
<div class="preview-info"> <div class="preview-info">
<div class="info-row"> <div class="info-row">
@@ -44,7 +44,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="inspection-box empty" v-else> <div v-else class="inspection-box empty">
将鼠标悬停在左侧画布的方块上 将鼠标悬停在左侧画布的方块上
</div> </div>
</div> </div>
File diff suppressed because it is too large Load Diff
@@ -15,7 +15,7 @@
</div> </div>
</div> </div>
<div class="detail-panel" v-if="currentLayer"> <div v-if="currentLayer" class="detail-panel">
<div class="detail-header"> <div class="detail-header">
<span class="detail-icon">{{ currentLayer.icon }}</span> <span class="detail-icon">{{ currentLayer.icon }}</span>
<span class="detail-name">{{ currentLayer.name }}</span> <span class="detail-name">{{ currentLayer.name }}</span>
@@ -38,13 +38,13 @@
</div> </div>
<div class="traffic-controls"> <div class="traffic-controls">
<button @click="allocateUser" class="btn-primary"> <button class="btn-primary" @click="allocateUser">
👤 分配1个用户 👤 分配1个用户
</button> </button>
<button @click="allocateBatch" class="btn-secondary"> <button class="btn-secondary" @click="allocateBatch">
👥 分配100个用户 👥 分配100个用户
</button> </button>
<button @click="resetTraffic" class="btn-tertiary">🔄 重置</button> <button class="btn-tertiary" @click="resetTraffic">🔄 重置</button>
</div> </div>
<div class="traffic-stats"> <div class="traffic-stats">
@@ -64,9 +64,7 @@
<div class="tips"> <div class="tips">
<span class="tips-icon">💡</span> <span class="tips-icon">💡</span>
<span class="tips-text" <span class="tips-text">50/50分配能最快检测出差异确保两组样本量足够大以获得统计显著性</span>
>50/50分配能最快检测出差异确保两组样本量足够大以获得统计显著性</span
>
</div> </div>
</div> </div>
@@ -260,7 +258,7 @@
</div> </div>
</div> </div>
<button @click="calculateSampleSize" class="btn-primary btn-calc"> <button class="btn-primary btn-calc" @click="calculateSampleSize">
🧮 计算所需样本量 🧮 计算所需样本量
</button> </button>
@@ -296,9 +294,7 @@
<div class="tips"> <div class="tips">
<span class="tips-icon">💡</span> <span class="tips-icon">💡</span>
<span class="tips-text" <span class="tips-text">提升目标越小所需样本量越大5%的提升比20%的提升需要更多样本</span>
>提升目标越小所需样本量越大5%的提升比20%的提升需要更多样本</span
>
</div> </div>
</div> </div>
@@ -175,19 +175,11 @@
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong> <strong>核心思想</strong>
<span v-if="activeMode === 'playground'" <span v-if="activeMode === 'playground'">正则表达式是一种用特殊符号描述文本模式的语言在搜索替换数据验证中无处不在</span>
>正则表达式是一种用特殊符号描述文本模式的语言在搜索替换数据验证中无处不在</span <span v-else-if="activeMode === 'cheatsheet'">记住几个核心符号. * + ? \d \w [] ()就能覆盖 80%
> 的使用场景点击任意符号可直接试验</span>
<span v-else-if="activeMode === 'cheatsheet'" <span v-else-if="activeMode === 'patterns'">不需要自己从零写正则常见场景邮箱手机号URL都有成熟的模式可以直接复用</span>
>记住几个核心符号. * + ? \d \w [] ()就能覆盖 80% <span v-else>正则引擎从左到右逐字符匹配遇到量词会"贪婪"地尽量多匹配失败时"回溯"尝试其他路径</span>
的使用场景点击任意符号可直接试验</span
>
<span v-else-if="activeMode === 'patterns'"
>不需要自己从零写正则常见场景邮箱手机号URL都有成熟的模式可以直接复用</span
>
<span v-else
>正则引擎从左到右逐字符匹配遇到量词会"贪婪"地尽量多匹配失败时"回溯"尝试其他路径</span
>
</div> </div>
</div> </div>
</template> </template>
@@ -2,9 +2,7 @@
<div class="ssh-auth-demo"> <div class="ssh-auth-demo">
<div class="demo-header"> <div class="demo-header">
<span class="title">SSH 密钥认证你的数字身份证</span> <span class="title">SSH 密钥认证你的数字身份证</span>
<span class="subtitle" <span class="subtitle">对称加密 vs 非对称加密 · 密钥对生成 · 认证流程</span>
>对称加密 vs 非对称加密 · 密钥对生成 · 认证流程</span
>
</div> </div>
<div class="control-panel"> <div class="control-panel">
@@ -28,7 +26,7 @@
<div class="card-icon">🔑</div> <div class="card-icon">🔑</div>
<div class="card-title">密码登录</div> <div class="card-title">密码登录</div>
<div class="card-flow"> <div class="card-flow">
<div class="flow-step" v-for="(step, i) in passwordFlow" :key="i"> <div v-for="(step, i) in passwordFlow" :key="i" class="flow-step">
<span class="step-num">{{ i + 1 }}</span> <span class="step-num">{{ i + 1 }}</span>
<span class="step-text">{{ step }}</span> <span class="step-text">{{ step }}</span>
</div> </div>
@@ -43,7 +41,7 @@
<div class="card-icon">🔐</div> <div class="card-icon">🔐</div>
<div class="card-title">密钥登录</div> <div class="card-title">密钥登录</div>
<div class="card-flow"> <div class="card-flow">
<div class="flow-step" v-for="(step, i) in keyFlow" :key="i"> <div v-for="(step, i) in keyFlow" :key="i" class="flow-step">
<span class="step-num">{{ i + 1 }}</span> <span class="step-num">{{ i + 1 }}</span>
<span class="step-text">{{ step }}</span> <span class="step-text">{{ step }}</span>
</div> </div>
@@ -144,18 +142,14 @@
</div> </div>
<div :class="['msg', { active: authStep >= 2 }]" class="msg-left"> <div :class="['msg', { active: authStep >= 2 }]" class="msg-left">
<span class="msg-label"> 发送随机挑战</span> <span class="msg-label"> 发送随机挑战</span>
<span class="msg-detail" <span class="msg-detail">"请证明你有私钥:用它签名这段随机数据"</span>
>"请证明你有私钥:用它签名这段随机数据"</span
>
</div> </div>
<div <div
:class="['msg', { active: authStep >= 3 }]" :class="['msg', { active: authStep >= 3 }]"
class="msg-right" class="msg-right"
> >
<span class="msg-label"> 返回签名</span> <span class="msg-label"> 返回签名</span>
<span class="msg-detail" <span class="msg-detail">"用私钥签名后的结果(私钥本身不发送)"</span>
>"用私钥签名后的结果(私钥本身不发送)"</span
>
</div> </div>
<div :class="['msg', { active: authStep >= 4 }]" class="msg-left"> <div :class="['msg', { active: authStep >= 4 }]" class="msg-left">
<span class="msg-label"> 用公钥验证</span> <span class="msg-label"> 用公钥验证</span>
@@ -163,9 +157,7 @@
</div> </div>
<div :class="['msg', 'msg-result', { active: authStep >= 5 }]"> <div :class="['msg', 'msg-result', { active: authStep >= 5 }]">
<span class="msg-label"> 认证成功</span> <span class="msg-label"> 认证成功</span>
<span class="msg-detail" <span class="msg-detail">"欢迎登录!从始至终,私钥没离开过你的电脑"</span>
>"欢迎登录!从始至终,私钥没离开过你的电脑"</span
>
</div> </div>
</div> </div>
@@ -211,21 +203,13 @@ Host github.com
<div class="info-box"> <div class="info-box">
<strong>核心思想</strong> <strong>核心思想</strong>
<span v-if="activeScenario === 'compare'" <span v-if="activeScenario === 'compare'">SSH
>SSH 密钥登录比密码更安全因为私钥从不在网络上传输无法被中间人窃取</span>
密钥登录比密码更安全因为私钥从不在网络上传输无法被中间人窃取</span <span v-else-if="activeScenario === 'keygen'">一次 ssh-keygen
> 生成一对密钥私钥自己保管公钥放到目标服务器或平台</span>
<span v-else-if="activeScenario === 'keygen'" <span v-else-if="activeScenario === 'auth'">认证过程基于"挑战-响应"机制服务器出题你的私钥签名作答公钥验证答案全程私钥不离开本机</span>
>一次 ssh-keygen <span v-else>SSH 密钥不仅用于服务器登录也是 Git (GitHub/GitLab)
生成一对密钥私钥自己保管公钥放到目标服务器或平台</span 等开发工具的标准身份认证方式</span>
>
<span v-else-if="activeScenario === 'auth'"
>认证过程基于"挑战-响应"机制服务器出题你的私钥签名作答公钥验证答案全程私钥不离开本机</span
>
<span v-else
>SSH 密钥不仅用于服务器登录也是 Git (GitHub/GitLab)
等开发工具的标准身份认证方式</span
>
</div> </div>
</div> </div>
</template> </template>
@@ -48,7 +48,7 @@
</span> </span>
</div> </div>
<div class="step-list"> <div class="step-list">
<div class="step-item" v-for="i in Math.min(selectedCount, 4)" :key="i"> <div v-for="i in Math.min(selectedCount, 4)" :key="i" class="step-item">
<span class="step-num">{{ i }}</span> <span class="step-num">{{ i }}</span>
<span class="step-text">修改 布局 绘制</span> <span class="step-text">修改 布局 绘制</span>
</div> </div>
@@ -38,7 +38,7 @@
@mouseenter="highlightedTag = node.tag" @mouseenter="highlightedTag = node.tag"
@mouseleave="highlightedTag = ''" @mouseleave="highlightedTag = ''"
> >
<span class="connector" v-if="node.depth > 0"></span> <span v-if="node.depth > 0" class="connector"></span>
<span class="node-tag">{{ node.label }}</span> <span class="node-tag">{{ node.label }}</span>
<span v-if="node.text" class="node-text">"{{ node.text }}"</span> <span v-if="node.text" class="node-text">"{{ node.text }}"</span>
</div> </div>
@@ -143,11 +143,11 @@
<button class="action-btn outline" @click="reset">重置</button> <button class="action-btn outline" @click="reset">重置</button>
</div> </div>
<div class="info-box" v-if="mode === 'native'"> <div v-if="mode === 'native'" class="info-box">
<strong>为什么不自动</strong> <strong>为什么不自动</strong>
<span>JavaScript 的变量是"无感知"你执行 <code>count = 4</code> JavaScript 引擎只是把内存中 count 的值从 3 改成 4仅此而已它不会通知任何人不会触发任何回调不会去检查页面上哪里显示了 count所以界面不会有任何变化除非你自己写代码去更新 DOM</span> <span>JavaScript 的变量是"无感知"你执行 <code>count = 4</code> JavaScript 引擎只是把内存中 count 的值从 3 改成 4仅此而已它不会通知任何人不会触发任何回调不会去检查页面上哪里显示了 count所以界面不会有任何变化除非你自己写代码去更新 DOM</span>
</div> </div>
<div class="info-box" v-else> <div v-else class="info-box">
<strong>框架怎么做到的</strong> <strong>框架怎么做到的</strong>
<span>框架把你的数据用特殊机制包裹起来 Vue 为例它用 JavaScript Proxy代理功能拦截你对变量的赋值操作当你写 <code>count = 4</code> Proxy 会在赋值的同时自动执行一段"通知"代码告诉框架"count 变了"框架再去找到所有用到 count DOM 节点并更新它们整个过程你不需要写任何额外代码</span> <span>框架把你的数据用特殊机制包裹起来 Vue 为例它用 JavaScript Proxy代理功能拦截你对变量的赋值操作当你写 <code>count = 4</code> Proxy 会在赋值的同时自动执行一段"通知"代码告诉框架"count 变了"框架再去找到所有用到 count DOM 节点并更新它们整个过程你不需要写任何额外代码</span>
</div> </div>
@@ -40,7 +40,7 @@
<div class="area-header"> <div class="area-header">
<span class="area-icon">📝</span> <span class="area-icon">📝</span>
<span class="area-title">工作区</span> <span class="area-title">工作区</span>
<span class="area-desc">Working Directory<br/>你正在改的文件</span> <span class="area-desc">Working Directory<br />你正在改的文件</span>
</div> </div>
<div class="area-body"> <div class="area-body">
<div class="area-label">Changes not staged for commit:</div> <div class="area-label">Changes not staged for commit:</div>
@@ -65,7 +65,7 @@
<div class="area-header"> <div class="area-header">
<span class="area-icon">📦</span> <span class="area-icon">📦</span>
<span class="area-title">暂存区</span> <span class="area-title">暂存区</span>
<span class="area-desc">Staging Area<br/>准备这次提交的文件</span> <span class="area-desc">Staging Area<br />准备这次提交的文件</span>
</div> </div>
<div class="area-body"> <div class="area-body">
<div class="area-label">Changes to be committed:</div> <div class="area-label">Changes to be committed:</div>
@@ -90,7 +90,7 @@
<div class="area-header"> <div class="area-header">
<span class="area-icon">🗄</span> <span class="area-icon">🗄</span>
<span class="area-title">仓库</span> <span class="area-title">仓库</span>
<span class="area-desc">Repository (.git)<br/>永久保存的版本</span> <span class="area-desc">Repository (.git)<br />永久保存的版本</span>
</div> </div>
<div class="area-body"> <div class="area-body">
<div class="area-label">已提交记录 (git log):</div> <div class="area-label">已提交记录 (git log):</div>
@@ -105,7 +105,8 @@ function reset() {
<div :class="['panel browser-panel', { <div :class="['panel browser-panel', {
highlight: steps[currentStep].highlight === 'browser' || steps[currentStep].highlight === 'page' || steps[currentStep].highlight === 'hmr' highlight: steps[currentStep].highlight === 'browser' || steps[currentStep].highlight === 'page' || steps[currentStep].highlight === 'hmr'
}]"> }]"
>
<div class="panel-header"> <div class="panel-header">
<span class="dot red" /><span class="dot yellow" /><span class="dot green" /> <span class="dot red" /><span class="dot yellow" /><span class="dot green" />
<span class="panel-title">浏览器</span> <span class="panel-title">浏览器</span>
File diff suppressed because it is too large Load Diff
@@ -1,700 +1,51 @@
<!--
DnsLookupDemo.vue
DNS查询演示 - 增强技术细节版
设计理念
1. 循循善诱通过"接力跑腿"的比喻展示浏览器如何一步步找到IP
2. 技术硬核新增终端模拟器展示真实的 dig/系统命令输出解决"太抽象"的问题
3. 紧凑布局横向流式布局固定底部详情板
-->
<template> <template>
<div class="dns-compact"> <div class="dns-lookup-demo">
<!-- 顶部控制栏 --> <div class="flow">
<div class="top-bar"> <span class="domain">google.com</span>
<div class="title-section"> <span class="arrow"></span>
<span class="app-icon">🌐</span> <span class="dns">DNS</span>
<span class="app-title">DNS 寻址原理</span> <span class="arrow"></span>
</div> <span class="ip">142.250.80.46</span>
<div class="target-select">
<span class="label">目标</span>
<select
v-model="selectedTargetIndex"
:disabled="isSearching"
@change="reset"
>
<option
v-for="(t, i) in targets"
:key="t.name"
:value="i"
>
{{ t.name }} ({{ t.domain }})
</option>
</select>
</div>
<div class="actions">
<button
v-if="!isSearching && !isFinished"
class="action-btn primary"
@click="startAutoSearch"
>
开始寻址
</button>
<button
v-if="isSearching && !autoPlay"
class="action-btn secondary"
@click="nextStep"
>
下一步
</button>
<button
v-if="isFinished || isSearching"
class="action-btn outline"
@click="reset"
>
重置
</button>
</div>
</div>
<!-- 进度条/状态展示 -->
<div class="status-bar">
<div
v-if="!isSearching && !isFinished"
class="status-text"
>
<span class="icon">👋</span>
准备出发去问问 <strong>{{ targets[selectedTargetIndex].domain }}</strong> IP 是多少
</div>
<div
v-else-if="isSearching"
class="status-text running"
>
<span class="icon spin"></span>
正在询问{{ queryLevels[currentStep]?.analogyName }}...
</div>
<div
v-else
class="status-text success"
>
<span class="icon"></span>
找到了IP 地址是<strong>{{ targets[selectedTargetIndex].ip }}</strong>
</div>
</div>
<!-- 可视化流程 (横向) -->
<div class="flow-stage">
<div
v-for="(level, index) in queryLevels"
:key="level.id"
class="flow-step"
:class="{
active: currentStep === index,
passed: currentStep > index,
pending: currentStep < index
}"
@click="jumpToStep(index)"
>
<div
class="step-icon-box"
:style="{ '--step-color': level.color }"
>
<span class="step-icon">{{ level.analogyIcon }}</span>
</div>
<div class="step-label">
{{ level.analogyName }}
</div>
<!-- 连接线 -->
<div
v-if="index < queryLevels.length - 1"
class="step-line"
/>
</div>
</div>
<!-- 底部双面板左侧生活比喻右侧技术终端 -->
<div class="info-panels">
<!-- 左侧生活场景 -->
<div class="detail-panel analogy-panel">
<transition
name="fade"
mode="out-in"
>
<div
v-if="currentStep >= 0"
:key="currentStep"
class="panel-content"
>
<div
class="panel-header"
:style="{ color: currentLevel.color }"
>
<span class="header-icon">{{ currentLevel.analogyIcon }}</span>
<span class="header-title">{{ currentLevel.analogyName }} ({{ currentLevel.techName }})</span>
</div>
<div class="panel-body">
<p class="analogy-text">
{{ currentLevel.analogyAction }}
</p>
<div class="tech-hint-badge">
{{ currentLevel.techAction }}
</div>
</div>
</div>
<div
v-else
class="panel-placeholder"
>
<span>生活场景视角</span>
</div>
</transition>
</div>
<!-- 右侧硬核终端 -->
<div class="detail-panel terminal-panel">
<div class="terminal-header">
<div class="terminal-dots">
<span /><span /><span />
</div>
<div class="terminal-title">
Terminal
</div>
</div>
<transition
name="fade"
mode="out-in"
>
<div
v-if="currentStep >= 0"
:key="currentStep"
class="terminal-body"
>
<div class="cmd-line">
<span class="prompt">$</span>
<span class="cmd">{{ formatText(currentLevel.techCommand) }}</span>
</div>
<div class="cmd-output">
<pre>{{ formatText(currentLevel.techOutput) }}</pre>
</div>
</div>
<div
v-else
class="terminal-placeholder"
>
<span>Waiting for command...</span>
</div>
</transition>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup>
import { ref, computed, onUnmounted } 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' }
]
const selectedTargetIndex = ref(0)
const currentStep = ref(-1)
const isSearching = ref(false)
const isFinished = ref(false)
const autoPlay = ref(false)
let timer = null
const queryLevels = [
{
id: 'browser',
analogyName: '通讯录',
analogyIcon: '📒',
analogyAction: '先翻翻自己的通讯录(缓存),看最近有没有记过。',
techIcon: 'Browser',
techName: '浏览器缓存',
techAction: '检查 Browser DNS Cache',
color: '#67c23a',
techCommand: 'chrome://net-internals/#dns',
techOutput: 'Active entries: 0\nCache miss: No entry found for ${domain}',
qa: {
title: '🤔 浏览器能记多久?',
content: [
{
q: '是一直记着吗?',
a: '不是的。通常只有几分钟(比如 chrome 是一分钟)。而且每家浏览器(Chrome, Firefox)的"记性"都不太一样。'
}
]
}
},
{
id: 'os',
analogyName: '记事本',
analogyIcon: '📝',
analogyAction: '问问操作系统(管家),查查系统 hosts 文件或缓存。',
techIcon: 'OS',
techName: '系统缓存/Hosts',
techAction: '检查 OS Cache / hosts',
color: '#409eff',
techCommand: 'cat /etc/hosts',
techOutput: '127.0.0.1 localhost\n::1 localhost\n# No match for ${domain}',
qa: {
title: '🤔 什么是 hosts 文件?',
content: [
{
q: '它有什么用?',
a: '它是你电脑里的"私人通讯录"。你可以在这里手动强行指定 IP(比如开发时把 test.com 指向本机)。黑客有时也改这里来劫持网站。'
}
]
}
},
{
id: 'ldns',
analogyName: '传达室',
analogyIcon: '💁',
analogyAction: '问小区的传达室(本地DNS),让他帮忙去外面问。',
techIcon: 'LDNS',
techName: '本地DNS (递归)',
techAction: '向 ISP DNS 发起递归查询',
color: '#e6a23c',
techCommand: 'dig ${domain} @192.168.1.1',
techOutput: ';; QUESTION SECTION:\n;${domain}. IN A\n\n;; ANSWER SECTION:\n(empty) -> Recursion Desired',
qa: {
title: '🤔 为什么叫"递归"',
content: [
{
q: '通俗点解释?',
a: '就像你问传达室大爷,大爷去帮你跑腿问一圈回来告诉你结果。你只问了一次,大爷跑了好几趟,这就叫递归(帮你跑到底)。'
}
]
}
},
{
id: 'root',
analogyName: '总局',
analogyIcon: '🏛️',
analogyAction: '传达室问全球总局:".com" 归谁管?',
techIcon: 'Root',
techName: '根域名服务器',
techAction: '查询 Root Server',
color: '#f56c6c',
techCommand: 'dig ${domain} @a.root-servers.net +norecurse',
techOutput: ';; AUTHORITY SECTION:\ncom. 172800 IN NS a.gtld-servers.net.\n;; ADDITIONAL SECTION:\na.gtld-servers.net. IN A 192.5.6.30',
qa: {
title: '🤔 全球只有13台吗?',
content: [
{
q: '那样不会被挤爆吗?',
a: '不是13台,是13组!每组都有几百台"分身"(镜像服务器)分布在全球,包括中国也有,所以不用担心断网。'
}
]
}
},
{
id: 'tld',
analogyName: '分局',
analogyIcon: '🏢',
analogyAction: '去问 ".com" 分局:baidu.com 归谁管?',
techIcon: 'TLD',
techName: '顶级域名服务器',
techAction: '查询 TLD Server',
color: '#909399',
techCommand: 'dig ${domain} @a.gtld-servers.net +norecurse',
techOutput: ';; AUTHORITY SECTION:\n${domain}. 172800 IN NS ns1.${domain}.\n;; ADDITIONAL SECTION:\nns1.${domain}. IN A 202.108.22.5',
qa: {
title: '🤔 谁在管理 .com',
content: [
{
q: '所有的后缀都一样吗?',
a: '不一样。.com 归 Verisign 公司管,.cn 归中国互联网络信息中心(CNNIC)管。所以要找不同的"分局"。'
}
]
}
},
{
id: 'auth',
analogyName: '办事处',
analogyIcon: '📍',
analogyAction: '找到目标办事处:请告诉我 www 的 IP。',
techIcon: 'Auth',
techName: '权威DNS服务器',
techAction: '查询 Authoritative Server',
color: '#8e44ad',
techCommand: 'dig ${domain} @ns1.${domain} +norecurse',
techOutput: ';; ANSWER SECTION:\n${domain}. 600 IN A ${ip}\n;; Query time: 24 msec',
qa: {
title: '🤔 为什么叫"权威"',
content: [
{
q: '它说的话最准吗?',
a: '对!因为它就是域名的主人(比如百度自己)管理的服务器,它说的话是一手资料,不像前面的可能只是传话。'
}
]
}
}
]
const currentLevel = computed(() =>
currentStep.value >= 0 ? queryLevels[currentStep.value] : {}
)
//
const formatText = (text) => {
if (!text) return ''
const currentTarget = targets[selectedTargetIndex.value]
return text
.replace(/\${domain}/g, currentTarget.domain)
.replace(/\${ip}/g, currentTarget.ip)
}
const startAutoSearch = () => {
reset()
isSearching.value = true
autoPlay.value = true
nextStep()
}
const nextStep = () => {
if (currentStep.value < queryLevels.length - 1) {
currentStep.value++
if (autoPlay.value) {
timer = setTimeout(nextStep, 2500) //
}
} else {
finish()
}
}
const finish = () => {
isFinished.value = true
isSearching.value = false
autoPlay.value = false
}
const reset = () => {
currentStep.value = -1
isSearching.value = false
isFinished.value = false
autoPlay.value = false
clearTimeout(timer)
}
const jumpToStep = (index) => {
if (isSearching.value && autoPlay.value) return
currentStep.value = index
isSearching.value = true
isFinished.value = false
}
onUnmounted(() => {
clearTimeout(timer)
})
</script>
<style scoped> <style scoped>
.dns-compact { .dns-lookup-demo {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 6px;
background: var(--vp-c-bg); background: var(--vp-c-bg-soft);
padding: 16px; padding: 0.6rem 0.8rem;
margin: 16px 0; margin: 0.75rem 0;
font-size: 14px; font-family: var(--vp-font-family-mono);
} }
.top-bar { .flow {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 16px; justify-content: center;
flex-wrap: wrap; gap: 0.5rem;
gap: 10px; font-size: 0.8rem;
} }
.title-section { .domain {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--vp-c-text-1); color: var(--vp-c-text-1);
} }
.target-select { .dns {
display: flex; background: #dbeafe;
align-items: center; color: #2563eb;
gap: 8px; padding: 0.15rem 0.4rem;
font-size: 13px;
}
.target-select select {
padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
border: 1px solid var(--vp-c-divider); font-size: 0.75rem;
background: var(--vp-c-bg-alt);
} }
.action-btn { .ip {
padding: 4px 12px; color: #059669;
border-radius: 4px; font-weight: 500;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
} }
.action-btn.primary { background: var(--vp-c-brand); color: white; } .arrow {
.action-btn.secondary { background: var(--vp-c-bg-alt); border-color: var(--vp-c-divider); color: var(--vp-c-text-1); }
.action-btn.outline { border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-2); background: transparent; }
.status-bar {
background: var(--vp-c-bg-soft);
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 13px;
text-align: center;
min-height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.status-text strong { color: var(--vp-c-brand); margin: 0 4px; }
/* 流程图部分 */
.flow-stage {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
padding: 0 4px;
position: relative;
overflow-x: auto;
}
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
flex: 1;
min-width: 50px;
cursor: pointer;
opacity: 0.6;
transition: all 0.3s;
}
.flow-step:last-child { flex: 0 0 auto; }
.flow-step.active, .flow-step.passed { opacity: 1; }
.step-icon-box {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
position: relative;
z-index: 2;
transition: all 0.3s;
font-size: 18px;
}
.flow-step.active .step-icon-box {
border-color: var(--step-color);
background: var(--step-color);
color: white;
transform: scale(1.1);
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.flow-step.passed .step-icon-box {
border-color: var(--step-color);
color: var(--step-color);
}
.step-label {
font-size: 12px;
color: var(--vp-c-text-2);
white-space: nowrap;
}
.flow-step.active .step-label {
color: var(--step-color);
font-weight: bold;
}
.step-line {
position: absolute;
top: 20px;
left: 50%;
width: 100%;
height: 2px;
background: var(--vp-c-divider);
z-index: 1;
}
.flow-step.passed .step-line { background: var(--step-color); }
/* 底部双面板布局 */
.info-panels {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 16px;
}
@media (max-width: 640px) {
.info-panels { grid-template-columns: 1fr; }
}
.detail-panel {
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
overflow: hidden;
height: 180px; /* 固定高度防止跳动 */
display: flex;
flex-direction: column;
}
/* 左侧生活比喻面板 */
.analogy-panel {
padding: 16px;
position: relative;
}
.panel-content {
height: 100%;
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.panel-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.analogy-text {
font-size: 14px;
line-height: 1.5;
color: var(--vp-c-text-1);
}
.tech-hint-badge {
display: inline-block;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
align-self: flex-start;
margin-top: 8px;
}
.panel-placeholder {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
font-size: 14px;
}
/* 右侧终端面板 */
.terminal-panel {
background: #1e1e1e; /* 强制深色背景 */
border-color: #333;
color: #d4d4d4;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
}
.terminal-header {
background: #2d2d2d;
padding: 6px 12px;
display: flex;
align-items: center;
border-bottom: 1px solid #333;
}
.terminal-dots {
display: flex;
gap: 6px;
margin-right: 12px;
}
.terminal-dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: #555;
}
.terminal-dots span:nth-child(1) { background: #ff5f56; }
.terminal-dots span:nth-child(2) { background: #ffbd2e; }
.terminal-dots span:nth-child(3) { background: #27c93f; }
.terminal-title {
font-size: 12px;
color: #999;
}
.terminal-body {
padding: 12px;
font-size: 12px;
line-height: 1.4;
flex: 1;
}
.cmd-line {
display: flex;
gap: 8px;
color: #fff;
margin-bottom: 8px;
}
.prompt { color: #27c93f; font-weight: bold; }
.cmd { color: #fff; }
.cmd-output pre {
margin: 0;
color: #aaa;
white-space: pre-wrap;
word-break: break-all;
}
.terminal-placeholder {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #555;
font-size: 13px;
}
.spin {
display: inline-block;
animation: spin 1s linear infinite;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
/* Dark mode 适配 - 这里主要针对非终端部分 */
:root.dark .dns-compact {
background: var(--vp-c-bg);
} }
</style> </style>
@@ -1,531 +1,198 @@
<!--
HttpExchangeDemo.vue
HTTP请求响应演示 - 紧凑交互版
设计理念
1. 循循善诱"快递员投递"类比 HTTP 请求响应
2. 紧凑布局横向舞台固定底部详情板
-->
<template> <template>
<div class="http-compact"> <div class="http-exchange-demo">
<!-- 顶部标题与场景选择 --> <div class="demo-header">
<div class="top-bar"> <span class="title">HTTP 请求/响应</span>
<div class="title-section"> <span class="subtitle">浏览器与服务器的对话</span>
<span class="app-icon">📦</span>
<span class="app-title">HTTP 请求/响应</span>
</div>
<div class="scenario-tabs">
<button
v-for="s in scenarios"
:key="s.id"
class="tab-btn"
:class="{ active: currentScenario.id === s.id }"
:disabled="isAnimating"
@click="selectScenario(s)"
>
{{ s.name }}
</button>
</div>
<div class="actions">
<button
class="action-btn primary"
@click="toggleAutoPlay"
>
{{ isAutoPlaying ? '⏸' : '▶ 演示' }}
</button>
<button
class="action-btn outline"
@click="reset"
>
</button>
</div>
</div> </div>
<!-- 核心可视化舞台 --> <div class="exchange-flow">
<div class="stage-area"> <div class="actor browser">
<!-- 客户端 --> <span class="actor-icon">🧑💻</span>
<div class="actor client"> <span class="actor-name">浏览器</span>
<div class="avatar-box"> </div>
<span class="avatar-icon">🧑💻</span>
<span class="avatar-label">浏览器</span> <div class="messages">
<div class="request-box">
<span class="method">GET</span>
<span class="path">/search?q=hello</span>
<span class="arrow"></span>
</div>
<div class="response-box">
<span class="arrow"></span>
<span class="status">200 OK</span>
<span class="size">HTML 页面</span>
</div> </div>
</div> </div>
<!-- 传输通道 -->
<div class="channel">
<div class="channel-bg" />
<!-- 请求包 -->
<div
class="packet request"
:class="{ moving: step === 1, done: step > 1 }"
>
<span class="packet-icon">📤</span>
<span class="packet-label">GET</span>
</div>
<!-- 响应包 -->
<div
v-if="step >= 2"
class="packet response"
:class="{ moving: step === 2, done: step > 2 }"
>
<span class="packet-icon">📦</span>
<span class="packet-label">{{ currentScenario.status }}</span>
</div>
</div>
<!-- 服务器 -->
<div class="actor server"> <div class="actor server">
<div class="avatar-box"> <span class="actor-icon">🖥</span>
<span class="avatar-icon">🏢</span> <span class="actor-name">服务器</span>
<span class="avatar-label">服务器</span>
</div>
</div> </div>
</div> </div>
<!-- 底部详情面板 (固定高度) --> <div class="code-preview">
<div class="detail-panel"> <div class="code-block">
<transition <div class="code-header">请求</div>
name="fade" <code>GET /search?q=hello HTTP/1.1</code>
mode="out-in" <code>Host: www.google.com</code>
> </div>
<div <div class="code-block">
v-if="step > 0" <div class="code-header">响应</div>
:key="step" <code>HTTP/1.1 200 OK</code>
class="detail-content" <code>Content-Type: text/html</code>
> </div>
<!-- 左侧状态徽章 --> </div>
<div
class="detail-left"
:style="{ borderColor: getStatusColor() }"
>
<div
class="status-badge"
:class="currentScenario.statusType"
>
{{ step === 1 ? '请求中' : currentScenario.status + ' ' + currentScenario.statusText }}
</div>
</div>
<div class="detail-divider" />
<!-- 右侧详情 --> <div class="info-box">
<div class="detail-right"> <strong>核心思想</strong>
<div class="info-row"> HTTP 是请求-响应模式浏览器发送请求服务器返回状态码和响应内容
<span class="tag life">快递员说</span>
<span class="text highlight">
{{ step === 1 ? currentScenario.requestText : currentScenario.responseText }}
</span>
</div>
<div class="info-row">
<span class="tag tech">技术报文</span>
<span class="text code">
{{ step === 1 ? `${currentScenario.method} ${currentScenario.path} HTTP/1.1` : `HTTP/1.1 ${currentScenario.status} ${currentScenario.statusText}` }}
</span>
</div>
</div>
</div>
<div
v-else
class="detail-placeholder"
>
<span class="guide-bounce">📦</span>
<span>选择一个场景点击"演示"看看发生了什么</span>
</div>
</transition>
</div> </div>
</div> </div>
</template> </template>
<script setup>
import { ref, onUnmounted } from 'vue'
const scenarios = [
{
id: 'success',
name: '正常送达',
method: 'GET',
path: '/index.html',
requestText: '您好,请给我 index.html 的包裹!',
status: '200',
statusText: 'OK',
statusType: 'success',
responseText: '好的,这是您的 index.html,请签收!',
qa: {
title: '🤔 200 OK 是什么意思?',
content: [
{
q: '200 这个数字代表什么?',
a: '就像快递单上的"已妥投"。代表一切顺利,服务器成功找到了你要的东西并给了你。'
},
{
q: 'GET 是什么?',
a: '就像你对服务员说"给我来一份菜单"。是向服务器"索要"东西的意思。绝大多数网页访问都是 GET 请求。'
}
]
}
},
{
id: 'notfound',
name: '查无此人',
method: 'GET',
path: '/nopage',
requestText: '您好,我要找 nopage 这个人。',
status: '404',
statusText: 'Not Found',
statusType: 'error',
responseText: '抱歉,这里查无此人 (404)。',
qa: {
title: '🤔 为什么叫 404',
content: [
{
q: '是谁的错?',
a: '通常是"你"(客户端)的错。4开头的代码都代表客户端问题,比如你地址输错了,或者这个网页已经被删除了。'
}
]
}
},
{
id: 'redirect',
name: '搬家了',
method: 'GET',
path: '/old-path',
requestText: '您好,送到 old-path 这里。',
status: '301',
statusText: 'Moved',
statusType: 'warn',
responseText: '这里搬家了,请去新地址 (301)。',
qa: {
title: '🤔 301 重定向是什么?',
content: [
{
q: '浏览器会怎么做?',
a: '浏览器收到 301 后,会自动去访问新的地址。这个过程很快,你可能都感觉不到。'
},
{
q: '为什么要重定向?',
a: '就像店铺搬迁要在门口贴个告示。保证收藏了旧网址的老顾客也能找到新店。'
}
]
}
},
{
id: 'servererror',
name: '系统崩溃',
method: 'GET',
path: '/api/data',
requestText: '您好,我要取数据。',
status: '500',
statusText: 'Error',
statusType: 'error',
responseText: '抱歉,仓库塌了,暂时无法取货 (500)。',
qa: {
title: '🤔 500 是谁的错?',
content: [
{
q: '我能修好它吗?',
a: '不能。5开头的代码代表"服务器"出问题了(比如代码崩了、数据库挂了)。跟你没关系,只能等待网站管理员修复。'
}
]
}
}
]
const currentScenario = ref(scenarios[0])
const step = ref(0) // 0: Idle, 1: Requesting, 2: Responding, 3: Done
const isAnimating = ref(false)
const isAutoPlaying = ref(false)
let timer = null
const selectScenario = (s) => {
if (isAnimating.value) return
currentScenario.value = s
reset()
}
const toggleAutoPlay = () => {
if (isAutoPlaying.value) {
reset()
} else {
startAnimation()
}
}
const startAnimation = () => {
if (isAnimating.value) return
isAnimating.value = true
isAutoPlaying.value = true
step.value = 1
// Step 1: Request (Client -> Server)
timer = setTimeout(() => {
step.value = 2
// Step 2: Response (Server -> Client)
timer = setTimeout(() => {
step.value = 3
isAnimating.value = false
isAutoPlaying.value = false
}, 1500)
}, 1500)
}
const reset = () => {
clearTimeout(timer)
step.value = 0
isAnimating.value = false
isAutoPlaying.value = false
}
const getStatusColor = () => {
if (step.value === 1) return '#3b82f6' // Blue for request
const type = currentScenario.value.statusType
if (type === 'success') return '#10b981'
if (type === 'warn') return '#f59e0b'
if (type === 'error') return '#ef4444'
return '#909399'
}
onUnmounted(() => {
clearTimeout(timer)
})
</script>
<style scoped> <style scoped>
.http-compact { .http-exchange-demo {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 8px;
background: var(--vp-c-bg);
padding: 16px;
margin: 16px 0;
font-size: 14px;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title-section {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.scenario-tabs {
display: flex;
background: var(--vp-c-bg-alt);
padding: 2px;
border-radius: 6px;
gap: 2px;
}
.tab-btn {
padding: 2px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
font-weight: 500;
}
.actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn.primary {
background: var(--vp-c-brand);
color: white;
border: 1px solid var(--vp-c-brand);
}
.action-btn.outline {
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
/* 舞台区 */
.stage-area {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
background: var(--vp-c-bg-soft); background: var(--vp-c-bg-soft);
border-radius: 6px; padding: 1rem;
padding: 0 30px; margin: 1rem 0;
position: relative; font-family: var(--vp-font-family-mono);
margin-bottom: 20px; }
.demo-header {
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
display: block;
}
.demo-header .subtitle {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.exchange-flow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem 0;
margin-bottom: 0.75rem;
} }
.actor { .actor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 60px; gap: 0.25rem;
z-index: 2;
} }
.avatar-icon { font-size: 28px; } .actor-icon {
.avatar-label { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; } font-size: 1.5rem;
.channel {
flex: 1;
height: 40px;
margin: 0 20px;
position: relative;
display: flex;
align-items: center;
} }
.channel-bg { .actor-name {
position: absolute; font-size: 0.7rem;
width: 100%; color: var(--vp-c-text-2);
height: 2px;
background: var(--vp-c-divider);
top: 50%;
} }
.packet { .messages {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
transition: all 1.5s ease-in-out;
opacity: 0;
top: 0;
}
.packet-icon { font-size: 20px; }
.packet-label { font-size: 10px; color: var(--vp-c-text-2); background: var(--vp-c-bg); padding: 0 2px; }
.packet.request { left: 0; opacity: 1; }
.packet.request.moving { left: 100%; transform: translateX(-100%); opacity: 0; }
/* Request moves from 0 to 100% then disappears */
.packet.response { left: 100%; transform: translateX(-100%); opacity: 0; }
.packet.response.moving { left: 0; transform: translateX(0); opacity: 1; }
/* Response starts at 100%, moves to 0 */
/* 动画调整 */
.packet.request.moving {
animation: sendRequest 1.5s forwards;
}
.packet.response.moving {
animation: sendResponse 1.5s forwards;
}
@keyframes sendRequest {
0% { left: 0; opacity: 1; transform: translateX(0); }
90% { left: 100%; opacity: 1; transform: translateX(-100%); }
100% { left: 100%; opacity: 0; transform: translateX(-100%); }
}
@keyframes sendResponse {
0% { left: 100%; opacity: 1; transform: translateX(-100%); }
100% { left: 0; opacity: 1; transform: translateX(0); }
}
/* 详情面板 */
.detail-panel {
height: 80px;
background: var(--vp-c-bg-alt);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
padding: 0 16px;
display: flex;
align-items: center;
overflow: hidden;
}
.detail-content {
display: flex;
width: 100%;
align-items: center;
}
.detail-left {
padding-right: 16px;
border-right: 2px solid transparent;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
color: white;
font-size: 12px;
font-weight: bold;
}
.status-badge.success { background: #10b981; }
.status-badge.warn { background: #f59e0b; }
.status-badge.error { background: #ef4444; }
.detail-divider {
width: 1px;
height: 40px;
background: var(--vp-c-divider);
margin: 0 16px;
}
.detail-right {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 0.4rem;
} }
.info-row { .request-box,
.response-box {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
}
.tag {
font-size: 11px;
padding: 1px 6px;
border-radius: 3px;
white-space: nowrap;
}
.tag.life { background: #e6f7ff; color: #1890ff; }
.tag.tech { background: #f6ffed; color: #52c41a; }
.text { font-size: 13px; color: var(--vp-c-text-1); }
.text.highlight { font-weight: 500; color: var(--vp-c-brand); }
.text.code { font-family: monospace; }
.detail-placeholder {
display: flex;
align-items: center;
gap: 8px;
color: var(--vp-c-text-3);
width: 100%;
justify-content: center; justify-content: center;
gap: 0.4rem;
padding: 0.4rem 0.6rem;
background: var(--vp-c-bg);
border-radius: 4px;
} }
.guide-bounce { animation: bounce 1.5s infinite; } .method {
@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } } font-size: 0.75rem;
font-weight: bold;
color: #2563eb;
font-family: var(--vp-font-family-mono);
}
.path {
font-size: 0.75rem;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.status {
font-size: 0.75rem;
font-weight: bold;
color: #059669;
font-family: var(--vp-font-family-mono);
}
.size {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.arrow {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.code-preview {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.code-block {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0.5rem;
}
.code-header {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.3rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.code-block code {
display: block;
font-size: 0.7rem;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
line-height: 1.5;
}
.info-box {
display: flex;
gap: 0.25rem;
background-color: var(--vp-c-bg-alt);
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
</style> </style>
@@ -1,810 +1,172 @@
<!--
TcpHandshakeDemo.vue
TCP三次握手演示 - 紧凑交互版
设计理念
1. 循循善诱"打电话"的生活场景类比 TCP 连接建立过程
2. 紧凑布局保留核心可视化区使用固定底部详情板代替长列表
-->
<template> <template>
<div class="tcp-compact"> <div class="tcp-handshake-demo">
<!-- 顶部标题与控制 --> <div class="demo-header">
<div class="top-bar"> <span class="title">TCP 三次握手</span>
<div class="title-section"> <span class="subtitle">建立可靠连接的过程</span>
<span class="app-icon">📞</span>
<span class="app-title">TCP 三次握手</span>
</div>
<div class="actions">
<button
v-if="currentStep < 3"
class="action-btn primary"
:disabled="currentStep >= 3"
@click="nextStep"
>
{{ currentStep === 0 ? '▶ 开始拨号' : '下一步 ➔' }}
</button>
<button
class="action-btn outline"
@click="reset"
>
重置
</button>
</div>
</div> </div>
<!-- 核心可视化舞台 --> <div class="handshake-flow">
<div class="stage-area">
<!-- 左侧客户端/快递员 -->
<div class="actor client"> <div class="actor client">
<div class="avatar-box"> <span class="actor-icon">🧑💻</span>
<span class="avatar-icon">🧑💻</span> <span class="actor-name">客户端</span>
<span class="avatar-label">客户端 ()</span>
</div>
<transition name="pop">
<div
v-if="currentStep >= 1"
class="bubble client"
>
{{ getBubbleText(1) }}
</div>
</transition>
</div> </div>
<!-- 中间连接状态线 --> <div class="messages">
<div class="connection-line"> <div class="message-row">
<div class="line-bg" /> <span class="msg-label">SYN</span>
<div <span class="msg-arrow"></span>
class="signal-packet" <span class="msg-desc">"我能连你吗?"</span>
:class="getSignalClass()"
>
<span
v-if="currentStep > 0"
class="packet-icon"
>{{ getSignalIcon() }}</span>
</div> </div>
<div <div class="message-row">
class="status-badge" <span class="msg-desc">"能,你也能收到我吗?"</span>
:class="{ connected: currentStep === 3 }" <span class="msg-arrow"></span>
> <span class="msg-label">SYN-ACK</span>
{{ currentStep === 3 ? '✅ 连接建立' : '⏳ 连接中...' }} </div>
<div class="message-row">
<span class="msg-label">ACK</span>
<span class="msg-arrow"></span>
<span class="msg-desc">"能,开始吧!"</span>
</div> </div>
</div> </div>
<!-- 右侧服务器/收件人 -->
<div class="actor server"> <div class="actor server">
<div class="avatar-box"> <span class="actor-icon">🖥</span>
<span class="avatar-icon">🖥</span> <span class="actor-name">服务器</span>
<span class="avatar-label">服务器</span>
</div>
<transition name="pop">
<div
v-if="currentStep >= 2"
class="bubble server"
>
{{ getBubbleText(2) }}
</div>
</transition>
</div> </div>
</div> </div>
<!-- 步骤进度条 (可点击跳转) --> <div class="status-bar">
<div class="step-indicator"> <span class="status-badge success"> 连接已建立</span>
<div
v-for="(step, index) in steps"
:key="index"
class="step-dot"
:class="{ active: currentStep === index + 1, passed: currentStep > index + 1 }"
:title="step.techTitle"
@click="goToStep(index + 1)"
>
<span class="dot-num">{{ index + 1 }}</span>
<span
v-if="index < steps.length - 1"
class="dot-line"
/>
</div>
</div> </div>
<!-- 底部详情面板 (固定高度) --> <div class="info-box">
<div class="detail-panel"> <strong>核心思想</strong>
<transition 三次握手确保双方都能收发数据就像打电话时互相确认"能听到吗"
name="fade"
mode="out-in"
>
<div
v-if="currentStep > 0"
:key="currentStep"
class="detail-content"
>
<div
class="detail-left"
:style="{ borderColor: getCurrentStepColor() }"
>
<div
class="step-badge"
:style="{ background: getCurrentStepColor() }"
>
步骤 {{ currentStep }}
</div>
</div>
<div class="detail-divider" />
<div class="detail-right">
<div class="info-row">
<span class="tag life">生活对话</span>
<span class="text highlight">{{ steps[currentStep-1].simpleTitle }}</span>
</div>
<div class="info-row">
<span class="tag tech">技术原理</span>
<div class="tech-content">
<div class="tech-desc">
{{ steps[currentStep-1].techDesc }}
</div>
<!-- 动态名词解码卡片 -->
<div class="term-glossary">
<div
v-for="term in steps[currentStep-1].terms"
:key="term.key"
class="term-item"
>
<span class="term-key">{{ term.key }}</span>
<span class="term-val">{{ term.val }}</span>
</div>
</div>
<!-- 代码实现细节 (折叠) -->
<details
v-if="steps[currentStep-1].codeImpl"
class="code-details"
>
<summary class="code-summary">
<span class="summary-icon">🛠</span>
<span class="summary-text">技术深究底层代码如何实现</span>
</summary>
<div class="code-block-wrapper">
<div class="code-title">
{{ steps[currentStep-1].codeImpl.title }}
</div>
<pre class="code-block"><code v-html="steps[currentStep-1].codeImpl.code" /></pre>
</div>
</details>
<!-- 技术问答 (折叠) - 仅在有问答时显示 -->
<details
v-if="steps[currentStep-1].qa"
class="code-details qa-details"
>
<summary class="code-summary qa-summary">
<span class="summary-icon">🎓</span>
<span class="summary-text">{{ steps[currentStep-1].qa.title }}</span>
</summary>
<div class="code-block-wrapper qa-content">
<div
v-for="(item, idx) in steps[currentStep-1].qa.content"
:key="idx"
class="qa-item"
>
<div class="qa-q">
Q: {{ item.q }}
</div>
<div
class="qa-a"
v-html="item.a"
/>
</div>
</div>
</details>
</div>
</div>
</div>
<!-- 下一步按钮 -->
<button
v-if="currentStep < 3"
class="next-btn"
@click="nextStep"
>
下一步
</button>
</div>
<div
v-else
class="detail-placeholder"
>
<span class="guide-bounce">📞</span>
<span>点击"开始拨号"或步骤圆点开始拨打电话</span>
</div>
</transition>
</div> </div>
</div> </div>
</template> </template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{
simpleTitle: '喂,在家吗?我是快递员!',
techTitle: 'SYN',
techDesc: '客户端发送 SYN 包 (Seq=x),请求建立连接。',
color: '#3b82f6',
terms: [
{ key: 'SYN', val: '是单词 Synchronize (同步) 的缩写。💡 为什么叫"同步"?因为建立连接的第一步,就是双方要"对表",把暗号(序号)对齐,确保后续对话在同一个频道上。' },
{ key: 'Seq=x', val: '意思是:"我的数据计数器,从 x 开始"。💡 为什么要特意告诉对方?这就像是两个人约定"暗号"。我告诉你:"我的暗号是从 x 开始算的"。以后我每发给你一个字,暗号就加 1。如果你不知道我的起始暗号是 x,以后收到 x+100 你就不知道它是第几个字,也没法判断中间有没有丢字。' }
],
codeImpl: {
title: '💻 真实 TCP 报文构建 (伪代码)',
code: `// 1. 设置标志位:只置 SYN
<span class="kw">tcph->syn</span> = 1;
<span class="kw">tcph->ack</span> = 0;
// 2. (Seq)
//
// htonl
<span class="kw">tcph->seq</span> = htonl(<span class="var">random_x</span>);
// 3.
send_packet(client_socket, tcph);`
}
},
{
simpleTitle: '在的!我听到了,请说!',
techTitle: 'SYN-ACK',
techDesc: '服务器回复 SYN-ACK 包 (Seq=y, Ack=x+1),确认并请求连接。',
color: '#10b981',
terms: [
{ key: 'ACK', val: '是单词 Acknowledgment (确认) 的缩写。💡 就像快递签收单,表示"我收到你的请求了"。' },
{ key: 'Ack=x+1', val: '确认号。💡 为什么要 +1?这是一种期待机制。意思是:"x 号那一页我已经收好了,请你下次从 x+1 页开始讲"。' },
{ key: 'Seq=y', val: '服务器也生成自己的随机序号 y,方便客户端确认服务器发来的话。' }
],
codeImpl: {
title: '💻 服务器内核响应逻辑 (伪代码)',
code: `// 1. 检查收到的是否是 SYN
if (<span class="kw">recv_tcph->syn</span> == 1) {
// 2.
<span class="kw">reply_tcph->syn</span> = 1; //
<span class="kw">reply_tcph->ack</span> = 1; //
// 3. = Seq + 1
//
<span class="kw">reply_tcph->ack_seq</span> = htonl(ntohl(<span class="var">recv_tcph->seq</span>) + 1);
// 4.
<span class="kw">reply_tcph->seq</span> = htonl(<span class="var">random_y</span>);
send_packet(server_socket, reply_tcph);
}`
}
},
{
simpleTitle: '好的,那我开始说了!',
techTitle: 'ACK',
techDesc: '客户端发送 ACK 包 (Ack=y+1),连接建立成功,可以传输数据。',
color: '#8b5cf6',
terms: [
{ key: 'Ack=y+1', val: '客户端确认收到。意思是:"服务器你的 y 号信我也收到了,我们正式开始聊天吧!"' },
{ key: '连接建立', val: '双方都确认了对方"能听能说",通道正式打通。' }
],
codeImpl: {
title: '💻 客户端最终确认 (伪代码)',
code: `// 1. 检查收到的包
if (<span class="kw">recv_tcph->syn</span> == 1 && <span class="kw">recv_tcph->ack</span> == 1) {
// 2. ACK
<span class="kw">ack_tcph->syn</span> = 0; // SYN
<span class="kw">ack_tcph->ack</span> = 1;
// 3. = Seq + 1
<span class="kw">ack_tcph->ack_seq</span> = htonl(ntohl(<span class="var">recv_tcph->seq</span>) + 1);
// 4. = Seq + 1
<span class="kw">ack_tcph->seq</span> = htonl(<span class="var">my_seq</span> + 1);
// 5. ESTABLISHED
<span class="hl">socket->state = TCP_ESTABLISHED;</span>
send_packet(client_socket, ack_tcph);
}`
},
qa: {
title: '🤔 为什么必须是三次?(核心逻辑)',
content: [
{
q: '为什么一定要三次?(双工确认原理)',
a: `这其实是在验证<strong>双方的"听说能力"</strong>是否正常。TCP 是全双工的(双方都能同时发和收),所以必须双方都确认对方能发能收:<br>
1 <strong>第一次 (Client -> Server)</strong>Server 收到证明 <strong>Client 能发</strong><strong>Server 能收</strong><br>
2 <strong>第二次 (Server -> Client)</strong>Client 收到证明 <strong>Server 能发</strong><strong>Client 能收</strong>同时 Client 知道 Server 收到了自己的第一次请求<br>
3 <strong>第三次 (Client -> Server)</strong>Server 收到证明 <strong>Client 能收</strong>因为 Client 回复了 Server 的消息<br>
<br>
<strong>结论</strong> 只有经过这三次双方都明确知道"自己""对方"的发送接收功能全是好的少一次都不行Server 不知道 Client 能不能收多一次没必要`
},
{
q: '为什么这就算"连上"了?',
a: `所谓的"连接建立",在计算机里并不是真的拉了一根线。它的本质是:<strong>双方内存里都保存好了对方的"状态信息"</strong>。<br>
通过这三次握手双方主要完成了两件事<br>
1. <strong>确认通道畅通</strong>就是上面说的双工能力确认<br>
2. <strong>同步初始序号 (ISN)</strong>双方交换了 Seq (x y)<br>
<br>
只要双方都记住了对方的 Seq并且确认了对方在线操作系统就会把 Socket 状态标记为 <code style="color:#10b981">ESTABLISHED</code> (已建立)这就叫"连上了"`
}
]
}
}
]
const nextStep = () => {
if (currentStep.value < 3) currentStep.value++
}
const goToStep = (step) => {
currentStep.value = step
}
const reset = () => {
currentStep.value = 0
}
const getBubbleText = (stepIndex) => {
// stepIndex 1: Client speaks (Step 1)
// stepIndex 2: Server speaks (Step 2)
if (stepIndex === 1) return steps[0].simpleTitle
if (stepIndex === 2) return steps[1].simpleTitle
return ''
}
const getSignalClass = () => {
if (currentStep.value === 1) return 'sending' // Left to Right
if (currentStep.value === 2) return 'receiving' // Right to Left
if (currentStep.value === 3) return 'sending-final' // Left to Right
return ''
}
const getSignalIcon = () => {
return '🔔'
}
const getCurrentStepColor = () => {
return steps[currentStep.value - 1]?.color || '#ccc'
}
</script>
<style scoped> <style scoped>
.tcp-compact { .tcp-handshake-demo {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 8px;
background: var(--vp-c-bg);
padding: 16px;
margin: 16px 0;
font-size: 14px;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title-section {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn.primary {
background: var(--vp-c-brand);
color: white;
border: 1px solid var(--vp-c-brand);
}
.action-btn.outline {
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
/* 舞台区 */
.stage-area {
display: flex;
justify-content: space-between;
align-items: center;
height: 140px;
background: var(--vp-c-bg-soft); background: var(--vp-c-bg-soft);
border-radius: 6px; padding: 1rem;
padding: 0 30px; margin: 1rem 0;
position: relative; font-family: var(--vp-font-family-mono);
margin-bottom: 20px; }
.demo-header {
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
display: block;
}
.demo-header .subtitle {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.handshake-flow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem 0;
margin-bottom: 0.75rem;
} }
.actor { .actor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 120px; gap: 0.25rem;
position: relative;
} }
.avatar-box { .actor-icon {
font-size: 1.5rem;
}
.actor-name {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.messages {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; gap: 0.4rem;
z-index: 2;
} }
.avatar-icon { font-size: 32px; } .message-row {
.avatar-label { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
/* 气泡 */
.bubble {
position: absolute;
top: -40px;
background: white;
padding: 6px 10px;
border-radius: 12px;
font-size: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
white-space: nowrap;
border: 1px solid var(--vp-c-divider);
color: #333;
}
.bubble.client { left: 50%; transform: translateX(-50%); }
.bubble.server { left: 50%; transform: translateX(-50%); }
.pop-enter-active, .pop-leave-active { transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.pop-enter-from, .pop-leave-to { opacity: 0; transform: translateX(-50%) scale(0.8); }
/* 连接线 */
.connection-line {
flex: 1;
height: 2px;
background: var(--vp-c-divider);
margin: 0 20px;
position: relative;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
justify-content: center;
gap: 0.4rem;
padding: 0.35rem 0.5rem;
background: var(--vp-c-bg);
border-radius: 4px;
}
.msg-label {
font-size: 0.7rem;
font-weight: bold;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
min-width: 50px;
text-align: center;
}
.msg-arrow {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.msg-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
flex: 1;
text-align: center;
}
.status-bar {
text-align: center;
margin-bottom: 0.75rem;
} }
.status-badge { .status-badge {
position: absolute; display: inline-block;
top: 15px; padding: 0.3rem 0.75rem;
font-size: 12px; border-radius: 12px;
color: var(--vp-c-text-3); font-size: 0.75rem;
transition: all 0.3s;
}
.status-badge.connected { color: var(--vp-c-brand); font-weight: bold; }
.signal-packet {
position: absolute;
width: 24px;
height: 24px;
background: var(--vp-c-brand);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
opacity: 0;
top: -11px;
}
.signal-packet.sending {
animation: moveRight 1.5s forwards;
opacity: 1;
}
.signal-packet.receiving {
animation: moveLeft 1.5s forwards;
opacity: 1;
background: #10b981;
}
.signal-packet.sending-final {
animation: moveRight 1.5s forwards;
opacity: 1;
background: #8b5cf6;
}
@keyframes moveRight {
0% { left: 0; }
100% { left: 100%; }
}
@keyframes moveLeft {
0% { left: 100%; }
100% { left: 0; }
}
/* 步骤指示器 */
.step-indicator {
display: flex;
justify-content: center;
gap: 40px;
margin-bottom: 20px;
}
.step-dot {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: var(--vp-c-text-3);
cursor: pointer;
position: relative;
transition: all 0.3s;
}
.step-dot.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
transform: scale(1.1);
}
.step-dot.passed {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.dot-line {
position: absolute;
left: 24px;
width: 40px;
height: 2px;
background: var(--vp-c-divider);
}
/* 详情面板 */
.detail-panel {
min-height: 80px; /* 改为最小高度 */
background: var(--vp-c-bg-alt);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
padding: 12px 16px;
display: flex;
align-items: flex-start; /* 顶部对齐 */
/* overflow: hidden; 移除隐藏 */
}
.detail-content {
display: flex;
width: 100%;
align-items: flex-start; /* 顶部对齐 */
}
.detail-left {
padding-right: 16px;
border-right: 2px solid transparent;
margin-top: 4px; /* 微调对齐 */
}
.step-badge {
padding: 4px 8px;
border-radius: 4px;
color: white;
font-size: 12px;
font-weight: bold;
white-space: nowrap;
}
.detail-divider {
width: 1px;
align-self: stretch; /* 拉伸高度 */
background: var(--vp-c-divider);
margin: 0 16px;
}
.detail-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px; /* 增加间距 */
padding-bottom: 4px;
}
.info-row {
display: flex;
align-items: flex-start; /* 顶部对齐 */
gap: 8px;
}
.tag {
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
margin-top: 2px;
}
.tag.life { background: #e6f7ff; color: #1890ff; }
.tag.tech { background: #f6ffed; color: #52c41a; }
.text {
font-size: 13px;
color: var(--vp-c-text-1);
line-height: 1.5;
}
.text.highlight {
font-weight: 500; font-weight: 500;
color: var(--vp-c-brand);
} }
/* 新增:术语解释样式 */ .status-badge.success {
.term-glossary { background: #d1fae5;
margin-top: 8px; color: #059669;
}
.info-box {
display: flex; display: flex;
flex-direction: column; gap: 0.25rem;
gap: 6px; background-color: var(--vp-c-bg-alt);
background: rgba(0,0,0,0.03); padding: 0.5rem 0.75rem;
padding: 8px;
border-radius: 6px; border-radius: 6px;
} font-size: 0.8rem;
line-height: 1.4;
.term-item {
font-size: 12px;
line-height: 1.5;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
} }
.term-key { .info-box strong {
font-weight: bold;
color: var(--vp-c-brand-dark);
margin-right: 6px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
padding: 0 4px;
border-radius: 3px;
}
.next-btn {
padding: 6px 16px;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin-left: 12px;
margin-top: 4px;
white-space: nowrap; white-space: nowrap;
align-self: flex-start; /* 按钮顶部对齐 */ flex-shrink: 0;
}
.detail-placeholder {
display: flex;
align-items: center;
gap: 8px;
color: var(--vp-c-text-3);
width: 100%;
justify-content: center;
}
.guide-bounce {
animation: bounce 1.5s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
/* 代码实现折叠块 */
.code-details {
margin-top: 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
background: var(--vp-c-bg-alt);
}
.code-summary {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
user-select: none;
background: rgba(0,0,0,0.02);
transition: background 0.2s;
}
.code-summary:hover {
background: rgba(0,0,0,0.05);
color: var(--vp-c-brand);
}
.code-block-wrapper {
padding: 12px;
border-top: 1px solid var(--vp-c-divider);
background: #282c34; /* 深色背景适合代码 */
color: #abb2bf;
}
.code-title {
font-size: 11px;
color: #61afef;
margin-bottom: 6px;
font-family: monospace;
font-weight: bold;
}
.code-block {
margin: 0;
font-family: var(--vp-font-family-mono);
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
/* 语法高亮类 (深色模式) */
:deep(.kw) { color: #c678dd; } /* 紫色 - 关键字/字段 */
:deep(.var) { color: #d19a66; } /* 橙色 - 变量 */
:deep(.hl) { color: #98c379; font-weight: bold; } /* 绿色 - 高亮行 */
/* 问答折叠块 */
.qa-details {
background: rgba(255, 165, 0, 0.05); /* 淡淡的橙色背景 */
border-color: rgba(255, 165, 0, 0.2);
}
.qa-summary {
color: #d46b08;
}
.qa-summary:hover {
color: #ff7a45;
background: rgba(255, 165, 0, 0.1);
}
.qa-content {
background: var(--vp-c-bg); /* 恢复浅色/深色背景 */
color: var(--vp-c-text-1); color: var(--vp-c-text-1);
padding: 16px;
}
.qa-item {
margin-bottom: 12px;
}
.qa-item:last-child { margin-bottom: 0; }
.qa-q {
font-weight: bold;
font-size: 13px;
color: var(--vp-c-brand-dark);
margin-bottom: 4px;
}
.qa-a {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
} }
</style> </style>
@@ -1,654 +1,57 @@
<!--
UrlParserDemo.vue
URL解析演示 - 网购订单隐喻版
设计理念
1. 隐喻对齐严格对应 url-to-browser.md 中的"网购订单"比喻
2. 视觉映射将枯燥的 URL 字符串映射为一张清晰的"购物清单"
3. 实时交互输入即解析所见即所得
-->
<template> <template>
<div class="url-parser-order"> <div class="url-parser-demo">
<!-- 顶部输入区 --> <div class="url-line">
<div class="input-section"> <span class="part protocol">https://</span><span class="part host">www.google.com</span><span class="part path">/search</span><span class="part query">?q=hello</span>
<div
class="url-input-box"
:class="{ 'has-error': error }"
>
<span class="input-label">URL</span>
<input
v-model="urlInput"
type="text"
placeholder="https://www.example.com/path?query=1"
class="real-input"
@input="parseUrl"
>
<button
v-if="urlInput"
class="clear-btn"
@click="clear"
>
</button>
</div>
<div class="quick-actions">
<span class="action-label">试一试</span>
<button
v-for="ex in examples"
:key="ex.name"
class="action-chip"
:class="{ active: currentExample === ex.name }"
@click="useExample(ex)"
>
{{ ex.name }}
</button>
</div>
</div> </div>
<div class="labels">
<!-- 核心区域左右对照布局 --> <span class="label protocol">协议</span>
<template v-if="parsed.protocol"> <span class="label host">域名</span>
<div class="core-stage"> <span class="label path">路径</span>
<!-- 左侧解析结果 (技术视角) --> <span class="label query">参数</span>
<div class="tech-view">
<div class="view-header">
<span class="icon">💻</span>
<span class="title">技术解析</span>
</div>
<div class="code-blocks">
<div
v-for="(field, key) in formFields"
v-show="shouldShowField(key)"
:key="key"
class="code-block"
:class="[key, { active: hovered === key }]"
:style="{ '--color': field.color }"
@mouseenter="hovered = key"
@mouseleave="hovered = null"
>
<span class="field-name">{{ key }}</span>
<span class="field-value">{{ getDisplayValue(key) }}</span>
</div>
</div>
</div>
<!-- 中间转换箭头 -->
<div class="transform-arrow">
<span class="arrow-icon"></span>
<span class="arrow-text">映射为</span>
</div>
<!-- 右侧购物单 (生活视角) -->
<div class="life-view">
<div class="view-header">
<span class="icon">🧾</span>
<span class="title">购物订单</span>
</div>
<div class="order-ticket">
<div class="ticket-hole" />
<div
v-for="(field, key) in formFields"
v-show="shouldShowField(key)"
:key="key"
class="ticket-row"
:class="{ active: hovered === key }"
:style="{ '--color': field.color }"
@mouseenter="hovered = key"
@mouseleave="hovered = null"
>
<div
class="ticket-icon"
:style="{ backgroundColor: field.color }"
>
{{ field.icon }}
</div>
<div class="ticket-content">
<span class="ticket-label">{{ field.analogyLabel }}</span>
<span class="ticket-desc">{{ field.analogyDesc }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 技术答疑面板 -->
<transition name="fade">
<div
v-if="activeQa"
class="qa-panel"
>
<div class="qa-header">
{{ activeQa.title }}
</div>
<div class="qa-content">
<div
v-for="(item, idx) in activeQa.content"
:key="idx"
class="qa-item"
>
<div class="qa-q">
Q: {{ item.q }}
</div>
<div class="qa-a">
A: {{ item.a }}
</div>
</div>
</div>
</div>
<div
v-else
class="qa-placeholder"
>
👆 鼠标悬停在上方色块查看详细技术解释
</div>
</transition>
</template>
<!-- 空状态引导 -->
<div
v-else
class="empty-state"
>
<div class="empty-icon">
🛒
</div>
<div class="empty-text">
<p>输入网址生成你的"数字购物单"</p>
<span class="sub-text">看看浏览器如何理解这一长串字符</span>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup>
import { ref, computed } from 'vue'
const urlInput = ref('')
const parsed = ref({})
const hovered = ref(null)
const currentExample = ref('')
const error = ref(false)
const examples = [
{ name: '标准网购', url: 'https://www.nike.com/shoes/running?size=42&color=red' },
{ name: '带端口', url: 'http://localhost:8080/api/status' },
{ name: '带锚点', url: 'https://vuejs.org/guide.html#setup' }
]
// url-to-browser.md
const formFields = {
protocol: {
color: '#f43f5e', // Red
icon: '🚚',
analogyLabel: '物流方式',
analogyDesc: '决定怎么送货(HTTP普通/HTTPS加密保密)。',
qa: {
title: '🤔 为什么要写 http/https',
content: [
{
q: '这两者有什么区别?',
a: 'HTTP 就像寄明信片,邮递员(黑客)能看到内容。HTTPS 就像寄密封的信封,只有收件人能拆开看。'
},
{
q: '为什么现在都是 https',
a: '为了安全!现在的浏览器如果发现不是 HTTPS,会提示"不安全",就像快递公司拒收没封口的信件一样。'
}
]
}
},
hostname: {
color: '#3b82f6', // Blue
icon: '🏠',
analogyLabel: '店铺名称',
analogyDesc: '告诉浏览器去哪家店(服务器)买东西。',
qa: {
title: '🤔 域名还是 IP',
content: [
{
q: '浏览器认识域名吗?',
a: '其实不认识。浏览器只认识 IP 地址(一串数字)。域名是为了方便人记的。下一步(DNS 查询)就是把这个名字翻译成数字。'
}
]
}
},
port: {
color: '#f59e0b', // Amber
icon: '🔢',
analogyLabel: '柜台编号',
analogyDesc: '店铺很大,指定去几号柜台办理业务。',
qa: {
title: '🤔 这里的数字是什么意思?',
content: [
{
q: '为什么平时上网看不到它?',
a: '因为有默认值!就像去银行默认去"综合柜台"一样。HTTP 默认是 80HTTPS 默认是 443。只有特殊的才需要写出来。'
}
]
}
},
pathname: {
color: '#10b981', // Emerald
icon: '📦',
analogyLabel: '货架位置',
analogyDesc: '商品在仓库里的具体存放位置。',
qa: {
title: '🤔 这一长串是干嘛的?',
content: [
{
q: '它对应服务器上的文件吗?',
a: '通常是的。/shoes/running 就像告诉仓库管理员:去"鞋子区"的"跑步架"上拿货。'
}
]
}
},
search: {
color: '#8b5cf6', // Violet
icon: '📝',
analogyLabel: '订单备注',
analogyDesc: '给商家的额外要求(如:红色、42码)。',
qa: {
title: '🤔 问号后面的内容?',
content: [
{
q: '这对网页有什么影响?',
a: '就像你点外卖备注"不要香菜"。网页内容会根据这些参数变化,比如只显示红色的鞋子。'
}
]
}
},
hash: {
color: '#ec4899', // Pink
icon: '🔖',
analogyLabel: '说明书页码',
analogyDesc: '拿到商品后,直接翻到说明书的第几页。',
qa: {
title: '🤔 为什么要用 # 号?',
content: [
{
q: '这部分会发给服务器吗?',
a: '不会。这只是给你自己(浏览器)看的。就像你买书回家后翻到第10页,书店老板并不需要知道你看哪一页。'
}
]
}
}
}
const activeField = computed(() => hovered.value || null)
const activeQa = computed(() => {
if (!activeField.value) return null
return formFields[activeField.value].qa
})
const shouldShowField = (key) => {
const val = parsed.value[key]
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
return val
}
const useExample = (ex) => {
urlInput.value = ex.url
currentExample.value = ex.name
parseUrl()
}
const clear = () => {
urlInput.value = ''
parsed.value = {}
currentExample.value = ''
hovered.value = null
error.value = false
}
const parseUrl = () => {
if (!urlInput.value) {
parsed.value = {}
return
}
try {
let urlStr = urlInput.value.trim()
// new URL()
if (!urlStr.match(/^https?:\/\//)) {
urlStr = (urlStr.startsWith('localhost') ? 'http://' : 'https://') + urlStr
}
const u = new URL(urlStr)
parsed.value = {
protocol: u.protocol.replace(':', ''),
hostname: u.hostname,
port: u.port || (u.protocol === 'https:' ? '443 (默认)' : '80 (默认)'),
pathname: u.pathname,
search: u.search,
hash: u.hash
}
error.value = false
} catch (e) {
// parsed
//
if (urlInput.value.length > 10) {
// error.value = true
}
}
}
</script>
<style scoped> <style scoped>
.url-parser-order { .url-parser-demo {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background-color: var(--vp-c-bg);
padding: 20px;
margin: 16px 0;
font-family: var(--vp-font-family-base);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
/* 输入区 */
.input-section {
margin-bottom: 24px;
}
.url-input-box {
display: flex;
align-items: center;
background: var(--vp-c-bg-alt);
border: 2px solid transparent;
border-radius: 6px; border-radius: 6px;
padding: 8px 12px;
transition: all 0.2s;
}
.url-input-box:focus-within {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg);
}
.input-label {
font-weight: bold;
font-size: 13px;
color: var(--vp-c-text-2);
margin-right: 12px;
user-select: none;
}
.real-input {
flex: 1;
background: transparent;
border: none;
font-family: var(--vp-font-family-mono);
font-size: 14px;
color: var(--vp-c-text-1);
outline: none;
min-width: 0;
}
.clear-btn {
color: var(--vp-c-text-3);
cursor: pointer;
padding: 4px;
}
.clear-btn:hover { color: var(--vp-c-text-1); }
.quick-actions {
display: flex;
gap: 8px;
margin-top: 12px;
flex-wrap: wrap;
}
.action-label {
font-size: 12px;
color: var(--vp-c-text-2);
padding-top: 4px;
}
.action-chip {
padding: 4px 12px;
border-radius: 100px;
background: var(--vp-c-bg-soft); background: var(--vp-c-bg-soft);
font-size: 12px; padding: 0.5rem 0.6rem;
color: var(--vp-c-text-1); margin: 0.5rem 0;
transition: all 0.2s;
border: 1px solid transparent;
}
.action-chip:hover {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
}
.action-chip.active {
background: var(--vp-c-brand);
color: white;
}
/* 核心展示区 */
.core-stage {
display: flex;
align-items: stretch;
gap: 16px;
min-height: 200px;
}
/* 通用标题 */
.view-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
}
.view-header .icon { font-size: 16px; }
.view-header .title { font-weight: bold; font-size: 14px; color: var(--vp-c-text-1); }
/* 左侧:技术视图 */
.tech-view {
flex: 1;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
display: flex;
flex-direction: column;
}
.code-blocks {
display: flex;
flex-direction: column;
gap: 8px;
}
.code-block {
display: flex;
flex-direction: column;
padding: 8px;
border-radius: 6px;
background: var(--vp-c-bg);
border-left: 3px solid var(--color);
transition: all 0.2s;
cursor: default;
}
.code-block:hover, .code-block.active {
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.field-name {
font-size: 11px;
color: var(--vp-c-text-2);
text-transform: uppercase;
margin-bottom: 2px;
}
.field-value {
font-family: var(--vp-font-family-mono); font-family: var(--vp-font-family-mono);
font-size: 13px; }
color: var(--vp-c-text-1);
.url-line {
font-size: 0.75rem;
margin-bottom: 0.3rem;
word-break: break-all; word-break: break-all;
line-height: 1.6;
} }
/* 中间:箭头 */ .part {
.transform-arrow { padding: 0.05rem 0.15rem;
border-radius: 3px;
}
.part.protocol { background: #fee2e2; color: #dc2626; }
.part.host { background: #dbeafe; color: #2563eb; }
.part.path { background: #d1fae5; color: #059669; }
.part.query { background: #ede9fe; color: #7c3aed; }
.labels {
display: flex; display: flex;
flex-direction: column; gap: 0.3rem;
justify-content: center;
align-items: center;
color: var(--vp-c-text-3);
font-size: 12px;
width: 40px;
}
.arrow-icon { font-size: 20px; }
/* 右侧:生活视图 (票据样式) */
.life-view {
flex: 1;
background: #fff;
border-radius: 6px;
padding: 12px;
position: relative;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
border: 1px solid var(--vp-c-divider);
}
/* 暗黑模式适配票据背景 */
:root.dark .life-view {
background: #1e1e20;
} }
.order-ticket { .label {
display: flex; font-size: 0.6rem;
flex-direction: column; padding: 0.05rem 0.25rem;
gap: 12px; border-radius: 3px;
background: var(--vp-c-bg-alt);
border: 1px dashed var(--vp-c-divider);
padding: 16px;
border-radius: 6px;
position: relative;
}
.ticket-hole {
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
width: 16px;
height: 16px;
background: var(--vp-c-bg);
border-radius: 50%;
border: 1px solid var(--vp-c-divider);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}
.ticket-row {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px;
border-radius: 6px;
transition: all 0.2s;
opacity: 0.8;
}
.ticket-row:hover, .ticket-row.active {
background: var(--vp-c-bg-soft);
opacity: 1;
transform: scale(1.02);
}
.ticket-icon {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: white;
flex-shrink: 0;
}
.ticket-content {
display: flex;
flex-direction: column;
}
.ticket-label {
font-weight: bold;
font-size: 13px;
color: var(--vp-c-text-1);
}
.ticket-desc {
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.4;
} }
/* 空状态 */ .label.protocol { background: #fee2e2; color: #dc2626; }
.empty-state { .label.host { background: #dbeafe; color: #2563eb; }
display: flex; .label.path { background: #d1fae5; color: #059669; }
flex-direction: column; .label.query { background: #ede9fe; color: #7c3aed; }
align-items: center; </style>
justify-content: center;
padding: 40px;
color: var(--vp-c-text-3);
text-align: center;
}
.empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
.empty-text p { font-size: 16px; font-weight: bold; margin: 0 0 8px 0; color: var(--vp-c-text-2); }
.sub-text { font-size: 13px; }
/* 响应式 */
@media (max-width: 640px) {
.core-stage {
flex-direction: column;
}
.transform-arrow {
flex-direction: row;
width: 100%;
height: 40px;
gap: 8px;
}
.arrow-icon { transform: rotate(90deg); }
}
/* QA Panel */
.qa-panel {
margin-top: 16px;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
font-size: 13px;
}
.qa-header {
font-weight: 600;
color: var(--vp-c-brand);
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed var(--vp-c-divider);
}
.qa-item {
margin-bottom: 10px;
}
.qa-item:last-child {
margin-bottom: 0;
}
.qa-q {
color: var(--vp-c-text-1);
font-weight: 500;
margin-bottom: 4px;
}
.qa-a {
color: var(--vp-c-text-2);
line-height: 1.5;
}
.qa-placeholder {
margin-top: 16px;
text-align: center;
color: var(--vp-c-text-3);
font-size: 13px;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border: 1px dashed var(--vp-c-divider);
}
/* Transitions */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
@@ -172,7 +172,7 @@ const url = ref('')
const isActive = ref(false) const isActive = ref(false)
const currentStep = ref(0) const currentStep = ref(0)
const quickUrls = ['baidu.com', 'bilibili.com', 'github.com'] const quickUrls = ['baidu.com', 'google.com', 'github.com']
const steps = [ const steps = [
{ {
@@ -72,7 +72,7 @@
@mouseenter="hoveredPart = 'card'" @mouseenter="hoveredPart = 'card'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
&lt;div class="card"&gt; &lt;div class="player"&gt;
</div> </div>
<div <div
class="line indent-3" class="line indent-3"
@@ -83,7 +83,7 @@
@mouseenter="hoveredPart = 'img'" @mouseenter="hoveredPart = 'img'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
&lt;img class="icon" src="castle.png" /&gt; &lt;img class="cover" src="cat.jpg" /&gt;
</div> </div>
<div <div
class="line indent-3" class="line indent-3"
@@ -94,7 +94,7 @@
@mouseenter="hoveredPart = 'title'" @mouseenter="hoveredPart = 'title'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
&lt;h2 class="title"&gt;乐高城堡&lt;/h2&gt; &lt;h2 class="title"&gt;搞笑猫咪合集&lt;/h2&gt;
</div> </div>
<div <div
class="line indent-3" class="line indent-3"
@@ -105,7 +105,7 @@
@mouseenter="hoveredPart = 'btn'" @mouseenter="hoveredPart = 'btn'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
&lt;button class="btn"&gt;购买&lt;/button&gt; &lt;button class="btn"&gt; 播放&lt;/button&gt;
</div> </div>
<div <div
class="line indent-2" class="line indent-2"
@@ -154,7 +154,7 @@
@mouseenter="hoveredPart = 'card'" @mouseenter="hoveredPart = 'card'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
.card { display: flex; padding: 10px; } .player { margin: auto; padding: 20px; }
</div> </div>
<div <div
class="line" class="line"
@@ -165,7 +165,7 @@
@mouseenter="hoveredPart = 'img'" @mouseenter="hoveredPart = 'img'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
.icon { width: 50px; height: 50px; } .cover { width: 100%; height: 200px; }
</div> </div>
<!-- Style properties --> <!-- Style properties -->
<div <div
@@ -177,7 +177,7 @@
@mouseenter="hoveredPart = 'title'" @mouseenter="hoveredPart = 'title'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
.title { color: red; } .title { color: #fb7299; /* B站主题色 */ }
</div> </div>
<div <div
class="line" class="line"
@@ -188,7 +188,7 @@
@mouseenter="hoveredPart = 'btn'" @mouseenter="hoveredPart = 'btn'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
.btn { background: blue; } .btn { background: #00aeec; color: white; }
</div> </div>
</div> </div>
</div> </div>
@@ -233,7 +233,7 @@
@mouseenter.stop="hoveredPart = 'card'" @mouseenter.stop="hoveredPart = 'card'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
<span class="block-label">div.card</span> <span class="block-label">div.player</span>
<!-- Image --> <!-- Image -->
<div <div
@@ -245,11 +245,11 @@
@mouseenter.stop="hoveredPart = 'img'" @mouseenter.stop="hoveredPart = 'img'"
@mouseleave="hoveredPart = null" @mouseleave="hoveredPart = null"
> >
<span class="block-label">img.icon</span> <span class="block-label">img.cover</span>
<span <span
v-if="currentStep >= 3" v-if="currentStep >= 3"
class="content-img" class="content-img"
>🏰</span> >🐈</span>
</div> </div>
<!-- Title --> <!-- Title -->
@@ -267,7 +267,7 @@
<span <span
v-if="currentStep >= 3" v-if="currentStep >= 3"
class="content" class="content"
>乐高城堡</span> >搞笑猫咪合集</span>
</div> </div>
<!-- Button --> <!-- Button -->
@@ -285,7 +285,7 @@
<span <span
v-if="currentStep >= 3" v-if="currentStep >= 3"
class="content-btn" class="content-btn"
>购买</span> > 播放</span>
</div> </div>
</div> </div>
</div> </div>
@@ -332,26 +332,26 @@ import { ref } from 'vue'
const steps = [ const steps = [
{ {
label: 'DOM (搭骨架)', label: 'DOM (搭骨架)',
title: '1. 搭建骨架 (DOM)', title: '1. 搭建骨架 (DOM 解析)',
desc: '浏览器工头 (Parser) 解析 HTML 代码,构建出完整的文档树结构。注意:即使代码中省略了 html/body,浏览器也会自动补全。', desc: '浏览器工厂看懂了 HTML 代码,搭建好了页面的“骨架”(比如哪里是包裹盒 div,哪里是按钮 button)。',
resultTitle: 'DOM 树结构' resultTitle: 'DOM 树结构'
}, },
{ {
label: 'Style (上色)', label: 'Style (看图纸)',
title: '2. 计算样式 (Recalculate Style)', title: '2. 匹配样式 (CSS 解析)',
desc: '装修工 (CSS Parser) 匹配 CSS 规则。比如发现 .title 需要红色,.btn 需要蓝色背景。此时只关心"长什么样",不关心"在哪"。', desc: '仔细看了眼配色的说明书。比如发现 .title 字体要是粉色的,.btn 背景要是蓝色的(此时只在脑子里确立样式,但不计算尺寸)。',
resultTitle: '附带样式的节点' resultTitle: '获取了各种配置规则'
}, },
{ {
label: 'Layout (排版)', label: 'Layout (定尺寸)',
title: '3. 布局排版 (Layout/Reflow)', title: '3. 排版规划 (Layout)',
desc: '测量员 (Layout) 根据 display:flex 和 padding 等属性,计算每个盒子的精确位置和大小。图片在左,文字在右。', desc: '拿尺子量每个骨架的大小。考虑到用户的屏幕尺寸,精确计算出猫咪的图片要多高、播放按钮要挤到哪个坐标上。',
resultTitle: '几何布局' resultTitle: '排版布局盒子'
}, },
{ {
label: 'Paint (绘制)', label: 'Paint (绘制)',
title: '4. 像素绘制 (Paint)', title: '4. 像素上色 (Paint)',
desc: '画家 (Paint) 按照计算好的位置和样式,真正把像素点画在屏幕上。最终你看到了一个完整的商品卡片。', desc: '根据前面的几何位置和颜色计划,正式拿起画笔,将一个个像素填到你的屏幕上,一个可以看视频的播放器就诞生了。',
resultTitle: '最终画面' resultTitle: '最终画面'
} }
] ]
@@ -576,34 +576,39 @@ const hoveredPart = ref(null)
/* Step 2: Style */ /* Step 2: Style */
.block-box.title.styled { .block-box.title.styled {
color: red; /* Text color applied but not painted yet */ color: #fb7299;
border: 1px solid red; /* Visual cue for style applied */ border: 1px solid #fb7299;
background: #fee2e2; background: #fdf2f8;
} }
.block-box.btn.styled { .block-box.btn.styled {
background: blue; background: #00aeec;
color: white; color: white;
border: 1px solid blue; border: 1px solid #00aeec;
} }
/* Step 3: Layout */ /* Step 3: Layout */
.block-box.card.layout { .block-box.card.layout {
display: flex; display: flex;
flex-direction: row; /* Horizontal layout */ flex-direction: column;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 15px; padding: 15px;
background: white; background: white;
border: 1px solid #ccc; border: 1px solid #ccc;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
} }
.block-box.img.layout { .block-box.img.layout {
width: 50px; width: 100%;
height: 50px; height: 120px;
background: #eee; background: #eee;
border: none; border: none;
font-size: 3rem;
display: flex;
align-items: center;
justify-content: center;
} }
.block-box.title.layout { .block-box.title.layout {
@@ -614,9 +619,11 @@ const hoveredPart = ref(null)
} }
.block-box.btn.layout { .block-box.btn.layout {
margin-left: auto; /* Push to right */ width: 100%;
padding: 5px 15px; padding: 8px;
border-radius: 4px; border-radius: 4px;
text-align: center;
cursor: pointer;
} }
/* Content visibility for Paint step */ /* Content visibility for Paint step */
@@ -5,10 +5,10 @@
<strong>为什么需要 DNS(查导航)</strong> <strong>为什么需要 DNS(查导航)</strong>
</p> </p>
<p class="why-desc-zh"> <p class="why-desc-zh">
你知道店铺名字叫 "Shop.com"但快递员需要知道具体的经纬度坐标 (IP 地址) 你知道店铺名字叫 "bilibili.com"但快递员需要知道具体的经纬度坐标 (IP 地址)
才能送达 才能送达
<br> <br>
DNS 就像是<strong>地图导航</strong>输入店名告诉你具体的坐标 DNS 就像是<strong>地图导航</strong>输入店名通过114查号台帮你找到坐标
</p> </p>
</div> </div>
@@ -16,7 +16,7 @@
<div class="input-area"> <div class="input-area">
<span class="label">店铺名称 (域名)</span> <span class="label">店铺名称 (域名)</span>
<div class="fake-input"> <div class="fake-input">
shop.com bilibili.com
</div> </div>
</div> </div>
@@ -29,10 +29,10 @@
🧭 🧭
</div> </div>
<div class="title"> <div class="title">
DNS (地图导航) DNS (查号台)
</div> </div>
<div class="desc"> <div class="desc">
正在查 shop.com 位置... 正在查 bilibili.com IP...
</div> </div>
</div> </div>
<div class="arrow-down"> <div class="arrow-down">
@@ -41,9 +41,9 @@
</div> </div>
<div class="output-area"> <div class="output-area">
<span class="label">GPS 坐标 (IP 地址)</span> <span class="label">精准坐标 (IP 地址)</span>
<div class="fake-output"> <div class="fake-output">
93.184.216.34 110.43.12.55
</div> </div>
</div> </div>
</div> </div>
@@ -169,20 +169,20 @@ const props = defineProps({
const t = { const t = {
send: '提交订单 (发送请求)', send: '提交订单 (发送请求)',
noRequests: '购物车是空的 (无请求)', noRequests: '还没发请求 (网络空闲)',
placeholder: '点击 "提交订单" 向店员购买玩具', placeholder: '点击 "提交订单" 向服务器索要页面',
general: '订单详情 (General)', general: '请求概要 (General)',
requestUrl: '商品地址 (URL)', requestUrl: '目标地址 (URL)',
requestMethod: '操作类型 (Method)', requestMethod: '操作类型 (Method)',
statusCode: '店员回复 (Status)', statusCode: '服务器回复状态 (Status)',
responseHeaders: '包裹标签 (Headers)', responseHeaders: '包裹标签 / 补充说明 (Headers)',
tabs: { tabs: {
headers: '订单信息', headers: '头部信息(Headers)',
response: '包裹内容', response: '代码内容(Response)',
preview: '玩具预览' preview: '大致预览(Preview)'
}, },
cols: { cols: {
name: '商品', name: '请求体',
status: '状态', status: '状态',
type: '类型', type: '类型',
time: '耗时' time: '耗时'
@@ -190,7 +190,7 @@ const t = {
} }
const method = ref('GET') const method = ref('GET')
const path = ref('/toys/lego-castle') const path = ref('/video/BV1xx411c7mD')
const loading = ref(false) const loading = ref(false)
const requestSent = ref(false) const requestSent = ref(false)
const activeTab = ref('headers') const activeTab = ref('headers')
@@ -210,20 +210,33 @@ const sendRequest = async () => {
loading.value = false loading.value = false
if (method.value === 'GET') { if (method.value === 'GET') {
responseStatus.value = '200 OK (有货)' responseStatus.value = '200 OK (交易成功)'
responseHeaders.value = { responseHeaders.value = {
'Content-Type': 'application/json (积木)', 'Content-Type': 'text/html; charset=utf-8',
Date: new Date().toLocaleString(), 'Server': 'BWS/1.1 (Bilibili Web Server)',
Store: '乐高官方店' 'Date': new Date().toUTCString()
} }
responseBody.value = `{\n "id": 101,\n "name": "Lego Castle",\n "pieces": 500,\n "price": "$99"\n}` responseBody.value = `<!DOCTYPE html>
<html>
<head>
<title>B站超级搞笑的猫咪合集</title>
</head>
<body>
<h1>超级搞笑的猫咪合集</h1>
<div class="player">
<img src="cat_cover.jpg" alt="封面" />
<button> 播放</button>
</div>
</body>
</html>`
} else { } else {
responseStatus.value = '201 Created (下单成功)' responseStatus.value = '201 Created (操作成功)'
responseHeaders.value = { responseHeaders.value = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Date: new Date().toLocaleString() 'Server': 'BWS/1.1',
'Date': new Date().toUTCString()
} }
responseBody.value = `{\n "success": true,\n "message": "Order placed"\n}` responseBody.value = `{\n "success": true,\n "message": "点赞成功!"\n}`
} }
} }
@@ -139,21 +139,21 @@ const props = defineProps({
// Bilingual text directly // Bilingual text directly
const t = { const t = {
statusLabel: '通话状态', statusLabel: '连接状态',
connect: '拨打电话', connect: '建立连接',
reset: '挂断重拨', reset: '断开重连',
client: '我 (顾客)', client: '我 (浏览器)',
server: '玩具店', server: '对面 (B站服务器)',
status: { status: {
closed: '未通话', closed: '未连接',
handshaking: '正在拨号...', handshaking: '正在打招呼确认通道...',
established: '通话中 (连接已建立)' established: 'TCP 通道已建立 (ESTABLISHED)'
}, },
steps: { steps: {
0: '点击 "拨打电话" 开始确认店铺是否营业。', 0: '点击 "建立连接" 开始三次握手(电话试音)。',
1: '步骤 1: 我问 "喂?有人在吗?" (SYN)', 1: '第一次握手: "喂,服务器老哥在吗?我能发信息,你能收到吗?" (SYN)',
2: '步骤 2: 店员答 "在的!请问有什么事" (SYN-ACK)', 2: '第二次握手: "喂喂在的!我收到了!那你现在能听到我说话吗" (SYN-ACK)',
3: '步骤 3: 我说 "太好了,我想买东西" (ACK)' 3: '第三次握手: "妥了,我也能听到!通道没问题,准备看视频" (ACK)'
} }
} }
@@ -109,14 +109,14 @@ const props = defineProps({
} }
}) })
const inputUrl = ref('https://shop.com/toys/lego-castle?color=red#summary') const inputUrl = ref('https://www.bilibili.com/video/BV1xx411c7mD?t=60#comments')
const highlightedPart = ref(null) const highlightedPart = ref(null)
const icons = { const icons = {
protocol: '🚛', protocol: '🚛',
host: '🏢', host: '🏢',
port: '🚪', port: '🚪',
pathname: '🧸', pathname: '📺',
search: '📝', search: '📝',
hash: '📍' hash: '📍'
} }
@@ -125,18 +125,18 @@ const labels = {
protocol: '交通方式 (Protocol)', protocol: '交通方式 (Protocol)',
host: '店铺地址 (Host)', host: '店铺地址 (Host)',
port: '大门号 (Port)', port: '大门号 (Port)',
pathname: '商品位置 (Path)', pathname: '具体货架 (Path)',
search: '备注要求 (Query)', search: '特殊要求 (Search/Query)',
hash: '快速定位 (Hash)' hash: '直接跳转 (Hash)'
} }
const descriptions = { const descriptions = {
protocol: '怎么去?(例如 https = 坐装甲车去,安全)', protocol: '怎么去?(https = 坐押运车去,比 http 安全)',
host: '去哪家店?(域名例如 shop.com)', host: '去哪家店?(域名例如 www.bilibili.com)',
port: '哪个门(默认 443 号)', port: '哪个门?(默认隐藏了 443 端口号)',
pathname: '商品在哪个货架?(路径)', pathname: '拿什么货?(去 /video 区拿编号为 BV... 的视频)',
search: '给店员的备注 (例如 ?color=red)', search: '给店员的备注说明 (例如 ?t=60 表示要求从 60 秒开始看)',
hash: '直接翻到说明书第几页 (锚点)' hash: '拿到货后自己做的事 (例如滚动到评论区 #comments)'
} }
const parsedUrl = computed(() => { const parsedUrl = computed(() => {
+12 -4
View File
@@ -44,6 +44,8 @@ import ApiPlayground from './components/appendix/api-intro/ApiPlayground.vue'
import RealWorldApiDemo from './components/appendix/api-intro/RealWorldApiDemo.vue' import RealWorldApiDemo from './components/appendix/api-intro/RealWorldApiDemo.vue'
import FunctionApiDemo from './components/appendix/api-intro/FunctionApiDemo.vue' import FunctionApiDemo from './components/appendix/api-intro/FunctionApiDemo.vue'
import ApiTypesComparison from './components/appendix/api-intro/ApiTypesComparison.vue' import ApiTypesComparison from './components/appendix/api-intro/ApiTypesComparison.vue'
import ApiFunctionVsHttp from './components/appendix/api-intro/ApiFunctionVsHttp.vue'
import DocumentTypesComparison from './components/appendix/api-intro/DocumentTypesComparison.vue'
import HttpMethodsDemo from './components/appendix/api-intro/HttpMethodsDemo.vue' import HttpMethodsDemo from './components/appendix/api-intro/HttpMethodsDemo.vue'
import StatusCodeCategories from './components/appendix/api-intro/StatusCodeCategories.vue' import StatusCodeCategories from './components/appendix/api-intro/StatusCodeCategories.vue'
@@ -110,6 +112,7 @@ import NetworkTroubleshooting from './components/appendix/web-basics/NetworkTrou
// Computer Fundamentals Components // Computer Fundamentals Components
import TransistorDemo from './components/appendix/computer-fundamentals/TransistorDemo.vue' import TransistorDemo from './components/appendix/computer-fundamentals/TransistorDemo.vue'
import LogicGateDemo from './components/appendix/computer-fundamentals/LogicGateDemo.vue' import LogicGateDemo from './components/appendix/computer-fundamentals/LogicGateDemo.vue'
import BinaryAdditionRulesDemo from './components/appendix/computer-fundamentals/BinaryAdditionRulesDemo.vue'
import HalfAdderDemo from './components/appendix/computer-fundamentals/HalfAdderDemo.vue' import HalfAdderDemo from './components/appendix/computer-fundamentals/HalfAdderDemo.vue'
import FullAdderDemo from './components/appendix/computer-fundamentals/FullAdderDemo.vue' import FullAdderDemo from './components/appendix/computer-fundamentals/FullAdderDemo.vue'
import AdderDemo from './components/appendix/computer-fundamentals/AdderDemo.vue' import AdderDemo from './components/appendix/computer-fundamentals/AdderDemo.vue'
@@ -118,6 +121,7 @@ import CompleteAdderDemo from './components/appendix/computer-fundamentals/Compl
import FunctionalUnitDemo from './components/appendix/computer-fundamentals/FunctionalUnitDemo.vue' import FunctionalUnitDemo from './components/appendix/computer-fundamentals/FunctionalUnitDemo.vue'
import CpuArchitectureDemo from './components/appendix/computer-fundamentals/CpuArchitectureDemo.vue' import CpuArchitectureDemo from './components/appendix/computer-fundamentals/CpuArchitectureDemo.vue'
import RegisterDemo from './components/appendix/computer-fundamentals/RegisterDemo.vue' import RegisterDemo from './components/appendix/computer-fundamentals/RegisterDemo.vue'
import FlipFlopDemo from './components/appendix/computer-fundamentals/FlipFlopDemo.vue'
// import EvolutionFlowDemo from './components/appendix/computer-fundamentals/EvolutionFlowDemo.vue' // import EvolutionFlowDemo from './components/appendix/computer-fundamentals/EvolutionFlowDemo.vue'
import ProcessDemo from './components/appendix/computer-fundamentals/ProcessDemo.vue' import ProcessDemo from './components/appendix/computer-fundamentals/ProcessDemo.vue'
import MemoryDemo from './components/appendix/computer-fundamentals/MemoryDemo.vue' import MemoryDemo from './components/appendix/computer-fundamentals/MemoryDemo.vue'
@@ -135,8 +139,8 @@ import CFSubnetCalculator from './components/appendix/computer-fundamentals/Subn
import CFTcpUdpComparison from './components/appendix/computer-fundamentals/TcpUdpComparison.vue' import CFTcpUdpComparison from './components/appendix/computer-fundamentals/TcpUdpComparison.vue'
// Computer Fundamentals Additional Components // Computer Fundamentals Additional Components
import OSSystemOverviewDemo from './components/appendix/computer-fundamentals/OSSystemOverviewDemo.vue' import OSArchitectureDemo from './components/appendix/computer-fundamentals/OSArchitectureDemo.vue'
import ProcessMemoryFilesystemDemo from './components/appendix/computer-fundamentals/ProcessMemoryFilesystemDemo.vue' import ProgramLaunchDemo from './components/appendix/computer-fundamentals/ProgramLaunchDemo.vue'
import DataLifecycleDemo from './components/appendix/computer-fundamentals/DataLifecycleDemo.vue' import DataLifecycleDemo from './components/appendix/computer-fundamentals/DataLifecycleDemo.vue'
import EncodingStorageTransmissionDemo from './components/appendix/computer-fundamentals/EncodingStorageTransmissionDemo.vue' import EncodingStorageTransmissionDemo from './components/appendix/computer-fundamentals/EncodingStorageTransmissionDemo.vue'
import NetworkOverviewDemo from './components/appendix/computer-fundamentals/NetworkOverviewDemo.vue' import NetworkOverviewDemo from './components/appendix/computer-fundamentals/NetworkOverviewDemo.vue'
@@ -673,6 +677,8 @@ export default {
app.component('RealWorldApiDemo', RealWorldApiDemo) app.component('RealWorldApiDemo', RealWorldApiDemo)
app.component('FunctionApiDemo', FunctionApiDemo) app.component('FunctionApiDemo', FunctionApiDemo)
app.component('ApiTypesComparison', ApiTypesComparison) app.component('ApiTypesComparison', ApiTypesComparison)
app.component('ApiFunctionVsHttp', ApiFunctionVsHttp)
app.component('DocumentTypesComparison', DocumentTypesComparison)
app.component('HttpMethodsDemo', HttpMethodsDemo) app.component('HttpMethodsDemo', HttpMethodsDemo)
app.component('StatusCodeCategories', StatusCodeCategories) app.component('StatusCodeCategories', StatusCodeCategories)
@@ -742,6 +748,7 @@ export default {
// Computer Fundamentals Components Registration // Computer Fundamentals Components Registration
app.component('TransistorDemo', TransistorDemo) app.component('TransistorDemo', TransistorDemo)
app.component('LogicGateDemo', LogicGateDemo) app.component('LogicGateDemo', LogicGateDemo)
app.component('BinaryAdditionRulesDemo', BinaryAdditionRulesDemo)
app.component('HalfAdderDemo', HalfAdderDemo) app.component('HalfAdderDemo', HalfAdderDemo)
app.component('FullAdderDemo', FullAdderDemo) app.component('FullAdderDemo', FullAdderDemo)
app.component('AdderDemo', AdderDemo) app.component('AdderDemo', AdderDemo)
@@ -750,6 +757,7 @@ export default {
app.component('FunctionalUnitDemo', FunctionalUnitDemo) app.component('FunctionalUnitDemo', FunctionalUnitDemo)
app.component('CpuArchitectureDemo', CpuArchitectureDemo) app.component('CpuArchitectureDemo', CpuArchitectureDemo)
app.component('RegisterDemo', RegisterDemo) app.component('RegisterDemo', RegisterDemo)
app.component('FlipFlopDemo', FlipFlopDemo)
// app.component('EvolutionFlowDemo', EvolutionFlowDemo) // app.component('EvolutionFlowDemo', EvolutionFlowDemo)
app.component('ProcessDemo', ProcessDemo) app.component('ProcessDemo', ProcessDemo)
app.component('MemoryDemo', MemoryDemo) app.component('MemoryDemo', MemoryDemo)
@@ -767,8 +775,8 @@ export default {
app.component('CFTcpUdpComparison', CFTcpUdpComparison) app.component('CFTcpUdpComparison', CFTcpUdpComparison)
// Computer Fundamentals Additional Components Registration // Computer Fundamentals Additional Components Registration
app.component('OSSystemOverviewDemo', OSSystemOverviewDemo) app.component('OSArchitectureDemo', OSArchitectureDemo)
app.component('ProcessMemoryFilesystemDemo', ProcessMemoryFilesystemDemo) app.component('ProgramLaunchDemo', ProgramLaunchDemo)
app.component('DataLifecycleDemo', DataLifecycleDemo) app.component('DataLifecycleDemo', DataLifecycleDemo)
app.component( app.component(
'EncodingStorageTransmissionDemo', 'EncodingStorageTransmissionDemo',
@@ -1,84 +1,224 @@
# 计算机网络:从输入网址到返回结果的过程 # 计算机网络:从输入网址到返回结果的过程
::: tip 🎯 核心问题 ::: tip 🎯 核心问题
**当你浏览器输入 www.google.com 并按下回车,到底发生了什么** **当你舒服地靠在沙发上,在手机浏览器输入 `www.google.com` 并按下回车,为什么几百毫秒后,搜索结果就能准确无误地出现在你的屏幕上**
这个看似简单的动作,背后隐藏着一个庞大精密的跨国“快递系统”。从填写订单(URL解析)到查询地址簿(DNS解析),从建立运输通道(TCP握手)到快递员送货(HTTP请求与响应),最终在你屏幕上拆开包裹组装(浏览器渲染)。本章带你零基础、完整理解这个神奇的过程。 在上一章中,我们知道了数据是如何被编码成 0 和 1 并通过海底光缆传输的。但这还不够。互联网上的服务器浩如烟海,你的手机是怎么在茫茫机海中精准找到 Google 的服务器,商量好暗号,并成功把页面要回来的呢?
这个看似无比简单的"敲回车"动作,背后其实隐藏着一个精密到令人震撼的跨国"快递接力系统"。本章,我们不讲枯燥的八股文概念,而是顺着**"填写购物单 -> 查地址簿 -> 打电话确认 -> 寄包裹 -> 自己拆解组装"**这条主线,带你零基础看清网络世界的全貌。
::: :::
--- ---
## 全景演示:网络世界的快递系统 ## 第一步:填写购物单 (URL 解析)
你可以通过下方的交互组件,直观地体验从输入网址到看到网页的 5 个关键步骤。先自己点一点,然后再看底下的详细解释! **目标**:把人类能看懂的网址,翻译成浏览器能理解的结构化信息。
<UrlToBrowserDemo /> 当你在地址栏中输入 `https://www.google.com/search` 时,浏览器第一步必须先把你输入的这段"人类文字",仔细拆解成它能看懂的标准化字段。
这就像是你准备去商店买东西,首先要在**购物单**上写清楚:用什么交通工具去、去哪家店、拿什么货。
<UrlParserDemo />
**💡 核心原理解析:URL是怎么分工的?**
- **交通方式(Protocol/协议)**:比如开头写的 `https://`。这代表你要求坐安全级别最高的"运钞车"(加密通信)去。如果是老式的 `http://`,就相当于坐敞篷车,你一路上买什么都会被别人看光。
- **店铺名(Host/主机名)**:比如 `www.google.com`。这就是你要去哪家店(也就是服务器的域名)。
- **具体货架(Path/路径)**:比如后面的 `/search`。这代表进了店门之后,你要去哪个房间拿具体的哪份文件。
**这一步完成了什么?** 浏览器现在知道了:我要用 HTTPS 协议,去 `www.google.com` 这个域名对应的服务器,获取 `/search` 路径下的内容。
**但问题来了**:浏览器知道了域名,但网络世界只认数字 IP 地址。就像你知道"王府井大饭店",但司机需要 GPS 坐标。下一步,我们需要把域名转换成 IP 地址。
--- ---
## 1. 填写购物单 (URL 解析) ## 第二步:查地址簿 (DNS 解析)
当你在浏览器的地址栏中输入 `https://www.google.com` 这样一段地址并按下回车,这就像是你准备去商店买东西,首先要在**购物单**上写清楚: **上一步完成了**:浏览器拆解了 URL,知道了目标域名是 `www.google.com`
- **交通方式 (Protocol)**:例如 `https://`,代表你想坐安全级别的最高的“运钞车”(加密通信)去。如果是单纯的 `http://`,就相当于坐普通的“大巴”(明文传输),路上可能会被人偷看行李 **这一步要实现**:把域名转换成 IP 地址,让浏览器知道服务器的精确位置
- **店铺地址 (Host)**:例如 `www.google.com`,也就是你要去哪家店(域名)。
- **商品位置 (Path)**:例如 `/search`,意思是进了商店之后,你要去哪个货架找什么东西(即请求的具体资源路径)。
浏览器第一步要做的,就是把这段“人类语言”拆解开,看看你到底想要什么 **目的**:网络世界的底层路由器(负责指路的交警)根本不懂英文,它们**只认数字**,也就是所谓的 **IP 地址(如 142.250.80.46**
<DnsLookupDemo />
**💡 核心原理解析:找"114查号台"**
既然必须用 IP 地址,浏览器就会走一个叫做 **DNS (Domain Name System)** 的打听流程:
1. **翻自己的备忘录(本地缓存)**:浏览器会先翻翻自己的浏览历史,看看前几天是不是刚去过这家店,记没记过它的数字地址。如果记了,直接用。
2. **打电话给查号台(递归查询)**:如果实在没见过,它就会向互联网的"总查号台"(通常由你的宽带运营商提供,比如联通、电信的 DNS 服务器)发请求:"你好,请帮我查一下,google.com 对应的数字坐标是几?"
3. **拿到坐标**:查号台通过逐级查询,最终把一个准确的 IP 地址(如 `142.250.80.46`)发回给你的手机。
**这一步完成了什么?** 浏览器现在拿到了 Google 服务器的精确 IP 地址 `142.250.80.46`
**但问题来了**:有了 IP 地址就能直接发请求了吗?万一服务器宕机了呢?万一网线断了呢?如果直接发请求,对方没收到,就成了鸡同鸭讲。下一步,我们需要先确认双方能正常通信。
--- ---
## 2. 查找店铺地址 (DNS 解析) ## 第三步:打电话确认 (TCP 三次握手)
网络世界的“快递员”(路由器设备)是不懂英文的,它们只认数字(也就是 **IP 地址** **上一步完成了**:浏览器通过 DNS 查询,拿到了服务器的 IP 地址 `142.250.80.46`
它们需要知道对方的精确数字坐标!这就像快递员不知道“王府井百货”在哪,他必须先查地图,找到“北京市东城区王府井大街255号”这个确切的门牌号(比如 `142.250.66.4` **这一步要实现**:建立一条可靠的通信通道,确保双方都能收发数据
- **本地缓存**:浏览器会先翻翻自己的备忘录(看之前有没有访问过该网站)。 **目的**:在正式传输数据之前,必须先确认"对方在线"且"双方收发通道都正常"。这就像打电话前要先确认"喂,能听到吗?"
- **DNS 系统**:如果在本地找不到,它就会向互联网的“查号台”(DNS 服务器)打电话询问:“请问 google.com 的数字地址是什么?”。一旦获得了对应的 IP 地址,浏览器的快递车就知道该往哪里开了。
<TcpHandshakeDemo />
**💡 核心原理解析:为什么非得是"三"次?**
不要被专业名词吓到,它完全可以在现实生活中还原。想象一下你给朋友打电话:
--- ---
## 3. 建立通话 (TCP 握手) ### 第一次握手:SYN(同步请求)
拿到了地址,浏览器不能直接冲过去,万一店今天没开门呢?所以,要先进行一次**“电话确认”**(这叫建立 TCP 连接)。为了确保通话稳定可靠,会有三次非常严谨的“确认打招呼”机制,行业里叫**三次握手 (Three-way Handshake)** **浏览器发送 SYN 包**
- **第一次握手 (浏览器)**:“喂,你好,我要来买东西,你在吗?” (SYN) 就像你拨通朋友电话后说的第一句话:"喂,你好,能听到我说话吗?"
- **第二次握手 (服务器)**:“我在的,欢迎光临!你也听得到我说话吗?” (SYN-ACK)
- **第三次握手 (浏览器)**:“我也听到了!那我就要过来了!” (ACK)
经过这三次确认,双方都知道了彼此的听力和表达能力都没问题,一条稳定可靠的通信通道就正式建立了。 - **SYN****Synchronize**(同步)的缩写
- 浏览器生成一个随机数字(比如 `Seq = 100`),告诉服务器:"我要开始建立连接了,我的初始序号是 100"
- 这个序号用来标记后续发送的数据顺序,防止乱序
**这一步确认了什么?** 服务器收到了浏览器的消息 → 浏览器的**发送通道**正常。
--- ---
## 4. 购买商品 (HTTP 请求与响应) ### 第二次握手:SYN-ACK(同步+确认)
通道建好后,业务正式开始。 **服务器回复 SYN-ACK 包**
- **浏览器(买家)提交订单**:浏览器会打包一份极其规范的订单表格(**HTTP 请求报文**),里面写着:“老板,请给我拿一份你的主页 HTML 文件,我是用 Chrome 浏览器来访问的哦。” 就像朋友回答:"喂喂,我能听到你!你也能听到我吗?"
- **服务器(卖家)根据订单发货**:位于地球另一端的 Google 服务器收到请求后,立刻开始在仓库里配货,生成网页的 HTML 代码,然后打包成包裹(**HTTP 响应报文**),发回给你的浏览器。包裹外面还会贴个标签“200 OK”,意思是“交易成功,你要的货全齐了”。
- **SYN-ACK** = **Synchronize + Acknowledge**(同步+确认)
- 服务器做两件事:
1. **ACK**:确认收到浏览器的消息(`Ack = 101`,表示"我期待收到你序号为 101 的下一个包")
2. **SYN**:服务器也生成自己的随机序号(比如 `Seq = 200`),告诉浏览器:"我的初始序号是 200"
**这一步确认了什么?** 浏览器收到了服务器的回复 → 服务器的**发送通道**正常,浏览器的**接收通道**正常。
--- ---
## 5. 拆盒组装 (浏览器渲染) ### 第三次握手:ACK(确认)
最后一步,货物送到了你的电脑。但发过来的只是一堆代码(HTML、CSS、JavaScript),这就好比你网购买了一箱乐高积木,还需要自己组装: **浏览器回复 ACK 包**
1. **看说明书 (解析 HTML)**:浏览器先把 HTML 代码解读出来,拼装成网页的骨架(DOM 树)。 就像你回答:"能听到!那我们开始聊正事吧!"
2. **涂抹颜色 (解析 CSS)**:然后检查 CSS 代码,看看字体要多大、按钮是什么颜色,给网页穿上漂亮的外衣(CSSOM 树)。
3. **计算布局并拼装 (Layout & Paint)**:浏览器计算好每个元素在屏幕上的确切位置,用画笔把它们画在你的显示器上。
4. **注入灵魂 (执行 JavaScript)**:最后,各种能点击、能滑动的交互效果都通过 JavaScript 激活。
**只要短短的几百毫秒,所有的步骤就已全部完成,你也就看到了那个熟悉的页面!** - **ACK****Acknowledge**(确认)的缩写
- 浏览器回复:`Ack = 201`,表示"我期待收到你序号为 201 的下一个包"
**这一步确认了什么?** 服务器收到了浏览器的确认 → 服务器的**接收通道**也正常。
--- ---
## 总结:从微观到宏观 ### 为什么必须是三次?两次行不行?
如果我们把目光再拉远一点,整个网络通讯的本质,就是在做**接力跑和翻译** **假设只有两次握手:**
- 我们上面看到的这五步,大多是发生在你眼前的**应用程序**层面的事情。 1. 浏览器:"喂,能听到吗?"
- 在肉眼看不见的底层,刚才那个充满代码的 HTML 包裹,会被切分成无数块极小的碎片(数据包)。这些碎片顺着你家墙上的网线、海底的万兆光缆,像接力棒一样在各种路由器之间传递。 2. 服务器:"能听到!"
- 最终,这一切碎片完好无损地抵达,并在哪怕是几十个毫秒的时间里,化成你屏幕上的绚丽像素。
就是计算机网络的神奇魅力! 时候服务器以为连接建立了,开始发送数据。但如果服务器的回复在半路丢了,浏览器根本没收到,浏览器就不会认为连接建立成功,也不会处理服务器发来的数据。
**结果**:服务器单方面认为连接已建立,疯狂发数据,但浏览器全当垃圾丢弃。服务器资源被白白浪费。
**三次握手的精妙之处**
第三次握手的 ACK 包,**证明了浏览器确实收到了服务器的回复**。只有浏览器收到了,才会回复 ACK;服务器收到了这个 ACK,才能**100%确定**双方通道都是通的。
这就像打电话时的完整确认:
- 你:"喂,能听到吗?"SYN
- 朋友:"能听到,你呢?"SYN-ACK
- 你:"我也能听到!"ACK
**这一步完成了什么?** 浏览器和服务器都确认了:**我能发给你,我能收到你的,你也能发给我,你也能收到我的**。一条可靠的 TCP 通道正式建立!
**现在可以开始了吗?** 通道已建立,下一步就是正式发送请求,获取网页内容。
---
## 第四步:寄包裹 (HTTP 请求与响应)
**上一步完成了**:通过 TCP 三次握手,建立了可靠的通信通道。
**这一步要实现**:正式发送请求,获取网页内容。
**目的**:浏览器向服务器"下单",服务器返回"货物"(网页内容)。
<HttpExchangeDemo />
**💡 核心原理解析:HTTP 请求与响应的小纸条**
浏览器会把你刚才写好的购物单,按照一种极为规范的格式打包(这叫 **HTTP 请求头**),正式塞进刚才建立好的 TCP 通道里,发给服务器。
- **买方发纸条(HTTP Request**
浏览器发出的包裹里,写着大写的请求指令。如果是看网页就是 `GET`,如果是提交账号密码登录就是 `POST`。不仅如此,这张纸条里还附带了一些重要情报:"嗨,我是用 Mac 电脑的 Chrome 浏览器访问的哦,另外我只能听懂中文,请把给我的货也转换成中文。"(这些补充说明就被叫做 **请求 Headers**)。
- **卖方发纸条(HTTP Response**
位于千里之外的服务器收到这包东西后,看了一眼:"哦,他要 `GET` 这个页面啊"。于是服务器飞速在自己的硬盘里找到相应的 HTML 网页代码打包好,在包裹最外面贴上一个标签:`200 OK`(意思是交易非常成功,你要的货全齐了),然后借由同一个通道,原路寄回给你的电脑。
> **小科普**:如果是找不到你要找得页面,服务器就会贴个 `404 Not Found` 的悲伤标签给你退回来。如果是服务器自己代码写错了挂掉了,就会贴个 `500 Server Error` 的崩溃标签。
**这一步完成了什么?** 浏览器收到了服务器返回的 HTML、CSS、JavaScript 代码(也就是网页的"原材料")。
**但问题来了**:这些代码只是文本,还不是你能看到的网页画面。下一步,浏览器需要把这些代码"翻译"成屏幕上的像素。
---
## 第五步:拆解组装 (浏览器渲染)
**上一步完成了**:通过 HTTP 请求,浏览器获取了网页的源代码(HTML、CSS、JavaScript)。
**这一步要实现**:把代码转换成屏幕上可见的网页画面。
**目的**:将文本代码"翻译"成像素,让用户看到最终的网页。
<BrowserRenderingDemo />
**💡 核心原理解析:毫秒级的画家**
此时你电脑收到的,仅仅是一大串干瘪枯燥的文本代码(HTML 骨架、CSS 色彩图纸、JS 交互动效代码)。这就像你网购了一箱子乐高,它给你的只有几千个塑料零件和一本极度复杂的说明书。
浏览器的组装过程堪比惊心动魄的全自动工厂流水线:
1. **搭骨架 (DOM 解析)**:工人先把 HTML 文件通读一遍,理清楚网页的结构。比如"这里要有一个标题框,那里要有三个图片框"。这个骨架叫做 DOM 树。
2. **上颜色 (CSS 解析)**:紧接着看 CSS 文件,"哦,老王说标题框必须是红色的,图片框必须有圆角。"
3. **几何计算排版 (Layout)**:结合骨架和颜色后,开始拿尺子计算。因为每个人的屏幕大小不一样,同样是三个图片框,在手机上只能竖着放,在电脑上可以横着放。必须计算出每一个像素块极其精确的摆放坐标。
4. **上色绘制 (Paint)**:最后拿起了画笔,按照前面算出来的精确设计图,把真真切切的颜色和像素渲染到了你的显示器上!
**这一步完成了什么?** 浏览器把代码转换成了屏幕上的像素,用户终于看到了完整的网页!
---
## 完整流程回顾
让我们把整个过程串起来:
| 步骤 | 完成了什么 | 下一步需要什么 |
|------|-----------|---------------|
| **1. URL 解析** | 拆解网址,知道要去哪 | 需要把域名转成 IP |
| **2. DNS 解析** | 拿到服务器 IP 地址 | 需要确认服务器在线 |
| **3. TCP 握手** | 建立可靠通信通道 | 需要发送正式请求 |
| **4. HTTP 交换** | 获取网页源代码 | 需要把代码转成画面 |
| **5. 浏览器渲染** | 把代码渲染成像素 | ✅ 用户看到网页! |
---
## 结语:0.5 秒里发生了什么
敲下回车,等上半秒,页面就跳出来了——我们早就习惯了这个速度,甚至觉得慢。
但仔细想想,就在这眨眼的功夫里:
- **第一步**:浏览器把你输入的网址拆开看懂
- **第二步**:跑去问了好多台服务器才要到 IP 地址
- **第三步**:跟大洋彼岸的服务器来回确认了三次"能听见吗"
- **第四步**:把请求打包发过去,再等着收回来
- **第五步**:最后还要把成千上万行代码瞬间组装成你能看到的画面
这些步骤一环扣一环,**前一步的输出是后一步的输入**,中间哪个环节出问题,页面就打不开。而那些路由器、服务器、光缆,就默默在后台 24 小时运转,保证你每次滑动手机时,内容都能准时出现。
下次等网页加载的时候,或许可以想想:这 0.5 秒,其实挺忙的。
@@ -2,98 +2,90 @@
::: tip 🎯 核心问题 ::: tip 🎯 核心问题
**有了完美的 CPU 和无限的内存,电脑就能直接用了吗?** **有了完美的 CPU 和无限的内存,电脑就能直接用了吗?**
在上一章,我们见证了晶体管如何组合成强大的 CPU。但其实,如果直接使用这些冷冰冰的硬件,哪怕只是想在屏幕上打出一个字母,你都需要写几百行晦涩的机器指令。 在上一章,我们见证了晶体管如何组合成强大的 CPU。但即使你拥有最顶级的硬件,如果直接让它们工作,连在屏幕上显示一个字母都需要写几百行晦涩的机器指令。不仅麻烦,还极其危险——稍有差池,你的代码就可能把别人的数据覆盖掉。
为了不让大家在每次用电脑时都被逼疯,前辈们创造了一个夹在“硬件”和“你”之间的超级管家——**操作系统(Operating System, 简称 OS)**。本章我们不谈深奥的理论,只聊聊这个大管家是怎么通过三大“障眼法”,把复杂的硬件调教得服服帖帖的。 为了解决这些噩梦,**操作系统(Operating System, 简称 OS)**诞生了。它是挡在你和冰冷硬件之间的一层最伟大的"软件"。本章我们将抛开深奥的代码,用通俗的比喻,看看这个"超级管家"是如何把杂乱无章的硬件调教得服服帖帖的。
::: :::
--- ---
## 0. 承上启下:如果没有操作系统会怎样? ## 0. 全景图:没有操作系统会怎样?
上一章我们提到,CPU 是一个不知疲倦的无情计算机器,通电后就会一行一行地执行指令 想象一下,你开了一家极具潜力的"计算工厂"(你的电脑),厂里有一个全能、不知疲倦的顶级干将(CPU),还有一片巨大的仓库(内存)和无数的集装箱(硬盘)
但这带来了几个现实的灾难 如果你**不雇佣**一个厂长(操作系统)来管理
1. **CPU 独占危机**CPU 一次只能干一件事。如果你正在听歌,想切出去看个网页?抱歉,没有操作系统的调度,你的电脑必须停下音乐,才能去加载网页 1. **CPU 独占危机**CPU 一次只能干一件事。如果有人在用它听歌,其他任何人想看网页?抱歉,大家必须排队等听歌的人主动把 CPU 让出来
2. **内存踩踏事故**:微信和游戏都在使用内存。如果没有保安管理,游戏一不小心把数据到了微信的内存地盘,微信当场崩溃。 2. **内存踩踏事故**:微信和游戏都在使用仓库(内存。如果没有保安规划区域,游戏一不小心把装备数据到了微信的盒子里,微信直接当场崩溃。
3. **硬盘迷宫**:硬盘本质上只是一张密密麻麻刻满 0 和 1 的巨大光盘。要想找到昨天存的照片,你必须准确记住它存放在第 12345 圈磁道678 扇区。 3. **硬盘迷宫**:硬盘硬件只是一张刻满 0 和 1 的巨大光盘。要想找到昨天存的照片,你必须准确记住它存放在"第 1 盘面、第 56 磁道第 8 扇区",没人能记住这种反人类的坐标
为了解决这些噩梦,操作系统诞生了。它对外提供了一套优雅的“幻觉”,这就是它的三大核心魔法:**进程(管理 CPU)**、**虚拟内存(管理内存)** 和 **文件系统(管理硬盘)** <OSArchitectureDemo />
为了解决上述的三大噩梦,操作系统祭出了它的三板斧:**进程管理**、**内存管理**和**文件系统**。
--- ---
## 1. 进程管理:制造“同时运行”的幻觉 ## 1. 进程管理:CPU 的时分复用
你平时用电脑,常常是一边挂着微信,一边听着音乐,还能一边打字。但如果你买的电脑其实只有一个 CPU 核心,它是怎么同时做这三件事的? 你平时用电脑,常常是一边挂着微信,一边听着音乐,还能一边打字。但如果你买的电脑其实只有一个 CPU 核心,它是怎么同时做这三件事的?
答案是:**它并没有同时做**。是操作系统在进行疯狂的时间管理”。 答案是:**它并没有同时做。是操作系统在进行疯狂的"时间管理"。**
<ProcessDemo /> <ProcessDemo />
::: tip 💡 核心原理解析:时间片轮转(Time Slicing ### 1.1 什么是"进程"
操作系统把 CPU 的时间切成了极其微小的片段(比如 10 毫秒) 每一个正在运行的程序,就被称为一个**进程**。你可以把它理解为一个"项目组",有自己的代码(做事清单)、自己的内存数据(项目资金),排着队等待 CPU 接见
- 第 1-10 毫秒:让 CPU 去执行**微信**的接收消息逻辑。
- 第 11-20 毫秒:把微信强制暂停,让 CPU 去执行**音乐**的播放逻辑。
- 第 21-30 毫秒:把音乐暂停,让 CPU 去响应你的**键盘打字**。
因为切换的速度实在太快了(一秒钟切换成百上千次),在人类迟钝的感知中,就觉得这三个软件是“同时”在运行的。 ### 1.2 时间片轮转
为了不让某个流氓软件一直霸占 CPU,操作系统把 CPU 的时间切成极小的片段(约 10 毫秒),轮流分配给各个进程。因为切换速度太快了,你感觉是"同时运行"。
在操作系统的术语里,运行中的程序就被称为**进程(Process)**。操作系统就是这群进程的冷酷无情的排班经理。
:::
--- ---
## 2. 内存管理:给每个程序画个“海市蜃楼” ## 2. 内存管理:虚拟地址空间
解决了 CPU 轮流用的问题,接下来是存放数据的内存。如果所有的进程都挤在同一块物理内存里,很容易发生互相干扰和偷看数据的危险 解决了 CPU 轮流用的问题,接下来是内存空间。如果不加管理,所有软件都直接往物理内存条写数据,必然会发生**互相覆盖**的踩踏惨剧
操作系统的第二大魔法,叫作**虚拟内存(Virtual Memory**。
<MemoryDemo /> <MemoryDemo />
::: tip 💡 核心原理解析:内存映射 ### 2.1 虚拟内存(Virtual Memory
操作系统对每一个启动的进程撒了一个弥天大谎:嘿,你独占了整整 4GB 的纯净内存空间,随便用!”(这就是**虚拟内存**)。 操作系统对每一个进程撒了一个大谎:"嘿,你独占了整台电脑所有的可用内存,随便用!"
但实际上,当进程往这个“虚拟空间”里放东西时,操作系统的底层会拿出一个**映射表(页表)**,偷偷把数据塞进**真实物理内存(Physical Memory)**中各种零碎、不连续的角落里 在进程眼里,自己的内存条永远是**连续**且**干净**的。它心安理得地往里面写数据
**这么做有两个巨大的好处:** ### 2.2 页表映射(Page Table
1. **绝对安全**:微信永远只能看到自己的虚拟空间,它根本不知道音乐的数据在物理内存的哪个角落,自然就不会发生“踩踏”。 实际上呢?操作系统偷偷把数据塞进**真实物理内存**中各种零碎的缝隙里。这么做有两个绝顶天才的好处:
2. **碎片利用**物理内存就算被用得像狗皮膏药一样稀碎,映射给进程的虚拟空间依然是连续且整齐的。 1. **绝对安全**微信永远只能看到自己的空间,没法篡改别人的数据
::: 2. **碎片利用**:不管物理内存多乱,映射给进程的虚拟空间依然是整齐的
--- ---
## 3. 文件系统:把“荒地”变成“档案馆” ## 3. 文件系统:持久化存储的组织
如果你买了一块崭新的硬盘,它里面其实是一片荒芜的存储单元。如果你想存一张照片,硬盘硬件只会问你:请告诉我你要存在第几个字节地址?”这显然反人类。 如果你买了一块崭新的硬盘,它里面其实是一片荒芜的存储单元。如果你想存一张照片,硬盘只会问你:"请告诉我你要存在第几个字节"
操作系统的第三大魔法是**文件系统(File System)**,它为你构建了我们最熟悉的:文件夹(目录)和文件的概念。
<FilesystemDemo /> <FilesystemDemo />
::: tip 💡 核心原理解析:从地址到路径 ### 3.1 文件系统做了什么?
文件系统本质上是一个超级大型的“翻译官”加“账本”: 1. **切割硬盘**:把硬盘切成无数个固定大小的**块**(通常是 4KB)
1. **账本功能**它悄悄地把硬盘切分成无数个小块(Block),然后用一个账本记录下来“哪几个小块现在是空的可以存数据,哪几个小块已经存了东西”。 2. **建立账本**记录哪些块是满的,哪些是空的
2. **翻译功能**当你双击一层层文件夹,打开 `D盘/照片/宠物.jpg` 时,并不是硬盘真的长出了树枝一样的结构。而是文件系统在它的账本里疯狂翻阅,最终翻译出:哦,这个路径其实对应的是硬盘上的第 1056、1057 和 998 块小地方,然后把数据取出来交给你。 3. **翻译路径** `D盘/照片/宠物.jpg` 翻译成"第 3、7、11 块"
:::
这就是为什么你重命名文件瞬间就能完成(只改账本上的名字),而复制文件需要好久(要真实读写硬盘数据块)。
--- ---
## 4. 总结:伟大的幕后英雄 ## 4. 三者协同:程序启动的完整过程
我们通过一个你每天都在经历的场景,串联起今天学到的知识。当你**双击鼠标打开一个游戏**时,为了伺候你,大管家做了什么? 我们已经分别了解了操作系统的三大模块,下面看看当你**双击打开一个程序**时,它们是如何协同工作的:
1. **文件系统**:立刻从底层硬盘的杂乱数据块中,拼凑出游戏的执行文件和美术资产。 <ProgramLaunchDemo />
2. **内存管理**:为你分配一个巨大的虚拟内存空间,制造出“这台电脑只有这一个游戏”的幻觉,并把刚才找到的文件放进物理内存的空隙里。
3. **进程管理**:在它的名册上新建一个“游戏进程”,并在下一个瞬间,立刻剥夺其他正在运行软件的 CPU 权利,把 CPU 的计算力全盘移交给你的游戏。
我们之所以能那么轻松、优雅地在数字世界里冲浪,全都是因为底层的操作系统在替我们负重前行。 无论是你点击桌面图标,还是代码中的一句 `print("Hello World")`,都离不开这一套复杂的暗箱操作。我们之所以能那么轻松地在数字世界里冲浪,全都是因为底层的操作系统在替我们负重前行。
--- ---
## 延伸阅读 ## 延伸阅读
如果你觉得操作系统的各种管理学十分有趣,你可以看看这些进阶话题: 如果你觉得操作系统的各种"管理学和骗术"十分有趣,你可以看看这些进阶话题:
- **进程与线程的区别**除了进程,还有一种叫作“线程”的东西,它们是干什么用的?(为什么 Google Chrome 那么吃内存?) - **进程与线程**如果进程是项目组,那"线程"就是组里干活的员工
- **页面置换算法**:当物理内存全都塞满了,但你又打开了一个新软件,操作系统该把谁的数据临时踢到硬盘里?(LRU 算法) - **并发与锁**:当两个进程同时竞争同一个资源时,如何防止死锁
- **操作系统的多态**Windows 和 macOS 会在底层实现上有什么不同?为什么有些软件只能在特定系统上运行? - **系统调用**:操作系统给上层应用提供的"服务窗口"
@@ -112,23 +112,20 @@
如果刚才介绍的逻辑门只能做简单的条件判断,那计算机到底是如何做数学运算的呢? 如果刚才介绍的逻辑门只能做简单的条件判断,那计算机到底是如何做数学运算的呢?
我们先回想一下手算加法的方式:对应位相加,如果超出了限制(十进制是满十进一,二进制是满二进一),就向更高位“进位”。
在二进制中,只有 0 和 1。对于一位数的加法,可能的情况只有四种: <BinaryAdditionRulesDemo />
- `0 + 0 = 0` (本位是 0,不进位)
- `0 + 1 = 1` (本位是 1,不进位)
- `1 + 0 = 1` (本位是 1,不进位)
- `1 + 1 = 10` (本位是 0,进位 1
仔细观察这四种情况,你会发现: 因此,只要把一个 XOR 门(负责算本位)和一个 AND 门(负责算进位)组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder**。
1. **本位的结果**,只有在两个输入**不同**时才为 1,这正是 **XOR 门(异或门)** 的逻辑。
2. **进位的结果**,只有在两个输入**都为 1** 时才为 1,这正是 **AND 门(与门)** 的逻辑。
因此,只要把一个 XOR 门和一个 AND 门组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder**。
<HalfAdderDemo /> <HalfAdderDemo />
但半加器有个致命缺陷:它无法处理来自低位的进位。在多位加法中,中间的每一位不仅要加 A 和 B,还要加上低位传来的进位。这就需要**全加器(Full Adder** 但半加器有个致命缺陷:它在物理结构上**只有两个输入端口(A 和 B**
想象我们在做十进制竖式加法(比如 `19 + 22`):
- **算个位**`9 + 2 = 11`。只需两个数相加,写 `1``1`。这刚好是两个输入,半加器能完美胜任。
- **算十位**:不仅要算 `1 + 2`,还要**加上刚才个位传过来的“进位 1”**(即 `1 + 2 + 1 = 4`)。这意味着在多位加法中,除了最低位,其他位实际上是在做**三个数字**的相加!
因为半加器没有接纳“低位传来的进位(Carry-in)”的第三个输入口,所以除了最右边的那一位,它在别的位全都没法用。为了解决这个问题,我们需要能接收三个信号的**全加器(Full Adder**
<FullAdderDemo /> <FullAdderDemo />
@@ -185,6 +182,10 @@
当我们将 32 个抑或 64 个这种触发器整齐地编排成一列,施加同一种强劲的时钟频率信号(Clock)来号令它们统一行动时,**寄存器(Register)**便应运而生了。它身居 CPU 系统的心脏位置,被当做极速的“工作草稿纸”,默默捍卫着你每一个即时的关键变量。 当我们将 32 个抑或 64 个这种触发器整齐地编排成一列,施加同一种强劲的时钟频率信号(Clock)来号令它们统一行动时,**寄存器(Register)**便应运而生了。它身居 CPU 系统的心脏位置,被当做极速的“工作草稿纸”,默默捍卫着你每一个即时的关键变量。
::: :::
请通过下面的互动演示,亲自体验这个打破和恢复闭环的过程:
<FlipFlopDemo />
--- ---
## 4. CPU 架构:从功能单元到处理器 ## 4. CPU 架构:从功能单元到处理器
@@ -66,6 +66,18 @@ result = response.choices[0].message.content
<ApiTypesComparison /> <ApiTypesComparison />
### 1.3 函数 API vs HTTP API 的区别
很多初学者会困惑:函数 API 和 HTTP API 到底有什么区别?看文档时该如何区分?
<ApiFunctionVsHttp />
### 1.4 不同类型的 API 文档怎么看
面对不同类型的 API 文档,关注重点各不相同:
<DocumentTypesComparison />
--- ---
## 2. 一次完整的 API 调用 ## 2. 一次完整的 API 调用
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-652
View File
@@ -1,652 +0,0 @@
# SQL:与数据库对话的语言
::: tip 核心问题
**如何高效地查询和操作数据?** 这就像问:图书馆的书怎么快速找到?仓库的货物怎么精准定位?银行的账目怎么安全转账?SQL 解决的就是"与数据对话"的问题。
:::
---
## 0. SQL 的核心价值
在现代软件开发中,数据是核心资产。无论是电商平台的商品信息、社交网络的用户关系,还是银行系统的交易记录,都需要一种高效的方式来管理和查询。
**SQL**Structured Query Language,结构化查询语言)就是这样一种"与数据库对话"的语言。它让我们能够:
- **精准查询**:从百万级数据中快速找到目标
- **高效操作**:批量增删改,一条语句搞定
- **安全保障**:事务机制保证数据一致性
- **标准通用**:学一次,所有数据库都能用
---
## 1. SQL vs NoSQL:如何选择?
在深入了解 SQL 之前,先了解一下它与 NoSQL 的区别。
### 1.1 用仓库来类比
| 特性 | SQL(关系型数据库) | NoSQL(非关系型数据库) |
| :--- | :--- | :--- |
| **数据结构** | 严格的表结构(像 Excel) | 灵活的文档/键值/图结构 |
| **典型代表** | MySQL、PostgreSQL、Oracle | MongoDB、Redis、Elasticsearch |
| **适用场景** | 金融系统、电商订单、用户管理 | 社交动态、日志分析、实时缓存 |
| **优势** | 数据一致性、事务支持(ACID) | 高并发、灵活扩展、高性能 |
| **劣势** | 扩展性差、schema 固定 | 数据一致性弱、查询功能有限 |
### 1.2 一个直观的对比
**SQL 数据库**就像一个**规范化的仓库**:
- 每个货架有固定的编号、名称、容量
- 货物必须按照规则摆放
- 入库、出库有严格的流程和记录
- 适合需要严格管理的场景
**NoSQL 数据库**就像一个**灵活的杂物间**:
- 想放哪里就放哪里
- 不需要预先规划空间
- 快速存取,但可能找不到东西
- 适合需要快速迭代的场景
::: tip 💡 实际应用
大多数企业会**同时使用 SQL 和 NoSQL**
- MySQL 存储用户信息、订单数据(核心业务)
- Redis 缓存热点数据(提高性能)
- MongoDB 存储日志、用户行为(数据分析)
:::
---
## 2. CRUD 操作:数据的增删改查
SQL 的核心操作就是 CRUDCreate, Read, Update, Delete)。
### 2.1 用 Excel 来类比
| Excel 操作 | SQL 关键字 | 说明 |
| :--- | :--- | :--- |
| 插入新行 | INSERT | 添加数据 |
| 筛选行 | SELECT | 查询数据 |
| 修改单元格 | UPDATE | 更新数据 |
| 删除行 | DELETE | 删除数据 |
### 2.2 实战演示
👇 **动手试试看**:在下方交互式演示中体验 CRUD 操作:
<SqlDemo />
### 2.3 常用查询语法
#### **SELECT:查询数据**
```sql
-- 查询所有列
SELECT * FROM users;
-- 查询指定列
SELECT name, email FROM users;
-- 带条件查询
SELECT * FROM users WHERE age > 18;
-- 排序
SELECT * FROM users ORDER BY age DESC;
-- 限制结果数量
SELECT * FROM users LIMIT 10;
```
#### **INSERT:插入数据**
```sql
-- 插入完整数据
INSERT INTO users (name, email, age)
VALUES ('张三', 'zhangsan@example.com', 25);
-- 批量插入
INSERT INTO users (name, email, age) VALUES
('李四', 'lisi@example.com', 30),
('王五', 'wangwu@example.com', 28);
```
#### **UPDATE:更新数据**
```sql
-- 更新单个字段
UPDATE users SET age = 26 WHERE id = 1;
-- 更新多个字段
UPDATE users
SET age = 27, email = 'newemail@example.com'
WHERE id = 1;
-- ⚠️ 危险操作:不带 WHERE 会更新所有行!
UPDATE users SET age = 0; -- 慎用!
```
#### **DELETE:删除数据**
```sql
-- 删除指定行
DELETE FROM users WHERE id = 1;
-- ⚠️ 危险操作:不带 WHERE 会删除所有数据!
DELETE FROM users; -- 慎用!
```
::: warning 💡 最佳实践
- 先用 `SELECT` 验证 WHERE 条件是否正确
- 再用 `UPDATE/DELETE` 执行操作
- 生产环境务必加 `LIMIT` 限制影响行数
:::
---
## 3. SELECT 进阶:JOIN、GROUP BY、子查询
当数据分布在多个表中时,我们需要更强大的查询能力。
### 3.1 JOIN:连接多个表
**场景**:一个电商系统有两个表:
- `users`(用户表):id, name, email
- `orders`(订单表):order_id, user_id, amount
如何查询"每个用户的订单总金额"?
#### **INNER JOIN:只返回匹配的行**
```sql
SELECT users.name, SUM(orders.amount) as total
FROM users
INNER JOIN orders ON users.id = orders.user_id
GROUP BY users.id;
```
**结果**:只显示有订单的用户
#### **LEFT JOIN:返回左表所有行**
```sql
SELECT users.name, SUM(orders.amount) as total
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.id;
```
**结果**:显示所有用户,没有订单的用户 total 为 NULL
::: tip 💡 如何选择 JOIN
- **INNER JOIN**:只要两边都有数据才需要(如:订单明细)
- **LEFT JOIN**:需要保留主表所有数据(如:用户列表 + 统计信息)
- **RIGHT JOIN**:需要保留从表所有数据(很少用)
- **FULL OUTER JOIN**:需要所有数据(MySQL 不支持,可用 UNION 实现)
:::
### 3.2 GROUP BY:分组统计
**场景**:统计每个部门的平均工资。
```sql
SELECT department, AVG(salary) as avg_salary, COUNT(*) as count
FROM employees
GROUP BY department
HAVING AVG(salary) > 10000; -- HAVING 过滤分组后的结果
```
**注意**
- `WHERE` 过滤行(在 GROUP BY 之前)
- `HAVING` 过滤分组(在 GROUP BY 之后)
### 3.3 子查询:查询嵌套查询
**场景**:查找工资高于平均工资的员工。
```sql
-- 方式一:WHERE 子查询
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- 方式二:FROM 子查询(派生表)
SELECT dept_name, avg_salary
FROM (
SELECT department, AVG(salary) as avg_salary
FROM employees
GROUP BY department
) as dept_avg
WHERE avg_salary > 10000;
```
::: tip 💡 子查询 vs JOIN
- **子查询**:逻辑清晰,但性能较差(每个子查询都会执行一次)
- **JOIN**:性能更好,但需要理解连接逻辑
- **最佳实践**:优先使用 JOIN,必要时用子查询
:::
---
## 4. 索引原理:让查询快起来
### 4.1 为什么需要索引?
**场景**:在一个 100 万行的用户表中,查找 `id = 123456` 的用户。
**没有索引**
- 数据库需要逐行扫描,最多比较 100 万次
- 时间复杂度:O(n)
**有索引**
- 数据库通过 B+ 树快速定位,只需比较 log₂(100万) ≈ 20 次
- 时间复杂度:O(log n)
### 4.2 用图书馆来类比
| 概念 | 图书馆 | 数据库 |
| :--- | :--- | :--- |
| **数据** | 书籍 | 表的行 |
| **索引** | 目录卡片 | B+ 树 |
| **查询** | 按书名找书 | 按 WHERE 条件找行 |
| **无索引** | 逐排书架找 | 全表扫描 |
| **有索引** | 查目录定位 | 索引查找 |
### 4.3 索引的可视化演示
👇 **动手试试看**:在 SqlDemo 组件的"索引"标签页查看无索引 vs 有索引的对比:
<SqlDemo />
### 4.4 索引的使用建议
| 场景 | 是否建索引 | 说明 |
| :--- | :--- | :--- |
| **WHERE 条件** | 是 | 如 `WHERE user_id = 1` |
| **JOIN 连接** | 是 | 如 `JOIN ON user_id` |
| **ORDER BY 排序** | 是 | 如 `ORDER BY created_at` |
| **低选择性列** | 否 | 如性别(只有男/女) |
| **频繁更新的列** | 谨慎 | 索引会降低写入性能 |
| **小表** | 否 | 数据量小不需要索引 |
**创建索引**
```sql
-- 单列索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 复合索引(最左前缀原则)
CREATE INDEX idx_user_status ON orders(user_id, status);
-- 唯一索引
CREATE UNIQUE INDEX idx_email ON users(email);
```
::: tip 💡 索引的代价
- **空间**:每个索引都是额外的存储空间
- **时间**INSERT/UPDATE/DELETE 需要更新索引,降低写入速度
- **建议**:只在查询频繁、更新少的列上建索引
:::
---
## 5. 事务 ACID:保证数据一致性
### 5.1 什么是事务?
**事务**Transaction)是一组 SQL 操作,要么全部成功,要么全部失败。
**经典案例**:银行转账
```sql
BEGIN; -- 开始事务
-- 账户 A 扣款 100 元
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
-- 账户 B 加款 100 元
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT; -- 提交事务(如果中间出错,自动 ROLLBACK)
```
如果第二步失败(比如账户 B 不存在),整个事务会回滚,账户 A 不会被扣款。
### 5.2 ACID 四大特性
👇 **动手试试看**:在 SqlDemo 组件的"事务"标签页查看 ACID 可视化:
<SqlDemo />
#### **A - Atomicity(原子性)**
- **含义**:事务中的操作要么全部成功,要么全部失败
- **类比**:转账要么同时成功,要么同时失败,不会出现"扣款了但没到账"的情况
- **实现**Undo Log(回滚日志)
#### **C - Consistency(一致性)**
- **含义**:事务前后数据库状态一致,满足所有约束
- **类比**:转账前后总金额不变(A 余额 + B 余额 = 总金额)
- **实现**:应用层约束 + 数据库约束
#### **I - Isolation(隔离性)**
- **含义**:并发事务之间互不干扰
- **类比**:两个用户同时转账,不会相互影响
- **实现**:锁机制 + MVCC(多版本并发控制)
#### **D - Durability(持久性)**
- **含义**:事务提交后,永久保存,即使系统故障
- **类比**:转账成功后,断电也不会丢失记录
- **实现**Redo Log(重做日志)
### 5.3 事务隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **READ UNCOMMITTED** | 是 | 是 | 是 | 高 | 几乎不用 |
| **READ COMMITTED** | 否 | 是 | 是 | 中 | 大多数数据库默认 |
| **REPEATABLE READ** | 否 | 否 | 是 | 低 | MySQL 默认 |
| **SERIALIZABLE** | 否 | 否 | 否 | 最低 | 金融级要求 |
**设置隔离级别**
```sql
-- 查看
SELECT @@transaction_isolation;
-- 设置
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
```
::: tip 💡 如何选择隔离级别?
- **默认使用 READ COMMITTED**:避免脏读,性能可接受
- **金融场景**:使用 SERIALIZABLE 或 REPEATABLE READ
- **分析场景**:可降低到 READ UNCOMMITTED 提高性能
:::
---
## 6. SQL 注入:安全的警惕性
### 6.1 什么是 SQL 注入?
**SQL 注入**是一种常见的安全漏洞,攻击者通过构造恶意的输入,篡改 SQL 语句。
**示例**:一个登录接口
```sql
-- 正常 SQL
SELECT * FROM users WHERE username = 'admin' AND password = '123456';
-- 攻击者输入用户名:admin' --
-- 拼接后的 SQL
SELECT * FROM users WHERE username = 'admin' --' AND password = '123456';
-- ↑ 注释掉后面的密码验证,直接登录成功!
```
**更危险的攻击**
```sql
-- 用户名输入:admin'; DROP TABLE users; --
-- 拼接后的 SQL
SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
```
### 6.2 如何防御?
#### **方法一:参数化查询(推荐)**
```python
# ❌ 错误:直接拼接字符串(危险!)
sql = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(sql)
# ✅ 正确:使用参数化查询(安全)
sql = "SELECT * FROM users WHERE username = %s"
cursor.execute(sql, (username,))
```
#### **方法二:ORM 框架**
```python
# Django ORM
user = User.objects.get(username=username)
# SQLAlchemy
user = session.query(User).filter(User.username == username).first()
```
#### **方法三:输入验证**
```python
# 限制用户名只能包含字母、数字、下划线
import re
if not re.match(r'^\w+$', username):
raise ValueError('Invalid username')
```
::: warning 💡 防御 SQL 注入的黄金法则
1. **永远不要相信用户输入**
2. **永远使用参数化查询或 ORM**
3. **永远不要拼接 SQL 字符串**
4. **最小权限原则**:数据库用户只给必要权限
:::
---
## 7. 最佳实践
### 7.1 查询优化
| 优化技巧 | 说明 | 示例 |
| :--- | :--- | :--- |
| **避免 SELECT \*** | 只查询需要的列 | `SELECT name, email FROM users` |
| **使用 LIMIT** | 限制结果数量 | `SELECT * FROM users LIMIT 10` |
| **索引覆盖** | 查询条件使用索引列 | `WHERE indexed_col = 1` |
| **避免子查询** | 用 JOIN 替代子查询 | 见上文对比 |
| **批量操作** | 减少数据库往返 | `INSERT INTO ... VALUES (...), (...), (...)` |
| **分页查询** | 大数据量分页 | `SELECT * FROM users LIMIT 10 OFFSET 20` |
### 7.2 命名规范
| 类型 | 规范 | 示例 |
| :--- | :--- | :--- |
| **表名** | 小写 + 下划线 | `user_profiles`, `order_items` |
| **列名** | 小写 + 下划线 | `created_at`, `user_id` |
| **索引名** | `idx_表名_列名` | `idx_users_email` |
| **外键名** | `fk_表名_列名` | `fk_orders_user_id` |
| **主键名** | 统一使用 `id` | 无 |
### 7.3 数据库设计
| 设计原则 | 说明 | 示例 |
| :--- | :--- | :--- |
| **规范化** | 消除数据冗余 | 第三范式(3NF) |
| **反规范化** | 适当冗余提高性能 | 在订单表冗余用户姓名 |
| **主键选择** | 优先使用自增 ID | `id BIGINT AUTO_INCREMENT` |
| **时间字段** | 统一使用 DATETIME | `created_at DATETIME` |
| **软删除** | 用 `is_deleted` 标记 | 不真删除,便于恢复 |
---
## 8. 用 AI 辅助编写 SQL
AI 可以帮助你快速编写复杂的 SQL 查询。关键在于提供清晰的表结构和业务需求。
### 8.1 提示词模板
```
你是一位资深的数据库工程师,精通 SQL 查询优化。请帮我编写 SQL 查询。
## 数据库表结构
[提供表的 CREATE TABLE 语句或字段说明]
## 业务需求
[描述你想要查询的数据,例如:
- 统计每个月的订单总金额
- 查找购买过商品 A 和商品 B 的用户
- 计算用户的留存率]
## 要求
1. 使用标准 SQL 语法(兼容 MySQL 8.0
2. 注释关键逻辑
3. 考虑性能优化(索引、JOIN 顺序)
4. 提供多种实现方案(如子查询 vs JOIN)
## 输出格式
### SQL 查询
\`\`\`sql
[SQL 语句]
\`\`\`
### 逻辑说明
[解释查询的逻辑和优化思路]
```
### 8.2 实战示例:复杂统计查询
**输入提示词**
```
你是一位资深的数据库工程师,精通 SQL 查询优化。请帮我编写 SQL 查询。
## 数据库表结构
```sql
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
created_at DATETIME
);
-- 订单表
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
amount DECIMAL(10, 2),
status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled'),
created_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 订单详情表
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT,
product_id BIGINT,
quantity INT,
price DECIMAL(10, 2),
FOREIGN KEY (order_id) REFERENCES orders(id)
);
-- 商品表
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
category VARCHAR(50)
);
```
## 业务需求
1. 统计每个月的订单总金额和订单数量
2. 查找购买过商品 A 和商品 B 的用户
3. 计算用户复购率(有多少用户下了多个订单)
## 要求
1. 使用标准 SQL 语法(兼容 MySQL 8.0
2. 注释关键逻辑
3. 考虑性能优化
```
**AI 输出示例**
```sql
-- 需求 1:统计每个月的订单总金额和订单数量
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE status IN ('paid', 'shipped', 'completed') -- 只统计有效订单
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month DESC;
-- 需求 2:查找购买过商品 A 和商品 B 的用户
-- 方案一:INTERSECTMySQL 不支持,用 IN 替代)
SELECT DISTINCT user_id
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE p.name IN ('商品 A', '商品 B')
GROUP BY user_id
HAVING COUNT(DISTINCT p.name) = 2; -- 确保同时买了两个商品
-- 方案二:自连接(性能更好)
SELECT
oa.user_id
FROM orders oa
JOIN order_items oia ON oa.id = oia.order_id
JOIN products pa ON oia.product_id = pa.id
JOIN order_items oib ON oa.id = oib.order_id -- 同一个订单
JOIN products pb ON oib.product_id = pb.id
WHERE pa.name = '商品 A' AND pb.name = '商品 B';
-- 需求 3:计算用户复购率
WITH user_order_count AS (
SELECT
user_id,
COUNT(*) AS order_count
FROM orders
WHERE status IN ('paid', 'shipped', 'completed')
GROUP BY user_id
)
SELECT
COUNT(CASE WHEN order_count >= 2 THEN 1 END) AS repeat_users,
COUNT(*) AS total_users,
ROUND(COUNT(CASE WHEN order_count >= 2 THEN 1 END) * 100.0 / COUNT(*), 2) AS repeat_rate
FROM user_order_count;
```
### 8.3 AI 辅助的注意事项
| 注意点 | 说明 |
| :--- | :--- |
| **提供完整上下文** | 表结构、索引、数据量都要说清楚 |
| **明确性能要求** | 是否需要优化、数据量大小 |
| **验证 SQL 语法** | AI 生成的 SQL 可能有小错误,需要测试 |
| **理解执行计划** | 用 `EXPLAIN` 查看查询是否使用了索引 |
| **分步实现** | 复杂查询可以拆分成多个简单查询 |
::: tip 💡 追问技巧
- "请提供另一种实现方案(如用 JOIN 替代子查询)"
- "请分析这条查询的性能瓶颈"
- "请添加索引建议"
- "请解释每个步骤的逻辑"
:::
---
## 名词速查表
| 名词 | 英文 | 解释 |
| :--- | :--- | :--- |
| **SQL** | Structured Query Language | 结构化查询语言,与数据库对话的标准语言 |
| **数据库** | Database | 存储和管理数据的仓库 |
| **表** | Table | 数据的二维表格,类似 Excel |
| **行** | Row | 表中的一条记录 |
| **列** | Column | 表中的一个字段 |
| **主键** | Primary Key | 唯一标识一行的字段(如 id) |
| **外键** | Foreign Key | 关联其他表的字段 |
| **索引** | Index | 加速查询的数据结构(B+ 树) |
| **事务** | Transaction | 一组要么全成功、要么全失败的 SQL 操作 |
| **ACID** | Atomicity, Consistency, Isolation, Durability | 事务的四大特性 |
| **JOIN** | Join | 连接多个表的查询操作 |
| **子查询** | Subquery | 嵌套在另一个查询中的查询 |
| **聚合函数** | Aggregate Function | SUM, AVG, COUNT, MAX, MIN |
| **分组** | Group By | 按字段分组统计 |
| **SQL 注入** | SQL Injection | 通过输入篡改 SQL 语句的攻击方式 |
| **规范化** | Normalization | 消除数据冗余的设计原则 |
| **反规范化** | Denormalization | 适当冗余提高性能的设计 |
| **执行计划** | Execution Plan | 数据库执行 SQL 的详细步骤 |
| **B+ 树** | B+ Tree | 索引的底层数据结构 |
| **MVCC** | Multi-Version Concurrency Control | 多版本并发控制,实现事务隔离 |
| **脏读** | Dirty Read | 读取未提交的数据 |
| **不可重复读** | Non-Repeatable Read | 同一事务两次读取结果不同 |
| **幻读** | Phantom Read | 同一事务两次读取结果集不同 |
| **隔离级别** | Isolation Level | 事务隔离的程度(READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE |
+5 -1
View File
@@ -4,7 +4,11 @@ import vueParser from 'vue-eslint-parser'
export default [ export default [
{ {
ignores: ['node_modules/**', 'docs/.vitepress/dist/**', 'docs/.vitepress/cache/**'] ignores: [
'node_modules/**',
'docs/.vitepress/dist/**',
'docs/.vitepress/cache/**'
]
}, },
js.configs.recommended, js.configs.recommended,
...pluginVue.configs['flat/recommended'], ...pluginVue.configs['flat/recommended'],