feat: 更新附录交互组件和文档

This commit is contained in:
sanbuphy
2026-02-24 00:18:09 +08:00
parent d45df3cda5
commit 94f9db0834
88 changed files with 11797 additions and 7634 deletions
@@ -1,531 +1,198 @@
<!--
HttpExchangeDemo.vue
HTTP请求响应演示 - 紧凑交互版
设计理念
1. 循循善诱"快递员投递"类比 HTTP 请求响应
2. 紧凑布局横向舞台固定底部详情板
-->
<template>
<div class="http-compact">
<!-- 顶部标题与场景选择 -->
<div class="top-bar">
<div class="title-section">
<span class="app-icon">📦</span>
<span class="app-title">HTTP 请求/响应</span>
</div>
<div class="scenario-tabs">
<button
v-for="s in scenarios"
:key="s.id"
class="tab-btn"
:class="{ active: currentScenario.id === s.id }"
:disabled="isAnimating"
@click="selectScenario(s)"
>
{{ s.name }}
</button>
</div>
<div class="actions">
<button
class="action-btn primary"
@click="toggleAutoPlay"
>
{{ isAutoPlaying ? '⏸' : '▶ 演示' }}
</button>
<button
class="action-btn outline"
@click="reset"
>
</button>
</div>
<div class="http-exchange-demo">
<div class="demo-header">
<span class="title">HTTP 请求/响应</span>
<span class="subtitle">浏览器与服务器的对话</span>
</div>
<!-- 核心可视化舞台 -->
<div class="stage-area">
<!-- 客户端 -->
<div class="actor client">
<div class="avatar-box">
<span class="avatar-icon">🧑💻</span>
<span class="avatar-label">浏览器</span>
<div class="exchange-flow">
<div class="actor browser">
<span class="actor-icon">🧑💻</span>
<span class="actor-name">浏览器</span>
</div>
<div class="messages">
<div class="request-box">
<span class="method">GET</span>
<span class="path">/search?q=hello</span>
<span class="arrow"></span>
</div>
<div class="response-box">
<span class="arrow"></span>
<span class="status">200 OK</span>
<span class="size">HTML 页面</span>
</div>
</div>
<!-- 传输通道 -->
<div class="channel">
<div class="channel-bg" />
<!-- 请求包 -->
<div
class="packet request"
:class="{ moving: step === 1, done: step > 1 }"
>
<span class="packet-icon">📤</span>
<span class="packet-label">GET</span>
</div>
<!-- 响应包 -->
<div
v-if="step >= 2"
class="packet response"
:class="{ moving: step === 2, done: step > 2 }"
>
<span class="packet-icon">📦</span>
<span class="packet-label">{{ currentScenario.status }}</span>
</div>
</div>
<!-- 服务器 -->
<div class="actor server">
<div class="avatar-box">
<span class="avatar-icon">🏢</span>
<span class="avatar-label">服务器</span>
</div>
<span class="actor-icon">🖥</span>
<span class="actor-name">服务器</span>
</div>
</div>
<!-- 底部详情面板 (固定高度) -->
<div class="detail-panel">
<transition
name="fade"
mode="out-in"
>
<div
v-if="step > 0"
:key="step"
class="detail-content"
>
<!-- 左侧状态徽章 -->
<div
class="detail-left"
:style="{ borderColor: getStatusColor() }"
>
<div
class="status-badge"
:class="currentScenario.statusType"
>
{{ step === 1 ? '请求中' : currentScenario.status + ' ' + currentScenario.statusText }}
</div>
</div>
<div class="detail-divider" />
<div class="code-preview">
<div class="code-block">
<div class="code-header">请求</div>
<code>GET /search?q=hello HTTP/1.1</code>
<code>Host: www.google.com</code>
</div>
<div class="code-block">
<div class="code-header">响应</div>
<code>HTTP/1.1 200 OK</code>
<code>Content-Type: text/html</code>
</div>
</div>
<!-- 右侧详情 -->
<div class="detail-right">
<div class="info-row">
<span class="tag life">快递员说</span>
<span class="text highlight">
{{ step === 1 ? currentScenario.requestText : currentScenario.responseText }}
</span>
</div>
<div class="info-row">
<span class="tag tech">技术报文</span>
<span class="text code">
{{ step === 1 ? `${currentScenario.method} ${currentScenario.path} HTTP/1.1` : `HTTP/1.1 ${currentScenario.status} ${currentScenario.statusText}` }}
</span>
</div>
</div>
</div>
<div
v-else
class="detail-placeholder"
>
<span class="guide-bounce">📦</span>
<span>选择一个场景点击"演示"看看发生了什么</span>
</div>
</transition>
<div class="info-box">
<strong>核心思想</strong>
HTTP 是请求-响应模式浏览器发送请求服务器返回状态码和响应内容
</div>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
const scenarios = [
{
id: 'success',
name: '正常送达',
method: 'GET',
path: '/index.html',
requestText: '您好,请给我 index.html 的包裹!',
status: '200',
statusText: 'OK',
statusType: 'success',
responseText: '好的,这是您的 index.html,请签收!',
qa: {
title: '🤔 200 OK 是什么意思?',
content: [
{
q: '200 这个数字代表什么?',
a: '就像快递单上的"已妥投"。代表一切顺利,服务器成功找到了你要的东西并给了你。'
},
{
q: 'GET 是什么?',
a: '就像你对服务员说"给我来一份菜单"。是向服务器"索要"东西的意思。绝大多数网页访问都是 GET 请求。'
}
]
}
},
{
id: 'notfound',
name: '查无此人',
method: 'GET',
path: '/nopage',
requestText: '您好,我要找 nopage 这个人。',
status: '404',
statusText: 'Not Found',
statusType: 'error',
responseText: '抱歉,这里查无此人 (404)。',
qa: {
title: '🤔 为什么叫 404',
content: [
{
q: '是谁的错?',
a: '通常是"你"(客户端)的错。4开头的代码都代表客户端问题,比如你地址输错了,或者这个网页已经被删除了。'
}
]
}
},
{
id: 'redirect',
name: '搬家了',
method: 'GET',
path: '/old-path',
requestText: '您好,送到 old-path 这里。',
status: '301',
statusText: 'Moved',
statusType: 'warn',
responseText: '这里搬家了,请去新地址 (301)。',
qa: {
title: '🤔 301 重定向是什么?',
content: [
{
q: '浏览器会怎么做?',
a: '浏览器收到 301 后,会自动去访问新的地址。这个过程很快,你可能都感觉不到。'
},
{
q: '为什么要重定向?',
a: '就像店铺搬迁要在门口贴个告示。保证收藏了旧网址的老顾客也能找到新店。'
}
]
}
},
{
id: 'servererror',
name: '系统崩溃',
method: 'GET',
path: '/api/data',
requestText: '您好,我要取数据。',
status: '500',
statusText: 'Error',
statusType: 'error',
responseText: '抱歉,仓库塌了,暂时无法取货 (500)。',
qa: {
title: '🤔 500 是谁的错?',
content: [
{
q: '我能修好它吗?',
a: '不能。5开头的代码代表"服务器"出问题了(比如代码崩了、数据库挂了)。跟你没关系,只能等待网站管理员修复。'
}
]
}
}
]
const currentScenario = ref(scenarios[0])
const step = ref(0) // 0: Idle, 1: Requesting, 2: Responding, 3: Done
const isAnimating = ref(false)
const isAutoPlaying = ref(false)
let timer = null
const selectScenario = (s) => {
if (isAnimating.value) return
currentScenario.value = s
reset()
}
const toggleAutoPlay = () => {
if (isAutoPlaying.value) {
reset()
} else {
startAnimation()
}
}
const startAnimation = () => {
if (isAnimating.value) return
isAnimating.value = true
isAutoPlaying.value = true
step.value = 1
// Step 1: Request (Client -> Server)
timer = setTimeout(() => {
step.value = 2
// Step 2: Response (Server -> Client)
timer = setTimeout(() => {
step.value = 3
isAnimating.value = false
isAutoPlaying.value = false
}, 1500)
}, 1500)
}
const reset = () => {
clearTimeout(timer)
step.value = 0
isAnimating.value = false
isAutoPlaying.value = false
}
const getStatusColor = () => {
if (step.value === 1) return '#3b82f6' // Blue for request
const type = currentScenario.value.statusType
if (type === 'success') return '#10b981'
if (type === 'warn') return '#f59e0b'
if (type === 'error') return '#ef4444'
return '#909399'
}
onUnmounted(() => {
clearTimeout(timer)
})
</script>
<style scoped>
.http-compact {
.http-exchange-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
padding: 16px;
margin: 16px 0;
font-size: 14px;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title-section {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.scenario-tabs {
display: flex;
background: var(--vp-c-bg-alt);
padding: 2px;
border-radius: 6px;
gap: 2px;
}
.tab-btn {
padding: 2px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
font-weight: 500;
}
.actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn.primary {
background: var(--vp-c-brand);
color: white;
border: 1px solid var(--vp-c-brand);
}
.action-btn.outline {
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
/* 舞台区 */
.stage-area {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0 30px;
position: relative;
margin-bottom: 20px;
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.demo-header {
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
display: block;
}
.demo-header .subtitle {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.exchange-flow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem 0;
margin-bottom: 0.75rem;
}
.actor {
display: flex;
flex-direction: column;
align-items: center;
width: 60px;
z-index: 2;
gap: 0.25rem;
}
.avatar-icon { font-size: 28px; }
.avatar-label { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.channel {
flex: 1;
height: 40px;
margin: 0 20px;
position: relative;
display: flex;
align-items: center;
.actor-icon {
font-size: 1.5rem;
}
.channel-bg {
position: absolute;
width: 100%;
height: 2px;
background: var(--vp-c-divider);
top: 50%;
.actor-name {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.packet {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
transition: all 1.5s ease-in-out;
opacity: 0;
top: 0;
}
.packet-icon { font-size: 20px; }
.packet-label { font-size: 10px; color: var(--vp-c-text-2); background: var(--vp-c-bg); padding: 0 2px; }
.packet.request { left: 0; opacity: 1; }
.packet.request.moving { left: 100%; transform: translateX(-100%); opacity: 0; }
/* Request moves from 0 to 100% then disappears */
.packet.response { left: 100%; transform: translateX(-100%); opacity: 0; }
.packet.response.moving { left: 0; transform: translateX(0); opacity: 1; }
/* Response starts at 100%, moves to 0 */
/* 动画调整 */
.packet.request.moving {
animation: sendRequest 1.5s forwards;
}
.packet.response.moving {
animation: sendResponse 1.5s forwards;
}
@keyframes sendRequest {
0% { left: 0; opacity: 1; transform: translateX(0); }
90% { left: 100%; opacity: 1; transform: translateX(-100%); }
100% { left: 100%; opacity: 0; transform: translateX(-100%); }
}
@keyframes sendResponse {
0% { left: 100%; opacity: 1; transform: translateX(-100%); }
100% { left: 0; opacity: 1; transform: translateX(0); }
}
/* 详情面板 */
.detail-panel {
height: 80px;
background: var(--vp-c-bg-alt);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
padding: 0 16px;
display: flex;
align-items: center;
overflow: hidden;
}
.detail-content {
display: flex;
width: 100%;
align-items: center;
}
.detail-left {
padding-right: 16px;
border-right: 2px solid transparent;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
color: white;
font-size: 12px;
font-weight: bold;
}
.status-badge.success { background: #10b981; }
.status-badge.warn { background: #f59e0b; }
.status-badge.error { background: #ef4444; }
.detail-divider {
width: 1px;
height: 40px;
background: var(--vp-c-divider);
margin: 0 16px;
}
.detail-right {
.messages {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
gap: 0.4rem;
}
.info-row {
.request-box,
.response-box {
display: flex;
align-items: center;
gap: 8px;
}
.tag {
font-size: 11px;
padding: 1px 6px;
border-radius: 3px;
white-space: nowrap;
}
.tag.life { background: #e6f7ff; color: #1890ff; }
.tag.tech { background: #f6ffed; color: #52c41a; }
.text { font-size: 13px; color: var(--vp-c-text-1); }
.text.highlight { font-weight: 500; color: var(--vp-c-brand); }
.text.code { font-family: monospace; }
.detail-placeholder {
display: flex;
align-items: center;
gap: 8px;
color: var(--vp-c-text-3);
width: 100%;
justify-content: center;
gap: 0.4rem;
padding: 0.4rem 0.6rem;
background: var(--vp-c-bg);
border-radius: 4px;
}
.guide-bounce { animation: bounce 1.5s infinite; }
@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
.method {
font-size: 0.75rem;
font-weight: bold;
color: #2563eb;
font-family: var(--vp-font-family-mono);
}
.path {
font-size: 0.75rem;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.status {
font-size: 0.75rem;
font-weight: bold;
color: #059669;
font-family: var(--vp-font-family-mono);
}
.size {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.arrow {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.code-preview {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.code-block {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0.5rem;
}
.code-header {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.3rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.code-block code {
display: block;
font-size: 0.7rem;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
line-height: 1.5;
}
.info-box {
display: flex;
gap: 0.25rem;
background-color: var(--vp-c-bg-alt);
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
</style>