docs: 更新 API 文档和 AI 能力集成页面
@@ -1,85 +1,155 @@
|
||||
<!--
|
||||
ApiConceptDemo.vue
|
||||
目标:互动演示 API 必须写清楚的 4 个要点
|
||||
ApiConceptDemo.vue - 互动点餐版
|
||||
目标:通过"点菜"的过程演示 API 的三个核心要素
|
||||
-->
|
||||
<template>
|
||||
<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 class="header">
|
||||
<span class="icon">🍳</span>
|
||||
<span class="title">互动演示:向 AI 厨房点菜</span>
|
||||
</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 class="stepper">
|
||||
<!-- Step 1: Endpoint -->
|
||||
<div class="step-group">
|
||||
<div class="step-label">1. 跟谁说?(Endpoint)</div>
|
||||
<select v-model="endpoint" class="control">
|
||||
<option value="/kitchen/chef">👨🍳 主厨 (/kitchen/chef)</option>
|
||||
<option value="/kitchen/bar">🍸 调酒师 (/kitchen/bar)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div class="detail-desc">{{ selected.desc }}</div>
|
||||
<div class="detail-example">
|
||||
<strong>例子:</strong>
|
||||
<code>{{ selected.example }}</code>
|
||||
|
||||
<!-- Step 2: Method -->
|
||||
<div class="step-group">
|
||||
<div class="step-label">2. 怎么说?(Method)</div>
|
||||
<div class="toggle-group">
|
||||
<button
|
||||
:class="['toggle-btn', { active: method === 'GET' }]"
|
||||
@click="method = 'GET'"
|
||||
>
|
||||
GET (看看有什么)
|
||||
</button>
|
||||
<button
|
||||
:class="['toggle-btn', { active: method === 'POST' }]"
|
||||
@click="method = 'POST'"
|
||||
>
|
||||
POST (我要下单)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Params -->
|
||||
<div class="step-group" v-if="method === 'POST'">
|
||||
<div class="step-label">3. 点什么?(Body)</div>
|
||||
<div class="params-editor">
|
||||
{ "food":
|
||||
<select v-model="food" class="inline-select">
|
||||
<option value="steak">🥩 牛排</option>
|
||||
<option value="pasta">🍝 意面</option>
|
||||
<option value="salad">🥗 沙拉</option>
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-group" v-else>
|
||||
<div class="step-label">3. 查什么?(Params)</div>
|
||||
<div class="params-editor">
|
||||
?type=
|
||||
<select v-model="menuType" class="inline-select">
|
||||
<option value="today">📅 今日特供</option>
|
||||
<option value="all">📜 全部菜单</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<button class="send-btn" @click="sendRequest" :disabled="loading">
|
||||
{{ loading ? '🍳 正在做...' : '🚀 发送请求' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div class="result-box" v-if="response">
|
||||
<div class="result-header">
|
||||
<span class="badge" :class="response.status === 200 ? 'success' : 'error'">
|
||||
{{ response.status }} {{ response.statusText }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="result-content">
|
||||
{{ response.data }}
|
||||
</div>
|
||||
<div class="result-explanation">
|
||||
💡 <strong>解释:</strong> {{ response.explanation }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const selectedId = ref('entry')
|
||||
const endpoint = ref('/kitchen/chef')
|
||||
const method = ref('GET')
|
||||
const food = ref('steak')
|
||||
const menuType = ref('today')
|
||||
const loading = ref(false)
|
||||
const response = ref(null)
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 'entry',
|
||||
icon: '📍',
|
||||
title: '入口在哪',
|
||||
hint: '网址 / 函数名',
|
||||
desc: '你要调用的"按钮"在哪里。是 HTTP 网址,还是代码里的函数名?',
|
||||
example: 'GET /api/users/{id}'
|
||||
},
|
||||
{
|
||||
id: 'params',
|
||||
icon: '📝',
|
||||
title: '要填什么',
|
||||
hint: '需要哪些参数',
|
||||
desc: '调用这个 API 时,你需要提供哪些信息?哪些是必填的,哪些是可选的?',
|
||||
example: 'id(必填)、page(可选)'
|
||||
},
|
||||
{
|
||||
id: 'response',
|
||||
icon: '✅',
|
||||
title: '会得到什么',
|
||||
hint: '返回什么数据',
|
||||
desc: '成功的时候,API 会返回什么数据?有哪些字段,分别代表什么意思?',
|
||||
example: '{ id, name, email }'
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
icon: '⚠️',
|
||||
title: '失败怎么说',
|
||||
hint: '错误提示',
|
||||
desc: '调用失败的时候会返回什么错误信息?你应该怎么处理这些错误?',
|
||||
example: '401 没权限、404 找不到、429 太频繁'
|
||||
}
|
||||
]
|
||||
// Reset response when inputs change
|
||||
watch([endpoint, method, food, menuType], () => {
|
||||
response.value = null
|
||||
})
|
||||
|
||||
const selected = computed(() => items.find(i => i.id === selectedId.value))
|
||||
function sendRequest() {
|
||||
loading.value = true
|
||||
response.value = null
|
||||
|
||||
function select(id) {
|
||||
selectedId.value = id
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
|
||||
// Logic for different combinations
|
||||
if (endpoint.value === '/kitchen/bar') {
|
||||
if (method.value === 'GET') {
|
||||
response.value = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
data: { menu: ['Mojito', 'Martini', 'Beer'] },
|
||||
explanation: '你问调酒师有哪些酒,他给了你酒单。'
|
||||
}
|
||||
} else {
|
||||
response.value = {
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
data: { error: "Bar only serves drinks, not food!" },
|
||||
explanation: '你试图向调酒师点菜(牛排/意面),他拒绝了你。你应该去 /kitchen/chef 点菜,或者只点酒。'
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Chef logic
|
||||
if (method.value === 'GET') {
|
||||
response.value = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
data: { specials: ['Spicy Chicken', 'Tofu Soup'] },
|
||||
explanation: '你问主厨今天有什么特供,他告诉了你。'
|
||||
}
|
||||
} else {
|
||||
// POST to Chef
|
||||
const foodMap = {
|
||||
steak: '🥩 滋滋作响的牛排',
|
||||
pasta: '🍝 香气扑鼻的意面',
|
||||
salad: '🥗 新鲜健康的沙拉'
|
||||
}
|
||||
response.value = {
|
||||
status: 200,
|
||||
statusText: 'Created',
|
||||
data: { dish: foodMap[food.value], message: "Enjoy your meal!" },
|
||||
explanation: `你向主厨下了单 (${food.value}),主厨为你做好了菜。`
|
||||
}
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -87,136 +157,148 @@ function select(id) {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 16px 0;
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
.header {
|
||||
padding: 12px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stepper {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.step-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.control, .inline-select {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-group {
|
||||
display: flex;
|
||||
background: var(--vp-c-divider);
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: #f0f9ff;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.card-hint {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.detail {
|
||||
.toggle-btn.active {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-brand-1);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
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);
|
||||
}
|
||||
|
||||
.detail-example strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-example code {
|
||||
display: block;
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
.params-editor {
|
||||
font-family: monospace;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
margin-top: 8px;
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
margin: 0 20px 20px;
|
||||
background: #1e293b;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
color: #e2e8f0;
|
||||
font-family: monospace;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
padding: 8px 12px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge.success { background: #22c55e; color: #fff; }
|
||||
.badge.error { background: #ef4444; color: #fff; }
|
||||
|
||||
.result-content {
|
||||
padding: 16px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.result-explanation {
|
||||
padding: 12px;
|
||||
background: #334155;
|
||||
font-family: var(--vp-font-family-base);
|
||||
font-size: 13px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,194 +1,231 @@
|
||||
<!--
|
||||
ApiDocumentDemo.vue - 简化版
|
||||
目标:用简单的示例展示如何阅读 API 文档
|
||||
ApiDocumentDemo.vue - 翻译版
|
||||
目标:一键把"黑话"翻译成"人话"
|
||||
-->
|
||||
<template>
|
||||
<div class="demo">
|
||||
<div class="title">📖 怎么读 API 文档?</div>
|
||||
<p class="subtitle">找到这 3 个信息就够了</p>
|
||||
<div class="header">
|
||||
<div class="title-area">
|
||||
<span class="icon">📖</span>
|
||||
<span class="title">API 文档翻译机</span>
|
||||
</div>
|
||||
<button class="translate-btn" @click="isHuman = !isHuman">
|
||||
{{ isHuman ? '🔄 还原回黑话' : '✨ 翻译成人话' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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 class="doc-container">
|
||||
<!-- 模拟 API 文档 -->
|
||||
<div class="api-doc">
|
||||
<div class="doc-row method-row">
|
||||
<span class="label">Method:</span>
|
||||
<span class="value method" :class="{ human: isHuman }">
|
||||
{{ isHuman ? '我要下单 (POST)' : 'POST' }}
|
||||
</span>
|
||||
</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 class="doc-row url-row">
|
||||
<span class="label">Endpoint:</span>
|
||||
<span class="value url" :class="{ human: isHuman }">
|
||||
{{ isHuman ? '去哪里找厨师' : 'https://api.deepseek.com/chat' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="doc-row headers-row">
|
||||
<span class="label">Headers:</span>
|
||||
<div class="code-block" :class="{ human: isHuman }">
|
||||
<div class="line">
|
||||
<span class="key">{{ isHuman ? '我是谁:' : 'Authorization:' }}</span>
|
||||
<span class="val">{{ isHuman ? ' 这是我的会员卡号' : ' Bearer sk-8f9s...' }}</span>
|
||||
</div>
|
||||
<div class="line">
|
||||
<span class="key">{{ isHuman ? '用什么语言:' : 'Content-Type:' }}</span>
|
||||
<span class="val">{{ isHuman ? ' 标准格式(JSON)' : ' application/json' }}</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 class="doc-row body-row">
|
||||
<span class="label">Body:</span>
|
||||
<div class="code-block" :class="{ human: isHuman }">
|
||||
<div class="line">{</div>
|
||||
<div class="line indent">
|
||||
<span class="key">"model":</span>
|
||||
<span class="val">"deepseek-chat",</span>
|
||||
<span class="comment" v-if="isHuman"> // 选个聪明的厨师</span>
|
||||
</div>
|
||||
<div class="line indent">
|
||||
<span class="key">"messages":</span>
|
||||
<span class="val">[...]</span>
|
||||
<span class="comment" v-if="isHuman"> // 我要说的话</span>
|
||||
</div>
|
||||
<div class="line">}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="doc-row response-row">
|
||||
<span class="label">Response:</span>
|
||||
<div class="code-block" :class="{ human: isHuman }">
|
||||
<div class="line">
|
||||
<span class="key">{{ isHuman ? '状态:' : 'Status:' }}</span>
|
||||
<span class="status-ok">{{ isHuman ? '搞定了 (200)' : '200 OK' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</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 { ref } from 'vue'
|
||||
|
||||
const isHuman = ref(false)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 16px 0;
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 16px;
|
||||
.translate-btn {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.doc-example {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
.translate-btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.doc-header {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 12px 16px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
.doc-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.doc-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
.api-doc {
|
||||
background: #1e293b;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
color: #e2e8f0;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.section:last-child {
|
||||
.doc-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.doc-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
.label {
|
||||
width: 80px;
|
||||
color: #94a3b8;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
margin-left: 0;
|
||||
.value {
|
||||
color: #38bdf8;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 4px 8px;
|
||||
.method {
|
||||
font-weight: bold;
|
||||
color: #eab308;
|
||||
}
|
||||
|
||||
.method.human {
|
||||
color: #fbbf24;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
.url.human {
|
||||
color: #38bdf8;
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
flex: 1;
|
||||
background: #0f172a;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #334155;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.code-block.human {
|
||||
background: #1e293b;
|
||||
border-color: #64748b;
|
||||
}
|
||||
|
||||
.line {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.indent {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.key {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.val {
|
||||
color: #a5f3fc;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: #22c55e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.param {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.param-name {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
.status-ok {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.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,57 +1,82 @@
|
||||
<!--
|
||||
ApiPlayground.vue - 简化版
|
||||
目标:用最简单的演示展示 API 调用的各种情况
|
||||
ApiPlayground.vue - 闯关版
|
||||
目标:通过"通关"的方式让用户体验 401/404/200
|
||||
-->
|
||||
<template>
|
||||
<div class="demo">
|
||||
<div class="title">🎮 练习场:试试调用 API</div>
|
||||
<p class="subtitle">体验一下成功和失败的情况</p>
|
||||
<div class="header">
|
||||
<span class="icon">🎮</span>
|
||||
<span class="title">练手场:搞崩它,再修好它</span>
|
||||
</div>
|
||||
|
||||
<div class="playground">
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>🔑 钥匙(API Key):</label>
|
||||
<button
|
||||
:class="['toggle', { active: hasKey }]"
|
||||
@click="hasKey = !hasKey"
|
||||
>
|
||||
{{ hasKey ? '✅ 有钥匙' : '❌ 没有钥匙' }}
|
||||
<!-- 控制台 -->
|
||||
<div class="console">
|
||||
<div class="console-header">
|
||||
<div class="dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<span class="console-title">API Console</span>
|
||||
</div>
|
||||
|
||||
<div class="console-body">
|
||||
<div class="input-group">
|
||||
<label>METHOD</label>
|
||||
<select v-model="method" class="method-select" :class="method">
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>URL</label>
|
||||
<div class="url-input-wrapper">
|
||||
<span class="host">https://api.game.com</span>
|
||||
<input v-model="path" type="text" class="url-input" placeholder="/users/1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>HEADERS</label>
|
||||
<div class="code-editor">
|
||||
Authorization: <input v-model="token" placeholder="(空)" class="code-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="send-btn" @click="sendRequest" :disabled="loading">
|
||||
{{ loading ? 'Sending...' : 'SEND REQUEST' }}
|
||||
</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 class="mission-panel">
|
||||
<div class="mission-title">👇 点这些按钮试错</div>
|
||||
<div class="scenarios">
|
||||
<button class="scenario-btn error-401" @click="loadScenario('401')">
|
||||
1. 没带钥匙 (401)
|
||||
</button>
|
||||
<button class="scenario-btn error-404" @click="loadScenario('404')">
|
||||
2. 找错人了 (404)
|
||||
</button>
|
||||
<button class="scenario-btn success-200" @click="loadScenario('200')">
|
||||
3. 成功通关 (200)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<p><strong>💡 玩法建议:</strong></p>
|
||||
<ul>
|
||||
<li>试试把"钥匙"改成"没有钥匙",看看会发生什么</li>
|
||||
<li>试试把 ID 改成 <code>u_404</code>,看看会怎样</li>
|
||||
<li>连续快速点击,看看"限流"提示</li>
|
||||
</ul>
|
||||
<!-- 结果区 -->
|
||||
<div class="result-area" v-if="result">
|
||||
<div class="status-bar" :class="result.statusClass">
|
||||
<span class="status-code">{{ result.code }}</span>
|
||||
<span class="status-text">{{ result.text }}</span>
|
||||
</div>
|
||||
<div class="response-preview">
|
||||
{{ result.data }}
|
||||
</div>
|
||||
<div class="result-tip">
|
||||
<strong>💡 现象解析:</strong> {{ result.tip }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,71 +85,77 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const hasKey = ref(true)
|
||||
const userId = ref('u_123')
|
||||
const calling = ref(false)
|
||||
const method = ref('GET')
|
||||
const path = ref('/secret-treasure')
|
||||
const token = ref('')
|
||||
const loading = ref(false)
|
||||
const result = ref(null)
|
||||
const callCount = ref([])
|
||||
const now = ref(Date.now())
|
||||
|
||||
function callApi() {
|
||||
calling.value = true
|
||||
function loadScenario(type) {
|
||||
result.value = null
|
||||
|
||||
// 模拟限流
|
||||
const currentTime = Date.now()
|
||||
callCount.value = callCount.value.filter(t => currentTime - t < 2000)
|
||||
callCount.value.push(currentTime)
|
||||
|
||||
if (callCount.value.length >= 4) {
|
||||
setTimeout(() => {
|
||||
result.value = {
|
||||
type: 'error',
|
||||
message: '太频繁了!请慢一点再试(限流)'
|
||||
}
|
||||
calling.value = false
|
||||
}, 300)
|
||||
return
|
||||
if (type === '401') {
|
||||
method.value = 'GET'
|
||||
path.value = '/secret-treasure'
|
||||
token.value = '' // Empty token
|
||||
} else if (type === '404') {
|
||||
method.value = 'GET'
|
||||
path.value = '/nothing-here'
|
||||
token.value = 'Bearer my-secret-key'
|
||||
} else if (type === '200') {
|
||||
method.value = 'GET'
|
||||
path.value = '/secret-treasure'
|
||||
token.value = 'Bearer my-secret-key'
|
||||
}
|
||||
}
|
||||
|
||||
function sendRequest() {
|
||||
loading.value = true
|
||||
result.value = null
|
||||
|
||||
setTimeout(() => {
|
||||
// 检查钥匙
|
||||
if (!hasKey.value) {
|
||||
loading.value = false
|
||||
|
||||
// Logic
|
||||
if (path.value === '/nothing-here') {
|
||||
result.value = {
|
||||
type: 'error',
|
||||
message: '没有钥匙!你没有权限调用这个 API(401 未授权)'
|
||||
code: 404,
|
||||
text: 'Not Found',
|
||||
statusClass: 'error',
|
||||
data: 'Error: The resource "/nothing-here" does not exist.',
|
||||
tip: '请求的路径不存在。服务器无法找到对应的资源,因此返回 404 状态码。'
|
||||
}
|
||||
calling.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户 ID
|
||||
const id = userId.value.trim()
|
||||
if (!id) {
|
||||
if (!token.value || token.value.trim() === '') {
|
||||
result.value = {
|
||||
type: 'error',
|
||||
message: '你还没填用户 ID!'
|
||||
code: 401,
|
||||
text: 'Unauthorized',
|
||||
statusClass: 'error',
|
||||
data: 'Error: Missing authentication token.',
|
||||
tip: '请求头中缺少鉴权 Token。服务器无法识别身份,因此拒绝访问并返回 401。'
|
||||
}
|
||||
calling.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (id === 'u_404') {
|
||||
if (path.value === '/secret-treasure') {
|
||||
result.value = {
|
||||
type: 'error',
|
||||
message: '找不到这个用户!ID 不存在(404)'
|
||||
code: 200,
|
||||
text: 'OK',
|
||||
statusClass: 'success',
|
||||
data: '🎉 Congratulations! You found the secret treasure: [Gold, Diamond, Ruby]',
|
||||
tip: '请求成功。路径正确且鉴权通过,服务器正常返回了数据。'
|
||||
}
|
||||
} else {
|
||||
result.value = {
|
||||
code: 404,
|
||||
text: 'Not Found',
|
||||
statusClass: 'error',
|
||||
data: 'Error: Resource not found.',
|
||||
tip: '路径错误。'
|
||||
}
|
||||
calling.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 成功
|
||||
result.value = {
|
||||
type: 'success',
|
||||
message: `成功获取用户信息:\nID: ${id}\n姓名: 张三\n邮箱: zhang@example.com`
|
||||
}
|
||||
calling.value = false
|
||||
}, 800)
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -132,175 +163,227 @@ function callApi() {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 16px 0;
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 16px;
|
||||
.header {
|
||||
padding: 12px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.playground {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.console {
|
||||
background: #1e293b;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
.console-header {
|
||||
background: #0f172a;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
min-width: 120px;
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
padding: 8px 16px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
.dots span {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #334155;
|
||||
}
|
||||
.dots span:nth-child(1) { background: #ef4444; }
|
||||
.dots span:nth-child(2) { background: #eab308; }
|
||||
.dots span:nth-child(3) { background: #22c55e; }
|
||||
|
||||
.console-title {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.console-body {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.method-select {
|
||||
background: #334155;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.method-select.GET { color: #22c55e; }
|
||||
.method-select.POST { color: #eab308; }
|
||||
|
||||
.url-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #0f172a;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #334155;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.host {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.toggle.active {
|
||||
border-color: #22c55e;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.input {
|
||||
.url-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.call-btn {
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
background: var(--vp-c-brand-1);
|
||||
.code-editor {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
color: #eab308;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.code-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
margin-left: 8px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.call-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
.send-btn:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.call-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.result-area {
|
||||
min-height: 120px;
|
||||
.mission-panel {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
.mission-title {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-style: italic;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result {
|
||||
border: 2px solid;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.scenarios {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result.success {
|
||||
border-color: #22c55e;
|
||||
background: #f0fdf4;
|
||||
.scenario-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.result.error {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
.scenario-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
padding: 12px 16px;
|
||||
.error-401 { color: #ef4444; border-color: rgba(239,68,68,0.2); }
|
||||
.error-404 { color: #f97316; border-color: rgba(249,115,22,0.2); }
|
||||
.success-200 { color: #22c55e; border-color: rgba(34,197,94,0.2); }
|
||||
|
||||
.result-area {
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.status-bar.success { background: #dcfce7; color: #166534; }
|
||||
.status-bar.error { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
.response-preview {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 16px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
border-left: 1px solid #334155;
|
||||
border-right: 1px solid #334155;
|
||||
}
|
||||
|
||||
.result-tip {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-top: none;
|
||||
padding: 12px;
|
||||
border-radius: 0 0 6px 6px;
|
||||
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;
|
||||
}
|
||||
|
||||
.tips {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.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;
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
<!--
|
||||
ApiQuickStartDemo.vue - 简化版
|
||||
目标:用最简单的交互展示 API 调用流程
|
||||
ApiQuickStartDemo.vue - 演示版
|
||||
目标:展示最简单的 API 调用流程
|
||||
-->
|
||||
<template>
|
||||
<div class="demo">
|
||||
<div class="title">🎮 试试看:调用一次 API</div>
|
||||
<p class="subtitle">点一下按钮,看看会发生什么</p>
|
||||
<div class="header">
|
||||
<span class="icon">💡</span>
|
||||
<span class="title">试试看:获取一条技术格言</span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="action-area">
|
||||
<button class="call-btn" :disabled="calling" @click="callApi">
|
||||
<span v-if="!calling">📡 发起 API 请求</span>
|
||||
<span v-else>🔄 请求处理中...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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 class="result-area" v-if="result || calling">
|
||||
<div class="loading-dots" v-if="calling">
|
||||
<span>.</span><span>.</span><span>.</span>
|
||||
</div>
|
||||
<div class="error" v-else>
|
||||
❌ 失败了:{{ result.error }}
|
||||
<div class="response-card" v-else-if="result">
|
||||
<div class="response-header">
|
||||
<span class="status-badge success">200 OK</span>
|
||||
<span class="time">耗时: 230ms</span>
|
||||
</div>
|
||||
<div class="response-body">
|
||||
{{ result.data }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="explain">
|
||||
<p><strong>你看:</strong>你只需要点一下按钮(调用 API),就会得到结果。</p>
|
||||
<p>这就是 API 的本质:<strong>按约定把请求交给对方,对方按约定把结果给你</strong>。</p>
|
||||
<div class="footer">
|
||||
<p>👆 <strong>流程演示:</strong> 点击按钮 -> 发送请求 -> 服务器处理 -> 返回数据。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,15 +45,24 @@ import { ref } from 'vue'
|
||||
const calling = ref(false)
|
||||
const result = ref(null)
|
||||
|
||||
const quotes = [
|
||||
"Talk is cheap. Show me the code. — Linus Torvalds",
|
||||
"Programs must be written for people to read, and only incidentally for machines to execute. — Abelson & Sussman",
|
||||
"Truth can only be found in one place: the code. — Robert C. Martin",
|
||||
"Simplicity is the soul of efficiency. — Austin Freeman",
|
||||
"Code is like humor. When you have to explain it, it’s bad. — Cory House"
|
||||
]
|
||||
|
||||
function callApi() {
|
||||
calling.value = true
|
||||
result.value = null
|
||||
|
||||
// 模拟 API 调用
|
||||
// 模拟 API 网络延迟
|
||||
setTimeout(() => {
|
||||
const randomQuote = quotes[Math.floor(Math.random() * quotes.length)]
|
||||
result.value = {
|
||||
success: true,
|
||||
data: 'Hello from API!'
|
||||
data: randomQuote
|
||||
}
|
||||
calling.value = false
|
||||
}, 800)
|
||||
@@ -54,79 +73,125 @@ function callApi() {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 16px 0;
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
.content {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
min-height: 160px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.call-btn {
|
||||
background: var(--vp-c-brand-1);
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
padding: 12px 32px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.call-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.05);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 8px -1px rgba(59, 130, 246, 0.6);
|
||||
}
|
||||
|
||||
.call-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
.result-area {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.response-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.success {
|
||||
.response-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border: 1px solid #86efac;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
.response-body {
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.explain {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
.loading-dots span {
|
||||
animation: blink 1.4s infinite both;
|
||||
font-size: 24px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
.footer {
|
||||
padding: 12px 20px;
|
||||
background: rgba(0,0,0,0.02);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.2; }
|
||||
20% { opacity: 1; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,382 +1,152 @@
|
||||
# API 入门(0 基础版)
|
||||
# API 入门:怎么跟计算机"说人话"?
|
||||
|
||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示和生动比喻带你深入理解 API 的核心概念。我们将从"餐厅点餐"这个生活场景讲起,一步步揭开 API 的神秘面纱。
|
||||
> 💡 **写在前面**:这一章不教写代码,只教**"怎么跟别人的软件打交道"**。
|
||||
>
|
||||
> 你可能听说过 "API" 这个词一万次了,但它到底是个啥?别被它的英文全称吓到,把它当成**"连接器"**或者**"传声筒"**就好。
|
||||
|
||||
<ApiQuickStartDemo />
|
||||
|
||||
## 0. 引言:从餐厅点餐到软件协作
|
||||
## 0. 为什么需要 API?
|
||||
|
||||
想象一下,你走进一家餐厅:
|
||||
想象一下,你想用 DeepSeek 的 AI 能力,但 DeepSeek 的核心程序跑在他们自家机房的超级电脑里。
|
||||
你总不能直接跑去他们机房,插上键盘敲代码吧?
|
||||
|
||||
1. **你**(顾客)拿着菜单,告诉服务员:"我要一份宫保鸡丁,加辣。"
|
||||
2. **服务员**(接口)把你的要求记下来,送到厨房。
|
||||
3. **厨房**(对方的系统)根据要求做菜。
|
||||
4. **服务员**把做好的菜端给你。
|
||||
所以,DeepSeek 需要开一个**"窗口"**,让你在千里之外也能用上他们的 AI。
|
||||
|
||||
在这个过程中,**你不需要知道**:
|
||||
- 厨房有几个厨师
|
||||
- 他们用什么锅铲炒菜
|
||||
- 蔬菜是从哪个市场买的
|
||||
- 厨师今天心情好不好
|
||||
|
||||
**你只需要知道**:
|
||||
- 怎么点菜(喊服务员或填单子)
|
||||
- 要告诉对方什么(菜名、口味要求)
|
||||
- 会得到什么(你点的菜)
|
||||
|
||||
**这就是 API 的本质**:它就像餐厅的**服务员**,是你和"对方的系统"之间的**桥梁**。
|
||||
|
||||
> **API(Application Programming Interface)** = **应用之间的"接口/入口"**:你按约定把请求交给对方,对方按约定把结果给你。
|
||||
这个**"窗口"**,就是 **API**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心:API 的三个关键问题
|
||||
## 1. 核心概念:餐厅里的潜规则
|
||||
|
||||
就像去餐厅点菜一样,使用 API 时你只需要搞清楚 3 个问题:
|
||||
为了让你秒懂,我们还是得请出那位经典的**"服务员"**。
|
||||
但这次我们换个角度:**你是一个只会吃、不会做的顾客**(就像我们只会用 AI、不会写 AI 模型一样)。
|
||||
|
||||
### 1.1 怎么点菜?(入口在哪里)
|
||||
### 1.1 你面临的三个问题
|
||||
|
||||
你首先得知道"怎么叫服务员"。在软件世界里,入口通常有两种:
|
||||
当你走进一家外国餐厅(陌生的软件系统),你只想吃饱,但你面临三个问题:
|
||||
|
||||
- **HTTP API**:像一个"网址",你发送网络请求过去
|
||||
- 例如:`https://api.example.com/getUser`
|
||||
- **SDK/API**:像一个"函数名",你在代码里直接调用
|
||||
- 例如:`getUserInfo(userId)`
|
||||
1. **跟谁说?**(谁是服务员?)
|
||||
2. **怎么说?**(用中文还是英文?要填单子还是直接喊?)
|
||||
3. **结果咋样?**(菜上了还是卖完了?)
|
||||
|
||||
### 1.2 要说什么?(你要填什么信息)
|
||||
在 API 的世界里,这三个问题对应了三个术语:
|
||||
|
||||
你不能只说"我要菜",你得告诉服务员:
|
||||
- 你要什么菜?(模型名称)
|
||||
- 有什么要求?(提示词、参数)
|
||||
- 你是谁?(API Key,相当于会员卡)
|
||||
| 餐厅里的问题 | 计算机里的术语 | 黑话 (行话) |
|
||||
| :--- | :--- | :--- |
|
||||
| **跟谁说?** | **Endpoint (端点)** | "接口地址"、"URL" |
|
||||
| **怎么说?** | **Request (请求)** | "传参"、"Payload" |
|
||||
| **结果咋样?** | **Response (响应)** | "返回值"、"返回包" |
|
||||
|
||||
### 1.3 会得到什么?(成功/失败的结果)
|
||||
### 1.2 互动演示:点一道 AI 料理
|
||||
|
||||
服务员可能会端来:
|
||||
- ✅ 你点的菜(成功返回的数据)
|
||||
- ❌ "不好意思,这道菜卖完了"(错误提示,比如 404、500)
|
||||
别光看字,来亲自试一下"点菜"的感觉。
|
||||
下面这个小工具模拟了你向 AI **"点一道笑话"** 的过程。
|
||||
|
||||
<ApiConceptDemo />
|
||||
|
||||
### 1.4 API 的核心价值:把复杂度藏起来
|
||||
|
||||
回到餐厅的比喻:
|
||||
|
||||
**餐厅**需要做的事情(实现细节):
|
||||
- 采购食材、处理库存
|
||||
- 安排厨师、协调后厨
|
||||
- 控制火候、调味摆盘
|
||||
- 清洗餐具、打扫卫生
|
||||
|
||||
**顾客**完全不需要知道这些!顾客只需要:
|
||||
- 看菜单点菜
|
||||
- 等菜上桌
|
||||
- 享用美食
|
||||
|
||||
**API 就像菜单和服务员**,它把"怎么做"的复杂度全部藏起来,只暴露"怎么用"的简单接口。
|
||||
|
||||
这就带来两个好处:
|
||||
1. **简化使用**:调用者不需要理解内部实现
|
||||
2. **灵活变更**:餐厅换了厨师、改了做法,但菜单不变,顾客完全无感
|
||||
|
||||
---
|
||||
|
||||
## 2. 两种常见的 API 形式
|
||||
## 2. 两种"点菜"流派:外卖 vs 堂食
|
||||
|
||||
在现实世界里,你会遇到两种"点菜方式":
|
||||
你在教程里经常会看到两种调用方式:**HTTP** 和 **SDK**。
|
||||
很多新手会被绕晕,其实它们就是**"点外卖"**和**"堂食"**的区别。
|
||||
|
||||
### 2.1 外卖配送(HTTP API)
|
||||
### 2.1 HTTP API(点外卖)
|
||||
|
||||
你不用亲自去餐厅,只需要:
|
||||
1. 打开外卖 APP(找到入口:网址)
|
||||
2. 选好菜品、填好地址(准备请求:参数)
|
||||
3. 等外卖员送到(接收响应:数据)
|
||||
这是最原始、最通用的方式。就像**填一张外卖单子**。
|
||||
|
||||
**HTTP API** 就是这种方式:通过网络发送请求,等待返回结果。
|
||||
* **特点**:**死板但通用**。
|
||||
* **怎么做**:你需要严格按照格式,填好地址(URL)、选好菜(参数)、贴好邮票(API Key),然后通过网络发出去。
|
||||
* **谁在用**:所有编程语言都能用,甚至你在浏览器地址栏里敲一行字也是一种 HTTP 请求。
|
||||
|
||||
**流程是这样的**:
|
||||
```
|
||||
你的电脑 → 发送请求 → 网络传输 → 对方服务器
|
||||
↓
|
||||
处理你的请求
|
||||
↓
|
||||
你的电脑 ← 接收结果 ← 网络传输 ← 对方服务器
|
||||
```
|
||||
### 2.2 SDK(堂食/VIP服务)
|
||||
|
||||
<RequestResponseFlow />
|
||||
SDK (Software Development Kit) 就像是餐厅派给你的**专属管家**。
|
||||
|
||||
**举个例子**:调用 AI 模型生成文本
|
||||
- 你发送:"帮我写一首关于春天的诗"
|
||||
- 对方处理:调用大语言模型生成
|
||||
- 你接收:返回生成的诗歌
|
||||
|
||||
### 2.2 餐堂堂食(SDK/API)
|
||||
|
||||
你走进餐厅,直接对服务员说:
|
||||
- "来一份宫保鸡丁"
|
||||
|
||||
**SDK(软件开发工具包)** 就像是餐厅的"服务员",它已经在餐厅里了,你只需要说话(调用函数),它会帮你:
|
||||
- 把要求转达给厨房(内部帮你调用 HTTP API)
|
||||
- 处理各种复杂细节(鉴权、重试、数据格式)
|
||||
- 最后把结果整理好给你
|
||||
|
||||
**所以你会听到两种说法**:
|
||||
- "调用这个服务的 API"(通常指 HTTP API,像外卖)
|
||||
- "调用这个 SDK 的 API"(通常指函数接口,像堂食)
|
||||
|
||||
---
|
||||
|
||||
## 3. 真实世界:怎么和 AI 服务"对话"
|
||||
|
||||
让我们看一个真实的例子:调用 AI 模型。
|
||||
|
||||
**场景**:你想让 AI 帮你写一段产品文案。
|
||||
|
||||
### 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 的方式更简单,因为它帮你处理了很多细节!
|
||||
* **特点**:**省心但挑人**。
|
||||
* **怎么做**:你不需要自己填单子、贴邮票。你只需要跟管家说:"来份宫保鸡丁"。管家会自己在后台帮你填单子、发请求、处理报错。
|
||||
* **谁在用**:通常针对特定语言(比如 Python 版管家、Node.js 版管家)。
|
||||
|
||||
<RealWorldApiDemo />
|
||||
|
||||
### 3.3 两种方式的对比
|
||||
|
||||
| 特点 | HTTP API(外卖) | SDK API(堂食) |
|
||||
|------|------------------|-----------------|
|
||||
| **使用门槛** | 需要理解网络请求、数据格式 | 只需会调用函数 |
|
||||
| **灵活性** | 更灵活,任何语言都能用 | 通常限定特定语言 |
|
||||
| **复杂度** | 你要处理很多细节(鉴权、错误等) | SDK 帮你处理细节 |
|
||||
| **典型场景** | 跨语言调用、学习原理 | 日常开发、快速集成 |
|
||||
> **新手建议**:能用 SDK 就用 SDK,**把麻烦事留给别人,把时间留给自己**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 进阶:GET 和 POST 有什么区别?
|
||||
## 3. 进阶:GET 和 POST 到底有啥区别?
|
||||
|
||||
> 🎯 **新手提示**:这一节可以暂时跳过。等你熟悉了基本调用,再回来了解也不迟。
|
||||
在看文档时,你总能看到这俩词。
|
||||
其实很简单,就看**"你是否想改变世界"**。
|
||||
|
||||
<details>
|
||||
<summary>点我展开:进阶内容(用人话讲)</summary>
|
||||
### 3.1 GET:只看不买
|
||||
|
||||
在 HTTP API 的世界里,你会经常看到 **GET** 和 **POST** 这两个词。它们就像两种不同的"点菜方式"。
|
||||
* **含义**:**获取**信息。
|
||||
* **场景**:刷朋友圈、看新闻、查天气。
|
||||
* **特点**:你做这件事一万次,服务器里的数据也不会变(除了访问量+1)。
|
||||
* **比喻**:你看菜单,看一眼是看,看一百眼还是看,菜单不会变。
|
||||
|
||||
### 4.1 GET:像看菜单(只看不吃)
|
||||
### 3.2 POST:搞点事情
|
||||
|
||||
**特点**:
|
||||
- 只是想"获取"信息,不会改变服务器状态
|
||||
- 就像在餐厅看菜单,你看一遍菜单,厨房的菜不会被消耗
|
||||
- **可以安全重试**:看一遍菜单没看清,再看一遍,没问题
|
||||
|
||||
**例子**:
|
||||
- 查询用户信息:`GET /api/user/123`
|
||||
- 搜索商品:`GET /api/products?keyword=手机`
|
||||
- 获取文章列表:`GET /api/articles`
|
||||
|
||||
### 4.2 POST:像下单(会真的执行)
|
||||
|
||||
**特点**:
|
||||
- 会"创建"或"修改"服务器上的数据
|
||||
- 就像你下了单,厨房真的开始做菜了
|
||||
- **不能随意重试**:下错了单,再下一遍,你就点了双份!
|
||||
|
||||
**例子**:
|
||||
- 创建用户:`POST /api/users`(会真的创建一个新用户)
|
||||
- 下单购买:`POST /api/orders`(会真的扣钱、发货)
|
||||
- 发表评论:`POST /api/comments`(会真的保存一条评论)
|
||||
* **含义**:**提交**信息。
|
||||
* **场景**:发朋友圈、注册账号、**让 AI 写文章**。
|
||||
* **特点**:你做这件事,服务器里就会**多出**一条记录,或者**生成**一段新的内容。
|
||||
* **比喻**:你下单点菜。你下一次单,厨房就得忙活一次,你的钱包就得瘪一次。
|
||||
|
||||
<ApiMethodDemo />
|
||||
|
||||
### 4.3 还有哪些方法?(简单了解)
|
||||
|
||||
除了 GET 和 POST,还有:
|
||||
- **PUT**:更新(替换整个资源)
|
||||
- **PATCH**:打补丁(更新部分字段)
|
||||
- **DELETE**:删除
|
||||
|
||||
**新手建议**:先学会用 GET 和 POST,其他的慢慢来。
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 5. 怎么读 API 文档?(像看菜单一样简单)
|
||||
## 4. 怎么看 API 文档?
|
||||
|
||||
API 文档就像餐厅的**菜单 + 说明书**,告诉你:
|
||||
- 有哪些菜可以点(提供哪些功能)
|
||||
- 每道菜是什么(接口说明)
|
||||
- 怎么点(怎么调用)
|
||||
- 什么价格(返回什么数据)
|
||||
- 有没有忌口/限量(注意事项)
|
||||
文档就像**"说明书 + 菜单"**。
|
||||
你不需要从头读到尾,只需要学会**"查字典"**。
|
||||
|
||||
### 4.1 文档里的"藏宝图"
|
||||
|
||||
打开任何一个 API 文档(比如 OpenAI 或 DeepSeek),你只需要找这几样东西:
|
||||
|
||||
1. **Base URL**:根地址(餐厅在哪?)。
|
||||
2. **Authentication**:怎么证明你是会员?(通常是加个 `Authorization: Bearer sk-...` 头)。
|
||||
3. **Endpoints**:具体的菜品列表。
|
||||
* `/v1/chat/completions` -> 对话(最常用的)
|
||||
* `/v1/images/generations` -> 画图
|
||||
4. **Parameters**:必填项有哪些?
|
||||
* `model`: 选哪个厨师?
|
||||
* `messages`: 你想聊啥?
|
||||
|
||||
<ApiDocumentDemo />
|
||||
|
||||
### 5.1 阅读 API 文档的 5 步法
|
||||
### 4.2 常见的"餐厅黑话"(状态码)
|
||||
|
||||
就像看菜单点菜一样,按这个流程来:
|
||||
服务员(API)回复你的时候,通常会先喊一个数字代码:
|
||||
|
||||
**第 1 步:确认这道菜是不是你要的**
|
||||
- 这个接口能做什么?
|
||||
- 符合你的需求吗?
|
||||
|
||||
**第 2 步:找到"点菜入口"**
|
||||
- HTTP API:网址(URL)是什么?
|
||||
- SDK:函数名是什么?
|
||||
|
||||
**第 3 步:看看要填什么信息**
|
||||
- **必填项**:就像"必须选辣度/份量",不填不行
|
||||
- **可选项**:就像"要不要加葱花",可以不填
|
||||
- **默认值**:就像"默认中辣",你不填就按这个来
|
||||
|
||||
**第 4 步:看看会端上来什么**
|
||||
- 成功时返回什么数据?
|
||||
- 字段代表什么意思?
|
||||
- 可能是空的吗?
|
||||
|
||||
**第 5 步:了解"餐厅规则"**
|
||||
- 没钱了会怎样?(余额不足)
|
||||
- 点太快会怎样?(限流/Rate Limit)
|
||||
- 菜卖完了会怎样?(404 资源不存在)
|
||||
- 厨房出错会怎样?(500 服务器错误)
|
||||
|
||||
### 5.2 常见的状态码(就像餐厅的回复)
|
||||
|
||||
| 状态码 | 含义 | 餐厅类比 |
|
||||
|--------|------|----------|
|
||||
| **200** | 成功 | "这是您的菜,请慢用" |
|
||||
| **400** | 请求错误 | "您点的菜我们有,但您填的信息有问题" |
|
||||
| **401** | 未授权 | "请先出示会员卡" |
|
||||
| **403** | 禁止访问 | "您的会员卡等级不够,点不了这道菜" |
|
||||
| **404** | 资源不存在 | "对不起,您点的菜卖完了" |
|
||||
| **429** | 请求过多 | "您点太快了,请稍后再试" |
|
||||
| **500** | 服务器错误 | "厨房出故障了,请稍后再试" |
|
||||
* **200 OK**:菜齐了,慢用。(成功)
|
||||
* **400 Bad Request**:你点的菜菜单上没有,或者你没填辣度。(你填错了)
|
||||
* **401 Unauthorized**:会员卡过期了,或者假卡。(没权限)
|
||||
* **404 Not Found**:找不到这道菜,或者找不到这家店。(地址错了)
|
||||
* **429 Too Many Requests**:你点太快了,厨师炒不过来了。(限流)
|
||||
* **500 Internal Server Error**:厨房炸了,不是你的锅。(服务器崩了)
|
||||
|
||||
---
|
||||
|
||||
## 6. 实战:用"模拟 API"练出手感
|
||||
## 5. 练手场:弄坏它也没关系
|
||||
|
||||
理论讲完了,该动手了!在真实世界里,你会用 Postman、curl 或代码去调用 API。但这里我们准备了一个"练习场",不用担心网络问题、CORS 错误,专注于练出核心手感。
|
||||
光说不练假把式。
|
||||
这里有个模拟 API。你可以随便填参数、随便改地址,看看会发生什么。
|
||||
试着触发一下 **401**(假装没带钱)或者 **404**(瞎填地址)。
|
||||
|
||||
<ApiPlayground />
|
||||
|
||||
### 6.1 建议按顺序试试这些"场景"
|
||||
|
||||
就像去餐厅"踩点"一样,试试各种情况:
|
||||
|
||||
**场景 1:忘带会员卡**
|
||||
- 把"登录/钥匙"改成"没有"
|
||||
- 观察返回什么错误(通常是 401)
|
||||
|
||||
**场景 2:点太快被限流**
|
||||
- 连续快速点击"调用"按钮
|
||||
- 观察返回什么错误(通常是 429)
|
||||
|
||||
**场景 3:点菜信息填错了**
|
||||
- 选 POST 创建用户
|
||||
- 把 Body 改成非法的 JSON 格式
|
||||
- 观察返回什么错误(通常是 400)
|
||||
|
||||
**场景 4:点的菜卖完了**
|
||||
- 把用户 ID 改成 `u_404`(不存在的用户)
|
||||
- 观察返回什么错误(通常是 404)
|
||||
|
||||
### 6.2 练习目标
|
||||
|
||||
通过这些练习,你要掌握:
|
||||
1. **能看懂成功响应**:知道返回的数据在哪里
|
||||
2. **能看懂错误提示**:知道为什么失败、怎么改
|
||||
3. **有手感**:知道调用 API 的基本流程
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结:记住这三句话就够了
|
||||
## 6. 总结
|
||||
|
||||
### 7.1 API 的三种形式
|
||||
别把 API 想得太高大上。
|
||||
在 AI 编程的时代,你只需要记住:
|
||||
|
||||
| 类型 | 比喻 | 特点 |
|
||||
|------|------|------|
|
||||
| **HTTP API** | 外卖配送 | 通过网络调用,你发请求它回结果 |
|
||||
| **SDK API** | 餐厅堂食 | 通过函数调用,它内部帮你发请求 |
|
||||
| **库 API** | 自己做菜 | 本地函数,不走网络 |
|
||||
1. **API 就是传声筒**:帮你把话传给 AI 模型。
|
||||
2. **SDK 是好管家**:能用管家就别自己跑腿。
|
||||
3. **看文档找三样**:地址、密钥、参数。
|
||||
|
||||
### 7.2 核心记忆点
|
||||
|
||||
1. **API = 接口**:就像餐厅的服务员,是你和对方系统的桥梁
|
||||
2. **调用三要素**:入口(网址/函数名)、参数(要告诉什么)、返回(会得到什么)
|
||||
3. **学会读文档**:就像看菜单,先确认能不能用,再看怎么用
|
||||
|
||||
### 7.3 下一步建议
|
||||
|
||||
现在你已经理解了 API 的基本概念,可以去:
|
||||
- 读一读真实的 API 文档(比如 OpenAI、DeepSeek 的文档)
|
||||
- 用 Postman 或 curl 试试真实的 API 调用
|
||||
- 在你的项目里接入第一个 API
|
||||
|
||||
---
|
||||
|
||||
## 8. 名词速查表
|
||||
|
||||
> 💡 **使用建议**:不用背!遇到不懂的词回来查就行。你只要会"看文档、会填参数、能看懂成功/失败",就已经能开始用 API 了。
|
||||
|
||||
| 名词 | 英文 | 解释 |
|
||||
|------|------|------|
|
||||
| **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 | 请求体,像点菜单的详细内容(具体的菜品要求) |
|
||||
这就够了。剩下的,交给 IDE 去写吧。
|
||||
|
||||
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 441 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 641 KiB |
|
After Width: | Height: | Size: 608 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 813 KiB |
|
After Width: | Height: | Size: 540 KiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 771 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 940 KiB |
|
After Width: | Height: | Size: 460 KiB |