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:
sanbuphy
2026-02-24 18:22:58 +08:00
parent b5a55811cc
commit 3af119a598
86 changed files with 20311 additions and 340 deletions
@@ -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>
@@ -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>
@@ -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>
@@ -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>