Files
test-repo/docs/.vitepress/theme/components/appendix/api-intro/ApiConceptDemo.vue
T
2026-01-19 23:45:08 +08:00

448 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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>