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 内容,集成交互式演示
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
<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>
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="iac-best-practice-demo">
|
||||
<div class="demo-label">交互演示 ── IaC 最佳实践</div>
|
||||
|
||||
<div class="practice-tabs">
|
||||
<button
|
||||
v-for="(tab, i) in practices"
|
||||
:key="tab.key"
|
||||
:class="['practice-tab', { active: activeTab === i }]"
|
||||
@click="activeTab = i"
|
||||
>
|
||||
<span class="tab-icon">{{ tab.icon }}</span>
|
||||
<span class="tab-name">{{ tab.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="activeTab" class="practice-content">
|
||||
<div class="practice-header">
|
||||
<span class="practice-icon">{{ currentPractice.icon }}</span>
|
||||
<div>
|
||||
<div class="practice-title">{{ currentPractice.title }}</div>
|
||||
<div class="practice-subtitle">{{ currentPractice.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="do-dont-grid">
|
||||
<div class="do-card">
|
||||
<div class="card-label good-label">✅ 推荐做法</div>
|
||||
<div class="card-items">
|
||||
<div v-for="(item, i) in currentPractice.dos" :key="i" class="card-item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-card">
|
||||
<div class="card-label bad-label">❌ 反面模式</div>
|
||||
<div class="card-items">
|
||||
<div v-for="(item, i) in currentPractice.donts" :key="i" class="card-item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentPractice.code" class="code-example">
|
||||
<div class="code-header">
|
||||
<span>{{ currentPractice.codeTitle }}</span>
|
||||
</div>
|
||||
<pre class="code-body"><code>{{ currentPractice.code }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="maturity-bar">
|
||||
<div class="maturity-label">实践成熟度</div>
|
||||
<div class="maturity-track">
|
||||
<div
|
||||
v-for="(level, i) in maturityLevels"
|
||||
:key="i"
|
||||
:class="['maturity-segment', { filled: i <= currentPractice.maturity }]"
|
||||
>
|
||||
<span class="maturity-text">{{ level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeTab = ref(0)
|
||||
const maturityLevels = ['入门', '基础', '进阶', '成熟', '卓越']
|
||||
|
||||
const practices = [
|
||||
{
|
||||
key: 'vcs', icon: '📂', name: '版本控制',
|
||||
title: '实践一:基础设施代码纳入版本控制',
|
||||
subtitle: '像管理应用代码一样管理基础设施代码',
|
||||
dos: [
|
||||
'所有 .tf 文件提交到 Git 仓库',
|
||||
'使用分支策略(main / dev / feature)',
|
||||
'通过 Pull Request 进行代码审查',
|
||||
'在 CI 中自动运行 terraform plan'
|
||||
],
|
||||
donts: [
|
||||
'在本地执行 apply 后不提交代码',
|
||||
'直接在 main 分支上修改',
|
||||
'将 .tfstate 文件提交到 Git',
|
||||
'跳过 Code Review 直接部署'
|
||||
],
|
||||
codeTitle: '.gitignore 示例',
|
||||
code: `# 忽略本地状态文件
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
.terraform/
|
||||
|
||||
# 忽略敏感变量文件
|
||||
*.tfvars
|
||||
!example.tfvars`,
|
||||
maturity: 1
|
||||
},
|
||||
{
|
||||
key: 'modules', icon: '🧩', name: '模块化',
|
||||
title: '实践二:使用模块实现代码复用',
|
||||
subtitle: '避免复制粘贴,通过模块封装通用基础设施模式',
|
||||
dos: [
|
||||
'将通用模式抽取为可复用模块',
|
||||
'模块使用语义化版本号',
|
||||
'为模块编写 README 和使用示例',
|
||||
'通过 variables 暴露可配置参数'
|
||||
],
|
||||
donts: [
|
||||
'在多个项目中复制粘贴相同代码',
|
||||
'创建过于庞大的"万能"模块',
|
||||
'模块内硬编码环境特定的值',
|
||||
'不写文档直接发布模块'
|
||||
],
|
||||
codeTitle: '模块调用示例',
|
||||
code: `module "web_server" {
|
||||
source = "./modules/ec2-instance"
|
||||
version = "2.1.0"
|
||||
|
||||
instance_type = "t3.micro"
|
||||
environment = "production"
|
||||
app_name = "my-web-app"
|
||||
}`,
|
||||
maturity: 2
|
||||
},
|
||||
{
|
||||
key: 'state', icon: '💾', name: '状态管理',
|
||||
title: '实践三:远程状态存储与锁定',
|
||||
subtitle: 'State 文件是 IaC 的核心,必须安全可靠地管理',
|
||||
dos: [
|
||||
'使用远程后端(S3 + DynamoDB)',
|
||||
'启用状态文件加密',
|
||||
'配置状态锁防止并发冲突',
|
||||
'按环境/项目隔离状态文件'
|
||||
],
|
||||
donts: [
|
||||
'将 State 存储在本地文件系统',
|
||||
'多人共享同一个 State 无锁机制',
|
||||
'手动编辑 terraform.tfstate',
|
||||
'所有环境共用一个 State 文件'
|
||||
],
|
||||
codeTitle: '远程后端配置',
|
||||
code: `terraform {
|
||||
backend "s3" {
|
||||
bucket = "my-tf-state"
|
||||
key = "prod/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
encrypt = true
|
||||
dynamodb_table = "tf-lock"
|
||||
}
|
||||
}`,
|
||||
maturity: 2
|
||||
},
|
||||
{
|
||||
key: 'env', icon: '🌍', name: '环境管理',
|
||||
title: '实践四:多环境一致性管理',
|
||||
subtitle: '开发、测试、生产环境使用相同代码,仅参数不同',
|
||||
dos: [
|
||||
'使用 Workspace 或目录隔离环境',
|
||||
'通过 .tfvars 文件区分环境参数',
|
||||
'保持环境间代码结构完全一致',
|
||||
'先在 dev 验证,再推广到 prod'
|
||||
],
|
||||
donts: [
|
||||
'为每个环境维护独立的代码副本',
|
||||
'在代码中硬编码环境名称',
|
||||
'跳过测试环境直接部署生产',
|
||||
'不同环境使用不同的模块版本'
|
||||
],
|
||||
codeTitle: '多环境目录结构',
|
||||
code: `environments/
|
||||
├── dev/
|
||||
│ ├── main.tf # 引用相同模块
|
||||
│ └── dev.tfvars # 开发环境参数
|
||||
├── staging/
|
||||
│ ├── main.tf
|
||||
│ └── staging.tfvars
|
||||
└── prod/
|
||||
├── main.tf
|
||||
└── prod.tfvars`,
|
||||
maturity: 3
|
||||
}
|
||||
]
|
||||
|
||||
const currentPractice = computed(() => practices[activeTab.value])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iac-best-practice-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;
|
||||
}
|
||||
.practice-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.practice-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.practice-tab.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.practice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.practice-icon { font-size: 1.5rem; }
|
||||
.practice-title { font-weight: 600; font-size: 0.95rem; }
|
||||
.practice-subtitle { font-size: 0.78rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.do-dont-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.do-dont-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.do-card, .dont-card {
|
||||
border-radius: 6px;
|
||||
padding: 0.7rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.do-card { background: #d1fae508; border-color: #6ee7b740; }
|
||||
.dont-card { background: #fee2e208; border-color: #fca5a540; }
|
||||
.card-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.good-label { color: #10b981; }
|
||||
.bad-label { color: #ef4444; }
|
||||
.card-items { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.card-item {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
padding-left: 0.5rem;
|
||||
border-left: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.code-example {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.code-header {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 4px 10px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.code-body {
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
padding: 0.8rem;
|
||||
font-size: 0.73rem;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.maturity-bar { margin-top: 0.5rem; }
|
||||
.maturity-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.maturity-track {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
.maturity-segment {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-text-3);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.maturity-segment.filled {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div class="iac-concept-demo">
|
||||
<div class="demo-label">交互演示 ── 手动运维 vs 基础设施即代码</div>
|
||||
|
||||
<div class="toggle-bar">
|
||||
<button
|
||||
v-for="mode in modes"
|
||||
:key="mode.key"
|
||||
:class="['toggle-btn', { active: current === mode.key }]"
|
||||
@click="current = mode.key"
|
||||
>
|
||||
{{ mode.icon }} {{ mode.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scene-container">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="current === 'manual'" key="manual" class="scene manual-scene">
|
||||
<div class="scene-title">手动运维流程</div>
|
||||
<div class="steps">
|
||||
<div
|
||||
v-for="(step, i) in manualSteps"
|
||||
:key="i"
|
||||
:class="['step-card', { done: manualProgress > i, current: manualProgress === i }]"
|
||||
>
|
||||
<div class="step-num">{{ i + 1 }}</div>
|
||||
<div class="step-icon">{{ step.icon }}</div>
|
||||
<div class="step-text">{{ step.text }}</div>
|
||||
<div class="step-risk">{{ step.risk }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-btn manual-btn" @click="advanceManual" :disabled="manualProgress >= manualSteps.length">
|
||||
{{ manualProgress >= manualSteps.length ? '全部完成(耗时约 2 小时)' : '点击控制台按钮...' }}
|
||||
</button>
|
||||
<div v-if="manualProgress >= manualSteps.length" class="result-box warning">
|
||||
手动操作完成,但存在风险:步骤不可重复、无法审计、容易遗漏配置。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else key="iac" class="scene iac-scene">
|
||||
<div class="scene-title">IaC 代码驱动流程</div>
|
||||
<div class="code-block">
|
||||
<div class="code-header">main.tf</div>
|
||||
<pre class="code-content"><code>{{ iacCode }}</code></pre>
|
||||
</div>
|
||||
<div class="iac-steps">
|
||||
<div
|
||||
v-for="(step, i) in iacSteps"
|
||||
:key="i"
|
||||
:class="['iac-step', { done: iacProgress > i, current: iacProgress === i }]"
|
||||
>
|
||||
<span class="iac-arrow" v-if="i > 0">→</span>
|
||||
<span class="iac-badge">{{ step.icon }}</span>
|
||||
<span class="iac-label">{{ step.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-btn iac-btn" @click="advanceIac" :disabled="iacProgress >= iacSteps.length">
|
||||
{{ iacProgress >= iacSteps.length ? '全部完成(耗时约 30 秒)' : '执行下一步' }}
|
||||
</button>
|
||||
<div v-if="iacProgress >= iacSteps.length" class="result-box success">
|
||||
代码即文档,可重复、可审计、可版本控制,团队协作无忧。
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>对比维度</th>
|
||||
<th>手动运维</th>
|
||||
<th>基础设施即代码</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in comparisonRows" :key="row.dim">
|
||||
<td class="dim-cell">{{ row.dim }}</td>
|
||||
<td class="bad-cell">{{ row.manual }}</td>
|
||||
<td class="good-cell">{{ row.iac }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const current = ref('manual')
|
||||
const manualProgress = ref(0)
|
||||
const iacProgress = ref(0)
|
||||
|
||||
const modes = [
|
||||
{ key: 'manual', icon: '🖱️', label: '手动运维' },
|
||||
{ key: 'iac', icon: '📝', label: '基础设施即代码' }
|
||||
]
|
||||
|
||||
const manualSteps = [
|
||||
{ icon: '🌐', text: '登录云控制台', risk: '需要记住密码' },
|
||||
{ icon: '🖥️', text: '手动创建服务器', risk: '配置可能遗漏' },
|
||||
{ icon: '🔧', text: '配置安全组规则', risk: '容易开放过多端口' },
|
||||
{ icon: '💾', text: '挂载存储卷', risk: '大小可能选错' },
|
||||
{ icon: '🔗', text: '配置负载均衡', risk: '路由规则易出错' },
|
||||
{ icon: '📋', text: '手动记录到文档', risk: '文档很快过时' }
|
||||
]
|
||||
|
||||
const iacSteps = [
|
||||
{ icon: '📝', text: 'Write(编写代码)' },
|
||||
{ icon: '🔍', text: 'Plan(预览变更)' },
|
||||
{ icon: '🚀', text: 'Apply(自动执行)' },
|
||||
{ icon: '✅', text: 'Done(状态记录)' }
|
||||
]
|
||||
|
||||
const iacCode = `resource "aws_instance" "web" {
|
||||
ami = "ami-0c55b159"
|
||||
instance_type = "t3.micro"
|
||||
|
||||
tags = {
|
||||
Name = "web-server"
|
||||
Env = "production"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_security_group" "web_sg" {
|
||||
ingress {
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
}`
|
||||
|
||||
const comparisonRows = [
|
||||
{ dim: '可重复性', manual: '每次操作可能不同', iac: '代码保证完全一致' },
|
||||
{ dim: '速度', manual: '分钟到小时级', iac: '秒到分钟级' },
|
||||
{ dim: '审计追踪', manual: '依赖人工记录', iac: 'Git 历史自动记录' },
|
||||
{ dim: '协作', manual: '口头传达、截图', iac: 'Code Review、PR 流程' },
|
||||
{ dim: '回滚', manual: '几乎不可能', iac: 'git revert 一键回滚' }
|
||||
]
|
||||
|
||||
function advanceManual() {
|
||||
if (manualProgress.value < manualSteps.length) {
|
||||
manualProgress.value++
|
||||
}
|
||||
}
|
||||
|
||||
function advanceIac() {
|
||||
if (iacProgress.value < iacSteps.length) {
|
||||
iacProgress.value++
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iac-concept-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;
|
||||
}
|
||||
.toggle-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.toggle-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.toggle-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.scene-container { min-height: 200px; }
|
||||
.scene-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
.steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.step-card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
background: var(--vp-c-bg);
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.step-card.done { opacity: 1; border-color: #f59e0b; background: #fef3c710; }
|
||||
.step-card.current { opacity: 1; border-color: var(--vp-c-brand); box-shadow: 0 0 0 2px var(--vp-c-brand-light); }
|
||||
.step-num { font-size: 0.65rem; color: var(--vp-c-text-3); }
|
||||
.step-icon { font-size: 1.4rem; margin: 4px 0; }
|
||||
.step-text { font-size: 0.75rem; font-weight: 600; }
|
||||
.step-risk { font-size: 0.65rem; color: #ef4444; margin-top: 2px; }
|
||||
.action-btn {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.action-btn:disabled { opacity: 0.6; cursor: default; }
|
||||
.manual-btn { background: #fbbf24; color: #78350f; }
|
||||
.iac-btn { background: var(--vp-c-brand); color: #fff; }
|
||||
.code-block {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.code-header {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 4px 10px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.code-content {
|
||||
padding: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
.iac-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.iac-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.78rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
opacity: 0.4;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.iac-step.done { opacity: 1; border-color: #10b981; background: #d1fae510; }
|
||||
.iac-step.current { opacity: 1; border-color: var(--vp-c-brand); box-shadow: 0 0 0 2px var(--vp-c-brand-light); }
|
||||
.iac-arrow { color: var(--vp-c-text-3); font-size: 0.8rem; }
|
||||
.result-box {
|
||||
margin-top: 0.8rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
.result-box.warning { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
.result-box.success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
:root.dark .result-box.warning { background: #451a0320; color: #fcd34d; }
|
||||
:root.dark .result-box.success { background: #022c2220; color: #6ee7b7; }
|
||||
.comparison-table { margin-top: 1rem; overflow-x: auto; }
|
||||
.comparison-table table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
|
||||
.comparison-table th, .comparison-table td {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
text-align: center;
|
||||
}
|
||||
.comparison-table th { background: var(--vp-c-bg-alt); font-weight: 600; }
|
||||
.dim-cell { font-weight: 600; }
|
||||
.bad-cell { color: #ef4444; }
|
||||
.good-cell { color: #10b981; }
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
<template>
|
||||
<div class="iac-tool-comparison-demo">
|
||||
<div class="demo-label">交互演示 ── 主流 IaC 工具对比</div>
|
||||
|
||||
<div class="tool-selector">
|
||||
<span class="selector-hint">选择要对比的工具(至少选 2 个):</span>
|
||||
<div class="tool-chips">
|
||||
<button
|
||||
v-for="tool in tools"
|
||||
:key="tool.name"
|
||||
:class="['tool-chip', { selected: selectedTools.includes(tool.name) }]"
|
||||
:style="selectedTools.includes(tool.name) ? { background: tool.color, borderColor: tool.color, color: '#fff' } : {}"
|
||||
@click="toggleTool(tool.name)"
|
||||
>
|
||||
{{ tool.icon }} {{ tool.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedTools.length >= 2" class="comparison-grid">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="feature-col">特性</th>
|
||||
<th v-for="name in selectedTools" :key="name" class="tool-col">
|
||||
<span class="tool-header-icon">{{ getToolByName(name).icon }}</span>
|
||||
<span>{{ name }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="feature in features" :key="feature.key">
|
||||
<td class="feature-cell">{{ feature.label }}</td>
|
||||
<td v-for="name in selectedTools" :key="name" class="value-cell">
|
||||
<span :class="getCellClass(name, feature.key)">
|
||||
{{ getToolByName(name).features[feature.key] }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-hint">
|
||||
请至少选择 2 个工具进行对比
|
||||
</div>
|
||||
|
||||
<Transition name="fade">
|
||||
<div v-if="selectedDetail" class="detail-card">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ selectedDetail.icon }}</span>
|
||||
<span class="detail-name">{{ selectedDetail.name }}</span>
|
||||
<button class="close-btn" @click="detailName = ''">✕</button>
|
||||
</div>
|
||||
<p class="detail-desc">{{ selectedDetail.desc }}</p>
|
||||
<div class="detail-code">
|
||||
<div class="code-label">示例代码片段:</div>
|
||||
<pre class="code-block"><code>{{ selectedDetail.example }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="detail-hint" v-if="selectedTools.length >= 2 && !detailName">
|
||||
点击下方工具名称查看详细介绍和代码示例
|
||||
</div>
|
||||
<div class="tool-detail-btns" v-if="selectedTools.length >= 2">
|
||||
<button
|
||||
v-for="name in selectedTools"
|
||||
:key="name"
|
||||
:class="['detail-btn', { active: detailName === name }]"
|
||||
@click="detailName = detailName === name ? '' : name"
|
||||
>
|
||||
{{ getToolByName(name).icon }} {{ name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selectedTools = ref(['Terraform', 'CloudFormation'])
|
||||
const detailName = ref('')
|
||||
|
||||
const selectedDetail = computed(() => {
|
||||
if (!detailName.value) return null
|
||||
return tools.find(t => t.name === detailName.value)
|
||||
})
|
||||
|
||||
const features = [
|
||||
{ key: 'vendor', label: '厂商' },
|
||||
{ key: 'language', label: '配置语言' },
|
||||
{ key: 'style', label: '声明式/命令式' },
|
||||
{ key: 'multiCloud', label: '多云支持' },
|
||||
{ key: 'stateManagement', label: '状态管理' },
|
||||
{ key: 'learning', label: '学习曲线' },
|
||||
{ key: 'community', label: '社区生态' },
|
||||
{ key: 'bestFor', label: '最佳场景' }
|
||||
]
|
||||
|
||||
const tools = [
|
||||
{
|
||||
name: 'Terraform',
|
||||
icon: '🟣',
|
||||
color: '#7c3aed',
|
||||
features: {
|
||||
vendor: 'HashiCorp',
|
||||
language: 'HCL',
|
||||
style: '声明式',
|
||||
multiCloud: '原生多云',
|
||||
stateManagement: 'State 文件',
|
||||
learning: '中等',
|
||||
community: '非常活跃',
|
||||
bestFor: '多云/混合云'
|
||||
},
|
||||
desc: 'Terraform 是目前最流行的开源 IaC 工具,由 HashiCorp 开发。它使用自研的 HCL 语言,通过 Provider 机制支持几乎所有主流云平台。',
|
||||
example: `resource "aws_s3_bucket" "data" {
|
||||
bucket = "my-data-bucket"
|
||||
tags = { Env = "prod" }
|
||||
}
|
||||
|
||||
resource "aws_instance" "web" {
|
||||
ami = "ami-0c55b159"
|
||||
instance_type = "t3.micro"
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: 'CloudFormation',
|
||||
icon: '🟠',
|
||||
color: '#ea580c',
|
||||
features: {
|
||||
vendor: 'AWS',
|
||||
language: 'YAML / JSON',
|
||||
style: '声明式',
|
||||
multiCloud: '仅 AWS',
|
||||
stateManagement: 'AWS 托管',
|
||||
learning: '中等偏高',
|
||||
community: 'AWS 生态',
|
||||
bestFor: '纯 AWS 环境'
|
||||
},
|
||||
desc: 'CloudFormation 是 AWS 原生的 IaC 服务,与 AWS 服务深度集成。状态由 AWS 自动管理,无需额外维护 State 文件。',
|
||||
example: `Resources:
|
||||
WebServer:
|
||||
Type: AWS::EC2::Instance
|
||||
Properties:
|
||||
ImageId: ami-0c55b159
|
||||
InstanceType: t3.micro
|
||||
Tags:
|
||||
- Key: Name
|
||||
Value: web-server`
|
||||
},
|
||||
{
|
||||
name: 'Pulumi',
|
||||
icon: '🔵',
|
||||
color: '#2563eb',
|
||||
features: {
|
||||
vendor: 'Pulumi',
|
||||
language: 'TypeScript/Python/Go',
|
||||
style: '命令式 + 声明式',
|
||||
multiCloud: '原生多云',
|
||||
stateManagement: 'Pulumi Cloud / 自管',
|
||||
learning: '低(熟悉编程语言)',
|
||||
community: '快速增长',
|
||||
bestFor: '开发者友好场景'
|
||||
},
|
||||
desc: 'Pulumi 允许使用真正的编程语言(TypeScript、Python、Go 等)来定义基础设施,对开发者非常友好,支持条件判断、循环等编程特性。',
|
||||
example: `import * as aws from "@pulumi/aws"
|
||||
|
||||
const bucket = new aws.s3.Bucket("data", {
|
||||
tags: { Env: "prod" }
|
||||
})
|
||||
|
||||
const server = new aws.ec2.Instance("web", {
|
||||
ami: "ami-0c55b159",
|
||||
instanceType: "t3.micro",
|
||||
})`
|
||||
},
|
||||
{
|
||||
name: 'Ansible',
|
||||
icon: '🔴',
|
||||
color: '#dc2626',
|
||||
features: {
|
||||
vendor: 'Red Hat',
|
||||
language: 'YAML (Playbook)',
|
||||
style: '命令式',
|
||||
multiCloud: '通过模块支持',
|
||||
stateManagement: '无状态(幂等)',
|
||||
learning: '低',
|
||||
community: '非常活跃',
|
||||
bestFor: '配置管理 + 编排'
|
||||
},
|
||||
desc: 'Ansible 是一个无代理的自动化工具,擅长配置管理和应用部署。它通过 SSH 连接目标机器执行任务,无需安装客户端。',
|
||||
example: `- name: 部署 Web 服务器
|
||||
hosts: webservers
|
||||
tasks:
|
||||
- name: 安装 Nginx
|
||||
apt:
|
||||
name: nginx
|
||||
state: present
|
||||
- name: 启动服务
|
||||
service:
|
||||
name: nginx
|
||||
state: started`
|
||||
}
|
||||
]
|
||||
|
||||
function getToolByName(name) {
|
||||
return tools.find(t => t.name === name)
|
||||
}
|
||||
|
||||
function toggleTool(name) {
|
||||
const idx = selectedTools.value.indexOf(name)
|
||||
if (idx >= 0) {
|
||||
if (selectedTools.value.length > 2) {
|
||||
selectedTools.value.splice(idx, 1)
|
||||
}
|
||||
} else {
|
||||
selectedTools.value.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
function getCellClass(toolName, featureKey) {
|
||||
const val = getToolByName(toolName).features[featureKey]
|
||||
if (featureKey === 'multiCloud') {
|
||||
if (val.includes('原生多云')) return 'cell-good'
|
||||
if (val.includes('仅')) return 'cell-warn'
|
||||
return ''
|
||||
}
|
||||
if (featureKey === 'learning') {
|
||||
if (val === '低') return 'cell-good'
|
||||
if (val.includes('高')) return 'cell-warn'
|
||||
return ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iac-tool-comparison-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;
|
||||
}
|
||||
.selector-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--vp-c-text-3);
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tool-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.tool-chip {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 20px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tool-chip:hover { transform: scale(1.05); }
|
||||
.comparison-grid { overflow-x: auto; margin-bottom: 1rem; }
|
||||
.comparison-grid table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.comparison-grid th,
|
||||
.comparison-grid td {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
text-align: center;
|
||||
}
|
||||
.comparison-grid th {
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-weight: 600;
|
||||
}
|
||||
.feature-col { text-align: left; min-width: 80px; }
|
||||
.feature-cell { font-weight: 600; text-align: left; }
|
||||
.tool-header-icon { margin-right: 4px; }
|
||||
.cell-good { color: #10b981; font-weight: 600; }
|
||||
.cell-warn { color: #f59e0b; }
|
||||
.empty-hint {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.detail-hint {
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tool-detail-btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.detail-btn {
|
||||
padding: 4px 12px;
|
||||
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;
|
||||
}
|
||||
.detail-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.detail-card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.detail-icon { font-size: 1.2rem; }
|
||||
.detail-name { font-weight: 600; font-size: 1rem; flex: 1; }
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.detail-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.code-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.code-block {
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
padding: 0.8rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.73rem;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="terraform-workflow-demo">
|
||||
<div class="demo-label">交互演示 ── Terraform 工作流四阶段</div>
|
||||
|
||||
<div class="stage-nav">
|
||||
<div
|
||||
v-for="(stage, i) in stages"
|
||||
:key="stage.key"
|
||||
:class="['stage-tab', { active: currentStage === i, completed: i < currentStage }]"
|
||||
@click="goToStage(i)"
|
||||
>
|
||||
<span class="stage-icon">{{ stage.icon }}</span>
|
||||
<span class="stage-name">{{ stage.name }}</span>
|
||||
<span v-if="i < stages.length - 1" class="stage-arrow">→</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stage-content">
|
||||
<Transition name="slide" mode="out-in">
|
||||
<div :key="currentStage" class="stage-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-icon">{{ stages[currentStage].icon }}</span>
|
||||
<span class="panel-title">{{ stages[currentStage].title }}</span>
|
||||
</div>
|
||||
<p class="panel-desc">{{ stages[currentStage].desc }}</p>
|
||||
|
||||
<div class="terminal-block">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-dot red"></span>
|
||||
<span class="terminal-dot yellow"></span>
|
||||
<span class="terminal-dot green"></span>
|
||||
<span class="terminal-title">Terminal</span>
|
||||
</div>
|
||||
<div class="terminal-body">
|
||||
<div v-for="(line, li) in visibleLines" :key="li" class="terminal-line">
|
||||
<span :class="line.cls">{{ line.text }}</span>
|
||||
</div>
|
||||
<span v-if="isTyping" class="cursor-blink">_</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-points">
|
||||
<div v-for="(point, pi) in stages[currentStage].points" :key="pi" class="point-item">
|
||||
<span class="point-bullet">{{ point.icon }}</span>
|
||||
<span class="point-text">{{ point.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div class="nav-buttons">
|
||||
<button class="nav-btn" :disabled="currentStage === 0" @click="goToStage(currentStage - 1)">
|
||||
← 上一步
|
||||
</button>
|
||||
<span class="stage-indicator">{{ currentStage + 1 }} / {{ stages.length }}</span>
|
||||
<button class="nav-btn primary" :disabled="currentStage === stages.length - 1" @click="goToStage(currentStage + 1)">
|
||||
下一步 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const currentStage = ref(0)
|
||||
const typingIndex = ref(0)
|
||||
const isTyping = ref(false)
|
||||
|
||||
const stages = [
|
||||
{
|
||||
key: 'write', icon: '📝', name: 'Write', title: 'Write ── 编写基础设施代码',
|
||||
desc: '用声明式语言(HCL)描述你期望的基础设施状态。代码就是文档,可以提交到 Git 进行版本管理和 Code Review。',
|
||||
lines: [
|
||||
{ text: '$ vim main.tf', cls: 'cmd' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'resource "aws_instance" "app" {', cls: 'code' },
|
||||
{ text: ' ami = "ami-0c55b159"', cls: 'code' },
|
||||
{ text: ' instance_type = "t3.micro"', cls: 'code' },
|
||||
{ text: ' tags = { Name = "my-app" }', cls: 'code' },
|
||||
{ text: '}', cls: 'code' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: '# 文件已保存 ✓', cls: 'success' }
|
||||
],
|
||||
points: [
|
||||
{ icon: '📄', text: '使用 .tf 文件描述资源' },
|
||||
{ icon: '🔧', text: 'HCL 语法简洁易读' },
|
||||
{ icon: '📦', text: '支持模块化复用' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'plan', icon: '🔍', name: 'Plan', title: 'Plan ── 预览变更计划',
|
||||
desc: 'Terraform 会对比当前状态和期望状态,生成一份详细的执行计划。这一步不会做任何实际变更,是安全的"预演"。',
|
||||
lines: [
|
||||
{ text: '$ terraform plan', cls: 'cmd' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Terraform will perform the following actions:', cls: 'info' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: ' + aws_instance.app', cls: 'add' },
|
||||
{ text: ' ami: "ami-0c55b159"', cls: 'detail' },
|
||||
{ text: ' instance_type: "t3.micro"', cls: 'detail' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Plan: 1 to add, 0 to change, 0 to destroy.', cls: 'success' }
|
||||
],
|
||||
points: [
|
||||
{ icon: '🛡️', text: '变更前先预览,避免意外' },
|
||||
{ icon: '➕', text: '绿色 + 表示新增资源' },
|
||||
{ icon: '🔄', text: '~ 表示修改,- 表示删除' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'apply', icon: '🚀', name: 'Apply', title: 'Apply ── 执行变更',
|
||||
desc: '确认计划无误后,Terraform 调用云平台 API 创建/修改/删除资源,并将最终状态写入 State 文件。',
|
||||
lines: [
|
||||
{ text: '$ terraform apply', cls: 'cmd' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'aws_instance.app: Creating...', cls: 'info' },
|
||||
{ text: 'aws_instance.app: Still creating... [10s elapsed]', cls: 'info' },
|
||||
{ text: 'aws_instance.app: Creation complete after 32s', cls: 'success' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Apply complete! Resources: 1 added, 0 changed, 0 destroyed.', cls: 'success' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Outputs:', cls: 'info' },
|
||||
{ text: ' public_ip = "54.123.45.67"', cls: 'output' }
|
||||
],
|
||||
points: [
|
||||
{ icon: '☁️', text: '自动调用云平台 API' },
|
||||
{ icon: '💾', text: '状态保存到 terraform.tfstate' },
|
||||
{ icon: '📤', text: '输出关键信息(IP、域名等)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'destroy', icon: '🗑️', name: 'Destroy', title: 'Destroy ── 销毁资源',
|
||||
desc: '不再需要时,一条命令即可安全销毁所有资源。Terraform 会按照依赖关系的逆序逐一清理,避免残留。',
|
||||
lines: [
|
||||
{ text: '$ terraform destroy', cls: 'cmd' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Terraform will perform the following actions:', cls: 'info' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: ' - aws_instance.app', cls: 'remove' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Plan: 0 to add, 0 to change, 1 to destroy.', cls: 'warn' },
|
||||
{ text: 'aws_instance.app: Destroying...', cls: 'info' },
|
||||
{ text: 'aws_instance.app: Destruction complete after 15s', cls: 'success' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Destroy complete! Resources: 1 destroyed.', cls: 'success' }
|
||||
],
|
||||
points: [
|
||||
{ icon: '🧹', text: '按依赖逆序安全清理' },
|
||||
{ icon: '💰', text: '避免资源遗忘产生费用' },
|
||||
{ icon: '♻️', text: '环境可随时重建' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const visibleLines = computed(() => {
|
||||
return stages[currentStage.value].lines.slice(0, typingIndex.value)
|
||||
})
|
||||
|
||||
function goToStage(i) {
|
||||
currentStage.value = i
|
||||
}
|
||||
|
||||
watch(currentStage, () => {
|
||||
typingIndex.value = 0
|
||||
isTyping.value = true
|
||||
typeNext()
|
||||
})
|
||||
|
||||
function typeNext() {
|
||||
const total = stages[currentStage.value].lines.length
|
||||
if (typingIndex.value < total) {
|
||||
setTimeout(() => {
|
||||
typingIndex.value++
|
||||
typeNext()
|
||||
}, 120)
|
||||
} else {
|
||||
isTyping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize first stage
|
||||
typeNext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terraform-workflow-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;
|
||||
}
|
||||
.stage-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.stage-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.stage-tab.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.stage-tab.completed {
|
||||
border-color: #10b981;
|
||||
background: #d1fae510;
|
||||
}
|
||||
.stage-arrow { color: var(--vp-c-text-3); margin: 0 2px; }
|
||||
.stage-content { min-height: 280px; }
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.panel-icon { font-size: 1.3rem; }
|
||||
.panel-title { font-weight: 600; font-size: 0.95rem; }
|
||||
.panel-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.8rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.terminal-block {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.terminal-header {
|
||||
background: #1e1e1e;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.terminal-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.terminal-dot.red { background: #ff5f57; }
|
||||
.terminal-dot.yellow { background: #febc2e; }
|
||||
.terminal-dot.green { background: #28c840; }
|
||||
.terminal-title {
|
||||
font-size: 0.7rem;
|
||||
color: #888;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.terminal-body {
|
||||
background: #1a1a2e;
|
||||
padding: 0.8rem;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
font-size: 0.73rem;
|
||||
line-height: 1.6;
|
||||
min-height: 160px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.terminal-line .cmd { color: #7dd3fc; }
|
||||
.terminal-line .code { color: #a5b4fc; }
|
||||
.terminal-line .info { color: #94a3b8; }
|
||||
.terminal-line .add { color: #4ade80; }
|
||||
.terminal-line .remove { color: #f87171; }
|
||||
.terminal-line .detail { color: #cbd5e1; padding-left: 1rem; }
|
||||
.terminal-line .success { color: #34d399; font-weight: 600; }
|
||||
.terminal-line .warn { color: #fbbf24; }
|
||||
.terminal-line .output { color: #c084fc; }
|
||||
.cursor-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
color: #7dd3fc;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
.key-points {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.point-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.nav-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-btn:disabled { opacity: 0.4; cursor: default; }
|
||||
.nav-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
|
||||
.stage-indicator { font-size: 0.75rem; color: var(--vp-c-text-3); }
|
||||
.slide-enter-active, .slide-leave-active { transition: all 0.3s ease; }
|
||||
.slide-enter-from { opacity: 0; transform: translateX(20px); }
|
||||
.slide-leave-to { opacity: 0; transform: translateX(-20px); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user