docs: 更新 API 文档和 AI 能力集成页面

This commit is contained in:
sanbuphy
2026-01-20 17:53:22 +08:00
parent f6195ee17a
commit 4bb9333b37
28 changed files with 1393 additions and 1305 deletions
@@ -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, its 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>