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:
@@ -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">> </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="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">> </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">> </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">
|
||||
REST(Representational 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">> </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="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">输入:A、B → 输出:和(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">输入:A、B、Cin → 输出:和(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>每位全加器接收 A、B 和上一位的进位(Cin),输出本位的和(S)与传给下一位的进位(Cout),和手算竖式「逢二进一」一致。
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>每位加法器接收 A、B 和上一位的进位,输出本位的和与传给下一位的进位。就像手算竖式"逢二进一",只是用电路自动完成。
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
-239
@@ -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>
|
||||
-237
@@ -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>
|
||||
Reference in New Issue
Block a user