refactor: 重构 api-intro、api-design、transistor-to-cpu 组件为紧凑布局

- 重构 api-intro 7 个 Vue 组件为更紧凑的左右布局
- 重构 api-design 相关组件
- 重构 transistor-to-cpu 相关组件
- 统一使用 demo-root -> demo-header -> demo-layout -> info-box 结构
- 扩写文章内容为 MIT 讲义风格
This commit is contained in:
sanbuphy
2026-02-23 01:50:43 +08:00
parent 2a0fdd3392
commit 1062e2e16f
68 changed files with 4455 additions and 3469 deletions
@@ -1,87 +1,90 @@
<!--
ApiConceptDemo.vue
ApiConceptDemo.vue - 紧凑版
目标直观演示 API 的基本要素地址 + 参数
-->
<template>
<div class="demo">
<div class="header">
<div class="demo-root">
<div class="demo-header">
<span class="icon">🔧</span>
<span class="title">互动演示调用 API 需要什么</span>
<span class="title">调用 API 需要什么</span>
</div>
<div class="content">
<div class="step">
<div class="step-header">
<span class="step-num">1</span>
<span class="step-title">地址 (Endpoint) - 告诉服务器你要找谁</span>
</div>
<div class="step-body">
<div class="demo-layout">
<div class="left-panel">
<div class="step">
<div class="step-header">
<span class="step-num">1</span>
<span class="step-title">地址 (Endpoint)</span>
</div>
<div class="url-bar">
<span class="url">https://api.example.com</span>
<span class="url-base">https://api.example.com</span>
<input
v-model="endpoint"
type="text"
class="endpoint-input"
placeholder="/users"
>
/>
</div>
</div>
</div>
<div class="step">
<div class="step-header">
<span class="step-num">2</span>
<span class="step-title">参数 (Params) - 告诉服务器你要什么</span>
</div>
<div class="step-body">
<div class="params-box">
<div class="params-row">
<span class="param-label">页码</span>
<input
v-model.number="page"
type="number"
class="param-input"
min="1"
>
</div>
<div class="params-row">
<span class="param-label">每页数量</span>
<input
v-model.number="limit"
type="number"
class="param-input small"
min="1"
max="100"
>
</div>
<div class="step">
<div class="step-header">
<span class="step-num">2</span>
<span class="step-title">参数 (Params)</span>
</div>
<div class="params-row">
<label>页码:</label>
<input
v-model.number="page"
type="number"
class="param-input"
min="1"
/>
<label>每页:</label>
<input
v-model.number="limit"
type="number"
class="param-input"
min="1"
max="100"
/>
</div>
</div>
<button class="send-btn" :disabled="loading" @click="sendRequest">
{{ loading ? '发送中...' : '🚀 发送请求' }}
</button>
</div>
<button
class="send-btn"
:disabled="loading"
@click="sendRequest"
>
{{ loading ? '发送中...' : '🚀 发送请求' }}
</button>
<div
v-if="response"
class="response"
>
<div class="right-panel">
<div class="response-header">
<span
v-if="response"
class="status-badge"
:class="response.status >= 200 && response.status < 300 ? 'success' : 'error'"
:class="
response.status >= 200 && response.status < 300
? 'success'
: 'error'
"
>
{{ response.status }} {{ response.statusText }}
</span>
<span class="response-time">耗时: {{ response.time }}ms</span>
<span v-else class="status-badge pending">等待请求</span>
</div>
<pre class="response-body">{{ JSON.stringify(response.data, null, 2) }}</pre>
<div v-if="response" class="response-body">
<pre>{{ JSON.stringify(response.data, null, 2) }}</pre>
</div>
<div v-else class="response-empty">点击发送按钮查看结果</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span
>无论哪种 API结构都一样地址找谁+ 参数要什么=
响应得到什么</span
>
</div>
</div>
</template>
@@ -99,130 +102,140 @@ function sendRequest() {
response.value = null
setTimeout(() => {
const startTime = Date.now()
if (endpoint.value === '/users') {
// 限制最多返回3条,避免结果太长
const actualLimit = Math.min(limit.value, 3)
const users = []
for (let i = 1; i <= actualLimit; i++) {
users.push({
id: i,
name: `用户${(page.value - 1) * limit.value + i}`,
age: 20 + i
name: `用户${(page.value - 1) * limit.value + i}`
})
}
response.value = {
status: 200,
statusText: 'OK',
time: Date.now() - startTime,
data: {
users,
total: 100,
page: page.value,
limit: limit.value,
note: limit.value > 3 ? `仅显示前3条,共${limit.value}` : null
}
data: { users, total: 100, page: page.value }
}
} else {
response.value = {
status: 404,
statusText: 'Not Found',
time: Date.now() - startTime,
data: { error: '找不到这个接口' }
}
}
loading.value = false
}, 300 + Math.random() * 200)
}, 300)
}
</script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
padding: 14px 20px;
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
}
.icon {
font-size: 20px;
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 15px;
font-size: 0.9rem;
}
.content {
padding: 20px;
.demo-layout {
display: flex;
}
.left-panel {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
gap: 16px;
gap: 10px;
border-right: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 220px;
padding: 12px;
background: var(--vp-c-bg);
display: flex;
flex-direction: column;
gap: 8px;
}
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.left-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 100%;
}
}
.step {
background: var(--vp-c-bg);
border-radius: 10px;
border-radius: 6px;
overflow: hidden;
}
.step-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: var(--vp-c-bg-soft);
gap: 8px;
padding: 6px 10px;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.step-num {
width: 22px;
height: 22px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-size: 0.7rem;
font-weight: bold;
}
.step-title {
font-size: 13px;
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.step-body {
padding: 14px;
}
.url-bar {
display: flex;
align-items: center;
gap: 8px;
gap: 4px;
background: #1e293b;
padding: 10px 12px;
border-radius: 6px;
padding: 8px 10px;
border-radius: 0 0 6px 6px;
}
.url {
.url-base {
color: #94a3b8;
font-size: 13px;
font-size: 0.7rem;
white-space: nowrap;
}
.endpoint-input {
@@ -231,73 +244,58 @@ function sendRequest() {
border: none;
color: #60a5fa;
font-family: monospace;
font-size: 14px;
font-size: 0.8rem;
outline: none;
}
.params-box {
display: flex;
flex-direction: column;
gap: 10px;
}
.params-row {
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
padding: 8px 10px;
background: var(--vp-c-bg-soft);
border-radius: 0 0 6px 6px;
}
.param-label {
font-size: 13px;
.params-row label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
min-width: 70px;
}
.param-input {
padding: 6px 10px;
width: 50px;
padding: 4px 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
font-size: 13px;
}
.param-input.small {
width: 60px;
border-radius: 4px;
font-size: 0.8rem;
background: var(--vp-c-bg);
}
.send-btn {
padding: 12px;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: opacity 0.2s;
}
.send-btn:disabled {
opacity: 0.7;
opacity: 0.6;
cursor: not-allowed;
}
.response {
margin-top: 8px;
animation: fadeIn 0.3s ease;
}
.response-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
justify-content: center;
}
.status-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-size: 0.75rem;
font-weight: bold;
}
@@ -311,32 +309,50 @@ function sendRequest() {
color: #991b1b;
}
.response-time {
font-size: 12px;
.status-badge.pending {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-3);
}
.response-body {
flex: 1;
background: #1e293b;
color: #e2e8f0;
padding: 12px;
border-radius: 6px;
font-family: monospace;
font-size: 12px;
overflow-x: auto;
max-height: 200px;
margin: 0;
padding: 8px;
overflow: auto;
max-height: 120px;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
.response-body pre {
margin: 0;
font-family: monospace;
font-size: 0.7rem;
color: #e2e8f0;
white-space: pre-wrap;
}
.response-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-3);
font-size: 0.8rem;
font-style: italic;
}
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,269 +1,241 @@
<!--
ApiDocumentDemo.vue - 翻译
目标一键把"黑话"翻译成"人话"
ApiDocumentDemo.vue - 紧凑
目标演示如何阅读 API 文档
-->
<template>
<div class="demo">
<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 class="demo-root">
<div class="demo-header">
<span class="icon">📖</span>
<span class="title">API 文档翻译机</span>
</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 class="demo-layout">
<div class="left-panel">
<div class="doc-section">
<div class="doc-title">Base URL</div>
<code class="doc-code">https://api.deepseek.com</code>
</div>
<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 class="doc-section">
<div class="doc-title">Endpoint</div>
<code class="doc-code">POST /v1/chat/completions</code>
</div>
<div class="doc-row headers-row">
<span class="label">Headers:</span>
<div
class="code-block"
:class="{ human: isHuman }"
<div class="doc-section">
<div class="doc-title">Headers</div>
<pre class="doc-pre">
Authorization: Bearer sk-xxx
Content-Type: application/json</pre
>
<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>
</div>
</div>
<div class="doc-row body-row">
<span class="label">Body:</span>
<div
class="code-block"
:class="{ human: isHuman }"
>
<div class="line">
{
<div class="doc-section">
<div class="doc-title">Body 参数</div>
<div class="params-list">
<div class="param-item">
<span class="p-name">model</span>
<span class="p-req">必填</span>
<span class="p-desc">模型名称</span>
</div>
<div class="line indent">
<span class="key">"model":</span>
<span class="val">"deepseek-chat",</span>
<span
v-if="isHuman"
class="comment"
> // 选个聪明的厨师</span>
<div class="param-item">
<span class="p-name">messages</span>
<span class="p-req">必填</span>
<span class="p-desc">对话消息</span>
</div>
<div class="line indent">
<span class="key">"messages":</span>
<span class="val">[...]</span>
<span
v-if="isHuman"
class="comment"
> // 我要说的话</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 class="param-item">
<span class="p-name">temperature</span>
<span class="p-opt">可选</span>
<span class="p-desc">0-2默认1</span>
</div>
</div>
</div>
</div>
<div class="right-panel">
<div class="result-title">翻译成代码</div>
<pre class="result-code"><code>from openai import OpenAI
client = OpenAI(
api_key="sk-xxx",
base_url="https://api.deepseek.com"
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "你好"}]
)</code></pre>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span
>看文档找三样地址Base
URL鉴权Authorization参数Parameters</span
>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isHuman = ref(false)
</script>
<script setup></script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
padding: 16px 20px;
.demo-header {
padding: 10px 16px;
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;
gap: 8px;
}
.icon {
font-size: 24px;
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 16px;
font-size: 0.9rem;
}
.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;
}
.translate-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.doc-container {
padding: 20px;
}
.api-doc {
background: #1e293b;
border-radius: 6px;
padding: 20px;
color: #e2e8f0;
font-family: monospace;
font-size: 14px;
}
.doc-row {
.demo-layout {
display: flex;
margin-bottom: 16px;
align-items: flex-start;
}
.doc-row:last-child {
margin-bottom: 0;
}
.label {
width: 80px;
color: #94a3b8;
font-weight: bold;
flex-shrink: 0;
padding-top: 2px;
}
.value {
color: #38bdf8;
transition: all 0.3s;
}
.method {
font-weight: bold;
color: #eab308;
}
.method.human {
color: #fbbf24;
background: rgba(251, 191, 36, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
.url.human {
color: #38bdf8;
background: rgba(56, 189, 248, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
.code-block {
.left-panel {
flex: 1;
background: #0f172a;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
border-right: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 280px;
padding: 12px;
background: var(--vp-c-bg);
}
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.left-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 100%;
}
}
.doc-section {
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid #334155;
transition: all 0.3s;
overflow: hidden;
}
.code-block.human {
.doc-title {
padding: 6px 10px;
background: var(--vp-c-bg-alt);
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.doc-code {
display: block;
padding: 8px 10px;
font-family: monospace;
font-size: 0.8rem;
color: var(--vp-c-brand);
}
.doc-pre {
margin: 0;
padding: 8px 10px;
font-family: monospace;
font-size: 0.75rem;
color: var(--vp-c-text-1);
}
.params-list {
padding: 6px 10px;
}
.param-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-size: 0.75rem;
}
.p-name {
font-family: monospace;
font-weight: 600;
}
.p-req {
background: #fee2e2;
color: #991b1b;
padding: 1px 6px;
border-radius: 3px;
font-size: 0.65rem;
}
.p-opt {
background: #dbeafe;
color: #1e40af;
padding: 1px 6px;
border-radius: 3px;
font-size: 0.65rem;
}
.p-desc {
color: var(--vp-c-text-3);
}
.result-title {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 8px;
}
.result-code {
margin: 0;
padding: 10px;
background: #1e293b;
border-color: #64748b;
border-radius: 6px;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.7rem;
line-height: 1.5;
color: #e2e8f0;
overflow-x: auto;
}
.line {
line-height: 1.6;
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.indent {
padding-left: 20px;
}
.key {
color: #94a3b8;
}
.val {
color: #a5f3fc;
}
.comment {
color: #22c55e;
font-style: italic;
}
.status-ok {
color: #22c55e;
font-weight: bold;
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,422 +1,242 @@
<!--
ApiMethodDemo.vue
目标清晰展示各种 HTTP 方法的含义和使用场景
ApiMethodDemo.vue - 紧凑版
目标展示 HTTP 方法的语义
-->
<template>
<div class="demo">
<div class="title">
🔍 HTTP 方法GETPOSTPUTDELETE 是什么
<div class="demo-root">
<div class="demo-header">
<span class="icon">📋</span>
<span class="title">HTTP 方法告诉服务器你想做什么</span>
</div>
<p class="subtitle">
把它们想象成对数据的"操作方式"
</p>
<div class="methods-grid">
<div class="method-card get">
<div class="method-badge">
GET
</div>
<div class="method-title">
📖 获取查询
</div>
<div class="method-desc">
<p><strong>只看不改</strong> - 从服务器获取数据</p>
<div class="method-examples">
<div class="example-item">
查询用户信息
</div>
<div class="example-item">
搜索商品
</div>
<div class="example-item">
获取文章列表
</div>
<div class="demo-layout">
<div class="methods-grid">
<div
v-for="m in methods"
:key="m.name"
class="method-card"
:class="{ active: selected === m.name }"
@click="selected = m.name"
>
<div class="m-badge" :class="m.color">
{{ m.name }}
</div>
<div class="m-desc">
{{ m.desc }}
</div>
<div class="m-example">
{{ m.example }}
</div>
</div>
<div class="method-tip">
💡 可以安全重试不会改变服务器数据
</div>
</div>
<div class="method-card post">
<div class="method-badge">
POST
<div class="right-panel">
<div class="compare-header">对比幂等性</div>
<div class="compare-row">
<span class="c-label">GET</span>
<span class="c-val">查询10次 = 查询1次 </span>
</div>
<div class="method-title">
创建新增
<div class="compare-row">
<span class="c-label">DELETE</span>
<span class="c-val">删除10次 = 删除1次 </span>
</div>
<div class="method-desc">
<p><strong>新建数据</strong> - 在服务器创建新资源</p>
<div class="method-examples">
<div class="example-item">
创建新用户
</div>
<div class="example-item">
提交订单
</div>
<div class="example-item">
发表评论
</div>
</div>
</div>
<div class="method-tip">
不能随意重试可能会重复创建
</div>
</div>
<div class="method-card put">
<div class="method-badge">
PUT
</div>
<div class="method-title">
更新替换
</div>
<div class="method-desc">
<p><strong>整体替换</strong> - 用新数据完全替换旧数据</p>
<div class="method-examples">
<div class="example-item">
修改用户全部信息
</div>
<div class="example-item">
更换文章全部内容
</div>
</div>
</div>
<div class="method-tip">
会覆盖整个资源需要提供完整数据
</div>
</div>
<div class="method-card patch">
<div class="method-badge">
PATCH
</div>
<div class="method-title">
🔧 修改部分
</div>
<div class="method-desc">
<p><strong>局部更新</strong> - 只修改资源的部分字段</p>
<div class="method-examples">
<div class="example-item">
只修改用户昵称
</div>
<div class="example-item">
更新文章标题
</div>
</div>
</div>
<div class="method-tip">
💡 只传需要修改的字段更灵活
</div>
</div>
<div class="method-card delete">
<div class="method-badge">
DELETE
</div>
<div class="method-title">
🗑 删除
</div>
<div class="method-desc">
<p><strong>移除数据</strong> - 从服务器删除资源</p>
<div class="method-examples">
<div class="example-item">
删除用户
</div>
<div class="example-item">
取消订单
</div>
<div class="example-item">
删除评论
</div>
</div>
</div>
<div class="method-tip">
操作不可逆删除后无法恢复
<div class="compare-row warn">
<span class="c-label">POST</span>
<span class="c-val">下单10次 = 10个订单 </span>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title">
📊 快速对比
</div>
<table>
<thead>
<tr>
<th>方法</th>
<th>操作</th>
<th>是否会改数据</th>
<th>能否重试</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="badge get">GET</span></td>
<td>查询</td>
<td> </td>
<td> 可以</td>
</tr>
<tr>
<td><span class="badge post">POST</span></td>
<td>创建</td>
<td> </td>
<td> 不建议</td>
</tr>
<tr>
<td><span class="badge put">PUT</span></td>
<td>替换</td>
<td> </td>
<td> 不建议</td>
</tr>
<tr>
<td><span class="badge patch">PATCH</span></td>
<td>修改</td>
<td> </td>
<td> 不建议</td>
</tr>
<tr>
<td><span class="badge delete">DELETE</span></td>
<td>删除</td>
<td> </td>
<td> 不建议</td>
</tr>
</tbody>
</table>
</div>
<div class="tips">
<p><strong>💡 新手建议</strong></p>
<ul>
<li>
先学会 <strong>GET</strong>查询
<strong>POST</strong>创建就够用了
</li>
<li>PUT/PATCH/DELETE 可以慢慢学理解原理更重要</li>
<li>实际开发中GET POST 占了 80% 的使用场景</li>
</ul>
<div class="info-box">
<strong>核心思想</strong>
<span
>HTTP 方法就是动词GET "问"POST "做"PUT/PATCH "改"DELETE
"删"</span
>
</div>
</div>
</template>
<script setup>
// 无需脚本逻辑
import { ref } from 'vue'
const selected = ref('GET')
const methods = [
{ name: 'GET', desc: '获取数据', example: 'GET /users', color: 'green' },
{ name: 'POST', desc: '创建数据', example: 'POST /users', color: 'blue' },
{ name: 'PUT', desc: '替换数据', example: 'PUT /users/1', color: 'orange' },
{
name: 'PATCH',
desc: '部分修改',
example: 'PATCH /users/1',
color: 'yellow'
},
{ name: 'DELETE', desc: '删除数据', example: 'DELETE /users/1', color: 'red' }
]
</script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 16px 0;
margin: 1rem 0;
font-size: 0.85rem;
}
.title {
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
}
.icon {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.title {
font-weight: 600;
font-size: 0.9rem;
}
.subtitle {
color: var(--vp-c-text-2);
margin-bottom: 24px;
.demo-layout {
display: flex;
}
.methods-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
flex: 1;
display: flex;
gap: 8px;
padding: 12px;
overflow-x: auto;
}
.right-panel {
width: 200px;
padding: 12px;
background: var(--vp-c-bg);
border-left: 1px solid var(--vp-c-divider);
}
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.methods-grid {
flex-wrap: wrap;
}
.right-panel {
width: 100%;
border-left: none;
border-top: 1px solid var(--vp-c-divider);
}
}
.method-card {
flex: 1;
min-width: 100px;
padding: 10px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
position: relative;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.method-card.get {
border-color: #3b82f6;
}
.method-card.post {
border-color: #22c55e;
}
.method-card.put {
border-color: #f59e0b;
}
.method-card.patch {
border-color: #8b5cf6;
}
.method-card.delete {
border-color: #ef4444;
}
.method-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 6px;
font-weight: bold;
font-size: 12px;
color: white;
}
.method-card.get .method-badge {
background: #3b82f6;
}
.method-card.post .method-badge {
background: #22c55e;
}
.method-card.put .method-badge {
background: #f59e0b;
}
.method-card.patch .method-badge {
background: #8b5cf6;
}
.method-card.delete .method-badge {
background: #ef4444;
}
.method-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
padding-right: 60px;
color: var(--vp-c-text-1);
}
.method-desc p {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
}
.method-examples {
.method-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
padding: 12px;
border-radius: 6px;
margin-bottom: 12px;
}
.example-item {
font-size: 13px;
padding: 4px 0;
color: var(--vp-c-text-1);
.m-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
margin-bottom: 6px;
}
.method-tip {
padding: 10px 12px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
.m-badge.green {
background: #dcfce7;
color: #166534;
}
.method-card.get .method-tip {
background: #eff6ff;
.m-badge.blue {
background: #dbeafe;
color: #1e40af;
}
.method-card.post .method-tip,
.method-card.put .method-tip,
.method-card.patch .method-tip,
.method-card.delete .method-tip {
background: #fef2f2;
.m-badge.orange {
background: #ffedd5;
color: #9a3412;
}
.m-badge.yellow {
background: #fef9c3;
color: #854d0e;
}
.m-badge.red {
background: #fee2e2;
color: #991b1b;
}
.comparison-table {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
.m-desc {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 4px;
}
.table-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
color: var(--vp-c-text-1);
.m-example {
font-family: monospace;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: var(--vp-c-bg-soft);
}
th {
padding: 12px;
text-align: left;
font-weight: bold;
font-size: 13px;
color: var(--vp-c-text-1);
border-bottom: 2px solid var(--vp-c-divider);
}
td {
padding: 12px;
font-size: 13px;
border-bottom: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-1);
}
tr:last-child td {
border-bottom: none;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-weight: bold;
font-size: 12px;
color: white;
}
.badge.get {
background: #3b82f6;
}
.badge.post {
background: #22c55e;
}
.badge.put {
background: #f59e0b;
}
.badge.patch {
background: #8b5cf6;
}
.badge.delete {
background: #ef4444;
}
.tips {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 6px;
font-size: 14px;
line-height: 1.8;
.compare-header {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 8px;
color: var(--vp-c-text-2);
}
.tips p {
margin-bottom: 8px;
.compare-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
margin-bottom: 6px;
font-size: 0.75rem;
}
.tips ul {
margin: 0;
padding-left: 20px;
.compare-row.warn {
background: #fef2f2;
}
.tips li {
margin: 4px 0;
.c-label {
font-weight: bold;
min-width: 50px;
}
.c-val {
color: var(--vp-c-text-2);
}
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,57 +1,40 @@
<!--
ApiPlayground.vue - 紧凑版
目标让用户动手尝试 API 调用
-->
<template>
<div class="api-playground">
<div class="header">
<div class="title">
🧪 API 练手场
</div>
<div class="subtitle">
随便玩坏了算我的
</div>
<div class="demo-root">
<div class="demo-header">
<span class="icon">🧪</span>
<span class="title">API 练手场</span>
<span class="subtitle">随便玩坏了算我的</span>
</div>
<div class="playground-layout">
<div class="demo-layout">
<div class="left-panel">
<div class="panel-title">
发送请求
</div>
<div class="input-group">
<label>Endpoint网址</label>
<div class="input-row">
<label>Endpoint</label>
<input
v-model="endpoint"
type="text"
placeholder="/users/123"
placeholder="/users"
class="input"
>
</div>
<div class="input-group">
<div class="input-row">
<label>方法</label>
<div class="method-buttons">
<div class="method-btns">
<button
v-for="m in methods"
:key="m"
:class="['method-btn', { active: method === m }]"
:class="['m-btn', { active: method === m }]"
@click="method = m"
>
{{ m }}
</button>
</div>
</div>
<div
v-if="method === 'POST'"
class="input-group"
>
<label>BodyJSON</label>
<textarea
v-model="body"
class="textarea"
placeholder="{&quot;name&quot;: &quot;张三&quot;}"
/>
</div>
<div class="input-group">
<div class="input-row">
<label>API Key</label>
<input
v-model="apiKey"
@@ -60,48 +43,36 @@
class="input"
>
</div>
<button
class="send-btn"
:disabled="loading"
@click="sendRequest"
>
{{ loading ? '发送中...' : '🚀 发送请求' }}
{{ loading ? '发送中...' : '🚀 发送' }}
</button>
</div>
<div class="right-panel">
<div class="panel-title">
响应结果
</div>
<div
v-if="!response"
class="empty-state"
class="empty"
>
<span class="empty-icon">📭</span>
<p>点击发送按钮看看会发生什么</p>
<p class="hint">
可以试试输入错误的地址或 Key
</p>
点击发送查看结果
</div>
<div
v-else
class="response-content"
class="response"
>
<div
class="status-bar"
:class="getStatusClass(response.status)"
>
<span class="status-code">{{ response.status }}</span>
<span class="status-text">{{ response.statusText }}</span>
<span class="code">{{ response.status }}</span>
<span class="text">{{ response.statusText }}</span>
</div>
<div class="response-body">
<div class="body">
<pre>{{ JSON.stringify(response.data, null, 2) }}</pre>
</div>
<div
v-if="response.explanation"
class="explanation"
@@ -112,30 +83,20 @@
</div>
</div>
<div class="tips">
<div class="tip-title">
可以试试这些玩法
</div>
<div class="tip-list">
<button @click="tryEndpoint('/users')">
GET /users
</button>
<button @click="tryEndpoint('/users/123')">
GET /users/123
</button>
<button @click="tryEndpoint('/posts')">
GET /posts
</button>
<button @click="tryError401">
401 没带 Key
</button>
<button @click="tryError404">
404 地址错了
</button>
<button @click="tryError429">
429 点太快了
</button>
</div>
<div class="quick-actions">
<span class="label">快速尝试</span>
<button @click="tryEndpoint('/users')">
GET /users
</button>
<button @click="tryError401">
401
</button>
<button @click="tryError404">
404
</button>
<button @click="tryError429">
429
</button>
</div>
</div>
</template>
@@ -145,41 +106,16 @@ import { ref } from 'vue'
const endpoint = ref('/users')
const method = ref('GET')
const methods = ['GET', 'POST']
const body = ref('{\n "name": "张三",\n "age": 25\n}')
const apiKey = ref('')
const apiKey = ref('sk-demo-key')
const loading = ref(false)
const response = ref(null)
function tryEndpoint(path) {
endpoint.value = path
method.value = 'GET'
apiKey.value = 'sk-test123'
}
function tryError401() {
endpoint.value = '/users'
method.value = 'GET'
apiKey.value = ''
}
function tryError404() {
endpoint.value = '/unknown-path'
method.value = 'GET'
apiKey.value = 'sk-test123'
}
function tryError429() {
endpoint.value = '/users'
method.value = 'GET'
apiKey.value = 'sk-test123'
}
const methods = ['GET', 'POST', 'PUT', 'DELETE']
function getStatusClass(status) {
if (status >= 200 && status < 300) return 'success'
if (status >= 400 && status < 500) return 'client-error'
if (status >= 500) return 'server-error'
return ''
return 'server-error'
}
function sendRequest() {
@@ -187,340 +123,256 @@ function sendRequest() {
response.value = null
setTimeout(() => {
loading.value = false
if (!apiKey.value) {
response.value = {
status: 401,
statusText: 'Unauthorized',
data: { error: 'Invalid API key' },
explanation: '没带 API Key,等于没带钱就想吃饭,被拒绝了'
data: { error: '缺少 API Key' },
explanation: '服务器不认识你,需要提供有效的身份证明'
}
return
}
if (endpoint.value === '/users' && method.value === 'GET') {
} else if (endpoint.value === '/users') {
response.value = {
status: 200,
statusText: 'OK',
data: {
users: [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' },
{ id: 3, name: '王五', email: 'wangwu@example.com' }
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
],
total: 3
total: 2
},
explanation: '成功!服务器返回了用户列表'
}
} else if (endpoint.value === '/users/123' && method.value === 'GET') {
response.value = {
status: 200,
statusText: 'OK',
data: { id: 123, name: '张三', email: 'zhangsan@example.com' },
explanation: '找到了!服务器返回了单个用户信息'
}
} else if (endpoint.value === '/posts' && method.value === 'GET') {
response.value = {
status: 200,
statusText: 'OK',
data: {
posts: [
{ id: 1, title: '学习 API 的第一天', author: '张三' },
{ id: 2, title: 'API 原来这么简单', author: '李四' }
]
},
explanation: '成功了!服务器返回了文章列表'
}
} else if (endpoint.value === '/posts' && method.value === 'POST') {
response.value = {
status: 201,
statusText: 'Created',
data: {
id: 3,
title: '学习 API 的第一天',
author: '张三',
created_at: '2025-01-15T10:30:00Z'
},
explanation: '新建成功了!服务器返回了新创建的帖子'
}
} else if (endpoint.value === '/unknown-path') {
response.value = {
status: 404,
statusText: 'Not Found',
data: { error: 'Resource not found' },
explanation: '地址错了,这个接口不存在'
}
} else if (endpoint.value === '/users' && method.value === 'DELETE') {
response.value = {
status: 429,
statusText: 'Too Many Requests',
data: { error: 'Rate limit exceeded' },
explanation: '点太快了!1 秒内只能请求 5 次,你超了'
explanation: '成功!服务器返回了用户列表'
}
} else {
response.value = {
status: 404,
statusText: 'Not Found',
data: { error: 'Endpoint not found' },
explanation: '这个地址不存在,换一个试试?'
data: { error: '接口不存在' },
explanation: '这个地址没有对应的 API,检查一下路径'
}
}
}, 500)
loading.value = false
}, 300)
}
function tryEndpoint(ep) {
endpoint.value = ep
method.value = 'GET'
sendRequest()
}
function tryError401() {
apiKey.value = ''
sendRequest()
}
function tryError404() {
endpoint.value = '/not-exist'
sendRequest()
}
function tryError429() {
loading.value = true
response.value = null
setTimeout(() => {
response.value = {
status: 429,
statusText: 'Too Many Requests',
data: { error: '请求太频繁' },
explanation: '你请求太快了,服务器让你歇会儿'
}
loading.value = false
}, 300)
}
</script>
<style scoped>
.api-playground {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 24px 0;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.playground-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.playground-layout {
grid-template-columns: 1fr;
}
}
.left-panel,
.right-panel {
background: var(--vp-c-bg);
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.panel-title {
font-weight: 600;
margin-bottom: 16px;
color: var(--vp-c-text-1);
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
}
.input-group {
margin-bottom: 12px;
.icon { font-size: 18px; }
.title { font-weight: 600; font-size: 0.9rem; }
.subtitle { font-size: 0.75rem; color: var(--vp-c-text-3); margin-left: auto; }
.demo-layout {
display: flex;
}
.input-group label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
.left-panel {
width: 240px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
border-right: 1px solid var(--vp-c-divider);
}
.right-panel {
flex: 1;
padding: 12px;
background: var(--vp-c-bg);
min-height: 180px;
}
@media (max-width: 640px) {
.demo-layout { flex-direction: column; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid var(--vp-c-divider); }
}
.input-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.input-row label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.input {
width: 100%;
padding: 10px;
padding: 8px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 14px;
font-family: monospace;
background: var(--vp-c-bg-soft);
font-size: 0.85rem;
background: var(--vp-c-bg);
}
.input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.textarea {
width: 100%;
height: 80px;
padding: 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 13px;
font-family: monospace;
background: var(--vp-c-bg-soft);
resize: vertical;
}
.method-buttons {
.method-btns {
display: flex;
gap: 8px;
gap: 4px;
}
.method-btn {
padding: 6px 16px;
.m-btn {
flex: 1;
padding: 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-size: 13px;
font-weight: 600;
border-radius: 4px;
background: transparent;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.method-btn.active {
.m-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.send-btn {
width: 100%;
padding: 12px;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
transition: opacity 0.2s;
}
.send-btn:disabled {
opacity: 0.7;
opacity: 0.6;
cursor: not-allowed;
}
.empty-state {
text-align: center;
padding: 40px 20px;
.empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-3);
font-style: italic;
}
.empty-icon {
font-size: 48px;
display: block;
margin-bottom: 12px;
opacity: 0.5;
}
.hint {
font-size: 12px;
margin-top: 8px;
opacity: 0.7;
}
.response-content {
animation: fadeIn 0.3s ease;
.response {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
}
.status-bar {
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 12px;
display: flex;
gap: 8px;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 4px;
font-size: 0.8rem;
}
.status-bar.success {
background: #dcfce7;
color: #166534;
}
.status-bar.success { background: #dcfce7; }
.status-bar.success .code { color: #166534; }
.status-bar.client-error { background: #fee2e2; }
.status-bar.client-error .code { color: #991b1b; }
.status-bar.server-error { background: #fef3c7; }
.status-bar.server-error .code { color: #92400e; }
.status-bar.client-error {
background: #fee2e2;
color: #991b1b;
}
.code { font-weight: bold; }
.text { color: var(--vp-c-text-2); }
.status-bar.server-error {
background: #fecaca;
color: #7f1d1d;
}
.status-code {
font-weight: 700;
font-family: monospace;
}
.response-body {
.body {
flex: 1;
background: #1e293b;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
overflow-x: auto;
max-height: 180px;
padding: 8px;
overflow: auto;
}
.response-body pre {
.body pre {
margin: 0;
font-family: monospace;
font-size: 12px;
font-size: 0.7rem;
color: #e2e8f0;
white-space: pre-wrap;
}
.explanation {
background: var(--vp-c-bg-soft);
padding: 12px;
border-radius: 6px;
font-size: 13px;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand);
padding: 8px;
background: #fef3c7;
border-radius: 4px;
font-size: 0.8rem;
color: #92400e;
}
.tips {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--vp-c-divider);
}
.tip-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
color: var(--vp-c-text-2);
}
.tip-list {
.quick-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
flex-wrap: wrap;
}
.tip-list button {
padding: 6px 12px;
.quick-actions .label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.quick-actions button {
padding: 4px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 12px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.tip-list button:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
.quick-actions button:hover {
background: var(--vp-c-bg-soft);
}
</style>
@@ -1,61 +1,98 @@
<!--
ApiQuickStartDemo.vue - 演示
目标展示最简单的 API 调用流程
ApiQuickStartDemo.vue - 紧凑
目标展示最简单的 API 调用流程一眼看懂
-->
<template>
<div class="demo">
<div class="header">
<div class="demo-root">
<div class="demo-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>
<div class="demo-layout">
<div class="left-panel">
<div class="terminal">
<div class="term-bar">
<span class="dot r" /><span class="dot y" /><span class="dot g" />
<span class="term-title">API 请求</span>
</div>
<div class="term-body">
<div class="t-line">
<span class="t-ps">&gt; </span>
<span class="t-cmd">GET /api/time</span>
</div>
<div v-if="calling" class="t-line">
<span class="t-dim">请求中...</span>
<span class="t-loading"></span>
</div>
<div v-if="result" class="t-line">
<span class="t-grn">HTTP/1.1 200 OK</span>
</div>
<div v-if="result" class="t-line">
<span class="t-dim">{ "time": "{{ result.timeString }}" }</span>
</div>
</div>
</div>
<button class="call-btn" :disabled="calling" @click="callApi">
{{ calling ? '请求中...' : '📡 发起请求' }}
</button>
</div>
<div
v-if="result || calling"
class="result-area"
>
<div
v-if="calling"
class="loading-dots"
>
<span>.</span><span>.</span><span>.</span>
</div>
<div
v-else-if="result"
class="response-card"
>
<div class="response-header">
<span class="status-badge success">200 OK</span>
<span class="time">耗时: {{ result.time }}ms</span>
<div class="right-panel">
<div class="flow-col" :class="{ 'flow-highlight': stage === 'client' }">
<div class="flow-header">
<span class="flow-icon">💻</span>
<span class="flow-title">客户端</span>
</div>
<div class="response-body">
<div class="time-display">
{{ result.timeString }}
</div>
<div class="timezone">
{{ result.timezone }}
</div>
<div class="flow-body">
{{ stage === 'client' ? '准备请求...' : '等待中' }}
</div>
</div>
<div class="flow-arrow" :class="{ 'arrow-lit': stage === 'request' }">
<span class="arrow-symbol"></span>
<code class="arrow-label">Request</code>
</div>
<div class="flow-col" :class="{ 'flow-highlight': stage === 'server' }">
<div class="flow-header">
<span class="flow-icon">🖥</span>
<span class="flow-title">服务器</span>
</div>
<div class="flow-body">
{{ stage === 'server' ? '处理中...' : '等待中' }}
</div>
</div>
<div class="flow-arrow" :class="{ 'arrow-lit': stage === 'response' }">
<code class="arrow-label">Response</code>
<span class="arrow-symbol"></span>
</div>
<div
class="flow-col"
:class="{ 'flow-highlight': stage === 'response' }"
>
<div class="flow-header">
<span class="flow-icon">📦</span>
<span class="flow-title">响应</span>
</div>
<div class="flow-body">
<span v-if="result" class="result-time">{{
result.timeString
}}</span>
<span v-else>等待响应</span>
</div>
</div>
</div>
</div>
<div class="footer">
<p>
👆 <strong>流程演示</strong> 点击按钮 -> 发送请求 -> 服务器处理 ->
返回数据
</p>
<div class="info-box">
<strong>核心思想</strong>
<span
>点击按钮 发送请求 服务器处理 返回数据这就是 API
调用的完整流程</span
>
</div>
</div>
</template>
@@ -65,180 +102,257 @@ import { ref } from 'vue'
const calling = ref(false)
const result = ref(null)
const stage = ref(null)
function callApi() {
calling.value = true
result.value = null
const startTime = Date.now()
stage.value = 'client'
setTimeout(() => {
stage.value = 'request'
}, 150)
setTimeout(() => {
stage.value = 'server'
}, 300)
setTimeout(() => {
stage.value = 'response'
}, 450)
setTimeout(() => {
const now = new Date()
const timeString = now.toLocaleString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
result.value = {
time: Date.now() - startTime,
timeString: `🕐 ${timeString}`,
timezone: '亚洲/上海 (UTC+8)'
timeString: now.toLocaleString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
calling.value = false
}, 300 + Math.random() * 200)
}, 500)
}
</script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
padding: 16px 20px;
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
}
.icon {
font-size: 24px;
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 16px;
font-size: 0.9rem;
}
.content {
padding: 24px;
.demo-layout {
display: flex;
align-items: stretch;
}
.left-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 200px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 0;
background: var(--vp-c-bg);
}
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.left-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.flow-arrow {
display: none;
}
}
.terminal {
background: #141420;
}
.term-bar {
display: flex;
align-items: center;
gap: 20px;
min-height: 160px;
justify-content: center;
gap: 5px;
padding: 6px 10px;
background: #1e1e2e;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.term-title {
margin-left: 6px;
font-size: 0.7rem;
color: #666;
font-family: monospace;
}
.term-body {
min-height: 80px;
padding: 10px 12px;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.75rem;
line-height: 1.6;
color: #cdd6f4;
}
.t-line {
margin-bottom: 4px;
}
.t-ps {
color: #89b4fa;
}
.t-cmd {
color: #cdd6f4;
}
.t-dim {
color: #585b70;
}
.t-grn {
color: #a6e3a1;
}
.t-loading {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.call-btn {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
margin: 10px;
padding: 8px 16px;
background: var(--vp-c-brand);
color: white;
border: none;
padding: 12px 32px;
font-size: 16px;
font-weight: 600;
border-radius: 50px;
cursor: pointer;
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) {
transform: translateY(-2px);
box-shadow: 0 6px 8px -1px rgba(59, 130, 246, 0.6);
}
.call-btn:disabled {
opacity: 0.7;
cursor: wait;
transform: scale(0.98);
}
.result-area {
width: 100%;
max-width: 400px;
}
.response-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 16px;
animation: slideUp 0.3s ease-out;
}
.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;
padding: 2px 8px;
border-radius: 4px;
font-weight: bold;
}
.response-body {
text-align: center;
}
.time-display {
font-size: 24px;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
cursor: pointer;
}
.call-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.timezone {
font-size: 13px;
.flow-col {
border: 1.5px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
transition:
border-color 0.2s,
box-shadow 0.2s;
}
.flow-col.flow-highlight {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--vp-c-brand) 12%, transparent);
}
.flow-header {
padding: 4px 8px;
background: var(--vp-c-bg-alt);
display: flex;
align-items: center;
gap: 4px;
}
.flow-icon {
font-size: 0.8rem;
}
.flow-title {
font-weight: 600;
font-size: 0.75rem;
}
.flow-body {
padding: 6px 8px;
font-size: 0.72rem;
color: var(--vp-c-text-2);
}
.loading-dots span {
animation: blink 1.4s infinite both;
font-size: 24px;
margin: 0 2px;
.result-time {
color: var(--vp-c-brand);
font-weight: 600;
}
.loading-dots span:nth-child(2) {
animation-delay: 0.2s;
.flow-arrow {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 2px 0;
opacity: 0.3;
transition: opacity 0.2s;
}
.loading-dots span:nth-child(3) {
animation-delay: 0.4s;
.flow-arrow.arrow-lit {
opacity: 1;
}
.arrow-symbol {
font-size: 0.8rem;
color: var(--vp-c-brand);
}
.arrow-label {
font-size: 0.6rem;
font-family: monospace;
color: var(--vp-c-brand);
}
.footer {
padding: 12px 20px;
background: rgba(0, 0, 0, 0.02);
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 13px;
font-size: 0.8rem;
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;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,108 +1,62 @@
<!--
FunctionApiDemo.vue - 紧凑版
目标展示函数就是最基础的 API
-->
<template>
<div class="function-api-demo">
<div class="header">
<div class="title">
🔧 你早就在用 API
</div>
<div class="subtitle">
函数就是最基础的 API
</div>
<div class="demo-root">
<div class="demo-header">
<span class="icon">🔧</span>
<span class="title">函数就是最基础的 API</span>
</div>
<div class="demo-container">
<div class="left">
<div class="code-panel">
<div class="code-title">
📝 代码
</div>
<pre><code><span class="keyword">def</span> <span class="function">greet</span>(name, greeting=<span class="string">"你好"</span>):
<span class="keyword">return</span> f<span class="string">"{greeting}{name}"</span>
<div class="demo-layout">
<div class="code-panel">
<div class="code-title">📝 Python 代码</div>
<pre><code><span class="keyword">def</span> <span class="func">greet</span>(name, greeting=<span class="str">"你好"</span>):
<span class="keyword">return</span> <span class="str">f"{greeting}{name}"</span>
<span class="comment"># 调用这个函数</span>
result = <span class="function">greet</span>(<span class="string">"张三"</span>)
print(result)</code></pre>
</div>
result = <span class="func">greet</span>(<span class="str">"张三"</span>)</code></pre>
</div>
<div class="right">
<div class="explanation">
<p>这个 <code>greet()</code> 函数就是一个 API</p>
<div class="point">
<span class="icon">📦</span>
<div>
<strong>输入参数</strong>
<p>你传进去什么<code>"张三"</code></p>
</div>
<div class="right-panel">
<div class="api-structure">
<div class="structure-item">
<span class="label">📦 输入参数</span>
<code class="value">name="张三"</code>
</div>
<div class="point">
<span class="icon"></span>
<div>
<strong>处理</strong>
<p>函数内部帮你做了拼接字符串的操作</p>
</div>
<div class="structure-item">
<span class="label"> 处理</span>
<span class="value">函数内部拼接字符串</span>
</div>
<div class="point">
<span class="icon">📤</span>
<div>
<strong>输出返回值</strong>
<p>得到什么<code>"你好,张三!"</code></p>
</div>
<div class="structure-item">
<span class="label">📤 输出返回</span>
<code class="value highlight">"你好,张三!"</code>
</div>
</div>
<div class="try-it">
<div class="try-title">
🎮 试试调用
</div>
<div class="interactive">
<input
v-model="name"
placeholder="输入名字"
class="name-input"
>
<select
v-model="greeting"
class="greeting-select"
>
<option value="你好">
你好
</option>
<option value="Hello">
Hello
</option>
<option value="早上好">
早上好
</option>
<option value="晚安">
晚安
</option>
<div class="try-area">
<div class="try-row">
<input v-model="name" placeholder="名字" class="input" />
<select v-model="greeting" class="select">
<option value="你好">你好</option>
<option value="Hello">Hello</option>
<option value="早上好">早上好</option>
</select>
<button
class="call-btn"
@click="callFunction"
>
调用 greet()
</button>
<button class="btn" @click="callFunction">调用</button>
</div>
<div
v-if="result"
class="result"
>
<span class="arrow"></span>
<code>{{ result }}</code>
<div v-if="result" class="result">
<code>{{ result }}</code>
</div>
</div>
</div>
</div>
<div class="summary">
<div class="summary-item">
<span class="icon">🔑</span>
<p><strong>关键点</strong> 你不需要知道函数内部怎么实现只需要知道怎么调用它</p>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span
>你不需要知道函数内部怎么实现只需要知道怎么调用它这就是 API
的本质</span
>
</div>
</div>
</template>
@@ -114,192 +68,194 @@ const name = ref('张三')
const greeting = ref('你好')
const result = ref('')
function greet(name, greeting) {
return `${greeting}${name}`
}
function callFunction() {
result.value = greet(name.value, greeting.value)
result.value = `${greeting.value}${name.value}`
}
</script>
<style scoped>
.function-api-demo {
background: var(--vp-c-bg-soft);
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
text-align: center;
margin-bottom: 20px;
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
}
.title {
.icon {
font-size: 18px;
font-weight: 700;
margin-bottom: 4px;
}
.title {
font-weight: 600;
font-size: 0.9rem;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.demo-container {
grid-template-columns: 1fr;
}
.demo-layout {
display: flex;
gap: 0;
}
.code-panel {
flex: 1;
background: #1e293b;
border-radius: 10px;
padding: 16px;
font-family: monospace;
font-size: 14px;
padding: 12px 14px;
border-right: 1px solid var(--vp-c-divider);
}
.code-title {
color: #94a3b8;
margin-bottom: 12px;
font-size: 13px;
font-size: 0.75rem;
margin-bottom: 8px;
}
.code-panel pre {
margin: 0;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.75rem;
line-height: 1.6;
}
.code-panel code {
color: #e2e8f0;
line-height: 1.8;
}
.keyword {
color: #c084fc;
}
.function {
.func {
color: #60a5fa;
}
.string {
.str {
color: #4ade80;
}
.comment {
color: #64748b;
}
.explanation {
margin-bottom: 20px;
}
.explanation > p {
margin-bottom: 16px;
}
.point {
.right-panel {
width: 260px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 12px;
align-items: flex-start;
background: var(--vp-c-bg);
}
.point .icon {
font-size: 20px;
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.code-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 100%;
}
}
.point strong {
color: var(--vp-c-text-1);
.api-structure {
display: flex;
flex-direction: column;
gap: 6px;
}
.point p {
margin: 4px 0 0;
font-size: 13px;
.structure-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.75rem;
}
.structure-item .label {
color: var(--vp-c-text-2);
}
.try-it {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
.structure-item .value {
font-family: monospace;
font-size: 0.72rem;
}
.try-title {
font-weight: 600;
margin-bottom: 12px;
.structure-item .highlight {
background: #dcfce7;
color: #166534;
padding: 2px 6px;
border-radius: 3px;
}
.interactive {
.try-area {
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.try-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
gap: 6px;
}
.name-input {
.input {
flex: 1;
min-width: 120px;
padding: 8px 12px;
padding: 6px 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 14px;
}
.greeting-select {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 14px;
border-radius: 4px;
font-size: 0.8rem;
background: var(--vp-c-bg);
}
.call-btn {
padding: 8px 16px;
.select {
padding: 6px 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.8rem;
background: var(--vp-c-bg);
}
.btn {
padding: 6px 12px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
}
.result {
margin-top: 16px;
padding: 12px;
margin-top: 8px;
padding: 6px 8px;
background: #dcfce7;
border-radius: 6px;
font-size: 14px;
}
.result .arrow {
margin-right: 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.result code {
color: #166534;
font-weight: 600;
}
.summary {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--vp-c-divider);
}
.summary-item {
.info-box {
display: flex;
gap: 12px;
align-items: center;
font-size: 14px;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.summary-item .icon {
font-size: 20px;
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,256 +1,71 @@
<!--
RealWorldApiDemo.vue
目标展示真实场景中调用 AI 服务的两种方式
RealWorldApiDemo.vue - 紧凑版
目标对比 HTTP 调用 SDK 调用
-->
<template>
<div class="demo">
<div class="title">
🤖 真实场景 AI 帮你写产品文案
</div>
<p class="subtitle">
体验两种调用方式的区别
</p>
<div class="scenario">
<div class="scenario-header">
<span class="scenario-icon">📝</span>
<span class="scenario-title">你的需求</span>
</div>
<div class="scenario-body">
我想让 AI 帮智能手表写一段吸引人的产品文案
</div>
<div class="demo-root">
<div class="demo-header">
<span class="icon"></span>
<span class="title">HTTP vs SDK自己跑腿还是让管家代办</span>
</div>
<div class="modes">
<div class="mode-tabs">
<div class="demo-layout">
<div class="tabs">
<button
:class="['tab', { active: mode === 'http' }]"
@click="mode = 'http'"
>
🌐 HTTP API外卖模式
HTTP API
</button>
<button
:class="['tab', { active: mode === 'sdk' }]"
@click="mode = 'sdk'"
>
📦 SDK堂食模式
SDK
</button>
</div>
<div class="mode-content">
<!-- HTTP 模式 -->
<div
v-if="mode === 'http'"
class="mode-details"
>
<div class="steps">
<div
class="step"
:class="{ active: currentStep >= 1 }"
>
<div class="step-number">
1
</div>
<div class="step-content">
<div class="step-title">
找到网址打开外卖 APP
</div>
<div class="step-code">
https://api.openai.com/v1/chat/completions
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 2 }"
>
<div class="step-number">
2
</div>
<div class="step-content">
<div class="step-title">
准备订单填写信息
</div>
<div class="step-code">
Authorization: Bearer 你的API密钥<br>
Content-Type: application/json
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 3 }"
>
<div class="step-number">
3
</div>
<div class="step-content">
<div class="step-title">
下单发送请求
</div>
<div class="step-code">
{<br>
&nbsp;&nbsp;"model": "gpt-4",<br>
&nbsp;&nbsp;"messages": [<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ "role": "system", "content":
"你是营销文案专家" },<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ "role": "user", "content":
"写智能手表文案" }<br>
&nbsp;&nbsp;]<br>
}
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 4 }"
>
<div class="step-number">
4
</div>
<div class="step-content">
<div class="step-title">
等待配送解析响应
</div>
<div class="step-code">
response.choices[0].message.content<br>
<span class="step-hint"> 需要自己处理解析错误</span>
</div>
</div>
</div>
</div>
<div class="summary">
<p><strong>💡 HTTP API 特点</strong></p>
<ul>
<li> 灵活任何语言都能用</li>
<li> 复杂要手动处理很多细节</li>
<li> 容易出错鉴权数据格式错误处理都要自己写</li>
</ul>
</div>
<div class="code-area">
<div class="code-header">
<span>{{
mode === 'http' ? '自己处理所有细节' : '管家帮你处理'
}}</span>
</div>
<pre
class="code"
><code>{{ mode === 'http' ? httpCode : sdkCode }}</code></pre>
</div>
<!-- SDK 模式 -->
<div
v-else
class="mode-details"
>
<div class="steps">
<div
class="step"
:class="{ active: currentStep >= 1 }"
>
<div class="step-number">
1
</div>
<div class="step-content">
<div class="step-title">
走进餐厅安装 SDK
</div>
<div class="step-code">
import OpenAI from 'openai'
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 2 }"
>
<div class="step-number">
2
</div>
<div class="step-content">
<div class="step-title">
找服务员初始化客户端
</div>
<div class="step-code">
const client = new OpenAI({<br>
&nbsp;&nbsp;apiKey: '你的密钥'<br>
})
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 3 }"
>
<div class="step-number">
3
</div>
<div class="step-content">
<div class="step-title">
直接点菜调用函数
</div>
<div class="step-code">
const response = await client.chat.completions.create({<br>
&nbsp;&nbsp;model: 'gpt-4',<br>
&nbsp;&nbsp;messages: [<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ role: 'system', content:
'你是营销文案专家' },<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ role: 'user', content:
'写智能手表文案' }<br>
&nbsp;&nbsp;]<br>
})
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 4 }"
>
<div class="step-number">
4
</div>
<div class="step-content">
<div class="step-title">
享用美食直接使用
</div>
<div class="step-code">
console.log(response.choices[0].message.content)<br>
<span class="step-hint"> SDK 帮你处理好了所有细节</span>
</div>
</div>
</div>
<div class="compare-panel">
<div class="compare-title">对比</div>
<div class="compare-list">
<div class="compare-item">
<span class="ci-label">代码量</span>
<span class="ci-val">{{ mode === 'http' ? '多' : '少' }}</span>
</div>
<div class="summary">
<p><strong>💡 SDK 特点</strong></p>
<ul>
<li> 简单只管调用函数</li>
<li> 省心SDK 自动处理鉴权错误数据格式</li>
<li> 限制通常只能在特定语言使用</li>
</ul>
<div class="compare-item">
<span class="ci-label">错误处理</span>
<span class="ci-val">{{
mode === 'http' ? '自己写' : '自动处理'
}}</span>
</div>
<div class="compare-item">
<span class="ci-label">重试逻辑</span>
<span class="ci-val">{{
mode === 'http' ? '自己写' : '内置'
}}</span>
</div>
<div class="compare-item">
<span class="ci-label">类型提示</span>
<span class="ci-val">{{ mode === 'http' ? '无' : '有' }}</span>
</div>
</div>
</div>
</div>
<div class="action">
<button
class="run-btn"
:disabled="running"
@click="runDemo"
>
{{ running ? '调用中...' : '🚀 开始调用 AI' }}
</button>
<div
v-if="result"
class="result"
>
<div class="result-header">
{{ mode === 'http' ? '🌐 HTTP API 返回' : '📦 SDK 返回' }}
</div>
<div class="result-body">
"这款智能手表,是你的贴身健康管家。全天候心率监测,运动模式自动识别..."
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span>能用 SDK 就用 SDK把麻烦事留给库把时间留给自己</span>
</div>
</div>
</template>
@@ -258,270 +73,168 @@
<script setup>
import { ref } from 'vue'
const mode = ref('http')
const currentStep = ref(0)
const running = ref(false)
const result = ref(null)
const mode = ref('sdk')
async function runDemo() {
running.value = true
result.value = null
currentStep.value = 0
const httpCode = `import requests
// 模拟逐步执行
for (let i = 1; i <= 4; i++) {
await new Promise((resolve) => setTimeout(resolve, 600))
currentStep.value = i
}
response = requests.post(
"https://api.deepseek.com/v1/chat/completions",
headers={
"Authorization": "Bearer sk-xxx",
"Content-Type": "application/json"
},
json={
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "你好"}]
}
)
await new Promise((resolve) => setTimeout(resolve, 400))
result.value = true
running.value = false
}
if response.status_code == 200:
result = response.json()
content = result["choices"][0]["message"]["content"]
else:
# 处理错误...
pass`
const sdkCode = `from openai import OpenAI
client = OpenAI(
api_key="sk-xxx",
base_url="https://api.deepseek.com"
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "你好"}]
)
content = response.choices[0].message.content`
</script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 16px 0;
margin: 1rem 0;
font-size: 0.85rem;
}
.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;
}
.scenario {
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-brand-1);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
}
.scenario-header {
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.scenario-icon {
font-size: 24px;
.icon {
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 0.9rem;
}
.scenario-title {
font-size: 16px;
font-weight: bold;
color: var(--vp-c-text-1);
}
.scenario-body {
font-size: 15px;
line-height: 1.6;
color: var(--vp-c-text-1);
padding-left: 32px;
}
.modes {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
}
.mode-tabs {
.demo-layout {
display: flex;
border-bottom: 2px solid var(--vp-c-divider);
flex-direction: column;
}
.tabs {
display: flex;
gap: 4px;
padding: 10px 12px;
background: var(--vp-c-bg);
}
.tab {
flex: 1;
padding: 14px 20px;
border: none;
background: var(--vp-c-bg-soft);
font-size: 15px;
font-weight: bold;
padding: 6px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: transparent;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
border-bottom: 3px solid transparent;
}
.tab:hover {
background: var(--vp-c-bg);
}
.tab.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.code-area {
background: #1e293b;
}
.code-header {
padding: 6px 12px;
font-size: 0.75rem;
color: #94a3b8;
border-bottom: 1px solid #334155;
}
.code {
margin: 0;
padding: 12px;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.72rem;
line-height: 1.5;
color: #e2e8f0;
overflow-x: auto;
white-space: pre;
}
.compare-panel {
padding: 12px;
background: var(--vp-c-bg);
border-bottom-color: var(--vp-c-brand-1);
}
.mode-content {
padding: 20px;
.compare-title {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 8px;
}
.steps {
.compare-list {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
gap: 12px;
flex-wrap: wrap;
}
.step {
display: flex;
gap: 16px;
opacity: 0.4;
transition: opacity 0.3s;
}
.step.active {
opacity: 1;
}
.step-number {
flex-shrink: 0;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
.compare-item {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
gap: 6px;
padding: 6px 10px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.75rem;
}
.ci-label {
color: var(--vp-c-text-2);
}
.step.active .step-number {
background: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
color: white;
.ci-val {
font-weight: 600;
}
.step-content {
flex: 1;
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.step-title {
font-weight: bold;
font-size: 14px;
margin-bottom: 6px;
color: var(--vp-c-text-1);
}
.step-code {
background: #1e293b;
color: #e2e8f0;
padding: 12px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.6;
position: relative;
}
.step-hint {
display: block;
margin-top: 8px;
font-size: 11px;
padding: 6px 8px;
border-radius: 4px;
}
.step.active .step-hint {
background: rgba(255, 255, 255, 0.1);
}
.summary {
background: var(--vp-c-bg-soft);
padding: 16px;
border-radius: 6px;
font-size: 14px;
line-height: 1.8;
}
.summary p {
margin-bottom: 8px;
}
.summary ul {
margin: 0;
padding-left: 20px;
}
.summary li {
margin: 4px 0;
}
.action {
padding: 20px;
border-top: 2px solid var(--vp-c-divider);
}
.run-btn {
width: 100%;
padding: 14px 24px;
background: var(--vp-c-brand-1);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.run-btn:hover:not(:disabled) {
opacity: 0.9;
transform: scale(1.02);
}
.run-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.result {
margin-top: 16px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-header {
font-weight: bold;
font-size: 14px;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.result-body {
background: #f0fdf4;
border: 2px solid #86efac;
border-radius: 6px;
padding: 16px;
font-size: 14px;
line-height: 1.6;
color: #166534;
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -4,48 +4,29 @@
-->
<template>
<div class="demo">
<div class="title">
🔄 一次 API 调用的流程
</div>
<p class="subtitle">
点一下按钮看请求怎么飞过去再飞回来
</p>
<div class="title">🔄 一次 API 调用的流程</div>
<p class="subtitle">点一下按钮看请求怎么飞过去再飞回来</p>
<div class="flow-container">
<div class="side you">
<div class="window">
<div class="window-header">
👤 你这边
</div>
<div class="window-header">👤 你这边</div>
<div class="window-body">
<div class="message">
我想调用 API
</div>
<div class="message">我想调用 API</div>
</div>
</div>
</div>
<div class="middle">
<div
class="arrow"
:class="{ animating: isAnimating }"
>
</div>
<button
class="send-btn"
:disabled="isAnimating"
@click="send"
>
<div class="arrow" :class="{ animating: isAnimating }"></div>
<button class="send-btn" :disabled="isAnimating" @click="send">
{{ isAnimating ? '发送中...' : '🚀 发送请求' }}
</button>
</div>
<div class="side server">
<div class="window">
<div class="window-header">
🖥 对方服务器
</div>
<div class="window-header">🖥 对方服务器</div>
<div class="window-body">
<div class="message">
{{ serverMessage }}
@@ -55,14 +36,8 @@
</div>
</div>
<div
v-if="result"
class="result"
>
<div
class="result-box"
:class="result.type"
>
<div v-if="result" class="result">
<div class="result-box" :class="result.type">
{{ result.text }}
</div>
</div>