chore: update docs and configurations
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
<!--
|
||||
ApiConceptDemo.vue
|
||||
参考 ide-intro 的“虚拟 UI + 先玩再讲”风格。
|
||||
目标:用一个简单的“分类小游戏”讲清楚 API 到底要写清楚什么,
|
||||
以及哪些是“你不用管的内部细节”。
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">API 到底是什么?(先玩 10 秒)</div>
|
||||
<div class="sub">
|
||||
下面有一些卡片。请你把它们分成两类:<b>必须写清楚</b> vs
|
||||
<b>不用写给别人看</b>。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arena">
|
||||
<div class="cards">
|
||||
<div class="cardsTitle">卡片池</div>
|
||||
<div class="cardGrid">
|
||||
<button
|
||||
v-for="c in pool"
|
||||
:key="c.id"
|
||||
class="card"
|
||||
@click="pick(c.id)"
|
||||
>
|
||||
<div class="cardTop">
|
||||
<span class="tag" :class="c.type">{{ c.typeLabel }}</span>
|
||||
</div>
|
||||
<div class="cardText">{{ c.text }}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="muted">
|
||||
提示:API
|
||||
的核心就是“怎么用”。所以:入口、要填什么、会返回什么、失败怎么说。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bins">
|
||||
<div class="bin must">
|
||||
<div class="binHead">
|
||||
<div class="binTitle">必须写清楚</div>
|
||||
<div class="binHint">不写清楚就会用错</div>
|
||||
</div>
|
||||
<div class="drop">
|
||||
<div v-if="must.length === 0" class="empty">点卡片,把它放进来</div>
|
||||
<button v-for="id in must" :key="id" class="chip" @click="undo(id)">
|
||||
{{ byId(id).text }}
|
||||
<span class="x">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bin internal">
|
||||
<div class="binHead">
|
||||
<div class="binTitle">不用写给别人看</div>
|
||||
<div class="binHint">这是内部实现,换了也不影响“怎么用”</div>
|
||||
</div>
|
||||
<div class="drop">
|
||||
<div v-if="internal.length === 0" class="empty">
|
||||
点卡片,把它放进来
|
||||
</div>
|
||||
<button
|
||||
v-for="id in internal"
|
||||
:key="id"
|
||||
class="chip"
|
||||
@click="undo(id)"
|
||||
>
|
||||
{{ byId(id).text }}
|
||||
<span class="x">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bar">
|
||||
<button class="btn" @click="check">检查一下</button>
|
||||
<button class="ghost" @click="reset">重来</button>
|
||||
<div class="score">
|
||||
<span
|
||||
>正确:<b>{{ score.ok }}</b></span
|
||||
>
|
||||
<span
|
||||
>错误:<b>{{ score.bad }}</b></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result" v-if="checked">
|
||||
<div class="resultTitle">结论(新手版一句话)</div>
|
||||
<div class="resultText">
|
||||
API =
|
||||
你告诉别人“这个按钮怎么按”。<b>入口在哪</b>、<b>要填什么</b>、<b>会得到什么</b>、<b>失败怎么说</b>。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
const all = [
|
||||
// must
|
||||
{
|
||||
id: 'where',
|
||||
bucket: 'must',
|
||||
type: 'must',
|
||||
typeLabel: '必填脑子',
|
||||
text: '入口在哪(网址 / 函数名)'
|
||||
},
|
||||
{
|
||||
id: 'input',
|
||||
bucket: 'must',
|
||||
type: 'must',
|
||||
typeLabel: '必填脑子',
|
||||
text: '要填什么(需要哪些信息)'
|
||||
},
|
||||
{
|
||||
id: 'output',
|
||||
bucket: 'must',
|
||||
type: 'must',
|
||||
typeLabel: '必填脑子',
|
||||
text: '会拿到什么(成功的结果)'
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
bucket: 'must',
|
||||
type: 'must',
|
||||
typeLabel: '必填脑子',
|
||||
text: '失败怎么说(提示/原因)'
|
||||
},
|
||||
|
||||
// internal
|
||||
{
|
||||
id: 'db',
|
||||
bucket: 'internal',
|
||||
type: 'internal',
|
||||
typeLabel: '内部',
|
||||
text: '数据库表怎么设计'
|
||||
},
|
||||
{
|
||||
id: 'topo',
|
||||
bucket: 'internal',
|
||||
type: 'internal',
|
||||
typeLabel: '内部',
|
||||
text: '服务有几个机器/怎么部署'
|
||||
},
|
||||
{
|
||||
id: 'perf',
|
||||
bucket: 'internal',
|
||||
type: 'internal',
|
||||
typeLabel: '内部',
|
||||
text: '内部怎么做性能优化'
|
||||
}
|
||||
]
|
||||
|
||||
const must = ref([])
|
||||
const internal = ref([])
|
||||
const checked = ref(false)
|
||||
const score = reactive({ ok: 0, bad: 0 })
|
||||
|
||||
const used = computed(() => new Set([...must.value, ...internal.value]))
|
||||
const pool = computed(() => all.filter((c) => !used.value.has(c.id)))
|
||||
|
||||
function byId(id) {
|
||||
return all.find((x) => x.id === id) || { id, text: id }
|
||||
}
|
||||
|
||||
function pick(id) {
|
||||
// 简化交互:按顺序放进“必须写清楚”,再点一次放进“内部”
|
||||
// 这样新手不需要理解拖拽,也不需要想“放哪儿”:玩起来更顺。
|
||||
// 如果想放“内部”,可以先把它放错,再点 chip 撤回重新放。
|
||||
if (must.value.length <= internal.value.length) {
|
||||
must.value.push(id)
|
||||
} else {
|
||||
internal.value.push(id)
|
||||
}
|
||||
checked.value = false
|
||||
}
|
||||
|
||||
function undo(id) {
|
||||
must.value = must.value.filter((x) => x !== id)
|
||||
internal.value = internal.value.filter((x) => x !== id)
|
||||
checked.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
must.value = []
|
||||
internal.value = []
|
||||
checked.value = false
|
||||
score.ok = 0
|
||||
score.bad = 0
|
||||
}
|
||||
|
||||
function check() {
|
||||
let ok = 0
|
||||
let bad = 0
|
||||
for (const id of must.value) {
|
||||
byId(id).bucket === 'must' ? (ok += 1) : (bad += 1)
|
||||
}
|
||||
for (const id of internal.value) {
|
||||
byId(id).bucket === 'internal' ? (ok += 1) : (bad += 1)
|
||||
}
|
||||
score.ok = ok
|
||||
score.bad = bad
|
||||
checked.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.arena {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cards,
|
||||
.bin {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.cardsTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.cardGrid {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.cardTop {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.tag.must {
|
||||
border-color: color-mix(in srgb, #22c55e 40%, var(--vp-c-divider));
|
||||
background: color-mix(in srgb, #22c55e 12%, var(--vp-c-bg));
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.tag.internal {
|
||||
border-color: color-mix(in srgb, #f59e0b 45%, var(--vp-c-divider));
|
||||
background: color-mix(in srgb, #f59e0b 14%, var(--vp-c-bg));
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.cardText {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bins {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.binHead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.binTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.binHint {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.drop {
|
||||
margin-top: 10px;
|
||||
min-height: 120px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.x {
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.bar {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.score {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.resultTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.resultText {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.arena {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.cardGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.score {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,396 @@
|
||||
<!--
|
||||
ApiDocumentDemo.vue
|
||||
参考 ide-intro 的“虚拟界面 + 点击探索”风格。
|
||||
目标:让新手学会看 API 文档的 3 个重点:入口在哪 / 要填什么 / 会得到什么。
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">怎么读 API 文档?(像找按钮一样找)</div>
|
||||
<div class="sub">
|
||||
任务:请在下面的“假文档”里,依次点出:<b>入口</b>、<b>要填什么</b>、<b>会得到什么</b>。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game">
|
||||
<div class="doc">
|
||||
<div class="docBar">
|
||||
<span class="dot red" />
|
||||
<span class="dot yellow" />
|
||||
<span class="dot green" />
|
||||
<span class="docTitle">API 文档(示例)</span>
|
||||
</div>
|
||||
|
||||
<div class="docBody">
|
||||
<button
|
||||
class="block"
|
||||
:class="{ hit: hits.entry }"
|
||||
@click="hit('entry')"
|
||||
>
|
||||
<div class="blockK">入口</div>
|
||||
<div class="blockV">GET /v1/users/{id}</div>
|
||||
<div class="blockHint">(你要按哪个按钮)</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="block"
|
||||
:class="{ hit: hits.input }"
|
||||
@click="hit('input')"
|
||||
>
|
||||
<div class="blockK">要填什么</div>
|
||||
<div class="blockV">id(用户编号)</div>
|
||||
<div class="blockHint">(你要告诉它什么)</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="block"
|
||||
:class="{ hit: hits.output }"
|
||||
@click="hit('output')"
|
||||
>
|
||||
<div class="blockK">会得到什么</div>
|
||||
<div class="blockV">{ id, name }</div>
|
||||
<div class="blockHint">(成功时给你的结果)</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="block gray"
|
||||
:class="{ hit: hits.fail }"
|
||||
@click="hit('fail')"
|
||||
>
|
||||
<div class="blockK">失败会怎样(常见)</div>
|
||||
<div class="blockV">没钥匙 / 找不到 / 太频繁</div>
|
||||
<div class="blockHint">(你要能看懂失败原因)</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side">
|
||||
<div class="task">
|
||||
<div class="taskTitle">你要找的 3 个重点</div>
|
||||
<div class="taskList">
|
||||
<div :class="['taskItem', hits.entry && 'done']">
|
||||
① 入口在哪(点“入口”)
|
||||
</div>
|
||||
<div :class="['taskItem', hits.input && 'done']">
|
||||
② 要填什么(点“要填什么”)
|
||||
</div>
|
||||
<div :class="['taskItem', hits.output && 'done']">
|
||||
③ 会得到什么(点“会得到什么”)
|
||||
</div>
|
||||
</div>
|
||||
<div class="muted">你只要先会这三件事,就能开始用 API 了。</div>
|
||||
</div>
|
||||
|
||||
<div class="explain" v-if="last">
|
||||
<div class="explainTitle">你刚刚点的是:{{ labelOf(last) }}</div>
|
||||
<div class="explainText">{{ explainOf(last) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn" @click="autoWin">一键帮我找对</button>
|
||||
<button class="ghost" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="win" v-if="won">
|
||||
<div class="winTitle">完成!</div>
|
||||
<div class="winText">
|
||||
你已经会读 80% 的 API 文档了:入口 / 要填 / 会得到。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
const hits = reactive({
|
||||
entry: false,
|
||||
input: false,
|
||||
output: false,
|
||||
fail: false
|
||||
})
|
||||
const last = ref('')
|
||||
|
||||
const won = computed(() => hits.entry && hits.input && hits.output)
|
||||
|
||||
function hit(key) {
|
||||
hits[key] = true
|
||||
last.value = key
|
||||
}
|
||||
|
||||
function reset() {
|
||||
hits.entry = false
|
||||
hits.input = false
|
||||
hits.output = false
|
||||
hits.fail = false
|
||||
last.value = ''
|
||||
}
|
||||
|
||||
function autoWin() {
|
||||
hits.entry = true
|
||||
hits.input = true
|
||||
hits.output = true
|
||||
last.value = 'output'
|
||||
}
|
||||
|
||||
function labelOf(key) {
|
||||
if (key === 'entry') return '入口'
|
||||
if (key === 'input') return '要填什么'
|
||||
if (key === 'output') return '会得到什么'
|
||||
if (key === 'fail') return '失败会怎样'
|
||||
return key
|
||||
}
|
||||
|
||||
function explainOf(key) {
|
||||
if (key === 'entry') {
|
||||
return '入口就是“按钮名字”。你要按哪个按钮,先找到它。'
|
||||
}
|
||||
if (key === 'input') {
|
||||
return '要填什么 = 你需要提供的信息。比如 id、页码、搜索词。'
|
||||
}
|
||||
if (key === 'output') {
|
||||
return '会得到什么 = 成功时返回的数据。你要关心字段有什么、有没有可能为空。'
|
||||
}
|
||||
if (key === 'fail') {
|
||||
return '失败会怎样 = 你要能看懂失败原因,好给用户提示/重试。'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.game {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 0.8fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.doc {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.docBar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.dot.red {
|
||||
background: #ef4444;
|
||||
}
|
||||
.dot.yellow {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.dot.green {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.docTitle {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.docBody {
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.block {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.block:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.block.gray {
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.block.hit {
|
||||
border-color: #22c55e;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, #22c55e 18%, transparent);
|
||||
}
|
||||
|
||||
.blockK {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.blockV {
|
||||
margin-top: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.blockHint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.side {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.taskTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.taskList {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.taskItem {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.taskItem.done {
|
||||
border-color: #22c55e;
|
||||
background: color-mix(in srgb, #22c55e 12%, var(--vp-c-bg));
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.explain {
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.explainTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.explainText {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.win {
|
||||
border: 1px solid #22c55e;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, #22c55e 12%, var(--vp-c-bg));
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.winTitle {
|
||||
font-weight: 900;
|
||||
color: #166534;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.winText {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #166534;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.game {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,438 @@
|
||||
<!--
|
||||
ApiMethodDemo.vue
|
||||
参考 ide-intro 的“虚拟环境演示”风格。
|
||||
目标:把 GET/POST/DELETE 讲成“拿/加/删”三个按钮,并用可视化列表展示效果。
|
||||
注意:这是选读内容,但组件本身要足够好玩、足够直观。
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">三个按钮:拿(GET)/ 加(POST)/ 删(DELETE)</div>
|
||||
<div class="sub">你不用记英文。先玩:点按钮,看看“列表怎么变”。</div>
|
||||
</div>
|
||||
|
||||
<div class="board">
|
||||
<div class="left">
|
||||
<div class="panelTitle">小列表(服务器里有的东西)</div>
|
||||
<div class="list">
|
||||
<div v-if="items.length === 0" class="empty">空的</div>
|
||||
<div v-for="it in items" :key="it.id" class="row">
|
||||
<div class="pillId">#{{ it.id }}</div>
|
||||
<div class="name">{{ it.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mini">
|
||||
<span class="miniK">提示:</span>
|
||||
<span class="miniV">你可以一直点“拿(GET)”,列表不会变。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="panelTitle">按按钮(模拟 API)</div>
|
||||
|
||||
<div class="btnRow">
|
||||
<button class="btn get" :disabled="busy" @click="getList">
|
||||
拿(GET)
|
||||
</button>
|
||||
<button class="btn post" :disabled="busy" @click="addOne">
|
||||
加(POST)
|
||||
</button>
|
||||
<button class="btn del" :disabled="busy" @click="removeOne">
|
||||
删(DELETE)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inputs">
|
||||
<label class="field">
|
||||
<span class="k">要加什么</span>
|
||||
<input v-model="newName" class="input" placeholder="随便写个名字" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="result">
|
||||
<div class="resultTitle">返回结果</div>
|
||||
<div v-if="!last" class="muted">点一个按钮试试。</div>
|
||||
<div v-else class="resBox" :class="{ ok: last.ok, bad: !last.ok }">
|
||||
<div class="badge">{{ last.ok ? '成功' : '失败' }}</div>
|
||||
<div class="text">{{ last.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="foot">
|
||||
<div class="stat">
|
||||
<span class="statK">你点了</span>
|
||||
<span class="statV">{{ clicks }}</span>
|
||||
<span class="statK">次</span>
|
||||
</div>
|
||||
<button class="ghost" :disabled="busy" @click="reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="one">
|
||||
<div class="oneTitle">一句话总结</div>
|
||||
<div class="oneText">
|
||||
GET 通常只是“拿数据”;POST/DELETE 会“改数据”。所以网络抖动时,重试 POST
|
||||
要更小心。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const busy = ref(false)
|
||||
const clicks = ref(0)
|
||||
|
||||
const seq = ref(3)
|
||||
const items = ref([
|
||||
{ id: 1, name: 'Apple' },
|
||||
{ id: 2, name: 'Banana' },
|
||||
{ id: 3, name: 'Cookie' }
|
||||
])
|
||||
|
||||
const newName = ref('Donut')
|
||||
const last = ref(null)
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
clicks.value += 1
|
||||
busy.value = true
|
||||
await sleep(220)
|
||||
last.value = {
|
||||
ok: true,
|
||||
text: `拿到了 ${items.value.length} 条数据(列表不变)`
|
||||
}
|
||||
busy.value = false
|
||||
}
|
||||
|
||||
async function addOne() {
|
||||
clicks.value += 1
|
||||
busy.value = true
|
||||
await sleep(260)
|
||||
|
||||
const name = String(newName.value || '').trim()
|
||||
if (!name) {
|
||||
last.value = { ok: false, text: '你还没写“要加什么”' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
seq.value += 1
|
||||
items.value = [...items.value, { id: seq.value, name }]
|
||||
last.value = { ok: true, text: `已添加:#${seq.value} ${name}` }
|
||||
busy.value = false
|
||||
}
|
||||
|
||||
async function removeOne() {
|
||||
clicks.value += 1
|
||||
busy.value = true
|
||||
await sleep(240)
|
||||
|
||||
if (items.value.length === 0) {
|
||||
last.value = { ok: false, text: '已经空了,删不了' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const removed = items.value[items.value.length - 1]
|
||||
items.value = items.value.slice(0, -1)
|
||||
last.value = { ok: true, text: `已删除:#${removed.id} ${removed.name}` }
|
||||
busy.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
seq.value = 3
|
||||
items.value = [
|
||||
{ id: 1, name: 'Apple' },
|
||||
{ id: 2, name: 'Banana' },
|
||||
{ id: 3, name: 'Cookie' }
|
||||
]
|
||||
newName.value = 'Donut'
|
||||
last.value = null
|
||||
clicks.value = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.board {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.pillId {
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-2);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.mini {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.miniK {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.btnRow {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn.get {
|
||||
border-color: color-mix(in srgb, #60a5fa 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.btn.post {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.btn.del {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.k {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resultTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.resBox {
|
||||
margin-top: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resBox.ok {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.resBox.bad {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.foot {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.statV {
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ghost:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.one {
|
||||
margin-top: 12px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.oneTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.oneText {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.btnRow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,484 @@
|
||||
<!--
|
||||
ApiPlayground.vue
|
||||
参考 ide-intro 的“虚拟工具”风格:做一个极简 Postman。
|
||||
目标:可玩、可视化、少字段:
|
||||
- 选操作(拿/加/删)
|
||||
- 填一点(id 或 name)
|
||||
- 钥匙开关
|
||||
- 点发送 -> 看结果
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">练习场:一个“迷你 Postman”</div>
|
||||
<div class="sub">
|
||||
你不用懂代码。把它当成“按钮调试器”:按对了拿结果,按错了看失败原因。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app">
|
||||
<div class="topbar">
|
||||
<div class="brand">API Console</div>
|
||||
<div class="toggles">
|
||||
<button :class="['toggle', { on: keyOn }]" @click="keyOn = !keyOn">
|
||||
钥匙:{{ keyOn ? '有' : '没有' }}
|
||||
</button>
|
||||
<button class="toggle" @click="reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="left">
|
||||
<div class="panelTitle">① 选操作</div>
|
||||
<div class="ops">
|
||||
<button
|
||||
v-for="o in ops"
|
||||
:key="o.id"
|
||||
:class="['op', { active: opId === o.id }]"
|
||||
@click="setOp(o.id)"
|
||||
>
|
||||
<div class="opTitle">{{ o.label }}</div>
|
||||
<div class="opHint">{{ o.hint }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mid">
|
||||
<div class="panelTitle">② 填一点</div>
|
||||
<div class="form">
|
||||
<label class="row" v-if="opId !== 'list'">
|
||||
<span class="k">id</span>
|
||||
<input v-model="idText" class="input" placeholder="例如:u_123" />
|
||||
</label>
|
||||
<label class="row" v-if="opId === 'create'">
|
||||
<span class="k">name</span>
|
||||
<input
|
||||
v-model="nameText"
|
||||
class="input"
|
||||
placeholder="例如:Alice"
|
||||
/>
|
||||
</label>
|
||||
<div class="tip">
|
||||
玩法:
|
||||
<span class="mono">钥匙=没有</span> 再发送一次;或者连续点 4
|
||||
次触发“太频繁”。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="send" :disabled="busy" @click="send">
|
||||
{{ busy ? '发送中…' : '③ 发送(模拟)' }}
|
||||
</button>
|
||||
|
||||
<details class="details">
|
||||
<summary>(选看)这一次你“相当于”发了什么</summary>
|
||||
<pre class="code"><code>{{ pseudo }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="panelTitle">④ 返回结果</div>
|
||||
<div v-if="!res" class="muted">还没有结果。点“发送”。</div>
|
||||
<div v-else class="resBox" :class="{ ok: res.ok, bad: !res.ok }">
|
||||
<div class="badge">{{ res.ok ? '成功' : '失败' }}</div>
|
||||
<div class="resText">{{ res.text }}</div>
|
||||
<pre v-if="res.data" class="code"><code>{{ res.data }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const ops = [
|
||||
{ id: 'list', label: '拿列表(GET)', hint: '不改数据' },
|
||||
{ id: 'get', label: '拿一个(GET)', hint: '需要 id' },
|
||||
{ id: 'create', label: '加一个(POST)', hint: '需要 name' },
|
||||
{ id: 'delete', label: '删一个(DELETE)', hint: '需要 id' }
|
||||
]
|
||||
|
||||
const opId = ref('list')
|
||||
const keyOn = ref(true)
|
||||
const idText = ref('u_123')
|
||||
const nameText = ref('Alice')
|
||||
const busy = ref(false)
|
||||
const res = ref(null)
|
||||
const callTimes = ref([])
|
||||
|
||||
const pseudo = computed(() => {
|
||||
const key = keyOn.value ? "X-Api-Key: '****'" : '(没有钥匙)'
|
||||
if (opId.value === 'list') return `GET /v1/users\n${key}`
|
||||
if (opId.value === 'get')
|
||||
return `GET /v1/users/${idText.value || '{id}'}\n${key}`
|
||||
if (opId.value === 'create')
|
||||
return `POST /v1/users\n${key}\nBody: { name: '${nameText.value || ''}' }`
|
||||
return `DELETE /v1/users/${idText.value || '{id}'}\n${key}`
|
||||
})
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
function setOp(id) {
|
||||
opId.value = id
|
||||
res.value = null
|
||||
}
|
||||
|
||||
function reset() {
|
||||
opId.value = 'list'
|
||||
keyOn.value = true
|
||||
idText.value = 'u_123'
|
||||
nameText.value = 'Alice'
|
||||
res.value = null
|
||||
callTimes.value = []
|
||||
}
|
||||
|
||||
async function send() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
res.value = null
|
||||
|
||||
const now = Date.now()
|
||||
callTimes.value = callTimes.value.filter((t) => now - t < 1200)
|
||||
callTimes.value.push(now)
|
||||
if (callTimes.value.length >= 4) {
|
||||
await sleep(180)
|
||||
res.value = { ok: false, text: '太频繁了,请慢一点(模拟)' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
await sleep(300)
|
||||
|
||||
if (!keyOn.value) {
|
||||
res.value = { ok: false, text: '没有钥匙(没权限,模拟)' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (opId.value === 'list') {
|
||||
res.value = {
|
||||
ok: true,
|
||||
text: '拿到列表(模拟)',
|
||||
data: JSON.stringify(
|
||||
{
|
||||
data: [
|
||||
{ id: 'u_123', name: 'Alice' },
|
||||
{ id: 'u_124', name: 'Bob' }
|
||||
]
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (opId.value === 'get') {
|
||||
const id = String(idText.value || '').trim()
|
||||
if (!id) {
|
||||
res.value = { ok: false, text: '你还没填 id' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
if (id === 'u_404') {
|
||||
res.value = { ok: false, text: '找不到这个 id(模拟)' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
res.value = {
|
||||
ok: true,
|
||||
text: `拿到用户 ${id}(模拟)`,
|
||||
data: JSON.stringify({ id, name: 'Alice' }, null, 2)
|
||||
}
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (opId.value === 'create') {
|
||||
const name = String(nameText.value || '').trim()
|
||||
if (!name) {
|
||||
res.value = { ok: false, text: '你还没填 name' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
res.value = {
|
||||
ok: true,
|
||||
text: `创建成功(模拟)`,
|
||||
data: JSON.stringify({ id: 'u_789', name }, null, 2)
|
||||
}
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// delete
|
||||
const id = String(idText.value || '').trim()
|
||||
if (!id) {
|
||||
res.value = { ok: false, text: '你还没填 id' }
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
res.value = { ok: true, text: `删除成功:${id}(模拟)` }
|
||||
busy.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.app {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.toggles {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle.on {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 0.9fr 1fr 1.1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.left,
|
||||
.mid,
|
||||
.right {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.ops {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.op {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.op.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent);
|
||||
}
|
||||
|
||||
.opTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.opHint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.k {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.send {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
'Courier New', monospace;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 12px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.details summary {
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.resBox {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resBox.ok {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.resBox.bad {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.resText {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.code {
|
||||
margin: 8px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,755 @@
|
||||
<!--
|
||||
ApiQuickStartDemo.vue
|
||||
参考 ide-intro 的“虚拟 UI + 先玩再讲”风格。
|
||||
目标:0 基础读者能立刻理解 API = 按钮/入口:
|
||||
选择按钮 -> 填一点 -> 点一下 -> 看结果(成功/失败)。
|
||||
-->
|
||||
<template>
|
||||
<div class="machine">
|
||||
<div class="top">
|
||||
<div>
|
||||
<div class="title">先玩一下:把 API 当成一个“按钮机”</div>
|
||||
<div class="sub">你只管:选按钮、填信息、点一下。别背术语。</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span class="dot in" /> 你填的信息
|
||||
<span class="dot mid" /> “按钮”(API) <span class="dot out" /> 返回结果
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="screen">
|
||||
<div class="left">
|
||||
<div class="panelTitle">① 选一个按钮</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
v-for="b in buttons"
|
||||
:key="b.id"
|
||||
:class="['bigBtn', { active: currentId === b.id }]"
|
||||
@click="select(b.id)"
|
||||
>
|
||||
<div class="icon">{{ b.icon }}</div>
|
||||
<div class="label">{{ b.label }}</div>
|
||||
<div class="hint">{{ b.hint }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle">
|
||||
<div class="panelTitle">② 填一点信息</div>
|
||||
|
||||
<div class="form" v-if="currentId === 'date'">
|
||||
<label class="row">
|
||||
<span class="k">日期</span>
|
||||
<input
|
||||
v-model="form.dateText"
|
||||
class="input"
|
||||
placeholder="2026-01-19"
|
||||
/>
|
||||
</label>
|
||||
<div class="row">
|
||||
<span class="k">格式</span>
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="f in formats"
|
||||
:key="f"
|
||||
:class="['chip', { active: form.dateFormat === f }]"
|
||||
@click="form.dateFormat = f"
|
||||
>
|
||||
{{ f }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tip">玩法:把日期写错试试(比如 2026-99-99)。</div>
|
||||
</div>
|
||||
|
||||
<div class="form" v-else-if="currentId === 'ai'">
|
||||
<label class="row">
|
||||
<span class="k">问题</span>
|
||||
<textarea
|
||||
v-model="form.question"
|
||||
class="textarea"
|
||||
placeholder="例如:用一句话解释什么是 API"
|
||||
/>
|
||||
</label>
|
||||
<div class="tip">玩法:清空问题再点一下,看看会发生什么。</div>
|
||||
</div>
|
||||
|
||||
<div class="form" v-else>
|
||||
<div class="row">
|
||||
<span class="k">选择</span>
|
||||
<div class="chips">
|
||||
<button
|
||||
:class="['chip', { active: form.loginOk }]"
|
||||
@click="form.loginOk = true"
|
||||
>
|
||||
同意
|
||||
</button>
|
||||
<button
|
||||
:class="['chip', { active: !form.loginOk }]"
|
||||
@click="form.loginOk = false"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tip">玩法:选“取消”,再点一下。</div>
|
||||
</div>
|
||||
|
||||
<div class="callBar">
|
||||
<button class="callBtn" :disabled="busy" @click="call">
|
||||
{{ busy ? '执行中…' : '③ 点一下(调用)' }}
|
||||
</button>
|
||||
<button class="ghost" :disabled="busy" @click="resetScore">
|
||||
清零计分
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="score">
|
||||
<div class="scoreItem">
|
||||
<div class="scoreK">成功</div>
|
||||
<div class="scoreV">{{ score.ok }}</div>
|
||||
</div>
|
||||
<div class="scoreItem">
|
||||
<div class="scoreK">失败</div>
|
||||
<div class="scoreV">{{ score.bad }}</div>
|
||||
</div>
|
||||
<div class="scoreItem">
|
||||
<div class="scoreK">连续成功</div>
|
||||
<div class="scoreV">{{ score.streak }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="panelTitle">④ 看结果</div>
|
||||
|
||||
<div class="flow">
|
||||
<div class="node">
|
||||
<div class="nodeTop">
|
||||
<span class="dot in" />
|
||||
<span class="nodeTitle">你填的信息</span>
|
||||
</div>
|
||||
<div class="nodeBody">{{ requestPreview }}</div>
|
||||
</div>
|
||||
<div class="arrow" :class="{ go: animating }">→</div>
|
||||
<div class="node">
|
||||
<div class="nodeTop">
|
||||
<span class="dot mid" />
|
||||
<span class="nodeTitle">按钮(API)</span>
|
||||
</div>
|
||||
<div class="nodeBody">{{ currentHow }}</div>
|
||||
</div>
|
||||
<div class="arrow" :class="{ go: animating }">→</div>
|
||||
<div class="node">
|
||||
<div class="nodeTop">
|
||||
<span class="dot out" />
|
||||
<span class="nodeTitle">返回结果</span>
|
||||
</div>
|
||||
<div class="nodeBody">
|
||||
<div v-if="!result" class="muted">
|
||||
还没有结果。点一下“调用”试试。
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="resultBox"
|
||||
:class="{ ok: result.ok, bad: !result.ok }"
|
||||
>
|
||||
<div class="badge">{{ result.ok ? '成功' : '失败' }}</div>
|
||||
<div class="resultText">{{ result.text }}</div>
|
||||
<pre
|
||||
v-if="result.debug"
|
||||
class="code"
|
||||
><code>{{ result.debug }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="details">
|
||||
<summary>(选看)你不用管的细节</summary>
|
||||
<ul class="list">
|
||||
<li v-for="x in currentDetails" :key="x">{{ x }}</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: 'date',
|
||||
icon: '🗓️',
|
||||
label: '日期格式化',
|
||||
hint: '最简单:填日期 -> 得到结果',
|
||||
how: '调用一个日期格式化函数(本地)',
|
||||
details: ['内部怎么处理时区', '怎么优化性能', '怎么兼容不同语言环境']
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
icon: '🤖',
|
||||
label: '问 AI(模拟)',
|
||||
hint: '写一句话 -> 得到回答',
|
||||
how: '调用一个“问答入口”(可能是 SDK 或 HTTP)',
|
||||
details: ['模型怎么训练', '服务端怎么排队', '怎么限流/重试']
|
||||
},
|
||||
{
|
||||
id: 'login',
|
||||
icon: '🔑',
|
||||
label: '一键登录(模拟)',
|
||||
hint: '点同意/取消 -> 得到结果',
|
||||
how: '按登录流程走一遍(这里用模拟)',
|
||||
details: ['它怎么做安全校验', '登录凭证怎么生成', '怎么风控']
|
||||
}
|
||||
]
|
||||
|
||||
const formats = ['YYYY-MM-DD', 'YYYY/MM/DD', 'MM-DD']
|
||||
|
||||
const currentId = ref('date')
|
||||
const busy = ref(false)
|
||||
const animating = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
dateText: '2026-01-19',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
question: '用一句话解释什么是 API',
|
||||
loginOk: true
|
||||
})
|
||||
|
||||
const score = reactive({ ok: 0, bad: 0, streak: 0 })
|
||||
const result = ref(null)
|
||||
|
||||
const callTimes = ref([]) // for "too frequent"
|
||||
|
||||
const current = computed(
|
||||
() => buttons.find((b) => b.id === currentId.value) || buttons[0]
|
||||
)
|
||||
|
||||
const currentHow = computed(() => current.value.how)
|
||||
const currentDetails = computed(() => current.value.details || [])
|
||||
|
||||
const requestPreview = computed(() => {
|
||||
if (currentId.value === 'date') {
|
||||
return `日期=${form.dateText || '(空)'};格式=${form.dateFormat}`
|
||||
}
|
||||
if (currentId.value === 'ai') {
|
||||
return `问题=${(form.question || '').trim() || '(空)'}`
|
||||
}
|
||||
return `选择=${form.loginOk ? '同意' : '取消'}`
|
||||
})
|
||||
|
||||
function select(id) {
|
||||
currentId.value = id
|
||||
result.value = null
|
||||
}
|
||||
|
||||
function resetScore() {
|
||||
score.ok = 0
|
||||
score.bad = 0
|
||||
score.streak = 0
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
function fmtDate(d, fmt) {
|
||||
const y = d.getFullYear()
|
||||
const m = pad2(d.getMonth() + 1)
|
||||
const day = pad2(d.getDate())
|
||||
if (fmt === 'YYYY/MM/DD') return `${y}/${m}/${day}`
|
||||
if (fmt === 'MM-DD') return `${m}-${day}`
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
function record(ok) {
|
||||
if (ok) {
|
||||
score.ok += 1
|
||||
score.streak += 1
|
||||
} else {
|
||||
score.bad += 1
|
||||
score.streak = 0
|
||||
}
|
||||
}
|
||||
|
||||
async function call() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
animating.value = true
|
||||
|
||||
// simulate "too frequent"
|
||||
const now = Date.now()
|
||||
callTimes.value = callTimes.value.filter((t) => now - t < 1200)
|
||||
callTimes.value.push(now)
|
||||
if (callTimes.value.length >= 4) {
|
||||
await sleep(220)
|
||||
result.value = {
|
||||
ok: false,
|
||||
text: '太频繁了,请慢一点再试(模拟)',
|
||||
debug: '现实里:有些 API 会限制你“点太快”。'
|
||||
}
|
||||
record(false)
|
||||
animating.value = false
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
await sleep(380)
|
||||
|
||||
if (currentId.value === 'date') {
|
||||
const raw = String(form.dateText || '').trim()
|
||||
const d = new Date(raw)
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
result.value = {
|
||||
ok: false,
|
||||
text: '日期写错了(我看不懂)',
|
||||
debug: `输入:${raw}`
|
||||
}
|
||||
record(false)
|
||||
animating.value = false
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
const out = fmtDate(d, form.dateFormat)
|
||||
result.value = {
|
||||
ok: true,
|
||||
text: `结果:${out}`,
|
||||
debug: `你填的:${raw}\n你选的格式:${form.dateFormat}\n它给你的:${out}`
|
||||
}
|
||||
record(true)
|
||||
animating.value = false
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (currentId.value === 'ai') {
|
||||
const q = String(form.question || '').trim()
|
||||
if (!q) {
|
||||
result.value = { ok: false, text: '你还没写问题', debug: '' }
|
||||
record(false)
|
||||
animating.value = false
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
result.value = {
|
||||
ok: true,
|
||||
text: '回答:API 就是“别的软件给你用的按钮/入口”。',
|
||||
debug: `你的问题:${q}\n回答:API 就是“别的软件给你用的按钮/入口”。`
|
||||
}
|
||||
record(true)
|
||||
animating.value = false
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// login
|
||||
if (!form.loginOk) {
|
||||
result.value = { ok: false, text: '用户取消了登录(模拟)', debug: '' }
|
||||
record(false)
|
||||
animating.value = false
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
result.value = {
|
||||
ok: true,
|
||||
text: '登录成功:拿到用户信息(模拟)',
|
||||
debug: '用户:Alice\n状态:成功'
|
||||
}
|
||||
record(true)
|
||||
animating.value = false
|
||||
busy.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.machine {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.dot.in {
|
||||
background: color-mix(in srgb, #60a5fa 40%, var(--vp-c-bg));
|
||||
}
|
||||
|
||||
.dot.mid {
|
||||
background: color-mix(in srgb, var(--vp-c-brand-1) 35%, var(--vp-c-bg));
|
||||
}
|
||||
|
||||
.dot.out {
|
||||
background: color-mix(in srgb, #22c55e 30%, var(--vp-c-bg));
|
||||
}
|
||||
|
||||
.screen {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr 1fr 1.2fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
font-weight: 800;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.left,
|
||||
.middle,
|
||||
.right {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bigBtn {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bigBtn:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.bigBtn.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 6px;
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.k {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
min-height: 78px;
|
||||
resize: vertical;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent);
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.callBar {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.callBtn {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.callBtn:disabled,
|
||||
.ghost:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.score {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.scoreItem {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.scoreK {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.scoreV {
|
||||
margin-top: 4px;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.flow {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.node {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nodeTop {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.nodeTitle {
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.nodeBody {
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.arrow.go {
|
||||
transform: translateX(6px);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.muted {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.resultBox {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resultBox.ok {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.resultBox.bad {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.resultText {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.code {
|
||||
margin: 8px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 12px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.details summary {
|
||||
cursor: pointer;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 10px 0 0;
|
||||
padding-left: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.top {
|
||||
flex-direction: column;
|
||||
}
|
||||
.screen {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.score {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,251 @@
|
||||
<!--
|
||||
RealWorldApiDemo.vue
|
||||
参考 ide-intro 的“虚拟 UI + 一眼看懂”的风格。
|
||||
目标:解释“为什么 SDK 的调用也叫 API”:
|
||||
- 左边:SDK 按钮(你调用函数)
|
||||
- 右边:HTTP 按钮(你发请求)
|
||||
两个按钮得到的结果一样:只是包装方式不同。
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">SDK vs HTTP:其实都是“按按钮拿结果”</div>
|
||||
<div class="sub">
|
||||
你可以把它理解成:<b>同一个按钮</b>,只是有两种“按法”。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="cardTitle">方式 A:SDK 按钮(调用函数)</div>
|
||||
<div class="cardBody">
|
||||
<button class="call sdk" :disabled="busy" @click="call('sdk')">
|
||||
{{ busy ? '执行中…' : '按一下(SDK)' }}
|
||||
</button>
|
||||
<div class="muted">你写的通常是:<b>client.users.get(...)</b></div>
|
||||
<details class="details">
|
||||
<summary>(选看)长什么样</summary>
|
||||
<pre class="code"><code>{{ sdkCode }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="cardTitle">方式 B:HTTP 按钮(发请求)</div>
|
||||
<div class="cardBody">
|
||||
<button class="call http" :disabled="busy" @click="call('http')">
|
||||
{{ busy ? '执行中…' : '按一下(HTTP)' }}
|
||||
</button>
|
||||
<div class="muted">你写的通常是:<b>fetch(url, headers)</b></div>
|
||||
<details class="details">
|
||||
<summary>(选看)长什么样</summary>
|
||||
<pre class="code"><code>{{ httpCode }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="cardTitle">结果(两边一样)</div>
|
||||
<div class="cardBody">
|
||||
<div v-if="!res" class="muted">先按一下左边或右边的按钮。</div>
|
||||
<div v-else class="resBox">
|
||||
<div class="badge">返回</div>
|
||||
<div class="resText">{{ res }}</div>
|
||||
</div>
|
||||
<div class="tip">
|
||||
结论:<b>SDK 的“函数入口”也叫 API</b
|
||||
>,因为它也是对外公开的“怎么用”。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const busy = ref(false)
|
||||
const res = ref('')
|
||||
|
||||
const sdkCode = computed(
|
||||
() => `import { AcmeClient } from 'acme-sdk'
|
||||
|
||||
const client = new AcmeClient({ apiKey: '****' })
|
||||
const user = await client.users.get({ id: 'u_123' })
|
||||
console.log(user)`
|
||||
)
|
||||
|
||||
const httpCode = computed(
|
||||
() => `const res = await fetch('https://api.acme.com/v1/users/u_123', {
|
||||
headers: { 'X-Api-Key': '****' }
|
||||
})
|
||||
const user = await res.json()
|
||||
console.log(user)`
|
||||
)
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function call(which) {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
res.value = ''
|
||||
await sleep(280)
|
||||
res.value =
|
||||
which === 'sdk'
|
||||
? '用户:{ id: "u_123", name: "Alice" }(模拟)'
|
||||
: '用户:{ id: "u_123", name: "Alice" }(模拟)'
|
||||
busy.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.grid {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.cardBody {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.call {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
padding: 12px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.call.sdk {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.call.http {
|
||||
border-color: color-mix(in srgb, #60a5fa 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.call:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.muted {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.details {
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.details summary {
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.code {
|
||||
margin: 10px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.resBox {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.resText {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,403 @@
|
||||
<!--
|
||||
RequestResponseFlow.vue
|
||||
参考 ide-intro 的“可视化演示”风格:更像一个小动画,而不是表单。
|
||||
目标:让新手理解一次 API 调用:你发过去 -> 对方处理 -> 回给你。
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<div class="title">一次 API 调用,会发生什么?</div>
|
||||
<div class="sub">点一下“发送”,看小纸飞机飞过去再飞回来。</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="ctrl">
|
||||
<div class="ctrlK">地址</div>
|
||||
<div class="ctrlV">
|
||||
<button :class="['pill', { active: addrOk }]" @click="addrOk = true">
|
||||
正确
|
||||
</button>
|
||||
<button
|
||||
:class="['pill', { active: !addrOk }]"
|
||||
@click="addrOk = false"
|
||||
>
|
||||
错误
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl">
|
||||
<div class="ctrlK">钥匙</div>
|
||||
<div class="ctrlV">
|
||||
<button :class="['pill', { active: keyOk }]" @click="keyOk = true">
|
||||
有
|
||||
</button>
|
||||
<button :class="['pill', { active: !keyOk }]" @click="keyOk = false">
|
||||
没有
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="send" :disabled="busy" @click="send">
|
||||
{{ busy ? '飞行中…' : '发送一次(模拟)' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
<div class="side">
|
||||
<div class="window">
|
||||
<div class="bar">
|
||||
<span class="dot red" />
|
||||
<span class="dot yellow" />
|
||||
<span class="dot green" />
|
||||
<span class="barText">你这边(你的程序)</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="bubble mine">
|
||||
我想按按钮拿结果。
|
||||
<div class="small">
|
||||
地址:{{ addrOk ? '正确' : '错误' }};钥匙:{{
|
||||
keyOk ? '有' : '没有'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mid">
|
||||
<div class="line" />
|
||||
<div class="plane" :class="{ go: flying }">✈︎</div>
|
||||
<div class="line" />
|
||||
</div>
|
||||
|
||||
<div class="side">
|
||||
<div class="window">
|
||||
<div class="bar">
|
||||
<span class="dot red" />
|
||||
<span class="dot yellow" />
|
||||
<span class="dot green" />
|
||||
<span class="barText">对方那边(提供能力)</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="bubble theirs">
|
||||
{{ serverText }}
|
||||
<div class="small">(它按规则检查:地址/钥匙/参数…)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result">
|
||||
<div class="resultTitle">返回结果</div>
|
||||
<div v-if="!response" class="muted">还没有结果。点一下“发送一次”。</div>
|
||||
<div
|
||||
v-else
|
||||
class="resBox"
|
||||
:class="{ ok: response.ok, bad: !response.ok }"
|
||||
>
|
||||
<div class="badge">{{ response.ok ? '成功' : '失败' }}</div>
|
||||
<div class="resText">{{ response.text }}</div>
|
||||
</div>
|
||||
<div class="tip">
|
||||
玩法:把“地址”改成错误,或者把“钥匙”改成没有,再发送一次。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const addrOk = ref(true)
|
||||
const keyOk = ref(true)
|
||||
const busy = ref(false)
|
||||
const flying = ref(false)
|
||||
const response = ref(null)
|
||||
|
||||
const serverText = computed(() => {
|
||||
if (!addrOk.value) return '我找不到这个按钮(地址错了)'
|
||||
if (!keyOk.value) return '你没有钥匙,我不能给你结果'
|
||||
return '收到!我去处理一下…'
|
||||
})
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function send() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
flying.value = true
|
||||
response.value = null
|
||||
|
||||
await sleep(600)
|
||||
flying.value = false
|
||||
|
||||
await sleep(260)
|
||||
if (!addrOk.value) {
|
||||
response.value = { ok: false, text: '失败:地址不对(找不到这个按钮)' }
|
||||
} else if (!keyOk.value) {
|
||||
response.value = { ok: false, text: '失败:没有钥匙(没权限)' }
|
||||
} else {
|
||||
response.value = { ok: true, text: '成功:拿到结果(模拟)' }
|
||||
}
|
||||
busy.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.ctrl {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.ctrlK {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.ctrlV {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pill {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pill.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent);
|
||||
}
|
||||
|
||||
.send {
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stage {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.window {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 14px;
|
||||
background: var(--vp-c-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background: #ef4444;
|
||||
}
|
||||
.dot.yellow {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.dot.green {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.barText {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 12px;
|
||||
min-height: 92px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.bubble.mine {
|
||||
border-color: color-mix(in srgb, #60a5fa 40%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.bubble.theirs {
|
||||
border-color: color-mix(in srgb, #a78bfa 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.small {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mid {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 99px;
|
||||
}
|
||||
|
||||
.plane {
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-text-3);
|
||||
transition:
|
||||
transform 600ms ease,
|
||||
color 200ms ease;
|
||||
}
|
||||
|
||||
.plane.go {
|
||||
transform: translateX(28px);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.resultTitle {
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.resBox {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.resBox.ok {
|
||||
border-color: color-mix(in srgb, #22c55e 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.resBox.bad {
|
||||
border-color: color-mix(in srgb, #ef4444 45%, var(--vp-c-divider));
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.resText {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.stage {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.mid {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user