Files
test-repo/docs/.vitepress/theme/components/appendix/infrastructure-as-code/ConfigDriftDemo.vue
T
sanbuphy 3af119a598 feat(appendix): 添加多个交互式演示组件,完善 AI/Infra 等章节内容
- 新增 Vibe Coding 全栈相关演示组件 (DeveloperSkillShift, FrontendTriad, BackendCore 等)
- 新增 RAG 相关组件 (RAGPipeline, ChunkingStrategy, Retrieval 等)
- 新增 Embedding & Vector 相关组件 (EmbeddingConcept, VectorSimilarity 等)
- 新增 AI Native App 设计组件 (AINativeArch, PromptDesign 等)
- 新增 Infrastructure as Code 组件 (IaCConcept, TerraformWorkflow 等)
- 新增 DNS & HTTPS 演示组件 (DnsResolution, HttpsHandshake 等)
- 新增 Model Finetuning 组件 (FinetuningPipeline 等)
- 更新多个章节的 markdown 内容,集成交互式演示
2026-02-24 18:22:58 +08:00

361 lines
11 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="config-drift-demo">
<div class="demo-label">交互演示 配置漂移无声的定时炸弹</div>
<div class="timeline">
<div class="timeline-track">
<div
v-for="(event, i) in events"
:key="i"
:class="['timeline-node', event.type, { active: step >= i }]"
@click="goToStep(i)"
>
<div class="node-dot"></div>
<div class="node-label">{{ event.label }}</div>
</div>
</div>
</div>
<div class="scene-area">
<div class="infra-visual">
<div class="server-group">
<div class="group-title">期望状态代码定义</div>
<div class="server-cards">
<div v-for="s in expectedServers" :key="s.name" class="server-card expected">
<div class="server-icon">🖥</div>
<div class="server-name">{{ s.name }}</div>
<div class="server-config">{{ s.config }}</div>
</div>
</div>
</div>
<div class="drift-indicator">
<div :class="['drift-status', driftLevel]">
<span class="drift-icon">{{ driftIcon }}</span>
<span class="drift-text">{{ driftText }}</span>
</div>
</div>
<div class="server-group">
<div class="group-title">实际状态线上环境</div>
<div class="server-cards">
<div
v-for="s in actualServers"
:key="s.name"
:class="['server-card', 'actual', { drifted: s.drifted }]"
>
<div class="server-icon">{{ s.drifted ? '⚠️' : '🖥️' }}</div>
<div class="server-name">{{ s.name }}</div>
<div class="server-config">{{ s.config }}</div>
<div v-if="s.driftReason" class="drift-reason">{{ s.driftReason }}</div>
</div>
</div>
</div>
</div>
<div class="event-desc">
<div class="event-title">{{ events[step].title }}</div>
<p class="event-detail">{{ events[step].detail }}</p>
</div>
</div>
<div class="controls">
<button class="ctrl-btn" :disabled="step === 0" @click="goToStep(step - 1)"> 上一步</button>
<button class="ctrl-btn reset" @click="goToStep(0)">重置</button>
<button class="ctrl-btn primary" :disabled="step >= events.length - 1" @click="goToStep(step + 1)">
下一步
</button>
</div>
<div class="lesson-box">
<div class="lesson-title">关键教训</div>
<div class="lesson-items">
<div v-for="(lesson, i) in lessons" :key="i" class="lesson-item">
<span class="lesson-icon">{{ lesson.icon }}</span>
<span class="lesson-text">{{ lesson.text }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const step = ref(0)
const events = [
{
label: '初始部署',
type: 'good',
title: '第 0 步:通过 IaC 初始部署',
detail: '团队使用 Terraform 部署了 3 台 Web 服务器,配置完全一致:Nginx 1.24、端口 443、2GB 内存。代码和实际状态完美匹配。'
},
{
label: '手动修改',
type: 'warn',
title: '第 1 步:深夜紧急手动修改',
detail: '凌晨 3 点,Server-B 出现性能问题。值班工程师直接 SSH 登录,手动将内存从 2GB 升级到 4GB,并修改了 Nginx 配置。没有更新 IaC 代码。'
},
{
label: '又一次修改',
type: 'warn',
title: '第 2 步:另一位同事的"临时"调整',
detail: '一周后,另一位工程师为了调试,在 Server-C 上开放了 22 端口(SSH),并安装了调试工具。同样没有更新代码。'
},
{
label: '漂移加剧',
type: 'bad',
title: '第 3 步:配置漂移已经失控',
detail: '此时 3 台"相同"的服务器实际配置已经各不相同。代码描述的状态和线上真实状态严重脱节,没有人能说清楚线上到底是什么配置。'
},
{
label: 'IaC 检测',
type: 'fix',
title: '第 4 步:terraform plan 发现漂移',
detail: '运行 terraform plan 后,Terraform 对比 State 文件和实际资源,清晰列出所有差异。团队决定将手动变更回退,统一通过代码管理。'
}
]
const expectedServers = [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB' },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB' },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB' }
]
const actualServers = computed(() => {
if (step.value === 0) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
}
if (step.value === 1) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.25 | 443 | 4GB', drifted: true, driftReason: '手动升级内存和 Nginx' },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
}
if (step.value === 2 || step.value === 3) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.25 | 443 | 4GB', drifted: true, driftReason: '手动升级内存和 Nginx' },
{ name: 'Server-C', config: 'Nginx 1.24 | 22+443 | 2GB', drifted: true, driftReason: '开放了 SSH 端口' }
]
}
// step 4: fix
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
})
const driftLevel = computed(() => {
if (step.value === 0 || step.value === 4) return 'ok'
if (step.value <= 2) return 'warning'
return 'danger'
})
const driftIcon = computed(() => {
if (driftLevel.value === 'ok') return '✅'
if (driftLevel.value === 'warning') return '⚠️'
return '🔥'
})
const driftText = computed(() => {
if (step.value === 0) return '状态一致'
if (step.value === 4) return '漂移已修复'
if (step.value === 1) return '1 台漂移'
if (step.value === 2) return '2 台漂移'
return '严重漂移!'
})
const lessons = [
{ icon: '🚫', text: '禁止手动修改线上环境,所有变更必须通过代码' },
{ icon: '🔍', text: '定期运行 terraform plan 检测漂移' },
{ icon: '🔒', text: '限制生产环境的 SSH 权限,减少人为干预' },
{ icon: '📋', text: '建立变更审批流程(PR → Review → Merge → Apply' }
]
function goToStep(i) {
step.value = Math.max(0, Math.min(i, events.length - 1))
}
</script>
<style scoped>
.config-drift-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
text-align: center;
}
.timeline { margin-bottom: 1rem; overflow-x: auto; }
.timeline-track {
display: flex;
align-items: flex-start;
gap: 0;
min-width: max-content;
position: relative;
padding: 0 0.5rem;
}
.timeline-node {
flex: 1;
min-width: 90px;
text-align: center;
cursor: pointer;
position: relative;
padding-top: 20px;
}
.timeline-node::before {
content: '';
position: absolute;
top: 8px;
left: 0;
right: 0;
height: 2px;
background: var(--vp-c-divider);
}
.timeline-node:first-child::before { left: 50%; }
.timeline-node:last-child::before { right: 50%; }
.node-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
margin: 0 auto 4px;
position: relative;
z-index: 1;
transition: all 0.3s;
}
.timeline-node.active .node-dot { transform: scale(1.3); }
.timeline-node.active.good .node-dot { background: #10b981; border-color: #10b981; }
.timeline-node.active.warn .node-dot { background: #f59e0b; border-color: #f59e0b; }
.timeline-node.active.bad .node-dot { background: #ef4444; border-color: #ef4444; }
.timeline-node.active.fix .node-dot { background: #3b82f6; border-color: #3b82f6; }
.node-label { font-size: 0.68rem; color: var(--vp-c-text-3); }
.timeline-node.active .node-label { font-weight: 600; color: var(--vp-c-text-1); }
.scene-area { margin-bottom: 1rem; }
.infra-visual {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.8rem;
}
.group-title {
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.3rem;
text-align: center;
}
.server-cards {
display: flex;
gap: 0.4rem;
justify-content: center;
flex-wrap: wrap;
}
.server-card {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.6rem;
background: var(--vp-c-bg);
text-align: center;
min-width: 120px;
transition: all 0.3s;
font-size: 0.73rem;
}
.server-card.expected { border-color: #10b981; }
.server-card.drifted {
border-color: #ef4444;
background: #fef2f210;
box-shadow: 0 0 0 1px #fca5a540;
}
.server-icon { font-size: 1.2rem; }
.server-name { font-weight: 600; font-size: 0.75rem; }
.server-config { font-size: 0.68rem; color: var(--vp-c-text-2); }
.drift-reason {
font-size: 0.62rem;
color: #ef4444;
margin-top: 2px;
font-style: italic;
}
.drift-indicator { text-align: center; }
.drift-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 14px;
border-radius: 16px;
font-size: 0.78rem;
font-weight: 600;
}
.drift-status.ok { background: #d1fae5; color: #065f46; }
.drift-status.warning { background: #fef3c7; color: #92400e; }
.drift-status.danger { background: #fee2e2; color: #991b1b; }
:root.dark .drift-status.ok { background: #022c2240; color: #6ee7b7; }
:root.dark .drift-status.warning { background: #451a0340; color: #fcd34d; }
:root.dark .drift-status.danger { background: #450a0a40; color: #fca5a5; }
.event-desc {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.8rem;
background: var(--vp-c-bg);
}
.event-title { font-weight: 600; font-size: 0.88rem; margin-bottom: 4px; }
.event-detail { font-size: 0.8rem; color: var(--vp-c-text-2); line-height: 1.6; margin: 0; }
.controls {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.ctrl-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.78rem;
transition: all 0.2s;
}
.ctrl-btn:disabled { opacity: 0.4; cursor: default; }
.ctrl-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.ctrl-btn.reset { color: var(--vp-c-text-3); }
.lesson-box {
border: 1px solid #3b82f640;
border-radius: 6px;
padding: 0.8rem;
background: #dbeafe10;
}
.lesson-title {
font-weight: 600;
font-size: 0.82rem;
margin-bottom: 0.5rem;
color: #2563eb;
}
:root.dark .lesson-title { color: #93c5fd; }
.lesson-items { display: flex; flex-direction: column; gap: 0.3rem; }
.lesson-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
}
</style>