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:
@@ -1,447 +1,222 @@
|
||||
<!--
|
||||
ApiConceptDemo.vue
|
||||
参考 ide-intro 的“虚拟 UI + 先玩再讲”风格。
|
||||
目标:用一个简单的“分类小游戏”讲清楚 API 到底要写清楚什么,
|
||||
以及哪些是“你不用管的内部细节”。
|
||||
目标:互动演示 API 必须写清楚的 4 个要点
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">API 到底是什么?(先玩 10 秒)</div>
|
||||
<div class="sub">
|
||||
下面有一些卡片。请你把它们分成两类:<b>必须写清楚</b> vs
|
||||
<b>不用写给别人看</b>。
|
||||
<div class="demo">
|
||||
<div class="title">📋 API 必须写清楚的 4 件事</div>
|
||||
<p class="subtitle">点每一项看看是什么意思</p>
|
||||
|
||||
<div class="cards">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:class="['card', { active: selectedId === item.id }]"
|
||||
@click="select(item.id)"
|
||||
>
|
||||
<div class="card-icon">{{ item.icon }}</div>
|
||||
<div class="card-title">{{ item.title }}</div>
|
||||
<div class="card-hint">{{ item.hint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arena">
|
||||
<div class="cards">
|
||||
<div class="cardsTitle">卡片池</div>
|
||||
<div class="cardGrid">
|
||||
<button
|
||||
v-for="c in pool"
|
||||
:key="c.id"
|
||||
class="card"
|
||||
@click="pick(c.id)"
|
||||
>
|
||||
<div class="cardTop">
|
||||
<span class="tag" :class="c.type">{{ c.typeLabel }}</span>
|
||||
</div>
|
||||
<div class="cardText">{{ c.text }}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="muted">
|
||||
提示:API
|
||||
的核心就是“怎么用”。所以:入口、要填什么、会返回什么、失败怎么说。
|
||||
</div>
|
||||
<div class="detail" v-if="selected">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ selected.icon }}</span>
|
||||
<span class="detail-title">{{ selected.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="bins">
|
||||
<div class="bin must">
|
||||
<div class="binHead">
|
||||
<div class="binTitle">必须写清楚</div>
|
||||
<div class="binHint">不写清楚就会用错</div>
|
||||
</div>
|
||||
<div class="drop">
|
||||
<div v-if="must.length === 0" class="empty">点卡片,把它放进来</div>
|
||||
<button v-for="id in must" :key="id" class="chip" @click="undo(id)">
|
||||
{{ byId(id).text }}
|
||||
<span class="x">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div class="detail-desc">{{ selected.desc }}</div>
|
||||
<div class="detail-example">
|
||||
<strong>例子:</strong>
|
||||
<code>{{ selected.example }}</code>
|
||||
</div>
|
||||
|
||||
<div class="bin internal">
|
||||
<div class="binHead">
|
||||
<div class="binTitle">不用写给别人看</div>
|
||||
<div class="binHint">这是内部实现,换了也不影响“怎么用”</div>
|
||||
</div>
|
||||
<div class="drop">
|
||||
<div v-if="internal.length === 0" class="empty">
|
||||
点卡片,把它放进来
|
||||
</div>
|
||||
<button
|
||||
v-for="id in internal"
|
||||
:key="id"
|
||||
class="chip"
|
||||
@click="undo(id)"
|
||||
>
|
||||
{{ byId(id).text }}
|
||||
<span class="x">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bar">
|
||||
<button class="btn" @click="check">检查一下</button>
|
||||
<button class="ghost" @click="reset">重来</button>
|
||||
<div class="score">
|
||||
<span
|
||||
>正确:<b>{{ score.ok }}</b></span
|
||||
>
|
||||
<span
|
||||
>错误:<b>{{ score.bad }}</b></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result" v-if="checked">
|
||||
<div class="resultTitle">结论(新手版一句话)</div>
|
||||
<div class="resultText">
|
||||
API =
|
||||
你告诉别人“这个按钮怎么按”。<b>入口在哪</b>、<b>要填什么</b>、<b>会得到什么</b>、<b>失败怎么说</b>。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const all = [
|
||||
// must
|
||||
const selectedId = ref('entry')
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 'where',
|
||||
bucket: 'must',
|
||||
type: 'must',
|
||||
typeLabel: '必填脑子',
|
||||
text: '入口在哪(网址 / 函数名)'
|
||||
id: 'entry',
|
||||
icon: '📍',
|
||||
title: '入口在哪',
|
||||
hint: '网址 / 函数名',
|
||||
desc: '你要调用的"按钮"在哪里。是 HTTP 网址,还是代码里的函数名?',
|
||||
example: 'GET /api/users/{id}'
|
||||
},
|
||||
{
|
||||
id: 'input',
|
||||
bucket: 'must',
|
||||
type: 'must',
|
||||
typeLabel: '必填脑子',
|
||||
text: '要填什么(需要哪些信息)'
|
||||
id: 'params',
|
||||
icon: '📝',
|
||||
title: '要填什么',
|
||||
hint: '需要哪些参数',
|
||||
desc: '调用这个 API 时,你需要提供哪些信息?哪些是必填的,哪些是可选的?',
|
||||
example: 'id(必填)、page(可选)'
|
||||
},
|
||||
{
|
||||
id: 'output',
|
||||
bucket: 'must',
|
||||
type: 'must',
|
||||
typeLabel: '必填脑子',
|
||||
text: '会拿到什么(成功的结果)'
|
||||
id: 'response',
|
||||
icon: '✅',
|
||||
title: '会得到什么',
|
||||
hint: '返回什么数据',
|
||||
desc: '成功的时候,API 会返回什么数据?有哪些字段,分别代表什么意思?',
|
||||
example: '{ id, name, email }'
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
bucket: 'must',
|
||||
type: 'must',
|
||||
typeLabel: '必填脑子',
|
||||
text: '失败怎么说(提示/原因)'
|
||||
},
|
||||
|
||||
// internal
|
||||
{
|
||||
id: 'db',
|
||||
bucket: 'internal',
|
||||
type: 'internal',
|
||||
typeLabel: '内部',
|
||||
text: '数据库表怎么设计'
|
||||
},
|
||||
{
|
||||
id: 'topo',
|
||||
bucket: 'internal',
|
||||
type: 'internal',
|
||||
typeLabel: '内部',
|
||||
text: '服务有几个机器/怎么部署'
|
||||
},
|
||||
{
|
||||
id: 'perf',
|
||||
bucket: 'internal',
|
||||
type: 'internal',
|
||||
typeLabel: '内部',
|
||||
text: '内部怎么做性能优化'
|
||||
icon: '⚠️',
|
||||
title: '失败怎么说',
|
||||
hint: '错误提示',
|
||||
desc: '调用失败的时候会返回什么错误信息?你应该怎么处理这些错误?',
|
||||
example: '401 没权限、404 找不到、429 太频繁'
|
||||
}
|
||||
]
|
||||
|
||||
const must = ref([])
|
||||
const internal = ref([])
|
||||
const checked = ref(false)
|
||||
const score = reactive({ ok: 0, bad: 0 })
|
||||
const selected = computed(() => items.find(i => i.id === selectedId.value))
|
||||
|
||||
const used = computed(() => new Set([...must.value, ...internal.value]))
|
||||
const pool = computed(() => all.filter((c) => !used.value.has(c.id)))
|
||||
|
||||
function byId(id) {
|
||||
return all.find((x) => x.id === id) || { id, text: id }
|
||||
}
|
||||
|
||||
function pick(id) {
|
||||
// 简化交互:按顺序放进“必须写清楚”,再点一次放进“内部”
|
||||
// 这样新手不需要理解拖拽,也不需要想“放哪儿”:玩起来更顺。
|
||||
// 如果想放“内部”,可以先把它放错,再点 chip 撤回重新放。
|
||||
if (must.value.length <= internal.value.length) {
|
||||
must.value.push(id)
|
||||
} else {
|
||||
internal.value.push(id)
|
||||
}
|
||||
checked.value = false
|
||||
}
|
||||
|
||||
function undo(id) {
|
||||
must.value = must.value.filter((x) => x !== id)
|
||||
internal.value = internal.value.filter((x) => x !== id)
|
||||
checked.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
must.value = []
|
||||
internal.value = []
|
||||
checked.value = false
|
||||
score.ok = 0
|
||||
score.bad = 0
|
||||
}
|
||||
|
||||
function check() {
|
||||
let ok = 0
|
||||
let bad = 0
|
||||
for (const id of must.value) {
|
||||
byId(id).bucket === 'must' ? (ok += 1) : (bad += 1)
|
||||
}
|
||||
for (const id of internal.value) {
|
||||
byId(id).bucket === 'internal' ? (ok += 1) : (bad += 1)
|
||||
}
|
||||
score.ok = ok
|
||||
score.bad = bad
|
||||
checked.value = true
|
||||
function select(id) {
|
||||
selectedId.value = id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.arena {
|
||||
margin-top: 12px;
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cards,
|
||||
.bin {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.cardsTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.cardGrid {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cardTop {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.card.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: #f0f9ff;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-2);
|
||||
.card-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tag.must {
|
||||
border-color: color-mix(in srgb, #22c55e 40%, var(--vp-c-divider));
|
||||
background: color-mix(in srgb, #22c55e 12%, var(--vp-c-bg));
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.tag.internal {
|
||||
border-color: color-mix(in srgb, #f59e0b 45%, var(--vp-c-divider));
|
||||
background: color-mix(in srgb, #f59e0b 14%, var(--vp-c-bg));
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.cardText {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bins {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.binHead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.binTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
.card-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.binHint {
|
||||
font-size: 12px;
|
||||
.card-hint {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.drop {
|
||||
margin-top: 10px;
|
||||
min-height: 120px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
.detail {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-brand-1);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
font-size: 15px;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-example {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
.detail-example strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.x {
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.bar {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 8px 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: 10px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.score {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
.detail-example code {
|
||||
display: block;
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.resultTitle {
|
||||
font-weight: 900;
|
||||
border-radius: 6px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.resultText {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.arena {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.cardGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.score {
|
||||
margin-left: 0;
|
||||
}
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,396 +1,194 @@
|
||||
<!--
|
||||
ApiDocumentDemo.vue
|
||||
参考 ide-intro 的“虚拟界面 + 点击探索”风格。
|
||||
目标:让新手学会看 API 文档的 3 个重点:入口在哪 / 要填什么 / 会得到什么。
|
||||
ApiDocumentDemo.vue - 简化版
|
||||
目标:用简单的示例展示如何阅读 API 文档
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">怎么读 API 文档?(像找按钮一样找)</div>
|
||||
<div class="sub">
|
||||
任务:请在下面的“假文档”里,依次点出:<b>入口</b>、<b>要填什么</b>、<b>会得到什么</b>。
|
||||
<div class="demo">
|
||||
<div class="title">📖 怎么读 API 文档?</div>
|
||||
<p class="subtitle">找到这 3 个信息就够了</p>
|
||||
|
||||
<div class="doc-example">
|
||||
<div class="doc-header">API 文档示例</div>
|
||||
<div class="doc-body">
|
||||
<div class="section">
|
||||
<div class="section-title">📍 1️⃣ 入口在哪</div>
|
||||
<div class="section-content">
|
||||
<code>GET /api/users/{id}</code>
|
||||
<p class="hint">这就是你要调用的"按钮"</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">📝 2️⃣ 要填什么</div>
|
||||
<div class="section-content">
|
||||
<div class="param">
|
||||
<span class="param-name">id</span>
|
||||
<span class="param-desc">用户编号(必填)</span>
|
||||
</div>
|
||||
<p class="hint">你需要提供这个参数</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">✅ 3️⃣ 会得到什么</div>
|
||||
<div class="section-content">
|
||||
<pre><code>{
|
||||
"id": "123",
|
||||
"name": "张三",
|
||||
"email": "zhang@example.com"
|
||||
}</code></pre>
|
||||
<p class="hint">成功时返回的数据格式</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game">
|
||||
<div class="doc">
|
||||
<div class="docBar">
|
||||
<span class="dot red" />
|
||||
<span class="dot yellow" />
|
||||
<span class="dot green" />
|
||||
<span class="docTitle">API 文档(示例)</span>
|
||||
</div>
|
||||
|
||||
<div class="docBody">
|
||||
<button
|
||||
class="block"
|
||||
:class="{ hit: hits.entry }"
|
||||
@click="hit('entry')"
|
||||
>
|
||||
<div class="blockK">入口</div>
|
||||
<div class="blockV">GET /v1/users/{id}</div>
|
||||
<div class="blockHint">(你要按哪个按钮)</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="block"
|
||||
:class="{ hit: hits.input }"
|
||||
@click="hit('input')"
|
||||
>
|
||||
<div class="blockK">要填什么</div>
|
||||
<div class="blockV">id(用户编号)</div>
|
||||
<div class="blockHint">(你要告诉它什么)</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="block"
|
||||
:class="{ hit: hits.output }"
|
||||
@click="hit('output')"
|
||||
>
|
||||
<div class="blockK">会得到什么</div>
|
||||
<div class="blockV">{ id, name }</div>
|
||||
<div class="blockHint">(成功时给你的结果)</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="block gray"
|
||||
:class="{ hit: hits.fail }"
|
||||
@click="hit('fail')"
|
||||
>
|
||||
<div class="blockK">失败会怎样(常见)</div>
|
||||
<div class="blockV">没钥匙 / 找不到 / 太频繁</div>
|
||||
<div class="blockHint">(你要能看懂失败原因)</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side">
|
||||
<div class="task">
|
||||
<div class="taskTitle">你要找的 3 个重点</div>
|
||||
<div class="taskList">
|
||||
<div :class="['taskItem', hits.entry && 'done']">
|
||||
① 入口在哪(点“入口”)
|
||||
</div>
|
||||
<div :class="['taskItem', hits.input && 'done']">
|
||||
② 要填什么(点“要填什么”)
|
||||
</div>
|
||||
<div :class="['taskItem', hits.output && 'done']">
|
||||
③ 会得到什么(点“会得到什么”)
|
||||
</div>
|
||||
</div>
|
||||
<div class="muted">你只要先会这三件事,就能开始用 API 了。</div>
|
||||
</div>
|
||||
|
||||
<div class="explain" v-if="last">
|
||||
<div class="explainTitle">你刚刚点的是:{{ labelOf(last) }}</div>
|
||||
<div class="explainText">{{ explainOf(last) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn" @click="autoWin">一键帮我找对</button>
|
||||
<button class="ghost" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="win" v-if="won">
|
||||
<div class="winTitle">完成!</div>
|
||||
<div class="winText">
|
||||
你已经会读 80% 的 API 文档了:入口 / 要填 / 会得到。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<p><strong>💡 小贴士:</strong></p>
|
||||
<ul>
|
||||
<li>先确认这个 API 是不是你需要的</li>
|
||||
<li>再看要填什么参数(必填 vs 可选)</li>
|
||||
<li>最后看返回什么、失败会怎样</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
const hits = reactive({
|
||||
entry: false,
|
||||
input: false,
|
||||
output: false,
|
||||
fail: false
|
||||
})
|
||||
const last = ref('')
|
||||
|
||||
const won = computed(() => hits.entry && hits.input && hits.output)
|
||||
|
||||
function hit(key) {
|
||||
hits[key] = true
|
||||
last.value = key
|
||||
}
|
||||
|
||||
function reset() {
|
||||
hits.entry = false
|
||||
hits.input = false
|
||||
hits.output = false
|
||||
hits.fail = false
|
||||
last.value = ''
|
||||
}
|
||||
|
||||
function autoWin() {
|
||||
hits.entry = true
|
||||
hits.input = true
|
||||
hits.output = true
|
||||
last.value = 'output'
|
||||
}
|
||||
|
||||
function labelOf(key) {
|
||||
if (key === 'entry') return '入口'
|
||||
if (key === 'input') return '要填什么'
|
||||
if (key === 'output') return '会得到什么'
|
||||
if (key === 'fail') return '失败会怎样'
|
||||
return key
|
||||
}
|
||||
|
||||
function explainOf(key) {
|
||||
if (key === 'entry') {
|
||||
return '入口就是“按钮名字”。你要按哪个按钮,先找到它。'
|
||||
}
|
||||
if (key === 'input') {
|
||||
return '要填什么 = 你需要提供的信息。比如 id、页码、搜索词。'
|
||||
}
|
||||
if (key === 'output') {
|
||||
return '会得到什么 = 成功时返回的数据。你要关心字段有什么、有没有可能为空。'
|
||||
}
|
||||
if (key === 'fail') {
|
||||
return '失败会怎样 = 你要能看懂失败原因,好给用户提示/重试。'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
// 无需脚本逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.game {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 0.8fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.doc {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
.doc-example {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.docBar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
.doc-header {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.dot.red {
|
||||
background: #ef4444;
|
||||
}
|
||||
.dot.yellow {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.dot.green {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.docTitle {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.docBody {
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.block {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.block:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.block.gray {
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.block.hit {
|
||||
border-color: #22c55e;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, #22c55e 18%, transparent);
|
||||
}
|
||||
|
||||
.blockK {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.blockV {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.blockHint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
.doc-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.side {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.taskTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.taskList {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.taskItem {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.taskItem.done {
|
||||
border-color: #22c55e;
|
||||
background: color-mix(in srgb, #22c55e 12%, var(--vp-c-bg));
|
||||
.section-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.explain {
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.explainTitle {
|
||||
font-weight: 900;
|
||||
code {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.explainText {
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.actions {
|
||||
.param {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 8px 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: 10px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.win {
|
||||
border: 1px solid #22c55e;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, #22c55e 12%, var(--vp-c-bg));
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.winTitle {
|
||||
font-weight: 900;
|
||||
color: #166534;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.winText {
|
||||
margin-top: 8px;
|
||||
.param-name {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
color: #166534;
|
||||
line-height: 1.6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.game {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.param-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #1e293b;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: #e2e8f0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tips {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.tips ul {
|
||||
margin: 8px 0 0 20px;
|
||||
}
|
||||
|
||||
.tips li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,438 +1,337 @@
|
||||
<!--
|
||||
ApiMethodDemo.vue
|
||||
参考 ide-intro 的“虚拟环境演示”风格。
|
||||
目标:把 GET/POST/DELETE 讲成“拿/加/删”三个按钮,并用可视化列表展示效果。
|
||||
注意:这是选读内容,但组件本身要足够好玩、足够直观。
|
||||
目标:清晰展示各种 HTTP 方法的含义和使用场景
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">三个按钮:拿(GET)/ 加(POST)/ 删(DELETE)</div>
|
||||
<div class="sub">你不用记英文。先玩:点按钮,看看“列表怎么变”。</div>
|
||||
</div>
|
||||
<div class="demo">
|
||||
<div class="title">🔍 HTTP 方法:GET、POST、PUT、DELETE 是什么?</div>
|
||||
<p class="subtitle">把它们想象成对数据的"操作方式"</p>
|
||||
|
||||
<div class="board">
|
||||
<div class="left">
|
||||
<div class="panelTitle">小列表(服务器里有的东西)</div>
|
||||
<div class="list">
|
||||
<div v-if="items.length === 0" class="empty">空的</div>
|
||||
<div v-for="it in items" :key="it.id" class="row">
|
||||
<div class="pillId">#{{ it.id }}</div>
|
||||
<div class="name">{{ it.name }}</div>
|
||||
<div class="methods-grid">
|
||||
<div class="method-card get">
|
||||
<div class="method-badge">GET</div>
|
||||
<div class="method-title">📖 获取(查询)</div>
|
||||
<div class="method-desc">
|
||||
<p><strong>只看不改</strong> - 从服务器获取数据</p>
|
||||
<div class="method-examples">
|
||||
<div class="example-item">• 查询用户信息</div>
|
||||
<div class="example-item">• 搜索商品</div>
|
||||
<div class="example-item">• 获取文章列表</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mini">
|
||||
<span class="miniK">提示:</span>
|
||||
<span class="miniV">你可以一直点“拿(GET)”,列表不会变。</span>
|
||||
<div class="method-tip">
|
||||
💡 可以安全重试,不会改变服务器数据
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="panelTitle">按按钮(模拟 API)</div>
|
||||
|
||||
<div class="btnRow">
|
||||
<button class="btn get" :disabled="busy" @click="getList">
|
||||
拿(GET)
|
||||
</button>
|
||||
<button class="btn post" :disabled="busy" @click="addOne">
|
||||
加(POST)
|
||||
</button>
|
||||
<button class="btn del" :disabled="busy" @click="removeOne">
|
||||
删(DELETE)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inputs">
|
||||
<label class="field">
|
||||
<span class="k">要加什么</span>
|
||||
<input v-model="newName" class="input" placeholder="随便写个名字" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="result">
|
||||
<div class="resultTitle">返回结果</div>
|
||||
<div v-if="!last" class="muted">点一个按钮试试。</div>
|
||||
<div v-else class="resBox" :class="{ ok: last.ok, bad: !last.ok }">
|
||||
<div class="badge">{{ last.ok ? '成功' : '失败' }}</div>
|
||||
<div class="text">{{ last.text }}</div>
|
||||
<div class="method-card post">
|
||||
<div class="method-badge">POST</div>
|
||||
<div class="method-title">➕ 创建(新增)</div>
|
||||
<div class="method-desc">
|
||||
<p><strong>新建数据</strong> - 在服务器创建新资源</p>
|
||||
<div class="method-examples">
|
||||
<div class="example-item">• 创建新用户</div>
|
||||
<div class="example-item">• 提交订单</div>
|
||||
<div class="example-item">• 发表评论</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-tip">
|
||||
⚠️ 不能随意重试,可能会重复创建
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="foot">
|
||||
<div class="stat">
|
||||
<span class="statK">你点了</span>
|
||||
<span class="statV">{{ clicks }}</span>
|
||||
<span class="statK">次</span>
|
||||
<div class="method-card put">
|
||||
<div class="method-badge">PUT</div>
|
||||
<div class="method-title">✏️ 更新(替换)</div>
|
||||
<div class="method-desc">
|
||||
<p><strong>整体替换</strong> - 用新数据完全替换旧数据</p>
|
||||
<div class="method-examples">
|
||||
<div class="example-item">• 修改用户全部信息</div>
|
||||
<div class="example-item">• 更换文章全部内容</div>
|
||||
</div>
|
||||
<button class="ghost" :disabled="busy" @click="reset">重置</button>
|
||||
</div>
|
||||
<div class="method-tip">
|
||||
⚠️ 会覆盖整个资源,需要提供完整数据
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-card patch">
|
||||
<div class="method-badge">PATCH</div>
|
||||
<div class="method-title">🔧 修改(部分)</div>
|
||||
<div class="method-desc">
|
||||
<p><strong>局部更新</strong> - 只修改资源的部分字段</p>
|
||||
<div class="method-examples">
|
||||
<div class="example-item">• 只修改用户昵称</div>
|
||||
<div class="example-item">• 更新文章标题</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-tip">
|
||||
💡 只传需要修改的字段,更灵活
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-card delete">
|
||||
<div class="method-badge">DELETE</div>
|
||||
<div class="method-title">🗑️ 删除</div>
|
||||
<div class="method-desc">
|
||||
<p><strong>移除数据</strong> - 从服务器删除资源</p>
|
||||
<div class="method-examples">
|
||||
<div class="example-item">• 删除用户</div>
|
||||
<div class="example-item">• 取消订单</div>
|
||||
<div class="example-item">• 删除评论</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-tip">
|
||||
⚠️ 操作不可逆,删除后无法恢复
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="one">
|
||||
<div class="oneTitle">一句话总结</div>
|
||||
<div class="oneText">
|
||||
GET 通常只是“拿数据”;POST/DELETE 会“改数据”。所以网络抖动时,重试 POST
|
||||
要更小心。
|
||||
</div>
|
||||
<div class="comparison-table">
|
||||
<div class="table-title">📊 快速对比</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>方法</th>
|
||||
<th>操作</th>
|
||||
<th>是否会改数据</th>
|
||||
<th>能否重试</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="badge get">GET</span></td>
|
||||
<td>查询</td>
|
||||
<td>❌ 否</td>
|
||||
<td>✅ 可以</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge post">POST</span></td>
|
||||
<td>创建</td>
|
||||
<td>✅ 是</td>
|
||||
<td>⚠️ 不建议</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge put">PUT</span></td>
|
||||
<td>替换</td>
|
||||
<td>✅ 是</td>
|
||||
<td>⚠️ 不建议</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge patch">PATCH</span></td>
|
||||
<td>修改</td>
|
||||
<td>✅ 是</td>
|
||||
<td>⚠️ 不建议</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge delete">DELETE</span></td>
|
||||
<td>删除</td>
|
||||
<td>✅ 是</td>
|
||||
<td>⚠️ 不建议</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<p><strong>💡 新手建议:</strong></p>
|
||||
<ul>
|
||||
<li>先学会 <strong>GET</strong>(查询)和 <strong>POST</strong>(创建)就够用了</li>
|
||||
<li>PUT/PATCH/DELETE 可以慢慢学,理解原理更重要</li>
|
||||
<li>实际开发中,GET 和 POST 占了 80% 的使用场景</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const busy = ref(false)
|
||||
const clicks = ref(0)
|
||||
|
||||
const seq = ref(3)
|
||||
const items = ref([
|
||||
{ id: 1, name: 'Apple' },
|
||||
{ id: 2, name: 'Banana' },
|
||||
{ id: 3, name: 'Cookie' }
|
||||
])
|
||||
|
||||
const newName = ref('Donut')
|
||||
const last = ref(null)
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
clicks.value += 1
|
||||
busy.value = true
|
||||
await sleep(220)
|
||||
last.value = {
|
||||
ok: true,
|
||||
text: `拿到了 ${items.value.length} 条数据(列表不变)`
|
||||
}
|
||||
busy.value = false
|
||||
}
|
||||
|
||||
async function addOne() {
|
||||
clicks.value += 1
|
||||
busy.value = true
|
||||
await sleep(260)
|
||||
|
||||
const name = String(newName.value || '').trim()
|
||||
if (!name) {
|
||||
last.value = { ok: false, text: '你还没写“要加什么”' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
seq.value += 1
|
||||
items.value = [...items.value, { id: seq.value, name }]
|
||||
last.value = { ok: true, text: `已添加:#${seq.value} ${name}` }
|
||||
busy.value = false
|
||||
}
|
||||
|
||||
async function removeOne() {
|
||||
clicks.value += 1
|
||||
busy.value = true
|
||||
await sleep(240)
|
||||
|
||||
if (items.value.length === 0) {
|
||||
last.value = { ok: false, text: '已经空了,删不了' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const removed = items.value[items.value.length - 1]
|
||||
items.value = items.value.slice(0, -1)
|
||||
last.value = { ok: true, text: `已删除:#${removed.id} ${removed.name}` }
|
||||
busy.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
seq.value = 3
|
||||
items.value = [
|
||||
{ id: 1, name: 'Apple' },
|
||||
{ id: 2, name: 'Banana' },
|
||||
{ id: 3, name: 'Cookie' }
|
||||
]
|
||||
newName.value = 'Donut'
|
||||
last.value = null
|
||||
clicks.value = 0
|
||||
}
|
||||
// 无需脚本逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.methods-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.method-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.method-card.get { border-color: #3b82f6; }
|
||||
.method-card.post { border-color: #22c55e; }
|
||||
.method-card.put { border-color: #f59e0b; }
|
||||
.method-card.patch { border-color: #8b5cf6; }
|
||||
.method-card.delete { border-color: #ef4444; }
|
||||
|
||||
.method-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.method-card.get .method-badge { background: #3b82f6; }
|
||||
.method-card.post .method-badge { background: #22c55e; }
|
||||
.method-card.put .method-badge { background: #f59e0b; }
|
||||
.method-card.patch .method-badge { background: #8b5cf6; }
|
||||
.method-card.delete .method-badge { background: #ef4444; }
|
||||
|
||||
.method-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 60px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
.method-desc p {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.board {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
.method-examples {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
font-weight: 900;
|
||||
.example-item {
|
||||
font-size: 13px;
|
||||
padding: 4px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.pillId {
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-2);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.mini {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.miniK {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.btnRow {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 12px;
|
||||
.method-tip {
|
||||
padding: 10px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.method-card.get .method-tip {
|
||||
background: #eff6ff;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.method-card.post .method-tip,
|
||||
.method-card.put .method-tip,
|
||||
.method-card.patch .method-tip,
|
||||
.method-card.delete .method-tip {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn.get {
|
||||
border-color: color-mix(in srgb, #60a5fa 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.btn.post {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.btn.del {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.k {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.input {
|
||||
table {
|
||||
width: 100%;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resultTitle {
|
||||
font-weight: 900;
|
||||
td {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.resBox {
|
||||
margin-top: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resBox.ok {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.resBox.bad {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.badge.get { background: #3b82f6; }
|
||||
.badge.post { background: #22c55e; }
|
||||
.badge.put { background: #f59e0b; }
|
||||
.badge.patch { background: #8b5cf6; }
|
||||
.badge.delete { background: #ef4444; }
|
||||
|
||||
.foot {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat {
|
||||
font-size: 12px;
|
||||
.tips {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.statV {
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 6px;
|
||||
.tips p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
.tips ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ghost:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.one {
|
||||
margin-top: 12px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.oneTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.oneText {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.btnRow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.tips li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,484 +1,306 @@
|
||||
<!--
|
||||
ApiPlayground.vue
|
||||
参考 ide-intro 的“虚拟工具”风格:做一个极简 Postman。
|
||||
目标:可玩、可视化、少字段:
|
||||
- 选操作(拿/加/删)
|
||||
- 填一点(id 或 name)
|
||||
- 钥匙开关
|
||||
- 点发送 -> 看结果
|
||||
ApiPlayground.vue - 简化版
|
||||
目标:用最简单的演示展示 API 调用的各种情况
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">练习场:一个“迷你 Postman”</div>
|
||||
<div class="sub">
|
||||
你不用懂代码。把它当成“按钮调试器”:按对了拿结果,按错了看失败原因。
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo">
|
||||
<div class="title">🎮 练习场:试试调用 API</div>
|
||||
<p class="subtitle">体验一下成功和失败的情况</p>
|
||||
|
||||
<div class="app">
|
||||
<div class="topbar">
|
||||
<div class="brand">API Console</div>
|
||||
<div class="toggles">
|
||||
<button :class="['toggle', { on: keyOn }]" @click="keyOn = !keyOn">
|
||||
钥匙:{{ keyOn ? '有' : '没有' }}
|
||||
<div class="playground">
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>🔑 钥匙(API Key):</label>
|
||||
<button
|
||||
:class="['toggle', { active: hasKey }]"
|
||||
@click="hasKey = !hasKey"
|
||||
>
|
||||
{{ hasKey ? '✅ 有钥匙' : '❌ 没有钥匙' }}
|
||||
</button>
|
||||
<button class="toggle" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>📍 用户 ID:</label>
|
||||
<input
|
||||
v-model="userId"
|
||||
class="input"
|
||||
placeholder="例如:u_123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="call-btn" :disabled="calling" @click="callApi">
|
||||
{{ calling ? '调用中...' : '🚀 调用 API' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="result-area">
|
||||
<div v-if="!result" class="placeholder">
|
||||
还没有结果。点一下"调用 API"试试!
|
||||
</div>
|
||||
<div v-else class="result" :class="result.type">
|
||||
<div class="result-header">
|
||||
{{ result.type === 'success' ? '✅ 成功' : '❌ 失败' }}
|
||||
</div>
|
||||
<div class="result-body">{{ result.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="left">
|
||||
<div class="panelTitle">① 选操作</div>
|
||||
<div class="ops">
|
||||
<button
|
||||
v-for="o in ops"
|
||||
:key="o.id"
|
||||
:class="['op', { active: opId === o.id }]"
|
||||
@click="setOp(o.id)"
|
||||
>
|
||||
<div class="opTitle">{{ o.label }}</div>
|
||||
<div class="opHint">{{ o.hint }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mid">
|
||||
<div class="panelTitle">② 填一点</div>
|
||||
<div class="form">
|
||||
<label class="row" v-if="opId !== 'list'">
|
||||
<span class="k">id</span>
|
||||
<input v-model="idText" class="input" placeholder="例如:u_123" />
|
||||
</label>
|
||||
<label class="row" v-if="opId === 'create'">
|
||||
<span class="k">name</span>
|
||||
<input
|
||||
v-model="nameText"
|
||||
class="input"
|
||||
placeholder="例如:Alice"
|
||||
/>
|
||||
</label>
|
||||
<div class="tip">
|
||||
玩法:
|
||||
<span class="mono">钥匙=没有</span> 再发送一次;或者连续点 4
|
||||
次触发“太频繁”。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="send" :disabled="busy" @click="send">
|
||||
{{ busy ? '发送中…' : '③ 发送(模拟)' }}
|
||||
</button>
|
||||
|
||||
<details class="details">
|
||||
<summary>(选看)这一次你“相当于”发了什么</summary>
|
||||
<pre class="code"><code>{{ pseudo }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="panelTitle">④ 返回结果</div>
|
||||
<div v-if="!res" class="muted">还没有结果。点“发送”。</div>
|
||||
<div v-else class="resBox" :class="{ ok: res.ok, bad: !res.ok }">
|
||||
<div class="badge">{{ res.ok ? '成功' : '失败' }}</div>
|
||||
<div class="resText">{{ res.text }}</div>
|
||||
<pre v-if="res.data" class="code"><code>{{ res.data }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<p><strong>💡 玩法建议:</strong></p>
|
||||
<ul>
|
||||
<li>试试把"钥匙"改成"没有钥匙",看看会发生什么</li>
|
||||
<li>试试把 ID 改成 <code>u_404</code>,看看会怎样</li>
|
||||
<li>连续快速点击,看看"限流"提示</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const ops = [
|
||||
{ id: 'list', label: '拿列表(GET)', hint: '不改数据' },
|
||||
{ id: 'get', label: '拿一个(GET)', hint: '需要 id' },
|
||||
{ id: 'create', label: '加一个(POST)', hint: '需要 name' },
|
||||
{ id: 'delete', label: '删一个(DELETE)', hint: '需要 id' }
|
||||
]
|
||||
const hasKey = ref(true)
|
||||
const userId = ref('u_123')
|
||||
const calling = ref(false)
|
||||
const result = ref(null)
|
||||
const callCount = ref([])
|
||||
const now = ref(Date.now())
|
||||
|
||||
const opId = ref('list')
|
||||
const keyOn = ref(true)
|
||||
const idText = ref('u_123')
|
||||
const nameText = ref('Alice')
|
||||
const busy = ref(false)
|
||||
const res = ref(null)
|
||||
const callTimes = ref([])
|
||||
function callApi() {
|
||||
calling.value = true
|
||||
result.value = null
|
||||
|
||||
const pseudo = computed(() => {
|
||||
const key = keyOn.value ? "X-Api-Key: '****'" : '(没有钥匙)'
|
||||
if (opId.value === 'list') return `GET /v1/users\n${key}`
|
||||
if (opId.value === 'get')
|
||||
return `GET /v1/users/${idText.value || '{id}'}\n${key}`
|
||||
if (opId.value === 'create')
|
||||
return `POST /v1/users\n${key}\nBody: { name: '${nameText.value || ''}' }`
|
||||
return `DELETE /v1/users/${idText.value || '{id}'}\n${key}`
|
||||
})
|
||||
// 模拟限流
|
||||
const currentTime = Date.now()
|
||||
callCount.value = callCount.value.filter(t => currentTime - t < 2000)
|
||||
callCount.value.push(currentTime)
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
function setOp(id) {
|
||||
opId.value = id
|
||||
res.value = null
|
||||
}
|
||||
|
||||
function reset() {
|
||||
opId.value = 'list'
|
||||
keyOn.value = true
|
||||
idText.value = 'u_123'
|
||||
nameText.value = 'Alice'
|
||||
res.value = null
|
||||
callTimes.value = []
|
||||
}
|
||||
|
||||
async function send() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
res.value = null
|
||||
|
||||
const now = Date.now()
|
||||
callTimes.value = callTimes.value.filter((t) => now - t < 1200)
|
||||
callTimes.value.push(now)
|
||||
if (callTimes.value.length >= 4) {
|
||||
await sleep(180)
|
||||
res.value = { ok: false, text: '太频繁了,请慢一点(模拟)' }
|
||||
busy.value = false
|
||||
if (callCount.value.length >= 4) {
|
||||
setTimeout(() => {
|
||||
result.value = {
|
||||
type: 'error',
|
||||
message: '太频繁了!请慢一点再试(限流)'
|
||||
}
|
||||
calling.value = false
|
||||
}, 300)
|
||||
return
|
||||
}
|
||||
|
||||
await sleep(300)
|
||||
|
||||
if (!keyOn.value) {
|
||||
res.value = { ok: false, text: '没有钥匙(没权限,模拟)' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (opId.value === 'list') {
|
||||
res.value = {
|
||||
ok: true,
|
||||
text: '拿到列表(模拟)',
|
||||
data: JSON.stringify(
|
||||
{
|
||||
data: [
|
||||
{ id: 'u_123', name: 'Alice' },
|
||||
{ id: 'u_124', name: 'Bob' }
|
||||
]
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
setTimeout(() => {
|
||||
// 检查钥匙
|
||||
if (!hasKey.value) {
|
||||
result.value = {
|
||||
type: 'error',
|
||||
message: '没有钥匙!你没有权限调用这个 API(401 未授权)'
|
||||
}
|
||||
calling.value = false
|
||||
return
|
||||
}
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (opId.value === 'get') {
|
||||
const id = String(idText.value || '').trim()
|
||||
// 检查用户 ID
|
||||
const id = userId.value.trim()
|
||||
if (!id) {
|
||||
res.value = { ok: false, text: '你还没填 id' }
|
||||
busy.value = false
|
||||
result.value = {
|
||||
type: 'error',
|
||||
message: '你还没填用户 ID!'
|
||||
}
|
||||
calling.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (id === 'u_404') {
|
||||
res.value = { ok: false, text: '找不到这个 id(模拟)' }
|
||||
busy.value = false
|
||||
result.value = {
|
||||
type: 'error',
|
||||
message: '找不到这个用户!ID 不存在(404)'
|
||||
}
|
||||
calling.value = false
|
||||
return
|
||||
}
|
||||
res.value = {
|
||||
ok: true,
|
||||
text: `拿到用户 ${id}(模拟)`,
|
||||
data: JSON.stringify({ id, name: 'Alice' }, null, 2)
|
||||
}
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (opId.value === 'create') {
|
||||
const name = String(nameText.value || '').trim()
|
||||
if (!name) {
|
||||
res.value = { ok: false, text: '你还没填 name' }
|
||||
busy.value = false
|
||||
return
|
||||
// 成功
|
||||
result.value = {
|
||||
type: 'success',
|
||||
message: `成功获取用户信息:\nID: ${id}\n姓名: 张三\n邮箱: zhang@example.com`
|
||||
}
|
||||
res.value = {
|
||||
ok: true,
|
||||
text: `创建成功(模拟)`,
|
||||
data: JSON.stringify({ id: 'u_789', name }, null, 2)
|
||||
}
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// delete
|
||||
const id = String(idText.value || '').trim()
|
||||
if (!id) {
|
||||
res.value = { ok: false, text: '你还没填 id' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
res.value = { ok: true, text: `删除成功:${id}(模拟)` }
|
||||
busy.value = false
|
||||
calling.value = false
|
||||
}, 800)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.app {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
.playground {
|
||||
background: var(--vp-c-bg);
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.toggles {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle.on {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 0.9fr 1fr 1.1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.left,
|
||||
.mid,
|
||||
.right {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
.control-group label {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.ops {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.op {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
.toggle {
|
||||
padding: 8px 16px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.op.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);
|
||||
}
|
||||
|
||||
.opTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.opHint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.k {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 900;
|
||||
.toggle.active {
|
||||
border-color: #22c55e;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.send {
|
||||
margin-top: 12px;
|
||||
.call-btn {
|
||||
width: 100%;
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
padding: 12px 20px;
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 900;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.send:disabled {
|
||||
.call-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.call-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
.result-area {
|
||||
min-height: 120px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.result {
|
||||
border: 2px solid;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result.success {
|
||||
border-color: #22c55e;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.result.error {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
padding: 12px 16px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result.success .result-header {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.result.error .result-header {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.result-body {
|
||||
padding: 12px 16px;
|
||||
font-size: 13px;
|
||||
white-space: pre-line;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
'Courier New', monospace;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 12px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
.tips {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.details summary {
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 10px;
|
||||
.tips p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tips ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.tips li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.tips code {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.resBox {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resBox.ok {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.resBox.bad {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.resText {
|
||||
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);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,60 +1,172 @@
|
||||
<!--
|
||||
RealWorldApiDemo.vue
|
||||
参考 ide-intro 的“虚拟 UI + 一眼看懂”的风格。
|
||||
目标:解释“为什么 SDK 的调用也叫 API”:
|
||||
- 左边:SDK 按钮(你调用函数)
|
||||
- 右边:HTTP 按钮(你发请求)
|
||||
两个按钮得到的结果一样:只是包装方式不同。
|
||||
目标:展示真实场景中调用 AI 服务的两种方式
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">SDK vs HTTP:其实都是“按按钮拿结果”</div>
|
||||
<div class="sub">
|
||||
你可以把它理解成:<b>同一个按钮</b>,只是有两种“按法”。
|
||||
<div class="demo">
|
||||
<div class="title">🤖 真实场景:让 AI 帮你写产品文案</div>
|
||||
<p class="subtitle">体验两种调用方式的区别</p>
|
||||
|
||||
<div class="scenario">
|
||||
<div class="scenario-header">
|
||||
<span class="scenario-icon">📝</span>
|
||||
<span class="scenario-title">你的需求</span>
|
||||
</div>
|
||||
<div class="scenario-body">
|
||||
我想让 AI 帮智能手表写一段吸引人的产品文案
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="cardTitle">方式 A:SDK 按钮(调用函数)</div>
|
||||
<div class="cardBody">
|
||||
<button class="call sdk" :disabled="busy" @click="call('sdk')">
|
||||
{{ busy ? '执行中…' : '按一下(SDK)' }}
|
||||
</button>
|
||||
<div class="muted">你写的通常是:<b>client.users.get(...)</b></div>
|
||||
<details class="details">
|
||||
<summary>(选看)长什么样</summary>
|
||||
<pre class="code"><code>{{ sdkCode }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
<div class="modes">
|
||||
<div class="mode-tabs">
|
||||
<button
|
||||
:class="['tab', { active: mode === 'http' }]"
|
||||
@click="mode = 'http'"
|
||||
>
|
||||
🌐 HTTP API(外卖模式)
|
||||
</button>
|
||||
<button
|
||||
:class="['tab', { active: mode === 'sdk' }]"
|
||||
@click="mode = 'sdk'"
|
||||
>
|
||||
📦 SDK(堂食模式)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="cardTitle">方式 B:HTTP 按钮(发请求)</div>
|
||||
<div class="cardBody">
|
||||
<button class="call http" :disabled="busy" @click="call('http')">
|
||||
{{ busy ? '执行中…' : '按一下(HTTP)' }}
|
||||
</button>
|
||||
<div class="muted">你写的通常是:<b>fetch(url, headers)</b></div>
|
||||
<details class="details">
|
||||
<summary>(选看)长什么样</summary>
|
||||
<pre class="code"><code>{{ httpCode }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-content">
|
||||
<!-- HTTP 模式 -->
|
||||
<div v-if="mode === 'http'" class="mode-details">
|
||||
<div class="steps">
|
||||
<div class="step" :class="{ active: currentStep >= 1 }">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">找到网址(打开外卖 APP)</div>
|
||||
<div class="step-code">https://api.openai.com/v1/chat/completions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="cardTitle">结果(两边一样)</div>
|
||||
<div class="cardBody">
|
||||
<div v-if="!res" class="muted">先按一下左边或右边的按钮。</div>
|
||||
<div v-else class="resBox">
|
||||
<div class="badge">返回</div>
|
||||
<div class="resText">{{ res }}</div>
|
||||
<div class="step" :class="{ active: currentStep >= 2 }">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">准备订单(填写信息)</div>
|
||||
<div class="step-code">
|
||||
Authorization: Bearer 你的API密钥<br>
|
||||
Content-Type: application/json
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">下单(发送请求)</div>
|
||||
<div class="step-code">
|
||||
{<br>
|
||||
"model": "gpt-4",<br>
|
||||
"messages": [<br>
|
||||
{ "role": "system", "content": "你是营销文案专家" },<br>
|
||||
{ "role": "user", "content": "写智能手表文案" }<br>
|
||||
]<br>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" :class="{ active: currentStep >= 4 }">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">等待配送(解析响应)</div>
|
||||
<div class="step-code">
|
||||
response.choices[0].message.content<br>
|
||||
<span class="step-hint">⚠️ 需要自己处理解析错误</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tip">
|
||||
结论:<b>SDK 的“函数入口”也叫 API</b
|
||||
>,因为它也是对外公开的“怎么用”。
|
||||
|
||||
<div class="summary">
|
||||
<p><strong>💡 HTTP API 特点:</strong></p>
|
||||
<ul>
|
||||
<li>✅ 灵活:任何语言都能用</li>
|
||||
<li>❌ 复杂:要手动处理很多细节</li>
|
||||
<li>❌ 容易出错:鉴权、数据格式、错误处理都要自己写</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SDK 模式 -->
|
||||
<div v-else class="mode-details">
|
||||
<div class="steps">
|
||||
<div class="step" :class="{ active: currentStep >= 1 }">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">走进餐厅(安装 SDK)</div>
|
||||
<div class="step-code">import OpenAI from 'openai'</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" :class="{ active: currentStep >= 2 }">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">找服务员(初始化客户端)</div>
|
||||
<div class="step-code">
|
||||
const client = new OpenAI({<br>
|
||||
apiKey: '你的密钥'<br>
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">直接点菜(调用函数)</div>
|
||||
<div class="step-code">
|
||||
const response = await client.chat.completions.create({<br>
|
||||
model: 'gpt-4',<br>
|
||||
messages: [<br>
|
||||
{ role: 'system', content: '你是营销文案专家' },<br>
|
||||
{ role: 'user', content: '写智能手表文案' }<br>
|
||||
]<br>
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" :class="{ active: currentStep >= 4 }">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">享用美食(直接使用)</div>
|
||||
<div class="step-code">
|
||||
console.log(response.choices[0].message.content)<br>
|
||||
<span class="step-hint">✅ SDK 帮你处理好了所有细节</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<p><strong>💡 SDK 特点:</strong></p>
|
||||
<ul>
|
||||
<li>✅ 简单:只管调用函数</li>
|
||||
<li>✅ 省心:SDK 自动处理鉴权、错误、数据格式</li>
|
||||
<li>❌ 限制:通常只能在特定语言使用</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action">
|
||||
<button class="run-btn" :disabled="running" @click="runDemo">
|
||||
{{ running ? '调用中...' : '🚀 开始调用 AI' }}
|
||||
</button>
|
||||
|
||||
<div class="result" v-if="result">
|
||||
<div class="result-header">
|
||||
{{ mode === 'http' ? '🌐 HTTP API 返回' : '📦 SDK 返回' }}
|
||||
</div>
|
||||
<div class="result-body">
|
||||
"这款智能手表,是你的贴身健康管家。全天候心率监测,运动模式自动识别..."
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,189 +175,272 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const busy = ref(false)
|
||||
const res = ref('')
|
||||
const mode = ref('http')
|
||||
const currentStep = ref(0)
|
||||
const running = ref(false)
|
||||
const result = ref(null)
|
||||
|
||||
const sdkCode = computed(
|
||||
() => `import { AcmeClient } from 'acme-sdk'
|
||||
async function runDemo() {
|
||||
running.value = true
|
||||
result.value = null
|
||||
currentStep.value = 0
|
||||
|
||||
const client = new AcmeClient({ apiKey: '****' })
|
||||
const user = await client.users.get({ id: 'u_123' })
|
||||
console.log(user)`
|
||||
)
|
||||
// 模拟逐步执行
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 600))
|
||||
currentStep.value = i
|
||||
}
|
||||
|
||||
const httpCode = computed(
|
||||
() => `const res = await fetch('https://api.acme.com/v1/users/u_123', {
|
||||
headers: { 'X-Api-Key': '****' }
|
||||
})
|
||||
const user = await res.json()
|
||||
console.log(user)`
|
||||
)
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function call(which) {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
res.value = ''
|
||||
await sleep(280)
|
||||
res.value =
|
||||
which === 'sdk'
|
||||
? '用户:{ id: "u_123", name: "Alice" }(模拟)'
|
||||
: '用户:{ id: "u_123", name: "Alice" }(模拟)'
|
||||
busy.value = false
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
result.value = true
|
||||
running.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
.scenario {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-brand-1);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scenario-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.scenario-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.scenario-body {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: var(--vp-c-text-1);
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.modes {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.cardBody {
|
||||
padding: 12px;
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 14px 20px;
|
||||
border: none;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.mode-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.call {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
padding: 12px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step.active .step-number {
|
||||
background: var(--vp-c-brand-1);
|
||||
border-color: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.call.sdk {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
.step-code {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.call.http {
|
||||
border-color: color-mix(in srgb, #60a5fa 45%, var(--vp-c-divider));
|
||||
.step-hint {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.call:disabled {
|
||||
.step.active .step-hint {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.summary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.summary p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.summary ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.summary li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
padding: 20px;
|
||||
border-top: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.run-btn {
|
||||
width: 100%;
|
||||
padding: 14px 24px;
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.run-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.run-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.muted {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
.result {
|
||||
margin-top: 16px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.details {
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.details summary {
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.code {
|
||||
margin: 10px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.resBox {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.resText {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.result-header {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.result-body {
|
||||
background: #f0fdf4;
|
||||
border: 2px solid #86efac;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #166534;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,403 +1,207 @@
|
||||
<!--
|
||||
RequestResponseFlow.vue
|
||||
参考 ide-intro 的“可视化演示”风格:更像一个小动画,而不是表单。
|
||||
目标:让新手理解一次 API 调用:你发过去 -> 对方处理 -> 回给你。
|
||||
RequestResponseFlow.vue - 简化版
|
||||
目标:用简单的动画展示请求-响应流程
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">一次 API 调用,会发生什么?</div>
|
||||
<div class="sub">点一下“发送”,看小纸飞机飞过去再飞回来。</div>
|
||||
</div>
|
||||
<div class="demo">
|
||||
<div class="title">🔄 一次 API 调用的流程</div>
|
||||
<p class="subtitle">点一下按钮,看请求怎么飞过去再飞回来</p>
|
||||
|
||||
<div class="controls">
|
||||
<div class="ctrl">
|
||||
<div class="ctrlK">地址</div>
|
||||
<div class="ctrlV">
|
||||
<button :class="['pill', { active: addrOk }]" @click="addrOk = true">
|
||||
正确
|
||||
</button>
|
||||
<button
|
||||
:class="['pill', { active: !addrOk }]"
|
||||
@click="addrOk = false"
|
||||
>
|
||||
错误
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl">
|
||||
<div class="ctrlK">钥匙</div>
|
||||
<div class="ctrlV">
|
||||
<button :class="['pill', { active: keyOk }]" @click="keyOk = true">
|
||||
有
|
||||
</button>
|
||||
<button :class="['pill', { active: !keyOk }]" @click="keyOk = false">
|
||||
没有
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="send" :disabled="busy" @click="send">
|
||||
{{ busy ? '飞行中…' : '发送一次(模拟)' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
<div class="side">
|
||||
<div class="flow-container">
|
||||
<div class="side you">
|
||||
<div class="window">
|
||||
<div class="bar">
|
||||
<span class="dot red" />
|
||||
<span class="dot yellow" />
|
||||
<span class="dot green" />
|
||||
<span class="barText">你这边(你的程序)</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="bubble mine">
|
||||
我想按按钮拿结果。
|
||||
<div class="small">
|
||||
地址:{{ addrOk ? '正确' : '错误' }};钥匙:{{
|
||||
keyOk ? '有' : '没有'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-header">👤 你这边</div>
|
||||
<div class="window-body">
|
||||
<div class="message">我想调用 API</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mid">
|
||||
<div class="line" />
|
||||
<div class="plane" :class="{ go: flying }">✈︎</div>
|
||||
<div class="line" />
|
||||
<div class="middle">
|
||||
<div class="arrow" :class="{ animating: isAnimating }">➔</div>
|
||||
<button class="send-btn" :disabled="isAnimating" @click="send">
|
||||
{{ isAnimating ? '发送中...' : '🚀 发送请求' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="side">
|
||||
<div class="side server">
|
||||
<div class="window">
|
||||
<div class="bar">
|
||||
<span class="dot red" />
|
||||
<span class="dot yellow" />
|
||||
<span class="dot green" />
|
||||
<span class="barText">对方那边(提供能力)</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="bubble theirs">
|
||||
{{ serverText }}
|
||||
<div class="small">(它按规则检查:地址/钥匙/参数…)</div>
|
||||
</div>
|
||||
<div class="window-header">🖥️ 对方服务器</div>
|
||||
<div class="window-body">
|
||||
<div class="message">{{ serverMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result">
|
||||
<div class="resultTitle">返回结果</div>
|
||||
<div v-if="!response" class="muted">还没有结果。点一下“发送一次”。</div>
|
||||
<div
|
||||
v-else
|
||||
class="resBox"
|
||||
:class="{ ok: response.ok, bad: !response.ok }"
|
||||
>
|
||||
<div class="badge">{{ response.ok ? '成功' : '失败' }}</div>
|
||||
<div class="resText">{{ response.text }}</div>
|
||||
</div>
|
||||
<div class="tip">
|
||||
玩法:把“地址”改成错误,或者把“钥匙”改成没有,再发送一次。
|
||||
<div class="result" v-if="result">
|
||||
<div class="result-box" :class="result.type">
|
||||
{{ result.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const addrOk = ref(true)
|
||||
const keyOk = ref(true)
|
||||
const busy = ref(false)
|
||||
const flying = ref(false)
|
||||
const response = ref(null)
|
||||
const isAnimating = ref(false)
|
||||
const serverMessage = ref('等待请求...')
|
||||
const result = ref(null)
|
||||
|
||||
const serverText = computed(() => {
|
||||
if (!addrOk.value) return '我找不到这个按钮(地址错了)'
|
||||
if (!keyOk.value) return '你没有钥匙,我不能给你结果'
|
||||
return '收到!我去处理一下…'
|
||||
})
|
||||
function send() {
|
||||
isAnimating.value = true
|
||||
serverMessage.value = '收到请求,处理中...'
|
||||
result.value = null
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function send() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
flying.value = true
|
||||
response.value = null
|
||||
|
||||
await sleep(600)
|
||||
flying.value = false
|
||||
|
||||
await sleep(260)
|
||||
if (!addrOk.value) {
|
||||
response.value = { ok: false, text: '失败:地址不对(找不到这个按钮)' }
|
||||
} else if (!keyOk.value) {
|
||||
response.value = { ok: false, text: '失败:没有钥匙(没权限)' }
|
||||
} else {
|
||||
response.value = { ok: true, text: '成功:拿到结果(模拟)' }
|
||||
}
|
||||
busy.value = false
|
||||
// 模拟请求流程
|
||||
setTimeout(() => {
|
||||
serverMessage.value = '处理完成!'
|
||||
result.value = {
|
||||
type: 'success',
|
||||
text: '✅ 请求成功!服务器返回了数据'
|
||||
}
|
||||
isAnimating.value = false
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 12px;
|
||||
.flow-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ctrl {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
.side {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.window {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.ctrlK {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.ctrlV {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pill {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pill.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);
|
||||
}
|
||||
|
||||
.send {
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.send:disabled {
|
||||
.window-header {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 12px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.window-body {
|
||||
padding: 20px;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.middle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 32px;
|
||||
color: var(--vp-c-brand-1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.arrow.animating {
|
||||
animation: pulse 0.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stage {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.window {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background: #ef4444;
|
||||
}
|
||||
.dot.yellow {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.dot.green {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.barText {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 12px;
|
||||
min-height: 92px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.bubble.mine {
|
||||
border-color: color-mix(in srgb, #60a5fa 40%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.bubble.theirs {
|
||||
border-color: color-mix(in srgb, #a78bfa 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.small {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mid {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 99px;
|
||||
}
|
||||
|
||||
.plane {
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-text-3);
|
||||
transition:
|
||||
transform 600ms ease,
|
||||
color 200ms ease;
|
||||
}
|
||||
|
||||
.plane.go {
|
||||
transform: translateX(28px);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.resultTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
.result-box {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
.result-box.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border: 1px solid #86efac;
|
||||
}
|
||||
|
||||
.resBox {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resBox.ok {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.resBox.bad {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.resText {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
.result-box.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.stage {
|
||||
grid-template-columns: 1fr;
|
||||
.flow-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.mid {
|
||||
display: none;
|
||||
|
||||
.middle {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,148 +1,382 @@
|
||||
# API 入门(0 基础版)
|
||||
|
||||
> 💡 **学习指南**:这是写给 0 基础新手的。你先记住一句话:**API 就是“别的软件给你用的按钮/入口”**。你按它的规则“提交信息”,它按规则“把结果给你”。
|
||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示和生动比喻带你深入理解 API 的核心概念。我们将从"餐厅点餐"这个生活场景讲起,一步步揭开 API 的神秘面纱。
|
||||
|
||||
<ApiQuickStartDemo />
|
||||
|
||||
---
|
||||
## 0. 引言:从餐厅点餐到软件协作
|
||||
|
||||
## 0. 引言:你真正依赖的不是“服务器”,而是“接口”
|
||||
想象一下,你走进一家餐厅:
|
||||
|
||||
当你说“我要调一下接口”,你其实是在说:
|
||||
1. **你**(顾客)拿着菜单,告诉服务员:"我要一份宫保鸡丁,加辣。"
|
||||
2. **服务员**(接口)把你的要求记下来,送到厨房。
|
||||
3. **厨房**(对方的系统)根据要求做菜。
|
||||
4. **服务员**把做好的菜端给你。
|
||||
|
||||
> 我希望<strong>按某种约定</strong>把输入交给对方系统,然后<strong>按约定</strong>拿到输出(成功或失败)。
|
||||
在这个过程中,**你不需要知道**:
|
||||
- 厨房有几个厨师
|
||||
- 他们用什么锅铲炒菜
|
||||
- 蔬菜是从哪个市场买的
|
||||
- 厨师今天心情好不好
|
||||
|
||||
这份“约定”就是 API。
|
||||
**你只需要知道**:
|
||||
- 怎么点菜(喊服务员或填单子)
|
||||
- 要告诉对方什么(菜名、口味要求)
|
||||
- 会得到什么(你点的菜)
|
||||
|
||||
你可以先把 API 当成一句大白话:
|
||||
**这就是 API 的本质**:它就像餐厅的**服务员**,是你和"对方的系统"之间的**桥梁**。
|
||||
|
||||
> API = 别的软件给你用的“入口”:你按它说的来,它把结果给你。
|
||||
> **API(Application Programming Interface)** = **应用之间的"接口/入口"**:你按约定把请求交给对方,对方按约定把结果给你。
|
||||
|
||||
---
|
||||
|
||||
## 1. 什么是 API?(新手版一句话)
|
||||
## 1. 核心:API 的三个关键问题
|
||||
|
||||
**API** 可以翻译成:**应用之间的“接口/入口”**。
|
||||
就像去餐厅点菜一样,使用 API 时你只需要搞清楚 3 个问题:
|
||||
|
||||
对新手来说,你只要先记住 3 件事(够用了):
|
||||
### 1.1 怎么点菜?(入口在哪里)
|
||||
|
||||
1. **怎么用它**(入口是什么:一个网址 / 一个函数名)
|
||||
2. **要填什么**(你要告诉它哪些信息)
|
||||
3. **会得到什么**(成功给你什么;失败会怎么提示)
|
||||
你首先得知道"怎么叫服务员"。在软件世界里,入口通常有两种:
|
||||
|
||||
- **HTTP API**:像一个"网址",你发送网络请求过去
|
||||
- 例如:`https://api.example.com/getUser`
|
||||
- **SDK/API**:像一个"函数名",你在代码里直接调用
|
||||
- 例如:`getUserInfo(userId)`
|
||||
|
||||
### 1.2 要说什么?(你要填什么信息)
|
||||
|
||||
你不能只说"我要菜",你得告诉服务员:
|
||||
- 你要什么菜?(模型名称)
|
||||
- 有什么要求?(提示词、参数)
|
||||
- 你是谁?(API Key,相当于会员卡)
|
||||
|
||||
### 1.3 会得到什么?(成功/失败的结果)
|
||||
|
||||
服务员可能会端来:
|
||||
- ✅ 你点的菜(成功返回的数据)
|
||||
- ❌ "不好意思,这道菜卖完了"(错误提示,比如 404、500)
|
||||
|
||||
<ApiConceptDemo />
|
||||
|
||||
### 1.1 API 不等于“实现”
|
||||
### 1.4 API 的核心价值:把复杂度藏起来
|
||||
|
||||
API 只描述“怎么用”,不描述“怎么做”。
|
||||
回到餐厅的比喻:
|
||||
|
||||
比如一个“获取用户信息”的 API,调用方需要知道:
|
||||
**餐厅**需要做的事情(实现细节):
|
||||
- 采购食材、处理库存
|
||||
- 安排厨师、协调后厨
|
||||
- 控制火候、调味摆盘
|
||||
- 清洗餐具、打扫卫生
|
||||
|
||||
- 你要带用户 id 吗?
|
||||
- 你要不要带 Token?
|
||||
- 成功返回什么字段?
|
||||
- 用户不存在会返回什么错误?
|
||||
**顾客**完全不需要知道这些!顾客只需要:
|
||||
- 看菜单点菜
|
||||
- 等菜上桌
|
||||
- 享用美食
|
||||
|
||||
但调用方不需要知道:
|
||||
**API 就像菜单和服务员**,它把"怎么做"的复杂度全部藏起来,只暴露"怎么用"的简单接口。
|
||||
|
||||
- 用户表怎么设计
|
||||
- 服务拆成几个微服务
|
||||
- 缓存怎么做
|
||||
|
||||
把实现细节藏起来,让双方能**各做各的**,这就是 API 的价值。
|
||||
这就带来两个好处:
|
||||
1. **简化使用**:调用者不需要理解内部实现
|
||||
2. **灵活变更**:餐厅换了厨师、改了做法,但菜单不变,顾客完全无感
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么 HTTP 调用可以叫 API?
|
||||
## 2. 两种常见的 API 形式
|
||||
|
||||
因为用“网址发请求”本身就像按一个按钮:你把信息发过去,对方把结果回给你。(这类通常就叫 HTTP API)
|
||||
在现实世界里,你会遇到两种"点菜方式":
|
||||
|
||||
你不用记复杂的术语,先看流程就够了:**发过去 → 对方处理 → 回给你**。
|
||||
### 2.1 外卖配送(HTTP API)
|
||||
|
||||
你不用亲自去餐厅,只需要:
|
||||
1. 打开外卖 APP(找到入口:网址)
|
||||
2. 选好菜品、填好地址(准备请求:参数)
|
||||
3. 等外卖员送到(接收响应:数据)
|
||||
|
||||
**HTTP API** 就是这种方式:通过网络发送请求,等待返回结果。
|
||||
|
||||
**流程是这样的**:
|
||||
```
|
||||
你的电脑 → 发送请求 → 网络传输 → 对方服务器
|
||||
↓
|
||||
处理你的请求
|
||||
↓
|
||||
你的电脑 ← 接收结果 ← 网络传输 ← 对方服务器
|
||||
```
|
||||
|
||||
<RequestResponseFlow />
|
||||
|
||||
一句话总结:HTTP API 就是“用网址去叫别人做事”。
|
||||
**举个例子**:调用 AI 模型生成文本
|
||||
- 你发送:"帮我写一首关于春天的诗"
|
||||
- 对方处理:调用大语言模型生成
|
||||
- 你接收:返回生成的诗歌
|
||||
|
||||
### 2.2 餐堂堂食(SDK/API)
|
||||
|
||||
你走进餐厅,直接对服务员说:
|
||||
- "来一份宫保鸡丁"
|
||||
|
||||
**SDK(软件开发工具包)** 就像是餐厅的"服务员",它已经在餐厅里了,你只需要说话(调用函数),它会帮你:
|
||||
- 把要求转达给厨房(内部帮你调用 HTTP API)
|
||||
- 处理各种复杂细节(鉴权、重试、数据格式)
|
||||
- 最后把结果整理好给你
|
||||
|
||||
**所以你会听到两种说法**:
|
||||
- "调用这个服务的 API"(通常指 HTTP API,像外卖)
|
||||
- "调用这个 SDK 的 API"(通常指函数接口,像堂食)
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么 SDK 的调用接口也叫 API?
|
||||
## 3. 真实世界:怎么和 AI 服务"对话"
|
||||
|
||||
因为 SDK 本质上也是一个“工具包/库”。它对外公开的函数/方法,本来就可以叫 API(入口)。
|
||||
让我们看一个真实的例子:调用 AI 模型。
|
||||
|
||||
同时,很多 SDK 会在背后帮你调用 HTTP API(还会顺便帮你处理:加“钥匙”、失败重试、把数据整理成好用的样子),所以大家会同时说:
|
||||
**场景**:你想让 AI 帮你写一段产品文案。
|
||||
|
||||
- “这个服务的 API”(通常指 HTTP 接口)
|
||||
- “这个 SDK 的 API”(通常指库里的函数接口)
|
||||
### 3.1 用 HTTP API 的方式(外卖模式)
|
||||
|
||||
就像发外卖订单一样,你需要:
|
||||
|
||||
```bash
|
||||
# 1. 打开外卖 APP(找到网址)
|
||||
curl https://api.openai.com/v1/chat/completions
|
||||
|
||||
# 2. 选好菜品、填好地址(带上你的信息和要求)
|
||||
--header 'Authorization: Bearer 你的API密钥' # 你的会员卡
|
||||
--header 'Content-Type: application/json' # 说明你点的是菜单(JSON格式)
|
||||
|
||||
# 3. 告诉服务员你要什么(请求内容)
|
||||
--data '{
|
||||
"model": "gpt-4", # 选哪个厨师
|
||||
"messages": [ # 你的要求
|
||||
{ "role": "system", "content": "你是一个营销文案专家" },
|
||||
{ "role": "user", "content": "帮我为这款智能手表写一段吸引人的产品文案" }
|
||||
]
|
||||
}'
|
||||
|
||||
# 4. 等待配送(接收响应)
|
||||
# 返回:{"choices": [{"message": {"content": "生成的文案..."}}]}
|
||||
```
|
||||
|
||||
### 3.2 用 SDK 的方式(堂食模式)
|
||||
|
||||
就像走进餐厅直接点餐:
|
||||
|
||||
```javascript
|
||||
// 安装 SDK(相当于走进餐厅)
|
||||
import OpenAI from 'openai';
|
||||
|
||||
// 创建"服务员"(初始化客户端)
|
||||
const client = new OpenAI({
|
||||
apiKey: '你的API密钥' # 你的会员卡
|
||||
});
|
||||
|
||||
// 直接点菜(调用函数)
|
||||
const response = await client.chat.completions.create({
|
||||
model: 'gpt-4', # 选哪个厨师
|
||||
messages: [ # 你的要求
|
||||
{ role: 'system', content: '你是一个营销文案专家' },
|
||||
{ role: 'user', content: '帮我为这款智能手表写一段吸引人的产品文案' }
|
||||
]
|
||||
});
|
||||
|
||||
// 享用美食(使用结果)
|
||||
console.log(response.choices[0].message.content);
|
||||
```
|
||||
|
||||
**看出来了吗?** SDK 的方式更简单,因为它帮你处理了很多细节!
|
||||
|
||||
<RealWorldApiDemo />
|
||||
|
||||
### 3.3 两种方式的对比
|
||||
|
||||
| 特点 | HTTP API(外卖) | SDK API(堂食) |
|
||||
|------|------------------|-----------------|
|
||||
| **使用门槛** | 需要理解网络请求、数据格式 | 只需会调用函数 |
|
||||
| **灵活性** | 更灵活,任何语言都能用 | 通常限定特定语言 |
|
||||
| **复杂度** | 你要处理很多细节(鉴权、错误等) | SDK 帮你处理细节 |
|
||||
| **典型场景** | 跨语言调用、学习原理 | 日常开发、快速集成 |
|
||||
|
||||
---
|
||||
|
||||
## 4. (选读)GET/POST 这些词到底在说什么?
|
||||
## 4. 进阶:GET 和 POST 有什么区别?
|
||||
|
||||
新手可以先跳过这一节:你只要先学会“看文档、会调用”就够用了。
|
||||
> 🎯 **新手提示**:这一节可以暂时跳过。等你熟悉了基本调用,再回来了解也不迟。
|
||||
|
||||
<details>
|
||||
<summary>点我展开:进阶一点点(但我尽量讲人话)</summary>
|
||||
<summary>点我展开:进阶内容(用人话讲)</summary>
|
||||
|
||||
在 HTTP API 的世界里,你会经常看到 **GET** 和 **POST** 这两个词。它们就像两种不同的"点菜方式"。
|
||||
|
||||
### 4.1 GET:像看菜单(只看不吃)
|
||||
|
||||
**特点**:
|
||||
- 只是想"获取"信息,不会改变服务器状态
|
||||
- 就像在餐厅看菜单,你看一遍菜单,厨房的菜不会被消耗
|
||||
- **可以安全重试**:看一遍菜单没看清,再看一遍,没问题
|
||||
|
||||
**例子**:
|
||||
- 查询用户信息:`GET /api/user/123`
|
||||
- 搜索商品:`GET /api/products?keyword=手机`
|
||||
- 获取文章列表:`GET /api/articles`
|
||||
|
||||
### 4.2 POST:像下单(会真的执行)
|
||||
|
||||
**特点**:
|
||||
- 会"创建"或"修改"服务器上的数据
|
||||
- 就像你下了单,厨房真的开始做菜了
|
||||
- **不能随意重试**:下错了单,再下一遍,你就点了双份!
|
||||
|
||||
**例子**:
|
||||
- 创建用户:`POST /api/users`(会真的创建一个新用户)
|
||||
- 下单购买:`POST /api/orders`(会真的扣钱、发货)
|
||||
- 发表评论:`POST /api/comments`(会真的保存一条评论)
|
||||
|
||||
<ApiMethodDemo />
|
||||
|
||||
这节想讲的只有一件事:有些请求“重试很安全”(比如 GET),有些“重试可能出事”(比如创建接口)。
|
||||
### 4.3 还有哪些方法?(简单了解)
|
||||
|
||||
除了 GET 和 POST,还有:
|
||||
- **PUT**:更新(替换整个资源)
|
||||
- **PATCH**:打补丁(更新部分字段)
|
||||
- **DELETE**:删除
|
||||
|
||||
**新手建议**:先学会用 GET 和 POST,其他的慢慢来。
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 5. 怎么读 API 文档?(先看能不能用,再看怎么用)
|
||||
## 5. 怎么读 API 文档?(像看菜单一样简单)
|
||||
|
||||
API 文档可以当成“菜单 + 说明书”:
|
||||
API 文档就像餐厅的**菜单 + 说明书**,告诉你:
|
||||
- 有哪些菜可以点(提供哪些功能)
|
||||
- 每道菜是什么(接口说明)
|
||||
- 怎么点(怎么调用)
|
||||
- 什么价格(返回什么数据)
|
||||
- 有没有忌口/限量(注意事项)
|
||||
|
||||
<ApiDocumentDemo />
|
||||
|
||||
### 5.1 阅读 API 文档的 5 步
|
||||
### 5.1 阅读 API 文档的 5 步法
|
||||
|
||||
1. **确认能力**:这个接口是不是你要的(做什么)
|
||||
2. **确认怎么用**:网址/函数名 + 需要填什么
|
||||
3. **确认参数**:必填/可选/默认值/类型
|
||||
4. **确认返回**:字段含义、是否可能为空
|
||||
5. **确认边界**:失败会怎样、太频繁会不会被拒绝
|
||||
就像看菜单点菜一样,按这个流程来:
|
||||
|
||||
**第 1 步:确认这道菜是不是你要的**
|
||||
- 这个接口能做什么?
|
||||
- 符合你的需求吗?
|
||||
|
||||
**第 2 步:找到"点菜入口"**
|
||||
- HTTP API:网址(URL)是什么?
|
||||
- SDK:函数名是什么?
|
||||
|
||||
**第 3 步:看看要填什么信息**
|
||||
- **必填项**:就像"必须选辣度/份量",不填不行
|
||||
- **可选项**:就像"要不要加葱花",可以不填
|
||||
- **默认值**:就像"默认中辣",你不填就按这个来
|
||||
|
||||
**第 4 步:看看会端上来什么**
|
||||
- 成功时返回什么数据?
|
||||
- 字段代表什么意思?
|
||||
- 可能是空的吗?
|
||||
|
||||
**第 5 步:了解"餐厅规则"**
|
||||
- 没钱了会怎样?(余额不足)
|
||||
- 点太快会怎样?(限流/Rate Limit)
|
||||
- 菜卖完了会怎样?(404 资源不存在)
|
||||
- 厨房出错会怎样?(500 服务器错误)
|
||||
|
||||
### 5.2 常见的状态码(就像餐厅的回复)
|
||||
|
||||
| 状态码 | 含义 | 餐厅类比 |
|
||||
|--------|------|----------|
|
||||
| **200** | 成功 | "这是您的菜,请慢用" |
|
||||
| **400** | 请求错误 | "您点的菜我们有,但您填的信息有问题" |
|
||||
| **401** | 未授权 | "请先出示会员卡" |
|
||||
| **403** | 禁止访问 | "您的会员卡等级不够,点不了这道菜" |
|
||||
| **404** | 资源不存在 | "对不起,您点的菜卖完了" |
|
||||
| **429** | 请求过多 | "您点太快了,请稍后再试" |
|
||||
| **500** | 服务器错误 | "厨房出故障了,请稍后再试" |
|
||||
|
||||
---
|
||||
|
||||
## 6. 实战:用“模拟 API”练出手感
|
||||
## 6. 实战:用"模拟 API"练出手感
|
||||
|
||||
真实世界里你会用 Postman/curl/代码去调 API;这里我们用一个“不会被 CORS/网络干扰”的练习场,把核心手感练出来。
|
||||
理论讲完了,该动手了!在真实世界里,你会用 Postman、curl 或代码去调用 API。但这里我们准备了一个"练习场",不用担心网络问题、CORS 错误,专注于练出核心手感。
|
||||
|
||||
<ApiPlayground />
|
||||
|
||||
建议你按顺序试这几件事:
|
||||
### 6.1 建议按顺序试试这些"场景"
|
||||
|
||||
1. 把 “登录/钥匙” 改成“没有” → 看看失败会怎么提示
|
||||
2. 连续点击“调用” → 看看“太频繁会被拒绝”的提示
|
||||
3. 选 POST 创建用户,把 Body 改成非法 JSON → 观察 400
|
||||
4. 把用户 id 改成 `u_404` → 观察 404(资源不存在)
|
||||
就像去餐厅"踩点"一样,试试各种情况:
|
||||
|
||||
**场景 1:忘带会员卡**
|
||||
- 把"登录/钥匙"改成"没有"
|
||||
- 观察返回什么错误(通常是 401)
|
||||
|
||||
**场景 2:点太快被限流**
|
||||
- 连续快速点击"调用"按钮
|
||||
- 观察返回什么错误(通常是 429)
|
||||
|
||||
**场景 3:点菜信息填错了**
|
||||
- 选 POST 创建用户
|
||||
- 把 Body 改成非法的 JSON 格式
|
||||
- 观察返回什么错误(通常是 400)
|
||||
|
||||
**场景 4:点的菜卖完了**
|
||||
- 把用户 ID 改成 `u_404`(不存在的用户)
|
||||
- 观察返回什么错误(通常是 404)
|
||||
|
||||
### 6.2 练习目标
|
||||
|
||||
通过这些练习,你要掌握:
|
||||
1. **能看懂成功响应**:知道返回的数据在哪里
|
||||
2. **能看懂错误提示**:知道为什么失败、怎么改
|
||||
3. **有手感**:知道调用 API 的基本流程
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结:一句话把三种“API”说清楚
|
||||
## 7. 总结:记住这三句话就够了
|
||||
|
||||
- **HTTP API**:通过网络调用(你发请求,它回结果)
|
||||
- **SDK API**:通过库函数调用(你调函数,它内部帮你发请求)
|
||||
- **库 API**:本地函数接口(不走网络)
|
||||
### 7.1 API 的三种形式
|
||||
|
||||
它们共同点只有一个:**把“怎么用”写清楚**。
|
||||
| 类型 | 比喻 | 特点 |
|
||||
|------|------|------|
|
||||
| **HTTP API** | 外卖配送 | 通过网络调用,你发请求它回结果 |
|
||||
| **SDK API** | 餐厅堂食 | 通过函数调用,它内部帮你发请求 |
|
||||
| **库 API** | 自己做菜 | 本地函数,不走网络 |
|
||||
|
||||
### 7.2 核心记忆点
|
||||
|
||||
1. **API = 接口**:就像餐厅的服务员,是你和对方系统的桥梁
|
||||
2. **调用三要素**:入口(网址/函数名)、参数(要告诉什么)、返回(会得到什么)
|
||||
3. **学会读文档**:就像看菜单,先确认能不能用,再看怎么用
|
||||
|
||||
### 7.3 下一步建议
|
||||
|
||||
现在你已经理解了 API 的基本概念,可以去:
|
||||
- 读一读真实的 API 文档(比如 OpenAI、DeepSeek 的文档)
|
||||
- 用 Postman 或 curl 试试真实的 API 调用
|
||||
- 在你的项目里接入第一个 API
|
||||
|
||||
---
|
||||
|
||||
## 8. 名词速查表
|
||||
|
||||
> 不想背词也没关系:你只要会“看文档、会填参数、能看懂成功/失败”,就已经能开始用 API 了。
|
||||
> 💡 **使用建议**:不用背!遇到不懂的词回来查就行。你只要会"看文档、会填参数、能看懂成功/失败",就已经能开始用 API 了。
|
||||
|
||||
| 名词 | 英文 | 解释 |
|
||||
| :--------- | :-------------------------------- | :---------------------------------------- |
|
||||
| API | Application Programming Interface | 软件对外公开的接口/入口 |
|
||||
| URL/地址 | - | 你要访问的“网址/路径” |
|
||||
| 参数 | - | 你要告诉对方的信息(例如:id、页码) |
|
||||
| 返回 | - | 对方给你的结果(数据或错误提示) |
|
||||
| 状态码 | - | 成功/失败的数字提示(例如:200 表示成功) |
|
||||
| Rate Limit | - | 限流/配额(常见 429) |
|
||||
| 名词 | 英文 | 解释 |
|
||||
|------|------|------|
|
||||
| **API** | Application Programming Interface | 软件对外公开的接口/入口,像餐厅的服务员 |
|
||||
| **HTTP API** | HTTP API | 通过网络调用的接口,像外卖配送 |
|
||||
| **SDK** | Software Development Kit | 软件开发工具包,像餐厅的服务员(帮你处理细节) |
|
||||
| **URL** | Uniform Resource Locator | 你要访问的"网址",像餐厅的地址 |
|
||||
| **参数** | Parameter | 你要告诉对方的信息,像点菜时的要求(辣度、份量) |
|
||||
| **请求** | Request | 你发给对方的要求,像点菜单 |
|
||||
| **响应** | Response | 对方给你的结果,像端上来的菜 |
|
||||
| **状态码** | Status Code | 成功/失败的数字提示,200=成功,4xx=你错了,5xx=服务器错了 |
|
||||
| **API Key** | API Key | 调用 API 的密钥,像餐厅的会员卡 |
|
||||
| **限流** | Rate Limit | 限制调用频率,像餐厅说"您点太快了" |
|
||||
| **GET/POST** | HTTP Methods | 请求方法,GET=获取信息(看菜单),POST=创建/修改(下单) |
|
||||
| **JSON** | JavaScript Object Notation | 数据格式,像菜单上的格式(统一的排版) |
|
||||
| **Header** | Header | 请求头,像点菜单上的备注栏(放会员卡等信息) |
|
||||
| **Body** | Body | 请求体,像点菜单的详细内容(具体的菜品要求) |
|
||||
|
||||
@@ -1,30 +1,153 @@
|
||||
# 1.3 给原型加上 AI 能力
|
||||
---
|
||||
title: '给原型加上 AI 能力 - 接入文本与图像 API'
|
||||
description: '在已有 Web 原型中接入真实的 AI 能力:理解 API 的核心概念,学会找到 API Key 和官方示例;实战集成 DeepSeek 文本模型与多种图像生成服务(SiliconFlow Qwen-Image、Recraft、Seedream),并掌握常用的模型选型方法。'
|
||||
---
|
||||
|
||||
<script setup>
|
||||
const duration = '约 <strong>1 天</strong>'
|
||||
</script>
|
||||
|
||||
# 什么是 API
|
||||
# 初级四:给原型加上 AI 能力
|
||||
|
||||
# 如何接入文本 API
|
||||
## 本章导读
|
||||
|
||||
<ChapterIntroduction :duration="duration" :tags="['API', '文本模型', '文生图', '原型集成']" coreOutput="原型接入 1 个文本模型 + 1 个图像模型(可选)" expectedOutput="可调用真实 API 的 AI 原型">
|
||||
|
||||
# 3. **使用 API:调用 LLM 和图像生成器**
|
||||
在上一节中,你已经做出了一个「能跑起来」的产品原型;但只靠静态页面和前端逻辑,它离“能真正帮你提高效率”还差一步:把 AI 能力接进来。
|
||||
|
||||
## 3.1 什么是 API
|
||||
本章会用非常务实的视角讲清楚一件事:**接入任何 AI API,本质上都是“拿到 API Key + 读懂官方示例 + 让 AI 帮你落到代码里”**。你会以 DeepSeek 作为文本模型示例,并从多个图像生成服务中挑一个集成到自己的原型里。
|
||||
|
||||
首先,**你需要知道什么是 API** `Extra Knowledge 2 - What is API`
|
||||
</ChapterIntroduction>
|
||||
|
||||
我们将尝试集成两个 API:一个是调用 DeepSeek LLM,另一个是调用 Seedream (即梦) 模型。这两个模型都很棒,性能出色。
|
||||
::: warning 🔐 安全与费用提醒
|
||||
- **API Key 相当于密码**:它能代表“你本人”去调用接口,并且会产生费用。拿到你 Key 的人,不需要再问你确认,也能直接调用。
|
||||
- 不要发群、不要截图公开、不要贴到评论区/论坛。
|
||||
- 不要写进代码并提交到 Git(尤其是公开仓库);一旦提交,哪怕你后来删掉,历史记录里也可能还在。
|
||||
- 如果你怀疑 Key 泄露了:立刻去平台把 Key 删除/禁用,然后重新生成一个新的。
|
||||
- **原型阶段可以先跑通,但不要把 Key 放在前端公开代码里**:如果你的网页里直接写了 Key(哪怕你觉得“别人看不到”),只要页面能打开,别人就有机会从浏览器里把 Key 找出来。
|
||||
- 原型阶段:你可以先让功能跑通,理解流程即可。
|
||||
- 要上线/交付:务必用后端来保存 Key,并由后端去调用 API(初中级开发中会详细展开)。
|
||||
- **费用是按用量算的**:调用一次就可能消耗一点额度;图片/视频通常比文字更贵。
|
||||
- 建议先用免费额度或小额充值完成验证。
|
||||
- 测试时尽量用短输入、小图片、少次数;确认没问题再逐步加量。
|
||||
:::
|
||||
|
||||
在使用 API 的过程中,只有两个最重要的元素:
|
||||
<div style="margin: 50px 0;">
|
||||
<ClientOnly>
|
||||
<StepBar :active="0" :items="[
|
||||
{ title: 'API 基础', description: '可跳过,但很有用' },
|
||||
{ title: '接入文字', description: '5 分钟跑通一次' },
|
||||
{ title: '接入图片', description: '5 分钟跑通一次' },
|
||||
{ title: '实战接入', description: '接进上节原型' },
|
||||
{ title: '模型选型', description: '看榜单做选择' }
|
||||
]" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
1. API key (密钥)
|
||||
2. 官方文档示例
|
||||
## 1. API 基础
|
||||
|
||||
只要你能找到这两个,你就可以让 LLM 帮你修改并实现所有类型的 API 调用。
|
||||
如果你已经跑通了“生成文字”和“生成图片”,这一节可以先跳过;等你遇到报错、或者想更稳地改代码时,再回来看会更有用。
|
||||
|
||||
## 3.2 将 DeepSeek API 集成到 z.ai 中
|
||||
API 可以简单理解为:**你按对方要求的格式“发一个问题”,对方就按同样的格式“回一个结果”**。
|
||||
|
||||
- **你发出去的内容**:通常包括“密钥(API Key)”和“你要生成什么”
|
||||
- **对方回给你的内容**:成功就给结果;失败会告诉你原因(比如“密钥不对”“余额不足”“参数写错”)
|
||||
|
||||
在原型阶段,你只要记住一句话就够了:
|
||||
|
||||
> **拿到 API Key + 找到官方示例 + 让 AI IDE 帮你接到按钮上。**
|
||||
|
||||
如果你想看更详细的 0 基础解释,可以看附录:[《API 入门(0 基础版)》](/zh-cn/appendix/api-intro)。
|
||||
|
||||
## 2. 接入文本 API(生成文字)
|
||||
|
||||
在 `1.2 动手做出原型` 里,你已经做出了一个可交互的原型。接下来我们要做的,是把原型里“看起来像 AI 的功能”变成真正可用的能力:**当用户点击按钮时,原型会向外部的 AI 服务发出请求,并把返回的文字展示出来。**
|
||||
|
||||
这一节我们先用一个例子“快速跑通一次”,再把整条接入链条讲清楚。你照着做,0 基础也能完成。
|
||||
|
||||
### 2.1 5 分钟接入文字 API:以 DeepSeek 为例
|
||||
|
||||
先说明:你现在**不需要写复杂代码,也不需要懂原理**。这 5 分钟要做的事很单纯:
|
||||
|
||||
> **把 DeepSeek 的“密钥 + 官方示例”复制到 AI IDE 里,让 AI IDE 帮你把上一节原型的“生成文案”按钮改成真实调用,然后你再点按钮测试。**
|
||||
|
||||
你可以把它当成 4 步小任务(按顺序做):
|
||||
|
||||
1. **拿到密钥(API Key)**:去 DeepSeek 平台创建一个 API Key(它相当于“通行证”)。
|
||||
2. **找到官方示例**:在 DeepSeek 文档里找到“生成文字”的示例(通常可以直接复制)。
|
||||
3. **复制粘贴到 AI IDE**:把 **API Key + 官方示例** 粘贴进 AI IDE,并告诉它:我要改的是上一节原型里的“生成标题/生成卖点/一键改写”按钮。
|
||||
4. **回到页面点一下测试**:打开原型,输入一点内容,点击按钮,能看到生成结果就算跑通。
|
||||
|
||||
为了让你“更好对齐到上一节的项目”,你可以边打开原型边做这一小节:
|
||||
|
||||
> 上一节课项目:[1.2 动手做出原型(电商素材工作台)](/zh-cn/stage-1/1.2-building-prototype/)
|
||||
>
|
||||
> 你只需要找到里面的“生成标题/生成卖点/一键改写”这类按钮,把它从“假数据”升级成“真实调用”。
|
||||
|
||||
你在素材工作台里,通常会看到这样的流程:
|
||||
|
||||
- 输入商品信息(商品名、卖点、目标人群、风格)
|
||||
- 点击“生成标题/生成卖点”
|
||||
- 页面出现一段可复制的文字结果
|
||||
|
||||
建议你对 AI IDE 这样说(把方括号里的内容换成你项目实际页面名称/按钮文字):
|
||||
|
||||
```text
|
||||
我有一个上一节做的电商素材工作台原型。
|
||||
|
||||
现在我需要把 [生成标题/生成卖点文案/一键改写] 这个按钮接入真实的文本 API:
|
||||
1) 点击按钮时,读取页面上的输入(商品名/卖点/风格等),拼成一段提示词;
|
||||
2) 调用 DeepSeek(或你看到我提供的文本 API)的接口拿到生成结果;
|
||||
3) 把结果显示回页面(替换掉原来的 mock 文本),并加上“加载中/失败提示”;
|
||||
4) 告诉我改了哪些文件,以及怎么验证。
|
||||
```
|
||||
|
||||
<!-- TODO: 插入截图:DeepSeek 平台创建 API Key 的页面 -->
|
||||
<!-- TODO: 插入截图:DeepSeek 文档示例(可复制的那段) -->
|
||||
<!-- TODO: 插入截图:AI IDE 对话(说明“我要接入上一节的哪个按钮/页面”) -->
|
||||
<!-- TODO: 插入截图:原型接入成功(点击按钮后出现生成文案) -->
|
||||
<!-- TODO: 插入截图:素材工作台页面(文案输入区 + “生成标题/生成卖点”按钮) -->
|
||||
<!-- TODO: 插入截图:接入成功后的页面(展示生成文案结果) -->
|
||||
|
||||
### 2.2 文本 API 的接入链条(0 基础版)
|
||||
|
||||
把“文字 AI”接进原型,通常就 6 步:
|
||||
|
||||
1. **确定入口**:用户在哪里点一下,就应该开始“生成文字”?(例如“生成标题/生成卖点/一键改写”按钮)
|
||||
2. **收集输入**:用户在页面上填了什么?(商品名、卖点、风格、字数、语气……)
|
||||
3. **拼出请求内容**:把这些输入拼成一段清楚的话(也就是“提示词”),作为你发给 AI 的内容
|
||||
4. **发出请求**:把“密钥 + 官方示例”接到代码里,让它真的去请求 AI
|
||||
5. **把结果显示出来**:把返回的文字写回页面(例如显示在文本框/卡片里)
|
||||
6. **加上“加载中/报错提示”**:请求需要时间,失败也很正常,给用户一个清晰反馈
|
||||
|
||||
你会发现:这条链路里,最难的不是“写代码”,而是第 1~3 步的“把需求变成输入”。这些步骤越清楚,后面的代码就越顺。
|
||||
|
||||
<!-- TODO: 插入截图:原型中“生成文案/改写/总结”的入口(来自上一节的原型页面) -->
|
||||
|
||||
### 2.3 接入 DeepSeek 文本API
|
||||
|
||||
这一小节更“细一点”,把你在 AI IDE 里需要说清楚的话写出来。你照着做就行。
|
||||
|
||||
**目标**:从 DeepSeek 拿到密钥 → 从文档复制示例 → 粘贴到 AI IDE → AI IDE 改代码 → 我们回到页面再次测试。
|
||||
|
||||
你可以按下面的顺序来:
|
||||
|
||||
1. **准备两样东西**
|
||||
- **DeepSeek API Key**(在平台创建)
|
||||
- **DeepSeek 文档里的“生成文字示例”**(复制即可)
|
||||
2. **告诉 AI IDE:要改哪个“入口”**
|
||||
- 入口就是你原型里那个按钮/页面(例如“生成标题/生成卖点/一键改写”)
|
||||
- 你可以补一句:原来是“写死/假数据”,现在要换成真实调用
|
||||
3. **把“密钥 + 示例 + 入口说明”粘贴给 AI IDE**
|
||||
- 让它直接改项目,并要求它告诉你:改了哪些文件、怎么验证
|
||||
4. **你自己回到页面再次测试**
|
||||
- 输入一条商品信息 → 点生成 → 看是否出现“真实生成”的文字
|
||||
- 如果失败:把报错提示原样复制回 AI IDE,让它继续修复
|
||||
|
||||
### 什么是 DeepSeek
|
||||
|
||||
> 提示:文档里可能会出现 “LLM” 这个词。你可以先把它理解为“能生成文字的 AI 模型”,不影响你把 API 接进原型。
|
||||
|
||||

|
||||
|
||||
> 📚 信息引用自 [DeepSeek Wiki](https://en.wikipedia.org/wiki/DeepSeek)
|
||||
@@ -39,61 +162,148 @@
|
||||
>
|
||||
> GPQA 包含 448 个多项选择题,涵盖生物学、物理学和化学的子领域,如量子力学、有机化学、分子生物学等。这些问题由 61 位持有博士学位或正在攻读博士学位的专家编写,并经过了严格的验证过程。
|
||||
|
||||
### 如何获取 deepseek API
|
||||
### 如何获取 DeepSeek API
|
||||
|
||||
我们将尝试根据我们已有的信息,让 z.ai 直接将 DeepSeek API 集成到项目中。
|
||||
我们将尝试根据我们已有的信息,让 AI IDE 直接将 DeepSeek API 集成到项目中。
|
||||
|
||||
首先,我们需要在 DeepSeek 开放平台注册一个账户。
|
||||
|
||||
https://platform.deepseek.com/sign_up
|
||||
注册入口:<https://platform.deepseek.com/sign_up>
|
||||
|
||||
然后,你会看到像这样的网页界面:
|
||||
|
||||

|
||||
|
||||
要使用 API,我们需要先充值 token。10 元人民币足够使用一段时间了!
|
||||
要使用 API,我们需要先充值一点调用额度。10 元人民币通常足够你完成一轮接入与测试。
|
||||
|
||||

|
||||
|
||||
点击“API KEYS”并在屏幕下方找到“create new API key”。你最终会得到一个像 `sk-8573341c39fc44315aadc071c53rh7d2` 这样的 API key。
|
||||
点击 “API KEYS”,并在屏幕下方找到 “create new API key”。你最终会得到一个类似 `sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` 的 API Key。
|
||||
|
||||

|
||||
|
||||
一旦你获得了密钥,你就拥有了调用模型的权限。
|
||||
|
||||
此时,你可以直接阅读 [API 文档](https://api-docs.deepseek.com/),它通常提供 curl 或 Python 的调用示例。
|
||||
此时,你可以直接阅读 [API 文档](https://api-docs.deepseek.com/),它通常提供 curl 或 Python 等调用示例。
|
||||
|
||||

|
||||
|
||||
找到示例后,你可以将文档中与密钥相关的所有内容复制到 z.ai,并要求它尝试帮你集成 LLM。
|
||||
找到示例后,你可以将文档中与「鉴权」和「请求结构」相关的内容复制到 AI IDE,并要求它把 DeepSeek 接入你的项目(例如把“文案生成”“智能改写”“总结”等功能,替换成真实 API 调用)。
|
||||
|
||||
你可以直接把下面这段话(按你的项目实际情况改一下)发给 AI IDE,减少沟通成本:
|
||||
|
||||
```text
|
||||
我在上一章已经做了一个可运行的 Web 原型。现在我需要把“文案生成/改写/总结”等功能接入真实的文本 API(生成文字)。
|
||||
|
||||
请你根据我提供的 DeepSeek API Key 和官方文档示例:
|
||||
1) 找到项目中触发文案生成的入口(按钮/表单/页面),把原来的 mock 逻辑替换为真实 API 调用;
|
||||
2) 把 API Key 放到安全的位置(先不要硬编码到前端源码里;如果不得不临时放,也要提醒我风险);
|
||||
3) 告诉我你改动了哪些文件,以及如何验证(例如在页面上输入什么,点哪个按钮,能看到什么返回)。
|
||||
```
|
||||
|
||||
<!-- TODO: 插入截图:AI IDE 对话中粘贴 API Key + 文档示例 + 上面这段说明 -->
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
自动集成可以在很短的时间内完成。我们可以询问它的操作员确认 DeepSeek API 是否已经在使用中。
|
||||
自动集成通常可以在很短时间内完成。完成后,你可以让 AI IDE 明确回答两件事:
|
||||
|
||||
- **它把“调用 AI”的代码放在了哪里**
|
||||
- **现在请求的是不是 DeepSeek(看“请求地址”和“模型名称”即可)**
|
||||
|
||||

|
||||
|
||||
或者,我们可以要求 z.ai 帮我们定位项目中调用 LLM 的部分。
|
||||
或者,你也可以要求 AI IDE 帮你定位项目中“调用文字生成”的部分。
|
||||
|
||||
然后我们可以独立确认是否正在使用 DeepSeek。具体来说,我们可以直接请求:`"告诉我项目中所有需要调用 LLM 的代码位置,我需要检查是否是 DeepSeek。"`,z.ai 将返回所有 API 调用的详细地址。
|
||||
为了独立确认是否真的在调用 DeepSeek,你可以直接让 AI IDE 帮你定位调用点,例如:
|
||||
|
||||
`告诉我项目中所有需要调用文字生成的代码位置,我需要检查是否是 DeepSeek。`
|
||||
|
||||
它会返回对应代码位置与请求细节,便于你人工检查(尤其是:请求地址、是否带了密钥、以及模型名称)。
|
||||
|
||||

|
||||
|
||||
接下来,我们将简要介绍目前可用的三种最先进的图像生成模型。你可以根据自己的喜好选择一种集成到 z.ai 中。
|
||||
接下来,我们将简要介绍三种常见的“生成图片”的服务。你可以根据自己的预算、可访问性和效果偏好,选择其中一种集成到 AI IDE 中即可。
|
||||
|
||||
# 如何接入图像 API
|
||||
## 3. 接入图片 API(生成图片):从“提示词”到“图片展示”
|
||||
|
||||
如果说大语言模型专注于理解、推理和分析我们不知道的所有事物;那么图像和视频模型则专注于生成——将你脑海中的所有想法转化为视觉现实。在今年的 AI 生成领域(2025),图像编辑和视频生成非常流行。你一定在抖音或 YouTube 上看过 AI 生成的可爱动物视频、AI 创建的角色照片、AI 生成的肖像拍摄、切玻璃苹果的视频等等。在上完今天的图像和视频课程后,你也完全有能力创建同样的内容!
|
||||
|
||||
在今天的课程中,我们需要生成大量的图像和视频。为了方便起见,我们将使用统一连接的云服务提供商,并将提供相应的代码和 Token。在上一节课中,大家已经学习了如何集成 API 和使用 Token。你只需要按照以下步骤操作,回忆上节课学到的内容,就可以在 Z.AI 中成功启动你自己的图像/视频应用程序。
|
||||
图片 API 的“接入链条”其实也不复杂。你可以先按 0 基础版本跑通一遍,再去追求更好的效果。
|
||||
|
||||

|
||||
这一节同样先用一个例子“5 分钟跑通一次”,再讲清楚接入链条。
|
||||
|
||||
### 3.1 5 分钟接入生图 API:以 SiliconFlow Qwen-Image 为例
|
||||
|
||||
这一小节的目标只有一个:让你的原型“真的能生成一张图片”,快速跑通链路。
|
||||
|
||||
同样是 4 步(按顺序做):
|
||||
|
||||
1. **拿到密钥(API Key)**:在 SiliconFlow 创建一个 API Key(相当于“通行证”)。
|
||||
2. **找到官方示例**:在 SiliconFlow 文档里找到“生成图片”的示例(通常可以直接复制)。
|
||||
3. **让 AI IDE 帮你接进原型**:把“密钥 + 官方示例 + 你原型里哪个按钮要生成图片”发给 AI IDE,让它把占位图/假图替换成真实生成。
|
||||
4. **验证是否成功**:打开原型,输入一句图片描述(例如“白底电商主图、产品居中、柔光棚拍风格”),点击“生成主图”,能看到图片出来就算跑通。
|
||||
|
||||
同样,为了对齐到上一节的项目,你可以边打开原型边做这一小节:
|
||||
|
||||
> 上一节课项目:[1.2 动手做出原型(电商素材工作台)](/zh-cn/stage-1/1.2-building-prototype/)
|
||||
>
|
||||
> 你只需要找到里面的“生成主图/生成海报/生成配图”这类按钮,把它从“占位图/假图”升级成“真实生成”。
|
||||
|
||||
在素材工作台里,图片通常对应这样的入口:
|
||||
|
||||
- 输入“想要的画面描述”(例如白底、场景、风格、需要的文案)
|
||||
- 点击“生成主图/生成海报”
|
||||
- 页面出现一张图片(或一组图片)
|
||||
|
||||
你可以对 AI IDE 这样说:
|
||||
|
||||
```text
|
||||
我有一个上一节做的电商素材工作台原型。
|
||||
|
||||
现在我需要把 [生成主图/生成海报/生成配图] 这个按钮接入真实的图片 API:
|
||||
1) 点击按钮时,读取页面输入,整理成一句清楚的提示词;
|
||||
2) 调用图片生成 API(例如 SiliconFlow Qwen-Image / Recraft / Seedream);
|
||||
3) 拿到返回的图片地址后,把图片显示在页面上;
|
||||
4) 加上“生成中/失败提示”,并告诉我怎么验证。
|
||||
```
|
||||
|
||||
<!-- TODO: 插入截图:SiliconFlow 创建 API Key 的页面 -->
|
||||
<!-- TODO: 插入截图:SiliconFlow 图片生成的文档示例(可复制的那段) -->
|
||||
<!-- TODO: 插入截图:AI IDE 对话(说明“我要把生图接入上一节的哪个按钮/页面”) -->
|
||||
<!-- TODO: 插入截图:原型接入成功(点击按钮后出现生成图片) -->
|
||||
<!-- TODO: 插入截图:素材工作台页面(图片提示词输入区 + “生成主图/生成海报”按钮) -->
|
||||
<!-- TODO: 插入截图:接入成功后的页面(展示生成图片结果) -->
|
||||
|
||||
### 3.2 图片 API 的接入
|
||||
|
||||
把“图片 AI”接进原型,通常就 6 步:
|
||||
|
||||
1. **确定入口**:用户在哪里点一下,就应该开始“生成图片”?(例如“生成主图/生成海报/生成配图”)
|
||||
2. **收集输入**:用户希望图片长什么样?(风格、场景、文字、颜色……)
|
||||
3. **准备提示词**:把输入整理成一句清楚的话(例如“白底电商主图、产品居中、柔光棚拍风格”)
|
||||
4. **发出请求**:用“密钥 + 官方示例”去请求图片服务
|
||||
5. **拿到图片并展示**:通常会返回一个“图片地址”,你把它放到页面里就能看到图片
|
||||
6. **加上加载/失败提示**:图片生成更慢,失败也更常见,提示要更清楚
|
||||
|
||||
<!-- TODO: 插入截图:原型中“生成图片/生成海报/生成主图”的入口(来自上一节的原型页面) -->
|
||||
|
||||
### 3.3 选择一个图片服务接入(示例)
|
||||
|
||||
下面给出三个常见选择。你只需要先选一个跑通即可:跑通之后,再尝试替换成你更喜欢的模型。
|
||||
|
||||
在今天的课程中,我们需要生成大量的图像和视频。为了方便起见,我们将使用统一连接的云服务提供商,并将提供相应的示例代码与调用密钥。你只需要按照以下步骤操作,就可以在你的原型中接入图像/视频能力。
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
## 3.3 将 SiliconFlow QwenImage API 集成到 z.ai 中
|
||||
## 3.4 将 SiliconFlow Qwen-Image API 集成到 AI IDE 中
|
||||
|
||||
在原型里,图片 API 最常见的落点是:**“生成主图 / 生成海报 / 生成配图”**。你需要做的事情很简单:把用户输入整理成一句话,请求图片 API,然后把返回的图片展示出来。
|
||||
|
||||
### 什么是 SiliconFlow
|
||||
|
||||
@@ -101,11 +311,11 @@ https://platform.deepseek.com/sign_up
|
||||
>
|
||||
> 
|
||||
|
||||
### 什么是 QwenImage
|
||||
### 什么是 Qwen-Image
|
||||
|
||||
> Qwen-Image 是一个强大的图像生成基础模型,能够进行复杂的文本渲染和精确的图像编辑。这是一个 20B MMDiT 图像基础模型,在复杂的文本渲染和精确的图像编辑方面取得了重大进展。实验表明,它在图像生成和编辑方面都具有很强的通用能力,在文本渲染方面表现尤为出色,尤其是中文。
|
||||
>
|
||||
> 从中文到英文,QwenImage 可以像 GPT-4o 或 Seedream 模型一样生成高质量的文本。
|
||||
> 从中文到英文,Qwen-Image 可以像 GPT-4o 或 Seedream 模型一样生成高质量的文本。
|
||||
>
|
||||
> 
|
||||
>
|
||||
@@ -115,9 +325,9 @@ https://platform.deepseek.com/sign_up
|
||||
>
|
||||
> 
|
||||
|
||||
### 如何获取 SiliconFlow QwenImage API
|
||||
### 如何获取 SiliconFlow Qwen-Image API
|
||||
|
||||
https://cloud.siliconflow.com/me/models
|
||||
入口:<https://cloud.siliconflow.com/me/models>
|
||||
|
||||
查看 SiliconFlow 的官网。左侧有一个“Playground”部分,你可以在不进行 API 调用的情况下试用不同的模型。在网页顶部有一个“Filters”按钮;点击它可以筛选右侧的模型列表。
|
||||
|
||||
@@ -131,20 +341,20 @@ https://cloud.siliconflow.com/me/models
|
||||
|
||||
要查看可用余额,我们需要打开左侧设置中的“Payments”。在这里,你可以看到 1 美元的赠金。但是,如果你想使用 FLUX 文生图模型,你需要先充值账户。
|
||||
|
||||
https://cloud.siliconflow.com/me/account/ak
|
||||
充值/余额:<https://cloud.siliconflow.com/me/account/ak>
|
||||
|
||||

|
||||
|
||||
一切设置好后,我们需要参考相应的图像生成 API 文档。你可以在官方文档页面找到任何标记为“API Reference”的部分。点击它,然后导航到图像生成的 API 端点部分并找到相关的请求示例。
|
||||
|
||||
https://docs.siliconflow.com/en/userguide/introduction
|
||||
文档入口:<https://docs.siliconflow.com/en/userguide/introduction>
|
||||
|
||||

|
||||
|
||||
```Bash
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url https://api.siliconflow.com/v1/images/generations \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Authorization: Bearer <YOUR_API_KEY>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"model": "black-forest-labs/FLUX.1-Kontext-max",
|
||||
@@ -154,10 +364,10 @@ curl --request POST \
|
||||
|
||||
记得将你打算使用的模型和 API key 填入相应的字段。之后,你可以在计算机的命令行中使用该命令运行直接请求测试。
|
||||
|
||||
```Bash
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url https://api.siliconflow.com/v1/images/generations \
|
||||
--header 'Authorization: Bearer sk-defrgqrgrganpncxxibfyzfocgafga' \
|
||||
--header 'Authorization: Bearer <your_api_key>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"model": "Qwen/Qwen-Image",
|
||||
@@ -167,11 +377,17 @@ curl --request POST \
|
||||
|
||||

|
||||
|
||||
你可以将下面修改后的代码行发送给 z.ai,并要求它帮你创建一个前端测试演示。很快,你就能实现 SiliconFlow 的基本 API 调用。
|
||||
你可以把「API Key + 官方请求示例 + 你的原型需求」发送给 AI IDE,并要求它帮你创建一个前端测试演示或直接改造当前项目。很快,你就能跑通 SiliconFlow 的基本 API 调用。
|
||||
|
||||
<!-- TODO: 插入截图:AI IDE 中说明“我要把图像 API 接到原型的哪个按钮/页面” -->
|
||||
|
||||

|
||||
|
||||
## 3.4 将 Recraft API 集成到 z.ai 中
|
||||
## 3.5 将 Recraft API 集成到 AI IDE 中
|
||||
|
||||
如果你的原型更偏“设计生产”(例如生成品牌风格插画、营销海报、矢量风格素材),Recraft 往往会更顺手。接入方式与上一节完全一致:**拿到 Key + 找到官方示例 + 让 AI IDE 把示例落到你的按钮/页面里**。
|
||||
|
||||
<!-- TODO: 插入截图:原型中 Recraft 的使用入口(例如“生成插画/生成海报”) -->
|
||||
|
||||
### 什么是 Recraft
|
||||
|
||||
@@ -185,35 +401,39 @@ curl --request POST \
|
||||
|
||||
### 如何获取 Recraft API
|
||||
|
||||
首先,我们仍然需要找到重要的 API 入口以获取我们的 API key。 https://www.recraft.ai/profile/api
|
||||
首先,我们仍然需要找到 API 入口以获取 API Key:<https://www.recraft.ai/profile/api>
|
||||
|
||||
由于这里没有提供免费额度,我们需要自己充值 1,000 积分。这个网站支持支付宝和微信支付,所以很容易获得 1,000 积分(注意:不要充值超过必要的金额)。
|
||||
|
||||

|
||||
|
||||
之后,我们仍然遵循通常的方法:去官方文档找到相应的请求示例。
|
||||
之后,我们仍然遵循同样的方法:去官方文档找到相应的请求示例:
|
||||
|
||||
https://www.recraft.ai/docs/api-reference/getting-started
|
||||
- <https://www.recraft.ai/docs/api-reference/getting-started>
|
||||
- <https://www.recraft.ai/docs/api-reference/usage>
|
||||
- <https://www.recraft.ai/docs/api-reference/guides>
|
||||
|
||||
https://www.recraft.ai/docs/api-reference/usage
|
||||
|
||||
https://www.recraft.ai/docs/api-reference/guides
|
||||
|
||||
在这里,我们可以直接复制整个内容并粘贴到 z.ai。
|
||||
在这里,我们可以直接复制官方文档中的请求示例,并粘贴到 AI IDE。
|
||||
|
||||

|
||||
|
||||
注意,在聊天窗口中,输入你的 API key 和文档内容就足够了;z.ai 会自动为你构建前端。
|
||||
注意:在聊天窗口中,输入你的 API Key 和文档示例通常就足够了;AI IDE 会自动为你构建前端交互与请求代码。
|
||||
|
||||
如果过程中出现错误,你可以直接将错误信息粘贴到聊天窗口,让 z.ai 帮你自动解决。
|
||||
<!-- TODO: 插入截图:AI IDE 粘贴 Recraft 示例并生成代码 -->
|
||||
|
||||
如果过程中出现错误,你可以直接将错误信息粘贴到聊天窗口,让 AI IDE 帮你自动解决。
|
||||
|
||||

|
||||
|
||||
## 3.5 将 Seedream API 集成到 z.ai 中(针对中国用户)
|
||||
## 3.6 将 Seedream API 集成到 AI IDE 中(针对中国用户)
|
||||
|
||||
如果你希望使用国内网络更稳定、且效果不错的图像生成服务,可以考虑 Seedream(火山引擎)。思路同样不变:把它当成一个“图片生成 API”,接到你的原型按钮上即可。
|
||||
|
||||
<!-- TODO: 插入截图:原型中 Seedream 的使用入口(例如“生成商品主图”) -->
|
||||
|
||||
### 什么是 Seedream 4.0
|
||||
|
||||
https://seed.bytedance.com/en/seedream4_0
|
||||
模型介绍:<https://seed.bytedance.com/en/seedream4_0>
|
||||
|
||||

|
||||
|
||||
@@ -227,9 +447,9 @@ https://seed.bytedance.com/en/seedream4_0
|
||||
|
||||
### 如何获取 Seedream API - 火山引擎 (Volcengine)(针对中国用户)
|
||||
|
||||
我们将逐步演示如何将 Seedream API 集成到 z.ai 示例中。
|
||||
我们将逐步演示如何将 Seedream API 集成到你的项目中(通过 AI IDE 辅助完成)。
|
||||
|
||||
https://www.volcengine.com/experience/ark?launch=seedream
|
||||
入口:<https://www.volcengine.com/experience/ark?launch=seedream>
|
||||
|
||||
访问页面后,点击登录。
|
||||
|
||||
@@ -245,7 +465,7 @@ https://www.volcengine.com/experience/ark?launch=seedream
|
||||
|
||||
认证成功后,你可以充值 1 元用于测试。
|
||||
|
||||
https://console.volcengine.com/finance/fund/recharge
|
||||
充值入口:<https://console.volcengine.com/finance/fund/recharge>
|
||||
|
||||

|
||||
|
||||
@@ -261,7 +481,9 @@ https://console.volcengine.com/finance/fund/recharge
|
||||
|
||||

|
||||
|
||||
准备好 API key 和调用示例后,你可以直接将它们粘贴到 z.ai 中以生成前端交互演示。
|
||||
准备好 API Key 和调用示例后,你可以直接将它们粘贴到 AI IDE 中,让它生成前端交互演示或把能力接入现有原型。
|
||||
|
||||
<!-- TODO: 插入截图:AI IDE 粘贴 Seedream 示例并完成接入 -->
|
||||
|
||||
重要提示:这里的默认示例相对复杂。记得禁用“添加水印”选项和“流式响应”选项,以确保不生成水印且不会发生请求失败。
|
||||
|
||||
@@ -271,15 +493,77 @@ https://console.volcengine.com/finance/fund/recharge
|
||||
|
||||

|
||||
|
||||
# 附录:如何找到最好的 AI 模型
|
||||
## 4. 📚 作业:给你自己的抖音电商工作台加上 AI 能力
|
||||
|
||||
大语言模型(LLM)的发展速度非常快,我们总是需要确保我们使用的是排名靠前的模型之一。通过以下两个网站,我们可以轻松确定哪些文本、视频和图像大模型目前最强大。一般来说,我们将此类网站称为 **“LLM 竞技场”** —— 我们可以并排比较两个模型的输出。你需要选择你更喜欢的响应,这种选择算作一票。获得更多选票的模型被认为具有更好的性能。
|
||||
<el-card shadow="hover" style="margin: 20px 0; border-radius: 12px;">
|
||||
<template #header>
|
||||
<div style="font-weight: bold; font-size: 16px;">🚀 挑战任务:让你的工作台“真的在用 AI”</div>
|
||||
</template>
|
||||
|
||||
<p>
|
||||
请你基于上一节完成的「电商素材工作台」原型,完成一次“从原型到真实调用”的小闭环:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>必做 1:接入文字生成</strong>
|
||||
<ul>
|
||||
<li>把一个按钮接入真实的文字 API(例如“生成标题 / 生成卖点 / 一键改写”)</li>
|
||||
<li>点击后要能返回真实生成的文字,并展示在页面上(而不是写死的假数据)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>必做 2:接入图片生成</strong>
|
||||
<ul>
|
||||
<li>把一个按钮接入真实的生图 API(例如“生成主图 / 生成海报 / 生成配图”)</li>
|
||||
<li>点击后要能返回真实生成的图片,并展示在页面上(而不是占位图/本地假图)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>必做 3:补齐基础体验</strong>
|
||||
<ul>
|
||||
<li>至少包含“生成中”的提示</li>
|
||||
<li>失败时能给出提示(例如密钥无效、额度不足、网络失败)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>自检清单</strong>
|
||||
<ul>
|
||||
<li>我能在页面上清楚地看到“生成按钮”和“生成结果”</li>
|
||||
<li>点击后不是写死的假数据,而是每次可能都不一样的真实结果</li>
|
||||
<li>报错时我能看懂原因,并知道下一步怎么处理(把报错复制给 AI IDE)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>成果分享(可选)</strong>
|
||||
<ul>
|
||||
<li>截图你的页面(文字结果 + 图片结果)分享给同学/朋友</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
|
||||
## 下一步
|
||||
|
||||
当你把“生成文字”和“生成图片”都接入成功后,你的工作台就已经具备了最核心的 AI 能力雏形:**点击按钮 → 发请求 → 拿结果 → 展示出来**。
|
||||
|
||||
接下来,你可以在这个内容生产工作台的基础上,继续扩展更多 AI 能力组合,例如:
|
||||
|
||||
- **文字生成文字**:一键生成多版标题/卖点、自动改写、批量生成不同风格文案
|
||||
- **图片生成文字**:上传竞品/爆款截图,让 AI 自动总结卖点、提取关键信息、生成上新文案
|
||||
- **文字生成图片**:根据商品描述自动生成配图草稿(主图/海报/详情页配图)
|
||||
|
||||
## 5. 附录:如何找到“当前更强”的 AI 模型
|
||||
|
||||
文字模型(也常被叫作“大语言模型”)的发展速度非常快,我们总是需要确保我们用的是表现更好的模型之一。通过以下两个网站,你可以很方便地看到“现在大家常用、评价也更好的模型”。
|
||||
|
||||
一般来说,这类网站可以理解为 **“模型竞技场”**:它会把两个模型的输出放在一起,你投票选你更喜欢的那个。票数高的模型,通常意味着更多人觉得它“更好用”。
|
||||
|
||||
此外,你偶尔可能会在这些大模型竞技场中看到神秘的匿名模型。通常,这些是来自 OpenAI 或 Google 等公司的内部测试模型。你可能有机会意外体验到最先进模型的能力!
|
||||
|
||||
### LMArena
|
||||
### 5.1 LMArena
|
||||
|
||||
网站:https://lmarena.ai/
|
||||
网站:<https://lmarena.ai/>
|
||||
|
||||
简介:LMArena 最初由加州大学伯克利分校大模型系统组织(LMSYS)作为一个学术副项目推出,现已发展成为一家公司。它是一个开源的众包 AI 基准测试平台。
|
||||
|
||||
@@ -287,13 +571,13 @@ https://console.volcengine.com/finance/fund/recharge
|
||||
|
||||
它使用 Elo 评分系统,可以更真实地反映用户对模型回答质量的评价。根据用户投票数据,它编制了一个排行榜,涵盖七个类别,包括文本/语言能力、Web 开发和视觉/图像理解。
|
||||
|
||||
截至 2025 年 4 月,它已记录了超过 300 万次比较,并评估了 400 多个模型,成为比较和评估 AI 模型性能的流行方法。
|
||||
截至 2025 年 4 月(撰写时),它已记录了超过 300 万次比较,并评估了 400 多个模型,是非常流行的众包对比平台。
|
||||
|
||||

|
||||
|
||||
### Artificial Analysis
|
||||
### 5.2 Artificial Analysis
|
||||
|
||||
网站:[https://artificialanalysis.ai](https://artificialanalysis.ai/)
|
||||
网站:<https://artificialanalysis.ai/>
|
||||
|
||||
Artificial Analysis 是领先的独立 AI 基准测试和分析平台。它专注于对 AI 模型和 API 提供商进行独立分析。该网站提供详细的数据和图表,可以帮助开发者、用户、研究人员和其他用户做出明智的选择。
|
||||
|
||||
@@ -301,4 +585,4 @@ Artificial Analysis 是领先的独立 AI 基准测试和分析平台。它专
|
||||
|
||||
其功能包括模型比较、质量评估、价格分析、性能测试和上下文窗口分析。它还提供了详细的用户指南和常见问题解答,涵盖各种类型模型的评估,如大语言模型、文本到图像模型和语音到文本模型。此外,它还提供了一个专注于模型基准的免费 API 和一个具有更全面数据的商业 API。
|
||||
|
||||

|
||||

|
||||
|
||||
Reference in New Issue
Block a user