refactor(docs): simplify deployment demo components

This commit is contained in:
sanbuphy
2026-02-14 01:00:26 +08:00
parent d174ceea32
commit f9c4ae9320
25 changed files with 819 additions and 8613 deletions
@@ -1,279 +0,0 @@
<!--
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>
@@ -1,296 +0,0 @@
<!--
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>
@@ -1,360 +0,0 @@
<!--
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>
@@ -1,524 +0,0 @@
<script setup>
import { ref } from 'vue'
const backupType = ref('full')
const isBackingUp = ref(false)
const backupProgress = ref(0)
const lastBackup = ref('2024-01-15 14:30')
const backups = ref([
{ id: 1, type: 'full', date: '2024-01-15 14:30', size: '2.3 GB', status: 'completed' },
{ id: 2, type: 'incremental', date: '2024-01-15 10:00', size: '156 MB', status: 'completed' },
{ id: 3, type: 'full', date: '2024-01-14 14:30', size: '2.2 GB', status: 'completed' }
])
const backupTypes = [
{ id: 'full', name: '全量备份', desc: '备份所有数据,像拍整套照片', icon: '📸' },
{ id: 'incremental', name: '增量备份', desc: '只备份新增/修改的部分', icon: '📝' },
{ id: 'differential', name: '差异备份', desc: '备份自上次全量后的变化', icon: '🔄' }
]
const startBackup = () => {
isBackingUp.value = true
backupProgress.value = 0
const interval = setInterval(() => {
backupProgress.value += 5
if (backupProgress.value >= 100) {
clearInterval(interval)
isBackingUp.value = false
lastBackup.value = new Date().toLocaleString()
backups.value.unshift({
id: Date.now(),
type: backupType.value,
date: lastBackup.value,
size: backupType.value === 'full' ? '2.4 GB' : '180 MB',
status: 'completed'
})
}
}, 200)
}
</script>
<template>
<div class="deployment-backup">
<div class="demo-header">
<h3>备份演示</h3>
<p class="subtitle">数据安全就像保险柜</p>
</div>
<div class="intro-text">
<p>
就像小明每天<strong>记账</strong><strong>保留发票</strong>定期<strong>盘点库存</strong>
数据备份是防止数据丢失的最后一道防线服务器故障人为误操作都可能丢失数据
</p>
</div>
<div class="demo-content">
<!-- 备份类型选择 -->
<div class="backup-type-section">
<div class="section-title">💾 选择备份类型</div>
<div class="type-cards">
<div
v-for="type in backupTypes"
:key="type.id"
class="type-card"
:class="{ active: backupType === type.id }"
@click="backupType = type.id"
>
<span class="type-icon">{{ type.icon }}</span>
<span class="type-name">{{ type.name }}</span>
<span class="type-desc">{{ type.desc }}</span>
</div>
</div>
</div>
<!-- 备份操作 -->
<div class="backup-action">
<div class="action-info">
<div class="info-row">
<span class="info-label">上次备份</span>
<span class="info-value">{{ lastBackup }}</span>
</div>
<div class="info-row">
<span class="info-label">备份总数</span>
<span class="info-value">{{ backups.length }} </span>
</div>
</div>
<button
v-if="!isBackingUp"
@click="startBackup"
class="btn primary"
>
🚀 开始备份
</button>
<button v-else class="btn" disabled>
备份中...
</button>
</div>
<!-- 备份进度 -->
<div v-if="isBackingUp" class="backup-progress">
<div class="progress-header">
<span class="progress-label">正在备份...</span>
<span class="progress-percent">{{ backupProgress }}%</span>
</div>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${backupProgress}%` }"
></div>
</div>
</div>
<!-- 备份历史 -->
<div class="backup-history">
<div class="section-title">📜 备份历史</div>
<div class="history-list">
<div
v-for="backup in backups"
:key="backup.id"
class="history-item"
>
<div class="backup-icon">
{{ backup.type === 'full' ? '📦' : '📄' }}
</div>
<div class="backup-info">
<div class="backup-type">
{{ backup.type === 'full' ? '全量备份' : backup.type === 'incremental' ? '增量备份' : '差异备份' }}
</div>
<div class="backup-date">{{ backup.date }}</div>
</div>
<div class="backup-meta">
<div class="backup-size">{{ backup.size }}</div>
<div class="backup-status">
<span class="status-dot completed"></span>
<span class="status-text">成功</span>
</div>
</div>
</div>
</div>
</div>
<!-- 备份策略 -->
<div class="backup-strategy">
<div class="strategy-title">🎯 推荐备份策略 (3-2-1 原则)</div>
<div class="strategy-content">
<div class="strategy-item">
<div class="strategy-number">3</div>
<div class="strategy-desc">至少保留 <strong>3 </strong>备份</div>
</div>
<div class="strategy-item">
<div class="strategy-number">2</div>
<div class="strategy-desc">使用 <strong>2 </strong>不同存储介质</div>
</div>
<div class="strategy-item">
<div class="strategy-number">1</div>
<div class="strategy-desc"><strong>1 </strong>异地备份</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
💡 <strong>小明教训</strong>曾经因系统崩溃丢了所有销售数据现在每天自动备份
<strong>备份不是是否需要的问题而是何时需要的问题</strong>
</p>
</div>
</div>
</template>
<style scoped>
.deployment-backup {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.75rem;
}
.backup-type-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.type-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
}
.type-card {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.type-card:hover {
border-color: var(--vp-c-brand-soft);
}
.type-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.type-icon {
font-size: 1.5rem;
}
.type-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.type-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.backup-action {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.action-info {
display: flex;
gap: 1.5rem;
}
.info-row {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.info-value {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.btn {
padding: 0.6rem 1.25rem;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.3s ease;
}
.btn.primary {
background: var(--vp-c-brand);
color: white;
}
.btn.primary:hover {
background: var(--vp-c-brand-1);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.backup-progress {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.progress-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.progress-percent {
font-size: 1.1rem;
font-weight: 700;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
}
.progress-bar {
height: 10px;
background: var(--vp-c-bg-alt);
border-radius: 5px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
transition: width 0.3s ease;
}
.backup-history {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.history-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 250px;
overflow-y: auto;
}
.history-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.backup-icon {
font-size: 1.5rem;
}
.backup-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.backup-type {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.backup-date {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.backup-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
}
.backup-size {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
}
.backup-status {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.status-dot.completed {
background: var(--vp-c-brand-delta);
}
.backup-strategy {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-left: 3px solid var(--vp-c-brand);
}
.strategy-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
}
.strategy-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1rem;
}
.strategy-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
text-align: center;
}
.strategy-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: 700;
}
.strategy-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.backup-action {
flex-direction: column;
}
.type-cards {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,519 +1,192 @@
<script setup>
import { ref, computed } from 'vue'
const files = ref([
{ name: 'App.vue', size: '5KB', status: 'pending' },
{ name: 'main.js', size: '2KB', status: 'pending' },
{ name: 'utils.js', size: '8KB', status: 'pending' },
{ name: 'style.css', size: '15KB', status: 'pending' }
])
const buildProgress = ref(0)
const buildStatus = ref('idle') // idle, building, completed
const optimizedSize = ref('0KB')
const originalSize = ref('30KB')
const startBuild = () => {
buildStatus.value = 'building'
buildProgress.value = 0
files.value.forEach(f => f.status = 'pending')
// 模拟构建过程
const steps = [
{ progress: 20, file: 0 },
{ progress: 40, file: 1 },
{ progress: 60, file: 2 },
{ progress: 80, file: 3 },
{ progress: 100, file: -1 }
]
steps.forEach((step, idx) => {
setTimeout(() => {
buildProgress.value = step.progress
if (step.file >= 0) {
files.value[step.file].status = 'completed'
}
if (idx === steps.length - 1) {
buildStatus.value = 'completed'
optimizedSize.value = '12KB'
}
}, (idx + 1) * 600)
})
}
const resetBuild = () => {
buildStatus.value = 'idle'
buildProgress.value = 0
files.value.forEach(f => f.status = 'pending')
optimizedSize.value = '0KB'
}
</script>
<!--
DeploymentBuildDemo.vue
构建过程演示原材料变成品简化版
-->
<template>
<div class="deployment-build">
<div class="demo-header">
<h3>构建过程演示</h3>
<p class="subtitle">把原材料变成成品的过程</p>
<div class="header">
<span class="icon">📦</span>
<span class="title">代码构建</span>
</div>
<div class="intro-text">
<p>
就像小明制作咖啡前要<strong>研磨咖啡豆</strong><strong>调配比例</strong><strong>加热融合</strong>
代码构建也需要<strong>编译</strong><strong>压缩</strong><strong>优化</strong>才能变成可以部署的成品
</p>
</div>
<div class="demo-content">
<!-- 源文件列表 -->
<div class="source-files">
<div class="section-title">📁 源代码文件</div>
<div class="file-list">
<div
v-for="(file, idx) in files"
:key="file.name"
class="file-item"
:class="{ completed: file.status === 'completed' }"
>
<div class="file-icon">📄</div>
<div class="file-info">
<div class="file-name">{{ file.name }}</div>
<div class="file-size">{{ file.size }}</div>
</div>
<div class="file-status">
<span v-if="file.status === 'completed'" class="status-badge success"> 已处理</span>
<span v-else class="status-badge pending">待处理</span>
</div>
</div>
<div class="content">
<div class="flow">
<div class="step" :class="{ done: buildProgress >= 25 }">
<span class="num">1</span>
<span class="text">解析依赖</span>
</div>
<span class="arrow"></span>
<div class="step" :class="{ done: buildProgress >= 50 }">
<span class="num">2</span>
<span class="text">编译转换</span>
</div>
<span class="arrow"></span>
<div class="step" :class="{ done: buildProgress >= 75 }">
<span class="num">3</span>
<span class="text">打包压缩</span>
</div>
<span class="arrow"></span>
<div class="step" :class="{ done: buildProgress >= 100 }">
<span class="num">4</span>
<span class="text">完成</span>
</div>
</div>
<!-- 构建流程 -->
<div class="build-process">
<div class="section-title"> 构建流程</div>
<div class="pipeline">
<div class="pipeline-step">
<div class="step-icon">1</div>
<div class="step-content">
<div class="step-title">解析依赖</div>
<div class="step-desc">分析文件导入关系</div>
</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-step">
<div class="step-icon">2</div>
<div class="step-content">
<div class="step-title">编译转换</div>
<div class="step-desc">Vue JavaScript</div>
</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-step">
<div class="step-icon">3</div>
<div class="step-content">
<div class="step-title">打包压缩</div>
<div class="step-desc">合并文件去除空格</div>
</div>
</div>
<div class="progress">
<div class="bar">
<div class="fill" :style="{ width: `${buildProgress}%` }"></div>
</div>
<div class="percent">{{ buildProgress }}%</div>
</div>
<!-- 构建进度 -->
<div class="build-status">
<div class="status-header">
<span class="status-label">构建进度</span>
<span class="status-percent">{{ buildProgress }}%</span>
</div>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${buildProgress}%` }"
></div>
</div>
<!-- 对比展示 -->
<div v-if="buildStatus === 'completed'" class="size-comparison">
<div class="size-item original">
<div class="size-label">原始大小</div>
<div class="size-value">{{ originalSize }}</div>
</div>
<div class="size-arrow"></div>
<div class="size-item optimized">
<div class="size-label">优化后</div>
<div class="size-value success">{{ optimizedSize }}</div>
</div>
<div class="compression-badge">
压缩 60%
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button
v-if="buildStatus === 'idle' || buildStatus === 'completed'"
@click="startBuild"
class="btn primary"
>
{{ buildStatus === 'idle' ? '开始构建' : '重新构建' }}
</button>
<button
v-if="buildStatus === 'completed'"
@click="resetBuild"
class="btn secondary"
>
重置
</button>
</div>
</div>
<div class="info-box">
<p v-if="buildStatus === 'idle'">
💡 <strong>待机中</strong>点击"开始构建"按钮看看代码是如何一步步变成可部署的文件的
</p>
<p v-else-if="buildStatus === 'building'">
<strong>构建中</strong>正在处理源文件就像小明在准备咖啡材料...
</p>
<p v-else class="success-text">
<strong>构建完成</strong>所有文件已打包压缩体积减少了60%就像把咖啡豆研磨成粉更易冲泡
</p>
<button @click="startBuild" class="build-btn" :disabled="building">
{{ building ? '构建中...' : '▶ 开始构建' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const building = ref(false)
const buildProgress = ref(0)
const startBuild = () => {
if (building.value) return
building.value = true
buildProgress.value = 0
const interval = setInterval(() => {
buildProgress.value += 5
if (buildProgress.value >= 100) {
clearInterval(interval)
building.value = false
setTimeout(() => {
buildProgress.value = 0
}, 2000)
}
}, 150)
}
</script>
<style scoped>
.deployment-build {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
background: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
.header {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.source-files {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
.header .icon {
font-size: 1.25rem;
}
.file-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.file-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
transition: all 0.3s ease;
}
.file-item.completed {
border-color: var(--vp-c-brand-delta);
background: var(--vp-c-brand-dimm);
}
.file-icon {
font-size: 1.5rem;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.file-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.file-size {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.file-status {
display: flex;
align-items: center;
}
.status-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.status-badge.pending {
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-3);
}
.status-badge.success {
background: var(--vp-c-brand-delta);
color: white;
}
.build-process {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.pipeline {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.pipeline-step {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
width: 100%;
max-width: 350px;
}
.step-icon {
font-size: 1.5rem;
}
.step-content {
flex: 1;
}
.step-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.step-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.pipeline-arrow {
font-size: 1.5rem;
color: var(--vp-c-brand);
margin: 0.25rem 0;
}
.build-status {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.status-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.status-percent {
font-size: 1.1rem;
.header .title {
font-weight: 700;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
font-size: 1rem;
color: var(--vp-c-text-1);
}
.progress-bar {
height: 10px;
background: var(--vp-c-bg-alt);
border-radius: 5px;
overflow: hidden;
margin-bottom: 1rem;
.content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
transition: width 0.6s ease;
border-radius: 5px;
}
.size-comparison {
.flow {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
position: relative;
gap: 0.5rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 8px;
overflow-x: auto;
}
.size-item {
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.size-label {
font-size: 0.8rem;
text-align: center;
font-size: 0.75rem;
color: var(--vp-c-text-2);
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
min-width: 60px;
}
.size-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.size-value.success {
.step.done {
background: var(--vp-c-brand-dimm);
color: var(--vp-c-brand-delta);
}
.size-arrow {
font-size: 1.2rem;
.step .num {
font-weight: 700;
font-size: 0.9rem;
}
.arrow {
font-size: 1rem;
color: var(--vp-c-text-3);
flex-shrink: 0;
}
.progress {
display: flex;
align-items: center;
gap: 0.75rem;
}
.bar {
flex: 1;
height: 8px;
background: var(--vp-c-bg-alt);
border-radius: 4px;
overflow: hidden;
}
.fill {
height: 100%;
background: var(--vp-c-brand);
transition: width 0.3s ease;
}
.percent {
font-weight: 700;
font-size: 0.9rem;
color: var(--vp-c-brand);
}
.compression-badge {
position: absolute;
top: -0.5rem;
right: 1rem;
.build-btn {
width: 100%;
padding: 0.75rem;
background: var(--vp-c-brand);
color: white;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.action-buttons {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.3s ease;
transition: background 0.2s ease;
}
.btn.primary {
background: var(--vp-c-brand);
color: white;
}
.btn.primary:hover {
.build-btn:hover:not(:disabled) {
background: var(--vp-c-brand-1);
transform: translateY(-2px);
}
.btn.secondary {
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.btn.secondary:hover {
border-color: var(--vp-c-brand);
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
.success-text {
color: var(--vp-c-brand-delta);
}
@media (max-width: 640px) {
.file-item {
padding: 0.5rem;
}
.size-comparison {
flex-direction: column;
gap: 0.75rem;
}
.compression-badge {
position: static;
}
.build-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
@@ -1,414 +0,0 @@
<template>
<div class="deployment-cdn-demo">
<div class="demo-header">
<span class="icon">🌍</span>
<span class="title">CDN 加速原理</span>
<span class="subtitle">把资源送到用户家门口</span>
</div>
<div class="intro-text">
<strong>CDN</strong>Content Delivery Network就像在全世界开了连锁仓库用户访问时从<strong>最近的仓库</strong>取货速度超快
</div>
<div class="demo-content">
<div class="world-map">
<div class="server-origin">
<span class="server-icon">🏠</span>
<span class="server-label">源站服务器<br/>(北京)</span>
</div>
<div class="cdn-nodes">
<div
v-for="node in cdnNodes"
:key="node.id"
class="cdn-node"
:class="{ active: activeNode === node.id }"
@click="selectNode(node)"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-label">{{ node.city }}</span>
<span class="node-time">{{ node.time }}</span>
</div>
</div>
<div class="user-requests">
<div
v-for="user in users"
:key="user.id"
class="user-request"
:class="{ active: activeUser === user.id }"
@click="selectUser(user)"
>
<span class="user-icon">{{ user.icon }}</span>
<span class="user-label">{{ user.location }}</span>
<span class="user-arrow"></span>
<span class="user-target">{{ user.cdnNode }}</span>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title"> 性能对比</div>
<table>
<thead>
<tr>
<th>用户位置</th>
<th>不使用 CDN</th>
<th>使用 CDN</th>
<th>提速</th>
</tr>
</thead>
<tbody>
<tr v-for="row in speedData" :key="row.location">
<td>{{ row.location }}</td>
<td class="slow">{{ row.withoutCdn }}</td>
<td class="fast">{{ row.withCdn }}</td>
<td class="speedup">{{ row.speedup }}</td>
</tr>
</tbody>
</table>
</div>
<div class="benefits">
<div class="benefit-title"> CDN 的好处</div>
<div class="benefit-list">
<div class="benefit-item">
<span class="benefit-icon">🚀</span>
<span class="benefit-text">访问速度提升 50-80%</span>
</div>
<div class="benefit-item">
<span class="benefit-icon">💰</span>
<span class="benefit-text">节省源站带宽成本</span>
</div>
<div class="benefit-item">
<span class="benefit-icon">🛡</span>
<span class="benefit-text">DDoS 防护能力</span>
</div>
<div class="benefit-item">
<span class="benefit-icon">📱</span>
<span class="benefit-text">全球覆盖无死角</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>适用场景</strong>静态资源图片CSSJS最适合上 CDN动态数据还是走源站
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeNode = ref(null)
const activeUser = ref(null)
const cdnNodes = [
{ id: 'beijing', city: '北京', icon: '🏙️', time: '10ms' },
{ id: 'shanghai', city: '上海', icon: '🏙️', time: '15ms' },
{ id: 'tokyo', city: '东京', icon: '🗼', time: '20ms' },
{ id: 'london', city: '伦敦', icon: '🎡', time: '25ms' },
{ id: 'newyork', city: '纽约', icon: '🗽', time: '18ms' }
]
const users = [
{ id: 'user1', location: '北京用户', cdnNode: '北京节点', icon: '👤' },
{ id: 'user2', location: '纽约用户', cdnNode: '纽约节点', icon: '👤' },
{ id: 'user3', location: '伦敦用户', cdnNode: '伦敦节点', icon: '👤' }
]
const speedData = [
{ location: '北京', withoutCdn: '10ms', withCdn: '10ms', speedup: '-' },
{ location: '上海', withoutCdn: '30ms', withCdn: '15ms', speedup: '50%' },
{ location: '纽约', withoutCdn: '200ms', withCdn: '18ms', speedup: '91%' },
{ location: '伦敦', withoutCdn: '180ms', withCdn: '25ms', speedup: '86%' }
]
const selectNode = (node) => {
activeNode.value = node.id
activeUser.value = null
}
const selectUser = (user) => {
activeUser.value = user.id
activeNode.value = null
}
</script>
<style scoped>
.deployment-cdn-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text strong {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
margin-bottom: 1rem;
}
.world-map {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.server-origin {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--vp-c-brand-soft), var(--vp-c-bg));
border-radius: 8px;
border: 2px solid var(--vp-c-brand);
}
.server-icon {
font-size: 2rem;
}
.server-label {
font-size: 0.85rem;
text-align: center;
color: var(--vp-c-text-1);
font-weight: 600;
}
.cdn-nodes {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1rem;
}
.cdn-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
flex: 1;
min-width: 80px;
}
.cdn-node:hover,
.cdn-node.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
transform: translateY(-2px);
}
.node-icon {
font-size: 1.5rem;
}
.node-label {
font-size: 0.75rem;
text-align: center;
color: var(--vp-c-text-1);
font-weight: 500;
}
.node-time {
font-size: 0.7rem;
color: var(--vp-c-brand);
font-weight: 600;
}
.user-requests {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.user-request {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
}
.user-request:hover,
.user-request.active {
background: var(--vp-c-brand-soft);
}
.user-icon {
font-size: 1rem;
}
.user-label {
color: var(--vp-c-text-1);
flex: 1;
}
.user-arrow {
color: var(--vp-c-text-3);
}
.user-target {
color: var(--vp-c-brand);
font-weight: 600;
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.table-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
thead {
background: var(--vp-c-bg-soft);
}
th {
padding: 0.5rem;
text-align: left;
font-weight: 600;
color: var(--vp-c-text-1);
border-bottom: 2px solid var(--vp-c-divider);
}
td {
padding: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
td.slow {
color: #dc3545;
font-weight: 500;
}
td.fast {
color: #28a745;
font-weight: 500;
}
td.speedup {
color: var(--vp-c-brand);
font-weight: 600;
}
.benefits {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
}
.benefit-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.benefit-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.75rem;
}
.benefit-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.benefit-icon {
font-size: 1.25rem;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 0.25rem;
}
@media (max-width: 768px) {
.benefit-list {
grid-template-columns: 1fr;
}
.cdn-nodes {
justify-content: center;
}
}
</style>
@@ -1,474 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
const checklist = ref([
{ id: 1, category: '代码', task: '代码已通过测试', checked: true, critical: true },
{ id: 2, category: '代码', task: '移除调试代码和 console.log', checked: false, critical: false },
{ id: 3, category: '环境', task: '生产环境配置已设置', checked: true, critical: true },
{ id: 4, category: '环境', task: '数据库迁移脚本已准备', checked: false, critical: true },
{ id: 5, category: '环境', task: '环境变量已配置', checked: true, critical: true },
{ id: 6, category: '安全', task: '敏感信息已从代码中移除', checked: true, critical: true },
{ id: 7, category: '安全', task: 'HTTPS 证书已配置', checked: false, critical: true },
{ id: 8, category: '安全', task: '防火墙规则已设置', checked: true, critical: false },
{ id: 9, category: '性能', task: '静态资源已压缩', checked: true, critical: false },
{ id: 10, category: '性能', task: 'CDN 已配置', checked: false, critical: false },
{ id: 11, category: '监控', task: '日志收集已启用', checked: true, critical: false },
{ id: 12, category: '监控', task: '错误监控已配置', checked: false, critical: true },
{ id: 13, category: '备份', task: '数据备份策略已确认', checked: true, critical: true },
{ id: 14, category: '回滚', task: '回滚方案已准备', checked: false, critical: true },
{ id: 15, category: '文档', task: '部署文档已更新', checked: false, critical: false }
])
const categories = computed(() => {
const cats = [...new Set(checklist.value.map(item => item.category))]
return cats
})
const itemsByCategory = (category) => {
return checklist.value.filter(item => item.category === category)
}
const totalTasks = computed(() => checklist.value.length)
const completedTasks = computed(() => checklist.value.filter(item => item.checked).length)
const progress = computed(() => Math.round((completedTasks.value / totalTasks.value) * 100))
const criticalCompleted = computed(() => {
const criticalItems = checklist.value.filter(item => item.critical)
return criticalItems.filter(item => item.checked).length
})
const criticalTotal = computed(() => checklist.value.filter(item => item.critical).length)
const allCriticalDone = computed(() => criticalCompleted.value === criticalTotal.value)
const readyToDeploy = computed(() => {
return progress.value === 100 && allCriticalDone.value
})
</script>
<template>
<div class="deployment-checklist">
<div class="demo-header">
<h3>上线前检查清单</h3>
<p class="subtitle">确保万无一失的起飞前检查</p>
</div>
<div class="intro-text">
<p>
就像飞机起飞前飞行员要逐项检查<strong>仪表盘</strong><strong>油量</strong><strong>跑道</strong>
软件上线前的检查清单能避免很多低级错误导致的线上事故
</p>
</div>
<div class="demo-content">
<!-- 进度概览 -->
<div class="progress-overview">
<div class="progress-circle">
<div class="progress-value" :class="{ success: progress === 100 }">
{{ progress }}%
</div>
<svg class="progress-ring" viewBox="0 0 100 100">
<circle
cx="50"
cy="50"
r="45"
fill="none"
:stroke="progress === 100 ? 'var(--vp-c-brand-delta)' : 'var(--vp-c-bg-alt)'"
stroke-width="8"
/>
<circle
cx="50"
cy="50"
r="45"
fill="none"
:stroke="readyToDeploy ? 'var(--vp-c-brand-delta)' : 'var(--vp-c-brand)'"
stroke-width="8"
stroke-dasharray="283"
:stroke-dashoffset="283 - (283 * progress) / 100"
transform="rotate(-90 50 50)"
class="progress-ring-circle"
/>
</svg>
</div>
<div class="progress-stats">
<div class="stat-item">
<span class="stat-value">{{ completedTasks }}/{{ totalTasks }}</span>
<span class="stat-label">总任务</span>
</div>
<div class="stat-item critical">
<span class="stat-value">{{ criticalCompleted }}/{{ criticalTotal }}</span>
<span class="stat-label">关键任务</span>
</div>
<div class="deploy-status" :class="{ ready: readyToDeploy }">
{{ readyToDeploy ? '✅ 准备就绪' : '⚠️ 还有待办项' }}
</div>
</div>
</div>
<!-- 分类检查清单 -->
<div class="checklist-categories">
<div
v-for="category in categories"
:key="category"
class="category-section"
>
<div class="category-title">{{ category }}</div>
<div class="checklist-items">
<div
v-for="item in itemsByCategory(category)"
:key="item.id"
class="checklist-item"
:class="{ checked: item.checked, critical: item.critical }"
>
<label class="checkbox-wrapper">
<input
type="checkbox"
v-model="item.checked"
class="checkbox"
/>
<span class="checkbox-custom"></span>
<span class="task-text">{{ item.task }}</span>
<span v-if="item.critical" class="critical-badge">关键</span>
</label>
</div>
</div>
</div>
</div>
<!-- 提示信息 -->
<div v-if="!allCriticalDone" class="warning-box">
<span class="warning-icon"></span>
<span class="warning-text">
还有 <strong>{{ criticalTotal - criticalCompleted }}</strong> 项关键任务未完成
建议优先处理这些关键项后再上线
</span>
</div>
<div v-else-if="progress < 100" class="info-box-inline">
<span class="info-icon"></span>
<span class="info-text">
关键任务已完成还有 <strong>{{ totalTasks - completedTasks }}</strong> 项可选任务
建议尽快完成以提升系统稳定性
</span>
</div>
<div v-else class="success-box">
<span class="success-icon">🎉</span>
<span class="success-text">
太棒了所有检查项都已完成可以放心上线了
</span>
</div>
</div>
<div class="info-box">
<p>
💡 <strong>最佳实践</strong>将此检查清单集成到 CI/CD 流程中
关键项不通过则禁止自动部署避免人为疏忽导致线上故障
</p>
</div>
</div>
</template>
<style scoped>
.deployment-checklist {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.progress-overview {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
}
.progress-circle {
position: relative;
width: 100px;
height: 100px;
}
.progress-ring {
width: 100%;
height: 100%;
}
.progress-ring-circle {
transition: stroke-dashoffset 0.5s ease;
}
.progress-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.5rem;
font-weight: 700;
color: var(--vp-c-brand);
}
.progress-value.success {
color: var(--vp-c-brand-delta);
}
.progress-stats {
display: flex;
flex-direction: column;
gap: 1rem;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.stat-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.stat-item.critical .stat-value {
color: #ef4444;
}
.deploy-status {
font-size: 0.9rem;
font-weight: 600;
padding: 0.5rem 1rem;
background: #fef3c7;
color: #92400e;
border-radius: 6px;
text-align: center;
}
.deploy-status.ready {
background: var(--vp-c-brand-dimm);
color: var(--vp-c-brand-delta);
}
.checklist-categories {
display: flex;
flex-direction: column;
gap: 1rem;
}
.category-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.category-title {
font-size: 0.95rem;
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--vp-c-divider);
}
.checklist-items {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.checklist-item {
padding: 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
transition: all 0.3s ease;
}
.checklist-item.checked {
background: var(--vp-c-brand-dimm);
border-color: var(--vp-c-brand-delta);
}
.checklist-item.critical {
border-left: 3px solid #ef4444;
}
.checklist-item.critical.checked {
border-left-color: var(--vp-c-brand-delta);
}
.checkbox-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
user-select: none;
}
.checkbox {
position: absolute;
opacity: 0;
pointer-events: none;
}
.checkbox-custom {
width: 20px;
height: 20px;
border: 2px solid var(--vp-c-divider);
border-radius: 4px;
position: relative;
transition: all 0.3s ease;
flex-shrink: 0;
}
.checkbox:checked + .checkbox-custom {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
.checkbox:checked + .checkbox-custom::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 0.75rem;
font-weight: 700;
}
.task-text {
flex: 1;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.checklist-item.checked .task-text {
text-decoration: line-through;
color: var(--vp-c-text-3);
}
.critical-badge {
font-size: 0.7rem;
padding: 0.15rem 0.4rem;
background: #fee2e2;
color: #dc2626;
border-radius: 4px;
font-weight: 600;
}
.warning-box,
.info-box-inline,
.success-box {
padding: 1rem;
border-radius: 8px;
display: flex;
align-items: center;
gap: 0.75rem;
}
.warning-box {
background: #fef3c7;
border-left: 3px solid #f59e0b;
}
.info-box-inline {
background: #dbeafe;
border-left: 3px solid #3b82f6;
}
.success-box {
background: var(--vp-c-brand-dimm);
border-left: 3px solid var(--vp-c-brand-delta);
}
.warning-icon,
.info-icon,
.success-icon {
font-size: 1.2rem;
}
.warning-text,
.info-text,
.success-text {
flex: 1;
font-size: 0.9rem;
color: var(--vp-c-text-1);
line-height: 1.5;
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.progress-overview {
flex-direction: column;
}
.checkbox-wrapper {
gap: 0.5rem;
}
.task-text {
font-size: 0.85rem;
}
}
</style>
@@ -1,358 +1,151 @@
<!--
DeploymentCicdDemo.vue
CI/CD 自动化精简版
-->
<template>
<div class="deployment-cicd-demo">
<div class="demo-header">
<div class="deployment-cicd">
<div class="header">
<span class="icon">🔄</span>
<span class="title">CI/CD 自动化流程</span>
<span class="title">CI/CD 自动化</span>
<span class="subtitle">从代码到上线一键搞定</span>
</div>
<div class="intro-text">
<strong>CI/CD</strong> 就像一条<strong>自动化流水线</strong>你只管写代码剩下的测试构建部署流水线自动帮你完成
<div class="pipeline">
<div class="step">
<span class="num">1</span>
<span class="text">代码推送</span>
</div>
<span class="arrow"></span>
<div class="step">
<span class="num">2</span>
<span class="text">自动测试</span>
</div>
<span class="arrow"></span>
<div class="step">
<span class="num">3</span>
<span class="text">自动构建</span>
</div>
<span class="arrow"></span>
<div class="step">
<span class="num">4</span>
<span class="text">自动部署</span>
</div>
</div>
<div class="demo-content">
<div class="pipeline">
<div
v-for="(step, index) in pipelineSteps"
:key="index"
class="pipeline-step"
:class="{ active: currentStep === index, completed: currentStep > index }"
>
<div class="step-connector" v-if="index > 0"></div>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-info">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
</div>
<div class="step-status" v-if="currentStep === index">
<span class="spinner"></span>
</div>
<div class="step-status" v-if="currentStep > index">
<span class="check"></span>
</div>
</div>
<div class="compare">
<div class="col">
<div class="title">手动部署</div>
<div class="item"> 容易出错</div>
</div>
<div class="manual-vs-auto">
<div class="compare-column manual">
<div class="column-header">
<span class="column-icon">😰</span>
<span class="column-title">手动部署</span>
</div>
<div class="column-body">
<div class="step-list">
<div class="step-item"> 手动改代码</div>
<div class="step-item"> 手动上传 FTP</div>
<div class="step-item"> 手动 SSH 连接</div>
<div class="step-item"> 手动重启服务</div>
<div class="step-item"> 容易出错</div>
</div>
</div>
</div>
<div class="compare-column auto">
<div class="column-header">
<span class="column-icon">🎉</span>
<span class="column-title">CI/CD 自动化</span>
</div>
<div class="column-body">
<div class="step-list">
<div class="step-item"> Git 推送代码</div>
<div class="step-item"> 自动运行测试</div>
<div class="step-item"> 自动构建打包</div>
<div class="step-item"> 自动部署上线</div>
<div class="step-item"> 快速可靠</div>
</div>
</div>
</div>
<div class="col highlight">
<div class="title">CI/CD</div>
<div class="item"> 快速可靠</div>
</div>
<button @click="startPipeline" class="start-btn" :disabled="isRunning">
{{ isRunning ? '⏳ 流水线运行中...' : '▶ 启动流水线' }}
</button>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>推荐工具</strong>GitHub Actions免费GitLab CIJenkins几分钟就能配置好
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(-1)
const isRunning = ref(false)
const pipelineSteps = [
{ icon: '💻', title: '代码提交', desc: 'git push 到 GitHub' },
{ icon: '🧪', title: '自动测试', desc: '运行单元测试' },
{ icon: '📦', title: '自动构建', desc: 'npm run build' },
{ icon: '🚀', title: '自动部署', desc: '部署到服务器' },
{ icon: '✨', title: '完成上线', desc: '新版本可用' }
]
const startPipeline = () => {
if (isRunning.value) return
isRunning.value = true
currentStep.value = -1
const steps = [0, 1, 2, 3, 4]
let delay = 0
steps.forEach((step, index) => {
delay += 1500
setTimeout(() => {
currentStep.value = step
if (index === steps.length - 1) {
setTimeout(() => {
isRunning.value = false
setTimeout(() => {
currentStep.value = -1
}, 2000)
}, 1000)
}
}, delay)
})
}
</script>
<style scoped>
.deployment-cicd-demo {
.deployment-cicd {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 0.75rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
.header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
.header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
.header .title {
font-weight: 700;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text strong {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
margin-bottom: 1rem;
}
.pipeline {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
overflow-x: auto;
padding: 0.5rem 0;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 8px;
margin-bottom: 0.75rem;
}
.pipeline-step {
.step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
min-width: 100px;
flex: 1;
}
.step-connector {
position: absolute;
top: 20px;
left: -50%;
width: 100%;
height: 2px;
background: var(--vp-c-divider);
z-index: 0;
}
.pipeline-step.completed .step-connector {
background: var(--vp-c-brand);
}
.step-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
z-index: 1;
background: var(--vp-c-bg);
padding: 0 0.5rem;
}
.step-info {
gap: 0.25rem;
text-align: center;
z-index: 1;
font-size: 0.8rem;
color: var(--vp-c-text-2);
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
min-width: 60px;
}
.step-title {
.step .num {
font-weight: 700;
color: var(--vp-c-brand);
}
.step .text {
font-weight: 600;
font-size: 0.8rem;
}
.arrow {
font-size: 1rem;
color: var(--vp-c-text-3);
}
.compare {
display: flex;
gap: 0.75rem;
}
.col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.col.highlight {
background: var(--vp-c-brand-dimm);
}
.col .title {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.step-desc {
font-size: 0.7rem;
color: var(--vp-c-text-2);
line-height: 1.3;
}
.step-status {
margin-top: 0.5rem;
font-size: 1rem;
}
.pipeline-step.active .step-icon {
animation: pulse 1s infinite;
}
.pipeline-step.completed .step-icon {
opacity: 0.5;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
}
.manual-vs-auto {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.compare-column {
border-radius: 8px;
overflow: hidden;
}
.compare-column.manual {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
}
.compare-column.auto {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
}
.column-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.column-icon {
font-size: 1.5rem;
}
.column-title {
font-size: 0.9rem;
}
.column-body {
background: white;
padding: 0.75rem;
}
.step-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.step-item {
.col .item {
font-size: 0.8rem;
padding: 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
text-align: center;
}
.start-btn {
width: 100%;
padding: 0.875rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.start-btn:hover:not(:disabled) {
background: var(--vp-c-brand-1);
transform: translateY(-1px);
}
.start-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 0.25rem;
}
@media (max-width: 768px) {
.manual-vs-auto {
grid-template-columns: 1fr;
}
.pipeline {
flex-wrap: wrap;
}
.pipeline-step {
min-width: 80px;
}
}
</style>
@@ -1,315 +1,135 @@
<!--
DeploymentDnsDemo.vue
DNS 解析精简版
-->
<template>
<div class="deployment-dns-demo">
<div class="demo-header">
<div class="deployment-dns">
<div class="header">
<span class="icon">🔍</span>
<span class="title">DNS 解析流程</span>
<span class="subtitle">域名是怎么变成 IP 地址的</span>
<span class="title">DNS 解析</span>
<span class="subtitle">"好记的名字"变成"机器能懂的IP"</span>
</div>
<div class="intro-text">
想象你要给朋友打电话但你只记得他的名字不记得电话号<strong>DNS</strong> 就像一个<strong>电话本</strong>帮你把名字翻译成号码
</div>
<div class="demo-content">
<div class="dns-flow">
<div class="flow-step" :class="{ active: currentStep >= 1 }">
<div class="step-icon">💻</div>
<div class="step-info">
<div class="step-title">用户输入域名</div>
<div class="step-desc">在浏览器输入 coffee.example.com</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 2 }">
<div class="step-icon">📋</div>
<div class="step-info">
<div class="step-title">查询本地 DNS</div>
<div class="step-desc">先查电脑的"电话本"缓存</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 3 }">
<div class="step-icon">🌐</div>
<div class="step-info">
<div class="step-title">向上级 DNS 查询</div>
<div class="step-desc">本地没有"上级电话局"</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 4 }">
<div class="step-icon">🏠</div>
<div class="step-info">
<div class="step-title">返回 IP 地址</div>
<div class="step-desc">找到了123.45.67.89</div>
</div>
</div>
<div class="flow">
<div class="step">
<span class="emoji">💻</span>
<span class="text">用户输入域名</span>
</div>
<div class="example-box">
<div class="example-title">DNS 记录示例</div>
<div class="record-list">
<div class="record-item">
<span class="record-type">A 记录</span>
<span class="record-name">coffee.example.com</span>
<span class="record-arrow"></span>
<span class="record-value">123.45.67.89</span>
</div>
<div class="record-item">
<span class="record-type">CNAME</span>
<span class="record-name">www.coffee.example.com</span>
<span class="record-arrow"></span>
<span class="record-value">coffee.example.com</span>
</div>
</div>
<span class="arrow"></span>
<div class="step">
<span class="emoji">📋</span>
<span class="text">查询 DNS</span>
</div>
<span class="arrow"></span>
<div class="step success">
<span class="emoji"></span>
<span class="text">返回 IP</span>
</div>
<button @click="playAnimation" class="play-btn">
{{ isPlaying ? '▶ 重新播放' : '▶ 播放动画' }}
</button>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>关键点</strong>DNS 修改不是立即生效的全球同步需要几分钟到几小时 TTL 影响
<div class="example">
<span class="label">示例</span>
<code>example.com 192.168.1.1</code>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const isPlaying = ref(false)
const playAnimation = () => {
isPlaying.value = true
currentStep.value = 0
const steps = [1, 2, 3, 4]
let delay = 0
steps.forEach((step, index) => {
delay += 800
setTimeout(() => {
currentStep.value = step
if (index === steps.length - 1) {
setTimeout(() => {
isPlaying.value = false
}, 1000)
}
}, delay)
})
}
</script>
<style scoped>
.deployment-dns-demo {
.deployment-dns {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 0.75rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
.header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
.header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
.header .title {
font-weight: 700;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text strong {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
margin-bottom: 1rem;
}
.dns-flow {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
gap: 0.5rem;
}
.flow-step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
transition: all 0.3s ease;
}
.flow-step.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
transform: translateY(-2px);
}
.step-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.step-info {
text-align: center;
}
.step-title {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.step-desc {
font-size: 0.75rem;
.header .subtitle {
color: var(--vp-c-text-2);
line-height: 1.4;
font-size: 0.85rem;
}
.flow-arrow {
font-size: 1.25rem;
color: var(--vp-c-text-3);
}
.example-box {
.flow {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.example-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.record-list {
.step {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.record-item {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.25rem;
text-align: center;
font-size: 0.8rem;
color: var(--vp-c-text-2);
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
}
.record-type {
background: var(--vp-c-brand);
color: white;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 600;
font-size: 0.7rem;
border-radius: 6px;
min-width: 60px;
text-align: center;
}
.record-name {
color: var(--vp-c-text-2);
flex: 1;
font-family: monospace;
.step.success {
background: var(--vp-c-brand-dimm);
color: var(--vp-c-brand-delta);
}
.record-arrow {
.step .emoji {
font-size: 1.25rem;
}
.step .text {
font-weight: 600;
}
.arrow {
font-size: 1rem;
color: var(--vp-c-text-3);
}
.record-value {
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 500;
}
.play-btn {
width: 100%;
padding: 0.75rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
.play-btn:hover {
background: var(--vp-c-brand-1);
}
.info-box {
background: var(--vp-c-bg-alt);
.example {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.85rem;
}
.example .label {
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 0.25rem;
}
@media (max-width: 768px) {
.dns-flow {
flex-wrap: wrap;
}
.flow-arrow {
display: none;
}
.flow-step {
min-width: 45%;
}
.example code {
font-family: var(--vp-font-family-mono);
color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
</style>
@@ -1,395 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
const environments = ref(['dev', 'test', 'prod'])
const currentEnv = ref('dev')
const envConfigs = {
dev: {
name: '开发环境',
icon: '🔧',
color: '#3b82f6',
apiUrl: 'http://dev.api.example.com',
dbUrl: 'dev-db.example.com',
features: ['热重载', '详细日志', '调试工具'],
analogy: '小明的测试厨房,不断尝试新配方'
},
test: {
name: '测试环境',
icon: '🧪',
color: '#f59e0b',
apiUrl: 'http://test.api.example.com',
dbUrl: 'test-db.example.com',
features: ['模拟数据', '自动化测试', 'Bug 追踪'],
analogy: '内部试吃环节,确保品质稳定'
},
prod: {
name: '生产环境',
icon: '🚀',
color: '#10b981',
apiUrl: 'https://api.example.com',
dbUrl: 'prod-db.example.com',
features: ['性能优化', '安全加固', '监控告警'],
analogy: '正式营业的咖啡店,服务真实顾客'
}
}
const currentConfig = computed(() => envConfigs[currentEnv.value])
</script>
<template>
<div class="deployment-environment">
<div class="demo-header">
<h3>环境配置演示</h3>
<p class="subtitle">开发测试生产三分离</p>
</div>
<div class="intro-text">
<p>
就像小明有<strong>研发厨房</strong><strong>试吃区域</strong><strong>正式门店</strong>三个独立空间
软件也需要隔离的环境避免开发测试影响真实用户
</p>
</div>
<div class="demo-content">
<!-- 环境切换 -->
<div class="env-tabs">
<div
v-for="env in environments"
:key="env"
class="env-tab"
:class="{ active: currentEnv === env }"
@click="currentEnv = env"
:style="{ '--env-color': envConfigs[env].color }"
>
<span class="tab-icon">{{ envConfigs[env].icon }}</span>
<span class="tab-name">{{ envConfigs[env].name }}</span>
</div>
</div>
<!-- 环境详情 -->
<div class="env-detail">
<div class="detail-header" :style="{ '--env-color': currentConfig.color }">
<span class="detail-icon">{{ currentConfig.icon }}</span>
<div class="detail-info">
<h4>{{ currentConfig.name }}</h4>
<p class="detail-analogy">{{ currentConfig.analogy }}</p>
</div>
</div>
<div class="config-list">
<div class="config-item">
<span class="config-label">API 地址</span>
<span class="config-value">{{ currentConfig.apiUrl }}</span>
</div>
<div class="config-item">
<span class="config-label">数据库</span>
<span class="config-value">{{ currentConfig.dbUrl }}</span>
</div>
</div>
<div class="features">
<div class="features-title"> 特性</div>
<div class="features-list">
<span
v-for="feature in currentConfig.features"
:key="feature"
class="feature-tag"
>
{{ feature }}
</span>
</div>
</div>
</div>
<!-- 流程说明 -->
<div class="flow-diagram">
<div class="flow-title">🔄 环境流转</div>
<div class="flow-steps">
<div class="flow-step" :class="{ active: currentEnv === 'dev' }">
<div class="step-badge">1</div>
<div class="step-text">开发</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentEnv === 'test' }">
<div class="step-badge">2</div>
<div class="step-text">测试</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentEnv === 'prod' }">
<div class="step-badge">3</div>
<div class="step-text">生产</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
💡 <strong>最佳实践</strong>永远不要在开发环境直接修改生产配置就像小明不会在正式营业时突然换咖啡配方
</p>
</div>
</div>
</template>
<style scoped>
.deployment-environment {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.env-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.env-tab {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.env-tab:hover {
border-color: var(--vp-c-brand-soft);
}
.env-tab.active {
border-color: var(--env-color);
background: var(--vp-c-brand-soft);
}
.tab-icon {
font-size: 1.5rem;
}
.tab-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.env-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
display: flex;
align-items: center;
gap: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--env-color);
margin-bottom: 1rem;
}
.detail-icon {
font-size: 2.5rem;
}
.detail-info h4 {
margin: 0 0 0.25rem 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.detail-analogy {
margin: 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
font-style: italic;
}
.config-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
}
.config-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.config-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
font-weight: 600;
}
.config-value {
font-size: 0.85rem;
color: var(--vp-c-brand-1);
font-family: var(--vp-font-family-mono);
}
.features {
margin-top: 1rem;
}
.features-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.features-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.feature-tag {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
padding: 0.3rem 0.6rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
}
.flow-diagram {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.flow-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
}
.flow-steps {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 6px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
transition: all 0.3s ease;
min-width: 80px;
}
.flow-step.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.step-badge {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
font-weight: 700;
}
.step-text {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.flow-arrow {
font-size: 1.2rem;
color: var(--vp-c-brand);
margin: 0 0.25rem;
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.env-tabs {
grid-template-columns: 1fr;
}
.flow-steps {
flex-direction: column;
}
.flow-arrow {
transform: rotate(90deg);
}
}
</style>
@@ -1,98 +1,28 @@
<!--
DeploymentHttpsDemo.vue
HTTPS 安全精简版
-->
<template>
<div class="deployment-https-demo">
<div class="demo-header">
<div class="deployment-https">
<div class="header">
<span class="icon">🔒</span>
<span class="title">HTTPS 安全传输</span>
<span class="title">HTTPS 安全</span>
<span class="subtitle">给数据传输加把锁</span>
</div>
<div class="intro-text">
<strong>HTTP</strong> 就像寄明信片邮递员能看见内容<strong>HTTPS</strong> 就像寄保险箱只有收件人能打开
</div>
<div class="demo-content">
<div class="comparison">
<div class="compare-card http">
<div class="card-header">
<span class="lock-icon">🔓</span>
<span class="card-title">HTTP不安全</span>
</div>
<div class="card-body">
<div class="data-flow">
<div class="sender">👤 用户</div>
<div class="envelope open">
<div class="content-visible">密码: 123456</div>
</div>
<div class="thief">😈 黑客</div>
<div class="receiver">🌐 服务器</div>
</div>
<div class="warning-text"> 数据"裸奔"黑客能看见密码</div>
</div>
</div>
<div class="compare-card https">
<div class="card-header">
<span class="lock-icon">🔒</span>
<span class="card-title">HTTPS安全</span>
</div>
<div class="card-body">
<div class="data-flow">
<div class="sender">👤 用户</div>
<div class="envelope locked">
<div class="content-hidden">??加密??</div>
</div>
<div class="thief-confused">😕 黑客看不懂</div>
<div class="receiver">🌐 服务器</div>
</div>
<div class="success-text"> 数据加密黑客看不懂</div>
</div>
</div>
<div class="compare">
<div class="col">
<div class="title">HTTP</div>
<div class="item"> 明文传输</div>
</div>
<div class="certificate-box">
<div class="cert-title">📜 SSL/TLS 证书的作用</div>
<div class="cert-features">
<div class="cert-feature">
<span class="feature-icon">🔐</span>
<span class="feature-text">加密传输数据</span>
</div>
<div class="cert-feature">
<span class="feature-icon"></span>
<span class="feature-text">验证网站身份</span>
</div>
<div class="cert-feature">
<span class="feature-icon">🛡</span>
<span class="feature-text">防止数据篡改</span>
</div>
</div>
</div>
<div class="setup-steps">
<div class="steps-title">🚀 快速上手Let's Encrypt 免费)</div>
<div class="step-list">
<div class="step-item">
<span class="step-num">1</span>
<span class="step-text">安装 Certbot 工具</span>
</div>
<div class="step-item">
<span class="step-num">2</span>
<span class="step-text">运行命令申请证书</span>
</div>
<div class="step-item">
<span class="step-num">3</span>
<span class="step-text">自动配置 Nginx</span>
</div>
<div class="step-item">
<span class="step-num">4</span>
<span class="step-text">完成!网站显示🔒小锁</span>
</div>
</div>
<div class="col highlight">
<div class="title">HTTPS</div>
<div class="item"> 加密传输</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>推荐工具:</strong>Let's Encrypt 免费证书 + Certbot 自动工具几分钟就能搞定 HTTPS
<div class="info">
<span class="text">💡 推荐Let's Encrypt 免费证书</span>
</div>
</div>
</template>
@@ -101,244 +31,78 @@
</script>
<style scoped>
.deployment-https-demo {
.deployment-https {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 0.75rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
.header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
.header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
.header .title {
font-weight: 700;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text strong {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
margin-bottom: 1rem;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.compare-card {
border-radius: 8px;
overflow: hidden;
}
.compare-card.http {
background: linear-gradient(135deg, #ff6b6b 0%, #ff8787 100%);
}
.compare-card.https {
background: linear-gradient(135deg, #51cf66 0%, #69db7c 100%);
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
color: white;
}
.lock-icon {
font-size: 1.5rem;
}
.card-title {
font-weight: 600;
font-size: 0.9rem;
}
.card-body {
background: white;
padding: 0.75rem;
}
.data-flow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.sender,
.receiver {
font-size: 1.25rem;
}
.envelope {
flex: 1;
padding: 0.5rem;
border: 2px dashed var(--vp-c-divider);
border-radius: 6px;
text-align: center;
font-size: 0.75rem;
font-family: monospace;
}
.envelope.open {
background: #fff3cd;
}
.envelope.locked {
background: #d4edda;
}
.thief {
font-size: 1.25rem;
}
.thief-confused {
font-size: 1.25rem;
opacity: 0.5;
}
.warning-text {
color: #dc3545;
font-size: 0.75rem;
text-align: center;
font-weight: 500;
}
.success-text {
color: #28a745;
font-size: 0.75rem;
text-align: center;
font-weight: 500;
}
.certificate-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.cert-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.cert-features {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.cert-feature {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
.feature-icon {
font-size: 1rem;
}
.setup-steps {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
}
.steps-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.step-list {
.compare {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.step-item {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
}
.step-num {
.col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
gap: 0.5rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
.col.highlight {
background: var(--vp-c-brand-dimm);
}
.col .title {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.col .item {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 0.25rem;
.info {
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
text-align: center;
font-size: 0.85rem;
}
@media (max-width: 768px) {
.comparison {
grid-template-columns: 1fr;
}
.data-flow {
flex-wrap: wrap;
}
.info .text {
color: var(--vp-c-text-2);
}
</style>
@@ -1,465 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
const algorithm = ref('round-robin')
const totalRequests = ref(0)
const servers = ref([
{ id: 1, name: '服务器 1', requests: 0, status: 'healthy' },
{ id: 2, name: '服务器 2', requests: 0, status: 'healthy' },
{ id: 3, name: '服务器 3', requests: 0, status: 'healthy' }
])
const algorithms = [
{ id: 'round-robin', name: '轮询 (Round Robin)', desc: '依次分配,像排队发号' },
{ id: 'least-connections', name: '最少连接 (Least Connections)', desc: '谁最空闲分配给谁' },
{ id: 'ip-hash', name: 'IP 哈希 (IP Hash)', desc: '同一IP总是分配给同一服务器' }
]
const currentAlgorithm = computed(() => {
return algorithms.find(a => a.id === algorithm.value)
})
let requestIndex = 0
const sendRequest = () => {
totalRequests.value++
requestIndex++
let serverIndex = 0
if (algorithm.value === 'round-robin') {
serverIndex = (totalRequests.value - 1) % servers.value.length
} else if (algorithm.value === 'least-connections') {
const minRequests = Math.min(...servers.value.map(s => s.requests))
serverIndex = servers.value.findIndex(s => s.requests === minRequests)
} else if (algorithm.value === 'ip-hash') {
const mockIp = `192.168.1.${(requestIndex % 10) + 1}`
serverIndex = parseInt(mockIp.split('.')[3]) % servers.value.length
}
servers.value[serverIndex].requests++
}
// Auto simulate
setInterval(() => {
sendRequest()
}, 1500)
</script>
<template>
<div class="deployment-lb">
<div class="demo-header">
<h3>负载均衡演示</h3>
<p class="subtitle">多店协同分散客流</p>
</div>
<div class="intro-text">
<p>
就像小明开了三家咖啡店<strong>引导员</strong>根据不同策略把顾客分流到不同门店
避免单店过载提高整体服务能力负载均衡器就是那个"引导员"
</p>
</div>
<div class="demo-content">
<!-- 算法选择 -->
<div class="algorithm-section">
<div class="section-title">🎯 负载均衡算法</div>
<div class="algorithm-list">
<div
v-for="algo in algorithms"
:key="algo.id"
class="algorithm-item"
:class="{ active: algorithm === algo.id }"
@click="algorithm = algo.id"
>
<div class="algo-header">
<span class="algo-icon">{{ algorithm === algo.id ? '✓' : '○' }}</span>
<span class="algo-name">{{ algo.name }}</span>
</div>
<div class="algo-desc">{{ algo.desc }}</div>
</div>
</div>
</div>
<!-- 负载均衡器可视化 -->
<div class="lb-visualization">
<div class="lb-node">
<div class="lb-icon"></div>
<div class="lb-title">负载均衡器</div>
<div class="lb-algorithm">{{ currentAlgorithm.name }}</div>
<div class="lb-stats">{{ totalRequests }} 次请求</div>
</div>
<div class="lb-arrows">
<div
v-for="i in 3"
:key="i"
class="arrow-line"
:style="{ animationDelay: `${i * 0.2}s` }"
>
</div>
</div>
<div class="servers-grid">
<div
v-for="(server, idx) in servers"
:key="server.id"
class="server-card"
:class="{ highlighted: server.requests > 0 }"
>
<div class="server-icon">🖥</div>
<div class="server-name">{{ server.name }}</div>
<div class="server-status">
<span class="status-dot healthy"></span>
<span class="status-text">健康</span>
</div>
<div class="server-metrics">
<div class="metric-item">
<span class="metric-label">请求数</span>
<span class="metric-value">{{ server.requests }}</span>
</div>
<div class="metric-item">
<span class="metric-label">负载</span>
<div class="load-bar">
<div
class="load-fill"
:style="{ width: `${Math.min(server.requests * 5, 100)}%` }"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 生活类比 -->
<div class="analogy-box">
<div class="analogy-title">💡 生活类比</div>
<div class="analogy-content">
<div v-if="algorithm === 'round-robin'" class="analogy-item">
<strong>轮询</strong>就像三家咖啡店轮流接待A店B店C店A店B店C店...公平分配
</div>
<div v-if="algorithm === 'least-connections'" class="analogy-item">
<strong>最少连接</strong>就像引导员看哪家店人少就往哪家导确保每家都不会太忙
</div>
<div v-if="algorithm === 'ip-hash'" class="analogy-item">
<strong>IP哈希</strong>就像记住老顾客的习惯张三总是去A店李四总是去B店保证体验一致
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
💡 <strong>关键价值</strong>负载均衡不仅能<strong>提高吞吐量</strong>还能提供<strong>高可用性</strong>
某台服务器挂了负载均衡器会自动把流量导向其他健康的服务器
</p>
</div>
</div>
</template>
<style scoped>
.deployment-lb {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.75rem;
}
.algorithm-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.algorithm-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.algorithm-item {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
}
.algorithm-item:hover {
border-color: var(--vp-c-brand-soft);
}
.algorithm-item.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.algo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.algo-icon {
font-size: 0.9rem;
color: var(--vp-c-brand);
font-weight: 700;
}
.algo-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.algo-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
padding-left: 1.4rem;
}
.lb-visualization {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.lb-node {
background: var(--vp-c-brand-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 1rem;
text-align: center;
width: 100%;
max-width: 280px;
}
.lb-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.lb-title {
font-size: 1rem;
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.lb-algorithm {
font-size: 0.85rem;
color: var(--vp-c-brand);
margin-bottom: 0.5rem;
}
.lb-stats {
font-size: 0.8rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.lb-arrows {
display: flex;
gap: 1rem;
justify-content: center;
}
.arrow-line {
font-size: 1.5rem;
color: var(--vp-c-brand);
animation: flow 1.5s infinite;
}
@keyframes flow {
0%, 100% { opacity: 0.3; transform: translateX(-5px); }
50% { opacity: 1; transform: translateX(5px); }
}
.servers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.75rem;
width: 100%;
}
.server-card {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
text-align: center;
transition: all 0.3s ease;
}
.server-card.highlighted {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
}
.server-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.server-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.server-status {
display: flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
margin-bottom: 0.75rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--vp-c-text-3);
}
.status-dot.healthy {
background: var(--vp-c-brand-delta);
}
.status-text {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.server-metrics {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.metric-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
.metric-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.metric-value {
font-size: 0.85rem;
font-weight: 700;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
}
.load-bar {
width: 50px;
height: 6px;
background: var(--vp-c-bg-alt);
border-radius: 3px;
overflow: hidden;
}
.load-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
transition: width 0.3s ease;
}
.analogy-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-left: 3px solid var(--vp-c-brand);
}
.analogy-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.analogy-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.servers-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,525 +1,138 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
<!--
DeploymentMonitorDemo.vue
监控备份精简版
-->
<template>
<div class="deployment-monitor">
<div class="header">
<span class="icon">📊</span>
<span class="title">监控 & 备份</span>
<span class="subtitle">守住网站底线的最后一道防线</span>
</div>
<div class="metrics">
<div class="metric">
<span class="label">CPU 使用率</span>
<span class="value">{{ cpuUsage }}%</span>
</div>
<div class="metric">
<span class="label">内存使用率</span>
<span class="value">{{ memoryUsage }}%</span>
</div>
<div class="metric">
<span class="label">在线用户</span>
<span class="value">{{ activeUsers }}</span>
</div>
</div>
<div class="backup">
<div class="label">上次备份</div>
<span class="value">{{ lastBackup }}</span>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
const isMonitoring = ref(false)
const cpuUsage = ref(45)
const memoryUsage = ref(62)
const activeUsers = ref(23)
const requestRate = ref(145)
const errorRate = ref(0.8)
const alerts = ref([
{ id: 1, type: 'warning', message: 'CPU 使用率超过 70%', time: '2分钟前' },
{ id: 2, type: 'info', message: '新用户注册激增', time: '5分钟前' }
])
const metrics = ref([
{ name: '响应时间', value: '120ms', status: 'good', threshold: '200ms' },
{ name: '可用性', value: '99.9%', status: 'good', threshold: '99.5%' },
{ name: '错误率', value: '0.8%', status: 'warning', threshold: '1%' },
{ name: '吞吐量', value: '145 req/s', status: 'good', threshold: '100 req/s' }
])
const lastBackup = ref('2024-01-15 14:30')
let interval = null
const toggleMonitoring = () => {
isMonitoring.value = !isMonitoring.value
if (isMonitoring.value) {
startSimulation()
} else {
stopSimulation()
}
}
const startSimulation = () => {
onMounted(() => {
interval = setInterval(() => {
cpuUsage.value = Math.max(20, Math.min(95, cpuUsage.value + (Math.random() - 0.5) * 10))
memoryUsage.value = Math.max(30, Math.min(90, memoryUsage.value + (Math.random() - 0.5) * 5))
activeUsers.value = Math.max(10, Math.min(100, activeUsers.value + Math.floor((Math.random() - 0.5) * 5)))
requestRate.value = Math.max(50, Math.min(300, requestRate.value + Math.floor((Math.random() - 0.5) * 20)))
errorRate.value = Math.max(0, Math.min(5, errorRate.value + (Math.random() - 0.5) * 0.3))
}, 2000)
}
const stopSimulation = () => {
if (interval) {
clearInterval(interval)
interval = null
}
}
})
onUnmounted(() => {
stopSimulation()
if (interval) {
clearInterval(interval)
}
})
</script>
<template>
<div class="deployment-monitor">
<div class="demo-header">
<h3>监控演示</h3>
<p class="subtitle">实时掌握咖啡店运营状况</p>
</div>
<div class="intro-text">
<p>
就像小明通过<strong>监控摄像头</strong><strong>收银系统</strong>实时了解客流量销售额库存情况
服务监控能帮助我们发现性能瓶颈提前预警故障
</p>
</div>
<div class="demo-content">
<!-- 控制栏 -->
<div class="control-bar">
<div class="monitor-status">
<span class="status-indicator" :class="{ active: isMonitoring }"></span>
<span class="status-text">{{ isMonitoring ? '监控运行中' : '监控已停止' }}</span>
</div>
<button
class="toggle-btn"
:class="{ active: isMonitoring }"
@click="toggleMonitoring"
>
{{ isMonitoring ? '⏸ 暂停' : '▶ 启动监控' }}
</button>
</div>
<!-- 核心指标 -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-header">
<span class="metric-icon">💻</span>
<span class="metric-name">CPU 使用率</span>
</div>
<div class="metric-value">
{{ cpuUsage.toFixed(1) }}%
</div>
<div class="metric-bar">
<div
class="metric-fill"
:class="cpuUsage > 80 ? 'danger' : cpuUsage > 60 ? 'warning' : 'good'"
:style="{ width: `${cpuUsage}%` }"
></div>
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-icon">🧠</span>
<span class="metric-name">内存使用率</span>
</div>
<div class="metric-value">
{{ memoryUsage.toFixed(1) }}%
</div>
<div class="metric-bar">
<div
class="metric-fill"
:class="memoryUsage > 80 ? 'danger' : memoryUsage > 60 ? 'warning' : 'good'"
:style="{ width: `${memoryUsage}%` }"
></div>
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-icon">👥</span>
<span class="metric-name">在线用户</span>
</div>
<div class="metric-value large">
{{ activeUsers }}
</div>
<div class="metric-sub">实时</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-icon"></span>
<span class="metric-name">请求速率</span>
</div>
<div class="metric-value large">
{{ requestRate }}
</div>
<div class="metric-sub">req/s</div>
</div>
</div>
<!-- 业务指标 -->
<div class="business-metrics">
<div class="section-title">📊 业务指标</div>
<div class="metrics-list">
<div
v-for="(metric, idx) in metrics"
:key="idx"
class="business-metric-item"
:class="metric.status"
>
<div class="metric-info">
<span class="metric-label">{{ metric.name }}</span>
<span class="metric-threshold">目标: {{ metric.threshold }}</span>
</div>
<div class="metric-val">{{ metric.value }}</div>
<div class="metric-status-badge">
<span v-if="metric.status === 'good'" class="badge good"> 正常</span>
<span v-else-if="metric.status === 'warning'" class="badge warning"> 警告</span>
<span v-else class="badge danger"> 异常</span>
</div>
</div>
</div>
</div>
<!-- 告警列表 -->
<div class="alerts-section">
<div class="section-title">🔔 最近告警</div>
<div class="alerts-list">
<div
v-for="alert in alerts"
:key="alert.id"
class="alert-item"
:class="alert.type"
>
<span class="alert-icon">{{ alert.type === 'warning' ? '⚠️' : '️' }}</span>
<span class="alert-message">{{ alert.message }}</span>
<span class="alert-time">{{ alert.time }}</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<p v-if="!isMonitoring">
💡 <strong>准备就绪</strong>点击"启动监控"按钮开始实时查看服务器各项指标就像打开咖啡店的监控系统
</p>
<p v-else>
<strong>监控中</strong>各项指标实时更新设置合理的阈值和告警才能在问题发生时第一时间响应
</p>
</div>
</div>
</template>
<style scoped>
.deployment-monitor {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
background: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.control-bar {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
}
.monitor-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vp-c-text-3);
}
.status-indicator.active {
background: var(--vp-c-brand-delta);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text {
font-size: 0.9rem;
color: var(--vp-c-text-1);
font-weight: 600;
}
.toggle-btn {
padding: 0.6rem 1.25rem;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
cursor: pointer;
transition: all 0.3s ease;
}
.toggle-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
}
.metric-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.metric-header {
.header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.metric-icon {
font-size: 1.2rem;
.header .icon {
font-size: 1.25rem;
}
.metric-name {
.header .title {
font-weight: 700;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.metric {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.5rem;
background: var(--vp-c-bg);
border-radius: 6px;
text-align: center;
}
.metric .label {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.metric-value {
font-size: 1.8rem;
.metric .value {
font-weight: 700;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
margin-bottom: 0.5rem;
}
.metric-value.large {
font-size: 2rem;
}
.metric-bar {
height: 8px;
background: var(--vp-c-bg-alt);
border-radius: 4px;
overflow: hidden;
}
.metric-fill {
height: 100%;
transition: width 0.5s ease;
}
.metric-fill.good {
background: var(--vp-c-brand-delta);
}
.metric-fill.warning {
background: #f59e0b;
}
.metric-fill.danger {
background: #ef4444;
}
.metric-sub {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-top: 0.25rem;
}
.business-metrics {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.section-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
}
.metrics-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.business-metric-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.business-metric-item.warning {
border-left-color: #f59e0b;
}
.business-metric-item.danger {
border-left-color: #ef4444;
}
.metric-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.metric-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.metric-threshold {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.metric-val {
font-size: 1rem;
font-weight: 700;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
}
.metric-status-badge {
margin-left: 0.75rem;
}
.badge {
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.badge.good {
background: var(--vp-c-brand-dimm);
color: var(--vp-c-brand-delta);
}
.badge.warning {
background: #fef3c7;
color: #92400e;
}
.badge.danger {
background: #fee2e2;
color: #dc2626;
}
.alerts-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.alerts-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.alert-item {
.backup {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg-soft);
}
.alert-item.warning {
background: #fef3c7;
border-left: 3px solid #f59e0b;
}
.alert-item.info {
background: #dbeafe;
border-left: 3px solid #3b82f6;
}
.alert-icon {
font-size: 1rem;
}
.alert-message {
flex: 1;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.alert-time {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
border-radius: 6px;
font-size: 0.85rem;
}
.backup .label {
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
.backup .value {
font-weight: 600;
color: var(--vp-c-text-1);
}
</style>
@@ -1,460 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
const enableProxy = ref(false)
const incomingRequests = ref(0)
const proxiedRequests = ref(0)
const serverPort = ref(3000)
const nginxPort = ref(80)
const servers = ref([
{ id: 1, name: 'Node.js 应用', port: 3000, status: 'running', load: 45 },
{ id: 2, name: 'Python API', port: 4000, status: 'running', load: 30 }
])
const selectedServer = ref(0)
const toggleProxy = () => {
enableProxy.value = !enableProxy.value
if (!enableProxy.value) {
proxiedRequests.value = 0
}
}
const simulateRequest = () => {
if (!enableProxy.value) return
incomingRequests.value++
proxiedRequests.value++
servers.value[selectedServer.value].load = Math.min(100, servers.value[selectedServer.value].load + 5)
setTimeout(() => {
servers.value[selectedServer.value].load = Math.max(10, servers.value[selectedServer.value].load - 5)
}, 1000)
}
// Auto simulate
setInterval(() => {
if (enableProxy.value) {
simulateRequest()
}
}, 2000)
</script>
<template>
<div class="deployment-nginx">
<div class="demo-header">
<h3>Nginx反向代理演示</h3>
<p class="subtitle">门店服务员引导顾客</p>
</div>
<div class="intro-text">
<p>
就像咖啡店的<strong>服务员</strong>引导顾客到相应的吧台咖啡蛋糕收银
Nginx 作为反向代理把外部请求转发给后端的不同服务
</p>
</div>
<div class="demo-content">
<!-- 开关控制 -->
<div class="control-panel">
<div class="switch-section">
<span class="switch-label">启用反向代理</span>
<button
class="toggle-btn"
:class="{ active: enableProxy }"
@click="toggleProxy"
>
<span class="toggle-slider"></span>
</button>
</div>
<div class="stats">
<div class="stat-item">
<span class="stat-value">{{ incomingRequests }}</span>
<span class="stat-label">总请求</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ proxiedRequests }}</span>
<span class="stat-label">已转发</span>
</div>
</div>
</div>
<!-- 架构图 -->
<div class="architecture-diagram">
<div class="diagram-layer client">
<div class="layer-icon">👥</div>
<div class="layer-title">用户浏览器</div>
<div class="layer-detail">访问 :80</div>
</div>
<div class="diagram-arrow"></div>
<div class="diagram-layer nginx" :class="{ active: enableProxy }">
<div class="layer-icon">🛎</div>
<div class="layer-title">Nginx (反向代理)</div>
<div class="layer-detail">监听 {{ nginxPort }} 端口</div>
</div>
<div class="diagram-arrow" :class="{ active: enableProxy }"> 转发</div>
<div class="diagram-layer backend">
<div class="layer-title">后端服务</div>
<div class="server-list">
<div
v-for="(server, idx) in servers"
:key="server.id"
class="server-item"
:class="{ active: selectedServer === idx }"
>
<div class="server-icon">🖥</div>
<div class="server-info">
<div class="server-name">{{ server.name }}</div>
<div class="server-port">:{{ server.port }}</div>
</div>
<div class="server-load">
<div class="load-bar">
<div
class="load-fill"
:style="{ width: `${server.load}%` }"
></div>
</div>
<span class="load-text">{{ server.load }}%</span>
</div>
</div>
</div>
</div>
</div>
<!-- 配置示例 -->
<div class="config-example">
<div class="config-title">📝 Nginx 配置示例</div>
<pre class="code-block"><code>server {
listen {{ nginxPort }};
server_name example.com;
location / {
proxy_pass http://localhost:{{ serverPort }};
proxy_set_header Host $host;
}
}</code></pre>
</div>
</div>
<div class="info-box">
<p v-if="!enableProxy">
💡 <strong>等待启用</strong>点击"启用反向代理"开关看看 Nginx 如何将请求转发给后端服务
</p>
<p v-else>
<strong>运行中</strong>Nginx 正在监听 {{ nginxPort }} 端口将请求转发到后端的 {{ serverPort }} 端口
</p>
</div>
</div>
</template>
<style scoped>
.deployment-nginx {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.control-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.switch-section {
display: flex;
align-items: center;
gap: 0.75rem;
}
.switch-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.toggle-btn {
width: 50px;
height: 26px;
background: var(--vp-c-bg-alt);
border-radius: 13px;
border: 2px solid var(--vp-c-divider);
cursor: pointer;
position: relative;
transition: all 0.3s ease;
}
.toggle-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
.toggle-slider {
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
position: absolute;
top: 1px;
left: 2px;
transition: all 0.3s ease;
}
.toggle-btn.active .toggle-slider {
left: 25px;
}
.stats {
display: flex;
gap: 1.5rem;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.stat-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
}
.stat-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.architecture-diagram {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.diagram-layer {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
text-align: center;
width: 100%;
max-width: 350px;
transition: all 0.3s ease;
}
.diagram-layer.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.layer-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.layer-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.layer-detail {
font-size: 0.8rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.diagram-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
}
.diagram-arrow.active {
color: var(--vp-c-brand);
animation: bounce 1s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
.server-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.75rem;
}
.server-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.server-item.active {
border-color: var(--vp-c-brand);
}
.server-icon {
font-size: 1.5rem;
}
.server-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.server-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.server-port {
font-size: 0.75rem;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
}
.server-load {
display: flex;
align-items: center;
gap: 0.5rem;
}
.load-bar {
width: 60px;
height: 6px;
background: var(--vp-c-bg-alt);
border-radius: 3px;
overflow: hidden;
}
.load-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
transition: width 0.3s ease;
}
.load-text {
font-size: 0.75rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
min-width: 2.5rem;
text-align: right;
}
.config-example {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.config-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.75rem;
}
.code-block {
background: #1e1e1e;
border-radius: 6px;
padding: 1rem;
overflow-x: auto;
margin: 0;
}
.code-block code {
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.5;
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.control-panel {
flex-direction: column;
}
.stats {
width: 100%;
justify-content: space-around;
}
}
</style>
@@ -1,300 +1,200 @@
<script setup>
import { ref, computed } from 'vue'
const currentStep = ref(0)
const steps = [
{ id: 'code', title: '写代码', desc: '小明在厨房开发新咖啡配方', icon: '☕' },
{ id: 'build', title: '构建打包', desc: '准备原材料,清洗咖啡豆', icon: '📦' },
{ id: 'test', title: '测试验证', desc: '小明自己先品尝确认没问题', icon: '🧪' },
{ id: 'deploy', title: '部署上线', desc: '把咖啡上架到门店售卖', icon: '🚀' },
{ id: 'monitor', title: '监控维护', desc: '观察顾客反馈,持续优化', icon: '📊' }
]
const stepProgress = computed(() => ((currentStep.value + 1) / steps.length) * 100)
</script>
<template>
<div class="deployment-overview">
<div class="demo-header">
<h3>服务上线全流程</h3>
<p class="subtitle">从小明咖啡店看部署流程</p>
</div>
<div class="intro-text">
<p>
就像小明要推出一款新咖啡需要经过<strong>配方研发</strong><strong>材料准备</strong><strong>试喝确认</strong><strong>上架售卖</strong><strong>收集反馈</strong>
软件上线也需要完整的流程保障质量
</p>
<span class="icon">🚀</span>
<span class="title">服务上线全流程</span>
<span class="subtitle">从代码到用户眼中的网页</span>
</div>
<div class="demo-content">
<!-- 进度条 -->
<div class="progress-section">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${stepProgress}%` }"></div>
<div class="service-flow">
<div class="flow-step" :class="{ active: currentStep >= 1 }">
<div class="step-icon">📦</div>
<div class="step-title">Build</div>
<div class="tech-term">源码可执行文件</div>
</div>
<div class="progress-label">{{ currentStep + 1 }} / {{ steps.length }}</div>
</div>
<!-- 步骤卡片 -->
<div class="steps-container">
<div
v-for="(step, index) in steps"
:key="step.id"
class="step-card"
:class="{ active: currentStep === index, completed: index < currentStep }"
@click="currentStep = index"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
<!-- 状态指示 -->
<div class="step-status">
<span v-if="index < currentStep" class="status-icon completed"></span>
<span v-else-if="index === currentStep" class="status-icon active"></span>
<span v-else class="status-icon pending"></span>
</div>
<span class="flow-arrow"></span>
<div class="flow-step" :class="{ active: currentStep >= 2 }">
<div class="step-icon">🖥</div>
<div class="step-title">Server</div>
<div class="tech-term">24h运行的电脑</div>
</div>
<span class="flow-arrow"></span>
<div class="flow-step" :class="{ active: currentStep >= 3 }">
<div class="step-icon"></div>
<div class="step-title">Deploy</div>
<div class="tech-term">代码放到服务器</div>
</div>
<span class="flow-arrow"></span>
<div class="flow-step" :class="{ active: currentStep >= 4 }">
<div class="step-icon">🔀</div>
<div class="step-title">Nginx</div>
<div class="tech-term">接收请求分发响应</div>
</div>
<span class="flow-arrow"></span>
<div class="flow-step" :class="{ active: currentStep >= 5 }">
<div class="step-icon"></div>
<div class="step-title">DNS</div>
<div class="tech-term">域名IP地址</div>
</div>
<span class="flow-arrow"></span>
<div class="flow-step" :class="{ active: currentStep >= 6 }">
<div class="step-icon"></div>
<div class="step-title">HTTPS</div>
<div class="tech-term">加密数据传输</div>
</div>
<span class="flow-arrow"></span>
<div class="flow-step" :class="{ active: currentStep >= 7 }">
<div class="step-icon"></div>
<div class="step-title">CDN</div>
<div class="tech-term">就近访问加速</div>
</div>
<span class="flow-arrow"></span>
<div class="flow-step" :class="{ active: currentStep >= 8 }">
<div class="step-icon">🔄</div>
<div class="step-title">CI/CD</div>
<div class="tech-term">自动化部署流程</div>
</div>
<span class="flow-arrow"></span>
<div class="flow-step" :class="{ active: currentStep >= 9 }">
<div class="step-icon"></div>
<div class="step-title">Monitor</div>
<div class="tech-term">监控运行状态</div>
</div>
</div>
<!-- 当前步骤详情 -->
<div class="step-detail">
<h4>{{ steps[currentStep].title }}</h4>
<p>{{ steps[currentStep].desc }}</p>
<div class="detail-analogy">
<div class="analogy-label">💡 技术对应</div>
<div class="analogy-content">
<span v-if="currentStep === 0">编写代码开发新功能</span>
<span v-if="currentStep === 1">构建打包Webpack/Vite 编译资源</span>
<span v-if="currentStep === 2">测试验证单元测试集成测试</span>
<span v-if="currentStep === 3">部署上线推送到服务器/云平台</span>
<span v-if="currentStep === 4">监控维护日志性能监控告警</span>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心原则</strong>小步快跑 先上线MVP 逐步完善
</div>
</div>
<div class="info-box">
<p>💡 <strong>关键要点</strong>每个环节都不可或缺跳过测试就上线就像没试喝就卖咖啡可能让顾客喝到难喝的咖啡Bug</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
</script>
<style scoped>
.deployment-overview {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
background: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
margin: 0;
font-size: 0.9rem;
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
line-height: 1.5;
margin-bottom: 0.75rem;
padding: 0.5rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text strong {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
padding: 1.25rem;
margin-bottom: 0.5rem;
}
.progress-section {
.service-flow {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
gap: 0.25rem;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.progress-bar {
flex: 1;
height: 8px;
background: var(--vp-c-bg-alt);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
transition: width 0.3s ease;
border-radius: 4px;
}
.progress-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-brand);
min-width: 3rem;
text-align: right;
}
.steps-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.step-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
min-height: 120px;
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 0.25rem;
padding: 0.5rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
transition: all 0.2s ease;
flex-shrink: 0;
min-width: 85px;
}
.step-card:hover {
border-color: var(--vp-c-brand-soft);
transform: translateY(-2px);
.flow-step:hover {
border-color: var(--vp-c-brand);
}
.step-card.active {
.flow-step.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.step-card.completed {
border-color: var(--vp-c-brand-delta);
opacity: 0.8;
}
.step-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.step-title {
font-weight: 600;
font-size: 0.95rem;
font-size: 0.75rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
text-align: center;
white-space: nowrap;
}
.step-desc {
font-size: 0.8rem;
.tech-term {
font-size: 0.6rem;
color: var(--vp-c-text-2);
line-height: 1.3;
text-align: center;
white-space: normal;
line-height: 1.2;
}
.step-status {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
.status-icon {
font-size: 0.9rem;
}
.status-icon.completed {
color: var(--vp-c-brand-delta);
}
.status-icon.active {
color: var(--vp-c-brand);
}
.status-icon.pending {
.flow-arrow {
font-size: 1rem;
color: var(--vp-c-text-3);
}
.step-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.25rem;
border: 1px solid var(--vp-c-divider);
}
.step-detail h4 {
margin: 0 0 0.75rem 0;
font-size: 1.1rem;
color: var(--vp-c-brand);
}
.step-detail p {
margin: 0 0 1rem 0;
font-size: 0.95rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.detail-analogy {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.75rem;
border-left: 3px solid var(--vp-c-brand);
}
.analogy-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.analogy-content {
font-size: 0.9rem;
color: var(--vp-c-brand-1);
font-family: var(--vp-font-family-mono);
flex-shrink: 0;
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
background: var(--vp-c-bg-alt);
padding: 0.5rem;
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-top: 0.5rem;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.steps-container {
grid-template-columns: repeat(2, 1fr);
}
.step-card {
min-height: 100px;
padding: 0.75rem;
}
.step-icon {
font-size: 1.5rem;
}
.info-box .icon {
margin-right: 0.25rem;
}
</style>
@@ -1,561 +0,0 @@
<script setup>
import { ref } from 'vue'
const connected = ref(false)
const connecting = ref(false)
const currentStep = ref(0)
const commandHistory = ref([])
const currentCommand = ref('')
const steps = [
{ text: '正在连接服务器...', icon: '🔌' },
{ text: '身份验证中...', icon: '🔑' },
{ text: '建立安全通道...', icon: '🛡️' },
{ text: '连接成功!', icon: '✅' }
]
const connect = () => {
if (connected.value || connecting.value) return
connecting.value = true
currentStep.value = 0
commandHistory.value = []
const interval = setInterval(() => {
if (currentStep.value < steps.length - 1) {
currentStep.value++
} else {
clearInterval(interval)
connecting.value = false
connected.value = true
commandHistory.value.push({
type: 'success',
text: 'Welcome to Ubuntu 22.04 LTS'
})
commandHistory.value.push({
type: 'info',
text: 'Last login: ' + new Date().toLocaleString()
})
}
}, 800)
}
const disconnect = () => {
connected.value = false
currentStep.value = 0
commandHistory.value = []
}
const executeCommand = () => {
if (!currentCommand.value.trim()) return
commandHistory.value.push({
type: 'command',
text: `$ ${currentCommand.value}`
})
// 模拟命令响应
setTimeout(() => {
if (currentCommand.value === 'ls') {
commandHistory.value.push({
type: 'output',
text: 'app.js package.json node_modules/ README.md'
})
} else if (currentCommand.value === 'pwd') {
commandHistory.value.push({
type: 'output',
text: '/home/user/my-app'
})
} else if (currentCommand.value === 'whoami') {
commandHistory.value.push({
type: 'output',
text: 'user'
})
} else {
commandHistory.value.push({
type: 'output',
text: `Command '${currentCommand.value}' executed`
})
}
}, 300)
currentCommand.value = ''
}
</script>
<template>
<div class="deployment-ssh">
<div class="demo-header">
<h3>SSH远程连接演示</h3>
<p class="subtitle">像小明远程指挥咖啡店</p>
</div>
<div class="intro-text">
<p>
SSH就像小明通过<strong>电话远程指挥</strong>咖啡店员工工作
不需要亲自到店里就能执行命令查看状态部署应用
</p>
</div>
<div class="demo-content">
<!-- 连接控制 -->
<div class="connection-panel">
<div class="connection-info">
<div class="info-item">
<span class="label">服务器地址</span>
<span class="value">192.168.1.100</span>
</div>
<div class="info-item">
<span class="label">用户名</span>
<span class="value">xiaoming</span>
</div>
<div class="info-item">
<span class="label">状态</span>
<span class="status" :class="{ connected, connecting }">
{{ connecting ? '连接中...' : connected ? '已连接' : '未连接' }}
</span>
</div>
</div>
<button
v-if="!connected && !connecting"
@click="connect"
class="btn primary"
>
🔗 连接服务器
</button>
<button
v-else-if="connected"
@click="disconnect"
class="btn danger"
>
断开连接
</button>
<button v-else class="btn" disabled>
连接中...
</button>
</div>
<!-- 连接进度 -->
<div v-if="connecting || (connected && currentStep === steps.length - 1)" class="connection-progress">
<div class="progress-steps">
<div
v-for="(step, idx) in steps"
:key="idx"
class="progress-step"
:class="{ active: idx === currentStep, completed: idx < currentStep }"
>
<span class="step-icon">{{ step.icon }}</span>
<span class="step-text">{{ step.text }}</span>
</div>
</div>
</div>
<!-- 终端模拟 -->
<div v-if="connected" class="terminal">
<div class="terminal-header">
<span class="terminal-title">xiaoming@server ~</span>
<div class="terminal-buttons">
<span class="btn-dot red"></span>
<span class="btn-dot yellow"></span>
<span class="btn-dot green"></span>
</div>
</div>
<div class="terminal-body">
<div
v-for="(cmd, idx) in commandHistory"
:key="idx"
class="terminal-line"
:class="cmd.type"
>
{{ cmd.text }}
</div>
<div class="terminal-input-line">
<span class="prompt">$</span>
<input
v-model="currentCommand"
@keyup.enter="executeCommand"
type="text"
class="terminal-input"
placeholder="输入命令 (try: ls, pwd, whoami)"
autofocus
/>
</div>
</div>
</div>
<!-- 说明 -->
<div v-if="!connected && !connecting" class="ssh-features">
<div class="feature-grid">
<div class="feature-item">
<div class="feature-icon">🔐</div>
<div class="feature-title">加密通信</div>
<div class="feature-desc">所有数据加密传输防止被窃听</div>
</div>
<div class="feature-item">
<div class="feature-icon">🎫</div>
<div class="feature-title">身份验证</div>
<div class="feature-desc">密码或密钥验证确保只有授权用户访问</div>
</div>
<div class="feature-item">
<div class="feature-icon"></div>
<div class="feature-title">远程执行</div>
<div class="feature-desc">像在本地一样操作远程服务器</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<p v-if="!connected">
💡 <strong>生活类比</strong>SSH就像小明用专用电话打给咖啡店只有知道号码IP和密码密钥的人才能指挥店里工作
</p>
<p v-else>
<strong>已连接</strong>现在你可以像在本地一样操作远程服务器了试试输入 <code>ls</code><code>pwd</code> <code>whoami</code>
</p>
</div>
</div>
</template>
<style scoped>
.deployment-ssh {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.connection-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.connection-info {
display: flex;
gap: 1.5rem;
flex: 1;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-item .label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.info-item .value {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.status {
font-size: 0.85rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-3);
}
.status.connected {
background: var(--vp-c-brand-delta);
color: white;
}
.status.connecting {
background: var(--vp-c-brand);
color: white;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.btn {
padding: 0.6rem 1.25rem;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.3s ease;
}
.btn.primary {
background: var(--vp-c-brand);
color: white;
}
.btn.primary:hover {
background: var(--vp-c-brand-1);
}
.btn.danger {
background: var(--vp-c-red);
color: white;
}
.btn.danger:hover {
background: #dc2626;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.connection-progress {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.progress-steps {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.progress-step {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
border-radius: 6px;
opacity: 0.4;
transition: all 0.3s ease;
}
.progress-step.active {
opacity: 1;
background: var(--vp-c-brand-soft);
}
.progress-step.completed {
opacity: 0.7;
}
.step-icon {
font-size: 1.2rem;
}
.step-text {
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.terminal {
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
}
.terminal-header {
background: #2d2d2d;
padding: 0.5rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #404040;
}
.terminal-title {
font-size: 0.8rem;
color: #b4b4b4;
font-family: var(--vp-font-family-mono);
}
.terminal-buttons {
display: flex;
gap: 0.4rem;
}
.btn-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.btn-dot.red { background: #ff5f56; }
.btn-dot.yellow { background: #ffbd2e; }
.btn-dot.green { background: #27c93f; }
.terminal-body {
padding: 1rem;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
}
.terminal-line {
margin-bottom: 0.5rem;
line-height: 1.4;
}
.terminal-line.command {
color: #4ec9b0;
}
.terminal-line.output {
color: #d4d4d4;
}
.terminal-line.success {
color: #4ec9b0;
}
.terminal-line.info {
color: #9cdcfe;
}
.terminal-input-line {
display: flex;
align-items: center;
gap: 0.5rem;
}
.prompt {
color: #4ec9b0;
font-weight: 600;
}
.terminal-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #d4d4d4;
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
}
.terminal-input::placeholder {
color: #606060;
}
.ssh-features {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.feature-item {
text-align: center;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.feature-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.feature-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.feature-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.3;
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
.info-box code {
background: var(--vp-c-bg-soft);
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
color: var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.connection-panel {
flex-direction: column;
}
.connection-info {
flex-direction: column;
gap: 0.75rem;
width: 100%;
}
}
</style>
@@ -1,426 +1,113 @@
<script setup>
import { ref, computed } from 'vue'
const scenarios = ref([
{
id: 'personal',
name: '个人博客',
icon: '📝',
users: '100/天',
cpu: '1核',
memory: '1GB',
cost: '¥50/月',
suitable: '适合',
color: '#10b981'
},
{
id: 'small',
name: '小型电商',
icon: '🛒',
users: '1000/天',
cpu: '2核',
memory: '4GB',
cost: '¥200/月',
suitable: '适合',
color: '#3b82f6'
},
{
id: 'medium',
name: '中型应用',
icon: '🏢',
users: '10000/天',
cpu: '4核',
memory: '8GB',
cost: '¥800/月',
suitable: '适合',
color: '#f59e0b'
},
{
id: 'large',
name: '大型平台',
icon: '🏛️',
users: '100000+/天',
cpu: '8核+',
memory: '16GB+',
cost: '¥3000+/月',
suitable: '集群',
color: '#ef4444'
}
])
const selectedScenario = ref('small')
const serverTypes = ['云服务器', '物理服务器', '容器化部署']
const selectedServerType = ref('云服务器')
const currentScenario = computed(() => {
return scenarios.value.find(s => s.id === selectedScenario.value)
})
</script>
<!--
DeploymentServerDemo.vue
服务器选择精简版
-->
<template>
<div class="deployment-server">
<div class="demo-header">
<h3>服务器选择演示</h3>
<p class="subtitle">根据客流量选择合适的店面</p>
<div class="header">
<span class="icon">🖥</span>
<span class="title">服务器选择</span>
<span class="subtitle">根据客流量选择合适的店面</span>
</div>
<div class="intro-text">
<p>
就像小明开咖啡店<strong>街边小摊</strong><strong>社区店</strong><strong>商场店</strong><strong>旗舰店</strong>需要的场地面积和设备完全不同
选择服务器也要根据用户量来匹配避免<strong>资源浪费</strong><strong>性能不足</strong>
</p>
</div>
<div class="demo-content">
<!-- 场景选择 -->
<div class="scenario-section">
<div class="section-title">🎯 选择你的场景</div>
<div class="scenario-cards">
<div
v-for="scenario in scenarios"
:key="scenario.id"
class="scenario-card"
:class="{ active: selectedScenario === scenario.id }"
@click="selectedScenario = scenario.id"
:style="{ '--scenario-color': scenario.color }"
>
<div class="scenario-icon">{{ scenario.icon }}</div>
<div class="scenario-name">{{ scenario.name }}</div>
<div class="scenario-users">{{ scenario.users }}</div>
<div class="scenario-badge" :class="scenario.suitable === '适合' ? 'good' : 'cluster'">
{{ scenario.suitable === '适合' ? '✓ 单机' : '需要集群' }}
</div>
</div>
</div>
<div class="scenarios">
<div class="scenario" :class="{ active: scenario === 'personal' }" @click="scenario = 'personal'">
<span class="name">个人博客</span>
<span class="spec">1 1G</span>
<span class="cost">¥50/</span>
</div>
<!-- 服务器配置 -->
<div class="config-section">
<div class="section-title"> 推荐配置</div>
<div class="config-cards">
<div class="config-card">
<div class="config-icon">🖥</div>
<div class="config-label">CPU</div>
<div class="config-value">{{ currentScenario.cpu }}</div>
<div class="config-desc">处理订单的厨师数量</div>
</div>
<div class="config-card">
<div class="config-icon">💾</div>
<div class="config-label">内存</div>
<div class="config-value">{{ currentScenario.memory }}</div>
<div class="config-desc">同时处理订单的能力</div>
</div>
<div class="config-card">
<div class="config-icon">💰</div>
<div class="config-label">成本</div>
<div class="config-value">{{ currentScenario.cost }}</div>
<div class="config-desc">相当于租金+水电费</div>
</div>
</div>
<div class="scenario" :class="{ active: scenario === 'small' }" @click="scenario = 'small'">
<span class="name">小型电商</span>
<span class="spec">2 4G</span>
<span class="cost">¥300/</span>
</div>
<!-- 服务器类型选择 -->
<div class="server-type-section">
<div class="section-title">🏗 部署方式</div>
<div class="server-types">
<div
v-for="type in serverTypes"
:key="type"
class="type-item"
:class="{ active: selectedServerType === type }"
@click="selectedServerType = type"
>
<span class="type-icon">
{{ type === '云服务器' ? '☁️' : type === '物理服务器' ? '🏢' : '📦' }}
</span>
<span class="type-name">{{ type }}</span>
<span v-if="selectedServerType === type" class="type-check"></span>
</div>
</div>
<!-- 类型说明 -->
<div class="type-description">
<div v-if="selectedServerType === '云服务器'" class="desc-content">
<strong> 云服务器推荐</strong>
<p>像租用共享厨房灵活扩展按需付费适合大多数场景</p>
</div>
<div v-if="selectedServerType === '物理服务器'" class="desc-content">
<strong>🏢 物理服务器</strong>
<p>像买下整个店面性能稳定但成本高适合大规模应用</p>
</div>
<div v-if="selectedServerType === '容器化部署'" class="desc-content">
<strong>📦 容器化部署Docker/K8s</strong>
<p>像用预制盒做饭标准化可复制适合快速扩容和微服务</p>
</div>
</div>
<div class="scenario" :class="{ active: scenario === 'medium' }" @click="scenario = 'medium'">
<span class="name">中型应用</span>
<span class="spec">4 8G</span>
<span class="cost">¥1000/</span>
</div>
</div>
<div class="info-box">
<p>
💡 <strong>小明建议</strong>刚开始用云服务器最合适就像开咖啡店先租个小店面测试生意
客流量大了再升级或开分店集群部署<strong>不要一开始就租豪华店面</strong>
</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const scenario = ref('small')
</script>
<style scoped>
.deployment-server {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
background: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
.header {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.scenario-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 0.75rem;
}
.scenario-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
position: relative;
}
.scenario-card:hover {
border-color: var(--vp-c-brand-soft);
transform: translateY(-2px);
}
.scenario-card.active {
border-color: var(--scenario-color);
background: var(--vp-c-brand-soft);
}
.scenario-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.scenario-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.scenario-users {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.scenario-badge {
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 600;
display: inline-block;
}
.scenario-badge.good {
background: var(--vp-c-brand-delta);
color: white;
}
.scenario-badge.cluster {
background: var(--vp-c-red);
color: white;
}
.config-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.config-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.75rem;
}
.config-card {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 1rem;
text-align: center;
border: 1px solid var(--vp-c-divider);
}
.config-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.config-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.config-value {
font-size: 1.1rem;
font-weight: 700;
color: var(--vp-c-brand);
font-family: var(--vp-font-family-mono);
margin-bottom: 0.25rem;
}
.config-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
line-height: 1.3;
}
.server-type-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.server-types {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.type-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.type-item:hover {
border-color: var(--vp-c-brand-soft);
}
.type-item.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.type-icon {
.header .icon {
font-size: 1.25rem;
}
.type-name {
flex: 1;
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.type-check {
color: var(--vp-c-brand);
.header .title {
font-weight: 700;
}
.type-description {
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.75rem;
border-left: 3px solid var(--vp-c-brand);
}
.desc-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.desc-content strong {
display: block;
font-size: 1rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.desc-content p {
margin: 0;
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
.header .subtitle {
color: var(--vp-c-text-2);
line-height: 1.6;
font-size: 0.85rem;
}
.info-box p {
margin: 0;
.scenarios {
display: flex;
gap: 0.5rem;
}
@media (max-width: 640px) {
.scenario-cards {
grid-template-columns: repeat(2, 1fr);
}
.scenario {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.config-cards {
grid-template-columns: 1fr;
}
.scenario:hover {
border-color: var(--vp-c-brand-soft);
}
.scenario.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
}
.scenario .name {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.scenario .spec {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.scenario .cost {
font-weight: 700;
color: var(--vp-c-brand);
}
</style>
@@ -1,456 +0,0 @@
<script setup>
import { ref } from 'vue'
const symptoms = ref([
{ id: 'slow', text: '网站访问很慢', icon: '🐌' },
{ id: 'error', text: '显示500错误', icon: '❌' },
{ id: 'timeout', text: '请求超时', icon: '⏰' },
{ id: 'blank', text: '页面空白', icon: '📄' }
])
const selectedSymptom = ref('')
const diagnosis = ref('')
const solution = ref('')
const step = ref(1)
const diagnose = (symptom) => {
selectedSymptom.value = symptom
step.value = 2
if (symptom === 'slow') {
diagnosis.value = '可能原因:服务器负载过高、数据库查询慢、带宽不足'
solution.value = '检查CPU/内存使用率,优化数据库查询,考虑使用CDN加速'
} else if (symptom === 'error') {
diagnosis.value = '可能原因:代码Bug、配置错误、依赖缺失'
solution.value = '查看服务器日志,检查环境变量,确认依赖包是否安装'
} else if (symptom === 'timeout') {
diagnosis.value = '可能原因:网络问题、防火墙阻拦、服务未启动'
solution.value = '检查网络连通性,确认防火墙规则,验证服务状态'
} else if (symptom === 'blank') {
diagnosis.value = '可能原因:前端资源加载失败、JS报错、路径配置错误'
solution.value = '检查浏览器控制台错误,验证静态资源路径,查看构建日志'
}
}
const reset = () => {
selectedSymptom.value = ''
diagnosis.value = ''
solution.value = ''
step.value = 1
}
</script>
<template>
<div class="deployment-troubleshoot">
<div class="demo-header">
<h3>故障排查演示</h3>
<p class="subtitle">像医生诊断病人一样排查问题</p>
</div>
<div class="intro-text">
<p>
就像小明发现咖啡店<strong>出餐慢</strong><strong>机器故障</strong>
需要系统性地排查问题咖啡豆磨豆机咖啡机操作员
服务器故障也需要科学的排查流程
</p>
</div>
<div class="demo-content">
<!-- 步骤1: 选择症状 -->
<div v-if="step === 1" class="symptom-selection">
<div class="section-title">🔍 第一步选择你遇到的问题</div>
<div class="symptom-grid">
<div
v-for="symptom in symptoms"
:key="symptom.id"
class="symptom-card"
@click="diagnose(symptom.id)"
>
<div class="symptom-icon">{{ symptom.icon }}</div>
<div class="symptom-text">{{ symptom.text }}</div>
</div>
</div>
</div>
<!-- 步骤2: 诊断结果 -->
<div v-if="step === 2" class="diagnosis-result">
<div class="section-title">🩺 诊断结果</div>
<div class="result-card">
<div class="result-header">
<span class="result-icon">😷</span>
<span class="result-title">问题症状</span>
</div>
<div class="result-content">
{{ symptoms.find(s => s.id === selectedSymptom).text }}
</div>
</div>
<div class="result-card">
<div class="result-header">
<span class="result-icon">🔬</span>
<span class="result-title">可能原因</span>
</div>
<div class="result-content">{{ diagnosis }}</div>
</div>
<div class="result-card success">
<div class="result-header">
<span class="result-icon">💊</span>
<span class="result-title">解决方案</span>
</div>
<div class="result-content">{{ solution }}</div>
</div>
<button class="btn secondary" @click="reset">
重新诊断
</button>
</div>
<!-- 通用排查流程 -->
<div class="troubleshoot-flow">
<div class="flow-title">📋 通用排查流程</div>
<div class="flow-steps">
<div class="flow-step">
<div class="step-number">1</div>
<div class="step-content">
<div class="step-title">查看日志</div>
<div class="step-desc">服务器日志应用日志错误日志</div>
</div>
</div>
<div class="flow-step">
<div class="step-number">2</div>
<div class="step-content">
<div class="step-title">检查状态</div>
<div class="step-desc">服务是否运行端口是否监听进程是否存在</div>
</div>
</div>
<div class="flow-step">
<div class="step-number">3</div>
<div class="step-content">
<div class="step-title">资源监控</div>
<div class="step-desc">CPU内存磁盘网络使用情况</div>
</div>
</div>
<div class="flow-step">
<div class="step-number">4</div>
<div class="step-content">
<div class="step-title">网络测试</div>
<div class="step-desc">pingtelnetcurl 测试连通性</div>
</div>
</div>
</div>
</div>
<!-- 常用命令 -->
<div class="commands-cheatsheet">
<div class="cheatsheet-title"> 常用排查命令</div>
<div class="command-list">
<div class="command-item">
<code class="command-code">tail -f /var/log/nginx/error.log</code>
<span class="command-desc">实时查看 Nginx 错误日志</span>
</div>
<div class="command-item">
<code class="command-code">systemctl status nginx</code>
<span class="command-desc">检查 Nginx 服务状态</span>
</div>
<div class="command-item">
<code class="command-code">netstat -tlnp | grep :80</code>
<span class="command-desc">检查 80 端口是否被监听</span>
</div>
<div class="command-item">
<code class="command-code">ps aux | grep node</code>
<span class="command-desc">查看 Node.js 进程</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
💡 <strong>小明经验</strong>遇到问题不要慌按照"查看症状→分析原因→尝试解决→验证效果"的流程
90%的问题都能快速定位记得记录问题避免重复踩坑
</p>
</div>
</div>
</template>
<style scoped>
.deployment-troubleshoot {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
max-height: 600px;
overflow-y: auto;
margin: 1rem 0;
}
.demo-header {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.intro-text {
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
}
.symptom-selection {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.symptom-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
}
.symptom-card {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.symptom-card:hover {
border-color: var(--vp-c-brand-soft);
transform: translateY(-2px);
}
.symptom-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.symptom-text {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.diagnosis-result {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.result-card {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.result-card.success {
border-color: var(--vp-c-brand-delta);
background: var(--vp-c-brand-dimm);
}
.result-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.result-icon {
font-size: 1.2rem;
}
.result-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.result-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.5;
padding-left: 1.7rem;
}
.btn {
padding: 0.6rem 1.25rem;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.3s ease;
}
.btn.secondary {
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.btn.secondary:hover {
border-color: var(--vp-c-brand);
}
.troubleshoot-flow {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.flow-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.flow-step {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.step-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
font-weight: 700;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.15rem;
}
.step-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.commands-cheatsheet {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.cheatsheet-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
}
.command-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.command-item {
background: #1e1e1e;
border-radius: 4px;
padding: 0.6rem 0.75rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.command-code {
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
color: #4ec9b0;
background: transparent;
}
.command-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.info-box {
padding: 1rem 1.25rem;
margin: 0;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.symptom-grid {
grid-template-columns: repeat(2, 1fr);
}
.command-item {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -1,217 +0,0 @@
<!--
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>
@@ -1,212 +0,0 @@
<!--
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>
@@ -1,267 +0,0 @@
<!--
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>
@@ -1,194 +0,0 @@
<!--
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>
@@ -1,246 +0,0 @@
<!--
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>
+2 -34
View File
@@ -102,29 +102,13 @@ import SubnetCalculator from './components/appendix/web-basics/SubnetCalculator.
import NetworkTroubleshooting from './components/appendix/web-basics/NetworkTroubleshooting.vue'
// Deployment appendix components
import DeploymentArchitecture from './components/appendix/deployment/DeploymentArchitecture.vue'
import DnsFlowDemo from './components/appendix/deployment/DnsFlowDemo.vue'
import ServerSizerDemo from './components/appendix/deployment/ServerSizerDemo.vue'
import HttpsNginxDemo from './components/appendix/deployment/HttpsNginxDemo.vue'
import CdnCacheDemo from './components/appendix/deployment/CdnCacheDemo.vue'
import CicdPipelineDemo from './components/appendix/deployment/CicdPipelineDemo.vue'
import RollbackSwitchDemo from './components/appendix/deployment/RollbackSwitchDemo.vue'
import ObservabilityBackupDemo from './components/appendix/deployment/ObservabilityBackupDemo.vue'
import DeploymentOverviewDemo from './components/appendix/deployment/DeploymentOverviewDemo.vue'
import DeploymentBuildDemo from './components/appendix/deployment/DeploymentBuildDemo.vue'
import DeploymentServerDemo from './components/appendix/deployment/DeploymentServerDemo.vue'
import DeploymentSSHDemo from './components/appendix/deployment/DeploymentSSHDemo.vue'
import DeploymentEnvironmentDemo from './components/appendix/deployment/DeploymentEnvironmentDemo.vue'
import DeploymentNginxDemo from './components/appendix/deployment/DeploymentNginxDemo.vue'
import DeploymentLbDemo from './components/appendix/deployment/DeploymentLbDemo.vue'
import DeploymentMonitorDemo from './components/appendix/deployment/DeploymentMonitorDemo.vue'
import DeploymentBackupDemo from './components/appendix/deployment/DeploymentBackupDemo.vue'
import DeploymentTroubleshootDemo from './components/appendix/deployment/DeploymentTroubleshootDemo.vue'
import DeploymentChecklistDemo from './components/appendix/deployment/DeploymentChecklistDemo.vue'
import DeploymentDnsDemo from './components/appendix/deployment/DeploymentDnsDemo.vue'
import DeploymentHttpsDemo from './components/appendix/deployment/DeploymentHttpsDemo.vue'
import DeploymentCdnDemo from './components/appendix/deployment/DeploymentCdnDemo.vue'
import DeploymentCicdDemo from './components/appendix/deployment/DeploymentCicdDemo.vue'
import DeploymentMonitorDemo from './components/appendix/deployment/DeploymentMonitorDemo.vue'
import CssBoxModel from './components/appendix/web-basics/CssBoxModel.vue'
import CssFlexbox from './components/appendix/web-basics/CssFlexbox.vue'
import CssLayoutDemo from './components/appendix/web-basics/CssLayoutDemo.vue'
@@ -583,29 +567,13 @@ export default {
app.component('SubnetCalculator', SubnetCalculator)
app.component('NetworkTroubleshooting', NetworkTroubleshooting)
// Deployment appendix
app.component('DeploymentArchitecture', DeploymentArchitecture)
app.component('DnsFlowDemo', DnsFlowDemo)
app.component('ServerSizerDemo', ServerSizerDemo)
app.component('HttpsNginxDemo', HttpsNginxDemo)
app.component('CdnCacheDemo', CdnCacheDemo)
app.component('CicdPipelineDemo', CicdPipelineDemo)
app.component('RollbackSwitchDemo', RollbackSwitchDemo)
app.component('ObservabilityBackupDemo', ObservabilityBackupDemo)
app.component('DeploymentOverviewDemo', DeploymentOverviewDemo)
app.component('DeploymentBuildDemo', DeploymentBuildDemo)
app.component('DeploymentServerDemo', DeploymentServerDemo)
app.component('DeploymentSSHDemo', DeploymentSSHDemo)
app.component('DeploymentEnvironmentDemo', DeploymentEnvironmentDemo)
app.component('DeploymentNginxDemo', DeploymentNginxDemo)
app.component('DeploymentLbDemo', DeploymentLbDemo)
app.component('DeploymentMonitorDemo', DeploymentMonitorDemo)
app.component('DeploymentBackupDemo', DeploymentBackupDemo)
app.component('DeploymentTroubleshootDemo', DeploymentTroubleshootDemo)
app.component('DeploymentChecklistDemo', DeploymentChecklistDemo)
app.component('DeploymentDnsDemo', DeploymentDnsDemo)
app.component('DeploymentHttpsDemo', DeploymentHttpsDemo)
app.component('DeploymentCdnDemo', DeploymentCdnDemo)
app.component('DeploymentCicdDemo', DeploymentCicdDemo)
app.component('DeploymentMonitorDemo', DeploymentMonitorDemo)
app.component('CssBoxModel', CssBoxModel)
app.component('CssFlexbox', CssFlexbox)
app.component('CssLayoutDemo', CssLayoutDemo)