docs(api-intro): rewrite API introduction with interactive examples and clearer explanations

- Restructure content with more engaging metaphors and practical examples
- Add simplified interactive components to demonstrate key concepts
- Improve readability with better organization and visual aids
- Update terminology to be more beginner-friendly
- Include real-world API usage scenarios
This commit is contained in:
sanbuphy
2026-01-20 08:51:04 +08:00
parent 6806f05deb
commit 389c9126a1
9 changed files with 2008 additions and 2820 deletions
@@ -1,755 +1,132 @@
<!--
ApiQuickStartDemo.vue
参考 ide-intro 虚拟 UI + 先玩再讲风格
目标0 基础读者能立刻理解 API = 按钮/入口
选择按钮 -> 填一点 -> 点一下 -> 看结果成功/失败
ApiQuickStartDemo.vue - 简化版
目标用最简单的交互展示 API 调用流程
-->
<template>
<div class="machine">
<div class="top">
<div>
<div class="title">先玩一下 API 当成一个按钮机</div>
<div class="sub">你只管选按钮填信息点一下别背术语</div>
</div>
<div class="legend">
<span class="dot in" /> 你填的信息
<span class="dot mid" /> 按钮API <span class="dot out" /> 返回结果
<div class="demo">
<div class="title">🎮 试试看调用一次 API</div>
<p class="subtitle">点一下按钮看看会发生什么</p>
<div class="box">
<button class="call-btn" :disabled="calling" @click="callApi">
{{ calling ? '调用中...' : '🔘 点我调用 API' }}
</button>
<div class="result" v-if="result">
<div class="success" v-if="result.success">
成功API 返回了{{ result.data }}
</div>
<div class="error" v-else>
失败了{{ result.error }}
</div>
</div>
</div>
<div class="screen">
<div class="left">
<div class="panelTitle"> 选一个按钮</div>
<div class="buttons">
<button
v-for="b in buttons"
:key="b.id"
:class="['bigBtn', { active: currentId === b.id }]"
@click="select(b.id)"
>
<div class="icon">{{ b.icon }}</div>
<div class="label">{{ b.label }}</div>
<div class="hint">{{ b.hint }}</div>
</button>
</div>
</div>
<div class="middle">
<div class="panelTitle"> 填一点信息</div>
<div class="form" v-if="currentId === 'date'">
<label class="row">
<span class="k">日期</span>
<input
v-model="form.dateText"
class="input"
placeholder="2026-01-19"
/>
</label>
<div class="row">
<span class="k">格式</span>
<div class="chips">
<button
v-for="f in formats"
:key="f"
:class="['chip', { active: form.dateFormat === f }]"
@click="form.dateFormat = f"
>
{{ f }}
</button>
</div>
</div>
<div class="tip">玩法把日期写错试试比如 2026-99-99</div>
</div>
<div class="form" v-else-if="currentId === 'ai'">
<label class="row">
<span class="k">问题</span>
<textarea
v-model="form.question"
class="textarea"
placeholder="例如:用一句话解释什么是 API"
/>
</label>
<div class="tip">玩法清空问题再点一下看看会发生什么</div>
</div>
<div class="form" v-else>
<div class="row">
<span class="k">选择</span>
<div class="chips">
<button
:class="['chip', { active: form.loginOk }]"
@click="form.loginOk = true"
>
同意
</button>
<button
:class="['chip', { active: !form.loginOk }]"
@click="form.loginOk = false"
>
取消
</button>
</div>
</div>
<div class="tip">玩法取消再点一下</div>
</div>
<div class="callBar">
<button class="callBtn" :disabled="busy" @click="call">
{{ busy ? '执行中' : ' 点一下调用' }}
</button>
<button class="ghost" :disabled="busy" @click="resetScore">
清零计分
</button>
</div>
<div class="score">
<div class="scoreItem">
<div class="scoreK">成功</div>
<div class="scoreV">{{ score.ok }}</div>
</div>
<div class="scoreItem">
<div class="scoreK">失败</div>
<div class="scoreV">{{ score.bad }}</div>
</div>
<div class="scoreItem">
<div class="scoreK">连续成功</div>
<div class="scoreV">{{ score.streak }}</div>
</div>
</div>
</div>
<div class="right">
<div class="panelTitle"> 看结果</div>
<div class="flow">
<div class="node">
<div class="nodeTop">
<span class="dot in" />
<span class="nodeTitle">你填的信息</span>
</div>
<div class="nodeBody">{{ requestPreview }}</div>
</div>
<div class="arrow" :class="{ go: animating }"></div>
<div class="node">
<div class="nodeTop">
<span class="dot mid" />
<span class="nodeTitle">按钮API</span>
</div>
<div class="nodeBody">{{ currentHow }}</div>
</div>
<div class="arrow" :class="{ go: animating }"></div>
<div class="node">
<div class="nodeTop">
<span class="dot out" />
<span class="nodeTitle">返回结果</span>
</div>
<div class="nodeBody">
<div v-if="!result" class="muted">
还没有结果点一下调用试试
</div>
<div
v-else
class="resultBox"
:class="{ ok: result.ok, bad: !result.ok }"
>
<div class="badge">{{ result.ok ? '成功' : '失败' }}</div>
<div class="resultText">{{ result.text }}</div>
<pre
v-if="result.debug"
class="code"
><code>{{ result.debug }}</code></pre>
</div>
</div>
</div>
</div>
<details class="details">
<summary>选看你不用管的细节</summary>
<ul class="list">
<li v-for="x in currentDetails" :key="x">{{ x }}</li>
</ul>
</details>
</div>
<div class="explain">
<p><strong>你看</strong>你只需要点一下按钮调用 API就会得到结果</p>
<p>这就是 API 的本质<strong>按约定把请求交给对方对方按约定把结果给你</strong></p>
</div>
</div>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { ref } from 'vue'
const buttons = [
{
id: 'date',
icon: '🗓️',
label: '日期格式化',
hint: '最简单:填日期 -> 得到结果',
how: '调用一个日期格式化函数(本地)',
details: ['内部怎么处理时区', '怎么优化性能', '怎么兼容不同语言环境']
},
{
id: 'ai',
icon: '🤖',
label: '问 AI(模拟)',
hint: '写一句话 -> 得到回答',
how: '调用一个“问答入口”(可能是 SDK 或 HTTP',
details: ['模型怎么训练', '服务端怎么排队', '怎么限流/重试']
},
{
id: 'login',
icon: '🔑',
label: '一键登录(模拟)',
hint: '点同意/取消 -> 得到结果',
how: '按登录流程走一遍(这里用模拟)',
details: ['它怎么做安全校验', '登录凭证怎么生成', '怎么风控']
}
]
const formats = ['YYYY-MM-DD', 'YYYY/MM/DD', 'MM-DD']
const currentId = ref('date')
const busy = ref(false)
const animating = ref(false)
const form = reactive({
dateText: '2026-01-19',
dateFormat: 'YYYY-MM-DD',
question: '用一句话解释什么是 API',
loginOk: true
})
const score = reactive({ ok: 0, bad: 0, streak: 0 })
const calling = ref(false)
const result = ref(null)
const callTimes = ref([]) // for "too frequent"
const current = computed(
() => buttons.find((b) => b.id === currentId.value) || buttons[0]
)
const currentHow = computed(() => current.value.how)
const currentDetails = computed(() => current.value.details || [])
const requestPreview = computed(() => {
if (currentId.value === 'date') {
return `日期=${form.dateText || '(空)'};格式=${form.dateFormat}`
}
if (currentId.value === 'ai') {
return `问题=${(form.question || '').trim() || '(空)'}`
}
return `选择=${form.loginOk ? '同意' : '取消'}`
})
function select(id) {
currentId.value = id
function callApi() {
calling.value = true
result.value = null
}
function resetScore() {
score.ok = 0
score.bad = 0
score.streak = 0
}
function pad2(n) {
return String(n).padStart(2, '0')
}
function fmtDate(d, fmt) {
const y = d.getFullYear()
const m = pad2(d.getMonth() + 1)
const day = pad2(d.getDate())
if (fmt === 'YYYY/MM/DD') return `${y}/${m}/${day}`
if (fmt === 'MM-DD') return `${m}-${day}`
return `${y}-${m}-${day}`
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
function record(ok) {
if (ok) {
score.ok += 1
score.streak += 1
} else {
score.bad += 1
score.streak = 0
}
}
async function call() {
if (busy.value) return
busy.value = true
animating.value = true
// simulate "too frequent"
const now = Date.now()
callTimes.value = callTimes.value.filter((t) => now - t < 1200)
callTimes.value.push(now)
if (callTimes.value.length >= 4) {
await sleep(220)
// 模拟 API 调用
setTimeout(() => {
result.value = {
ok: false,
text: '太频繁了,请慢一点再试(模拟)',
debug: '现实里:有些 API 会限制你“点太快”。'
success: true,
data: 'Hello from API!'
}
record(false)
animating.value = false
busy.value = false
return
}
await sleep(380)
if (currentId.value === 'date') {
const raw = String(form.dateText || '').trim()
const d = new Date(raw)
if (Number.isNaN(d.getTime())) {
result.value = {
ok: false,
text: '日期写错了(我看不懂)',
debug: `输入:${raw}`
}
record(false)
animating.value = false
busy.value = false
return
}
const out = fmtDate(d, form.dateFormat)
result.value = {
ok: true,
text: `结果:${out}`,
debug: `你填的:${raw}\n你选的格式:${form.dateFormat}\n它给你的:${out}`
}
record(true)
animating.value = false
busy.value = false
return
}
if (currentId.value === 'ai') {
const q = String(form.question || '').trim()
if (!q) {
result.value = { ok: false, text: '你还没写问题', debug: '' }
record(false)
animating.value = false
busy.value = false
return
}
result.value = {
ok: true,
text: '回答:API 就是“别的软件给你用的按钮/入口”。',
debug: `你的问题:${q}\n回答:API 就是“别的软件给你用的按钮/入口”。`
}
record(true)
animating.value = false
busy.value = false
return
}
// login
if (!form.loginOk) {
result.value = { ok: false, text: '用户取消了登录(模拟)', debug: '' }
record(false)
animating.value = false
busy.value = false
return
}
result.value = {
ok: true,
text: '登录成功:拿到用户信息(模拟)',
debug: '用户:Alice\n状态:成功'
}
record(true)
animating.value = false
busy.value = false
calling.value = false
}, 800)
}
</script>
<style scoped>
.machine {
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 14px;
border-radius: 12px;
padding: 20px;
background: var(--vp-c-bg-soft);
padding: 16px;
}
.top {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin: 16px 0;
}
.title {
font-weight: 900;
font-size: 16px;
color: var(--vp-c-text-1);
}
.sub {
margin-top: 6px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.legend {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
font-size: 12px;
color: var(--vp-c-text-2);
}
.dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
border: 1px solid var(--vp-c-divider);
}
.dot.in {
background: color-mix(in srgb, #60a5fa 40%, var(--vp-c-bg));
}
.dot.mid {
background: color-mix(in srgb, var(--vp-c-brand-1) 35%, var(--vp-c-bg));
}
.dot.out {
background: color-mix(in srgb, #22c55e 30%, var(--vp-c-bg));
}
.screen {
margin-top: 12px;
display: grid;
grid-template-columns: 1.05fr 1fr 1.2fr;
gap: 12px;
}
.panelTitle {
font-weight: 800;
font-size: 13px;
color: var(--vp-c-text-1);
}
.left,
.middle,
.right {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg);
padding: 12px;
}
.buttons {
margin-top: 10px;
display: grid;
gap: 10px;
}
.bigBtn {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border-radius: 12px;
padding: 12px;
text-align: left;
cursor: pointer;
}
.bigBtn:hover {
border-color: var(--vp-c-brand-1);
}
.bigBtn.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent);
}
.icon {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.label {
margin-top: 6px;
font-weight: 900;
font-size: 13px;
}
.hint {
margin-top: 6px;
font-size: 12px;
.subtitle {
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 16px;
}
.form {
margin-top: 10px;
display: grid;
gap: 10px;
}
.row {
display: grid;
grid-template-columns: 56px 1fr;
gap: 10px;
align-items: start;
}
.k {
font-size: 12px;
color: var(--vp-c-text-3);
padding-top: 8px;
}
.input {
width: 100%;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 8px 10px;
color: var(--vp-c-text-1);
font-size: 13px;
}
.textarea {
width: 100%;
min-height: 78px;
resize: vertical;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 8px 10px;
color: var(--vp-c-text-1);
font-size: 13px;
line-height: 1.5;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
border: 1px solid var(--vp-c-divider);
.box {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
padding: 6px 10px;
border-radius: 999px;
font-size: 13px;
cursor: pointer;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
text-align: center;
}
.chip.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent);
}
.tip {
margin-top: 4px;
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.callBar {
margin-top: 12px;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.callBtn {
flex: 1;
min-width: 200px;
border: 1px solid var(--vp-c-brand-1);
.call-btn {
background: var(--vp-c-brand-1);
color: #fff;
border-radius: 12px;
padding: 10px 12px;
font-weight: 900;
color: white;
border: none;
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.ghost {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 12px;
padding: 10px 12px;
font-weight: 900;
cursor: pointer;
.call-btn:hover:not(:disabled) {
opacity: 0.9;
transform: scale(1.05);
}
.callBtn:disabled,
.ghost:disabled {
.call-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.score {
margin-top: 12px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
.result {
margin-top: 16px;
padding: 12px;
border-radius: 8px;
font-size: 14px;
}
.scoreItem {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 10px 12px;
.success {
background: #dcfce7;
color: #166534;
border: 1px solid #86efac;
}
.scoreK {
font-size: 12px;
color: var(--vp-c-text-2);
.error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #fca5a5;
}
.scoreV {
margin-top: 4px;
font-size: 18px;
font-weight: 900;
color: var(--vp-c-text-1);
}
.flow {
margin-top: 10px;
display: grid;
gap: 10px;
}
.node {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
overflow: hidden;
}
.nodeTop {
display: flex;
gap: 8px;
align-items: center;
padding: 10px 12px;
border-bottom: 1px solid var(--vp-c-divider);
}
.nodeTitle {
font-weight: 900;
font-size: 12px;
color: var(--vp-c-text-1);
}
.nodeBody {
padding: 10px 12px;
font-size: 12px;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.arrow {
text-align: center;
color: var(--vp-c-text-3);
font-weight: 900;
font-size: 16px;
transition: transform 180ms ease;
}
.arrow.go {
transform: translateX(6px);
color: var(--vp-c-brand-1);
}
.muted {
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.resultBox {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
.explain {
margin-top: 16px;
padding: 12px;
background: var(--vp-c-bg);
padding: 10px 12px;
}
.resultBox.ok {
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
}
.resultBox.bad {
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
}
.badge {
display: inline-block;
font-size: 12px;
padding: 2px 10px;
border-radius: 999px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
font-weight: 900;
}
.resultText {
margin-top: 8px;
font-size: 13px;
font-weight: 900;
color: var(--vp-c-text-1);
}
.code {
margin: 8px 0 0;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
color: var(--vp-c-text-1);
}
.details {
margin-top: 12px;
border: 1px dashed var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg);
padding: 10px 12px;
}
.details summary {
cursor: pointer;
color: var(--vp-c-text-1);
font-weight: 900;
font-size: 13px;
}
.list {
margin: 10px 0 0;
padding-left: 16px;
color: var(--vp-c-text-1);
font-size: 12px;
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
}
@media (max-width: 720px) {
.top {
flex-direction: column;
}
.screen {
grid-template-columns: 1fr;
}
.score {
grid-template-columns: 1fr;
}
color: var(--vp-c-text-2);
}
</style>