refactor: 重构 api-intro、api-design、transistor-to-cpu 组件为紧凑布局

- 重构 api-intro 7 个 Vue 组件为更紧凑的左右布局
- 重构 api-design 相关组件
- 重构 transistor-to-cpu 相关组件
- 统一使用 demo-root -> demo-header -> demo-layout -> info-box 结构
- 扩写文章内容为 MIT 讲义风格
This commit is contained in:
sanbuphy
2026-02-23 01:50:43 +08:00
parent 2a0fdd3392
commit 1062e2e16f
68 changed files with 4455 additions and 3469 deletions
@@ -14,7 +14,9 @@
</div>
<div class="t-line">
<span class="t-ps">&gt; </span>
<span class="t-typing">{{ typing }}<span class="t-cur"></span></span>
<span class="t-typing"
>{{ typing }}<span class="t-cur"></span></span
>
</div>
</div>
</div>
@@ -23,18 +25,30 @@
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['ar-btn', { 'ar-btn--on': active === op.id, 'ar-btn--dim': !op.ok() }]"
:class="[
'ar-btn',
{ 'ar-btn--on': active === op.id, 'ar-btn--dim': !op.ok() }
]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="ar-btn ar-btn--reset" :disabled="running" @click="reset">重置</button>
<button
class="ar-btn ar-btn--reset"
:disabled="running"
@click="reset"
>
重置
</button>
</div>
</div>
<div class="ar-right">
<div class="ar-flow">
<div class="flow-col flow-client" :class="{ 'flow-highlight': pulseArea === 'client' }">
<div
class="flow-col flow-client"
:class="{ 'flow-highlight': pulseArea === 'client' }"
>
<div class="flow-header">
<span class="flow-icon">💻</span>
<span class="flow-title">客户端</span>
@@ -43,7 +57,9 @@
<div class="flow-body">
<div v-if="requestData" class="req-preview">
<div class="req-line">
<span class="req-method" :class="requestData.method">{{ requestData.method }}</span>
<span class="req-method" :class="requestData.method">{{
requestData.method
}}</span>
<span class="req-url">{{ requestData.url }}</span>
</div>
<div v-if="requestData.body" class="req-body">
@@ -54,12 +70,18 @@
</div>
</div>
<div class="flow-arrow" :class="{ 'arrow-lit': pulseArea === 'request' }">
<div
class="flow-arrow"
:class="{ 'arrow-lit': pulseArea === 'request' }"
>
<code class="arrow-label">HTTP Request</code>
<span class="arrow-symbol"></span>
</div>
<div class="flow-col flow-server" :class="{ 'flow-highlight': pulseArea === 'server' }">
<div
class="flow-col flow-server"
:class="{ 'flow-highlight': pulseArea === 'server' }"
>
<div class="flow-header">
<span class="flow-icon">🖥</span>
<span class="flow-title">服务器</span>
@@ -74,12 +96,18 @@
</div>
</div>
<div class="flow-arrow" :class="{ 'arrow-lit': pulseArea === 'response' }">
<div
class="flow-arrow"
:class="{ 'arrow-lit': pulseArea === 'response' }"
>
<code class="arrow-label">HTTP Response</code>
<span class="arrow-symbol"></span>
</div>
<div class="flow-col flow-response" :class="{ 'flow-highlight': pulseArea === 'response' }">
<div
class="flow-col flow-response"
:class="{ 'flow-highlight': pulseArea === 'response' }"
>
<div class="flow-header">
<span class="flow-icon">📦</span>
<span class="flow-title">响应</span>
@@ -109,7 +137,9 @@
import { ref, nextTick } from 'vue'
const termEl = ref(null)
const lines = ref([{ kind: 'dim', text: '// 点击下方按钮,模拟不同的 API 请求' }])
const lines = ref([
{ kind: 'dim', text: '// 点击下方按钮,模拟不同的 API 请求' }
])
const typing = ref('')
const running = ref(false)
const active = ref(null)
@@ -120,7 +150,7 @@ const requestData = ref(null)
const serverStatus = ref(null)
const responseData = ref(null)
const sleep = ms => new Promise(r => setTimeout(r, ms))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ops = [
{
@@ -132,7 +162,7 @@ const ops = [
{ kind: 'grn', text: 'HTTP/1.1 200 OK' },
{ kind: 'dim', text: 'Content-Type: application/json' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 0, "data": { "items": [...] } }' },
{ kind: 'grn', text: '{ "code": 0, "data": { "items": [...] } }' }
],
hint: 'GET 请求成功!状态码 200 表示请求正常。服务器返回了用户列表数据。',
do: async () => {
@@ -163,7 +193,10 @@ const ops = [
{ kind: 'grn', text: 'HTTP/1.1 201 Created' },
{ kind: 'dim', text: 'Location: /api/users/3' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 0, "data": { "id": 3, "name": "王五" } }' },
{
kind: 'grn',
text: '{ "code": 0, "data": { "id": 3, "name": "王五" } }'
}
],
hint: 'POST 创建成功!状态码 201 表示资源已创建,响应头 Location 指向新资源地址。',
do: async () => {
@@ -199,7 +232,7 @@ const ops = [
{ kind: 'dim', text: '// 获取不存在的用户' },
{ kind: 'red', text: 'HTTP/1.1 404 Not Found' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10002, "message": "用户不存在" }' },
{ kind: 'red', text: '{ "code": 10002, "message": "用户不存在" }' }
],
hint: '404 错误!请求的资源不存在。客户端应该检查请求的 ID 是否正确。',
do: async () => {
@@ -230,7 +263,7 @@ const ops = [
{ kind: 'red', text: 'HTTP/1.1 401 Unauthorized' },
{ kind: 'dim', text: 'WWW-Authenticate: Bearer' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10018, "message": "请先登录" }' },
{ kind: 'red', text: '{ "code": 10018, "message": "请先登录" }' }
],
hint: '401 错误!需要身份认证。客户端应该引导用户登录后再重试。',
do: async () => {
@@ -255,7 +288,7 @@ const ops = [
body: '{\n "code": 10018,\n "message": "请先登录"\n}'
}
}
},
}
]
async function run(op) {
@@ -291,7 +324,9 @@ async function run(op) {
await sleep(120)
hint.value = op.hint
running.value = false
setTimeout(() => { pulseArea.value = null }, 1500)
setTimeout(() => {
pulseArea.value = null
}, 1500)
}
function scroll() {
@@ -342,11 +377,19 @@ function reset() {
}
@media (max-width: 768px) {
.ar-layout { flex-direction: column; }
.ar-right { width: 100%; border-left: none; border-top: 1px solid var(--vp-c-divider); }
.ar-layout {
flex-direction: column;
}
.ar-right {
width: 100%;
border-left: none;
border-top: 1px solid var(--vp-c-divider);
}
}
.ar-terminal { background: #141420; }
.ar-terminal {
background: #141420;
}
.term-bar {
display: flex;
align-items: center;
@@ -354,11 +397,26 @@ function reset() {
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.term-title {
margin-left: 8px;
font-size: 0.72rem;
color: #666;
font-family: monospace;
}
.term-body {
min-height: 120px;
@@ -371,15 +429,41 @@ function reset() {
line-height: 1.65;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #89b4fa; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.t-line {
display: flex;
min-width: min-content;
}
.t-ps {
color: #89b4fa;
flex-shrink: 0;
}
.t-cmd {
color: #cdd6f4;
}
.t-dim {
color: #585b70;
}
.t-grn {
color: #a6e3a1;
}
.t-red {
color: #f38ba8;
}
.t-typing {
color: #cdd6f4;
}
.t-cur {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.ar-btns {
display: flex;
@@ -397,18 +481,38 @@ function reset() {
cursor: pointer;
transition: border-color 0.2s;
}
.ar-btn code { font-size: 0.7rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.ar-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.ar-btn--on { border-color: var(--vp-c-brand) !important; }
.ar-btn--on code { color: var(--vp-c-brand); }
.ar-btn--dim { opacity: 0.3; cursor: not-allowed; }
.ar-btn code {
font-size: 0.7rem;
color: #7f849c;
font-family: monospace;
white-space: nowrap;
}
.ar-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
}
.ar-btn--on {
border-color: var(--vp-c-brand) !important;
}
.ar-btn--on code {
color: var(--vp-c-brand);
}
.ar-btn--dim {
opacity: 0.3;
cursor: not-allowed;
}
.ar-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.ar-btn--reset code { display: none; }
.ar-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.ar-btn--reset code {
display: none;
}
.ar-btn--reset::after {
content: '重置';
font-size: 0.7rem;
color: #585b70;
}
.ar-flow {
display: flex;
@@ -426,15 +530,23 @@ function reset() {
display: flex;
flex-direction: column;
min-height: 60px;
transition: border-color 0.25s, box-shadow 0.25s;
transition:
border-color 0.25s,
box-shadow 0.25s;
}
.flow-col.flow-highlight {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 14%, transparent);
}
.flow-client { border-left: 4px solid #89b4fa; }
.flow-server { border-left: 4px solid #f9e2af; }
.flow-response { border-left: 4px solid #a6e3a1; }
.flow-client {
border-left: 4px solid #89b4fa;
}
.flow-server {
border-left: 4px solid #f9e2af;
}
.flow-response {
border-left: 4px solid #a6e3a1;
}
.flow-header {
padding: 6px 8px;
@@ -444,9 +556,19 @@ function reset() {
align-items: center;
gap: 6px;
}
.flow-icon { font-size: 0.9rem; }
.flow-title { font-weight: 700; font-size: 0.8rem; color: var(--vp-c-text-1); }
.flow-desc { font-size: 0.7rem; color: var(--vp-c-text-3); margin-left: auto; }
.flow-icon {
font-size: 0.9rem;
}
.flow-title {
font-weight: 700;
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.flow-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-left: auto;
}
.flow-body {
padding: 8px 10px;
@@ -459,18 +581,36 @@ function reset() {
font-style: italic;
}
.req-preview, .res-preview { font-size: 0.72rem; }
.req-line { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.req-preview,
.res-preview {
font-size: 0.72rem;
}
.req-line {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.req-method {
padding: 2px 6px;
border-radius: 3px;
font-weight: 700;
font-size: 0.68rem;
}
.req-method.GET { background: #22c55e22; color: #22c55e; }
.req-method.POST { background: #3b82f622; color: #3b82f6; }
.req-url { font-family: monospace; color: var(--vp-c-text-1); }
.req-body pre, .res-body pre {
.req-method.GET {
background: #22c55e22;
color: #22c55e;
}
.req-method.POST {
background: #3b82f622;
color: #3b82f6;
}
.req-url {
font-family: monospace;
color: var(--vp-c-text-1);
}
.req-body pre,
.res-body pre {
margin: 0;
padding: 6px;
background: var(--vp-c-bg-soft);
@@ -486,8 +626,12 @@ function reset() {
gap: 8px;
font-size: 0.8rem;
}
.status-icon { font-size: 1rem; }
.status-text { color: var(--vp-c-text-2); }
.status-icon {
font-size: 1rem;
}
.status-text {
color: var(--vp-c-text-2);
}
.res-status {
display: inline-block;
@@ -497,8 +641,14 @@ function reset() {
font-size: 0.7rem;
margin-bottom: 6px;
}
.res-status.success { background: #22c55e22; color: #22c55e; }
.res-status.error { background: #ef444422; color: #ef4444; }
.res-status.success {
background: #22c55e22;
color: #22c55e;
}
.res-status.error {
background: #ef444422;
color: #ef4444;
}
.flow-arrow {
display: flex;
@@ -509,7 +659,9 @@ function reset() {
opacity: 0.3;
transition: opacity 0.3s;
}
.flow-arrow.arrow-lit { opacity: 1; }
.flow-arrow.arrow-lit {
opacity: 1;
}
.arrow-label {
font-size: 0.65rem;
font-family: monospace;
@@ -22,12 +22,17 @@
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['av-btn', { 'av-btn--on': active === op.id, 'av-btn--dim': !op.ok() }]"
:class="[
'av-btn',
{ 'av-btn--on': active === op.id, 'av-btn--dim': !op.ok() }
]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="av-btn av-btn--reset" :disabled="running" @click="reset">重置</button>
<button class="av-btn av-btn--reset" :disabled="running" @click="reset">
重置
</button>
</div>
<div class="av-versions">
@@ -90,7 +95,7 @@ const active = ref(null)
const activeVersion = ref('')
const hint = ref('点击按钮,了解 API 版本控制的策略和最佳实践。')
const sleep = ms => new Promise(r => setTimeout(r, ms))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ops = [
{
@@ -108,10 +113,12 @@ const ops = [
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '✅ 正确做法:' },
{ kind: 'grn', text: ' /v1/orders - 旧接口,继续服务旧 App' },
{ kind: 'grn', text: ' /v2/orders - 新接口,新功能在这里' },
{ kind: 'grn', text: ' /v2/orders - 新接口,新功能在这里' }
],
hint: '版本控制让新旧客户端都能正常工作。旧 App 用户可以慢慢升级,不会突然崩溃。',
do: () => { activeVersion.value = '' }
do: () => {
activeVersion.value = ''
}
},
{
id: 'url',
@@ -125,10 +132,12 @@ const ops = [
{ kind: 'grn', text: 'GET /v3/users' },
{ kind: 'dim', text: '' },
{ kind: 'dim', text: '优点:直观、易缓存、浏览器友好' },
{ kind: 'dim', text: '缺点:URL 变长' },
{ kind: 'dim', text: '缺点:URL 变长' }
],
hint: 'URL 路径版本是最常用的方式。GitHub、Twitter、Stripe 都用这种方式。',
do: () => { activeVersion.value = 'v1' }
do: () => {
activeVersion.value = 'v1'
}
},
{
id: 'header',
@@ -145,10 +154,12 @@ const ops = [
{ kind: 'grn', text: 'X-API-Version: 2' },
{ kind: 'dim', text: '' },
{ kind: 'dim', text: '优点:URL 干净' },
{ kind: 'dim', text: '缺点:不便调试、缓存复杂' },
{ kind: 'dim', text: '缺点:不便调试、缓存复杂' }
],
hint: 'Header 版本让 URL 更干净,但调试时需要额外设置 Header,不如 URL 版本直观。',
do: () => { activeVersion.value = 'v2' }
do: () => {
activeVersion.value = 'v2'
}
},
{
id: 'query',
@@ -161,10 +172,12 @@ const ops = [
{ kind: 'grn', text: 'GET /users?version=2' },
{ kind: 'dim', text: '' },
{ kind: 'dim', text: '优点:简单、向后兼容' },
{ kind: 'dim', text: '缺点:容易被忽略、不是 RESTful 标准' },
{ kind: 'dim', text: '缺点:容易被忽略、不是 RESTful 标准' }
],
hint: '查询参数版本简单但不够"正规"。适合内部 API 或快速迭代的项目。',
do: () => { activeVersion.value = '' }
do: () => {
activeVersion.value = ''
}
},
{
id: 'best',
@@ -177,11 +190,13 @@ const ops = [
{ kind: 'grn', text: '2. 新功能放新版本,旧版本保持稳定' },
{ kind: 'grn', text: '3. 设置废弃时间线(如 v1 将在 2025-06 废弃)' },
{ kind: 'grn', text: '4. 响应头标注当前版本和废弃信息' },
{ kind: 'grn', text: '5. 文档明确标注每个版本的变更' },
{ kind: 'grn', text: '5. 文档明确标注每个版本的变更' }
],
hint: '版本控制不是"以后再说"的事,从第一天就应该规划好。废弃旧版本要给用户足够的迁移时间。',
do: () => { activeVersion.value = 'v2' }
},
do: () => {
activeVersion.value = 'v2'
}
}
]
async function run(op) {
@@ -239,7 +254,9 @@ function reset() {
font-size: 0.85rem;
}
.av-terminal { background: #141420; }
.av-terminal {
background: #141420;
}
.term-bar {
display: flex;
align-items: center;
@@ -247,11 +264,26 @@ function reset() {
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.term-title {
margin-left: 8px;
font-size: 0.72rem;
color: #666;
font-family: monospace;
}
.term-body {
min-height: 100px;
@@ -264,16 +296,44 @@ function reset() {
line-height: 1.6;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #a6e3a1; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-yel { color: #f9e2af; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.t-line {
display: flex;
min-width: min-content;
}
.t-ps {
color: #a6e3a1;
flex-shrink: 0;
}
.t-cmd {
color: #cdd6f4;
}
.t-dim {
color: #585b70;
}
.t-grn {
color: #a6e3a1;
}
.t-red {
color: #f38ba8;
}
.t-yel {
color: #f9e2af;
}
.t-typing {
color: #cdd6f4;
}
.t-cur {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.av-btns {
display: flex;
@@ -291,18 +351,38 @@ function reset() {
cursor: pointer;
transition: border-color 0.2s;
}
.av-btn code { font-size: 0.68rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.av-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.av-btn--on { border-color: var(--vp-c-brand) !important; }
.av-btn--on code { color: var(--vp-c-brand); }
.av-btn--dim { opacity: 0.3; cursor: not-allowed; }
.av-btn code {
font-size: 0.68rem;
color: #7f849c;
font-family: monospace;
white-space: nowrap;
}
.av-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
}
.av-btn--on {
border-color: var(--vp-c-brand) !important;
}
.av-btn--on code {
color: var(--vp-c-brand);
}
.av-btn--dim {
opacity: 0.3;
cursor: not-allowed;
}
.av-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.av-btn--reset code { display: none; }
.av-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.av-btn--reset code {
display: none;
}
.av-btn--reset::after {
content: '重置';
font-size: 0.7rem;
color: #585b70;
}
.av-versions {
display: flex;
@@ -22,17 +22,20 @@
<div class="compare-row">
<div class="compare-col">
<div class="compare-title">单对象</div>
<pre class="code-sm">{
<pre class="code-sm">
{
"code": 0,
"data": {
"id": 123,
"name": "张三"
}
}</pre>
}</pre
>
</div>
<div class="compare-col">
<div class="compare-title">列表</div>
<pre class="code-sm">{
<pre class="code-sm">
{
"code": 0,
"data": {
"items": [...],
@@ -41,34 +44,27 @@
"total": 100
}
}
}</pre>
}</pre
>
</div>
</div>
<div class="note">列表数据包裹在 items 数组中分页信息放在 pagination 对象</div>
<div class="note">
列表数据包裹在 items 数组中分页信息放在 pagination 对象
</div>
</div>
<div v-if="active === 'naming'" class="section">
<h4>字段命名规范</h4>
<div class="rule-list">
<div
v-for="rule in namingRules"
:key="rule.name"
class="rule-item"
>
<div v-for="rule in namingRules" :key="rule.name" class="rule-item">
<div class="rule-header">
<span class="rule-icon">{{ rule.icon }}</span>
<span class="rule-name">{{ rule.name }}</span>
</div>
<div class="rule-examples">
<code class="good">{{ rule.good }}</code>
<span
v-if="rule.bad"
class="vs"
>vs</span>
<code
v-if="rule.bad"
class="bad"
>{{ rule.bad }}</code>
<span v-if="rule.bad" class="vs">vs</span>
<code v-if="rule.bad" class="bad">{{ rule.bad }}</code>
</div>
<div class="rule-desc">{{ rule.desc }}</div>
</div>
@@ -78,11 +74,13 @@
<div v-if="active === 'datetime'" class="section">
<h4>时间格式设计</h4>
<div class="time-example">
<pre class="code-block">{
<pre class="code-block">
{
"created_at": "2024-01-15T09:30:00.000Z",
"updated_at": "2024-01-15T10:00:00.000Z",
"expired_at": "2025-01-15T00:00:00.000Z"
}</pre>
}</pre
>
</div>
<div class="time-rules">
<div class="time-rule">
@@ -99,7 +97,9 @@
</div>
<div class="time-rule">
<span class="rule-label">命名</span>
<span class="rule-value">xxx_at 表示时间点xxx_duration 表示时长</span>
<span class="rule-value"
>xxx_at 表示时间点xxx_duration 表示时长</span
>
</div>
</div>
</div>
@@ -109,18 +109,22 @@
<div class="compare-row">
<div class="compare-col good-col">
<div class="compare-title"> 推荐</div>
<pre class="code-sm">{
<pre class="code-sm">
{
"name": "张三",
"nickname": null,
"avatar": null
}</pre>
}</pre
>
<div class="compare-desc">字段存在但无值时返回 null</div>
</div>
<div class="compare-col bad-col">
<div class="compare-title"> 不推荐</div>
<pre class="code-sm">{
<pre class="code-sm">
{
"name": "张三"
}</pre>
}</pre
>
<div class="compare-desc">省略字段前端需判断是否存在</div>
</div>
</div>
@@ -152,7 +156,9 @@
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">参考 ISO 8601 时间标准字段命名保持 snake_case 风格</span>
<span class="tips-text"
>参考 ISO 8601 时间标准字段命名保持 snake_case 风格</span
>
</div>
</div>
</template>
@@ -172,11 +178,41 @@ const tabs = [
]
const namingRules = [
{ icon: '🔡', name: '使用 snake_case', good: 'created_at', bad: 'createdAt', desc: 'JSON 字段名统一用下划线' },
{ icon: '📖', name: '避免缩写', good: 'user_id', bad: 'uid', desc: '保持可读性' },
{ icon: '✅', name: '布尔值加前缀', good: 'is_active, has_permission', bad: 'active, permission', desc: '一眼识别布尔类型' },
{ icon: '📅', name: '时间带后缀', good: 'created_at, expired_at', bad: 'created, expired', desc: '明确是时间字段' },
{ icon: '🔢', name: '数量带后缀', good: 'total_count, page_size', bad: 'total, size', desc: '明确是数值类型' }
{
icon: '🔡',
name: '使用 snake_case',
good: 'created_at',
bad: 'createdAt',
desc: 'JSON 字段名统一用下划线'
},
{
icon: '📖',
name: '避免缩写',
good: 'user_id',
bad: 'uid',
desc: '保持可读性'
},
{
icon: '✅',
name: '布尔值加前缀',
good: 'is_active, has_permission',
bad: 'active, permission',
desc: '一眼识别布尔类型'
},
{
icon: '📅',
name: '时间带后缀',
good: 'created_at, expired_at',
bad: 'created, expired',
desc: '明确是时间字段'
},
{
icon: '🔢',
name: '数量带后缀',
good: 'total_count, page_size',
bad: 'total, size',
desc: '明确是数值类型'
}
]
const relations = [
@@ -220,7 +256,7 @@ const relations = [
]
const currentRelation = computed(() => {
return relations.find(r => r.id === rId.value) || relations[0]
return relations.find((r) => r.id === rId.value) || relations[0]
})
</script>
@@ -22,18 +22,25 @@
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['eh-btn', { 'eh-btn--on': active === op.id, 'eh-btn--dim': !op.ok() }]"
:class="[
'eh-btn',
{ 'eh-btn--on': active === op.id, 'eh-btn--dim': !op.ok() }
]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="eh-btn eh-btn--reset" :disabled="running" @click="reset">重置</button>
<button class="eh-btn eh-btn--reset" :disabled="running" @click="reset">
重置
</button>
</div>
<div class="eh-response">
<div class="res-header">
<span class="res-label">响应结构</span>
<span class="res-status" :class="responseStatus">{{ responseStatus }}</span>
<span class="res-status" :class="responseStatus">{{
responseStatus
}}</span>
</div>
<div class="res-body">
<pre v-if="responseData">{{ responseData }}</pre>
@@ -57,7 +64,7 @@ const hint = ref('点击按钮,对比"好的"和"差的"错误响应设计。'
const responseData = ref('')
const responseStatus = ref('')
const sleep = ms => new Promise(r => setTimeout(r, ms))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ops = [
{
@@ -68,7 +75,7 @@ const ops = [
{ kind: 'dim', text: '// HTTP 200 但业务失败' },
{ kind: 'yel', text: 'HTTP/1.1 200 OK' },
{ kind: 'dim', text: '' },
{ kind: 'yel', text: '{ "error": "出错了" }' },
{ kind: 'yel', text: '{ "error": "出错了" }' }
],
hint: '问题:HTTP 状态码说"成功",但业务说"出错"。缓存层会缓存这个"成功"响应,监控系统也发现不了问题。',
do: () => {
@@ -86,7 +93,7 @@ const ops = [
{ kind: 'dim', text: '// 错误信息没有帮助' },
{ kind: 'red', text: 'HTTP/1.1 400 Bad Request' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "message": "参数错误" }' },
{ kind: 'red', text: '{ "message": "参数错误" }' }
],
hint: '问题:客户端不知道哪个参数错了、为什么错。用户只能看到"参数错误",无法修正。',
do: () => {
@@ -104,9 +111,12 @@ const ops = [
{ kind: 'dim', text: '// 500 错误暴露堆栈' },
{ kind: 'red', text: 'HTTP/1.1 500 Internal Server Error' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "error": "TypeError: Cannot read property..." }' },
{
kind: 'red',
text: '{ "error": "TypeError: Cannot read property..." }'
},
{ kind: 'red', text: '{ "stack": "at UserService.login..." }' },
{ kind: 'red', text: '{ "sql": "SELECT * FROM users WHERE..." }' },
{ kind: 'red', text: '{ "sql": "SELECT * FROM users WHERE..." }' }
],
hint: '危险!暴露了代码结构、数据库查询。攻击者可以利用这些信息进行攻击。',
do: () => {
@@ -126,7 +136,7 @@ const ops = [
{ kind: 'dim', text: '// HTTP 状态码准确表达错误类型' },
{ kind: 'grn', text: 'HTTP/1.1 404 Not Found' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 10002, "message": "用户不存在" }' },
{ kind: 'grn', text: '{ "code": 10002, "message": "用户不存在" }' }
],
hint: '正确!404 表示资源不存在,客户端一看就知道问题所在。',
do: () => {
@@ -147,7 +157,7 @@ const ops = [
{ kind: 'grn', text: 'HTTP/1.1 422 Unprocessable Entity' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 20003, "message": "密码强度不足" }' },
{ kind: 'grn', text: '{ "errors": [{ "field": "password", ... }] }' },
{ kind: 'grn', text: '{ "errors": [{ "field": "password", ... }] }' }
],
hint: '正确!提供了错误码、字段级别的错误详情,前端可以精确提示用户。',
do: () => {
@@ -175,7 +185,7 @@ const ops = [
{ kind: 'grn', text: 'HTTP/1.1 500 Internal Server Error' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 10000, "message": "服务器错误" }' },
{ kind: 'grn', text: '{ "error_id": "err-a1b2c3d4" }' },
{ kind: 'grn', text: '{ "error_id": "err-a1b2c3d4" }' }
],
hint: '正确!只返回错误 ID,详细日志记录在服务器。用户反馈错误 ID,技术人员可以快速定位。',
do: () => {
@@ -188,7 +198,7 @@ const ops = [
"help_url": "https://docs.example.com/errors/10000"
}`
}
},
}
]
async function run(op) {
@@ -249,7 +259,9 @@ function reset() {
font-size: 0.85rem;
}
.eh-terminal { background: #141420; }
.eh-terminal {
background: #141420;
}
.term-bar {
display: flex;
align-items: center;
@@ -257,11 +269,26 @@ function reset() {
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.term-title {
margin-left: 8px;
font-size: 0.72rem;
color: #666;
font-family: monospace;
}
.term-body {
min-height: 100px;
@@ -274,16 +301,44 @@ function reset() {
line-height: 1.6;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #89b4fa; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-yel { color: #f9e2af; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.t-line {
display: flex;
min-width: min-content;
}
.t-ps {
color: #89b4fa;
flex-shrink: 0;
}
.t-cmd {
color: #cdd6f4;
}
.t-dim {
color: #585b70;
}
.t-grn {
color: #a6e3a1;
}
.t-red {
color: #f38ba8;
}
.t-yel {
color: #f9e2af;
}
.t-typing {
color: #cdd6f4;
}
.t-cur {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.eh-btns {
display: flex;
@@ -301,18 +356,38 @@ function reset() {
cursor: pointer;
transition: border-color 0.2s;
}
.eh-btn code { font-size: 0.68rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.eh-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.eh-btn--on { border-color: var(--vp-c-brand) !important; }
.eh-btn--on code { color: var(--vp-c-brand); }
.eh-btn--dim { opacity: 0.3; cursor: not-allowed; }
.eh-btn code {
font-size: 0.68rem;
color: #7f849c;
font-family: monospace;
white-space: nowrap;
}
.eh-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
}
.eh-btn--on {
border-color: var(--vp-c-brand) !important;
}
.eh-btn--on code {
color: var(--vp-c-brand);
}
.eh-btn--dim {
opacity: 0.3;
cursor: not-allowed;
}
.eh-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.eh-btn--reset code { display: none; }
.eh-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.eh-btn--reset code {
display: none;
}
.eh-btn--reset::after {
content: '重置';
font-size: 0.7rem;
color: #585b70;
}
.eh-response {
border-top: 1px solid var(--vp-c-divider);
@@ -326,7 +401,11 @@ function reset() {
border-bottom: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
}
.res-label { font-weight: 700; font-size: 0.8rem; color: var(--vp-c-text-1); }
.res-label {
font-weight: 700;
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.res-status {
font-family: monospace;
font-size: 0.72rem;
@@ -334,11 +413,26 @@ function reset() {
padding: 2px 8px;
border-radius: 4px;
}
.res-status\.200\ \(错误\) { background: #f9e2af22; color: #d97706; }
.res-status\.400 { background: #f59e0b22; color: #d97706; }
.res-status\.404 { background: #3b82f622; color: #3b82f6; }
.res-status\.422 { background: #8b5cf622; color: #8b5cf6; }
.res-status\.500 { background: #ef444422; color: #ef4444; }
.res-status\.200\ \(错误\) {
background: #f9e2af22;
color: #d97706;
}
.res-status\.400 {
background: #f59e0b22;
color: #d97706;
}
.res-status\.404 {
background: #3b82f622;
color: #3b82f6;
}
.res-status\.422 {
background: #8b5cf622;
color: #8b5cf6;
}
.res-status\.500 {
background: #ef444422;
color: #ef4444;
}
.res-body {
padding: 12px;
@@ -19,7 +19,8 @@
<div class="content">
<div v-if="active === 'validate'" class="section">
<h4>参数校验错误</h4>
<pre class="code-block">{
<pre class="code-block">
{
"code": 10001,
"message": "参数校验失败",
"data": {
@@ -36,7 +37,8 @@
}
]
}
}</pre>
}</pre
>
<div class="field-tips">
<div class="tip-row">
<code>field</code>
@@ -55,7 +57,8 @@
<div v-if="active === 'business'" class="section">
<h4>业务错误</h4>
<pre class="code-block">{
<pre class="code-block">
{
"code": 20001,
"message": "余额不足",
"data": {
@@ -64,7 +67,8 @@
"shortfall": 49.00,
"suggestion": "请充值后重试"
}
}</pre>
}</pre
>
<div class="business-tips">
<div class="b-tip"> 返回当前状态数据便于前端展示</div>
<div class="b-tip"> 提供 suggestion 给出解决建议</div>
@@ -75,11 +79,7 @@
<div v-if="active === 'layers'" class="section">
<h4>错误码分层设计</h4>
<div class="layer-list">
<div
v-for="layer in layers"
:key="layer.range"
class="layer-item"
>
<div v-for="layer in layers" :key="layer.range" class="layer-item">
<div class="layer-range">{{ layer.range }}</div>
<div class="layer-info">
<div class="layer-name">{{ layer.name }}</div>
@@ -88,7 +88,9 @@
<div class="layer-desc">{{ layer.desc }}</div>
</div>
</div>
<div class="layer-note">错误码从外到内系统 服务 业务 认证 参数</div>
<div class="layer-note">
错误码从外到内系统 服务 业务 认证 参数
</div>
</div>
<div v-if="active === 'http'" class="section">
@@ -164,7 +166,9 @@
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">错误信息要"机器可读 + 人类友好"便于前端统一处理</span>
<span class="tips-text"
>错误信息要"机器可读 + 人类友好"便于前端统一处理</span
>
</div>
</div>
</template>
@@ -184,11 +188,36 @@ const tabs = [
]
const layers = [
{ range: '50001-59999', name: '系统层', example: '50001 数据库异常', desc: '基础设施问题' },
{ range: '40001-49999', name: '服务层', example: '40001 第三方服务超时', desc: '外部依赖问题' },
{ range: '30001-39999', name: '认证层', example: '30001 未登录', desc: '身份权限问题' },
{ range: '20001-29999', name: '业务层', example: '20001 余额不足', desc: '业务规则校验' },
{ range: '10001-19999', name: '参数层', example: '10001 参数缺失', desc: '客户端输入问题' }
{
range: '50001-59999',
name: '系统层',
example: '50001 数据库异常',
desc: '基础设施问题'
},
{
range: '40001-49999',
name: '服务层',
example: '40001 第三方服务超时',
desc: '外部依赖问题'
},
{
range: '30001-39999',
name: '认证层',
example: '30001 未登录',
desc: '身份权限问题'
},
{
range: '20001-29999',
name: '业务层',
example: '20001 余额不足',
desc: '业务规则校验'
},
{
range: '10001-19999',
name: '参数层',
example: '10001 参数缺失',
desc: '客户端输入问题'
}
]
const examples = [
@@ -234,7 +263,7 @@ const examples = [
]
const currentExample = computed(() => {
return examples.find(e => e.id === exId.value) || examples[0]
return examples.find((e) => e.id === exId.value) || examples[0]
})
</script>
@@ -434,7 +463,7 @@ const currentExample = computed(() => {
.http-compare {
flex-direction: column;
}
.http-arrow {
display: none;
}
@@ -29,7 +29,8 @@
{ "result": { "user": {...} } }
// 接口 C
{ "user": {...} }</pre>
{ "user": {...} }</pre
>
<div class="problem-desc">
前端需要针对每个接口单独处理代码冗余容易出错
</div>
@@ -42,7 +43,8 @@
"message": "success",
"data": { ... },
"request_id": "req-xxx"
}</pre>
}</pre
>
</div>
</div>
@@ -139,7 +141,8 @@
"total": 156,
"total_pages": 8,
"has_next": true
}</pre>
}</pre
>
</div>
</div>
</div>
@@ -147,7 +150,9 @@
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">request_id 用于问题追踪建议使用 UUID v4 或雪花算法生成</span>
<span class="tips-text"
>request_id 用于问题追踪建议使用 UUID v4 或雪花算法生成</span
>
</div>
</div>
</template>
@@ -7,11 +7,11 @@
<span class="raf-icon">💻</span>
<span class="raf-title">Client (Browser/App)</span>
</div>
<div class="raf-controls">
<div class="raf-scenarios">
<button
v-for="s in scenarios"
<button
v-for="s in scenarios"
:key="s.id"
:class="['raf-chip', { active: currentScenario.id === s.id }]"
@click="selectScenario(s)"
@@ -24,15 +24,17 @@
<div class="raf-request-box">
<div class="raf-http-line">
<span :class="['raf-method', currentScenario.method]">{{ currentScenario.method }}</span>
<span :class="['raf-method', currentScenario.method]">{{
currentScenario.method
}}</span>
<span class="raf-url">{{ currentScenario.url }}</span>
</div>
<div v-if="currentScenario.body" class="raf-code-block">
{{ JSON.stringify(currentScenario.body, null, 2) }}
</div>
<button
class="raf-send-btn"
@click="sendRequest"
<button
class="raf-send-btn"
@click="sendRequest"
:disabled="processing"
>
{{ processing ? 'Sending...' : 'Send Request' }}
@@ -42,7 +44,9 @@
<div class="raf-response-box" v-if="response">
<div class="raf-status-line">
<span class="raf-label">Response Status:</span>
<span :class="['raf-status-badge', getStatusColor(response.status)]">
<span
:class="['raf-status-badge', getStatusColor(response.status)]"
>
{{ response.status }} {{ response.statusText }}
</span>
</div>
@@ -100,16 +104,46 @@ const logs = ref([])
const logsRef = ref(null)
const db = ref([
{ id: 1, name: "Alice", role: "admin" },
{ id: 2, name: "Bob", role: "user" }
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' }
])
const scenarios = [
{ id: 'get-all', label: 'GET /users', method: 'GET', url: '/api/users', body: null },
{ id: 'get-one', label: 'GET /users/1', method: 'GET', url: '/api/users/1', body: null },
{ id: 'create', label: 'POST /users', method: 'POST', url: '/api/users', body: { name: "Charlie", role: "user" } },
{ id: 'not-found', label: 'GET /users/99', method: 'GET', url: '/api/users/99', body: null },
{ id: 'delete', label: 'DELETE /users/1', method: 'DELETE', url: '/api/users/1', body: null },
{
id: 'get-all',
label: 'GET /users',
method: 'GET',
url: '/api/users',
body: null
},
{
id: 'get-one',
label: 'GET /users/1',
method: 'GET',
url: '/api/users/1',
body: null
},
{
id: 'create',
label: 'POST /users',
method: 'POST',
url: '/api/users',
body: { name: 'Charlie', role: 'user' }
},
{
id: 'not-found',
label: 'GET /users/99',
method: 'GET',
url: '/api/users/99',
body: null
},
{
id: 'delete',
label: 'DELETE /users/1',
method: 'DELETE',
url: '/api/users/1',
body: null
}
]
const currentScenario = ref(scenarios[0])
@@ -137,43 +171,54 @@ function getStatusColor(status) {
async function sendRequest() {
processing.value = true
response.value = null
addLog(`Received ${currentScenario.value.method} ${currentScenario.value.url}`, 'info')
await new Promise(r => setTimeout(r, 600)) // Simulate network latency
addLog(
`Received ${currentScenario.value.method} ${currentScenario.value.url}`,
'info'
)
await new Promise((r) => setTimeout(r, 600)) // Simulate network latency
const { method, url, body } = currentScenario.value
// Router Logic Simulation
if (method === 'GET' && url === '/api/users') {
response.value = { status: 200, statusText: 'OK', body: db.value }
addLog('Matched route: GET /users -> listUsers()', 'success')
}
else if (method === 'GET' && url.match(/\/api\/users\/\d+/)) {
} else if (method === 'GET' && url.match(/\/api\/users\/\d+/)) {
const id = parseInt(url.split('/').pop())
const user = db.value.find(u => u.id === id)
const user = db.value.find((u) => u.id === id)
if (user) {
response.value = { status: 200, statusText: 'OK', body: user }
addLog(`Found user ${id}`, 'success')
} else {
response.value = { status: 404, statusText: 'Not Found', body: { error: "User not found" } }
response.value = {
status: 404,
statusText: 'Not Found',
body: { error: 'User not found' }
}
addLog(`User ${id} not found in DB`, 'error')
}
}
else if (method === 'POST' && url === '/api/users') {
const newUser = { id: Math.max(0, ...db.value.map(u => u.id)) + 1, ...body }
} else if (method === 'POST' && url === '/api/users') {
const newUser = {
id: Math.max(0, ...db.value.map((u) => u.id)) + 1,
...body
}
db.value.push(newUser)
response.value = { status: 201, statusText: 'Created', body: newUser }
addLog(`Created user ${newUser.id}`, 'success')
}
else if (method === 'DELETE' && url.match(/\/api\/users\/\d+/)) {
} else if (method === 'DELETE' && url.match(/\/api\/users\/\d+/)) {
const id = parseInt(url.split('/').pop())
const idx = db.value.findIndex(u => u.id === id)
const idx = db.value.findIndex((u) => u.id === id)
if (idx !== -1) {
db.value.splice(idx, 1)
response.value = { status: 204, statusText: 'No Content', body: null }
addLog(`Deleted user ${id}`, 'success')
} else {
response.value = { status: 404, statusText: 'Not Found', body: { error: "User not found" } }
response.value = {
status: 404,
statusText: 'Not Found',
body: { error: 'User not found' }
}
addLog(`User ${id} not found for deletion`, 'error')
}
}
@@ -198,7 +243,8 @@ async function sendRequest() {
min-height: 400px;
}
.raf-left, .raf-right {
.raf-left,
.raf-right {
flex: 1;
padding: 1.2rem;
display: flex;
@@ -259,7 +305,7 @@ async function sendRequest() {
padding: 1rem;
background: var(--vp-c-bg-soft);
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.raf-http-line {
@@ -274,9 +320,15 @@ async function sendRequest() {
.raf-method {
font-weight: bold;
}
.raf-method.GET { color: #61affe; }
.raf-method.POST { color: #49cc90; }
.raf-method.DELETE { color: #f93e3e; }
.raf-method.GET {
color: #61affe;
}
.raf-method.POST {
color: #49cc90;
}
.raf-method.DELETE {
color: #f93e3e;
}
.raf-code-block {
background: var(--vp-c-bg);
@@ -317,8 +369,14 @@ async function sendRequest() {
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.raf-status-line {
@@ -334,9 +392,18 @@ async function sendRequest() {
font-weight: bold;
margin-left: 8px;
}
.status-success { background: #d1fae5; color: #065f46; }
.status-error { background: #fee2e2; color: #991b1b; }
.status-neutral { background: #f3f4f6; color: #374151; }
.status-success {
background: #d1fae5;
color: #065f46;
}
.status-error {
background: #fee2e2;
color: #991b1b;
}
.status-neutral {
background: #f3f4f6;
color: #374151;
}
.raf-db-view {
display: flex;
@@ -355,9 +422,17 @@ async function sendRequest() {
align-items: center;
}
.raf-db-id { color: var(--vp-c-text-3); font-family: monospace; }
.raf-db-name { font-weight: bold; }
.raf-db-role { color: var(--vp-c-brand); font-size: 0.9em; }
.raf-db-id {
color: var(--vp-c-text-3);
font-family: monospace;
}
.raf-db-name {
font-weight: bold;
}
.raf-db-role {
color: var(--vp-c-brand);
font-size: 0.9em;
}
.raf-logs {
height: 180px;
@@ -377,10 +452,19 @@ async function sendRequest() {
margin-bottom: 4px;
}
.raf-log-time { color: #6b7280; flex-shrink: 0; }
.info { color: #93c5fd; }
.success { color: #86efac; }
.error { color: #fca5a5; }
.raf-log-time {
color: #6b7280;
flex-shrink: 0;
}
.info {
color: #93c5fd;
}
.success {
color: #86efac;
}
.error {
color: #fca5a5;
}
.raf-section-title {
font-size: 12px;
@@ -391,7 +475,9 @@ async function sendRequest() {
color: var(--vp-c-text-3);
margin-top: 1.5rem;
}
.raf-section:first-child .raf-section-title { margin-top: 0; }
.raf-section:first-child .raf-section-title {
margin-top: 0;
}
.raf-empty {
color: var(--vp-c-text-3);
@@ -411,7 +497,12 @@ async function sendRequest() {
}
@media (max-width: 768px) {
.raf-layout { flex-direction: column; }
.raf-left { border-right: none; border-bottom: 1px solid var(--vp-c-divider); }
.raf-layout {
flex-direction: column;
}
.raf-left {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
}
</style>
@@ -22,12 +22,17 @@
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['ru-btn', { 'ru-btn--on': active === op.id, 'ru-btn--dim': !op.ok() }]"
:class="[
'ru-btn',
{ 'ru-btn--on': active === op.id, 'ru-btn--dim': !op.ok() }
]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="ru-btn ru-btn--reset" :disabled="running" @click="reset">重置</button>
<button class="ru-btn ru-btn--reset" :disabled="running" @click="reset">
重置
</button>
</div>
<div class="ru-compare">
@@ -37,7 +42,12 @@
<span class="compare-title">错误示例</span>
</div>
<div class="compare-body">
<div v-for="(item, i) in badExamples" :key="i" class="url-row" :class="{ highlight: item.active }">
<div
v-for="(item, i) in badExamples"
:key="i"
class="url-row"
:class="{ highlight: item.active }"
>
<code class="url-text">{{ item.url }}</code>
<span class="url-reason">{{ item.reason }}</span>
</div>
@@ -50,7 +60,12 @@
<span class="compare-title">正确示例</span>
</div>
<div class="compare-body">
<div v-for="(item, i) in goodExamples" :key="i" class="url-row" :class="{ highlight: item.active }">
<div
v-for="(item, i) in goodExamples"
:key="i"
class="url-row"
:class="{ highlight: item.active }"
>
<code class="url-text">{{ item.url }}</code>
<span class="url-reason">{{ item.reason }}</span>
</div>
@@ -66,7 +81,9 @@
import { ref, nextTick } from 'vue'
const termEl = ref(null)
const lines = ref([{ kind: 'dim', text: '# 对比 RESTful URL 的正确与错误写法' }])
const lines = ref([
{ kind: 'dim', text: '# 对比 RESTful URL 的正确与错误写法' }
])
const typing = ref('')
const running = ref(false)
const active = ref(null)
@@ -77,8 +94,16 @@ const badExamples = ref([
{ url: 'GET /user', reason: '单数形式', active: false },
{ url: 'GET /UserProfiles', reason: '大写字母', active: false },
{ url: 'GET /user_profiles', reason: '下划线连接', active: false },
{ url: 'GET /users/123/orders/456/items/789', reason: '层级过深', active: false },
{ url: 'GET /products/category/phone/price/5000', reason: '过滤条件放路径', active: false },
{
url: 'GET /users/123/orders/456/items/789',
reason: '层级过深',
active: false
},
{
url: 'GET /products/category/phone/price/5000',
reason: '过滤条件放路径',
active: false
}
])
const goodExamples = ref([
@@ -87,10 +112,14 @@ const goodExamples = ref([
{ url: 'GET /user-profiles', reason: '小写 + 连字符', active: false },
{ url: 'GET /user-profiles', reason: '连字符连接', active: false },
{ url: 'GET /users/123/orders', reason: '最多 3 层', active: false },
{ url: 'GET /products?category=phone&price_max=5000', reason: '过滤用查询参数', active: false },
{
url: 'GET /products?category=phone&price_max=5000',
reason: '过滤用查询参数',
active: false
}
])
const sleep = ms => new Promise(r => setTimeout(r, ms))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ops = [
{
@@ -104,7 +133,7 @@ const ops = [
{ kind: 'red', text: '❌ POST /createOrder' },
{ kind: 'grn', text: '✅ GET /users' },
{ kind: 'grn', text: '✅ GET /users/123' },
{ kind: 'grn', text: '✅ POST /orders' },
{ kind: 'grn', text: '✅ POST /orders' }
],
hint: 'URL 是资源的"地址"HTTP 方法已经表达了"操作"。不要在 URL 里重复说"做什么"。',
do: () => {
@@ -122,7 +151,7 @@ const ops = [
{ kind: 'red', text: '❌ GET /order' },
{ kind: 'grn', text: '✅ GET /users' },
{ kind: 'grn', text: '✅ GET /orders' },
{ kind: 'grn', text: '✅ GET /users/123 (获取单个)' },
{ kind: 'grn', text: '✅ GET /users/123 (获取单个)' }
],
hint: '统一用复数,避免 /user 和 /users 混用。获取单个资源时用 /users/123。',
do: () => {
@@ -139,7 +168,7 @@ const ops = [
{ kind: 'red', text: '❌ GET /UserProfiles' },
{ kind: 'red', text: '❌ GET /user_profiles' },
{ kind: 'grn', text: '✅ GET /user-profiles' },
{ kind: 'grn', text: '✅ GET /order-items' },
{ kind: 'grn', text: '✅ GET /order-items' }
],
hint: 'URL 大小写敏感,统一用小写 + 连字符(-)是最安全的做法。',
do: () => {
@@ -158,7 +187,7 @@ const ops = [
{ kind: 'red', text: '❌ /users/123/orders/456/items/789/status' },
{ kind: 'grn', text: '✅ /users/123/orders (用户订单)' },
{ kind: 'grn', text: '✅ /orders/456/items (订单商品)' },
{ kind: 'grn', text: '✅ /order-items/789 (直接访问)' },
{ kind: 'grn', text: '✅ /order-items/789 (直接访问)' }
],
hint: '超过 3 层考虑重构。可以用扁平化路径或查询参数替代深层嵌套。',
do: () => {
@@ -175,14 +204,14 @@ const ops = [
{ kind: 'red', text: '❌ /products/category/phone/price/5000' },
{ kind: 'grn', text: '✅ /products?category=phone&price_max=5000' },
{ kind: 'grn', text: '✅ /products?status=active&sort=created_desc' },
{ kind: 'grn', text: '✅ /products?category=phone,electronics' },
{ kind: 'grn', text: '✅ /products?category=phone,electronics' }
],
hint: '查询参数可以灵活组合,路径则固定不变。过滤、排序、分页都用查询参数。',
do: () => {
badExamples.value[5].active = true
goodExamples.value[5].active = true
}
},
}
]
async function run(op) {
@@ -192,8 +221,8 @@ async function run(op) {
hint.value = ''
typing.value = ''
badExamples.value.forEach(e => e.active = false)
goodExamples.value.forEach(e => e.active = false)
badExamples.value.forEach((e) => (e.active = false))
goodExamples.value.forEach((e) => (e.active = false))
for (const ch of op.cmd) {
typing.value += ch
@@ -225,8 +254,8 @@ function scroll() {
function reset() {
lines.value = [{ kind: 'dim', text: '# 对比 RESTful URL 的正确与错误写法' }]
badExamples.value.forEach(e => e.active = false)
goodExamples.value.forEach(e => e.active = false)
badExamples.value.forEach((e) => (e.active = false))
goodExamples.value.forEach((e) => (e.active = false))
active.value = null
hint.value = '点击命令按钮,查看不同场景下的 URL 设计对比。'
typing.value = ''
@@ -244,7 +273,9 @@ function reset() {
font-size: 0.85rem;
}
.ru-terminal { background: #141420; }
.ru-terminal {
background: #141420;
}
.term-bar {
display: flex;
align-items: center;
@@ -252,11 +283,26 @@ function reset() {
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.term-title {
margin-left: 8px;
font-size: 0.72rem;
color: #666;
font-family: monospace;
}
.term-body {
min-height: 100px;
@@ -269,15 +315,41 @@ function reset() {
line-height: 1.6;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #a6e3a1; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.t-line {
display: flex;
min-width: min-content;
}
.t-ps {
color: #a6e3a1;
flex-shrink: 0;
}
.t-cmd {
color: #cdd6f4;
}
.t-dim {
color: #585b70;
}
.t-grn {
color: #a6e3a1;
}
.t-red {
color: #f38ba8;
}
.t-typing {
color: #cdd6f4;
}
.t-cur {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.ru-btns {
display: flex;
@@ -295,18 +367,38 @@ function reset() {
cursor: pointer;
transition: border-color 0.2s;
}
.ru-btn code { font-size: 0.68rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.ru-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.ru-btn--on { border-color: var(--vp-c-brand) !important; }
.ru-btn--on code { color: var(--vp-c-brand); }
.ru-btn--dim { opacity: 0.3; cursor: not-allowed; }
.ru-btn code {
font-size: 0.68rem;
color: #7f849c;
font-family: monospace;
white-space: nowrap;
}
.ru-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
}
.ru-btn--on {
border-color: var(--vp-c-brand) !important;
}
.ru-btn--on code {
color: var(--vp-c-brand);
}
.ru-btn--dim {
opacity: 0.3;
cursor: not-allowed;
}
.ru-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.ru-btn--reset code { display: none; }
.ru-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.ru-btn--reset code {
display: none;
}
.ru-btn--reset::after {
content: '重置';
font-size: 0.7rem;
color: #585b70;
}
.ru-compare {
display: grid;
@@ -332,8 +424,14 @@ function reset() {
gap: 6px;
margin-bottom: 10px;
}
.compare-icon { font-size: 1rem; }
.compare-title { font-weight: 700; font-size: 0.85rem; color: var(--vp-c-text-1); }
.compare-icon {
font-size: 1rem;
}
.compare-title {
font-weight: 700;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.compare-body {
display: flex;
@@ -346,7 +444,9 @@ function reset() {
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid transparent;
transition: border-color 0.2s, background 0.2s;
transition:
border-color 0.2s,
background 0.2s;
}
.url-row.highlight {
border-color: var(--vp-c-brand);
@@ -22,12 +22,17 @@
v-for="op in ops"
:key="op.id"
:disabled="running || !op.ok()"
:class="['sc-btn', { 'sc-btn--on': active === op.id, 'sc-btn--dim': !op.ok() }]"
:class="[
'sc-btn',
{ 'sc-btn--on': active === op.id, 'sc-btn--dim': !op.ok() }
]"
@click="run(op)"
>
<code>{{ op.cmd }}</code>
</button>
<button class="sc-btn sc-btn--reset" :disabled="running" @click="reset">重置</button>
<button class="sc-btn sc-btn--reset" :disabled="running" @click="reset">
重置
</button>
</div>
<div class="sc-codes">
@@ -37,7 +42,12 @@
<span class="section-title">2xx 成功</span>
</div>
<div class="section-body">
<div v-for="c in successCodes" :key="c.code" class="code-item" :class="{ active: activeCode === c.code }">
<div
v-for="c in successCodes"
:key="c.code"
class="code-item"
:class="{ active: activeCode === c.code }"
>
<span class="code-num">{{ c.code }}</span>
<span class="code-name">{{ c.name }}</span>
<span class="code-desc">{{ c.desc }}</span>
@@ -51,7 +61,12 @@
<span class="section-title">4xx 客户端错误</span>
</div>
<div class="section-body">
<div v-for="c in clientCodes" :key="c.code" class="code-item" :class="{ active: activeCode === c.code }">
<div
v-for="c in clientCodes"
:key="c.code"
class="code-item"
:class="{ active: activeCode === c.code }"
>
<span class="code-num">{{ c.code }}</span>
<span class="code-name">{{ c.name }}</span>
<span class="code-desc">{{ c.desc }}</span>
@@ -65,7 +80,12 @@
<span class="section-title">5xx 服务端错误</span>
</div>
<div class="section-body">
<div v-for="c in serverCodes" :key="c.code" class="code-item" :class="{ active: activeCode === c.code }">
<div
v-for="c in serverCodes"
:key="c.code"
class="code-item"
:class="{ active: activeCode === c.code }"
>
<span class="code-num">{{ c.code }}</span>
<span class="code-name">{{ c.name }}</span>
<span class="code-desc">{{ c.desc }}</span>
@@ -92,7 +112,7 @@ const hint = ref('点击命令按钮,了解常见的 HTTP 状态码。')
const successCodes = ref([
{ code: 200, name: 'OK', desc: '请求成功' },
{ code: 201, name: 'Created', desc: '创建成功' },
{ code: 204, name: 'No Content', desc: '成功但无返回内容' },
{ code: 204, name: 'No Content', desc: '成功但无返回内容' }
])
const clientCodes = ref([
@@ -101,16 +121,16 @@ const clientCodes = ref([
{ code: 403, name: 'Forbidden', desc: '无权限' },
{ code: 404, name: 'Not Found', desc: '资源不存在' },
{ code: 422, name: 'Unprocessable', desc: '语义错误' },
{ code: 429, name: 'Too Many', desc: '请求过多' },
{ code: 429, name: 'Too Many', desc: '请求过多' }
])
const serverCodes = ref([
{ code: 500, name: 'Server Error', desc: '服务器内部错误' },
{ code: 502, name: 'Bad Gateway', desc: '网关错误' },
{ code: 503, name: 'Unavailable', desc: '服务不可用' },
{ code: 503, name: 'Unavailable', desc: '服务不可用' }
])
const sleep = ms => new Promise(r => setTimeout(r, ms))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ops = [
{
@@ -122,10 +142,12 @@ const ops = [
{ kind: 'grn', text: 'HTTP/1.1 200 OK' },
{ kind: 'dim', text: 'Content-Type: application/json' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 0, "data": { ... } }' },
{ kind: 'grn', text: '{ "code": 0, "data": { ... } }' }
],
hint: '200 表示请求成功处理。GET 查询、PUT/PATCH 更新成功时常用。',
do: () => { activeCode.value = 200 }
do: () => {
activeCode.value = 200
}
},
{
id: '201',
@@ -136,10 +158,12 @@ const ops = [
{ kind: 'grn', text: 'HTTP/1.1 201 Created' },
{ kind: 'dim', text: 'Location: /api/users/123' },
{ kind: 'dim', text: '' },
{ kind: 'grn', text: '{ "code": 0, "data": { "id": 123 } }' },
{ kind: 'grn', text: '{ "code": 0, "data": { "id": 123 } }' }
],
hint: '201 表示资源创建成功。响应头 Location 指向新资源的地址。',
do: () => { activeCode.value = 201 }
do: () => {
activeCode.value = 201
}
},
{
id: '400',
@@ -149,10 +173,12 @@ const ops = [
{ kind: 'dim', text: '// 客户端请求有问题' },
{ kind: 'red', text: 'HTTP/1.1 400 Bad Request' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10001, "message": "参数格式错误" }' },
{ kind: 'red', text: '{ "code": 10001, "message": "参数格式错误" }' }
],
hint: '400 表示请求语法错误。比如 JSON 格式不对、缺少必填参数。',
do: () => { activeCode.value = 400 }
do: () => {
activeCode.value = 400
}
},
{
id: '401',
@@ -163,10 +189,12 @@ const ops = [
{ kind: 'red', text: 'HTTP/1.1 401 Unauthorized' },
{ kind: 'dim', text: 'WWW-Authenticate: Bearer' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10018, "message": "请先登录" }' },
{ kind: 'red', text: '{ "code": 10018, "message": "请先登录" }' }
],
hint: '401 表示未认证。Token 过期、未登录时返回,客户端应引导用户登录。',
do: () => { activeCode.value = 401 }
do: () => {
activeCode.value = 401
}
},
{
id: '403',
@@ -176,10 +204,12 @@ const ops = [
{ kind: 'dim', text: '// 已登录但无权限' },
{ kind: 'red', text: 'HTTP/1.1 403 Forbidden' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10021, "message": "需要管理员权限" }' },
{ kind: 'red', text: '{ "code": 10021, "message": "需要管理员权限" }' }
],
hint: '403 表示已认证但无权限。普通用户访问管理员接口时返回。',
do: () => { activeCode.value = 403 }
do: () => {
activeCode.value = 403
}
},
{
id: '404',
@@ -189,10 +219,12 @@ const ops = [
{ kind: 'dim', text: '// 资源不存在' },
{ kind: 'red', text: 'HTTP/1.1 404 Not Found' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10002, "message": "用户不存在" }' },
{ kind: 'red', text: '{ "code": 10002, "message": "用户不存在" }' }
],
hint: '404 表示请求的资源不存在。URL 错误或资源已被删除。',
do: () => { activeCode.value = 404 }
do: () => {
activeCode.value = 404
}
},
{
id: '500',
@@ -202,11 +234,16 @@ const ops = [
{ kind: 'dim', text: '// 服务器内部错误' },
{ kind: 'red', text: 'HTTP/1.1 500 Internal Server Error' },
{ kind: 'dim', text: '' },
{ kind: 'red', text: '{ "code": 10000, "message": "服务器错误,请联系管理员" }' },
{
kind: 'red',
text: '{ "code": 10000, "message": "服务器错误,请联系管理员" }'
}
],
hint: '500 表示服务器内部错误。代码 bug、数据库连接失败等,不要暴露堆栈信息!',
do: () => { activeCode.value = 500 }
},
do: () => {
activeCode.value = 500
}
}
]
async function run(op) {
@@ -265,7 +302,9 @@ function reset() {
font-size: 0.85rem;
}
.sc-terminal { background: #141420; }
.sc-terminal {
background: #141420;
}
.term-bar {
display: flex;
align-items: center;
@@ -273,11 +312,26 @@ function reset() {
padding: 7px 12px;
background: #1e1e2e;
}
.dot { width: 11px; height: 11px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #febc2e; }
.dot.g { background: #28c840; }
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.term-title {
margin-left: 8px;
font-size: 0.72rem;
color: #666;
font-family: monospace;
}
.term-body {
min-height: 90px;
@@ -290,15 +344,41 @@ function reset() {
line-height: 1.6;
color: #cdd6f4;
}
.t-line { display: flex; min-width: min-content; }
.t-ps { color: #89b4fa; flex-shrink: 0; }
.t-cmd { color: #cdd6f4; }
.t-dim { color: #585b70; }
.t-grn { color: #a6e3a1; }
.t-red { color: #f38ba8; }
.t-typing { color: #cdd6f4; }
.t-cur { animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.t-line {
display: flex;
min-width: min-content;
}
.t-ps {
color: #89b4fa;
flex-shrink: 0;
}
.t-cmd {
color: #cdd6f4;
}
.t-dim {
color: #585b70;
}
.t-grn {
color: #a6e3a1;
}
.t-red {
color: #f38ba8;
}
.t-typing {
color: #cdd6f4;
}
.t-cur {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.sc-btns {
display: flex;
@@ -316,18 +396,38 @@ function reset() {
cursor: pointer;
transition: border-color 0.2s;
}
.sc-btn code { font-size: 0.68rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
.sc-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
.sc-btn--on { border-color: var(--vp-c-brand) !important; }
.sc-btn--on code { color: var(--vp-c-brand); }
.sc-btn--dim { opacity: 0.3; cursor: not-allowed; }
.sc-btn code {
font-size: 0.68rem;
color: #7f849c;
font-family: monospace;
white-space: nowrap;
}
.sc-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
}
.sc-btn--on {
border-color: var(--vp-c-brand) !important;
}
.sc-btn--on code {
color: var(--vp-c-brand);
}
.sc-btn--dim {
opacity: 0.3;
cursor: not-allowed;
}
.sc-btn--reset {
background: transparent;
border-color: #313244;
margin-left: auto;
}
.sc-btn--reset code { display: none; }
.sc-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
.sc-btn--reset code {
display: none;
}
.sc-btn--reset::after {
content: '重置';
font-size: 0.7rem;
color: #585b70;
}
.sc-codes {
display: grid;
@@ -351,12 +451,25 @@ function reset() {
font-weight: 700;
font-size: 0.8rem;
}
.section-header.success { background: color-mix(in srgb, #22c55e 8%, var(--vp-c-bg-alt)); color: #22c55e; }
.section-header.client { background: color-mix(in srgb, #f59e0b 8%, var(--vp-c-bg-alt)); color: #d97706; }
.section-header.server { background: color-mix(in srgb, #ef4444 8%, var(--vp-c-bg-alt)); color: #ef4444; }
.section-header.success {
background: color-mix(in srgb, #22c55e 8%, var(--vp-c-bg-alt));
color: #22c55e;
}
.section-header.client {
background: color-mix(in srgb, #f59e0b 8%, var(--vp-c-bg-alt));
color: #d97706;
}
.section-header.server {
background: color-mix(in srgb, #ef4444 8%, var(--vp-c-bg-alt));
color: #ef4444;
}
.section-icon { font-size: 0.9rem; }
.section-title { font-size: 0.75rem; }
.section-icon {
font-size: 0.9rem;
}
.section-title {
font-size: 0.75rem;
}
.section-body {
padding: 8px;
@@ -386,7 +499,9 @@ function reset() {
font-size: 0.75rem;
min-width: 28px;
}
.code-item.active .code-num { color: var(--vp-c-brand); }
.code-item.active .code-num {
color: var(--vp-c-brand);
}
.code-name {
font-size: 0.72rem;
@@ -1,87 +1,90 @@
<!--
ApiConceptDemo.vue
ApiConceptDemo.vue - 紧凑版
目标直观演示 API 的基本要素地址 + 参数
-->
<template>
<div class="demo">
<div class="header">
<div class="demo-root">
<div class="demo-header">
<span class="icon">🔧</span>
<span class="title">互动演示调用 API 需要什么</span>
<span class="title">调用 API 需要什么</span>
</div>
<div class="content">
<div class="step">
<div class="step-header">
<span class="step-num">1</span>
<span class="step-title">地址 (Endpoint) - 告诉服务器你要找谁</span>
</div>
<div class="step-body">
<div class="demo-layout">
<div class="left-panel">
<div class="step">
<div class="step-header">
<span class="step-num">1</span>
<span class="step-title">地址 (Endpoint)</span>
</div>
<div class="url-bar">
<span class="url">https://api.example.com</span>
<span class="url-base">https://api.example.com</span>
<input
v-model="endpoint"
type="text"
class="endpoint-input"
placeholder="/users"
>
/>
</div>
</div>
</div>
<div class="step">
<div class="step-header">
<span class="step-num">2</span>
<span class="step-title">参数 (Params) - 告诉服务器你要什么</span>
</div>
<div class="step-body">
<div class="params-box">
<div class="params-row">
<span class="param-label">页码</span>
<input
v-model.number="page"
type="number"
class="param-input"
min="1"
>
</div>
<div class="params-row">
<span class="param-label">每页数量</span>
<input
v-model.number="limit"
type="number"
class="param-input small"
min="1"
max="100"
>
</div>
<div class="step">
<div class="step-header">
<span class="step-num">2</span>
<span class="step-title">参数 (Params)</span>
</div>
<div class="params-row">
<label>页码:</label>
<input
v-model.number="page"
type="number"
class="param-input"
min="1"
/>
<label>每页:</label>
<input
v-model.number="limit"
type="number"
class="param-input"
min="1"
max="100"
/>
</div>
</div>
<button class="send-btn" :disabled="loading" @click="sendRequest">
{{ loading ? '发送中...' : '🚀 发送请求' }}
</button>
</div>
<button
class="send-btn"
:disabled="loading"
@click="sendRequest"
>
{{ loading ? '发送中...' : '🚀 发送请求' }}
</button>
<div
v-if="response"
class="response"
>
<div class="right-panel">
<div class="response-header">
<span
v-if="response"
class="status-badge"
:class="response.status >= 200 && response.status < 300 ? 'success' : 'error'"
:class="
response.status >= 200 && response.status < 300
? 'success'
: 'error'
"
>
{{ response.status }} {{ response.statusText }}
</span>
<span class="response-time">耗时: {{ response.time }}ms</span>
<span v-else class="status-badge pending">等待请求</span>
</div>
<pre class="response-body">{{ JSON.stringify(response.data, null, 2) }}</pre>
<div v-if="response" class="response-body">
<pre>{{ JSON.stringify(response.data, null, 2) }}</pre>
</div>
<div v-else class="response-empty">点击发送按钮查看结果</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span
>无论哪种 API结构都一样地址找谁+ 参数要什么=
响应得到什么</span
>
</div>
</div>
</template>
@@ -99,130 +102,140 @@ function sendRequest() {
response.value = null
setTimeout(() => {
const startTime = Date.now()
if (endpoint.value === '/users') {
// 限制最多返回3条,避免结果太长
const actualLimit = Math.min(limit.value, 3)
const users = []
for (let i = 1; i <= actualLimit; i++) {
users.push({
id: i,
name: `用户${(page.value - 1) * limit.value + i}`,
age: 20 + i
name: `用户${(page.value - 1) * limit.value + i}`
})
}
response.value = {
status: 200,
statusText: 'OK',
time: Date.now() - startTime,
data: {
users,
total: 100,
page: page.value,
limit: limit.value,
note: limit.value > 3 ? `仅显示前3条,共${limit.value}` : null
}
data: { users, total: 100, page: page.value }
}
} else {
response.value = {
status: 404,
statusText: 'Not Found',
time: Date.now() - startTime,
data: { error: '找不到这个接口' }
}
}
loading.value = false
}, 300 + Math.random() * 200)
}, 300)
}
</script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
padding: 14px 20px;
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
}
.icon {
font-size: 20px;
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 15px;
font-size: 0.9rem;
}
.content {
padding: 20px;
.demo-layout {
display: flex;
}
.left-panel {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
gap: 16px;
gap: 10px;
border-right: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 220px;
padding: 12px;
background: var(--vp-c-bg);
display: flex;
flex-direction: column;
gap: 8px;
}
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.left-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 100%;
}
}
.step {
background: var(--vp-c-bg);
border-radius: 10px;
border-radius: 6px;
overflow: hidden;
}
.step-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: var(--vp-c-bg-soft);
gap: 8px;
padding: 6px 10px;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.step-num {
width: 22px;
height: 22px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-size: 0.7rem;
font-weight: bold;
}
.step-title {
font-size: 13px;
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.step-body {
padding: 14px;
}
.url-bar {
display: flex;
align-items: center;
gap: 8px;
gap: 4px;
background: #1e293b;
padding: 10px 12px;
border-radius: 6px;
padding: 8px 10px;
border-radius: 0 0 6px 6px;
}
.url {
.url-base {
color: #94a3b8;
font-size: 13px;
font-size: 0.7rem;
white-space: nowrap;
}
.endpoint-input {
@@ -231,73 +244,58 @@ function sendRequest() {
border: none;
color: #60a5fa;
font-family: monospace;
font-size: 14px;
font-size: 0.8rem;
outline: none;
}
.params-box {
display: flex;
flex-direction: column;
gap: 10px;
}
.params-row {
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
padding: 8px 10px;
background: var(--vp-c-bg-soft);
border-radius: 0 0 6px 6px;
}
.param-label {
font-size: 13px;
.params-row label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
min-width: 70px;
}
.param-input {
padding: 6px 10px;
width: 50px;
padding: 4px 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
font-size: 13px;
}
.param-input.small {
width: 60px;
border-radius: 4px;
font-size: 0.8rem;
background: var(--vp-c-bg);
}
.send-btn {
padding: 12px;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: opacity 0.2s;
}
.send-btn:disabled {
opacity: 0.7;
opacity: 0.6;
cursor: not-allowed;
}
.response {
margin-top: 8px;
animation: fadeIn 0.3s ease;
}
.response-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
justify-content: center;
}
.status-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-size: 0.75rem;
font-weight: bold;
}
@@ -311,32 +309,50 @@ function sendRequest() {
color: #991b1b;
}
.response-time {
font-size: 12px;
.status-badge.pending {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-3);
}
.response-body {
flex: 1;
background: #1e293b;
color: #e2e8f0;
padding: 12px;
border-radius: 6px;
font-family: monospace;
font-size: 12px;
overflow-x: auto;
max-height: 200px;
margin: 0;
padding: 8px;
overflow: auto;
max-height: 120px;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
.response-body pre {
margin: 0;
font-family: monospace;
font-size: 0.7rem;
color: #e2e8f0;
white-space: pre-wrap;
}
.response-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-3);
font-size: 0.8rem;
font-style: italic;
}
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,269 +1,241 @@
<!--
ApiDocumentDemo.vue - 翻译
目标一键把"黑话"翻译成"人话"
ApiDocumentDemo.vue - 紧凑
目标演示如何阅读 API 文档
-->
<template>
<div class="demo">
<div class="header">
<div class="title-area">
<span class="icon">📖</span>
<span class="title">API 文档翻译机</span>
</div>
<button
class="translate-btn"
@click="isHuman = !isHuman"
>
{{ isHuman ? '🔄 还原回黑话' : '✨ 翻译成人话' }}
</button>
<div class="demo-root">
<div class="demo-header">
<span class="icon">📖</span>
<span class="title">API 文档翻译机</span>
</div>
<div class="doc-container">
<!-- 模拟 API 文档 -->
<div class="api-doc">
<div class="doc-row method-row">
<span class="label">Method:</span>
<span
class="value method"
:class="{ human: isHuman }"
>
{{ isHuman ? '我要下单 (POST)' : 'POST' }}
</span>
<div class="demo-layout">
<div class="left-panel">
<div class="doc-section">
<div class="doc-title">Base URL</div>
<code class="doc-code">https://api.deepseek.com</code>
</div>
<div class="doc-row url-row">
<span class="label">Endpoint:</span>
<span
class="value url"
:class="{ human: isHuman }"
>
{{ isHuman ? '去哪里找厨师' : 'https://api.deepseek.com/chat' }}
</span>
<div class="doc-section">
<div class="doc-title">Endpoint</div>
<code class="doc-code">POST /v1/chat/completions</code>
</div>
<div class="doc-row headers-row">
<span class="label">Headers:</span>
<div
class="code-block"
:class="{ human: isHuman }"
<div class="doc-section">
<div class="doc-title">Headers</div>
<pre class="doc-pre">
Authorization: Bearer sk-xxx
Content-Type: application/json</pre
>
<div class="line">
<span class="key">{{
isHuman ? '我是谁:' : 'Authorization:'
}}</span>
<span class="val">{{
isHuman ? ' 这是我的会员卡号' : ' Bearer sk-8f9s...'
}}</span>
</div>
<div class="line">
<span class="key">{{
isHuman ? '用什么语言:' : 'Content-Type:'
}}</span>
<span class="val">{{
isHuman ? ' 标准格式(JSON)' : ' application/json'
}}</span>
</div>
</div>
</div>
<div class="doc-row body-row">
<span class="label">Body:</span>
<div
class="code-block"
:class="{ human: isHuman }"
>
<div class="line">
{
<div class="doc-section">
<div class="doc-title">Body 参数</div>
<div class="params-list">
<div class="param-item">
<span class="p-name">model</span>
<span class="p-req">必填</span>
<span class="p-desc">模型名称</span>
</div>
<div class="line indent">
<span class="key">"model":</span>
<span class="val">"deepseek-chat",</span>
<span
v-if="isHuman"
class="comment"
> // 选个聪明的厨师</span>
<div class="param-item">
<span class="p-name">messages</span>
<span class="p-req">必填</span>
<span class="p-desc">对话消息</span>
</div>
<div class="line indent">
<span class="key">"messages":</span>
<span class="val">[...]</span>
<span
v-if="isHuman"
class="comment"
> // 我要说的话</span>
</div>
<div class="line">
}
</div>
</div>
</div>
<div class="doc-row response-row">
<span class="label">Response:</span>
<div
class="code-block"
:class="{ human: isHuman }"
>
<div class="line">
<span class="key">{{ isHuman ? '状态:' : 'Status:' }}</span>
<span class="status-ok">{{
isHuman ? '搞定了 (200)' : '200 OK'
}}</span>
<div class="param-item">
<span class="p-name">temperature</span>
<span class="p-opt">可选</span>
<span class="p-desc">0-2默认1</span>
</div>
</div>
</div>
</div>
<div class="right-panel">
<div class="result-title">翻译成代码</div>
<pre class="result-code"><code>from openai import OpenAI
client = OpenAI(
api_key="sk-xxx",
base_url="https://api.deepseek.com"
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "你好"}]
)</code></pre>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span
>看文档找三样地址Base
URL鉴权Authorization参数Parameters</span
>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isHuman = ref(false)
</script>
<script setup></script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
padding: 16px 20px;
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
}
.title-area {
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
}
.icon {
font-size: 24px;
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 16px;
font-size: 0.9rem;
}
.translate-btn {
background: var(--vp-c-brand-1);
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.translate-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.doc-container {
padding: 20px;
}
.api-doc {
background: #1e293b;
border-radius: 6px;
padding: 20px;
color: #e2e8f0;
font-family: monospace;
font-size: 14px;
}
.doc-row {
.demo-layout {
display: flex;
margin-bottom: 16px;
align-items: flex-start;
}
.doc-row:last-child {
margin-bottom: 0;
}
.label {
width: 80px;
color: #94a3b8;
font-weight: bold;
flex-shrink: 0;
padding-top: 2px;
}
.value {
color: #38bdf8;
transition: all 0.3s;
}
.method {
font-weight: bold;
color: #eab308;
}
.method.human {
color: #fbbf24;
background: rgba(251, 191, 36, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
.url.human {
color: #38bdf8;
background: rgba(56, 189, 248, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
.code-block {
.left-panel {
flex: 1;
background: #0f172a;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
border-right: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 280px;
padding: 12px;
background: var(--vp-c-bg);
}
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.left-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 100%;
}
}
.doc-section {
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid #334155;
transition: all 0.3s;
overflow: hidden;
}
.code-block.human {
.doc-title {
padding: 6px 10px;
background: var(--vp-c-bg-alt);
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.doc-code {
display: block;
padding: 8px 10px;
font-family: monospace;
font-size: 0.8rem;
color: var(--vp-c-brand);
}
.doc-pre {
margin: 0;
padding: 8px 10px;
font-family: monospace;
font-size: 0.75rem;
color: var(--vp-c-text-1);
}
.params-list {
padding: 6px 10px;
}
.param-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-size: 0.75rem;
}
.p-name {
font-family: monospace;
font-weight: 600;
}
.p-req {
background: #fee2e2;
color: #991b1b;
padding: 1px 6px;
border-radius: 3px;
font-size: 0.65rem;
}
.p-opt {
background: #dbeafe;
color: #1e40af;
padding: 1px 6px;
border-radius: 3px;
font-size: 0.65rem;
}
.p-desc {
color: var(--vp-c-text-3);
}
.result-title {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 8px;
}
.result-code {
margin: 0;
padding: 10px;
background: #1e293b;
border-color: #64748b;
border-radius: 6px;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.7rem;
line-height: 1.5;
color: #e2e8f0;
overflow-x: auto;
}
.line {
line-height: 1.6;
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.indent {
padding-left: 20px;
}
.key {
color: #94a3b8;
}
.val {
color: #a5f3fc;
}
.comment {
color: #22c55e;
font-style: italic;
}
.status-ok {
color: #22c55e;
font-weight: bold;
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,422 +1,242 @@
<!--
ApiMethodDemo.vue
目标清晰展示各种 HTTP 方法的含义和使用场景
ApiMethodDemo.vue - 紧凑版
目标展示 HTTP 方法的语义
-->
<template>
<div class="demo">
<div class="title">
🔍 HTTP 方法GETPOSTPUTDELETE 是什么
<div class="demo-root">
<div class="demo-header">
<span class="icon">📋</span>
<span class="title">HTTP 方法告诉服务器你想做什么</span>
</div>
<p class="subtitle">
把它们想象成对数据的"操作方式"
</p>
<div class="methods-grid">
<div class="method-card get">
<div class="method-badge">
GET
</div>
<div class="method-title">
📖 获取查询
</div>
<div class="method-desc">
<p><strong>只看不改</strong> - 从服务器获取数据</p>
<div class="method-examples">
<div class="example-item">
查询用户信息
</div>
<div class="example-item">
搜索商品
</div>
<div class="example-item">
获取文章列表
</div>
<div class="demo-layout">
<div class="methods-grid">
<div
v-for="m in methods"
:key="m.name"
class="method-card"
:class="{ active: selected === m.name }"
@click="selected = m.name"
>
<div class="m-badge" :class="m.color">
{{ m.name }}
</div>
<div class="m-desc">
{{ m.desc }}
</div>
<div class="m-example">
{{ m.example }}
</div>
</div>
<div class="method-tip">
💡 可以安全重试不会改变服务器数据
</div>
</div>
<div class="method-card post">
<div class="method-badge">
POST
<div class="right-panel">
<div class="compare-header">对比幂等性</div>
<div class="compare-row">
<span class="c-label">GET</span>
<span class="c-val">查询10次 = 查询1次 </span>
</div>
<div class="method-title">
创建新增
<div class="compare-row">
<span class="c-label">DELETE</span>
<span class="c-val">删除10次 = 删除1次 </span>
</div>
<div class="method-desc">
<p><strong>新建数据</strong> - 在服务器创建新资源</p>
<div class="method-examples">
<div class="example-item">
创建新用户
</div>
<div class="example-item">
提交订单
</div>
<div class="example-item">
发表评论
</div>
</div>
</div>
<div class="method-tip">
不能随意重试可能会重复创建
</div>
</div>
<div class="method-card put">
<div class="method-badge">
PUT
</div>
<div class="method-title">
更新替换
</div>
<div class="method-desc">
<p><strong>整体替换</strong> - 用新数据完全替换旧数据</p>
<div class="method-examples">
<div class="example-item">
修改用户全部信息
</div>
<div class="example-item">
更换文章全部内容
</div>
</div>
</div>
<div class="method-tip">
会覆盖整个资源需要提供完整数据
</div>
</div>
<div class="method-card patch">
<div class="method-badge">
PATCH
</div>
<div class="method-title">
🔧 修改部分
</div>
<div class="method-desc">
<p><strong>局部更新</strong> - 只修改资源的部分字段</p>
<div class="method-examples">
<div class="example-item">
只修改用户昵称
</div>
<div class="example-item">
更新文章标题
</div>
</div>
</div>
<div class="method-tip">
💡 只传需要修改的字段更灵活
</div>
</div>
<div class="method-card delete">
<div class="method-badge">
DELETE
</div>
<div class="method-title">
🗑 删除
</div>
<div class="method-desc">
<p><strong>移除数据</strong> - 从服务器删除资源</p>
<div class="method-examples">
<div class="example-item">
删除用户
</div>
<div class="example-item">
取消订单
</div>
<div class="example-item">
删除评论
</div>
</div>
</div>
<div class="method-tip">
操作不可逆删除后无法恢复
<div class="compare-row warn">
<span class="c-label">POST</span>
<span class="c-val">下单10次 = 10个订单 </span>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title">
📊 快速对比
</div>
<table>
<thead>
<tr>
<th>方法</th>
<th>操作</th>
<th>是否会改数据</th>
<th>能否重试</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="badge get">GET</span></td>
<td>查询</td>
<td> </td>
<td> 可以</td>
</tr>
<tr>
<td><span class="badge post">POST</span></td>
<td>创建</td>
<td> </td>
<td> 不建议</td>
</tr>
<tr>
<td><span class="badge put">PUT</span></td>
<td>替换</td>
<td> </td>
<td> 不建议</td>
</tr>
<tr>
<td><span class="badge patch">PATCH</span></td>
<td>修改</td>
<td> </td>
<td> 不建议</td>
</tr>
<tr>
<td><span class="badge delete">DELETE</span></td>
<td>删除</td>
<td> </td>
<td> 不建议</td>
</tr>
</tbody>
</table>
</div>
<div class="tips">
<p><strong>💡 新手建议</strong></p>
<ul>
<li>
先学会 <strong>GET</strong>查询
<strong>POST</strong>创建就够用了
</li>
<li>PUT/PATCH/DELETE 可以慢慢学理解原理更重要</li>
<li>实际开发中GET POST 占了 80% 的使用场景</li>
</ul>
<div class="info-box">
<strong>核心思想</strong>
<span
>HTTP 方法就是动词GET "问"POST "做"PUT/PATCH "改"DELETE
"删"</span
>
</div>
</div>
</template>
<script setup>
// 无需脚本逻辑
import { ref } from 'vue'
const selected = ref('GET')
const methods = [
{ name: 'GET', desc: '获取数据', example: 'GET /users', color: 'green' },
{ name: 'POST', desc: '创建数据', example: 'POST /users', color: 'blue' },
{ name: 'PUT', desc: '替换数据', example: 'PUT /users/1', color: 'orange' },
{
name: 'PATCH',
desc: '部分修改',
example: 'PATCH /users/1',
color: 'yellow'
},
{ name: 'DELETE', desc: '删除数据', example: 'DELETE /users/1', color: 'red' }
]
</script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 16px 0;
margin: 1rem 0;
font-size: 0.85rem;
}
.title {
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
}
.icon {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.title {
font-weight: 600;
font-size: 0.9rem;
}
.subtitle {
color: var(--vp-c-text-2);
margin-bottom: 24px;
.demo-layout {
display: flex;
}
.methods-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
flex: 1;
display: flex;
gap: 8px;
padding: 12px;
overflow-x: auto;
}
.right-panel {
width: 200px;
padding: 12px;
background: var(--vp-c-bg);
border-left: 1px solid var(--vp-c-divider);
}
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.methods-grid {
flex-wrap: wrap;
}
.right-panel {
width: 100%;
border-left: none;
border-top: 1px solid var(--vp-c-divider);
}
}
.method-card {
flex: 1;
min-width: 100px;
padding: 10px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
position: relative;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.method-card.get {
border-color: #3b82f6;
}
.method-card.post {
border-color: #22c55e;
}
.method-card.put {
border-color: #f59e0b;
}
.method-card.patch {
border-color: #8b5cf6;
}
.method-card.delete {
border-color: #ef4444;
}
.method-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 6px;
font-weight: bold;
font-size: 12px;
color: white;
}
.method-card.get .method-badge {
background: #3b82f6;
}
.method-card.post .method-badge {
background: #22c55e;
}
.method-card.put .method-badge {
background: #f59e0b;
}
.method-card.patch .method-badge {
background: #8b5cf6;
}
.method-card.delete .method-badge {
background: #ef4444;
}
.method-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
padding-right: 60px;
color: var(--vp-c-text-1);
}
.method-desc p {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
}
.method-examples {
.method-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
padding: 12px;
border-radius: 6px;
margin-bottom: 12px;
}
.example-item {
font-size: 13px;
padding: 4px 0;
color: var(--vp-c-text-1);
.m-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
margin-bottom: 6px;
}
.method-tip {
padding: 10px 12px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
.m-badge.green {
background: #dcfce7;
color: #166534;
}
.method-card.get .method-tip {
background: #eff6ff;
.m-badge.blue {
background: #dbeafe;
color: #1e40af;
}
.method-card.post .method-tip,
.method-card.put .method-tip,
.method-card.patch .method-tip,
.method-card.delete .method-tip {
background: #fef2f2;
.m-badge.orange {
background: #ffedd5;
color: #9a3412;
}
.m-badge.yellow {
background: #fef9c3;
color: #854d0e;
}
.m-badge.red {
background: #fee2e2;
color: #991b1b;
}
.comparison-table {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
.m-desc {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 4px;
}
.table-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
color: var(--vp-c-text-1);
.m-example {
font-family: monospace;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: var(--vp-c-bg-soft);
}
th {
padding: 12px;
text-align: left;
font-weight: bold;
font-size: 13px;
color: var(--vp-c-text-1);
border-bottom: 2px solid var(--vp-c-divider);
}
td {
padding: 12px;
font-size: 13px;
border-bottom: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-1);
}
tr:last-child td {
border-bottom: none;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-weight: bold;
font-size: 12px;
color: white;
}
.badge.get {
background: #3b82f6;
}
.badge.post {
background: #22c55e;
}
.badge.put {
background: #f59e0b;
}
.badge.patch {
background: #8b5cf6;
}
.badge.delete {
background: #ef4444;
}
.tips {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 6px;
font-size: 14px;
line-height: 1.8;
.compare-header {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 8px;
color: var(--vp-c-text-2);
}
.tips p {
margin-bottom: 8px;
.compare-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
margin-bottom: 6px;
font-size: 0.75rem;
}
.tips ul {
margin: 0;
padding-left: 20px;
.compare-row.warn {
background: #fef2f2;
}
.tips li {
margin: 4px 0;
.c-label {
font-weight: bold;
min-width: 50px;
}
.c-val {
color: var(--vp-c-text-2);
}
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,57 +1,40 @@
<!--
ApiPlayground.vue - 紧凑版
目标让用户动手尝试 API 调用
-->
<template>
<div class="api-playground">
<div class="header">
<div class="title">
🧪 API 练手场
</div>
<div class="subtitle">
随便玩坏了算我的
</div>
<div class="demo-root">
<div class="demo-header">
<span class="icon">🧪</span>
<span class="title">API 练手场</span>
<span class="subtitle">随便玩坏了算我的</span>
</div>
<div class="playground-layout">
<div class="demo-layout">
<div class="left-panel">
<div class="panel-title">
发送请求
</div>
<div class="input-group">
<label>Endpoint网址</label>
<div class="input-row">
<label>Endpoint</label>
<input
v-model="endpoint"
type="text"
placeholder="/users/123"
placeholder="/users"
class="input"
>
</div>
<div class="input-group">
<div class="input-row">
<label>方法</label>
<div class="method-buttons">
<div class="method-btns">
<button
v-for="m in methods"
:key="m"
:class="['method-btn', { active: method === m }]"
:class="['m-btn', { active: method === m }]"
@click="method = m"
>
{{ m }}
</button>
</div>
</div>
<div
v-if="method === 'POST'"
class="input-group"
>
<label>BodyJSON</label>
<textarea
v-model="body"
class="textarea"
placeholder="{&quot;name&quot;: &quot;张三&quot;}"
/>
</div>
<div class="input-group">
<div class="input-row">
<label>API Key</label>
<input
v-model="apiKey"
@@ -60,48 +43,36 @@
class="input"
>
</div>
<button
class="send-btn"
:disabled="loading"
@click="sendRequest"
>
{{ loading ? '发送中...' : '🚀 发送请求' }}
{{ loading ? '发送中...' : '🚀 发送' }}
</button>
</div>
<div class="right-panel">
<div class="panel-title">
响应结果
</div>
<div
v-if="!response"
class="empty-state"
class="empty"
>
<span class="empty-icon">📭</span>
<p>点击发送按钮看看会发生什么</p>
<p class="hint">
可以试试输入错误的地址或 Key
</p>
点击发送查看结果
</div>
<div
v-else
class="response-content"
class="response"
>
<div
class="status-bar"
:class="getStatusClass(response.status)"
>
<span class="status-code">{{ response.status }}</span>
<span class="status-text">{{ response.statusText }}</span>
<span class="code">{{ response.status }}</span>
<span class="text">{{ response.statusText }}</span>
</div>
<div class="response-body">
<div class="body">
<pre>{{ JSON.stringify(response.data, null, 2) }}</pre>
</div>
<div
v-if="response.explanation"
class="explanation"
@@ -112,30 +83,20 @@
</div>
</div>
<div class="tips">
<div class="tip-title">
可以试试这些玩法
</div>
<div class="tip-list">
<button @click="tryEndpoint('/users')">
GET /users
</button>
<button @click="tryEndpoint('/users/123')">
GET /users/123
</button>
<button @click="tryEndpoint('/posts')">
GET /posts
</button>
<button @click="tryError401">
401 没带 Key
</button>
<button @click="tryError404">
404 地址错了
</button>
<button @click="tryError429">
429 点太快了
</button>
</div>
<div class="quick-actions">
<span class="label">快速尝试</span>
<button @click="tryEndpoint('/users')">
GET /users
</button>
<button @click="tryError401">
401
</button>
<button @click="tryError404">
404
</button>
<button @click="tryError429">
429
</button>
</div>
</div>
</template>
@@ -145,41 +106,16 @@ import { ref } from 'vue'
const endpoint = ref('/users')
const method = ref('GET')
const methods = ['GET', 'POST']
const body = ref('{\n "name": "张三",\n "age": 25\n}')
const apiKey = ref('')
const apiKey = ref('sk-demo-key')
const loading = ref(false)
const response = ref(null)
function tryEndpoint(path) {
endpoint.value = path
method.value = 'GET'
apiKey.value = 'sk-test123'
}
function tryError401() {
endpoint.value = '/users'
method.value = 'GET'
apiKey.value = ''
}
function tryError404() {
endpoint.value = '/unknown-path'
method.value = 'GET'
apiKey.value = 'sk-test123'
}
function tryError429() {
endpoint.value = '/users'
method.value = 'GET'
apiKey.value = 'sk-test123'
}
const methods = ['GET', 'POST', 'PUT', 'DELETE']
function getStatusClass(status) {
if (status >= 200 && status < 300) return 'success'
if (status >= 400 && status < 500) return 'client-error'
if (status >= 500) return 'server-error'
return ''
return 'server-error'
}
function sendRequest() {
@@ -187,340 +123,256 @@ function sendRequest() {
response.value = null
setTimeout(() => {
loading.value = false
if (!apiKey.value) {
response.value = {
status: 401,
statusText: 'Unauthorized',
data: { error: 'Invalid API key' },
explanation: '没带 API Key,等于没带钱就想吃饭,被拒绝了'
data: { error: '缺少 API Key' },
explanation: '服务器不认识你,需要提供有效的身份证明'
}
return
}
if (endpoint.value === '/users' && method.value === 'GET') {
} else if (endpoint.value === '/users') {
response.value = {
status: 200,
statusText: 'OK',
data: {
users: [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' },
{ id: 3, name: '王五', email: 'wangwu@example.com' }
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
],
total: 3
total: 2
},
explanation: '成功!服务器返回了用户列表'
}
} else if (endpoint.value === '/users/123' && method.value === 'GET') {
response.value = {
status: 200,
statusText: 'OK',
data: { id: 123, name: '张三', email: 'zhangsan@example.com' },
explanation: '找到了!服务器返回了单个用户信息'
}
} else if (endpoint.value === '/posts' && method.value === 'GET') {
response.value = {
status: 200,
statusText: 'OK',
data: {
posts: [
{ id: 1, title: '学习 API 的第一天', author: '张三' },
{ id: 2, title: 'API 原来这么简单', author: '李四' }
]
},
explanation: '成功了!服务器返回了文章列表'
}
} else if (endpoint.value === '/posts' && method.value === 'POST') {
response.value = {
status: 201,
statusText: 'Created',
data: {
id: 3,
title: '学习 API 的第一天',
author: '张三',
created_at: '2025-01-15T10:30:00Z'
},
explanation: '新建成功了!服务器返回了新创建的帖子'
}
} else if (endpoint.value === '/unknown-path') {
response.value = {
status: 404,
statusText: 'Not Found',
data: { error: 'Resource not found' },
explanation: '地址错了,这个接口不存在'
}
} else if (endpoint.value === '/users' && method.value === 'DELETE') {
response.value = {
status: 429,
statusText: 'Too Many Requests',
data: { error: 'Rate limit exceeded' },
explanation: '点太快了!1 秒内只能请求 5 次,你超了'
explanation: '成功!服务器返回了用户列表'
}
} else {
response.value = {
status: 404,
statusText: 'Not Found',
data: { error: 'Endpoint not found' },
explanation: '这个地址不存在,换一个试试?'
data: { error: '接口不存在' },
explanation: '这个地址没有对应的 API,检查一下路径'
}
}
}, 500)
loading.value = false
}, 300)
}
function tryEndpoint(ep) {
endpoint.value = ep
method.value = 'GET'
sendRequest()
}
function tryError401() {
apiKey.value = ''
sendRequest()
}
function tryError404() {
endpoint.value = '/not-exist'
sendRequest()
}
function tryError429() {
loading.value = true
response.value = null
setTimeout(() => {
response.value = {
status: 429,
statusText: 'Too Many Requests',
data: { error: '请求太频繁' },
explanation: '你请求太快了,服务器让你歇会儿'
}
loading.value = false
}, 300)
}
</script>
<style scoped>
.api-playground {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 24px 0;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.playground-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.playground-layout {
grid-template-columns: 1fr;
}
}
.left-panel,
.right-panel {
background: var(--vp-c-bg);
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.panel-title {
font-weight: 600;
margin-bottom: 16px;
color: var(--vp-c-text-1);
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
}
.input-group {
margin-bottom: 12px;
.icon { font-size: 18px; }
.title { font-weight: 600; font-size: 0.9rem; }
.subtitle { font-size: 0.75rem; color: var(--vp-c-text-3); margin-left: auto; }
.demo-layout {
display: flex;
}
.input-group label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
.left-panel {
width: 240px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
border-right: 1px solid var(--vp-c-divider);
}
.right-panel {
flex: 1;
padding: 12px;
background: var(--vp-c-bg);
min-height: 180px;
}
@media (max-width: 640px) {
.demo-layout { flex-direction: column; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid var(--vp-c-divider); }
}
.input-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.input-row label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.input {
width: 100%;
padding: 10px;
padding: 8px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 14px;
font-family: monospace;
background: var(--vp-c-bg-soft);
font-size: 0.85rem;
background: var(--vp-c-bg);
}
.input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.textarea {
width: 100%;
height: 80px;
padding: 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 13px;
font-family: monospace;
background: var(--vp-c-bg-soft);
resize: vertical;
}
.method-buttons {
.method-btns {
display: flex;
gap: 8px;
gap: 4px;
}
.method-btn {
padding: 6px 16px;
.m-btn {
flex: 1;
padding: 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-size: 13px;
font-weight: 600;
border-radius: 4px;
background: transparent;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.method-btn.active {
.m-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.send-btn {
width: 100%;
padding: 12px;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
transition: opacity 0.2s;
}
.send-btn:disabled {
opacity: 0.7;
opacity: 0.6;
cursor: not-allowed;
}
.empty-state {
text-align: center;
padding: 40px 20px;
.empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-3);
font-style: italic;
}
.empty-icon {
font-size: 48px;
display: block;
margin-bottom: 12px;
opacity: 0.5;
}
.hint {
font-size: 12px;
margin-top: 8px;
opacity: 0.7;
}
.response-content {
animation: fadeIn 0.3s ease;
.response {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
}
.status-bar {
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 12px;
display: flex;
gap: 8px;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 4px;
font-size: 0.8rem;
}
.status-bar.success {
background: #dcfce7;
color: #166534;
}
.status-bar.success { background: #dcfce7; }
.status-bar.success .code { color: #166534; }
.status-bar.client-error { background: #fee2e2; }
.status-bar.client-error .code { color: #991b1b; }
.status-bar.server-error { background: #fef3c7; }
.status-bar.server-error .code { color: #92400e; }
.status-bar.client-error {
background: #fee2e2;
color: #991b1b;
}
.code { font-weight: bold; }
.text { color: var(--vp-c-text-2); }
.status-bar.server-error {
background: #fecaca;
color: #7f1d1d;
}
.status-code {
font-weight: 700;
font-family: monospace;
}
.response-body {
.body {
flex: 1;
background: #1e293b;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
overflow-x: auto;
max-height: 180px;
padding: 8px;
overflow: auto;
}
.response-body pre {
.body pre {
margin: 0;
font-family: monospace;
font-size: 12px;
font-size: 0.7rem;
color: #e2e8f0;
white-space: pre-wrap;
}
.explanation {
background: var(--vp-c-bg-soft);
padding: 12px;
border-radius: 6px;
font-size: 13px;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand);
padding: 8px;
background: #fef3c7;
border-radius: 4px;
font-size: 0.8rem;
color: #92400e;
}
.tips {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--vp-c-divider);
}
.tip-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
color: var(--vp-c-text-2);
}
.tip-list {
.quick-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
flex-wrap: wrap;
}
.tip-list button {
padding: 6px 12px;
.quick-actions .label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.quick-actions button {
padding: 4px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 12px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.tip-list button:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
.quick-actions button:hover {
background: var(--vp-c-bg-soft);
}
</style>
@@ -1,61 +1,98 @@
<!--
ApiQuickStartDemo.vue - 演示
目标展示最简单的 API 调用流程
ApiQuickStartDemo.vue - 紧凑
目标展示最简单的 API 调用流程一眼看懂
-->
<template>
<div class="demo">
<div class="header">
<div class="demo-root">
<div class="demo-header">
<span class="icon">🌐</span>
<span class="title">试试看获取当前时间</span>
</div>
<div class="content">
<div class="action-area">
<button
class="call-btn"
:disabled="calling"
@click="callApi"
>
<span v-if="!calling">📡 发起 API 请求</span>
<span v-else>🔄 请求处理中...</span>
<div class="demo-layout">
<div class="left-panel">
<div class="terminal">
<div class="term-bar">
<span class="dot r" /><span class="dot y" /><span class="dot g" />
<span class="term-title">API 请求</span>
</div>
<div class="term-body">
<div class="t-line">
<span class="t-ps">&gt; </span>
<span class="t-cmd">GET /api/time</span>
</div>
<div v-if="calling" class="t-line">
<span class="t-dim">请求中...</span>
<span class="t-loading"></span>
</div>
<div v-if="result" class="t-line">
<span class="t-grn">HTTP/1.1 200 OK</span>
</div>
<div v-if="result" class="t-line">
<span class="t-dim">{ "time": "{{ result.timeString }}" }</span>
</div>
</div>
</div>
<button class="call-btn" :disabled="calling" @click="callApi">
{{ calling ? '请求中...' : '📡 发起请求' }}
</button>
</div>
<div
v-if="result || calling"
class="result-area"
>
<div
v-if="calling"
class="loading-dots"
>
<span>.</span><span>.</span><span>.</span>
</div>
<div
v-else-if="result"
class="response-card"
>
<div class="response-header">
<span class="status-badge success">200 OK</span>
<span class="time">耗时: {{ result.time }}ms</span>
<div class="right-panel">
<div class="flow-col" :class="{ 'flow-highlight': stage === 'client' }">
<div class="flow-header">
<span class="flow-icon">💻</span>
<span class="flow-title">客户端</span>
</div>
<div class="response-body">
<div class="time-display">
{{ result.timeString }}
</div>
<div class="timezone">
{{ result.timezone }}
</div>
<div class="flow-body">
{{ stage === 'client' ? '准备请求...' : '等待中' }}
</div>
</div>
<div class="flow-arrow" :class="{ 'arrow-lit': stage === 'request' }">
<span class="arrow-symbol"></span>
<code class="arrow-label">Request</code>
</div>
<div class="flow-col" :class="{ 'flow-highlight': stage === 'server' }">
<div class="flow-header">
<span class="flow-icon">🖥</span>
<span class="flow-title">服务器</span>
</div>
<div class="flow-body">
{{ stage === 'server' ? '处理中...' : '等待中' }}
</div>
</div>
<div class="flow-arrow" :class="{ 'arrow-lit': stage === 'response' }">
<code class="arrow-label">Response</code>
<span class="arrow-symbol"></span>
</div>
<div
class="flow-col"
:class="{ 'flow-highlight': stage === 'response' }"
>
<div class="flow-header">
<span class="flow-icon">📦</span>
<span class="flow-title">响应</span>
</div>
<div class="flow-body">
<span v-if="result" class="result-time">{{
result.timeString
}}</span>
<span v-else>等待响应</span>
</div>
</div>
</div>
</div>
<div class="footer">
<p>
👆 <strong>流程演示</strong> 点击按钮 -> 发送请求 -> 服务器处理 ->
返回数据
</p>
<div class="info-box">
<strong>核心思想</strong>
<span
>点击按钮 发送请求 服务器处理 返回数据这就是 API
调用的完整流程</span
>
</div>
</div>
</template>
@@ -65,180 +102,257 @@ import { ref } from 'vue'
const calling = ref(false)
const result = ref(null)
const stage = ref(null)
function callApi() {
calling.value = true
result.value = null
const startTime = Date.now()
stage.value = 'client'
setTimeout(() => {
stage.value = 'request'
}, 150)
setTimeout(() => {
stage.value = 'server'
}, 300)
setTimeout(() => {
stage.value = 'response'
}, 450)
setTimeout(() => {
const now = new Date()
const timeString = now.toLocaleString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
result.value = {
time: Date.now() - startTime,
timeString: `🕐 ${timeString}`,
timezone: '亚洲/上海 (UTC+8)'
timeString: now.toLocaleString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
calling.value = false
}, 300 + Math.random() * 200)
}, 500)
}
</script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
padding: 16px 20px;
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
}
.icon {
font-size: 24px;
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 16px;
font-size: 0.9rem;
}
.content {
padding: 24px;
.demo-layout {
display: flex;
align-items: stretch;
}
.left-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 200px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 0;
background: var(--vp-c-bg);
}
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.left-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.flow-arrow {
display: none;
}
}
.terminal {
background: #141420;
}
.term-bar {
display: flex;
align-items: center;
gap: 20px;
min-height: 160px;
justify-content: center;
gap: 5px;
padding: 6px 10px;
background: #1e1e2e;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.term-title {
margin-left: 6px;
font-size: 0.7rem;
color: #666;
font-family: monospace;
}
.term-body {
min-height: 80px;
padding: 10px 12px;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.75rem;
line-height: 1.6;
color: #cdd6f4;
}
.t-line {
margin-bottom: 4px;
}
.t-ps {
color: #89b4fa;
}
.t-cmd {
color: #cdd6f4;
}
.t-dim {
color: #585b70;
}
.t-grn {
color: #a6e3a1;
}
.t-loading {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.call-btn {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
margin: 10px;
padding: 8px 16px;
background: var(--vp-c-brand);
color: white;
border: none;
padding: 12px 32px;
font-size: 16px;
font-weight: 600;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.5);
}
.call-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 8px -1px rgba(59, 130, 246, 0.6);
}
.call-btn:disabled {
opacity: 0.7;
cursor: wait;
transform: scale(0.98);
}
.result-area {
width: 100%;
max-width: 400px;
}
.response-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 16px;
animation: slideUp 0.3s ease-out;
}
.response-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 12px;
color: var(--vp-c-text-3);
}
.status-badge {
background: #dcfce7;
color: #166534;
padding: 2px 8px;
border-radius: 4px;
font-weight: bold;
}
.response-body {
text-align: center;
}
.time-display {
font-size: 24px;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
cursor: pointer;
}
.call-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.timezone {
font-size: 13px;
.flow-col {
border: 1.5px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
transition:
border-color 0.2s,
box-shadow 0.2s;
}
.flow-col.flow-highlight {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--vp-c-brand) 12%, transparent);
}
.flow-header {
padding: 4px 8px;
background: var(--vp-c-bg-alt);
display: flex;
align-items: center;
gap: 4px;
}
.flow-icon {
font-size: 0.8rem;
}
.flow-title {
font-weight: 600;
font-size: 0.75rem;
}
.flow-body {
padding: 6px 8px;
font-size: 0.72rem;
color: var(--vp-c-text-2);
}
.loading-dots span {
animation: blink 1.4s infinite both;
font-size: 24px;
margin: 0 2px;
.result-time {
color: var(--vp-c-brand);
font-weight: 600;
}
.loading-dots span:nth-child(2) {
animation-delay: 0.2s;
.flow-arrow {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 2px 0;
opacity: 0.3;
transition: opacity 0.2s;
}
.loading-dots span:nth-child(3) {
animation-delay: 0.4s;
.flow-arrow.arrow-lit {
opacity: 1;
}
.arrow-symbol {
font-size: 0.8rem;
color: var(--vp-c-brand);
}
.arrow-label {
font-size: 0.6rem;
font-family: monospace;
color: var(--vp-c-brand);
}
.footer {
padding: 12px 20px;
background: rgba(0, 0, 0, 0.02);
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 13px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
text-align: center;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,108 +1,62 @@
<!--
FunctionApiDemo.vue - 紧凑版
目标展示函数就是最基础的 API
-->
<template>
<div class="function-api-demo">
<div class="header">
<div class="title">
🔧 你早就在用 API
</div>
<div class="subtitle">
函数就是最基础的 API
</div>
<div class="demo-root">
<div class="demo-header">
<span class="icon">🔧</span>
<span class="title">函数就是最基础的 API</span>
</div>
<div class="demo-container">
<div class="left">
<div class="code-panel">
<div class="code-title">
📝 代码
</div>
<pre><code><span class="keyword">def</span> <span class="function">greet</span>(name, greeting=<span class="string">"你好"</span>):
<span class="keyword">return</span> f<span class="string">"{greeting}{name}"</span>
<div class="demo-layout">
<div class="code-panel">
<div class="code-title">📝 Python 代码</div>
<pre><code><span class="keyword">def</span> <span class="func">greet</span>(name, greeting=<span class="str">"你好"</span>):
<span class="keyword">return</span> <span class="str">f"{greeting}{name}"</span>
<span class="comment"># 调用这个函数</span>
result = <span class="function">greet</span>(<span class="string">"张三"</span>)
print(result)</code></pre>
</div>
result = <span class="func">greet</span>(<span class="str">"张三"</span>)</code></pre>
</div>
<div class="right">
<div class="explanation">
<p>这个 <code>greet()</code> 函数就是一个 API</p>
<div class="point">
<span class="icon">📦</span>
<div>
<strong>输入参数</strong>
<p>你传进去什么<code>"张三"</code></p>
</div>
<div class="right-panel">
<div class="api-structure">
<div class="structure-item">
<span class="label">📦 输入参数</span>
<code class="value">name="张三"</code>
</div>
<div class="point">
<span class="icon"></span>
<div>
<strong>处理</strong>
<p>函数内部帮你做了拼接字符串的操作</p>
</div>
<div class="structure-item">
<span class="label"> 处理</span>
<span class="value">函数内部拼接字符串</span>
</div>
<div class="point">
<span class="icon">📤</span>
<div>
<strong>输出返回值</strong>
<p>得到什么<code>"你好,张三!"</code></p>
</div>
<div class="structure-item">
<span class="label">📤 输出返回</span>
<code class="value highlight">"你好,张三!"</code>
</div>
</div>
<div class="try-it">
<div class="try-title">
🎮 试试调用
</div>
<div class="interactive">
<input
v-model="name"
placeholder="输入名字"
class="name-input"
>
<select
v-model="greeting"
class="greeting-select"
>
<option value="你好">
你好
</option>
<option value="Hello">
Hello
</option>
<option value="早上好">
早上好
</option>
<option value="晚安">
晚安
</option>
<div class="try-area">
<div class="try-row">
<input v-model="name" placeholder="名字" class="input" />
<select v-model="greeting" class="select">
<option value="你好">你好</option>
<option value="Hello">Hello</option>
<option value="早上好">早上好</option>
</select>
<button
class="call-btn"
@click="callFunction"
>
调用 greet()
</button>
<button class="btn" @click="callFunction">调用</button>
</div>
<div
v-if="result"
class="result"
>
<span class="arrow"></span>
<code>{{ result }}</code>
<div v-if="result" class="result">
<code>{{ result }}</code>
</div>
</div>
</div>
</div>
<div class="summary">
<div class="summary-item">
<span class="icon">🔑</span>
<p><strong>关键点</strong> 你不需要知道函数内部怎么实现只需要知道怎么调用它</p>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span
>你不需要知道函数内部怎么实现只需要知道怎么调用它这就是 API
的本质</span
>
</div>
</div>
</template>
@@ -114,192 +68,194 @@ const name = ref('张三')
const greeting = ref('你好')
const result = ref('')
function greet(name, greeting) {
return `${greeting}${name}`
}
function callFunction() {
result.value = greet(name.value, greeting.value)
result.value = `${greeting.value}${name.value}`
}
</script>
<style scoped>
.function-api-demo {
background: var(--vp-c-bg-soft);
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 24px 0;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-size: 0.85rem;
}
.header {
text-align: center;
margin-bottom: 20px;
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
}
.title {
.icon {
font-size: 18px;
font-weight: 700;
margin-bottom: 4px;
}
.title {
font-weight: 600;
font-size: 0.9rem;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.demo-container {
grid-template-columns: 1fr;
}
.demo-layout {
display: flex;
gap: 0;
}
.code-panel {
flex: 1;
background: #1e293b;
border-radius: 10px;
padding: 16px;
font-family: monospace;
font-size: 14px;
padding: 12px 14px;
border-right: 1px solid var(--vp-c-divider);
}
.code-title {
color: #94a3b8;
margin-bottom: 12px;
font-size: 13px;
font-size: 0.75rem;
margin-bottom: 8px;
}
.code-panel pre {
margin: 0;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.75rem;
line-height: 1.6;
}
.code-panel code {
color: #e2e8f0;
line-height: 1.8;
}
.keyword {
color: #c084fc;
}
.function {
.func {
color: #60a5fa;
}
.string {
.str {
color: #4ade80;
}
.comment {
color: #64748b;
}
.explanation {
margin-bottom: 20px;
}
.explanation > p {
margin-bottom: 16px;
}
.point {
.right-panel {
width: 260px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 12px;
align-items: flex-start;
background: var(--vp-c-bg);
}
.point .icon {
font-size: 20px;
@media (max-width: 640px) {
.demo-layout {
flex-direction: column;
}
.code-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.right-panel {
width: 100%;
}
}
.point strong {
color: var(--vp-c-text-1);
.api-structure {
display: flex;
flex-direction: column;
gap: 6px;
}
.point p {
margin: 4px 0 0;
font-size: 13px;
.structure-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.75rem;
}
.structure-item .label {
color: var(--vp-c-text-2);
}
.try-it {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
.structure-item .value {
font-family: monospace;
font-size: 0.72rem;
}
.try-title {
font-weight: 600;
margin-bottom: 12px;
.structure-item .highlight {
background: #dcfce7;
color: #166534;
padding: 2px 6px;
border-radius: 3px;
}
.interactive {
.try-area {
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.try-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
gap: 6px;
}
.name-input {
.input {
flex: 1;
min-width: 120px;
padding: 8px 12px;
padding: 6px 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 14px;
}
.greeting-select {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 14px;
border-radius: 4px;
font-size: 0.8rem;
background: var(--vp-c-bg);
}
.call-btn {
padding: 8px 16px;
.select {
padding: 6px 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.8rem;
background: var(--vp-c-bg);
}
.btn {
padding: 6px 12px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
}
.result {
margin-top: 16px;
padding: 12px;
margin-top: 8px;
padding: 6px 8px;
background: #dcfce7;
border-radius: 6px;
font-size: 14px;
}
.result .arrow {
margin-right: 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.result code {
color: #166534;
font-weight: 600;
}
.summary {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--vp-c-divider);
}
.summary-item {
.info-box {
display: flex;
gap: 12px;
align-items: center;
font-size: 14px;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.summary-item .icon {
font-size: 20px;
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -1,256 +1,71 @@
<!--
RealWorldApiDemo.vue
目标展示真实场景中调用 AI 服务的两种方式
RealWorldApiDemo.vue - 紧凑版
目标对比 HTTP 调用 SDK 调用
-->
<template>
<div class="demo">
<div class="title">
🤖 真实场景 AI 帮你写产品文案
</div>
<p class="subtitle">
体验两种调用方式的区别
</p>
<div class="scenario">
<div class="scenario-header">
<span class="scenario-icon">📝</span>
<span class="scenario-title">你的需求</span>
</div>
<div class="scenario-body">
我想让 AI 帮智能手表写一段吸引人的产品文案
</div>
<div class="demo-root">
<div class="demo-header">
<span class="icon"></span>
<span class="title">HTTP vs SDK自己跑腿还是让管家代办</span>
</div>
<div class="modes">
<div class="mode-tabs">
<div class="demo-layout">
<div class="tabs">
<button
:class="['tab', { active: mode === 'http' }]"
@click="mode = 'http'"
>
🌐 HTTP API外卖模式
HTTP API
</button>
<button
:class="['tab', { active: mode === 'sdk' }]"
@click="mode = 'sdk'"
>
📦 SDK堂食模式
SDK
</button>
</div>
<div class="mode-content">
<!-- HTTP 模式 -->
<div
v-if="mode === 'http'"
class="mode-details"
>
<div class="steps">
<div
class="step"
:class="{ active: currentStep >= 1 }"
>
<div class="step-number">
1
</div>
<div class="step-content">
<div class="step-title">
找到网址打开外卖 APP
</div>
<div class="step-code">
https://api.openai.com/v1/chat/completions
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 2 }"
>
<div class="step-number">
2
</div>
<div class="step-content">
<div class="step-title">
准备订单填写信息
</div>
<div class="step-code">
Authorization: Bearer 你的API密钥<br>
Content-Type: application/json
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 3 }"
>
<div class="step-number">
3
</div>
<div class="step-content">
<div class="step-title">
下单发送请求
</div>
<div class="step-code">
{<br>
&nbsp;&nbsp;"model": "gpt-4",<br>
&nbsp;&nbsp;"messages": [<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ "role": "system", "content":
"你是营销文案专家" },<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ "role": "user", "content":
"写智能手表文案" }<br>
&nbsp;&nbsp;]<br>
}
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 4 }"
>
<div class="step-number">
4
</div>
<div class="step-content">
<div class="step-title">
等待配送解析响应
</div>
<div class="step-code">
response.choices[0].message.content<br>
<span class="step-hint"> 需要自己处理解析错误</span>
</div>
</div>
</div>
</div>
<div class="summary">
<p><strong>💡 HTTP API 特点</strong></p>
<ul>
<li> 灵活任何语言都能用</li>
<li> 复杂要手动处理很多细节</li>
<li> 容易出错鉴权数据格式错误处理都要自己写</li>
</ul>
</div>
<div class="code-area">
<div class="code-header">
<span>{{
mode === 'http' ? '自己处理所有细节' : '管家帮你处理'
}}</span>
</div>
<pre
class="code"
><code>{{ mode === 'http' ? httpCode : sdkCode }}</code></pre>
</div>
<!-- SDK 模式 -->
<div
v-else
class="mode-details"
>
<div class="steps">
<div
class="step"
:class="{ active: currentStep >= 1 }"
>
<div class="step-number">
1
</div>
<div class="step-content">
<div class="step-title">
走进餐厅安装 SDK
</div>
<div class="step-code">
import OpenAI from 'openai'
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 2 }"
>
<div class="step-number">
2
</div>
<div class="step-content">
<div class="step-title">
找服务员初始化客户端
</div>
<div class="step-code">
const client = new OpenAI({<br>
&nbsp;&nbsp;apiKey: '你的密钥'<br>
})
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 3 }"
>
<div class="step-number">
3
</div>
<div class="step-content">
<div class="step-title">
直接点菜调用函数
</div>
<div class="step-code">
const response = await client.chat.completions.create({<br>
&nbsp;&nbsp;model: 'gpt-4',<br>
&nbsp;&nbsp;messages: [<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ role: 'system', content:
'你是营销文案专家' },<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ role: 'user', content:
'写智能手表文案' }<br>
&nbsp;&nbsp;]<br>
})
</div>
</div>
</div>
<div
class="step"
:class="{ active: currentStep >= 4 }"
>
<div class="step-number">
4
</div>
<div class="step-content">
<div class="step-title">
享用美食直接使用
</div>
<div class="step-code">
console.log(response.choices[0].message.content)<br>
<span class="step-hint"> SDK 帮你处理好了所有细节</span>
</div>
</div>
</div>
<div class="compare-panel">
<div class="compare-title">对比</div>
<div class="compare-list">
<div class="compare-item">
<span class="ci-label">代码量</span>
<span class="ci-val">{{ mode === 'http' ? '多' : '少' }}</span>
</div>
<div class="summary">
<p><strong>💡 SDK 特点</strong></p>
<ul>
<li> 简单只管调用函数</li>
<li> 省心SDK 自动处理鉴权错误数据格式</li>
<li> 限制通常只能在特定语言使用</li>
</ul>
<div class="compare-item">
<span class="ci-label">错误处理</span>
<span class="ci-val">{{
mode === 'http' ? '自己写' : '自动处理'
}}</span>
</div>
<div class="compare-item">
<span class="ci-label">重试逻辑</span>
<span class="ci-val">{{
mode === 'http' ? '自己写' : '内置'
}}</span>
</div>
<div class="compare-item">
<span class="ci-label">类型提示</span>
<span class="ci-val">{{ mode === 'http' ? '无' : '有' }}</span>
</div>
</div>
</div>
</div>
<div class="action">
<button
class="run-btn"
:disabled="running"
@click="runDemo"
>
{{ running ? '调用中...' : '🚀 开始调用 AI' }}
</button>
<div
v-if="result"
class="result"
>
<div class="result-header">
{{ mode === 'http' ? '🌐 HTTP API 返回' : '📦 SDK 返回' }}
</div>
<div class="result-body">
"这款智能手表,是你的贴身健康管家。全天候心率监测,运动模式自动识别..."
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span>能用 SDK 就用 SDK把麻烦事留给库把时间留给自己</span>
</div>
</div>
</template>
@@ -258,270 +73,168 @@
<script setup>
import { ref } from 'vue'
const mode = ref('http')
const currentStep = ref(0)
const running = ref(false)
const result = ref(null)
const mode = ref('sdk')
async function runDemo() {
running.value = true
result.value = null
currentStep.value = 0
const httpCode = `import requests
// 模拟逐步执行
for (let i = 1; i <= 4; i++) {
await new Promise((resolve) => setTimeout(resolve, 600))
currentStep.value = i
}
response = requests.post(
"https://api.deepseek.com/v1/chat/completions",
headers={
"Authorization": "Bearer sk-xxx",
"Content-Type": "application/json"
},
json={
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "你好"}]
}
)
await new Promise((resolve) => setTimeout(resolve, 400))
result.value = true
running.value = false
}
if response.status_code == 200:
result = response.json()
content = result["choices"][0]["message"]["content"]
else:
# 处理错误...
pass`
const sdkCode = `from openai import OpenAI
client = OpenAI(
api_key="sk-xxx",
base_url="https://api.deepseek.com"
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "你好"}]
)
content = response.choices[0].message.content`
</script>
<style scoped>
.demo {
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
border-radius: 10px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 16px 0;
margin: 1rem 0;
font-size: 0.85rem;
}
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
margin-bottom: 20px;
}
.scenario {
.demo-header {
padding: 10px 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-brand-1);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
}
.scenario-header {
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.scenario-icon {
font-size: 24px;
.icon {
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 0.9rem;
}
.scenario-title {
font-size: 16px;
font-weight: bold;
color: var(--vp-c-text-1);
}
.scenario-body {
font-size: 15px;
line-height: 1.6;
color: var(--vp-c-text-1);
padding-left: 32px;
}
.modes {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
}
.mode-tabs {
.demo-layout {
display: flex;
border-bottom: 2px solid var(--vp-c-divider);
flex-direction: column;
}
.tabs {
display: flex;
gap: 4px;
padding: 10px 12px;
background: var(--vp-c-bg);
}
.tab {
flex: 1;
padding: 14px 20px;
border: none;
background: var(--vp-c-bg-soft);
font-size: 15px;
font-weight: bold;
padding: 6px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: transparent;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
border-bottom: 3px solid transparent;
}
.tab:hover {
background: var(--vp-c-bg);
}
.tab.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.code-area {
background: #1e293b;
}
.code-header {
padding: 6px 12px;
font-size: 0.75rem;
color: #94a3b8;
border-bottom: 1px solid #334155;
}
.code {
margin: 0;
padding: 12px;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 0.72rem;
line-height: 1.5;
color: #e2e8f0;
overflow-x: auto;
white-space: pre;
}
.compare-panel {
padding: 12px;
background: var(--vp-c-bg);
border-bottom-color: var(--vp-c-brand-1);
}
.mode-content {
padding: 20px;
.compare-title {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 8px;
}
.steps {
.compare-list {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
gap: 12px;
flex-wrap: wrap;
}
.step {
display: flex;
gap: 16px;
opacity: 0.4;
transition: opacity 0.3s;
}
.step.active {
opacity: 1;
}
.step-number {
flex-shrink: 0;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
.compare-item {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
gap: 6px;
padding: 6px 10px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.75rem;
}
.ci-label {
color: var(--vp-c-text-2);
}
.step.active .step-number {
background: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
color: white;
.ci-val {
font-weight: 600;
}
.step-content {
flex: 1;
.info-box {
display: flex;
gap: 0.25rem;
padding: 10px 14px;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.step-title {
font-weight: bold;
font-size: 14px;
margin-bottom: 6px;
color: var(--vp-c-text-1);
}
.step-code {
background: #1e293b;
color: #e2e8f0;
padding: 12px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.6;
position: relative;
}
.step-hint {
display: block;
margin-top: 8px;
font-size: 11px;
padding: 6px 8px;
border-radius: 4px;
}
.step.active .step-hint {
background: rgba(255, 255, 255, 0.1);
}
.summary {
background: var(--vp-c-bg-soft);
padding: 16px;
border-radius: 6px;
font-size: 14px;
line-height: 1.8;
}
.summary p {
margin-bottom: 8px;
}
.summary ul {
margin: 0;
padding-left: 20px;
}
.summary li {
margin: 4px 0;
}
.action {
padding: 20px;
border-top: 2px solid var(--vp-c-divider);
}
.run-btn {
width: 100%;
padding: 14px 24px;
background: var(--vp-c-brand-1);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.run-btn:hover:not(:disabled) {
opacity: 0.9;
transform: scale(1.02);
}
.run-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.result {
margin-top: 16px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-header {
font-weight: bold;
font-size: 14px;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.result-body {
background: #f0fdf4;
border: 2px solid #86efac;
border-radius: 6px;
padding: 16px;
font-size: 14px;
line-height: 1.6;
color: #166534;
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
@@ -4,48 +4,29 @@
-->
<template>
<div class="demo">
<div class="title">
🔄 一次 API 调用的流程
</div>
<p class="subtitle">
点一下按钮看请求怎么飞过去再飞回来
</p>
<div class="title">🔄 一次 API 调用的流程</div>
<p class="subtitle">点一下按钮看请求怎么飞过去再飞回来</p>
<div class="flow-container">
<div class="side you">
<div class="window">
<div class="window-header">
👤 你这边
</div>
<div class="window-header">👤 你这边</div>
<div class="window-body">
<div class="message">
我想调用 API
</div>
<div class="message">我想调用 API</div>
</div>
</div>
</div>
<div class="middle">
<div
class="arrow"
:class="{ animating: isAnimating }"
>
</div>
<button
class="send-btn"
:disabled="isAnimating"
@click="send"
>
<div class="arrow" :class="{ animating: isAnimating }"></div>
<button class="send-btn" :disabled="isAnimating" @click="send">
{{ isAnimating ? '发送中...' : '🚀 发送请求' }}
</button>
</div>
<div class="side server">
<div class="window">
<div class="window-header">
🖥 对方服务器
</div>
<div class="window-header">🖥 对方服务器</div>
<div class="window-body">
<div class="message">
{{ serverMessage }}
@@ -55,14 +36,8 @@
</div>
</div>
<div
v-if="result"
class="result"
>
<div
class="result-box"
:class="result.type"
>
<div v-if="result" class="result">
<div class="result-box" :class="result.type">
{{ result.text }}
</div>
</div>
@@ -1,16 +1,30 @@
<template>
<div class="adder-demo">
<div class="demo-label">二进制加法器 输入 015 的两个数观察逐位计算过程</div>
<div class="demo-label">
二进制加法器 输入 015 的两个数观察逐位计算过程
</div>
<div class="control-row">
<label class="input-group">
<span class="input-label">A</span>
<input v-model.number="inputA" type="number" min="0" max="15" class="num-input" />
<input
v-model.number="inputA"
type="number"
min="0"
max="15"
class="num-input"
/>
</label>
<span class="op-sign">+</span>
<label class="input-group">
<span class="input-label">B</span>
<input v-model.number="inputB" type="number" min="0" max="15" class="num-input" />
<input
v-model.number="inputB"
type="number"
min="0"
max="15"
class="num-input"
/>
</label>
<span class="op-sign">=</span>
<span class="result-num">{{ resultDec }}</span>
@@ -20,21 +34,39 @@
<div class="binary-row">
<span class="binary-label">A</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsA" :key="'a'+i" class="bit" :class="{ hl: activeBit === (3 - i) }">{{ b }}</span>
<span
v-for="(b, i) in bitsA"
:key="'a' + i"
class="bit"
:class="{ hl: activeBit === 3 - i }"
>{{ b }}</span
>
</span>
<span class="binary-dec">= {{ clampedA }}</span>
</div>
<div class="binary-row">
<span class="binary-label">B</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsB" :key="'b'+i" class="bit" :class="{ hl: activeBit === (3 - i) }">{{ b }}</span>
<span
v-for="(b, i) in bitsB"
:key="'b' + i"
class="bit"
:class="{ hl: activeBit === 3 - i }"
>{{ b }}</span
>
</span>
<span class="binary-dec">= {{ clampedB }}</span>
</div>
<div class="binary-row sum-row">
<span class="binary-label">结果</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsSum" :key="'s'+i" class="bit" :class="{ hl: activeBit === (3 - i) }">{{ b }}</span>
<span
v-for="(b, i) in bitsSum"
:key="'s' + i"
class="bit"
:class="{ hl: activeBit === 3 - i }"
>{{ b }}</span
>
</span>
<span class="binary-dec">= {{ fourBitResult }}</span>
</div>
@@ -45,7 +77,8 @@
<div class="stages-row">
<div
v-for="(stage, idx) in stages" :key="idx"
v-for="(stage, idx) in stages"
:key="idx"
class="stage-card"
:class="{ active: activeBit === stage.bitPos }"
@mouseenter="activeBit = stage.bitPos"
@@ -53,24 +86,40 @@
>
<div class="stage-head">
<span class="stage-pos">{{ stage.bitPos }}</span>
<span class="stage-type" :class="stage.carryIn !== null ? 'full' : 'half'">
<span
class="stage-type"
:class="stage.carryIn !== null ? 'full' : 'half'"
>
{{ stage.carryIn !== null ? '全加器' : '半加器' }}
</span>
</div>
<div class="stage-io">
<span class="io-item"><span class="io-tag a">A</span>{{ stage.a }}</span>
<span class="io-item"><span class="io-tag b">B</span>{{ stage.b }}</span>
<span v-if="stage.carryIn !== null" class="io-item"><span class="io-tag cin">Cin</span>{{ stage.carryIn }}</span>
<span class="io-item"
><span class="io-tag a">A</span>{{ stage.a }}</span
>
<span class="io-item"
><span class="io-tag b">B</span>{{ stage.b }}</span
>
<span v-if="stage.carryIn !== null" class="io-item"
><span class="io-tag cin">Cin</span>{{ stage.carryIn }}</span
>
</div>
<div class="stage-divider"></div>
<div class="stage-io">
<span class="io-item"><span class="io-tag s">S</span><strong>{{ stage.sum }}</strong></span>
<span class="io-item"><span class="io-tag cout">C</span>{{ stage.carryOut }}</span>
<span class="io-item"
><span class="io-tag s">S</span
><strong>{{ stage.sum }}</strong></span
>
<span class="io-item"
><span class="io-tag cout">C</span>{{ stage.carryOut }}</span
>
</div>
</div>
</div>
<div class="demo-caption">鼠标悬停某一位查看该位加法器的输入 / 输出 · 就像手算竖式"逢二进一"</div>
<div class="demo-caption">
鼠标悬停某一位查看该位加法器的输入 / 输出 · 就像手算竖式"逢二进一"
</div>
</div>
</template>
@@ -90,8 +139,12 @@ function clamp(n) {
const clampedA = computed(() => clamp(inputA.value))
const clampedB = computed(() => clamp(inputB.value))
const bitsA = computed(() => (clampedA.value >>> 0).toString(2).padStart(4, '0').split(''))
const bitsB = computed(() => (clampedB.value >>> 0).toString(2).padStart(4, '0').split(''))
const bitsA = computed(() =>
(clampedA.value >>> 0).toString(2).padStart(4, '0').split('')
)
const bitsB = computed(() =>
(clampedB.value >>> 0).toString(2).padStart(4, '0').split('')
)
const stages = computed(() => {
const A = clampedA.value
@@ -106,10 +159,17 @@ const stages = computed(() => {
sum = a ^ b
carryOut = a & b
} else {
sum = (a ^ b) ^ carryIn
sum = a ^ b ^ carryIn
carryOut = (a & b) | (carryIn & (a ^ b))
}
result.push({ bitPos: i, a, b, carryIn: carryIn === null ? null : carryIn, sum, carryOut })
result.push({
bitPos: i,
a,
b,
carryIn: carryIn === null ? null : carryIn,
sum,
carryOut
})
carryIn = carryOut
}
return result
@@ -126,7 +186,9 @@ const fourBitResult = computed(() =>
const overflow = computed(() => clampedA.value + clampedB.value > 15)
const resultDec = computed(() =>
overflow.value ? `${fourBitResult.value}(溢出)` : String(fourBitResult.value)
overflow.value
? `${fourBitResult.value}(溢出)`
: String(fourBitResult.value)
)
</script>
@@ -334,11 +396,21 @@ const resultDec = computed(() =>
font-family: system-ui;
}
.io-tag.a { background: var(--vp-c-brand-1); }
.io-tag.b { background: #8b5cf6; }
.io-tag.cin { background: #d97706; }
.io-tag.s { background: var(--vp-c-green-1, #16a34a); }
.io-tag.cout { background: #d97706; }
.io-tag.a {
background: var(--vp-c-brand-1);
}
.io-tag.b {
background: #8b5cf6;
}
.io-tag.cin {
background: #d97706;
}
.io-tag.s {
background: var(--vp-c-green-1, #16a34a);
}
.io-tag.cout {
background: #d97706;
}
.stage-divider {
height: 1px;
@@ -7,8 +7,8 @@
<div class="demo-content">
<div class="algorithm-tabs">
<button
v-for="algo in algorithms"
<button
v-for="algo in algorithms"
:key="algo.name"
:class="['tab-btn', { active: activeAlgo === algo.name }]"
@click="activeAlgo = algo.name"
@@ -24,10 +24,7 @@
</div>
<div class="visual-content">
<div
v-if="activeAlgo === '二分查找'"
class="binary-search"
>
<div v-if="activeAlgo === '二分查找'" class="binary-search">
<div class="search-input">
<span>在有序数组中查找</span>
<input
@@ -35,20 +32,15 @@
type="number"
class="num-input"
placeholder="输入数字"
>
<button
class="search-btn"
@click="runBinarySearch"
>
查找
</button>
/>
<button class="search-btn" @click="runBinarySearch">查找</button>
</div>
<div class="array-display">
<div
v-for="(num, i) in sortedArray"
<div
v-for="(num, i) in sortedArray"
:key="i"
class="array-cell"
:class="{
:class="{
highlight: i >= searchRange.left && i <= searchRange.right,
found: i === foundIndex,
mid: i === midIndex
@@ -57,44 +49,27 @@
{{ num }}
</div>
</div>
<div
v-if="searchSteps.length"
class="search-info"
>
<div
v-for="(step, i) in searchSteps"
:key="i"
class="step"
>
<div v-if="searchSteps.length" class="search-info">
<div v-for="(step, i) in searchSteps" :key="i" class="step">
{{ step }}
</div>
</div>
</div>
<div
v-else-if="activeAlgo === '排序'"
class="sorting"
>
<div v-else-if="activeAlgo === '排序'" class="sorting">
<div class="sort-controls">
<button
class="sort-btn"
@click="resetArray"
>
重置数组
</button>
<button
class="sort-btn"
@click="runSort"
>
开始排序
</button>
<button class="sort-btn" @click="resetArray">重置数组</button>
<button class="sort-btn" @click="runSort">开始排序</button>
</div>
<div class="array-display">
<div
v-for="(num, i) in sortArray"
<div
v-for="(num, i) in sortArray"
:key="i"
class="array-cell"
:class="{ comparing: comparingIndices.includes(i), sorted: sortedIndices.includes(i) }"
:class="{
comparing: comparingIndices.includes(i),
sorted: sortedIndices.includes(i)
}"
>
{{ num }}
</div>
@@ -104,10 +79,7 @@
</div>
</div>
<div
v-else-if="activeAlgo === '递归'"
class="recursion"
>
<div v-else-if="activeAlgo === '递归'" class="recursion">
<div class="recursion-input">
<span>计算斐波那契数列第</span>
<input
@@ -116,28 +88,15 @@
min="1"
max="15"
class="num-input"
>
/>
<span></span>
<button
class="calc-btn"
@click="calcFib"
>
计算
</button>
<button class="calc-btn" @click="calcFib">计算</button>
</div>
<div
v-if="fibResult !== null"
class="fib-result"
>
<div v-if="fibResult !== null" class="fib-result">
<span class="result-value">F({{ fibN }}) = {{ fibResult }}</span>
</div>
<div
v-if="fibSteps.length"
class="recursion-tree"
>
<div class="tree-title">
递归调用过程
</div>
<div v-if="fibSteps.length" class="recursion-tree">
<div class="tree-title">递归调用过程</div>
<div class="tree-content">
<div
v-for="(step, i) in fibSteps.slice(0, 8)"
@@ -146,20 +105,14 @@
>
{{ step }}
</div>
<div
v-if="fibSteps.length > 8"
class="tree-more"
>
<div v-if="fibSteps.length > 8" class="tree-more">
... {{ fibSteps.length }} 次调用
</div>
</div>
</div>
</div>
<div
v-else-if="activeAlgo === '贪心'"
class="greedy"
>
<div v-else-if="activeAlgo === '贪心'" class="greedy">
<div class="greedy-desc">
硬币找零问题用最少的硬币凑出指定金额
</div>
@@ -170,30 +123,18 @@
type="number"
min="1"
class="num-input"
>
<button
class="calc-btn"
@click="calcCoins"
>
计算
</button>
/>
<button class="calc-btn" @click="calcCoins">计算</button>
</div>
<div class="coins-available">
可用硬币{{ coins.join(', ') }}
</div>
<div
v-if="coinResult.length"
class="coin-result"
>
<div class="result-title">
找零方案
</div>
<div v-if="coinResult.length" class="coin-result">
<div class="result-title">找零方案</div>
<div class="coin-list">
<span
v-for="(c, i) in coinResult"
:key="i"
class="coin"
>{{ c }}</span>
<span v-for="(c, i) in coinResult" :key="i" class="coin"
>{{ c }}</span
>
</div>
<div class="result-summary">
{{ coinResult.length }} 枚硬币
@@ -204,20 +145,11 @@
</div>
<div class="complexity-info">
<div class="info-title">
时间复杂度速查
</div>
<div class="info-title">时间复杂度速查</div>
<div class="complexity-list">
<div
v-for="c in complexities"
:key="c.name"
class="complexity-item"
>
<div v-for="c in complexities" :key="c.name" class="complexity-item">
<span class="c-name">{{ c.name }}</span>
<span
class="c-value"
:class="c.class"
>{{ c.value }}</span>
<span class="c-value" :class="c.class">{{ c.value }}</span>
<span class="c-desc">{{ c.desc }}</span>
</div>
</div>
@@ -225,7 +157,8 @@
</div>
<div class="info-box">
<strong>核心思想</strong>算法是解决问题的方法好的算法能让程序效率提升几个数量级理解算法思维比记住具体算法更重要
<strong>核心思想</strong
>算法是解决问题的方法好的算法能让程序效率提升几个数量级理解算法思维比记住具体算法更重要
</div>
</div>
</template>
@@ -243,7 +176,7 @@ const algorithms = [
]
const currentAlgo = computed(() => {
return algorithms.find(a => a.name === activeAlgo.value)
return algorithms.find((a) => a.name === activeAlgo.value)
})
const sortedArray = ref([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25])
@@ -259,25 +192,31 @@ const runBinarySearch = () => {
midIndex.value = -1
let left = 0
let right = sortedArray.value.length - 1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
midIndex.value = mid
searchRange.left = left
searchRange.right = right
searchSteps.value.push(`查找范围 [${left}, ${right}],中间位置 ${mid},值 ${sortedArray.value[mid]}`)
searchSteps.value.push(
`查找范围 [${left}, ${right}],中间位置 ${mid},值 ${sortedArray.value[mid]}`
)
if (sortedArray.value[mid] === searchTarget.value) {
foundIndex.value = mid
searchSteps.value.push(`找到目标 ${searchTarget.value} 在位置 ${mid}`)
return
} else if (sortedArray.value[mid] < searchTarget.value) {
left = mid + 1
searchSteps.value.push(`${sortedArray.value[mid]} < ${searchTarget.value},在右半部分继续查找`)
searchSteps.value.push(
`${sortedArray.value[mid]} < ${searchTarget.value},在右半部分继续查找`
)
} else {
right = mid - 1
searchSteps.value.push(`${sortedArray.value[mid]} > ${searchTarget.value},在左半部分继续查找`)
searchSteps.value.push(
`${sortedArray.value[mid]} > ${searchTarget.value},在左半部分继续查找`
)
}
}
searchSteps.value.push(`未找到目标 ${searchTarget.value}`)
@@ -299,18 +238,18 @@ const runSort = async () => {
sortedIndices.value = []
const arr = [...sortArray.value]
const n = arr.length
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - i - 1; j++) {
comparingIndices.value = [j, j + 1]
sortStatus.value = `比较 ${arr[j]}${arr[j + 1]}`
await new Promise(r => setTimeout(r, 300))
await new Promise((r) => setTimeout(r, 300))
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
;[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
sortArray.value = [...arr]
sortStatus.value = `交换 ${arr[j + 1]}${arr[j]}`
await new Promise(r => setTimeout(r, 200))
await new Promise((r) => setTimeout(r, 200))
}
}
sortedIndices.value.push(n - i - 1)
@@ -353,7 +292,12 @@ const complexities = [
{ name: 'O(1)', value: '常数', desc: '最优,如数组访问', class: 'good' },
{ name: 'O(log n)', value: '对数', desc: '很好,如二分查找', class: 'good' },
{ name: 'O(n)', value: '线性', desc: '一般,如遍历', class: 'mid' },
{ name: 'O(n log n)', value: '线性对数', desc: '可接受,如快速排序', class: 'mid' },
{
name: 'O(n log n)',
value: '线性对数',
desc: '可接受,如快速排序',
class: 'mid'
},
{ name: 'O(n²)', value: '平方', desc: '较慢,如冒泡排序', class: 'bad' },
{ name: 'O(2ⁿ)', value: '指数', desc: '很慢,如暴力递归', class: 'bad' }
]
@@ -375,8 +319,15 @@ const complexities = [
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.algorithm-tabs {
display: flex;
@@ -422,7 +373,9 @@ const complexities = [
margin-left: 0.5rem;
}
.search-input, .greedy-input, .recursion-input {
.search-input,
.greedy-input,
.recursion-input {
display: flex;
align-items: center;
gap: 0.5rem;
@@ -438,7 +391,9 @@ const complexities = [
background: var(--vp-c-bg-alt);
}
.search-btn, .sort-btn, .calc-btn {
.search-btn,
.sort-btn,
.calc-btn {
padding: 0.25rem 0.75rem;
background: var(--vp-c-brand);
color: white;
@@ -487,7 +442,8 @@ const complexities = [
background: var(--vp-c-success-soft);
}
.search-info, .sort-info {
.search-info,
.sort-info {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
@@ -614,9 +570,15 @@ const complexities = [
min-width: 50px;
}
.c-value.good { color: var(--vp-c-success); }
.c-value.mid { color: var(--vp-c-warning); }
.c-value.bad { color: var(--vp-c-danger); }
.c-value.good {
color: var(--vp-c-success);
}
.c-value.mid {
color: var(--vp-c-warning);
}
.c-value.bad {
color: var(--vp-c-danger);
}
.c-desc {
color: var(--vp-c-text-2);
@@ -632,5 +594,4 @@ const complexities = [
display: flex;
gap: 0.25rem;
}
</style>
@@ -9,9 +9,9 @@
<div class="analogy-content">
<div class="analogy-icon">📖</div>
<div class="analogy-text">
<strong>算法就像菜谱</strong><br>
食材 = 数据<br>
烹饪步骤 = 算法<br>
<strong>算法就像菜谱</strong><br />
食材 = 数据<br />
烹饪步骤 = 算法<br />
美味菜肴 = 结果
</div>
</div>
@@ -49,8 +49,12 @@
<div class="detail-section">
<div class="section-title">生活类比</div>
<div class="analogy-card">
<div class="analogy-scenario">{{ currentCategory.analogy.scenario }}</div>
<div class="analogy-explanation">{{ currentCategory.analogy.explanation }}</div>
<div class="analogy-scenario">
{{ currentCategory.analogy.scenario }}
</div>
<div class="analogy-explanation">
{{ currentCategory.analogy.explanation }}
</div>
</div>
</div>
@@ -58,7 +62,9 @@
<div class="section-title">时间复杂度</div>
<div class="complexity-display">
<div class="complexity-bigO">{{ currentCategory.complexity }}</div>
<div class="complexity-desc">{{ currentCategory.complexityDesc }}</div>
<div class="complexity-desc">
{{ currentCategory.complexityDesc }}
</div>
</div>
</div>
@@ -140,7 +146,8 @@ const categories = [
idea: '从数据集合中找到特定元素的过程',
analogy: {
scenario: '在字典里查单词',
explanation: '顺序查找 = 从第一页翻到最后一页;二分查找 = 直接翻到中间,判断在前半还是后半'
explanation:
'顺序查找 = 从第一页翻到最后一页;二分查找 = 直接翻到中间,判断在前半还是后半'
},
complexity: 'O(log n)',
complexityDesc: '二分查找非常快,每次排除一半数据',
@@ -154,7 +161,8 @@ const categories = [
idea: '将无序数据重新排列成有序序列',
analogy: {
scenario: '整理扑克牌',
explanation: '插入排序 = 每次拿一张牌插到正确的位置;快速排序 = 把牌分成大小两堆,递归整理'
explanation:
'插入排序 = 每次拿一张牌插到正确的位置;快速排序 = 把牌分成大小两堆,递归整理'
},
complexity: 'O(n log n)',
complexityDesc: '快速排序、归并排序是最高效的通用排序算法',
@@ -168,7 +176,8 @@ const categories = [
idea: '将大问题分解为相同类型的小问题',
analogy: {
scenario: '俄罗斯套娃',
explanation: '打开一个大娃娃,里面有个小一点的娃娃,再打开还有更小的...直到最小的一个'
explanation:
'打开一个大娃娃,里面有个小一点的娃娃,再打开还有更小的...直到最小的一个'
},
complexity: 'O(log n) 到 O(2ⁿ)',
complexityDesc: '取决于问题类型,二分查找递归很快,斐波那契递归较慢',
@@ -182,7 +191,8 @@ const categories = [
idea: '在每一步选择中都采取当前状态下最优的选择',
analogy: {
scenario: '找零钱',
explanation: '找 37 元零钱:先拿一张 20(最大可能),再拿 10、5、1、1,每次都选最大的面值'
explanation:
'找 37 元零钱:先拿一张 20(最大可能),再拿 10、5、1、1,每次都选最大的面值'
},
complexity: 'O(n) 或 O(n log n)',
complexityDesc: '通常很快,但可能得不到全局最优解',
@@ -196,7 +206,8 @@ const categories = [
idea: '将复杂问题分解为子问题,保存子问题的解',
analogy: {
scenario: '爬楼梯',
explanation: '要爬到第 n 级,可以从 n-1 级跨 1 步,或从 n-2 级跨 2 步,记住之前的结果避免重复计算'
explanation:
'要爬到第 n 级,可以从 n-1 级跨 1 步,或从 n-2 级跨 2 步,记住之前的结果避免重复计算'
},
complexity: 'O(n²) 或 O(n³)',
complexityDesc: '用空间换时间,比递归快很多',
@@ -206,12 +217,19 @@ const categories = [
const complexityChart = [
{ name: '二分查找', complexity: 'O(log n)', width: '10%', color: '#10b981' },
{ name: '快速排序', complexity: 'O(n log n)', width: '25%', color: '#3b82f6' },
{
name: '快速排序',
complexity: 'O(n log n)',
width: '25%',
color: '#3b82f6'
},
{ name: '插入排序', complexity: 'O(n²)', width: '50%', color: '#f59e0b' },
{ name: '暴力递归', complexity: 'O(2ⁿ)', width: '100%', color: '#ef4444' }
]
const currentCategory = computed(() => categories.find(c => c.id === activeCategory.value))
const currentCategory = computed(() =>
categories.find((c) => c.id === activeCategory.value)
)
</script>
<style scoped>
@@ -230,8 +248,14 @@ const currentCategory = computed(() => categories.find(c => c.id === activeCateg
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.analogy-box {
background: var(--vp-c-bg);
@@ -344,7 +368,8 @@ const currentCategory = computed(() => categories.find(c => c.id === activeCateg
gap: 1.5rem;
}
.detail-section {}
.detail-section {
}
.section-title {
font-weight: 600;
@@ -66,7 +66,9 @@
<div class="section-title">时间复杂度</div>
<div class="complexity-box">
<div class="complexity-value">{{ currentParadigm.complexity }}</div>
<div class="complexity-note">{{ currentParadigm.complexityNote }}</div>
<div class="complexity-note">
{{ currentParadigm.complexityNote }}
</div>
</div>
</div>
</div>
@@ -216,7 +218,9 @@ const comparisonData = [
}
]
const currentParadigm = computed(() => paradigms.find(p => p.id === activeParadigm.value))
const currentParadigm = computed(() =>
paradigms.find((p) => p.id === activeParadigm.value)
)
</script>
<style scoped>
@@ -235,8 +239,14 @@ const currentParadigm = computed(() => paradigms.find(p => p.id === activeParadi
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.intro-text {
padding: 1rem;
@@ -324,7 +334,8 @@ const currentParadigm = computed(() => paradigms.find(p => p.id === activeParadi
gap: 1.5rem;
}
.detail-section {}
.detail-section {
}
.section-title {
font-weight: 600;
@@ -34,7 +34,11 @@
<div class="detail-section">
<div class="section-title">工作原理</div>
<div class="section-steps">
<div v-for="(step, index) in currentProtocol.steps" :key="index" class="step-item">
<div
v-for="(step, index) in currentProtocol.steps"
:key="index"
class="step-item"
>
<span class="step-num">{{ index + 1 }}</span>
<span class="step-text">{{ step }}</span>
</div>
@@ -44,7 +48,11 @@
<div class="detail-section">
<div class="section-title">日常应用</div>
<div class="app-list">
<div v-for="(app, index) in currentProtocol.apps" :key="index" class="app-tag">
<div
v-for="(app, index) in currentProtocol.apps"
:key="index"
class="app-tag"
>
{{ app.icon }} {{ app.name }}
</div>
</div>
@@ -268,8 +276,14 @@ const currentProtocol = computed(() => protocolDetails[activeProtocol.value])
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.protocol-gallery {
display: grid;
@@ -7,8 +7,8 @@
<div class="code-input">
<div class="input-title">输入代码</div>
<textarea
v-model="sourceCode"
<textarea
v-model="sourceCode"
class="code-textarea"
placeholder="输入 C 语言代码..."
></textarea>
@@ -17,11 +17,7 @@
<div class="compilation-steps">
<div class="steps-title">编译步骤</div>
<div class="steps-flow">
<div
v-for="(step, index) in steps"
:key="index"
class="step-item"
>
<div v-for="(step, index) in steps" :key="index" class="step-item">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-name">{{ step.name }}</div>
@@ -35,11 +31,7 @@
<div class="file-outputs">
<div class="outputs-title">生成的文件</div>
<div class="file-list">
<div
v-for="file in outputFiles"
:key="file.name"
class="file-item"
>
<div v-for="file in outputFiles" :key="file.name" class="file-item">
<div class="file-icon">{{ file.icon }}</div>
<div class="file-info">
<div class="file-name">{{ file.name }}</div>
@@ -147,8 +139,14 @@ const outputFiles = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.code-input {
margin-bottom: 2rem;
@@ -28,7 +28,9 @@
<div class="step-desc">{{ step.desc }}</div>
<div class="step-example">{{ step.example }}</div>
</div>
<div v-if="index < translationSteps.length - 1" class="step-arrow"></div>
<div v-if="index < translationSteps.length - 1" class="step-arrow">
</div>
</div>
</div>
</div>
@@ -42,11 +44,7 @@
</div>
<div class="token-arrow"></div>
<div class="tokens-list">
<div
v-for="(token, index) in tokens"
:key="index"
class="token-item"
>
<div v-for="(token, index) in tokens" :key="index" class="token-item">
<span class="token-type">{{ token.type }}</span>
<span class="token-value">{{ token.value }}</span>
</div>
@@ -127,9 +125,7 @@
<div class="opt-code">x = 10</div>
</div>
</div>
<div class="opt-note">
编译器会自动优化代码提高运行效率
</div>
<div class="opt-note">编译器会自动优化代码提高运行效率</div>
</div>
</div>
</div>
@@ -196,8 +192,14 @@ const tokens = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.analogy-intro {
margin-bottom: 2rem;
@@ -105,8 +105,7 @@
</div>
</template>
<script setup>
</script>
<script setup></script>
<style scoped>
.data-encoding-basics-demo {
@@ -124,8 +123,14 @@
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.encoding-intro {
padding: 1rem;
@@ -22,7 +22,11 @@
<div class="stage-example">
<div class="example-label">示例{{ stage.example.label }}</div>
<div class="example-content">
<div v-for="(item, i) in stage.example.items" :key="i" class="example-item">
<div
v-for="(item, i) in stage.example.items"
:key="i"
class="example-item"
>
<span class="item-label">{{ item.label }}:</span>
<span class="item-value">{{ item.value }}</span>
</div>
@@ -44,7 +48,11 @@
<div class="lifecycle-summary">
<div class="summary-title">数据转换的关键点</div>
<div class="summary-grid">
<div v-for="(point, index) in keyPoints" :key="index" class="summary-card">
<div
v-for="(point, index) in keyPoints"
:key="index"
class="summary-card"
>
<div class="card-icon">{{ point.icon }}</div>
<div class="card-text">
<div class="card-title">{{ point.title }}</div>
@@ -67,7 +75,8 @@ const stages = [
name: '数据输入',
icon: '⌨️',
title: '阶段 1:数据输入',
description: '用户通过各种输入设备(键盘、鼠标、触摸屏、麦克风等)将信息输入到计算机系统中。',
description:
'用户通过各种输入设备(键盘、鼠标、触摸屏、麦克风等)将信息输入到计算机系统中。',
example: {
label: '用户输入文字',
items: [
@@ -83,7 +92,8 @@ const stages = [
name: '数据处理',
icon: '🔄',
title: '阶段 2:数据处理',
description: 'CPU 对输入的数据进行计算、转换、格式化等操作,应用程序根据业务逻辑处理数据。',
description:
'CPU 对输入的数据进行计算、转换、格式化等操作,应用程序根据业务逻辑处理数据。',
example: {
label: '文本编辑器处理',
items: [
@@ -99,7 +109,8 @@ const stages = [
name: '数据存储',
icon: '💾',
title: '阶段 3:数据存储',
description: '处理后的数据被保存到存储设备中(内存、硬盘、SSD、云存储等),以便后续使用。',
description:
'处理后的数据被保存到存储设备中(内存、硬盘、SSD、云存储等),以便后续使用。',
example: {
label: '保存文档',
items: [
@@ -115,7 +126,8 @@ const stages = [
name: '数据传输',
icon: '📡',
title: '阶段 4:数据传输',
description: '数据通过网络(局域网、互联网)或内部总线从一个位置传输到另一个位置。',
description:
'数据通过网络(局域网、互联网)或内部总线从一个位置传输到另一个位置。',
example: {
label: '上传文件',
items: [
@@ -131,7 +143,8 @@ const stages = [
name: '数据输出',
icon: '🖥️',
title: '阶段 5:数据输出',
description: '数据通过输出设备(显示器、打印机、扬声器等)呈现给用户,或传输给其他系统。',
description:
'数据通过输出设备(显示器、打印机、扬声器等)呈现给用户,或传输给其他系统。',
example: {
label: '显示网页',
items: [
@@ -184,8 +197,14 @@ const keyPoints = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.lifecycle-flow {
display: flex;
@@ -11,13 +11,22 @@
<div
v-for="device in devices"
:key="device.id"
:class="['lan-device', { active: activeDevice === device.id, sender: device.role === 'sender', receiver: device.role === 'receiver' }]"
:class="[
'lan-device',
{
active: activeDevice === device.id,
sender: device.role === 'sender',
receiver: device.role === 'receiver'
}
]"
@click="activeDevice = device.id"
>
<div class="device-icon">{{ device.icon }}</div>
<div class="device-name">{{ device.name }}</div>
<div class="device-mac">{{ device.mac }}</div>
<div v-if="device.role" class="device-role">{{ device.roleText }}</div>
<div v-if="device.role" class="device-role">
{{ device.roleText }}
</div>
</div>
</div>
@@ -77,7 +86,9 @@
<div class="arp-arrow"> 广播到局域网</div>
<div class="arp-answer">
<span class="answer-icon"></span>
<span class="answer-text">我是我的 MAC 地址是 00:11:22:33:44:66</span>
<span class="answer-text"
>我是我的 MAC 地址是 00:11:22:33:44:66</span
>
</div>
</div>
</div>
@@ -194,8 +205,14 @@ const transferSteps = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.lan-scene {
background: var(--vp-c-bg);
@@ -7,8 +7,8 @@
<div class="demo-content">
<div class="structure-tabs">
<button
v-for="s in structures"
<button
v-for="s in structures"
:key="s.name"
:class="['tab-btn', { active: activeStructure === s.name }]"
@click="activeStructure = s.name"
@@ -24,16 +24,9 @@
</div>
<div class="visual-content">
<div
v-if="activeStructure === '数组'"
class="array-visual"
>
<div v-if="activeStructure === '数组'" class="array-visual">
<div class="array-container">
<div
v-for="(item, i) in arrayData"
:key="i"
class="array-item"
>
<div v-for="(item, i) in arrayData" :key="i" class="array-item">
<span class="index">{{ i }}</span>
<span class="value">{{ item }}</span>
</div>
@@ -43,21 +36,13 @@
</div>
</div>
<div
v-else-if="activeStructure === '链表'"
class="linked-visual"
>
<div v-else-if="activeStructure === '链表'" class="linked-visual">
<div class="linked-container">
<div
v-for="(item, i) in linkedData"
:key="i"
class="linked-node"
>
<div v-for="(item, i) in linkedData" :key="i" class="linked-node">
<span class="node-value">{{ item.value }}</span>
<span
v-if="i < linkedData.length - 1"
class="node-arrow"
></span>
<span v-if="i < linkedData.length - 1" class="node-arrow"
></span
>
</div>
</div>
<div class="operation-hint">
@@ -65,104 +50,53 @@
</div>
</div>
<div
v-else-if="activeStructure === '栈'"
class="stack-visual"
>
<div v-else-if="activeStructure === '栈'" class="stack-visual">
<div class="stack-container">
<div
v-for="(item, i) in stackData"
:key="i"
class="stack-item"
>
<div v-for="(item, i) in stackData" :key="i" class="stack-item">
{{ item }}
</div>
<div class="stack-bottom">
栈底
</div>
<div class="stack-bottom">栈底</div>
</div>
<div class="stack-ops">
<button
class="op-btn"
@click="pushStack"
>
入栈 Push
</button>
<button
class="op-btn"
@click="popStack"
>
出栈 Pop
</button>
</div>
<div class="operation-hint">
后进先出 (LIFO)操作都是 O(1)
<button class="op-btn" @click="pushStack">入栈 Push</button>
<button class="op-btn" @click="popStack">出栈 Pop</button>
</div>
<div class="operation-hint">后进先出 (LIFO)操作都是 O(1)</div>
</div>
<div
v-else-if="activeStructure === '队列'"
class="queue-visual"
>
<div v-else-if="activeStructure === '队列'" class="queue-visual">
<div class="queue-container">
<span class="queue-label"> </span>
<div
v-for="(item, i) in queueData"
:key="i"
class="queue-item"
>
<div v-for="(item, i) in queueData" :key="i" class="queue-item">
{{ item }}
</div>
<span class="queue-label"> </span>
</div>
<div class="queue-ops">
<button
class="op-btn"
@click="enqueue"
>
入队
</button>
<button
class="op-btn"
@click="dequeue"
>
出队
</button>
</div>
<div class="operation-hint">
先进先出 (FIFO)操作都是 O(1)
<button class="op-btn" @click="enqueue">入队</button>
<button class="op-btn" @click="dequeue">出队</button>
</div>
<div class="operation-hint">先进先出 (FIFO)操作都是 O(1)</div>
</div>
<div
v-else-if="activeStructure === '哈希表'"
class="hash-visual"
>
<div v-else-if="activeStructure === '哈希表'" class="hash-visual">
<div class="hash-container">
<div
v-for="(bucket, i) in hashData"
:key="i"
class="hash-bucket"
>
<div v-for="(bucket, i) in hashData" :key="i" class="hash-bucket">
<span class="bucket-index">{{ i }}</span>
<div class="bucket-items">
<span
v-for="(item, j) in bucket"
:key="j"
class="bucket-item"
>{{ item }}</span>
>{{ item }}</span
>
</div>
</div>
</div>
<div class="operation-hint">
查找/插入/删除平均 O(1)最坏 O(n)
</div>
<div class="operation-hint">查找/插入/删除平均 O(1)最坏 O(n)</div>
</div>
<div
v-else-if="activeStructure === '树'"
class="tree-visual"
>
<div v-else-if="activeStructure === '树'" class="tree-visual">
<div class="tree-container">
<div class="tree-level">
<div class="tree-node root">
@@ -192,17 +126,13 @@
</div>
</div>
</div>
<div class="operation-hint">
查找/插入/删除 O(log n)遍历 O(n)
</div>
<div class="operation-hint">查找/插入/删除 O(log n)遍历 O(n)</div>
</div>
</div>
</div>
<div class="complexity-table">
<div class="table-title">
时间复杂度对比
</div>
<div class="table-title">时间复杂度对比</div>
<table>
<thead>
<tr>
@@ -216,63 +146,31 @@
<tbody>
<tr>
<td>访问</td>
<td class="good">
O(1)
</td>
<td class="bad">
O(n)
</td>
<td class="good">
O(1)
</td>
<td class="mid">
O(log n)
</td>
<td class="good">O(1)</td>
<td class="bad">O(n)</td>
<td class="good">O(1)</td>
<td class="mid">O(log n)</td>
</tr>
<tr>
<td>查找</td>
<td class="bad">
O(n)
</td>
<td class="bad">
O(n)
</td>
<td class="good">
O(1)
</td>
<td class="mid">
O(log n)
</td>
<td class="bad">O(n)</td>
<td class="bad">O(n)</td>
<td class="good">O(1)</td>
<td class="mid">O(log n)</td>
</tr>
<tr>
<td>插入</td>
<td class="bad">
O(n)
</td>
<td class="good">
O(1)
</td>
<td class="good">
O(1)
</td>
<td class="mid">
O(log n)
</td>
<td class="bad">O(n)</td>
<td class="good">O(1)</td>
<td class="good">O(1)</td>
<td class="mid">O(log n)</td>
</tr>
<tr>
<td>删除</td>
<td class="bad">
O(n)
</td>
<td class="good">
O(1)
</td>
<td class="good">
O(1)
</td>
<td class="mid">
O(log n)
</td>
<td class="bad">O(n)</td>
<td class="good">O(1)</td>
<td class="good">O(1)</td>
<td class="mid">O(log n)</td>
</tr>
</tbody>
</table>
@@ -280,7 +178,8 @@
</div>
<div class="info-box">
<strong>核心思想</strong>数据结构是数据的"容器"不同的容器有不同的特点选择合适的数据结构能让程序效率提升几个数量级
<strong>核心思想</strong
>数据结构是数据的"容器"不同的容器有不同的特点选择合适的数据结构能让程序效率提升几个数量级
</div>
</div>
</template>
@@ -300,7 +199,7 @@ const structures = [
]
const currentStructure = computed(() => {
return structures.find(s => s.name === activeStructure.value)
return structures.find((s) => s.name === activeStructure.value)
})
const arrayData = ref([10, 20, 30, 40, 50, 60, 70, 80])
@@ -382,8 +281,15 @@ const treeData = ref({
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.structure-tabs {
display: flex;
@@ -500,7 +406,8 @@ const treeData = ref({
margin-top: 0.25rem;
}
.stack-ops, .queue-ops {
.stack-ops,
.queue-ops {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
@@ -626,7 +533,8 @@ table {
font-size: 0.8rem;
}
th, td {
th,
td {
border: 1px solid var(--vp-c-divider);
padding: 0.35rem;
text-align: center;
@@ -636,9 +544,16 @@ th {
background: var(--vp-c-bg);
}
.good { color: var(--vp-c-success); font-weight: bold; }
.mid { color: var(--vp-c-warning); }
.bad { color: var(--vp-c-danger); }
.good {
color: var(--vp-c-success);
font-weight: bold;
}
.mid {
color: var(--vp-c-warning);
}
.bad {
color: var(--vp-c-danger);
}
.info-box {
background: var(--vp-c-bg-alt);
@@ -650,5 +565,4 @@ th {
display: flex;
gap: 0.25rem;
}
</style>
@@ -152,11 +152,7 @@ const categories = [
icon: '🗂️',
desc: '通过关键词快速查找',
examples: ['哈希表', '字典', '集合'],
features: [
'通过键值对存储数据',
'查找速度极快',
'数据之间没有顺序关系'
],
features: ['通过键值对存储数据', '查找速度极快', '数据之间没有顺序关系'],
scenarios: [
{
icon: '📖',
@@ -189,11 +185,7 @@ const categories = [
icon: '🌳',
desc: '层级关系,像家谱',
examples: ['二叉树', 'B 树', '堆'],
features: [
'一对多的层级关系',
'有明确的根节点',
'适合表示分类和层级'
],
features: ['一对多的层级关系', '有明确的根节点', '适合表示分类和层级'],
scenarios: [
{
icon: '📁',
@@ -226,11 +218,7 @@ const categories = [
icon: '🕸️',
desc: '复杂关系网络',
examples: ['有向图', '无向图', '网络图'],
features: [
'多对多的复杂关系',
'节点之间可以任意连接',
'可以表示复杂网络'
],
features: ['多对多的复杂关系', '节点之间可以任意连接', '可以表示复杂网络'],
scenarios: [
{
icon: '🗺️',
@@ -259,7 +247,9 @@ const categories = [
}
]
const currentCategory = computed(() => categories.find(c => c.id === activeCategory.value))
const currentCategory = computed(() =>
categories.find((c) => c.id === activeCategory.value)
)
</script>
<style scoped>
@@ -278,8 +268,14 @@ const currentCategory = computed(() => categories.find(c => c.id === activeCateg
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.structure-map {
margin-bottom: 2rem;
@@ -383,7 +379,8 @@ const currentCategory = computed(() => categories.find(c => c.id === activeCateg
gap: 1.5rem;
}
.detail-section {}
.detail-section {
}
.section-title {
font-weight: 600;
@@ -24,7 +24,9 @@
<!-- 推荐结果 -->
<div v-if="activeScenario" class="recommendation">
<div class="rec-header">
<span class="rec-title">推荐使用{{ currentScenario.recommendation }}</span>
<span class="rec-title"
>推荐使用{{ currentScenario.recommendation }}</span
>
</div>
<div class="rec-reason">
@@ -153,11 +155,7 @@ const scenarios = [
name: '后进先出',
desc: '最后进入的最先处理',
recommendation: '栈',
reasons: [
'只能在栈顶操作',
'入栈出栈都是 O(1)',
'适合回溯和撤销操作'
],
reasons: ['只能在栈顶操作', '入栈出栈都是 O(1)', '适合回溯和撤销操作'],
example: '浏览器后退、编辑器撤销、函数调用栈'
},
{
@@ -166,11 +164,7 @@ const scenarios = [
name: '先进先出',
desc: '先来的先处理',
recommendation: '队列',
reasons: [
'一端入队,另一端出队',
'入队出队都是 O(1)',
'公平的调度方式'
],
reasons: ['一端入队,另一端出队', '入队出队都是 O(1)', '公平的调度方式'],
example: '打印队列、任务调度、消息队列'
},
{
@@ -179,11 +173,7 @@ const scenarios = [
name: '层级关系',
desc: '数据之间有父子层级关系',
recommendation: '树',
reasons: [
'清晰表达层级结构',
'查找效率 O(log n)',
'支持多种遍历方式'
],
reasons: ['清晰表达层级结构', '查找效率 O(log n)', '支持多种遍历方式'],
example: '文件系统、组织架构、HTML DOM'
},
{
@@ -192,11 +182,7 @@ const scenarios = [
name: '复杂关系',
desc: '数据之间有多对多的复杂连接',
recommendation: '图',
reasons: [
'可以表示任意关系',
'支持路径搜索算法',
'适合网络和社交关系'
],
reasons: ['可以表示任意关系', '支持路径搜索算法', '适合网络和社交关系'],
example: '社交网络、地图导航、网页链接'
}
]
@@ -211,7 +197,7 @@ const referenceTable = [
]
const currentScenario = computed(() => {
return scenarios.find(s => s.id === activeScenario.value)
return scenarios.find((s) => s.id === activeScenario.value)
})
</script>
@@ -231,8 +217,14 @@ const currentScenario = computed(() => {
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.scenario-selector {
margin-bottom: 2rem;
@@ -298,8 +290,14 @@ const currentScenario = computed(() => {
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.rec-header {
@@ -7,8 +7,8 @@
<div class="demo-content">
<div class="encoding-tabs">
<button
v-for="tab in tabs"
<button
v-for="tab in tabs"
:key="tab.name"
:class="['tab-btn', { active: activeTab === tab.name }]"
@click="activeTab = tab.name"
@@ -20,24 +20,19 @@
<div class="encoding-area">
<div class="input-section">
<label>输入内容</label>
<input
v-model="inputValue"
<input
v-model="inputValue"
class="input-field"
:placeholder="currentTab.placeholder"
>
/>
</div>
<div class="output-section">
<div class="output-label">
编码结果
</div>
<div class="output-label">编码结果</div>
<div class="output-box">
<code>{{ encodedResult }}</code>
</div>
<div
v-if="currentTab.name === 'text'"
class="output-info"
>
<div v-if="currentTab.name === 'text'" class="output-info">
<span>字符数: {{ inputValue.length }}</span>
<span>字节数: {{ byteCount }}</span>
</div>
@@ -47,18 +42,22 @@
v-if="currentTab.name === 'text' && inputValue"
class="encoding-table"
>
<div class="table-title">
字符编码详情
</div>
<div class="table-title">字符编码详情</div>
<div class="char-list">
<div
v-for="(char, i) in inputValue.slice(0, 10)"
<div
v-for="(char, i) in inputValue.slice(0, 10)"
:key="i"
class="char-item"
>
<span class="char-display">{{ char }}</span>
<span class="char-unicode">U+{{ char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0') }}</span>
<span class="char-binary">{{ char.charCodeAt(0).toString(2).padStart(8, '0') }}</span>
<span class="char-unicode"
>U+{{
char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')
}}</span
>
<span class="char-binary">{{
char.charCodeAt(0).toString(2).padStart(8, '0')
}}</span>
</div>
</div>
</div>
@@ -66,7 +65,9 @@
</div>
<div class="info-box">
<strong>核心思想</strong>所有数据最终都要变成 0 1不同类型的数据用不同的编码规则字符用 ASCII/Unicode数字用二进制图像用像素值
<strong>核心思想</strong>所有数据最终都要变成 0
1不同类型的数据用不同的编码规则字符用
ASCII/Unicode数字用二进制图像用像素值
</div>
</div>
</template>
@@ -84,21 +85,25 @@ const tabs = [
]
const currentTab = computed(() => {
const tab = tabs.find(t => t.name === activeTab.value)
const tab = tabs.find((t) => t.name === activeTab.value)
return {
...tab,
placeholder: tab.name === 'text' ? '输入文字...' :
tab.name === 'number' ? '输入数字...' : '输入颜色值(如 #FF5733)'
placeholder:
tab.name === 'text'
? '输入文字...'
: tab.name === 'number'
? '输入数字...'
: '输入颜色值(如 #FF5733)'
}
})
const encodedResult = computed(() => {
if (!inputValue.value) return ''
switch (activeTab.value) {
case 'text':
return Array.from(inputValue.value)
.map(c => c.charCodeAt(0).toString(2).padStart(8, '0'))
.map((c) => c.charCodeAt(0).toString(2).padStart(8, '0'))
.join(' ')
case 'number':
const num = parseInt(inputValue.value)
@@ -137,8 +142,15 @@ const byteCount = computed(() => {
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.encoding-tabs {
display: flex;
@@ -269,5 +281,4 @@ const byteCount = computed(() => {
display: flex;
gap: 0.25rem;
}
</style>
@@ -35,7 +35,9 @@
<div class="arrow"></div>
<div class="output-box">
<div class="box-label">编码后</div>
<div class="box-value code">{{ currentScenario.encoding.output }}</div>
<div class="box-value code">
{{ currentScenario.encoding.output }}
</div>
</div>
</div>
</div>
@@ -62,11 +64,15 @@
<div class="storage-info">
<div class="info-item">
<span class="info-label">位置:</span>
<span class="info-value">{{ currentScenario.storage.location }}</span>
<span class="info-value">{{
currentScenario.storage.location
}}</span>
</div>
<div class="info-item">
<span class="info-label">大小:</span>
<span class="info-value">{{ currentScenario.storage.size }}</span>
<span class="info-value">{{
currentScenario.storage.size
}}</span>
</div>
</div>
</div>
@@ -83,7 +89,12 @@
<div class="transmission-packet">
<div class="packet-header">数据包</div>
<div class="packet-body">
<div class="packet-layer" v-for="(layer, index) in currentScenario.transmission.layers" :key="index">
<div
class="packet-layer"
v-for="(layer, index) in currentScenario.transmission
.layers"
:key="index"
>
<span class="layer-name">{{ layer.name }}:</span>
<span class="layer-value">{{ layer.value }}</span>
</div>
@@ -93,11 +104,15 @@
<div class="transmission-info">
<div class="info-item">
<span class="info-label">协议:</span>
<span class="info-value">{{ currentScenario.transmission.protocol }}</span>
<span class="info-value">{{
currentScenario.transmission.protocol
}}</span>
</div>
<div class="info-item">
<span class="info-label">路径:</span>
<span class="info-value">{{ currentScenario.transmission.path }}</span>
<span class="info-value">{{
currentScenario.transmission.path
}}</span>
</div>
</div>
</div>
@@ -107,11 +122,15 @@
<!-- 协作关系 -->
<div class="collab-relationships">
<div class="relationship-arrow encoding-to-storage">
<span class="arrow-text">{{ currentScenario.relationships.encodingToStorage }}</span>
<span class="arrow-text">{{
currentScenario.relationships.encodingToStorage
}}</span>
<span class="arrow-icon">→</span>
</div>
<div class="relationship-arrow storage-to-transmission">
<span class="arrow-text">{{ currentScenario.relationships.storageToTransmission }}</span>
<span class="arrow-text">{{
currentScenario.relationships.storageToTransmission
}}</span>
<span class="arrow-icon">→</span>
</div>
</div>
@@ -121,7 +140,11 @@
<div class="key-points">
<div class="points-title">协作要点</div>
<div class="points-grid">
<div v-for="(point, index) in currentScenario.points" :key="index" class="point-card">
<div
v-for="(point, index) in currentScenario.points"
:key="index"
class="point-card"
>
<div class="point-icon">{{ point.icon }}</div>
<div class="point-content">
<div class="point-title">{{ point.title }}</div>
@@ -347,8 +370,14 @@ const currentScenario = computed(() => scenarioData[activeScenario.value])
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.scenario-selector {
margin-bottom: 2rem;
@@ -420,8 +449,13 @@ const currentScenario = computed(() => scenarioData[activeScenario.value])
border-bottom: 1px solid var(--vp-c-divider);
}
.stage-icon { font-size: 1.3rem; }
.stage-title { font-weight: 600; font-size: 0.95rem; }
.stage-icon {
font-size: 1.3rem;
}
.stage-title {
font-weight: 600;
font-size: 0.95rem;
}
.stage-content {
display: flex;
@@ -1,34 +1,32 @@
<template>
<div class="filesystem-demo">
<div class="demo-wrapper">
<!-- 文件树逻辑视角 -->
<div class="logical-view">
<div class="view-title">
<span>📁 你的视角 (文件系统)</span>
<span class="subtitle">漂亮整洁的目录树</span>
</div>
<div class="file-tree">
<div class="tree-node folder expanded">
<span class="icon">💾</span> D盘 (根目录)
</div>
<div class="tree-children">
<div class="tree-node folder expanded">
<span class="icon">📂</span> 照片
</div>
<div class="tree-children">
<div
class="tree-node file"
<div
class="tree-node file"
:class="{ active: activeFile === 'pet' }"
@click="selectFile('pet')"
>
<span class="icon">🖼</span> 宠物.jpg
<span class="size-badge">3 </span>
</div>
<div
class="tree-node file"
<div
class="tree-node file"
:class="{ active: activeFile === 'vacation' }"
@click="selectFile('vacation')"
>
@@ -41,8 +39,8 @@
<span class="icon">📂</span> 工作
</div>
<div class="tree-children">
<div
class="tree-node file"
<div
class="tree-node file"
:class="{ active: activeFile === 'doc' }"
@click="selectFile('doc')"
>
@@ -50,7 +48,6 @@
<span class="size-badge">4 </span>
</div>
</div>
</div>
</div>
</div>
@@ -58,7 +55,7 @@
<!-- 翻译官动画 -->
<div class="translator">
<div class="arrow"></div>
<div class="badge">文件系统账本<br/>(inode表)</div>
<div class="badge">文件系统账本<br />(inode表)</div>
<div class="arrow"></div>
</div>
@@ -68,39 +65,36 @@
<span>🖨 硬盘的视角 (物理存储)</span>
<span class="subtitle">无序零散的数据块</span>
</div>
<div class="disk-grid">
<div
v-for="block in 24"
<div
v-for="block in 24"
:key="block"
class="disk-block"
:class="[
getBlockOwner(block),
{ active: isBlockActive(block) }
]"
:class="[getBlockOwner(block), { active: isBlockActive(block) }]"
>
{{ block }}
</div>
</div>
</div>
</div>
<div class="explanation-box" v-if="activeFile">
<span v-if="activeFile === 'pet'">
💡 宠物.jpg 其实被切碎分别放在了第 3814 文件系统帮你做好了翻译你只需双击它
💡 宠物.jpg 其实被切碎分别放在了第 3814
文件系统帮你做好了翻译你只需双击它
</span>
<span v-if="activeFile === 'vacation'">
💡 旅游.png 放在了第 56
</span>
<span v-if="activeFile === 'doc'">
💡 总结.docx 被分散存放在 10111822 如果没有文件系统你得自己背下这些数字才能打开文件
💡 总结.docx 被分散存放在 10111822
如果没有文件系统你得自己背下这些数字才能打开文件
</span>
</div>
<div class="explanation-box default" v-else>
试着点击左侧的文件看看它们在硬盘里到底长什么样
</div>
</div>
</template>
@@ -160,7 +154,8 @@ const isBlockActive = (block) => {
}
}
.logical-view, .physical-view {
.logical-view,
.physical-view {
flex: 1;
background: var(--vp-c-bg-alt);
border-radius: 10px;
@@ -278,20 +273,38 @@ const isBlockActive = (block) => {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.disk-block.owner-pet { background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.3); }
.disk-block.owner-vacation { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); }
.disk-block.owner-doc { background: rgba(245, 158, 11, 0.1); border-color: rgba(245, 158, 11, 0.3); }
.disk-block.owner-pet {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
}
.disk-block.owner-vacation {
background: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.3);
}
.disk-block.owner-doc {
background: rgba(245, 158, 11, 0.1);
border-color: rgba(245, 158, 11, 0.3);
}
.disk-block.active {
transform: scale(1.1);
color: white;
font-weight: bold;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 2;
}
.disk-block.owner-pet.active { background: var(--vp-c-success-1); border-color: var(--vp-c-success-1); }
.disk-block.owner-vacation.active { background: var(--vp-c-brand-1); border-color: var(--vp-c-brand-1); }
.disk-block.owner-doc.active { background: var(--vp-c-warning-1); border-color: var(--vp-c-warning-1); }
.disk-block.owner-pet.active {
background: var(--vp-c-success-1);
border-color: var(--vp-c-success-1);
}
.disk-block.owner-vacation.active {
background: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
}
.disk-block.owner-doc.active {
background: var(--vp-c-warning-1);
border-color: var(--vp-c-warning-1);
}
.explanation-box {
padding: 1rem;
@@ -308,7 +321,13 @@ const isBlockActive = (block) => {
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
</style>
@@ -20,7 +20,8 @@
<!-- MUX Demo -->
<div v-if="currentTab === 'mux'" class="demo-panel">
<div class="panel-desc">
<strong>多路选择器 (MUX)</strong>像铁路道岔一样根据"选择信号"决定让哪一路数据通过
<strong>多路选择器 (MUX)</strong
>像铁路道岔一样根据"选择信号"决定让哪一路数据通过
</div>
<div class="mux-container">
<div class="inputs">
@@ -80,7 +81,8 @@
<!-- Decoder Demo -->
<div v-if="currentTab === 'decoder'" class="demo-panel">
<div class="panel-desc">
<strong>译码器 (Decoder)</strong>将二进制输入转换为特定输出线的激活信号例如 2位输入可以激活
<strong>译码器 (Decoder)</strong
>将二进制输入转换为特定输出线的激活信号例如 2位输入可以激活
4根输出线中的一根
</div>
<div class="decoder-container">
@@ -69,7 +69,11 @@
:cx="node.x"
:cy="node.y"
r="20"
:fill="selectedNode === index ? 'var(--vp-c-brand)' : 'var(--vp-c-brand-soft)'"
:fill="
selectedNode === index
? 'var(--vp-c-brand)'
: 'var(--vp-c-brand-soft)'
"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
@@ -154,7 +158,7 @@ const edges = ref([
])
const averageDegree = computed(() => {
return (edges.value.length * 2 / nodes.length).toFixed(1)
return ((edges.value.length * 2) / nodes.length).toFixed(1)
})
</script>
@@ -174,8 +178,14 @@ const averageDegree = computed(() => {
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.graph-types {
margin-bottom: 2rem;
@@ -8,7 +8,7 @@
<div class="core-idea">
<div class="idea-box">
<div class="idea-text">
贪心算法在每一步选择中都采取当前状态下<strong>最优</strong>的选择<br>
贪心算法在每一步选择中都采取当前状态下<strong>最优</strong>的选择<br />
希望通过一系列局部最优选择达到<strong>全局最优</strong>
</div>
</div>
@@ -36,7 +36,11 @@
需要找零<span class="amount">{{ changeAmount }}</span>
</div>
<div class="change-process">
<div class="process-step" v-for="(step, index) in changeSteps" :key="index">
<div
class="process-step"
v-for="(step, index) in changeSteps"
:key="index"
>
<div class="step-coin">{{ step.coin }}</div>
<div class="step-text">× {{ step.count }} = {{ step.value }}</div>
</div>
@@ -46,7 +50,7 @@
</div>
</div>
<div class="scenario-note">
贪心策略每次选择面值最大的硬币<br>
贪心策略每次选择面值最大的硬币<br />
适用于人民币美元等货币系统
</div>
</div>
@@ -59,9 +63,14 @@
<div
v-for="(activity, index) in activities"
:key="index"
:class="['activity-item', { selected: activity.selected, conflicting: activity.conflicting }]"
:class="[
'activity-item',
{ selected: activity.selected, conflicting: activity.conflicting }
]"
>
<div class="activity-time">{{ activity.start }} - {{ activity.end }}</div>
<div class="activity-time">
{{ activity.start }} - {{ activity.end }}
</div>
<div class="activity-name">{{ activity.name }}</div>
</div>
</div>
@@ -183,17 +192,51 @@ const changeSteps = [
{ coin: '1元', count: 2, value: 2 }
]
const totalCoins = computed(() => changeSteps.reduce((sum, step) => sum + step.count, 0))
const totalCoins = computed(() =>
changeSteps.reduce((sum, step) => sum + step.count, 0)
)
const activities = [
{ start: '9:00', end: '10:00', name: '活动1', selected: true, conflicting: false },
{ start: '9:30', end: '11:30', name: '活动2', selected: false, conflicting: true },
{ start: '10:00', end: '11:00', name: '活动3', selected: true, conflicting: false },
{ start: '10:30', end: '12:00', name: '活动4', selected: false, conflicting: true },
{ start: '11:00', end: '12:00', name: '活动5', selected: true, conflicting: false }
{
start: '9:00',
end: '10:00',
name: '活动1',
selected: true,
conflicting: false
},
{
start: '9:30',
end: '11:30',
name: '活动2',
selected: false,
conflicting: true
},
{
start: '10:00',
end: '11:00',
name: '活动3',
selected: true,
conflicting: false
},
{
start: '10:30',
end: '12:00',
name: '活动4',
selected: false,
conflicting: true
},
{
start: '11:00',
end: '12:00',
name: '活动5',
selected: true,
conflicting: false
}
]
const selectedCount = computed(() => activities.filter(a => a.selected).length)
const selectedCount = computed(
() => activities.filter((a) => a.selected).length
)
</script>
<style scoped>
@@ -212,8 +255,14 @@ const selectedCount = computed(() => activities.filter(a => a.selected).length)
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.core-idea {
margin-bottom: 2rem;
@@ -0,0 +1,333 @@
<template>
<div class="half-adder-demo">
<div class="demo-label">
半加器 (Half Adder) 内部构造 尝试组合 A B观察 XOR异或门
AND与门的分工
</div>
<div class="circuit-container">
<!-- 输入端 -->
<div class="inputs">
<div class="input-line">
<button
class="toggle-btn"
:class="{ on: inputA }"
@click="inputA = !inputA"
>
{{ inputA ? '1' : '0' }}
</button>
<span class="label">输入 A</span>
</div>
<div class="input-line">
<button
class="toggle-btn"
:class="{ on: inputB }"
@click="inputB = !inputB"
>
{{ inputB ? '1' : '0' }}
</button>
<span class="label">输入 B</span>
</div>
</div>
<!-- 连线区域 -->
<div class="wires">
<!-- Path visualization can be complex, using simple SVG lines -->
<svg class="wire-svg" viewBox="0 0 100 150" preserveAspectRatio="none">
<!-- A to XOR -->
<path
d="M 0,30 C 50,30 50,40 100,40"
fill="none"
:stroke="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<!-- B to XOR -->
<path
d="M 0,120 C 50,120 50,60 100,60"
fill="none"
:stroke="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<!-- A to AND -->
<path
d="M 20,30 L 20,90 C 20,90 50,90 100,90"
fill="none"
:stroke="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<!-- B to AND -->
<path
d="M 40,120 L 40,110 C 40,110 50,110 100,110"
fill="none"
:stroke="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
<!-- Nodes -->
<circle
cx="20"
cy="30"
r="3"
:fill="inputA ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
/>
<circle
cx="40"
cy="120"
r="3"
:fill="inputB ? 'var(--vp-c-brand-1)' : 'var(--vp-c-text-3)'"
/>
</svg>
</div>
<!-- 逻辑门 -->
<div class="gates">
<div class="gate-box xor-gate" :class="{ active: sumOut }">
<div class="gate-name">XOR </div>
<div class="gate-desc">计算"本位" (相加结果)</div>
</div>
<div class="gate-box and-gate" :class="{ active: carryOut }">
<div class="gate-name">AND </div>
<div class="gate-desc">计算"进位" (满2进1)</div>
</div>
</div>
<!-- 线 -->
<div class="wires outputs-wires">
<svg class="wire-svg" viewBox="0 0 50 150" preserveAspectRatio="none">
<line
x1="0"
y1="50"
x2="50"
y2="50"
:stroke="
sumOut ? 'var(--vp-c-green-1, #16a34a)' : 'var(--vp-c-text-3)'
"
stroke-width="2"
/>
<line
x1="0"
y1="100"
x2="50"
y2="100"
:stroke="carryOut ? '#d97706' : 'var(--vp-c-text-3)'"
stroke-width="2"
/>
</svg>
</div>
<!-- 输出端 -->
<div class="outputs">
<div class="output-line" :class="{ active: sumOut }">
<span class="label">本位 (Sum)</span>
<span class="out-val s-val">{{ sumOut ? '1' : '0' }}</span>
</div>
<div class="output-line" :class="{ active: carryOut }">
<span class="label">向前进位 (Carry)</span>
<span class="out-val c-val">{{ carryOut ? '1' : '0' }}</span>
</div>
</div>
</div>
<div class="logic-explain">
<p>
你的输入是 {{ inputA ? '1' : '0' }} {{ inputB ? '1' : '0' }}<br />
<strong>XOR </strong>判断它们不仅要"相加"还看是否"不同"{{
inputA !== inputB ? '不同,出1' : '相同,出0'
}}
> 核心本位 <strong>{{ sumOut ? '1' : '0' }}</strong
><br />
<strong>AND </strong>暗中观察是否"全为真"{{
inputA && inputB ? '全为 1,产生进位!' : '没有全为 1,不进位'
}}
> 进位信号 <strong>{{ carryOut ? '1' : '0' }}</strong
>
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inputA = ref(false)
const inputB = ref(true)
const sumOut = computed(() => inputA.value !== inputB.value)
const carryOut = computed(() => inputA.value && inputB.value)
</script>
<style scoped>
.half-adder-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
letter-spacing: 0.2px;
}
.circuit-container {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 1rem;
overflow-x: auto;
}
.inputs,
.outputs {
display: flex;
flex-direction: column;
gap: 3.5rem;
min-width: 6rem;
z-index: 2;
}
.outputs {
min-width: 8rem;
}
.input-line,
.output-line {
display: flex;
align-items: center;
gap: 0.5rem;
}
.label {
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.toggle-btn {
width: 2.2rem;
height: 2.2rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-weight: bold;
font-family: monospace;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.toggle-btn.on {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
}
.out-val {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.2rem;
height: 2.2rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-weight: bold;
font-family: monospace;
font-size: 1rem;
transition: all 0.2s;
}
.s-val {
color: var(--vp-c-text-3);
}
.c-val {
color: var(--vp-c-text-3);
}
.output-line.active .s-val {
background: #dcfce7;
color: #16a34a;
border-color: #16a34a;
}
.output-line.active .c-val {
background: #fef3c7;
color: #d97706;
border-color: #d97706;
}
.wires {
width: 80px;
height: 150px;
position: relative;
}
.outputs-wires {
width: 40px;
}
.wire-svg {
width: 100%;
height: 100%;
}
.gates {
display: flex;
flex-direction: column;
gap: 1.5rem;
z-index: 2;
margin-top: 5px;
}
.gate-box {
width: 7.5rem;
height: 4rem;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.gate-box.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 8px var(--vp-c-brand-soft);
}
.gate-name {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.gate-desc {
font-size: 0.65rem;
color: var(--vp-c-text-3);
margin-top: 0.2rem;
}
.logic-explain {
margin-top: 1.5rem;
padding: 0.8rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.85rem;
text-align: center;
color: var(--vp-c-text-2);
line-height: 1.5;
}
@media (max-width: 600px) {
.circuit-container {
transform: scale(0.85);
transform-origin: left top;
padding-bottom: 0;
}
}
</style>
@@ -28,9 +28,7 @@
placeholder="值 (如: 苹果)"
class="hash-input"
/>
<button @click="addData" class="add-btn">
添加
</button>
<button @click="addData" class="add-btn">添加</button>
</div>
</div>
@@ -60,7 +58,10 @@
<div
v-for="(slot, index) in hashTable"
:key="index"
:class="['table-slot', { occupied: slot !== null, highlighted: index === exampleIndex }]"
:class="[
'table-slot',
{ occupied: slot !== null, highlighted: index === exampleIndex }
]"
>
<div class="slot-index">{{ index }}</div>
<div class="slot-value">{{ slot || '空' }}</div>
@@ -121,7 +122,18 @@ const newKey = ref('')
const newValue = ref('')
const exampleKey = ref('apple')
const hashTable = ref([null, null, null, null, null, null, null, null, null, null])
const hashTable = ref([
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
])
//
const initData = () => {
@@ -130,7 +142,7 @@ const initData = () => {
{ key: 'banana', value: '香蕉' },
{ key: 'orange', value: '橙子' }
]
data.forEach(item => {
data.forEach((item) => {
const index = simpleHash(item.key)
hashTable.value[index] = `${item.key}: ${item.value}`
})
@@ -174,8 +186,14 @@ initData()
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.analogy-box {
display: flex;
@@ -55,13 +55,17 @@
<div class="pros">
<div class="list-title"> 优点</div>
<ul>
<li v-for="(pro, index) in currentEra.pros" :key="index">{{ pro }}</li>
<li v-for="(pro, index) in currentEra.pros" :key="index">
{{ pro }}
</li>
</ul>
</div>
<div class="cons">
<div class="list-title"> 缺点</div>
<ul>
<li v-for="(con, index) in currentEra.cons" :key="index">{{ con }}</li>
<li v-for="(con, index) in currentEra.cons" :key="index">
{{ con }}
</li>
</ul>
</div>
</div>
@@ -99,11 +103,7 @@
<div class="modern-languages">
<div class="modern-title">现代编程语言生态</div>
<div class="language-grid">
<div
v-for="lang in modernLanguages"
:key="lang.name"
class="lang-card"
>
<div v-for="lang in modernLanguages" :key="lang.name" class="lang-card">
<div class="lang-name">{{ lang.name }}</div>
<div class="lang-year">{{ lang.year }}</div>
<div class="lang-uses">
@@ -134,11 +134,7 @@ const eras = [
icon: '0️⃣',
position: '5%',
example: '10110000 11000000\n(add two numbers)',
features: [
'直接用二进制代码',
'机器可以直接执行',
'完全依赖硬件'
],
features: ['直接用二进制代码', '机器可以直接执行', '完全依赖硬件'],
pros: ['执行速度最快', '直接控制硬件'],
cons: ['极难编写', '容易出错', '不可移植']
},
@@ -149,11 +145,7 @@ const eras = [
icon: '🔧',
position: '25%',
example: 'MOV AX, 5\nADD AX, 3\n(add 5 and 3)',
features: [
'用助记符代替二进制',
'需要汇编器翻译',
'仍然依赖硬件'
],
features: ['用助记符代替二进制', '需要汇编器翻译', '仍然依赖硬件'],
pros: ['比机器语言好懂', '效率仍然很高'],
cons: ['代码冗长', '不可移植', '需要了解硬件']
},
@@ -164,11 +156,7 @@ const eras = [
icon: '📋',
position: '50%',
example: 'int add(int a, int b) {\n return a + b;\n}',
features: [
'函数、变量等抽象',
'结构化编程',
'可移植性好'
],
features: ['函数、变量等抽象', '结构化编程', '可移植性好'],
pros: ['易读易写', '可移植', '效率较高'],
cons: ['大型项目难以维护', '代码重用性差']
},
@@ -179,11 +167,7 @@ const eras = [
icon: '🎯',
position: '75%',
example: 'class Calculator {\n add(a, b) { return a + b; }\n}',
features: [
'类、对象、封装、继承',
'模块化设计',
'代码复用性强'
],
features: ['类、对象、封装、继承', '模块化设计', '代码复用性强'],
pros: ['适合大型项目', '易维护', '可扩展'],
cons: ['学习曲线陡', '代码量较大']
},
@@ -194,11 +178,7 @@ const eras = [
icon: '🚀',
position: '95%',
example: 'const add = (a, b) => a + b;\n(add arrow function)',
features: [
'简洁优雅的语法',
'多范式支持',
'强大的标准库'
],
features: ['简洁优雅的语法', '多范式支持', '强大的标准库'],
pros: ['开发效率高', '生态丰富', '社区活跃'],
cons: ['抽象层多', '性能可能不如底层语言']
}
@@ -232,8 +212,14 @@ const currentEra = computed(() => eras[activeEra.value])
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.evolution-timeline {
background: var(--vp-c-bg);
@@ -321,7 +307,8 @@ const currentEra = computed(() => eras[activeEra.value])
gap: 1.5rem;
}
.content-section {}
.content-section {
}
.section-title {
font-weight: 600;
@@ -120,9 +120,7 @@
<div class="evo-name">现代语言</div>
<div class="evo-arrow"></div>
</div>
<div class="evo-result">
越来越接近<br>人类思维
</div>
<div class="evo-result">越来越接近<br />人类思维</div>
</div>
</div>
</div>
@@ -139,7 +137,8 @@ const scenarios = [
icon: '🌐',
title: '开发网站',
desc: '创建交互式网页',
fullDesc: '你需要创建一个在线购物网站,用户可以浏览商品、加入购物车、下单支付',
fullDesc:
'你需要创建一个在线购物网站,用户可以浏览商品、加入购物车、下单支付',
reasons: [
'HTML 定义网页结构',
'CSS 实现美观样式',
@@ -211,7 +210,9 @@ const scenarios = [
}
]
const currentScenario = computed(() => scenarios.find(s => s.id === activeScenario.value))
const currentScenario = computed(() =>
scenarios.find((s) => s.id === activeScenario.value)
)
</script>
<style scoped>
@@ -230,8 +231,14 @@ const currentScenario = computed(() => scenarios.find(s => s.id === activeScenar
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.scenario-intro {
padding: 1rem;
@@ -319,7 +326,8 @@ const currentScenario = computed(() => scenarios.find(s => s.id === activeScenar
gap: 1.5rem;
}
.detail-section {}
.detail-section {
}
.section-title {
font-weight: 600;
@@ -6,18 +6,10 @@
</div>
<div class="dimension-grid">
<div
v-for="dim in dimensions"
:key="dim.id"
class="dimension-card"
>
<div v-for="dim in dimensions" :key="dim.id" class="dimension-card">
<div class="card-title">{{ dim.title }}</div>
<div class="card-options">
<div
v-for="opt in dim.options"
:key="opt.name"
class="option-item"
>
<div v-for="opt in dim.options" :key="opt.name" class="option-item">
<div class="option-name">{{ opt.name }}</div>
<div class="option-langs">{{ opt.langs }}</div>
</div>
@@ -111,8 +103,14 @@ const dimensions = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.dimension-grid {
display: grid;
@@ -76,12 +76,8 @@
<div class="stack-bottom">栈底</div>
</div>
<div class="stack-operations">
<button class="op-btn" @click="pushStack">
入栈 (PUSH)
</button>
<button class="op-btn" @click="popStack">
出栈 (POP)
</button>
<button class="op-btn" @click="pushStack">入栈 (PUSH)</button>
<button class="op-btn" @click="popStack">出栈 (POP)</button>
</div>
<div class="visual-note">
后进先出 (LIFO) | 应用撤销操作函数调用
@@ -104,12 +100,8 @@
<div class="queue-rear"> 队尾</div>
</div>
<div class="queue-operations">
<button class="op-btn" @click="enqueue">
入队 (ENQUEUE)
</button>
<button class="op-btn" @click="dequeue">
出队 (DEQUEUE)
</button>
<button class="op-btn" @click="enqueue">入队 (ENQUEUE)</button>
<button class="op-btn" @click="dequeue">出队 (DEQUEUE)</button>
</div>
<div class="visual-note">
先进先出 (FIFO) | 应用任务队列打印队列
@@ -239,7 +231,9 @@ const linkedListData = ref(['A', 'B', 'C', 'D', 'E'])
const stackData = ref(['书5', '书4', '书3', '书2', '书1'])
const queueData = ref(['人1', '人2', '人3', '人4'])
const currentStructure = computed(() => structures.find(s => s.id === activeStructure.value))
const currentStructure = computed(() =>
structures.find((s) => s.id === activeStructure.value)
)
const pushStack = () => {
const newItem = `${stackData.value.length + 1}`
@@ -280,8 +274,14 @@ const dequeue = () => {
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.structure-tabs {
display: flex;
@@ -18,7 +18,12 @@
<tr v-for="(row, i) in gate.rows" :key="i">
<td>{{ row[0] }}</td>
<td v-if="gate.name !== 'NOT'">{{ row[1] }}</td>
<td class="result-cell" :class="{ one: row[row.length - 1] === 1 }">{{ row[row.length - 1] }}</td>
<td
class="result-cell"
:class="{ one: row[row.length - 1] === 1 }"
>
{{ row[row.length - 1] }}
</td>
</tr>
</tbody>
</table>
@@ -34,22 +39,40 @@ const gates = [
{
name: 'AND',
rule: '都为 1 才得 1',
rows: [[0,0,0],[0,1,0],[1,0,0],[1,1,1]]
rows: [
[0, 0, 0],
[0, 1, 0],
[1, 0, 0],
[1, 1, 1]
]
},
{
name: 'OR',
rule: '有一个 1 就得 1',
rows: [[0,0,0],[0,1,1],[1,0,1],[1,1,1]]
rows: [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 1]
]
},
{
name: 'NOT',
rule: '取反',
rows: [[0,1],[1,0]]
rows: [
[0, 1],
[1, 0]
]
},
{
name: 'XOR',
rule: '不同才得 1',
rows: [[0,0,0],[0,1,1],[1,0,1],[1,1,0]]
rows: [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 0]
]
}
]
</script>
@@ -1,33 +1,51 @@
<template>
<div class="memory-demo">
<div class="demo-controls">
<button class="allocate-btn wechat" @click="allocate('wechat')" :disabled="!hasFreeSpace">
<button
class="allocate-btn wechat"
@click="allocate('wechat')"
:disabled="!hasFreeSpace"
>
+ 给微信分配数据
</button>
<button class="allocate-btn game" @click="allocate('game')" :disabled="!hasFreeSpace">
<button
class="allocate-btn game"
@click="allocate('game')"
:disabled="!hasFreeSpace"
>
+ 给游戏分配数据
</button>
<button class="reset-btn" @click="reset">
重置
</button>
<button class="reset-btn" @click="reset"> 重置</button>
</div>
<div class="system-view">
<!-- 虚拟内存试图 -->
<div class="virtual-cluster">
<div class="process-vm wechat">
<div class="title">💬 微信的虚拟内存<br/>(它认为自己独占了空间)</div>
<div class="title">💬 微信的虚拟内存<br />(它认为自己独占了空间)</div>
<div class="vm-blocks">
<div v-for="i in 4" :key="'w'+i" class="block" :class="{ filled: wechatBlocks >= i }">
<div
v-for="i in 4"
:key="'w' + i"
class="block"
:class="{ filled: wechatBlocks >= i }"
>
{{ wechatBlocks >= i ? '数据 ' + i : '虚拟空闲' }}
</div>
</div>
</div>
<div class="process-vm game">
<div class="title">🎮 游戏的虚拟内存<br/>(它也认为自己独占了空间)</div>
<div class="title">
🎮 游戏的虚拟内存<br />(它也认为自己独占了空间)
</div>
<div class="vm-blocks">
<div v-for="i in 4" :key="'g'+i" class="block" :class="{ filled: gameBlocks >= i }">
<div
v-for="i in 4"
:key="'g' + i"
class="block"
:class="{ filled: gameBlocks >= i }"
>
{{ gameBlocks >= i ? '数据 ' + i : '虚拟空闲' }}
</div>
</div>
@@ -38,17 +56,17 @@
<div class="os-page-table">
<div class="title">保安大叔 (OS 页表)</div>
<div class="table-info">
当程序存数据时<br/>由我暗中转移到真正的物理缝隙里
当程序存数据时<br />由我暗中转移到真正的物理缝隙里
</div>
</div>
<!-- 物理内存 -->
<div class="physical-memory">
<div class="title">🗄 真实的物理内存条<br/>(其实像个大杂烩一样乱)</div>
<div class="title">🗄 真实的物理内存条<br />(其实像个大杂烩一样乱)</div>
<div class="pm-blocks">
<div
v-for="(block, idx) in physicalBlocks"
:key="'p'+idx"
<div
v-for="(block, idx) in physicalBlocks"
:key="'p' + idx"
class="block"
:class="[block.type, { occupied: block.type !== 'empty' }]"
>
@@ -57,9 +75,10 @@
</div>
</div>
</div>
<div class="explanation-box" v-if="wechatBlocks > 0 || gameBlocks > 0">
💡 发现了没尽管右侧真正的物理内存已经被塞得像个狗皮膏药但在左侧的微信和游戏眼里自己的内存条永远是连续且干净的更重要的是微信绝对访问不到橘色的物理块保证了安全
💡
发现了没尽管右侧真正的物理内存已经被塞得像个狗皮膏药但在左侧的微信和游戏眼里自己的内存条永远是连续且干净的更重要的是微信绝对访问不到橘色的物理块保证了安全
</div>
</div>
</template>
@@ -80,20 +99,20 @@ const initialPhysicalBlocks = [
{ type: 'empty', label: '空闲' },
{ type: 'empty', label: '空闲' },
{ type: 'os', label: '系统驱动' },
{ type: 'empty', label: '空闲' },
{ type: 'empty', label: '空闲' }
]
const physicalBlocks = ref(JSON.parse(JSON.stringify(initialPhysicalBlocks)))
const freeSpaceCount = computed(() => {
return physicalBlocks.value.filter(b => b.type === 'empty').length
return physicalBlocks.value.filter((b) => b.type === 'empty').length
})
const hasFreeSpace = computed(() => freeSpaceCount.value > 0)
const allocate = (process) => {
if (!hasFreeSpace.value) return
// Find a process block logic
if (process === 'wechat' && wechatBlocks.value < 4) {
wechatBlocks.value++
@@ -109,9 +128,10 @@ const fillRandomEmptyBlock = (type, label) => {
physicalBlocks.value.forEach((b, i) => {
if (b.type === 'empty') emptyIndices.push(i)
})
if (emptyIndices.length > 0) {
const randomIndex = emptyIndices[Math.floor(Math.random() * emptyIndices.length)]
const randomIndex =
emptyIndices[Math.floor(Math.random() * emptyIndices.length)]
physicalBlocks.value[randomIndex] = { type, label }
}
}
@@ -263,7 +283,7 @@ const reset = () => {
padding: 1rem;
position: relative;
border: 2px solid var(--vp-c-brand-1);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.os-page-table .table-info {
@@ -317,9 +337,17 @@ const reset = () => {
}
@keyframes popIn {
0% { transform: scale(0.9); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
0% {
transform: scale(0.9);
opacity: 0;
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.explanation-box {
@@ -333,7 +361,13 @@ const reset = () => {
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -7,8 +7,8 @@
<div class="demo-content">
<div class="layers-container">
<div
v-for="(layer, i) in layers"
<div
v-for="(layer, i) in layers"
:key="i"
:class="['layer', { active: activeLayer === i }]"
@click="activeLayer = i"
@@ -24,19 +24,13 @@
{{ layer.protocols }}
</div>
</div>
<div
v-if="layer.device"
class="layer-device"
>
<div v-if="layer.device" class="layer-device">
{{ layer.device }}
</div>
</div>
</div>
<div
v-if="currentLayer"
class="layer-detail"
>
<div v-if="currentLayer" class="layer-detail">
<div class="detail-header">
<span class="detail-name">{{ currentLayer.name }}</span>
<span class="detail-analogy">{{ currentLayer.analogy }}</span>
@@ -45,14 +39,9 @@
{{ currentLayer.desc }}
</div>
<div class="detail-tasks">
<div class="task-title">
核心任务
</div>
<div class="task-title">核心任务</div>
<ul>
<li
v-for="(task, j) in currentLayer.tasks"
:key="j"
>
<li v-for="(task, j) in currentLayer.tasks" :key="j">
{{ task }}
</li>
</ul>
@@ -64,35 +53,25 @@
</div>
<div class="encapsulation-demo">
<div class="encap-title">
数据封装过程
</div>
<div class="encap-title">数据封装过程</div>
<div class="encap-flow">
<div
v-for="(step, i) in encapsulation"
:key="i"
class="encap-step"
>
<div v-for="(step, i) in encapsulation" :key="i" class="encap-step">
<div class="step-layer">
{{ step.layer }}
</div>
<div class="step-data">
<span
v-if="step.header"
class="header"
>{{ step.header }}</span>
<span v-if="step.header" class="header">{{ step.header }}</span>
<span class="payload">{{ step.payload }}</span>
</div>
</div>
<div class="arrow">
发送
</div>
<div class="arrow"> 发送</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>分层设计让网络协议模块化每层只关心自己的职责数据从应用层向下传递时每层都会添加自己的"信封"(头部)接收时再逐层拆开
<strong>核心思想</strong
>分层设计让网络协议模块化每层只关心自己的职责数据从应用层向下传递时每层都会添加自己的"信封"(头部)接收时再逐层拆开
</div>
</div>
</template>
@@ -118,7 +97,12 @@ const layers = [
device: '',
analogy: '包裹分拣组',
desc: '负责端到端的数据传输,确保数据的可靠性或实时性。',
tasks: ['建立和管理端到端连接', '分段和重组数据', '流量控制和拥塞控制', '端口寻址'],
tasks: [
'建立和管理端到端连接',
'分段和重组数据',
'流量控制和拥塞控制',
'端口寻址'
],
unit: '段(Segment)'
},
{
@@ -177,8 +161,15 @@ const encapsulation = [
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
display: grid;
@@ -201,8 +201,14 @@ const protocolLayers = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.network-scene {
margin-bottom: 2rem;
@@ -253,8 +259,14 @@ const protocolLayers = [
border-radius: 6px;
}
.app-icon { font-size: 1.5rem; }
.app-name { font-size: 0.8rem; font-weight: 600; color: var(--vp-c-brand); }
.app-icon {
font-size: 1.5rem;
}
.app-name {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-brand);
}
.network-path {
flex: 1;
@@ -316,8 +328,15 @@ const protocolLayers = [
}
@keyframes flowMove {
0%, 100% { transform: translateX(-20px); opacity: 0; }
50% { transform: translateX(20px); opacity: 1; }
0%,
100% {
transform: translateX(-20px);
opacity: 0;
}
50% {
transform: translateX(20px);
opacity: 1;
}
}
.flow-packet {
@@ -106,8 +106,14 @@ const principles = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.principle-cards {
display: grid;
@@ -8,9 +8,7 @@
<div class="demo-content">
<div class="os-layers">
<div class="layer user-apps">
<div class="layer-title">
应用程序层
</div>
<div class="layer-title">应用程序层</div>
<div class="layer-content">
<div
v-for="app in applications"
@@ -29,9 +27,7 @@
</div>
<div class="layer kernel">
<div class="layer-title">
操作系统内核
</div>
<div class="layer-title">操作系统内核</div>
<div class="layer-content">
<div class="kernel-components">
<div
@@ -45,15 +41,11 @@
</div>
</div>
</div>
<div class="layer-desc">
进程管理内存管理文件系统设备管理
</div>
<div class="layer-desc">进程管理内存管理文件系统设备管理</div>
</div>
<div class="layer hardware">
<div class="layer-title">
硬件层
</div>
<div class="layer-title">硬件层</div>
<div class="layer-content">
<div class="hardware-icons">
<span>💻 CPU</span>
@@ -70,27 +62,18 @@
<div class="flow-content">
<div class="flow-item" :class="{ active: showFlow }">
<div class="flow-arrow"></div>
<div class="flow-desc">
应用程序请求资源内存CPU文件
</div>
<div class="flow-desc">应用程序请求资源内存CPU文件</div>
</div>
<div class="flow-item" :class="{ active: showFlow }">
<div class="flow-arrow"></div>
<div class="flow-desc">
操作系统内核统一分配和调度
</div>
<div class="flow-desc">操作系统内核统一分配和调度</div>
</div>
<div class="flow-item" :class="{ active: showFlow }">
<div class="flow-arrow"></div>
<div class="flow-desc">
硬件执行实际操作
</div>
<div class="flow-desc">硬件执行实际操作</div>
</div>
</div>
<button
class="flow-btn"
@click="showFlow = !showFlow"
>
<button class="flow-btn" @click="showFlow = !showFlow">
{{ showFlow ? '隐藏' : '显示' }}资源流
</button>
</div>
@@ -132,13 +115,13 @@ const kernelComponents = [
]
const activeAppName = computed(() => {
const app = applications.find(a => a.id === activeApp.value)
const app = applications.find((a) => a.id === activeApp.value)
return app?.name || ''
})
const getActiveAppDesc = () => {
const component = kernelComponents.find(c => c.id === activeComponent.value)
const app = applications.find(a => a.id === activeApp.value)
const component = kernelComponents.find((c) => c.id === activeComponent.value)
const app = applications.find((a) => a.id === activeApp.value)
if (!app || !component) return '请选择应用程序和内核组件'
return `${app.name} 需要使用 ${component.name} 的功能`
@@ -161,8 +144,14 @@ const getActiveAppDesc = () => {
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.os-layers {
display: flex;
@@ -213,7 +202,8 @@ const getActiveAppDesc = () => {
transition: all 0.3s;
}
.app-icon:hover, .app-icon.active {
.app-icon:hover,
.app-icon.active {
transform: scale(1.1);
background: rgba(255, 255, 255, 0.2);
}
@@ -227,7 +217,8 @@ const getActiveAppDesc = () => {
transition: all 0.3s;
}
.kernel-component:hover, .kernel-component.active {
.kernel-component:hover,
.kernel-component.active {
background: rgba(255, 255, 255, 0.3);
font-weight: 600;
}
@@ -30,32 +30,55 @@
<div class="signal-wave">
<svg viewBox="0 0 800 150" class="wave-svg">
<!-- 坐标轴 -->
<line x1="50" y1="75" x2="750" y2="75" stroke="var(--vp-c-divider)" stroke-width="2" />
<line
x1="50"
y1="75"
x2="750"
y2="75"
stroke="var(--vp-c-divider)"
stroke-width="2"
/>
<!-- 信号波形 -->
<path
:d="currentMedia.wavePath"
fill="none"
:stroke="activeMedia === 'fiber' ? '#ff6b6b' : 'var(--vp-c-brand)'"
:stroke="
activeMedia === 'fiber' ? '#ff6b6b' : 'var(--vp-c-brand)'
"
stroke-width="3"
class="signal-path"
/>
<!-- 数据标记 -->
<g v-if="activeMedia === 'copper'">
<text x="100" y="40" fill="var(--vp-c-text-2)" font-size="12">1</text>
<text x="180" y="110" fill="var(--vp-c-text-2)" font-size="12">0</text>
<text x="260" y="40" fill="var(--vp-c-text-2)" font-size="12">1</text>
<text x="340" y="40" fill="var(--vp-c-text-2)" font-size="12">1</text>
<text x="420" y="110" fill="var(--vp-c-text-2)" font-size="12">0</text>
<text x="100" y="40" fill="var(--vp-c-text-2)" font-size="12">
1
</text>
<text x="180" y="110" fill="var(--vp-c-text-2)" font-size="12">
0
</text>
<text x="260" y="40" fill="var(--vp-c-text-2)" font-size="12">
1
</text>
<text x="340" y="40" fill="var(--vp-c-text-2)" font-size="12">
1
</text>
<text x="420" y="110" fill="var(--vp-c-text-2)" font-size="12">
0
</text>
</g>
<g v-if="activeMedia === 'fiber'">
<text x="100" y="40" fill="#ff6b6b" font-size="12"></text>
<text x="180" y="110" fill="var(--vp-c-text-2)" font-size="12"></text>
<text x="180" y="110" fill="var(--vp-c-text-2)" font-size="12">
</text>
<text x="260" y="40" fill="#ff6b6b" font-size="12"></text>
<text x="340" y="40" fill="#ff6b6b" font-size="12"></text>
<text x="420" y="110" fill="var(--vp-c-text-2)" font-size="12"></text>
<text x="420" y="110" fill="var(--vp-c-text-2)" font-size="12">
</text>
</g>
</svg>
</div>
@@ -114,7 +137,11 @@
<div class="applications">
<div class="app-title">典型应用场景</div>
<div class="app-list">
<div v-for="(app, index) in currentMedia.applications" :key="index" class="app-item">
<div
v-for="(app, index) in currentMedia.applications"
:key="index"
class="app-item"
>
<span class="app-icon">{{ app.icon }}</span>
<span class="app-text">{{ app.text }}</span>
</div>
@@ -138,7 +165,8 @@ const mediaData = {
copper: {
signalName: '电信号(电压高低)',
signalDesc: '用高低电压表示 0 和 1',
wavePath: 'M 50 75 L 100 75 L 100 25 L 150 25 L 150 125 L 200 125 L 200 25 L 250 25 L 250 25 L 300 25 L 300 125 L 350 125 L 350 25 L 400 25',
wavePath:
'M 50 75 L 100 75 L 100 25 L 150 25 L 150 125 L 200 125 L 200 25 L 250 25 L 250 25 L 300 25 L 300 125 L 350 125 L 350 25 L 400 25',
speed: '最高 10 Gbps',
distance: '100 米',
immunity: '较差(易受电磁干扰)',
@@ -152,7 +180,8 @@ const mediaData = {
fiber: {
signalName: '光信号(光的开关)',
signalDesc: '用光脉冲表示 0 和 1',
wavePath: 'M 50 75 L 100 75 L 100 25 L 150 25 L 150 125 L 200 125 L 200 25 L 250 25 L 250 25 L 300 25 L 300 125 L 350 125 L 350 25 L 400 25',
wavePath:
'M 50 75 L 100 75 L 100 25 L 150 25 L 150 125 L 200 125 L 200 25 L 250 25 L 250 25 L 300 25 L 300 125 L 350 125 L 350 25 L 400 25',
speed: '最高 100+ Tbps',
distance: '几十公里',
immunity: '极强(不受电磁干扰)',
@@ -198,8 +227,14 @@ const currentMedia = computed(() => mediaData[activeMedia.value])
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.media-selector {
margin-bottom: 2rem;
@@ -283,8 +318,12 @@ const currentMedia = computed(() => mediaData[activeMedia.value])
}
@keyframes drawSignal {
0% { stroke-dashoffset: 1000; }
100% { stroke-dashoffset: 0; }
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: 0;
}
}
.signal-legend {
@@ -1,13 +1,21 @@
<template>
<div class="process-demo">
<div class="controls-section">
<button class="action-btn" :class="{ active: isRunning }" @click="toggleSimulation">
<button
class="action-btn"
:class="{ active: isRunning }"
@click="toggleSimulation"
>
{{ isRunning ? '⏸ 暂停时间片轮转' : '▶️ 启动 CPU' }}
</button>
<div class="speed-control">
<label>时间流速:</label>
<button :class="{ active: speed === 'slow' }" @click="setSpeed('slow')">极慢动作</button>
<button :class="{ active: speed === 'fast' }" @click="setSpeed('fast')">真实速度</button>
<button :class="{ active: speed === 'slow' }" @click="setSpeed('slow')">
极慢动作
</button>
<button :class="{ active: speed === 'fast' }" @click="setSpeed('fast')">
真实速度
</button>
</div>
</div>
@@ -18,23 +26,21 @@
<span v-if="activeProcess" class="task-badge">
正在处理: {{ activeProcess.icon }} {{ activeProcess.name }}
</span>
<span v-else class="task-badge idle">
空闲中...
</span>
<span v-else class="task-badge idle"> 空闲中... </span>
</div>
</div>
<!-- 连接线动画 -->
<div class="connector">
<div
class="data-flow"
:class="[ `flow-${activeProcessId}`, { running: isRunning }]">
</div>
<div
class="data-flow"
:class="[`flow-${activeProcessId}`, { running: isRunning }]"
></div>
</div>
</div>
<div class="processes-grid">
<div
v-for="p in processes"
<div
v-for="p in processes"
:key="p.id"
class="process-card"
:class="{ active: p.id === activeProcessId }"
@@ -44,21 +50,32 @@
<span class="icon">{{ p.icon }}</span>
<span class="name">{{ p.name }}</span>
</div>
<span class="status-badge" :class="p.id === activeProcessId ? 'running' : 'waiting'">
<span
class="status-badge"
:class="p.id === activeProcessId ? 'running' : 'waiting'"
>
{{ p.id === activeProcessId ? '独占 CPU' : '排队等待' }}
</span>
</div>
<div class="p-progress">
<div class="progress-track">
<div class="progress-fill" :style="{ width: p.progress + '%' }"></div>
<div
class="progress-fill"
:style="{ width: p.progress + '%' }"
></div>
</div>
<div class="progress-text">{{ Math.floor(p.progress) }}% 完成</div>
</div>
</div>
</div>
<div class="explanation-box" :class="{ show: isRunning && speed === 'fast' }">
💡 **关键启示**当切换速度足够快时肉眼已经无法分辨谁在等待这也就是为什么只有一个 CPU 核心的电脑依然能让你一边听歌一边打字
<div
class="explanation-box"
:class="{ show: isRunning && speed === 'fast' }"
>
💡
**关键启示**当切换速度足够快时肉眼已经无法分辨谁在等待这也就是为什么只有一个
CPU 核心的电脑依然能让你一边听歌一边打字
</div>
</div>
</template>
@@ -77,7 +94,9 @@ const processes = ref([
{ id: 3, name: '游戏渲染', icon: '🎮', progress: 0 }
])
const activeProcess = computed(() => processes.value.find(p => p.id === activeProcessId.value))
const activeProcess = computed(() =>
processes.value.find((p) => p.id === activeProcessId.value)
)
const setSpeed = (s) => {
speed.value = s
@@ -88,17 +107,17 @@ const setSpeed = (s) => {
}
const startLoop = () => {
const switchTime = speed.value === 'slow' ? 1200 : 80; // 1.2s
const switchTime = speed.value === 'slow' ? 1200 : 80 // 1.2s
if (!activeProcessId.value) {
activeProcessId.value = 1
}
interval = setInterval(() => {
//
const curr = processes.value.find(p => p.id === activeProcessId.value)
const curr = processes.value.find((p) => p.id === activeProcessId.value)
if (curr) {
curr.progress += (speed.value === 'slow' ? 15 : 4)
curr.progress += speed.value === 'slow' ? 15 : 4
if (curr.progress >= 100) curr.progress = 0
}
@@ -106,7 +125,6 @@ const startLoop = () => {
let nextId = activeProcessId.value + 1
if (nextId > 3) nextId = 1
activeProcessId.value = nextId
}, switchTime)
}
@@ -203,7 +221,7 @@ onUnmounted(() => {
flex-direction: column;
justify-content: center;
align-items: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
position: relative;
}
@@ -276,7 +294,9 @@ onUnmounted(() => {
.process-card.active::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--vp-c-brand-1);
}
@@ -7,9 +7,7 @@
<div class="demo-content">
<div class="collab-scene">
<div class="scene-title">
场景选择
</div>
<div class="scene-title">场景选择</div>
<div class="scene-buttons">
<button
v-for="scene in scenes"
@@ -88,7 +86,9 @@
>
<span class="file-icon">{{ getIcon(file.type) }}</span>
<span class="file-name">{{ file.name }}</span>
<span v-if="file.size" class="file-size">{{ file.size }}</span>
<span v-if="file.size" class="file-size">{{
file.size
}}</span>
</div>
</div>
</div>
@@ -207,19 +207,21 @@ const sceneData = {
}
}
const currentSceneData = computed(() => sceneData[activeScene.value] || sceneData.launch)
const currentSceneData = computed(
() => sceneData[activeScene.value] || sceneData.launch
)
const getProcessName = (id) => {
const proc = processes.value.find(p => p.id === id)
const proc = processes.value.find((p) => p.id === id)
return proc?.name || '系统'
}
const getIcon = (type) => {
const icons = {
'json': '📋',
'db': '🗄️',
'folder': '📁',
'audio': '🎵'
json: '📋',
db: '🗄️',
folder: '📁',
audio: '🎵'
}
return icons[type] || '📄'
}
@@ -241,8 +243,14 @@ const getIcon = (type) => {
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.scene-buttons {
display: flex;
@@ -300,8 +308,12 @@ const getIcon = (type) => {
font-size: 0.9rem;
}
.zone-icon { font-size: 1.2rem; }
.zone-name { font-size: 0.85rem; }
.zone-icon {
font-size: 1.2rem;
}
.zone-name {
font-size: 0.85rem;
}
.process-list {
display: flex;
@@ -324,7 +336,9 @@ const getIcon = (type) => {
background: var(--vp-c-brand-soft);
}
.proc-name, .proc-pid, .proc-state {
.proc-name,
.proc-pid,
.proc-state {
flex: 1;
}
@@ -389,8 +403,13 @@ const getIcon = (type) => {
background: var(--vp-c-brand-soft);
}
.file-name { flex: 1; }
.file-size { color: var(--vp-c-text-2); font-size: 0.7rem; }
.file-name {
flex: 1;
}
.file-size {
color: var(--vp-c-text-2);
font-size: 0.7rem;
}
.explanation {
background: var(--vp-c-bg);
@@ -65,13 +65,17 @@
<div class="pros">
<div class="list-title"> 优点</div>
<ul>
<li v-for="(pro, index) in currentLang.pros" :key="index">{{ pro }}</li>
<li v-for="(pro, index) in currentLang.pros" :key="index">
{{ pro }}
</li>
</ul>
</div>
<div class="cons">
<div class="list-title"> 缺点</div>
<ul>
<li v-for="(con, index) in currentLang.cons" :key="index">{{ con }}</li>
<li v-for="(con, index) in currentLang.cons" :key="index">
{{ con }}
</li>
</ul>
</div>
</div>
@@ -92,7 +96,11 @@
</tr>
</thead>
<tbody>
<tr v-for="(lang, index) in languages" :key="index" :class="{ highlighted: lang.name === activeLang }">
<tr
v-for="(lang, index) in languages"
:key="index"
:class="{ highlighted: lang.name === activeLang }"
>
<td>{{ lang.icon }} {{ lang.name }}</td>
<td>{{ lang.difficulty }}</td>
<td>{{ lang.efficiency }}</td>
@@ -197,7 +205,9 @@ const languages = [
}
]
const currentLang = computed(() => languages.find(l => l.name === activeLang.value))
const currentLang = computed(() =>
languages.find((l) => l.name === activeLang.value)
)
</script>
<style scoped>
@@ -216,8 +226,14 @@ const currentLang = computed(() => languages.find(l => l.name === activeLang.val
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.comparison-grid {
display: grid;
@@ -147,7 +147,8 @@ const paradigms = [
icon: '📋',
desc: '告诉计算机怎么做',
idea: '通过一系列命令(语句)来改变程序状态,关注"怎么做"',
example: '// 计算1-10的和\nlet sum = 0;\nfor (let i = 1; i <= 10; i++) {\n sum += i;\n}',
example:
'// 计算1-10的和\nlet sum = 0;\nfor (let i = 1; i <= 10; i++) {\n sum += i;\n}',
features: ['变量', '循环', '条件判断', '语句'],
languages: ['C', 'Python', 'JavaScript']
},
@@ -157,7 +158,8 @@ const paradigms = [
icon: '🎯',
desc: '用对象来组织代码',
idea: '将数据和操作数据的方法封装成对象,通过对象间交互来完成任务',
example: 'class Calculator {\n add(a, b) { return a + b; }\n}\nconst calc = new Calculator();',
example:
'class Calculator {\n add(a, b) { return a + b; }\n}\nconst calc = new Calculator();',
features: ['封装', '继承', '多态', '类'],
languages: ['Java', 'C++', 'Python', 'Ruby']
},
@@ -167,13 +169,16 @@ const paradigms = [
icon: 'λ',
desc: '函数是核心',
idea: '强调纯函数、不可变数据,避免副作用,关注"做什么"',
example: '// 计算1-10的和\nconst sum = Array.from(\n {length: 10}, (_, i) => i + 1\n).reduce((a, b) => a + b, 0);',
example:
'// 计算1-10的和\nconst sum = Array.from(\n {length: 10}, (_, i) => i + 1\n).reduce((a, b) => a + b, 0);',
features: ['纯函数', '不可变性', '高阶函数', '无副作用'],
languages: ['Haskell', 'F#', 'Erlang', 'Clojure']
}
]
const currentParadigm = computed(() => paradigms.find(p => p.id === activeParadigm.value))
const currentParadigm = computed(() =>
paradigms.find((p) => p.id === activeParadigm.value)
)
</script>
<style scoped>
@@ -192,8 +197,14 @@ const currentParadigm = computed(() => paradigms.find(p => p.id === activeParadi
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.paradigm-intro {
padding: 1rem;
@@ -281,7 +292,8 @@ const currentParadigm = computed(() => paradigms.find(p => p.id === activeParadi
gap: 1.5rem;
}
.detail-section {}
.detail-section {
}
.section-title {
font-weight: 600;
@@ -11,8 +11,8 @@
<div class="analogy-content">
<div class="analogy-title">俄罗斯套娃</div>
<div class="analogy-desc">
打开一个大娃娃里面有个小一点的娃娃<br>
再打开还有更小的...直到最小的一个<br>
打开一个大娃娃里面有个小一点的娃娃<br />
再打开还有更小的...直到最小的一个<br />
<strong>这就是递归</strong>
</div>
</div>
@@ -101,7 +101,8 @@
</div>
<div class="dir-pseudocode">
<div class="pseudo-title">伪代码</div>
<pre>function traverse(folder) {
<pre>
function traverse(folder) {
for each item in folder {
if item is file {
print(item)
@@ -109,7 +110,8 @@
traverse(item) //
}
}
}</pre>
}</pre
>
</div>
</div>
</div>
@@ -127,7 +129,9 @@
<div class="element-card">
<div class="element-number">2</div>
<div class="element-title">递归调用</div>
<div class="element-desc">如何让问题规模变小调用自己处理更小的规模</div>
<div class="element-desc">
如何让问题规模变小调用自己处理更小的规模
</div>
<div class="element-example">n! 转换成 (n-1)!</div>
</div>
<div class="element-card">
@@ -189,8 +193,14 @@ const examples = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.analogy-section {
margin-bottom: 2rem;
@@ -10,29 +10,45 @@
class="toggle-btn"
:class="{ on: inputData === 1 }"
@click="inputData = inputData === 1 ? 0 : 1"
>{{ inputData }}</button>
>
{{ inputData }}
</button>
</div>
<!-- Write -->
<button class="write-btn" :class="{ flash: isWriting }" @click="writeOnce">
<button
class="write-btn"
:class="{ flash: isWriting }"
@click="writeOnce"
>
写入
</button>
<!-- Stored -->
<div class="reg-block">
<span class="reg-title">存储</span>
<span class="val-box" :class="{ on: storedData === 1, flash: isWriting }">{{ storedData }}</span>
<span
class="val-box"
:class="{ on: storedData === 1, flash: isWriting }"
>{{ storedData }}</span
>
</div>
<!-- Output -->
<div class="reg-block">
<span class="reg-title">输出</span>
<span class="val-box out" :class="{ on: storedData === 1 }">{{ storedData }}</span>
<span class="val-box out" :class="{ on: storedData === 1 }">{{
storedData
}}</span>
</div>
</div>
<div class="status-line">
{{ inputData !== storedData ? '⚡ 输入≠存储 → 点"写入"即可更新' : '✓ 输入与存储一致' }}
{{
inputData !== storedData
? '⚡ 输入≠存储 → 点"写入"即可更新'
: '✓ 输入与存储一致'
}}
</div>
</div>
</template>
@@ -47,7 +63,9 @@ const isWriting = ref(false)
const writeOnce = () => {
isWriting.value = true
storedData.value = inputData.value
setTimeout(() => { isWriting.value = false }, 400)
setTimeout(() => {
isWriting.value = false
}, 400)
}
</script>
@@ -4,7 +4,8 @@
<div class="layers">
<div
v-for="(layer, i) in layers" :key="i"
v-for="(layer, i) in layers"
:key="i"
class="layer-row"
:class="{ active: activeLayer === i }"
@mouseenter="activeLayer = i"
@@ -23,7 +24,9 @@
</div>
</div>
<div class="demo-caption">层层抽象封装最底层的物理材料最终变成通用计算平台</div>
<div class="demo-caption">
层层抽象封装最底层的物理材料最终变成通用计算平台
</div>
</div>
</template>
@@ -33,13 +36,55 @@ import { ref } from 'vue'
const activeLayer = ref(null)
const layers = [
{ icon: '🏖️', name: '沙子(硅)', desc: '地球上最丰富的元素之一,提炼出高纯度硅', scale: '原材料', arrow: '提纯 → 切割' },
{ icon: '💿', name: '硅晶圆', desc: '直径约 30cm 的单晶硅片,表面极其光滑', scale: '基底', arrow: '光刻 → 蚀刻 → 掺杂' },
{ icon: '🔌', name: '晶体管(开关)', desc: 'Gate=1 导通,Gate=0 断开,用电压控制电流', scale: '数百亿 / 芯片', arrow: '组合成逻辑电路' },
{ icon: '🔲', name: '逻辑门', desc: 'AND / OR / NOT / XOR,实现基本布尔运算', scale: '数十亿', arrow: '组合成功能模块' },
{ icon: '⚙️', name: '功能单元', desc: '加法器、寄存器、多路选择器……各司其职', scale: '数百个', arrow: '集成为处理器' },
{ icon: '🧠', name: 'CPU 核心', desc: 'ALU + 控制器 + 寄存器组,取指→解码→执行→写回', scale: '1128 核', arrow: '软件编程' },
{ icon: '🚀', name: '软件应用', desc: '操作系统 / AI / 游戏 / 网页……一切皆指令', scale: '无限可能', arrow: '' },
{
icon: '🏖️',
name: '沙子(硅)',
desc: '地球上最丰富的元素之一,提炼出高纯度硅',
scale: '原材料',
arrow: '提纯 → 切割'
},
{
icon: '💿',
name: '硅晶圆',
desc: '直径约 30cm 的单晶硅片,表面极其光滑',
scale: '基底',
arrow: '光刻 → 蚀刻 → 掺杂'
},
{
icon: '🔌',
name: '晶体管(开关)',
desc: 'Gate=1 导通,Gate=0 断开,用电压控制电流',
scale: '数百亿 / 芯片',
arrow: '组合成逻辑电路'
},
{
icon: '🔲',
name: '逻辑门',
desc: 'AND / OR / NOT / XOR,实现基本布尔运算',
scale: '数十亿',
arrow: '组合成功能模块'
},
{
icon: '⚙️',
name: '功能单元',
desc: '加法器、寄存器、多路选择器……各司其职',
scale: '数百个',
arrow: '集成为处理器'
},
{
icon: '🧠',
name: 'CPU 核心',
desc: 'ALU + 控制器 + 寄存器组,取指→解码→执行→写回',
scale: '1128 核',
arrow: '软件编程'
},
{
icon: '🚀',
name: '软件应用',
desc: '操作系统 / AI / 游戏 / 网页……一切皆指令',
scale: '无限可能',
arrow: ''
}
]
</script>
@@ -28,17 +28,29 @@
<div
v-for="(num, index) in numbers"
:key="index"
:class="['array-cell', { found: index === foundIndex, searching: index <= searchStep && searching }]"
:class="[
'array-cell',
{
found: index === foundIndex,
searching: index <= searchStep && searching
}
]"
>
{{ num }}
</div>
</div>
<div class="search-controls">
<button @click="startLinearSearch" class="search-btn">开始查找</button>
<button @click="startLinearSearch" class="search-btn">
开始查找
</button>
<button @click="reset" class="reset-btn">重置</button>
</div>
<div class="search-info">
目标数字<input v-model="targetNumber" type="number" class="target-input" />
目标数字<input
v-model="targetNumber"
type="number"
class="target-input"
/>
</div>
</div>
<div class="algo-stats">
@@ -55,19 +67,26 @@
<div
v-for="(num, index) in sortedNumbers"
:key="index"
:class="['array-cell', {
found: index === binaryFoundIndex,
left: index >= binaryLeft && index <= binaryRight,
eliminated: index < binaryLeft || index > binaryRight
}]"
:class="[
'array-cell',
{
found: index === binaryFoundIndex,
left: index >= binaryLeft && index <= binaryRight,
eliminated: index < binaryLeft || index > binaryRight
}
]"
>
{{ num }}
</div>
</div>
<div class="binary-info">
<div class="info-step">查找范围[{{ binaryLeft }}, {{ binaryRight }}]</div>
<div class="info-step">
查找范围[{{ binaryLeft }}, {{ binaryRight }}]
</div>
<div class="info-mid">中间位置{{ binaryMid }}</div>
<div class="info-comparison">{{ sortedNumbers[binaryMid] }} vs {{ binaryTarget }}</div>
<div class="info-comparison">
{{ sortedNumbers[binaryMid] }} vs {{ binaryTarget }}
</div>
</div>
<div class="search-controls">
<button @click="binaryStep" class="search-btn">下一步</button>
@@ -125,7 +144,7 @@ const startLinearSearch = () => {
searching.value = true
searchStep.value = -1
foundIndex.value = -1
let step = 0
const interval = setInterval(() => {
if (step < numbers.value.length) {
@@ -151,7 +170,7 @@ const reset = () => {
const binaryStep = () => {
binaryMid.value = Math.floor((binaryLeft.value + binaryRight.value) / 2)
if (sortedNumbers.value[binaryMid.value] === binaryTarget.value) {
binaryFoundIndex.value = binaryMid.value
} else if (sortedNumbers.value[binaryMid.value] < binaryTarget.value) {
@@ -185,8 +204,14 @@ const resetBinary = () => {
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.algorithm-selector {
display: flex;
@@ -10,7 +10,7 @@
v-for="(item, index) in array"
:key="index"
class="array-bar"
:class="{
:class="{
comparing: comparingIndices.includes(index),
swapping: swappingIndices.includes(index),
sorted: index < sortedCount
@@ -92,27 +92,30 @@ const currentAlgoDesc = ref('选择一个排序算法开始演示')
const complexity = ref('')
const generateArray = () => {
array.value = Array.from({ length: 10 }, () => Math.floor(Math.random() * 90) + 10)
array.value = Array.from(
{ length: 10 },
() => Math.floor(Math.random() * 90) + 10
)
sortedCount.value = 0
comparingIndices.value = []
swappingIndices.value = []
}
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const startBubbleSort = async () => {
currentAlgo.value = '冒泡排序'
currentAlgoDesc.value = '重复遍历数组,比较相邻元素并交换'
complexity.value = 'O(n²)'
sortedCount.value = 0
const arr = [...array.value]
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
comparingIndices.value = [j, j + 1]
await sleep(300)
if (arr[j] > arr[j + 1]) {
swappingIndices.value = [j, j + 1]
await sleep(300)
@@ -124,7 +127,7 @@ const startBubbleSort = async () => {
}
sortedCount.value++
}
comparingIndices.value = []
sortedCount.value = arr.length
}
@@ -133,10 +136,10 @@ const startQuickSort = async () => {
currentAlgo.value = '快速排序'
currentAlgoDesc.value = '选择基准,将数组分成小于和大于基准的两部分'
complexity.value = 'O(n log n)'
sortedCount.value = 0
const arr = [...array.value]
await quickSort(arr, 0, arr.length - 1)
array.value = arr
sortedCount.value = arr.length
@@ -154,11 +157,11 @@ const quickSort = async (arr, low, high) => {
const partition = async (arr, low, high) => {
const pivot = arr[high]
let i = low - 1
for (let j = low; j < high; j++) {
comparingIndices.value = [j, high]
await sleep(300)
if (arr[j] < pivot) {
i++
swappingIndices.value = [i, j]
@@ -168,14 +171,14 @@ const partition = async (arr, low, high) => {
await sleep(300)
}
}
swappingIndices.value = [i + 1, high]
await sleep(300)
;[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]]
array.value = [...arr]
await sleep(300)
swappingIndices.value = []
return i + 1
}
</script>
@@ -196,8 +199,14 @@ const partition = async (arr, low, high) => {
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.visual-array {
display: flex;
@@ -7,8 +7,8 @@
<div class="demo-content">
<div class="storage-pyramid">
<div
v-for="(level, i) in storageLevels"
<div
v-for="(level, i) in storageLevels"
:key="level.name"
class="level"
:class="{ active: activeLevel === i }"
@@ -27,13 +27,8 @@
</div>
</div>
<div
v-if="currentLevel"
class="level-detail"
>
<div class="detail-title">
{{ currentLevel.name }} 详情
</div>
<div v-if="currentLevel" class="level-detail">
<div class="detail-title">{{ currentLevel.name }} 详情</div>
<div class="detail-grid">
<div class="detail-item">
<span class="label">访问速度</span>
@@ -59,7 +54,9 @@
</div>
<div class="info-box">
<strong>核心思想</strong>存储遵循"金字塔"原则越快的存储越贵容量越小CPU 需要的数据放在最快的存储寄存器缓存暂时不用的放在慢速大容量存储磁盘云端
<strong>核心思想</strong
>存储遵循"金字塔"原则越快的存储越贵容量越小CPU
需要的数据放在最快的存储寄存器缓存暂时不用的放在慢速大容量存储磁盘云端
</div>
</div>
</template>
@@ -70,55 +67,55 @@ import { ref, computed } from 'vue'
const activeLevel = ref(0)
const storageLevels = [
{
name: '寄存器',
speed: '~1 纳秒',
size: '几百字节',
{
name: '寄存器',
speed: '~1 纳秒',
size: '几百字节',
width: '30%',
cost: '极高',
volatile: '是',
desc: 'CPU 内部最快的存储,直接参与运算。数量有限,由编译器自动管理。'
},
{
name: 'L1 缓存',
speed: '~2 纳秒',
size: '32-64 KB',
{
name: 'L1 缓存',
speed: '~2 纳秒',
size: '32-64 KB',
width: '45%',
cost: '很高',
volatile: '是',
desc: 'CPU 内置的高速缓存,存储最常用的数据。每个核心独立拥有。'
},
{
name: 'L2/L3 缓存',
speed: '~10 纳秒',
size: '几 MB',
{
name: 'L2/L3 缓存',
speed: '~10 纳秒',
size: '几 MB',
width: '60%',
cost: '高',
volatile: '是',
desc: '更大但稍慢的缓存,L3 通常多核心共享。'
},
{
name: '内存 (RAM)',
speed: '~100 纳秒',
size: '8-128 GB',
{
name: '内存 (RAM)',
speed: '~100 纳秒',
size: '8-128 GB',
width: '75%',
cost: '中等',
volatile: '是',
desc: '程序运行时的主要工作区。断电后数据丢失。'
},
{
name: 'SSD 固态硬盘',
speed: '~100 微秒',
size: '256 GB - 4 TB',
{
name: 'SSD 固态硬盘',
speed: '~100 微秒',
size: '256 GB - 4 TB',
width: '90%',
cost: '较低',
volatile: '否',
desc: '比机械硬盘快很多,无机械部件。断电数据保留。'
},
{
name: 'HDD 机械硬盘',
speed: '~10 毫秒',
size: '1-20 TB',
{
name: 'HDD 机械硬盘',
speed: '~10 毫秒',
size: '1-20 TB',
width: '100%',
cost: '低',
volatile: '否',
@@ -145,8 +142,15 @@ const currentLevel = computed(() => storageLevels[activeLevel.value])
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
display: flex;
@@ -251,5 +255,4 @@ const currentLevel = computed(() => storageLevels[activeLevel.value])
display: flex;
gap: 0.25rem;
}
</style>
@@ -106,8 +106,7 @@
</div>
</template>
<script setup>
</script>
<script setup></script>
<style scoped>
.storage-hierarchy-demo {
@@ -125,8 +124,14 @@
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.hierarchy-pyramid {
display: flex;
@@ -16,7 +16,7 @@
min="0"
max="255"
@input="calculate"
>
/>
<span>.</span>
<input
v-model="ip[1]"
@@ -24,7 +24,7 @@
min="0"
max="255"
@input="calculate"
>
/>
<span>.</span>
<input
v-model="ip[2]"
@@ -32,7 +32,7 @@
min="0"
max="255"
@input="calculate"
>
/>
<span>.</span>
<input
v-model="ip[3]"
@@ -40,7 +40,7 @@
min="0"
max="255"
@input="calculate"
>
/>
</div>
</div>
<div class="input-group">
@@ -53,7 +53,7 @@
min="8"
max="30"
@input="calculate"
>
/>
</div>
</div>
</div>
@@ -82,9 +82,7 @@
</div>
<div class="binary-section">
<div class="binary-title">
二进制表示
</div>
<div class="binary-title">二进制表示</div>
<div class="binary-row">
<span class="binary-label">IP 地址:</span>
<span class="binary-value">{{ ipBinary }}</span>
@@ -104,13 +102,11 @@
</div>
<div class="visual-section">
<div class="visual-title">
地址结构可视化
</div>
<div class="visual-title">地址结构可视化</div>
<div class="address-visual">
<div class="bit-blocks">
<div
v-for="(bit, i) in bits"
<div
v-for="(bit, i) in bits"
:key="i"
:class="['bit', { network: i < cidr, host: i >= cidr }]"
>
@@ -118,15 +114,21 @@
</div>
</div>
<div class="legend">
<span class="legend-item"><span class="network-box" /> 网络位 ({{ cidr }})</span>
<span class="legend-item"><span class="host-box" /> 主机 ({{ 32 - cidr }})</span>
<span class="legend-item"
><span class="network-box" /> 网络 ({{ cidr }})</span
>
<span class="legend-item"
><span class="host-box" /> 主机位 ({{ 32 - cidr }})</span
>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>子网掩码决定了 IP 地址的哪部分是"网络号"(小区)哪部分是"主机号"(房间)/24 表示前 24 位是网络位 8 位是主机位
<strong>核心思想</strong>子网掩码决定了 IP
地址的哪部分是"网络号"(小区)哪部分是"主机号"(房间)/24 表示前 24
位是网络位 8 位是主机位
</div>
</div>
</template>
@@ -138,7 +140,7 @@ const ip = ref([192, 168, 1, 100])
const cidr = ref(24)
const mask = computed(() => {
const maskValue = 0xFFFFFFFF << (32 - cidr.value)
const maskValue = 0xffffffff << (32 - cidr.value)
return [
(maskValue >>> 24) & 255,
(maskValue >>> 16) & 255,
@@ -148,14 +150,16 @@ const mask = computed(() => {
})
const ipValue = computed(() => {
return (parseInt(ip.value[0]) << 24) +
(parseInt(ip.value[1]) << 16) +
(parseInt(ip.value[2]) << 8) +
parseInt(ip.value[3])
return (
(parseInt(ip.value[0]) << 24) +
(parseInt(ip.value[1]) << 16) +
(parseInt(ip.value[2]) << 8) +
parseInt(ip.value[3])
)
})
const maskValue = computed(() => {
return 0xFFFFFFFF << (32 - cidr.value)
return 0xffffffff << (32 - cidr.value)
})
const networkAddress = computed(() => {
@@ -187,21 +191,21 @@ const hostRange = computed(() => {
const network = ipValue.value & maskValue.value
const first = network + 1
const last = (network | (~maskValue.value >>> 0)) - 1
const firstIP = [
(first >>> 24) & 255,
(first >>> 16) & 255,
(first >>> 8) & 255,
first & 255
].join('.')
const lastIP = [
(last >>> 24) & 255,
(last >>> 16) & 255,
(last >>> 8) & 255,
last & 255
].join('.')
return `${firstIP} - ${lastIP}`
})
@@ -224,7 +228,10 @@ const maskBinary = computed(() => {
})
const bits = computed(() => {
return ip.value.map(octet => toBinary(parseInt(octet))).join('').split('')
return ip.value
.map((octet) => toBinary(parseInt(octet)))
.join('')
.split('')
})
const networkBinary = computed(() => {
@@ -236,7 +243,7 @@ const hostBinary = computed(() => {
})
const calculate = () => {
ip.value = ip.value.map(v => Math.min(255, Math.max(0, parseInt(v) || 0)))
ip.value = ip.value.map((v) => Math.min(255, Math.max(0, parseInt(v) || 0)))
cidr.value = Math.min(30, Math.max(8, cidr.value || 24))
}
@@ -261,8 +268,15 @@ onMounted(() => {
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.input-section {
display: flex;
@@ -434,7 +448,8 @@ onMounted(() => {
gap: 0.25rem;
}
.network-box, .host-box {
.network-box,
.host-box {
width: 12px;
height: 12px;
border-radius: 2px;
@@ -458,5 +473,4 @@ onMounted(() => {
display: flex;
gap: 0.25rem;
}
</style>
@@ -7,14 +7,14 @@
<div class="demo-content">
<div class="comparison-tabs">
<button
<button
:class="['tab-btn', { active: activeTab === 'tcp' }]"
@click="activeTab = 'tcp'"
>
<span class="tab-icon">📨</span>
<span>TCP (可靠)</span>
</button>
<button
<button
:class="['tab-btn', { active: activeTab === 'udp' }]"
@click="activeTab = 'udp'"
>
@@ -23,15 +23,12 @@
</button>
</div>
<div
v-if="currentProtocol"
class="protocol-detail"
>
<div v-if="currentProtocol" class="protocol-detail">
<div class="detail-header">
<span class="detail-name">{{ currentProtocol.name }}</span>
<span class="detail-full">{{ currentProtocol.fullName }}</span>
</div>
<div class="feature-grid">
<div
v-for="(feature, i) in currentProtocol.features"
@@ -45,9 +42,7 @@
</div>
<div class="mechanism-section">
<div class="mechanism-title">
核心机制
</div>
<div class="mechanism-title">核心机制</div>
<div class="mechanism-list">
<div
v-for="(m, i) in currentProtocol.mechanisms"
@@ -61,70 +56,53 @@
</div>
<div class="use-cases">
<div class="use-title">
适用场景
</div>
<div class="use-title">适用场景</div>
<div class="use-tags">
<span
v-for="(use, i) in currentProtocol.useCases"
:key="i"
class="use-tag"
>{{ use }}</span>
>{{ use }}</span
>
</div>
</div>
</div>
<div class="visual-demo">
<div class="visual-title">
传输过程演示
</div>
<div class="visual-title">传输过程演示</div>
<div class="transmission-demo">
<div class="sender">
<div class="node-label">
发送方
</div>
<div class="node-label">发送方</div>
<div class="packets">
<div
v-for="(packet, i) in packets"
<div
v-for="(packet, i) in packets"
:key="i"
:class="['packet', { sent: packet.sent, acked: packet.acked, lost: packet.lost }]"
:class="[
'packet',
{ sent: packet.sent, acked: packet.acked, lost: packet.lost }
]"
>
{{ packet.seq }}
</div>
</div>
</div>
<div class="network-channel">
<div class="channel-label">
网络通道
</div>
<div
class="channel-status"
:class="{ congested: isCongested }"
>
<div class="channel-label">网络通道</div>
<div class="channel-status" :class="{ congested: isCongested }">
{{ isCongested ? '拥堵' : '正常' }}
</div>
<button
class="demo-btn"
@click="runDemo"
>
开始演示
</button>
<button
class="demo-btn"
@click="toggleCongestion"
>
<button class="demo-btn" @click="runDemo">开始演示</button>
<button class="demo-btn" @click="toggleCongestion">
{{ isCongested ? '恢复网络' : '模拟丢包' }}
</button>
</div>
<div class="receiver">
<div class="node-label">
接收方
</div>
<div class="node-label">接收方</div>
<div class="received-packets">
<div
v-for="(packet, i) in receivedPackets"
<div
v-for="(packet, i) in receivedPackets"
:key="i"
class="received-packet"
>
@@ -133,17 +111,11 @@
</div>
</div>
</div>
<div class="demo-log">
<div class="log-title">
传输日志
</div>
<div class="log-title">传输日志</div>
<div class="log-content">
<div
v-for="(log, i) in logs"
:key="i"
class="log-item"
>
<div v-for="(log, i) in logs" :key="i" class="log-item">
{{ log }}
</div>
</div>
@@ -151,9 +123,7 @@
</div>
<div class="comparison-table">
<div class="table-title">
特性对比
</div>
<div class="table-title">特性对比</div>
<table>
<thead>
<tr>
@@ -163,10 +133,7 @@
</tr>
</thead>
<tbody>
<tr
v-for="(row, i) in comparisonData"
:key="i"
>
<tr v-for="(row, i) in comparisonData" :key="i">
<td class="feature-col">
{{ row.feature }}
</td>
@@ -183,7 +150,9 @@
</div>
<div class="info-box">
<strong>核心思想</strong>TCP 像挂号信确保送达但较慢UDP 像平信快速但不保证送达选择哪种协议取决于应用场景需要可靠性选 TCP需要实时性选 UDP
<strong>核心思想</strong>TCP 像挂号信确保送达但较慢UDP
像平信快速但不保证送达选择哪种协议取决于应用场景需要可靠性选
TCP需要实时性选 UDP
</div>
</div>
</template>
@@ -267,25 +236,25 @@ const toggleCongestion = () => {
const runDemo = async () => {
receivedPackets.value = []
logs.value = ['开始传输演示...']
for (let i = 0; i < packets.value.length; i++) {
packets.value[i].sent = false
packets.value[i].acked = false
packets.value[i].lost = false
}
const isTcp = activeTab.value === 'tcp'
for (let i = 0; i < packets.value.length; i++) {
const packet = packets.value[i]
packet.sent = true
if (isCongested.value && Math.random() > 0.5) {
packet.lost = true
logs.value.push(`${packet.seq} 丢失!`)
if (isTcp) {
await new Promise(r => setTimeout(r, 500))
await new Promise((r) => setTimeout(r, 500))
logs.value.push(`TCP 重传包 ${packet.seq}...`)
packet.lost = false
receivedPackets.value.push(packet.seq)
@@ -297,12 +266,14 @@ const runDemo = async () => {
packet.acked = true
logs.value.push(`${packet.seq} 送达`)
}
await new Promise(r => setTimeout(r, 300))
await new Promise((r) => setTimeout(r, 300))
}
if (isTcp) {
logs.value.push(`TCP 完成: 收到 ${receivedPackets.value.length} 个包,顺序: ${receivedPackets.value.join(', ')}`)
logs.value.push(
`TCP 完成: 收到 ${receivedPackets.value.length} 个包,顺序: ${receivedPackets.value.join(', ')}`
)
} else {
logs.value.push(`UDP 完成: 收到 ${receivedPackets.value.length} 个包`)
}
@@ -325,8 +296,15 @@ const runDemo = async () => {
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.comparison-tabs {
display: flex;
@@ -353,7 +331,9 @@ const runDemo = async () => {
background: var(--vp-c-brand-soft);
}
.tab-icon { font-size: 1.1rem; }
.tab-icon {
font-size: 1.1rem;
}
.protocol-detail {
background: var(--vp-c-bg);
@@ -395,15 +375,24 @@ const runDemo = async () => {
border-radius: 4px;
}
.feature-icon { font-size: 1rem; }
.feature-name { font-size: 0.75rem; color: var(--vp-c-text-2); }
.feature-value { font-size: 0.8rem; font-weight: bold; }
.feature-icon {
font-size: 1rem;
}
.feature-name {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.feature-value {
font-size: 0.8rem;
font-weight: bold;
}
.mechanism-section {
margin-bottom: 0.75rem;
}
.mechanism-title, .use-title {
.mechanism-title,
.use-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.5rem;
@@ -465,7 +454,8 @@ const runDemo = async () => {
margin-bottom: 0.75rem;
}
.sender, .receiver {
.sender,
.receiver {
flex: 1;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
@@ -478,7 +468,8 @@ const runDemo = async () => {
margin-bottom: 0.25rem;
}
.packets, .received-packets {
.packets,
.received-packets {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
@@ -604,7 +595,8 @@ table {
font-size: 0.8rem;
}
th, td {
th,
td {
border: 1px solid var(--vp-c-divider);
padding: 0.4rem;
text-align: center;
@@ -635,5 +627,4 @@ th {
display: flex;
gap: 0.25rem;
}
</style>
@@ -5,7 +5,7 @@
<div class="schematic" @click="gateOn = !gateOn">
<!-- Source terminal -->
<div class="terminal-box source">
<span class="pin-label">源极<br><span class="en">Source</span></span>
<span class="pin-label">源极<br /><span class="en">Source</span></span>
<div class="pin-wire" :class="{ active: gateOn }"></div>
</div>
@@ -23,13 +23,15 @@
</template>
<span v-else class="block-mark"></span>
</div>
<div class="channel-status">{{ gateOn ? '导通 → 输出 1' : '断开 → 输出 0' }}</div>
<div class="channel-status">
{{ gateOn ? '导通 → 输出 1' : '断开 → 输出 0' }}
</div>
</div>
<!-- Drain terminal -->
<div class="terminal-box drain">
<div class="pin-wire" :class="{ active: gateOn }"></div>
<span class="pin-label">漏极<br><span class="en">Drain</span></span>
<span class="pin-label">漏极<br /><span class="en">Drain</span></span>
</div>
</div>
@@ -179,14 +181,28 @@ const gateOn = ref(false)
animation: flow 1.2s linear infinite;
}
.electron.e2 { animation-delay: 0.4s; }
.electron.e3 { animation-delay: 0.8s; }
.electron.e2 {
animation-delay: 0.4s;
}
.electron.e3 {
animation-delay: 0.8s;
}
@keyframes flow {
0% { left: -8%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { left: 108%; opacity: 0; }
0% {
left: -8%;
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
left: 108%;
opacity: 0;
}
}
.channel-status {
@@ -208,7 +224,11 @@ const gateOn = ref(false)
}
@media (max-width: 480px) {
.pin-wire { width: 1.5rem; }
.channel-area { min-width: 5rem; }
.pin-wire {
width: 1.5rem;
}
.channel-area {
min-width: 5rem;
}
}
</style>
@@ -12,36 +12,20 @@
:class="{ active: activeType === 'serial' }"
@click="activeType = 'serial'"
>
<div class="card-icon">
</div>
<div class="card-title">
串行传输
</div>
<div class="card-desc">
一位一位依次传输
</div>
<div class="card-examples">
USBSATAPCIe
</div>
<div class="card-icon"></div>
<div class="card-title">串行传输</div>
<div class="card-desc">一位一位依次传输</div>
<div class="card-examples">USBSATAPCIe</div>
</div>
<div
class="type-card"
:class="{ active: activeType === 'parallel' }"
@click="activeType = 'parallel'"
>
<div class="card-icon">
</div>
<div class="card-title">
并行传输
</div>
<div class="card-desc">
多位同时传输
</div>
<div class="card-examples">
旧式打印机接口IDE
</div>
<div class="card-icon"></div>
<div class="card-title">并行传输</div>
<div class="card-desc">多位同时传输</div>
<div class="card-examples">旧式打印机接口IDE</div>
</div>
</div>
@@ -51,47 +35,35 @@
</div>
<div class="visual-area">
<div class="sender">
<div class="device-label">
发送端
</div>
<div class="device-label">发送端</div>
<div class="data-bits">
<span
v-for="(bit, i) in dataBits"
:key="i"
class="bit"
:class="{ sending: sendingBit === i && activeType === 'serial' }"
>{{ bit }}</span>
:class="{
sending: sendingBit === i && activeType === 'serial'
}"
>{{ bit }}</span
>
</div>
</div>
<div class="channels">
<div
v-if="activeType === 'serial'"
class="channel serial"
>
<div class="channel-label">
单通道
</div>
<div v-if="activeType === 'serial'" class="channel serial">
<div class="channel-label">单通道</div>
<div class="channel-flow">
<span
v-for="i in 5"
:key="i"
class="flow-dot"
:class="{ active: sendingBit !== null }"
></span>
></span
>
</div>
</div>
<div
v-else
class="channel parallel"
>
<div
v-for="i in 4"
:key="i"
class="channel-row"
>
<div class="channel-label">
通道{{ i }}
</div>
<div v-else class="channel parallel">
<div v-for="i in 4" :key="i" class="channel-row">
<div class="channel-label">通道{{ i }}</div>
<div class="channel-flow">
<span class="flow-dot active"></span>
</div>
@@ -99,30 +71,19 @@
</div>
</div>
<div class="receiver">
<div class="device-label">
接收端
</div>
<div class="device-label">接收端</div>
<div class="data-bits received">
<span
v-for="(bit, i) in receivedBits"
:key="i"
class="bit"
>{{ bit }}</span>
<span v-for="(bit, i) in receivedBits" :key="i" class="bit">{{
bit
}}</span>
</div>
</div>
</div>
<button
class="send-btn"
@click="startTransmission"
>
发送数据
</button>
<button class="send-btn" @click="startTransmission">发送数据</button>
</div>
<div class="comparison-table">
<div class="table-title">
串行 vs 并行对比
</div>
<div class="table-title">串行 vs 并行对比</div>
<table>
<thead>
<tr>
@@ -158,7 +119,8 @@
</div>
<div class="info-box">
<strong>核心思想</strong>现代高速传输多采用串行方式虽然并行"看起来"更快一次传多位但串行可以跑更高频率抗干扰更强实际速度反而更快
<strong>核心思想</strong
>现代高速传输多采用串行方式虽然并行"看起来"更快一次传多位但串行可以跑更高频率抗干扰更强实际速度反而更快
</div>
</div>
</template>
@@ -207,8 +169,15 @@ const startTransmission = () => {
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
display: flex;
@@ -279,7 +248,8 @@ const startTransmission = () => {
margin-bottom: 0.75rem;
}
.sender, .receiver {
.sender,
.receiver {
text-align: center;
}
@@ -382,7 +352,8 @@ table {
font-size: 0.8rem;
}
th, td {
th,
td {
border: 1px solid var(--vp-c-divider);
padding: 0.4rem;
text-align: center;
@@ -402,5 +373,4 @@ th {
display: flex;
gap: 0.25rem;
}
</style>
@@ -107,7 +107,8 @@
<div class="port-title">端口号应用程序的标识</div>
<div class="port-examples">
<div class="port-intro">
端口号就像公寓房间号IP 地址是公寓楼地址合起来才能找到具体的应用程序
端口号就像公寓房间号IP
地址是公寓楼地址合起来才能找到具体的应用程序
</div>
<div class="port-list">
<div
@@ -222,8 +223,14 @@ const currentProtocol = computed(() => protocolData[activeProtocol.value])
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.protocol-tabs {
display: flex;
@@ -422,9 +429,17 @@ const currentProtocol = computed(() => protocolData[activeProtocol.value])
}
@keyframes slideRight {
0% { transform: translateX(-100%); opacity: 0; }
50% { opacity: 1; }
100% { transform: translateX(100%); opacity: 0; }
0% {
transform: translateX(-100%);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 0;
}
}
.side-desc {
@@ -24,20 +24,43 @@
<div class="tree-canvas">
<svg viewBox="0 0 600 350" class="tree-svg">
<!-- 连接线 -->
<line v-for="line in binaryTreeLines" :key="line.id"
:x1="line.x1" :y1="line.y1"
:x2="line.x2" :y2="line.y2"
<line
v-for="line in binaryTreeLines"
:key="line.id"
:x1="line.x1"
:y1="line.y1"
:x2="line.x2"
:y2="line.y2"
stroke="var(--vp-c-divider)"
stroke-width="2"
/>
<!-- 节点 -->
<g v-for="node in binaryTreeNodes" :key="node.id"
:class="['tree-node', { root: node.isRoot, leaf: node.isLeaf }]"
:style="{ transform: `translate(${node.x}px, ${node.y}px)` }"
<g
v-for="node in binaryTreeNodes"
:key="node.id"
:class="['tree-node', { root: node.isRoot, leaf: node.isLeaf }]"
:style="{ transform: `translate(${node.x}px, ${node.y}px)` }"
>
<circle cx="0" cy="0" r="25" fill="var(--vp-c-brand-soft)" stroke="var(--vp-c-brand)" stroke-width="2" />
<text x="0" y="0" text-anchor="middle" dominant-baseline="middle" fill="var(--vp-c-brand)" font-size="14" font-weight="600">{{ node.value }}</text>
<circle
cx="0"
cy="0"
r="25"
fill="var(--vp-c-brand-soft)"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<text
x="0"
y="0"
text-anchor="middle"
dominant-baseline="middle"
fill="var(--vp-c-brand)"
font-size="14"
font-weight="600"
>
{{ node.value }}
</text>
</g>
</svg>
</div>
@@ -83,14 +106,9 @@
<div class="dom-preview">
<div class="preview-title">HTML 结构</div>
<div class="preview-html">
&lt;html&gt;
&lt;body&gt;
&lt;div class="container"&gt;
&lt;h1&gt;标题&lt;/h1&gt;
&lt;p&gt;段落&lt;/p&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
&lt;html&gt; &lt;body&gt; &lt;div class="container"&gt;
&lt;h1&gt;标题&lt;/h1&gt; &lt;p&gt;&lt;/p&gt; &lt;/div&gt;
&lt;/body&gt; &lt;/html&gt;
</div>
</div>
<div class="dom-structure">
@@ -235,8 +253,14 @@ const binaryTreeLines = [
margin-bottom: 1.5rem;
}
.demo-header .title { font-weight: 700; font-size: 1.1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.demo-header .title {
font-weight: 700;
font-size: 1.1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.tree-selector {
margin-bottom: 2rem;
@@ -126,6 +126,10 @@
因此,只要把一个 XOR 门和一个 AND 门组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder**。
<HalfAdderDemo />
再进一步:我们可以把多个半加器和全加器级联组合起来,完成多个位数的加法:
<AdderDemo />
::: tip 核心解析:分解加法器
@@ -1,60 +1,66 @@
# API 入门
::: tip 🎯 学习目标
阅读完本节后,你将能够:
- 理解 API 的本质概念和设计哲学
- 区分不同类型的 API(函数 API、操作系统 API、Web API
- 掌握 HTTP 方法的语义和使用场景
- 学会阅读和使用 API 文档
- 理解 HTTP 调用与 SDK 调用的区别
:::
---
## 1. 从一个按钮开始
<ApiQuickStartDemo />
👆 看见了吗?点一下按钮,230 毫秒后,一句格言就回来了
👆 看见了吗?点按钮,几百毫秒后,服务器返回了当前时间
这个过程,就是 **API 调用**
这个过程,就是一次完整的 **API 调用**虽然简单,但它包含了 API 交互的所有核心要素:
| 阶段 | 发生了什么 |
|------|-----------|
| **请求** | 客户端向服务器发送 "给我当前时间" 的请求 |
| **处理** | 服务器接收请求,查询系统时间 |
| **响应** | 服务器将时间数据返回给客户端 |
这就像你去便利店买水——你说"来瓶可乐",店员给你一瓶可乐。你问一句,他给一笔。
**APIApplication Programming Interface,应用程序编程接口)**,本质上就是一种"对话约定":一方提出请求,另一方给出响应。
---
## 说白了,API 就是"问一句,拿一笔"
## 2. API 的三种形态
你可能觉得"调用 API"是很高大上的操作
很多人一提到 API,就觉得这是很高深的东西。其实,你写代码的第一天就在用 API 了
其实吧,就像你去便利店买水——你说"来瓶可乐",店员给你一瓶可乐。你问一句,他给一笔。
API 也是一样:
1. **你问**(发送请求)
2. **别人答**(服务器处理)
3. **你拿到**(返回结果)
只不过这次,你问的不是店员,而是一台远在千里之外的电脑。
---
## 但 API 远不止"网络接口"
一提到 API,很多人觉得这是很高深的东西,离自己很远。
其实吧,你写代码的第一天就在用 API 了,只是你不知道而已。
### 函数,就是最基础的 API
### 2.1 函数 API:最基础的形态
<FunctionApiDemo />
```python
length = len("hello")
```
当你调用 `len("hello")` 时,你就在使用 Python 提供的一个 API。你不需要知道 `len()` 内部是怎么数字符串长度的——是用 C 实现的还是用 Python 实现的?是遍历计数还是直接读取长度字段?这些细节都被隐藏了。
`len()` 这个东西,你觉得它是什么?
**API 的核心价值:把复杂的东西藏起来,只给你一个简单的用法。**
它是个函数,没错。但同时,它也是 Python 给你留的一个 API。
什么意思呢?你不需要知道 `len()` 内部是怎么数字符串长度的,你不需要知道它是用 C 写的还是用 Python 写的,你只需要知道一件事——**把字符串给我,我告诉你多长**。
这就叫 API:**我把复杂的东西藏起来,只给你一个简单的用法**。
再看一个:
再看一个更贴近实际的例子:
```python
my_list = []
my_list.append("item")
# 你写这行代码
response = requests.get("https://api.example.com/users")
# 背后发生了什么?
# 1. DNS 解析:把域名转成 IP 地址
# 2. TCP 连接:建立网络通道
# 3. TLS 握手:加密通信
# 4. HTTP 请求:发送数据包
# 5. 等待响应:接收服务器返回
# 6. 解析数据:把字节流转成 Python 对象
```
`append()` 也是 API。背后可能是内存分配、指针移动、容量扩容...但你不用关心这些。你只需要说"给我加上去",它就帮你搞定
如果你要自己处理这 6 步,每次请求都要写几百行代码。但 `requests.get()` 把这些都封装好了,你只需要一行代码
### 操作系统 API:让你的程序能"碰"硬件
### 2.2 操作系统 API:让程序能"碰"硬件
当你在电脑上打开一个文件:
@@ -63,27 +69,17 @@ with open("file.txt", "r") as f:
content = f.read()
```
这行代码背后,Python 其实在调用**操作系统的 API**。
这行代码背后,Python 调用**操作系统的 API**。
Windows 有 Win32 APILinux 有 POSIX APImacOS 有 Cocoa API。这些 API 是干嘛的?让程序能真正操作硬件——读写硬盘、显示窗口、播放声音。
| 操作系统 | API 名称 | 作用 |
|---------|---------|------|
| Windows | Win32 API | 文件操作、窗口管理、进程控制 |
| Linux | POSIX API | 系统调用、进程通信、设备访问 |
| macOS | Cocoa API | 图形界面、文件系统、网络 |
没有操作系统 API,你的 Python 代码就是一堆文字,根本动不了硬盘里的文件。
没有操作系统 API,你的 Python 代码就是一堆文字,根本动不了硬盘里的文件、显示不了窗口、连不上网络
### 第三方库的 API:站在巨人的肩膀上
当你用 NumPy 做矩阵运算:
```python
import numpy as np
matrix = np.array([[1, 2], [3, 4]])
result = np.dot(matrix, matrix)
```
`np.dot()` 就是 NumPy 给你留的 API。背后可能是经过优化的 C++ 代码、多线程计算、SIMD 指令...但你只需要知道一件事——**给我两个矩阵,我还你一个乘积**。
这就是 API 的魅力:**把别人的能力,变成你的能力**。
### Web API:跨越网络的"超能力"
### 2.3 Web API:跨越网络的"超能力"
最后,才是大多数人熟知的 Web API:
@@ -93,136 +89,180 @@ import requests
response = requests.post(
"https://api.deepseek.com/v1/chat/completions",
headers={"Authorization": "Bearer sk-xxx"},
json={"model": "deepseek-chat", "messages": [{"role": "user", "content": "你好"}]}
json={
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "你好"}]
}
)
```
这行代码做了什么?
这行代码做了什么?它让你的程序穿越互联网,调用了千里之外 DeepSeek 服务器上的 AI 模型。就像你打了个越洋电话,让大洋彼岸的厨师帮你做了一道菜。
它让你的程序穿越互联网,调用了千里之外 DeepSeek 服务器上的 AI 模型。就像你打了个越洋电话,让大洋彼岸的厨师帮你做了一道菜。
**Web API 的神奇之处就在于:它让"调用别人的超级电脑"变得像调用本地函数一样简单。**
**Web API 的神奇之处:让"调用别人的超级电脑"变得像调用本地函数一样简单。**
---
## 无论哪种 API,结构都一样
## 3. API 的统一结构
说了这么多,你可能发现了——不管哪种 API它们的结构都一样
就像插头和插座,怎么变都离不开三样东西:
| 要素 | 函数 API 的例子 | Web API 的例子 |
|------|----------------|---------------|
| **地址/名称** | `len()` | `https://api.example.com/users` |
| **输入/参数** | `"hello"` | `{"name": "张三"}` |
| **输出/返回** | `5` | `{"id": 1}` |
无论哪种 API,结构都一样。就像插头和插座,怎么变都离不开三样东西:
<ApiConceptDemo />
| 要素 | 函数 API | Web API |
|------|---------|---------|
| **地址/名称** | `len()` | `https://api.example.com/users` |
| **输入/参数** | `"hello"` | `{"name": "张三"}` |
| **输出/返回** | `5` | `{"id": 1, "name": "张三"}` |
理解了这个统一结构,你就掌握了 API 的本质。无论是调用一个 Python 函数,还是请求一个远程服务器,思路都是一样的:
1. **找到入口**(函数名或 URL
2. **传入参数**(按要求的格式)
3. **处理返回**(按约定的结构解析)
---
## 调用服务器:你是在"问"还是在"做"
## 4. HTTP 方法:你是在"问"还是在"做"
好,现在你知道调用 API 需要地址和参数
但还有个问题没说:**你跟服务器说话的方式,不止一种。**
什么意思?
调用 Web API 时,你需要告诉服务器你想做什么。这就是 HTTP 方法的由来
想象你去一家餐厅:
| 场景 | 现实中你会怎么说? | 对应的 API |
| 场景 | 现实中你会怎么说? | 对应的 HTTP |
|------|-------------------|-----------------|
| 你想知道今天有什么菜 | "服务员,菜单给我看看" | **GET** - 纯"问",不改数据 |
| 你想点一份宫保鸡丁 | "给我来份宫保鸡丁" | **POST** - "做"件事 |
| 你想点一份宫保鸡丁 | "给我来份宫保鸡丁" | **POST** - "做"件事,创建数据 |
| 你想换一道菜 | "把宫保鸡丁改成糖醋里脊" | **PUT** - 替换数据 |
| 你想改口味 | "宫保鸡丁不要放花生" | **PATCH** - 部分修改 |
| 你不想要了 | "算了,那道菜不要了" | **DELETE** - 删除数据 |
这就是 HTTP 方法的来历:**不同的动词,对应不同的操作。**
但最常用的就两个:**GET 和 POST**。其他的先不用管。
<ApiMethodDemo />
::: warning 关于幂等性
**幂等性**是一个重要概念:多次执行结果是否相同?
- **GET**:查询 10 次和查询 1 次,结果一样 → 幂等
- **DELETE**:删除 10 次和删除 1 次,结果一样 → 幂等
- **POST**:下单 10 次,可能创建 10 个订单 → 不幂等
实际开发中,POST 操作通常需要用唯一 ID 来防止重复处理。
:::
---
## HTTP vs SDK:自己跑腿还是让管家代办?
## 5. HTTP vs SDK:自己跑腿还是让管家代办?
既然教程的重点是 AI 编程,我们就重点讲讲 Web API——这是你和 AI 模型打交道的主要方式
你在教程里经常会看到两种调用方式:**HTTP** 和 **SDK**。很多人会被绕晕,其实很简单,就是**"自己跑腿"**和**"让管家代办"**的区别。
### HTTP API:自己跑腿
这是最原始的方式。就像你自己去餐厅,从头开始点菜。
所有编程语言都能用,甚至你在浏览器地址栏里敲一行字也是一种 HTTP 请求。
### SDK:让管家代办
SDK (Software Development Kit) 就像是餐厅派给你的专属管家。
你不需要自己填单子、贴邮票。你只需要跟管家说"来份宫保鸡丁",管家会自己在后台帮你填单子、发请求、处理报错。
在 AI 编程教程中,你会经常看到两种调用方式:**HTTP** 和 **SDK**。它们的区别就像"自己跑腿"和"让管家代办"
<RealWorldApiDemo />
> 能用 SDK 就用 SDK,把麻烦事留给别人,把时间留给自己。
### 5.1 HTTP API:自己跑腿
## 怎么看 API 文档?
这是最原始的方式。你需要:
文档就像说明书和菜单的结合体。你不需要从头读到尾,只需要学会查字典。
1. **找到网址**Base URL + Endpoint
2. **准备请求头**Authorization、Content-Type
3. **构造请求体**JSON 格式的参数)
4. **发送请求**(处理网络错误、超时)
5. **解析响应**(把 JSON 转成可用的数据)
所有编程语言都能用,但你需要处理很多细节。
### 5.2 SDK:让管家代办
SDKSoftware Development Kit)就像是 API 提供方派给你的专属管家:
```python
# HTTP 方式:自己处理所有细节
import requests
response = requests.post(
"https://api.deepseek.com/v1/chat/completions",
headers={"Authorization": "Bearer sk-xxx"},
json={"model": "deepseek-chat", "messages": [...]}
)
result = response.json()["choices"][0]["message"]["content"]
# SDK 方式:管家帮你处理
from openai import OpenAI
client = OpenAI(api_key="sk-xxx")
response = client.chat.completions.create(
model="deepseek-chat",
messages=[...]
)
result = response.choices[0].message.content
```
SDK 自动处理了:鉴权、请求格式、错误处理、重试逻辑、响应解析。
::: tip 建议
**能用 SDK 就用 SDK**,把麻烦事留给库,把时间留给自己。
:::
---
## 6. 如何阅读 API 文档?
API 文档就像说明书和菜单的结合体。你不需要从头读到尾,只需要学会"查字典"。
打开任何一个 API 文档(比如 OpenAI 或 DeepSeek),你只需要找这几样东西:
1. **Base URL**:根地址(餐厅在哪?)
2. **Authentication**:怎么证明你是会员?(通常是 `Authorization: Bearer sk-...`
3. **Endpoints**:具体的接口列表
- `/v1/chat/completions` -> 对话(最常用的)
- `/v1/images/generations` -> 画图
4. **Parameters**:必填项有哪些?
<ApiDocumentDemo />
### 常见的"餐厅黑话"(状态码)
### 6.1 文档阅读清单
服务员(API)回复你的时候,通常会先喊一个数字代码:
| 项目 | 说明 | 示例 |
|------|------|------|
| **Base URL** | API 的根地址 | `https://api.deepseek.com` |
| **Authentication** | 如何证明身份 | `Authorization: Bearer sk-xxx` |
| **Endpoints** | 具体的接口列表 | `/v1/chat/completions` |
| **Parameters** | 必填/可选参数 | `model`(必填)、`temperature`(可选) |
| **Response** | 返回数据结构 | `{"choices": [...]}` |
| 状态码 | 含义 |
|--------|------|
| **200 OK** | 成功了 |
| **400 Bad Request** | 你填错了 |
| **401 Unauthorized** | 没权限 |
| **404 Not Found** | 地址错了 |
| **429 Too Many Requests** | 你点太快了 |
| **500 Internal Server Error** | 对方服务器崩了 |
### 6.2 常见状态码
服务器回复时,会先返回一个状态码,告诉你请求是否成功:
| 状态码 | 含义 | 常见原因 |
|--------|------|---------|
| **200 OK** | 成功 | 请求正常处理 |
| **201 Created** | 创建成功 | POST 请求成功创建资源 |
| **400 Bad Request** | 请求格式错误 | 参数缺失或格式不对 |
| **401 Unauthorized** | 未认证 | 没有提供有效的 API Key |
| **403 Forbidden** | 无权限 | API Key 没有访问该资源的权限 |
| **404 Not Found** | 不存在 | 请求的地址或资源不存在 |
| **429 Too Many Requests** | 请求过多 | 超过了速率限制 |
| **500 Internal Server Error** | 服务器错误 | 服务端出了问题 |
---
## 练手场:弄坏它也没关系
## 7. 动手练习
光说不练假把式。这里有个模拟 API,你可以随便填参数、随便改地址,看看会发生什么。
试着触发一下 401(假装没带钱)或者 404(瞎填地址)。
<ApiPlayground />
试着触发以下场景:
- ✅ **成功请求**:填入正确的 Endpoint 和 API Key
- ❌ **401 错误**:不填 API Key,看看服务器怎么拒绝你
- ❌ **404 错误**:填一个不存在的地址
---
##
## 8. 小
别把 API 想得太复杂。在 AI 编程的时代,你只需要记住这几件事:
1. **API 就是传声筒**,帮你把话传给 AI 模型
2. **你早就用过 API**了,从 `len()``open()`
::: info 核心要点
1. **API 就是传声筒**,帮你把话传给另一段代码或远程服务器
2. **你早就用过 API 了**,从 `len()``open()` 都是 API
3. **Web API 是超能力**,让你调用千里之外的超级电脑
4. **SDK 是好管家**,能用管家就别自己跑腿
5. **看文档找三样**:地址、密钥、参数
4. **SDK 是好管家**,能用 SDK 就别自己跑腿
5. **看文档找三样**:地址、鉴权、参数
:::
这就够了。剩下的,交给 IDE 去写吧
在 AI 编程的时代,你只需要记住这几个核心概念。剩下的细节,IDE 和 AI 助手会帮你处理
---
## 名词速查表 (Glossary)
## 名词速查表
| 名词 | 全称 | 解释 |
|------|------|------|
@@ -242,3 +282,4 @@ SDK (Software Development Kit) 就像是餐厅派给你的专属管家。
| **Header** | - | HTTP 头,包含元信息 |
| **Payload** | - | 请求或响应的实际数据 |
| **Rate Limit** | - | 速率限制 |
| **Idempotent** | - | 幂等,多次执行结果相同 |