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:
@@ -14,7 +14,9 @@
|
||||
</div>
|
||||
<div class="t-line">
|
||||
<span class="t-ps">> </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;
|
||||
|
||||
Reference in New Issue
Block a user