feat(docs): enhance interactive demos and improve documentation

- Add new interactive components for frontend routing, browser rendering pipeline, and database transactions
- Improve existing demos with better visuals, explanations, and examples
- Update documentation structure and content for better clarity
- Add new utility scripts and update package.json with new commands
- Fix formatting and alignment in documentation tables
This commit is contained in:
sanbuphy
2026-02-13 22:10:03 +08:00
parent 599052b2e0
commit d174ceea32
88 changed files with 26273 additions and 15539 deletions
@@ -0,0 +1,524 @@
<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>
@@ -0,0 +1,519 @@
<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>
<template>
<div class="deployment-build">
<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><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>
</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>
</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>
</div>
</div>
</template>
<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;
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;
}
.source-files {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.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;
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;
margin-bottom: 1rem;
}
.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 {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
position: relative;
}
.size-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.size-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.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 {
color: var(--vp-c-brand-delta);
}
.size-arrow {
font-size: 1.2rem;
color: var(--vp-c-brand);
}
.compression-badge {
position: absolute;
top: -0.5rem;
right: 1rem;
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-radius: 6px;
font-size: 0.95rem;
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);
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;
}
}
</style>
@@ -0,0 +1,414 @@
<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>
@@ -0,0 +1,474 @@
<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>
@@ -0,0 +1,358 @@
<template>
<div class="deployment-cicd-demo">
<div class="demo-header">
<span class="icon">🔄</span>
<span class="title">CI/CD 自动化流程</span>
<span class="subtitle">从代码到上线一键搞定</span>
</div>
<div class="intro-text">
<strong>CI/CD</strong> 就像一条<strong>自动化流水线</strong>你只管写代码剩下的测试构建部署流水线自动帮你完成
</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>
<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>
<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 {
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;
}
.pipeline {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
overflow-x: auto;
padding: 0.5rem 0;
}
.pipeline-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 {
text-align: center;
z-index: 1;
}
.step-title {
font-weight: 600;
font-size: 0.8rem;
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 {
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>
@@ -0,0 +1,315 @@
<template>
<div class="deployment-dns-demo">
<div class="demo-header">
<span class="icon">🔍</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>
<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>
</div>
<button @click="playAnimation" class="play-btn">
{{ isPlaying ? '▶ 重新播放' : '▶ 播放动画' }}
</button>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>关键点</strong>DNS 修改不是立即生效的全球同步需要几分钟到几小时 TTL 影响
</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 {
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;
}
.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;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.flow-arrow {
font-size: 1.25rem;
color: var(--vp-c-text-3);
}
.example-box {
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 {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.record-item {
display: flex;
align-items: center;
gap: 0.5rem;
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;
min-width: 60px;
text-align: center;
}
.record-name {
color: var(--vp-c-text-2);
flex: 1;
font-family: monospace;
}
.record-arrow {
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);
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) {
.dns-flow {
flex-wrap: wrap;
}
.flow-arrow {
display: none;
}
.flow-step {
min-width: 45%;
}
}
</style>
@@ -0,0 +1,395 @@
<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>
@@ -0,0 +1,344 @@
<template>
<div class="deployment-https-demo">
<div class="demo-header">
<span class="icon">🔒</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>
<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>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>推荐工具:</strong>Let's Encrypt 免费证书 + Certbot 自动工具几分钟就能搞定 HTTPS
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.deployment-https-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;
}
.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;
color: var(--vp-c-text-2);
}
.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 {
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);
}
.step-num {
display: flex;
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;
}
.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) {
.comparison {
grid-template-columns: 1fr;
}
.data-flow {
flex-wrap: wrap;
}
}
</style>
@@ -0,0 +1,465 @@
<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>
@@ -0,0 +1,525 @@
<script setup>
import { ref, onMounted, onUnmounted } 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' }
])
let interval = null
const toggleMonitoring = () => {
isMonitoring.value = !isMonitoring.value
if (isMonitoring.value) {
startSimulation()
} else {
stopSimulation()
}
}
const startSimulation = () => {
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()
})
</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;
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 {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.metric-icon {
font-size: 1.2rem;
}
.metric-name {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.metric-value {
font-size: 1.8rem;
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 {
display: flex;
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;
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) {
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -0,0 +1,460 @@
<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>
@@ -0,0 +1,300 @@
<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>
</div>
<div class="demo-content">
<!-- 进度条 -->
<div class="progress-section">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${stepProgress}%` }"></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>
</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>
</div>
<div class="info-box">
<p>💡 <strong>关键要点</strong>每个环节都不可或缺跳过测试就上线就像没试喝就卖咖啡可能让顾客喝到难喝的咖啡Bug</p>
</div>
</div>
</template>
<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;
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;
}
.progress-section {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.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;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.step-card:hover {
border-color: var(--vp-c-brand-soft);
transform: translateY(-2px);
}
.step-card.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;
}
.step-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.step-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.3;
}
.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 {
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);
}
.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) {
.steps-container {
grid-template-columns: repeat(2, 1fr);
}
.step-card {
min-height: 100px;
padding: 0.75rem;
}
.step-icon {
font-size: 1.5rem;
}
}
</style>
@@ -0,0 +1,561 @@
<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>
@@ -0,0 +1,426 @@
<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>
<template>
<div class="deployment-server">
<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><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>
<!-- 服务器配置 -->
<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>
<!-- 服务器类型选择 -->
<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>
</div>
<div class="info-box">
<p>
💡 <strong>小明建议</strong>刚开始用云服务器最合适就像开咖啡店先租个小店面测试生意
客流量大了再升级或开分店集群部署<strong>不要一开始就租豪华店面</strong>
</p>
</div>
</div>
</template>
<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;
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;
}
.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 {
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);
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;
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;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.scenario-cards {
grid-template-columns: repeat(2, 1fr);
}
.config-cards {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,456 @@
<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>