feat: enhance demo components with consistent styling and info boxes
- Add standardized header and info box components to all demo files - Improve visual consistency with theme colors and spacing - Add max-height and overflow-y for better content containment - Update package.json build script with --force flag - Add .gitignore entries for REFACTORING files - Fix table formatting in audio-intro.md
This commit is contained in:
@@ -1,50 +1,496 @@
|
||||
<!--
|
||||
DocumentationDemo.vue - API 文档演示组件
|
||||
展示 API 文档的编写规范和最佳实践
|
||||
-->
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<div class="demo">
|
||||
<div class="header">
|
||||
<span class="icon">📚</span>
|
||||
<span class="title">API 文档:最好的 API 文档就是代码本身</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
文档演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
|
||||
<div class="content">
|
||||
<div class="tools-tabs">
|
||||
<button
|
||||
v-for="tool in tools"
|
||||
:key="tool.id"
|
||||
class="tool-btn"
|
||||
:class="{ active: selectedTool === tool.id }"
|
||||
@click="selectedTool = tool.id"
|
||||
>
|
||||
<span class="tool-icon">{{ tool.icon }}</span>
|
||||
<span class="tool-name">{{ tool.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tool-detail" v-if="currentTool">
|
||||
<div class="tool-header">
|
||||
<div class="tool-title">{{ currentTool.name }}</div>
|
||||
<div class="tool-tags">
|
||||
<span class="tag" :class="tag.class" v-for="tag in currentTool.tags" :key="tag.text">
|
||||
{{ tag.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-description">
|
||||
<p>{{ currentTool.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h4>核心特性</h4>
|
||||
<div class="feature-list">
|
||||
<div v-for="(feature, idx) in currentTool.features" :key="idx" class="feature-item">
|
||||
<span class="feature-icon">✓</span>
|
||||
<span class="feature-text">{{ feature }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h4>文档示例(OpenAPI 3.0)</h4>
|
||||
<div class="code-block">
|
||||
<pre><code>{{ currentTool.example }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tools-section">
|
||||
<h4>🔧 推荐工具</h4>
|
||||
<div class="tools-grid">
|
||||
<div v-for="(rec, idx) in currentTool.recommendations" :key="idx" class="tool-card">
|
||||
<div class="rec-name">{{ rec.name }}</div>
|
||||
<div class="rec-desc">{{ rec.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const title = ref('API文档演示')
|
||||
const description = ref('展示RESTful API文档的编写规范和最佳实践,包括Swagger、OpenAPI等工具的使用')
|
||||
const tools = [
|
||||
{
|
||||
id: 'openapi',
|
||||
name: 'OpenAPI 规范',
|
||||
icon: '📋',
|
||||
tags: [
|
||||
{ text: '行业标准', class: 'primary' },
|
||||
{ text: '语言无关', class: 'secondary' }
|
||||
],
|
||||
description: 'OpenAPI Specification(原 Swagger)是描述 REST API 的标准格式,可以被工具解析生成交互式文档、客户端 SDK、服务器存根等。',
|
||||
features: [
|
||||
'标准化的 YAML/JSON 格式描述 API',
|
||||
'支持路径、参数、响应模型、认证等完整定义',
|
||||
'生态系统丰富,支持 100+ 工具',
|
||||
'可以生成交互式文档(Swagger UI)',
|
||||
'可以从代码注释自动生成',
|
||||
'支持 API 版本控制和演进'
|
||||
],
|
||||
example: `openapi: 3.0.0
|
||||
info:
|
||||
title: 用户服务 API
|
||||
version: 1.0.0
|
||||
description: 提供用户管理相关接口
|
||||
servers:
|
||||
- url: https://api.example.com/v1
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
summary: 获取用户列表
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
format: email`,
|
||||
recommendations: [
|
||||
{ name: 'Swagger UI', description: '最流行的交互式文档界面' },
|
||||
{ name: 'Redoc', description: '美观的现代文档生成器' },
|
||||
{ name: 'Stoplight', description: '可视化的 API 设计平台' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'swagger',
|
||||
name: 'Swagger 工具链',
|
||||
icon: '🛠️',
|
||||
tags: [
|
||||
{ text: '工具集', class: 'success' },
|
||||
{ text: '自动化', class: 'info' }
|
||||
],
|
||||
description: 'Swagger 是一套围绕 OpenAPI 规范构建的工具,包括编辑器、UI、代码生成器等,帮助开发者快速构建和使用 API。',
|
||||
features: [
|
||||
'Swagger Editor:在线编写和验证 OpenAPI 文档',
|
||||
'Swagger UI:自动生成交互式文档',
|
||||
'Swagger Codegen:根据文档生成客户端 SDK',
|
||||
'支持主流编程语言和框架',
|
||||
'集成到 CI/CD 流程',
|
||||
'自动保持文档与代码同步'
|
||||
],
|
||||
example: `# Swagger Editor 示例配置
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: 示例 API
|
||||
version: '1.0.0'
|
||||
host: api.example.com
|
||||
basePath: /v1
|
||||
schemes:
|
||||
- https
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
tags:
|
||||
- Users
|
||||
summary: 获取所有用户
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
200:
|
||||
description: 成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
data:
|
||||
type: array`,
|
||||
recommendations: [
|
||||
{ name: 'Swagger Editor', description: '在线编辑器,实时预览' },
|
||||
{ name: 'Swagger Codegen', description: '生成 40+ 种语言的客户端' },
|
||||
{ name: 'Postman', description: '导入 OpenAPI 进行测试' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'best-practices',
|
||||
name: '文档最佳实践',
|
||||
icon: '⭐',
|
||||
tags: [
|
||||
{ text: '经验', class: 'warning' },
|
||||
{ text: '规范', class: 'secondary' }
|
||||
],
|
||||
description: '好的 API 文档应该像用户手册一样清晰,让开发者不问问题就能完成集成。',
|
||||
features: [
|
||||
'每个接口都有完整的请求示例',
|
||||
'提供多种语言的代码示例(curl、JavaScript、Python)',
|
||||
'错误码文档化,附带解决方案',
|
||||
'提供沙箱环境或测试工具',
|
||||
'包含认证流程和获取 Token 的方法',
|
||||
'实时更新,与代码保持一致',
|
||||
'版本变更日志和迁移指南'
|
||||
],
|
||||
example: `# 完整的接口文档示例
|
||||
|
||||
## 获取用户信息
|
||||
|
||||
**请求示例:**
|
||||
|
||||
\`\`\`bash
|
||||
curl -X GET \\
|
||||
https://api.example.com/v1/users/123 \\
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
\`\`\`
|
||||
|
||||
**成功响应:**
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 123,
|
||||
"name": "张三",
|
||||
"email": "zhangsan@example.com"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**错误响应:**
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 10010 | 用户不存在 | 检查 user_id 是否正确 |
|
||||
| 10018 | Token 已过期 | 重新调用登录接口 |
|
||||
|
||||
**在线测试:**
|
||||
|
||||
[🚀 在 API Explorer 中测试](https://api.example.com/docs)`,
|
||||
recommendations: [
|
||||
{ name: 'API Blueprint', description: ' Markdown 风格的 API 文档' },
|
||||
{ name: 'Docusaurus', description: ' Facebook 开源的文档平台' },
|
||||
{ name: 'GitBook', description: '美观的文档托管平台' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const selectedTool = ref('openapi')
|
||||
const currentTool = computed(() =>
|
||||
tools.find(t => t.id === selectedTool.value)
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.tools-tabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tool-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
.tool-title {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
.tool-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
.tag {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tag.primary {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.tag.secondary {
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.tag.success {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.tag.info {
|
||||
background: #ccfbf1;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.tag.warning {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.tool-description {
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.7;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.feature-section, .example-section, .tools-section {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.feature-section h4, .example-section h4, .tools-section h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
color: #22c55e;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-block code {
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tools-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rec-name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rec-desc {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tools-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tools-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,97 +1,379 @@
|
||||
<!--
|
||||
ErrorHandlingDemo.vue - 错误处理演示组件
|
||||
展示错误处理的正确和错误示例对比
|
||||
-->
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>错误处理演示</h4>
|
||||
<p class="hint">展示RESTful API中的错误处理机制</p>
|
||||
<div class="demo">
|
||||
<div class="header">
|
||||
<span class="icon">🚨</span>
|
||||
<span class="title">错误处理:优雅地"拒绝"</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<div class="error-types">
|
||||
<div class="error-item">
|
||||
<span class="code">400</span>
|
||||
<span class="name">Bad Request</span>
|
||||
<span class="desc">请求参数错误</span>
|
||||
|
||||
<div class="content">
|
||||
<div class="comparison-tabs">
|
||||
<button
|
||||
class="tab-btn bad"
|
||||
:class="{ active: selectedTab === 'bad' }"
|
||||
@click="selectedTab = 'bad'"
|
||||
>
|
||||
❌ 错误示范
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn good"
|
||||
:class="{ active: selectedTab === 'good' }"
|
||||
@click="selectedTab = 'good'"
|
||||
>
|
||||
✅ 正确示范
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 错误示范 -->
|
||||
<div v-if="selectedTab === 'bad'" class="comparison bad">
|
||||
<div class="response-preview">
|
||||
<div class="status-line bad">
|
||||
<span>HTTP/1.1</span>
|
||||
<span class="code">200 OK</span>
|
||||
</div>
|
||||
<div class="response-body">
|
||||
<pre><code>{
|
||||
"error": "出错了"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-item">
|
||||
<span class="code">401</span>
|
||||
<span class="name">Unauthorized</span>
|
||||
<span class="desc">未授权访问</span>
|
||||
|
||||
<div class="problems">
|
||||
<h4>问题分析</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="icon">⚠️</span>
|
||||
HTTP 状态码说"成功",但业务说"出错" - 前后端状态不一致
|
||||
</li>
|
||||
<li>
|
||||
<span class="icon">⚠️</span>
|
||||
错误信息太笼统,无法定位问题
|
||||
</li>
|
||||
<li>
|
||||
<span class="icon">⚠️</span>
|
||||
没有错误代码,难以程序化判断
|
||||
</li>
|
||||
<li>
|
||||
<span class="icon">⚠️</span>
|
||||
浏览器和 CDN 会缓存这个"成功的"响应
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="error-item">
|
||||
<span class="code">404</span>
|
||||
<span class="name">Not Found</span>
|
||||
<span class="desc">资源不存在</span>
|
||||
</div>
|
||||
|
||||
<!-- 正确示范 -->
|
||||
<div v-if="selectedTab === 'good'" class="comparison good">
|
||||
<div class="response-preview">
|
||||
<div class="status-line">
|
||||
<span>HTTP/1.1</span>
|
||||
<span class="code">422 Unprocessable Entity</span>
|
||||
</div>
|
||||
<div class="response-body">
|
||||
<pre><code>{{ JSON.stringify(goodResponse, null, 2) }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-item">
|
||||
<span class="code">500</span>
|
||||
<span class="name">Server Error</span>
|
||||
<span class="desc">服务器内部错误</span>
|
||||
|
||||
<div class="highlights">
|
||||
<h4>正确做法</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="icon">✅</span>
|
||||
<strong>正确的 HTTP 状态码</strong>: 422 表示语义错误
|
||||
</li>
|
||||
<li>
|
||||
<span class="icon">✅</span>
|
||||
<strong>业务错误码</strong>: `code: 20003` 可用于程序判断
|
||||
</li>
|
||||
<li>
|
||||
<span class="icon">✅</span>
|
||||
<strong>详细错误信息</strong>: `errors` 数组包含具体字段和原因
|
||||
</li>
|
||||
<li>
|
||||
<span class="icon">✅</span>
|
||||
<strong>可追踪性</strong>: `request_id` 用于日志查询
|
||||
</li>
|
||||
<li>
|
||||
<span class="icon">✅</span>
|
||||
<strong>帮助链接</strong>: `help_url` 引导用户查看文档
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="error-codes">
|
||||
<h4>错误码体系</h4>
|
||||
<div class="code-list">
|
||||
<div v-for="item in errorCodeItems" :key="item.code" class="code-item">
|
||||
<span class="code-badge">{{ item.code }}</span>
|
||||
<span class="code-desc">{{ item.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const selectedTab = ref('bad')
|
||||
|
||||
const goodResponse = {
|
||||
code: 20003,
|
||||
message: '密码强度不足',
|
||||
errors: [
|
||||
{
|
||||
field: 'password',
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: '密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位'
|
||||
}
|
||||
],
|
||||
request_id: 'req-550e8400-e29b-41d4-a716-44665544000',
|
||||
timestamp: '2024-01-15T09:30:00.000Z',
|
||||
help_url: 'https://docs.example.com/errors/20003'
|
||||
}
|
||||
|
||||
const errorCodeItems = [
|
||||
{ code: '1XXYY', desc: '通用错误(第1位固定为1)' },
|
||||
{ code: '10001', desc: '参数错误' },
|
||||
{ code: '10010', desc: '用户不存在' },
|
||||
{ code: '10018', desc: 'Token 已过期' },
|
||||
{ code: '10021', desc: '权限不足' },
|
||||
{ code: '20003', desc: '密码强度不足' },
|
||||
{ code: '20014', desc: '余额不足' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
.header {
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.error-types {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.error-item {
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.comparison-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 2px solid;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-btn.bad {
|
||||
border-color: #ef4444;
|
||||
background: var(--vp-c-bg);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.tab-btn.bad:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.tab-btn.bad.active {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-btn.good {
|
||||
border-color: #22c55e;
|
||||
background: var(--vp-c-bg);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.tab-btn.good:hover {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.tab-btn.good.active {
|
||||
background: #22c55e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.comparison {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #f56c6c;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error-item .code {
|
||||
.response-preview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
color: #f56c6c;
|
||||
font-size: 16px;
|
||||
min-width: 50px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.error-item .name {
|
||||
font-weight: 500;
|
||||
.status-line.bad .code {
|
||||
color: #ef4444;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-line:not(.bad) .code {
|
||||
color: #d97706;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.response-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.response-body pre {
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.response-body code {
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.error-item .desc {
|
||||
color: var(--vp-c-text-2);
|
||||
.problems, .highlights {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.problems h4, .highlights h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.problems ul, .highlights ul {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.problems li, .highlights li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.problems li {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.highlights li {
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.problems li .icon, .highlights li .icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.problems li strong, .highlights li strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error-codes {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.error-codes h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.code-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.code-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.code-badge {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 4px 8px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.code-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.comparison-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,50 +1,397 @@
|
||||
<!--
|
||||
ResponseStructureDemo.vue - HTTP 响应结构演示组件
|
||||
展示标准化 API 响应结构和分页响应
|
||||
-->
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<div class="demo">
|
||||
<div class="header">
|
||||
<span class="icon">📦</span>
|
||||
<span class="title">HTTP 响应结构:标准化的数据契约</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
响应结构演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
|
||||
<div class="content">
|
||||
<div class="response-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="tab-btn"
|
||||
:class="{ active: selectedTab === tab.id }"
|
||||
@click="selectedTab = tab.id"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="response-detail">
|
||||
<div class="response-header">
|
||||
<div class="status-line">
|
||||
<span class="http-version">HTTP/1.1</span>
|
||||
<span class="status-code" :class="getStatusClass(currentResponse.status)">{{ currentResponse.status }}</span>
|
||||
<span class="status-text">{{ currentResponse.statusText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-headers">
|
||||
<div class="header-item">
|
||||
<span class="header-key">Content-Type:</span>
|
||||
<span class="header-value">application/json</span>
|
||||
</div>
|
||||
<div class="header-item">
|
||||
<span class="header-key">X-Request-ID:</span>
|
||||
<span class="header-value">req-550e8400-e29b-41d4</span>
|
||||
</div>
|
||||
<div class="header-item">
|
||||
<span class="header-key">X-Response-Time:</span>
|
||||
<span class="header-value">45ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-body">
|
||||
<pre><code>{{ JSON.stringify(currentResponse.body, null, 2) }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="field-descriptions">
|
||||
<h4>字段说明</h4>
|
||||
<div class="field-list">
|
||||
<div v-for="field in currentResponse.fields" :key="field.name" class="field-item">
|
||||
<div class="field-name">
|
||||
<code>{{ field.name }}</code>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
</div>
|
||||
<div class="field-desc">{{ field.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const title = ref('HTTP响应结构演示')
|
||||
const description = ref('展示HTTP响应的结构,包括状态行、响应头和响应体')
|
||||
const tabs = [
|
||||
{ id: 'success', name: '成功响应' },
|
||||
{ id: 'pagination', name: '分页响应' },
|
||||
{ id: 'error', name: '错误响应' }
|
||||
]
|
||||
|
||||
const responses = {
|
||||
success: {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
body: {
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
id: 123,
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com',
|
||||
phone: '13800138000',
|
||||
created_at: '2024-01-15T10:30:00.000Z'
|
||||
},
|
||||
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
|
||||
timestamp: '2024-01-15T10:30:00.000Z'
|
||||
},
|
||||
fields: [
|
||||
{ name: 'code', type: 'integer', description: '业务状态码,0 表示成功' },
|
||||
{ name: 'message', type: 'string', description: '状态描述,成功时为 "success"' },
|
||||
{ name: 'data', type: 'object', description: '业务数据,成功时返回具体数据' },
|
||||
{ name: 'request_id', type: 'string', description: '请求唯一标识,用于问题追踪' },
|
||||
{ name: 'timestamp', type: 'string', description: '响应时间戳,ISO 8601 格式' }
|
||||
]
|
||||
},
|
||||
pagination: {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
body: {
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
list: [
|
||||
{ id: 1, name: '商品A', price: 100 },
|
||||
{ id: 2, name: '商品B', price: 200 }
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 156,
|
||||
total_pages: 8,
|
||||
has_next: true,
|
||||
has_prev: false
|
||||
}
|
||||
},
|
||||
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
|
||||
timestamp: '2024-01-15T10:30:00.000Z'
|
||||
},
|
||||
fields: [
|
||||
{ name: 'list', type: 'array', description: '数据列表' },
|
||||
{ name: 'pagination', type: 'object', description: '分页信息对象' },
|
||||
{ name: 'page', type: 'integer', description: '当前页码' },
|
||||
{ name: 'page_size', type: 'integer', description: '每页数量' },
|
||||
{ name: 'total', type: 'integer', description: '总记录数' },
|
||||
{ name: 'total_pages', type: 'integer', description: '总页数' },
|
||||
{ name: 'has_next', type: 'boolean', description: '是否有下一页' },
|
||||
{ name: 'has_prev', type: 'boolean', description: '是否有上一页' }
|
||||
]
|
||||
},
|
||||
error: {
|
||||
status: 422,
|
||||
statusText: 'Unprocessable Entity',
|
||||
body: {
|
||||
code: 20003,
|
||||
message: '密码强度不足',
|
||||
errors: [
|
||||
{
|
||||
field: 'password',
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: '密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位'
|
||||
}
|
||||
],
|
||||
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
|
||||
timestamp: '2024-01-15T10:30:00.000Z',
|
||||
help_url: 'https://docs.example.com/errors/20003'
|
||||
},
|
||||
fields: [
|
||||
{ name: 'code', type: 'integer', description: '错误码,非 0 表示失败' },
|
||||
{ name: 'message', type: 'string', description: '错误描述,供用户阅读' },
|
||||
{ name: 'errors', type: 'array', description: '详细错误信息数组(可选)' },
|
||||
{ name: 'help_url', type: 'string', description: '错误文档链接(可选)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTab = ref('success')
|
||||
const currentResponse = computed(() => responses[selectedTab.value])
|
||||
|
||||
function getStatusClass(status) {
|
||||
const prefix = Math.floor(status / 100)
|
||||
switch (prefix) {
|
||||
case 2: return 'success'
|
||||
case 3: return 'redirect'
|
||||
case 4: return 'client-error'
|
||||
case 5: return 'server-error'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
.header {
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.response-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
.tab-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
.tab-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.response-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.status-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.http-version {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
.status-code {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.status-code.success {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.status-code.client-error {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.response-headers {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.header-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-key {
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.header-value {
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.response-body {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.response-body pre {
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.response-body code {
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.field-descriptions {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.field-descriptions h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.field-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.field-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.field-name code {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-type {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
color: var(--vp-c-brand);
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.field-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.response-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,50 +1,413 @@
|
||||
<!--
|
||||
VersioningStrategyDemo.vue - API 版本控制策略演示
|
||||
展示 4 种版本控制策略的对比
|
||||
-->
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<div class="demo">
|
||||
<div class="header">
|
||||
<span class="icon">🔢</span>
|
||||
<span class="title">API 版本控制:向后兼容的艺术</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
版本策略演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
|
||||
<div class="content">
|
||||
<div class="strategies">
|
||||
<div
|
||||
v-for="strategy in strategies"
|
||||
:key="strategy.id"
|
||||
class="strategy-card"
|
||||
:class="{ active: selectedStrategy === strategy.id }"
|
||||
@click="selectedStrategy = strategy.id"
|
||||
>
|
||||
<div class="strategy-header">
|
||||
<div class="strategy-name">{{ strategy.name }}</div>
|
||||
<div class="strategy-stars">
|
||||
<span v-for="n in strategy.stars" :key="n" class="star">⭐</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strategy-example">{{ strategy.example }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-detail" v-if="currentStrategy">
|
||||
<div class="detail-header">
|
||||
<div class="detail-title">{{ currentStrategy.name }}</div>
|
||||
<div class="detail-recommendation" :class="currentStrategy.level">
|
||||
{{ currentStrategy.level === 'high' ? '强烈推荐' : currentStrategy.level === 'medium' ? '可以使用' : '不推荐' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-sections">
|
||||
<div class="detail-section">
|
||||
<h4>✅ 优点</h4>
|
||||
<ul>
|
||||
<li v-for="(pro, idx) in currentStrategy.pros" :key="idx">{{ pro }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>❌ 缺点</h4>
|
||||
<ul>
|
||||
<li v-for="(con, idx) in currentStrategy.cons" :key="idx">{{ con }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section example">
|
||||
<h4>💻 实现示例</h4>
|
||||
<div class="code-box">
|
||||
<div class="code-header">Request</div>
|
||||
<pre><code>{{ currentStrategy.codeExample.request }}</code></pre>
|
||||
<div class="code-header">Response Headers</div>
|
||||
<pre><code>{{ currentStrategy.codeExample.response }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section tips">
|
||||
<h4>💡 最佳实践</h4>
|
||||
<ul>
|
||||
<li v-for="(tip, idx) in currentStrategy.tips" :key="idx">{{ tip }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const title = ref('版本策略演示')
|
||||
const description = ref('展示API版本控制的策略,包括URL版本、Header版本、内容协商等方式')
|
||||
const strategies = [
|
||||
{
|
||||
id: 'url-path',
|
||||
name: 'URL Path 版本',
|
||||
example: '/v1/users',
|
||||
stars: 4,
|
||||
level: 'high',
|
||||
pros: [
|
||||
'最直观,一目了然看到版本号',
|
||||
'易于缓存和控制权限',
|
||||
'文档清晰,社区主流做法',
|
||||
'支持不同版本的并行部署'
|
||||
],
|
||||
cons: [
|
||||
'URL 会变化,不符合 REST 资源唯一性',
|
||||
'需要配置路由规则'
|
||||
],
|
||||
codeExample: {
|
||||
request: `GET /v1/users HTTP/1.1
|
||||
Host: api.example.com`,
|
||||
response: `HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
X-API-Version: v1`
|
||||
},
|
||||
tips: [
|
||||
'版本号放在路径开头:`/v1/users`',
|
||||
'使用语义化版本号(Semantic Versioning)',
|
||||
'废弃版本返回 Sunset 头部',
|
||||
'客户端升级提示可通过响应头提示'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'header',
|
||||
name: 'Header 版本',
|
||||
example: 'API-Version: v1',
|
||||
stars: 2,
|
||||
level: 'medium',
|
||||
pros: [
|
||||
'URL 保持简洁不变',
|
||||
'版本控制不影响路由'
|
||||
],
|
||||
cons: [
|
||||
'不直观,需要在工具里配置 Header',
|
||||
'缓存策略复杂',
|
||||
'文档不够清晰',
|
||||
'调试不便'
|
||||
],
|
||||
codeExample: {
|
||||
request: `GET /users HTTP/1.1
|
||||
Host: api.example.com
|
||||
API-Version: v1`,
|
||||
response: `HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
X-API-Version: v1`
|
||||
},
|
||||
tips: [
|
||||
'使用自定义 Header:`API-Version` 或 `Accept`',
|
||||
'需在 API Gateway 中统一处理',
|
||||
'适合内部系统或对 API 规范要求高的场景'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'content-negotiation',
|
||||
name: '内容协商',
|
||||
example: 'Accept: application/vnd.api.v1+json',
|
||||
stars: 2,
|
||||
level: 'medium',
|
||||
pros: [
|
||||
'符合 HTTP 标准',
|
||||
'URL 完全不变'
|
||||
],
|
||||
cons: [
|
||||
'复杂,理解成本高',
|
||||
'开发者容易用错',
|
||||
'缓存和代理支持不佳'
|
||||
],
|
||||
codeExample: {
|
||||
request: `GET /users HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/vnd.api.v1+json`,
|
||||
response: `HTTP/1.1 200 OK
|
||||
Content-Type: application/vnd.api.v1+json`
|
||||
},
|
||||
tips: [
|
||||
'使用 Vendor MIME 类型:`application/vnd.{company}.{resource}.v{version}+json`',
|
||||
'需要 API Gateway 或框架支持内容协商',
|
||||
'GitHub API 使用此策略'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'query-param',
|
||||
name: 'Query 参数',
|
||||
example: '/users?version=v1',
|
||||
stars: 1,
|
||||
level: 'low',
|
||||
pros: [
|
||||
'实现简单'
|
||||
],
|
||||
cons: [
|
||||
'不专业,容易忽视',
|
||||
'缓存麻烦(不同参数视为不同资源)',
|
||||
'URL 混乱'
|
||||
],
|
||||
codeExample: {
|
||||
request: `GET /users?version=v1 HTTP/1.1
|
||||
Host: api.example.com`,
|
||||
response: `HTTP/1.1 200 OK
|
||||
Content-Type: application/json`
|
||||
},
|
||||
tips: [
|
||||
'仅用于快速原型或内部工具',
|
||||
'生产环境不推荐使用'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const selectedStrategy = ref('url-path')
|
||||
const currentStrategy = computed(() =>
|
||||
strategies.find(s => s.id === selectedStrategy.value)
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
.header {
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.strategies {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.strategy-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.strategy-card:hover {
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.strategy-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.15);
|
||||
}
|
||||
|
||||
.strategy-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.strategy-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
.strategy-stars {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
.star {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.strategy-example {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.strategy-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-recommendation {
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-recommendation.high {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.detail-recommendation.medium {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.detail-recommendation.low {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.detail-sections {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.detail-section ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.detail-section li {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.detail-section.example {
|
||||
grid-column: 1 / -1;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.code-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
padding: 8px 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.code-box pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-box code {
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-section.tips {
|
||||
background: #eff6ff;
|
||||
border-left: 3px solid #3b82f6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.strategies {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-sections {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user