feat: add header image and remove unused demo components
- Add header.png to assets directory - Remove deprecated Vue demo components for API and context engineering - Update README.md to include new header image
This commit is contained in:
@@ -43,12 +43,13 @@
|
||||
<a href="docs-readme/de-DE/README.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
<img src="assets/header.png" width="100%" />
|
||||
|
||||
<p align="center">
|
||||
<h3>⭐ 欢迎点击 <span style="color: #660874;">Star</span> 加速更新 ❤️</h3>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td width="50%" valign="top" align="center">
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 MiB |
@@ -1,395 +0,0 @@
|
||||
<template>
|
||||
<div class="api-concept-demo">
|
||||
<!-- 标题和说明 -->
|
||||
<div class="demo-header">
|
||||
<h3>🍽️ API = 软件世界的"服务员"</h3>
|
||||
<p class="subtitle">点击菜单项,观察 API 如何传递请求</p>
|
||||
</div>
|
||||
|
||||
<!-- 主场景 -->
|
||||
<div class="scene-container">
|
||||
<!-- 顾客区域 -->
|
||||
<div class="customer-zone">
|
||||
<div class="customer-avatar">👤</div>
|
||||
<div class="menu">
|
||||
<h4>菜单</h4>
|
||||
<button
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
@click="orderDish(item)"
|
||||
:disabled="isProcessing"
|
||||
class="menu-item"
|
||||
>
|
||||
{{ item.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API/服务员 -->
|
||||
<div class="api-zone">
|
||||
<div class="waiter" :class="{ 'moving': isProcessing }">
|
||||
<div class="waiter-avatar">🧑💼</div>
|
||||
<div class="api-label">API</div>
|
||||
</div>
|
||||
<div class="request-flow" v-if="currentRequest">
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="request-info">
|
||||
<div>请求: GET /{{ currentRequest }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 厨房/服务器区域 -->
|
||||
<div class="kitchen-zone">
|
||||
<div class="kitchen-avatar">👨🍳</div>
|
||||
<div class="kitchen-label">服务器</div>
|
||||
<div class="status" :class="statusClass">
|
||||
{{ statusText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对比演示 -->
|
||||
<div class="comparison">
|
||||
<button @click="showComparison = !showComparison">
|
||||
{{ showComparison ? '隐藏' : '显示' }}对比:有 API vs 无 API
|
||||
</button>
|
||||
|
||||
<div v-if="showComparison" class="comparison-scene">
|
||||
<div class="with-api">
|
||||
<h4>✅ 有 API(服务员)</h4>
|
||||
<div class="comparison-visual">
|
||||
顾客 → 服务员 → 厨房
|
||||
</div>
|
||||
<p>秩序井然,高效清晰</p>
|
||||
</div>
|
||||
<div class="without-api">
|
||||
<h4>❌ 无 API(直接冲进厨房)</h4>
|
||||
<div class="comparison-visual chaotic">
|
||||
顾客 厨房 👷 厨房 👨🍳
|
||||
</div>
|
||||
<p>混乱不堪,效率低下</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关键点总结 -->
|
||||
<div class="key-points">
|
||||
<h4>💡 关键点</h4>
|
||||
<ul>
|
||||
<li>API 是软件之间的"服务员"</li>
|
||||
<li>调用 API = 向服务员点餐</li>
|
||||
<li>API 返回数据 = 服务员端菜上来</li>
|
||||
<li>有了 API,软件之间可以"对话"</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const isProcessing = ref(false)
|
||||
const currentRequest = ref(null)
|
||||
const statusText = ref('空闲')
|
||||
const showComparison = ref(false)
|
||||
|
||||
const menuItems = [
|
||||
{ id: 1, name: '宫保鸡丁', endpoint: 'dishes/kungpao' },
|
||||
{ id: 2, name: '鱼香肉丝', endpoint: 'dishes/yuxiang' },
|
||||
{ id: 3, name: '麻婆豆腐', endpoint: 'dishes/mapo' }
|
||||
]
|
||||
|
||||
const statusClass = computed(() => {
|
||||
if (isProcessing.value) return 'processing'
|
||||
return 'idle'
|
||||
})
|
||||
|
||||
function orderDish(item) {
|
||||
if (isProcessing.value) return
|
||||
|
||||
currentRequest.value = item.endpoint
|
||||
isProcessing.value = true
|
||||
statusText.value = '处理中...'
|
||||
|
||||
// 模拟 API 调用过程
|
||||
setTimeout(() => {
|
||||
statusText.value = '制作完成'
|
||||
setTimeout(() => {
|
||||
isProcessing.value = false
|
||||
currentRequest.value = null
|
||||
statusText.value = '空闲'
|
||||
}, 1000)
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-concept-demo {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
font-size: 24px;
|
||||
margin: 0 0 8px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scene-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin: 32px 0;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.customer-zone,
|
||||
.kitchen-zone {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.api-zone {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: #fff3cd;
|
||||
border: 2px dashed #ffc107;
|
||||
}
|
||||
|
||||
.customer-avatar,
|
||||
.kitchen-avatar {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.waiter-avatar {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.waiter.moving .waiter-avatar {
|
||||
animation: bounce 0.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.api-label,
|
||||
.kitchen-label {
|
||||
font-weight: bold;
|
||||
color: #856404;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-item:hover:not(:disabled) {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.menu-item:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 12px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status.idle {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status.processing {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.comparison {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comparison button {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.comparison button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.comparison-scene {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.with-api,
|
||||
.without-api {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.with-api {
|
||||
background: #d4edda;
|
||||
}
|
||||
|
||||
.without-api {
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
.comparison-visual {
|
||||
font-size: 24px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.comparison-visual.chaotic {
|
||||
animation: shake 0.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.key-points {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #007bff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.key-points h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.key-points ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.key-points li {
|
||||
margin: 8px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.api-concept-demo {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.scene-container {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.customer-zone,
|
||||
.kitchen-zone {
|
||||
background: #363636;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
background: #2d2d2d;
|
||||
border-color: #555;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.menu-item:hover:not(:disabled) {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.key-points {
|
||||
background: #1e3a5f;
|
||||
border-left-color: #4dabf7;
|
||||
}
|
||||
|
||||
.key-points li {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,830 +0,0 @@
|
||||
<template>
|
||||
<div class="api-document-demo">
|
||||
<div class="demo-header">
|
||||
<h3>📚 API 文档导航</h3>
|
||||
<p>学会像阅读菜单一样阅读 API 文档</p>
|
||||
</div>
|
||||
|
||||
<!-- 文档导航 -->
|
||||
<div class="doc-nav">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
:class="{ active: activeTab === tab.id }"
|
||||
>
|
||||
{{ tab.icon }} {{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 概述标签页 -->
|
||||
<div v-if="activeTab === 'overview'" class="tab-content">
|
||||
<div class="api-overview">
|
||||
<h4>用户管理 API</h4>
|
||||
<p>本 API 提供用户的增删改查功能,支持分页查询和高级过滤。</p>
|
||||
|
||||
<div class="info-cards">
|
||||
<div class="info-card">
|
||||
<div class="card-icon">🔗</div>
|
||||
<div class="card-title">Base URL</div>
|
||||
<div class="card-value"><code>https://api.example.com/v1</code></div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<div class="card-icon">🔑</div>
|
||||
<div class="card-title">认证方式</div>
|
||||
<div class="card-value">API Key 或 Bearer Token</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<div class="card-icon">📊</div>
|
||||
<div class="card-title">数据格式</div>
|
||||
<div class="card-value">JSON</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 接口列表标签页 -->
|
||||
<div v-if="activeTab === 'endpoints'" class="tab-content">
|
||||
<div class="endpoint-list">
|
||||
<div
|
||||
v-for="endpoint in endpoints"
|
||||
:key="endpoint.id"
|
||||
@click="selectedEndpoint = endpoint"
|
||||
:class="['endpoint-item', endpoint.method.toLowerCase(), { active: selectedEndpoint?.id === endpoint.id }]"
|
||||
>
|
||||
<div class="method-badge">{{ endpoint.method }}</div>
|
||||
<div class="endpoint-path">{{ endpoint.path }}</div>
|
||||
<div class="endpoint-desc">{{ endpoint.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 接口详情 -->
|
||||
<div v-if="selectedEndpoint" class="endpoint-detail">
|
||||
<div class="detail-header">
|
||||
<span class="method-badge large" :class="selectedEndpoint.method.toLowerCase()">
|
||||
{{ selectedEndpoint.method }}
|
||||
</span>
|
||||
<span class="endpoint-path">{{ selectedEndpoint.path }}</span>
|
||||
</div>
|
||||
|
||||
<p class="detail-description">{{ selectedEndpoint.description }}</p>
|
||||
|
||||
<!-- 请求参数 -->
|
||||
<div class="params-section">
|
||||
<h5>📥 请求参数</h5>
|
||||
<table class="params-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>参数名</th>
|
||||
<th>类型</th>
|
||||
<th>必填</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="param in selectedEndpoint.params" :key="param.name">
|
||||
<td><code>{{ param.name }}</code></td>
|
||||
<td><span class="type-badge">{{ param.type }}</span></td>
|
||||
<td>{{ param.required ? '✅ 是' : '❌ 否' }}</td>
|
||||
<td>{{ param.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 请求示例 -->
|
||||
<div class="example-section">
|
||||
<h5>📤 请求示例</h5>
|
||||
<div class="code-tabs">
|
||||
<button
|
||||
v-for="lang in ['curl', 'javascript', 'python']"
|
||||
:key="lang"
|
||||
@click="codeLang = lang"
|
||||
:class="{ active: codeLang === lang }"
|
||||
>
|
||||
{{ lang }}
|
||||
</button>
|
||||
</div>
|
||||
<pre class="code-block"><code>{{ getCodeExample() }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- 响应示例 -->
|
||||
<div class="response-section">
|
||||
<h5>📥 响应示例</h5>
|
||||
<pre class="code-block json"><code>{{ JSON.stringify(selectedEndpoint.response, null, 2) }}</code></pre>
|
||||
</div>
|
||||
|
||||
<button @click="tryApi" class="try-btn">🚀 试试这个 API</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据模型标签页 -->
|
||||
<div v-if="activeTab === 'models'" class="tab-content">
|
||||
<div class="models-list">
|
||||
<div v-for="model in models" :key="model.name" class="model-card">
|
||||
<h5>{{ model.name }}</h5>
|
||||
<p>{{ model.description }}</p>
|
||||
<table class="fields-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>字段</th>
|
||||
<th>类型</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="field in model.fields" :key="field.name">
|
||||
<td><code>{{ field.name }}</code></td>
|
||||
<td>{{ field.type }}</td>
|
||||
<td>{{ field.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误码标签页 -->
|
||||
<div v-if="activeTab === 'errors'" class="tab-content">
|
||||
<div class="error-codes">
|
||||
<div v-for="error in errorCodes" :key="error.code" class="error-item">
|
||||
<div class="error-code" :class="getErrorClass(error.code)">
|
||||
{{ error.code }}
|
||||
</div>
|
||||
<div class="error-info">
|
||||
<div class="error-title">{{ error.title }}</div>
|
||||
<div class="error-description">{{ error.description }}</div>
|
||||
<div class="error-solution">💡 {{ error.solution }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API 测试弹窗 -->
|
||||
<div v-if="showTestModal" class="modal-overlay" @click="showTestModal = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h4>🧪 测试 API</h4>
|
||||
<button @click="showTestModal = false" class="close-btn">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="test-config">
|
||||
<label>
|
||||
接口地址:
|
||||
<input v-model="testUrl" readonly />
|
||||
</label>
|
||||
<label v-for="param in testParams" :key="param.name">
|
||||
{{ param.name }}:
|
||||
<input v-model="param.value" :placeholder="param.placeholder" />
|
||||
</label>
|
||||
</div>
|
||||
<button @click="sendTestRequest" class="send-btn" :disabled="testLoading">
|
||||
{{ testLoading ? '发送中...' : '🚀 发送请求' }}
|
||||
</button>
|
||||
|
||||
<div v-if="testResponse" class="test-result">
|
||||
<h5>响应结果:</h5>
|
||||
<pre><code>{{ JSON.stringify(testResponse, null, 2) }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="tips">
|
||||
<h4>💡 阅读技巧</h4>
|
||||
<ul>
|
||||
<li>先看 <strong>概述</strong> 了解 API 的基本信息</li>
|
||||
<li>再看 <strong>接口列表</strong> 找到你需要的功能</li>
|
||||
<li>仔细看 <strong>请求参数</strong>,注意必填项和类型</li>
|
||||
<li>参考 <strong>请求示例</strong>,复制粘贴修改</li>
|
||||
<li>遇到错误查 <strong>错误码</strong> 快速定位问题</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeTab = ref('overview')
|
||||
const selectedEndpoint = ref(null)
|
||||
const codeLang = ref('curl')
|
||||
const showTestModal = ref(false)
|
||||
const testUrl = ref('')
|
||||
const testResponse = ref(null)
|
||||
const testLoading = ref(false)
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: '概述', icon: '📖' },
|
||||
{ id: 'endpoints', label: '接口列表', icon: '🔌' },
|
||||
{ id: 'models', label: '数据模型', icon: '📊' },
|
||||
{ id: 'errors', label: '错误码', icon: '⚠️' }
|
||||
]
|
||||
|
||||
const endpoints = [
|
||||
{
|
||||
id: 1,
|
||||
method: 'GET',
|
||||
path: '/users',
|
||||
description: '获取用户列表(支持分页)',
|
||||
params: [
|
||||
{ name: 'page', type: 'number', required: false, description: '页码,默认 1' },
|
||||
{ name: 'limit', type: 'number', required: false, description: '每页数量,默认 20' },
|
||||
{ name: 'search', type: 'string', required: false, description: '搜索关键词' }
|
||||
],
|
||||
response: {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: [
|
||||
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
|
||||
{ id: 2, name: '李四', email: 'lisi@example.com' }
|
||||
],
|
||||
pagination: { page: 1, limit: 20, total: 100 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
method: 'POST',
|
||||
path: '/users',
|
||||
description: '创建新用户',
|
||||
params: [
|
||||
{ name: 'name', type: 'string', required: true, description: '用户名' },
|
||||
{ name: 'email', type: 'string', required: true, description: '邮箱地址' },
|
||||
{ name: 'password', type: 'string', required: true, description: '密码' }
|
||||
],
|
||||
response: {
|
||||
code: 201,
|
||||
message: 'created',
|
||||
data: { id: 3, name: '王五', email: 'wangwu@example.com', createdAt: '2024-01-15' }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const models = [
|
||||
{
|
||||
name: 'User',
|
||||
description: '用户对象',
|
||||
fields: [
|
||||
{ name: 'id', type: 'number', description: '用户 ID' },
|
||||
{ name: 'name', type: 'string', description: '用户名' },
|
||||
{ name: 'email', type: 'string', description: '邮箱地址' },
|
||||
{ name: 'createdAt', type: 'string', description: '创建时间(ISO 8601)' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const errorCodes = [
|
||||
{ code: 400, title: '参数错误', description: '请求参数格式错误或缺少必填参数', solution: '检查参数名、类型和格式' },
|
||||
{ code: 401, title: '未授权', description: '缺少有效的认证信息', solution: '添加 API Key 或 Bearer Token' },
|
||||
{ code: 404, title: '资源不存在', description: '请求的资源未找到', solution: '检查 URL 路径是否正确' },
|
||||
{ code: 429, title: '请求过于频繁', description: '超过了 API 的速率限制', solution: '降低请求频率或联系提供方' },
|
||||
{ code: 500, title: '服务器错误', description: '服务器内部错误', solution: '稍后重试或联系技术支持' }
|
||||
]
|
||||
|
||||
const testParams = ref([])
|
||||
|
||||
function getCodeExample() {
|
||||
if (!selectedEndpoint.value) return ''
|
||||
|
||||
const ep = selectedEndpoint.value
|
||||
|
||||
if (codeLang.value === 'curl') {
|
||||
return `curl -X ${ep.method} \\
|
||||
https://api.example.com/v1${ep.path} \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
${ep.method === 'POST' ? '-d \'{"name":"张三","email":"zhangsan@example.com"}\'' : ''}`
|
||||
}
|
||||
|
||||
if (codeLang.value === 'javascript') {
|
||||
return `fetch('https://api.example.com/v1${ep.path}', {
|
||||
method: '${ep.method}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer YOUR_API_KEY'
|
||||
}${ep.method === 'POST' ? `,
|
||||
body: JSON.stringify({
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com'
|
||||
})` : ''}
|
||||
}).then(res => res.json())
|
||||
.then(data => console.log(data))`
|
||||
}
|
||||
|
||||
if (codeLang.value === 'python') {
|
||||
return `import requests
|
||||
|
||||
url = 'https://api.example.com/v1${ep.path}'
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer YOUR_API_KEY'
|
||||
}
|
||||
${ep.method === 'POST' ? 'data = {"name": "张三", "email": "zhangsan@example.com"}\n\n' : ''}response = requests.${ep.method.toLowerCase()}(url, headers=headers${ep.method === 'POST' ? ', json=data' : ''})
|
||||
|
||||
print(response.json())`
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function getErrorClass(code) {
|
||||
if (code >= 200 && code < 300) return 'success'
|
||||
if (code >= 300 && code < 400) return 'redirect'
|
||||
if (code >= 400 && code < 500) return 'client-error'
|
||||
return 'server-error'
|
||||
}
|
||||
|
||||
function tryApi() {
|
||||
if (!selectedEndpoint.value) return
|
||||
|
||||
testUrl.value = `https://api.example.com/v1${selectedEndpoint.value.path}`
|
||||
testParams.value = selectedEndpoint.value.params.map(p => ({
|
||||
name: p.name,
|
||||
value: '',
|
||||
placeholder: p.description
|
||||
}))
|
||||
testResponse.value = null
|
||||
showTestModal.value = true
|
||||
}
|
||||
|
||||
function sendTestRequest() {
|
||||
testLoading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
testResponse.value = selectedEndpoint.value.response
|
||||
testLoading.value = false
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-document-demo {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
font-size: 20px;
|
||||
margin: 0 0 8px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.doc-nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.doc-nav button {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.doc-nav button:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.doc-nav button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.api-overview h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.info-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.endpoint-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.endpoint-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.endpoint-item:hover {
|
||||
border-color: #007bff;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.endpoint-item.active {
|
||||
border-color: #007bff;
|
||||
background: #e8f4ff;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.method-badge.large {
|
||||
font-size: 16px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.method-badge.get {
|
||||
background: #61affe;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.method-badge.post {
|
||||
background: #49cc90;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.endpoint-path {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.endpoint-desc {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.endpoint-detail {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.params-section,
|
||||
.example-section,
|
||||
.response-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.params-section h5,
|
||||
.example-section h5,
|
||||
.response-section h5 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.params-table,
|
||||
.fields-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.params-table th,
|
||||
.params-table td,
|
||||
.fields-table th,
|
||||
.fields-table td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.params-table th,
|
||||
.fields-table th {
|
||||
background: #f0f0f0;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.code-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.code-tabs button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.code-tabs button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.code-block.json {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.try-btn {
|
||||
margin-top: 20px;
|
||||
padding: 12px 24px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.try-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.model-card h5 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.model-card p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.error-codes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.error-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
font-size: 18px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-code.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.error-code.client-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.error-code.server-error {
|
||||
background: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.error-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.error-solution {
|
||||
color: #007bff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.modal-header h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-config label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.test-config input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.test-result {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.test-result h5 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.test-result pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #007bff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tips h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.tips ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.tips li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f8f9fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #e83e8c;
|
||||
}
|
||||
</style>
|
||||
@@ -1,615 +0,0 @@
|
||||
<template>
|
||||
<div class="api-method-demo">
|
||||
<div class="demo-header">
|
||||
<h3>🎯 HTTP 方法:四种基本操作</h3>
|
||||
<p>点击不同方法,观察对数据的影响</p>
|
||||
</div>
|
||||
|
||||
<!-- 方法选择器 -->
|
||||
<div class="method-selector">
|
||||
<button
|
||||
v-for="method in methods"
|
||||
:key="method.name"
|
||||
@click="selectMethod(method)"
|
||||
:class="['method-card', method.name.toLowerCase(), { active: selectedMethod?.name === method.name }]"
|
||||
>
|
||||
<div class="method-icon">{{ method.icon }}</div>
|
||||
<div class="method-name">{{ method.name }}</div>
|
||||
<div class="method-label">{{ method.label }}</div>
|
||||
<div class="method-analogy">{{ method.analogy }}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 演示区域 -->
|
||||
<div class="demo-area" v-if="selectedMethod">
|
||||
<!-- 左侧:请求详情 -->
|
||||
<div class="request-panel">
|
||||
<h4>📤 请求</h4>
|
||||
<div class="request-line">
|
||||
<span class="http-method" :class="selectedMethod.name.toLowerCase()">
|
||||
{{ selectedMethod.name }}
|
||||
</span>
|
||||
<span class="url">{{ getMockUrl() }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedMethod.name !== 'GET'" class="request-body">
|
||||
<div class="panel-title">Request Body:</div>
|
||||
<pre><code>{{ getMockBody() }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:响应详情 -->
|
||||
<div class="response-panel">
|
||||
<h4>📥 响应</h4>
|
||||
<div class="status-line">
|
||||
<span class="status-code" :class="getStatusClass()">
|
||||
{{ getMockStatus() }}
|
||||
</span>
|
||||
<span class="status-text">{{ getMockStatusText() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="response-body">
|
||||
<div class="panel-title">Response Body:</div>
|
||||
<pre><code>{{ getMockResponse() }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据展示区 -->
|
||||
<div class="data-view">
|
||||
<h4>👥 用户数据</h4>
|
||||
<div class="user-list">
|
||||
<div v-for="user in users" :key="user.id" class="user-card">
|
||||
<div class="user-avatar">{{ user.avatar }}</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">{{ user.name }}</div>
|
||||
<div class="user-email">{{ user.email }}</div>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<button @click="editUser(user)" class="btn-edit">✏️</button>
|
||||
<button @click="deleteUser(user.id)" class="btn-delete">❌</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-bar">
|
||||
<button @click="addNewUser" class="btn-add">
|
||||
<span>+ POST /users</span>
|
||||
<span class="hint">添加新用户</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 方法对比表 -->
|
||||
<div class="comparison-table">
|
||||
<h4>📊 方法对比</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>方法</th>
|
||||
<th>作用</th>
|
||||
<th>餐厅类比</th>
|
||||
<th>幂等性</th>
|
||||
<th>示例</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="method in methods" :key="method.name">
|
||||
<td><span :class="['badge', method.name.toLowerCase()]">{{ method.name }}</span></td>
|
||||
<td>{{ method.label }}</td>
|
||||
<td>{{ method.analogy }}</td>
|
||||
<td>{{ method.idempotent ? '✅ 是' : '❌ 否' }}</td>
|
||||
<td><code>{{ method.example }}</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selectedMethod = ref(null)
|
||||
|
||||
const methods = [
|
||||
{
|
||||
name: 'GET',
|
||||
label: '获取数据',
|
||||
icon: '📋',
|
||||
analogy: '查看菜单',
|
||||
idempotent: true,
|
||||
example: 'GET /users'
|
||||
},
|
||||
{
|
||||
name: 'POST',
|
||||
label: '创建数据',
|
||||
icon: '🍽️',
|
||||
analogy: '点新菜',
|
||||
idempotent: false,
|
||||
example: 'POST /users'
|
||||
},
|
||||
{
|
||||
name: 'PUT',
|
||||
label: '更新数据',
|
||||
icon: '✏️',
|
||||
analogy: '修改订单',
|
||||
idempotent: true,
|
||||
example: 'PUT /users/1'
|
||||
},
|
||||
{
|
||||
name: 'DELETE',
|
||||
label: '删除数据',
|
||||
icon: '❌',
|
||||
analogy: '取消订单',
|
||||
idempotent: true,
|
||||
example: 'DELETE /users/1'
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟用户数据
|
||||
const users = ref([
|
||||
{ id: 1, name: '张三', email: 'zhangsan@example.com', avatar: '👨' },
|
||||
{ id: 2, name: '李四', email: 'lisi@example.com', avatar: '👩' },
|
||||
{ id: 3, name: '王五', email: 'wangwu@example.com', avatar: '👨💼' }
|
||||
])
|
||||
|
||||
function selectMethod(method) {
|
||||
selectedMethod.value = method
|
||||
}
|
||||
|
||||
function getMockUrl() {
|
||||
const baseUrl = '/api/users'
|
||||
if (selectedMethod.value.name === 'GET' && users.value.length > 0) {
|
||||
return `${baseUrl}/${users.value[0].id}`
|
||||
}
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
function getMockBody() {
|
||||
if (selectedMethod.value.name === 'POST') {
|
||||
return JSON.stringify(
|
||||
{
|
||||
name: '赵六',
|
||||
email: 'zhaoliu@example.com'
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
if (selectedMethod.value.name === 'PUT') {
|
||||
return JSON.stringify(
|
||||
{
|
||||
name: '张三(已修改)',
|
||||
email: 'zhangsan_new@example.com'
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function getMockStatus() {
|
||||
const statusMap = {
|
||||
GET: 200,
|
||||
POST: 201,
|
||||
PUT: 200,
|
||||
DELETE: 204
|
||||
}
|
||||
return statusMap[selectedMethod.value.name] || 200
|
||||
}
|
||||
|
||||
function getMockStatusText() {
|
||||
const textMap = {
|
||||
200: 'OK',
|
||||
201: 'Created',
|
||||
204: 'No Content'
|
||||
}
|
||||
return textMap[getMockStatus()] || 'OK'
|
||||
}
|
||||
|
||||
function getStatusClass() {
|
||||
const status = getMockStatus()
|
||||
if (status >= 200 && status < 300) return 'success'
|
||||
if (status >= 300 && status < 400) return 'redirect'
|
||||
if (status >= 400 && status < 500) return 'client-error'
|
||||
return 'server-error'
|
||||
}
|
||||
|
||||
function getMockResponse() {
|
||||
if (selectedMethod.value.name === 'DELETE') {
|
||||
return '(无返回内容)'
|
||||
}
|
||||
|
||||
if (selectedMethod.value.name === 'GET') {
|
||||
return JSON.stringify(users.value[0] || {}, null, 2)
|
||||
}
|
||||
|
||||
if (selectedMethod.value.name === 'POST') {
|
||||
return JSON.stringify(
|
||||
{
|
||||
id: 4,
|
||||
name: '赵六',
|
||||
email: 'zhaoliu@example.com',
|
||||
createdAt: '2024-01-15T10:30:00Z'
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedMethod.value.name === 'PUT') {
|
||||
return JSON.stringify(
|
||||
{
|
||||
id: 1,
|
||||
name: '张三(已修改)',
|
||||
email: 'zhangsan_new@example.com',
|
||||
updatedAt: '2024-01-15T10:30:00Z'
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
|
||||
return '{}'
|
||||
}
|
||||
|
||||
function addNewUser() {
|
||||
selectMethod(methods[1]) // POST
|
||||
}
|
||||
|
||||
function editUser(user) {
|
||||
selectMethod(methods[2]) // PUT
|
||||
}
|
||||
|
||||
function deleteUser(userId) {
|
||||
selectMethod(methods[3]) // DELETE
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-method-demo {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
font-size: 20px;
|
||||
margin: 0 0 8px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.method-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.method-card {
|
||||
padding: 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.method-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.method-card.active {
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.method-card.get {
|
||||
border-color: #61affe;
|
||||
}
|
||||
.method-card.get.active {
|
||||
background: #e8f4ff;
|
||||
}
|
||||
|
||||
.method-card.post {
|
||||
border-color: #49cc90;
|
||||
}
|
||||
.method-card.post.active {
|
||||
background: #e8fff5;
|
||||
}
|
||||
|
||||
.method-card.put {
|
||||
border-color: #fca130;
|
||||
}
|
||||
.method-card.put.active {
|
||||
background: #fff8e8;
|
||||
}
|
||||
|
||||
.method-card.delete {
|
||||
border-color: #f93e3e;
|
||||
}
|
||||
.method-card.delete.active {
|
||||
background: #ffe8e8;
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.method-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.method-analogy {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.demo-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.request-panel,
|
||||
.response-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.request-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.http-method {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.http-method.get {
|
||||
background: #61affe;
|
||||
color: white;
|
||||
}
|
||||
.http-method.post {
|
||||
background: #49cc90;
|
||||
color: white;
|
||||
}
|
||||
.http-method.put {
|
||||
background: #fca130;
|
||||
color: white;
|
||||
}
|
||||
.http-method.delete {
|
||||
background: #f93e3e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.url {
|
||||
font-family: monospace;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.request-panel pre,
|
||||
.response-panel pre {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-code {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.status-code.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status-code.redirect {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.status-code.client-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.status-code.server-error {
|
||||
background: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.data-view {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-edit,
|
||||
.btn-delete {
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: #fff3cd;
|
||||
}
|
||||
.btn-delete:hover {
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
padding: 12px 24px;
|
||||
background: #49cc90;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: #3db880;
|
||||
}
|
||||
|
||||
.btn-add .hint {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.comparison-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.comparison-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.badge.get {
|
||||
background: #61affe;
|
||||
color: white;
|
||||
}
|
||||
.badge.post {
|
||||
background: #49cc90;
|
||||
color: white;
|
||||
}
|
||||
.badge.put {
|
||||
background: #fca130;
|
||||
color: white;
|
||||
}
|
||||
.badge.delete {
|
||||
background: #f93e3e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f8f9fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #e83e8c;
|
||||
}
|
||||
</style>
|
||||
@@ -1,856 +0,0 @@
|
||||
<template>
|
||||
<div class="api-playground">
|
||||
<div class="playground-header">
|
||||
<h3>🎮 API 调用游乐场</h3>
|
||||
<p>像使用 Postman 一样测试 API</p>
|
||||
</div>
|
||||
|
||||
<!-- 预设场景 -->
|
||||
<div class="presets">
|
||||
<span class="presets-label">快速场景:</span>
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
:key="preset.id"
|
||||
@click="loadPreset(preset)"
|
||||
class="preset-btn"
|
||||
>
|
||||
{{ preset.icon }} {{ preset.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="playground-container">
|
||||
<!-- 左侧:请求配置 -->
|
||||
<div class="request-panel">
|
||||
<div class="panel-header">
|
||||
<h4>📤 请求</h4>
|
||||
<button @click="sendRequest" class="send-btn" :disabled="isLoading">
|
||||
{{ isLoading ? '发送中...' : '🚀 发送请求' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 请求行 -->
|
||||
<div class="request-line">
|
||||
<select v-model="request.method" class="method-select">
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="request.url"
|
||||
class="url-input"
|
||||
placeholder="https://api.example.com/users"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<div class="request-tabs">
|
||||
<button
|
||||
v-for="tab in ['headers', 'body', 'auth']"
|
||||
:key="tab"
|
||||
@click="activeTab = tab"
|
||||
:class="{ active: activeTab === tab }"
|
||||
class="tab-btn"
|
||||
>
|
||||
{{ tab.toUpperCase() }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Headers 编辑器 -->
|
||||
<div v-if="activeTab === 'headers'" class="tab-content">
|
||||
<div class="key-value-editor">
|
||||
<div v-for="(header, index) in request.headers" :key="index" class="kv-row">
|
||||
<input v-model="header.key" placeholder="Header 名称" />
|
||||
<input v-model="header.value" placeholder="Header 值" />
|
||||
<button @click="removeHeader(index)" class="remove-btn">×</button>
|
||||
</div>
|
||||
<button @click="addHeader" class="add-btn">+ 添加 Header</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body 编辑器 -->
|
||||
<div v-if="activeTab === 'body'" class="tab-content">
|
||||
<div class="body-toolbar">
|
||||
<button @click="formatBody" class="tool-btn">✨ 格式化</button>
|
||||
<button @click="minifyBody" class="tool-btn">🗜️ 压缩</button>
|
||||
<button @click="copyBody" class="tool-btn">📋 复制</button>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="request.body"
|
||||
class="body-editor"
|
||||
placeholder='{"key": "value"}'
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
<div v-if="bodyError" class="error-message">
|
||||
⚠️ JSON 格式错误:{{ bodyError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth 编辑器 -->
|
||||
<div v-if="activeTab === 'auth'" class="tab-content">
|
||||
<div class="auth-section">
|
||||
<label>
|
||||
<input type="radio" v-model="authType" value="none" />
|
||||
无认证
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" v-model="authType" value="apikey" />
|
||||
API Key
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" v-model="authType" value="bearer" />
|
||||
Bearer Token
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="authType === 'apikey'" class="auth-inputs">
|
||||
<input v-model="auth.apikey.name" placeholder="X-API-Key" />
|
||||
<input v-model="auth.apikey.value" placeholder="your-api-key" />
|
||||
</div>
|
||||
|
||||
<div v-if="authType === 'bearer'" class="auth-inputs">
|
||||
<input v-model="auth.bearer.token" placeholder="your-bearer-token" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:响应展示 -->
|
||||
<div class="response-panel">
|
||||
<div class="panel-header">
|
||||
<h4>📥 响应</h4>
|
||||
<div v-if="response" class="response-meta">
|
||||
<span class="status-badge" :class="getStatusClass(response.status)">
|
||||
{{ response.status }}
|
||||
</span>
|
||||
<span class="response-time">{{ responseTime }}ms</span>
|
||||
<span class="response-size">{{ responseSize }}B</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!response && !isLoading" class="empty-state">
|
||||
<div class="empty-icon">📡</div>
|
||||
<p>点击"发送请求"开始测试</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>请求发送中...</p>
|
||||
</div>
|
||||
|
||||
<div v-if="response" class="response-content">
|
||||
<pre class="response-body"><code>{{ formatJson(response.data) }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<div class="history-section">
|
||||
<div class="history-header">
|
||||
<h4>📜 历史记录</h4>
|
||||
<button @click="clearHistory" class="clear-btn">清空</button>
|
||||
</div>
|
||||
<div class="history-list">
|
||||
<div
|
||||
v-for="(item, index) in history"
|
||||
:key="index"
|
||||
@click="loadFromHistory(item)"
|
||||
class="history-item"
|
||||
>
|
||||
<span class="history-method" :class="item.method.toLowerCase()">
|
||||
{{ item.method }}
|
||||
</span>
|
||||
<span class="history-url">{{ truncateUrl(item.url) }}</span>
|
||||
<span class="history-time">{{ formatTime(item.timestamp) }}</span>
|
||||
<span class="history-status" :class="getStatusClass(item.status)">
|
||||
{{ item.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="history.length === 0" class="history-empty">
|
||||
暂无历史记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="tips">
|
||||
<h4>💡 使用技巧</h4>
|
||||
<ul>
|
||||
<li>🎯 使用<strong>快速场景</strong>快速填充常用配置</li>
|
||||
<li>✨ 点击<strong>格式化</strong>按钮美化 JSON</li>
|
||||
<li>📋 查看历史记录可以快速重发之前的请求</li>
|
||||
<li>🔒 在 Auth 标签页中添加 API Key 或 Token</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const isLoading = ref(false)
|
||||
const activeTab = ref('headers')
|
||||
const authType = ref('none')
|
||||
const response = ref(null)
|
||||
const responseTime = ref(0)
|
||||
const responseSize = ref(0)
|
||||
const bodyError = ref('')
|
||||
|
||||
const request = ref({
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [
|
||||
{ key: 'Content-Type', value: 'application/json' }
|
||||
],
|
||||
body: '{\n "name": "张三",\n "email": "zhangsan@example.com"\n}'
|
||||
})
|
||||
|
||||
const auth = ref({
|
||||
apikey: { name: 'X-API-Key', value: '' },
|
||||
bearer: { token: '' }
|
||||
})
|
||||
|
||||
const history = ref([])
|
||||
|
||||
const presets = [
|
||||
{
|
||||
id: 1,
|
||||
name: '获取用户列表',
|
||||
icon: '👥',
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '创建用户',
|
||||
icon: '➕',
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: '{\n "name": "李四",\n "email": "lisi@example.com",\n "password": "123456"\n}'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '更新用户',
|
||||
icon: '✏️',
|
||||
method: 'PUT',
|
||||
url: 'https://api.example.com/users/1',
|
||||
headers: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: '{\n "name": "张三(已修改)",\n "email": "new_zhangsan@example.com"\n}'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '删除用户',
|
||||
icon: '❌',
|
||||
method: 'DELETE',
|
||||
url: 'https://api.example.com/users/1',
|
||||
headers: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: ''
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟响应数据
|
||||
const mockResponses = {
|
||||
'GET:https://api.example.com/users': {
|
||||
status: 200,
|
||||
data: {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: [
|
||||
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
|
||||
{ id: 2, name: '李四', email: 'lisi@example.com' }
|
||||
],
|
||||
pagination: { page: 1, limit: 20, total: 2 }
|
||||
}
|
||||
},
|
||||
'POST:https://api.example.com/users': {
|
||||
status: 201,
|
||||
data: {
|
||||
code: 201,
|
||||
message: 'created',
|
||||
data: { id: 3, name: '李四', email: 'lisi@example.com', createdAt: '2024-01-15T10:30:00Z' }
|
||||
}
|
||||
},
|
||||
'PUT:https://api.example.com/users/1': {
|
||||
status: 200,
|
||||
data: {
|
||||
code: 200,
|
||||
message: 'updated',
|
||||
data: { id: 1, name: '张三(已修改)', email: 'new_zhangsan@example.com', updatedAt: '2024-01-15T10:30:00Z' }
|
||||
}
|
||||
},
|
||||
'DELETE:https://api.example.com/users/1': {
|
||||
status: 204,
|
||||
data: { code: 204, message: 'deleted' }
|
||||
}
|
||||
}
|
||||
|
||||
function loadPreset(preset) {
|
||||
request.value = {
|
||||
method: preset.method,
|
||||
url: preset.url,
|
||||
headers: [...preset.headers],
|
||||
body: preset.body
|
||||
}
|
||||
activeTab.value = 'headers'
|
||||
}
|
||||
|
||||
async function sendRequest() {
|
||||
if (isLoading.value) return
|
||||
|
||||
// 验证 JSON 格式
|
||||
if (request.value.method !== 'GET' && request.value.body.trim()) {
|
||||
try {
|
||||
JSON.parse(request.value.body)
|
||||
bodyError.value = ''
|
||||
} catch (e) {
|
||||
bodyError.value = e.message
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
response.value = null
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
// 模拟网络请求
|
||||
await sleep(1000 + Math.random() * 1000)
|
||||
|
||||
const key = `${request.value.method}:${request.value.url}`
|
||||
const mockResponse = mockResponses[key] || {
|
||||
status: 200,
|
||||
data: { code: 200, message: 'OK', data: {} }
|
||||
}
|
||||
|
||||
responseTime.value = Date.now() - startTime
|
||||
responseSize.value = JSON.stringify(mockResponse.data).length
|
||||
response.value = mockResponse
|
||||
|
||||
// 添加到历史记录
|
||||
history.value.unshift({
|
||||
...request.value,
|
||||
status: mockResponse.status,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
if (history.value.length > 10) {
|
||||
history.value = history.value.slice(0, 10)
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
function formatBody() {
|
||||
try {
|
||||
const parsed = JSON.parse(request.value.body)
|
||||
request.value.body = JSON.stringify(parsed, null, 2)
|
||||
bodyError.value = ''
|
||||
} catch (e) {
|
||||
bodyError.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
function minifyBody() {
|
||||
try {
|
||||
const parsed = JSON.parse(request.value.body)
|
||||
request.value.body = JSON.stringify(parsed)
|
||||
bodyError.value = ''
|
||||
} catch (e) {
|
||||
bodyError.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
function copyBody() {
|
||||
navigator.clipboard.writeText(request.value.body)
|
||||
}
|
||||
|
||||
function addHeader() {
|
||||
request.value.headers.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
function removeHeader(index) {
|
||||
request.value.headers.splice(index, 1)
|
||||
}
|
||||
|
||||
function formatJson(data) {
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (status >= 200 && status < 300) return 'success'
|
||||
if (status >= 300 && status < 400) return 'redirect'
|
||||
if (status >= 400 && status < 500) return 'client-error'
|
||||
return 'server-error'
|
||||
}
|
||||
|
||||
function truncateUrl(url) {
|
||||
return url.length > 40 ? url.substring(0, 40) + '...' : url
|
||||
}
|
||||
|
||||
function formatTime(timestamp) {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
}
|
||||
|
||||
function loadFromHistory(item) {
|
||||
request.value = {
|
||||
method: item.method,
|
||||
url: item.url,
|
||||
headers: [...item.headers],
|
||||
body: item.body
|
||||
}
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
history.value = []
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// 监听 auth 类型变化,自动添加 headers
|
||||
watch(authType, (newType) => {
|
||||
if (newType === 'apikey') {
|
||||
request.value.headers.push({ key: auth.value.apikey.name, value: auth.value.apikey.value })
|
||||
} else if (newType === 'bearer') {
|
||||
request.value.headers.push({ key: 'Authorization', value: `Bearer ${auth.value.bearer.token}` })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-playground {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.playground-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.playground-header h3 {
|
||||
font-size: 20px;
|
||||
margin: 0 0 8px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.presets-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.playground-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.request-panel,
|
||||
.response-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel-header h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 8px 16px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.request-line {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.method-select {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.url-input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.request-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.key-value-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kv-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kv-row input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
padding: 4px 12px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
padding: 8px 16px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.body-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.body-editor {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.auth-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.auth-section label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.auth-inputs input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.response-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.status-badge.success { background: #d4edda; color: #155724; }
|
||||
.status-badge.redirect { background: #fff3cd; color: #856404; }
|
||||
.status-badge.client-error { background: #f8d7da; color: #721c24; }
|
||||
.status-badge.server-error { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.response-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.response-body {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.history-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.history-header h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.history-method {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-method.get { background: #61affe; color: white; }
|
||||
.history-method.post { background: #49cc90; color: white; }
|
||||
.history-method.put { background: #fca130; color: white; }
|
||||
.history-method.delete { background: #f93e3e; color: white; }
|
||||
|
||||
.history-url {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.history-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.history-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.history-status.success { background: #d4edda; color: #155724; }
|
||||
.history-status.client-error { background: #f8d7da; color: #721c24; }
|
||||
|
||||
.history-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding: 16px;
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #007bff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tips h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.tips ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.tips li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.playground-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,725 +0,0 @@
|
||||
<template>
|
||||
<div class="api-quick-start-demo">
|
||||
<div class="demo-header">
|
||||
<h2>⚡ API 快速入门</h2>
|
||||
<p class="subtitle">3 分钟理解 API 是什么</p>
|
||||
</div>
|
||||
|
||||
<!-- 场景选择器 -->
|
||||
<div class="scene-selector">
|
||||
<button
|
||||
v-for="scene in scenes"
|
||||
:key="scene.id"
|
||||
@click="switchScene(scene.id)"
|
||||
:class="{ active: currentScene === scene.id }"
|
||||
class="scene-btn"
|
||||
>
|
||||
{{ scene.icon }} {{ scene.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 主演示区域 -->
|
||||
<div class="demo-stage">
|
||||
<!-- 客户端 -->
|
||||
<div class="client-zone">
|
||||
<div class="phone-frame">
|
||||
<div class="phone-screen">
|
||||
<div class="app-header">{{ getSceneData().appTitle }}</div>
|
||||
<div class="app-content">
|
||||
<!-- 外卖点餐场景 -->
|
||||
<div v-if="currentScene === 'delivery'" class="delivery-ui">
|
||||
<div class="restaurant-info">
|
||||
<div class="restaurant-name">🍔 汉堡王</div>
|
||||
<div class="dish-list">
|
||||
<div class="dish-item">
|
||||
<span class="dish-name">牛肉汉堡</span>
|
||||
<span class="dish-price">¥35</span>
|
||||
</div>
|
||||
<div class="dish-item">
|
||||
<span class="dish-name">薯条</span>
|
||||
<span class="dish-price">¥12</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="placeOrder" :disabled="isProcessing" class="order-btn">
|
||||
{{ isProcessing ? '配送中...' : '🛒 立即下单' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 微信登录场景 -->
|
||||
<div v-if="currentScene === 'wechat'" class="wechat-ui">
|
||||
<div class="login-logo">👤</div>
|
||||
<div class="login-title">欢迎登录</div>
|
||||
<button @click="wechatLogin" :disabled="isProcessing" class="login-btn">
|
||||
<span class="wechat-icon">💬</span>
|
||||
{{ isProcessing ? '登录中...' : '微信快速登录' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 天气查询场景 -->
|
||||
<div v-if="currentScene === 'weather'" class="weather-ui">
|
||||
<div class="weather-search">
|
||||
<input
|
||||
v-model="searchCity"
|
||||
placeholder="输入城市名称"
|
||||
class="search-input"
|
||||
@keyup.enter="searchWeather"
|
||||
/>
|
||||
<button @click="searchWeather" :disabled="isProcessing" class="search-btn">
|
||||
{{ isProcessing ? '查询中...' : '🔍 查询' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="zone-label">👤 客户端 (你)</div>
|
||||
</div>
|
||||
|
||||
<!-- API 中间层 -->
|
||||
<div class="api-zone">
|
||||
<div class="api-container">
|
||||
<div class="api-icon" :class="{ moving: isProcessing }">
|
||||
{{ getSceneData().apiIcon }}
|
||||
</div>
|
||||
<div class="api-label">API</div>
|
||||
<div v-if="isProcessing" class="data-flow">
|
||||
<div class="data-packet">{{ getSceneData().requestData }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="zone-label">🔗 API (桥梁)</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务器 -->
|
||||
<div class="server-zone">
|
||||
<div class="server-container">
|
||||
<div class="server-icon">🏢</div>
|
||||
<div class="server-label">{{ getSceneData().serverName }}</div>
|
||||
<div v-if="isProcessing && currentStep >= 3" class="processing-indicator">
|
||||
<div class="dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="processing-text">处理中...</div>
|
||||
</div>
|
||||
<div v-if="response && !isProcessing" class="result-display">
|
||||
<div class="result-label">返回数据:</div>
|
||||
<pre class="result-data">{{ formatResponse(response) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="zone-label">🖥️ 服务器</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 流程说明 -->
|
||||
<div class="flow-explanation">
|
||||
<div class="step" :class="{ active: currentStep >= 1 }">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-text">发起请求</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" :class="{ active: currentStep >= 2 }">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-text">API 传递</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-text">服务器处理</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" :class="{ active: currentStep >= 4 }">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-text">返回结果</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关键要点 -->
|
||||
<div class="key-points">
|
||||
<h4>💡 理解 API 的三个关键点</h4>
|
||||
<div class="points-grid">
|
||||
<div class="point-card">
|
||||
<div class="point-icon">🔌</div>
|
||||
<div class="point-title">API 是"接口"</div>
|
||||
<div class="point-desc">就像插头连接电器,API 连接不同的软件系统</div>
|
||||
</div>
|
||||
<div class="point-card">
|
||||
<div class="point-icon">📨</div>
|
||||
<div class="point-title">API 是"信使"</div>
|
||||
<div class="point-desc">你告诉 API 需要什么,API 去服务器取来给你</div>
|
||||
</div>
|
||||
<div class="point-card">
|
||||
<div class="point-icon">📋</div>
|
||||
<div class="point-title">API 是"菜单"</div>
|
||||
<div class="point-desc">API 文档告诉你有哪些功能可以调用</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const currentScene = ref('delivery')
|
||||
const isProcessing = ref(false)
|
||||
const currentStep = ref(0)
|
||||
const response = ref(null)
|
||||
const searchCity = ref('北京')
|
||||
|
||||
const scenes = [
|
||||
{ id: 'delivery', name: '外卖点餐', icon: '🍔' },
|
||||
{ id: 'wechat', name: '微信登录', icon: '💬' },
|
||||
{ id: 'weather', name: '天气查询', icon: '🌤️' }
|
||||
]
|
||||
|
||||
function getSceneData() {
|
||||
const sceneMap = {
|
||||
delivery: {
|
||||
appTitle: '外卖 APP',
|
||||
apiIcon: '🛵',
|
||||
serverName: '餐厅系统',
|
||||
requestData: '订单: 汉堡+薯条'
|
||||
},
|
||||
wechat: {
|
||||
appTitle: '第三方 APP',
|
||||
apiIcon: '🔐',
|
||||
serverName: '微信服务器',
|
||||
requestData: '验证用户身份'
|
||||
},
|
||||
weather: {
|
||||
appTitle: '天气 APP',
|
||||
apiIcon: '📡',
|
||||
serverName: '气象局数据',
|
||||
requestData: `查询: ${searchCity.value}天气`
|
||||
}
|
||||
}
|
||||
return sceneMap[currentScene.value]
|
||||
}
|
||||
|
||||
async function placeOrder() {
|
||||
if (isProcessing.value) return
|
||||
await processRequest({
|
||||
status: 'success',
|
||||
message: '下单成功',
|
||||
data: {
|
||||
orderId: 'DD20240115001',
|
||||
estimatedTime: '30分钟',
|
||||
items: [
|
||||
{ name: '牛肉汉堡', quantity: 1, price: 35 },
|
||||
{ name: '薯条', quantity: 1, price: 12 }
|
||||
],
|
||||
total: 47
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function wechatLogin() {
|
||||
if (isProcessing.value) return
|
||||
await processRequest({
|
||||
status: 'success',
|
||||
message: '登录成功',
|
||||
data: {
|
||||
userId: 'wx_123456',
|
||||
nickname: '微信用户',
|
||||
avatar: 'https://example.com/avatar.jpg'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function searchWeather() {
|
||||
if (isProcessing.value || !searchCity.value) return
|
||||
await processRequest({
|
||||
status: 'success',
|
||||
message: '查询成功',
|
||||
data: {
|
||||
city: searchCity.value,
|
||||
temperature: '22°C',
|
||||
weather: '晴',
|
||||
humidity: '45%',
|
||||
wind: '东南风 3级'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function processRequest(mockResponse) {
|
||||
isProcessing.value = true
|
||||
response.value = null
|
||||
|
||||
// 步骤1: 发起请求
|
||||
currentStep.value = 1
|
||||
await sleep(600)
|
||||
|
||||
// 步骤2: API 传递
|
||||
currentStep.value = 2
|
||||
await sleep(800)
|
||||
|
||||
// 步骤3: 服务器处理
|
||||
currentStep.value = 3
|
||||
await sleep(1000)
|
||||
|
||||
// 步骤4: 返回结果
|
||||
currentStep.value = 4
|
||||
response.value = mockResponse
|
||||
await sleep(500)
|
||||
|
||||
isProcessing.value = false
|
||||
}
|
||||
|
||||
function switchScene(sceneId) {
|
||||
currentScene.value = sceneId
|
||||
currentStep.value = 0
|
||||
response.value = null
|
||||
searchCity.value = '北京'
|
||||
}
|
||||
|
||||
function formatResponse(resp) {
|
||||
return JSON.stringify(resp, null, 2)
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-quick-start-demo {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.demo-header h2 {
|
||||
font-size: 32px;
|
||||
margin: 0 0 12px 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scene-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.scene-btn {
|
||||
padding: 12px 24px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.scene-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.scene-btn.active {
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.demo-stage {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.client-zone,
|
||||
.api-zone,
|
||||
.server-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.phone-frame {
|
||||
width: 180px;
|
||||
height: 320px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 24px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.phone-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.restaurant-info {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.restaurant-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dish-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dish-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.order-btn,
|
||||
.login-btn,
|
||||
.search-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.order-btn:disabled,
|
||||
.login-btn:disabled,
|
||||
.search-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
font-size: 48px;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.weather-search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.api-container {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.api-icon {
|
||||
font-size: 48px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.api-icon.moving {
|
||||
animation: deliveryMove 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes deliveryMove {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
}
|
||||
|
||||
.api-label {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.data-flow {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
background: white;
|
||||
color: #333;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
animation: floatData 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes floatData {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.server-container {
|
||||
width: 200px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.server-label {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.processing-indicator {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dots span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.dots span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.dots span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.processing-text {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-display {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-size: 11px;
|
||||
margin-bottom: 6px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.result-data {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #4ade80;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.zone-label {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.flow-explanation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.step.active .step-number {
|
||||
background: white;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 24px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.key-points {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.key-points h4 {
|
||||
margin: 0 0 20px 0;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.points-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.point-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.point-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.point-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.point-desc {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-stage {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.flow-explanation {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.api-quick-start-demo {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.demo-header h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.points-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,452 +0,0 @@
|
||||
<template>
|
||||
<div class="request-response-flow">
|
||||
<div class="demo-header">
|
||||
<h3>📡 API 请求-响应流程</h3>
|
||||
<p>观察一个完整的 API 调用过程</p>
|
||||
</div>
|
||||
|
||||
<!-- 请求配置 -->
|
||||
<div class="request-config">
|
||||
<label>
|
||||
请求方法:
|
||||
<select v-model="requestMethod">
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
接口地址:
|
||||
<input v-model="requestUrl" placeholder="/api/users" />
|
||||
</label>
|
||||
|
||||
<button @click="sendRequest" :disabled="isLoading">
|
||||
{{ isLoading ? '发送中...' : '🚀 发送请求' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 流程可视化 -->
|
||||
<div class="flow-visualization">
|
||||
<div
|
||||
class="flow-step"
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:class="{
|
||||
active: currentStep === index,
|
||||
completed: currentStep > index
|
||||
}"
|
||||
>
|
||||
<div class="step-number">{{ index + 1 }}</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">{{ step.title }}</div>
|
||||
<div class="step-description">{{ step.description }}</div>
|
||||
<div v-if="currentStep === index" class="step-detail">
|
||||
{{ step.detail }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index < steps.length - 1" class="step-arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 请求/响应详情 -->
|
||||
<div class="details-panel">
|
||||
<div class="request-detail">
|
||||
<h4>📤 请求详情</h4>
|
||||
<pre><code>{{ requestDetail }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="response-detail">
|
||||
<h4>📥 响应详情</h4>
|
||||
<div v-if="responseData">
|
||||
<div class="status-badge" :class="responseStatusClass">
|
||||
{{ responseData.status }} {{ responseData.statusText }}
|
||||
</div>
|
||||
<pre><code>{{ JSON.stringify(responseData.data, null, 2) }}</code></pre>
|
||||
<div class="response-meta">
|
||||
<span>⏱️ 耗时: {{ responseTime }}ms</span>
|
||||
<span>📦 大小: {{ responseSize }} bytes</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="waiting">等待请求...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态码说明 -->
|
||||
<div class="status-codes">
|
||||
<h4>常见状态码</h4>
|
||||
<div class="code-list">
|
||||
<span class="code success">200 - 成功</span>
|
||||
<span class="code redirect">301 - 重定向</span>
|
||||
<span class="code client-error">400 - 客户端错误</span>
|
||||
<span class="code client-error">401 - 未授权</span>
|
||||
<span class="code client-error">404 - 未找到</span>
|
||||
<span class="code server-error">500 - 服务器错误</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const requestMethod = ref('GET')
|
||||
const requestUrl = ref('/api/users')
|
||||
const isLoading = ref(false)
|
||||
const currentStep = ref(-1)
|
||||
const responseData = ref(null)
|
||||
const responseTime = ref(0)
|
||||
const responseSize = ref(0)
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: '客户端发起请求',
|
||||
description: '浏览器/APP 构建请求',
|
||||
get detail() {
|
||||
return `${requestMethod.value} ${requestUrl.value}`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '网络传输',
|
||||
description: '请求通过互联网发送',
|
||||
get detail() {
|
||||
return 'TCP/IP 数据包传输中...'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '服务器接收并处理',
|
||||
description: '解析请求,查询数据库/执行逻辑',
|
||||
get detail() {
|
||||
return `处理 ${requestMethod.value} 请求...`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '服务器返回响应',
|
||||
description: '生成 JSON 数据并返回',
|
||||
get detail() {
|
||||
return 'HTTP/1.1 200 OK'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '客户端接收响应',
|
||||
description: '解析数据并更新界面',
|
||||
get detail() {
|
||||
return '接收数据,渲染页面'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const requestDetail = computed(() => {
|
||||
return `${requestMethod.value} ${requestUrl.value} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Content-Type: application/json
|
||||
|
||||
${
|
||||
requestMethod.value !== 'GET'
|
||||
? '{\n "name": "张三",\n "email": "zhangsan@example.com"\n}'
|
||||
: ''
|
||||
}`
|
||||
})
|
||||
|
||||
const responseStatusClass = computed(() => {
|
||||
if (!responseData.value) return ''
|
||||
const status = responseData.value.status
|
||||
if (status >= 200 && status < 300) return 'success'
|
||||
if (status >= 300 && status < 400) return 'redirect'
|
||||
if (status >= 400 && status < 500) return 'client-error'
|
||||
if (status >= 500) return 'server-error'
|
||||
return ''
|
||||
})
|
||||
|
||||
async function sendRequest() {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
responseData.value = null
|
||||
currentStep.value = -1
|
||||
|
||||
// 模拟请求流程
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
currentStep.value = i
|
||||
await sleep(800)
|
||||
}
|
||||
|
||||
// 模拟响应数据
|
||||
const startTime = Date.now()
|
||||
responseTime.value = Math.floor(Math.random() * 200) + 50
|
||||
responseSize.value = Math.floor(Math.random() * 1000) + 100
|
||||
|
||||
responseData.value = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
data: {
|
||||
id: 1,
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com',
|
||||
createdAt: '2024-01-01T00:00:00Z'
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(responseTime.value)
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.request-response-flow {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
font-size: 20px;
|
||||
margin: 0 0 8px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.request-config {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.request-config label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.request-config select,
|
||||
.request-config input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.request-config button {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.request-config button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.flow-visualization {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flow-step.active .step-number {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.flow-step.completed .step-number {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.step-detail {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.step-arrow {
|
||||
font-size: 24px;
|
||||
color: #ccc;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.flow-step.active .step-arrow {
|
||||
color: #007bff;
|
||||
animation: arrowMove 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes arrowMove {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.details-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.request-detail,
|
||||
.response-detail {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.request-detail h4,
|
||||
.response-detail h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.request-detail pre,
|
||||
.response-detail pre {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.response-meta {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.waiting {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-codes {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.status-codes h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.code-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.code {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.code.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.code.redirect {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.code.client-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.code.server-error {
|
||||
background: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
@@ -1,284 +0,0 @@
|
||||
<template>
|
||||
<div class="context-window-demo">
|
||||
<div class="window-comparison">
|
||||
<!-- Short Context -->
|
||||
<div class="model-card">
|
||||
<div class="model-header">
|
||||
<span class="model-icon">📱</span>
|
||||
<span class="model-title">短上下文模型</span>
|
||||
</div>
|
||||
<div class="model-body">
|
||||
<div class="window-visual">
|
||||
<div class="window-bar">
|
||||
<div class="window-label">上下文窗口: 4K tokens</div>
|
||||
<div class="window-size">
|
||||
<div class="size-fill short" :style="{ width: '20%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-preview">
|
||||
<div class="content-item system">系统提示词</div>
|
||||
<div class="content-item user">用户消息</div>
|
||||
<div class="content-item assistant">助手回复</div>
|
||||
<div class="content-item warning">⚠️ 只能处理短文档</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-info">
|
||||
<div class="info-item">
|
||||
<span class="label">窗口大小:</span>
|
||||
<span>4K tokens (~3000 字)</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">适用场景:</span>
|
||||
<span>问答、摘要、对话</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">代表模型:</span>
|
||||
<span>GPT-3.5, Claude 2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Long Context -->
|
||||
<div class="model-card highlight">
|
||||
<div class="model-header">
|
||||
<span class="model-icon">📚</span>
|
||||
<span class="model-title">长上下文模型</span>
|
||||
</div>
|
||||
<div class="model-body">
|
||||
<div class="window-visual">
|
||||
<div class="window-bar">
|
||||
<div class="window-label">上下文窗口: 200K tokens</div>
|
||||
<div class="window-size">
|
||||
<div class="size-fill long" :style="{ width: '100%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-preview">
|
||||
<div class="content-item system">系统提示词</div>
|
||||
<div class="content-item user">用户消息</div>
|
||||
<div class="content-item docs">📄 完整技术文档</div>
|
||||
<div class="content-item docs">📄 代码库文件</div>
|
||||
<div class="content-item assistant">助手回复</div>
|
||||
<div class="content-item success">✅ 可处理长文档</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-info">
|
||||
<div class="info-item">
|
||||
<span class="label">窗口大小:</span>
|
||||
<span>200K tokens (~15 万字)</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">适用场景:</span>
|
||||
<span>长文档分析、代码审查</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">代表模型:</span>
|
||||
<span>GPT-4, Claude 3, Gemini</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<div class="tip">
|
||||
<span class="tip-icon">💡</span>
|
||||
<span
|
||||
><strong>选择合适的模型</strong>:短对话用短上下文模型(更快更便宜),
|
||||
长文档分析用长上下文模型(避免信息丢失)</span
|
||||
>
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span class="tip-icon">📏</span>
|
||||
<span
|
||||
><strong>注意 Token 计算</strong>:1 Token ≈ 0.75 个英文单词 或 0.5-1
|
||||
个汉字。 100K tokens 大约等于一本 300 页的书</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.context-window-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.window-comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.window-comparison {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.model-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.model-card.highlight {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.model-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 15px;
|
||||
background: var(--vp-c-bg-mute);
|
||||
}
|
||||
|
||||
.model-card.highlight .model-header {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
}
|
||||
|
||||
.model-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.model-title {
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.model-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.window-visual {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.window-bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.window-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.window-size {
|
||||
height: 8px;
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.size-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.size-fill.short {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.size-fill.long {
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.content-item {
|
||||
font-size: 0.8rem;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content-item.system {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.content-item.user {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.content-item.assistant {
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.content-item.docs {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.content-item.warning {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.content-item.success {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.model-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.info-item span:last-child {
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,272 +0,0 @@
|
||||
<template>
|
||||
<div class="rag-pipeline-demo">
|
||||
<div class="pipeline-title">🔄 RAG (检索增强生成) 工作流程</div>
|
||||
|
||||
<div class="pipeline-flow">
|
||||
<!-- Step 1: Query -->
|
||||
<div class="flow-step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-icon">❓</div>
|
||||
<div class="step-title">用户提问</div>
|
||||
<div class="step-desc">"什么是 RAG?"</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow">→</div>
|
||||
|
||||
<!-- Step 2: Retrieve -->
|
||||
<div class="flow-step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-icon">🔍</div>
|
||||
<div class="step-title">检索相关文档</div>
|
||||
<div class="step-desc">从知识库找到最相关的 3-5 篇文章</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow">→</div>
|
||||
|
||||
<!-- Step 3: Augment -->
|
||||
<div class="flow-step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-icon">📝</div>
|
||||
<div class="step-title">增强提示词</div>
|
||||
<div class="step-desc">将文档内容插入到提示词中</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow">→</div>
|
||||
|
||||
<!-- Step 4: Generate -->
|
||||
<div class="flow-step highlight">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-icon">🤖</div>
|
||||
<div class="step-title">生成回答</div>
|
||||
<div class="step-desc">基于检索到的信息生成答案</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example">
|
||||
<div class="example-header">📄 增强后的提示词示例</div>
|
||||
<div class="example-content">
|
||||
<div class="example-section">
|
||||
<span class="section-label">系统提示:</span>
|
||||
<span class="section-text">你是一个技术助手。</span>
|
||||
</div>
|
||||
<div class="example-section">
|
||||
<span class="section-label">检索到的文档:</span>
|
||||
<div class="retrieved-docs">
|
||||
<div class="doc-item">
|
||||
📄 Doc 1: "RAG 是一种结合检索和生成的技术..."
|
||||
</div>
|
||||
<div class="doc-item">
|
||||
📄 Doc 2: "RAG 的优势是减少幻觉、提高准确性..."
|
||||
</div>
|
||||
<div class="doc-item">
|
||||
📄 Doc 3: "常见的 RAG 框架包括 LangChain..."
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="example-section">
|
||||
<span class="section-label">用户问题:</span>
|
||||
<span class="section-text">什么是 RAG?</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefits">
|
||||
<div class="benefit-item">
|
||||
<span class="benefit-icon">✅</span>
|
||||
<span><strong>减少幻觉</strong>:基于真实文档回答,不瞎编</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<span class="benefit-icon">📚</span>
|
||||
<span><strong>知识更新</strong>:无需重新训练,只需更新文档库</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<span class="benefit-icon">🎯</span>
|
||||
<span><strong>答案可溯源</strong>:可以知道答案来自哪篇文档</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<span class="benefit-icon">💰</span>
|
||||
<span><strong>降低成本</strong>:不需要频繁微调模型</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-pipeline-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.pipeline-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pipeline-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 25px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg-mute);
|
||||
color: var(--vp-c-text-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.flow-step.highlight .step-number {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flow-step.highlight .step-content {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.example {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.example-header {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.example-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.example-section {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.retrieved-docs {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.doc-item {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-3);
|
||||
padding: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 3px;
|
||||
border-left: 3px solid #fbbf24;
|
||||
}
|
||||
|
||||
.benefits {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.benefits {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user