feat: comprehensive documentation and demo updates

- Update READMEs and docs across multiple languages
- Enhance interactive demos for Agent, LLM, VLM, Audio, Image Gen, Terminal, and Web Basics
- Add new appendix sections for Database and IDE intros
- Update VitePress config, theme, and utility scripts
- Clean up unused assets and components
This commit is contained in:
sanbuphy
2026-01-16 19:10:21 +08:00
parent c8567ce23f
commit 73f4788d7e
150 changed files with 19530 additions and 13401 deletions
@@ -0,0 +1,277 @@
<!--
CdnCacheDemo.vue
CDN 加速原理快递柜隐喻
-->
<template>
<div class="cdn">
<div class="header">
<div class="title">CDN 加速演示</div>
<div class="subtitle">就像在小区楼下装了个丰巢快递柜</div>
</div>
<div class="controls">
<div class="control">
<label>你要取什么东西(资源类型)</label>
<div class="chips">
<button
v-for="r in resourceTypes"
:key="r.id"
:class="['chip', { active: r.id === resourceType }]"
@click="resourceType = r.id"
>
{{ r.label }}
</button>
</div>
</div>
<div class="control">
<label>快递柜里有吗(命中率)</label>
<input type="range" min="0" max="100" v-model.number="hit" />
<div class="hint">当前概率{{ hit }}% ({{ hit > 80 ? '大部分都有' : '经常要跑远路' }})</div>
</div>
</div>
<div class="summary">
<div class="card">
<div class="label">跑总仓库的次数 (回源)</div>
<div class="value">{{ miss }}%</div>
<div class="note">次数越少总仓库越轻松</div>
</div>
<div class="card">
<div class="label">省下的路费 (带宽节省)</div>
<div class="value">{{ saved }}%</div>
<div class="note">省到就是赚到</div>
</div>
<div class="card">
<div class="label">老司机的建议</div>
<div class="value">{{ cacheAdvice }}</div>
</div>
</div>
<div class="flow">
<div class="step" v-for="(s, idx) in flow" :key="idx">
<div class="head">
<span class="dot" :style="{ background: s.color }"></span>
<span class="name">{{ s.name }}</span>
<span class="time">{{ s.time }}</span>
</div>
<div class="desc">{{ s.desc }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const resourceTypes = [
{ id: 'static', label: '标准件 (图片/CSS/JS)' },
{ id: 'html', label: '信件 (HTML)' },
{ id: 'api', label: '生鲜 (API数据)' }
]
const resourceType = ref('static')
const hit = ref(85)
const miss = computed(() => 100 - hit.value)
const saved = computed(() => hit.value)
const cacheAdvice = computed(() => {
if (resourceType.value === 'static')
return '标准件保质期长,建议放柜子里一年 (max-age=1年)'
if (resourceType.value === 'html')
return '信件可能随时更新,每次取之前问一下 (no-cache)'
return '生鲜容易坏,不要放柜子,直接去产地拿 (no-store)'
})
const flow = computed(() => {
const base = [
{
name: '用户 🙋‍♂️',
time: '0ms',
desc: '我想取个包裹',
color: '#6366f1'
},
{
name: '家门口快递柜 📦',
time: '15ms',
desc: '看看柜子里有没有...',
color: '#6366f1'
}
]
if (hit.value >= 70 && resourceType.value === 'static') {
base.push({
name: '有货!✅',
time: '+5ms',
desc: '直接拿走,不用跑远路',
color: '#22c55e'
})
} else {
base.push({
name: '没货... ❌',
time: '+10ms',
desc: '柜子是空的,得去总仓库',
color: '#f59e0b'
})
base.push({
name: '总仓库 (源站) 🏭',
time: resourceType.value === 'api' ? '+60ms' : '+40ms',
desc: '翻山越岭把货取回来',
color: '#e11d48'
})
if (resourceType.value !== 'api') {
base.push({
name: '顺手存柜子',
time: '+8ms',
desc: '下次邻居来拿就不用跑了',
color: '#22c55e'
})
}
}
base.push({
name: '拿到手 🎁',
time: 'Total',
desc: '交易完成',
color: '#0ea5e9'
})
return base
})
</script>
<style scoped>
.cdn {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.header .title {
font-weight: 800;
font-size: 18px;
}
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.control {
display: flex;
flex-direction: column;
gap: 8px;
}
label {
font-weight: 600;
font-size: 14px;
}
.chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
padding: 6px 12px;
border-radius: 999px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.chip.active {
border-color: var(--vp-c-brand);
color: white;
background: var(--vp-c-brand);
}
.hint {
color: var(--vp-c-text-2);
font-size: 12px;
margin-top: 4px;
}
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.card {
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 12px;
}
.label {
color: var(--vp-c-text-2);
font-size: 12px;
}
.value {
font-weight: 800;
margin-top: 4px;
font-size: 16px;
}
.note {
color: var(--vp-c-text-2);
font-size: 12px;
margin-top: 4px;
}
.flow {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.step {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 12px;
}
.head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.name {
font-weight: 700;
font-size: 14px;
}
.time {
margin-left: auto;
color: var(--vp-c-text-2);
font-size: 12px;
}
.desc {
color: var(--vp-c-text-2);
font-size: 12px;
}
</style>
@@ -0,0 +1,292 @@
<!--
CicdPipelineDemo.vue
CI/CD 流水线自动炒菜机隐喻
-->
<template>
<div class="cicd">
<div class="header">
<div>
<div class="title">自动化流水线 (CI/CD)</div>
<div class="subtitle">就像一台全自动炒菜机</div>
</div>
<div class="actions">
<label class="fail-toggle"><input type="checkbox" v-model="failTest" /> 混入一颗烂菜 (模拟报错)</label>
<button :disabled="running" @click="run" class="run-btn">
{{ running ? '机器运转中...' : '开始做菜 (触发构建)' }}
</button>
</div>
</div>
<div class="steps">
<div class="step" v-for="step in steps" :key="step.id">
<div class="step-head">
<span class="badge" :class="step.status">{{
statusIcon(step.status)
}}</span>
<span class="name">{{ step.name }}</span>
</div>
<div class="analogy">{{ step.analogy }}</div>
<div class="desc">{{ step.desc }}</div>
</div>
</div>
<div class="log" v-if="log">
<div class="log-title">🖥 机器日志</div>
<pre><code>{{ log }}</code></pre>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const steps = ref([
{
id: 'install',
name: '安装依赖 (Install)',
analogy: '🥬 准备食材',
desc: 'npm install',
status: 'idle'
},
{
id: 'test',
name: '自动测试 (Test)',
analogy: '🔍 食品安检',
desc: 'npm test',
status: 'idle'
},
{
id: 'build',
name: '打包构建 (Build)',
analogy: '🍳 下锅烹饪',
desc: 'npm run build',
status: 'idle'
},
{
id: 'deploy',
name: '自动部署 (Deploy)',
analogy: '🍽️ 端上桌',
desc: 'pm2 restart',
status: 'idle'
}
])
const running = ref(false)
const failTest = ref(false)
const log = ref('')
const wait = (ms) => new Promise((r) => setTimeout(r, ms))
const statusIcon = (status) => {
if (status === 'done') return '✔'
if (status === 'running') return '⏳'
if (status === 'fail') return '✖'
return '•'
}
const reset = () => {
steps.value = steps.value.map((s) => ({ ...s, status: 'idle' }))
log.value = ''
}
const run = async () => {
if (running.value) return
running.value = true
reset()
const timeline = [
{
id: 'install',
ms: 1000,
log: '> 正在去菜市场买菜...\n> 成功买到 842 个包裹 (node_modules)'
},
{
id: 'test',
ms: 800,
log: '> 正在检查食材新鲜度...\n> 单元测试运行中...'
},
{
id: 'build',
ms: 1200,
log: '> 开始烹饪...\n> 正在压缩混淆代码...\n> 产出 dist/ 目录 (一盘好菜)'
},
{
id: 'deploy',
ms: 1000,
log: '> 正在把菜端给顾客...\n> 重启服务器...\n> 上线成功!'
}
]
for (const item of timeline) {
const step = steps.value.find((s) => s.id === item.id)
step.status = 'running'
log.value = item.log
await wait(item.ms)
if (item.id === 'test' && failTest.value) {
step.status = 'fail'
log.value = '❌ 警告:发现一颗烂白菜!(测试失败)\n❌ 立即停机,防止端给顾客。'
steps.value
.filter((s) => s.id !== 'test')
.forEach((s) => (s.status = 'idle'))
running.value = false
return
}
step.status = 'done'
}
log.value = '✅ 流程结束:大家吃得很开心 (服务正常运行)'
running.value = false
}
</script>
<style scoped>
.cicd {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.header {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.title {
font-weight: 800;
font-size: 18px;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
}
.actions {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.fail-toggle {
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.run-btn {
background: var(--vp-c-brand);
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: opacity 0.2s;
}
.run-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.step {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.step-head {
display: flex;
align-items: center;
gap: 8px;
}
.badge {
width: 24px;
height: 24px;
border-radius: 50%;
display: grid;
place-items: center;
border: 2px solid var(--vp-c-divider);
font-size: 12px;
}
.badge.running {
border-color: #f59e0b;
color: #f59e0b;
}
.badge.done {
border-color: #22c55e;
color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
.badge.fail {
border-color: #ef4444;
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.name {
font-weight: 700;
font-size: 14px;
}
.analogy {
color: var(--vp-c-brand);
font-size: 13px;
font-weight: 600;
margin-left: 32px;
}
.desc {
color: var(--vp-c-text-2);
font-size: 12px;
margin-left: 32px;
font-family: var(--vp-font-family-mono);
}
.log {
background: #1e1e20;
border-radius: 10px;
padding: 12px;
color: #eee;
font-family: var(--vp-font-family-mono);
font-size: 13px;
}
.log-title {
font-weight: 700;
margin-bottom: 8px;
color: #aaa;
font-size: 12px;
text-transform: uppercase;
}
pre {
margin: 0;
white-space: pre-wrap;
line-height: 1.6;
}
</style>
@@ -0,0 +1,308 @@
<!--
DeploymentArchitecture.vue
全景图快递配送隐喻
-->
<template>
<div class="arch">
<div class="header">
<div>
<div class="title">全景演示一个请求的奇幻漂流</div>
<div class="subtitle">
点击下方按钮看看三种模式的配送路线有什么不同
</div>
</div>
<div class="modes">
<button
v-for="mode in modes"
:key="mode.id"
:class="['mode', { active: mode.id === currentMode }]"
@click="currentMode = mode.id"
>
<span class="icon">{{ mode.icon }}</span>
{{ mode.label }}
</button>
</div>
</div>
<div class="flow">
<div
v-for="(node, idx) in nodes"
:key="node.name"
class="node"
:style="{ borderColor: node.color }"
>
<div class="node-head">
<div class="dot" :style="{ background: node.color }">
{{ node.icon }}
</div>
<div class="name-box">
<div class="role">{{ node.role }}</div>
<div class="tech-name">{{ node.name }}</div>
</div>
</div>
<div class="desc">{{ node.desc }}</div>
<div v-if="idx < nodes.length - 1" class="arrow"></div>
</div>
</div>
<div class="summary">
<div class="metric">
<div class="label">当前场景</div>
<div class="value">{{ currentModeLabel }}</div>
</div>
<div class="metric">
<div class="label">瓶颈环节</div>
<div class="value">{{ bottleneck }}</div>
</div>
<div class="metric">
<div class="label">通俗解释</div>
<div class="value">{{ advice }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const modes = [
{ id: 'static', label: '看海报 (静态)', icon: '🖼️' },
{ id: 'spa', label: '玩 App (SPA)', icon: '📱' },
{ id: 'ssr', label: '刷动态 (SSR)', icon: '🔄' },
]
const currentMode = ref('spa')
const currentModeLabel = computed(() =>
modes.find(m => m.id === currentMode.value)?.label
)
// 角色:User(寄件人), DNS(查号台), CDN(快递柜), WAF(保安), LB(大堂经理), Server(办事员), DB(档案室)
const commonNodes = {
user: { role: '寄件人', name: 'User', icon: '🧑', color: '#64748b', desc: '发出请求' },
dns: { role: '查号台', name: 'DNS', icon: '📒', color: '#0ea5e9', desc: '查询 IP 地址' },
cdn: { role: '快递柜', name: 'CDN', icon: '📦', color: '#22c55e', desc: '就近取货' },
waf: { role: '保安', name: 'WAF', icon: '🛡️', color: '#ef4444', desc: '拦截黑客' },
lb: { role: '大堂经理', name: 'LB', icon: '💁', color: '#f59e0b', desc: '分配窗口' },
server: { role: '办事员', name: 'Server', icon: '👨‍💼', color: '#8b5cf6', desc: '处理业务' },
db: { role: '档案室', name: 'Database', icon: '🗄️', color: '#d946ef', desc: '存取数据' },
obj: { role: '仓库', name: 'OSS', icon: '🏭', color: '#f97316', desc: '拿静态文件' }
}
const flowMap = {
static: [
{ ...commonNodes.user, desc: '想看一张图片' },
{ ...commonNodes.dns, desc: '找到图片仓库地址' },
{ ...commonNodes.cdn, desc: '家门口就有?直接拿走!' },
{ ...commonNodes.obj, desc: '没有?去总仓库拿' }
],
spa: [
{ ...commonNodes.user, desc: '打开网页 App' },
{ ...commonNodes.dns, desc: '找到服务器地址' },
{ ...commonNodes.cdn, desc: '先拿网页外壳 (HTML/JS)' },
{ ...commonNodes.server, desc: '再拿动态数据 (API)' },
{ ...commonNodes.db, desc: '查用户数据' }
],
ssr: [
{ ...commonNodes.user, desc: '打开复杂网页' },
{ ...commonNodes.dns, desc: '找到服务器地址' },
{ ...commonNodes.lb, desc: '人多排队,你以此去 2 号窗口' },
{ ...commonNodes.server, desc: '现场拼装好整个页面' },
{ ...commonNodes.db, desc: '查所有需要的数据' }
]
}
const nodes = computed(() => flowMap[currentMode.value])
const bottleneck = computed(() => {
switch (currentMode.value) {
case 'static': return '几乎没有瓶颈,起飞!'
case 'spa': return 'API 接口响应速度'
case 'ssr': return '办事员 (Server) 拼装页面的速度'
default: return ''
}
})
const advice = computed(() => {
switch (currentMode.value) {
case 'static':
return '这是最简单的模式。就像去看公告栏的海报(或者发传单),内容印死在上面了,所有人看到的都一样。速度最快!'
case 'spa':
return '就像送你一套乐高积木。先给你个空盒子和图纸(网页壳子),你的浏览器自己在本地把页面拼出来。拼好后怎么玩都快。'
case 'ssr':
return '就像点了一份热披萨。厨师(服务器)必须现场烤好,再热乎乎地送给你。虽然慢点,但保证新鲜、不仅能吃(能看)还能闻到香味(SEO友好)。'
default:
return ''
}
})
</script>
<style scoped>
.arch {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 20px;
}
.header {
display: flex;
flex-direction: column;
gap: 16px;
}
@media (min-width: 640px) {
.header {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
.title {
font-weight: 800;
font-size: 18px;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
margin-top: 4px;
}
.modes {
display: flex;
gap: 8px;
background: var(--vp-c-bg);
padding: 4px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.mode {
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
transition: all 0.2s;
white-space: nowrap;
}
.mode:hover {
background: var(--vp-c-bg-soft);
}
.mode.active {
background: var(--vp-c-brand);
color: white;
}
.flow {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 16px;
align-items: start;
}
.node {
position: relative;
background: var(--vp-c-bg);
border: 2px solid transparent;
border-radius: 12px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
}
.node:hover {
transform: translateY(-2px);
}
.node-head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.dot {
width: 36px;
height: 36px;
border-radius: 10px;
color: #fff;
display: grid;
place-items: center;
font-size: 20px;
flex-shrink: 0;
}
.name-box {
line-height: 1.2;
}
.role {
font-size: 12px;
color: var(--vp-c-text-2);
}
.tech-name {
font-weight: 700;
font-size: 14px;
}
.desc {
color: var(--vp-c-text-2);
font-size: 12px;
line-height: 1.4;
}
.arrow {
position: absolute;
right: -14px;
top: 50%;
transform: translateY(-50%);
color: var(--vp-c-divider);
font-size: 18px;
display: none;
}
@media (min-width: 768px) {
.arrow {
display: block;
}
}
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
background: var(--vp-c-bg);
border-radius: 12px;
padding: 16px;
border: 1px solid var(--vp-c-divider);
}
.metric {
display: flex;
flex-direction: column;
gap: 4px;
}
.label {
color: var(--vp-c-text-2);
font-size: 12px;
}
.value {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,216 @@
<!--
DnsFlowDemo.vue
DNS 记录操练台地址簿隐喻
-->
<template>
<div class="dns">
<div class="header">
<div class="title">DNS 查号台</div>
<div class="subtitle">好记的名字变成机器的 IP</div>
</div>
<div class="controls">
<div class="field">
<label>你要配哪个域名</label>
<input v-model="domain" placeholder="例如:baidu.com" />
</div>
<div class="field">
<label>你要做什么(记录类型)</label>
<div class="chips">
<button
v-for="r in recordTypes"
:key="r.type"
:class="['chip', { active: recordType === r.type }]"
@click="recordType = r.type"
>
{{ r.desc }}
</button>
</div>
</div>
</div>
<div class="card">
<div class="row">
<span>类型 (Type)</span>
<code class="highlight">{{ recordType }}</code>
</div>
<div class="row">
<span>前缀 (Host)</span>
<code>{{ hostLabel }}</code>
</div>
<div class="row">
<span>目标 (Value)</span>
<code>{{ recordValue }}</code>
</div>
<div class="row">
<span>记忆时间 (TTL)</span>
<code>{{ ttlSuggestion }}</code>
</div>
<div class="human-speak">
<span class="emoji">💡</span>
<div class="text">
<strong>人话解释</strong>
{{ humanExplanation }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const domain = ref('my-site.com')
const recordType = ref('A')
const recordTypes = [
{
type: 'A',
desc: '直接指向 IP',
value: '1.2.3.4',
explanation: '告诉查号台:我家住在“1.2.3.4”这个门牌号。最常用!',
ttl: '600s (10分钟)'
},
{
type: 'CNAME',
desc: '指向别名',
value: 'shops.myshopify.com',
explanation: '告诉查号台:我搬家了,你去问问“shops.myshopify.com”我在哪。',
ttl: '600s (10分钟)'
},
{
type: 'MX',
desc: '配置邮箱',
value: 'mxbiz1.qq.com',
explanation: '告诉邮递员:寄给我的信,请送到“mxbiz1.qq.com”这个邮局去。',
ttl: '3600s (1小时)'
}
]
const currentRecord = computed(() => recordTypes.find(r => r.type === recordType.value))
const hostLabel = computed(() => (recordType.value === 'CNAME' ? 'www' : '@'))
const recordValue = computed(() => currentRecord.value?.value || '')
const ttlSuggestion = computed(() => currentRecord.value?.ttl || '600s')
const humanExplanation = computed(() => currentRecord.value?.explanation || '')
</script>
<style scoped>
.dns {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.header .title {
font-weight: 800;
font-size: 18px;
}
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
}
.controls {
display: grid;
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 8px;
}
label {
font-weight: 600;
font-size: 14px;
}
input {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 10px;
background: var(--vp-c-bg);
width: 100%;
}
.chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
border: 1px solid var(--vp-c-divider);
border-radius: 999px;
padding: 6px 14px;
background: var(--vp-c-bg);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.chip.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
}
.row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
align-items: center;
}
.row span {
color: var(--vp-c-text-2);
}
code {
background: var(--vp-c-bg-soft);
padding: 4px 8px;
border-radius: 6px;
font-family: var(--vp-font-family-mono);
font-size: 13px;
}
.highlight {
color: var(--vp-c-brand);
font-weight: bold;
}
.human-speak {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed var(--vp-c-divider);
display: flex;
gap: 10px;
}
.emoji {
font-size: 20px;
}
.text {
font-size: 14px;
line-height: 1.5;
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,208 @@
<!--
HttpsNginxDemo.vue
Nginx 配置生成器极简版
-->
<template>
<div class="https">
<div class="header">
<div class="title">Nginx 配置生成器</div>
<div class="subtitle">复制粘贴就能用自动配置 HTTPS</div>
</div>
<div class="options">
<div
class="option"
:class="{ active: mode === 'static' }"
@click="mode = 'static'"
>
<span class="icon">📄</span>
<span>静态网站</span>
</div>
<div
class="option"
:class="{ active: mode === 'proxy' }"
@click="mode = 'proxy'"
>
<span class="icon">🔄</span>
<span>反向代理 (Node/Python)</span>
</div>
</div>
<div class="code-box">
<div class="code-header">
<span>/etc/nginx/sites-available/default</span>
<button class="copy-btn" @click="copy">{{ copied ? '已复制' : '复制' }}</button>
</div>
<pre><code>{{ snippet }}</code></pre>
</div>
<div class="tips">
<div class="tip-item">
<span class="emoji">🔑</span>
<div>
<strong>开启 HTTPS 神器</strong>
<div class="cmd">sudo certbot --nginx</div>
<div class="desc">运行这行命令它会自动修改上面的配置帮你加上 SSL 证书</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const mode = ref('proxy')
const copied = ref(false)
const snippet = computed(() => {
if (mode.value === 'static') {
return `server {
listen 80;
server_name example.com; # 改成你的域名
# 静态文件在哪里?
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}`
} else {
return `server {
listen 80;
server_name example.com; # 改成你的域名
location / {
# 把请求转发给 3000 端口的程序
proxy_pass http://127.0.0.1:3000;
# 告诉后端真实的客户 IP 是多少
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}`
}
})
function copy() {
navigator.clipboard.writeText(snippet.value)
copied.value = true
setTimeout(() => copied.value = false, 2000)
}
</script>
<style scoped>
.https {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
}
.header {
margin-bottom: 16px;
}
.header .title {
font-weight: 800;
font-size: 18px;
}
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
}
.options {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.option {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.2s;
}
.option.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.code-box {
background: #1e1e20;
border-radius: 8px;
overflow: hidden;
color: #fff;
font-family: var(--vp-font-family-mono);
font-size: 13px;
margin-bottom: 16px;
}
.code-header {
background: #2d2d30;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #aaa;
}
.copy-btn {
color: #fff;
background: rgba(255,255,255,0.1);
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
}
pre {
padding: 16px;
margin: 0;
overflow-x: auto;
line-height: 1.5;
}
.tips {
display: flex;
flex-direction: column;
gap: 10px;
}
.tip-item {
display: flex;
gap: 10px;
background: var(--vp-c-bg);
padding: 12px;
border-radius: 8px;
border: 1px dashed var(--vp-c-divider);
}
.cmd {
background: #1e1e20;
color: #22c55e;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
margin: 4px 0;
font-family: var(--vp-font-family-mono);
font-size: 13px;
}
.desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
</style>
@@ -0,0 +1,259 @@
<!--
ObservabilityBackupDemo.vue
监控与备份买保险隐喻
-->
<template>
<div class="obs">
<div class="header">
<div class="title">监控与备份</div>
<div class="subtitle">给你的网站买份保险</div>
</div>
<div class="controls">
<div class="control">
<label>安保级别 (监控)</label>
<select v-model="monitorLevel">
<option value="lite">入门只装个摄像头 (日志)</option>
<option value="std">标准雇个保安 (指标+告警)</option>
<option value="pro">专业24h 安保中心 (全链路追踪)</option>
</select>
</div>
<div class="control">
<label>通知谁(告警渠道)</label>
<div class="chips">
<button
v-for="c in channels"
:key="c.id"
:class="['chip', { active: channel === c.id }]"
@click="channel = c.id"
>
{{ c.label }}
</button>
</div>
</div>
<div class="control">
<label>备份频率 (小时)</label>
<input
type="range"
min="6"
max="48"
step="6"
v-model.number="backupHours"
/>
<div class="hint"> {{ backupHours }} 小时存一次盘</div>
</div>
</div>
<div class="grid">
<div class="card">
<div class="label">安全评分</div>
<div
class="value big"
:class="{
green: riskScore <= 40,
orange: riskScore > 40 && riskScore <= 70,
red: riskScore > 70
}"
>
{{ 100 - riskScore }}
</div>
<div class="note">{{ riskScore > 70 ? '极其危险!' : (riskScore > 40 ? '勉强及格' : '非常稳!') }}</div>
</div>
<div class="card">
<div class="label">最坏情况 (丢数据)</div>
<div class="value">{{ rpo }}</div>
<div class="note">最多丢失多少小时的数据</div>
</div>
<div class="card">
<div class="label">恢复速度</div>
<div class="value">{{ rto }}</div>
<div class="note">出事后多久能修好</div>
</div>
</div>
<div class="checklist-box">
<div class="box-title">保命清单 (Checklist)</div>
<div class="checks">
<div class="check" v-for="item in checklist" :key="item.label">
<input type="checkbox" v-model="item.done" />
<span>{{ item.label }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
const monitorLevel = ref('std')
const channels = [
{ id: 'email', label: '发邮件 (慢)' },
{ id: 'chat', label: '企业微信/飞书 (快)' },
{ id: 'pager', label: '电话轰炸 (急)' }
]
const channel = ref('chat')
const backupHours = ref(24)
const checklist = reactive([
{ label: '日志能不能查到?', done: true },
{ label: 'CPU 飙高了会不会报警?', done: false },
{ label: '关键接口慢了知不知道?', done: false },
{ label: '数据库有没有自动备份?', done: true },
{ label: '备份文件真的能恢复吗?(演练过)', done: false }
])
const riskScore = computed(() => {
let score = 70
if (monitorLevel.value === 'pro') score -= 20
else if (monitorLevel.value === 'std') score -= 10
if (channel.value === 'pager') score -= 10
else if (channel.value === 'chat') score -= 5
score -= Math.min(20, (48 - backupHours.value) * 0.8)
const doneCount = checklist.filter((i) => i.done).length
score -= doneCount * 4
return Math.max(0, Math.min(100, Math.round(score)))
})
const rpo = computed(() => `${backupHours.value} 小时`)
const rto = computed(() => {
if (monitorLevel.value === 'pro') return '15-30 分钟'
if (monitorLevel.value === 'std') return '30-60 分钟'
return '1-2 小时 (甚至更久)'
})
</script>
<style scoped>
.obs {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.header .title {
font-weight: 800;
font-size: 18px;
}
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.control {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
label {
font-weight: 700;
font-size: 14px;
}
select,
input[type='range'] {
width: 100%;
}
.hint {
color: var(--vp-c-text-2);
font-size: 12px;
margin-top: 4px;
}
.chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
padding: 6px 12px;
border-radius: 999px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.chip.active {
border-color: var(--vp-c-brand);
color: white;
background: var(--vp-c-brand);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.card {
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
}
.label {
color: var(--vp-c-text-2);
font-size: 13px;
}
.value {
font-weight: 800;
margin-top: 4px;
font-size: 15px;
}
.value.big {
font-size: 24px;
}
.value.green {
color: #22c55e;
}
.value.orange {
color: #f59e0b;
}
.value.red {
color: #ef4444;
}
.note {
color: var(--vp-c-text-2);
font-size: 12px;
margin-top: 4px;
}
.checklist-box {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
}
.box-title {
font-weight: 700;
margin-bottom: 12px;
font-size: 14px;
}
.checks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.check {
display: flex;
gap: 8px;
align-items: center;
font-size: 13px;
}
</style>
@@ -0,0 +1,193 @@
<!--
RollbackSwitchDemo.vue
发布策略如何不关门装修
-->
<template>
<div class="roll">
<div class="header">
<div class="title">发布策略对比</div>
<div class="subtitle">网站升级就像店铺装修怎么才能不影响做生意</div>
</div>
<div class="tabs">
<button
v-for="s in strategies"
:key="s.id"
:class="['tab', { active: current === s.id }]"
@click="current = s.id"
>
<span class="emoji">{{ s.emoji }}</span>
{{ s.label }}
</button>
</div>
<div class="stats">
<div class="card">
<div class="label">操作方式</div>
<div class="value">{{ flow }}</div>
</div>
<div class="card">
<div class="label">后悔药 (回滚时间)</div>
<div class="value">{{ rollbackTime }}</div>
</div>
<div class="card">
<div class="label">成本代价</div>
<div class="value">{{ cost }}</div>
</div>
</div>
<div class="analogy-section">
<div class="col">
<div class="section-title">🧐 通俗理解</div>
<div class="analogy-text">{{ analogy }}</div>
</div>
<div class="col">
<div class="section-title"> 风险点</div>
<div class="risk-text">{{ risk }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const strategies = [
{ id: 'rolling', label: '滚动更新 (Rolling)', emoji: '🔄' },
{ id: 'blue', label: '蓝绿发布 (Blue/Green)', emoji: '🔵🟢' },
{ id: 'canary', label: '金丝雀发布 (Canary)', emoji: '🐦' }
]
const current = ref('rolling')
const flow = computed(() => {
if (current.value === 'rolling') return '分批替换'
if (current.value === 'blue') return '全量切换'
return '按比例慢慢切'
})
const rollbackTime = computed(() => {
if (current.value === 'rolling') return '慢 (3-10 分钟)'
if (current.value === 'blue') return '极快 (秒级)'
return '快 (秒级)'
})
const cost = computed(() => {
if (current.value === 'rolling') return '低 (资源利用率高)'
if (current.value === 'blue') return '高 (需要双倍机器)'
return '中 (需要复杂网关)'
})
const analogy = computed(() => {
switch (current.value) {
case 'rolling':
return '就像餐厅换桌布。不关门,一桌一桌换。客人来了坐新桌子,还没换好的桌子先空着。'
case 'blue':
return '有钱任性。在隔壁新开一家一模一样的店。装修好了,直接把大门指路牌改到新店。'
case 'canary':
return '先让 VIP 客户去新包间体验一下。如果 VIP 没投诉,再把所有客人都请进去。'
default: return ''
}
})
const risk = computed(() => {
if (current.value === 'rolling')
return '中间状态比较乱,有的客人看到新装修,有的看到旧装修。'
if (current.value === 'blue') return '太贵了!'
return '技术要求高,得有能识别 VIP 的“门童” (流量网关)。'
})
</script>
<style scoped>
.roll {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.header .title {
font-weight: 800;
font-size: 18px;
}
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
}
.tabs {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.tab {
padding: 8px 16px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.2s;
}
.tab.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.card {
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 12px;
}
.label {
color: var(--vp-c-text-2);
font-size: 12px;
}
.value {
font-weight: 800;
margin-top: 4px;
font-size: 15px;
}
.analogy-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.col {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
}
.section-title {
font-weight: 700;
margin-bottom: 8px;
font-size: 14px;
}
.analogy-text {
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.risk-text {
font-size: 14px;
line-height: 1.6;
color: #ef4444;
}
</style>
@@ -0,0 +1,237 @@
<!--
ServerSizerDemo.vue
服务器选购指南租房隐喻
-->
<template>
<div class="sizer">
<div class="header">
<div class="title">服务器选购指南</div>
<div class="subtitle">不花冤枉钱够用就好</div>
</div>
<div class="controls">
<div class="control">
<label>你的业务规模</label>
<div class="range-box">
<input type="range" min="0" max="3" step="1" v-model.number="scaleIndex" />
<div class="scale-labels">
<span>个人博客</span>
<span>初创官网</span>
<span>小型 App</span>
<span>中型平台</span>
</div>
</div>
</div>
</div>
<div class="result-card">
<div class="main-rec">
<div class="icon">{{ recommendation.icon }}</div>
<div class="details">
<div class="rec-title">{{ recommendation.title }}</div>
<div class="rec-spec">{{ recommendation.spec }}</div>
</div>
</div>
<div class="analogy-box">
<p><strong>🤔 通俗理解</strong></p>
<p>{{ recommendation.analogy }}</p>
</div>
<div class="specs-grid">
<div class="spec-item">
<span class="label">带宽 (水管)</span>
<span class="val">{{ recommendation.bandwidth }}</span>
</div>
<div class="spec-item">
<span class="label">预算估算</span>
<span class="val">{{ recommendation.cost }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const scaleIndex = ref(1)
const levels = [
{
title: '入门级 (Entry)',
spec: '1核 1G ~ 2G',
icon: '🚲',
bandwidth: '1 Mbps',
cost: '¥50~99 / 年 (活动价)',
analogy: '就像租了个单间。放个博客、跑个脚本完全够用。人多了会挤不动。'
},
{
title: '标准级 (Standard)',
spec: '2核 4G',
icon: '🚗',
bandwidth: '3~5 Mbps',
cost: '¥300 / 年',
analogy: '就像租了个两居室。正经跑个公司官网、小程序后端没问题。大多数人的首选。'
},
{
title: '专业级 (Pro)',
spec: '4核 8G',
icon: '🏎️',
bandwidth: '5~10 Mbps',
cost: '¥1000+ / 年',
analogy: '就像租了个大平层办公室。能抗住几千人同时在线,跑复杂的计算任务也不虚。'
},
{
title: '企业级 (Enterprise)',
spec: '8核 16G + 负载均衡',
icon: '✈️',
bandwidth: '按量付费',
cost: '¥5000+ / 年',
analogy: '这已经不是租房了,是包下了一整层楼。通常需要多台机器配合,专人维护。'
}
]
const recommendation = computed(() => levels[scaleIndex.value])
</script>
<style scoped>
.sizer {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
}
.header {
margin-bottom: 20px;
}
.header .title {
font-weight: 800;
font-size: 18px;
}
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
}
.controls {
margin-bottom: 20px;
}
.control label {
font-weight: 600;
font-size: 14px;
display: block;
margin-bottom: 10px;
}
.range-box {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
input[type='range'] {
width: 100%;
margin-bottom: 10px;
}
.scale-labels {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--vp-c-text-2);
}
.result-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-brand);
border-radius: 12px;
padding: 20px;
position: relative;
overflow: hidden;
}
.result-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--vp-c-brand);
}
.main-rec {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.icon {
font-size: 40px;
background: var(--vp-c-bg-soft);
width: 64px;
height: 64px;
border-radius: 12px;
display: grid;
place-items: center;
}
.rec-title {
font-weight: 800;
font-size: 18px;
color: var(--vp-c-brand);
}
.rec-spec {
font-size: 14px;
color: var(--vp-c-text-1);
margin-top: 4px;
}
.analogy-box {
background: var(--vp-c-bg-soft);
padding: 12px;
border-radius: 8px;
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
}
.analogy-box p {
margin: 0;
}
.analogy-box strong {
color: var(--vp-c-text-1);
}
.specs-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 16px;
}
.spec-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.spec-item .label {
font-size: 12px;
color: var(--vp-c-text-2);
}
.spec-item .val {
font-weight: 600;
font-size: 14px;
}
</style>