feat(docs): add interactive demo components for technical appendices

Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation.

Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
This commit is contained in:
sanbuphy
2026-02-06 03:34:50 +08:00
parent e8bba6f7c0
commit 7c70c37072
171 changed files with 69830 additions and 6689 deletions
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
文档演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('API文档演示')
const description = ref('展示RESTful API文档的编写规范和最佳实践,包括Swagger、OpenAPI等工具的使用')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.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 {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,97 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>错误处理演示</h4>
<p class="hint">展示RESTful API中的错误处理机制</p>
</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>
<div class="error-item">
<span class="code">401</span>
<span class="name">Unauthorized</span>
<span class="desc">未授权访问</span>
</div>
<div class="error-item">
<span class="code">404</span>
<span class="name">Not Found</span>
<span class="desc">资源不存在</span>
</div>
<div class="error-item">
<span class="code">500</span>
<span class="name">Server Error</span>
<span class="desc">服务器内部错误</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.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 {
display: flex;
flex-direction: column;
gap: 16px;
}
.error-types {
display: flex;
flex-direction: column;
gap: 12px;
}
.error-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid #f56c6c;
}
.error-item .code {
font-family: monospace;
font-weight: 600;
color: #f56c6c;
font-size: 16px;
min-width: 50px;
}
.error-item .name {
font-weight: 500;
color: var(--vp-c-text-1);
min-width: 120px;
}
.error-item .desc {
color: var(--vp-c-text-2);
font-size: 14px;
}
</style>
@@ -0,0 +1,428 @@
<!--
HttpMethodsDemo.vue - HTTP 方法对比演示组件
展示 GET/POST/PUT/PATCH/DELETE 的区别和使用场景
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">🎯</span>
<span class="title">HTTP 方法用正确的姿势操作资源</span>
</div>
<div class="content">
<!-- HTTP 方法选择器 -->
<div class="method-selector">
<button
v-for="method in methods"
:key="method.name"
class="method-btn"
:class="[method.name, { active: selectedMethod === method.name }]"
@click="selectedMethod = method.name"
>
{{ method.name }}
</button>
</div>
<!-- 当前方法详情 -->
<div class="method-detail" v-if="currentMethod">
<div class="detail-header">
<span class="http-badge" :class="currentMethod.name">{{ currentMethod.name }}</span>
<span class="method-desc">{{ currentMethod.description }}</span>
</div>
<div class="properties">
<div class="property" :class="{ yes: currentMethod.idempotent }">
<span class="prop-icon">{{ currentMethod.idempotent ? '✅' : '❌' }}</span>
<span class="prop-label">幂等性</span>
<span class="prop-hint">{{ currentMethod.idempotent ? '多次执行结果相同' : '每次执行可能产生不同结果' }}</span>
</div>
<div class="property" :class="{ yes: currentMethod.safe }">
<span class="prop-icon">{{ currentMethod.safe ? '✅' : '❌' }}</span>
<span class="prop-label">安全性</span>
<span class="prop-hint">{{ currentMethod.safe ? '不修改服务器状态' : '可能会修改服务器状态' }}</span>
</div>
<div class="property has-body">
<span class="prop-icon">{{ currentMethod.hasBody ? '✅' : '❌' }}</span>
<span class="prop-label">请求体</span>
<span class="prop-hint">{{ currentMethod.hasBody ? '可以携带请求体数据' : '通常不携带请求体' }}</span>
</div>
</div>
<div class="example-section">
<div class="example-title">📝 使用示例</div>
<div class="example-content">
<div class="example-item" v-for="(example, idx) in currentMethod.examples" :key="idx">
<div class="example-scenario">{{ example.scenario }}</div>
<div class="example-request">
<span class="http-method" :class="currentMethod.name">{{ currentMethod.name }}</span>
<span class="request-url">{{ example.url }}</span>
</div>
<div class="example-body" v-if="example.body">
<pre><code>{{ JSON.stringify(example.body, null, 2) }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const methods = [
{
name: 'GET',
description: '获取资源',
idempotent: true,
safe: true,
hasBody: false,
examples: [
{
scenario: '获取用户列表',
url: '/api/v1/users?page=1&page_size=20'
},
{
scenario: '获取单个用户详情',
url: '/api/v1/users/123'
}
]
},
{
name: 'POST',
description: '创建资源',
idempotent: false,
safe: false,
hasBody: true,
examples: [
{
scenario: '创建新用户',
url: '/api/v1/users',
body: {
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000'
}
},
{
scenario: '提交订单',
url: '/api/v1/orders',
body: {
user_id: 123,
items: [
{ product_id: 'P001', quantity: 2 },
{ product_id: 'P002', quantity: 1 }
],
shipping_address: {...}
}
}
]
},
{
name: 'PUT',
description: '全量更新资源',
idempotent: true,
safe: false,
hasBody: true,
examples: [
{
scenario: '更新用户全部信息(替换)',
url: '/api/v1/users/123',
body: {
id: 123,
name: '张三(已更新)',
email: 'zhangsan_new@example.com',
phone: '13900139000',
avatar: 'https://...',
status: 'active'
}
}
]
},
{
name: 'PATCH',
description: '部分更新资源',
idempotent: false,
safe: false,
hasBody: true,
examples: [
{
scenario: '仅修改用户邮箱',
url: '/api/v1/users/123',
body: {
email: 'newemail@example.com'
}
},
{
scenario: '更新多个字段',
url: '/api/v1/users/123',
body: {
phone: '13800138000',
avatar: 'https://...'
}
}
]
},
{
name: 'DELETE',
description: '删除资源',
idempotent: true,
safe: false,
hasBody: false,
examples: [
{
scenario: '删除单个用户',
url: '/api/v1/users/123'
},
{
scenario: '批量删除(通过查询参数)',
url: '/api/v1/users?ids=1,2,3,4,5'
}
]
}
]
const selectedMethod = ref('GET')
const currentMethod = computed(() =>
methods.find(m => m.name === selectedMethod.value)
)
</script>
<style scoped>
.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, #3b82f6 0%, #2563eb 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.method-selector {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--vp-c-divider);
}
.method-btn {
padding: 8px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.method-btn:hover {
transform: translateY(-1px);
}
.method-btn.active {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* HTTP 方法颜色 */
.method-btn.GET, .http-method.GET { border-color: #22c55e; color: #16a34a; }
.method-btn.GET.active, .http-badge.GET { background: #22c55e; color: white; }
.method-btn.POST, .http-method.POST { border-color: #3b82f6; color: #2563eb; }
.method-btn.POST.active, .http-badge.POST { background: #3b82f6; color: white; }
.method-btn.PUT, .http-method.PUT { border-color: #f59e0b; color: #d97706; }
.method-btn.PUT.active, .http-badge.PUT { background: #f59e0b; color: white; }
.method-btn.PATCH, .http-method.PATCH { border-color: #8b5cf6; color: #7c3aed; }
.method-btn.PATCH.active, .http-badge.PATCH { background: #8b5cf6; color: white; }
.method-btn.DELETE, .http-method.DELETE { border-color: #ef4444; color: #dc2626; }
.method-btn.DELETE.active, .http-badge.DELETE { background: #ef4444; color: white; }
.method-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.http-badge {
padding: 6px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
}
.method-desc {
font-size: 15px;
color: var(--vp-c-text-2);
}
.properties {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.property {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
opacity: 0.6;
transition: all 0.2s ease;
}
.property.yes {
opacity: 1;
border-color: #22c55e;
background: #f0fdf4;
}
.property.has-body {
opacity: 1;
}
.property.has-body:not(.yes) {
border-color: #f59e0b;
background: #fffbeb;
}
.prop-icon {
font-size: 20px;
margin-bottom: 4px;
}
.prop-label {
font-weight: 600;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 2px;
}
.prop-hint {
font-size: 11px;
color: var(--vp-c-text-3);
text-align: center;
}
.example-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--vp-c-divider);
}
.example-title {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.example-item {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.example-item:last-child {
margin-bottom: 0;
}
.example-scenario {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.example-request {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.http-method {
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 11px;
background: var(--vp-c-bg);
}
.request-url {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-text-1);
}
.example-body {
background: var(--vp-c-bg);
border-radius: 4px;
padding: 8px;
}
.example-body pre {
margin: 0;
font-size: 11px;
line-height: 1.4;
overflow-x: auto;
}
.example-body code {
font-family: monospace;
color: var(--vp-c-text-1);
}
@media (max-width: 640px) {
.properties {
grid-template-columns: 1fr;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -0,0 +1,145 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>HTTP 请求结构解析</h4>
<p class="hint">详解 HTTP 请求的组成部分</p>
</div>
<div class="demo-content">
<div class="structure-box">
<div class="section request-line">
<div class="label">请求行</div>
<div class="content">
<code>GET /api/users/123 HTTP/1.1</code>
</div>
<div class="explain">
<span>方法</span> + <span>路径</span> + <span>协议版本</span>
</div>
</div>
<div class="section headers">
<div class="label">请求头</div>
<div class="content header-list">
<div><code>Host: api.example.com</code></div>
<div><code>Content-Type: application/json</code></div>
<div><code>Authorization: Bearer token123</code></div>
</div>
<div class="explain">元信息域名数据格式认证等</div>
</div>
<div class="section body">
<div class="label">请求体 (可选)</div>
<div class="content">
<pre>{
"name": "张三",
"email": "zhangsan@example.com"
}</pre>
</div>
<div class="explain">POST/PUT 请求携带的数据</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// 纯静态展示组件
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.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 {
display: flex;
flex-direction: column;
gap: 16px;
}
.structure-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
border-left: 4px solid var(--vp-c-brand);
}
.section.headers {
border-left-color: #67c23a;
}
.section.body {
border-left-color: #e6a23c;
}
.label {
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
font-size: 14px;
}
.content {
background: #1e1e1e;
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
overflow-x: auto;
}
.content code {
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 13px;
}
.content pre {
margin: 0;
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 13px;
line-height: 1.5;
}
.header-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.explain {
font-size: 12px;
color: var(--vp-c-text-2);
}
.explain span {
display: inline-block;
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
margin: 0 2px;
}
</style>
@@ -0,0 +1,103 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>RESTful 资源类比</h4>
<p class="hint">通过生活中的类比理解 RESTful 资源概念</p>
</div>
<div class="demo-content">
<div class="analogy-box">
<div class="analogy-item">
<div class="icon">📚</div>
<div class="text">
<strong>资源 = 图书</strong>
<p>每本书有唯一的 ISBN资源标识</p>
</div>
</div>
<div class="analogy-item">
<div class="icon">🏪</div>
<div class="text">
<strong>URL = 书架位置</strong>
<p>/library/books/123 表示第 123 号书</p>
</div>
</div>
<div class="analogy-item">
<div class="icon">📝</div>
<div class="text">
<strong>HTTP 方法 = 操作</strong>
<p>GET(查看)POST(借书)PUT(修改)DELETE(还书)</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// 静态组件,无需逻辑
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.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 {
display: flex;
flex-direction: column;
gap: 16px;
}
.analogy-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.analogy-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: var(--vp-c-bg);
border-radius: 8px;
}
.icon {
font-size: 24px;
flex-shrink: 0;
}
.text {
flex: 1;
}
.text strong {
display: block;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.text p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
响应结构演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('HTTP响应结构演示')
const description = ref('展示HTTP响应的结构,包括状态行、响应头和响应体')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.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 {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,365 @@
<template>
<div class="restful-design-demo">
<div class="header">
<div class="title">RESTful API 设计核心原则</div>
<div class="subtitle">
RESTRepresentational State Transfer是一种架构风格让接口设计像自然资源一样直观
</div>
</div>
<div class="principles-grid">
<div
v-for="principle in principles"
:key="principle.id"
class="principle-card"
:class="{ active: selectedPrinciple === principle.id }"
@click="selectedPrinciple = principle.id"
>
<div class="principle-icon">{{ principle.icon }}</div>
<div class="principle-name">{{ principle.name }}</div>
<div class="principle-brief">{{ principle.brief }}</div>
</div>
</div>
<div class="detail-panel">
<div class="detail-header">
<span class="detail-title">{{ activePrinciple.name }}</span>
<span class="detail-tag">{{ activePrinciple.tag }}</span>
</div>
<div class="detail-content">
<div class="explanation">
<h4>核心概念</h4>
<p>{{ activePrinciple.explanation }}</p>
</div>
<div class="comparison">
<h4>对比示例</h4>
<div class="code-comparison">
<div class="code-block bad">
<div class="code-label">传统方式不推荐</div>
<pre><code>{{ activePrinciple.badExample }}</code></pre>
</div>
<div class="code-block good">
<div class="code-label">RESTful 方式推荐</div>
<pre><code>{{ activePrinciple.goodExample }}</code></pre>
</div>
</div>
</div>
<div class="tips">
<h4>设计要点</h4>
<ul>
<li v-for="(tip, index) in activePrinciple.tips" :key="index">{{ tip }}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const principles = [
{
id: 'resource',
name: '资源导向',
icon: '📦',
brief: 'URL 表示资源,而非动作',
tag: '核心原则',
explanation: '将系统中的实体抽象为资源(Resource),每个资源对应唯一的 URL。资源是名词,而不是动词或动作。',
badExample: `GET /getUserById?id=123
GET /deleteOrder?orderId=456
POST /createProduct`,
goodExample: `GET /users/123
DELETE /orders/456
POST /products`,
tips: [
'使用名词复数形式(/users 而非 /user',
'避免在 URL 中出现动词(get、create、delete 等)',
'资源层级用路径表示(/users/123/orders',
'资源名使用小写字母,多个单词用连字符(/order-items'
]
},
{
id: 'method',
name: 'HTTP 方法',
icon: '🎯',
brief: '用 HTTP 方法表达操作语义',
tag: '动作表达',
explanation: '使用标准的 HTTP 方法(GET、POST、PUT、DELETE 等)来表示对资源的操作类型,让接口语义更加清晰。',
badExample: `POST /users/query // 查询用户
POST /users/create // 创建用户
POST /users/update // 更新用户
POST /users/delete // 删除用户`,
goodExample: `GET /users // 查询用户列表
POST /users // 创建用户
GET /users/123 // 查询单个用户
PUT /users/123 // 全量更新用户
PATCH /users/123 // 部分更新用户
DELETE /users/123 // 删除用户`,
tips: [
'GET 用于获取资源,是幂等且安全的',
'POST 用于创建资源,返回 201 和新资源 URI',
'PUT 用于全量更新,替换整个资源',
'PATCH 用于部分更新,只修改指定字段',
'DELETE 用于删除资源,返回 204 或 200'
]
},
{
id: 'stateless',
name: '无状态',
icon: '🔄',
brief: '每个请求独立,服务端不保存会话',
tag: '可扩展性',
explanation: '服务端不保存客户端的上下文状态,每个请求都必须包含服务端处理该请求所需的全部信息。这让系统更容易水平扩展。',
badExample: `// 服务端维护会话状态
POST /login
→ 服务端创建 session,返回 session_id cookie
GET /profile
→ 服务端根据 session_id 查找用户
→ 如果会话过期,需要重新登录`,
goodExample: `// 无状态认证
POST /auth/token
→ 验证凭证,返回 JWT token
GET /profile
Authorization: Bearer <token>
→ 服务端验证 token,提取用户信息
→ 请求独立,可随时扩展到多台服务器`,
tips: [
'使用 JWT 或 API Key 进行无状态认证',
'避免在服务端存储会话状态',
'每个请求包含完整的认证信息',
'便于负载均衡和水平扩展',
'使用 Redis 等缓存共享必要的状态数据'
]
},
{
id: 'representation',
name: '统一表现',
icon: '📋',
brief: '使用标准数据格式',
tag: '数据交换',
explanation: '资源的表示(Representation)应该使用标准的数据格式,通常是 JSON。客户端可以通过 Accept 头部请求不同的表示格式。',
badExample: `// 混合格式,字段不一致
GET /users
{
"user_list": [...],
"total_count": 100
}
GET /orders
{
"data": [...],
"pagination": {
"total": 100,
"page": 1
}
}`,
goodExample: `// 统一的响应结构
GET /users
{
"code": 200,
"message": "success",
"data": {
"items": [...],
"pagination": {
"total": 100,
"page": 1,
"page_size": 20
}
},
"timestamp": "2024-01-15T10:30:00Z"
}`,
tips: [
'使用 JSON 作为默认数据格式',
'统一的响应结构(code、message、data',
'支持字段过滤(fields=id,name,email',
'日期使用 ISO 8601 格式',
'字段命名使用 camelCase 或 snake_case,保持一致'
]
}
]
const selectedPrinciple = ref('resource')
const activePrinciple = computed(() =>
principles.find(p => p.id === selectedPrinciple.value) || principles[0]
)
</script>
<style scoped>
.restful-design-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 800;
font-size: 1.25rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin-top: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.principle-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.principle-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
}
.principle-card.active {
border-color: rgba(var(--vp-c-brand-rgb), 0.8);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.15);
}
.principle-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.principle-name {
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.principle-brief {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.detail-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1.25rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 700;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.detail-tag {
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 999px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
color: var(--vp-c-brand);
font-weight: 600;
}
.detail-content h4 {
font-weight: 700;
color: var(--vp-c-text-1);
margin: 1rem 0 0.5rem;
}
.explanation p {
color: var(--vp-c-text-2);
line-height: 1.7;
}
.code-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.code-block {
border-radius: 6px;
overflow: hidden;
}
.code-block.bad {
border: 1px solid #ef4444;
}
.code-block.good {
border: 1px solid #22c55e;
}
.code-label {
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 700;
color: white;
}
.code-block.bad .code-label {
background: #ef4444;
}
.code-block.good .code-label {
background: #22c55e;
}
.code-block pre {
margin: 0;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
font-size: 0.8rem;
line-height: 1.5;
overflow-x: auto;
}
.tips ul {
margin: 0;
padding-left: 1.25rem;
color: var(--vp-c-text-2);
}
.tips li {
margin: 0.4rem 0;
line-height: 1.6;
}
@media (max-width: 768px) {
.principles-grid {
grid-template-columns: repeat(2, 1fr);
}
.code-comparison {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,618 @@
<!--
StatusCodeDemo.vue - HTTP 状态码演示组件
展示常见 HTTP 状态码的含义和使用场景
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">📡</span>
<span class="title">HTTP 状态码服务器的"情绪表达"</span>
</div>
<div class="content">
<div class="category-tabs">
<button
v-for="category in categories"
:key="category.code"
class="category-btn"
:class="[category.class, { active: selectedCategory === category.code }]"
@click="selectedCategory = category.code"
>
<span class="category-code">{{ category.code }}xx</span>
<span class="category-name">{{ category.name }}</span>
</button>
</div>
<div class="status-codes" v-if="filteredCodes.length > 0">
<div
v-for="code in filteredCodes"
:key="code.number"
class="status-card"
:class="{ expanded: expandedCode === code.number }"
@click="toggleExpand(code.number)"
>
<div class="status-header">
<span class="status-number" :class="getCategoryClass(code.number)">{{ code.number }}</span>
<span class="status-name">{{ code.name }}</span>
<span class="expand-icon">{{ expandedCode === code.number ? '▼' : '▶' }}</span>
</div>
<div class="status-detail" v-show="expandedCode === code.number">
<div class="detail-section">
<h4>💡 含义解释</h4>
<p>{{ code.description }}</p>
</div>
<div class="detail-section">
<h4>📝 使用场景</h4>
<ul>
<li v-for="(scenario, idx) in code.scenarios" :key="idx">{{ scenario }}</li>
</ul>
</div>
<div class="detail-section" v-if="code.example">
<h4>💻 示例代码</h4>
<div class="code-example">
<div class="code-request">
<span class="method-badge" :class="getCategoryClass(code.number)">{{ code.example.method }}</span>
<code>{{ code.example.path }}</code>
</div>
<div class="code-response">
<pre><code>{{ JSON.stringify(code.example.response, null, 2) }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const categories = [
{ code: '2', name: '成功', class: 'success' },
{ code: '3', name: '重定向', class: 'redirect' },
{ code: '4', name: '客户端错误', class: 'client-error' },
{ code: '5', name: '服务器错误', class: 'server-error' }
]
const statusCodes = [
{
number: 200,
name: 'OK',
description: '请求已成功处理。这是最常用的成功状态码。',
scenarios: [
'GET 请求成功返回数据',
'POST 请求成功处理但未创建新资源',
'PUT/PATCH 更新成功'
],
example: {
method: 'GET',
path: '/api/v1/users/123',
response: {
code: 0,
data: {
id: 123,
name: '张三',
email: 'zhangsan@example.com'
}
}
}
},
{
number: 201,
name: 'Created',
description: '请求成功处理并创建了新的资源。通常用于 POST 请求。',
scenarios: [
'成功创建用户账号',
'成功创建订单',
'成功上传文件'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 0,
data: {
id: 124,
name: '李四',
created_at: '2024-01-15T10:30:00Z'
}
}
}
},
{
number: 204,
name: 'No Content',
description: '请求成功处理,但响应中没有返回内容。',
scenarios: [
'DELETE 删除成功',
'PUT/PATCH 更新成功但无需返回数据',
'预检请求(OPTIONS)响应'
],
example: {
method: 'DELETE',
path: '/api/v1/users/123',
response: null
}
},
{
number: 301,
name: 'Moved Permanently',
description: '请求的资源已永久移动到新的 URL。',
scenarios: [
'API 版本升级,旧版本废弃',
'网站重构,URL 结构变更',
'资源合并或重命名'
]
},
{
number: 304,
name: 'Not Modified',
description: '资源自上次请求以来未被修改,客户端可使用缓存版本。',
scenarios: [
'客户端带有 If-None-Match 或 If-Modified-Since 头部',
'静态资源缓存优化',
'减少不必要的数据传输'
]
},
{
number: 400,
name: 'Bad Request',
description: '请求语法错误或参数无效,服务器无法理解请求。',
scenarios: [
'请求体格式不正确(如 JSON 语法错误)',
'缺少必填参数',
'参数类型不匹配(字符串传数字)'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 10001,
message: '参数校验失败',
errors: [
{ field: 'email', message: '邮箱格式不正确' }
]
}
}
},
{
number: 401,
name: 'Unauthorized',
description: '请求需要用户身份验证,但未提供或凭证无效。',
scenarios: [
'未登录就访问受保护资源',
'Token 过期或无效',
'缺少 Authorization 头部'
],
example: {
method: 'GET',
path: '/api/v1/user/profile',
response: {
code: 10018,
message: '认证令牌已过期,请重新登录'
}
}
},
{
number: 403,
name: 'Forbidden',
description: '服务器理解请求,但拒绝执行(权限不足)。',
scenarios: [
'已登录但访问了没有权限的资源',
'普通用户尝试访问管理员功能',
'账号被禁用或权限被撤销'
],
example: {
method: 'DELETE',
path: '/api/v1/users/456',
response: {
code: 10021,
message: '权限不足,需要管理员权限才能删除用户'
}
}
},
{
number: 404,
name: 'Not Found',
description: '服务器找不到请求的资源。',
scenarios: [
'URL 拼写错误',
'资源已被删除或不存在',
'API 版本已废弃'
],
example: {
method: 'GET',
path: '/api/v1/users/99999',
response: {
code: 10002,
message: '用户不存在'
}
}
},
{
number: 409,
name: 'Conflict',
description: '请求与服务器当前状态冲突(如资源重复)。',
scenarios: [
'尝试创建已存在的用户(唯一约束冲突)',
'乐观锁版本号不匹配',
'并发修改导致的状态冲突'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 10011,
message: '邮箱已被注册'
}
}
},
{
number: 422,
name: 'Unprocessable Entity',
description: '请求格式正确,但语义上有错误(验证失败)。',
scenarios: [
'请求体 JSON 格式正确,但字段值不符合业务规则',
'密码强度不足',
'余额不足无法完成支付'
],
example: {
method: 'POST',
path: '/api/v1/orders',
response: {
code: 10014,
message: '订单金额不能为负数'
}
}
},
{
number: 429,
name: 'Too Many Requests',
description: '客户端发送请求过多,触发了限流。',
scenarios: [
'短时间内大量请求',
'超出 API 配额限制',
'触发防刷机制'
],
example: {
method: 'GET',
path: '/api/v1/data',
response: {
code: 10005,
message: '请求过于频繁,请 60 秒后重试'
}
}
},
{
number: 500,
name: 'Internal Server Error',
description: '服务器内部错误,无法完成请求。',
scenarios: [
'代码抛出未捕获的异常',
'数据库连接失败',
'依赖服务不可用'
],
example: {
method: 'GET',
path: '/api/v1/users',
response: {
code: 10000,
message: '服务器内部错误,请联系管理员'
}
}
},
{
number: 502,
name: 'Bad Gateway',
description: '网关或代理从上游服务器收到无效响应。',
scenarios: [
'反向代理(Nginx)无法连接到后端服务',
'后端服务崩溃或重启中',
'网关配置错误'
]
},
{
number: 503,
name: 'Service Unavailable',
description: '服务器暂时无法处理请求(维护或过载)。',
scenarios: [
'服务器正在进行维护',
'服务器过载,触发熔断',
'依赖服务大面积故障'
],
example: {
method: 'GET',
path: '/api/v1/status',
response: {
code: 10007,
message: '服务维护中,预计 10 分钟后恢复'
}
}
},
{
number: 504,
name: 'Gateway Timeout',
description: '网关或代理等待上游服务器响应超时。',
scenarios: [
'后端处理时间过长',
'网络延迟或丢包',
'数据库查询超时'
]
}
]
const selectedCategory = ref('2')
const expandedCode = ref(null)
const filteredCodes = computed(() => {
const prefix = selectedCategory.value
return statusCodes.filter(code => {
const codePrefix = Math.floor(code.number / 100).toString()
return codePrefix === prefix
})
})
function getCategoryClass(number) {
const prefix = Math.floor(number / 100)
switch (prefix) {
case 2: return 'success'
case 3: return 'redirect'
case 4: return 'client-error'
case 5: return 'server-error'
default: return ''
}
}
function toggleExpand(number) {
expandedCode.value = expandedCode.value === number ? null : number
}
</script>
<style scoped>
.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, #8b5cf6 0%, #7c3aed 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.category-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.category-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s ease;
min-width: 100px;
}
.category-btn:hover {
transform: translateY(-2px);
}
.category-btn.active {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 分类颜色 */
.category-btn.success, .status-number.success { border-color: #22c55e; color: #16a34a; }
.category-btn.success.active { background: #22c55e; color: white; }
.category-btn.redirect, .status-number.redirect { border-color: #3b82f6; color: #2563eb; }
.category-btn.redirect.active { background: #3b82f6; color: white; }
.category-btn.client-error, .status-number.client-error { border-color: #f59e0b; color: #d97706; }
.category-btn.client-error.active { background: #f59e0b; color: white; }
.category-btn.server-error, .status-number.server-error { border-color: #ef4444; color: #dc2626; }
.category-btn.server-error.active { background: #ef4444; color: white; }
.category-code {
font-size: 18px;
font-weight: 700;
}
.category-name {
font-size: 12px;
margin-top: 4px;
}
.status-codes {
display: flex;
flex-direction: column;
gap: 12px;
}
.status-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
}
.status-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
}
.status-card.expanded {
border-color: rgba(var(--vp-c-brand-rgb), 0.8);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.1);
}
.status-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
}
.status-number {
padding: 6px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
background: var(--vp-c-bg-soft);
border: 1px solid;
}
.status-name {
flex: 1;
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
}
.expand-icon {
font-size: 12px;
color: var(--vp-c-text-3);
}
.status-detail {
padding: 16px;
border-top: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.detail-section {
margin-bottom: 16px;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section h4 {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 8px 0;
}
.detail-section p {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 0;
}
.detail-section ul {
margin: 0;
padding-left: 16px;
}
.detail-section li {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 4px 0;
}
.code-example {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-request {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.method-badge {
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 11px;
background: var(--vp-c-bg);
border: 1px solid;
}
.code-request code {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-text-1);
}
.code-response {
padding: 12px;
background: var(--vp-c-bg);
}
.code-response pre {
margin: 0;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.code-response code {
font-family: monospace;
color: var(--vp-c-text-1);
}
@media (max-width: 640px) {
.category-tabs {
flex-direction: column;
}
.category-btn {
flex-direction: row;
justify-content: space-between;
}
.status-header {
flex-wrap: wrap;
}
.code-request {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
版本策略演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('版本策略演示')
const description = ref('展示API版本控制的策略,包括URL版本、Header版本、内容协商等方式')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.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 {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>