feat(docs): restructure API design guide with interactive demos and practical examples

refactor(components): replace static API design components with interactive demos
- Add ApiRequestDemo, RestfulUrlDemo, StatusCodeDemo, ErrorHandlingDemo, and ApiVersioningDemo
- Remove outdated ResourceAnalogy, RequestStructureDemo, and VersioningStrategyDemo

docs(api-design): completely rewrite API design chapter with restaurant analogy
- Add clear problem scenarios and solutions
- Include practical e-commerce API examples
- Add terminology glossary
- Improve error handling and versioning sections

style(ai-history): enhance FoundationDemo with better visual hierarchy
- Add section blocks for core theories and early breakthroughs
- Improve typography and highlighting

chore: remove unused components (CpuArchitectureDemo, EvolutionFlowDemo)
This commit is contained in:
sanbuphy
2026-02-22 23:20:27 +08:00
parent e5a5b9df5b
commit 5b622800b8
26 changed files with 3217 additions and 4784 deletions
+1 -1
View File
@@ -629,8 +629,8 @@ export default defineConfig({
{ text: 'HTTP 协议', link: '/zh-cn/appendix/4-server-and-backend/http-protocol' },
{ text: '一个请求的完整旅程', link: '/zh-cn/appendix/4-server-and-backend/request-journey' },
{ text: 'Web 框架的本质', link: '/zh-cn/appendix/4-server-and-backend/web-frameworks' },
{ text: 'API 设计哲学(REST / GraphQL / gRPC', link: '/zh-cn/appendix/4-server-and-backend/api-design' },
{ text: 'API 入门', link: '/zh-cn/appendix/4-server-and-backend/api-intro' },
{ text: 'API 设计哲学(REST / GraphQL / gRPC', link: '/zh-cn/appendix/4-server-and-backend/api-design' },
{ text: '序列化与数据格式', link: '/zh-cn/appendix/4-server-and-backend/serialization' },
{ text: '认证与授权体系', link: '/zh-cn/appendix/4-server-and-backend/auth-authorization' },
{ text: '并发、异步与多线程', link: '/zh-cn/appendix/4-server-and-backend/concurrency-async' },
@@ -0,0 +1,107 @@
<template>
<div class="demo-card">
<div class="era-container">
<div class="era-header">
🌟 AI 发展阶段与核心范式全景对比
</div>
<div class="era-grid">
<div class="era-item" v-for="era in eras" :key="era.name" :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>
<div class="e-section">
<div class="e-label">驱动方式</div>
<div class="e-value">{{ era.driver }}</div>
</div>
<div class="e-section">
<div class="e-label">核心机制</div>
<div class="e-value">
<span class="highlight">{{ era.mechanism }}</span>
</div>
</div>
<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>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const eras = [
{
name: '规则系统时代',
time: '1960s - 1980s',
icon: '📜',
color: '#059669', // emerald
driver: '人类硬编码知识',
mechanism: 'If-Then 逻辑推演',
examples: ['Dendral', '深蓝 (Deep Blue)']
},
{
name: '传统机器学习',
time: '1990s - 2000s',
icon: '📊',
color: '#d97706', // amber
driver: '人工特征工程 + 统计学',
mechanism: '寻找数学决策边界',
examples: ['支持向量机 (SVM)', '随机森林']
},
{
name: '深度学习革命',
time: '2010s',
icon: '🧠',
color: '#dc2626', // red
driver: '大数据 + 算力爬升',
mechanism: '神经网络自动提取特征',
examples: ['AlexNet (CNN)', 'AlphaGo (RL)']
},
{
name: '大语言模型 (LLM)',
time: '2018 - 至今',
icon: '💬',
color: '#7c3aed', // violet
driver: '海量无标注数据 + 暴力计算',
mechanism: '预测下一个词 + 涌现常识',
examples: ['GPT-4', 'Claude 3']
},
{
name: '智能体 (Agentic AI)',
time: '现在 - 未来',
icon: '🤖',
color: '#0284c7', // light blue
driver: '大模型大脑 + 环境感知',
mechanism: '自主规划 + 工具调用',
examples: ['AI 程序员', '具身智能']
}
]
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1.5rem 0; overflow-x: auto; }
.era-container { min-width: 800px; display: flex; flex-direction: column; gap: 1rem; }
.era-header { text-align: center; font-weight: bold; font-size: 1.1rem; color: var(--vp-c-text-1); margin-bottom: 0.5rem; }
.era-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.8rem; }
.era-item { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-top: 4px solid; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.8rem; }
.e-icon { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; margin-bottom: 0.2rem; }
.e-name { font-weight: 800; font-size: 0.95rem; }
.e-time { font-size: 0.75rem; color: var(--vp-c-text-3); font-weight: bold; margin-top: -0.6rem; }
.e-section { width: 100%; display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.2rem; }
.e-label { font-size: 0.7rem; color: var(--vp-c-text-3); text-transform: uppercase; letter-spacing: 0.5px; }
.e-value { font-size: 0.8rem; color: var(--vp-c-text-2); line-height: 1.4; }
.highlight { display: inline-block; background: var(--vp-c-bg-soft); padding: 0.2rem 0.5rem; border-radius: 4px; font-weight: 600; color: var(--vp-c-text-1); border: 1px dashed var(--vp-c-divider); }
.e-tags { display: flex; flex-direction: column; gap: 0.4rem; align-items: center; justify-content: center; }
.e-tag { font-size: 0.75rem; background: var(--vp-c-bg-alt); border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-1); padding: 0.25rem 0.6rem; border-radius: 12px; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.html.dark .highlight { background: var(--vp-c-bg-alt); }
</style>
@@ -10,11 +10,26 @@
</div>
<div class="loss-visual">
<div class="loss-label">Loss误差随训练轮次下降</div>
<svg viewBox="0 0 300 60" class="loss-svg">
<polyline :points="lossPoints" fill="none" stroke="var(--vp-c-brand)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<text x="5" y="10" class="ax"></text>
<text x="5" y="56" class="ax"></text>
<text x="220" y="56" class="ax">训练轮次 </text>
<svg viewBox="0 0 320 130" class="loss-svg">
<!-- Axes -->
<line x1="40" y1="110" x2="300" y2="110" stroke="var(--vp-c-text-3)" stroke-width="1.5" />
<line x1="40" y1="110" x2="40" y2="15" stroke="var(--vp-c-text-3)" stroke-width="1.5" />
<!-- X Arrow -->
<polygon points="300,107 305,110 300,113" fill="var(--vp-c-text-3)" />
<!-- Y Arrow -->
<polygon points="37,15 40,10 43,15" fill="var(--vp-c-text-3)" />
<!-- Y Label -->
<text x="30" y="25" text-anchor="end" class="ax-text"></text>
<text x="30" y="105" text-anchor="end" class="ax-text"></text>
<text x="20" y="65" text-anchor="middle" transform="rotate(-90 20 65)" class="ax-title">Loss</text>
<!-- X Label -->
<text x="300" y="125" text-anchor="end" class="ax-title">训练轮次 (Epochs)</text>
<!-- Loss 曲线 -->
<polyline :points="lossPoints" fill="none" stroke="var(--vp-c-brand)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</div>
@@ -30,8 +45,15 @@ const steps = [
const lossPoints = (() => {
const pts = []
for (let i = 0; i <= 50; i++) {
const x = 20 + i * 5.2
const y = 52 - 44 * Math.exp(-i * 0.09) + Math.sin(i * 0.7) * 1
const x = 40 + i * 5; // 40 to 290
// Y从上(小值)到下(大值),Loss越来越低,意味着Y越来越大,靠近110
// 我们让一开始的高Loss出现在 y=20 附近,最终的低Loss停留 在 y=105 附近
let noise = (Math.random() - 0.5) * 3;
let y = 105 - 85 * Math.exp(-i * 0.12) + noise;
if (i === 0) y = 20; // 确保起点干净
if (y > 108) y = 108; // 不超过底轴
pts.push(`${x},${y}`)
}
return pts.join(' ')
@@ -49,6 +71,8 @@ const lossPoints = (() => {
.step-desc { font-size: 0.68rem; color: var(--vp-c-text-2); line-height: 1.3; }
.loss-visual { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.7rem; }
.loss-label { font-size: 0.75rem; color: var(--vp-c-text-2); margin-bottom: 0.3rem; }
.loss-svg { width: 100%; max-width: 380px; height: auto; display: block; margin: 0 auto; }
.ax { font-size: 6px; fill: var(--vp-c-text-3); }
.loss-svg { width: 100%; max-width: 460px; height: auto; display: block; margin: 0 auto; overflow: visible; font-family: sans-serif; }
.axis-line { color: var(--vp-c-text-3); }
.ax-text { font-size: 10px; fill: var(--vp-c-text-2); }
.ax-title { font-size: 11px; fill: var(--vp-c-text-1); font-weight: 500; }
</style>
@@ -0,0 +1,87 @@
<template>
<div class="demo-card">
<div class="expert-system-flow">
<div class="es-card success">
<div class="es-title">🌟 专家系统的辉煌</div>
<div class="es-list">
<div class="es-item">
<span class="es-box input">人类专家经验</span>
<span class="es-arrow"></span>
<span class="es-box rules">转为 IF-THEN 规则库</span>
</div>
<div class="es-item">
<span class="es-box input">特定领域问题</span>
<span class="es-arrow"></span>
<span class="es-box output">推理解答 (诊断/配置)</span>
</div>
</div>
<div class="es-tags">
<span class="es-tag">1965: Dendral (化学)</span>
<span class="es-tag">1977: MYCIN (医疗)</span>
<span class="es-tag">1980: XCON (配置)</span>
</div>
</div>
<div class="es-arrow-down"> 局限性爆发 </div>
<div class="es-card winter">
<div class="es-title"><span class="snow"></span> 第一次 AI 寒冬 (1974-1980)</div>
<div class="winter-reasons">
<div class="reason">
<span class="r-icon">📝</span>
<div class="r-text">
<strong>知识获取瓶颈</strong>
<span>波兰尼悖论人类无法说清所有规律大量"常识"无法被人工硬编码</span>
</div>
</div>
<div class="reason">
<span class="r-icon">💥</span>
<div class="r-text">
<strong>组合爆炸 & 脆性问题</strong>
<span>现实情况太多穷举极难且缺少常识稍微偏离规则库系统就直接崩溃</span>
</div>
</div>
<div class="reason">
<span class="r-icon">📉</span>
<div class="r-text">
<strong>算力不足 & 经费断层</strong>
<span>当时的硬件算力根本无法支撑爆发性的逻辑推演遭遇 DARPA 研发经费大削减</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
.expert-system-flow { display: flex; flex-direction: column; align-items: center; gap: 0.8rem; }
.es-card { width: 100%; max-width: 500px; border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 1rem; background: var(--vp-c-bg); }
.es-card.success { border-top: 3px solid #059669; }
.es-card.winter { border-top: 3px solid #3b82f6; background: rgba(59, 130, 246, 0.03); }
.es-title { font-weight: bold; font-size: 0.9rem; margin-bottom: 0.8rem; text-align: center; color: var(--vp-c-text-1); }
.es-list { display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem; }
.es-item { display: flex; align-items: center; justify-content: center; gap: 0.4rem; font-size: 0.75rem; }
.es-box { padding: 0.4rem 0.6rem; border-radius: 4px; font-weight: 500; border: 1px solid var(--vp-c-divider); text-align: center; }
.es-box.input { background: var(--vp-c-bg-soft); color: var(--vp-c-text-2); }
.es-box.rules { background: #d1fae5; color: #065f46; border-color: #34d399; }
.es-box.output { background: #e0e7ff; color: #3730a3; border-color: #818cf8; }
.html.dark .es-box.rules { background: rgba(5, 150, 105, 0.2); color: #a7f3d0; border-color: #059669; }
.html.dark .es-box.output { background: rgba(79, 70, 229, 0.2); color: #c7d2fe; border-color: #4f46e5; }
.es-arrow { color: var(--vp-c-text-3); font-weight: bold; }
.es-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; }
.es-tag { font-size: 0.65rem; background: var(--vp-c-bg-soft); padding: 0.15rem 0.5rem; border-radius: 12px; color: var(--vp-c-text-2); border: 1px solid var(--vp-c-divider); }
.es-arrow-down { font-size: 0.8rem; color: var(--vp-c-text-3); font-weight: bold; margin: 0.2rem 0; }
.snow { color: #3b82f6; margin-right: 0.2rem; }
.winter-reasons { display: flex; flex-direction: column; gap: 0.6rem; }
.reason { display: flex; align-items: flex-start; gap: 0.6rem; background: var(--vp-c-bg-alt); padding: 0.6rem; border-radius: 6px; border: 1px solid var(--vp-c-divider); }
.r-icon { font-size: 1.2rem; margin-top: 0.1rem; }
.r-text { display: flex; flex-direction: column; }
.r-text strong { font-size: 0.8rem; color: var(--vp-c-text-1); }
.r-text span { font-size: 0.7rem; color: var(--vp-c-text-2); line-height: 1.4; margin-top: 0.15rem; }
</style>
@@ -1,44 +1,87 @@
<template>
<div class="demo-card">
<div class="events">
<div class="event" v-for="e in events" :key="e.year">
<div class="year-col">
<span class="year-badge">{{ e.year }}</span>
</div>
<div class="dot-col">
<div class="dot" :style="{ background: e.color }"></div>
<div class="line" v-if="e !== events[events.length - 1]"></div>
</div>
<div class="content-col">
<div class="event-title">{{ e.title }}</div>
<div class="event-note">{{ e.note }}</div>
<div class="foundation-container">
<!-- Part 1: Core Theories -->
<div class="section-block">
<div class="section-title">核心人物与理论</div>
<div class="event-list">
<div class="event-item">
<span class="e-year">1943</span>
<span class="e-text"><strong>沃伦·麦卡洛克 & 沃尔特·皮茨</strong> 提出 <em>MP 神经元模型</em>首次用数学描述神经网络</span>
</div>
<div class="event-item">
<span class="e-year">1950</span>
<span class="e-text"><strong>艾伦·图灵</strong> 发表计算机器与智能提出 <em>图灵测试</em>定义机器智能标准</span>
</div>
<div class="event-item highlight-event">
<span class="e-year">1956</span>
<span class="e-text"><strong>达特茅斯会议</strong>约翰·麦卡锡首次提出"人工智能"概念标志 AI 学科正式诞生</span>
</div>
</div>
</div>
<!-- Part 2: Symbolism -->
<div class="symbolism-panel">
<div class="s-header">
<span class="s-icon">📜</span>
<span class="s-title">符号主义兴起 (Symbolism)</span>
</div>
<div class="s-body">
<div class="s-equation">智能 = 符号推理</div>
<div class="s-desc">
符号主义逻辑主义/计算机学派主张将知识编码为符号通过 <strong>逻辑规则与推导</strong> 解决问题这是一条极其依赖人类专家的 <em>自上而下</em> 的智能模拟路径
</div>
</div>
</div>
<!-- Part 3: Early Breakthroughs -->
<div class="section-block">
<div class="section-title">早期突破</div>
<div class="event-list">
<div class="event-item">
<span class="e-year">1956</span>
<span class="e-text">纽厄尔和西蒙开发 <strong>逻辑理论家 (Logic Theorist)</strong>首个能证明数学定理的 AI 程序</span>
</div>
<div class="event-item">
<span class="e-year">1958</span>
<span class="e-text">麦卡锡发明 <strong>LISP 语言</strong>成为 AI 研究的重要工具</span>
</div>
<div class="event-item">
<span class="e-year">1959</span>
<span class="e-text">乔治·德沃尔与约瑟夫·恩格尔伯格开发首台 <strong>工业机器人</strong>标志 AI 从理论走向应用</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const events = [
{ year: '1943', title: 'MP 神经元模型', note: '麦卡洛克 & 皮茨:首次用数学公式模拟神经元,证明"神经元可被计算"。', color: '#3b82f6' },
{ year: '1950', title: '图灵测试', note: '图灵:如果机器的回答让人无法分辨是人还是机器,则认为它具备智能。', color: '#7c3aed' },
{ year: '1956', title: '达特茅斯会议 — AI 学科诞生', note: '麦卡锡等人首次提出"人工智能"概念,AI 正式成为一门学科。', color: '#059669' },
{ year: '1956', title: '逻辑理论家(Logic Theorist', note: '纽厄尔 & 西蒙:第一个用规则自动证明数学定理的 AI 程序。', color: '#059669' },
{ year: '1958', title: 'LISP 语言诞生', note: '麦卡锡发明,成为此后数十年 AI 研究的核心编程语言。', color: '#d97706' },
{ year: '1959', title: '首台工业机器人', note: '德沃尔 & 恩格尔伯格:AI 从实验室走向工厂,开始改变工业生产。', color: '#dc2626' },
]
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
.events { display: flex; flex-direction: column; }
.event { display: grid; grid-template-columns: 52px 24px 1fr; gap: 0 0.6rem; }
.year-col { display: flex; align-items: flex-start; padding-top: 0.15rem; justify-content: flex-end; }
.year-badge { font-size: 0.7rem; font-weight: bold; color: var(--vp-c-text-3); white-space: nowrap; }
.dot-col { display: flex; flex-direction: column; align-items: center; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; margin-top: 0.2rem; }
.line { width: 2px; flex: 1; background: var(--vp-c-divider); margin: 3px 0; min-height: 16px; }
.content-col { padding-bottom: 0.9rem; }
.event-title { font-weight: bold; font-size: 0.85rem; color: var(--vp-c-text-1); margin-bottom: 0.15rem; }
.event-note { font-size: 0.78rem; color: var(--vp-c-text-2); line-height: 1.5; }
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.2rem; margin: 1rem 0; }
.foundation-container { display: flex; flex-direction: column; gap: 1.2rem; }
.section-block { display: flex; flex-direction: column; gap: 0.5rem; }
.section-title { font-weight: bold; font-size: 0.9rem; color: var(--vp-c-brand-1); border-bottom: 1px solid var(--vp-c-divider); padding-bottom: 0.3rem; margin-bottom: 0.3rem; }
.event-list { display: flex; flex-direction: column; gap: 0.5rem; }
.event-item { display: flex; align-items: flex-start; gap: 0.6rem; font-size: 0.8rem; line-height: 1.4; }
.e-year { font-weight: bold; background: var(--vp-c-bg-alt); padding: 0.1rem 0.4rem; border-radius: 4px; color: var(--vp-c-text-1); border: 1px solid var(--vp-c-divider); font-family: monospace; flex-shrink: 0; }
.e-text { color: var(--vp-c-text-2); padding-top: 0.1rem; }
.e-text strong { color: var(--vp-c-text-1); }
.e-text em { color: var(--vp-c-brand-1); font-style: normal; font-weight: 500; }
.highlight-event .e-year { background: #10b981; color: white; border-color: #059669; }
.symbolism-panel { background: rgba(16, 185, 129, 0.05); border: 1px dashed #10b981; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; gap: 0.6rem; text-align: center; }
.s-header { display: flex; align-items: center; justify-content: center; gap: 0.4rem; }
.s-icon { font-size: 1.2rem; }
.s-title { font-weight: bold; font-size: 0.95rem; color: #059669; }
.s-equation { font-family: monospace; font-size: 1rem; font-weight: bold; color: var(--vp-c-text-1); background: var(--vp-c-bg); padding: 0.4rem 1rem; border-radius: 6px; border: 1px solid var(--vp-c-divider); display: inline-block; margin: 0 auto; }
.s-desc { font-size: 0.78rem; color: var(--vp-c-text-2); line-height: 1.5; max-width: 90%; margin: 0 auto; }
.html.dark .symbolism-panel { background: rgba(5, 150, 105, 0.15); border-color: #059669; }
.html.dark .s-title { color: #34d399; }
</style>
@@ -0,0 +1,532 @@
<template>
<div class="ar-root">
<div class="ar-layout">
<div class="ar-left">
<div class="ar-terminal">
<div class="term-bar">
<span class="dot r" /><span class="dot y" /><span class="dot g" />
<span class="term-title">API 请求演示</span>
</div>
<div ref="termEl" class="term-body">
<div v-for="(l, i) in lines" :key="i" class="t-line">
<span v-if="l.kind === 'cmd'" class="t-ps">&gt; </span>
<span :class="'t-' + l.kind">{{ l.text }}</span>
</div>
<div class="t-line">
<span class="t-ps">&gt; </span>
<span class="t-typing">{{ typing }}<span class="t-cur"></span></span>
</div>
</div>
</div>
<div class="ar-btns">
<button
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['ar-btn', { 'ar-btn--on': active === op.id, 'ar-btn--dim': !op.ok() }]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="ar-btn ar-btn--reset" :disabled="running" @click="reset">重置</button>
</div>
</div>
<div class="ar-right">
<div class="ar-flow">
<div class="flow-col flow-client" :class="{ 'flow-highlight': pulseArea === 'client' }">
<div class="flow-header">
<span class="flow-icon">💻</span>
<span class="flow-title">客户端</span>
<span class="flow-desc">发起请求</span>
</div>
<div class="flow-body">
<div v-if="requestData" class="req-preview">
<div class="req-line">
<span class="req-method" :class="requestData.method">{{ requestData.method }}</span>
<span class="req-url">{{ requestData.url }}</span>
</div>
<div v-if="requestData.body" class="req-body">
<pre>{{ requestData.body }}</pre>
</div>
</div>
<div v-else class="flow-empty">等待请求...</div>
</div>
</div>
<div class="flow-arrow" :class="{ 'arrow-lit': pulseArea === 'request' }">
<code class="arrow-label">HTTP Request</code>
<span class="arrow-symbol"></span>
</div>
<div class="flow-col flow-server" :class="{ 'flow-highlight': pulseArea === 'server' }">
<div class="flow-header">
<span class="flow-icon">🖥</span>
<span class="flow-title">服务器</span>
<span class="flow-desc">处理请求</span>
</div>
<div class="flow-body">
<div v-if="serverStatus" class="server-status">
<span class="status-icon">{{ serverStatus.icon }}</span>
<span class="status-text">{{ serverStatus.text }}</span>
</div>
<div v-else class="flow-empty">等待中...</div>
</div>
</div>
<div class="flow-arrow" :class="{ 'arrow-lit': pulseArea === 'response' }">
<code class="arrow-label">HTTP Response</code>
<span class="arrow-symbol"></span>
</div>
<div class="flow-col flow-response" :class="{ 'flow-highlight': pulseArea === 'response' }">
<div class="flow-header">
<span class="flow-icon">📦</span>
<span class="flow-title">响应</span>
<span class="flow-desc">返回结果</span>
</div>
<div class="flow-body">
<div v-if="responseData" class="res-preview">
<div class="res-status" :class="responseData.statusClass">
{{ responseData.status }}
</div>
<div class="res-body">
<pre>{{ responseData.body }}</pre>
</div>
</div>
<div v-else class="flow-empty">等待响应...</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="hint" class="ar-hint">💡 {{ hint }}</div>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const termEl = ref(null)
const lines = ref([{ kind: 'dim', text: '// 点击下方按钮,模拟不同的 API 请求' }])
const typing = ref('')
const running = ref(false)
const active = ref(null)
const hint = ref('点击命令按钮,观察一次完整的 API 请求-响应流程。')
const pulseArea = ref(null)
const requestData = ref(null)
const serverStatus = ref(null)
const responseData = ref(null)
const sleep = ms => new Promise(r => setTimeout(r, ms))
const ops = [
{
id: 'get-users',
cmd: 'GET /api/users',
ok: () => true,
output: [
{ kind: 'dim', text: '// 获取用户列表' },
{ kind: 'grn', text: 'HTTP/1.1 200 OK' },
{ kind: 'dim', text: 'Content-Type: application/json' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 0, "data": { "items": [...] } }' },
],
hint: 'GET 请求成功!状态码 200 表示请求正常。服务器返回了用户列表数据。',
do: async () => {
requestData.value = { method: 'GET', url: '/api/users' }
pulseArea.value = 'client'
await sleep(300)
pulseArea.value = 'request'
await sleep(300)
serverStatus.value = { icon: '⚡', text: '查询数据库...' }
pulseArea.value = 'server'
await sleep(500)
serverStatus.value = { icon: '✓', text: '处理完成' }
pulseArea.value = 'response'
await sleep(300)
responseData.value = {
status: '200 OK',
statusClass: 'success',
body: '{\n "code": 0,\n "data": {\n "items": [\n {"id": 1, "name": "张三"},\n {"id": 2, "name": "李四"}\n ]\n }\n}'
}
}
},
{
id: 'post-user',
cmd: 'POST /api/users',
ok: () => true,
output: [
{ kind: 'dim', text: '// 创建新用户' },
{ kind: 'grn', text: 'HTTP/1.1 201 Created' },
{ kind: 'dim', text: 'Location: /api/users/3' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 0, "data": { "id": 3, "name": "王五" } }' },
],
hint: 'POST 创建成功!状态码 201 表示资源已创建,响应头 Location 指向新资源地址。',
do: async () => {
requestData.value = {
method: 'POST',
url: '/api/users',
body: '{\n "name": "王五",\n "email": "wangwu@example.com"\n}'
}
pulseArea.value = 'client'
await sleep(300)
pulseArea.value = 'request'
await sleep(300)
serverStatus.value = { icon: '⚡', text: '验证数据...' }
pulseArea.value = 'server'
await sleep(400)
serverStatus.value = { icon: '⚡', text: '写入数据库...' }
await sleep(400)
serverStatus.value = { icon: '✓', text: '创建成功' }
pulseArea.value = 'response'
await sleep(300)
responseData.value = {
status: '201 Created',
statusClass: 'success',
body: '{\n "code": 0,\n "data": {\n "id": 3,\n "name": "王五",\n "email": "wangwu@example.com"\n }\n}'
}
}
},
{
id: 'get-404',
cmd: 'GET /api/users/999',
ok: () => true,
output: [
{ kind: 'dim', text: '// 获取不存在的用户' },
{ kind: 'red', text: 'HTTP/1.1 404 Not Found' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10002, "message": "用户不存在" }' },
],
hint: '404 错误!请求的资源不存在。客户端应该检查请求的 ID 是否正确。',
do: async () => {
requestData.value = { method: 'GET', url: '/api/users/999' }
pulseArea.value = 'client'
await sleep(300)
pulseArea.value = 'request'
await sleep(300)
serverStatus.value = { icon: '🔍', text: '查找用户...' }
pulseArea.value = 'server'
await sleep(500)
serverStatus.value = { icon: '✗', text: '未找到' }
pulseArea.value = 'response'
await sleep(300)
responseData.value = {
status: '404 Not Found',
statusClass: 'error',
body: '{\n "code": 10002,\n "message": "用户不存在"\n}'
}
}
},
{
id: 'post-401',
cmd: 'POST /api/orders (无Token)',
ok: () => true,
output: [
{ kind: 'dim', text: '// 未登录尝试下单' },
{ kind: 'red', text: 'HTTP/1.1 401 Unauthorized' },
{ kind: 'dim', text: 'WWW-Authenticate: Bearer' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10018, "message": "请先登录" }' },
],
hint: '401 错误!需要身份认证。客户端应该引导用户登录后再重试。',
do: async () => {
requestData.value = {
method: 'POST',
url: '/api/orders',
body: '{\n "product_id": "P001",\n "quantity": 2\n}'
}
pulseArea.value = 'client'
await sleep(300)
pulseArea.value = 'request'
await sleep(300)
serverStatus.value = { icon: '🔐', text: '验证身份...' }
pulseArea.value = 'server'
await sleep(400)
serverStatus.value = { icon: '✗', text: '未授权' }
pulseArea.value = 'response'
await sleep(300)
responseData.value = {
status: '401 Unauthorized',
statusClass: 'error',
body: '{\n "code": 10018,\n "message": "请先登录"\n}'
}
}
},
]
async function run(op) {
if (running.value) return
running.value = true
active.value = op.id
hint.value = ''
typing.value = ''
pulseArea.value = null
requestData.value = null
serverStatus.value = null
responseData.value = null
for (const ch of op.cmd) {
typing.value += ch
await sleep(18)
}
await sleep(80)
lines.value.push({ kind: 'cmd', text: op.cmd })
typing.value = ''
await nextTick()
scroll()
await sleep(150)
for (const l of op.output) {
lines.value.push(l)
await nextTick()
scroll()
await sleep(50)
}
await op.do()
await sleep(120)
hint.value = op.hint
running.value = false
setTimeout(() => { pulseArea.value = null }, 1500)
}
function scroll() {
if (termEl.value) termEl.value.scrollTop = termEl.value.scrollHeight
}
function reset() {
lines.value = [{ kind: 'dim', text: '// 点击下方按钮,模拟不同的 API 请求' }]
active.value = null
pulseArea.value = null
hint.value = '点击命令按钮,观察一次完整的 API 请求-响应流程。'
typing.value = ''
running.value = false
requestData.value = null
serverStatus.value = null
responseData.value = null
}
</script>
<style scoped>
.ar-root {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.ar-layout {
display: flex;
align-items: stretch;
gap: 0;
}
.ar-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.ar-right {
width: 280px;
flex-shrink: 0;
border-left: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
@media (max-width: 768px) {
.ar-layout { flex-direction: column; }
.ar-right { width: 100%; border-left: none; border-top: 1px solid var(--vp-c-divider); }
}
.ar-terminal { background: #141420; }
.term-bar {
display: flex;
align-items: center;
gap: 5px;
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.term-body {
min-height: 120px;
max-height: 180px;
overflow-y: auto;
overflow-x: auto;
padding: 0.8rem 1rem;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.76rem;
line-height: 1.65;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #89b4fa; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.ar-btns {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 10px;
background: #0d0d1a;
border-top: 1px solid #2a2a3e;
}
.ar-btn {
background: #1e1e2e;
border: 1px solid #313244;
border-radius: 5px;
padding: 4px 9px;
cursor: pointer;
transition: border-color 0.2s;
}
.ar-btn code { font-size: 0.7rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.ar-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.ar-btn--on { border-color: var(--vp-c-brand) !important; }
.ar-btn--on code { color: var(--vp-c-brand); }
.ar-btn--dim { opacity: 0.3; cursor: not-allowed; }
.ar-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.ar-btn--reset code { display: none; }
.ar-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.ar-flow {
display: flex;
flex-direction: column;
gap: 0;
padding: 10px 12px;
background: var(--vp-c-bg);
font-size: 0.75rem;
}
.flow-col {
border: 1.5px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 60px;
transition: border-color 0.25s, box-shadow 0.25s;
}
.flow-col.flow-highlight {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 14%, transparent);
}
.flow-client { border-left: 4px solid #89b4fa; }
.flow-server { border-left: 4px solid #f9e2af; }
.flow-response { border-left: 4px solid #a6e3a1; }
.flow-header {
padding: 6px 8px;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 6px;
}
.flow-icon { font-size: 0.9rem; }
.flow-title { font-weight: 700; font-size: 0.8rem; color: var(--vp-c-text-1); }
.flow-desc { font-size: 0.7rem; color: var(--vp-c-text-3); margin-left: auto; }
.flow-body {
padding: 8px 10px;
flex: 1;
min-height: 48px;
}
.flow-empty {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-style: italic;
}
.req-preview, .res-preview { font-size: 0.72rem; }
.req-line { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.req-method {
padding: 2px 6px;
border-radius: 3px;
font-weight: 700;
font-size: 0.68rem;
}
.req-method.GET { background: #22c55e22; color: #22c55e; }
.req-method.POST { background: #3b82f622; color: #3b82f6; }
.req-url { font-family: monospace; color: var(--vp-c-text-1); }
.req-body pre, .res-body pre {
margin: 0;
padding: 6px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.65rem;
line-height: 1.4;
overflow-x: auto;
}
.server-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
}
.status-icon { font-size: 1rem; }
.status-text { color: var(--vp-c-text-2); }
.res-status {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 0.7rem;
margin-bottom: 6px;
}
.res-status.success { background: #22c55e22; color: #22c55e; }
.res-status.error { background: #ef444422; color: #ef4444; }
.flow-arrow {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 4px 0;
opacity: 0.3;
transition: opacity 0.3s;
}
.flow-arrow.arrow-lit { opacity: 1; }
.arrow-label {
font-size: 0.65rem;
font-family: monospace;
color: var(--vp-c-brand);
white-space: nowrap;
}
.arrow-symbol {
font-size: 0.9rem;
color: var(--vp-c-brand);
}
.ar-hint {
padding: 10px 12px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
</style>
@@ -0,0 +1,419 @@
<template>
<div class="av-root">
<div class="av-terminal">
<div class="term-bar">
<span class="dot r" /><span class="dot y" /><span class="dot g" />
<span class="term-title">API 版本控制演示</span>
</div>
<div ref="termEl" class="term-body">
<div v-for="(l, i) in lines" :key="i" class="t-line">
<span v-if="l.kind === 'cmd'" class="t-ps">$ </span>
<span :class="'t-' + l.kind">{{ l.text }}</span>
</div>
<div class="t-line">
<span class="t-ps">$ </span>
<span class="t-typing">{{ typing }}<span class="t-cur"></span></span>
</div>
</div>
</div>
<div class="av-btns">
<button
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['av-btn', { 'av-btn--on': active === op.id, 'av-btn--dim': !op.ok() }]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="av-btn av-btn--reset" :disabled="running" @click="reset">重置</button>
</div>
<div class="av-versions">
<div class="version-col" :class="{ active: activeVersion === 'v1' }">
<div class="version-header v1">
<span class="version-name">v1 (旧版)</span>
<span class="version-status">兼容旧客户端</span>
</div>
<div class="version-body">
<div class="api-item">
<code>GET /v1/users</code>
<span class="api-desc">返回 name, email</span>
</div>
<div class="api-item">
<code>POST /v1/orders</code>
<span class="api-desc">接收 items 数组</span>
</div>
</div>
</div>
<div class="version-arrow">
<span class="arrow-text">升级</span>
<span class="arrow-symbol"></span>
</div>
<div class="version-col" :class="{ active: activeVersion === 'v2' }">
<div class="version-header v2">
<span class="version-name">v2 (新版)</span>
<span class="version-status">新功能在这里</span>
</div>
<div class="version-body">
<div class="api-item">
<code>GET /v2/users</code>
<span class="api-desc">返回 name, email, avatar, phone</span>
</div>
<div class="api-item">
<code>POST /v2/orders</code>
<span class="api-desc">接收 items + coupons</span>
</div>
<div class="api-item new">
<code>POST /v2/orders/batch</code>
<span class="api-desc">🆕 批量下单</span>
</div>
</div>
</div>
</div>
<div v-if="hint" class="av-hint">💡 {{ hint }}</div>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const termEl = ref(null)
const lines = ref([{ kind: 'dim', text: '# API 版本控制:让新旧接口和平共处' }])
const typing = ref('')
const running = ref(false)
const active = ref(null)
const activeVersion = ref('')
const hint = ref('点击按钮,了解 API 版本控制的策略和最佳实践。')
const sleep = ms => new Promise(r => setTimeout(r, ms))
const ops = [
{
id: 'why',
cmd: '为什么需要版本控制?',
ok: () => true,
output: [
{ kind: 'dim', text: '# 场景:你的 App 有 100 万用户' },
{ kind: 'dim', text: '' },
{ kind: 'yel', text: '问题:需要修改订单接口,添加新字段、废弃旧字段' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '❌ 如果不做版本控制:' },
{ kind: 'red', text: ' 新 App 调用新接口 → 正常' },
{ kind: 'red', text: ' 旧 App 调用新接口 → 字段缺失,崩溃!' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '✅ 正确做法:' },
{ kind: 'grn', text: ' /v1/orders - 旧接口,继续服务旧 App' },
{ kind: 'grn', text: ' /v2/orders - 新接口,新功能在这里' },
],
hint: '版本控制让新旧客户端都能正常工作。旧 App 用户可以慢慢升级,不会突然崩溃。',
do: () => { activeVersion.value = '' }
},
{
id: 'url',
cmd: '方式1: URL 路径版本',
ok: () => true,
output: [
{ kind: 'dim', text: '# 最常用的方式' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: 'GET /v1/users' },
{ kind: 'grn', text: 'GET /v2/users' },
{ kind: 'grn', text: 'GET /v3/users' },
{ kind: 'dim', text: '' },
{ kind: 'dim', text: '优点:直观、易缓存、浏览器友好' },
{ kind: 'dim', text: '缺点:URL 变长' },
],
hint: 'URL 路径版本是最常用的方式。GitHub、Twitter、Stripe 都用这种方式。',
do: () => { activeVersion.value = 'v1' }
},
{
id: 'header',
cmd: '方式2: Header 版本',
ok: () => true,
output: [
{ kind: 'dim', text: '# 通过请求头指定版本' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: 'GET /users' },
{ kind: 'grn', text: 'Accept: application/vnd.myapi.v2+json' },
{ kind: 'dim', text: '' },
{ kind: 'dim', text: '或者:' },
{ kind: 'grn', text: 'GET /users' },
{ kind: 'grn', text: 'X-API-Version: 2' },
{ kind: 'dim', text: '' },
{ kind: 'dim', text: '优点:URL 干净' },
{ kind: 'dim', text: '缺点:不便调试、缓存复杂' },
],
hint: 'Header 版本让 URL 更干净,但调试时需要额外设置 Header,不如 URL 版本直观。',
do: () => { activeVersion.value = 'v2' }
},
{
id: 'query',
cmd: '方式3: 查询参数版本',
ok: () => true,
output: [
{ kind: 'dim', text: '# 通过查询参数指定版本' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: 'GET /users?version=1' },
{ kind: 'grn', text: 'GET /users?version=2' },
{ kind: 'dim', text: '' },
{ kind: 'dim', text: '优点:简单、向后兼容' },
{ kind: 'dim', text: '缺点:容易被忽略、不是 RESTful 标准' },
],
hint: '查询参数版本简单但不够"正规"。适合内部 API 或快速迭代的项目。',
do: () => { activeVersion.value = '' }
},
{
id: 'best',
cmd: '最佳实践',
ok: () => true,
output: [
{ kind: 'dim', text: '# 版本控制的最佳实践' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '1. 从一开始就加版本号 /v1/' },
{ kind: 'grn', text: '2. 新功能放新版本,旧版本保持稳定' },
{ kind: 'grn', text: '3. 设置废弃时间线(如 v1 将在 2025-06 废弃)' },
{ kind: 'grn', text: '4. 响应头标注当前版本和废弃信息' },
{ kind: 'grn', text: '5. 文档明确标注每个版本的变更' },
],
hint: '版本控制不是"以后再说"的事,从第一天就应该规划好。废弃旧版本要给用户足够的迁移时间。',
do: () => { activeVersion.value = 'v2' }
},
]
async function run(op) {
if (running.value) return
running.value = true
active.value = op.id
hint.value = ''
typing.value = ''
for (const ch of op.cmd) {
typing.value += ch
await sleep(18)
}
await sleep(80)
lines.value.push({ kind: 'cmd', text: op.cmd })
typing.value = ''
await nextTick()
scroll()
await sleep(150)
for (const l of op.output) {
lines.value.push(l)
await nextTick()
scroll()
await sleep(50)
}
op.do()
await sleep(120)
hint.value = op.hint
running.value = false
}
function scroll() {
if (termEl.value) termEl.value.scrollTop = termEl.value.scrollHeight
}
function reset() {
lines.value = [{ kind: 'dim', text: '# API 版本控制:让新旧接口和平共处' }]
active.value = null
activeVersion.value = ''
hint.value = '点击按钮,了解 API 版本控制的策略和最佳实践。'
typing.value = ''
running.value = false
}
</script>
<style scoped>
.av-root {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.av-terminal { background: #141420; }
.term-bar {
display: flex;
align-items: center;
gap: 5px;
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.term-body {
min-height: 100px;
max-height: 180px;
overflow-y: auto;
overflow-x: auto;
padding: 0.7rem 1rem;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.76rem;
line-height: 1.6;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #a6e3a1; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-yel { color: #f9e2af; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.av-btns {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 10px;
background: #0d0d1a;
border-top: 1px solid #2a2a3e;
}
.av-btn {
background: #1e1e2e;
border: 1px solid #313244;
border-radius: 5px;
padding: 4px 9px;
cursor: pointer;
transition: border-color 0.2s;
}
.av-btn code { font-size: 0.68rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.av-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.av-btn--on { border-color: var(--vp-c-brand) !important; }
.av-btn--on code { color: var(--vp-c-brand); }
.av-btn--dim { opacity: 0.3; cursor: not-allowed; }
.av-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.av-btn--reset code { display: none; }
.av-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.av-versions {
display: flex;
align-items: stretch;
gap: 0;
border-top: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
.version-col {
flex: 1;
border: 1px solid var(--vp-c-divider);
border-top: none;
border-left: none;
transition: all 0.3s;
}
.version-col:last-child {
border-right: none;
}
.version-col.active {
background: color-mix(in srgb, var(--vp-c-brand) 4%, var(--vp-c-bg));
}
.version-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
font-weight: 700;
font-size: 0.85rem;
}
.version-header.v1 {
background: color-mix(in srgb, #64748b 10%, var(--vp-c-bg-alt));
color: #64748b;
}
.version-header.v2 {
background: color-mix(in srgb, var(--vp-c-brand) 10%, var(--vp-c-bg-alt));
color: var(--vp-c-brand);
}
.version-status {
font-size: 0.7rem;
font-weight: 400;
opacity: 0.8;
}
.version-body {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.api-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.api-item.new {
border-left: 3px solid #22c55e;
background: color-mix(in srgb, #22c55e 8%, var(--vp-c-bg-soft));
}
.api-item code {
font-family: monospace;
font-size: 0.72rem;
color: var(--vp-c-text-1);
}
.api-desc {
font-size: 0.68rem;
color: var(--vp-c-text-3);
}
.version-arrow {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 12px;
color: var(--vp-c-text-3);
}
.arrow-text {
font-size: 0.7rem;
}
.arrow-symbol {
font-size: 1.2rem;
color: var(--vp-c-brand);
}
.av-hint {
padding: 10px 12px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
@media (max-width: 640px) {
.av-versions {
flex-direction: column;
}
.version-col {
border-left: none;
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.version-arrow {
flex-direction: row;
padding: 8px 0;
}
}
</style>
@@ -1,518 +0,0 @@
<!--
DocumentationDemo.vue - API 文档演示组件
展示 API 文档的编写规范和最佳实践
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">📚</span>
<span class="title">API 文档最好的 API 文档就是代码本身</span>
</div>
<div class="content">
<div class="tools-tabs">
<button
v-for="tool in tools"
:key="tool.id"
class="tool-btn"
:class="{ active: selectedTool === tool.id }"
@click="selectedTool = tool.id"
>
<span class="tool-icon">{{ tool.icon }}</span>
<span class="tool-name">{{ tool.name }}</span>
</button>
</div>
<div
v-if="currentTool"
class="tool-detail"
>
<div class="tool-header">
<div class="tool-title">
{{ currentTool.name }}
</div>
<div class="tool-tags">
<span
v-for="tag in currentTool.tags"
:key="tag.text"
class="tag"
:class="tag.class"
>
{{ tag.text }}
</span>
</div>
</div>
<div class="tool-description">
<p>{{ currentTool.description }}</p>
</div>
<div class="feature-section">
<h4>核心特性</h4>
<div class="feature-list">
<div
v-for="(feature, idx) in currentTool.features"
:key="idx"
class="feature-item"
>
<span class="feature-icon"></span>
<span class="feature-text">{{ feature }}</span>
</div>
</div>
</div>
<div class="example-section">
<h4>文档示例OpenAPI 3.0</h4>
<div class="code-block">
<pre><code>{{ currentTool.example }}</code></pre>
</div>
</div>
<div class="tools-section">
<h4>🔧 推荐工具</h4>
<div class="tools-grid">
<div
v-for="(rec, idx) in currentTool.recommendations"
:key="idx"
class="tool-card"
>
<div class="rec-name">
{{ rec.name }}
</div>
<div class="rec-desc">
{{ rec.description }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const tools = [
{
id: 'openapi',
name: 'OpenAPI 规范',
icon: '📋',
tags: [
{ text: '行业标准', class: 'primary' },
{ text: '语言无关', class: 'secondary' }
],
description: 'OpenAPI Specification(原 Swagger)是描述 REST API 的标准格式,可以被工具解析生成交互式文档、客户端 SDK、服务器存根等。',
features: [
'标准化的 YAML/JSON 格式描述 API',
'支持路径、参数、响应模型、认证等完整定义',
'生态系统丰富,支持 100+ 工具',
'可以生成交互式文档(Swagger UI',
'可以从代码注释自动生成',
'支持 API 版本控制和演进'
],
example: `openapi: 3.0.0
info:
title: 用户服务 API
version: 1.0.0
description: 提供用户管理相关接口
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: 获取用户列表
parameters:
- name: page
in: query
schema:
type: integer
default: 1
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
code:
type: integer
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email`,
recommendations: [
{ name: 'Swagger UI', description: '最流行的交互式文档界面' },
{ name: 'Redoc', description: '美观的现代文档生成器' },
{ name: 'Stoplight', description: '可视化的 API 设计平台' }
]
},
{
id: 'swagger',
name: 'Swagger 工具链',
icon: '🛠️',
tags: [
{ text: '工具集', class: 'success' },
{ text: '自动化', class: 'info' }
],
description: 'Swagger 是一套围绕 OpenAPI 规范构建的工具,包括编辑器、UI、代码生成器等,帮助开发者快速构建和使用 API。',
features: [
'Swagger Editor:在线编写和验证 OpenAPI 文档',
'Swagger UI:自动生成交互式文档',
'Swagger Codegen:根据文档生成客户端 SDK',
'支持主流编程语言和框架',
'集成到 CI/CD 流程',
'自动保持文档与代码同步'
],
example: `# Swagger Editor 示例配置
swagger: '2.0'
info:
title: 示例 API
version: '1.0.0'
host: api.example.com
basePath: /v1
schemes:
- https
paths:
/users:
get:
tags:
- Users
summary: 获取所有用户
produces:
- application/json
responses:
200:
description: 成功
schema:
type: object
properties:
code:
type: integer
data:
type: array`,
recommendations: [
{ name: 'Swagger Editor', description: '在线编辑器,实时预览' },
{ name: 'Swagger Codegen', description: '生成 40+ 种语言的客户端' },
{ name: 'Postman', description: '导入 OpenAPI 进行测试' }
]
},
{
id: 'best-practices',
name: '文档最佳实践',
icon: '⭐',
tags: [
{ text: '经验', class: 'warning' },
{ text: '规范', class: 'secondary' }
],
description: '好的 API 文档应该像用户手册一样清晰,让开发者不问问题就能完成集成。',
features: [
'每个接口都有完整的请求示例',
'提供多种语言的代码示例(curl、JavaScript、Python',
'错误码文档化,附带解决方案',
'提供沙箱环境或测试工具',
'包含认证流程和获取 Token 的方法',
'实时更新,与代码保持一致',
'版本变更日志和迁移指南'
],
example: `# 完整的接口文档示例
## 获取用户信息
**请求示例:**
\`\`\`bash
curl -X GET \\
https://api.example.com/v1/users/123 \\
-H "Authorization: Bearer YOUR_TOKEN"
\`\`\`
**成功响应:**
\`\`\`json
{
"code": 0,
"message": "success",
"data": {
"id": 123,
"name": "张三",
"email": "zhangsan@example.com"
}
}
\`\`\`
**错误响应:**
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| 10010 | 用户不存在 | 检查 user_id 是否正确 |
| 10018 | Token 已过期 | 重新调用登录接口 |
**在线测试:**
[🚀 在 API Explorer 中测试](https://api.example.com/docs)`,
recommendations: [
{ name: 'API Blueprint', description: ' Markdown 风格的 API 文档' },
{ name: 'Docusaurus', description: ' Facebook 开源的文档平台' },
{ name: 'GitBook', description: '美观的文档托管平台' }
]
}
]
const selectedTool = ref('openapi')
const currentTool = computed(() =>
tools.find(t => t.id === selectedTool.value)
)
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.tools-tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tool-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s ease;
}
.tool-btn:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
transform: translateY(-2px);
}
.tool-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.tool-icon {
font-size: 18px;
}
.tool-name {
font-weight: 600;
font-size: 14px;
}
.tool-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.tool-header {
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
}
.tool-title {
font-weight: 700;
font-size: 16px;
color: var(--vp-c-text-1);
}
.tool-tags {
display: flex;
gap: 8px;
}
.tag {
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
}
.tag.primary {
background: #dbeafe;
color: #1d4ed8;
}
.tag.secondary {
background: #e0e7ff;
color: #4338ca;
}
.tag.success {
background: #dcfce7;
color: #16a34a;
}
.tag.info {
background: #ccfbf1;
color: #0f766e;
}
.tag.warning {
background: #fef3c7;
color: #d97706;
}
.tool-description {
padding: 16px;
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.7;
border-bottom: 1px solid var(--vp-c-divider);
}
.feature-section, .example-section, .tools-section {
padding: 16px;
}
.feature-section h4, .example-section h4, .tools-section h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.feature-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.feature-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.feature-icon {
color: #22c55e;
font-weight: 700;
font-size: 16px;
flex-shrink: 0;
}
.feature-text {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.code-block {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-block pre {
margin: 0;
padding: 16px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.code-block code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.tools-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.tool-card {
padding: 12px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
text-align: center;
}
.rec-name {
font-weight: 600;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.rec-desc {
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
@media (max-width: 768px) {
.tools-tabs {
flex-direction: column;
}
.feature-list {
grid-template-columns: 1fr;
}
.tools-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,389 +1,370 @@
<!--
ErrorHandlingDemo.vue - 错误处理演示组件
展示错误处理的正确和错误示例对比
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">🚨</span>
<span class="title">错误处理优雅地"拒绝"</span>
</div>
<div class="content">
<div class="comparison-tabs">
<button
class="tab-btn bad"
:class="{ active: selectedTab === 'bad' }"
@click="selectedTab = 'bad'"
>
错误示范
</button>
<button
class="tab-btn good"
:class="{ active: selectedTab === 'good' }"
@click="selectedTab = 'good'"
>
正确示范
</button>
<div class="eh-root">
<div class="eh-terminal">
<div class="term-bar">
<span class="dot r" /><span class="dot y" /><span class="dot g" />
<span class="term-title">错误处理演示</span>
</div>
<!-- 错误示范 -->
<div
v-if="selectedTab === 'bad'"
class="comparison bad"
>
<div class="response-preview">
<div class="status-line bad">
<span>HTTP/1.1</span>
<span class="code">200 OK</span>
</div>
<div class="response-body">
<pre><code>{
"error": "出错了"
}</code></pre>
</div>
<div ref="termEl" class="term-body">
<div v-for="(l, i) in lines" :key="i" class="t-line">
<span v-if="l.kind === 'cmd'" class="t-ps">&gt; </span>
<span :class="'t-' + l.kind">{{ l.text }}</span>
</div>
<div class="problems">
<h4>问题分析</h4>
<ul>
<li>
<span class="icon"></span>
HTTP 状态码说"成功"但业务说"出错" - 前后端状态不一致
</li>
<li>
<span class="icon"></span>
错误信息太笼统无法定位问题
</li>
<li>
<span class="icon"></span>
没有错误代码难以程序化判断
</li>
<li>
<span class="icon"></span>
浏览器和 CDN 会缓存这个"成功的"响应
</li>
</ul>
</div>
</div>
<!-- 正确示范 -->
<div
v-if="selectedTab === 'good'"
class="comparison good"
>
<div class="response-preview">
<div class="status-line">
<span>HTTP/1.1</span>
<span class="code">422 Unprocessable Entity</span>
</div>
<div class="response-body">
<pre><code>{{ JSON.stringify(goodResponse, null, 2) }}</code></pre>
</div>
</div>
<div class="highlights">
<h4>正确做法</h4>
<ul>
<li>
<span class="icon"></span>
<strong>正确的 HTTP 状态码</strong>: 422 表示语义错误
</li>
<li>
<span class="icon"></span>
<strong>业务错误码</strong>: `code: 20003` 可用于程序判断
</li>
<li>
<span class="icon"></span>
<strong>详细错误信息</strong>: `errors` 数组包含具体字段和原因
</li>
<li>
<span class="icon"></span>
<strong>可追踪性</strong>: `request_id` 用于日志查询
</li>
<li>
<span class="icon"></span>
<strong>帮助链接</strong>: `help_url` 引导用户查看文档
</li>
</ul>
</div>
<div class="error-codes">
<h4>错误码体系</h4>
<div class="code-list">
<div
v-for="item in errorCodeItems"
:key="item.code"
class="code-item"
>
<span class="code-badge">{{ item.code }}</span>
<span class="code-desc">{{ item.desc }}</span>
</div>
</div>
<div class="t-line">
<span class="t-ps">&gt; </span>
<span class="t-typing">{{ typing }}<span class="t-cur"></span></span>
</div>
</div>
</div>
<div class="eh-btns">
<button
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['eh-btn', { 'eh-btn--on': active === op.id, 'eh-btn--dim': !op.ok() }]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="eh-btn eh-btn--reset" :disabled="running" @click="reset">重置</button>
</div>
<div class="eh-response">
<div class="res-header">
<span class="res-label">响应结构</span>
<span class="res-status" :class="responseStatus">{{ responseStatus }}</span>
</div>
<div class="res-body">
<pre v-if="responseData">{{ responseData }}</pre>
<div v-else class="res-empty">点击上方按钮查看错误响应示例</div>
</div>
</div>
<div v-if="hint" class="eh-hint">💡 {{ hint }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, nextTick } from 'vue'
const selectedTab = ref('bad')
const termEl = ref(null)
const lines = ref([{ kind: 'dim', text: '// 对比好的和差的错误处理方式' }])
const typing = ref('')
const running = ref(false)
const active = ref(null)
const hint = ref('点击按钮,对比"好的"和"差的"错误响应设计。')
const responseData = ref('')
const responseStatus = ref('')
const goodResponse = {
code: 20003,
message: '密码强度不足',
errors: [
const sleep = ms => new Promise(r => setTimeout(r, ms))
const ops = [
{
id: 'bad1',
cmd: '❌ 差: 所有错误都 200',
ok: () => true,
output: [
{ kind: 'dim', text: '// HTTP 200 但业务失败' },
{ kind: 'yel', text: 'HTTP/1.1 200 OK' },
{ kind: 'dim', text: '' },
{ kind: 'yel', text: '{ "error": "出错了" }' },
],
hint: '问题:HTTP 状态码说"成功",但业务说"出错"。缓存层会缓存这个"成功"响应,监控系统也发现不了问题。',
do: () => {
responseStatus.value = '200 (错误)'
responseData.value = `{
"error": "出错了"
}`
}
},
{
id: 'bad2',
cmd: '❌ 差: 错误信息太笼统',
ok: () => true,
output: [
{ kind: 'dim', text: '// 错误信息没有帮助' },
{ kind: 'red', text: 'HTTP/1.1 400 Bad Request' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "message": "参数错误" }' },
],
hint: '问题:客户端不知道哪个参数错了、为什么错。用户只能看到"参数错误",无法修正。',
do: () => {
responseStatus.value = '400'
responseData.value = `{
"message": "参数错误"
}`
}
},
{
id: 'bad3',
cmd: '❌ 差: 暴露敏感信息',
ok: () => true,
output: [
{ kind: 'dim', text: '// 500 错误暴露堆栈' },
{ kind: 'red', text: 'HTTP/1.1 500 Internal Server Error' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "error": "TypeError: Cannot read property..." }' },
{ kind: 'red', text: '{ "stack": "at UserService.login..." }' },
{ kind: 'red', text: '{ "sql": "SELECT * FROM users WHERE..." }' },
],
hint: '危险!暴露了代码结构、数据库查询。攻击者可以利用这些信息进行攻击。',
do: () => {
responseStatus.value = '500'
responseData.value = `{
"error": "TypeError: Cannot read property 'id' of undefined",
"stack": "at UserService.login (src/service.js:45)",
"sql": "SELECT * FROM users WHERE email='...'"
}`
}
},
{
id: 'good1',
cmd: '✅ 好: 正确的状态码',
ok: () => true,
output: [
{ kind: 'dim', text: '// HTTP 状态码准确表达错误类型' },
{ kind: 'grn', text: 'HTTP/1.1 404 Not Found' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 10002, "message": "用户不存在" }' },
],
hint: '正确!404 表示资源不存在,客户端一看就知道问题所在。',
do: () => {
responseStatus.value = '404'
responseData.value = `{
"code": 10002,
"message": "用户不存在",
"request_id": "req-550e8400"
}`
}
},
{
id: 'good2',
cmd: '✅ 好: 详细的错误信息',
ok: () => true,
output: [
{ kind: 'dim', text: '// 错误信息帮助定位问题' },
{ kind: 'grn', text: 'HTTP/1.1 422 Unprocessable Entity' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 20003, "message": "密码强度不足" }' },
{ kind: 'grn', text: '{ "errors": [{ "field": "password", ... }] }' },
],
hint: '正确!提供了错误码、字段级别的错误详情,前端可以精确提示用户。',
do: () => {
responseStatus.value = '422'
responseData.value = `{
"code": 20003,
"message": "密码强度不足",
"errors": [
{
field: 'password',
code: 'VALIDATION_ERROR',
message: '密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位'
"field": "password",
"code": "VALIDATION_ERROR",
"message": "密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字"
}
],
request_id: 'req-550e8400-e29b-41d4-a716-44665544000',
timestamp: '2024-01-15T09:30:00.000Z',
help_url: 'https://docs.example.com/errors/20003'
"request_id": "req-550e8400"
}`
}
},
{
id: 'good3',
cmd: '✅ 好: 安全的错误响应',
ok: () => true,
output: [
{ kind: 'dim', text: '// 500 只返回错误 ID' },
{ kind: 'grn', text: 'HTTP/1.1 500 Internal Server Error' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 10000, "message": "服务器错误" }' },
{ kind: 'grn', text: '{ "error_id": "err-a1b2c3d4" }' },
],
hint: '正确!只返回错误 ID,详细日志记录在服务器。用户反馈错误 ID,技术人员可以快速定位。',
do: () => {
responseStatus.value = '500'
responseData.value = `{
"code": 10000,
"message": "服务器内部错误,请联系管理员",
"error_id": "err-a1b2c3d4",
"request_id": "req-550e8400",
"help_url": "https://docs.example.com/errors/10000"
}`
}
},
]
async function run(op) {
if (running.value) return
running.value = true
active.value = op.id
hint.value = ''
typing.value = ''
responseData.value = ''
responseStatus.value = ''
for (const ch of op.cmd) {
typing.value += ch
await sleep(15)
}
await sleep(80)
lines.value.push({ kind: 'cmd', text: op.cmd })
typing.value = ''
await nextTick()
scroll()
await sleep(150)
for (const l of op.output) {
lines.value.push(l)
await nextTick()
scroll()
await sleep(50)
}
op.do()
await sleep(120)
hint.value = op.hint
running.value = false
}
const errorCodeItems = [
{ code: '1XXYY', desc: '通用错误(第1位固定为1' },
{ code: '10001', desc: '参数错误' },
{ code: '10010', desc: '用户不存在' },
{ code: '10018', desc: 'Token 已过期' },
{ code: '10021', desc: '权限不足' },
{ code: '20003', desc: '密码强度不足' },
{ code: '20014', desc: '余额不足' }
]
function scroll() {
if (termEl.value) termEl.value.scrollTop = termEl.value.scrollHeight
}
function reset() {
lines.value = [{ kind: 'dim', text: '// 对比好的和差的错误处理方式' }]
active.value = null
hint.value = '点击按钮,对比"好的"和"差的"错误响应设计。'
typing.value = ''
running.value = false
responseData.value = ''
responseStatus.value = ''
}
</script>
<style scoped>
.demo {
.eh-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
.eh-terminal { background: #141420; }
.term-bar {
display: flex;
align-items: center;
gap: 12px;
gap: 5px;
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.comparison-tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.tab-btn {
flex: 1;
padding: 12px;
border: 2px solid;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-btn.bad {
border-color: #ef4444;
background: var(--vp-c-bg);
color: #ef4444;
}
.tab-btn.bad:hover {
background: #fef2f2;
}
.tab-btn.bad.active {
background: #ef4444;
color: white;
}
.tab-btn.good {
border-color: #22c55e;
background: var(--vp-c-bg);
color: #22c55e;
}
.tab-btn.good:hover {
background: #f0fdf4;
}
.tab-btn.good.active {
background: #22c55e;
color: white;
}
.comparison {
background: var(--vp-c-bg);
border-radius: 6px;
overflow: hidden;
}
.response-preview {
margin-bottom: 20px;
}
.status-line {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
font-family: monospace;
font-size: 13px;
}
.status-line.bad .code {
color: #ef4444;
font-weight: 700;
}
.status-line:not(.bad) .code {
color: #d97706;
font-weight: 700;
}
.response-body {
padding: 16px;
}
.response-body pre {
margin: 0;
background: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
.term-body {
min-height: 100px;
max-height: 160px;
overflow-y: auto;
overflow-x: auto;
}
.response-body code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.problems, .highlights {
padding: 16px;
}
.problems h4, .highlights h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.problems ul, .highlights ul {
margin: 0;
padding-left: 0;
list-style: none;
}
.problems li, .highlights li {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px;
margin-bottom: 8px;
border-radius: 6px;
padding: 0.7rem 1rem;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.76rem;
line-height: 1.6;
font-size: 13px;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #89b4fa; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-yel { color: #f9e2af; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.problems li {
background: #fef2f2;
color: #991b1b;
}
.highlights li {
background: #f0fdf4;
color: #166534;
}
.problems li .icon, .highlights li .icon {
font-size: 16px;
flex-shrink: 0;
}
.problems li strong, .highlights li strong {
font-weight: 600;
}
.error-codes {
padding: 16px;
border-top: 1px solid var(--vp-c-divider);
}
.error-codes h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.code-list {
.eh-btns {
display: flex;
flex-direction: column;
gap: 8px;
flex-wrap: wrap;
gap: 6px;
padding: 8px 10px;
background: #0d0d1a;
border-top: 1px solid #2a2a3e;
}
.eh-btn {
background: #1e1e2e;
border: 1px solid #313244;
border-radius: 5px;
padding: 4px 9px;
cursor: pointer;
transition: border-color 0.2s;
}
.eh-btn code { font-size: 0.68rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.eh-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.eh-btn--on { border-color: var(--vp-c-brand) !important; }
.eh-btn--on code { color: var(--vp-c-brand); }
.eh-btn--dim { opacity: 0.3; cursor: not-allowed; }
.eh-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.eh-btn--reset code { display: none; }
.eh-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.code-item {
.eh-response {
border-top: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
.res-header {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
}
.code-badge {
.res-label { font-weight: 700; font-size: 0.8rem; color: var(--vp-c-text-1); }
.res-status {
font-family: monospace;
font-size: 12px;
font-size: 0.72rem;
font-weight: 700;
padding: 4px 8px;
background: var(--vp-c-brand);
color: white;
padding: 2px 8px;
border-radius: 4px;
min-width: 70px;
text-align: center;
}
.res-status\.200\ \(错误\) { background: #f9e2af22; color: #d97706; }
.res-status\.400 { background: #f59e0b22; color: #d97706; }
.res-status\.404 { background: #3b82f622; color: #3b82f6; }
.res-status\.422 { background: #8b5cf622; color: #8b5cf6; }
.res-status\.500 { background: #ef444422; color: #ef4444; }
.res-body {
padding: 12px;
min-height: 80px;
}
.res-body pre {
margin: 0;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.72rem;
line-height: 1.5;
color: var(--vp-c-text-1);
white-space: pre-wrap;
word-break: break-all;
}
.res-empty {
color: var(--vp-c-text-3);
font-style: italic;
font-size: 0.8rem;
}
.code-desc {
font-size: 13px;
.eh-hint {
padding: 10px 12px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.comparison-tabs {
flex-direction: column;
}
.status-line {
flex-wrap: wrap;
}
line-height: 1.5;
}
</style>
@@ -1,428 +0,0 @@
<!--
HttpMethodsDemo.vue - HTTP 方法对比演示组件
展示 GET/POST/PUT/PATCH/DELETE 的区别和使用场景
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">🎯</span>
<span class="title">HTTP 方法用正确的姿势操作资源</span>
</div>
<div class="content">
<!-- HTTP 方法选择器 -->
<div class="method-selector">
<button
v-for="method in methods"
:key="method.name"
class="method-btn"
:class="[method.name, { active: selectedMethod === method.name }]"
@click="selectedMethod = method.name"
>
{{ method.name }}
</button>
</div>
<!-- 当前方法详情 -->
<div v-if="currentMethod" class="method-detail">
<div class="detail-header">
<span class="http-badge" :class="currentMethod.name">{{ currentMethod.name }}</span>
<span class="method-desc">{{ currentMethod.description }}</span>
</div>
<div class="properties">
<div class="property" :class="{ yes: currentMethod.idempotent }">
<span class="prop-icon">{{ currentMethod.idempotent ? '✅' : '❌' }}</span>
<span class="prop-label">幂等性</span>
<span class="prop-hint">{{ currentMethod.idempotent ? '多次执行结果相同' : '每次执行可能产生不同结果' }}</span>
</div>
<div class="property" :class="{ yes: currentMethod.safe }">
<span class="prop-icon">{{ currentMethod.safe ? '✅' : '❌' }}</span>
<span class="prop-label">安全性</span>
<span class="prop-hint">{{ currentMethod.safe ? '不修改服务器状态' : '可能会修改服务器状态' }}</span>
</div>
<div class="property has-body">
<span class="prop-icon">{{ currentMethod.hasBody ? '✅' : '❌' }}</span>
<span class="prop-label">请求体</span>
<span class="prop-hint">{{ currentMethod.hasBody ? '可以携带请求体数据' : '通常不携带请求体' }}</span>
</div>
</div>
<div class="example-section">
<div class="example-title">📝 使用示例</div>
<div class="example-content">
<div v-for="(example, idx) in currentMethod.examples" :key="idx" class="example-item">
<div class="example-scenario">{{ example.scenario }}</div>
<div class="example-request">
<span class="http-method" :class="currentMethod.name">{{ currentMethod.name }}</span>
<span class="request-url">{{ example.url }}</span>
</div>
<div v-if="example.body" class="example-body">
<pre><code>{{ JSON.stringify(example.body, null, 2) }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const methods = [
{
name: 'GET',
description: '获取资源',
idempotent: true,
safe: true,
hasBody: false,
examples: [
{
scenario: '获取用户列表',
url: '/api/v1/users?page=1&page_size=20'
},
{
scenario: '获取单个用户详情',
url: '/api/v1/users/123'
}
]
},
{
name: 'POST',
description: '创建资源',
idempotent: false,
safe: false,
hasBody: true,
examples: [
{
scenario: '创建新用户',
url: '/api/v1/users',
body: {
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000'
}
},
{
scenario: '提交订单',
url: '/api/v1/orders',
body: {
user_id: 123,
items: [
{ product_id: 'P001', quantity: 2 },
{ product_id: 'P002', quantity: 1 }
],
shipping_address: { /* ... */ }
}
}
]
},
{
name: 'PUT',
description: '全量更新资源',
idempotent: true,
safe: false,
hasBody: true,
examples: [
{
scenario: '更新用户全部信息(替换)',
url: '/api/v1/users/123',
body: {
id: 123,
name: '张三(已更新)',
email: 'zhangsan_new@example.com',
phone: '13900139000',
avatar: 'https://...',
status: 'active'
}
}
]
},
{
name: 'PATCH',
description: '部分更新资源',
idempotent: false,
safe: false,
hasBody: true,
examples: [
{
scenario: '仅修改用户邮箱',
url: '/api/v1/users/123',
body: {
email: 'newemail@example.com'
}
},
{
scenario: '更新多个字段',
url: '/api/v1/users/123',
body: {
phone: '13800138000',
avatar: 'https://...'
}
}
]
},
{
name: 'DELETE',
description: '删除资源',
idempotent: true,
safe: false,
hasBody: false,
examples: [
{
scenario: '删除单个用户',
url: '/api/v1/users/123'
},
{
scenario: '批量删除(通过查询参数)',
url: '/api/v1/users?ids=1,2,3,4,5'
}
]
}
]
const selectedMethod = ref('GET')
const currentMethod = computed(() =>
methods.find(m => m.name === selectedMethod.value)
)
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.method-selector {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--vp-c-divider);
}
.method-btn {
padding: 8px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.method-btn:hover {
transform: translateY(-1px);
}
.method-btn.active {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* HTTP 方法颜色 */
.method-btn.GET, .http-method.GET { border-color: #22c55e; color: #16a34a; }
.method-btn.GET.active, .http-badge.GET { background: #22c55e; color: white; }
.method-btn.POST, .http-method.POST { border-color: #3b82f6; color: #2563eb; }
.method-btn.POST.active, .http-badge.POST { background: #3b82f6; color: white; }
.method-btn.PUT, .http-method.PUT { border-color: #f59e0b; color: #d97706; }
.method-btn.PUT.active, .http-badge.PUT { background: #f59e0b; color: white; }
.method-btn.PATCH, .http-method.PATCH { border-color: #8b5cf6; color: #7c3aed; }
.method-btn.PATCH.active, .http-badge.PATCH { background: #8b5cf6; color: white; }
.method-btn.DELETE, .http-method.DELETE { border-color: #ef4444; color: #dc2626; }
.method-btn.DELETE.active, .http-badge.DELETE { background: #ef4444; color: white; }
.method-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.http-badge {
padding: 6px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
}
.method-desc {
font-size: 15px;
color: var(--vp-c-text-2);
}
.properties {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.property {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
opacity: 0.6;
transition: all 0.2s ease;
}
.property.yes {
opacity: 1;
border-color: #22c55e;
background: #f0fdf4;
}
.property.has-body {
opacity: 1;
}
.property.has-body:not(.yes) {
border-color: #f59e0b;
background: #fffbeb;
}
.prop-icon {
font-size: 20px;
margin-bottom: 4px;
}
.prop-label {
font-weight: 600;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 2px;
}
.prop-hint {
font-size: 11px;
color: var(--vp-c-text-3);
text-align: center;
}
.example-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--vp-c-divider);
}
.example-title {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.example-item {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
}
.example-item:last-child {
margin-bottom: 0;
}
.example-scenario {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.example-request {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.http-method {
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 11px;
background: var(--vp-c-bg);
}
.request-url {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-text-1);
}
.example-body {
background: var(--vp-c-bg);
border-radius: 4px;
padding: 8px;
}
.example-body pre {
margin: 0;
font-size: 11px;
line-height: 1.4;
overflow-x: auto;
}
.example-body code {
font-family: monospace;
color: var(--vp-c-text-1);
}
@media (max-width: 640px) {
.properties {
grid-template-columns: 1fr;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -1,157 +0,0 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>HTTP 请求结构解析</h4>
<p class="hint">
详解 HTTP 请求的组成部分
</p>
</div>
<div class="demo-content">
<div class="structure-box">
<div class="section request-line">
<div class="label">
请求行
</div>
<div class="content">
<code>GET /api/users/123 HTTP/1.1</code>
</div>
<div class="explain">
<span>方法</span> + <span>路径</span> + <span>协议版本</span>
</div>
</div>
<div class="section headers">
<div class="label">
请求头
</div>
<div class="content header-list">
<div><code>Host: api.example.com</code></div>
<div><code>Content-Type: application/json</code></div>
<div><code>Authorization: Bearer token123</code></div>
</div>
<div class="explain">
元信息域名数据格式认证等
</div>
</div>
<div class="section body">
<div class="label">
请求体 (可选)
</div>
<div class="content">
<pre>{
"name": "张三",
"email": "zhangsan@example.com"
}</pre>
</div>
<div class="explain">
POST/PUT 请求携带的数据
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// 纯静态展示组件
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.structure-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 16px;
border-left: 4px solid var(--vp-c-brand);
}
.section.headers {
border-left-color: #67c23a;
}
.section.body {
border-left-color: #e6a23c;
}
.label {
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
font-size: 14px;
}
.content {
background: #1e1e1e;
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
overflow-x: auto;
}
.content code {
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 13px;
}
.content pre {
margin: 0;
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 13px;
line-height: 1.5;
}
.header-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.explain {
font-size: 12px;
color: var(--vp-c-text-2);
}
.explain span {
display: inline-block;
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
margin: 0 2px;
}
</style>
@@ -1,111 +0,0 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>RESTful 资源类比</h4>
<p class="hint">
通过生活中的类比理解 RESTful 资源概念
</p>
</div>
<div class="demo-content">
<div class="analogy-box">
<div class="analogy-item">
<div class="icon">
📚
</div>
<div class="text">
<strong>资源 = 图书</strong>
<p>每本书有唯一的 ISBN资源标识</p>
</div>
</div>
<div class="analogy-item">
<div class="icon">
🏪
</div>
<div class="text">
<strong>URL = 书架位置</strong>
<p>/library/books/123 表示第 123 号书</p>
</div>
</div>
<div class="analogy-item">
<div class="icon">
📝
</div>
<div class="text">
<strong>HTTP 方法 = 操作</strong>
<p>GET(查看)POST(借书)PUT(修改)DELETE(还书)</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// 静态组件,无需逻辑
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.analogy-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.analogy-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.icon {
font-size: 24px;
flex-shrink: 0;
}
.text {
flex: 1;
}
.text strong {
display: block;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.text p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
}
</style>
@@ -1,406 +0,0 @@
<!--
ResponseStructureDemo.vue - HTTP 响应结构演示组件
展示标准化 API 响应结构和分页响应
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">📦</span>
<span class="title">HTTP 响应结构标准化的数据契约</span>
</div>
<div class="content">
<div class="response-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-btn"
:class="{ active: selectedTab === tab.id }"
@click="selectedTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="response-detail">
<div class="response-header">
<div class="status-line">
<span class="http-version">HTTP/1.1</span>
<span
class="status-code"
:class="getStatusClass(currentResponse.status)"
>{{ currentResponse.status }}</span>
<span class="status-text">{{ currentResponse.statusText }}</span>
</div>
</div>
<div class="response-headers">
<div class="header-item">
<span class="header-key">Content-Type:</span>
<span class="header-value">application/json</span>
</div>
<div class="header-item">
<span class="header-key">X-Request-ID:</span>
<span class="header-value">req-550e8400-e29b-41d4</span>
</div>
<div class="header-item">
<span class="header-key">X-Response-Time:</span>
<span class="header-value">45ms</span>
</div>
</div>
<div class="response-body">
<pre><code>{{ JSON.stringify(currentResponse.body, null, 2) }}</code></pre>
</div>
<div class="field-descriptions">
<h4>字段说明</h4>
<div class="field-list">
<div
v-for="field in currentResponse.fields"
:key="field.name"
class="field-item"
>
<div class="field-name">
<code>{{ field.name }}</code>
<span class="field-type">{{ field.type }}</span>
</div>
<div class="field-desc">
{{ field.description }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const tabs = [
{ id: 'success', name: '成功响应' },
{ id: 'pagination', name: '分页响应' },
{ id: 'error', name: '错误响应' }
]
const responses = {
success: {
status: 200,
statusText: 'OK',
body: {
code: 0,
message: 'success',
data: {
id: 123,
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000',
created_at: '2024-01-15T10:30:00.000Z'
},
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z'
},
fields: [
{ name: 'code', type: 'integer', description: '业务状态码,0 表示成功' },
{ name: 'message', type: 'string', description: '状态描述,成功时为 "success"' },
{ name: 'data', type: 'object', description: '业务数据,成功时返回具体数据' },
{ name: 'request_id', type: 'string', description: '请求唯一标识,用于问题追踪' },
{ name: 'timestamp', type: 'string', description: '响应时间戳,ISO 8601 格式' }
]
},
pagination: {
status: 200,
statusText: 'OK',
body: {
code: 0,
message: 'success',
data: {
list: [
{ id: 1, name: '商品A', price: 100 },
{ id: 2, name: '商品B', price: 200 }
],
pagination: {
page: 1,
page_size: 20,
total: 156,
total_pages: 8,
has_next: true,
has_prev: false
}
},
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z'
},
fields: [
{ name: 'list', type: 'array', description: '数据列表' },
{ name: 'pagination', type: 'object', description: '分页信息对象' },
{ name: 'page', type: 'integer', description: '当前页码' },
{ name: 'page_size', type: 'integer', description: '每页数量' },
{ name: 'total', type: 'integer', description: '总记录数' },
{ name: 'total_pages', type: 'integer', description: '总页数' },
{ name: 'has_next', type: 'boolean', description: '是否有下一页' },
{ name: 'has_prev', type: 'boolean', description: '是否有上一页' }
]
},
error: {
status: 422,
statusText: 'Unprocessable Entity',
body: {
code: 20003,
message: '密码强度不足',
errors: [
{
field: 'password',
code: 'VALIDATION_ERROR',
message: '密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位'
}
],
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z',
help_url: 'https://docs.example.com/errors/20003'
},
fields: [
{ name: 'code', type: 'integer', description: '错误码,非 0 表示失败' },
{ name: 'message', type: 'string', description: '错误描述,供用户阅读' },
{ name: 'errors', type: 'array', description: '详细错误信息数组(可选)' },
{ name: 'help_url', type: 'string', description: '错误文档链接(可选)' }
]
}
}
const selectedTab = ref('success')
const currentResponse = computed(() => responses[selectedTab.value])
function getStatusClass(status) {
const prefix = Math.floor(status / 100)
switch (prefix) {
case 2: return 'success'
case 3: return 'redirect'
case 4: return 'client-error'
case 5: return 'server-error'
default: return ''
}
}
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.response-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.tab-btn {
padding: 8px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-btn:hover {
transform: translateY(-1px);
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
transform: scale(1.05);
}
.response-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.response-header {
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.status-line {
display: flex;
align-items: center;
gap: 12px;
}
.http-version {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-text-2);
}
.status-code {
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 13px;
font-family: monospace;
}
.status-code.success {
background: #dcfce7;
color: #16a34a;
}
.status-code.client-error {
background: #fef3c7;
color: #d97706;
}
.status-text {
font-size: 13px;
color: var(--vp-c-text-2);
}
.response-headers {
padding: 12px 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.header-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
font-size: 12px;
}
.header-key {
font-family: monospace;
font-weight: 600;
color: var(--vp-c-text-2);
min-width: 120px;
}
.header-value {
font-family: monospace;
color: var(--vp-c-text-1);
}
.response-body {
padding: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.response-body pre {
margin: 0;
background: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.response-body code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.field-descriptions {
padding: 16px;
}
.field-descriptions h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.field-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.field-item {
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.field-name {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.field-name code {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-brand);
font-weight: 600;
}
.field-type {
font-size: 11px;
padding: 2px 6px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
color: var(--vp-c-brand);
border-radius: 4px;
font-family: monospace;
}
.field-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@media (max-width: 640px) {
.response-tabs {
flex-direction: column;
}
.status-line {
flex-wrap: wrap;
}
.header-item {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -1,417 +0,0 @@
<template>
<div class="raf-root">
<div class="raf-layout">
<!-- Left: Client Side -->
<div class="raf-left">
<div class="raf-header">
<span class="raf-icon">💻</span>
<span class="raf-title">Client (Browser/App)</span>
</div>
<div class="raf-controls">
<div class="raf-scenarios">
<button
v-for="s in scenarios"
:key="s.id"
:class="['raf-chip', { active: currentScenario.id === s.id }]"
@click="selectScenario(s)"
:disabled="processing"
>
{{ s.label }}
</button>
</div>
</div>
<div class="raf-request-box">
<div class="raf-http-line">
<span :class="['raf-method', currentScenario.method]">{{ currentScenario.method }}</span>
<span class="raf-url">{{ currentScenario.url }}</span>
</div>
<div v-if="currentScenario.body" class="raf-code-block">
{{ JSON.stringify(currentScenario.body, null, 2) }}
</div>
<button
class="raf-send-btn"
@click="sendRequest"
:disabled="processing"
>
{{ processing ? 'Sending...' : 'Send Request' }}
</button>
</div>
<div class="raf-response-box" v-if="response">
<div class="raf-status-line">
<span class="raf-label">Response Status:</span>
<span :class="['raf-status-badge', getStatusColor(response.status)]">
{{ response.status }} {{ response.statusText }}
</span>
</div>
<div class="raf-code-block response-body">
{{ JSON.stringify(response.body, null, 2) }}
</div>
</div>
</div>
<!-- Right: Server Side -->
<div class="raf-right">
<div class="raf-header server-header">
<span class="raf-icon"></span>
<span class="raf-title">Server (API)</span>
</div>
<div class="raf-server-state">
<!-- Database View -->
<div class="raf-section">
<div class="raf-section-title">📦 Database (Users Resource)</div>
<div class="raf-db-view">
<transition-group name="list">
<div v-for="user in db" :key="user.id" class="raf-db-item">
<span class="raf-db-id">ID: {{ user.id }}</span>
<span class="raf-db-name">{{ user.name }}</span>
<span class="raf-db-role">({{ user.role }})</span>
</div>
</transition-group>
<div v-if="db.length === 0" class="raf-empty">No users found</div>
</div>
</div>
<!-- Logs -->
<div class="raf-section">
<div class="raf-section-title">📜 Server Logs</div>
<div class="raf-logs" ref="logsRef">
<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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, nextTick } from 'vue'
const processing = ref(false)
const response = ref(null)
const logs = ref([])
const logsRef = ref(null)
const db = ref([
{ id: 1, name: "Alice", role: "admin" },
{ id: 2, name: "Bob", role: "user" }
])
const scenarios = [
{ id: 'get-all', label: 'GET /users', method: 'GET', url: '/api/users', body: null },
{ id: 'get-one', label: 'GET /users/1', method: 'GET', url: '/api/users/1', body: null },
{ id: 'create', label: 'POST /users', method: 'POST', url: '/api/users', body: { name: "Charlie", role: "user" } },
{ id: 'not-found', label: 'GET /users/99', method: 'GET', url: '/api/users/99', body: null },
{ id: 'delete', label: 'DELETE /users/1', method: 'DELETE', url: '/api/users/1', body: null },
]
const currentScenario = ref(scenarios[0])
function selectScenario(s) {
currentScenario.value = s
response.value = null
}
function addLog(msg, type = 'info') {
const now = new Date()
const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`
logs.value.push({ time, msg, type })
nextTick(() => {
if (logsRef.value) logsRef.value.scrollTop = logsRef.value.scrollHeight
})
}
function getStatusColor(status) {
if (status >= 200 && status < 300) return 'status-success'
if (status >= 400 && status < 500) return 'status-error'
return 'status-neutral'
}
async function sendRequest() {
processing.value = true
response.value = null
addLog(`Received ${currentScenario.value.method} ${currentScenario.value.url}`, 'info')
await new Promise(r => setTimeout(r, 600)) // Simulate network latency
const { method, url, body } = currentScenario.value
// Router Logic Simulation
if (method === 'GET' && url === '/api/users') {
response.value = { status: 200, statusText: 'OK', body: db.value }
addLog('Matched route: GET /users -> listUsers()', 'success')
}
else if (method === 'GET' && url.match(/\/api\/users\/\d+/)) {
const id = parseInt(url.split('/').pop())
const user = db.value.find(u => u.id === id)
if (user) {
response.value = { status: 200, statusText: 'OK', body: user }
addLog(`Found user ${id}`, 'success')
} else {
response.value = { status: 404, statusText: 'Not Found', body: { error: "User not found" } }
addLog(`User ${id} not found in DB`, 'error')
}
}
else if (method === 'POST' && url === '/api/users') {
const newUser = { id: Math.max(0, ...db.value.map(u => u.id)) + 1, ...body }
db.value.push(newUser)
response.value = { status: 201, statusText: 'Created', body: newUser }
addLog(`Created user ${newUser.id}`, 'success')
}
else if (method === 'DELETE' && url.match(/\/api\/users\/\d+/)) {
const id = parseInt(url.split('/').pop())
const idx = db.value.findIndex(u => u.id === id)
if (idx !== -1) {
db.value.splice(idx, 1)
response.value = { status: 204, statusText: 'No Content', body: null }
addLog(`Deleted user ${id}`, 'success')
} else {
response.value = { status: 404, statusText: 'Not Found', body: { error: "User not found" } }
addLog(`User ${id} not found for deletion`, 'error')
}
}
processing.value = false
}
</script>
<style scoped>
.raf-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
font-size: 13px;
}
.raf-layout {
display: flex;
min-height: 400px;
}
.raf-left, .raf-right {
flex: 1;
padding: 1.2rem;
display: flex;
flex-direction: column;
}
.raf-left {
border-right: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
.raf-right {
background: var(--vp-c-bg-alt);
}
.raf-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 1rem;
font-weight: 600;
font-size: 1.1em;
color: var(--vp-c-text-1);
}
.raf-scenarios {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 1.5rem;
}
.raf-chip {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
color: var(--vp-c-text-2);
}
.raf-chip:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.raf-chip.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.raf-request-box {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
background: var(--vp-c-bg-soft);
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.raf-http-line {
display: flex;
gap: 10px;
font-family: monospace;
margin-bottom: 8px;
align-items: center;
font-size: 1.1em;
}
.raf-method {
font-weight: bold;
}
.raf-method.GET { color: #61affe; }
.raf-method.POST { color: #49cc90; }
.raf-method.DELETE { color: #f93e3e; }
.raf-code-block {
background: var(--vp-c-bg);
padding: 10px;
border-radius: 4px;
font-size: 12px;
white-space: pre;
overflow-x: auto;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.raf-send-btn {
margin-top: 10px;
width: 100%;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: opacity 0.2s;
}
.raf-send-btn:hover {
opacity: 0.9;
}
.raf-send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.raf-response-box {
margin-top: auto;
border-top: 1px solid var(--vp-c-divider);
padding-top: 1rem;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.raf-status-line {
margin-bottom: 8px;
display: flex;
align-items: center;
}
.raf-status-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-left: 8px;
}
.status-success { background: #d1fae5; color: #065f46; }
.status-error { background: #fee2e2; color: #991b1b; }
.status-neutral { background: #f3f4f6; color: #374151; }
.raf-db-view {
display: flex;
flex-direction: column;
gap: 6px;
}
.raf-db-item {
display: flex;
gap: 10px;
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
font-size: 12px;
align-items: center;
}
.raf-db-id { color: var(--vp-c-text-3); font-family: monospace; }
.raf-db-name { font-weight: bold; }
.raf-db-role { color: var(--vp-c-brand); font-size: 0.9em; }
.raf-logs {
height: 180px;
overflow-y: auto;
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 6px;
font-family: monospace;
font-size: 11px;
line-height: 1.5;
}
.raf-log-line {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
.raf-log-time { color: #6b7280; flex-shrink: 0; }
.info { color: #93c5fd; }
.success { color: #86efac; }
.error { color: #fca5a5; }
.raf-section-title {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-3);
margin-top: 1.5rem;
}
.raf-section:first-child .raf-section-title { margin-top: 0; }
.raf-empty {
color: var(--vp-c-text-3);
font-style: italic;
padding: 10px;
text-align: center;
}
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(20px);
}
@media (max-width: 768px) {
.raf-layout { flex-direction: column; }
.raf-left { border-right: none; border-bottom: 1px solid var(--vp-c-divider); }
}
</style>
@@ -1,382 +0,0 @@
<template>
<div class="restful-design-demo">
<div class="header">
<div class="title">
RESTful API 设计核心原则
</div>
<div class="subtitle">
RESTRepresentational State Transfer是一种架构风格让接口设计像自然资源一样直观
</div>
</div>
<div class="principles-grid">
<div
v-for="principle in principles"
:key="principle.id"
class="principle-card"
:class="{ active: selectedPrinciple === principle.id }"
@click="selectedPrinciple = principle.id"
>
<div class="principle-icon">
{{ principle.icon }}
</div>
<div class="principle-name">
{{ principle.name }}
</div>
<div class="principle-brief">
{{ principle.brief }}
</div>
</div>
</div>
<div class="detail-panel">
<div class="detail-header">
<span class="detail-title">{{ activePrinciple.name }}</span>
<span class="detail-tag">{{ activePrinciple.tag }}</span>
</div>
<div class="detail-content">
<div class="explanation">
<h4>核心概念</h4>
<p>{{ activePrinciple.explanation }}</p>
</div>
<div class="comparison">
<h4>对比示例</h4>
<div class="code-comparison">
<div class="code-block bad">
<div class="code-label">
传统方式不推荐
</div>
<pre><code>{{ activePrinciple.badExample }}</code></pre>
</div>
<div class="code-block good">
<div class="code-label">
RESTful 方式推荐
</div>
<pre><code>{{ activePrinciple.goodExample }}</code></pre>
</div>
</div>
</div>
<div class="tips">
<h4>设计要点</h4>
<ul>
<li
v-for="(tip, index) in activePrinciple.tips"
:key="index"
>
{{ tip }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const principles = [
{
id: 'resource',
name: '资源导向',
icon: '📦',
brief: 'URL 表示资源,而非动作',
tag: '核心原则',
explanation: '将系统中的实体抽象为资源(Resource),每个资源对应唯一的 URL。资源是名词,而不是动词或动作。',
badExample: `GET /getUserById?id=123
GET /deleteOrder?orderId=456
POST /createProduct`,
goodExample: `GET /users/123
DELETE /orders/456
POST /products`,
tips: [
'使用名词复数形式(/users 而非 /user',
'避免在 URL 中出现动词(get、create、delete 等)',
'资源层级用路径表示(/users/123/orders',
'资源名使用小写字母,多个单词用连字符(/order-items'
]
},
{
id: 'method',
name: 'HTTP 方法',
icon: '🎯',
brief: '用 HTTP 方法表达操作语义',
tag: '动作表达',
explanation: '使用标准的 HTTP 方法(GET、POST、PUT、DELETE 等)来表示对资源的操作类型,让接口语义更加清晰。',
badExample: `POST /users/query // 查询用户
POST /users/create // 创建用户
POST /users/update // 更新用户
POST /users/delete // 删除用户`,
goodExample: `GET /users // 查询用户列表
POST /users // 创建用户
GET /users/123 // 查询单个用户
PUT /users/123 // 全量更新用户
PATCH /users/123 // 部分更新用户
DELETE /users/123 // 删除用户`,
tips: [
'GET 用于获取资源,是幂等且安全的',
'POST 用于创建资源,返回 201 和新资源 URI',
'PUT 用于全量更新,替换整个资源',
'PATCH 用于部分更新,只修改指定字段',
'DELETE 用于删除资源,返回 204 或 200'
]
},
{
id: 'stateless',
name: '无状态',
icon: '🔄',
brief: '每个请求独立,服务端不保存会话',
tag: '可扩展性',
explanation: '服务端不保存客户端的上下文状态,每个请求都必须包含服务端处理该请求所需的全部信息。这让系统更容易水平扩展。',
badExample: `// 服务端维护会话状态
POST /login
→ 服务端创建 session,返回 session_id cookie
GET /profile
→ 服务端根据 session_id 查找用户
→ 如果会话过期,需要重新登录`,
goodExample: `// 无状态认证
POST /auth/token
→ 验证凭证,返回 JWT token
GET /profile
Authorization: Bearer <token>
→ 服务端验证 token,提取用户信息
→ 请求独立,可随时扩展到多台服务器`,
tips: [
'使用 JWT 或 API Key 进行无状态认证',
'避免在服务端存储会话状态',
'每个请求包含完整的认证信息',
'便于负载均衡和水平扩展',
'使用 Redis 等缓存共享必要的状态数据'
]
},
{
id: 'representation',
name: '统一表现',
icon: '📋',
brief: '使用标准数据格式',
tag: '数据交换',
explanation: '资源的表示(Representation)应该使用标准的数据格式,通常是 JSON。客户端可以通过 Accept 头部请求不同的表示格式。',
badExample: `// 混合格式,字段不一致
GET /users
{
"user_list": [...],
"total_count": 100
}
GET /orders
{
"data": [...],
"pagination": {
"total": 100,
"page": 1
}
}`,
goodExample: `// 统一的响应结构
GET /users
{
"code": 200,
"message": "success",
"data": {
"items": [...],
"pagination": {
"total": 100,
"page": 1,
"page_size": 20
}
},
"timestamp": "2024-01-15T10:30:00Z"
}`,
tips: [
'使用 JSON 作为默认数据格式',
'统一的响应结构(code、message、data',
'支持字段过滤(fields=id,name,email',
'日期使用 ISO 8601 格式',
'字段命名使用 camelCase 或 snake_case,保持一致'
]
}
]
const selectedPrinciple = ref('resource')
const activePrinciple = computed(() =>
principles.find(p => p.id === selectedPrinciple.value) || principles[0]
)
</script>
<style scoped>
.restful-design-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 1.5rem;
margin: 0.5rem 0;
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 800;
font-size: 1.25rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin-top: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.principle-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.principle-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
}
.principle-card.active {
border-color: rgba(var(--vp-c-brand-rgb), 0.8);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.15);
}
.principle-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.principle-name {
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.principle-brief {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.detail-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1.25rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 700;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.detail-tag {
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 999px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
color: var(--vp-c-brand);
font-weight: 600;
}
.detail-content h4 {
font-weight: 700;
color: var(--vp-c-text-1);
margin: 1rem 0 0.5rem;
}
.explanation p {
color: var(--vp-c-text-2);
line-height: 1.7;
}
.code-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.code-block {
border-radius: 6px;
overflow: hidden;
}
.code-block.bad {
border: 1px solid #ef4444;
}
.code-block.good {
border: 1px solid #22c55e;
}
.code-label {
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 700;
color: white;
}
.code-block.bad .code-label {
background: #ef4444;
}
.code-block.good .code-label {
background: #22c55e;
}
.code-block pre {
margin: 0;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
font-size: 0.8rem;
line-height: 1.5;
overflow-x: auto;
}
.tips ul {
margin: 0;
padding-left: 1.25rem;
color: var(--vp-c-text-2);
}
.tips li {
margin: 0.4rem 0;
line-height: 1.6;
}
@media (max-width: 768px) {
.principles-grid {
grid-template-columns: repeat(2, 1fr);
}
.code-comparison {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,385 @@
<template>
<div class="ru-root">
<div class="ru-terminal">
<div class="term-bar">
<span class="dot r" /><span class="dot y" /><span class="dot g" />
<span class="term-title">RESTful URL 设计规则</span>
</div>
<div ref="termEl" class="term-body">
<div v-for="(l, i) in lines" :key="i" class="t-line">
<span v-if="l.kind === 'cmd'" class="t-ps">$ </span>
<span :class="'t-' + l.kind">{{ l.text }}</span>
</div>
<div class="t-line">
<span class="t-ps">$ </span>
<span class="t-typing">{{ typing }}<span class="t-cur"></span></span>
</div>
</div>
</div>
<div class="ru-btns">
<button
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['ru-btn', { 'ru-btn--on': active === op.id, 'ru-btn--dim': !op.ok() }]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="ru-btn ru-btn--reset" :disabled="running" @click="reset">重置</button>
</div>
<div class="ru-compare">
<div class="compare-col compare-bad">
<div class="compare-header">
<span class="compare-icon"></span>
<span class="compare-title">错误示例</span>
</div>
<div class="compare-body">
<div v-for="(item, i) in badExamples" :key="i" class="url-row" :class="{ highlight: item.active }">
<code class="url-text">{{ item.url }}</code>
<span class="url-reason">{{ item.reason }}</span>
</div>
</div>
</div>
<div class="compare-col compare-good">
<div class="compare-header">
<span class="compare-icon"></span>
<span class="compare-title">正确示例</span>
</div>
<div class="compare-body">
<div v-for="(item, i) in goodExamples" :key="i" class="url-row" :class="{ highlight: item.active }">
<code class="url-text">{{ item.url }}</code>
<span class="url-reason">{{ item.reason }}</span>
</div>
</div>
</div>
</div>
<div v-if="hint" class="ru-hint">💡 {{ hint }}</div>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const termEl = ref(null)
const lines = ref([{ kind: 'dim', text: '# 对比 RESTful URL 的正确与错误写法' }])
const typing = ref('')
const running = ref(false)
const active = ref(null)
const hint = ref('点击命令按钮,查看不同场景下的 URL 设计对比。')
const badExamples = ref([
{ url: 'GET /getUsers', reason: 'URL 含动词', active: false },
{ url: 'GET /user', reason: '单数形式', active: false },
{ url: 'GET /UserProfiles', reason: '大写字母', active: false },
{ url: 'GET /user_profiles', reason: '下划线连接', active: false },
{ url: 'GET /users/123/orders/456/items/789', reason: '层级过深', active: false },
{ url: 'GET /products/category/phone/price/5000', reason: '过滤条件放路径', active: false },
])
const goodExamples = ref([
{ url: 'GET /users', reason: '名词 + 复数', active: false },
{ url: 'GET /users', reason: '复数形式', active: false },
{ url: 'GET /user-profiles', reason: '小写 + 连字符', active: false },
{ url: 'GET /user-profiles', reason: '连字符连接', active: false },
{ url: 'GET /users/123/orders', reason: '最多 3 层', active: false },
{ url: 'GET /products?category=phone&price_max=5000', reason: '过滤用查询参数', active: false },
])
const sleep = ms => new Promise(r => setTimeout(r, ms))
const ops = [
{
id: 'rule1',
cmd: '规则1: 用名词不用动词',
ok: () => true,
output: [
{ kind: 'dim', text: '# URL 表示资源地址,不是操作' },
{ kind: 'red', text: '❌ GET /getUsers' },
{ kind: 'red', text: '❌ GET /fetchUserInfo' },
{ kind: 'red', text: '❌ POST /createOrder' },
{ kind: 'grn', text: '✅ GET /users' },
{ kind: 'grn', text: '✅ GET /users/123' },
{ kind: 'grn', text: '✅ POST /orders' },
],
hint: 'URL 是资源的"地址"HTTP 方法已经表达了"操作"。不要在 URL 里重复说"做什么"。',
do: () => {
badExamples.value[0].active = true
goodExamples.value[0].active = true
}
},
{
id: 'rule2',
cmd: '规则2: 用复数形式',
ok: () => true,
output: [
{ kind: 'dim', text: '# 复数形式表示集合,风格统一' },
{ kind: 'red', text: '❌ GET /user' },
{ kind: 'red', text: '❌ GET /order' },
{ kind: 'grn', text: '✅ GET /users' },
{ kind: 'grn', text: '✅ GET /orders' },
{ kind: 'grn', text: '✅ GET /users/123 (获取单个)' },
],
hint: '统一用复数,避免 /user 和 /users 混用。获取单个资源时用 /users/123。',
do: () => {
badExamples.value[1].active = true
goodExamples.value[1].active = true
}
},
{
id: 'rule3',
cmd: '规则3: 小写+连字符',
ok: () => true,
output: [
{ kind: 'dim', text: '# URL 大小写敏感,统一小写避免混乱' },
{ kind: 'red', text: '❌ GET /UserProfiles' },
{ kind: 'red', text: '❌ GET /user_profiles' },
{ kind: 'grn', text: '✅ GET /user-profiles' },
{ kind: 'grn', text: '✅ GET /order-items' },
],
hint: 'URL 大小写敏感,统一用小写 + 连字符(-)是最安全的做法。',
do: () => {
badExamples.value[2].active = true
badExamples.value[3].active = true
goodExamples.value[2].active = true
goodExamples.value[3].active = true
}
},
{
id: 'rule4',
cmd: '规则4: 避免层级过深',
ok: () => true,
output: [
{ kind: 'dim', text: '# 层级太深 = 耦合度高,难以维护' },
{ kind: 'red', text: '❌ /users/123/orders/456/items/789/status' },
{ kind: 'grn', text: '✅ /users/123/orders (用户订单)' },
{ kind: 'grn', text: '✅ /orders/456/items (订单商品)' },
{ kind: 'grn', text: '✅ /order-items/789 (直接访问)' },
],
hint: '超过 3 层考虑重构。可以用扁平化路径或查询参数替代深层嵌套。',
do: () => {
badExamples.value[4].active = true
goodExamples.value[4].active = true
}
},
{
id: 'rule5',
cmd: '规则5: 过滤用查询参数',
ok: () => true,
output: [
{ kind: 'dim', text: '# 过滤条件多变,不适合放路径' },
{ kind: 'red', text: '❌ /products/category/phone/price/5000' },
{ kind: 'grn', text: '✅ /products?category=phone&price_max=5000' },
{ kind: 'grn', text: '✅ /products?status=active&sort=created_desc' },
{ kind: 'grn', text: '✅ /products?category=phone,electronics' },
],
hint: '查询参数可以灵活组合,路径则固定不变。过滤、排序、分页都用查询参数。',
do: () => {
badExamples.value[5].active = true
goodExamples.value[5].active = true
}
},
]
async function run(op) {
if (running.value) return
running.value = true
active.value = op.id
hint.value = ''
typing.value = ''
badExamples.value.forEach(e => e.active = false)
goodExamples.value.forEach(e => e.active = false)
for (const ch of op.cmd) {
typing.value += ch
await sleep(18)
}
await sleep(80)
lines.value.push({ kind: 'cmd', text: op.cmd })
typing.value = ''
await nextTick()
scroll()
await sleep(150)
for (const l of op.output) {
lines.value.push(l)
await nextTick()
scroll()
await sleep(50)
}
op.do()
await sleep(120)
hint.value = op.hint
running.value = false
}
function scroll() {
if (termEl.value) termEl.value.scrollTop = termEl.value.scrollHeight
}
function reset() {
lines.value = [{ kind: 'dim', text: '# 对比 RESTful URL 的正确与错误写法' }]
badExamples.value.forEach(e => e.active = false)
goodExamples.value.forEach(e => e.active = false)
active.value = null
hint.value = '点击命令按钮,查看不同场景下的 URL 设计对比。'
typing.value = ''
running.value = false
}
</script>
<style scoped>
.ru-root {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.ru-terminal { background: #141420; }
.term-bar {
display: flex;
align-items: center;
gap: 5px;
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.term-body {
min-height: 100px;
max-height: 160px;
overflow-y: auto;
overflow-x: auto;
padding: 0.7rem 1rem;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.76rem;
line-height: 1.6;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #a6e3a1; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.ru-btns {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 10px;
background: #0d0d1a;
border-top: 1px solid #2a2a3e;
}
.ru-btn {
background: #1e1e2e;
border: 1px solid #313244;
border-radius: 5px;
padding: 4px 9px;
cursor: pointer;
transition: border-color 0.2s;
}
.ru-btn code { font-size: 0.68rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.ru-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.ru-btn--on { border-color: var(--vp-c-brand) !important; }
.ru-btn--on code { color: var(--vp-c-brand); }
.ru-btn--dim { opacity: 0.3; cursor: not-allowed; }
.ru-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.ru-btn--reset code { display: none; }
.ru-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.ru-compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-top: 1px solid var(--vp-c-divider);
}
.compare-col {
padding: 12px;
}
.compare-bad {
background: color-mix(in srgb, #ef4444 4%, var(--vp-c-bg));
border-right: 1px solid var(--vp-c-divider);
}
.compare-good {
background: color-mix(in srgb, #22c55e 4%, var(--vp-c-bg));
}
.compare-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
}
.compare-icon { font-size: 1rem; }
.compare-title { font-weight: 700; font-size: 0.85rem; color: var(--vp-c-text-1); }
.compare-body {
display: flex;
flex-direction: column;
gap: 6px;
}
.url-row {
padding: 6px 8px;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid transparent;
transition: border-color 0.2s, background 0.2s;
}
.url-row.highlight {
border-color: var(--vp-c-brand);
background: color-mix(in srgb, var(--vp-c-brand) 8%, var(--vp-c-bg));
}
.url-text {
display: block;
font-family: monospace;
font-size: 0.72rem;
color: var(--vp-c-text-1);
margin-bottom: 2px;
}
.url-reason {
font-size: 0.68rem;
color: var(--vp-c-text-3);
}
.ru-hint {
padding: 10px 12px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
@media (max-width: 640px) {
.ru-compare {
grid-template-columns: 1fr;
}
.compare-bad {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
}
</style>
@@ -1,638 +1,424 @@
<!--
StatusCodeDemo.vue - HTTP 状态码演示组件
展示常见 HTTP 状态码的含义和使用场景
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">📡</span>
<span class="title">HTTP 状态码服务器的"情绪表达"</span>
<div class="sc-root">
<div class="sc-terminal">
<div class="term-bar">
<span class="dot r" /><span class="dot y" /><span class="dot g" />
<span class="term-title">HTTP 状态码演示</span>
</div>
<div ref="termEl" class="term-body">
<div v-for="(l, i) in lines" :key="i" class="t-line">
<span v-if="l.kind === 'cmd'" class="t-ps">&gt; </span>
<span :class="'t-' + l.kind">{{ l.text }}</span>
</div>
<div class="t-line">
<span class="t-ps">&gt; </span>
<span class="t-typing">{{ typing }}<span class="t-cur"></span></span>
</div>
</div>
</div>
<div class="content">
<div class="category-tabs">
<button
v-for="category in categories"
:key="category.code"
class="category-btn"
:class="[category.class, { active: selectedCategory === category.code }]"
@click="selectedCategory = category.code"
>
<span class="category-code">{{ category.code }}xx</span>
<span class="category-name">{{ category.name }}</span>
</button>
<div class="sc-btns">
<button
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['sc-btn', { 'sc-btn--on': active === op.id, 'sc-btn--dim': !op.ok() }]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="sc-btn sc-btn--reset" :disabled="running" @click="reset">重置</button>
</div>
<div class="sc-codes">
<div class="code-section">
<div class="section-header success">
<span class="section-icon"></span>
<span class="section-title">2xx 成功</span>
</div>
<div class="section-body">
<div v-for="c in successCodes" :key="c.code" class="code-item" :class="{ active: activeCode === c.code }">
<span class="code-num">{{ c.code }}</span>
<span class="code-name">{{ c.name }}</span>
<span class="code-desc">{{ c.desc }}</span>
</div>
</div>
</div>
<div
v-if="filteredCodes.length > 0"
class="status-codes"
>
<div
v-for="code in filteredCodes"
:key="code.number"
class="status-card"
:class="{ expanded: expandedCode === code.number }"
@click="toggleExpand(code.number)"
>
<div class="status-header">
<span
class="status-number"
:class="getCategoryClass(code.number)"
>{{ code.number }}</span>
<span class="status-name">{{ code.name }}</span>
<span class="expand-icon">{{ expandedCode === code.number ? '▼' : '▶' }}</span>
<div class="code-section">
<div class="section-header client">
<span class="section-icon"></span>
<span class="section-title">4xx 客户端错误</span>
</div>
<div class="section-body">
<div v-for="c in clientCodes" :key="c.code" class="code-item" :class="{ active: activeCode === c.code }">
<span class="code-num">{{ c.code }}</span>
<span class="code-name">{{ c.name }}</span>
<span class="code-desc">{{ c.desc }}</span>
</div>
</div>
</div>
<div
v-show="expandedCode === code.number"
class="status-detail"
>
<div class="detail-section">
<h4>💡 含义解释</h4>
<p>{{ code.description }}</p>
</div>
<div class="detail-section">
<h4>📝 使用场景</h4>
<ul>
<li
v-for="(scenario, idx) in code.scenarios"
:key="idx"
>
{{ scenario }}
</li>
</ul>
</div>
<div
v-if="code.example"
class="detail-section"
>
<h4>💻 示例代码</h4>
<div class="code-example">
<div class="code-request">
<span
class="method-badge"
:class="getCategoryClass(code.number)"
>{{ code.example.method }}</span>
<code>{{ code.example.path }}</code>
</div>
<div class="code-response">
<pre><code>{{ JSON.stringify(code.example.response, null, 2) }}</code></pre>
</div>
</div>
</div>
<div class="code-section">
<div class="section-header server">
<span class="section-icon">🔴</span>
<span class="section-title">5xx 服务端错误</span>
</div>
<div class="section-body">
<div v-for="c in serverCodes" :key="c.code" class="code-item" :class="{ active: activeCode === c.code }">
<span class="code-num">{{ c.code }}</span>
<span class="code-name">{{ c.name }}</span>
<span class="code-desc">{{ c.desc }}</span>
</div>
</div>
</div>
</div>
<div v-if="hint" class="sc-hint">💡 {{ hint }}</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, nextTick } from 'vue'
const categories = [
{ code: '2', name: '成功', class: 'success' },
{ code: '3', name: '重定向', class: 'redirect' },
{ code: '4', name: '客户端错误', class: 'client-error' },
{ code: '5', name: '服务器错误', class: 'server-error' }
const termEl = ref(null)
const lines = ref([{ kind: 'dim', text: '// 点击按钮查看不同状态码的含义' }])
const typing = ref('')
const running = ref(false)
const active = ref(null)
const activeCode = ref(null)
const hint = ref('点击命令按钮,了解常见的 HTTP 状态码。')
const successCodes = ref([
{ code: 200, name: 'OK', desc: '请求成功' },
{ code: 201, name: 'Created', desc: '创建成功' },
{ code: 204, name: 'No Content', desc: '成功但无返回内容' },
])
const clientCodes = ref([
{ code: 400, name: 'Bad Request', desc: '请求格式错误' },
{ code: 401, name: 'Unauthorized', desc: '未认证' },
{ code: 403, name: 'Forbidden', desc: '无权限' },
{ code: 404, name: 'Not Found', desc: '资源不存在' },
{ code: 422, name: 'Unprocessable', desc: '语义错误' },
{ code: 429, name: 'Too Many', desc: '请求过多' },
])
const serverCodes = ref([
{ code: 500, name: 'Server Error', desc: '服务器内部错误' },
{ code: 502, name: 'Bad Gateway', desc: '网关错误' },
{ code: 503, name: 'Unavailable', desc: '服务不可用' },
])
const sleep = ms => new Promise(r => setTimeout(r, ms))
const ops = [
{
id: '200',
cmd: '200 OK',
ok: () => true,
output: [
{ kind: 'dim', text: '// 最常用的成功状态码' },
{ kind: 'grn', text: 'HTTP/1.1 200 OK' },
{ kind: 'dim', text: 'Content-Type: application/json' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 0, "data": { ... } }' },
],
hint: '200 表示请求成功处理。GET 查询、PUT/PATCH 更新成功时常用。',
do: () => { activeCode.value = 200 }
},
{
id: '201',
cmd: '201 Created',
ok: () => true,
output: [
{ kind: 'dim', text: '// 创建资源成功' },
{ kind: 'grn', text: 'HTTP/1.1 201 Created' },
{ kind: 'dim', text: 'Location: /api/users/123' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 0, "data": { "id": 123 } }' },
],
hint: '201 表示资源创建成功。响应头 Location 指向新资源的地址。',
do: () => { activeCode.value = 201 }
},
{
id: '400',
cmd: '400 Bad Request',
ok: () => true,
output: [
{ kind: 'dim', text: '// 客户端请求有问题' },
{ kind: 'red', text: 'HTTP/1.1 400 Bad Request' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10001, "message": "参数格式错误" }' },
],
hint: '400 表示请求语法错误。比如 JSON 格式不对、缺少必填参数。',
do: () => { activeCode.value = 400 }
},
{
id: '401',
cmd: '401 Unauthorized',
ok: () => true,
output: [
{ kind: 'dim', text: '// 需要登录认证' },
{ kind: 'red', text: 'HTTP/1.1 401 Unauthorized' },
{ kind: 'dim', text: 'WWW-Authenticate: Bearer' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10018, "message": "请先登录" }' },
],
hint: '401 表示未认证。Token 过期、未登录时返回,客户端应引导用户登录。',
do: () => { activeCode.value = 401 }
},
{
id: '403',
cmd: '403 Forbidden',
ok: () => true,
output: [
{ kind: 'dim', text: '// 已登录但无权限' },
{ kind: 'red', text: 'HTTP/1.1 403 Forbidden' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10021, "message": "需要管理员权限" }' },
],
hint: '403 表示已认证但无权限。普通用户访问管理员接口时返回。',
do: () => { activeCode.value = 403 }
},
{
id: '404',
cmd: '404 Not Found',
ok: () => true,
output: [
{ kind: 'dim', text: '// 资源不存在' },
{ kind: 'red', text: 'HTTP/1.1 404 Not Found' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10002, "message": "用户不存在" }' },
],
hint: '404 表示请求的资源不存在。URL 错误或资源已被删除。',
do: () => { activeCode.value = 404 }
},
{
id: '500',
cmd: '500 Server Error',
ok: () => true,
output: [
{ kind: 'dim', text: '// 服务器内部错误' },
{ kind: 'red', text: 'HTTP/1.1 500 Internal Server Error' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10000, "message": "服务器错误,请联系管理员" }' },
],
hint: '500 表示服务器内部错误。代码 bug、数据库连接失败等,不要暴露堆栈信息!',
do: () => { activeCode.value = 500 }
},
]
const statusCodes = [
{
number: 200,
name: 'OK',
description: '请求已成功处理。这是最常用的成功状态码。',
scenarios: [
'GET 请求成功返回数据',
'POST 请求成功处理但未创建新资源',
'PUT/PATCH 更新成功'
],
example: {
method: 'GET',
path: '/api/v1/users/123',
response: {
code: 0,
data: {
id: 123,
name: '张三',
email: 'zhangsan@example.com'
}
}
}
},
{
number: 201,
name: 'Created',
description: '请求成功处理并创建了新的资源。通常用于 POST 请求。',
scenarios: [
'成功创建用户账号',
'成功创建订单',
'成功上传文件'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 0,
data: {
id: 124,
name: '李四',
created_at: '2024-01-15T10:30:00Z'
}
}
}
},
{
number: 204,
name: 'No Content',
description: '请求成功处理,但响应中没有返回内容。',
scenarios: [
'DELETE 删除成功',
'PUT/PATCH 更新成功但无需返回数据',
'预检请求(OPTIONS)响应'
],
example: {
method: 'DELETE',
path: '/api/v1/users/123',
response: null
}
},
{
number: 301,
name: 'Moved Permanently',
description: '请求的资源已永久移动到新的 URL。',
scenarios: [
'API 版本升级,旧版本废弃',
'网站重构,URL 结构变更',
'资源合并或重命名'
]
},
{
number: 304,
name: 'Not Modified',
description: '资源自上次请求以来未被修改,客户端可使用缓存版本。',
scenarios: [
'客户端带有 If-None-Match 或 If-Modified-Since 头部',
'静态资源缓存优化',
'减少不必要的数据传输'
]
},
{
number: 400,
name: 'Bad Request',
description: '请求语法错误或参数无效,服务器无法理解请求。',
scenarios: [
'请求体格式不正确(如 JSON 语法错误)',
'缺少必填参数',
'参数类型不匹配(字符串传数字)'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 10001,
message: '参数校验失败',
errors: [
{ field: 'email', message: '邮箱格式不正确' }
]
}
}
},
{
number: 401,
name: 'Unauthorized',
description: '请求需要用户身份验证,但未提供或凭证无效。',
scenarios: [
'未登录就访问受保护资源',
'Token 过期或无效',
'缺少 Authorization 头部'
],
example: {
method: 'GET',
path: '/api/v1/user/profile',
response: {
code: 10018,
message: '认证令牌已过期,请重新登录'
}
}
},
{
number: 403,
name: 'Forbidden',
description: '服务器理解请求,但拒绝执行(权限不足)。',
scenarios: [
'已登录但访问了没有权限的资源',
'普通用户尝试访问管理员功能',
'账号被禁用或权限被撤销'
],
example: {
method: 'DELETE',
path: '/api/v1/users/456',
response: {
code: 10021,
message: '权限不足,需要管理员权限才能删除用户'
}
}
},
{
number: 404,
name: 'Not Found',
description: '服务器找不到请求的资源。',
scenarios: [
'URL 拼写错误',
'资源已被删除或不存在',
'API 版本已废弃'
],
example: {
method: 'GET',
path: '/api/v1/users/99999',
response: {
code: 10002,
message: '用户不存在'
}
}
},
{
number: 409,
name: 'Conflict',
description: '请求与服务器当前状态冲突(如资源重复)。',
scenarios: [
'尝试创建已存在的用户(唯一约束冲突)',
'乐观锁版本号不匹配',
'并发修改导致的状态冲突'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 10011,
message: '邮箱已被注册'
}
}
},
{
number: 422,
name: 'Unprocessable Entity',
description: '请求格式正确,但语义上有错误(验证失败)。',
scenarios: [
'请求体 JSON 格式正确,但字段值不符合业务规则',
'密码强度不足',
'余额不足无法完成支付'
],
example: {
method: 'POST',
path: '/api/v1/orders',
response: {
code: 10014,
message: '订单金额不能为负数'
}
}
},
{
number: 429,
name: 'Too Many Requests',
description: '客户端发送请求过多,触发了限流。',
scenarios: [
'短时间内大量请求',
'超出 API 配额限制',
'触发防刷机制'
],
example: {
method: 'GET',
path: '/api/v1/data',
response: {
code: 10005,
message: '请求过于频繁,请 60 秒后重试'
}
}
},
{
number: 500,
name: 'Internal Server Error',
description: '服务器内部错误,无法完成请求。',
scenarios: [
'代码抛出未捕获的异常',
'数据库连接失败',
'依赖服务不可用'
],
example: {
method: 'GET',
path: '/api/v1/users',
response: {
code: 10000,
message: '服务器内部错误,请联系管理员'
}
}
},
{
number: 502,
name: 'Bad Gateway',
description: '网关或代理从上游服务器收到无效响应。',
scenarios: [
'反向代理(Nginx)无法连接到后端服务',
'后端服务崩溃或重启中',
'网关配置错误'
]
},
{
number: 503,
name: 'Service Unavailable',
description: '服务器暂时无法处理请求(维护或过载)。',
scenarios: [
'服务器正在进行维护',
'服务器过载,触发熔断',
'依赖服务大面积故障'
],
example: {
method: 'GET',
path: '/api/v1/status',
response: {
code: 10007,
message: '服务维护中,预计 10 分钟后恢复'
}
}
},
{
number: 504,
name: 'Gateway Timeout',
description: '网关或代理等待上游服务器响应超时。',
scenarios: [
'后端处理时间过长',
'网络延迟或丢包',
'数据库查询超时'
]
async function run(op) {
if (running.value) return
running.value = true
active.value = op.id
activeCode.value = null
hint.value = ''
typing.value = ''
for (const ch of op.cmd) {
typing.value += ch
await sleep(18)
}
]
await sleep(80)
lines.value.push({ kind: 'cmd', text: op.cmd })
typing.value = ''
await nextTick()
scroll()
await sleep(150)
const selectedCategory = ref('2')
const expandedCode = ref(null)
const filteredCodes = computed(() => {
const prefix = selectedCategory.value
return statusCodes.filter(code => {
const codePrefix = Math.floor(code.number / 100).toString()
return codePrefix === prefix
})
})
function getCategoryClass(number) {
const prefix = Math.floor(number / 100)
switch (prefix) {
case 2: return 'success'
case 3: return 'redirect'
case 4: return 'client-error'
case 5: return 'server-error'
default: return ''
for (const l of op.output) {
lines.value.push(l)
await nextTick()
scroll()
await sleep(50)
}
op.do()
await sleep(120)
hint.value = op.hint
running.value = false
}
function toggleExpand(number) {
expandedCode.value = expandedCode.value === number ? null : number
function scroll() {
if (termEl.value) termEl.value.scrollTop = termEl.value.scrollHeight
}
function reset() {
lines.value = [{ kind: 'dim', text: '// 点击按钮查看不同状态码的含义' }]
active.value = null
activeCode.value = null
hint.value = '点击命令按钮,了解常见的 HTTP 状态码。'
typing.value = ''
running.value = false
}
</script>
<style scoped>
.demo {
.sc-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
.sc-terminal { background: #141420; }
.term-bar {
display: flex;
align-items: center;
gap: 12px;
gap: 5px;
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.icon {
font-size: 24px;
.term-body {
min-height: 90px;
max-height: 140px;
overflow-y: auto;
overflow-x: auto;
padding: 0.7rem 1rem;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.76rem;
line-height: 1.6;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #89b4fa; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.category-tabs {
.sc-btns {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
gap: 6px;
padding: 8px 10px;
background: #0d0d1a;
border-top: 1px solid #2a2a3e;
}
.category-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
.sc-btn {
background: #1e1e2e;
border: 1px solid #313244;
border-radius: 5px;
padding: 4px 9px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 100px;
transition: border-color 0.2s;
}
.category-btn:hover {
transform: translateY(-2px);
.sc-btn code { font-size: 0.68rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.sc-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.sc-btn--on { border-color: var(--vp-c-brand) !important; }
.sc-btn--on code { color: var(--vp-c-brand); }
.sc-btn--dim { opacity: 0.3; cursor: not-allowed; }
.sc-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.sc-btn--reset code { display: none; }
.sc-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.category-btn.active {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 分类颜色 */
.category-btn.success, .status-number.success { border-color: #22c55e; color: #16a34a; }
.category-btn.success.active { background: #22c55e; color: white; }
.category-btn.redirect, .status-number.redirect { border-color: #3b82f6; color: #2563eb; }
.category-btn.redirect.active { background: #3b82f6; color: white; }
.category-btn.client-error, .status-number.client-error { border-color: #f59e0b; color: #d97706; }
.category-btn.client-error.active { background: #f59e0b; color: white; }
.category-btn.server-error, .status-number.server-error { border-color: #ef4444; color: #dc2626; }
.category-btn.server-error.active { background: #ef4444; color: white; }
.category-code {
font-size: 18px;
font-weight: 700;
}
.category-name {
font-size: 12px;
margin-top: 4px;
}
.status-codes {
display: flex;
flex-direction: column;
gap: 12px;
}
.status-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
}
.status-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
}
.status-card.expanded {
border-color: rgba(var(--vp-c-brand-rgb), 0.8);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.1);
}
.status-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
}
.status-number {
padding: 6px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
background: var(--vp-c-bg-soft);
border: 1px solid;
}
.status-name {
flex: 1;
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
}
.expand-icon {
font-size: 12px;
color: var(--vp-c-text-3);
}
.status-detail {
padding: 16px;
.sc-codes {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
border-top: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.detail-section {
margin-bottom: 16px;
.code-section {
border-right: 1px solid var(--vp-c-divider);
}
.code-section:last-child {
border-right: none;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section h4 {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 8px 0;
}
.detail-section p {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 0;
}
.detail-section ul {
margin: 0;
padding-left: 16px;
}
.detail-section li {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 4px 0;
}
.code-example {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-request {
.section-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.method-badge {
padding: 4px 8px;
border-radius: 4px;
gap: 6px;
padding: 8px 10px;
font-weight: 700;
font-size: 11px;
background: var(--vp-c-bg);
border: 1px solid;
font-size: 0.8rem;
}
.section-header.success { background: color-mix(in srgb, #22c55e 8%, var(--vp-c-bg-alt)); color: #22c55e; }
.section-header.client { background: color-mix(in srgb, #f59e0b 8%, var(--vp-c-bg-alt)); color: #d97706; }
.section-header.server { background: color-mix(in srgb, #ef4444 8%, var(--vp-c-bg-alt)); color: #ef4444; }
.section-icon { font-size: 0.9rem; }
.section-title { font-size: 0.75rem; }
.section-body {
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.code-request code {
.code-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid transparent;
transition: all 0.2s;
}
.code-item.active {
border-color: var(--vp-c-brand);
background: color-mix(in srgb, var(--vp-c-brand) 8%, var(--vp-c-bg));
}
.code-num {
font-family: monospace;
font-size: 13px;
font-weight: 700;
font-size: 0.75rem;
min-width: 28px;
}
.code-item.active .code-num { color: var(--vp-c-brand); }
.code-name {
font-size: 0.72rem;
color: var(--vp-c-text-1);
font-weight: 600;
}
.code-response {
padding: 12px;
background: var(--vp-c-bg);
.code-desc {
font-size: 0.68rem;
color: var(--vp-c-text-3);
margin-left: auto;
}
.code-response pre {
margin: 0;
font-size: 12px;
.sc-hint {
padding: 10px 12px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
overflow-x: auto;
}
.code-response code {
font-family: monospace;
color: var(--vp-c-text-1);
}
@media (max-width: 640px) {
.category-tabs {
flex-direction: column;
@media (max-width: 768px) {
.sc-codes {
grid-template-columns: 1fr;
}
.category-btn {
flex-direction: row;
justify-content: space-between;
.code-section {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.status-header {
flex-wrap: wrap;
}
.code-request {
flex-direction: column;
align-items: flex-start;
.code-section:last-child {
border-bottom: none;
}
}
</style>
@@ -1,448 +0,0 @@
<!--
VersioningStrategyDemo.vue - API 版本控制策略演示
展示 4 种版本控制策略的对比
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">🔢</span>
<span class="title">API 版本控制向后兼容的艺术</span>
</div>
<div class="content">
<div class="strategies">
<div
v-for="strategy in strategies"
:key="strategy.id"
class="strategy-card"
:class="{ active: selectedStrategy === strategy.id }"
@click="selectedStrategy = strategy.id"
>
<div class="strategy-header">
<div class="strategy-name">
{{ strategy.name }}
</div>
<div class="strategy-stars">
<span
v-for="n in strategy.stars"
:key="n"
class="star"
></span>
</div>
</div>
<div class="strategy-example">
{{ strategy.example }}
</div>
</div>
</div>
<div
v-if="currentStrategy"
class="strategy-detail"
>
<div class="detail-header">
<div class="detail-title">
{{ currentStrategy.name }}
</div>
<div
class="detail-recommendation"
:class="currentStrategy.level"
>
{{ currentStrategy.level === 'high' ? '强烈推荐' : currentStrategy.level === 'medium' ? '可以使用' : '不推荐' }}
</div>
</div>
<div class="detail-sections">
<div class="detail-section">
<h4> 优点</h4>
<ul>
<li
v-for="(pro, idx) in currentStrategy.pros"
:key="idx"
>
{{ pro }}
</li>
</ul>
</div>
<div class="detail-section">
<h4> 缺点</h4>
<ul>
<li
v-for="(con, idx) in currentStrategy.cons"
:key="idx"
>
{{ con }}
</li>
</ul>
</div>
</div>
<div class="detail-section example">
<h4>💻 实现示例</h4>
<div class="code-box">
<div class="code-header">
Request
</div>
<pre><code>{{ currentStrategy.codeExample.request }}</code></pre>
<div class="code-header">
Response Headers
</div>
<pre><code>{{ currentStrategy.codeExample.response }}</code></pre>
</div>
</div>
<div class="detail-section tips">
<h4>💡 最佳实践</h4>
<ul>
<li
v-for="(tip, idx) in currentStrategy.tips"
:key="idx"
>
{{ tip }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const strategies = [
{
id: 'url-path',
name: 'URL Path 版本',
example: '/v1/users',
stars: 4,
level: 'high',
pros: [
'最直观,一目了然看到版本号',
'易于缓存和控制权限',
'文档清晰,社区主流做法',
'支持不同版本的并行部署'
],
cons: [
'URL 会变化,不符合 REST 资源唯一性',
'需要配置路由规则'
],
codeExample: {
request: `GET /v1/users HTTP/1.1
Host: api.example.com`,
response: `HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: v1`
},
tips: [
'版本号放在路径开头:`/v1/users`',
'使用语义化版本号(Semantic Versioning',
'废弃版本返回 Sunset 头部',
'客户端升级提示可通过响应头提示'
]
},
{
id: 'header',
name: 'Header 版本',
example: 'API-Version: v1',
stars: 2,
level: 'medium',
pros: [
'URL 保持简洁不变',
'版本控制不影响路由'
],
cons: [
'不直观,需要在工具里配置 Header',
'缓存策略复杂',
'文档不够清晰',
'调试不便'
],
codeExample: {
request: `GET /users HTTP/1.1
Host: api.example.com
API-Version: v1`,
response: `HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: v1`
},
tips: [
'使用自定义 Header`API-Version` 或 `Accept`',
'需在 API Gateway 中统一处理',
'适合内部系统或对 API 规范要求高的场景'
]
},
{
id: 'content-negotiation',
name: '内容协商',
example: 'Accept: application/vnd.api.v1+json',
stars: 2,
level: 'medium',
pros: [
'符合 HTTP 标准',
'URL 完全不变'
],
cons: [
'复杂,理解成本高',
'开发者容易用错',
'缓存和代理支持不佳'
],
codeExample: {
request: `GET /users HTTP/1.1
Host: api.example.com
Accept: application/vnd.api.v1+json`,
response: `HTTP/1.1 200 OK
Content-Type: application/vnd.api.v1+json`
},
tips: [
'使用 Vendor MIME 类型:`application/vnd.{company}.{resource}.v{version}+json`',
'需要 API Gateway 或框架支持内容协商',
'GitHub API 使用此策略'
]
},
{
id: 'query-param',
name: 'Query 参数',
example: '/users?version=v1',
stars: 1,
level: 'low',
pros: [
'实现简单'
],
cons: [
'不专业,容易忽视',
'缓存麻烦(不同参数视为不同资源)',
'URL 混乱'
],
codeExample: {
request: `GET /users?version=v1 HTTP/1.1
Host: api.example.com`,
response: `HTTP/1.1 200 OK
Content-Type: application/json`
},
tips: [
'仅用于快速原型或内部工具',
'生产环境不推荐使用'
]
}
]
const selectedStrategy = ref('url-path')
const currentStrategy = computed(() =>
strategies.find(s => s.id === selectedStrategy.value)
)
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.strategies {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.strategy-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.strategy-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
transform: translateY(-2px);
}
.strategy-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.15);
}
.strategy-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.strategy-name {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
}
.strategy-stars {
display: flex;
gap: 2px;
}
.star {
font-size: 12px;
}
.strategy-example {
font-family: monospace;
font-size: 12px;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
padding: 6px 10px;
border-radius: 4px;
margin-top: 8px;
}
.strategy-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 700;
font-size: 16px;
color: var(--vp-c-text-1);
}
.detail-recommendation {
padding: 4px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.detail-recommendation.high {
background: #dcfce7;
color: #16a34a;
}
.detail-recommendation.medium {
background: #fef3c7;
color: #d97706;
}
.detail-recommendation.low {
background: #fee2e2;
color: #dc2626;
}
.detail-sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 16px;
}
.detail-section {
padding: 16px;
}
.detail-section h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.detail-section ul {
margin: 0;
padding-left: 20px;
}
.detail-section li {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 6px 0;
}
.detail-section.example {
grid-column: 1 / -1;
padding: 16px;
background: var(--vp-c-bg-soft);
}
.code-box {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-header {
padding: 8px 12px;
background: var(--vp-c-bg-soft);
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.code-box pre {
margin: 0;
padding: 12px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.code-box code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.detail-section.tips {
background: #eff6ff;
border-left: 3px solid #3b82f6;
}
@media (max-width: 768px) {
.strategies {
grid-template-columns: 1fr;
}
.detail-sections {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,113 +1,154 @@
<template>
<div class="adder-demo">
<div class="demo-header">
<span class="title">加法器用逻辑门做二进制加法</span>
<span class="subtitle">就像手算竖式从个位往高位算逢二进一进位往左传</span>
<span class="icon">🧮</span>
<span class="title">加法器CPU 怎么做加法</span>
<span class="subtitle">从手算竖式理解"逐位计算"的原理</span>
</div>
<div class="control-panel">
<label>
<span class="control-label">A被加数</span>
<input v-model.number="inputA" type="number" min="0" max="15" class="num-input" />
</label>
<span class="op">+</span>
<label>
<span class="control-label">B加数</span>
<input v-model.number="inputB" type="number" min="0" max="15" class="num-input" />
</label>
<span class="eq">=</span>
<span class="result-dec">{{ resultDec }}</span>
</div>
<div class="why-what-box">
<p class="why-p">
<strong>为啥要看这些</strong>CPU 只会处理 0 1所以加法要一位一位每一列 0 1 都需要一个小电路来算这一位写几要不要往左进位
</p>
<p class="what-p">
<strong>这些词是啥</strong>
<span class="term">半加器</span>只算这一位的 A+B最右边没有进位进来
<span class="term">全加器</span> A+B+上一位的进位
<span class="term">S</span>这一位写下的数字0 1
<span class="term">Cout</span>要不要往左边一位进 1进就是 1不进就是 0
</p>
</div>
<div class="example-block">
<div class="example-row">
<span class="example-label">A被加数</span>
<span class="example-bits">
<span v-for="(b, i) in bitsA" :key="'a'+i" class="bit" :class="{ active: highlightedBit === (3 - i) }">{{ b }}</span>
</span>
<span class="example-dec">= {{ inputA }}</span>
<div class="intro-section">
<div class="intro-title">🎯 先看十进制竖式理解"逐位计算"</div>
<div class="decimal-demo">
<div class="decimal-column">
<div class="decimal-row label-row">被加数</div>
<div class="decimal-row num-row">
<span class="d-digit">{{ decimalA }}</span>
</div>
</div>
<div class="decimal-column op-col">
<div class="decimal-row label-row">+</div>
<div class="decimal-row num-row">
<span class="d-digit">{{ decimalB }}</span>
</div>
</div>
<div class="decimal-column">
<div class="decimal-row label-row">结果</div>
<div class="decimal-row num-row result">
<span class="d-digit">{{ decimalA + decimalB }}</span>
</div>
</div>
</div>
<div class="example-row">
<span class="example-label">B加数</span>
<span class="example-bits">
<span v-for="(b, i) in bitsB" :key="'b'+i" class="bit" :class="{ active: highlightedBit === (3 - i) }">{{ b }}</span>
</span>
<span class="example-dec">= {{ inputB }}</span>
</div>
<div class="example-row result-row">
<span class="example-label">结果</span>
<span class="example-bits">
<span v-for="(b, i) in bitsSum" :key="'s'+i" class="bit" :class="{ active: highlightedBit === (3 - i) }">{{ b }}</span>
</span>
<span class="example-dec">= {{ resultDec }}</span>
</div>
<div class="bit-legend">
<span v-for="i in 4" :key="i" class="bit-legend-item">{{ 4 - i }}</span>
<div class="intro-hint">
<span class="icon">💡</span>
<span>手算时我们从<strong>个位往高位</strong>一位一位算<strong>逢十进一</strong>CPU 做加法也一样只是它只认识 0 1所以要<strong>逢二进一</strong></span>
</div>
</div>
<div class="stages-label">逐位计算从右往左 0 3 对应上面每一列</div>
<div class="adder-stages">
<div
v-for="(stage, idx) in stages"
:key="idx"
class="stage"
:class="{ 'stage-highlight': highlightedBit === stage.bitPos }"
@mouseenter="highlightedBit = stage.bitPos"
@mouseleave="highlightedBit = null"
>
<div class="stage-title"> {{ stage.bitPos }} {{ stage.posName }}</div>
<div class="stage-content">
<div class="io-col inputs-col">
<div class="io-row">
<span class="io-badge a-badge">A</span>
<div class="concept-section">
<div class="concept-title">📚 核心概念</div>
<div class="concepts-grid">
<div class="concept-card half-adder">
<div class="concept-name">半加器</div>
<div class="concept-simple">只算 A + B</div>
<div class="concept-detail">
<p>最右边一位用因为<strong>没有进位进来</strong></p>
<p class="formula">输入AB 输出(S)进位(C)</p>
</div>
</div>
<div class="concept-card full-adder">
<div class="concept-name">全加器</div>
<div class="concept-simple"> A + B + 进位</div>
<div class="concept-detail">
<p>其他位用因为<strong>要加上一位的进位</strong></p>
<p class="formula">输入ABCin 输出(S)进位(Cout)</p>
</div>
</div>
</div>
</div>
<div class="demo-section">
<div class="demo-title">🎮 动手试试二进制加法</div>
<div class="control-row">
<label class="input-group">
<span class="input-label">A被加数</span>
<input v-model.number="inputA" type="number" min="0" max="15" class="num-input" />
</label>
<span class="op-sign">+</span>
<label class="input-group">
<span class="input-label">B加数</span>
<input v-model.number="inputB" type="number" min="0" max="15" class="num-input" />
</label>
<span class="op-sign">=</span>
<span class="result-num">{{ resultDec }}</span>
</div>
<div class="binary-display">
<div class="binary-row">
<span class="binary-label">A</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsA" :key="'a'+i" class="bit" :class="{ highlight: activeBit === (3 - i) }">{{ b }}</span>
</span>
<span class="binary-dec">= {{ inputA }}</span>
</div>
<div class="binary-row">
<span class="binary-label">B</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsB" :key="'b'+i" class="bit" :class="{ highlight: activeBit === (3 - i) }">{{ b }}</span>
</span>
<span class="binary-dec">= {{ inputB }}</span>
</div>
<div class="binary-row result-row">
<span class="binary-label">结果</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsSum" :key="'s'+i" class="bit" :class="{ highlight: activeBit === (3 - i) }">{{ b }}</span>
</span>
<span class="binary-dec">= {{ fourBitResult }}</span>
</div>
<div class="bit-labels">
<span v-for="i in 4" :key="i" class="bit-label">{{ 4 - i }}</span>
</div>
</div>
<div class="stages-row">
<div
v-for="(stage, idx) in stages"
:key="idx"
class="stage-card"
:class="{ active: activeBit === stage.bitPos }"
@mouseenter="activeBit = stage.bitPos"
@mouseleave="activeBit = null"
>
<div class="stage-header">
<span class="stage-pos">{{ stage.bitPos }}</span>
<span class="stage-type" :class="stage.carryIn !== null ? 'full' : 'half'">
{{ stage.carryIn !== null ? '全加器' : '半加器' }}
</span>
</div>
<div class="stage-io">
<div class="io-line">
<span class="io-tag a">A</span>
<span class="io-val">{{ stage.a }}</span>
</div>
<div class="io-row">
<span class="io-badge b-badge">B</span>
<div class="io-line">
<span class="io-tag b">B</span>
<span class="io-val">{{ stage.b }}</span>
</div>
<div v-if="stage.carryIn !== null" class="io-row">
<span class="io-badge cin-badge">Cin</span>
<div v-if="stage.carryIn !== null" class="io-line">
<span class="io-tag cin">Cin</span>
<span class="io-val">{{ stage.carryIn }}</span>
</div>
</div>
<div class="fa-box">
<div class="fa-label">{{ stage.carryIn !== null ? '全加器' : '半加器' }}</div>
<div class="fa-hint">{{ stage.carryIn !== null ? 'A+B+进位' : '只算 A+B' }}</div>
</div>
<div class="io-col outputs-col">
<div class="io-row">
<span class="io-badge s-badge" :title="'S = 这一位写下的数'">S</span>
<span class="io-val sum-val">{{ stage.sum }}</span>
<div class="stage-divider"></div>
<div class="stage-io">
<div class="io-line">
<span class="io-tag s">S</span>
<span class="io-val sum">{{ stage.sum }}</span>
</div>
<div class="io-row">
<span class="io-badge cout-badge" :title="'Cout = 往左进 0 还是 1'">Cout</span>
<span class="io-val carry-val">{{ stage.carryOut }}</span>
<div class="io-line">
<span class="io-tag cout">Cout</span>
<span class="io-val">{{ stage.carryOut }}</span>
</div>
</div>
</div>
<div v-if="idx < stages.length - 1" class="carry-hint" :class="{ 'no-carry': !stage.carryOut }">
{{ stage.carryOut ? `进位 ${stage.carryOut} 传给第 ${stage.bitPos + 1} ` : '无进位' }}
<div v-if="idx < 3" class="carry-arrow" :class="{ hasCarry: stage.carryOut }">
{{ stage.carryOut ? ' 进位' : '' }}
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>每位全加器接收 AB 和上一位的进位Cin输出本位的和S与传给下一位的进位Cout和手算竖式逢二进一一致
<span class="icon">💡</span>
<strong>核心思想</strong>每位加法器接收 AB 和上一位的进位输出本位的和与传给下一位的进位就像手算竖式"逢二进一"只是用电路自动完成
</div>
</div>
</template>
@@ -115,7 +156,12 @@
<script setup>
import { ref, computed } from 'vue'
const POS_NAMES = ['最低位', '次低位', '次高位', '最高位']
const decimalA = 35
const decimalB = 47
const inputA = ref(3)
const inputB = ref(2)
const activeBit = ref(null)
function clamp(n) {
const v = Number(n)
@@ -123,10 +169,6 @@ function clamp(n) {
return Math.max(0, Math.min(15, Math.floor(v)))
}
const inputA = ref(3)
const inputB = ref(2)
const highlightedBit = ref(null)
const clampedA = computed(() => clamp(inputA.value))
const clampedB = computed(() => clamp(inputB.value))
@@ -151,7 +193,6 @@ const stages = computed(() => {
}
result.push({
bitPos: i,
posName: POS_NAMES[i],
a,
b,
carryIn: carryIn === null ? null : carryIn,
@@ -171,9 +212,10 @@ const bitsSum = computed(() => {
const fourBitResult = computed(() =>
stages.value.reduce((acc, s, i) => acc + (s.sum << i), 0)
)
const overflow = computed(() => clampedA.value + clampedB.value > 15)
const resultDec = computed(() =>
overflow.value ? `${fourBitResult.value}4 位溢出)` : String(fourBitResult.value)
overflow.value ? `${fourBitResult.value}4位溢出)` : String(fourBitResult.value)
)
</script>
@@ -187,287 +229,391 @@ const resultDec = computed(() =>
}
.demo-header {
margin-bottom: 0.35rem;
}
.demo-header .title {
display: block;
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
display: block;
font-size: 0.82rem;
color: var(--vp-c-text-2);
font-weight: normal;
}
.control-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding: 0.5rem 0;
}
.control-panel label {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.25rem; }
.control-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.num-input {
width: 3rem;
padding: 0.25rem 0.35rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.9rem;
background: var(--vp-c-bg);
}
.control-panel .op,
.control-panel .eq {
font-weight: bold;
color: var(--vp-c-text-2);
}
.control-panel .result-dec {
font-weight: bold;
color: var(--vp-c-brand-1);
}
.why-what-box {
.intro-section {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.65rem 0.85rem;
padding: 0.75rem;
margin-bottom: 0.75rem;
}
.intro-title {
font-weight: bold;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.55;
}
.why-what-box .why-p {
margin: 0 0 0.4rem;
}
.why-what-box .what-p {
margin: 0;
}
.why-what-box .term {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.example-block {
margin-bottom: 0.25rem;
}
.bit-legend {
display: flex;
gap: 0.25rem;
margin-left: 6rem;
margin-top: 0.2rem;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.bit-legend-item {
min-width: 1.2em;
text-align: center;
}
.example-row .example-bits {
display: flex;
gap: 0.25rem;
}
.bit {
display: inline-block;
min-width: 1.2em;
text-align: center;
padding: 0.1rem 0;
border-radius: 3px;
transition: background 0.15s ease;
}
.bit.active {
background: var(--vp-c-brand-2);
color: var(--vp-c-bg);
}
.stage.stage-highlight {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: 1px;
}
.example-row {
.decimal-demo {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.example-label {
color: var(--vp-c-text-2);
min-width: 6rem;
.decimal-column {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.example-bits {
.decimal-column.op-col {
min-width: 2rem;
text-align: center;
}
.decimal-row {
font-size: 0.85rem;
}
.decimal-row.label-row {
color: var(--vp-c-text-3);
font-size: 0.75rem;
}
.decimal-row.num-row {
font-family: monospace;
font-variant-numeric: tabular-nums;
}
.example-dec {
color: var(--vp-c-text-2);
font-variant-numeric: tabular-nums;
}
.result-row .example-bits {
font-size: 1.1rem;
font-weight: bold;
}
.decimal-row.num-row.result {
color: var(--vp-c-brand-1);
}
.stages-label {
font-size: 0.85rem;
font-weight: bold;
margin: 0.75rem 0 0.4rem;
color: var(--vp-c-text-2);
.d-digit {
display: inline-block;
min-width: 1.5rem;
text-align: center;
}
.adder-stages {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
.intro-hint {
display: flex;
align-items: flex-start;
gap: 0.35rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.intro-hint .icon {
flex-shrink: 0;
}
.concept-section {
margin-bottom: 0.75rem;
}
.stage {
.concept-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.concepts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.concept-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.55rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
border-radius: 6px;
padding: 0.6rem;
}
.stage-title {
font-size: 0.72rem;
.concept-name {
font-weight: bold;
color: var(--vp-c-text-2);
text-align: center;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
margin-bottom: 0.15rem;
}
.stage-content {
.concept-simple {
font-size: 0.8rem;
color: var(--vp-c-brand-1);
font-weight: 600;
margin-bottom: 0.25rem;
}
.concept-detail {
font-size: 0.75rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.concept-detail p {
margin: 0;
}
.concept-detail .formula {
margin-top: 0.2rem;
font-family: monospace;
color: var(--vp-c-text-3);
}
.half-adder .concept-name { color: var(--vp-c-brand-1); }
.full-adder .concept-name { color: #8b5cf6; }
.demo-section {
margin-bottom: 0.5rem;
}
.demo-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.control-row {
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
.input-group {
display: flex;
align-items: center;
gap: 0.3rem;
}
.io-col {
display: flex;
flex-direction: column;
gap: 0.22rem;
flex: 1;
min-width: 0;
.input-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.io-row {
.num-input {
width: 3rem;
padding: 0.2rem 0.35rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.85rem;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.op-sign {
font-weight: bold;
color: var(--vp-c-text-2);
}
.result-num {
font-weight: bold;
color: var(--vp-c-brand-1);
font-size: 0.95rem;
}
.binary-display {
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.5rem 0.75rem;
margin-bottom: 0.5rem;
}
.binary-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.2rem;
font-size: 0.85rem;
}
.binary-label {
color: var(--vp-c-text-2);
min-width: 2.5rem;
}
.binary-bits {
display: flex;
gap: 0.2rem;
font-family: monospace;
}
.bit {
display: inline-block;
min-width: 1.2rem;
text-align: center;
padding: 0.1rem 0.15rem;
border-radius: 3px;
transition: all 0.15s ease;
}
.bit.highlight {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-weight: bold;
}
.binary-dec {
color: var(--vp-c-text-3);
font-size: 0.8rem;
margin-left: 0.25rem;
}
.result-row .binary-bits {
font-weight: bold;
color: var(--vp-c-brand-1);
}
.bit-labels {
display: flex;
gap: 0.2rem;
margin-left: 3rem;
margin-top: 0.15rem;
}
.bit-label {
min-width: 1.2rem;
text-align: center;
font-size: 0.65rem;
color: var(--vp-c-text-3);
}
.stages-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.4rem;
}
.stage-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.4rem;
cursor: pointer;
transition: all 0.15s ease;
position: relative;
}
.stage-card.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 1px var(--vp-c-brand-1);
}
.stage-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
padding-bottom: 0.2rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.stage-pos {
font-size: 0.7rem;
font-weight: bold;
color: var(--vp-c-text-2);
}
.stage-type {
font-size: 0.65rem;
font-weight: bold;
padding: 0.1rem 0.25rem;
border-radius: 3px;
}
.stage-type.half {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.stage-type.full {
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
}
.stage-io {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.io-line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.2rem;
}
.io-badge {
font-size: 0.62rem;
.io-tag {
font-size: 0.6rem;
font-weight: bold;
padding: 0.05rem 0.3rem;
border-radius: 3px;
flex-shrink: 0;
padding: 0.05rem 0.2rem;
border-radius: 2px;
color: white;
}
.a-badge { background: var(--vp-c-brand); }
.b-badge { background: #8b5cf6; }
.cin-badge { background: #d97706; }
.s-badge { background: var(--vp-c-success, #16a34a); }
.cout-badge { background: #d97706; }
.io-tag.a { background: var(--vp-c-brand-1); }
.io-tag.b { background: #8b5cf6; }
.io-tag.cin { background: #d97706; }
.io-tag.s { background: var(--vp-c-green-1, #16a34a); }
.io-tag.cout { background: #d97706; }
.io-val {
font-weight: bold;
font-family: monospace;
font-size: 0.85rem;
}
.sum-val { color: var(--vp-c-success, #16a34a); }
.carry-val { color: #d97706; }
.fa-box {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.3rem 0.35rem;
flex-shrink: 0;
}
.fa-label {
font-size: 0.68rem;
font-size: 0.8rem;
font-weight: bold;
}
.fa-hint {
.io-val.sum {
color: var(--vp-c-green-1, #16a34a);
}
.stage-divider {
height: 1px;
background: var(--vp-c-divider);
margin: 0.2rem 0;
}
.carry-arrow {
position: absolute;
right: -0.5rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.6rem;
color: var(--vp-c-text-3);
margin-top: 0.1rem;
}
.carry-hint {
font-size: 0.65rem;
color: #d97706;
text-align: center;
padding: 0.15rem 0;
white-space: nowrap;
}
.carry-hint.no-carry {
color: var(--vp-c-text-3);
.carry-arrow.hasCarry {
font-weight: bold;
}
.info-box {
display: flex;
gap: 0.25rem;
align-items: flex-start;
gap: 0.35rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
padding: 0.6rem 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.info-box strong {
white-space: nowrap;
.info-box .icon {
flex-shrink: 0;
}
@media (max-width: 700px) {
.adder-stages {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (max-width: 600px) {
.stages-row {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 440px) {
.adder-stages {
.concepts-grid {
grid-template-columns: 1fr;
}
}
@@ -1,239 +0,0 @@
<template>
<div class="cpu-arch-demo">
<div class="demo-header">
<span class="title">CPU 架构全貌</span>
<span class="subtitle">从功能单元到完整核心</span>
</div>
<div class="architecture-overview">
<div class="overview-title">核心组件一览静态展示</div>
<div class="overview-grid">
<div v-for="comp in components" :key="comp.name" class="overview-card">
<div class="card-top">
<span class="comp-icon">{{ comp.icon }}</span>
<span class="comp-name">{{ comp.name }}</span>
</div>
<div class="comp-desc">{{ comp.desc }}</div>
<div class="comp-role">{{ comp.role }}</div>
</div>
</div>
</div>
<div class="instruction-flow">
<div class="flow-title">一条指令在 CPU 内部的流动</div>
<div class="flow-steps">
<div
v-for="(step, index) in instructionFlow"
:key="step.name"
class="flow-step"
>
<span class="step-index">{{ index + 1 }}</span>
<span class="step-name">{{ step.name }}</span>
<span class="step-desc">{{ step.desc }}</span>
<span
v-if="index < instructionFlow.length - 1"
class="step-arrow"
aria-hidden="true"
>
</span>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong
>CPU 不是单一部件而是多个功能单元的有序协作控制器负责调度ALU 负责计算寄存器负责高速暂存总线负责连接与传输
</div>
</div>
</template>
<script setup>
const components = [
{
icon: '🎮',
name: '控制器(CU',
desc: '负责取指、解码和发出控制信号',
role: '像指挥员,安排每个模块何时工作'
},
{
icon: '📊',
name: 'ALU',
desc: '执行加减与、或、比较等运算',
role: '像计算器,完成核心算术与逻辑处理'
},
{
icon: '📁',
name: '寄存器组',
desc: '保存当前最常用的数据和中间结果',
role: '像桌面便签,读写速度远高于内存'
},
{
icon: '🚌',
name: '内部总线',
desc: '在模块间传输数据、地址和控制信息',
role: '像高速通道,把各组件连接成整体'
}
]
const instructionFlow = [
{ name: '取指', desc: '控制器从缓存/内存取来指令' },
{ name: '解码', desc: '识别指令类型与需要的操作数' },
{ name: '执行', desc: 'ALU 或其他单元完成具体运算' },
{ name: '写回', desc: '结果写入寄存器,供后续指令使用' }
]
</script>
<style scoped>
.cpu-arch-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.25rem;
margin: 1.25rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.overview-title,
.flow-title {
font-weight: bold;
font-size: 0.92rem;
margin-bottom: 0.5rem;
}
.overview-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
}
.overview-card {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 0.7rem;
background: var(--vp-c-bg-alt);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.card-top {
display: flex;
align-items: center;
gap: 0.45rem;
}
.comp-icon {
font-size: 1rem;
}
.comp-name {
font-weight: bold;
font-size: 0.85rem;
}
.comp-desc {
font-size: 0.78rem;
color: var(--vp-c-text-2);
}
.comp-role {
font-size: 0.78rem;
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
border-radius: 4px;
padding: 0.25rem 0.4rem;
}
.instruction-flow {
margin-top: 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
}
.flow-steps {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.flow-step {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 999px;
padding: 0.35rem 0.55rem;
}
.step-index {
width: 1.1rem;
height: 1.1rem;
border-radius: 50%;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
font-weight: bold;
}
.step-name {
font-size: 0.78rem;
font-weight: bold;
}
.step-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.step-arrow {
margin-left: 0.1rem;
color: var(--vp-c-text-3);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.85rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 680px) {
.overview-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,237 +0,0 @@
<template>
<div class="evolution-flow-demo">
<div class="demo-header">
<span class="title">全景图从沙子到智能</span>
<span class="subtitle">每一层都是对下一层的抽象封装</span>
</div>
<div class="flow-list">
<div v-for="(step, index) in steps" :key="index" class="flow-row">
<!-- 卡片 -->
<div class="step-card">
<div class="card-left">
<span class="step-icon">{{ step.icon }}</span>
</div>
<div class="card-body">
<div class="card-title">{{ step.title }}</div>
<div class="card-desc">{{ step.desc }}</div>
</div>
<div class="card-right">
<span class="card-count">{{ step.count }}</span>
</div>
</div>
<!-- 箭头 -->
<div v-if="index < steps.length - 1" class="flow-arrow">
<div class="arrow-line" />
<div class="arrow-action">{{ step.action }}</div>
<div class="arrow-sym"></div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>计算机的本质是"开关的组合"通过一层层的抽象封装最底层的物理材料最终变成了能执行任意逻辑的通用计算平台
</div>
</div>
</template>
<script setup>
const steps = [
{
icon: '🏖️',
title: '沙子(硅)',
desc: '地球上最丰富的元素之一,提炼出高纯度硅',
count: '原材料',
action: '↓ 提纯 → 切割成晶圆'
},
{
icon: '💿',
title: '硅晶圆',
desc: '直径 30cm 的单晶硅片,表面极其光滑',
count: '基底',
action: '↓ 光刻 → 蚀刻 → 掺杂'
},
{
icon: '⚡',
title: '晶体管(开关)',
desc: 'Gate=1 导通,Gate=0 断开,用电压控制电流',
count: '数百亿个 / 芯片',
action: '↓ 组合成逻辑电路'
},
{
icon: '🔌',
title: '逻辑门',
desc: 'AND / OR / NOT / XOR,实现基本布尔运算',
count: '数十亿个',
action: '↓ 组合成功能模块'
},
{
icon: '🔧',
title: '功能单元',
desc: '加法器、寄存器、多路选择器……各司其职',
count: '数百个',
action: '↓ 集成为完整处理器'
},
{
icon: '🧠',
title: 'CPU 核心',
desc: 'ALU + 控制器 + 寄存器组,执行取指→解码→执行→写回',
count: '1 ~ 128 核',
action: '↓ 软件编程'
},
{
icon: '💻',
title: '软件应用',
desc: '操作系统 / AI 模型 / 游戏 / 网页……一切皆指令',
count: '无限可能',
action: ''
}
]
</script>
<style scoped>
.evolution-flow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.8rem;
flex-wrap: wrap;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.82rem;
margin-left: 0.5rem;
}
/* 整体竖向流程 */
.flow-list {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0;
}
.flow-row {
display: flex;
flex-direction: column;
align-items: stretch;
}
/* 卡片 */
.step-card {
display: flex;
align-items: center;
gap: 0.65rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.65rem 0.8rem;
transition: border-color 0.2s;
}
.step-card:hover {
border-color: var(--vp-c-brand);
}
.card-left {
flex-shrink: 0;
}
.step-icon {
font-size: 1.5rem;
}
.card-body {
flex: 1;
min-width: 0;
}
.card-title {
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.2rem;
}
.card-desc {
font-size: 0.78rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.card-right {
flex-shrink: 0;
text-align: right;
}
.card-count {
font-size: 0.72rem;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 999px;
padding: 0.15rem 0.45rem;
white-space: nowrap;
}
/* 箭头区域 */
.flow-arrow {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.2rem 1rem;
color: var(--vp-c-text-3);
}
.arrow-line {
width: 2px;
height: 0.8rem;
background: var(--vp-c-divider);
margin-left: 1.3rem;
flex-shrink: 0;
}
.arrow-action {
font-size: 0.72rem;
color: var(--vp-c-brand);
font-style: italic;
}
.arrow-sym {
font-size: 0.9rem;
color: var(--vp-c-brand);
margin-left: auto;
margin-right: 0.5rem;
}
/* info box */
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.8rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
+19 -13
View File
@@ -107,9 +107,9 @@ import NetworkTroubleshooting from './components/appendix/web-basics/NetworkTrou
import TransistorDemo from './components/appendix/computer-fundamentals/TransistorDemo.vue'
import LogicGateDemo from './components/appendix/computer-fundamentals/LogicGateDemo.vue'
import AdderDemo from './components/appendix/computer-fundamentals/AdderDemo.vue'
import CpuArchitectureDemo from './components/appendix/computer-fundamentals/CpuArchitectureDemo.vue'
// import CpuArchitectureDemo from './components/appendix/computer-fundamentals/CpuArchitectureDemo.vue'
import RegisterDemo from './components/appendix/computer-fundamentals/RegisterDemo.vue'
import EvolutionFlowDemo from './components/appendix/computer-fundamentals/EvolutionFlowDemo.vue'
// import EvolutionFlowDemo from './components/appendix/computer-fundamentals/EvolutionFlowDemo.vue'
import ProcessDemo from './components/appendix/computer-fundamentals/ProcessDemo.vue'
import MemoryDemo from './components/appendix/computer-fundamentals/MemoryDemo.vue'
import FilesystemDemo from './components/appendix/computer-fundamentals/FilesystemDemo.vue'
@@ -212,6 +212,8 @@ import AttentionMechanismDemo from './components/appendix/ai-history/AttentionMe
import DiscriminativeVsGenerativeDemo from './components/appendix/ai-history/DiscriminativeVsGenerativeDemo.vue'
import GPTEvolutionDemo from './components/appendix/ai-history/GPTEvolutionDemo.vue'
import FoundationDemo from './components/appendix/ai-history/FoundationDemo.vue'
import ExpertSystemWaveDemo from './components/appendix/ai-history/ExpertSystemWaveDemo.vue'
import AIErasComparisonDemo from './components/appendix/ai-history/AIErasComparisonDemo.vue'
// AI Protocols Components
import McpVisualDemo from './components/appendix/ai-protocols/McpVisualDemo.vue'
@@ -551,8 +553,12 @@ import TrafficSchedulingDemo from './components/appendix/cloud-storage-cdn/Traff
import HttpsOptimizationDemo from './components/appendix/cloud-storage-cdn/HttpsOptimizationDemo.vue'
import AccessAnalyticsDemo from './components/appendix/cloud-storage-cdn/AccessAnalyticsDemo.vue'
// API Design Extra Components
import RestfulApiFlow from './components/appendix/api-design/RestfulApiFlow.vue'
// API Design Components
import ApiRequestDemo from './components/appendix/api-design/ApiRequestDemo.vue'
import RestfulUrlDemo from './components/appendix/api-design/RestfulUrlDemo.vue'
import StatusCodeDemo from './components/appendix/api-design/StatusCodeDemo.vue'
import ErrorHandlingDemo from './components/appendix/api-design/ErrorHandlingDemo.vue'
import ApiVersioningDemo from './components/appendix/api-design/ApiVersioningDemo.vue'
// JavaScript Intro Components
import VariableBoxDemo from './components/appendix/javascript-intro/VariableBoxDemo.vue'
@@ -642,7 +648,6 @@ export default {
app.component('ApiDocumentDemo', ApiDocumentDemo)
app.component('ApiPlayground', ApiPlayground)
app.component('RealWorldApiDemo', RealWorldApiDemo)
app.component('RestfulApiFlow', RestfulApiFlow)
// LLM Intro Components Registration
app.component('EmbeddingDemo', EmbeddingDemo)
@@ -711,9 +716,9 @@ export default {
app.component('TransistorDemo', TransistorDemo)
app.component('LogicGateDemo', LogicGateDemo)
app.component('AdderDemo', AdderDemo)
app.component('CpuArchitectureDemo', CpuArchitectureDemo)
// app.component('CpuArchitectureDemo', CpuArchitectureDemo)
app.component('RegisterDemo', RegisterDemo)
app.component('EvolutionFlowDemo', EvolutionFlowDemo)
// app.component('EvolutionFlowDemo', EvolutionFlowDemo)
app.component('ProcessDemo', ProcessDemo)
app.component('MemoryDemo', MemoryDemo)
app.component('FilesystemDemo', FilesystemDemo)
@@ -810,6 +815,8 @@ export default {
app.component('BigFrontendScopeDemo', BigFrontendScopeDemo)
app.component('AiEvolutionDemo', AiEvolutionDemo)
app.component('FoundationDemo', FoundationDemo)
app.component('ExpertSystemWaveDemo', ExpertSystemWaveDemo)
app.component('AIErasComparisonDemo', AIErasComparisonDemo)
app.component('RuleBasedVsLearningDemo', RuleBasedVsLearningDemo)
app.component('PerceptronDemo', PerceptronDemo)
app.component('AIEvolutionTimelineDemo', AIEvolutionTimelineDemo)
@@ -1159,13 +1166,12 @@ export default {
app.component('HttpsOptimizationDemo', HttpsOptimizationDemo)
app.component('AccessAnalyticsDemo', AccessAnalyticsDemo)
// API Design Extra Components Registration
// API Design Components Registration
app.component('ApiRequestDemo', ApiRequestDemo)
app.component('RestfulUrlDemo', RestfulUrlDemo)
app.component('StatusCodeDemo', StatusCodeDemo)
app.component('ErrorHandlingDemo', ErrorHandlingDemo)
app.component('VersioningStrategyDemo', VersioningStrategyDemo)
app.component('DocumentationDemo', DocumentationDemo)
app.component('ResourceAnalogy', ResourceAnalogy)
app.component('RequestStructureDemo', RequestStructureDemo)
app.component('ResponseStructureDemo', ResponseStructureDemo)
app.component('ApiVersioningDemo', ApiVersioningDemo)
// Database Intro Extra Components Registration
app.component('DatabaseEvolutionDemo', DatabaseEvolutionDemo)
@@ -193,7 +193,16 @@
### 4.1 CPU 的核心组件
<CpuArchitectureDemo />
CPU 不是单一部件,而是多个功能单元协作工作:
| 组件 | 做什么 | 类比 |
| ------------ | ------------------------------ | ------------------------ |
| **控制器** | 取指、解码、发出控制信号 | 像指挥员,安排谁何时工作 |
| **ALU** | 加减、与、或、比较等运算 | 像计算器,做算术与逻辑 |
| **寄存器组** | 保存最常用的数据和中间结果 | 像桌面便签,比内存更快 |
| **内部总线** | 在模块间传数据、地址、控制信息 | 像高速通道,把组件连成整体 |
一句话:控制器负责调度,ALU 负责计算,寄存器负责高速暂存,总线负责连接与传输。
### 4.2 CPU 是如何执行指令的?
@@ -231,9 +240,29 @@ CPU 执行一条指令,需要经过四个阶段:
## 5. 总结:从沙子到智能
让我们回顾一下从晶体管到 CPU 的完整路径:
每一层都是对下一层的抽象封装,从沙子到可运行软件的完整路径如下
<EvolutionFlowDemo />
1. **沙子(硅)** — 原材料:地球上最丰富的元素之一,提炼出高纯度硅。
↓ 提纯 → 切割成晶圆
2. **硅晶圆** — 基底:直径约 30cm 的单晶硅片,表面极其光滑。
↓ 光刻 → 蚀刻 → 掺杂
3. **晶体管(开关)** — 数百亿个/芯片:Gate=1 导通,Gate=0 断开,用电压控制电流。
↓ 组合成逻辑电路
4. **逻辑门** — 数十亿个:AND / OR / NOT / XOR,实现基本布尔运算。
↓ 组合成功能模块
5. **功能单元** — 数百个:加法器、寄存器、多路选择器……各司其职。
↓ 集成为完整处理器
6. **CPU 核心** — 1128 核:ALU + 控制器 + 寄存器组,执行取指→解码→执行→写回。
↓ 软件编程
7. **软件应用** — 无限可能:操作系统 / AI 模型 / 游戏 / 网页……一切皆指令。
计算机的本质是「开关的组合」:通过一层层抽象封装,最底层的物理材料最终变成能执行任意逻辑的通用计算平台。
::: tip 核心启示
**计算机的本质是"开关的组合"**
@@ -1,124 +1,309 @@
# API 设计:前后端的通用语言
# API 设计:前后端的"对话协议"
> 💡 **学习指南**:这一章我们聊聊前后端如何高效对话。如果你被后端接口的命名搞晕过,或者不知道该返回 200 还是 404,这篇文章就是为你准备的。我们将通过一个交互式 Demo,带你理解 RESTful API 的设计精髓。
::: tip 🎯 核心问题
**前后端如何高效对话?** 这就像问:餐厅的菜单怎么设计,客人一看就懂?服务员怎么记单,不会出错?上菜怎么规范,客人满意?API 设计解决的就是"对话规则"的问题。
:::
---
## 0. 先问一个问题:你有没有经历过这些噩梦?
**场景一:接口猜谜**
**场景一:接口命名随心所欲**
后端给你一个接口 `/getUser`,你调用了,返回 `null`
你是传错了参数?还是数据库没数据?还是服务器崩了?完全不知道。
```
GET /getUserData
GET /fetchUserInfo
GET /queryUserById
GET /users/query
```
**场景二:状态码撒谎**
四个接口,功能一样,命名风格完全不同。新人入职一脸懵:我该用哪个?
**场景二:错误处理五花八门**
```json
// 有的返回 HTTP 状态码
HTTP/1.1 404 Not Found
// 有的返回 200 + code
HTTP/1.1 200 OK
{ "code": 404, "message": "用户不存在" }
// 有的直接抛异常
HTTP/1.1 200 OK
{ "error": "出错了" }
```
前端不知道该怎么判断请求是否成功。
**场景三:响应结构千人千面**
```json
// 接口 A
{ "data": { ... } }
// 接口 B
{ "result": { ... } }
// 接口 C
{ "content": { ... } }
```
每个接口返回格式都不一样,前端需要针对每个接口单独处理。
---
**好的 API 设计就像餐厅的点餐系统**——菜单清晰、流程规范、出错有提示。
---
## 1. 什么是 API
**API**Application Programming Interface,应用程序编程接口)就是"程序之间对话的约定"。
### 1.1 用餐厅来类比
| 餐厅角色 | 对应概念 | 说明 |
| :--- | :--- | :--- |
| 菜单 | API 文档 | 告诉你有哪些"菜"可以点 |
| 服务员 | HTTP 协议 | 标准化的"对话方式" |
| 后厨 | 服务端 | 按"订单"处理请求 |
| 上菜 | 响应 | 把结果返回给"客人" |
### 1.2 一个完整的 API 请求
👇 **动手试试看**:点击下方按钮,观察一次完整的 API 请求-响应流程:
<ApiRequestDemo />
---
## 2. RESTful 设计:让 URL 会说话
**REST**Representational State Transfer)是一种架构风格,核心思想是:
- 把网络上的事物抽象为"资源"Resource
- 用 URL 标识资源
- 用 HTTP 方法操作资源
### 2.1 用仓库来类比
| 仓库概念 | REST 对应 | 示例 |
| :--- | :--- | :--- |
| 货架地址 | URL | `/users``/orders` |
| 操作方式 | HTTP 方法 | GET(查看)、POST(入库) |
| 货物 | 资源 | 用户数据、订单数据 |
**关键原则**URL 是名词,不是动词。
### 2.2 URL 设计规则
👇 **动手试试看**:点击下方按钮,查看 RESTful URL 的正确与错误写法对比:
<RestfulUrlDemo />
### 2.3 HTTP 方法选择
| 方法 | 用途 | 幂等性 | 安全性 | 典型场景 |
| :--- | :--- | :--- | :--- | :--- |
| **GET** | 获取资源 | 是 | 是 | 查询列表、查看详情 |
| **POST** | 创建资源 | 否 | 否 | 新增用户、提交订单 |
| **PUT** | 全量更新 | 是 | 否 | 替换整个用户资料 |
| **PATCH** | 部分更新 | 否 | 否 | 只修改昵称 |
| **DELETE** | 删除资源 | 是 | 否 | 删除用户、取消订单 |
::: tip 💡 什么是幂等性?
**幂等性**:多次执行结果相同。
- **幂等的操作**GET/PUT/DELETE):点 10 次和点 1 次,结果一样
- **不幂等的操作**(POST):点 10 次,可能创建 10 个订单
**解决方案**:POST 操作用唯一 ID 校验,避免重复处理。
:::
---
## 3. 状态码:让错误"会说话"
HTTP 状态码是服务器告诉客户端"发生了什么"的标准方式。
### 3.1 状态码分类
| 分类 | 含义 | 典型状态码 |
| :--- | :--- | :--- |
| **2xx** | 成功 | 200 OK、201 Created、204 No Content |
| **3xx** | 重定向 | 301 永久移动、304 未修改 |
| **4xx** | 客户端错误 | 400 参数错误、401 未认证、404 不存在 |
| **5xx** | 服务端错误 | 500 内部错误、503 服务不可用 |
### 3.2 常用状态码演示
👇 **动手试试看**:点击下方按钮,了解常见状态码的含义:
<StatusCodeDemo />
---
## 4. 错误处理:优雅地"拒绝"
好的错误处理能让客户端"看状态码就知道怎么回事",而不是去猜。
### 4.1 错误处理的"避坑指南"
**坑 1:所有错误都返回 200**
```json
// ❌ 错误做法
HTTP/1.1 200 OK
{ "error": "出错了" }
```
问题:缓存层会缓存这个"成功"响应,监控系统发现不了问题。
**坑 2:错误信息太笼统**
```json
// ❌ 错误做法
HTTP/1.1 400 Bad Request
{ "message": "参数错误" }
```
问题:客户端不知道哪个参数错了、为什么错。
**坑 3:暴露敏感信息**
```json
// ❌ 危险做法
HTTP/1.1 500 Internal Server Error
{ "stack": "at UserService.login...", "sql": "SELECT * FROM..." }
```
危险:暴露了代码结构、数据库查询,攻击者可以利用这些信息。
### 4.2 正确的错误处理演示
👇 **动手试试看**:对比"好的"和"差的"错误响应设计:
<ErrorHandlingDemo />
---
## 5. 版本控制:API 的"向后兼容"
### 5.1 为什么要版本控制?
场景:你的 App 有 100 万用户,需要修改订单接口。
**如果不做版本控制**
- 新 App 调用新接口 → 正常
- 旧 App 调用新接口 → 字段缺失,崩溃!
**正确的做法**
- `/v1/orders` - 旧接口,继续服务旧 App
- `/v2/orders` - 新接口,新功能在这里
### 5.2 版本控制策略
| 策略 | 示例 | 优点 | 缺点 |
| :--- | :--- | :--- | :--- |
| **URL 路径** | `/v1/users` | 直观、易缓存 | URL 变长 |
| **请求头** | `Accept: vnd.api.v2+json` | URL 干净 | 不便调试 |
| **查询参数** | `/users?version=2` | 简单 | 不够标准 |
### 5.3 版本控制演示
👇 **动手试试看**:了解 API 版本控制的策略和最佳实践:
<ApiVersioningDemo />
---
## 6. 响应结构:标准化的"数据契约"
无论成功还是失败,响应结构应该保持一致:
### 6.1 标准响应格式
你收到了一个 HTTP 200 OK 的响应,心想“稳了”。
结果打开 Body 一看:
```json
{
"code": 500,
"msg": "系统内部错误",
"data": null
"code": 0,
"message": "success",
"data": { ... },
"request_id": "req-550e8400",
"timestamp": "2024-01-15T09:30:00.000Z"
}
```
浏览器缓存了它,监控系统认为它成功了,只有你的前端代码在风中凌乱。
**场景三:版本地狱**
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `code` | number | 业务状态码,0 表示成功 |
| `message` | string | 状态描述 |
| `data` | any | 业务数据 |
| `request_id` | string | 请求唯一标识,用于问题追踪 |
| `timestamp` | string | 响应时间戳 |
项目迭代了三年,你的代码里充满了这样的 URL:
- `/api/v1/user/update`
- `/api/v2/user/update_new`
- `/api/user/update_final_real`
### 6.2 分页响应格式
```json
{
"code": 0,
"data": {
"items": [...],
"pagination": {
"page": 1,
"page_size": 20,
"total": 156,
"total_pages": 8
}
}
}
```
::: tip 💡 为什么要 request_id
**request_id** 是问题追踪的关键:
1. 用户反馈:"支付失败,错误 ID 是 abc123"
2. 技术人员直接在日志里搜索 abc123,立即定位问题
3. 分布式系统中,每个服务都记录相同的 request_id,可以把所有相关日志聚合起来
:::
---
**API 设计就是为了解决这些问题。**
## 7. 实战:电商系统 API 设计示例
它就像餐厅的菜单和点餐流程:规定了我们**怎么点菜(请求)**、**怎么上菜(响应)**、**没菜了怎么办(错误处理)**。
```
# 用户模块
GET /v1/users # 获取用户列表
POST /v1/users # 创建新用户
GET /v1/users/{id} # 获取用户详情
PUT /v1/users/{id} # 全量更新用户
PATCH /v1/users/{id} # 部分更新用户
DELETE /v1/users/{id} # 删除用户
目前最流行的设计风格是 **RESTful**
# 订单模块
GET /v1/users/{id}/orders # 获取某用户的订单
POST /v1/orders # 创建订单
GET /v1/orders/{id} # 获取订单详情
PATCH /v1/orders/{id}/status # 更新订单状态
# 商品模块(复杂过滤用查询参数)
GET /v1/products?category=phone&price_max=5000&sort=price_desc&page=1
```
---
## 1. 核心概念:RESTful 是什么?
## 名词速查表
REST (Representational State Transfer) 听起来很学术,其实核心就三句话:
1. **资源 (Resource)**:网络上的所有东西都是资源(用户、订单、商品)。
2. **统一接口 (Uniform Interface)**:用标准的 HTTP 方法(GET, POST, DELETE)来操作这些资源。
3. **无状态 (Stateless)**:每次请求都包含所有必要信息,服务器不记“你是谁”(除非你带了 Token)。
### 比喻:餐厅点餐
- **URL 是桌号**`/tables/5` (资源地址)
- **HTTP 方法是动作**
- `GET`:看菜单
- `POST`:下单
- `PUT`:换一桌菜
- `DELETE`:吃完走人
---
## 2. 交互演示:RESTful API 全流程
别光听概念,我们来动手玩一下。
下面是一个模拟的“用户管理系统”。试着点击不同的场景,观察 **客户端发出了什么** 以及 **服务端返回了什么**
<RestfulApiFlow />
### 💡 观察重点
1. **URL 是名词**:注意看 URL 都是 `/users` 或者 `/users/1`,没有动词(如 `/getUsers`)。因为 HTTP 方法(GET/POST)已经表示了动作。
2. **状态码会说话**
- 创建成功返回 `201 Created`,而不是 200。
- 删除成功返回 `204 No Content`(没有 Body)。
- 找不到返回 `404 Not Found`
3. **复数形式**:通常使用 `/users` 而不是 `/user`,表示这是“用户集合”下的资源。
---
## 3. 设计黄金法则
### 3.1 URL 设计:让路径清晰
| 法则 | 正确 ✅ | 错误 ❌ | 原因 |
| :--- | :--- | :--- | :--- |
| **用名词,不用动词** | `GET /products` | `GET /getProducts` | HTTP 方法已经是动词了 |
| **用复数** | `/users/1` | `/user/1` | 保持一致性,`/users` 代表集合 |
| **层级不要太深** | `/users/1/orders` | `/users/1/orders/2/items/3` | 超过 3 层建议拆分或用查询参数 |
| **使用连字符** | `/user-profiles` | `/userProfiles` | URL 对大小写敏感,连字符更易读 |
### 3.2 HTTP 方法:动作要有语义
- **GET** (查):**安全且幂等**。不管调用多少次,服务器状态不变。
- **POST** (增)**不安全,不幂等**。调用 10 次可能创建 10 个用户。
- **PUT** (改-全量)**幂等**。把 ID=1 的用户替换为新数据,替换 10 次结果一样。
- **PATCH** (改-局部):通常用于只修改一个字段(如只改密码)。
- **DELETE** (删)**幂等**。删除 ID=1 的用户,删 1 次和删 10 次,结果都是“用户没了”。
### 3.3 状态码:别只用 200
| 类别 | 状态码 | 含义 | 场景 |
| :--- | :--- | :--- | :--- |
| **2xx 成功** | 200 OK | 通用成功 | GET, PUT |
| | 201 Created | 创建成功 | POST |
| | 204 No Content | 成功但无返回 | DELETE |
| **4xx 客户端错** | 400 Bad Request | 参数错 | 必填项没填,格式不对 |
| | 401 Unauthorized | 未登录 | 没有 Token 或 Token 过期 |
| | 403 Forbidden | 无权限 | 普通用户想删管理员 |
| | 404 Not Found | 找不到 | URL 错了或 ID 不存在 |
| **5xx 服务端错** | 500 Internal Error | 崩了 | 代码抛异常了,数据库挂了 |
---
## 4. 总结
好的 API 设计是**“自解释”**的。
当你的前端同事看到 `DELETE /api/orders/123`,他不需要问你,就应该知道:
1. 这是一个删除操作。
2. 操作对象是 ID 为 123 的订单。
3. 如果成功,应该收到 204 或 200。
4. 如果失败,应该去看状态码是 4xx 还是 5xx。
这就是**约定优于配置**的力量。
| 名词 | 英文 | 解释 |
| :--- | :--- | :--- |
| **API** | Application Programming Interface | 程序之间对话的约定 |
| **REST** | Representational State Transfer | 一种架构风格,用 URL 标识资源 |
| **资源** | Resource | REST 架构的核心概念,有唯一标识(URL) |
| **幂等性** | Idempotency | 多次执行结果相同 |
| **状态码** | Status Code | HTTP 协议定义的响应状态 |
| **版本控制** | Versioning | 让新旧 API 并存,平滑升级 |
| **请求体** | Request Body | POST/PUT/PATCH 请求携带的数据 |
| **响应体** | Response Body | 服务器返回的数据 |
| **Header** | Header | 请求/响应的元数据(如 Content-Type |
| **认证** | Authentication | 验证"你是谁"(登录、Token |
| **授权** | Authorization | 验证"你能做什么"(权限) |
@@ -5,150 +5,189 @@ description: 'AI 发展 70 年,经历了三次浪潮、两次寒冬,最终
# AI 简史:从符号逻辑到千亿参数大模型
AI 发展 70 年,经历了**三次浪潮、两次寒冬**,从符号主义的逻辑推演,到连接主义的神经网络,再到行为主义的强化学习,最终融合为今天的大模型时代。以下是清晰的发展脉络与关键里程碑
AI 发展 70 年,经历了**三次浪潮、两次寒冬**,从符号主义的逻辑推演,到连接主义的神经网络,再到行为主义的强化学习,最终融合为今天的大模型时代。了解 AI 的历史,能帮助我们看清今天大模型"智能"的本质来源
<AiEvolutionDemo />
<DiscriminativeVsGenerativeDemo />
---
## 一、理论奠基与符号主义的诞生(1940s-1950s
### 核心人物与理论
在计算机真正普及之前,先驱者们就开始思考"机器能否像人一样思考"。这个时期的研究主要集中在脑神经的数学建模、计算理论的探讨以及逻辑推理的自动化。1956 年的达特茅斯会议,正式宣告了"人工智能"Artificial Intelligence)作为一个独立学科的诞生。
- **1943 年**:沃伦・麦卡洛克与沃尔特・皮茨提出 **MP 神经元模型**,首次用数学描述神经网络
- **1950 年**:艾伦・图灵发表《计算机器与智能》,提出**图灵测试**,定义机器智能标准
- **1956 年**:**达特茅斯会议**,约翰・麦卡锡首次提出"人工智能"概念,标志 AI 学科正式诞生
<FoundationDemo />
::: tip 符号主义兴起
**符号主义**(逻辑主义/计算机学派)主张 **智能 = 符号推理**,将知识编码为符号,通过逻辑规则推导解决问题,是**自上而下**的智能模拟路径。
### 1.1 核心理论与里程碑事件
- **神经网络的最初设想(1943)**:神经生理学家沃伦·麦卡洛克(Warren McCulloch)和数学家沃尔特·皮茨(Walter Pitts)提出了 **MP 神经元模型**。他们首次尝试用简单的数学公式抽象人类大脑神经元的工作机制,证明了"神经元网络是可以计算的",这成为了今天所有深度网络的老祖宗。
- **图灵的终极追问(1950)**:计算机科学之父艾伦·图灵(Alan Turing)发表了一篇改变历史的论文《计算机器与智能》,提出了著名的**图灵测试**。他避开了"什么是智能"的哲学争论,给出了一个务实的操作标准:如果一台机器在对话中能让人类无法分辨它是人还是机器,它就具备了智能。
- **学科的正式确立(1956)**:在达特茅斯的夏季研讨会上,约翰·麦卡锡(John McCarthy)、马文·明斯基(Marvin Minsky)等年轻学者齐聚一堂。麦卡锡在提案中首次使用了"Artificial Intelligence"这一术语,这一年因此被称为 AI 元年。
::: tip 符号主义(Symbolism)的兴起
在早期的 AI 研究中,**符号主义**占据了绝对的主导地位。由于当时的计算机主要依靠逻辑电路运行,学者们自然地认为:**智能的本质就是符号的推演**。
只要我们把世界上的知识变成计算机能看懂的符号(如概念、规则),再用逻辑推理引擎(如 IF-THEN 规则)去处理这些符号,机器就能像人一样思考。这是一种**自上而下**的路径,高度依赖人类专家的知识输入。
:::
### 早期突破
- **1956 年**:纽厄尔和西蒙开发**逻辑理论家**(Logic Theorist),首个能证明数学定理的 AI 程序
- **1958 年**:麦卡锡发明 **LISP 语言**,成为 AI 研究的重要工具
- **1959 年**:乔治・德沃尔与约瑟夫・恩格尔伯格开发首台**工业机器人**,标志 AI 从理论走向应用
---
## 二、符号主义黄金时代与第一次 AI 浪潮(1960s-1970s
### 专家系统的辉煌
在诞生后的最初十几年里,AI 迎来了一段盲目乐观的黄金时期。研究者们相信,既然机器已经能证明数学定理,那写出能够解决任何人类问题的程序指日可待。
符号主义在**专家系统**领域取得巨大成功,通过将领域专家知识编码为规则库,解决特定领域复杂问题。
<ExpertSystemWaveDemo />
| 时间 | 标志性成果 | 意义 |
### 2.1 专家系统的光辉岁月
符号主义的集大成者是**专家系统(Expert Systems)**。通过向计算机输入各个领域顶级专家的"经验法则(Rule)",系统就能在某些特定垂直领域执行高水平的诊断或决策。
| 专家系统 | 诞生年份 | 历史意义与实际价值 |
| --- | --- | --- |
| **1965 年** | Dendral 系统 | 首个专家系统,用于化学分子结构分析 |
| **1977 年** | MYCIN 系统 | 诊断血液感染的专家系统,准确率达 69% |
| **1980 年** | XCON 系统 | 为 DEC 公司配置计算机,节省 4000 万美元/年 |
| **Dendral** | 1965 年 | **首个专家系统**,它能根据质谱数据推断化学分子结构,性能比肩人类化学专家。 |
| **MYCIN** | 1977 年 | 用于诊断血液感染并推荐抗生素,准确率达 69%,甚至超过了当时的许多非专业医生。 |
| **XCON** | 1980 年 | 早期最成功的商用专家系统,用于帮助数字设备公司(DEC)根据客户需求自动配置计算机系统,每年为公司节省 4000 万美元 |
### 第一次 AI 寒冬(1974-1980
然而,专家系统风光的背后,隐藏着无法逾越的鸿沟。
::: warning ❄️ 第一次 AI 寒冬
符号主义局限性显现:
- **知识获取瓶颈**:规则需人工编写,无法自动获取
- **脆性问题**:难以处理例外情况,稍微偏离规则就崩溃
- **计算能力不足**:当时的硬件无法支撑复杂推理
### 2.2 第一次 AI 寒冬(1974-1980
美国 DARPA 削减 AI 研究经费,AI 进入第一次低谷期
随着时间推移,人们发现"把人类知识写成规则"这条路越走越窄
::: warning ❄️ 第一次 AI 寒冬的爆发原因
符号主义的三大致命局限,最终导致了研究经费被全面撤销:
1. **知识获取瓶颈(Knowledge Acquisition Bottleneck**:有些知识人类也说不清(比如怎么认出一只猫),这被称为"波兰尼悖论"。专家系统只能硬编码那些能被清晰表达的规则,无法自动学习。
2. **"常识"的匮乏与脆性(Brittleness)**:当遇到规则库之外的边缘情况时,哪怕只是稍微偏离预期,系统就会表现出错乱,完全没有人类的"常识"底线。
3. **组合爆炸(Combinatorial Explosion**:现实世界的变量太多,当涉及复杂现实时,穷举规则所需的计算量会呈指数级爆炸,当时的硬件根本无法支撑。
:::
---
## 三、专家系统复兴与第二次 AI 浪潮(1980s
## 三、专家系统(把人类经验翻译成代码的程序)与第二次 AI 浪潮(1980s
### 商业应用爆发
到了 80 年代,随着微型计算机和专业 LISP 机器的普及,专家系统再次受到商业界的追捧。日本政府甚至抛出了雄心勃勃的"第五代计算机计划",试图打造能听懂自然语言的智能机器,引发了全球范围内的恐慌性跟投。
- 日本"**第五代计算机计划**"1982)推动全球 AI 投资热潮
- 美国 DEC、IBM 等公司推出商用专家系统开发工具
- 符号主义达到巅峰,成为 AI 领域绝对主流
### 3.1 商业应用的爆发与破灭
### 连接主义的早期尝试
在这个时代,几乎每家大型跨国公司都在研发自己的**专家系统(一种把人类专家的经验翻译成成千上万条 IF-THEN 代码的程序)**。然而,维护这些系统变得极其折磨人。规则库突破几万条后,修改一条新规则经常会导致另外十条旧规则产生冲突。随着 80 年代末通用个人电脑(PC)性能的爆发,昂贵且封闭的专用 AI 机器变得毫无竞争力。
::: warning ❄️ 第二次 AI 寒冬(1987-1993
1987 年,AI 硬件市场彻底崩盘。"第五代计算机计划"因为过度脱离实际硬件架构而最终烂尾。企业在专家系统上砸的钱打了水漂,AI 研究再次跌入底谷,"人工智能"这个词甚至在学术界成了骗经费的贬义词。
:::
### 3.2 黑暗中蛰伏的连接主义
在这两次起伏中,其实还存在着另一套完全不同的思路——**连接主义(Connectionism)**,也就是我们今天所说的**神经网络**。
<PerceptronDemo />
- **1958 年**罗森布拉特发明**感知机**,首个可学习的神经网络模型
- **1969 年**:明斯基与佩珀特出版《感知机》,指出单层感知机无法解决**异或问题**,导致连接主义研究陷入停滞
### 第二次 AI 寒冬(1987-1993
::: warning ❄️ 第二次 AI 寒冬
- 专家系统**维护成本高昂**,难以扩展到复杂领域
- 个人电脑崛起,第五代计算机计划失败
- AI 市场崩盘,研究经费再次大幅削减
:::
连接主义早在 1958 年就由罗森布拉特Frank Rosenblatt)以**感知机(Perceptron)**的形式提出。它模拟大脑通过调整神经元之间连接的权重来进行学习。与其教给机器明确的"规则",不如给机器看大量的"例子",让它自己归纳。不过,1969 年明斯基在《感知机》一书中用严密的数学证明了当时单层网络的局限(无法解决简单的异或问题)。这使得连接主义在符号主义的黄金时代一直坐冷板凳。直到历史的车轮前进到 90 年代。
---
## 四、机器学习兴起与连接主义复苏(1990s-2000s
### 符号主义衰落,机器学习崛起
进入 90 年代后,AI 领域出现了一个重要的务实转向。大家不再天天谈论如何实现"像人类一样的魔法智能",而是把重心放在了如何利用**严密的数据统计方法**,解决现实生活中的分类和预测问题。这也就是传统**机器学习(Machine Learning**的兴起。
- **1997 年**IBM **深蓝** 击败国际象棋世界冠军卡斯帕罗夫,是符号主义最后的辉煌
- 同时,**统计机器学习**开始取代基于规则的方法,支持向量机(SVM)、决策树等算法成为主流
### 4.1 从死板规则到"寻找数学边界"
### 连接主义的重生
1997 年,虽然 IBM 的"深蓝(Deep Blue)"击败了国际象棋世界冠军卡斯帕罗夫,为符号主义拿下了举世瞩目的荣光,但学术界立刻意识到,这只是一次"算力+海量硬编码"的胜利,深蓝并没有真正理解什么是下棋。
与此同时,以**支持向量机(SVM)**、决策树、随机森林为代表的经典机器学习算法异军突起,成为了接下来长达十余年的绝对主流。
如果说以前的专家系统是教电脑:"如果邮件里包含'中奖',那么就是垃圾邮件",那么**机器学习的思路就是:人类先设定好几个核心特征(特征工程)**,比如"邮件长度"、"特殊词汇频率"、"发件人可信度",然后把上万封标注好的邮件输入给电脑。在这个多维空间里,**支持向量机(SVM)**就像是一个拿着尺子的数学家,它会利用严密的核函数推演,在正常的邮件和垃圾邮件之间,精准地画出一条"最宽、最安全的数学分界线"。
尽管支持向量机在许多任务上大获成功,但它存在一个致命弱点:**特征工程(Feature Engineering)高度依赖人类。** 比如要识别一张猫的图片,人类科学家必须教机器"先提取边缘"、"再寻找三角形的耳朵",机器自己是找不出猫的样子的!这导致了模型能力的上限被人类的认知牢牢锁住。
### 4.2 反向传播让神经网络重见天日
深度学习的真正基础在这个时期被打下:
<BackpropagationDemo />
- **1986 年**:鲁梅尔哈特等人提出**反向传播算法**,解决多层神经网络训练难题
- **1997 年**:李飞飞创立 **ImageNet 数据集**,为后续深度学习提供数据基础
- **2006 年**:杰弗里・辛顿提出**深度信念网络**,通过逐层预训练解决梯度消失问题,开启深度学习时代
在这段蛰伏期,杰弗里·辛顿(Geoffrey Hinton)等人进一步明确了**反向传播(Backpropagation)**的核心价值:当多层神经网络得出错误预测时,能够将这种误差像水波一样,一层层倒推回去,告诉每一个隐藏层的老神经元:"你在这次错误中到底需要承担多大责任,下次赶紧改过来!"
这最终打破了 60 年代对神经网络的禁锢,使得具有隐藏层的网络成为可能。但由于当时数据太少,硬件太弱(连好点的显卡都没有),神经网络还无法全面战胜 SVM 等传统机器学习模型。直到 **三大引爆点** 的齐聚。
---
## 五、深度学习革命与连接主义主导(2010s)
### 关键技术突破
2010 年代,随着**大数据(如 ImageNet 项目)的成熟**、**算力爆发(GPU 大规模应用于并行计算)**以及**算法上的改良(解决梯度消失难题)**,"深度学习"轰轰烈烈地拉开了第三次 AI 浪潮的序幕。
**什么是深度学习与传统机器学习的本质区别?标志就是:特征自动提取(表征学习)。** 只要网络层数足够深(几十层到上百层),神经网络能够直接吃进最原始的像素,它的底层自己学会了识别线条,中层学会了识别毛发纹理,高层直接认出了这是一只"猫"。在这场革命中,傲慢的人类终于放权,让网络自己去寻找最重要的视觉、语音和文本特征。
### 5.1 图像与竞技的全面突破
2012 年,由辛顿带领团队研发的 **AlexNet(经典的卷积神经网络 CNN)** 参加了著名的 ImageNet 图像分类比赛。在别人还在苦苦用传统方法提取手工视觉特征时,AlexNet 直接暴力降维打击,将错误率从 26% 瞬间腰斩到 15.3%,震惊了整个传统计算机视觉学界。由于这种绝对统治力,在往后的几年里,几乎没有任何一篇不使用深度学习的论文能被顶级会议录用!
随后几年,AI 技术每分每秒都在狂飙:
<NeuralNetworkVisualizationDemo />
| 时间 | 突破 | 影响 |
| 突破年份 | 标志性成就 | 深远影响 |
| --- | --- | --- |
| **2012** | AlexNet 在 ImageNet 竞赛中错误率降至 15.3% | 标志深度学习超越传统方法,引爆计算机视觉革命 |
| **2014** | GAN(生成对抗网络)提出 | AI 具备生成逼真图像、音频能力,推动生成式 AI 发展 |
| **2015** | ResNet(残差网络)解决深层网络训练难题 | 网络层数突破 1000 层,进一步提升模型性能 |
| **2016 年** | AlphaGo 击败围棋世界冠军李世石 | 结合深度强化学习与蒙特卡洛树搜索,实现复杂决策能力 |
| **2017 年** | **Transformer 架构**发布 | 基于自注意力机制,解决长距离依赖问题,为大模型奠定基础 |
| **2014** | **GAN(生成对抗网络)**提出 | 两个网络"左右互搏"(一个造假,一个打假),让 AI 开始具备生成惊艳且逼真图像的能力。 |
| **2015** | **ResNet(残差网络)**问世 | 创新性地引入"捷径"结构,解决了网络加深后根本无法正常训练的问题,使神经网络动辄能堆叠几百上千层。 |
| **2016** | **AlphaGo** 击败李世石 | 深度学习与**强化学习**结合的巅峰,打破了"机器永远下不过人类围棋"的断言,轰动全球。 |
::: tip 行为主义(Behaviorism)与强化学习
AlphaGo 代表了另一个学派——**行为主义**的胜利。它认为智能来源于主体与环境的动态交互,就像训练一只小狗坐下:它做对了给奖励,做错了给惩罚。通过在巨大的虚拟环境中不断自行试错、对弈,AlphaGo 总结出了连人类顶级棋手都不曾发觉的策略。
:::
### 5.2 Transformer:孕育大模型的摇篮
2017 年,一切的命运齿轮开始转动。Google 在论文《Attention Is All You Need》中提出了一种全新的深度学习架构——**Transformer**。
<AttentionMechanismDemo />
::: tip 行为主义的发展
**行为主义**(进化主义)主张智能来自与环境的互动,通过试错学习优化行为,**强化学习**是其核心技术。AlphaGo 就是深度学习与强化学习结合的代表作。
:::
以前处理一句话时(比如 RNN 模型),AI 只能从左到右一个个词看,看完了后面的容易忘了前面的。而 Transformer 的**自注意力机制(Self-Attention)**彻底打破了这个限制:它能让 AI"一眼看全"整句话,并在看到"苹果"这个词时,自动根据上下文判断这是指水果,还是指乔布斯的手机公司。
它天生就适合并行计算,吃得下无限多的数据,也能够被堆叠得无尽庞大。这一刻,大模型(LLM)的地基打完了。
---
## 六、大模型时代与通用智能曙光(2018 至今)
### 预训练模型范式确立
当 Transformer 遇见了不计成本的疯狂算力与海量的数据,AI 开发的历史范式被永远改变了。科学家们发现了一个惊人的现象:基于自注意力的架构好像永远也"喂不饱"。以前的深度学习模型,聪明程度会遇到天花板,但 Transformer 能够完美适配 GPU 的大规模并行计算,只要给它的数据越多、网络层数越深,它的表现就能无限提升。
### 6.1"预训练+微调"范式的确立:从专才到通才
原本我们做 AI,是"一项任务配一个小模型":做翻译的专门训练翻译模型,聊天的专门训练聊天模型,就像培养一个个只会一门手艺的"专才"。但到了 2018 年,随着 OpenAI 的 **GPT-1** 和 Google 的 **BERT** 的发布,情况变成了**"大力出奇迹"**的新范式。
首先是**预训练(Pre-training)**,这构成了大语言模型 99% 的核心智力。科学家们把全人类在互联网上遗留的数万亿字的文章、名著典籍、计算机代码甚至百科知识,全部倾倒进庞大的 Transformer 网络里。而给它的训练任务,却仅仅是简单的**"文字接龙"(预测下一个词)**。
为了能无比精准地预测人类语言中的各种"下一个词",模型被迫在其成百上千亿的神经元参数中,自行内化并浓缩了整个世界的运作规律!它不仅彻底掌握了主谓宾语法,知道了"苹果"是一种红色的水果,还能掌握"牛顿因为苹果坠落而发现万有引力"的背后逻辑。这就像一个孩童没有刻意背诵过语法书,却依靠广泛地阅读千万本藏书,自动拥有了理解复杂世界的能力。
<GPTEvolutionDemo />
- **2018 年**OpenAI 发布 **GPT-1**1.17 亿参数),谷歌发布 **BERT**,确立"**预训练 + 微调**"新范式
- **2019 年****GPT-2**(15 亿参数)展现惊人的文本生成能力,引发对 AI 伦理的广泛讨论
- **2020 年****GPT-3**1750 亿参数)通过"暴力美学"展现**涌现能力**,无需微调即可完成多种任务
从 GPT-215亿参数)到 GPT-3(1750亿参数),科学家们震撼地发现了**涌现能力(Emergent Abilities)**——当模型足够巨大时,量变引起了可怕的质变。即使未经任何刻意训练,巨量参数的模型自己"悟"出了逻辑推理、代码编写和上下文学习的能力。这根本不需要人类专门通过代码去教它。
### 生成式 AI 爆发
### 6.2 生成式 AI 爆发与 ChatGPT 的核爆时刻
- **2022 年 11 月****ChatGPT**GPT-3.5)发布,通过 RLHF(人类反馈强化学习)大幅提升对话能力,成为现象级产品
- **2023 年 3 月****GPT-4** 发布,具备**多模态能力**(文本 + 图像),进一步提升逻辑推理与安全性
- **2023**Stable Diffusion、Midjourney 等图像生成模型兴起,多模态大模型成为主流
- **2024 年****Sora** 等视频生成模型发布,AI 生成能力扩展到动态内容领域
在拥有了一个满腹经纶、藏有世界常识的巨大预训练模型后,距离打造出一个完美的个人 AI 助理还差最后一步:**微调(Fine-tuning)**。因为预训练的模型只习惯盲目地做文字续写,它听懂使用者的"指令",也不知道该如何规矩地进行一问一答的交互。
2022 11 月,OpenAI 巧妙地引入了 **RLHF(基于人类反馈的强化学习)** 技术。他们雇佣了大批专家,对于模型的回答进行打分和纠正。这就好比给一个极其聪慧但口无遮拦的天才,设立了明确的沟通边界与礼仪指引,强行将其塑造成了一个温和、有条理且懂事的对话助手。于是,**ChatGPT** 诞生了。
一夜之间,AI 不再是枯燥的实验室玩具,而是成为了每个普通人手中的通用智慧大脑。
随后开启了波澜壮阔的多模态纪元:
* **2023 年:多重感官的打通。** 以 Midjourney、Stable Diffusion 为代表的生图模型重塑了数字艺术产业。同年发布的 **GPT-4** 则融合了极高难度的视觉图像理解与长程逻辑关联推理能力系统。
* **2024 年爆发至今:对物理世界的模拟。** 随着 Sora 等逼真视频生成模型的发布,以及实时端到端语音大模型在情感音色上的全面落地,AI 从单纯处理文本,迅速张开了对包含三维空间、光影流转甚至细腻声调情感的完整世界的全面感知。
---
## 七、AI 三大学派的融合与未来展望
<DiscriminativeVsGenerativeDemo />
回顾这70年,从让机器推理数学定理(符号主义),到寻找统计学边界(传统机器学习),到在试错中下围棋获胜(行为主义/强化学习),再到吞噬海量数据涌现出常识的大模型(连接主义的极致形态),人工智能的发展从未停歇。
### 未来趋势
今天的大模型看似放弃了人为编写死板"规则"(符号主义的初衷),但事实上,它在数千层网络隐式的海量参数里,学习并封装了比人类逻辑还要深邃得多的"暗规则"。如今大型预训练模型中的**思维链(Chain of Thought)**长程推理方式,何尝不是曾经符号学派追求逻辑验证与步骤严密的经典思想在神经网络中的重生?
- **多模态融合**:文本、图像、音频、视频等信息的统一处理
- **高效大模型**:降低训练成本,提升推理效率,推动边缘部署
- **可解释 AI**:解决黑箱问题,增强 AI 可信度
- **AGI 探索**:从专用智能向通用人工智能迈进,追求更全面的人类智能模拟
**站在大模型时代的巅峰往下看,未来的通用人工智能(AGI)正沿着以下几条极其广阔且深刻的探索大道推进:**
1. **走向原生的统一神经中枢(原生多模态):** 未来的模型不再是"文本模型+语音模型"拼接而成的弗兰肯斯坦。以 GPT-4o 为代表的架构直接用同一个超级网络同时吞吐、感知且理解文本、图像、视频流和超低延迟的高情感三维波形语音。
2. **具身智能(Embodied AI):** 当拥有极高智商的"大脑"只能被囚禁在硅基机房里时,它就无法从物理世界验证真理。通过与波士顿动力、人型机器人的结合,超级 AI 有望长出双手并在摔打磨砺中习得和我们完全相同的物理客观铁律。
3. **智能体系统(Agentic AI):** 目前大多数 LLM 依然停留在"一问一答的被动计算文字计算器"阶段。而 AI Agent 时代,大模型被彻底赋予了**独立行动的权力**。只要你下达一句宏观的自然语言指令(例如"帮我调研并规划下周去挪威看极光的所有机票、酒店并生成日历日程"),AI Agent 将凭借长程记忆,自主拆解下达几十个子任务,打开虚拟浏览器调用真实航空公司的检索 API,完成复杂的校验甚至比对确认。它们不再是被动等待敲击的回声壁,而是不知疲倦的数字劳动力集群。
AI 的发展是一条**螺旋上升**的道路,每个时代的技术都为后续突破奠定基础。今天的大模型并非完全抛弃符号主义,而是在连接主义框架下,通过海量数据学习到了类似符号推理的能力,实现了**不同学派思想的深度融合**
在这螺旋上升的漫长技术征途中,历史总是惊人的相似但绝不重复。我们正亲历从"向算法死硬输入规则"到"由机器自动定义世界法则"的最激动人心的历史横截面
<AIErasComparisonDemo />
@@ -138,6 +138,13 @@ VS Code 的核心理念是“一切皆插件”。它通过插件机制支持各
> - 自动生成测试:IDE 可以自动生成针对不同函数和模块的测试代码,方便你进行有针对性的测试。
> - Agent 式任务执行:智能 Agent 可以自动生成、打包、安装、运行和修改代码,在很多任务上可以部分替代初级软件工程师的工作。
::: details Antigravity
### [Antigravity](https://antigravity.google/)
Antigravity 是 Google 在 2025 年 11 月与 Gemini 3 一同发布的全新 AI IDE,采用"Agent-First"(智能体优先)开发模式。与传统 AI 辅助编码不同,Antigravity 让 AI 代理成为"主动执行者",可直接操作编辑器、终端、浏览器等工具,承担更多"执行""策划""验证"的工作。开发者只需提出高层意图,代理便会自动拆分任务、制定计划、执行代码、运行测试、生成成果。它支持多模型切换,包括 Gemini 3 Pro、Claude Sonnet 4.5 等,目前以公开预览形式提供,支持 Windows、macOS、Linux 全平台。
:::
::: details Trae
### [Trae](https://www.trae.ai/)