feat: 更新附录交互组件和文档
This commit is contained in:
@@ -797,7 +797,6 @@ export default defineConfig({
|
||||
text: '五、数据',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'SQL', link: '/zh-cn/appendix/5-data/sql' },
|
||||
{
|
||||
text: '数据库原理(索引 / 事务 / 查询优化)',
|
||||
link: '/zh-cn/appendix/5-data/database-fundamentals'
|
||||
|
||||
@@ -110,13 +110,13 @@ const i18n = {
|
||||
title: '真实项目',
|
||||
headline: '拒绝玩具代码。',
|
||||
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: '部署上线',
|
||||
headline: '让世界看到你的作品。',
|
||||
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 智能体',
|
||||
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: '长效稳定',
|
||||
@@ -1502,7 +1502,7 @@ const stage2Cards = [
|
||||
desc: '深入理解用户鉴权、数据存储、文件上传等核心业务逻辑。',
|
||||
imageColor: '#8EC5FC',
|
||||
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: '部署上线',
|
||||
@@ -1510,7 +1510,7 @@ const stage2Cards = [
|
||||
desc: '学习服务器配置、域名解析和自动化部署,打通产品落地的最后一公里。',
|
||||
imageColor: '#96E6A1',
|
||||
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 的无限可能。',
|
||||
tag: 'Advanced',
|
||||
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: '复杂业务架构',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
🌟 AI 发展阶段与核心范式全景对比
|
||||
</div>
|
||||
<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-name" :style="{ color: era.color }">{{ era.name }}</div>
|
||||
<div class="e-time">{{ era.time }}</div>
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="e-section">
|
||||
<div class="e-label">典型代表</div>
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="demo-card">
|
||||
<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-years">{{ era.years }}</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="bar-bg">
|
||||
<div class="bar-fill" :style="{ width: item.w * 100 + '%', background: barColor(item.w) }"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="demo-card">
|
||||
<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-icon">{{ step.icon }}</div>
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="demo-card">
|
||||
<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">
|
||||
<span class="school-icon">{{ s.icon }}</span>
|
||||
<span class="school-name" :style="{ color: s.color }">{{ s.name }}</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="demo-card">
|
||||
<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">
|
||||
<span class="gpt-name" :style="{ color: m.color }">{{ m.name }}</span>
|
||||
<span class="gpt-year">{{ m.year }}</span>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<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-desc">{{ info.desc }}</div>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<div class="demo-card">
|
||||
<div class="perceptron-layout">
|
||||
<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-label">{{ inp.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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="w-tag">×{{ inp.weight }}</span>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="title">关键发展路径总结</span>
|
||||
</div>
|
||||
<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-top">
|
||||
<span class="path-icon" :style="{ background: item.color }">{{ i + 1 }}</span>
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
</div>
|
||||
<div class="t-line">
|
||||
<span class="t-ps">> </span>
|
||||
<span class="t-typing"
|
||||
>{{ typing }}<span class="t-cur">▋</span></span
|
||||
>
|
||||
<span class="t-typing">{{ typing }}<span class="t-cur">▋</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
"id": 123,
|
||||
"name": "张三"
|
||||
}
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
</div>
|
||||
<div class="compare-col">
|
||||
<div class="compare-title">列表</div>
|
||||
@@ -44,8 +43,7 @@
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">
|
||||
@@ -79,8 +77,7 @@
|
||||
"created_at": "2024-01-15T09:30:00.000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000Z",
|
||||
"expired_at": "2025-01-15T00:00:00.000Z"
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
</div>
|
||||
<div class="time-rules">
|
||||
<div class="time-rule">
|
||||
@@ -97,9 +94,7 @@
|
||||
</div>
|
||||
<div class="time-rule">
|
||||
<span class="rule-label">命名</span>
|
||||
<span class="rule-value"
|
||||
>xxx_at 表示时间点,xxx_duration 表示时长</span
|
||||
>
|
||||
<span class="rule-value">xxx_at 表示时间点,xxx_duration 表示时长</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,8 +109,7 @@
|
||||
"name": "张三",
|
||||
"nickname": null,
|
||||
"avatar": null
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
<div class="compare-desc">字段存在但无值时返回 null</div>
|
||||
</div>
|
||||
<div class="compare-col bad-col">
|
||||
@@ -123,8 +117,7 @@
|
||||
<pre class="code-sm">
|
||||
{
|
||||
"name": "张三"
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
<div class="compare-desc">省略字段,前端需判断是否存在</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,9 +149,7 @@
|
||||
|
||||
<div class="tips">
|
||||
<span class="tips-icon">💡</span>
|
||||
<span class="tips-text"
|
||||
>参考 ISO 8601 时间标准,字段命名保持 snake_case 风格</span
|
||||
>
|
||||
<span class="tips-text">参考 ISO 8601 时间标准,字段命名保持 snake_case 风格</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
<div class="field-tips">
|
||||
<div class="tip-row">
|
||||
<code>field</code>
|
||||
@@ -67,8 +66,7 @@
|
||||
"shortfall": 49.00,
|
||||
"suggestion": "请充值后重试"
|
||||
}
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
<div class="business-tips">
|
||||
<div class="b-tip">✓ 返回当前状态数据,便于前端展示</div>
|
||||
<div class="b-tip">✓ 提供 suggestion 给出解决建议</div>
|
||||
@@ -166,9 +164,7 @@
|
||||
|
||||
<div class="tips">
|
||||
<span class="tips-icon">💡</span>
|
||||
<span class="tips-text"
|
||||
>错误信息要"机器可读 + 人类友好",便于前端统一处理</span
|
||||
>
|
||||
<span class="tips-text">错误信息要"机器可读 + 人类友好",便于前端统一处理</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
{ "result": { "user": {...} } }
|
||||
|
||||
// 接口 C
|
||||
{ "user": {...} }</pre
|
||||
>
|
||||
{ "user": {...} }</pre>
|
||||
<div class="problem-desc">
|
||||
前端需要针对每个接口单独处理,代码冗余,容易出错
|
||||
</div>
|
||||
@@ -43,8 +42,7 @@
|
||||
"message": "success",
|
||||
"data": { ... },
|
||||
"request_id": "req-xxx"
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,8 +139,7 @@
|
||||
"total": 156,
|
||||
"total_pages": 8,
|
||||
"has_next": true
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,9 +147,7 @@
|
||||
|
||||
<div class="tips">
|
||||
<span class="tips-icon">💡</span>
|
||||
<span class="tips-text"
|
||||
>request_id 用于问题追踪,建议使用 UUID v4 或雪花算法生成</span
|
||||
>
|
||||
<span class="tips-text">request_id 用于问题追踪,建议使用 UUID v4 或雪花算法生成</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
v-for="s in scenarios"
|
||||
:key="s.id"
|
||||
:class="['raf-chip', { active: currentScenario.id === s.id }]"
|
||||
@click="selectScenario(s)"
|
||||
:disabled="processing"
|
||||
@click="selectScenario(s)"
|
||||
>
|
||||
{{ s.label }}
|
||||
</button>
|
||||
@@ -34,14 +34,14 @@
|
||||
</div>
|
||||
<button
|
||||
class="raf-send-btn"
|
||||
@click="sendRequest"
|
||||
:disabled="processing"
|
||||
@click="sendRequest"
|
||||
>
|
||||
{{ processing ? 'Sending...' : 'Send Request' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="raf-response-box" v-if="response">
|
||||
<div v-if="response" class="raf-response-box">
|
||||
<div class="raf-status-line">
|
||||
<span class="raf-label">Response Status:</span>
|
||||
<span
|
||||
@@ -82,7 +82,7 @@
|
||||
<!-- Logs -->
|
||||
<div class="raf-section">
|
||||
<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">
|
||||
<span class="raf-log-time">[{{ log.time }}]</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 Start」或「Getting 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">
|
||||
<span class="calc-label">本位:</span>
|
||||
<span class="calc-formula">
|
||||
{{ stages[activeBit]?.a }} ⊕ {{ stages[activeBit]?.b }}
|
||||
{{ stages[activeBit]?.a }} XOR {{ stages[activeBit]?.b }}
|
||||
<span v-if="stages[activeBit]?.cin !== null">
|
||||
⊕ {{ stages[activeBit]?.cin }}</span>
|
||||
XOR {{ stages[activeBit]?.cin }}</span>
|
||||
= <strong>{{ stages[activeBit]?.sum }}</strong>
|
||||
</span>
|
||||
<span class="calc-reason">({{ getSumReason(stages[activeBit]) }})</span>
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
:key="'a' + i"
|
||||
class="bit"
|
||||
:class="{ hl: activeBit === 3 - i }"
|
||||
>{{ b }}</span
|
||||
>
|
||||
>{{ b }}</span>
|
||||
</span>
|
||||
<span class="binary-dec">= {{ clampedA }}</span>
|
||||
</div>
|
||||
@@ -52,8 +51,7 @@
|
||||
:key="'b' + i"
|
||||
class="bit"
|
||||
:class="{ hl: activeBit === 3 - i }"
|
||||
>{{ b }}</span
|
||||
>
|
||||
>{{ b }}</span>
|
||||
</span>
|
||||
<span class="binary-dec">= {{ clampedB }}</span>
|
||||
</div>
|
||||
@@ -65,8 +63,7 @@
|
||||
:key="'s' + i"
|
||||
class="bit"
|
||||
:class="{ hl: activeBit === 3 - i }"
|
||||
>{{ b }}</span
|
||||
>
|
||||
>{{ b }}</span>
|
||||
</span>
|
||||
<span class="binary-dec">= {{ fourBitResult }}</span>
|
||||
</div>
|
||||
@@ -94,25 +91,14 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="stage-io">
|
||||
<span class="io-item"
|
||||
><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 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>
|
||||
</div>
|
||||
<div class="stage-divider"></div>
|
||||
<div class="stage-io">
|
||||
<span class="io-item"
|
||||
><span class="io-tag s">S</span
|
||||
><strong>{{ stage.sum }}</strong></span
|
||||
>
|
||||
<span class="io-item"
|
||||
><span class="io-tag cout">C</span>{{ stage.carryOut }}</span
|
||||
>
|
||||
<span class="io-item"><span class="io-tag s">S</span><strong>{{ stage.sum }}</strong></span>
|
||||
<span class="io-item"><span class="io-tag cout">C</span>{{ stage.carryOut }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -132,9 +132,7 @@
|
||||
<div v-if="coinResult.length" class="coin-result">
|
||||
<div class="result-title">找零方案:</div>
|
||||
<div class="coin-list">
|
||||
<span v-for="(c, i) in coinResult" :key="i" class="coin"
|
||||
>{{ c }}元</span
|
||||
>
|
||||
<span v-for="(c, i) in coinResult" :key="i" class="coin">{{ c }}元</span>
|
||||
</div>
|
||||
<div class="result-summary">
|
||||
共 {{ coinResult.length }} 枚硬币
|
||||
@@ -157,8 +155,7 @@
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong
|
||||
>算法是解决问题的方法。好的算法能让程序效率提升几个数量级。理解算法思维,比记住具体算法更重要。
|
||||
<strong>核心思想:</strong>算法是解决问题的方法。好的算法能让程序效率提升几个数量级。理解算法思维,比记住具体算法更重要。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+322
@@ -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"
|
||||
:key="j"
|
||||
class="task-chip"
|
||||
>{{ task }}</span
|
||||
>
|
||||
>{{ task }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-example">
|
||||
@@ -88,9 +87,7 @@
|
||||
<div class="exec-flow">
|
||||
<span v-for="(step, i) in model.steps" :key="i" class="flow-tag">
|
||||
{{ step }}
|
||||
<span v-if="i < model.steps.length - 1" class="flow-arrow"
|
||||
>→</span
|
||||
>
|
||||
<span v-if="i < model.steps.length - 1" class="flow-arrow">→</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="exec-traits">
|
||||
@@ -104,8 +101,7 @@
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong
|
||||
>编译器像翻译官,把人类能读懂的代码逐步翻译成机器能执行的指令。六个阶段各司其职:识别单词
|
||||
<strong>核心思想:</strong>编译器像翻译官,把人类能读懂的代码逐步翻译成机器能执行的指令。六个阶段各司其职:识别单词
|
||||
→ 理解语法 → 检查语义 → 生成中间码 → 优化 → 生成机器码。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-10
@@ -146,7 +146,7 @@
|
||||
<span class="gate-name">XOR</span>
|
||||
<span class="gate-cn">异或门</span>
|
||||
</div>
|
||||
<div class="gate-formula">A ⊕ B</div>
|
||||
<div class="gate-formula">A XOR B</div>
|
||||
<div class="gate-desc">不同为 1 → 本位</div>
|
||||
</div>
|
||||
<div class="gate-box" :class="{ active: haCarry }">
|
||||
@@ -154,7 +154,7 @@
|
||||
<span class="gate-name">AND</span>
|
||||
<span class="gate-cn">与门</span>
|
||||
</div>
|
||||
<div class="gate-formula">A ∧ B</div>
|
||||
<div class="gate-formula">A AND B</div>
|
||||
<div class="gate-desc">全 1 为 1 → 进位</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,13 +203,13 @@
|
||||
</div>
|
||||
<div class="calc-row">
|
||||
<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>
|
||||
<span class="calc-reason">({{ haA !== haB ? '不同' : '相同' }})</span>
|
||||
</div>
|
||||
<div class="calc-row">
|
||||
<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>
|
||||
<span class="calc-reason">({{ haA && haB ? '全为 1' : '不全为 1' }})</span>
|
||||
</div>
|
||||
@@ -384,15 +384,15 @@
|
||||
</div>
|
||||
<div class="calc-row">
|
||||
<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 class="calc-row">
|
||||
<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 class="calc-row">
|
||||
<span class="calc-label">进位:</span>
|
||||
<span class="calc-formula">Cout = (A∧B) ∨ (xor1∧Cin) =
|
||||
<span class="calc-formula">进位 = (A AND B) OR (中间值 AND Cin) =
|
||||
<strong>{{ faCarryOut ? '1' : '0' }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -544,21 +544,21 @@ const gates = [
|
||||
name: 'AND',
|
||||
cn: '与门',
|
||||
symbol: '&',
|
||||
formula: 'A ∧ B',
|
||||
formula: 'A AND B',
|
||||
truth: [0, 0, 0, 1]
|
||||
},
|
||||
{
|
||||
name: 'OR',
|
||||
cn: '或门',
|
||||
symbol: '≥1',
|
||||
formula: 'A ∨ B',
|
||||
formula: 'A OR B',
|
||||
truth: [0, 1, 1, 1]
|
||||
},
|
||||
{
|
||||
name: 'XOR',
|
||||
cn: '异或门',
|
||||
symbol: '=1',
|
||||
formula: 'A ⊕ B',
|
||||
formula: 'A XOR B',
|
||||
truth: [0, 1, 1, 0]
|
||||
},
|
||||
{ name: 'NOT', cn: '非门', symbol: '1', formula: '¬A', truth: [1, 0] }
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<span class="stage-number">{{ index + 1 }}</span>
|
||||
<span class="stage-name">{{ stage.name }}</span>
|
||||
|
||||
+1
-3
@@ -86,9 +86,7 @@
|
||||
<div class="arp-arrow">↓ 广播到局域网</div>
|
||||
<div class="arp-answer">
|
||||
<span class="answer-icon">✅</span>
|
||||
<span class="answer-text"
|
||||
>我是!我的 MAC 地址是 00:11:22:33:44:66</span
|
||||
>
|
||||
<span class="answer-text">我是!我的 MAC 地址是 00:11:22:33:44:66</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-7
@@ -40,9 +40,7 @@
|
||||
<div class="linked-container">
|
||||
<div v-for="(item, i) in linkedData" :key="i" class="linked-node">
|
||||
<span class="node-value">{{ item.value }}</span>
|
||||
<span v-if="i < linkedData.length - 1" class="node-arrow"
|
||||
>→</span
|
||||
>
|
||||
<span v-if="i < linkedData.length - 1" class="node-arrow">→</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operation-hint">
|
||||
@@ -88,8 +86,7 @@
|
||||
v-for="(item, j) in bucket"
|
||||
:key="j"
|
||||
class="bucket-item"
|
||||
>{{ item }}</span
|
||||
>
|
||||
>{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,8 +175,7 @@
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong
|
||||
>数据结构是数据的"容器",不同的容器有不同的特点。选择合适的数据结构,能让程序效率提升几个数量级。
|
||||
<strong>核心思想:</strong>数据结构是数据的"容器",不同的容器有不同的特点。选择合适的数据结构,能让程序效率提升几个数量级。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+1
-3
@@ -24,9 +24,7 @@
|
||||
<!-- 推荐结果 -->
|
||||
<div v-if="activeScenario" class="recommendation">
|
||||
<div class="rec-header">
|
||||
<span class="rec-title"
|
||||
>推荐使用:{{ currentScenario.recommendation }}</span
|
||||
>
|
||||
<span class="rec-title">推荐使用:{{ currentScenario.recommendation }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rec-reason">
|
||||
|
||||
@@ -50,11 +50,9 @@
|
||||
class="char-item"
|
||||
>
|
||||
<span class="char-display">{{ char }}</span>
|
||||
<span class="char-unicode"
|
||||
>U+{{
|
||||
<span class="char-unicode">U+{{
|
||||
char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')
|
||||
}}</span
|
||||
>
|
||||
}}</span>
|
||||
<span class="char-binary">{{
|
||||
char.charCodeAt(0).toString(2).padStart(8, '0')
|
||||
}}</span>
|
||||
|
||||
+1
-1
@@ -90,10 +90,10 @@
|
||||
<div class="packet-header">数据包</div>
|
||||
<div class="packet-body">
|
||||
<div
|
||||
class="packet-layer"
|
||||
v-for="(layer, index) in currentScenario.transmission
|
||||
.layers"
|
||||
:key="index"
|
||||
class="packet-layer"
|
||||
>
|
||||
<span class="layer-name">{{ layer.name }}:</span>
|
||||
<span class="layer-value">{{ layer.value }}</span>
|
||||
|
||||
+334
-275
@@ -1,333 +1,392 @@
|
||||
<template>
|
||||
<div class="filesystem-demo">
|
||||
<div class="demo-wrapper">
|
||||
<!-- 文件树:逻辑视角 -->
|
||||
<div class="logical-view">
|
||||
<div class="view-title">
|
||||
<span>📁 你的视角 (文件系统)</span>
|
||||
<span class="subtitle">漂亮、整洁的目录树</span>
|
||||
</div>
|
||||
|
||||
<div class="file-tree">
|
||||
<div class="tree-node folder expanded">
|
||||
<span class="icon">💾</span> D盘 (根目录)
|
||||
<div class="demo">
|
||||
<div class="title">📁 你看到的文件 vs 硬盘上的碎片</div>
|
||||
|
||||
<div class="scene">
|
||||
<!-- 文件视图 -->
|
||||
<div class="file-view">
|
||||
<div class="view-label">📂 你看到的(文件夹)</div>
|
||||
<div class="folder-tree">
|
||||
<div class="folder">
|
||||
<span class="folder-icon">📁</span>
|
||||
<span>照片</span>
|
||||
</div>
|
||||
<div class="tree-children">
|
||||
<div class="tree-node folder expanded">
|
||||
<span class="icon">📂</span> 照片
|
||||
<div class="files">
|
||||
<div
|
||||
class="file-item"
|
||||
:class="{ active: currentFile === 'pet' }"
|
||||
>
|
||||
<span class="file-icon">🖼️</span>
|
||||
<span>宠物.jpg</span>
|
||||
<span class="file-size">2.5MB</span>
|
||||
</div>
|
||||
<div class="tree-children">
|
||||
<div
|
||||
class="tree-node file"
|
||||
:class="{ active: activeFile === 'pet' }"
|
||||
@click="selectFile('pet')"
|
||||
>
|
||||
<span class="icon">🖼️</span> 宠物.jpg
|
||||
<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
|
||||
class="file-item"
|
||||
:class="{ active: currentFile === 'trip' }"
|
||||
>
|
||||
<span class="file-icon">🖼️</span>
|
||||
<span>旅游.png</span>
|
||||
<span class="file-size">1.8MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 翻译官动画 -->
|
||||
<div class="translator">
|
||||
<div class="arrow"></div>
|
||||
<div class="badge">文件系统账本<br />(inode表)</div>
|
||||
<div class="arrow"></div>
|
||||
</div>
|
||||
|
||||
<!-- 磁盘块:物理视角 -->
|
||||
<div class="physical-view">
|
||||
<div class="view-title">
|
||||
<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) }]"
|
||||
<!-- 读取动画 -->
|
||||
<div class="read-animation" v-if="isReading">
|
||||
<div class="read-text">正在读取...</div>
|
||||
<div class="read-blocks">
|
||||
<div
|
||||
v-for="(block, idx) in readingBlocks"
|
||||
:key="idx"
|
||||
class="read-block"
|
||||
:class="{ read: idx <= readProgress }"
|
||||
:style="{ animationDelay: idx * 0.1 + 's' }"
|
||||
>
|
||||
{{ block }}
|
||||
</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 class="explanation-box" v-if="activeFile">
|
||||
<span v-if="activeFile === 'pet'">
|
||||
💡 宠物.jpg 其实被切碎分别放在了第 3、8、14
|
||||
块。文件系统帮你做好了翻译,你只需双击它!
|
||||
</span>
|
||||
<span v-if="activeFile === 'vacation'">
|
||||
💡 旅游.png 放在了第 5、6 块。
|
||||
</span>
|
||||
<span v-if="activeFile === 'doc'">
|
||||
💡 总结.docx 被分散存放在 10、11、18、22
|
||||
块,如果没有文件系统,你得自己背下这些数字才能打开文件。
|
||||
</span>
|
||||
</div>
|
||||
<div class="explanation-box default" v-else>
|
||||
☝️ 试着点击左侧的文件,看看它们在硬盘里到底长什么样。
|
||||
<div class="explain">
|
||||
<strong>💡 原理:</strong>文件系统把文件切成碎片存在硬盘各处(如宠物.jpg存在第3、7、11块),然后用"账本"记录位置。你看到的整齐文件夹只是账本上的记录。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 = {
|
||||
pet: [3, 8, 14],
|
||||
vacation: [5, 6],
|
||||
doc: [10, 11, 18, 22]
|
||||
// 文件存储位置
|
||||
const fileLocations = {
|
||||
pet: [3, 7, 11], // 宠物.jpg 存在第3、7、11块
|
||||
trip: [5, 6] // 旅游.png 存在第5、6块
|
||||
}
|
||||
|
||||
const selectFile = (file) => {
|
||||
activeFile.value = file
|
||||
// 每块的内容
|
||||
const blockContents = {
|
||||
3: '宠-1',
|
||||
7: '宠-2',
|
||||
11: '宠-3',
|
||||
5: '旅-1',
|
||||
6: '旅-2'
|
||||
}
|
||||
|
||||
const getBlockOwner = (block) => {
|
||||
for (const [key, blocks] of Object.entries(fileMap)) {
|
||||
if (blocks.includes(block)) return `owner-${key}`
|
||||
}
|
||||
let timer = null
|
||||
let phase = 0
|
||||
|
||||
const getBlockType = (n) => {
|
||||
if (fileLocations.pet.includes(n)) return 'pet'
|
||||
if (fileLocations.trip.includes(n)) return 'trip'
|
||||
return 'empty'
|
||||
}
|
||||
|
||||
const isBlockActive = (block) => {
|
||||
if (!activeFile.value) return false
|
||||
return fileMap[activeFile.value].includes(block)
|
||||
const getBlockContent = (n) => {
|
||||
return blockContents[n] || ''
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.filesystem-demo {
|
||||
background: var(--vp-c-bg-soft);
|
||||
.demo {
|
||||
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;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
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;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Disk Grid */
|
||||
.disk-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.4rem;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.disk-block {
|
||||
aspect-ratio: 1;
|
||||
|
||||
.scene {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.file-view, .disk-view {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.disk-block.owner-pet {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
.view-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.disk-block.owner-vacation {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
|
||||
.folder-tree {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.disk-block.owner-doc {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
|
||||
.folder {
|
||||
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 {
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
.disk-block.reading {
|
||||
transform: scale(1.1);
|
||||
font-weight: 600;
|
||||
animation: glow 0.5s ease infinite alternate;
|
||||
}
|
||||
|
||||
.disk-block.pet.reading {
|
||||
background: #16a34a;
|
||||
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 {
|
||||
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;
|
||||
animation: fadeIn 0.3s;
|
||||
.disk-block.trip.reading {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
.explanation-box.default {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-left-color: var(--vp-c-text-3);
|
||||
|
||||
.block-num {
|
||||
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);
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
.explain strong { color: var(--vp-c-text-1); }
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from { box-shadow: 0 0 5px currentColor; }
|
||||
to { box-shadow: 0 0 15px currentColor; }
|
||||
}
|
||||
</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>
|
||||
+239
-454
@@ -1,212 +1,131 @@
|
||||
<template>
|
||||
<div class="full-adder-demo">
|
||||
<div class="demo-header">
|
||||
<span class="title">全加器 (Full Adder)</span>
|
||||
<span class="subtitle">能处理进位输入的完整加法单元 ── 三个输入,两个输出</span>
|
||||
<span class="title">全加器 (Full Adder) — 交互演示</span>
|
||||
<span class="subtitle">比半加器多一个输入:来自低位的进位 (Cin)。点击三个输入试试</span>
|
||||
</div>
|
||||
|
||||
<div class="terms-box">
|
||||
<div class="term-item">
|
||||
<span class="term-name">Cin (进位输入)</span>
|
||||
<span class="term-desc">来自低位的进位信号</span>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-name">Sum (本位)</span>
|
||||
<span class="term-desc">三位异或的结果</span>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-name">Cout (进位输出)</span>
|
||||
<span class="term-desc">向高位的进位信号</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主交互区 -->
|
||||
<div class="main-area">
|
||||
<!-- 左:大号加法展示 -->
|
||||
<div class="left-panel">
|
||||
<div class="big-calc">
|
||||
<button class="big-bit" :class="{ on: inputA }" @click="inputA = !inputA">{{ inputA ? '1' : '0' }}</button>
|
||||
<span class="op">+</span>
|
||||
<button class="big-bit" :class="{ on: inputB }" @click="inputB = !inputB">{{ inputB ? '1' : '0' }}</button>
|
||||
<span class="op">+</span>
|
||||
<button class="big-bit cin" :class="{ on: carryIn }" @click="carryIn = !carryIn">{{ carryIn ? '1' : '0' }}</button>
|
||||
<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="circuit-container">
|
||||
<div class="inputs">
|
||||
<div class="input-line">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ on: inputA }"
|
||||
@click="inputA = !inputA"
|
||||
<div class="input-labels">
|
||||
<span class="il">A</span>
|
||||
<span class="il spacer"></span>
|
||||
<span class="il">B</span>
|
||||
<span class="il spacer"></span>
|
||||
<span class="il cin-label">低位进位</span>
|
||||
<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' }}
|
||||
</button>
|
||||
<span class="label">输入 A</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<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>
|
||||
<span>{{ row.a }}</span>
|
||||
<span>{{ row.b }}</span>
|
||||
<span>{{ row.cin }}</span>
|
||||
<span class="sum-col">{{ row.sum }}</span>
|
||||
<span class="carry-col">{{ row.carry }}</span>
|
||||
</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 class="calculation-box">
|
||||
<div class="calc-title">计算过程</div>
|
||||
<div class="calc-content">
|
||||
<div class="calc-row">
|
||||
<span class="calc-label">输入:</span>
|
||||
<span class="calc-value">A = {{ inputA ? '1' : '0' }},B = {{ inputB ? '1' : '0' }},Cin =
|
||||
{{ carryIn ? '1' : '0' }}</span>
|
||||
<!-- 内部结构:用两个半加器来理解 -->
|
||||
<div class="structure-section">
|
||||
<div class="structure-label">全加器的内部 = 两个半加器串联</div>
|
||||
<div class="structure-row">
|
||||
<!-- 半加器 1 -->
|
||||
<div class="ha-block">
|
||||
<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 class="calc-row">
|
||||
<span class="calc-label">中间值:</span>
|
||||
<span class="calc-formula">xor1 = A ⊕ B = {{ inputA ? '1' : '0' }} ⊕
|
||||
{{ inputB ? '1' : '0' }} =
|
||||
<strong>{{ xor1 ? '1' : '0' }}</strong></span>
|
||||
<span class="calc-reason">({{ inputA !== inputB ? '不同' : '相同' }})</span>
|
||||
|
||||
<div class="chain-arrow">▸</div>
|
||||
|
||||
<!-- 半加器 2 -->
|
||||
<div class="ha-block">
|
||||
<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 class="calc-row">
|
||||
<span class="calc-label">本位:</span>
|
||||
<span class="calc-formula">Sum = xor1 ⊕ Cin = {{ xor1 ? '1' : '0' }} ⊕
|
||||
{{ carryIn ? '1' : '0' }} =
|
||||
<strong>{{ sumOut ? '1' : '0' }}</strong></span>
|
||||
<span class="calc-reason">({{ xor1 !== carryIn ? '不同' : '相同' }})</span>
|
||||
</div>
|
||||
<div class="calc-row">
|
||||
<span class="calc-label">进位:</span>
|
||||
<span class="calc-formula">Cout = (A∧B) ∨ (xor1∧Cin) = ({{ carry1 ? '1' : '0' }}) ∨ ({{
|
||||
carry2 ? '1' : '0'
|
||||
}}) = <strong>{{ carryOut ? '1' : '0' }}</strong></span>
|
||||
|
||||
<div class="chain-arrow">▸</div>
|
||||
|
||||
<!-- OR 合并 -->
|
||||
<div class="or-block">
|
||||
<div class="ha-title">第三步:合并进位</div>
|
||||
<div class="ha-desc">两路进位只要有一个是 1,就向高位进 1</div>
|
||||
<div class="ha-io">
|
||||
<div class="ha-in">
|
||||
<span class="io-tag c1">进位① = {{ carry1 ? '1' : '0' }}</span>
|
||||
<span class="io-tag c2">进位② = {{ carry2 ? '1' : '0' }}</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 class="info-box">
|
||||
<strong>核心思想:</strong>
|
||||
全加器 = 两个半加器 + 一个 OR 门。第一级半加器算
|
||||
A+B,第二级半加器把结果加上 Cin,OR 门合并两路进位信号。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -214,302 +133,168 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const inputA = ref(true)
|
||||
const inputB = ref(true)
|
||||
const inputB = ref(false)
|
||||
const carryIn = ref(false)
|
||||
|
||||
// 第一步:半加器 1
|
||||
const xor1 = 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 carry2 = computed(() => xor1.value && carryIn.value)
|
||||
|
||||
// 第三步:OR 合并
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.full-adder-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem 1.2rem;
|
||||
padding: 1.2rem;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
margin-bottom: 0.75rem;
|
||||
/* main area */
|
||||
.main-area { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1.2rem; }
|
||||
.left-panel { flex: 1; min-width: 220px; }
|
||||
.right-panel { flex: 1; min-width: 220px; }
|
||||
|
||||
/* 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 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
.result-display { display: flex; gap: 0.15rem; }
|
||||
.result-bit {
|
||||
width: 2.8rem; height: 2.8rem; border-radius: 6px; border: 2px solid var(--vp-c-divider);
|
||||
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 {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
.input-labels { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
|
||||
.il { font-size: 0.65rem; color: var(--vp-c-text-3); text-align: center; width: 2.8rem; }
|
||||
.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 {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
.vs-half {
|
||||
font-size: 0.78rem; color: var(--vp-c-text-2); line-height: 1.4;
|
||||
padding: 0.5rem 0.7rem; background: var(--vp-c-bg-alt); border-radius: 6px;
|
||||
}
|
||||
.vs-half strong { color: var(--vp-c-text-1); }
|
||||
|
||||
.term-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
/* truth table */
|
||||
.table-title { font-size: 0.75rem; font-weight: 600; color: var(--vp-c-text-2); margin-bottom: 0.4rem; }
|
||||
.truth-table { border-radius: 6px; overflow: hidden; border: 1px solid var(--vp-c-divider); }
|
||||
.tr {
|
||||
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;
|
||||
}
|
||||
|
||||
.term-name {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand-1);
|
||||
.tr:last-child { border-bottom: none; }
|
||||
.tr.header {
|
||||
background: var(--vp-c-bg-alt); font-weight: bold; font-family: system-ui;
|
||||
font-size: 0.7rem; 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; }
|
||||
|
||||
.term-desc {
|
||||
font-size: 0.68rem;
|
||||
color: var(--vp-c-text-3);
|
||||
line-height: 1.3;
|
||||
/* structure section */
|
||||
.structure-section { border-top: 1px solid var(--vp-c-divider); padding-top: 1rem; }
|
||||
.structure-label { font-size: 0.8rem; font-weight: 600; color: var(--vp-c-text-1); margin-bottom: 0.6rem; }
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
.ha-title { font-size: 0.72rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.15rem; }
|
||||
.ha-desc { font-size: 0.65rem; color: var(--vp-c-text-3); margin-bottom: 0.4rem; }
|
||||
|
||||
.ha-io { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }
|
||||
.ha-in { display: flex; flex-direction: column; gap: 0.2rem; }
|
||||
.ha-arrow { font-size: 0.8rem; color: var(--vp-c-text-3); padding: 0 0.15rem; }
|
||||
.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,
|
||||
.outputs {
|
||||
display: flex;
|
||||
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;
|
||||
.io-result {
|
||||
font-size: 0.68rem; font-family: monospace; padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px; background: var(--vp-c-bg-alt); color: var(--vp-c-text-3);
|
||||
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 {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
border-color: var(--vp-c-brand-1);
|
||||
.chain-arrow {
|
||||
display: flex; align-items: center; font-size: 1.2rem; color: var(--vp-c-text-3);
|
||||
flex-shrink: 0; padding: 0 0.1rem;
|
||||
}
|
||||
|
||||
.toggle-btn.cin-btn.on {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
border-color: #d97706;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.main-area { flex-direction: column; }
|
||||
.structure-row { flex-direction: column; }
|
||||
.chain-arrow { transform: rotate(90deg); justify-content: center; padding: 0.3rem 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
+2
-4
@@ -20,8 +20,7 @@
|
||||
<!-- MUX Demo -->
|
||||
<div v-if="currentTab === 'mux'" class="demo-panel">
|
||||
<div class="panel-desc">
|
||||
<strong>多路选择器 (MUX)</strong
|
||||
>:像铁路道岔一样,根据"选择信号"决定让哪一路数据通过。
|
||||
<strong>多路选择器 (MUX)</strong>:像铁路道岔一样,根据"选择信号"决定让哪一路数据通过。
|
||||
</div>
|
||||
<div class="mux-container">
|
||||
<div class="inputs">
|
||||
@@ -81,8 +80,7 @@
|
||||
<!-- Decoder Demo -->
|
||||
<div v-if="currentTab === 'decoder'" class="demo-panel">
|
||||
<div class="panel-desc">
|
||||
<strong>译码器 (Decoder)</strong
|
||||
>:将二进制输入转换为特定输出线的激活信号(例如 2位输入可以激活
|
||||
<strong>译码器 (Decoder)</strong>:将二进制输入转换为特定输出线的激活信号(例如 2位输入可以激活
|
||||
4根输出线中的一根)。
|
||||
</div>
|
||||
<div class="decoder-container">
|
||||
|
||||
+1
-1
@@ -37,9 +37,9 @@
|
||||
</div>
|
||||
<div class="change-process">
|
||||
<div
|
||||
class="process-step"
|
||||
v-for="(step, index) in changeSteps"
|
||||
:key="index"
|
||||
class="process-step"
|
||||
>
|
||||
<div class="step-coin">{{ step.coin }}</div>
|
||||
<div class="step-text">× {{ step.count }} = {{ step.value }}元</div>
|
||||
|
||||
+287
-349
@@ -1,167 +1,116 @@
|
||||
<template>
|
||||
<div class="half-adder-demo">
|
||||
<div class="demo-header">
|
||||
<span class="title">半加器 (Half Adder)</span>
|
||||
<span class="subtitle">最基础的二进制加法单元 ── 只能处理两个 1 位输入</span>
|
||||
<span class="title">半加器 (Half Adder) — 交互演示</span>
|
||||
<span class="subtitle">点击输入 A / B,看看这一位加法的结果</span>
|
||||
</div>
|
||||
|
||||
<div class="terms-box">
|
||||
<div class="term-item">
|
||||
<span class="term-name">本位 (Sum)</span>
|
||||
<span class="term-desc">当前位的计算结果,不考虑外部进位</span>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<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"
|
||||
>
|
||||
<!-- 主交互区 -->
|
||||
<div class="main-area">
|
||||
<!-- 左:输入和直观结果 -->
|
||||
<div class="left-panel">
|
||||
<div class="big-calc">
|
||||
<button class="big-bit" :class="{ on: inputA }" @click="inputA = !inputA">
|
||||
{{ inputA ? '1' : '0' }}
|
||||
</button>
|
||||
<span class="label">输入 A</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ on: inputB }"
|
||||
@click="inputB = !inputB"
|
||||
>
|
||||
<span class="op">+</span>
|
||||
<button class="big-bit" :class="{ on: inputB }" @click="inputB = !inputB">
|
||||
{{ inputB ? '1' : '0' }}
|
||||
</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 class="wires">
|
||||
<svg class="wire-svg" viewBox="0 0 100 150" preserveAspectRatio="none">
|
||||
<path
|
||||
d="M 0,30 C 50,30 50,40 100,40"
|
||||
fill="none"
|
||||
:stroke="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
|
||||
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 class="right-panel">
|
||||
<div class="table-title">所有可能的情况</div>
|
||||
<div class="truth-table">
|
||||
<div class="tr header">
|
||||
<span>A</span><span>B</span><span class="sum-col">写下(本位)</span><span class="carry-col">进位</span>
|
||||
</div>
|
||||
<div class="gate-formula">A ⊕ B</div>
|
||||
<div class="gate-desc">不同为 1 → 本位</div>
|
||||
</div>
|
||||
<div class="gate-box and-gate" :class="{ active: carryOut }">
|
||||
<div class="gate-header">
|
||||
<span class="gate-name">AND</span>
|
||||
<span class="gate-cn">与门</span>
|
||||
<div
|
||||
v-for="row in cases"
|
||||
:key="row.a + '' + row.b"
|
||||
class="tr"
|
||||
:class="{ active: row.a === +inputA && row.b === +inputB }"
|
||||
>
|
||||
<span>{{ row.a }}</span>
|
||||
<span>{{ row.b }}</span>
|
||||
<span class="sum-col">{{ row.sum }}</span>
|
||||
<span class="carry-col">{{ row.carry }}</span>
|
||||
</div>
|
||||
<div class="gate-formula">A ∧ B</div>
|
||||
<div class="gate-desc">全 1 为 1 → 进位</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wires outputs-wires">
|
||||
<svg class="wire-svg" viewBox="0 0 50 150" preserveAspectRatio="none">
|
||||
<line
|
||||
x1="0"
|
||||
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 class="pattern-note">
|
||||
<p>仔细看这张表,你会发现两个规律:</p>
|
||||
<ul>
|
||||
<li>「写下」列:只有 A 和 B <strong>不一样</strong>时才是 1 → 这个规律叫 <code>XOR(异或)</code></li>
|
||||
<li>「进位」列:只有 A 和 B <strong>都是 1</strong> 时才是 1 → 这个规律叫 <code>AND(与)</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calculation-box">
|
||||
<div class="calc-title">计算过程</div>
|
||||
<div class="calc-content">
|
||||
<div class="calc-row">
|
||||
<span class="calc-label">输入:</span>
|
||||
<span class="calc-value">A = {{ inputA ? '1' : '0' }},B = {{ inputB ? '1' : '0' }}</span>
|
||||
<!-- 电路连接图 -->
|
||||
<div class="circuit-section">
|
||||
<div class="circuit-label">电路是这样连的:</div>
|
||||
<div class="circuit-row">
|
||||
<div class="wire-inputs">
|
||||
<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 class="calc-row">
|
||||
<span class="calc-label">本位:</span>
|
||||
<span class="calc-formula">A ⊕ B = {{ inputA ? '1' : '0' }} ⊕ {{ inputB ? '1' : '0' }} =
|
||||
<strong>{{ sumOut ? '1' : '0' }}</strong></span>
|
||||
<span class="calc-reason">({{ inputA !== inputB ? '不同' : '相同' }})</span>
|
||||
<svg class="split-svg" viewBox="0 0 60 80" preserveAspectRatio="none">
|
||||
<!-- A 到 XOR -->
|
||||
<line x1="0" y1="20" x2="30" y2="20" :stroke="inputA ? '#3b82f6' : '#ccc'" stroke-width="2" />
|
||||
<line x1="30" y1="20" x2="60" y2="15" :stroke="inputA ? '#3b82f6' : '#ccc'" stroke-width="2" />
|
||||
<!-- 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 class="calc-row">
|
||||
<span class="calc-label">进位:</span>
|
||||
<span class="calc-formula">A ∧ B = {{ inputA ? '1' : '0' }} ∧ {{ inputB ? '1' : '0' }} =
|
||||
<strong>{{ carryOut ? '1' : '0' }}</strong></span>
|
||||
<span class="calc-reason">({{ inputA && inputB ? '全为 1' : '不全为 1' }})</span>
|
||||
<svg class="out-svg" viewBox="0 0 40 80" preserveAspectRatio="none">
|
||||
<line x1="0" y1="20" x2="40" y2="20" :stroke="sumOut ? '#16a34a' : '#ccc'" stroke-width="2" />
|
||||
<line x1="0" y1="60" x2="40" y2="60" :stroke="carryOut ? '#d97706' : '#ccc'" stroke-width="2" />
|
||||
</svg>
|
||||
<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 class="info-box">
|
||||
<strong>核心思想:</strong>
|
||||
半加器用 XOR 算"本位和",用 AND
|
||||
算"进位"。它是最小的加法单元,但无法处理来自低位的进位。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -169,288 +118,277 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const inputA = ref(false)
|
||||
const inputB = ref(true)
|
||||
const inputB = ref(false)
|
||||
|
||||
const sumOut = 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>
|
||||
|
||||
<style scoped>
|
||||
.half-adder-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem 1.2rem;
|
||||
padding: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 0.9rem;
|
||||
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);
|
||||
}
|
||||
|
||||
.terms-box {
|
||||
/* ── main area ── */
|
||||
.main-area {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.term-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
/* left */
|
||||
.left-panel { flex: 1; min-width: 200px; }
|
||||
|
||||
.term-name {
|
||||
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 {
|
||||
.big-calc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.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);
|
||||
.big-bit {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.toggle-btn.on {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
border-color: var(--vp-c-brand-1);
|
||||
.big-bit.on {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.out-val {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
border-radius: 4px;
|
||||
.op {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.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);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
font-size: 1.5rem;
|
||||
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: 80px;
|
||||
height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.outputs-wires {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.wire-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gates {
|
||||
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;
|
||||
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);
|
||||
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 {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
.result-labels {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
margin-top: 0.2rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.gate-desc {
|
||||
.rl {
|
||||
font-size: 0.65rem;
|
||||
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 {
|
||||
margin-top: 1rem;
|
||||
padding: 0.6rem 0.8rem;
|
||||
.explain-box {
|
||||
background: var(--vp-c-bg);
|
||||
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-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.calc-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
.truth-table { border-radius: 6px; overflow: hidden; border: 1px solid var(--vp-c-divider); margin-bottom: 0.75rem; }
|
||||
.tr {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 2fr 1.5fr;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.3rem;
|
||||
.pattern-note {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem 0.8rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.calc-label {
|
||||
color: var(--vp-c-text-3);
|
||||
min-width: 3.5rem;
|
||||
}
|
||||
|
||||
.calc-formula {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.calc-formula strong {
|
||||
.pattern-note p { margin: 0 0 0.4rem 0; }
|
||||
.pattern-note ul { margin: 0; padding-left: 1.2rem; }
|
||||
.pattern-note li { margin-bottom: 0.3rem; line-height: 1.4; }
|
||||
.pattern-note code {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.05rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.calc-reason {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.72rem;
|
||||
/* ── circuit section ── */
|
||||
.circuit-section {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
.circuit-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
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;
|
||||
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 {
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
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;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.main-area { flex-direction: column; }
|
||||
.circuit-row { overflow-x: auto; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
placeholder="值 (如: 苹果)"
|
||||
class="hash-input"
|
||||
/>
|
||||
<button @click="addData" class="add-btn">添加</button>
|
||||
<button class="add-btn" @click="addData">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+9
-21
@@ -35,8 +35,7 @@
|
||||
v-for="lang in era.languages"
|
||||
:key="lang"
|
||||
class="lang-dot"
|
||||
>{{ lang }}</span
|
||||
>
|
||||
>{{ lang }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,8 +90,7 @@
|
||||
v-for="lang in selectedParadigm.languages"
|
||||
:key="lang"
|
||||
class="lang-tag"
|
||||
>{{ lang }}</span
|
||||
>
|
||||
>{{ lang }}</span>
|
||||
</div>
|
||||
<div class="paradigm-detail-example">
|
||||
<pre><code>{{ selectedParadigm.example }}</code></pre>
|
||||
@@ -102,8 +100,7 @@
|
||||
v-for="t in selectedParadigm.traits"
|
||||
:key="t"
|
||||
class="trait-chip"
|
||||
>{{ t }}</span
|
||||
>
|
||||
>{{ t }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,8 +158,7 @@
|
||||
v-for="lang in rec.langs"
|
||||
:key="lang"
|
||||
class="choose-lang-tag"
|
||||
>{{ lang }}</span
|
||||
>
|
||||
>{{ lang }}</span>
|
||||
</div>
|
||||
<div class="choose-reason">{{ rec.reason }}</div>
|
||||
</div>
|
||||
@@ -185,19 +181,11 @@
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong>
|
||||
<span v-if="activeTab === 'timeline'"
|
||||
>编程语言从机器语言到现代高级语言,一直在朝着"更接近人类思维"的方向演化。</span
|
||||
>
|
||||
<span v-else-if="activeTab === 'paradigms'"
|
||||
>编程范式是思考问题的方式——命令式关注"怎么做",声明式关注"做什么",选择范式比选语言更重要。</span
|
||||
>
|
||||
<span v-else-if="activeTab === 'compare'"
|
||||
>没有最好的语言,只有最适合场景的语言。类型系统、运行方式、生态都是选择时的关键考量。</span
|
||||
>
|
||||
<span v-else
|
||||
>初学者先学 Python(简单通用),再学 JavaScript(Web
|
||||
必备),最后选一门静态语言(TypeScript/Go/Rust)深入。</span
|
||||
>
|
||||
<span v-if="activeTab === 'timeline'">编程语言从机器语言到现代高级语言,一直在朝着"更接近人类思维"的方向演化。</span>
|
||||
<span v-else-if="activeTab === 'paradigms'">编程范式是思考问题的方式——命令式关注"怎么做",声明式关注"做什么",选择范式比选语言更重要。</span>
|
||||
<span v-else-if="activeTab === 'compare'">没有最好的语言,只有最适合场景的语言。类型系统、运行方式、生态都是选择时的关键考量。</span>
|
||||
<span v-else>初学者先学 Python(简单通用),再学 JavaScript(Web
|
||||
必备),最后选一门静态语言(TypeScript/Go/Rust)深入。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,373 +1,349 @@
|
||||
<template>
|
||||
<div class="memory-demo">
|
||||
<div class="demo-controls">
|
||||
<button
|
||||
class="allocate-btn wechat"
|
||||
@click="allocate('wechat')"
|
||||
:disabled="!hasFreeSpace"
|
||||
>
|
||||
+ 给微信分配数据
|
||||
</button>
|
||||
<button
|
||||
class="allocate-btn game"
|
||||
@click="allocate('game')"
|
||||
:disabled="!hasFreeSpace"
|
||||
>
|
||||
+ 给游戏分配数据
|
||||
</button>
|
||||
<button class="reset-btn" @click="reset">↺ 重置</button>
|
||||
</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 class="demo">
|
||||
<div class="title">🧠 操作系统给每个程序"画饼"</div>
|
||||
|
||||
<div class="scene">
|
||||
<!-- 程序视角 -->
|
||||
<div class="view-box">
|
||||
<div class="view-title">📱 程序以为的内存(虚拟)</div>
|
||||
<div class="virtual-mem">
|
||||
<div class="proc-mem wechat">
|
||||
<div class="proc-label">💬 微信</div>
|
||||
<div class="mem-blocks">
|
||||
<div
|
||||
v-for="n in 4"
|
||||
:key="n"
|
||||
class="v-block"
|
||||
:class="{ filled: wechatProgress >= n * 25 }"
|
||||
>{{ n }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="process-vm game">
|
||||
<div class="title">
|
||||
🎮 游戏的虚拟内存<br />(它也认为自己独占了空间)
|
||||
</div>
|
||||
<div class="vm-blocks">
|
||||
<div
|
||||
v-for="i in 4"
|
||||
:key="'g' + i"
|
||||
class="block"
|
||||
:class="{ filled: gameBlocks >= i }"
|
||||
>
|
||||
{{ gameBlocks >= i ? '数据 ' + i : '虚拟空闲' }}
|
||||
<div class="proc-mem game">
|
||||
<div class="proc-label">🎮 游戏</div>
|
||||
<div class="mem-blocks">
|
||||
<div
|
||||
v-for="n in 4"
|
||||
:key="n"
|
||||
class="v-block game"
|
||||
:class="{ filled: gameProgress >= n * 25 }"
|
||||
>{{ n }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OS 页表 (映射表) -->
|
||||
<div class="os-page-table">
|
||||
<div class="title">保安大叔 (OS 页表)</div>
|
||||
<div class="table-info">
|
||||
当程序存数据时,<br />由我暗中转移到真正的物理缝隙里。
|
||||
<!-- 映射箭头 -->
|
||||
<div class="mapping-arrow">
|
||||
<div class="arrow-text">操作系统偷偷映射 ↓</div>
|
||||
<div class="mapping-lines">
|
||||
<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 class="physical-memory">
|
||||
<div class="title">🗄️ 真实的物理内存条<br />(其实像个大杂烩一样乱)</div>
|
||||
<div class="pm-blocks">
|
||||
<div
|
||||
v-for="(block, idx) in physicalBlocks"
|
||||
:key="'p' + idx"
|
||||
class="block"
|
||||
:class="[block.type, { occupied: block.type !== 'empty' }]"
|
||||
<div class="view-box physical">
|
||||
<div class="view-title">💾 真实的内存条(物理)</div>
|
||||
<div class="physical-mem">
|
||||
<div
|
||||
v-for="(block, idx) in physicalBlocks"
|
||||
:key="idx"
|
||||
class="p-block"
|
||||
: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 class="explanation-box" v-if="wechatBlocks > 0 || gameBlocks > 0">
|
||||
💡
|
||||
发现了没?尽管右侧真正的物理内存已经被塞得像个狗皮膏药,但在左侧的微信和游戏眼里,自己的内存条永远是连续且干净的。更重要的是,微信绝对访问不到橘色的物理块,保证了安全!
|
||||
<div class="explain">
|
||||
<strong>💡 原理:</strong>每个程序以为自己独占连续的内存(左),实际上操作系统把数据分散存到真实内存各处(右)。程序看到的地址都是"假"的,操作系统负责翻译。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const wechatBlocks = ref(0)
|
||||
const gameBlocks = ref(0)
|
||||
const wechatProgress = ref(0)
|
||||
const gameProgress = ref(0)
|
||||
const currentMapping = ref(0)
|
||||
|
||||
// 初始物理内存状态,模拟碎片化
|
||||
// empty = 空, os = 系统占用
|
||||
const initialPhysicalBlocks = [
|
||||
{ type: 'os', label: '系统核心占用' },
|
||||
{ type: 'empty', label: '空闲' },
|
||||
{ type: 'os', label: '系统保留' },
|
||||
{ type: 'empty', label: '空闲' },
|
||||
{ type: 'empty', label: '空闲' },
|
||||
{ type: 'empty', label: '空闲' },
|
||||
{ type: 'os', label: '系统驱动' },
|
||||
{ type: 'empty', label: '空闲' }
|
||||
// 物理内存状态
|
||||
const physicalBlocks = ref([
|
||||
{ type: 'os', label: '系统', active: false },
|
||||
{ type: 'empty', label: '', active: false },
|
||||
{ type: 'empty', label: '', active: false },
|
||||
{ type: 'os', label: '系统', active: false },
|
||||
{ type: 'empty', label: '', active: false },
|
||||
{ type: 'empty', label: '', active: false },
|
||||
{ type: 'empty', label: '', active: false },
|
||||
{ type: 'os', label: '系统', active: false }
|
||||
])
|
||||
|
||||
// 映射关系(虚拟地址 -> 物理地址)
|
||||
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 freeSpaceCount = computed(() => {
|
||||
return physicalBlocks.value.filter((b) => b.type === 'empty').length
|
||||
const visibleMappings = computed(() => {
|
||||
return mappings.slice(0, currentMapping.value)
|
||||
})
|
||||
|
||||
const hasFreeSpace = computed(() => freeSpaceCount.value > 0)
|
||||
let timer = null
|
||||
let phase = 0
|
||||
|
||||
const allocate = (process) => {
|
||||
if (!hasFreeSpace.value) return
|
||||
|
||||
// Find a process block logic
|
||||
if (process === 'wechat' && wechatBlocks.value < 4) {
|
||||
wechatBlocks.value++
|
||||
fillRandomEmptyBlock('wechat', `微信数据 ${wechatBlocks.value}`)
|
||||
} else if (process === 'game' && gameBlocks.value < 4) {
|
||||
gameBlocks.value++
|
||||
fillRandomEmptyBlock('game', `游戏数据 ${gameBlocks.value}`)
|
||||
const runDemo = () => {
|
||||
switch(phase) {
|
||||
case 0: // 微信申请内存
|
||||
wechatProgress.value = 50
|
||||
physicalBlocks.value[1] = { type: 'wechat', label: 'W1', active: true }
|
||||
physicalBlocks.value[2] = { type: 'wechat', label: 'W2', active: true }
|
||||
currentMapping.value = 2
|
||||
phase = 1
|
||||
break
|
||||
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) => {
|
||||
const emptyIndices = []
|
||||
physicalBlocks.value.forEach((b, i) => {
|
||||
if (b.type === 'empty') emptyIndices.push(i)
|
||||
})
|
||||
onMounted(() => {
|
||||
timer = setInterval(runDemo, 2000)
|
||||
})
|
||||
|
||||
if (emptyIndices.length > 0) {
|
||||
const randomIndex =
|
||||
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))
|
||||
}
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.memory-demo {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
padding: 16px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.virtual-cluster {
|
||||
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 {
|
||||
.scene {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding: 0.6rem;
|
||||
.view-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
.process-vm .block {
|
||||
background: var(--vp-c-bg-mute);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-3);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.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;
|
||||
.v-block.filled {
|
||||
background: #16a34a33;
|
||||
border: 1px solid #16a34a;
|
||||
color: #16a34a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.os-page-table {
|
||||
flex: 1;
|
||||
.v-block.game.filled {
|
||||
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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
border: 2px solid var(--vp-c-brand-1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
font-size: 9px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.os-page-table .table-info {
|
||||
font-size: 0.8rem;
|
||||
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);
|
||||
.p-block.os {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-style: dashed;
|
||||
}
|
||||
.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;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
@keyframes popIn {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
.p-block.wechat {
|
||||
background: #16a34a22;
|
||||
border-color: #16a34a;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.explanation-box {
|
||||
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;
|
||||
animation: fadeIn 0.5s;
|
||||
.p-block.game {
|
||||
background: #d9770622;
|
||||
border-color: #d97706;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.p-block.active {
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.p-addr {
|
||||
font-size: 8px;
|
||||
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>
|
||||
|
||||
@@ -70,8 +70,7 @@
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong
|
||||
>分层设计让网络协议模块化,每层只关心自己的职责。数据从应用层向下传递时,每层都会添加自己的"信封"(头部),接收时再逐层拆开。
|
||||
<strong>核心思想:</strong>分层设计让网络协议模块化,每层只关心自己的职责。数据从应用层向下传递时,每层都会添加自己的"信封"(头部),接收时再逐层拆开。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+317
@@ -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>
|
||||
-313
@@ -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>
|
||||
<div class="process-demo">
|
||||
<div class="controls-section">
|
||||
<button
|
||||
class="action-btn"
|
||||
:class="{ active: isRunning }"
|
||||
@click="toggleSimulation"
|
||||
>
|
||||
{{ isRunning ? '⏸ 暂停时间片轮转' : '▶️ 启动 CPU' }}
|
||||
</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 class="demo">
|
||||
<div class="title">⏱️ CPU 在疯狂切换,你感觉不出来</div>
|
||||
|
||||
<div class="cpu-core">
|
||||
<div class="cpu-label">CPU</div>
|
||||
<div class="current-task" :class="{ switching: isSwitching }">
|
||||
<span class="task-icon">{{ currentTask.icon }}</span>
|
||||
<span class="task-name">{{ currentTask.name }}</span>
|
||||
</div>
|
||||
<div class="time-slice">时间片: {{ timeLeft }}ms</div>
|
||||
</div>
|
||||
|
||||
<div class="cpu-container">
|
||||
<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 class="process-queue">
|
||||
<div
|
||||
v-for="p in processes"
|
||||
:key="p.id"
|
||||
class="process-card"
|
||||
:class="{ active: p.id === activeProcessId }"
|
||||
v-for="(proc, idx) in processes"
|
||||
:key="proc.id"
|
||||
class="process"
|
||||
:class="{
|
||||
active: idx === currentIdx,
|
||||
waiting: idx !== currentIdx,
|
||||
done: proc.progress >= 100
|
||||
}"
|
||||
:style="{ '--progress': proc.progress + '%' }"
|
||||
>
|
||||
<div class="p-header">
|
||||
<div class="p-title">
|
||||
<span class="icon">{{ p.icon }}</span>
|
||||
<span class="name">{{ p.name }}</span>
|
||||
<span class="p-icon">{{ proc.icon }}</span>
|
||||
<div class="p-info">
|
||||
<span class="p-name">{{ proc.name }}</span>
|
||||
<div class="p-bar">
|
||||
<div class="p-fill"></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>
|
||||
<span class="p-status">{{ idx === currentIdx ? '运行中' : (proc.progress >= 100 ? '完成' : '等待') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="explanation-box"
|
||||
:class="{ show: isRunning && speed === 'fast' }"
|
||||
>
|
||||
💡
|
||||
**关键启示**:当切换速度足够快时,肉眼已经无法分辨谁在“等待”。这也就是为什么只有一个
|
||||
CPU 核心的电脑,依然能让你一边听歌一边打字!
|
||||
<div class="explain">
|
||||
<strong>💡 原理:</strong>CPU 每 {{ sliceTime }}ms 切换一次进程,因为太快了你感觉是"同时运行"。实际上每个进程都在断断续续地执行。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
|
||||
const isRunning = ref(false)
|
||||
const activeProcessId = ref(null)
|
||||
const speed = ref('slow')
|
||||
let interval = null
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const processes = ref([
|
||||
{ id: 1, name: '微信接收', icon: '💬', progress: 0 },
|
||||
{ id: 2, name: '音乐播放', icon: '🎵', progress: 0 },
|
||||
{ id: 3, name: '游戏渲染', icon: '🎮', progress: 0 }
|
||||
{ id: 1, name: '微信', icon: '💬', progress: 0 },
|
||||
{ id: 2, name: '音乐', icon: '🎵', progress: 0 },
|
||||
{ id: 3, name: '浏览器', icon: '🌐', progress: 0 }
|
||||
])
|
||||
|
||||
const activeProcess = computed(() =>
|
||||
processes.value.find((p) => p.id === activeProcessId.value)
|
||||
)
|
||||
const currentIdx = ref(0)
|
||||
const timeLeft = ref(0)
|
||||
const isSwitching = ref(false)
|
||||
const sliceTime = 100 // 每个时间片100ms(演示用,实际是10ms左右)
|
||||
|
||||
const setSpeed = (s) => {
|
||||
speed.value = s
|
||||
if (isRunning.value) {
|
||||
clearInterval(interval)
|
||||
startLoop()
|
||||
let timer = null
|
||||
let switchTimer = null
|
||||
|
||||
const switchTask = () => {
|
||||
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 = () => {
|
||||
const switchTime = speed.value === 'slow' ? 1200 : 80 // 慢动作 1.2s,快动作极快
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
timeLeft.value = sliceTime
|
||||
timer = setInterval(tick, 10) // 每10ms更新一次
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (interval) clearInterval(interval)
|
||||
clearInterval(timer)
|
||||
clearTimeout(switchTimer)
|
||||
})
|
||||
|
||||
const currentTask = computed(() => processes.value[currentIdx.value])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.process-demo {
|
||||
background: var(--vp-c-bg-soft);
|
||||
.demo {
|
||||
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;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 160px;
|
||||
}
|
||||
.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;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cpu-core {
|
||||
width: 240px;
|
||||
height: 90px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s;
|
||||
background: linear-gradient(135deg, #667eea22, #764ba222);
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
}
|
||||
.cpu-core.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 20px var(--vp-c-brand-soft);
|
||||
}
|
||||
.cpu-title {
|
||||
font-weight: 800;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: 2px;
|
||||
|
||||
.cpu-label {
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.current-task {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.task-badge {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
padding: 0.2rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.task-badge.idle {
|
||||
background: var(--vp-c-text-3);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* 连接线动画占位,简化效果,用发亮的虚线替代 */
|
||||
.connector {
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: var(--vp-c-divider);
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
.current-task.switching {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.processes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
.task-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.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: '';
|
||||
.time-slice {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.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;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-text-3);
|
||||
background: var(--vp-c-bg);
|
||||
padding: 2px 6px;
|
||||
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;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
.process {
|
||||
display: flex;
|
||||
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);
|
||||
border-radius: 4px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
|
||||
.p-fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand-1);
|
||||
width: var(--progress);
|
||||
background: #667eea;
|
||||
border-radius: 2px;
|
||||
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 {
|
||||
font-size: 0.75rem;
|
||||
.process.active .p-status {
|
||||
color: #667eea;
|
||||
background: #667eea22;
|
||||
}
|
||||
|
||||
.explain {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.explanation-box {
|
||||
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);
|
||||
}
|
||||
.explain strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
-446
@@ -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>
|
||||
+301
@@ -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>
|
||||
+1
-2
@@ -110,8 +110,7 @@ function traverse(folder) {
|
||||
traverse(item) // 递归调用!
|
||||
}
|
||||
}
|
||||
}</pre
|
||||
>
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
<span
|
||||
class="val-box"
|
||||
:class="{ on: storedData === 1, flash: isWriting }"
|
||||
>{{ storedData }}</span
|
||||
>
|
||||
>{{ storedData }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Output -->
|
||||
|
||||
+4
-4
@@ -40,10 +40,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-controls">
|
||||
<button @click="startLinearSearch" class="search-btn">
|
||||
<button class="search-btn" @click="startLinearSearch">
|
||||
开始查找
|
||||
</button>
|
||||
<button @click="reset" class="reset-btn">重置</button>
|
||||
<button class="reset-btn" @click="reset">重置</button>
|
||||
</div>
|
||||
<div class="search-info">
|
||||
目标数字:<input
|
||||
@@ -89,8 +89,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-controls">
|
||||
<button @click="binaryStep" class="search-btn">下一步</button>
|
||||
<button @click="resetBinary" class="reset-btn">重置</button>
|
||||
<button class="search-btn" @click="binaryStep">下一步</button>
|
||||
<button class="reset-btn" @click="resetBinary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="algo-stats">
|
||||
|
||||
+3
-3
@@ -22,9 +22,9 @@
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="generateArray" class="control-btn">生成新数组</button>
|
||||
<button @click="startBubbleSort" class="control-btn">冒泡排序</button>
|
||||
<button @click="startQuickSort" class="control-btn">快速排序</button>
|
||||
<button class="control-btn" @click="generateArray">生成新数组</button>
|
||||
<button class="control-btn" @click="startBubbleSort">冒泡排序</button>
|
||||
<button class="control-btn" @click="startQuickSort">快速排序</button>
|
||||
</div>
|
||||
|
||||
<div class="algorithm-info">
|
||||
|
||||
@@ -54,8 +54,7 @@
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong
|
||||
>存储遵循"金字塔"原则:越快的存储越贵、容量越小。CPU
|
||||
<strong>核心思想:</strong>存储遵循"金字塔"原则:越快的存储越贵、容量越小。CPU
|
||||
需要的数据放在最快的存储(寄存器、缓存),暂时不用的放在慢速大容量存储(磁盘、云端)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-6
@@ -114,12 +114,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span class="legend-item"
|
||||
><span class="network-box" /> 网络位 ({{ cidr }}位)</span
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="host-box" /> 主机位 ({{ 32 - cidr }}位)</span
|
||||
>
|
||||
<span class="legend-item"><span class="network-box" /> 网络位 ({{ cidr }}位)</span>
|
||||
<span class="legend-item"><span class="host-box" /> 主机位 ({{ 32 - cidr }}位)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-2
@@ -62,8 +62,7 @@
|
||||
v-for="(use, i) in currentProtocol.useCases"
|
||||
:key="i"
|
||||
class="use-tag"
|
||||
>{{ use }}</span
|
||||
>
|
||||
>{{ use }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-6
@@ -44,8 +44,7 @@
|
||||
:class="{
|
||||
sending: sendingBit === i && activeType === 'serial'
|
||||
}"
|
||||
>{{ bit }}</span
|
||||
>
|
||||
>{{ bit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channels">
|
||||
@@ -57,8 +56,7 @@
|
||||
:key="i"
|
||||
class="flow-dot"
|
||||
:class="{ active: sendingBit !== null }"
|
||||
>●</span
|
||||
>
|
||||
>●</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="channel parallel">
|
||||
@@ -119,8 +117,7 @@
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong
|
||||
>现代高速传输多采用串行方式。虽然并行"看起来"更快(一次传多位),但串行可以跑更高频率,抗干扰更强,实际速度反而更快。
|
||||
<strong>核心思想:</strong>现代高速传输多采用串行方式。虽然并行"看起来"更快(一次传多位),但串行可以跑更高频率,抗干扰更强,实际速度反而更快。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@
|
||||
<div class="comparison-side tcp-side">
|
||||
<div class="side-header">TCP</div>
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,7 +76,7 @@
|
||||
<div class="comparison-side udp-side">
|
||||
<div class="side-header">UDP</div>
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-28
@@ -52,8 +52,7 @@
|
||||
v-for="t in selectedQuadrant.traits"
|
||||
:key="t"
|
||||
class="trait-tag"
|
||||
>{{ t }}</span
|
||||
>
|
||||
>{{ t }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,18 +110,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="convert-summary">
|
||||
<span v-if="activeLang === 'JavaScript'" class="summary-tag weak"
|
||||
>弱类型:隐式转换,结果常出人意料</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 class="summary-tag strong"
|
||||
>强类型:类型不匹配就报错,零容忍</span
|
||||
>
|
||||
<span v-if="activeLang === 'JavaScript'" class="summary-tag weak">弱类型:隐式转换,结果常出人意料</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 class="summary-tag strong">强类型:类型不匹配就报错,零容忍</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -146,7 +137,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -155,19 +146,11 @@
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong>
|
||||
<span v-if="activeTab === 'quadrant'"
|
||||
>类型系统在两个维度上做选择——何时检查(静态/动态)和是否允许隐式转换(强/弱)。没有最好的组合,只有最适合的场景。</span
|
||||
>
|
||||
<span v-else-if="activeTab === 'check'"
|
||||
>静态类型在编译时就能发现错误,动态类型要到运行时才知道——越早发现
|
||||
bug,修复成本越低。</span
|
||||
>
|
||||
<span v-else-if="activeTab === 'convert'"
|
||||
>弱类型语言会"猜"你的意思做隐式转换(常出错),强类型语言要求你明确表达意图(更安全)。</span
|
||||
>
|
||||
<span v-else
|
||||
>类型推断让你两全其美:代码像动态语言一样简洁,编译器像静态语言一样严格检查。</span
|
||||
>
|
||||
<span v-if="activeTab === 'quadrant'">类型系统在两个维度上做选择——何时检查(静态/动态)和是否允许隐式转换(强/弱)。没有最好的组合,只有最适合的场景。</span>
|
||||
<span v-else-if="activeTab === 'check'">静态类型在编译时就能发现错误,动态类型要到运行时才知道——越早发现
|
||||
bug,修复成本越低。</span>
|
||||
<span v-else-if="activeTab === 'convert'">弱类型语言会"猜"你的意思做隐式转换(常出错),强类型语言要求你明确表达意图(更安全)。</span>
|
||||
<span v-else>类型推断让你两全其美:代码像动态语言一样简洁,编译器像静态语言一样严格检查。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<div class="slider-group">
|
||||
<label>采样频率:{{ sampleRate }} 次/秒</label>
|
||||
<input
|
||||
type="range"
|
||||
v-model="sliderValue"
|
||||
type="range"
|
||||
min="1"
|
||||
max="50"
|
||||
step="1"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</span>
|
||||
</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-info">
|
||||
<div class="info-row">
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inspection-box empty" v-else>
|
||||
<div v-else class="inspection-box empty">
|
||||
将鼠标悬停在左侧画布的方块上
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+736
-200
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-panel" v-if="currentLayer">
|
||||
<div v-if="currentLayer" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ currentLayer.icon }}</span>
|
||||
<span class="detail-name">{{ currentLayer.name }}</span>
|
||||
|
||||
@@ -38,13 +38,13 @@
|
||||
</div>
|
||||
|
||||
<div class="traffic-controls">
|
||||
<button @click="allocateUser" class="btn-primary">
|
||||
<button class="btn-primary" @click="allocateUser">
|
||||
👤 分配1个用户
|
||||
</button>
|
||||
<button @click="allocateBatch" class="btn-secondary">
|
||||
<button class="btn-secondary" @click="allocateBatch">
|
||||
👥 分配100个用户
|
||||
</button>
|
||||
<button @click="resetTraffic" class="btn-tertiary">🔄 重置</button>
|
||||
<button class="btn-tertiary" @click="resetTraffic">🔄 重置</button>
|
||||
</div>
|
||||
|
||||
<div class="traffic-stats">
|
||||
@@ -64,9 +64,7 @@
|
||||
|
||||
<div class="tips">
|
||||
<span class="tips-icon">💡</span>
|
||||
<span class="tips-text"
|
||||
>50/50分配能最快检测出差异,确保两组样本量足够大以获得统计显著性</span
|
||||
>
|
||||
<span class="tips-text">50/50分配能最快检测出差异,确保两组样本量足够大以获得统计显著性</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -260,7 +258,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="calculateSampleSize" class="btn-primary btn-calc">
|
||||
<button class="btn-primary btn-calc" @click="calculateSampleSize">
|
||||
🧮 计算所需样本量
|
||||
</button>
|
||||
|
||||
@@ -296,9 +294,7 @@
|
||||
|
||||
<div class="tips">
|
||||
<span class="tips-icon">💡</span>
|
||||
<span class="tips-text"
|
||||
>提升目标越小,所需样本量越大。5%的提升比20%的提升需要更多样本</span
|
||||
>
|
||||
<span class="tips-text">提升目标越小,所需样本量越大。5%的提升比20%的提升需要更多样本</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -175,19 +175,11 @@
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong>
|
||||
<span v-if="activeMode === 'playground'"
|
||||
>正则表达式是一种用特殊符号描述文本模式的语言,在搜索、替换、数据验证中无处不在。</span
|
||||
>
|
||||
<span v-else-if="activeMode === 'cheatsheet'"
|
||||
>记住几个核心符号(. * + ? \d \w [] ())就能覆盖 80%
|
||||
的使用场景。点击任意符号可直接试验。</span
|
||||
>
|
||||
<span v-else-if="activeMode === 'patterns'"
|
||||
>不需要自己从零写正则——常见场景(邮箱、手机号、URL)都有成熟的模式可以直接复用。</span
|
||||
>
|
||||
<span v-else
|
||||
>正则引擎从左到右逐字符匹配,遇到量词会"贪婪"地尽量多匹配,失败时"回溯"尝试其他路径。</span
|
||||
>
|
||||
<span v-if="activeMode === 'playground'">正则表达式是一种用特殊符号描述文本模式的语言,在搜索、替换、数据验证中无处不在。</span>
|
||||
<span v-else-if="activeMode === 'cheatsheet'">记住几个核心符号(. * + ? \d \w [] ())就能覆盖 80%
|
||||
的使用场景。点击任意符号可直接试验。</span>
|
||||
<span v-else-if="activeMode === 'patterns'">不需要自己从零写正则——常见场景(邮箱、手机号、URL)都有成熟的模式可以直接复用。</span>
|
||||
<span v-else>正则引擎从左到右逐字符匹配,遇到量词会"贪婪"地尽量多匹配,失败时"回溯"尝试其他路径。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<div class="ssh-auth-demo">
|
||||
<div class="demo-header">
|
||||
<span class="title">SSH 密钥认证:你的数字身份证</span>
|
||||
<span class="subtitle"
|
||||
>对称加密 vs 非对称加密 · 密钥对生成 · 认证流程</span
|
||||
>
|
||||
<span class="subtitle">对称加密 vs 非对称加密 · 密钥对生成 · 认证流程</span>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
@@ -28,7 +26,7 @@
|
||||
<div class="card-icon">🔑</div>
|
||||
<div class="card-title">密码登录</div>
|
||||
<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-text">{{ step }}</span>
|
||||
</div>
|
||||
@@ -43,7 +41,7 @@
|
||||
<div class="card-icon">🔐</div>
|
||||
<div class="card-title">密钥登录</div>
|
||||
<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-text">{{ step }}</span>
|
||||
</div>
|
||||
@@ -144,18 +142,14 @@
|
||||
</div>
|
||||
<div :class="['msg', { active: authStep >= 2 }]" class="msg-left">
|
||||
<span class="msg-label">② 发送随机挑战</span>
|
||||
<span class="msg-detail"
|
||||
>"请证明你有私钥:用它签名这段随机数据"</span
|
||||
>
|
||||
<span class="msg-detail">"请证明你有私钥:用它签名这段随机数据"</span>
|
||||
</div>
|
||||
<div
|
||||
:class="['msg', { active: authStep >= 3 }]"
|
||||
class="msg-right"
|
||||
>
|
||||
<span class="msg-label">③ 返回签名</span>
|
||||
<span class="msg-detail"
|
||||
>"用私钥签名后的结果(私钥本身不发送)"</span
|
||||
>
|
||||
<span class="msg-detail">"用私钥签名后的结果(私钥本身不发送)"</span>
|
||||
</div>
|
||||
<div :class="['msg', { active: authStep >= 4 }]" class="msg-left">
|
||||
<span class="msg-label">④ 用公钥验证</span>
|
||||
@@ -163,9 +157,7 @@
|
||||
</div>
|
||||
<div :class="['msg', 'msg-result', { active: authStep >= 5 }]">
|
||||
<span class="msg-label">⑤ 认证成功</span>
|
||||
<span class="msg-detail"
|
||||
>"欢迎登录!从始至终,私钥没离开过你的电脑"</span
|
||||
>
|
||||
<span class="msg-detail">"欢迎登录!从始至终,私钥没离开过你的电脑"</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -211,21 +203,13 @@ Host github.com
|
||||
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong>
|
||||
<span v-if="activeScenario === 'compare'"
|
||||
>SSH
|
||||
密钥登录比密码更安全,因为私钥从不在网络上传输,无法被中间人窃取。</span
|
||||
>
|
||||
<span v-else-if="activeScenario === 'keygen'"
|
||||
>一次 ssh-keygen
|
||||
生成一对密钥:私钥自己保管,公钥放到目标服务器或平台。</span
|
||||
>
|
||||
<span v-else-if="activeScenario === 'auth'"
|
||||
>认证过程基于"挑战-响应"机制:服务器出题,你的私钥签名作答,公钥验证答案。全程私钥不离开本机。</span
|
||||
>
|
||||
<span v-else
|
||||
>SSH 密钥不仅用于服务器登录,也是 Git (GitHub/GitLab)
|
||||
等开发工具的标准身份认证方式。</span
|
||||
>
|
||||
<span v-if="activeScenario === 'compare'">SSH
|
||||
密钥登录比密码更安全,因为私钥从不在网络上传输,无法被中间人窃取。</span>
|
||||
<span v-else-if="activeScenario === 'keygen'">一次 ssh-keygen
|
||||
生成一对密钥:私钥自己保管,公钥放到目标服务器或平台。</span>
|
||||
<span v-else-if="activeScenario === 'auth'">认证过程基于"挑战-响应"机制:服务器出题,你的私钥签名作答,公钥验证答案。全程私钥不离开本机。</span>
|
||||
<span v-else>SSH 密钥不仅用于服务器登录,也是 Git (GitHub/GitLab)
|
||||
等开发工具的标准身份认证方式。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<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-text">修改 → 布局 → 绘制</span>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
@mouseenter="highlightedTag = node.tag"
|
||||
@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 v-if="node.text" class="node-text">"{{ node.text }}"</span>
|
||||
</div>
|
||||
|
||||
@@ -143,11 +143,11 @@
|
||||
<button class="action-btn outline" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box" v-if="mode === 'native'">
|
||||
<div v-if="mode === 'native'" class="info-box">
|
||||
<strong>为什么不自动?</strong>
|
||||
<span>JavaScript 的变量是"无感知"的。你执行 <code>count = 4</code> 时,JavaScript 引擎只是把内存中 count 的值从 3 改成 4,仅此而已。它不会通知任何人,不会触发任何回调,不会去检查页面上哪里显示了 count。所以界面不会有任何变化——除非你自己写代码去更新 DOM。</span>
|
||||
</div>
|
||||
<div class="info-box" v-else>
|
||||
<div v-else class="info-box">
|
||||
<strong>框架怎么做到的?</strong>
|
||||
<span>框架把你的数据用特殊机制包裹起来。以 Vue 为例,它用 JavaScript 的 Proxy(代理)功能拦截你对变量的赋值操作。当你写 <code>count = 4</code> 时,Proxy 会在赋值的同时自动执行一段"通知"代码,告诉框架"count 变了",框架再去找到所有用到 count 的 DOM 节点并更新它们。整个过程你不需要写任何额外代码。</span>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="area-header">
|
||||
<span class="area-icon">📝</span>
|
||||
<span class="area-title">工作区</span>
|
||||
<span class="area-desc">Working Directory<br/>你正在改的文件</span>
|
||||
<span class="area-desc">Working Directory<br />你正在改的文件</span>
|
||||
</div>
|
||||
<div class="area-body">
|
||||
<div class="area-label">Changes not staged for commit:</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
<div class="area-header">
|
||||
<span class="area-icon">📦</span>
|
||||
<span class="area-title">暂存区</span>
|
||||
<span class="area-desc">Staging Area<br/>准备这次提交的文件</span>
|
||||
<span class="area-desc">Staging Area<br />准备这次提交的文件</span>
|
||||
</div>
|
||||
<div class="area-body">
|
||||
<div class="area-label">Changes to be committed:</div>
|
||||
@@ -90,7 +90,7 @@
|
||||
<div class="area-header">
|
||||
<span class="area-icon">🗄️</span>
|
||||
<span class="area-title">仓库</span>
|
||||
<span class="area-desc">Repository (.git)<br/>永久保存的版本</span>
|
||||
<span class="area-desc">Repository (.git)<br />永久保存的版本</span>
|
||||
</div>
|
||||
<div class="area-body">
|
||||
<div class="area-label">已提交记录 (git log):</div>
|
||||
|
||||
@@ -105,7 +105,8 @@ function reset() {
|
||||
|
||||
<div :class="['panel browser-panel', {
|
||||
highlight: steps[currentStep].highlight === 'browser' || steps[currentStep].highlight === 'page' || steps[currentStep].highlight === 'hmr'
|
||||
}]">
|
||||
}]"
|
||||
>
|
||||
<div class="panel-header">
|
||||
<span class="dot red" /><span class="dot yellow" /><span class="dot green" />
|
||||
<span class="panel-title">浏览器</span>
|
||||
|
||||
+161
-928
File diff suppressed because it is too large
Load Diff
@@ -1,700 +1,51 @@
|
||||
<!--
|
||||
DnsLookupDemo.vue
|
||||
DNS查询演示 - 增强技术细节版
|
||||
|
||||
设计理念:
|
||||
1. 循循善诱:通过"接力跑腿"的比喻,展示浏览器如何一步步找到IP。
|
||||
2. 技术硬核:新增终端模拟器,展示真实的 dig/系统命令输出,解决"太抽象"的问题。
|
||||
3. 紧凑布局:横向流式布局,固定底部详情板。
|
||||
-->
|
||||
<template>
|
||||
<div class="dns-compact">
|
||||
<!-- 顶部控制栏 -->
|
||||
<div class="top-bar">
|
||||
<div class="title-section">
|
||||
<span class="app-icon">🌐</span>
|
||||
<span class="app-title">DNS 寻址原理</span>
|
||||
</div>
|
||||
|
||||
<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 class="dns-lookup-demo">
|
||||
<div class="flow">
|
||||
<span class="domain">google.com</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="dns">DNS</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="ip">142.250.80.46</span>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
.dns-compact {
|
||||
.dns-lookup-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.6rem 0.8rem;
|
||||
margin: 0.75rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
.flow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
.domain {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.target-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.target-select select {
|
||||
padding: 4px 8px;
|
||||
.dns {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
.ip {
|
||||
color: #059669;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-btn.primary { background: var(--vp-c-brand); color: white; }
|
||||
.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;
|
||||
.arrow {
|
||||
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>
|
||||
|
||||
@@ -1,531 +1,198 @@
|
||||
<!--
|
||||
HttpExchangeDemo.vue
|
||||
HTTP请求响应演示 - 紧凑交互版
|
||||
|
||||
设计理念:
|
||||
1. 循循善诱:用"快递员投递"类比 HTTP 请求响应。
|
||||
2. 紧凑布局:横向舞台,固定底部详情板。
|
||||
-->
|
||||
<template>
|
||||
<div class="http-compact">
|
||||
<!-- 顶部标题与场景选择 -->
|
||||
<div class="top-bar">
|
||||
<div class="title-section">
|
||||
<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 class="http-exchange-demo">
|
||||
<div class="demo-header">
|
||||
<span class="title">HTTP 请求/响应</span>
|
||||
<span class="subtitle">浏览器与服务器的对话</span>
|
||||
</div>
|
||||
|
||||
<!-- 核心可视化舞台 -->
|
||||
<div class="stage-area">
|
||||
<!-- 客户端 -->
|
||||
<div class="actor client">
|
||||
<div class="avatar-box">
|
||||
<span class="avatar-icon">🧑💻</span>
|
||||
<span class="avatar-label">浏览器</span>
|
||||
<div class="exchange-flow">
|
||||
<div class="actor browser">
|
||||
<span class="actor-icon">🧑💻</span>
|
||||
<span class="actor-name">浏览器</span>
|
||||
</div>
|
||||
|
||||
<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 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="avatar-box">
|
||||
<span class="avatar-icon">🏢</span>
|
||||
<span class="avatar-label">服务器</span>
|
||||
</div>
|
||||
<span class="actor-icon">🖥️</span>
|
||||
<span class="actor-name">服务器</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部详情面板 (固定高度) -->
|
||||
<div class="detail-panel">
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
v-if="step > 0"
|
||||
:key="step"
|
||||
class="detail-content"
|
||||
>
|
||||
<!-- 左侧状态徽章 -->
|
||||
<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="code-preview">
|
||||
<div class="code-block">
|
||||
<div class="code-header">请求</div>
|
||||
<code>GET /search?q=hello HTTP/1.1</code>
|
||||
<code>Host: www.google.com</code>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<div class="code-header">响应</div>
|
||||
<code>HTTP/1.1 200 OK</code>
|
||||
<code>Content-Type: text/html</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧详情 -->
|
||||
<div class="detail-right">
|
||||
<div class="info-row">
|
||||
<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 class="info-box">
|
||||
<strong>核心思想:</strong>
|
||||
HTTP 是请求-响应模式:浏览器发送请求,服务器返回状态码和响应内容。
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
.http-compact {
|
||||
.http-exchange-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.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;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0 30px;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.exchange-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.actor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 60px;
|
||||
z-index: 2;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.avatar-icon { font-size: 28px; }
|
||||
.avatar-label { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
|
||||
|
||||
.channel {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
margin: 0 20px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.actor-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.channel-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
top: 50%;
|
||||
.actor-name {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.packet {
|
||||
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 {
|
||||
.messages {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
.request-box,
|
||||
.response-box {
|
||||
display: flex;
|
||||
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;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.guide-bounce { animation: bounce 1.5s infinite; }
|
||||
@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
|
||||
.method {
|
||||
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>
|
||||
|
||||
@@ -1,810 +1,172 @@
|
||||
<!--
|
||||
TcpHandshakeDemo.vue
|
||||
TCP三次握手演示 - 紧凑交互版
|
||||
|
||||
设计理念:
|
||||
1. 循循善诱:用"打电话"的生活场景类比 TCP 连接建立过程。
|
||||
2. 紧凑布局:保留核心可视化区,使用固定底部详情板代替长列表。
|
||||
-->
|
||||
<template>
|
||||
<div class="tcp-compact">
|
||||
<!-- 顶部标题与控制 -->
|
||||
<div class="top-bar">
|
||||
<div class="title-section">
|
||||
<span class="app-icon">📞</span>
|
||||
<span class="app-title">TCP 三次握手</span>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
v-if="currentStep < 3"
|
||||
class="action-btn primary"
|
||||
:disabled="currentStep >= 3"
|
||||
@click="nextStep"
|
||||
>
|
||||
{{ currentStep === 0 ? '▶ 开始拨号' : '下一步 ➔' }}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn outline"
|
||||
@click="reset"
|
||||
>
|
||||
↺ 重置
|
||||
</button>
|
||||
</div>
|
||||
<div class="tcp-handshake-demo">
|
||||
<div class="demo-header">
|
||||
<span class="title">TCP 三次握手</span>
|
||||
<span class="subtitle">建立可靠连接的过程</span>
|
||||
</div>
|
||||
|
||||
<!-- 核心可视化舞台 -->
|
||||
<div class="stage-area">
|
||||
<!-- 左侧:客户端/快递员 -->
|
||||
<div class="handshake-flow">
|
||||
<div class="actor client">
|
||||
<div class="avatar-box">
|
||||
<span class="avatar-icon">🧑💻</span>
|
||||
<span class="avatar-label">客户端 (你)</span>
|
||||
</div>
|
||||
<transition name="pop">
|
||||
<div
|
||||
v-if="currentStep >= 1"
|
||||
class="bubble client"
|
||||
>
|
||||
{{ getBubbleText(1) }}
|
||||
</div>
|
||||
</transition>
|
||||
<span class="actor-icon">🧑💻</span>
|
||||
<span class="actor-name">客户端</span>
|
||||
</div>
|
||||
|
||||
<!-- 中间:连接状态线 -->
|
||||
<div class="connection-line">
|
||||
<div class="line-bg" />
|
||||
<div
|
||||
class="signal-packet"
|
||||
:class="getSignalClass()"
|
||||
>
|
||||
<span
|
||||
v-if="currentStep > 0"
|
||||
class="packet-icon"
|
||||
>{{ getSignalIcon() }}</span>
|
||||
<div class="messages">
|
||||
<div class="message-row">
|
||||
<span class="msg-label">SYN</span>
|
||||
<span class="msg-arrow">→</span>
|
||||
<span class="msg-desc">"我能连你吗?"</span>
|
||||
</div>
|
||||
<div
|
||||
class="status-badge"
|
||||
:class="{ connected: currentStep === 3 }"
|
||||
>
|
||||
{{ currentStep === 3 ? '✅ 连接建立' : '⏳ 连接中...' }}
|
||||
<div class="message-row">
|
||||
<span class="msg-desc">"能,你也能收到我吗?"</span>
|
||||
<span class="msg-arrow">←</span>
|
||||
<span class="msg-label">SYN-ACK</span>
|
||||
</div>
|
||||
<div class="message-row">
|
||||
<span class="msg-label">ACK</span>
|
||||
<span class="msg-arrow">→</span>
|
||||
<span class="msg-desc">"能,开始吧!"</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:服务器/收件人 -->
|
||||
<div class="actor server">
|
||||
<div class="avatar-box">
|
||||
<span class="avatar-icon">🖥️</span>
|
||||
<span class="avatar-label">服务器</span>
|
||||
</div>
|
||||
<transition name="pop">
|
||||
<div
|
||||
v-if="currentStep >= 2"
|
||||
class="bubble server"
|
||||
>
|
||||
{{ getBubbleText(2) }}
|
||||
</div>
|
||||
</transition>
|
||||
<span class="actor-icon">🖥️</span>
|
||||
<span class="actor-name">服务器</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤进度条 (可点击跳转) -->
|
||||
<div class="step-indicator">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="step-dot"
|
||||
:class="{ active: currentStep === index + 1, passed: currentStep > index + 1 }"
|
||||
:title="step.techTitle"
|
||||
@click="goToStep(index + 1)"
|
||||
>
|
||||
<span class="dot-num">{{ index + 1 }}</span>
|
||||
<span
|
||||
v-if="index < steps.length - 1"
|
||||
class="dot-line"
|
||||
/>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<span class="status-badge success">✓ 连接已建立</span>
|
||||
</div>
|
||||
|
||||
<!-- 底部详情面板 (固定高度) -->
|
||||
<div class="detail-panel">
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
v-if="currentStep > 0"
|
||||
:key="currentStep"
|
||||
class="detail-content"
|
||||
>
|
||||
<div
|
||||
class="detail-left"
|
||||
:style="{ borderColor: getCurrentStepColor() }"
|
||||
>
|
||||
<div
|
||||
class="step-badge"
|
||||
:style="{ background: getCurrentStepColor() }"
|
||||
>
|
||||
步骤 {{ currentStep }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-divider" />
|
||||
|
||||
<div class="detail-right">
|
||||
<div class="info-row">
|
||||
<span class="tag life">生活对话</span>
|
||||
<span class="text highlight">{{ steps[currentStep-1].simpleTitle }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="tag tech">技术原理</span>
|
||||
<div class="tech-content">
|
||||
<div class="tech-desc">
|
||||
{{ steps[currentStep-1].techDesc }}
|
||||
</div>
|
||||
<!-- 动态名词解码卡片 -->
|
||||
<div class="term-glossary">
|
||||
<div
|
||||
v-for="term in steps[currentStep-1].terms"
|
||||
:key="term.key"
|
||||
class="term-item"
|
||||
>
|
||||
<span class="term-key">{{ term.key }}</span>
|
||||
<span class="term-val">{{ term.val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代码实现细节 (折叠) -->
|
||||
<details
|
||||
v-if="steps[currentStep-1].codeImpl"
|
||||
class="code-details"
|
||||
>
|
||||
<summary class="code-summary">
|
||||
<span class="summary-icon">🛠️</span>
|
||||
<span class="summary-text">技术深究:底层代码如何实现?</span>
|
||||
</summary>
|
||||
<div class="code-block-wrapper">
|
||||
<div class="code-title">
|
||||
{{ steps[currentStep-1].codeImpl.title }}
|
||||
</div>
|
||||
<pre class="code-block"><code v-html="steps[currentStep-1].codeImpl.code" /></pre>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- 技术问答 (折叠) - 仅在有问答时显示 -->
|
||||
<details
|
||||
v-if="steps[currentStep-1].qa"
|
||||
class="code-details qa-details"
|
||||
>
|
||||
<summary class="code-summary qa-summary">
|
||||
<span class="summary-icon">🎓</span>
|
||||
<span class="summary-text">{{ steps[currentStep-1].qa.title }}</span>
|
||||
</summary>
|
||||
<div class="code-block-wrapper qa-content">
|
||||
<div
|
||||
v-for="(item, idx) in steps[currentStep-1].qa.content"
|
||||
:key="idx"
|
||||
class="qa-item"
|
||||
>
|
||||
<div class="qa-q">
|
||||
Q: {{ item.q }}
|
||||
</div>
|
||||
<div
|
||||
class="qa-a"
|
||||
v-html="item.a"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下一步按钮 -->
|
||||
<button
|
||||
v-if="currentStep < 3"
|
||||
class="next-btn"
|
||||
@click="nextStep"
|
||||
>
|
||||
下一步 ➔
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="detail-placeholder"
|
||||
>
|
||||
<span class="guide-bounce">📞</span>
|
||||
<span>点击"开始拨号"或步骤圆点,开始拨打电话</span>
|
||||
</div>
|
||||
</transition>
|
||||
<div class="info-box">
|
||||
<strong>核心思想:</strong>
|
||||
三次握手确保双方都能收发数据,就像打电话时互相确认"能听到吗"。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const currentStep = ref(0)
|
||||
|
||||
const steps = [
|
||||
{
|
||||
simpleTitle: '喂,在家吗?我是快递员!',
|
||||
techTitle: 'SYN',
|
||||
techDesc: '客户端发送 SYN 包 (Seq=x),请求建立连接。',
|
||||
color: '#3b82f6',
|
||||
terms: [
|
||||
{ key: 'SYN', val: '是单词 Synchronize (同步) 的缩写。💡 为什么叫"同步"?因为建立连接的第一步,就是双方要"对表",把暗号(序号)对齐,确保后续对话在同一个频道上。' },
|
||||
{ key: 'Seq=x', val: '意思是:"我的数据计数器,从 x 开始"。💡 为什么要特意告诉对方?这就像是两个人约定"暗号"。我告诉你:"我的暗号是从 x 开始算的"。以后我每发给你一个字,暗号就加 1。如果你不知道我的起始暗号是 x,以后收到 x+100 你就不知道它是第几个字,也没法判断中间有没有丢字。' }
|
||||
],
|
||||
codeImpl: {
|
||||
title: '💻 真实 TCP 报文构建 (伪代码)',
|
||||
code: `// 1. 设置标志位:只置 SYN
|
||||
<span class="kw">tcph->syn</span> = 1;
|
||||
<span class="kw">tcph->ack</span> = 0;
|
||||
|
||||
// 2. 生成随机序号 (Seq)
|
||||
// 操作系统内核会维护一个计数器
|
||||
// 这里的 htonl 是为了处理网络字节序
|
||||
<span class="kw">tcph->seq</span> = htonl(<span class="var">random_x</span>);
|
||||
|
||||
// 3. 发送数据包
|
||||
send_packet(client_socket, tcph);`
|
||||
}
|
||||
},
|
||||
{
|
||||
simpleTitle: '在的!我听到了,请说!',
|
||||
techTitle: 'SYN-ACK',
|
||||
techDesc: '服务器回复 SYN-ACK 包 (Seq=y, Ack=x+1),确认并请求连接。',
|
||||
color: '#10b981',
|
||||
terms: [
|
||||
{ key: 'ACK', val: '是单词 Acknowledgment (确认) 的缩写。💡 就像快递签收单,表示"我收到你的请求了"。' },
|
||||
{ key: 'Ack=x+1', val: '确认号。💡 为什么要 +1?这是一种期待机制。意思是:"x 号那一页我已经收好了,请你下次从 x+1 页开始讲"。' },
|
||||
{ key: 'Seq=y', val: '服务器也生成自己的随机序号 y,方便客户端确认服务器发来的话。' }
|
||||
],
|
||||
codeImpl: {
|
||||
title: '💻 服务器内核响应逻辑 (伪代码)',
|
||||
code: `// 1. 检查收到的是否是 SYN
|
||||
if (<span class="kw">recv_tcph->syn</span> == 1) {
|
||||
// 2. 准备回复包
|
||||
<span class="kw">reply_tcph->syn</span> = 1; // 同步
|
||||
<span class="kw">reply_tcph->ack</span> = 1; // 确认
|
||||
|
||||
// 3. 确认号 = 对方 Seq + 1
|
||||
// 表示期待对方下一次发在这个序号之后的数据
|
||||
<span class="kw">reply_tcph->ack_seq</span> = htonl(ntohl(<span class="var">recv_tcph->seq</span>) + 1);
|
||||
|
||||
// 4. 生成服务器自己的序号
|
||||
<span class="kw">reply_tcph->seq</span> = htonl(<span class="var">random_y</span>);
|
||||
|
||||
send_packet(server_socket, reply_tcph);
|
||||
}`
|
||||
}
|
||||
},
|
||||
{
|
||||
simpleTitle: '好的,那我开始说了!',
|
||||
techTitle: 'ACK',
|
||||
techDesc: '客户端发送 ACK 包 (Ack=y+1),连接建立成功,可以传输数据。',
|
||||
color: '#8b5cf6',
|
||||
terms: [
|
||||
{ key: 'Ack=y+1', val: '客户端确认收到。意思是:"服务器你的 y 号信我也收到了,我们正式开始聊天吧!"' },
|
||||
{ key: '连接建立', val: '双方都确认了对方"能听能说",通道正式打通。' }
|
||||
],
|
||||
codeImpl: {
|
||||
title: '💻 客户端最终确认 (伪代码)',
|
||||
code: `// 1. 检查收到的包
|
||||
if (<span class="kw">recv_tcph->syn</span> == 1 && <span class="kw">recv_tcph->ack</span> == 1) {
|
||||
// 2. 准备 ACK 包
|
||||
<span class="kw">ack_tcph->syn</span> = 0; // 第三次握手不需要 SYN 了
|
||||
<span class="kw">ack_tcph->ack</span> = 1;
|
||||
|
||||
// 3. 确认号 = 服务器 Seq + 1
|
||||
<span class="kw">ack_tcph->ack_seq</span> = htonl(ntohl(<span class="var">recv_tcph->seq</span>) + 1);
|
||||
|
||||
// 4. 序号 = 自己的 Seq + 1
|
||||
<span class="kw">ack_tcph->seq</span> = htonl(<span class="var">my_seq</span> + 1);
|
||||
|
||||
// 5. 连接状态变为 ESTABLISHED
|
||||
<span class="hl">socket->state = TCP_ESTABLISHED;</span>
|
||||
send_packet(client_socket, ack_tcph);
|
||||
}`
|
||||
},
|
||||
qa: {
|
||||
title: '🤔 为什么必须是三次?(核心逻辑)',
|
||||
content: [
|
||||
{
|
||||
q: '为什么一定要三次?(双工确认原理)',
|
||||
a: `这其实是在验证<strong>双方的"听说能力"</strong>是否正常。TCP 是全双工的(双方都能同时发和收),所以必须双方都确认对方能发能收:<br>
|
||||
1️⃣ <strong>第一次 (Client -> Server)</strong>:Server 收到,证明 <strong>Client 能发</strong>,<strong>Server 能收</strong>。<br>
|
||||
2️⃣ <strong>第二次 (Server -> Client)</strong>:Client 收到,证明 <strong>Server 能发</strong>,<strong>Client 能收</strong>。同时 Client 知道 Server 收到了自己的第一次请求。<br>
|
||||
3️⃣ <strong>第三次 (Client -> Server)</strong>:Server 收到,证明 <strong>Client 能收</strong>(因为 Client 回复了 Server 的消息)。<br>
|
||||
<br>
|
||||
<strong>结论:</strong> 只有经过这三次,双方都明确知道"自己"和"对方"的发送、接收功能全是好的。少一次都不行(Server 不知道 Client 能不能收),多一次没必要。`
|
||||
},
|
||||
{
|
||||
q: '为什么这就算"连上"了?',
|
||||
a: `所谓的"连接建立",在计算机里并不是真的拉了一根线。它的本质是:<strong>双方内存里都保存好了对方的"状态信息"</strong>。<br>
|
||||
通过这三次握手,双方主要完成了两件事:<br>
|
||||
1. <strong>确认通道畅通</strong>:就是上面说的双工能力确认。<br>
|
||||
2. <strong>同步初始序号 (ISN)</strong>:双方交换了 Seq (x 和 y)。<br>
|
||||
<br>
|
||||
只要双方都记住了对方的 Seq,并且确认了对方在线,操作系统就会把 Socket 状态标记为 <code style="color:#10b981">ESTABLISHED</code> (已建立),这就叫"连上了"。`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep.value < 3) currentStep.value++
|
||||
}
|
||||
|
||||
const goToStep = (step) => {
|
||||
currentStep.value = step
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
currentStep.value = 0
|
||||
}
|
||||
|
||||
const getBubbleText = (stepIndex) => {
|
||||
// stepIndex 1: Client speaks (Step 1)
|
||||
// stepIndex 2: Server speaks (Step 2)
|
||||
if (stepIndex === 1) return steps[0].simpleTitle
|
||||
if (stepIndex === 2) return steps[1].simpleTitle
|
||||
return ''
|
||||
}
|
||||
|
||||
const getSignalClass = () => {
|
||||
if (currentStep.value === 1) return 'sending' // Left to Right
|
||||
if (currentStep.value === 2) return 'receiving' // Right to Left
|
||||
if (currentStep.value === 3) return 'sending-final' // Left to Right
|
||||
return ''
|
||||
}
|
||||
|
||||
const getSignalIcon = () => {
|
||||
return '🔔'
|
||||
}
|
||||
|
||||
const getCurrentStepColor = () => {
|
||||
return steps[currentStep.value - 1]?.color || '#ccc'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tcp-compact {
|
||||
.tcp-handshake-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.action-btn.outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* 舞台区 */
|
||||
.stage-area {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 140px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0 30px;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.handshake-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.actor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 120px;
|
||||
position: relative;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.avatar-box {
|
||||
.actor-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.actor-name {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.avatar-icon { font-size: 32px; }
|
||||
.avatar-label { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
|
||||
|
||||
/* 气泡 */
|
||||
.bubble {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
background: white;
|
||||
padding: 6px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: #333;
|
||||
}
|
||||
.bubble.client { left: 50%; transform: translateX(-50%); }
|
||||
.bubble.server { left: 50%; transform: translateX(-50%); }
|
||||
|
||||
.pop-enter-active, .pop-leave-active { transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
|
||||
.pop-enter-from, .pop-leave-to { opacity: 0; transform: translateX(-50%) scale(0.8); }
|
||||
|
||||
/* 连接线 */
|
||||
.connection-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
margin: 0 20px;
|
||||
position: relative;
|
||||
.message-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.msg-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.msg-arrow {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.msg-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
text-align: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.status-badge.connected { color: var(--vp-c-brand); font-weight: bold; }
|
||||
|
||||
.signal-packet {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
top: -11px;
|
||||
}
|
||||
|
||||
.signal-packet.sending {
|
||||
animation: moveRight 1.5s forwards;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.signal-packet.receiving {
|
||||
animation: moveLeft 1.5s forwards;
|
||||
opacity: 1;
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.signal-packet.sending-final {
|
||||
animation: moveRight 1.5s forwards;
|
||||
opacity: 1;
|
||||
background: #8b5cf6;
|
||||
}
|
||||
|
||||
@keyframes moveRight {
|
||||
0% { left: 0; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
@keyframes moveLeft {
|
||||
0% { left: 100%; }
|
||||
100% { left: 0; }
|
||||
}
|
||||
|
||||
/* 步骤指示器 */
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.step-dot.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.step-dot.passed {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.dot-line {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
/* 详情面板 */
|
||||
.detail-panel {
|
||||
min-height: 80px; /* 改为最小高度 */
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: flex-start; /* 顶部对齐 */
|
||||
/* overflow: hidden; 移除隐藏 */
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: flex-start; /* 顶部对齐 */
|
||||
}
|
||||
|
||||
.detail-left {
|
||||
padding-right: 16px;
|
||||
border-right: 2px solid transparent;
|
||||
margin-top: 4px; /* 微调对齐 */
|
||||
}
|
||||
|
||||
.step-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-divider {
|
||||
width: 1px;
|
||||
align-self: stretch; /* 拉伸高度 */
|
||||
background: var(--vp-c-divider);
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.detail-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px; /* 增加间距 */
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start; /* 顶部对齐 */
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.tag.life { background: #e6f7ff; color: #1890ff; }
|
||||
.tag.tech { background: #f6ffed; color: #52c41a; }
|
||||
|
||||
.text {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.text.highlight {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
/* 新增:术语解释样式 */
|
||||
.term-glossary {
|
||||
margin-top: 8px;
|
||||
.status-badge.success {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
background: rgba(0,0,0,0.03);
|
||||
padding: 8px;
|
||||
gap: 0.25rem;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.term-item {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.term-key {
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-brand-dark);
|
||||
margin-right: 6px;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
padding: 6px 16px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
margin-left: 12px;
|
||||
margin-top: 4px;
|
||||
.info-box strong {
|
||||
white-space: nowrap;
|
||||
align-self: flex-start; /* 按钮顶部对齐 */
|
||||
}
|
||||
|
||||
.detail-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--vp-c-text-3);
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.guide-bounce {
|
||||
animation: bounce 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-3px); }
|
||||
}
|
||||
|
||||
/* 代码实现折叠块 */
|
||||
.code-details {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.code-summary {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
user-select: none;
|
||||
background: rgba(0,0,0,0.02);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.code-summary:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.code-block-wrapper {
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
background: #282c34; /* 深色背景适合代码 */
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
.code-title {
|
||||
font-size: 11px;
|
||||
color: #61afef;
|
||||
margin-bottom: 6px;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
margin: 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 语法高亮类 (深色模式) */
|
||||
:deep(.kw) { color: #c678dd; } /* 紫色 - 关键字/字段 */
|
||||
:deep(.var) { color: #d19a66; } /* 橙色 - 变量 */
|
||||
:deep(.hl) { color: #98c379; font-weight: bold; } /* 绿色 - 高亮行 */
|
||||
|
||||
/* 问答折叠块 */
|
||||
.qa-details {
|
||||
background: rgba(255, 165, 0, 0.05); /* 淡淡的橙色背景 */
|
||||
border-color: rgba(255, 165, 0, 0.2);
|
||||
}
|
||||
|
||||
.qa-summary {
|
||||
color: #d46b08;
|
||||
}
|
||||
|
||||
.qa-summary:hover {
|
||||
color: #ff7a45;
|
||||
background: rgba(255, 165, 0, 0.1);
|
||||
}
|
||||
|
||||
.qa-content {
|
||||
background: var(--vp-c-bg); /* 恢复浅色/深色背景 */
|
||||
flex-shrink: 0;
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.qa-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.qa-item:last-child { margin-bottom: 0; }
|
||||
|
||||
.qa-q {
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-brand-dark);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.qa-a {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,654 +1,57 @@
|
||||
<!--
|
||||
UrlParserDemo.vue
|
||||
URL解析演示 - 网购订单隐喻版
|
||||
|
||||
设计理念:
|
||||
1. 隐喻对齐:严格对应 url-to-browser.md 中的"网购订单"比喻。
|
||||
2. 视觉映射:将枯燥的 URL 字符串映射为一张清晰的"购物清单"。
|
||||
3. 实时交互:输入即解析,所见即所得。
|
||||
-->
|
||||
<template>
|
||||
<div class="url-parser-order">
|
||||
<!-- 顶部:输入区 -->
|
||||
<div class="input-section">
|
||||
<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 class="url-parser-demo">
|
||||
<div class="url-line">
|
||||
<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>
|
||||
|
||||
<!-- 核心区域:左右对照布局 -->
|
||||
<template v-if="parsed.protocol">
|
||||
<div class="core-stage">
|
||||
<!-- 左侧:解析结果 (技术视角) -->
|
||||
<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 class="labels">
|
||||
<span class="label protocol">协议</span>
|
||||
<span class="label host">域名</span>
|
||||
<span class="label path">路径</span>
|
||||
<span class="label query">参数</span>
|
||||
</div>
|
||||
</div>
|
||||
</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 默认是 80,HTTPS 默认是 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>
|
||||
.url-parser-order {
|
||||
.url-parser-demo {
|
||||
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;
|
||||
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);
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
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 {
|
||||
padding: 0.5rem 0.6rem;
|
||||
margin: 0.5rem 0;
|
||||
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;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 中间:箭头 */
|
||||
.transform-arrow {
|
||||
.part {
|
||||
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;
|
||||
flex-direction: column;
|
||||
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;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.order-ticket {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
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 {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.05rem 0.25rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
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>
|
||||
.label.protocol { background: #fee2e2; color: #dc2626; }
|
||||
.label.host { background: #dbeafe; color: #2563eb; }
|
||||
.label.path { background: #d1fae5; color: #059669; }
|
||||
.label.query { background: #ede9fe; color: #7c3aed; }
|
||||
</style>
|
||||
|
||||
@@ -172,7 +172,7 @@ const url = ref('')
|
||||
const isActive = ref(false)
|
||||
const currentStep = ref(0)
|
||||
|
||||
const quickUrls = ['baidu.com', 'bilibili.com', 'github.com']
|
||||
const quickUrls = ['baidu.com', 'google.com', 'github.com']
|
||||
|
||||
const steps = [
|
||||
{
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
@mouseenter="hoveredPart = 'card'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<div class="card">
|
||||
<div class="player">
|
||||
</div>
|
||||
<div
|
||||
class="line indent-3"
|
||||
@@ -83,7 +83,7 @@
|
||||
@mouseenter="hoveredPart = 'img'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<img class="icon" src="castle.png" />
|
||||
<img class="cover" src="cat.jpg" />
|
||||
</div>
|
||||
<div
|
||||
class="line indent-3"
|
||||
@@ -94,7 +94,7 @@
|
||||
@mouseenter="hoveredPart = 'title'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<h2 class="title">乐高城堡</h2>
|
||||
<h2 class="title">搞笑猫咪合集</h2>
|
||||
</div>
|
||||
<div
|
||||
class="line indent-3"
|
||||
@@ -105,7 +105,7 @@
|
||||
@mouseenter="hoveredPart = 'btn'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<button class="btn">购买</button>
|
||||
<button class="btn">▶️ 播放</button>
|
||||
</div>
|
||||
<div
|
||||
class="line indent-2"
|
||||
@@ -154,7 +154,7 @@
|
||||
@mouseenter="hoveredPart = 'card'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
.card { display: flex; padding: 10px; }
|
||||
.player { margin: auto; padding: 20px; }
|
||||
</div>
|
||||
<div
|
||||
class="line"
|
||||
@@ -165,7 +165,7 @@
|
||||
@mouseenter="hoveredPart = 'img'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
.icon { width: 50px; height: 50px; }
|
||||
.cover { width: 100%; height: 200px; }
|
||||
</div>
|
||||
<!-- Style properties -->
|
||||
<div
|
||||
@@ -177,7 +177,7 @@
|
||||
@mouseenter="hoveredPart = 'title'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
.title { color: red; }
|
||||
.title { color: #fb7299; /* B站主题色 */ }
|
||||
</div>
|
||||
<div
|
||||
class="line"
|
||||
@@ -188,7 +188,7 @@
|
||||
@mouseenter="hoveredPart = 'btn'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
.btn { background: blue; }
|
||||
.btn { background: #00aeec; color: white; }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,7 +233,7 @@
|
||||
@mouseenter.stop="hoveredPart = 'card'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<span class="block-label">div.card</span>
|
||||
<span class="block-label">div.player</span>
|
||||
|
||||
<!-- Image -->
|
||||
<div
|
||||
@@ -245,11 +245,11 @@
|
||||
@mouseenter.stop="hoveredPart = 'img'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<span class="block-label">img.icon</span>
|
||||
<span class="block-label">img.cover</span>
|
||||
<span
|
||||
v-if="currentStep >= 3"
|
||||
class="content-img"
|
||||
>🏰</span>
|
||||
>🐈</span>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
@@ -267,7 +267,7 @@
|
||||
<span
|
||||
v-if="currentStep >= 3"
|
||||
class="content"
|
||||
>乐高城堡</span>
|
||||
>搞笑猫咪合集</span>
|
||||
</div>
|
||||
|
||||
<!-- Button -->
|
||||
@@ -285,7 +285,7 @@
|
||||
<span
|
||||
v-if="currentStep >= 3"
|
||||
class="content-btn"
|
||||
>购买</span>
|
||||
>▶️ 播放</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,26 +332,26 @@ import { ref } from 'vue'
|
||||
const steps = [
|
||||
{
|
||||
label: 'DOM (搭骨架)',
|
||||
title: '1. 搭建骨架 (DOM)',
|
||||
desc: '浏览器工头 (Parser) 解析 HTML 代码,构建出完整的文档树结构。注意:即使代码中省略了 html/body,浏览器也会自动补全。',
|
||||
title: '1. 搭建骨架 (DOM 解析)',
|
||||
desc: '浏览器工厂看懂了 HTML 代码,搭建好了页面的“骨架”(比如哪里是包裹盒 div,哪里是按钮 button)。',
|
||||
resultTitle: 'DOM 树结构'
|
||||
},
|
||||
{
|
||||
label: 'Style (上色)',
|
||||
title: '2. 计算样式 (Recalculate Style)',
|
||||
desc: '装修工 (CSS Parser) 匹配 CSS 规则。比如发现 .title 需要红色,.btn 需要蓝色背景。此时只关心"长什么样",不关心"在哪"。',
|
||||
resultTitle: '附带样式的节点'
|
||||
label: 'Style (看图纸)',
|
||||
title: '2. 匹配样式 (CSS 解析)',
|
||||
desc: '仔细看了眼配色的说明书。比如发现 .title 字体要是粉色的,.btn 背景要是蓝色的(此时只在脑子里确立样式,但不计算尺寸)。',
|
||||
resultTitle: '获取了各种配置规则'
|
||||
},
|
||||
{
|
||||
label: 'Layout (排版)',
|
||||
title: '3. 布局排版 (Layout/Reflow)',
|
||||
desc: '测量员 (Layout) 根据 display:flex 和 padding 等属性,计算每个盒子的精确位置和大小。图片在左,文字在右。',
|
||||
resultTitle: '几何布局'
|
||||
label: 'Layout (定尺寸)',
|
||||
title: '3. 排版规划 (Layout)',
|
||||
desc: '拿尺子量每个骨架的大小。考虑到用户的屏幕尺寸,精确计算出猫咪的图片要多高、播放按钮要挤到哪个坐标上。',
|
||||
resultTitle: '排版布局盒子'
|
||||
},
|
||||
{
|
||||
label: 'Paint (绘制)',
|
||||
title: '4. 像素绘制 (Paint)',
|
||||
desc: '画家 (Paint) 按照计算好的位置和样式,真正把像素点画在屏幕上。最终你看到了一个完整的商品卡片。',
|
||||
title: '4. 像素上色 (Paint)',
|
||||
desc: '根据前面的几何位置和颜色计划,正式拿起画笔,将一个个像素填到你的屏幕上,一个可以看视频的播放器就诞生了。',
|
||||
resultTitle: '最终画面'
|
||||
}
|
||||
]
|
||||
@@ -576,34 +576,39 @@ const hoveredPart = ref(null)
|
||||
|
||||
/* Step 2: Style */
|
||||
.block-box.title.styled {
|
||||
color: red; /* Text color applied but not painted yet */
|
||||
border: 1px solid red; /* Visual cue for style applied */
|
||||
background: #fee2e2;
|
||||
color: #fb7299;
|
||||
border: 1px solid #fb7299;
|
||||
background: #fdf2f8;
|
||||
}
|
||||
|
||||
.block-box.btn.styled {
|
||||
background: blue;
|
||||
background: #00aeec;
|
||||
color: white;
|
||||
border: 1px solid blue;
|
||||
border: 1px solid #00aeec;
|
||||
}
|
||||
|
||||
/* Step 3: Layout */
|
||||
.block-box.card.layout {
|
||||
display: flex;
|
||||
flex-direction: row; /* Horizontal layout */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
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 {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background: #eee;
|
||||
border: none;
|
||||
font-size: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.block-box.title.layout {
|
||||
@@ -614,9 +619,11 @@ const hoveredPart = ref(null)
|
||||
}
|
||||
|
||||
.block-box.btn.layout {
|
||||
margin-left: auto; /* Push to right */
|
||||
padding: 5px 15px;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Content visibility for Paint step */
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<strong>为什么需要 DNS?(查导航)</strong>
|
||||
</p>
|
||||
<p class="why-desc-zh">
|
||||
你知道店铺名字叫 "Shop.com",但快递员需要知道具体的经纬度坐标 (IP 地址)
|
||||
你知道店铺名字叫 "bilibili.com",但快递员需要知道具体的经纬度坐标 (IP 地址)
|
||||
才能送达。
|
||||
<br>
|
||||
DNS 就像是<strong>地图导航</strong>,输入店名,它告诉你具体的坐标。
|
||||
DNS 就像是<strong>地图导航</strong>,输入店名,它通过“114查号台”帮你找到坐标。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="input-area">
|
||||
<span class="label">店铺名称 (域名)</span>
|
||||
<div class="fake-input">
|
||||
shop.com
|
||||
bilibili.com
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
🧭
|
||||
</div>
|
||||
<div class="title">
|
||||
DNS (地图导航)
|
||||
DNS (查号台)
|
||||
</div>
|
||||
<div class="desc">
|
||||
正在查找 shop.com 的位置...
|
||||
正在查询 bilibili.com 的 IP...
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow-down">
|
||||
@@ -41,9 +41,9 @@
|
||||
</div>
|
||||
|
||||
<div class="output-area">
|
||||
<span class="label">GPS 坐标 (IP 地址)</span>
|
||||
<span class="label">精准坐标 (IP 地址)</span>
|
||||
<div class="fake-output">
|
||||
93.184.216.34
|
||||
110.43.12.55
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -169,20 +169,20 @@ const props = defineProps({
|
||||
|
||||
const t = {
|
||||
send: '提交订单 (发送请求)',
|
||||
noRequests: '购物车是空的 (无请求)',
|
||||
placeholder: '点击 "提交订单" 向店员购买玩具',
|
||||
general: '订单详情 (General)',
|
||||
requestUrl: '商品地址 (URL)',
|
||||
noRequests: '还没发请求 (网络空闲)',
|
||||
placeholder: '点击 "提交订单" 向服务器索要页面',
|
||||
general: '请求概要 (General)',
|
||||
requestUrl: '目标地址 (URL)',
|
||||
requestMethod: '操作类型 (Method)',
|
||||
statusCode: '店员回复 (Status)',
|
||||
responseHeaders: '包裹标签 (Headers)',
|
||||
statusCode: '服务器回复状态 (Status)',
|
||||
responseHeaders: '包裹标签 / 补充说明 (Headers)',
|
||||
tabs: {
|
||||
headers: '订单信息',
|
||||
response: '包裹内容',
|
||||
preview: '玩具预览'
|
||||
headers: '头部信息(Headers)',
|
||||
response: '代码内容(Response)',
|
||||
preview: '大致预览(Preview)'
|
||||
},
|
||||
cols: {
|
||||
name: '商品',
|
||||
name: '请求体',
|
||||
status: '状态',
|
||||
type: '类型',
|
||||
time: '耗时'
|
||||
@@ -190,7 +190,7 @@ const t = {
|
||||
}
|
||||
|
||||
const method = ref('GET')
|
||||
const path = ref('/toys/lego-castle')
|
||||
const path = ref('/video/BV1xx411c7mD')
|
||||
const loading = ref(false)
|
||||
const requestSent = ref(false)
|
||||
const activeTab = ref('headers')
|
||||
@@ -210,20 +210,33 @@ const sendRequest = async () => {
|
||||
loading.value = false
|
||||
|
||||
if (method.value === 'GET') {
|
||||
responseStatus.value = '200 OK (有货)'
|
||||
responseStatus.value = '200 OK (交易成功)'
|
||||
responseHeaders.value = {
|
||||
'Content-Type': 'application/json (积木)',
|
||||
Date: new Date().toLocaleString(),
|
||||
Store: '乐高官方店'
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Server': 'BWS/1.1 (Bilibili Web Server)',
|
||||
'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 {
|
||||
responseStatus.value = '201 Created (下单成功)'
|
||||
responseStatus.value = '201 Created (操作成功)'
|
||||
responseHeaders.value = {
|
||||
'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
|
||||
const t = {
|
||||
statusLabel: '通话状态',
|
||||
connect: '拨打电话',
|
||||
reset: '挂断重拨',
|
||||
client: '我 (顾客)',
|
||||
server: '玩具店',
|
||||
statusLabel: '连接状态',
|
||||
connect: '建立连接',
|
||||
reset: '断开重连',
|
||||
client: '我 (浏览器)',
|
||||
server: '对面 (B站服务器)',
|
||||
status: {
|
||||
closed: '未通话',
|
||||
handshaking: '正在拨号...',
|
||||
established: '通话中 (连接已建立)'
|
||||
closed: '未连接',
|
||||
handshaking: '正在打招呼确认通道...',
|
||||
established: 'TCP 通道已建立 (ESTABLISHED)'
|
||||
},
|
||||
steps: {
|
||||
0: '点击 "拨打电话" 开始确认店铺是否营业。',
|
||||
1: '步骤 1: 我问 "喂?有人在吗?" (SYN)',
|
||||
2: '步骤 2: 店员答 "在的!请问有什么事?" (SYN-ACK)',
|
||||
3: '步骤 3: 我说 "太好了,我想买东西!" (ACK)'
|
||||
0: '点击 "建立连接" 开始三次握手(电话试音)。',
|
||||
1: '第一次握手: "喂,服务器老哥在吗?我能发信息,你能收到吗?" (SYN)',
|
||||
2: '第二次握手: "喂喂在的!我收到了!那你现在能听到我说话吗?" (SYN-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 icons = {
|
||||
protocol: '🚛',
|
||||
host: '🏢',
|
||||
port: '🚪',
|
||||
pathname: '🧸',
|
||||
pathname: '📺',
|
||||
search: '📝',
|
||||
hash: '📍'
|
||||
}
|
||||
@@ -125,18 +125,18 @@ const labels = {
|
||||
protocol: '交通方式 (Protocol)',
|
||||
host: '店铺地址 (Host)',
|
||||
port: '大门号 (Port)',
|
||||
pathname: '商品位置 (Path)',
|
||||
search: '备注要求 (Query)',
|
||||
hash: '快速定位 (Hash)'
|
||||
pathname: '具体货架 (Path)',
|
||||
search: '特殊要求 (Search/Query)',
|
||||
hash: '直接跳转 (Hash)'
|
||||
}
|
||||
|
||||
const descriptions = {
|
||||
protocol: '怎么去?(例如 https = 坐装甲车去,很安全)',
|
||||
host: '去哪家店?(域名,例如 shop.com)',
|
||||
port: '从哪个门进?(默认 443 号门)',
|
||||
pathname: '商品在哪个货架?(路径)',
|
||||
search: '给店员的备注 (例如 ?color=red)',
|
||||
hash: '直接翻到说明书第几页 (锚点)'
|
||||
protocol: '怎么去?(https = 坐押运车去,比 http 安全)',
|
||||
host: '去哪家店?(域名:例如 www.bilibili.com)',
|
||||
port: '走哪个门?(默认隐藏了 443 端口号)',
|
||||
pathname: '拿什么货?(去 /video 区拿编号为 BV... 的视频)',
|
||||
search: '给店员的备注说明 (例如 ?t=60 表示要求从 60 秒开始看)',
|
||||
hash: '拿到货后自己做的事 (例如滚动到评论区 #comments)'
|
||||
}
|
||||
|
||||
const parsedUrl = computed(() => {
|
||||
|
||||
@@ -44,6 +44,8 @@ import ApiPlayground from './components/appendix/api-intro/ApiPlayground.vue'
|
||||
import RealWorldApiDemo from './components/appendix/api-intro/RealWorldApiDemo.vue'
|
||||
import FunctionApiDemo from './components/appendix/api-intro/FunctionApiDemo.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 StatusCodeCategories from './components/appendix/api-intro/StatusCodeCategories.vue'
|
||||
|
||||
@@ -110,6 +112,7 @@ import NetworkTroubleshooting from './components/appendix/web-basics/NetworkTrou
|
||||
// Computer Fundamentals Components
|
||||
import TransistorDemo from './components/appendix/computer-fundamentals/TransistorDemo.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 FullAdderDemo from './components/appendix/computer-fundamentals/FullAdderDemo.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 CpuArchitectureDemo from './components/appendix/computer-fundamentals/CpuArchitectureDemo.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 ProcessDemo from './components/appendix/computer-fundamentals/ProcessDemo.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'
|
||||
|
||||
// Computer Fundamentals Additional Components
|
||||
import OSSystemOverviewDemo from './components/appendix/computer-fundamentals/OSSystemOverviewDemo.vue'
|
||||
import ProcessMemoryFilesystemDemo from './components/appendix/computer-fundamentals/ProcessMemoryFilesystemDemo.vue'
|
||||
import OSArchitectureDemo from './components/appendix/computer-fundamentals/OSArchitectureDemo.vue'
|
||||
import ProgramLaunchDemo from './components/appendix/computer-fundamentals/ProgramLaunchDemo.vue'
|
||||
import DataLifecycleDemo from './components/appendix/computer-fundamentals/DataLifecycleDemo.vue'
|
||||
import EncodingStorageTransmissionDemo from './components/appendix/computer-fundamentals/EncodingStorageTransmissionDemo.vue'
|
||||
import NetworkOverviewDemo from './components/appendix/computer-fundamentals/NetworkOverviewDemo.vue'
|
||||
@@ -673,6 +677,8 @@ export default {
|
||||
app.component('RealWorldApiDemo', RealWorldApiDemo)
|
||||
app.component('FunctionApiDemo', FunctionApiDemo)
|
||||
app.component('ApiTypesComparison', ApiTypesComparison)
|
||||
app.component('ApiFunctionVsHttp', ApiFunctionVsHttp)
|
||||
app.component('DocumentTypesComparison', DocumentTypesComparison)
|
||||
app.component('HttpMethodsDemo', HttpMethodsDemo)
|
||||
app.component('StatusCodeCategories', StatusCodeCategories)
|
||||
|
||||
@@ -742,6 +748,7 @@ export default {
|
||||
// Computer Fundamentals Components Registration
|
||||
app.component('TransistorDemo', TransistorDemo)
|
||||
app.component('LogicGateDemo', LogicGateDemo)
|
||||
app.component('BinaryAdditionRulesDemo', BinaryAdditionRulesDemo)
|
||||
app.component('HalfAdderDemo', HalfAdderDemo)
|
||||
app.component('FullAdderDemo', FullAdderDemo)
|
||||
app.component('AdderDemo', AdderDemo)
|
||||
@@ -750,6 +757,7 @@ export default {
|
||||
app.component('FunctionalUnitDemo', FunctionalUnitDemo)
|
||||
app.component('CpuArchitectureDemo', CpuArchitectureDemo)
|
||||
app.component('RegisterDemo', RegisterDemo)
|
||||
app.component('FlipFlopDemo', FlipFlopDemo)
|
||||
// app.component('EvolutionFlowDemo', EvolutionFlowDemo)
|
||||
app.component('ProcessDemo', ProcessDemo)
|
||||
app.component('MemoryDemo', MemoryDemo)
|
||||
@@ -767,8 +775,8 @@ export default {
|
||||
app.component('CFTcpUdpComparison', CFTcpUdpComparison)
|
||||
|
||||
// Computer Fundamentals Additional Components Registration
|
||||
app.component('OSSystemOverviewDemo', OSSystemOverviewDemo)
|
||||
app.component('ProcessMemoryFilesystemDemo', ProcessMemoryFilesystemDemo)
|
||||
app.component('OSArchitectureDemo', OSArchitectureDemo)
|
||||
app.component('ProgramLaunchDemo', ProgramLaunchDemo)
|
||||
app.component('DataLifecycleDemo', DataLifecycleDemo)
|
||||
app.component(
|
||||
'EncodingStorageTransmissionDemo',
|
||||
|
||||
@@ -1,84 +1,224 @@
|
||||
# 计算机网络:从输入网址到返回结果的过程
|
||||
|
||||
::: 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://`,就相当于坐普通的“大巴”(明文传输),路上可能会被人偷看行李。
|
||||
- **店铺地址 (Host)**:例如 `www.google.com`,也就是你要去哪家店(域名)。
|
||||
- **商品位置 (Path)**:例如 `/search`,意思是进了商店之后,你要去哪个货架找什么东西(即请求的具体资源路径)。
|
||||
**这一步要实现**:把域名转换成 IP 地址,让浏览器知道服务器的精确位置。
|
||||
|
||||
浏览器第一步要做的,就是把这段“人类语言”拆解开,看看你到底想要什么。
|
||||
**目的**:网络世界的底层路由器(负责指路的交警)根本不懂英文,它们**只认数字**,也就是所谓的 **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 的下一个包"
|
||||
|
||||
**这一步确认了什么?** 服务器收到了浏览器的确认 → 服务器的**接收通道**也正常。
|
||||
|
||||
---
|
||||
|
||||
## 总结:从微观到宏观
|
||||
### 为什么必须是三次?两次行不行?
|
||||
|
||||
如果我们把目光再拉远一点,整个网络通讯的本质,就是在做**接力跑和翻译**:
|
||||
**假设只有两次握手:**
|
||||
|
||||
- 我们上面看到的这五步,大多是发生在你眼前的**应用程序**层面的事情。
|
||||
- 在肉眼看不见的底层,刚才那个充满代码的 HTML 包裹,会被切分成无数块极小的碎片(数据包)。这些碎片顺着你家墙上的网线、海底的万兆光缆,像接力棒一样在各种路由器之间传递。
|
||||
- 最终,这一切碎片完好无损地抵达,并在哪怕是几十个毫秒的时间里,化成你屏幕上的绚丽像素。
|
||||
1. 浏览器:"喂,能听到吗?"
|
||||
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 🎯 核心问题
|
||||
**有了完美的 CPU 和无限的内存,电脑就能直接用了吗?**
|
||||
在上一章,我们见证了晶体管如何组合成强大的 CPU。但其实,如果直接使用这些冷冰冰的硬件,哪怕只是想在屏幕上打出一个字母,你都需要手写几百行晦涩的机器指令。
|
||||
在上一章,我们见证了晶体管如何组合成强大的 CPU。但即使你拥有最顶级的硬件,如果直接让它们工作,连在屏幕上显示一个字母都需要写几百行晦涩的机器指令。不仅麻烦,还极其危险——稍有差池,你的代码就可能把别人的数据覆盖掉。
|
||||
|
||||
为了不让大家在每次用电脑时都被逼疯,前辈们创造了一个夹在“硬件”和“你”之间的超级管家——**操作系统(Operating System, 简称 OS)**。本章我们不谈深奥的理论,只聊聊这个大管家是怎么通过三大“障眼法”,把复杂的硬件调教得服服帖帖的。
|
||||
为了解决这些噩梦,**操作系统(Operating System, 简称 OS)**诞生了。它是挡在你和冰冷硬件之间的一层最伟大的"软件"。本章我们将抛开深奥的代码,用通俗的比喻,看看这个"超级管家"是如何把杂乱无章的硬件调教得服服帖帖的。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 承上启下:如果没有操作系统会怎样?
|
||||
## 0. 全景图:没有操作系统会怎样?
|
||||
|
||||
上一章我们提到,CPU 是一个不知疲倦的无情计算机器,通电后就会一行一行地执行指令。
|
||||
想象一下,你开了一家极具潜力的"计算工厂"(你的电脑),厂里有一个全能、不知疲倦的顶级干将(CPU),还有一片巨大的仓库(内存)和无数的集装箱(硬盘)。
|
||||
|
||||
但这带来了几个现实的灾难:
|
||||
1. **CPU 独占危机**:CPU 一次只能干一件事。如果你正在听歌,想切出去看个网页?抱歉,没有操作系统的调度,你的电脑必须停下音乐,才能去加载网页。
|
||||
2. **内存踩踏事故**:微信和游戏都在使用内存。如果没有保安管理,游戏一不小心把数据写到了微信的内存地盘,微信当场崩溃。
|
||||
3. **硬盘迷宫**:硬盘本质上只是一张密密麻麻刻满 0 和 1 的巨大光盘。要想找到你昨天存的照片,你必须准确记住它存放在第 12345 圈磁道的第 678 个扇区。
|
||||
如果你**不雇佣**一个厂长(操作系统)来管理:
|
||||
1. **CPU 独占危机**:CPU 一次只能干一件事。如果有人在用它听歌,其他任何人想看网页?抱歉,大家必须排队等听歌的人主动把 CPU 让出来。
|
||||
2. **内存踩踏事故**:微信和游戏都在使用仓库(内存)。如果没有保安规划区域,游戏一不小心把装备数据放到了微信的盒子里,微信直接当场崩溃。
|
||||
3. **硬盘迷宫**:硬盘硬件只是一张张刻满 0 和 1 的巨大光盘。要想找到昨天存的照片,你必须准确记住它存放在"第 1 盘面、第 56 磁道、第 8 扇区",没人能记住这种反人类的坐标。
|
||||
|
||||
为了解决这些噩梦,操作系统诞生了。它对外提供了一套优雅的“幻觉”,这就是它的三大核心魔法:**进程(管理 CPU)**、**虚拟内存(管理内存)** 和 **文件系统(管理硬盘)**。
|
||||
<OSArchitectureDemo />
|
||||
|
||||
为了解决上述的三大噩梦,操作系统祭出了它的三板斧:**进程管理**、**内存管理**和**文件系统**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 进程管理:制造“同时运行”的幻觉
|
||||
## 1. 进程管理:CPU 的时分复用
|
||||
|
||||
你平时用电脑,常常是一边挂着微信,一边听着音乐,还能一边打字。但如果你买的电脑其实只有一个 CPU 核心,它是怎么同时做这三件事的?
|
||||
|
||||
答案是:**它并没有同时做**。是操作系统在进行疯狂的“时间管理”。
|
||||
答案是:**它并没有同时做。而是操作系统在进行疯狂的"时间管理"。**
|
||||
|
||||
<ProcessDemo />
|
||||
|
||||
::: tip 💡 核心原理解析:时间片轮转(Time Slicing)
|
||||
操作系统把 CPU 的时间切成了极其微小的片段(比如 10 毫秒)。
|
||||
- 第 1-10 毫秒:让 CPU 去执行**微信**的接收消息逻辑。
|
||||
- 第 11-20 毫秒:把微信强制暂停,让 CPU 去执行**音乐**的播放逻辑。
|
||||
- 第 21-30 毫秒:把音乐暂停,让 CPU 去响应你的**键盘打字**。
|
||||
### 1.1 什么是"进程"?
|
||||
每一个正在运行的程序,就被称为一个**进程**。你可以把它理解为一个"项目组",有自己的代码(做事清单)、自己的内存数据(项目资金),排着队等待 CPU 接见。
|
||||
|
||||
因为切换的速度实在太快了(一秒钟切换成百上千次),在人类迟钝的感知中,就觉得这三个软件是“同时”在运行的。
|
||||
|
||||
在操作系统的术语里,运行中的程序就被称为**进程(Process)**。操作系统就是这群进程的冷酷无情的排班经理。
|
||||
:::
|
||||
### 1.2 时间片轮转
|
||||
为了不让某个流氓软件一直霸占 CPU,操作系统把 CPU 的时间切成极小的片段(约 10 毫秒),轮流分配给各个进程。因为切换速度太快了,你感觉是"同时运行"。
|
||||
|
||||
---
|
||||
|
||||
## 2. 内存管理:给每个程序画个“海市蜃楼”
|
||||
## 2. 内存管理:虚拟地址空间
|
||||
|
||||
解决了 CPU 轮流用的问题,接下来是存放数据的内存。如果所有的进程都挤在同一块物理内存里,很容易发生互相干扰和偷看数据的危险。
|
||||
|
||||
操作系统的第二大魔法,叫作**虚拟内存(Virtual Memory)**。
|
||||
解决了 CPU 轮流用的问题,接下来是内存空间。如果不加管理,所有软件都直接往物理内存条写数据,必然会发生**互相覆盖**的踩踏惨剧。
|
||||
|
||||
<MemoryDemo />
|
||||
|
||||
::: tip 💡 核心原理解析:内存映射
|
||||
操作系统对每一个启动的进程撒了一个弥天大谎:“嘿,你独占了整整 4GB 的纯净内存空间,随便用!”(这就是**虚拟内存**)。
|
||||
### 2.1 虚拟内存(Virtual Memory)
|
||||
操作系统对每一个进程都撒了一个大谎:"嘿,你独占了整台电脑所有的可用内存,随便用!"
|
||||
|
||||
但实际上,当进程往这个“虚拟空间”里放东西时,操作系统的底层会拿出一个**映射表(页表)**,偷偷把数据塞进**真实物理内存(Physical Memory)**中各种零碎、不连续的角落里。
|
||||
在进程眼里,自己的内存条永远是**连续**且**干净**的。它心安理得地往里面写数据。
|
||||
|
||||
**这么做有两个巨大的好处:**
|
||||
1. **绝对安全**:微信永远只能看到自己的虚拟空间,它根本不知道音乐的数据在物理内存的哪个角落,自然就不会发生“踩踏”。
|
||||
2. **碎片利用**:物理内存就算被用得像狗皮膏药一样稀碎,映射给进程的虚拟空间依然是连续且整齐的。
|
||||
:::
|
||||
### 2.2 页表映射(Page Table)
|
||||
实际上呢?操作系统偷偷把数据塞进**真实物理内存**中各种零碎的缝隙里。这么做有两个绝顶天才的好处:
|
||||
1. **绝对安全**:微信永远只能看到自己的空间,没法篡改别人的数据
|
||||
2. **碎片利用**:不管物理内存多乱,映射给进程的虚拟空间依然是整齐的
|
||||
|
||||
---
|
||||
|
||||
## 3. 文件系统:把“荒地”变成“档案馆”
|
||||
## 3. 文件系统:持久化存储的组织
|
||||
|
||||
如果你买了一块崭新的硬盘,它里面其实是一片荒芜的存储单元。如果你想存一张照片,硬盘硬件只会问你:“请告诉我你要存在第几个字节地址?”这显然反人类。
|
||||
|
||||
操作系统的第三大魔法是**文件系统(File System)**,它为你构建了我们最熟悉的:文件夹(目录)和文件的概念。
|
||||
如果你买了一块崭新的硬盘,它里面其实是一片荒芜的存储单元。如果你想存一张照片,硬盘只会问你:"请告诉我你要存在第几个字节?"
|
||||
|
||||
<FilesystemDemo />
|
||||
|
||||
::: tip 💡 核心原理解析:从地址到路径
|
||||
文件系统本质上是一个超级大型的“翻译官”加“账本”:
|
||||
1. **账本功能**:它悄悄地把硬盘切分成无数个小块(Block),然后用一个账本记录下来“哪几个小块现在是空的可以存数据,哪几个小块已经存了东西”。
|
||||
2. **翻译功能**:当你双击一层层文件夹,打开 `D盘/照片/宠物.jpg` 时,并不是硬盘真的长出了树枝一样的结构。而是文件系统在它的账本里疯狂翻阅,最终翻译出:哦,这个路径其实对应的是硬盘上的第 1056、1057 和 998 块小地方,然后把数据取出来交给你。
|
||||
:::
|
||||
### 3.1 文件系统做了什么?
|
||||
1. **切割硬盘**:把硬盘切成无数个固定大小的**块**(通常是 4KB)
|
||||
2. **建立账本**:记录哪些块是满的,哪些是空的
|
||||
3. **翻译路径**:把 `D盘/照片/宠物.jpg` 翻译成"第 3、7、11 块"
|
||||
|
||||
这就是为什么你重命名文件瞬间就能完成(只改账本上的名字),而复制文件需要好久(要真实读写硬盘数据块)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 总结:伟大的幕后英雄
|
||||
## 4. 三者协同:程序启动的完整过程
|
||||
|
||||
让我们通过一个你每天都在经历的场景,串联起今天学到的知识。当你**双击鼠标打开一个游戏**时,为了伺候你,大管家做了什么?
|
||||
我们已经分别了解了操作系统的三大模块,下面看看当你**双击打开一个程序**时,它们是如何协同工作的:
|
||||
|
||||
1. **文件系统**:立刻从底层硬盘的杂乱数据块中,拼凑出游戏的执行文件和美术资产。
|
||||
2. **内存管理**:为你分配一个巨大的虚拟内存空间,制造出“这台电脑只有这一个游戏”的幻觉,并把刚才找到的文件放进物理内存的空隙里。
|
||||
3. **进程管理**:在它的名册上新建一个“游戏进程”,并在下一个瞬间,立刻剥夺其他正在运行软件的 CPU 权利,把 CPU 的计算力全盘移交给你的游戏。
|
||||
<ProgramLaunchDemo />
|
||||
|
||||
我们之所以能那么轻松、优雅地在数字世界里冲浪,全都是因为底层的操作系统在替我们负重前行。
|
||||
无论是你点击桌面图标,还是代码中的一句 `print("Hello World")`,都离不开这一套复杂的暗箱操作。我们之所以能那么轻松地在数字世界里冲浪,全都是因为底层的操作系统在替我们负重前行。
|
||||
|
||||
---
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
如果你觉得操作系统的各种“管理学”十分有趣,你可以看看这些进阶话题:
|
||||
- **进程与线程的区别**:除了进程,还有一种叫作“线程”的东西,它们是干什么用的?(为什么 Google Chrome 那么吃内存?)
|
||||
- **页面置换算法**:当物理内存全都塞满了,但你又打开了一个新软件,操作系统该把谁的数据临时踢到硬盘里?(LRU 算法)
|
||||
- **操作系统的多态**:Windows 和 macOS 会在底层实现上有什么不同?为什么有些软件只能在特定系统上运行?
|
||||
如果你觉得操作系统的各种"管理学和骗术"十分有趣,你可以看看这些进阶话题:
|
||||
- **进程与线程**:如果进程是项目组,那"线程"就是组里干活的员工
|
||||
- **并发与锁**:当两个进程同时竞争同一个资源时,如何防止死锁
|
||||
- **系统调用**:操作系统给上层应用提供的"服务窗口"
|
||||
|
||||
@@ -112,23 +112,20 @@
|
||||
|
||||
如果刚才介绍的逻辑门只能做简单的条件判断,那计算机到底是如何做数学运算的呢?
|
||||
|
||||
我们先回想一下手算加法的方式:对应位相加,如果超出了限制(十进制是满十进一,二进制是满二进一),就向更高位“进位”。
|
||||
|
||||
在二进制中,只有 0 和 1。对于一位数的加法,可能的情况只有四种:
|
||||
- `0 + 0 = 0` (本位是 0,不进位)
|
||||
- `0 + 1 = 1` (本位是 1,不进位)
|
||||
- `1 + 0 = 1` (本位是 1,不进位)
|
||||
- `1 + 1 = 10` (本位是 0,进位 1)
|
||||
<BinaryAdditionRulesDemo />
|
||||
|
||||
仔细观察这四种情况,你会发现:
|
||||
1. **本位的结果**,只有在两个输入**不同**时才为 1,这正是 **XOR 门(异或门)** 的逻辑。
|
||||
2. **进位的结果**,只有在两个输入**都为 1** 时才为 1,这正是 **AND 门(与门)** 的逻辑。
|
||||
|
||||
因此,只要把一个 XOR 门和一个 AND 门组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder)**。
|
||||
因此,只要把一个 XOR 门(负责算本位)和一个 AND 门(负责算进位)组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder)**。
|
||||
|
||||
<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 />
|
||||
|
||||
@@ -185,6 +182,10 @@
|
||||
当我们将 32 个抑或 64 个这种触发器整齐地编排成一列,施加同一种强劲的时钟频率信号(Clock)来号令它们统一行动时,**寄存器(Register)**便应运而生了。它身居 CPU 系统的心脏位置,被当做极速的“工作草稿纸”,默默捍卫着你每一个即时的关键变量。
|
||||
:::
|
||||
|
||||
请通过下面的互动演示,亲自体验这个打破和恢复闭环的过程:
|
||||
|
||||
<FlipFlopDemo />
|
||||
|
||||
---
|
||||
|
||||
## 4. CPU 架构:从功能单元到处理器
|
||||
|
||||
@@ -66,6 +66,18 @@ result = response.choices[0].message.content
|
||||
|
||||
<ApiTypesComparison />
|
||||
|
||||
### 1.3 函数 API vs HTTP API 的区别
|
||||
|
||||
很多初学者会困惑:函数 API 和 HTTP API 到底有什么区别?看文档时该如何区分?
|
||||
|
||||
<ApiFunctionVsHttp />
|
||||
|
||||
### 1.4 不同类型的 API 文档怎么看
|
||||
|
||||
面对不同类型的 API 文档,关注重点各不相同:
|
||||
|
||||
<DocumentTypesComparison />
|
||||
|
||||
---
|
||||
|
||||
## 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
@@ -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 的核心操作就是 CRUD(Create, 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 的用户
|
||||
-- 方案一:INTERSECT(MySQL 不支持,用 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
@@ -4,7 +4,11 @@ import vueParser from 'vue-eslint-parser'
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['node_modules/**', 'docs/.vitepress/dist/**', 'docs/.vitepress/cache/**']
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'docs/.vitepress/dist/**',
|
||||
'docs/.vitepress/cache/**'
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
|
||||
Reference in New Issue
Block a user