Files
test-repo/docs/.vitepress/theme/components/appendix/api-intro/ApiQuickStartDemo.vue
T
2026-01-19 23:45:08 +08:00

756 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
ApiQuickStartDemo.vue
参考 ide-intro 虚拟 UI + 先玩再讲风格
目标0 基础读者能立刻理解 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>
</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>
</div>
</template>
<script setup>
import { computed, reactive, 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 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
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)
result.value = {
ok: false,
text: '太频繁了,请慢一点再试(模拟)',
debug: '现实里:有些 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
}
</script>
<style scoped>
.machine {
border: 1px solid var(--vp-c-divider);
border-radius: 14px;
background: var(--vp-c-bg-soft);
padding: 16px;
}
.top {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
}
.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;
}
.label {
margin-top: 6px;
font-weight: 900;
font-size: 13px;
}
.hint {
margin-top: 6px;
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.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);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
padding: 6px 10px;
border-radius: 999px;
font-size: 13px;
cursor: pointer;
}
.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);
background: var(--vp-c-brand-1);
color: #fff;
border-radius: 12px;
padding: 10px 12px;
font-weight: 900;
cursor: pointer;
}
.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;
}
.callBtn:disabled,
.ghost:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.score {
margin-top: 12px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.scoreItem {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 10px 12px;
}
.scoreK {
font-size: 12px;
color: var(--vp-c-text-2);
}
.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;
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;
line-height: 1.6;
}
@media (max-width: 720px) {
.top {
flex-direction: column;
}
.screen {
grid-template-columns: 1fr;
}
.score {
grid-template-columns: 1fr;
}
}
</style>