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:
@@ -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 方法:GET、POST、PUT、DELETE 是什么?
|
||||
<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>Body(JSON)</label>
|
||||
<textarea
|
||||
v-model="body"
|
||||
class="textarea"
|
||||
placeholder="{"name": "张三"}"
|
||||
/>
|
||||
</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">> </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>
|
||||
"model": "gpt-4",<br>
|
||||
"messages": [<br>
|
||||
{ "role": "system", "content":
|
||||
"你是营销文案专家" },<br>
|
||||
{ "role": "user", "content":
|
||||
"写智能手表文案" }<br>
|
||||
]<br>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="step"
|
||||
:class="{ active: currentStep >= 4 }"
|
||||
>
|
||||
<div class="step-number">
|
||||
4
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
等待配送(解析响应)
|
||||
</div>
|
||||
<div class="step-code">
|
||||
response.choices[0].message.content<br>
|
||||
<span class="step-hint">⚠️ 需要自己处理解析错误</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="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>
|
||||
apiKey: '你的密钥'<br>
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="step"
|
||||
:class="{ active: currentStep >= 3 }"
|
||||
>
|
||||
<div class="step-number">
|
||||
3
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
直接点菜(调用函数)
|
||||
</div>
|
||||
<div class="step-code">
|
||||
const response = await client.chat.completions.create({<br>
|
||||
model: 'gpt-4',<br>
|
||||
messages: [<br>
|
||||
{ role: 'system', content:
|
||||
'你是营销文案专家' },<br>
|
||||
{ role: 'user', content:
|
||||
'写智能手表文案' }<br>
|
||||
]<br>
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="step"
|
||||
:class="{ active: currentStep >= 4 }"
|
||||
>
|
||||
<div class="step-number">
|
||||
4
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
享用美食(直接使用)
|
||||
</div>
|
||||
<div class="step-code">
|
||||
console.log(response.choices[0].message.content)<br>
|
||||
<span class="step-hint">✅ SDK 帮你处理好了所有细节</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div 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>
|
||||
|
||||
Reference in New Issue
Block a user