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)
+178 -370
View File
@@ -1,373 +1,216 @@
# 服务上线之旅:从代码到用户眼中的网页
# 服务上线之旅
> **学习指南**:本章节带你完整体验"一个服务上线"的全过程。我们不讲复杂的运维术语,而是通过小明的咖啡店网站上线故事,让你看懂代码是怎么变成用户能访问的网站的。
::: tip 🎯 核心问题
**代码在本地跑得好好的,怎么让全世界的人都能访问?**
:::
---
## 0. 引言:小明的咖啡店网站要上线了
## 1. 为什么要"服务上线"
小明是个前端开发,他用 Vue 写了一个漂亮的咖啡店网站:可以看菜单、在线下单、查看订单状态
网站在**小明的电脑上**跑得好好的,但是问题来了:
> **怎么让全世界的人都能访问这个网站?**
这就像小明在家做了一桌子菜,现在要开餐厅让所有人来吃。这可不是简单地"把菜端出去"那么简单,他要经历一整套完整的流程。
想象小明在家做了一桌子菜,现在要开餐厅让所有人来吃。这可不是"把菜端出去"那么简单
<DeploymentOverviewDemo />
**服务上线是一场"搬家+开业"的大工程**
1. **打包行李**把代码打包成服务器能懂的格式
2. **找房子**选择服务器(云服务器/VPS
3. **搬家**把代码部署到服务器
4. **装修** → 配置运行环境
5. **挂牌**配置域名和DNS
6. **安防**安装HTTPS证书
7. **招服务员**配置负载均衡
8. **开分店** → 配置CDN加速
9. **自动化** → 建立CI/CD流程
10. **守夜人** → 监控和备份
让我们跟着小明,一步一步完成这场"上线之旅"!
**服务上线是一场"搬家+开业"的大工程**
1. **构建** → 打包代码成服务器能懂的格式
2. **服务器**租一台永远不关机的电脑
3. **部署**把代码上传到服务器
4. **环境**配置 Nginx、Node.js
5. **域名** → 配置 DNS,让用户能找到
6. **HTTPS**安装证书,保护数据安全
7. **CI/CD**自动化部署,解放双手
8. **监控**盯控和备份,守住底线
---
## 1. 打包行李:把代码变成"可携带的包裹"
## 2. 构建:把代码变成"可携带的包裹"
小明的网站代码在他电脑上是这样散落的:
### 2.1 为什么要构建?
```
my-coffee-shop/
├── src/ # 源代码
│ ├── App.vue # Vue 组件
│ ├── main.js # 入口文件
│ └── assets/ # 图片、样式
├── package.json # 依赖清单
└── vite.config.js # 构建配置
```
这些源代码浏览器**看不懂**。浏览器只认识:
- HTML 文件(网页骨架)
- CSS 文件(网页样式)
- JS 文件(网页逻辑)
### 1.1 为什么要"构建"
想象小明做了一桌子菜,现在要打包外卖:
浏览器只认识 HTML/CSS/JS,不认识 Vue 组件。**构建(Build)就是"打包外卖"的过程**。
<DeploymentBuildDemo />
**构建Build)就是"打包外卖"的过程**
1. **翻译**把 Vue 组件翻译成浏览器懂的 HTML/CSS/JS
2. **压缩**把代码体积缩小(省流量、加载快
3. **合并**把几十个小文件合成几个大包(减少请求)
4. **哈希**:给文件名加上指纹(`app.abc123.js`),浏览器缓存友好
运行构建命令:
**构建做了什么**
- **翻译**Vue → HTML/CSS/JS
- **压缩**减小代码体积(省流量、加载快)
- **合并**多个文件 → 几个大包(减少请求
- **哈希**文件名加指纹(`app.abc123.js`),缓存友好
```bash
npm run build
```
构建完成后会生成一个 `dist` 文件夹:
```
dist/
├── index.html # 主页面
├── assets/
│ ├── app.abc123.js # 打包后的JS278KB
│ ├── app.abc123.css # 打包后的CSS45KB
│ └── logo.789xyz.png # 优化后的图片
```
这个 `dist` 文件夹,就是小明的"行李箱",里面装着所有要搬去服务器的东西。
---
## 2. 找房子:选择服务器
## 3. 服务器:找房子
代码打包好了,现在要找个"房子"住。这个房子就是**服务器**。
### 2.1 服务器是什么?
### 3.1 服务器是什么?
服务器 = **一台永远不关机、连着互联网的电脑**
<DeploymentServerDemo />
### 2.2 怎么选服务器?
### 3.2 怎么选服务器?
小明有几个选择:
::: tip 💡 选型建议
- **个人项目/学习**1核2G,约¥100-200/年
- **小型商业项目**2核4G,约¥500/年
- **中型项目**4核8G,约¥1000+/年
:::
| 方案 | 类比 | 优点 | 缺点 | 价格 |
|------|------|------|------|------|
| **虚拟主机** | 租床位 | 便宜、简单 | 性能差、不能随便装软件 | ¥50-200/年 |
| **云服务器** | 租整套公寓 | 自由度高、可扩展 | 需自己配置 | ¥100-1000/年 |
| **容器服务** | 租豪华酒店 | 自动化管理 | 价格高 | ¥500+/月 |
| **Serverless** | 租用会议室 | 用多少付多少 | 冷启动慢 | 按量计费 |
| 虚拟主机 | 租床位 | 便宜、简单 | 性能差 | ¥50-200/年 |
| 云服务器 | 租公寓 | 自由度高、可扩展 | 需自己配置 | ¥100-1000/年 |
| Vercel | 租会议室 | 零配置、免费额度 | 受平台限制 | 免费/按量 |
**小明的选择**:云服务器(2核4G,约¥500/年)
选配置的误区:
- ❌ **太小了**1核1G,跑个Hello World还行,稍微多点人就卡死
- ❌ **太大了**:一上来就8核16G,每天10个访问,纯属浪费
- ✅ **刚刚好**:2核4G起步,不够了再升级(云服务器支持弹性伸缩)
### 2.3 主流云厂商
### 3.3 主流云厂商
| 厂商 | 特点 | 适合人群 |
|------|------|---------|
| **阿里云** | 国内访问快、文档多 | 国内业务首选 |
| **腾讯云** | 价格亲民、微信生态 | 小程序开发者 |
| **AWS** | 全球覆盖、功能最强 | 国际业务 |
| **Vercel** | 免费额度、零配置 | 前端项目快速上线 |
| 阿里云 | 国内访问快 | 国内业务 |
| 腾讯云 | 价格亲民 | 小程序开发者 |
| Vercel | 零配置、免费 | 前端项目 |
---
## 3. 搬家:把代码放到服务器上
## 4. 部署:搬家
服务器租好了,现在要把代码"搬"过去。
### 4.1 连接服务器
### 3.1 怎么连接服务器?
服务器在遥远的机房,怎么操作?用 **SSH(远程连接工具)**
**SSH(远程连接工具)**
```bash
# 连接到服务器
ssh root@123.45.67.89
# 输入密码后,你就"进入"服务器了
# 后面敲的命令,都是在服务器上执行
# 输入密码后,你就在服务器上了
```
<DeploymentSSHDemo />
### 4.2 部署方式
### 3.2 部署方式对比
| 方式 | 优点 | 缺点 |
|------|------|------|
| FTP上传 | 简单直观 | 容易漏传 |
| Git拉取 | 快、可追溯 | 需配置Git |
| 方式 | 类比 | 优点 | 缺点 |
|------|------|------|------|
| **FTP上传** | 自己扛箱子搬家 | 简单直观 | 容易漏传文件、慢 |
| **Git拉取** | 让快递公司送货 | 快、可追溯 | 需要配置Git |
| **Docker容器** | 用搬家集装箱 | 环境一致、易迁移 | 需要学习Docker |
**推荐方式**Git + Build Script
小明用 Git 把代码推送到 GitHub,然后让服务器自己拉取最新代码:
**推荐**Git + Build Script
```bash
# 服务器上执行的脚本
cd /var/www/coffee-shop
git pull origin main # 拉取最新代码
npm install # 安装依赖
npm run build # 构建项目
pm2 restart all # 重启服务
cd /var/www/my-site
git pull origin main # 拉取最新代码
npm install # 安装依赖
npm run build # 构建项目
pm2 restart all # 重启服务
```
---
## 4. 装修:配置运行环境
代码搬过去了,但是服务器还只是个"空房子",需要"装修"才能住人。
### 4.1 需要装什么?
<DeploymentEnvironmentDemo />
**最小化安装脚本**(Ubuntu系统):
### 4.3 环境配置(Ubuntu
::: details 点击展开:最小化安装脚本
```bash
# 1. 更新系统
# 更新系统
sudo apt update && sudo apt upgrade -y
# 2. 安装 NginxWeb服务器,负责接待客人
# 安装 NginxWeb服务器)
sudo apt install -y nginx
# 3. 安装 Node.js(运行JavaScript代码)
# 安装 Node.js 18
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
# 4. 安装 PM2(进程管家,防止程序崩溃
# 安装 PM2(进程管家)
sudo npm install -g pm2
# 5. 安装 Git(拉代码用)
# 安装 Git
sudo apt install -y git
```
:::
### 4.2 Nginx 反向代理是什么?
小明的程序跑在 `3000` 端口,但用户习惯访问 `80`HTTP)或 `443`HTTPS)端口。
**Nginx 就像个"门童"**
- 守在 80/443 端口接待客人
- 把请求转给后端的 3000 端口
- 把结果返回给用户
<DeploymentNginxDemo />
**Nginx 配置示例**
**Nginx 反向代理**:把 80 端口请求转发到 3000 端口
```nginx
server {
listen 80;
server_name coffee.example.com;
server_name example.com;
# 所有请求都转发给 3000 端口
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
```
---
## 5. 挂牌:配置域名和 DNS
房子装修好了,现在要挂个牌子,让客人能找到你。这个牌子就是**域名**。
## 5. 域名和 DNS:挂牌
### 5.1 域名是什么?
域名 = **网站的门牌号**
- IP地址(123.45.67.89)太难记
- 域名(coffee.example.com)好记
- 域名(example.com)好记
**DNS(域名解析)** = **电话本**负责把域名翻译成IP
**DNS(域名解析)** = **电话本**,把域名翻译成IP
<DeploymentDnsDemo />
### 5.2 域名配置步骤
### 5.2 配置步骤
1. **买域名**阿里云/腾讯云/Namecheap购买(¥50-100/年)
2. **配置DNS记录**告诉世界"我的网站在这个IP"
常用记录类型:
1. **买域名**:阿里云/腾讯云(¥50-100/年)
2. **配置DNS**
| 记录类型 | 用途 | 示例 |
|---------|------|------|
| **A记录** | 直接指向IP | `coffee.example.com``123.45.67.89` |
| **CNAME** | 指向另一个域名 | `www.coffee.example.com``coffee.example.com` |
| A记录 | 指向IP | `example.com``123.45.67.89` |
| CNAME | 指向域名 | `www.example.com``example.com` |
**注意事项**
- DNS生效慢:全球同步需要几分钟到几小时
- 📝 TTL值:设置小一点(如600秒),修改后生效快
::: tip ⚠️ 注意
- DNS生效慢:全球同步需要几分钟到几小时
- TTL值:设置小一点(如600秒),修改后生效快
:::
---
## 6. 安防:安装 HTTPS 证书
房子好了,牌子挂了,现在要装门锁,保证安全。
## 6. HTTPS:安防
### 6.1 为什么需要 HTTPS
<DeploymentHttpsDemo />
**HTTP 的问题**:数据"裸奔",谁都能看见
**HTTPS 的好处**:数据加密,黑客看见也是乱码
- **HTTP**:数据"裸奔",谁都能看见
- **HTTPS**:数据加密,黑客看见也是乱码
### 6.2 怎么搞定 HTTPS
不用花钱买证书!**Let's Encrypt(免费证书)** + **Certbot(自动工具)**
**Let's Encrypt(免费证书)** + **Certbot(自动工具)**
```bash
# 1. 安装 Certbot
# 安装 Certbot
sudo apt install -y certbot python3-certbot-nginx
# 2. 自动申请证书并配置 Nginx
sudo certbot --nginx -d coffee.example.com
# 自动申请证书并配置 Nginx
sudo certbot --nginx -d example.com
# 3. 证书自动续期Certbot会自动设置)
# 证书自动续期
sudo certbot renew --dry-run
```
完成后访问网站会看到小锁🔒,HTTPS 就搞定了!
完成后访问网站会看到🔒小锁
---
## 7. 招服务员:负载均衡
## 7. CI/CD:自动化
小明的咖啡店火了,一个人接待不过来,怎么办?招更多服务员!
### 7.1 什么是负载均衡?
<DeploymentLbDemo />
**负载均衡器(Load Balancer** = **餐厅领班**
- 看着后面N个服务器(服务员)
- 把客人分配到最空闲的那个
- 某个服务器挂了,自动把流量分给其他的
### 7.2 什么时候需要负载均衡?
| 访问量 | 服务器配置 | 是否需要LB |
|--------|-----------|-----------|
| <100/天 | 1核2G | ❌ 不需要 |
| 1000-10000/天 | 2核4G | ❌ 不需要 |
| >10000/天 | 4核8G | ✅ 建议配置 |
---
## 8. 开分店:CDN 加速
小明在全国都有客人,北京的服务器响应纽约的请求太慢了。怎么办?开分店!
### 8.1 什么是 CDN
**CDN(内容分发网络)** = **全球连锁仓库**
<DeploymentCdnDemo />
**CDN 的工作原理**
1. 你把图片、CSS、JS等"不变的东西"上传到CDN
2. CDN把这些文件复制到全球各地的节点
3. 用户访问时,CDN自动从最近的节点给文件
**效果**
- 北京用户 → 北京节点(10ms
- 纽约用户 → 纽约节点(15ms
- 伦敦用户 → 伦敦节点(20ms
### 8.2 怎么配置CDN
1. **开通CDN服务**:阿里云CDN/腾讯云CDN/Cloudflare
2. **添加域名**:填写你的网站域名
3. **配置源站**:告诉CDN你的服务器IP
4. **刷新缓存**:文件更新后,手动刷新CDN缓存
---
## 9. 自动化:建立 CI/CD 流程
每次更新代码都要手动SSH到服务器、拉代码、构建、重启,太累!自动化吧!
### 9.1 什么是 CI/CD
### 7.1 什么是 CI/CD
<DeploymentCicdDemo />
**CI(持续集成)** = 自动测试
- **CI(持续集成)**:每次提交自动测试
- **CD(持续部署)**:测试通过自动部署
- 每次提交代码自动运行测试
- 保证代码质量
### 7.2 GitHub Actions 示例
**CD(持续部署)** = 自动化上线
- 代码通过测试后自动部署
- 一键上线,安全可靠
### 9.2 怎么实现 CI/CD
**推荐:GitHub Actions**
在项目根目录创建 `.github/workflows/deploy.yml`
`.github/workflows/deploy.yml`
```yaml
name: Deploy to Production
@@ -387,186 +230,154 @@ jobs:
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install & Build
run: |
npm ci
npm run build
- name: Build
run: npm run build
- name: Deploy to server
- name: Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: root
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/coffee-shop
cd /var/www/my-site
git pull origin main
npm install
npm run build
pm2 restart all
```
**工作流程**
1. 小明提交代码到 GitHub
2. GitHub Actions 自动触发
3. 自动构建 + 自动部署
4. 几分钟后,新版本就上线了!
---
::: details 📖 高级主题:CDN 和负载均衡
**CDN(内容分发网络)**:全球连锁仓库
- 把图片、CSS、JS等静态资源上传到CDN
- CDN复制到全球节点
- 用户访问时从最近节点获取
**效果**:北京用户→北京节点(10ms),纽约用户→纽约节点(15ms)
**负载均衡(Load Balancer**:餐厅领班
- 看着N个服务器
- 把客人分配到最空闲的那个
- 某个服务器挂了,自动切换
**什么时候需要**
- 访问量 >10000/天
- 单台服务器 CPU/内存 >70%
:::
---
## 10. 守夜人:监控和备份
## 8. 监控和备份:守夜人
网站上线了,但工作还没完。就像开店后需要保安和账房,网站也需要**监控**和**备份**。
### 10.1 监控:当个好管家
### 8.1 监控什么?
<DeploymentMonitorDemo />
**需要监控什么?**
| 指标 | 正常范围 |
|------|---------|
| CPU使用率 | <70% |
| 内存使用率 | <80% |
| 磁盘空间 | <80% |
| 响应时间 | <2秒 |
| 错误率 | <1% |
| 指标 | 说明 | 正常范围 |
|------|------|---------|
| **CPU使用率** | 服务器"脑子"有多忙 | <70% |
| **内存使用率** | 服务器"记忆"占多少 | <80% |
| **磁盘空间** | 硬盘还剩多少 | <80% |
| **响应时间** | 网页加载多慢 | <2秒 |
| **错误率** | 有多少请求失败 | <1% |
### 8.2 备份策略
**监控工具推荐**
- **基础监控**云厂商自带的监控(阿里云云监控/腾讯云云监控)
- **APM监控**New Relic / Datadog(收费,功能强大)
- **开源方案**Prometheus + Grafana(免费,需自建)
### 10.2 备份:最后的安全网
**数据是无价的**!一定要定期备份。
<DeploymentBackupDemo />
**备份三要素**
1. **定期备份**:每天凌晨自动备份(用户访问少的时候)
::: tip 💾 备份三要素
1. **定期备份**每天凌晨自动备份
2. **多地存储**:本地 + 异地(防止单点故障)
3. **定期恢复测试**备份了要确保能恢复
**备份策略**
3. **定期恢复测试**:确保备份能恢复
:::
```bash
# 每天自动备份数据库
0 2 * * * /usr/bin/mysqldump -u root -p密码 coffee_shop > /backup/db_$(date +\%Y\%m\%d).sql
0 2 * * * /usr/bin/mysqldump -u root -p密码 my_db > /backup/db_$(date +\%Y\%m\%d).sql
# 保留最近7天的备份
find /backup -name "db_*.sql" -mtime +7 -delete
# 自动上传到云存储(如阿里云OSS
/usr/bin/ossutil cp /backup/db_$(date +\%Y\%m\%d).sql oss://my-backup/db/
# 自动上传到云存储
/usr/bin/ossutil cp /backup/db_$(date +\%Y\%m\%d).sql oss://my-backup/
```
---
## 11. 故障排查:遇到问题怎么办?
网站出问题了,别慌,按这个流程排查:
### 11.1 排查流程图
<DeploymentTroubleshootDemo />
### 11.2 常见问题速查表
## 9. 常见问题速查
| 现象 | 可能原因 | 怎么办 |
|------|---------|--------|
| **网站打不开** | 域名没解析?服务器挂了? | `ping 域名` 看通不通<br>`ssh` 连不上就是服务器挂了 |
| **404 Not Found** | Nginx配置错了?文件路径不对? | 检查 `nginx -t` 配置<br>看看 `root` 路径对不对 |
| **502 Bad Gateway** | 后端服务挂了?端口没开? | `pm2 list` 看服务在不在<br>`netstat -tlnp` 看端口监听 |
| **HTTPS证书报错** | 证书过期?域名不匹配? | `certbot renew` 续期<br>检查证书域名是否正确 |
| **更新不生效** | 浏览器缓存?CDN缓存? | Ctrl+F5 强制刷新<br>去CDN控制台"刷新缓存" |
| **很慢** | 服务器性能不够?CDN没配置? | 查监控看CPU/内存<br>静态资源上CDN |
| 网站打不开 | 域名没解析?服务器挂了? | `ping 域名`看通不通<br>`ssh`连不上就是服务器挂了 |
| 404 Not Found | Nginx配置错了?路径不对? | `nginx -t`检查配置 |
| 502 Bad Gateway | 后端服务挂了?端口没开? | `pm2 list`看服务状态 |
| HTTPS证书报错 | 证书过期?域名不匹配? | `certbot renew`续期 |
| 更新不生效 | 浏览器缓存?CDN缓存? | Ctrl+F5强制刷新<br>去CDN控制台"刷新缓存" |
| 很慢 | 性能不够?CDN没配置? | 查监控看CPU/内存<br>静态资源上CDN |
---
## 12. 上线前最后检查
## 10. 上线前检查清单
小明终于要正式开业了!在按下"发布"按钮前,再检查一遍:
<DeploymentChecklistDemo />
### 最终检查清单
**基础检查**
- [ ] 代码已经构建(`npm run build`
::: details ✅ 基础检查
- [ ] 代码已构建(`npm run build`
- [ ] 服务器环境配置完成(Nginx + Node.js
- [ ] 域名解析正确(能ping通)
- [ ] HTTPS证书正常(有🔒小锁)
:::
**性能检查**
- [ ] 首屏加载时间 <2秒
- [ ] 图片已经压缩优化
- [ ] CDN配置完成
- [ ] 开启了Gzip压缩
**安全检查**
::: details 🔒 安全检查
- [ ] 数据库密码不是弱密码
- [ ] 敏感信息没写在代码里
- [ ] 开启了防火墙(只开必要端口)
- [ ] 开启了防火墙(只开必要端口)
- [ ] 配置了SQL注入防护
:::
**运维检查**
::: details 🛡️ 运维检查
- [ ] 监控告警配置完成
- [ ] 日志正常记录
- [ ] 自动备份已设置
- [ ] CI/CD流程测试通过
**应急预案**
- [ ] 准备了回滚方案
- [ ] 有故障联系机制
- [ ] 备份恢复测试通过
:::
---
## 13. 总结:服务上线的关键点
小明的咖啡店网站终于上线了!让我们回顾一下整个过程:
## 总结
### 核心流程
1. **构建**:把代码打包成浏览器懂的格式
2. **部署**:把代码放到服务器上
3. **配置**Nginx、域名、HTTPS
4. **优化**CDN、负载均衡
5. **自动化**CI/CD解放双手
6. **保障**监控和备份守住底线
1. **构建**代码打包成浏览器懂的格式
2. **部署**代码放到服务器上
3. **配置**Nginx、域名、HTTPS
4. **优化**CDN、负载均衡(高级)
5. **自动化**CI/CD解放双手
6. **保障**监控和备份守住底线
### 关键原则
| 原则 | 说明 |
|------|------|
| **小步快跑** | 先上线MVP(最小可用产品),再逐步完善 |
| **小步快跑** | 先上线MVP,再逐步完善 |
| **自动化优先** | 能自动的别手动,减少人为失误 |
| **监控先行** | 先建监控,再上功能 |
| **备份为王** | 数据无价,备份是最后防线 |
| **备份为王** | 数据无价,备份是最后防线 |
| **安全第一** | HTTPS、防火墙、弱密码检查,一个都不能少 |
### 学习路线
**入门**(第1天)
- 用 Vercel/Netlify 免费部署一个静态网页
**入门(第1天)**:用 Vercel 免费部署静态网页
**进阶**(第1周)
- 租一台云服务器
- 手动部署一个 Node.js 应用
- 配置域名和 HTTPS
**进阶(第1周)**:租云服务器、手动部署 Node.js 应用、配置域名和 HTTPS
**实战**(第2-4周)
- 搭建完整的 CI/CD 流程
- 配置 CDN 加速
- 建立监控和备份体系
**实战(第2-4周)**:搭建完整 CI/CD 流程、配置 CDN、建立监控和备份
**深入**(持续)
- 学习 Docker 容器化部署
- 研究 Kubernetes 集群管理
- 探索微服务架构
**深入(持续)**:学习 Docker 容器化、Kubernetes 集群、微服务架构
---
@@ -574,19 +385,16 @@ find /backup -name "db_*.sql" -mtime +7 -delete
| 名词 | 英文 | 人话解释 |
|------|------|---------|
| **部署** | Deployment | 把代码放到服务器上让人能访问 |
| **构建** | Build | 把源代码翻译打包成浏览器懂的格式 |
| **服务器** | Server | 一台永远不关机、连着互联网的电脑 |
| **域名** | Domain Name | 网站的好记名字(如 baidu.com) |
| **DNS** | Domain Name System | 域名解析系统,把域名翻译成IP |
| **IP地址** | IP Address | 电脑在互联网上的门牌号 |
| **HTTP** | HyperText Transfer Protocol | 网页传输协议(不安全 |
| **HTTPS** | HTTP Secure | 安全的网页传输协议(加密) |
| **Nginx** | Engine X | 一个高性能的Web服务器(门童) |
| **反向代理** | Reverse Proxy | 转发请求到后端服务 |
| **SSH** | Secure Shell | 远程连接服务器的工具 |
| **CDN** | Content Delivery Network | 内容分发网络,全球加速 |
| **负载均衡** | Load Balancing | 把流量分到多台服务器 |
| **CI/CD** | Continuous Integration/Deployment | 持续集成/持续部署(自动化) |
| **监控** | Monitoring | 盯着服务器看有没有问题 |
| **备份** | Backup | 复份数据,防丢失 |
| 部署 | Deployment | 把代码放到服务器上让人能访问 |
| 构建 | Build | 把源代码翻译打包成浏览器懂的格式 |
| 服务器 | Server | 一台永远不关机、连着互联网的电脑 |
| 域名 | Domain Name | 网站的好记名字(如 baidu.com) |
| DNS | Domain Name System | 域名解析系统,把域名翻译成IP |
| HTTP/HTTPS | HyperText Transfer Protocol | 网页传输协议(HTTP不安全,HTTPS加密) |
| Nginx | Engine X | 高性能Web服务器(门童 |
| 反向代理 | Reverse Proxy | 转发请求到后端服务 |
| SSH | Secure Shell | 远程连接服务器的工具 |
| CDN | Content Delivery Network | 内容分发网络,全球加速 |
| CI/CD | Continuous Integration/Deployment | 持续集成/持续部署(自动化) |
| 监控 | Monitoring | 盯着服务器看有没有问题 |
| 备份 | Backup | 备份数据,防丢失 |