docs: 更新 API 文档和 AI 能力集成页面
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user