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
+2 -2
View File
@@ -466,7 +466,7 @@ watch(sidebarCollapsed, (collapsed) => {
font-size: 18px;
font-weight: 500;
white-space: pre-wrap;
color: #000000;
color: var(--vp-c-text-2);
min-height: 28px;
display: flex;
/* 居中对齐 */
@@ -488,7 +488,7 @@ watch(sidebarCollapsed, (collapsed) => {
margin-right: auto;
}
.VPHomeHero .text {
color: #000000 !important;
color: var(--vp-c-text-1) !important;
}
.VPHomeHero .actions {
justify-content: center;
@@ -1970,7 +1970,7 @@ a {
.nav-title {
font-weight: 600;
font-size: 19px;
color: #000 !important;
color: var(--vp-c-text-1) !important;
flex-shrink: 0;
letter-spacing: -0.01em;
}
@@ -1987,7 +1987,7 @@ a {
background: none;
border: none;
font-size: 12px;
color: #000 !important;
color: var(--vp-c-text-1) !important;
cursor: pointer;
transition: opacity 0.2s;
padding: 0;
@@ -2000,7 +2000,7 @@ a {
.nav-links button:hover,
.nav-links button.active,
.nav-link-item:hover {
color: #000 !important;
color: var(--vp-c-text-1) !important;
opacity: 1;
}
@@ -2021,7 +2021,7 @@ a {
background: none;
border: none;
padding: 0;
color: #000 !important;
color: var(--vp-c-text-1) !important;
cursor: pointer;
display: flex;
align-items: center;
@@ -2038,7 +2038,7 @@ a {
background: none;
border: none;
padding: 0;
color: #000 !important;
color: var(--vp-c-text-1) !important;
cursor: pointer;
display: flex;
align-items: center;
@@ -2060,13 +2060,13 @@ a {
.button .option-icon {
width: 20px;
height: 20px;
color: #000 !important;
color: var(--vp-c-text-1) !important;
}
.button .text-icon {
width: 14px;
height: 14px;
color: #000 !important;
color: var(--vp-c-text-1) !important;
}
/* GitHub Stars Styles */
@@ -2076,7 +2076,7 @@ a {
}
:deep(.nav-github-stars .github-stars-link) {
color: #000 !important;
color: var(--vp-c-text-1) !important;
display: flex;
align-items: center;
gap: 4px;
@@ -2134,8 +2134,8 @@ a {
}
.buy-btn {
background: #000;
color: #fff !important;
background: var(--vp-c-text-1);
color: var(--vp-c-bg) !important;
padding: 4px 12px;
border-radius: 980px;
font-size: 12px;
@@ -0,0 +1,245 @@
<template>
<div class="flow-demo">
<div class="header">
<div class="title">AI 应用请求处理流程</div>
<div class="subtitle">点击"发送请求"观察一次 AI 请求的完整生命周期</div>
</div>
<div class="pipeline">
<div
v-for="(step, idx) in steps"
:key="step.id"
:class="['pipe-step', {
active: currentStep === idx,
done: currentStep > idx
}]"
>
<div class="step-icon">{{ currentStep > idx ? '✅' : step.icon }}</div>
<div class="step-info">
<div class="step-name">{{ step.name }}</div>
<div class="step-en">{{ step.en }}</div>
</div>
<div v-if="idx < steps.length - 1" class="arrow"></div>
</div>
</div>
<div class="control-bar">
<button
v-if="!isRunning && currentStep < 0"
class="action-btn"
@click="startFlow"
>
发送请求
</button>
<button
v-else-if="!isRunning && currentStep >= steps.length"
class="action-btn reset"
@click="resetFlow"
>
🔄 重置
</button>
<div v-else-if="isRunning" class="running-hint">
处理中...
</div>
</div>
<div v-if="currentStep >= 0" class="detail-area">
<div class="detail-card">
<div class="detail-title">
{{ activeStep.icon }} {{ activeStep.name }}
</div>
<div class="detail-desc">{{ activeStep.detail }}</div>
<div class="io-section">
<div class="io-block">
<div class="io-label">输入</div>
<pre class="io-code"><code>{{ activeStep.input }}</code></pre>
</div>
<div class="io-block">
<div class="io-label">输出</div>
<pre class="io-code"><code>{{ activeStep.output }}</code></pre>
</div>
</div>
<div class="latency-bar">
<span class="latency-label">耗时</span>
<div class="latency-track">
<div
class="latency-fill"
:style="{ width: activeStep.latencyPct + '%' }"
/>
</div>
<span class="latency-val">{{ activeStep.latency }}</span>
</div>
</div>
</div>
<div class="insight-bar">
<span class="insight-label">💡 关键洞察</span>
<span class="insight-text">
AI 应用的请求链路比传统应用更长模型推理通常占总耗时的 60-80%
优化重点在于Prompt 缓存流式输出异步处理
</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const steps = [
{
id: 'input', icon: '👤', name: '用户输入', en: 'User Input',
detail: '用户通过自然语言输入请求。系统需要处理多种输入形式:文本、语音转文字、图片描述等。与传统应用的表单提交不同,输入是开放式的、非结构化的。',
input: '"帮我总结这篇文章的核心观点"',
output: '{ text: "帮我总结...", type: "text", lang: "zh" }',
latency: '~0ms', latencyPct: 2
},
{
id: 'preprocess', icon: '🔧', name: '预处理', en: 'Preprocessing',
detail: '对用户输入进行清洗和增强:意图识别、关键词提取、上下文拼接、RAG 检索相关文档片段、构建完整的 Prompt。这一步决定了模型能获得多少有效信息。',
input: '{ text: "帮我总结...", context: [...历史对话] }',
output: '{ system_prompt: "你是...", user_prompt: "...", retrieved_docs: [...] }',
latency: '~200ms', latencyPct: 15
},
{
id: 'model', icon: '🧠', name: '模型推理', en: 'Model Inference',
detail: '将构建好的 Prompt 发送给大语言模型进行推理。这是整个链路中耗时最长的环节。模型会根据 Prompt 中的指令、上下文和检索到的知识,生成回答。',
input: '{ messages: [...], model: "gpt-4", temperature: 0.7 }',
output: '{ content: "这篇文章的核心观点有三个...", tokens: 256 }',
latency: '~2-8s', latencyPct: 75
},
{
id: 'postprocess', icon: '🛡️', name: '后处理', en: 'Post-processing',
detail: '对模型输出进行安全检查和格式化:内容审核过滤、幻觉检测、格式转换(Markdown 渲染)、引用来源标注、敏感信息脱敏等。',
input: '{ raw_output: "这篇文章的核心观点有三个..." }',
output: '{ safe: true, formatted: "## 核心观点\\n1. ...", sources: [...] }',
latency: '~100ms', latencyPct: 8
},
{
id: 'response', icon: '💬', name: '响应输出', en: 'Response',
detail: '将处理后的结果以流式方式返回给用户。前端逐步渲染 Markdown 内容,同时展示引用来源和置信度。用户可以在生成过程中随时中断或追问。',
input: '{ formatted: "## 核心观点\\n1. ...", stream: true }',
output: '用户看到逐字出现的回答 + 来源引用',
latency: '~50ms (首字节)', latencyPct: 5
}
]
const currentStep = ref(-1)
const isRunning = ref(false)
const activeStep = computed(() => {
const idx = Math.min(currentStep.value, steps.length - 1)
return idx >= 0 ? steps[idx] : steps[0]
})
const startFlow = async () => {
isRunning.value = true
for (let i = 0; i < steps.length; i++) {
currentStep.value = i
await new Promise(r => setTimeout(r, 1200))
}
currentStep.value = steps.length
isRunning.value = false
}
const resetFlow = () => {
currentStep.value = -1
isRunning.value = false
}
</script>
<style scoped>
.flow-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 20px; margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, #10b981, #3b82f6);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.pipeline {
display: flex; align-items: center; justify-content: center;
gap: 4px; flex-wrap: wrap; margin-bottom: 16px;
}
.pipe-step {
display: flex; align-items: center; gap: 6px;
padding: 8px 12px; border-radius: 8px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); transition: all 0.3s;
font-size: 12px;
}
.pipe-step.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.pipe-step.done {
border-color: #86efac; background: #f0fdf4;
}
.step-icon { font-size: 18px; }
.step-name { font-weight: 600; font-size: 12px; }
.step-en { font-size: 10px; color: var(--vp-c-text-3); }
.arrow { color: var(--vp-c-text-3); font-size: 14px; margin: 0 2px; }
.control-bar { text-align: center; margin-bottom: 16px; }
.action-btn {
padding: 10px 28px; background: var(--vp-c-brand);
color: white; border: none; border-radius: 8px;
font-size: 13px; cursor: pointer; transition: background 0.2s;
}
.action-btn:hover { background: var(--vp-c-brand-dark); }
.action-btn.reset { background: #6b7280; }
.action-btn.reset:hover { background: #4b5563; }
.running-hint { color: var(--vp-c-brand); font-size: 13px; }
.detail-area { margin-bottom: 16px; }
.detail-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 16px;
}
.detail-title { font-weight: 700; font-size: 15px; margin-bottom: 8px; }
.detail-desc {
color: var(--vp-c-text-2); font-size: 13px;
line-height: 1.7; margin-bottom: 12px;
}
.io-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 10px; margin-bottom: 12px;
}
.io-label { font-weight: 600; font-size: 11px; margin-bottom: 4px; color: var(--vp-c-text-2); }
.io-code {
margin: 0; background: #0b1221; color: #e5e7eb;
border-radius: 8px; padding: 10px;
font-family: var(--vp-font-family-mono);
font-size: 11px; overflow-x: auto; white-space: pre-wrap;
}
.latency-bar {
display: flex; align-items: center; gap: 10px;
}
.latency-label { font-size: 11px; font-weight: 600; color: var(--vp-c-text-2); }
.latency-track {
flex: 1; height: 8px; background: var(--vp-c-bg-soft);
border-radius: 4px; overflow: hidden;
}
.latency-fill {
height: 100%; border-radius: 4px;
background: var(--vp-c-brand); transition: width 0.5s;
}
.latency-val { font-size: 11px; font-weight: 600; min-width: 80px; text-align: right; }
.insight-bar {
padding: 12px 16px; background: var(--vp-c-brand-soft);
border-radius: 6px; font-size: 13px;
}
.insight-label { font-weight: 600; color: var(--vp-c-brand-dark); }
.insight-text { color: var(--vp-c-text-1); }
</style>
@@ -0,0 +1,222 @@
<template>
<div class="principle-demo">
<div class="header">
<div class="title">AI 原生设计原则</div>
<div class="subtitle">点击卡片深入了解每条设计原则</div>
</div>
<div class="principle-grid">
<div
v-for="p in principles"
:key="p.id"
:class="['principle-card', { active: selected === p.id }]"
@click="selected = p.id"
>
<div class="p-icon">{{ p.icon }}</div>
<div class="p-name">{{ p.name }}</div>
<div class="p-brief">{{ p.brief }}</div>
</div>
</div>
<div v-if="selected" class="detail-panel">
<div class="detail-header">
<span>{{ currentPrinciple.icon }} {{ currentPrinciple.name }}</span>
</div>
<div class="detail-body">
<div class="detail-desc">{{ currentPrinciple.detail }}</div>
<div class="example-section">
<div class="example-title">实践对比</div>
<div class="compare-grid">
<div class="compare-bad">
<div class="compare-label bad-label"> 反面示例</div>
<div class="compare-text">{{ currentPrinciple.bad }}</div>
</div>
<div class="compare-good">
<div class="compare-label good-label"> 正确做法</div>
<div class="compare-text">{{ currentPrinciple.good }}</div>
</div>
</div>
</div>
<div class="checklist">
<div class="checklist-title">检查清单</div>
<div
v-for="(item, idx) in currentPrinciple.checklist"
:key="idx"
:class="['check-item', { checked: checkedItems[selected]?.[idx] }]"
@click="toggleCheck(idx)"
>
<span class="check-box">
{{ checkedItems[selected]?.[idx] ? '☑' : '☐' }}
</span>
<span>{{ item }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive } from 'vue'
const principles = [
{
id: 'graceful',
icon: '🛡️',
name: '优雅降级',
brief: 'AI 失败时,系统仍然可用',
detail: 'AI 模型可能超时、返回错误、产生幻觉。优雅降级意味着:当 AI 不可用时,系统应该有兜底方案,而不是直接崩溃。这是 AI 原生应用与玩具项目的分水岭。',
bad: '模型 API 超时后,页面显示空白错误页,用户只能刷新重试。',
good: '模型超时后,显示缓存的上一次回答或推荐相关文档,同时后台自动重试。',
checklist: [
'设置合理的 API 超时时间(通常 30-60s',
'准备降级方案:缓存、规则引擎、人工转接',
'向用户透明地展示当前状态',
'记录失败日志用于后续优化'
]
},
{
id: 'human',
icon: '🤝',
name: '人机协作',
brief: '关键决策由人类确认',
detail: 'AI 擅长生成和建议,但不应该在高风险场景中自主决策。人机协作(Human-in-the-Loop)模式让 AI 负责草稿和推荐,人类负责审核和确认。',
bad: 'AI 自动发送邮件给客户,内容未经人工审核,导致错误信息传播。',
good: 'AI 生成邮件草稿并高亮不确定的部分,用户审核修改后手动发送。',
checklist: [
'识别哪些操作是"高风险"的(发送、删除、支付)',
'高风险操作前必须有人工确认步骤',
'AI 输出标注置信度,低置信内容需人工复核',
'提供便捷的编辑和修改界面'
]
},
{
id: 'transparent',
icon: '🔍',
name: '透明可解释',
brief: '让用户理解 AI 的推理过程',
detail: 'AI 不是黑盒魔法。用户需要知道 AI 为什么给出这个回答、依据了哪些信息、有多大把握。透明性建立信任,也帮助用户判断何时该相信 AI、何时该质疑。',
bad: 'AI 直接给出一个结论,没有任何解释或来源引用,用户无法判断可靠性。',
good: '回答附带推理过程、引用来源链接、置信度指示,用户可以追溯验证。',
checklist: [
'展示 AI 的推理链路或思考过程',
'标注信息来源和引用',
'显示置信度或不确定性指标',
'提供"为什么这样回答"的解释入口'
]
},
{
id: 'feedback',
icon: '🔄',
name: '反馈闭环',
brief: '用户反馈驱动持续改进',
detail: '每一次用户交互都是改进的机会。通过收集用户对 AI 输出的评价(点赞/点踩、修改记录、追问模式),持续优化 Prompt、微调模型、改进检索策略。',
bad: 'AI 回答错误后,没有任何反馈渠道,同样的错误会反复出现。',
good: '用户可以标记错误回答,系统自动收集并用于优化 Prompt 和检索策略。',
checklist: [
'提供简单的反馈机制(👍👎 按钮)',
'记录用户的修改和追问作为隐式反馈',
'定期分析反馈数据,优化 Prompt 模板',
'建立 A/B 测试机制验证改进效果'
]
}
]
const selected = ref('graceful')
const checkedItems = reactive({})
const currentPrinciple = computed(() =>
principles.find(p => p.id === selected.value) || principles[0]
)
const toggleCheck = (idx) => {
if (!checkedItems[selected.value]) {
checkedItems[selected.value] = {}
}
checkedItems[selected.value][idx] = !checkedItems[selected.value][idx]
}
</script>
<style scoped>
.principle-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 20px; margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, #ef4444, #f59e0b);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.principle-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px; margin-bottom: 16px;
}
.principle-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 10px; padding: 14px; cursor: pointer;
transition: all 0.2s; text-align: center;
}
.principle-card:hover { background: var(--vp-c-bg-alt); }
.principle-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.p-icon { font-size: 24px; margin-bottom: 6px; }
.p-name { font-weight: 600; font-size: 13px; }
.p-brief { font-size: 11px; color: var(--vp-c-text-2); margin-top: 4px; }
.detail-panel {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; overflow: hidden;
}
.detail-header {
padding: 14px 16px; font-weight: 700; font-size: 15px;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-body { padding: 16px; }
.detail-desc {
color: var(--vp-c-text-2); font-size: 13px;
line-height: 1.7; margin-bottom: 16px;
}
.example-section { margin-bottom: 16px; }
.example-title { font-weight: 600; font-size: 13px; margin-bottom: 8px; }
.compare-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.compare-bad, .compare-good {
padding: 12px; border-radius: 8px; font-size: 13px; line-height: 1.6;
}
.compare-bad { background: #fef2f2; border: 1px solid #fecaca; }
.compare-good { background: #f0fdf4; border: 1px solid #bbf7d0; }
.compare-label {
font-weight: 600; font-size: 11px; margin-bottom: 6px;
}
.bad-label { color: #dc2626; }
.good-label { color: #16a34a; }
.compare-text { color: var(--vp-c-text-1); }
.checklist-title { font-weight: 600; font-size: 13px; margin-bottom: 8px; }
.check-item {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: 6px; font-size: 13px;
cursor: pointer; transition: background 0.2s;
border: 1px solid transparent;
}
.check-item:hover { background: var(--vp-c-bg-soft); }
.check-item.checked {
background: #f0fdf4; border-color: #bbf7d0;
text-decoration: line-through; color: var(--vp-c-text-3);
}
.check-box { font-size: 16px; flex-shrink: 0; }
</style>
@@ -0,0 +1,237 @@
<template>
<div class="arch-demo">
<div class="header">
<div class="title">传统应用 vs AI 原生应用</div>
<div class="subtitle">切换视图对比两种架构的核心差异</div>
</div>
<div class="toggle-bar">
<button
:class="['toggle-btn', { active: mode === 'traditional' }]"
@click="mode = 'traditional'"
>
<span>🏗</span>
<span>传统应用</span>
</button>
<button
:class="['toggle-btn', { active: mode === 'ai-native' }]"
@click="mode = 'ai-native'"
>
<span>🤖</span>
<span>AI 原生应用</span>
</button>
</div>
<div class="arch-grid">
<div class="stack">
<div class="stack-title">{{ currentArch.label }}</div>
<div
v-for="(layer, idx) in currentArch.layers"
:key="idx"
:class="['layer', { highlight: selectedLayer === idx }]"
:style="{ borderLeftColor: layer.color }"
@click="selectedLayer = idx"
>
<div class="layer-icon">{{ layer.icon }}</div>
<div class="layer-info">
<div class="layer-name">{{ layer.name }}</div>
<div class="layer-desc">{{ layer.brief }}</div>
</div>
</div>
</div>
<div class="detail-panel">
<div v-if="selectedLayer !== null" class="detail-content">
<div class="detail-title">
{{ currentArch.layers[selectedLayer].icon }}
{{ currentArch.layers[selectedLayer].name }}
</div>
<div class="detail-desc">
{{ currentArch.layers[selectedLayer].detail }}
</div>
<div class="detail-example">
<div class="example-label">典型技术</div>
<div class="tech-tags">
<span
v-for="t in currentArch.layers[selectedLayer].techs"
:key="t"
class="tech-tag"
>{{ t }}</span>
</div>
</div>
</div>
<div v-else class="detail-placeholder">
👆 点击左侧层级查看详情
</div>
</div>
</div>
<div class="comparison-bar">
<span class="compare-label">💡 核心区别</span>
<span class="compare-text">{{ mode === 'traditional'
? '传统应用的逻辑由开发者用 if/else 硬编码,行为完全确定。'
: 'AI 原生应用的核心逻辑由模型驱动,行为具有概率性,需要全新的设计思维。' }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('traditional')
const selectedLayer = ref(0)
const architectures = {
traditional: {
label: '传统应用架构',
layers: [
{
icon: '🖥️', name: '前端 UI', color: '#3b82f6',
brief: '用户界面与交互',
detail: '基于确定性的表单、按钮、页面路由。用户操作触发固定的业务流程,所有交互路径在开发时已经确定。',
techs: ['React', 'Vue', 'HTML/CSS']
},
{
icon: '⚙️', name: '业务逻辑层', color: '#8b5cf6',
brief: '硬编码的规则引擎',
detail: '开发者用 if/else、switch/case 编写所有业务规则。每一条路径都需要人工预设,无法处理规则之外的情况。',
techs: ['Node.js', 'Java', 'Python']
},
{
icon: '🗄️', name: '数据存储', color: '#06b6d4',
brief: '结构化数据管理',
detail: '关系型数据库存储结构化数据,Schema 固定。数据的读写遵循严格的 CRUD 模式。',
techs: ['MySQL', 'PostgreSQL', 'Redis']
},
{
icon: '🔌', name: 'API 接口', color: '#10b981',
brief: '固定的请求/响应',
detail: '每个 API 端点返回确定性的结果。相同的输入永远产生相同的输出,行为完全可预测。',
techs: ['REST', 'GraphQL', 'gRPC']
}
]
},
'ai-native': {
label: 'AI 原生应用架构',
layers: [
{
icon: '💬', name: '自然语言交互层', color: '#f59e0b',
brief: '对话式 + 流式输出',
detail: '用户通过自然语言表达意图,系统以流式方式逐步生成响应。交互不再是固定的表单,而是开放式的对话。',
techs: ['Streaming UI', 'Markdown 渲染', 'SSE']
},
{
icon: '🧠', name: '模型推理层', color: '#ef4444',
brief: 'LLM 驱动的决策引擎',
detail: '核心逻辑不再是 if/else,而是由大语言模型根据 Prompt 和上下文进行推理。输出具有概率性,同样的输入可能产生不同的结果。',
techs: ['GPT-4', 'Claude', 'Prompt 工程']
},
{
icon: '🔗', name: '编排与工具层', color: '#8b5cf6',
brief: 'Agent 编排 + 工具调用',
detail: '模型可以调用外部工具(搜索、数据库、API)来获取实时信息。编排层负责管理多步推理、工具选择和结果整合。',
techs: ['LangChain', 'Function Calling', 'RAG']
},
{
icon: '📦', name: '上下文管理层', color: '#06b6d4',
brief: '向量数据库 + 记忆系统',
detail: '使用向量数据库存储和检索非结构化知识。通过 Embedding 将文本转化为语义向量,实现基于含义的搜索而非关键词匹配。',
techs: ['Pinecone', 'ChromaDB', 'Embedding']
},
{
icon: '🛡️', name: '安全与护栏层', color: '#10b981',
brief: '输出过滤 + 幻觉检测',
detail: 'AI 输出不可完全信任,需要护栏机制:内容过滤、事实核查、幻觉检测、敏感信息脱敏等。这是传统应用不需要的全新层级。',
techs: ['Guardrails', '内容审核', '事实校验']
}
]
}
}
const currentArch = computed(() => architectures[mode.value])
</script>
<style scoped>
.arch-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, var(--vp-c-brand), #f59e0b);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.toggle-bar {
display: flex; gap: 8px; justify-content: center; margin-bottom: 16px;
}
.toggle-btn {
display: flex; align-items: center; gap: 6px;
padding: 8px 18px; border: 1px solid var(--vp-c-divider);
border-radius: 20px; background: var(--vp-c-bg);
cursor: pointer; transition: all 0.2s; font-size: 13px;
}
.toggle-btn:hover { background: var(--vp-c-bg-alt); }
.toggle-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.arch-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.stack {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 12px;
display: flex; flex-direction: column; gap: 8px;
}
.stack-title { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.layer {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; border-radius: 8px;
border: 1px solid var(--vp-c-divider);
border-left: 3px solid; background: var(--vp-c-bg);
cursor: pointer; transition: all 0.2s;
}
.layer:hover { background: var(--vp-c-bg-alt); }
.layer.highlight {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.layer-icon { font-size: 20px; flex-shrink: 0; }
.layer-name { font-weight: 600; font-size: 13px; }
.layer-desc { font-size: 11px; color: var(--vp-c-text-2); margin-top: 2px; }
.detail-panel {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 16px;
}
.detail-title { font-weight: 700; font-size: 15px; margin-bottom: 10px; }
.detail-desc { color: var(--vp-c-text-2); line-height: 1.7; font-size: 13px; margin-bottom: 12px; }
.example-label { font-weight: 600; font-size: 12px; margin-bottom: 6px; }
.tech-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.tech-tag {
padding: 3px 10px; border-radius: 12px; font-size: 11px;
background: var(--vp-c-brand-soft); color: var(--vp-c-brand-dark);
border: 1px solid var(--vp-c-brand-dimm);
}
.detail-placeholder {
color: var(--vp-c-text-3); text-align: center; padding: 40px 0; font-size: 13px;
}
.comparison-bar {
margin-top: 16px; padding: 12px 16px;
background: var(--vp-c-brand-soft); border-radius: 6px; font-size: 13px;
}
.compare-label { font-weight: 600; color: var(--vp-c-brand-dark); }
.compare-text { color: var(--vp-c-text-1); }
</style>
@@ -0,0 +1,301 @@
<template>
<div class="ux-demo">
<div class="header">
<div class="title">AI 原生交互模式</div>
<div class="subtitle">点击卡片体验每种 AI 交互模式的效果</div>
</div>
<div class="pattern-grid">
<div
v-for="p in patterns"
:key="p.id"
:class="['pattern-card', { active: activePattern === p.id }]"
@click="activatePattern(p.id)"
>
<div class="card-icon">{{ p.icon }}</div>
<div class="card-name">{{ p.name }}</div>
<div class="card-desc">{{ p.brief }}</div>
</div>
</div>
<div v-if="activePattern" class="preview-area">
<div class="preview-header">
<span>{{ currentPattern.icon }} {{ currentPattern.name }} 演示</span>
<button class="replay-btn" @click="replayDemo">🔄 重播</button>
</div>
<!-- 流式输出演示 -->
<div v-if="activePattern === 'streaming'" class="demo-box">
<div class="chat-bubble ai">
<span class="stream-text">{{ streamText }}</span>
<span v-if="isStreaming" class="cursor-blink">|</span>
</div>
<div class="demo-note">逐字输出用户无需等待完整响应</div>
</div>
<!-- 加载状态演示 -->
<div v-if="activePattern === 'loading'" class="demo-box">
<div class="loading-stages">
<div
v-for="(s, idx) in loadingStages"
:key="idx"
:class="['stage', { done: loadingStep > idx, current: loadingStep === idx }]"
>
<span class="stage-icon">
{{ loadingStep > idx ? '✅' : loadingStep === idx ? '⏳' : '⬜' }}
</span>
<span>{{ s }}</span>
</div>
</div>
<div class="demo-note">分阶段展示进度而非单一的"加载中"</div>
</div>
<!-- 置信度指示器演示 -->
<div v-if="activePattern === 'confidence'" class="demo-box">
<div class="confidence-list">
<div v-for="c in confidenceItems" :key="c.text" class="conf-item">
<div class="conf-bar-wrap">
<div
class="conf-bar"
:style="{ width: c.score + '%', background: c.color }"
/>
</div>
<div class="conf-score">{{ c.score }}%</div>
<div class="conf-label">{{ c.level }}</div>
<div class="conf-text">{{ c.text }}</div>
</div>
</div>
<div class="demo-note">让用户知道 AI 对自己的回答有多"确定"</div>
</div>
<!-- 降级处理演示 -->
<div v-if="activePattern === 'fallback'" class="demo-box">
<div class="fallback-flow">
<div :class="['fb-step', { active: fallbackStep >= 0 }]">
<span class="fb-icon">🤖</span>
<span>AI 尝试回答...</span>
</div>
<div class="fb-arrow" v-if="fallbackStep >= 1"> 检测到不确定</div>
<div :class="['fb-step warn', { active: fallbackStep >= 1 }]">
<span class="fb-icon"></span>
<span>提示用户此回答可能不准确</span>
</div>
<div class="fb-arrow" v-if="fallbackStep >= 2"> 提供替代方案</div>
<div :class="['fb-step safe', { active: fallbackStep >= 2 }]">
<span class="fb-icon">🔄</span>
<span>转接人工 / 推荐文档 / 换个方式提问</span>
</div>
</div>
<div class="demo-note">AI 不确定时优雅降级而非强行回答</div>
</div>
<div class="pattern-detail">
<div class="detail-label">设计要点</div>
<div class="detail-text">{{ currentPattern.detail }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const patterns = [
{
id: 'streaming', icon: '💬', name: '流式输出',
brief: '逐字生成,即时反馈',
detail: '流式输出让用户在 AI 思考时就能看到部分结果,大幅降低感知等待时间。技术上通过 SSEServer-Sent Events)或 WebSocket 实现,前端逐步渲染 Markdown 内容。'
},
{
id: 'loading', icon: '⏳', name: '智能加载态',
brief: '分阶段展示处理进度',
detail: 'AI 请求通常需要数秒,传统的转圈加载会让用户焦虑。智能加载态将处理过程拆解为可见的步骤(理解问题 → 检索知识 → 生成回答),让等待变得可预期。'
},
{
id: 'confidence', icon: '📊', name: '置信度指示',
brief: '展示 AI 的确定程度',
detail: 'AI 的输出具有概率性,不同回答的可靠程度不同。通过置信度指示器,用户可以判断哪些信息可以直接采纳,哪些需要二次验证。这是 AI 原生应用透明性的核心体现。'
},
{
id: 'fallback', icon: '🛡️', name: '优雅降级',
brief: '不确定时的兜底策略',
detail: '当 AI 无法给出可靠回答时,不应该硬编一个答案。优雅降级策略包括:坦诚告知不确定性、提供替代信息源、转接人工服务、引导用户换个方式提问。'
}
]
const activePattern = ref('')
const currentPattern = computed(() => patterns.find(p => p.id === activePattern.value) || {})
// Streaming demo
const streamText = ref('')
const isStreaming = ref(false)
const fullText = 'React 是一个用于构建用户界面的 JavaScript 库。它采用组件化的开发模式,让你可以将复杂的 UI 拆分成独立的、可复用的小模块。'
// Loading demo
const loadingStages = ['理解用户意图...', '检索相关知识...', '组织回答内容...', '生成最终响应']
const loadingStep = ref(-1)
// Confidence demo
const confidenceItems = [
{ text: 'React 由 Meta 开发', score: 98, level: '高置信', color: '#10b981' },
{ text: '全球约 40% 的网站使用 React', score: 72, level: '中置信', color: '#f59e0b' },
{ text: 'React 19 将在下月发布', score: 35, level: '低置信', color: '#ef4444' }
]
// Fallback demo
const fallbackStep = ref(-1)
let timer = null
const clearTimers = () => {
if (timer) { clearInterval(timer); timer = null }
}
const activatePattern = (id) => {
clearTimers()
activePattern.value = id
replayDemo()
}
const replayDemo = () => {
clearTimers()
if (activePattern.value === 'streaming') {
streamText.value = ''
isStreaming.value = true
let i = 0
timer = setInterval(() => {
if (i < fullText.length) {
streamText.value += fullText[i]
i++
} else {
isStreaming.value = false
clearTimers()
}
}, 50)
} else if (activePattern.value === 'loading') {
loadingStep.value = 0
let step = 0
timer = setInterval(() => {
step++
loadingStep.value = step
if (step >= loadingStages.length) clearTimers()
}, 900)
} else if (activePattern.value === 'fallback') {
fallbackStep.value = 0
let step = 0
timer = setInterval(() => {
step++
fallbackStep.value = step
if (step >= 2) clearTimers()
}, 1000)
}
}
</script>
<style scoped>
.ux-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 20px; margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, #06b6d4, #8b5cf6);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.pattern-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px; margin-bottom: 16px;
}
.pattern-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 10px; padding: 14px; cursor: pointer;
transition: all 0.2s; text-align: center;
}
.pattern-card:hover { background: var(--vp-c-bg-alt); }
.pattern-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.card-icon { font-size: 24px; margin-bottom: 6px; }
.card-name { font-weight: 600; font-size: 13px; }
.card-desc { font-size: 11px; color: var(--vp-c-text-2); margin-top: 4px; }
.preview-area {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 16px;
}
.preview-header {
display: flex; justify-content: space-between; align-items: center;
font-weight: 700; font-size: 14px; margin-bottom: 12px;
}
.replay-btn {
padding: 4px 12px; border: 1px solid var(--vp-c-divider);
border-radius: 6px; background: var(--vp-c-bg-soft);
cursor: pointer; font-size: 12px;
}
.demo-box {
background: var(--vp-c-bg-soft); border-radius: 8px;
padding: 16px; margin-bottom: 12px;
}
.demo-note {
font-size: 11px; color: var(--vp-c-text-3);
text-align: center; margin-top: 10px;
}
/* Streaming */
.chat-bubble.ai {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 10px; padding: 12px; font-size: 13px; line-height: 1.7;
}
.cursor-blink { animation: blink 0.8s infinite; color: var(--vp-c-brand); }
@keyframes blink { 50% { opacity: 0; } }
/* Loading */
.loading-stages { display: flex; flex-direction: column; gap: 8px; }
.stage {
display: flex; align-items: center; gap: 8px;
padding: 8px 12px; border-radius: 6px; font-size: 13px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
opacity: 0.4; transition: all 0.3s;
}
.stage.current { opacity: 1; border-color: var(--vp-c-brand); background: var(--vp-c-brand-soft); }
.stage.done { opacity: 1; border-color: #86efac; background: #f0fdf4; }
/* Confidence */
.confidence-list { display: flex; flex-direction: column; gap: 10px; }
.conf-item {
display: grid; grid-template-columns: 1fr 40px 60px 1fr;
align-items: center; gap: 8px; font-size: 12px;
}
.conf-bar-wrap {
height: 8px; background: var(--vp-c-bg);
border-radius: 4px; overflow: hidden;
}
.conf-bar { height: 100%; border-radius: 4px; transition: width 0.6s; }
.conf-score { font-weight: 600; text-align: right; }
.conf-label { font-size: 11px; color: var(--vp-c-text-2); }
.conf-text { color: var(--vp-c-text-1); }
/* Fallback */
.fallback-flow { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.fb-step {
display: flex; align-items: center; gap: 8px; width: 100%;
padding: 10px 14px; border-radius: 8px; font-size: 13px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
opacity: 0.3; transition: all 0.4s;
}
.fb-step.active { opacity: 1; }
.fb-step.warn.active { border-color: #fbbf24; background: #fef3c7; }
.fb-step.safe.active { border-color: #86efac; background: #f0fdf4; }
.fb-arrow { font-size: 12px; color: var(--vp-c-text-3); }
.pattern-detail { margin-top: 12px; }
.detail-label { font-weight: 600; font-size: 12px; margin-bottom: 4px; }
.detail-text { font-size: 13px; color: var(--vp-c-text-2); line-height: 1.7; }
</style>
@@ -0,0 +1,260 @@
<template>
<div class="prompt-demo">
<div class="header">
<div class="title">Prompt 工程实验室</div>
<div class="subtitle">修改 Prompt 结构观察输出质量的变化</div>
</div>
<div class="template-tabs">
<button
v-for="t in templates"
:key="t.id"
:class="['tab-btn', { active: currentTemplate === t.id }]"
@click="selectTemplate(t.id)"
>
<span>{{ t.icon }}</span>
<span>{{ t.name }}</span>
</button>
</div>
<div class="editor-grid">
<div class="editor-panel">
<div class="panel-label">System Prompt系统指令</div>
<textarea
v-model="systemPrompt"
class="prompt-input"
rows="3"
placeholder="设定 AI 的角色和行为规则..."
/>
<div class="panel-label">User Prompt用户输入</div>
<textarea
v-model="userPrompt"
class="prompt-input"
rows="3"
placeholder="用户的具体问题或指令..."
/>
<button class="run-btn" @click="runPrompt">
模拟生成
</button>
</div>
<div class="output-panel">
<div class="panel-label">模拟输出</div>
<div class="output-box">
<div v-if="isGenerating" class="generating">
<span class="dot-anim"></span> 生成中...
</div>
<div v-else-if="output" class="output-text">
{{ output }}
</div>
<div v-else class="output-placeholder">
点击"模拟生成"查看效果
</div>
</div>
<div v-if="output" class="quality-bar">
<div class="quality-label">输出质量评估</div>
<div class="quality-metrics">
<div
v-for="m in currentQuality"
:key="m.name"
class="metric"
>
<div class="metric-name">{{ m.name }}</div>
<div class="meter">
<div
class="meter-fill"
:style="{ width: m.score + '%', background: m.color }"
/>
</div>
<div class="metric-score">{{ m.score }}%</div>
</div>
</div>
</div>
</div>
</div>
<div class="tips-bar">
<span class="tips-label">💡 Prompt 技巧</span>
<span class="tips-text">{{ currentTip }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const templates = [
{ id: 'bad', icon: '❌', name: '模糊提问' },
{ id: 'basic', icon: '📝', name: '基础结构' },
{ id: 'good', icon: '✅', name: '最佳实践' }
]
const currentTemplate = ref('bad')
const systemPrompt = ref('')
const userPrompt = ref('')
const output = ref('')
const isGenerating = ref(false)
const templateData = {
bad: {
system: '',
user: '帮我写个介绍',
output: '好的,这是一个介绍:\n\n大家好,我是一个人,我喜欢很多东西。我觉得生活很有趣。谢谢大家。\n\n(输出模糊、缺乏方向、没有实际价值)',
quality: [
{ name: '相关性', score: 20, color: '#ef4444' },
{ name: '具体性', score: 10, color: '#ef4444' },
{ name: '可用性', score: 15, color: '#ef4444' }
],
tip: '没有 System Prompt,没有上下文,问题过于模糊 —— AI 只能猜测你的意图。'
},
basic: {
system: '你是一个技术文档写手。',
user: '帮我写一段关于 React 的介绍,大约100字。',
output: 'React 是由 Meta(原 Facebook)开发的开源 JavaScript 库,用于构建用户界面。它采用组件化开发模式和虚拟 DOM 技术,能够高效地更新和渲染页面。React 的声明式编程风格让开发者可以更直观地描述 UI 状态,广泛应用于现代 Web 开发中。',
quality: [
{ name: '相关性', score: 70, color: '#f59e0b' },
{ name: '具体性', score: 65, color: '#f59e0b' },
{ name: '可用性', score: 60, color: '#f59e0b' }
],
tip: '有了角色设定和基本约束(字数),输出质量明显提升,但仍缺少格式和受众定义。'
},
good: {
system: '你是一位资深前端技术博主。写作风格:简洁专业,善用类比。目标读者:编程初学者。输出格式:先用一句话总结,再展开说明,最后给出一个生活类比。',
user: '请介绍 React 是什么,以及为什么它如此流行。控制在150字以内。',
output: '一句话总结:React 是一个帮你高效搭建网页界面的 JavaScript 工具库。\n\n展开说明:React 由 Meta 团队开发,核心理念是"组件化"——把复杂页面拆成独立的小积木,每个积木管理自己的状态和外观。它的虚拟 DOM 机制能智能计算最小更新范围,避免不必要的页面重绘。\n\n生活类比:如果网页是一面乐高墙,React 就是那套标准化的乐高积木系统——你可以独立替换任何一块,而不用推倒重来。',
quality: [
{ name: '相关性', score: 95, color: '#10b981' },
{ name: '具体性', score: 90, color: '#10b981' },
{ name: '可用性', score: 95, color: '#10b981' }
],
tip: '角色 + 风格 + 受众 + 格式 + 约束 = 高质量输出。好的 Prompt 就是好的需求文档。'
}
}
const currentQuality = ref([])
const currentTip = computed(() => templateData[currentTemplate.value].tip)
const selectTemplate = (id) => {
currentTemplate.value = id
const data = templateData[id]
systemPrompt.value = data.system
userPrompt.value = data.user
output.value = ''
currentQuality.value = []
}
const runPrompt = async () => {
isGenerating.value = true
output.value = ''
currentQuality.value = []
await new Promise(r => setTimeout(r, 1200))
const data = templateData[currentTemplate.value]
output.value = data.output
currentQuality.value = data.quality
isGenerating.value = false
}
// Initialize
selectTemplate('bad')
</script>
<style scoped>
.prompt-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, #8b5cf6, var(--vp-c-brand));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.template-tabs {
display: flex; gap: 8px; justify-content: center;
margin-bottom: 16px; flex-wrap: wrap;
}
.tab-btn {
display: flex; align-items: center; gap: 6px;
padding: 8px 14px; border: 1px solid var(--vp-c-divider);
border-radius: 20px; background: var(--vp-c-bg);
cursor: pointer; transition: all 0.2s; font-size: 13px;
}
.tab-btn:hover { background: var(--vp-c-bg-alt); }
.tab-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.editor-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
}
.editor-panel, .output-panel {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 14px;
}
.panel-label {
font-weight: 600; font-size: 12px; margin-bottom: 6px;
color: var(--vp-c-text-2);
}
.prompt-input {
width: 100%; padding: 10px; border: 1px solid var(--vp-c-divider);
border-radius: 8px; background: var(--vp-c-bg-soft);
font-size: 13px; line-height: 1.5; resize: vertical;
font-family: var(--vp-font-family-mono);
color: var(--vp-c-text-1); margin-bottom: 10px;
box-sizing: border-box;
}
.prompt-input:focus {
outline: none; border-color: var(--vp-c-brand);
}
.run-btn {
width: 100%; padding: 10px; background: var(--vp-c-brand);
color: white; border: none; border-radius: 8px;
font-size: 13px; cursor: pointer; transition: background 0.2s;
}
.run-btn:hover { background: var(--vp-c-brand-dark); }
.output-box {
background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider);
border-radius: 8px; padding: 14px; min-height: 120px;
font-size: 13px; line-height: 1.7;
}
.output-text { white-space: pre-wrap; color: var(--vp-c-text-1); }
.output-placeholder { color: var(--vp-c-text-3); text-align: center; padding: 30px 0; }
.generating { color: var(--vp-c-brand); text-align: center; padding: 30px 0; }
.dot-anim { animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.3; } }
.quality-bar { margin-top: 12px; }
.quality-label { font-weight: 600; font-size: 12px; margin-bottom: 8px; }
.quality-metrics { display: flex; flex-direction: column; gap: 6px; }
.metric { display: flex; align-items: center; gap: 8px; }
.metric-name { font-size: 11px; width: 50px; color: var(--vp-c-text-2); }
.meter {
flex: 1; height: 8px; background: var(--vp-c-bg-soft);
border-radius: 4px; overflow: hidden;
}
.meter-fill {
height: 100%; border-radius: 4px;
transition: width 0.6s ease;
}
.metric-score { font-size: 11px; font-weight: 600; width: 36px; text-align: right; }
.tips-bar {
margin-top: 16px; padding: 12px 16px;
background: var(--vp-c-brand-soft); border-radius: 6px; font-size: 13px;
}
.tips-label { font-weight: 600; color: var(--vp-c-brand-dark); }
.tips-text { color: var(--vp-c-text-1); }
</style>
@@ -0,0 +1,373 @@
<template>
<div class="ai-vs-traditional-demo">
<div class="demo-header">
<span class="icon">🤖</span>
<span class="title">AI 工程师 vs 传统工程师</span>
<span class="subtitle">工作方式的差异</span>
</div>
<div class="comparison-container">
<div class="comparison-column traditional">
<div class="column-header">
<span class="col-icon">👨💻</span>
<span class="col-title">传统工程师</span>
</div>
<div class="work-flow">
<div v-for="(step, index) in traditionalSteps" :key="index" class="flow-step">
<span class="step-num">{{ index + 1 }}</span>
<span class="step-text">{{ step }}</span>
</div>
</div>
<div class="column-stats">
<div class="stat-item">
<span class="stat-label">编码时间占比</span>
<span class="stat-value">60-70%</span>
</div>
<div class="stat-item">
<span class="stat-label">思考时间占比</span>
<span class="stat-value">30-40%</span>
</div>
</div>
</div>
<div class="vs-divider">
<span class="vs-text">VS</span>
</div>
<div class="comparison-column ai">
<div class="column-header">
<span class="col-icon">🤖</span>
<span class="col-title">AI 工程师</span>
</div>
<div class="work-flow">
<div v-for="(step, index) in aiSteps" :key="index" class="flow-step">
<span class="step-num">{{ index + 1 }}</span>
<span class="step-text">{{ step }}</span>
</div>
</div>
<div class="column-stats">
<div class="stat-item">
<span class="stat-label">编码时间占比</span>
<span class="stat-value">20-30%</span>
</div>
<div class="stat-item">
<span class="stat-label">思考时间占比</span>
<span class="stat-value">70-80%</span>
</div>
</div>
</div>
</div>
<div class="skill-shift">
<div class="shift-title">📊 能力重心转移</div>
<div class="shift-grid">
<div v-for="item in skillShift" :key="item.from" class="shift-item">
<div class="shift-from">
<span class="arrow"></span>
<span class="text">{{ item.from }}</span>
<span class="trend down">重要性下降</span>
</div>
<div class="shift-to">
<span class="arrow"></span>
<span class="text">{{ item.to }}</span>
<span class="trend up">重要性上升</span>
</div>
</div>
</div>
</div>
<div class="ai-insight">
<div class="insight-icon">💡</div>
<div class="insight-text">
<strong>AI 时代的核心竞争力</strong>不是"会写代码"而是"会描述需求、会判断对错、会设计方案"AI 是你的编程助手<strong>决策者永远是你</strong>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const traditionalSteps = ref([
'理解需求',
'查阅文档学习语法',
'手写代码实现',
'调试修复 Bug',
'优化代码性能',
'编写测试用例'
])
const aiSteps = ref([
'理解需求',
'用自然语言描述给 AI',
'审核 AI 生成的代码',
'判断是否符合预期',
'调整需求重新生成',
'整合到项目中'
])
const skillShift = ref([
{ from: '语法记忆', to: '需求描述能力' },
{ from: '手写代码速度', to: '代码审核能力' },
{ from: '查文档能力', to: '架构设计能力' },
{ from: '调试技巧', to: '问题定位能力' }
])
</script>
<style scoped>
.ai-vs-traditional-demo {
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #fed7aa;
}
.icon {
font-size: 28px;
}
.title {
font-size: 20px;
font-weight: 700;
color: #1e293b;
}
.subtitle {
font-size: 14px;
color: #64748b;
margin-left: auto;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
margin-bottom: 24px;
}
.comparison-column {
background: white;
border-radius: 12px;
padding: 16px;
}
.column-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e2e8f0;
}
.col-icon {
font-size: 24px;
}
.col-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.work-flow {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.flow-step {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f8fafc;
border-radius: 6px;
}
.step-num {
width: 20px;
height: 20px;
background: #e2e8f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
color: #64748b;
}
.traditional .step-num {
background: #dbeafe;
color: #1d4ed8;
}
.ai .step-num {
background: #dcfce7;
color: #15803d;
}
.step-text {
font-size: 13px;
color: #475569;
}
.column-stats {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #e2e8f0;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-label {
font-size: 12px;
color: #64748b;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
}
.vs-text {
font-size: 18px;
font-weight: 700;
color: #f97316;
background: white;
padding: 8px 12px;
border-radius: 8px;
}
.skill-shift {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
}
.shift-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin-bottom: 16px;
}
.shift-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.shift-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
}
.shift-from,
.shift-to {
display: flex;
align-items: center;
gap: 8px;
}
.arrow {
font-size: 14px;
font-weight: 700;
}
.shift-from .arrow {
color: #ef4444;
}
.shift-to .arrow {
color: #22c55e;
}
.text {
font-size: 13px;
color: #475569;
}
.trend {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-left: auto;
}
.trend.down {
background: #fee2e2;
color: #dc2626;
}
.trend.up {
background: #dcfce7;
color: #16a34a;
}
.ai-insight {
background: white;
border-radius: 12px;
padding: 16px;
display: flex;
gap: 12px;
border-left: 4px solid #f97316;
}
.insight-icon {
font-size: 20px;
}
.insight-text {
font-size: 14px;
color: #475569;
line-height: 1.6;
}
.insight-text strong {
color: #1e293b;
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
.vs-divider {
padding: 8px 0;
}
.shift-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,139 @@
<template>
<div class="bios-post-demo">
<div class="demo-label">BIOS POST 硬件自检 点击查看检测项目</div>
<div class="post-items">
<div
v-for="item in postItems"
:key="item.name"
class="post-item"
:class="{ passed: item.passed, error: item.error }"
@click="item.passed = !item.passed"
>
<div class="item-status">{{ item.passed ? '✅' : item.error ? '❌' : '⏳' }}</div>
<div class="item-info">
<div class="item-name">{{ item.name }}</div>
<div class="item-desc">{{ item.desc }}</div>
</div>
</div>
</div>
<div class="post-result">
<span v-if="allPassed" class="result-pass"> 自检通过准备启动</span>
<span v-else class="result-pending"> 点击项目模拟检测状态</span>
</div>
<div class="tap-hint">👆 点击模拟检测结果</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const postItems = ref([
{ name: 'CPU', desc: '处理器完整性检测', passed: false, error: false },
{ name: '内存', desc: 'RAM 容量和可用性检测', passed: false, error: false },
{ name: '显卡', desc: '显示适配器初始化', passed: false, error: false },
{ name: '硬盘', desc: '存储设备识别', passed: false, error: false },
{ name: '键盘', desc: '键盘接口检测', passed: false, error: false },
{ name: '鼠标', desc: '鼠标接口检测', passed: false, error: false }
])
const allPassed = computed(() => postItems.value.every(item => item.passed))
</script>
<style scoped>
.bios-post-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.post-items {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.4rem;
}
.post-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
transition: all 0.3s;
}
.post-item.passed {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
.post-item.error {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.item-status {
font-size: 1rem;
flex-shrink: 0;
}
.item-info {
flex: 1;
min-width: 0;
}
.item-name {
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.item-desc {
font-size: 0.62rem;
color: var(--vp-c-text-3);
margin-top: 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.post-result {
margin-top: 0.75rem;
text-align: center;
padding: 0.4rem;
border-radius: 4px;
font-size: 0.75rem;
}
.result-pass {
color: #22c55e;
font-weight: 600;
}
.result-pending {
color: var(--vp-c-text-3);
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.5rem;
}
</style>
@@ -0,0 +1,167 @@
<template>
<div class="backend-demo">
<div class="demo-header">
<span class="title">后端核心概念</span>
<span class="subtitle">服务器端的核心职责</span>
</div>
<div class="core-grid">
<div v-for="core in coreConcepts" :key="core.name" class="core-card">
<div class="core-name">{{ core.name }}</div>
<div class="core-desc">{{ core.desc }}</div>
<div class="core-examples">
<span v-for="ex in core.examples" :key="ex" class="example-tag">{{ ex }}</span>
</div>
</div>
</div>
<div class="flow-section">
<div class="flow-title">请求处理流程</div>
<div class="flow-steps">
<span v-for="(step, i) in flowSteps" :key="step">
<span class="flow-step">{{ step }}</span>
<span v-if="i < flowSteps.length - 1" class="flow-arrow"></span>
</span>
</div>
</div>
<div class="info-box">
<strong>后端的核心价值</strong>不是写代码而是设计系统如何让系统稳定安全高效可扩展才是后端工程师的真正能力
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const coreConcepts = ref([
{ name: 'API 设计', desc: '定义客户端如何与服务端交互', examples: ['RESTful', 'GraphQL'] },
{ name: '业务逻辑', desc: '处理核心业务规则和流程', examples: ['订单处理', '支付流程'] },
{ name: '数据存储', desc: '数据的持久化和查询', examples: ['MySQL', 'Redis'] },
{ name: '认证授权', desc: '用户身份验证和权限控制', examples: ['JWT', 'OAuth'] },
{ name: '性能优化', desc: '缓存、异步、并发处理', examples: ['缓存', '消息队列'] },
{ name: '安全防护', desc: '防止攻击和数据泄露', examples: ['SQL注入防护', 'HTTPS'] }
])
const flowSteps = ref(['接收请求', '路由解析', '业务处理', '数据操作', '返回响应'])
</script>
<style scoped>
.backend-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.core-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.core-card {
padding: 0.6rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.core-name {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.core-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.35rem;
}
.core-examples {
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
}
.example-tag {
font-size: 0.65rem;
padding: 0.1rem 0.3rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.flow-section {
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
margin-bottom: 1rem;
}
.flow-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.flow-steps {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
}
.flow-step {
font-size: 0.72rem;
padding: 0.2rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
color: var(--vp-c-text-2);
}
.flow-arrow {
color: var(--vp-c-text-3);
font-size: 0.8rem;
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.core-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -0,0 +1,137 @@
<template>
<div class="boot-demo">
<div class="demo-label">操作系统启动流程 点击逐步启动</div>
<div class="timeline">
<div
v-for="(step, index) in steps"
:key="step.name"
class="timeline-item"
:class="{ active: currentStep >= index }"
@click="currentStep = index"
>
<div class="timeline-marker">
<span class="marker-dot" :class="{ filled: currentStep >= index }">{{ index + 1 }}</span>
<span v-if="index < steps.length - 1" class="marker-line" :class="{ filled: currentStep >= index }"></span>
</div>
<div class="timeline-content">
<div class="step-name">{{ step.name }}</div>
<div class="step-desc">{{ step.desc }}</div>
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各步骤</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{ name: '引导程序', desc: '从硬盘读取 bootloader' },
{ name: '内核加载', desc: '加载操作系统内核' },
{ name: '系统服务', desc: '启动各种后台服务' },
{ name: '桌面环境', desc: '显示登录界面和桌面' }
]
</script>
<style scoped>
.boot-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.timeline {
display: flex;
flex-direction: column;
}
.timeline-item {
display: flex;
gap: 0.6rem;
padding: 0.3rem 0;
opacity: 0.4;
transition: opacity 0.3s;
}
.timeline-item.active {
opacity: 1;
}
.timeline-marker {
display: flex;
flex-direction: column;
align-items: center;
width: 1.5rem;
}
.marker-dot {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 600;
color: var(--vp-c-text-3);
transition: all 0.3s;
}
.marker-dot.filled {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.marker-line {
flex: 1;
width: 2px;
background: var(--vp-c-divider);
min-height: 1.5rem;
}
.marker-line.filled {
background: var(--vp-c-brand);
}
.timeline-content {
flex: 1;
padding-bottom: 0.8rem;
}
.step-name {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.step-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.5rem;
}
</style>
@@ -0,0 +1,130 @@
<template>
<div class="browser-demo">
<div class="demo-label">浏览器架构 点击查看各模块</div>
<div class="browser-schema">
<div class="browser-layers">
<div
v-for="layer in layers"
:key="layer.name"
class="layer"
:class="{ active: activeLayer === layer.name }"
@click="activeLayer = activeLayer === layer.name ? '' : layer.name"
>
<div class="layer-header">
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-icon">{{ layer.icon }}</span>
</div>
<div v-if="activeLayer === layer.name" class="layer-detail">
<div class="detail-text">{{ layer.desc }}</div>
<div class="detail-examples">
<span v-for="ex in layer.examples" :key="ex" class="example-tag">{{ ex }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各模块详情</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeLayer = ref('')
const layers = [
{ name: '用户界面', icon: '🎨', desc: '你看到的地址栏、标签页、书签等', examples: ['地址栏', '标签页', '书签栏', '工具栏'] },
{ name: '浏览器引擎', icon: '⚙️', desc: '协调 UI 和渲染引擎的桥梁', examples: ['Chrome: Blink', 'Firefox: Gecko', 'Safari: WebKit'] },
{ name: '渲染引擎', icon: '📄', desc: '解析 HTML/CSS,把网页显示出来', examples: ['Blink', 'Gecko', 'WebKit'] },
{ name: 'JavaScript 引擎', icon: '⚡', desc: '执行页面中的 JavaScript 代码', examples: ['V8', 'SpiderMonkey', 'JavaScriptCore'] },
{ name: '网络模块', icon: '🌐', desc: '发送 HTTP/HTTPS 请求', examples: ['HTTP', 'HTTP/2', 'HTTP/3', 'WebSocket'] },
{ name: '数据存储', icon: '💾', desc: '保存 Cookie、缓存等数据', examples: ['Cookie', 'LocalStorage', 'Cache'] }
]
</script>
<style scoped>
.browser-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.browser-layers {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.layer {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.7rem;
transition: all 0.3s;
}
.layer.active {
border-color: var(--vp-c-brand);
}
.layer-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.layer-name {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.layer-icon {
font-size: 0.9rem;
}
.layer-detail {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.detail-text {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.4rem;
}
.detail-examples {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.example-tag {
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,286 @@
<template>
<div class="career-path-demo">
<div class="demo-header">
<span class="icon">🚀</span>
<span class="title">工程师成长路径</span>
<span class="subtitle">从入门到精通的技能演进</span>
</div>
<div class="path-container">
<div
v-for="(stage, index) in stages"
:key="stage.name"
class="stage-card"
:class="{ active: activeStage === index }"
@click="activeStage = index"
>
<div class="stage-header">
<span class="stage-icon">{{ stage.icon }}</span>
<span class="stage-name">{{ stage.name }}</span>
<span class="stage-time">{{ stage.time }}</span>
</div>
<div class="stage-content">
<div class="stage-desc">{{ stage.desc }}</div>
<div class="stage-skills">
<span class="skill-label">核心技能</span>
<div class="skill-tags">
<span v-for="skill in stage.skills" :key="skill" class="skill-tag">
{{ skill }}
</span>
</div>
</div>
<div class="stage-output">
<span class="output-label">典型产出</span>
<span class="output-text">{{ stage.output }}</span>
</div>
</div>
<div v-if="index < stages.length - 1" class="stage-arrow"></div>
</div>
</div>
<div class="path-insight">
<div class="insight-icon">💡</div>
<div class="insight-content">
<div class="insight-title">成长关键点</div>
<ul class="insight-list">
<li> 1-2 打基础建立"能独立完成任务"的能力</li>
<li>2-3 选方向在某个领域建立深度</li>
<li>3-5 横向扩展培养架构思维和团队协作能力</li>
<li>5 +技术决策团队带领技术影响力</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeStage = ref(0)
const stages = ref([
{
name: '入门期',
icon: '🌱',
time: '0-1 年',
desc: '学习基础语法和工具,能完成简单任务',
skills: ['一门语言基础', 'Git 使用', '调试技巧', '阅读文档'],
output: '能独立完成小功能、修复简单 Bug'
},
{
name: '成长期',
icon: '🌿',
time: '1-2 年',
desc: '熟悉常用框架和最佳实践,能独立负责模块',
skills: ['框架熟练', '代码规范', '单元测试', 'API 设计'],
output: '独立负责一个功能模块,代码质量稳定'
},
{
name: '进阶期',
icon: '🌳',
time: '2-3 年',
desc: '深入某个领域,开始有技术选型能力',
skills: ['领域深入', '性能优化', '架构设计', '技术选型'],
output: '主导技术方案设计,解决复杂问题'
},
{
name: '成熟期',
icon: '🌲',
time: '3-5 年',
desc: '全栈能力或领域专家,能带领小团队',
skills: ['全栈能力', '团队协作', '技术分享', '项目管理'],
output: '负责核心系统,指导新人成长'
},
{
name: '专家期',
icon: '🏔️',
time: '5 年+',
desc: '技术决策者,有行业影响力',
skills: ['技术战略', '团队建设', '行业洞察', '创新引领'],
output: '技术方向决策,培养技术团队'
}
])
</script>
<style scoped>
.career-path-demo {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #e2e8f0;
}
.icon {
font-size: 28px;
}
.title {
font-size: 20px;
font-weight: 700;
color: #1e293b;
}
.subtitle {
font-size: 14px;
color: #64748b;
margin-left: auto;
}
.path-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.stage-card {
background: white;
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
position: relative;
}
.stage-card:hover {
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stage-card.active {
border-color: #3b82f6;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
}
.stage-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.stage-icon {
font-size: 24px;
}
.stage-name {
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.stage-time {
font-size: 12px;
color: #64748b;
background: #f1f5f9;
padding: 2px 8px;
border-radius: 4px;
margin-left: auto;
}
.stage-content {
padding-left: 36px;
}
.stage-desc {
font-size: 14px;
color: #475569;
margin-bottom: 12px;
}
.stage-skills {
margin-bottom: 8px;
}
.skill-label,
.output-label {
font-size: 12px;
color: #64748b;
margin-right: 8px;
}
.skill-tags {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.skill-tag {
font-size: 12px;
background: #e0f2fe;
color: #0369a1;
padding: 2px 8px;
border-radius: 4px;
}
.stage-output {
font-size: 13px;
color: #475569;
}
.output-text {
color: #1e293b;
}
.stage-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: #cbd5e1;
}
.path-insight {
margin-top: 24px;
background: white;
border-radius: 12px;
padding: 16px;
display: flex;
gap: 16px;
border-left: 4px solid #f59e0b;
}
.insight-icon {
font-size: 24px;
}
.insight-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin-bottom: 8px;
}
.insight-list {
margin: 0;
padding-left: 20px;
font-size: 13px;
color: #475569;
line-height: 1.8;
}
.insight-list li {
margin-bottom: 4px;
}
@media (max-width: 640px) {
.stage-content {
padding-left: 0;
}
.stage-arrow {
display: none;
}
}
</style>
@@ -0,0 +1,164 @@
<template>
<div class="field-map-demo">
<div class="demo-header">
<span class="title">计算机领域全景图</span>
<span class="subtitle">点击查看详情</span>
</div>
<div class="field-grid">
<div
v-for="field in fields"
:key="field.name"
class="field-card"
:class="{ active: activeField === field.name }"
@click="activeField = field.name"
>
<div class="field-name">{{ field.name }}</div>
<div class="field-desc">{{ field.desc }}</div>
<div class="field-techs">
<span v-for="tech in field.techs" :key="tech" class="tech-tag">{{ tech }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>建议</strong>不要试图一次学完所有方向先选一个方向深入建立"根据地"再横向扩展
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeField = ref('前端')
const fields = ref([
{
name: '前端',
desc: '用户能看到、能交互的一切',
techs: ['HTML/CSS', 'JavaScript', 'React/Vue']
},
{
name: '后端',
desc: '服务器端的业务逻辑和数据处理',
techs: ['Node.js', 'Go', 'Java', 'Python']
},
{
name: '移动端',
desc: '手机上的应用体验',
techs: ['Swift', 'Kotlin', 'Flutter']
},
{
name: 'AI/算法',
desc: '让系统变"聪明"',
techs: ['PyTorch', 'TensorFlow', '机器学习']
},
{
name: '运维/DevOps',
desc: '保证系统稳定运行',
techs: ['Docker', 'K8s', 'CI/CD']
},
{
name: '数据工程',
desc: '数据采集、存储、分析',
techs: ['SQL', 'Spark', '数据仓库']
}
])
</script>
<style scoped>
.field-map-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.field-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.field-card {
padding: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.field-card:hover {
border-color: var(--vp-c-brand-1);
}
.field-card.active {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-bg-soft);
}
.field-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.field-desc {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.5rem;
}
.field-techs {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.tech-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.field-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -0,0 +1,172 @@
<template>
<div class="desktop-demo">
<div class="demo-label">桌面是如何出现的 点击加载</div>
<div class="desktop-view" @click="loading = !loading">
<div class="screen" :class="{ loaded: loading }">
<div class="boot-screen" v-if="!loading">
<div class="boot-logo"></div>
<div class="boot-text">正在启动...</div>
</div>
<div class="desktop" v-else>
<div class="desktop-icons">
<div class="desktop-icon">📁</div>
<div class="desktop-icon">🗑</div>
<div class="desktop-icon"></div>
<div class="desktop-icon">🌐</div>
</div>
<div class="taskbar">
<div class="taskbar-icon">🏠</div>
<div class="taskbar-icon">📊</div>
<div class="taskbar-time">{{ time }}</div>
</div>
</div>
</div>
</div>
<div class="tap-hint">👆 点击切换开机状态</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const loading = ref(false)
const time = ref('')
let timer = null
const updateTime = () => {
const now = new Date()
time.value = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
onMounted(() => {
updateTime()
timer = setInterval(updateTime, 1000)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<style scoped>
.desktop-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.desktop-view {
display: flex;
justify-content: center;
}
.screen {
width: 14rem;
height: 9rem;
background: #1a1a1a;
border-radius: 6px;
overflow: hidden;
transition: all 0.5s;
}
.screen.loaded {
background: linear-gradient(180deg, #2d5af5 0%, #1e3a8a 100%);
}
.boot-screen {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
}
.boot-logo {
font-size: 2rem;
margin-bottom: 0.5rem;
animation: spin 2s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.boot-text {
font-size: 0.75rem;
}
.desktop {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0.5rem;
}
.desktop-icons {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.desktop-icon {
width: 2.2rem;
height: 2.2rem;
background: rgba(255,255,255,0.15);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
}
.taskbar {
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(0,0,0,0.3);
padding: 0.3rem 0.5rem;
border-radius: 4px;
}
.taskbar-icon {
width: 1.5rem;
height: 1.5rem;
background: rgba(255,255,255,0.2);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
}
.taskbar-time {
margin-left: auto;
font-size: 0.7rem;
color: white;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,162 @@
<template>
<div class="skill-shift-demo">
<div class="demo-header">
<span class="title">能力重要性变化</span>
<span class="subtitle">AI 时代哪些能力更重要了</span>
</div>
<div class="comparison-grid">
<div class="column">
<div class="column-title">传统时代更重要</div>
<div class="skill-list">
<div v-for="skill in beforeSkills" :key="skill.name" class="skill-item">
<span class="skill-name">{{ skill.name }}</span>
<div class="skill-bar">
<div class="bar-fill before" :style="{ width: skill.level + '%' }"></div>
</div>
<span class="skill-desc">{{ skill.desc }}</span>
</div>
</div>
</div>
<div class="column">
<div class="column-title">AI 时代更重要</div>
<div class="skill-list">
<div v-for="skill in afterSkills" :key="skill.name" class="skill-item">
<span class="skill-name">{{ skill.name }}</span>
<div class="skill-bar">
<div class="bar-fill after" :style="{ width: skill.level + '%' }"></div>
</div>
<span class="skill-desc">{{ skill.desc }}</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>关键洞察</strong>AI 能帮你写代码但判断力架构思维领域知识调试能力是 AI 替代不了的
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const beforeSkills = ref([
{ name: '语法记忆', level: 90, desc: '熟记 API 和语法细节' },
{ name: '手写代码速度', level: 85, desc: '快速敲代码的能力' },
{ name: '查文档能力', level: 80, desc: '快速找到 API 用法' }
])
const afterSkills = ref([
{ name: '需求描述能力', level: 95, desc: '用自然语言准确描述需求' },
{ name: '代码审核能力', level: 90, desc: '判断 AI 生成代码的对错' },
{ name: '架构设计能力', level: 85, desc: '设计系统整体结构' },
{ name: '问题定位能力', level: 80, desc: '出问题时知道从哪排查' }
])
</script>
<style scoped>
.skill-shift-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.column-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px dashed var(--vp-c-divider);
}
.skill-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.skill-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.skill-name {
font-size: 0.82rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.skill-bar {
height: 6px;
background: var(--vp-c-divider);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.bar-fill.before {
background: var(--vp-c-text-3);
}
.bar-fill.after {
background: var(--vp-c-brand-1);
}
.skill-desc {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.comparison-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,190 @@
<template>
<div class="framework-demo">
<div class="demo-header">
<span class="title">前端框架演进</span>
<span class="subtitle"> jQuery 到现代框架</span>
</div>
<div class="timeline">
<div v-for="(era, index) in eras" :key="era.name" class="era-item">
<div class="era-marker">
<span class="era-dot"></span>
<span v-if="index < eras.length - 1" class="era-line"></span>
</div>
<div class="era-content">
<div class="era-header">
<span class="era-name">{{ era.name }}</span>
<span class="era-time">{{ era.time }}</span>
</div>
<div class="era-desc">{{ era.desc }}</div>
<div class="era-techs">
<span v-for="tech in era.techs" :key="tech" class="tech-tag">{{ tech }}</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>框架的本质</strong>解决"数据变化后如何高效更新 UI"的问题现代框架让你只需关注"数据是什么"框架自动处理"UI 怎么变"
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const eras = ref([
{
name: '原生时代',
time: '1990s',
desc: '直接操作 DOM,一切从零开始',
techs: ['HTML', 'CSS', 'JavaScript']
},
{
name: 'jQuery 时代',
time: '2006-2015',
desc: '简化 DOM 操作,跨浏览器兼容',
techs: ['jQuery', 'Bootstrap']
},
{
name: 'MVVM 时代',
time: '2010-2015',
desc: '数据驱动视图,双向绑定',
techs: ['Angular.js', 'Knockout']
},
{
name: '组件化时代',
time: '2013-至今',
desc: '声明式、组件化、虚拟 DOM',
techs: ['React', 'Vue', 'Angular']
},
{
name: '新时代',
time: '2020-至今',
desc: '编译时优化,更少运行时开销',
techs: ['Svelte', 'Solid']
}
])
</script>
<style scoped>
.framework-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.timeline {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.era-item {
display: flex;
gap: 0.75rem;
}
.era-marker {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.era-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vp-c-brand-1);
border: 2px solid var(--vp-c-bg);
}
.era-line {
width: 2px;
flex: 1;
background: var(--vp-c-divider);
margin: 2px 0;
}
.era-content {
flex: 1;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
margin-bottom: 0.25rem;
}
.era-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.era-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.era-time {
font-size: 0.7rem;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-soft);
padding: 0.1rem 0.35rem;
border-radius: 3px;
}
.era-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.era-techs {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.tech-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,158 @@
<template>
<div class="triad-demo">
<div class="demo-header">
<span class="title">前端三件套</span>
<span class="subtitle">网页开发的三大基石</span>
</div>
<div class="triad-grid">
<div
v-for="tech in triad"
:key="tech.name"
class="tech-card"
:class="{ active: activeTech === tech.name }"
@click="activeTech = tech.name"
>
<div class="tech-name">{{ tech.name }}</div>
<div class="tech-role">{{ tech.role }}</div>
<div class="tech-analogy">{{ tech.analogy }}</div>
<div class="tech-examples">
<span v-for="ex in tech.examples" :key="ex" class="example-tag">{{ ex }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>协作关系</strong>HTML 搭骨架CSS 穿衣服JavaScript 让它动起来三者缺一不可
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeTech = ref('HTML')
const triad = ref([
{
name: 'HTML',
role: '结构层',
analogy: '房子的骨架:墙、门、窗',
examples: ['div', 'span', 'form', 'input']
},
{
name: 'CSS',
role: '表现层',
analogy: '房子的装修:颜色、位置、大小',
examples: ['color', 'flex', 'grid', 'animation']
},
{
name: 'JavaScript',
role: '行为层',
analogy: '房子的智能:开关灯、开门',
examples: ['事件', 'DOM操作', '网络请求']
}
])
</script>
<style scoped>
.triad-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.triad-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.tech-card {
padding: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.tech-card:hover {
border-color: var(--vp-c-brand-1);
}
.tech-card.active {
border-color: var(--vp-c-brand-1);
}
.tech-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.tech-role {
font-size: 0.75rem;
color: var(--vp-c-brand-1);
margin-bottom: 0.25rem;
}
.tech-analogy {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.5rem;
}
.tech-examples {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.example-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.triad-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,120 @@
<template>
<div class="full-process-demo">
<div class="demo-label">完整流程概览 点击逐步演示</div>
<div class="process-flow">
<div
v-for="(phase, index) in phases"
:key="phase.name"
class="phase"
:class="{ active: currentPhase >= index }"
@click="currentPhase = index"
>
<div class="phase-icon">{{ phase.icon }}</div>
<div class="phase-name">{{ phase.name }}</div>
<div class="phase-steps">
<span v-for="step in phase.steps" :key="step" class="step-tag">{{ step }}</span>
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各阶段</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentPhase = ref(0)
const phases = [
{ icon: '🔌', name: '硬件启动', steps: ['电源', '主板', 'CPU', 'BIOS'] },
{ icon: '💻', name: '系统启动', steps: ['引导程序', '内核', '服务', '桌面'] },
{ icon: '🌐', name: '浏览器', steps: ['双击图标', '加载程序', '显示窗口'] },
{ icon: '📡', name: '网络请求', steps: ['URL解析', 'DNS', 'TCP', 'HTTP'] },
{ icon: '🎨', name: '页面渲染', steps: ['HTML解析', 'CSS', '布局', '绘制'] }
]
</script>
<style scoped>
.full-process-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.process-flow {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.phase {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.6rem 0.4rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
opacity: 0.4;
transition: all 0.3s;
}
.phase.active {
opacity: 1;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.phase-icon {
font-size: 1.4rem;
margin-bottom: 0.3rem;
}
.phase-name {
font-size: 0.7rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.4rem;
}
.phase-steps {
display: flex;
flex-direction: column;
gap: 0.15rem;
width: 100%;
}
.step-tag {
font-size: 0.58rem;
text-align: center;
padding: 0.15rem 0;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-3);
}
.phase.active .step-tag {
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,134 @@
<template>
<div class="fullstack-demo">
<div class="demo-header">
<span class="title">全栈技能树</span>
<span class="subtitle">前后端通吃的核心能力</span>
</div>
<div class="skill-sections">
<div class="skill-section">
<div class="section-title">前端能力</div>
<div class="skill-list">
<div v-for="skill in frontendSkills" :key="skill" class="skill-item">{{ skill }}</div>
</div>
</div>
<div class="skill-section bridge">
<div class="section-title">全栈核心</div>
<div class="skill-list">
<div v-for="skill in bridgeSkills" :key="skill" class="skill-item highlight">{{ skill }}</div>
</div>
</div>
<div class="skill-section">
<div class="section-title">后端能力</div>
<div class="skill-list">
<div v-for="skill in backendSkills" :key="skill" class="skill-item">{{ skill }}</div>
</div>
</div>
</div>
<div class="info-box">
<strong>全栈不等于全部精通</strong>核心是打通前后端能独立完成一个完整功能不需要在每个领域都达到专家级别
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const frontendSkills = ref(['HTML/CSS', 'JavaScript', '框架使用', '响应式设计'])
const backendSkills = ref(['API 设计', '数据库操作', '业务逻辑', '服务器部署'])
const bridgeSkills = ref(['HTTP 协议', 'Git 协作', '调试能力', '系统设计'])
</script>
<style scoped>
.fullstack-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.skill-sections {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.75rem;
}
.skill-section {
padding: 0.6rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.skill-section.bridge {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-brand-1);
}
.section-title {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
padding-bottom: 0.35rem;
border-bottom: 1px dashed var(--vp-c-divider);
}
.skill-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.skill-item {
font-size: 0.72rem;
padding: 0.25rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.skill-item.highlight {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.skill-sections {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,128 @@
<template>
<div class="language-selection-demo">
<div class="demo-header">
<span class="title">语言选择指南</span>
<span class="subtitle">根据目标选语言</span>
</div>
<div class="selection-grid">
<div v-for="item in selections" :key="item.goal" class="selection-card">
<div class="goal-name">{{ item.goal }}</div>
<div class="goal-desc">{{ item.desc }}</div>
<div class="goal-langs">
<span class="lang-label">推荐</span>
<span v-for="lang in item.langs" :key="lang" class="lang-tag">{{ lang }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>核心原则</strong>语言只是工具重要的是解决问题的能力先精通一门再触类旁通
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selections = ref([
{ goal: 'Web 前端', desc: '网页、小程序、H5', langs: ['JavaScript', 'TypeScript'] },
{ goal: 'Web 后端', desc: 'API 服务、业务系统', langs: ['Node.js', 'Go', 'Java', 'Python'] },
{ goal: '移动端', desc: 'iOS / Android 应用', langs: ['Swift', 'Kotlin', 'Flutter'] },
{ goal: 'AI / 数据科学', desc: '机器学习、数据分析', langs: ['Python'] },
{ goal: '系统编程', desc: '操作系统、嵌入式', langs: ['C', 'C++', 'Rust'] },
{ goal: '快速原型', desc: '脚本、自动化、小工具', langs: ['Python', 'Shell'] }
])
</script>
<style scoped>
.language-selection-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.selection-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.selection-card {
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.goal-name {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.15rem;
}
.goal-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.35rem;
}
.goal-langs {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25rem;
}
.lang-label {
font-size: 0.68rem;
color: var(--vp-c-text-3);
}
.lang-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-brand-1);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.selection-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,134 @@
<template>
<div class="strategy-demo">
<div class="demo-header">
<span class="title">Vibe Coding 学习策略</span>
<span class="subtitle">AI 时代怎么学更高效</span>
</div>
<div class="strategy-list">
<div v-for="(strategy, index) in strategies" :key="index" class="strategy-item">
<div class="strategy-num">{{ index + 1 }}</div>
<div class="strategy-content">
<div class="strategy-title">{{ strategy.title }}</div>
<div class="strategy-desc">{{ strategy.desc }}</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心原则</strong>AI 是你的编程助手但决策者永远是你学会提问学会判断学会整合比学会写代码更重要
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const strategies = ref([
{
title: '先理解,再让 AI 写',
desc: '不要一上来就让 AI 写代码。先理解问题是什么,想清楚解决方案,再用 AI 加速实现。'
},
{
title: '把 AI 当结对编程伙伴',
desc: '遇到不懂的概念,问 AI 解释。遇到复杂问题,和 AI 讨论方案。AI 是你的知识渊博的同事。'
},
{
title: '学会审核 AI 的输出',
desc: 'AI 生成的代码不一定对。你需要有能力判断:逻辑对不对?有没有安全隐患?性能如何?'
},
{
title: '建立自己的知识体系',
desc: 'AI 能帮你查漏补缺,但核心知识框架要自己建立。知道"有什么",才能问出"怎么用"。'
},
{
title: '在实践中学习',
desc: '做真实的项目,解决真实的问题。AI 帮你扫清语法障碍,你专注于解决业务问题。'
}
])
</script>
<style scoped>
.strategy-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.strategy-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.strategy-item {
display: flex;
gap: 0.75rem;
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.strategy-num {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
background: var(--vp-c-brand-1);
border-radius: 50%;
color: white;
flex-shrink: 0;
}
.strategy-content {
flex: 1;
}
.strategy-title {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.strategy-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
line-height: 1.5;
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,386 @@
<template>
<div class="cpu-internal-demo">
<div class="demo-title">CPU 内部微架构剖析</div>
<div class="demo-subtitle">点击下方各个模块查看其内部由哪些子电路构成以及工作原理</div>
<div class="demo-container">
<div class="cpu-chip">
<div class="chip-title">CPU 核心 (Central Processing Unit)</div>
<div class="bus-top address-bus" :class="{ active: currentModule === 'address_bus' }" @click="selectModule('address_bus')">地址总线 (Address Bus)</div>
<div class="bus-top data-bus" :class="{ active: currentModule === 'data_bus' }" @click="selectModule('data_bus')">数据总线 (Data Bus)</div>
<div class="cpu-layout">
<!-- 左侧控制单元 -->
<div class="cu-section section-box" :class="{ active: currentModule === 'cu' }" @click.stop="selectModule('cu')">
<h4 class="section-title">控制单元 (Control Unit)</h4>
<div class="sub-modules">
<div class="sub-mod" :class="{ active: currentModule === 'pc' }" @click.stop="selectModule('pc')">程序计数器 (PC)</div>
<div class="sub-mod" :class="{ active: currentModule === 'ir' }" @click.stop="selectModule('ir')">指令寄存器 (IR)</div>
<div class="sub-mod" :class="{ active: currentModule === 'decoder' }" @click.stop="selectModule('decoder')">指令译码器</div>
<div class="sub-mod" :class="{ active: currentModule === 'clock' }" @click.stop="selectModule('clock')">时钟发生器</div>
</div>
<div class="control-lines">控制信号线 </div>
</div>
<!-- 右侧数据通道ALU + 寄存器 -->
<div class="datapath-section">
<!-- 寄存器组 -->
<div class="reg-section section-box" :class="{ active: currentModule === 'reg' }" @click.stop="selectModule('reg')">
<h4 class="section-title">寄存器组 (Register File)</h4>
<div class="sub-modules grid-2">
<div class="sub-mod">通用寄存器 R0-R3</div>
<div class="sub-mod">累加器 (ACC)</div>
</div>
</div>
<!-- ALU -->
<div class="alu-section section-box" :class="{ active: currentModule === 'alu' }" @click.stop="selectModule('alu')">
<h4 class="section-title">算术逻辑单元 (ALU)</h4>
<div class="sub-modules">
<div class="sub-mod" :class="{ active: currentModule === 'adder' }" @click.stop="selectModule('adder')">加法器电路</div>
<div class="sub-mod" :class="{ active: currentModule === 'flags' }" @click.stop="selectModule('flags')">状态标志 (Flags)</div>
</div>
</div>
</div>
</div>
<div class="bus-bottom control-bus" :class="{ active: currentModule === 'control_bus' }" @click="selectModule('control_bus')">控制总线 (Control Bus)</div>
</div>
<!-- 右侧/下方详细说明面板 -->
<div class="details-panel" v-if="currentModuleData">
<h3>{{ currentModuleData.title }}</h3>
<p class="desc">{{ currentModuleData.description }}</p>
<div class="circuit-impl" v-if="currentModuleData.subCircuit">
<h4><span class="icon">🔌</span> 底层子电路实现</h4>
<p>{{ currentModuleData.subCircuit }}</p>
</div>
</div>
<div class="details-panel empty" v-else>
<div class="empty-icon">🖱</div>
<p>点击左侧 CPU 内部结构图的各个模块<br>深入探索其微观电路实现</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentModule = ref(null)
const moduleInfo = {
// ALU 相关
alu: {
title: '算术逻辑单元 (ALU)',
description: 'ALU 是 CPU 中负责进行数学运算(加减法)和逻辑运算(与、或、非、异或)的核心引擎。所有的高级数学计算最终都会被分解为 ALU 能够执行的这些基础操作。',
subCircuit: '由海量的逻辑门组成。核心是半加器和全加器的级联(如行波进位加法器或超前进位加法器),并结合多路选择器(MUX)来决定当前是输出加法结果还是某种逻辑运算结果。'
},
adder: {
title: '加法器电路 (Adder)',
description: '负责执行二进制加法。',
subCircuit: '底层由异或门(XOR)负责本位相加,与门(AND)和或门(OR)负责产生对高位的进位信号。几十个全加器串联即可实现 32/64 位数的加法。'
},
flags: {
title: '状态标志寄存器 (Flags)',
description: '记录上一次 ALU 运算的“副作用”特征,例如结果是否为零(Z)、是否产生进位(C)、符号是正还是负(S)、是否溢出(O)。它是实现 `if/else` 等条件跳转指令的核心物理依据。',
subCircuit: '一组特定的触发器(Flip-Flops),每个触发器通过逻辑门直接连接在 ALU 的输出端电路上。'
},
// 寄存器相关
reg: {
title: '寄存器组 (Register File)',
description: 'CPU 内部的高速“草稿本”。由于直接嵌在指令执行的数据通路中,其读写速度和 CPU 主频几乎一致。用来暂存 ALU 需要的输入数据和刚刚算出的输出结果。',
subCircuit: '本质上是由成千上万个 D 型触发器(D Flip-Flop)按位宽(如 64 位)并列组合而成。配合多路选择器和地址译码电路,实现对特定“草稿本”的数据寻址读写。'
},
// 控制单元相关
cu: {
title: '控制单元 (Control Unit, CU)',
description: '整个 CPU 的“大脑和总指挥”。它并不直接参与运算,而是负责从内存读取指令,翻译指令,并像“拉线木偶”一样向全芯片发出各种导通和关断电信号,指挥其余部件开始工作。',
subCircuit: '通常存在有限状态机(FSM)或微程序的实现方式。本质上是一组庞大复杂的逻辑门网络和触发器组合,将输入的二进制指令(如 0x01)映射为激活对应模块的控制电平。'
},
pc: {
title: '程序计数器 (Program Counter, PC)',
description: '永远指向“下一条要执行的指令”在内存中的具体地址。每次成功执行完一条指令,它就会自动递增。当程序发生函数调用或循环跳转时,它的值会被强行改写。',
subCircuit: '一个带有“自增电路(Incrementer)”的寄存器。通过内部的简单半加器加上时钟脉冲边界的触发来同步更新地址值。'
},
ir: {
title: '指令寄存器 (Instruction Register, IR)',
description: '暂存刚刚从内存中读出、当前正在处于“译码”阶段的那条二进制机器指令。',
subCircuit: '同样是一排带写使能(Write-Enable)控制端的触发器(Flip-Flop),在"取指"周期时,写使能为1,锁存进指令数据。'
},
decoder: {
title: '指令译码器 (Instruction Decoder)',
description: '负责破译 IR 中的一长串 0 和 1 到底是什么意思。把二进制的机器码切分成“操作码”(做什么,如做加法)和“操作数”(对谁做,如寻址寄存器)。',
subCircuit: '由大量的与门和非门组成的组合电路网络。比如输入操作码 0010,只有代表“ADD操作”的那根特定输出管脚会被置 1,其他管脚保持 0。'
},
clock: {
title: '时钟发生器 (Clock)',
description: 'CPU 的心脏节拍器。发出持续的方波信号,同步全系统各个部件的工作节奏。每一次时钟波形的上升沿,所有的触发器才会统一改变锁存状态(即节拍)。',
subCircuit: '外部主板上的石英晶振产生极准的基础震荡信号,结合 CPU 内部的锁相环(PLL)倍频电路生成极高频率的脉冲方波。'
},
// 总线
address_bus: {
title: '地址总线 (Address Bus)',
description: '单向传输总线。CPU 通过这组电线,将它想访问的内存单元或 I/O 设备地址发送出去。地址总线的宽度决定了 CPU 最大能寻址多少内存(比如 32 位地址总线最多覆盖 4GB 寻址)。',
subCircuit: '物理上就是一块芯片引出的几十根极其细微的平行导线。通常受到三态门缓冲器(Tri-state Buffer)所驱动。'
},
data_bus: {
title: '数据总线 (Data Bus)',
description: '双向传输总线。在这组电线上,数据可以从 CPU 流向内存,也可以从内存流回 CPU。它的宽度就是我们平常所说的 32位/64位 处理器一次性处理的数据通路宽度。',
subCircuit: '同样是平行的导电线路,但两端接有方向控制引脚的三态门,确保不会由于多方同时施加高低电平导致设备短路。'
},
control_bus: {
title: '控制总线 (Control Bus)',
description: '混合传输总线,承载各种不同类型的核心控制信号:例如“我要读(Read)”、“我要写(Write)”、“外设的中断请求”、“等待反馈”等。',
subCircuit: '每一条线路一般都有单独而明确的功能分配,直接由控制单元(CU)的逻辑组合端引出,连接并支配外部的所有硬件。'
}
}
const currentModuleData = computed(() => moduleInfo[currentModule.value])
function selectModule(mod) {
if (currentModule.value === mod) {
currentModule.value = null
} else {
currentModule.value = mod
}
}
</script>
<style scoped>
.cpu-internal-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1.5rem 0;
}
.demo-title {
text-align: center;
font-weight: 800;
font-size: 1.25rem;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.demo-subtitle {
text-align: center;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 1.5rem;
}
.demo-container {
display: flex;
gap: 1.5rem;
align-items: stretch;
}
.cpu-chip {
flex: 3;
background: var(--vp-c-bg);
border: 3px solid #64748b;
border-radius: 12px;
padding: 1rem;
position: relative;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.chip-title {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: #64748b;
color: #fff;
padding: 2px 12px;
border-radius: 4px;
font-weight: bold;
font-size: 0.85rem;
white-space: nowrap;
}
.bus-top, .bus-bottom {
text-align: center;
padding: 0.4rem;
font-size: 0.8rem;
font-weight: bold;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
border: 2px dashed var(--vp-c-text-3);
background: var(--vp-c-bg-alt);
}
.bus-top:hover, .bus-bottom:hover, .bus-top.active, .bus-bottom.active {
border-style: solid;
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.cpu-layout {
display: flex;
gap: 1rem;
flex: 1;
min-height: 280px;
}
.section-box {
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.8rem;
display: flex;
flex-direction: column;
transition: all 0.2s;
cursor: pointer;
background: var(--vp-c-bg-soft);
}
.section-box:hover, .section-box.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.cu-section { margin-top: 0; }
.cu-section:hover, .cu-section.active { border-color: #3b82f6; }
.reg-section:hover, .reg-section.active { border-color: #8b5cf6; }
.alu-section:hover, .alu-section.active { border-color: #f59e0b; }
.section-title {
margin: 0 0 0.8rem 0;
font-size: 0.95rem;
text-align: center;
color: var(--vp-c-text-1);
}
.cu-section {
flex: 5;
}
.datapath-section {
flex: 7;
display: flex;
flex-direction: column;
gap: 1rem;
}
.sub-modules {
display: flex;
flex-direction: column;
gap: 0.4rem;
flex: 1;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
}
.sub-mod {
font-size: 0.8rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 0.5rem;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.sub-mod:hover, .sub-mod.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.cu-section .sub-mod:hover, .cu-section .sub-mod.active { background: #3b82f6; border-color: #3b82f6; }
.alu-section .sub-mod:hover, .alu-section .sub-mod.active { background: #f59e0b; border-color: #f59e0b; }
.reg-section .sub-mod:hover, .reg-section .sub-mod.active { background: #8b5cf6; border-color: #8b5cf6; }
.control-lines {
text-align: center;
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-top: 0.5rem;
font-family: monospace;
}
.details-panel {
flex: 2;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
box-shadow: inset 0 0 0 1px var(--vp-c-divider);
display: flex;
flex-direction: column;
}
.details-panel h3 {
margin: 0 0 1rem 0;
color: var(--vp-c-brand-1);
font-size: 1.2rem;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 0.5rem;
}
.details-panel .desc {
font-size: 0.95rem;
line-height: 1.6;
color: var(--vp-c-text-2);
margin-bottom: 1.5rem;
}
.circuit-impl {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-2);
padding: 1rem;
border-radius: 0 4px 4px 0;
margin-top: auto;
}
.circuit-impl h4 {
margin: 0 0 0.5rem 0;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.circuit-impl p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.empty {
align-items: center;
justify-content: center;
background: transparent;
box-shadow: none;
border: 1px dashed var(--vp-c-divider);
text-align: center;
color: var(--vp-c-text-3);
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
@media (max-width: 800px) {
.demo-container {
flex-direction: column;
}
.cpu-layout {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,115 @@
<template>
<div class="power-on-demo">
<div class="demo-label">电脑启动第一步 点击通电</div>
<div class="sequence" @click="powerOn = !powerOn">
<div class="step" :class="{ active: powerOn }">
<div class="step-icon">🔌</div>
<div class="step-name">电源</div>
<div class="step-status">{{ powerOn ? '✅ 供电中' : '⬜ 等待' }}</div>
</div>
<div class="arrow"></div>
<div class="step" :class="{ active: powerOn }">
<div class="step-icon">🔋</div>
<div class="step-name">主板</div>
<div class="step-status">{{ powerOn ? '✅ 唤醒' : '⬜ 等待' }}</div>
</div>
<div class="arrow"></div>
<div class="step" :class="{ active: powerOn }">
<div class="step-icon"></div>
<div class="step-name">CPU</div>
<div class="step-status">{{ powerOn ? '✅ 复位' : '⬜ 等待' }}</div>
</div>
<div class="arrow"></div>
<div class="step" :class="{ active: powerOn }">
<div class="step-icon">💾</div>
<div class="step-name">BIOS</div>
<div class="step-status">{{ powerOn ? '✅ 启动' : '⬜ 等待' }}</div>
</div>
</div>
<div class="tap-hint">👆 点击切换电源状态</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const powerOn = ref(false)
</script>
<style scoped>
.power-on-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.sequence {
display: flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.6rem;
border-radius: 6px;
opacity: 0.4;
transition: all 0.3s;
}
.step.active {
opacity: 1;
background: var(--vp-c-bg);
}
.step-icon {
font-size: 1.3rem;
margin-bottom: 0.2rem;
}
.step-name {
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.step-status {
font-size: 0.65rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
}
.arrow {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,170 @@
<template>
<div class="language-map-demo">
<div class="demo-header">
<span class="title">编程语言分类</span>
<span class="subtitle">不同维度看语言</span>
</div>
<div class="classification-tabs">
<button
v-for="tab in tabs"
:key="tab.key"
class="tab-btn"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</div>
<div class="classification-content">
<div v-for="item in currentItems" :key="item.name" class="item-card">
<div class="item-name">{{ item.name }}</div>
<div class="item-desc">{{ item.desc }}</div>
<div class="item-examples">
<span v-for="ex in item.examples" :key="ex" class="example-tag">{{ ex }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>选择建议</strong>先学一门主流语言深入理解编程思想再学其他语言会容易很多
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('type')
const tabs = [
{ key: 'type', label: '按类型系统' },
{ key: 'level', label: '按抽象层级' },
{ key: 'paradigm', label: '按编程范式' }
]
const classifications = {
type: [
{ name: '静态类型', desc: '变量类型在编译时确定', examples: ['Java', 'C++', 'Go', 'TypeScript'] },
{ name: '动态类型', desc: '变量类型在运行时确定', examples: ['Python', 'JavaScript', 'Ruby'] }
],
level: [
{ name: '低级语言', desc: '接近硬件,执行效率高', examples: ['C', '汇编'] },
{ name: '高级语言', desc: '接近人类语言,开发效率高', examples: ['Python', 'Java', 'JavaScript'] }
],
paradigm: [
{ name: '面向对象', desc: '以对象为中心组织代码', examples: ['Java', 'C++', 'Python'] },
{ name: '函数式', desc: '以函数为中心,强调不可变', examples: ['Haskell', 'Elixir', 'Clojure'] },
{ name: '多范式', desc: '支持多种编程风格', examples: ['Python', 'JavaScript', 'Rust'] }
]
}
const currentItems = computed(() => classifications[activeTab.value])
</script>
<style scoped>
.language-map-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-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.classification-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.tab-btn {
padding: 0.35rem 0.75rem;
font-size: 0.78rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
}
.tab-btn:hover {
border-color: var(--vp-c-brand-1);
}
.tab-btn.active {
background: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
color: white;
}
.classification-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item-card {
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.item-name {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.15rem;
}
.item-desc {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.35rem;
}
.item-examples {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.example-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,131 @@
<template>
<div class="rendering-demo">
<div class="demo-label">浏览器渲染过程 点击逐步演示</div>
<div class="pipeline">
<div
v-for="(stage, index) in stages"
:key="stage.name"
class="pipeline-stage"
:class="{ active: currentStage >= index }"
@click="currentStage = index"
>
<div class="stage-header">
<span class="stage-num">{{ index + 1 }}</span>
<span class="stage-name">{{ stage.name }}</span>
</div>
<div class="stage-desc">{{ stage.desc }}</div>
<div v-if="currentStage >= index" class="stage-detail">
{{ stage.detail }}
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各阶段详情</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStage = ref(0)
const stages = [
{ name: 'HTML 解析', desc: '把 HTML 变成 DOM 树', detail: '<div> → Document Object Model 树结构' },
{ name: 'CSS 解析', desc: '把 CSS 变成样式规则', detail: '选择器 + 属性 → 样式规则表' },
{ name: '渲染树', desc: 'DOM + CSS = 渲染树', detail: '哪些节点显示、什么样式' },
{ name: '布局计算', desc: '计算每个元素的位置', detail: '宽高、坐标、层级' },
{ name: '绘制', desc: '把内容画到像素缓冲区', detail: '文字、颜色、图片、边框' },
{ name: '合成', desc: '多层合成一张图', detail: 'GPU 合成,显示到屏幕' }
]
</script>
<style scoped>
.rendering-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.pipeline {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.pipeline-stage {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.6rem;
opacity: 0.4;
transition: all 0.3s;
}
.pipeline-stage.active {
opacity: 1;
border-color: var(--vp-c-brand);
}
.stage-header {
display: flex;
align-items: center;
gap: 0.4rem;
}
.stage-num {
width: 1.2rem;
height: 1.2rem;
background: var(--vp-c-divider);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.pipeline-stage.active .stage-num {
background: var(--vp-c-brand);
color: white;
}
.stage-name {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.stage-desc {
font-size: 0.68rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
}
.stage-detail {
font-size: 0.65rem;
color: var(--vp-c-brand);
margin-top: 0.3rem;
padding: 0.25rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,133 @@
<template>
<div class="url-demo">
<div class="demo-label">URL 访问流程 点击逐步演示</div>
<div class="flow">
<div
v-for="(step, index) in steps"
:key="step.name"
class="flow-step"
:class="{ active: currentStep >= index }"
@click="currentStep = index"
>
<div class="step-num">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-name">{{ step.name }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div v-if="currentStep >= index && step.detail" class="step-detail">
{{ step.detail }}
</div>
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各步骤详情</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{ name: 'URL 解析', desc: '提取协议、域名、路径', detail: 'https://www.example.com → 协议:https, 域名:www.example.com' },
{ name: 'DNS 解析', desc: '域名转换为 IP 地址', detail: 'www.example.com → 93.184.216.34' },
{ name: 'TCP 连接', desc: '建立与服务器的连接', detail: '三次握手完成' },
{ name: 'TLS 握手', desc: '建立加密通道(HTTPS', detail: '交换密钥,验证证书' },
{ name: '发送请求', desc: '发送 HTTP 请求', detail: 'GET /index.html HTTP/1.1' },
{ name: '服务器处理', desc: '后端处理请求', detail: '查询数据库,返回 HTML' },
{ name: '返回响应', desc: '接收服务器响应', detail: 'HTTP/1.1 200 OK, Content-Type: text/html' },
{ name: '浏览器渲染', desc: '解析并显示页面', detail: '构建 DOM 树,计算样式,绘制页面' }
]
</script>
<style scoped>
.url-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.flow {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.flow-step {
display: flex;
gap: 0.6rem;
padding: 0.4rem;
border-radius: 6px;
opacity: 0.4;
transition: all 0.3s;
background: var(--vp-c-bg);
}
.flow-step.active {
opacity: 1;
}
.step-num {
width: 1.3rem;
height: 1.3rem;
background: var(--vp-c-divider);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: 600;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.flow-step.active .step-num {
background: var(--vp-c-brand);
color: white;
}
.step-content {
flex: 1;
}
.step-name {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.step-desc {
font-size: 0.68rem;
color: var(--vp-c-text-3);
margin-top: 0.1rem;
}
.step-detail {
font-size: 0.65rem;
color: var(--vp-c-brand);
margin-top: 0.3rem;
padding: 0.3rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-family: monospace;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,307 @@
<template>
<div class="cert-chain-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔗 证书信任链可视化
</h4>
<p class="intro-text">
点击每一层证书查看它的详细信息和在信任链中的角色
</p>
<div class="chain-container">
<div
v-for="(cert, idx) in certs"
:key="idx"
class="cert-node"
:class="{ selected: selectedIdx === idx }"
:style="{ '--level-color': cert.color }"
@click="selectedIdx = idx"
>
<div class="cert-icon">{{ cert.icon }}</div>
<div class="cert-title">{{ cert.title }}</div>
<div class="cert-subtitle">{{ cert.subtitle }}</div>
<div v-if="idx < certs.length - 1" class="chain-arrow">
<span class="arrow-text">签发</span>
<span class="arrow-symbol"></span>
</div>
</div>
</div>
<div v-if="selectedIdx >= 0" class="detail-panel">
<div
class="detail-header"
:style="{ borderColor: certs[selectedIdx].color }"
>
<span class="detail-icon">{{ certs[selectedIdx].icon }}</span>
<span class="detail-name">{{ certs[selectedIdx].title }}</span>
</div>
<div class="detail-body">
<div class="detail-row" v-for="(item, i) in certs[selectedIdx].details" :key="i">
<span class="detail-label">{{ item.label }}</span>
<span class="detail-value">{{ item.value }}</span>
</div>
</div>
<div class="detail-explain">
{{ certs[selectedIdx].explain }}
</div>
</div>
<div class="verify-box">
<div class="verify-title">🔍 浏览器验证流程</div>
<div class="verify-steps">
<div v-for="(s, i) in verifySteps" :key="i" class="verify-step">
<span class="verify-num">{{ i + 1 }}</span>
<span class="verify-text">{{ s }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedIdx = ref(0)
const certs = [
{
icon: '🏛️',
title: '根证书(Root CA',
subtitle: '信任的起点',
color: '#c62828',
explain:
'根证书是整个信任链的锚点。它由根证书颁发机构自签名,预装在操作系统和浏览器中。全球只有少数几十个根 CA,它们的安全性由严格的审计和物理安全措施保障。根 CA 的私钥通常存储在离线的硬件安全模块(HSM)中。',
details: [
{ label: '签发者', value: 'DigiCert Global Root G2(自签名)' },
{ label: '有效期', value: '25 年(2013 - 2038' },
{ label: '密钥长度', value: 'RSA 2048 位' },
{ label: '存储位置', value: '操作系统 / 浏览器内置信任库' },
{ label: '数量级', value: '全球约 150 个受信根证书' }
]
},
{
icon: '🏢',
title: '中间证书(Intermediate CA',
subtitle: '信任的桥梁',
color: '#e65100',
explain:
'中间证书由根 CA 签发,作为根证书和服务器证书之间的桥梁。这种分层设计的好处是:即使中间证书被泄露,也可以单独吊销它而不影响根证书。中间 CA 负责日常的证书签发工作,根 CA 的私钥因此可以保持离线状态。',
details: [
{ label: '签发者', value: 'DigiCert Global Root G2' },
{ label: '持有者', value: 'DigiCert SHA2 Extended Validation Server CA' },
{ label: '有效期', value: '10 年' },
{ label: '用途', value: '签发终端实体(服务器)证书' },
{ label: '可吊销', value: '是(通过 CRL 或 OCSP' }
]
},
{
icon: '🌐',
title: '服务器证书(Server Certificate',
subtitle: '网站的身份证',
color: '#1565c0',
explain:
'服务器证书是网站向浏览器证明自己身份的凭证。它由中间 CA 签发,包含网站的域名、公钥和有效期等信息。当浏览器收到这张证书后,会沿着信任链向上验证,直到找到一个已经信任的根证书为止。',
details: [
{ label: '签发者', value: 'DigiCert SHA2 Extended Validation Server CA' },
{ label: '持有者', value: 'www.example.com' },
{ label: '有效期', value: '1 年(行业标准)' },
{ label: '包含公钥', value: 'ECDSA P-256 公钥' },
{ label: '验证级别', value: 'EV(扩展验证)/ DV(域名验证)' }
]
}
]
const verifySteps = [
'浏览器收到服务器证书,读取其签发者信息',
'找到中间证书,用中间 CA 的公钥验证服务器证书的签名',
'再用根 CA 的公钥验证中间证书的签名',
'确认根证书在本地信任库中 → 整条链验证通过'
]
</script>
<style scoped>
.cert-chain-demo {
background: linear-gradient(135deg, #fce4ec 0%, #fff3e0 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.intro-text {
font-size: 13px;
color: #666;
margin: 0 0 16px 0;
}
.chain-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
margin-bottom: 18px;
}
.cert-node {
position: relative;
background: #fff;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 14px 24px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
width: 280px;
max-width: 100%;
}
.cert-node:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.cert-node.selected {
border-color: var(--level-color);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: scale(1.03);
}
.cert-icon {
font-size: 30px;
}
.cert-title {
font-weight: 700;
font-size: 14px;
color: #1a1a2e;
margin-top: 4px;
}
.cert-subtitle {
font-size: 12px;
color: #888;
}
.chain-arrow {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 0;
color: #999;
}
.arrow-text {
font-size: 11px;
}
.arrow-symbol {
font-size: 20px;
line-height: 1;
}
.detail-panel {
background: #fff;
border-radius: 10px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid #e0e0e0;
}
.detail-header {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 3px solid;
}
.detail-icon {
font-size: 24px;
}
.detail-name {
font-weight: 700;
font-size: 16px;
color: #1a1a2e;
}
.detail-body {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.detail-row {
display: flex;
gap: 8px;
font-size: 13px;
}
.detail-label {
color: #888;
min-width: 80px;
flex-shrink: 0;
}
.detail-value {
color: #333;
font-weight: 500;
word-break: break-all;
}
.detail-explain {
font-size: 13px;
color: #555;
line-height: 1.7;
background: #f5f5f5;
padding: 10px 14px;
border-radius: 8px;
}
.verify-box {
background: #e8f5e9;
border-radius: 10px;
padding: 14px 18px;
}
.verify-title {
font-weight: 700;
color: #2e7d32;
margin-bottom: 10px;
font-size: 14px;
}
.verify-steps {
display: flex;
flex-direction: column;
gap: 8px;
}
.verify-step {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: #333;
}
.verify-num {
background: #4caf50;
color: #fff;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.verify-text {
line-height: 1.5;
padding-top: 1px;
}
</style>
@@ -0,0 +1,391 @@
<template>
<div class="comparison-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔐 HTTP vs HTTPS 数据传输对比
</h4>
<div class="control-row">
<button
class="mode-btn"
:class="{ active: mode === 'http' }"
@click="mode = 'http'"
>
HTTP明文
</button>
<button
class="mode-btn https"
:class="{ active: mode === 'https' }"
@click="mode = 'https'"
>
HTTPS加密
</button>
<button class="send-btn" :disabled="isSending" @click="sendData">
{{ isSending ? '传输中...' : '发送数据' }}
</button>
</div>
<div class="flow-area">
<div class="endpoint">
<div class="ep-icon">💻</div>
<div class="ep-label">浏览器</div>
<div class="ep-data original">
<div class="data-title">原始数据</div>
<code>{{ originalData }}</code>
</div>
</div>
<div class="transmission">
<div class="wire" :class="mode">
<div class="wire-label">
{{ mode === 'http' ? '🔓 明文传输' : '🔒 加密传输' }}
</div>
<div
class="packet"
:class="{ moving: isSending, done: sendDone }"
>
<code class="packet-text">{{ transmittedData }}</code>
</div>
</div>
<div v-if="mode === 'http'" class="hacker-box">
<div class="hacker-icon">🕵</div>
<div class="hacker-label">中间人可窃听</div>
<div v-if="sendDone" class="hacker-sees">
<code>{{ originalData }}</code>
</div>
</div>
<div v-else class="hacker-box blocked">
<div class="hacker-icon">🕵</div>
<div class="hacker-label">中间人无法解密</div>
<div v-if="sendDone" class="hacker-sees encrypted">
<code>{{ encryptedData }}</code>
</div>
</div>
</div>
<div class="endpoint">
<div class="ep-icon">🖥</div>
<div class="ep-label">服务器</div>
<div v-if="sendDone" class="ep-data received">
<div class="data-title">收到数据</div>
<code>{{ originalData }}</code>
</div>
</div>
</div>
<div class="compare-table">
<table>
<thead>
<tr>
<th>对比项</th>
<th>HTTP</th>
<th>HTTPS</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in compareRows" :key="i">
<td class="row-label">{{ row.label }}</td>
<td class="http-cell">{{ row.http }}</td>
<td class="https-cell">{{ row.https }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('http')
const isSending = ref(false)
const sendDone = ref(false)
const originalData = 'password=MySecret123&user=zhangsan'
const encryptedData = 'a7f2c9...3b8e1d(密文)'
const transmittedData = ref('')
const compareRows = [
{ label: '端口', http: '80', https: '443' },
{ label: '数据加密', http: '无(明文传输)', https: 'TLS 对称加密' },
{ label: '身份验证', http: '无', https: 'CA 证书验证服务器身份' },
{ label: '数据完整性', http: '无保障', https: 'MAC 校验防篡改' },
{ label: 'SEO 影响', http: '搜索引擎降权', https: '搜索引擎优先收录' },
{ label: '性能开销', http: '无额外开销', https: 'TLS 握手增加约 1-2 RTT' }
]
async function sendData() {
if (isSending.value) return
isSending.value = true
sendDone.value = false
if (mode.value === 'http') {
transmittedData.value = originalData
} else {
transmittedData.value = encryptedData
}
await sleep(1500)
sendDone.value = true
isSending.value = false
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
</script>
<style scoped>
.comparison-demo {
background: linear-gradient(135deg, #e8eaf6 0%, #fce4ec 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.control-row {
display: flex;
gap: 8px;
margin-bottom: 18px;
flex-wrap: wrap;
}
.mode-btn {
padding: 7px 18px;
border: 2px solid #ef5350;
border-radius: 8px;
background: #fff;
color: #c62828;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.mode-btn.https {
border-color: #43a047;
color: #2e7d32;
}
.mode-btn.active {
background: #c62828;
color: #fff;
}
.mode-btn.https.active {
background: #2e7d32;
color: #fff;
}
.send-btn {
padding: 7px 18px;
background: #1565c0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.flow-area {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 18px;
flex-wrap: wrap;
justify-content: center;
}
.endpoint {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
}
.ep-icon {
font-size: 32px;
}
.ep-label {
font-size: 13px;
font-weight: 600;
color: #333;
margin: 4px 0 8px;
}
.ep-data {
background: #fff;
border-radius: 8px;
padding: 8px 12px;
font-size: 11px;
max-width: 160px;
word-break: break-all;
}
.ep-data.original {
border: 1px solid #90caf9;
}
.ep-data.received {
border: 1px solid #a5d6a7;
}
.data-title {
font-size: 10px;
color: #888;
margin-bottom: 4px;
font-weight: 600;
}
.transmission {
flex: 1;
min-width: 180px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.wire {
width: 100%;
padding: 10px;
border-radius: 8px;
text-align: center;
position: relative;
min-height: 60px;
}
.wire.http {
background: #ffebee;
border: 2px dashed #ef5350;
}
.wire.https {
background: #e8f5e9;
border: 2px solid #43a047;
}
.wire-label {
font-size: 12px;
font-weight: 600;
margin-bottom: 6px;
}
.packet {
font-size: 11px;
opacity: 0;
transition: opacity 0.5s;
}
.packet.moving {
opacity: 1;
animation: slide 1.2s ease-in-out;
}
.packet.done {
opacity: 1;
}
@keyframes slide {
0% {
transform: translateX(-30px);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.packet-text {
font-size: 10px;
word-break: break-all;
}
.hacker-box {
background: #fff3e0;
border: 1px solid #ffcc80;
border-radius: 8px;
padding: 8px 12px;
text-align: center;
width: 100%;
}
.hacker-box.blocked {
background: #f1f8e9;
border-color: #aed581;
}
.hacker-icon {
font-size: 24px;
}
.hacker-label {
font-size: 11px;
font-weight: 600;
color: #e65100;
}
.hacker-box.blocked .hacker-label {
color: #558b2f;
}
.hacker-sees {
margin-top: 4px;
font-size: 10px;
color: #c62828;
word-break: break-all;
}
.hacker-sees.encrypted {
color: #558b2f;
}
.compare-table {
overflow-x: auto;
}
.compare-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
background: #fff;
border-radius: 8px;
overflow: hidden;
}
.compare-table th {
background: #37474f;
color: #fff;
padding: 8px 12px;
text-align: left;
font-weight: 600;
}
.compare-table td {
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
.row-label {
font-weight: 600;
color: #333;
}
.http-cell {
color: #c62828;
}
.https-cell {
color: #2e7d32;
}
</style>
@@ -0,0 +1,249 @@
<template>
<div class="dns-record-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
📋 DNS 记录类型速查
</h4>
<div class="tab-row">
<button
v-for="rec in records"
:key="rec.type"
class="tab-btn"
:class="{ active: selected === rec.type }"
@click="selected = rec.type"
>
{{ rec.type }}
</button>
</div>
<div v-if="current" class="detail-card">
<div class="detail-header">
<span class="type-badge">{{ current.type }}</span>
<span class="type-name">{{ current.name }}</span>
</div>
<p class="type-desc">{{ current.desc }}</p>
<div class="example-block">
<div class="example-title">示例记录</div>
<code class="example-code">{{ current.example }}</code>
</div>
<div class="usage-block">
<div class="usage-title">常见用途</div>
<ul class="usage-list">
<li v-for="(u, i) in current.usages" :key="i">{{ u }}</li>
</ul>
</div>
</div>
<div class="info-box">
<strong>小贴士</strong>
DNS 不只是把域名翻译成 IP它还承载了邮件路由域名验证负载均衡等多种功能全靠不同的记录类型来实现
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selected = ref('A')
const records = [
{
type: 'A',
name: 'Address 记录',
desc: '将域名映射到一个 IPv4 地址。这是最常见的 DNS 记录类型,浏览器访问网站时最终需要的就是这条记录。',
example: 'example.com. IN A 93.184.216.34',
usages: [
'网站域名指向服务器 IP',
'子域名指向不同的服务器',
'配合负载均衡返回多个 IP'
]
},
{
type: 'AAAA',
name: 'IPv6 Address 记录',
desc: '将域名映射到一个 IPv6 地址。随着 IPv4 地址耗尽,AAAA 记录变得越来越重要。',
example: 'example.com. IN AAAA 2606:2800:220:1:248:1893:25c8:1946',
usages: [
'支持 IPv6 网络的设备访问',
'双栈部署(同时配置 A 和 AAAA',
'面向未来的网络架构'
]
},
{
type: 'CNAME',
name: 'Canonical Name 记录',
desc: '将一个域名指向另一个域名(别名)。浏览器会继续解析目标域名,直到找到 A 记录。',
example: 'www.example.com. IN CNAME example.com.',
usages: [
'www 子域名指向主域名',
'CDN 加速(指向 CDN 提供商域名)',
'多个域名指向同一服务'
]
},
{
type: 'MX',
name: 'Mail Exchange 记录',
desc: '指定负责接收该域名邮件的邮件服务器地址和优先级。数字越小优先级越高。',
example: 'example.com. IN MX 10 mail.example.com.',
usages: [
'配置企业邮箱(如 Gmail、Outlook',
'设置邮件服务器优先级',
'邮件备份和容灾'
]
},
{
type: 'TXT',
name: 'Text 记录',
desc: '存储任意文本信息。常用于域名所有权验证、邮件安全策略(SPF/DKIM/DMARC)等场景。',
example: 'example.com. IN TXT "v=spf1 include:_spf.google.com ~all"',
usages: [
'SPF 记录防止邮件伪造',
'SSL 证书申请时的域名验证',
'第三方服务的域名所有权确认'
]
},
{
type: 'NS',
name: 'Name Server 记录',
desc: '指定该域名由哪些 DNS 服务器负责解析。这是 DNS 委派机制的核心。',
example: 'example.com. IN NS ns1.exampledns.com.',
usages: [
'将域名托管到指定 DNS 服务商',
'子域名委派给不同团队管理',
'DNS 服务迁移'
]
}
]
const current = computed(() => records.find((r) => r.type === selected.value))
</script>
<style scoped>
.dns-record-demo {
background: linear-gradient(135deg, #f3e5f5 0%, #ede7f6 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.tab-row {
display: flex;
gap: 6px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.tab-btn {
padding: 6px 16px;
border: 2px solid #ce93d8;
border-radius: 20px;
background: #fff;
color: #7b1fa2;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn.active {
background: #7b1fa2;
color: #fff;
border-color: #7b1fa2;
}
.tab-btn:hover:not(.active) {
background: #f3e5f5;
}
.detail-card {
background: #fff;
border-radius: 10px;
padding: 18px;
margin-bottom: 14px;
border: 1px solid #e1bee7;
}
.detail-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.type-badge {
background: #7b1fa2;
color: #fff;
padding: 3px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
font-family: monospace;
}
.type-name {
font-size: 15px;
color: #555;
font-weight: 500;
}
.type-desc {
font-size: 14px;
color: #333;
line-height: 1.7;
margin: 0 0 14px 0;
}
.example-block {
background: #263238;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 14px;
}
.example-title {
font-size: 11px;
color: #80cbc4;
margin-bottom: 6px;
font-weight: 600;
}
.example-code {
color: #e0f7fa;
font-size: 13px;
font-family: 'Fira Code', monospace;
word-break: break-all;
}
.usage-block {
background: #f3e5f5;
border-radius: 8px;
padding: 12px 16px;
}
.usage-title {
font-size: 12px;
font-weight: 700;
color: #7b1fa2;
margin-bottom: 6px;
}
.usage-list {
margin: 0;
padding-left: 18px;
font-size: 13px;
color: #444;
line-height: 1.8;
}
.info-box {
margin-top: 14px;
padding: 10px 14px;
background: #fff3e0;
border-radius: 8px;
font-size: 13px;
color: #5d4037;
line-height: 1.6;
}
</style>
@@ -0,0 +1,357 @@
<template>
<div class="dns-resolution-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔍 DNS 解析过程模拟器
</h4>
<div class="input-row">
<input
v-model="domain"
type="text"
placeholder="输入域名,如 www.example.com"
class="domain-input"
@keyup.enter="startResolve"
/>
<button class="resolve-btn" :disabled="isResolving" @click="startResolve">
{{ isResolving ? '解析中...' : '开始解析' }}
</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="resolve-flow">
<div
v-for="(step, idx) in steps"
:key="idx"
class="step-card"
:class="{
active: currentStep === idx,
done: currentStep > idx,
pending: currentStep < idx
}"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-label">{{ step.label }}</div>
<div v-if="currentStep > idx" class="step-result">
{{ step.result }}
</div>
<div v-if="currentStep === idx && isResolving" class="step-spinner">
</div>
<div
v-if="idx < steps.length - 1"
class="arrow"
:class="{ 'arrow-active': currentStep > idx }"
>
</div>
</div>
</div>
<div v-if="resolved" class="result-box">
<div class="result-title"> 解析完成</div>
<div class="result-detail">
<span class="result-domain">{{ domain }}</span>
<span class="result-ip">{{ resolvedIp }}</span>
</div>
<div class="result-time">总耗时 {{ totalTime }}ms模拟</div>
</div>
<div class="info-box">
<strong>解析流程说明</strong>
浏览器访问网站时需要先将域名翻译成 IP
地址这个过程会依次查询多级缓存和服务器直到找到对应的 IP
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const domain = ref('www.example.com')
const isResolving = ref(false)
const resolved = ref(false)
const currentStep = ref(-1)
const resolvedIp = ref('')
const totalTime = ref(0)
const steps = reactive([
{
icon: '🌐',
label: '浏览器缓存',
result: '未命中,继续查询...'
},
{
icon: '💻',
label: '操作系统缓存',
result: '未命中,继续查询...'
},
{
icon: '🔄',
label: '递归解析器',
result: '向根服务器发起查询...'
},
{
icon: '🌍',
label: '根域名服务器',
result: '返回 .com TLD 服务器地址'
},
{
icon: '📂',
label: 'TLD 服务器',
result: '返回权威服务器地址'
},
{
icon: '🏠',
label: '权威 DNS 服务器',
result: ''
}
])
function generateIp() {
const a = 93 + Math.floor(Math.random() * 60)
const b = Math.floor(Math.random() * 256)
const c = Math.floor(Math.random() * 256)
const d = 1 + Math.floor(Math.random() * 254)
return `${a}.${b}.${c}.${d}`
}
async function startResolve() {
if (isResolving.value || !domain.value.trim()) return
isResolving.value = true
resolved.value = false
currentStep.value = -1
const ip = generateIp()
resolvedIp.value = ip
steps[5].result = `找到记录!IP = ${ip}`
const delays = [200, 300, 400, 500, 400, 300]
let total = 0
for (let i = 0; i < steps.length; i++) {
currentStep.value = i
await sleep(delays[i])
total += delays[i]
currentStep.value = i + 1
}
totalTime.value = total
resolved.value = true
isResolving.value = false
}
function reset() {
isResolving.value = false
resolved.value = false
currentStep.value = -1
resolvedIp.value = ''
totalTime.value = 0
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
</script>
<style scoped>
.dns-resolution-demo {
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.input-row {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.domain-input {
flex: 1;
min-width: 200px;
padding: 8px 14px;
border: 2px solid #c5cae9;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.domain-input:focus {
border-color: #5c6bc0;
}
.resolve-btn {
padding: 8px 20px;
background: #5c6bc0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.resolve-btn:hover:not(:disabled) {
background: #3f51b5;
}
.resolve-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
padding: 8px 16px;
background: #e0e0e0;
color: #333;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.reset-btn:hover {
background: #bdbdbd;
}
.resolve-flow {
display: flex;
align-items: flex-start;
gap: 4px;
overflow-x: auto;
padding: 10px 0;
flex-wrap: wrap;
justify-content: center;
}
.step-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 10px;
border-radius: 10px;
background: #fff;
min-width: 100px;
max-width: 120px;
text-align: center;
transition: all 0.3s;
border: 2px solid transparent;
position: relative;
opacity: 0.5;
}
.step-card.active {
border-color: #ff9800;
background: #fff8e1;
opacity: 1;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
}
.step-card.done {
border-color: #4caf50;
background: #e8f5e9;
opacity: 1;
}
.step-card.pending {
opacity: 0.4;
}
.step-icon {
font-size: 28px;
margin-bottom: 6px;
}
.step-label {
font-size: 12px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.step-result {
font-size: 10px;
color: #666;
line-height: 1.3;
}
.step-spinner {
font-size: 20px;
animation: pulse 0.8s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.arrow {
position: absolute;
right: -14px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
color: #ccc;
font-weight: bold;
z-index: 1;
}
.arrow-active {
color: #4caf50;
}
.result-box {
margin-top: 16px;
padding: 14px 18px;
background: #e8f5e9;
border-radius: 10px;
border: 1px solid #a5d6a7;
}
.result-title {
font-weight: 700;
color: #2e7d32;
margin-bottom: 6px;
}
.result-detail {
font-size: 15px;
color: #333;
}
.result-domain {
color: #5c6bc0;
font-weight: 600;
}
.result-ip {
color: #e65100;
font-weight: 600;
font-family: monospace;
}
.result-time {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.info-box {
margin-top: 14px;
padding: 10px 14px;
background: #fff3e0;
border-radius: 8px;
font-size: 13px;
color: #5d4037;
line-height: 1.6;
}
</style>
@@ -0,0 +1,298 @@
<template>
<div class="https-handshake-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🤝 TLS 握手过程演示
</h4>
<div class="control-row">
<button class="start-btn" :disabled="isRunning" @click="startHandshake">
{{ isRunning ? '握手进行中...' : '开始 TLS 握手' }}
</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="handshake-area">
<div class="side client-side">
<div class="side-icon">💻</div>
<div class="side-label">客户端浏览器</div>
</div>
<div class="message-lane">
<div
v-for="(msg, idx) in messages"
:key="idx"
class="msg-row"
:class="{
active: currentStep === idx,
done: currentStep > idx,
pending: currentStep < idx
}"
>
<div
class="msg-arrow"
:class="msg.direction === 'right' ? 'arrow-right' : 'arrow-left'"
>
<span class="arrow-line"></span>
<span class="arrow-head">{{ msg.direction === 'right' ? '→' : '←' }}</span>
</div>
<div class="msg-content">
<div class="msg-name">{{ msg.name }}</div>
<div class="msg-desc">{{ msg.desc }}</div>
</div>
</div>
</div>
<div class="side server-side">
<div class="side-icon">🖥</div>
<div class="side-label">服务器</div>
</div>
</div>
<div v-if="currentStep >= 0 && currentStep < messages.length" class="detail-box">
<div class="detail-title">
{{ messages[currentStep].name }}
</div>
<div class="detail-text">
{{ messages[currentStep].detail }}
</div>
</div>
<div v-if="handshakeDone" class="success-box">
TLS 握手完成后续所有 HTTP 数据都将通过对称加密传输第三方无法窃听
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isRunning = ref(false)
const currentStep = ref(-1)
const handshakeDone = ref(false)
const messages = [
{
name: 'Client Hello',
direction: 'right',
desc: '发送支持的 TLS 版本、加密套件列表、随机数',
detail:
'浏览器向服务器发起连接请求,告知自己支持的 TLS 版本(如 TLS 1.3)、可用的加密算法列表(如 AES-256-GCM)以及一个客户端随机数(Client Random)。这就像自我介绍:"我会这些加密方式,你选一个吧。"'
},
{
name: 'Server Hello',
direction: 'left',
desc: '选定 TLS 版本、加密套件、服务器随机数',
detail:
'服务器从客户端提供的列表中选择一个最优的加密套件,并返回自己的随机数(Server Random)。相当于回应:"好的,我们就用 TLS 1.3 + AES-256-GCM 来通信。"'
},
{
name: 'Certificate',
direction: 'left',
desc: '服务器发送数字证书(含公钥)',
detail:
'服务器将自己的数字证书发送给浏览器。证书中包含服务器的公钥、域名信息以及 CA 的签名。浏览器会验证证书是否由受信任的 CA 签发、是否过期、域名是否匹配。'
},
{
name: 'Key Exchange',
direction: 'right',
desc: '双方协商生成会话密钥',
detail:
'在 TLS 1.3 中,客户端和服务器通过 ECDHE(椭圆曲线 Diffie-Hellman)算法交换密钥材料。双方各自生成临时密钥对,交换公钥后独立计算出相同的"预主密钥",再结合之前的随机数推导出最终的对称会话密钥。'
},
{
name: 'Finished',
direction: 'right',
desc: '双方确认握手成功,开始加密通信',
detail:
'双方各自发送 Finished 消息,其中包含之前所有握手消息的摘要(用刚协商好的密钥加密)。如果对方能正确解密并验证,说明密钥协商成功,后续所有数据都将使用对称加密传输。'
}
]
async function startHandshake() {
if (isRunning.value) return
isRunning.value = true
handshakeDone.value = false
currentStep.value = -1
for (let i = 0; i < messages.length; i++) {
currentStep.value = i
await sleep(1200)
}
handshakeDone.value = true
isRunning.value = false
}
function reset() {
isRunning.value = false
currentStep.value = -1
handshakeDone.value = false
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
</script>
<style scoped>
.https-handshake-demo {
background: linear-gradient(135deg, #e3f2fd 0%, #e8eaf6 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.control-row {
display: flex;
gap: 8px;
margin-bottom: 18px;
}
.start-btn {
padding: 8px 20px;
background: #1565c0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.start-btn:hover:not(:disabled) {
background: #0d47a1;
}
.start-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
padding: 8px 16px;
background: #e0e0e0;
color: #333;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.handshake-area {
display: flex;
gap: 12px;
align-items: flex-start;
}
.side {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
padding-top: 10px;
}
.side-icon {
font-size: 36px;
}
.side-label {
font-size: 12px;
font-weight: 600;
color: #333;
margin-top: 4px;
text-align: center;
}
.message-lane {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.msg-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 8px;
background: #fff;
border: 2px solid transparent;
opacity: 0.35;
transition: all 0.4s;
}
.msg-row.active {
opacity: 1;
border-color: #ff9800;
background: #fff8e1;
transform: scale(1.02);
box-shadow: 0 3px 10px rgba(255, 152, 0, 0.2);
}
.msg-row.done {
opacity: 1;
border-color: #4caf50;
background: #e8f5e9;
}
.msg-arrow {
display: flex;
align-items: center;
min-width: 36px;
font-size: 18px;
font-weight: bold;
}
.arrow-right {
color: #1565c0;
}
.arrow-left {
color: #e65100;
}
.msg-name {
font-weight: 700;
font-size: 14px;
color: #1a1a2e;
}
.msg-desc {
font-size: 12px;
color: #666;
margin-top: 2px;
}
.detail-box {
margin-top: 14px;
padding: 14px 18px;
background: #fff;
border-radius: 10px;
border-left: 4px solid #1565c0;
}
.detail-title {
font-weight: 700;
color: #1565c0;
margin-bottom: 6px;
font-size: 15px;
}
.detail-text {
font-size: 13px;
color: #444;
line-height: 1.7;
}
.success-box {
margin-top: 14px;
padding: 12px 18px;
background: #e8f5e9;
border-radius: 10px;
border: 1px solid #a5d6a7;
color: #2e7d32;
font-weight: 600;
font-size: 14px;
}
</style>
@@ -0,0 +1,308 @@
<!--
EmbeddingConceptDemo.vue
嵌入概念可视化组件
用途
将词语/句子嵌入可视化为二维空间中的点展示语义相似的概念如何聚集在一起
交互功能
- 切换不同词组类别查看聚类效果
- 悬停查看词语详情和坐标
- 动态展示语义空间的分布
-->
<template>
<div class="embedding-demo">
<div class="demo-header">
<h4>词嵌入空间可视化</h4>
<p class="desc">语义相近的词语在向量空间中距离更近形成自然聚类</p>
</div>
<div class="controls">
<button
v-for="cat in categories"
:key="cat.key"
class="cat-btn"
:class="{ active: activeCategory === cat.key }"
@click="activeCategory = cat.key"
>
{{ cat.label }}
</button>
</div>
<div class="canvas-wrap">
<svg
ref="svgRef"
viewBox="0 0 500 400"
class="embed-svg"
>
<!-- 坐标轴 -->
<line x1="50" y1="370" x2="480" y2="370" stroke="var(--vp-c-divider)" stroke-width="1" />
<line x1="50" y1="370" x2="50" y2="20" stroke="var(--vp-c-divider)" stroke-width="1" />
<text x="265" y="395" text-anchor="middle" fill="var(--vp-c-text-3)" font-size="12">维度 1</text>
<text x="15" y="195" text-anchor="middle" fill="var(--vp-c-text-3)" font-size="12" transform="rotate(-90, 15, 195)">维度 2</text>
<!-- 聚类椭圆 -->
<ellipse
v-for="cluster in currentClusters"
:key="cluster.label"
:cx="cluster.cx"
:cy="cluster.cy"
:rx="cluster.rx"
:ry="cluster.ry"
:fill="cluster.color"
fill-opacity="0.08"
:stroke="cluster.color"
stroke-opacity="0.3"
stroke-width="1.5"
stroke-dasharray="4 3"
/>
<!-- 数据点 -->
<g
v-for="(point, idx) in currentPoints"
:key="point.word"
class="point-group"
@mouseenter="hoveredPoint = idx"
@mouseleave="hoveredPoint = -1"
>
<circle
:cx="point.x"
:cy="point.y"
:r="hoveredPoint === idx ? 8 : 6"
:fill="point.color"
stroke="#fff"
stroke-width="1.5"
class="data-point"
/>
<text
:x="point.x"
:y="point.y - 12"
text-anchor="middle"
:fill="point.color"
font-size="12"
font-weight="600"
>
{{ point.word }}
</text>
</g>
<!-- 聚类标签 -->
<text
v-for="cluster in currentClusters"
:key="'label-' + cluster.label"
:x="cluster.cx"
:y="cluster.cy + cluster.ry + 16"
text-anchor="middle"
:fill="cluster.color"
font-size="11"
font-weight="500"
opacity="0.7"
>
{{ cluster.label }}
</text>
</svg>
<!-- 悬停信息 -->
<div v-if="hoveredPoint >= 0" class="hover-info">
<span class="hw">{{ currentPoints[hoveredPoint].word }}</span>
<span class="hc">向量: [{{ currentPoints[hoveredPoint].vec.join(', ') }}]</span>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">&#x1F4A1;</span>
嵌入模型将文本映射到高维向量空间通常 768~1536 这里我们将其简化为二维来展示核心思想<strong>语义相近的词语向量距离也更近</strong>
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeCategory = ref('animals-royalty')
const hoveredPoint = ref(-1)
const categories = [
{ key: 'animals-royalty', label: '动物 vs 皇室' },
{ key: 'food-tech', label: '食物 vs 科技' },
{ key: 'emotions', label: '情感词汇' }
]
const dataMap = {
'animals-royalty': {
clusters: [
{ label: '动物', cx: 150, cy: 160, rx: 80, ry: 65, color: '#10b981' },
{ label: '皇室', cx: 370, cy: 200, rx: 75, ry: 60, color: '#8b5cf6' }
],
points: [
{ word: '猫', x: 120, y: 140, color: '#10b981', vec: [0.21, 0.68] },
{ word: '狗', x: 160, y: 180, color: '#10b981', vec: [0.28, 0.55] },
{ word: '老虎', x: 185, y: 130, color: '#10b981', vec: [0.35, 0.72] },
{ word: '兔子', x: 130, y: 195, color: '#10b981', vec: [0.22, 0.48] },
{ word: '国王', x: 350, y: 175, color: '#8b5cf6', vec: [0.82, 0.58] },
{ word: '王后', x: 390, y: 195, color: '#8b5cf6', vec: [0.88, 0.52] },
{ word: '王子', x: 360, y: 225, color: '#8b5cf6', vec: [0.84, 0.42] },
{ word: '公主', x: 395, y: 215, color: '#8b5cf6', vec: [0.89, 0.45] }
]
},
'food-tech': {
clusters: [
{ label: '食物', cx: 140, cy: 240, rx: 85, ry: 70, color: '#f59e0b' },
{ label: '科技', cx: 360, cy: 120, rx: 80, ry: 65, color: '#3b82f6' }
],
points: [
{ word: '苹果(水果)', x: 110, y: 220, color: '#f59e0b', vec: [0.15, 0.38] },
{ word: '面包', x: 155, y: 260, color: '#f59e0b', vec: [0.25, 0.28] },
{ word: '牛奶', x: 130, y: 280, color: '#f59e0b', vec: [0.20, 0.22] },
{ word: '蛋糕', x: 175, y: 230, color: '#f59e0b', vec: [0.30, 0.35] },
{ word: '电脑', x: 340, y: 100, color: '#3b82f6', vec: [0.78, 0.82] },
{ word: '手机', x: 375, y: 130, color: '#3b82f6', vec: [0.85, 0.75] },
{ word: '芯片', x: 355, y: 150, color: '#3b82f6', vec: [0.82, 0.70] },
{ word: '算法', x: 390, y: 110, color: '#3b82f6', vec: [0.88, 0.80] }
]
},
emotions: {
clusters: [
{ label: '积极情感', cx: 150, cy: 130, rx: 90, ry: 70, color: '#10b981' },
{ label: '消极情感', cx: 360, cy: 270, rx: 85, ry: 65, color: '#ef4444' },
{ label: '中性情感', cx: 260, cy: 200, rx: 60, ry: 45, color: '#6b7280' }
],
points: [
{ word: '快乐', x: 120, y: 110, color: '#10b981', vec: [0.15, 0.78] },
{ word: '幸福', x: 155, y: 130, color: '#10b981', vec: [0.22, 0.72] },
{ word: '兴奋', x: 180, y: 100, color: '#10b981', vec: [0.28, 0.82] },
{ word: '悲伤', x: 340, y: 250, color: '#ef4444', vec: [0.78, 0.30] },
{ word: '愤怒', x: 380, y: 270, color: '#ef4444', vec: [0.85, 0.25] },
{ word: '恐惧', x: 360, y: 295, color: '#ef4444', vec: [0.82, 0.18] },
{ word: '平静', x: 245, y: 190, color: '#6b7280', vec: [0.50, 0.52] },
{ word: '淡然', x: 275, y: 210, color: '#6b7280', vec: [0.55, 0.48] }
]
}
}
const currentClusters = computed(() => dataMap[activeCategory.value].clusters)
const currentPoints = computed(() => dataMap[activeCategory.value].points)
</script>
<style scoped>
.embedding-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.cat-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.cat-btn:hover {
background: var(--vp-c-bg-alt);
}
.cat-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.canvas-wrap {
position: relative;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
margin-bottom: 1rem;
overflow: hidden;
}
.embed-svg {
width: 100%;
height: auto;
display: block;
}
.data-point {
cursor: pointer;
transition: r 0.2s;
}
.point-group {
transition: opacity 0.2s;
}
.hover-info {
position: absolute;
top: 12px;
right: 12px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.hw {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.hc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-family: var(--vp-font-family-mono);
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 4px;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.embedding-demo {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,453 @@
<!--
EmbeddingPipelineDemo.vue
嵌入生成流水线演示组件
用途
展示从原始文本到向量存储的完整嵌入流水线
Text Tokenize Model Vector Store Query
交互功能
- 输入自定义文本
- 逐步执行流水线
- 每一步展示中间结果
-->
<template>
<div class="pipeline-demo">
<div class="demo-header">
<h4>嵌入生成流水线</h4>
<p class="desc">逐步体验从文本到向量的完整转换过程</p>
</div>
<div class="input-area">
<label>输入文本</label>
<input
v-model="inputText"
type="text"
placeholder="输入一段文本,观察嵌入生成过程..."
class="text-input"
/>
<button class="run-btn" @click="runPipeline">
{{ running ? '处理中...' : '开始处理' }}
</button>
</div>
<!-- 流水线步骤 -->
<div class="pipeline-steps">
<div
v-for="(step, idx) in steps"
:key="step.key"
class="step"
:class="{
active: currentStep >= idx,
current: currentStep === idx && running
}"
>
<div class="step-header">
<div class="step-num" :style="{ background: currentStep >= idx ? step.color : '' }">
{{ idx + 1 }}
</div>
<div class="step-title">{{ step.title }}</div>
<div v-if="currentStep > idx" class="step-check">&#x2713;</div>
</div>
<div v-if="currentStep >= idx" class="step-content">
<div class="step-desc">{{ step.desc }}</div>
<div class="step-output" v-if="stepOutputs[step.key]">
<code>{{ stepOutputs[step.key] }}</code>
</div>
</div>
<!-- 箭头连接 -->
<div v-if="idx < steps.length - 1" class="step-arrow">
<span :class="{ visible: currentStep > idx }">&#x2193;</span>
</div>
</div>
</div>
<!-- 最终结果 -->
<div v-if="currentStep >= steps.length - 1 && !running" class="final-result">
<div class="result-title">嵌入向量已生成</div>
<div class="vector-viz">
<div
v-for="(val, i) in finalVector"
:key="i"
class="vec-bar"
:style="{
height: Math.abs(val) * 60 + 'px',
background: val >= 0 ? '#3b82f6' : '#ef4444',
opacity: 0.4 + Math.abs(val) * 0.6
}"
>
<span class="vec-val">{{ val.toFixed(2) }}</span>
</div>
</div>
<p class="vec-note">
实际嵌入向量通常有 768~1536 个维度这里仅展示前 16 维的模拟值
</p>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const inputText = ref('今天天气真不错,适合出去散步')
const currentStep = ref(-1)
const running = ref(false)
const stepOutputs = reactive({})
const finalVector = ref([])
const steps = [
{
key: 'tokenize',
title: '分词 (Tokenize)',
desc: '将文本拆分为模型可处理的 Token 序列',
color: '#3b82f6'
},
{
key: 'encode',
title: '编码 (Encode)',
desc: '将 Token 映射为数字 ID',
color: '#8b5cf6'
},
{
key: 'model',
title: '模型推理 (Model)',
desc: '通过 Transformer 模型生成上下文感知的向量表示',
color: '#10b981'
},
{
key: 'pool',
title: '池化 (Pooling)',
desc: '将多个 Token 向量聚合为单一句子向量',
color: '#f59e0b'
},
{
key: 'normalize',
title: '归一化 (Normalize)',
desc: '将向量缩放到单位长度,便于余弦相似度计算',
color: '#ef4444'
}
]
function simulateTokenize(text) {
const tokens = []
const zhRegex = /[\u4e00-\u9fa5]/g
const enRegex = /[a-zA-Z]+/g
let i = 0
while (i < text.length) {
if (/[\u4e00-\u9fa5]/.test(text[i])) {
tokens.push(text[i])
i++
} else if (/[a-zA-Z]/.test(text[i])) {
let word = ''
while (i < text.length && /[a-zA-Z]/.test(text[i])) {
word += text[i]
i++
}
tokens.push(word)
} else if (/\s/.test(text[i])) {
i++
} else {
tokens.push(text[i])
i++
}
}
return tokens
}
function hashCode(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return Math.abs(hash)
}
function generateVector(text, dim = 16) {
const vec = []
for (let i = 0; i < dim; i++) {
const seed = hashCode(text + i)
vec.push(((seed % 2000) - 1000) / 1000)
}
// 归一化
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0))
return vec.map((v) => v / (mag || 1))
}
async function runPipeline() {
if (running.value) return
running.value = true
currentStep.value = -1
Object.keys(stepOutputs).forEach((k) => delete stepOutputs[k])
finalVector.value = []
const text = inputText.value || '你好世界'
// Step 1: Tokenize
await delay(400)
currentStep.value = 0
const tokens = simulateTokenize(text)
stepOutputs.tokenize = `[${tokens.map((t) => '"' + t + '"').join(', ')}]`
// Step 2: Encode
await delay(500)
currentStep.value = 1
const ids = tokens.map((t) => hashCode(t) % 50000)
stepOutputs.encode = `[${ids.join(', ')}]`
// Step 3: Model
await delay(600)
currentStep.value = 2
stepOutputs.model = `${tokens.length} 个 Token -> ${tokens.length} x 768 维隐藏状态矩阵`
// Step 4: Pool
await delay(500)
currentStep.value = 3
stepOutputs.pool = `Mean Pooling: ${tokens.length} 个向量 -> 1 个 768 维句子向量`
// Step 5: Normalize
await delay(400)
currentStep.value = 4
finalVector.value = generateVector(text)
stepOutputs.normalize = `L2 归一化: ||v|| = 1.0000`
running.value = false
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
</script>
<style scoped>
.pipeline-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.input-area {
display: flex;
gap: 0.5rem;
align-items: flex-end;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.input-area label {
width: 100%;
font-size: 0.85rem;
color: var(--vp-c-text-2);
font-weight: 600;
margin-bottom: 0.25rem;
}
.text-input {
flex: 1;
min-width: 200px;
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.9rem;
}
.text-input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.run-btn {
padding: 8px 18px;
border: none;
border-radius: 6px;
background: var(--vp-c-brand);
color: #fff;
cursor: pointer;
font-size: 0.85rem;
white-space: nowrap;
transition: background 0.2s;
}
.run-btn:hover {
opacity: 0.9;
}
.pipeline-steps {
display: flex;
flex-direction: column;
gap: 0;
}
.step {
padding: 0.75rem;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
opacity: 0.4;
transition: all 0.3s;
}
.step.active {
opacity: 1;
}
.step.current {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px var(--vp-c-brand-soft);
}
.step-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.step-num {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
color: #fff;
background: var(--vp-c-text-3);
flex-shrink: 0;
transition: background 0.3s;
}
.step-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.step-check {
margin-left: auto;
color: #10b981;
font-weight: 700;
}
.step-content {
margin-top: 0.5rem;
padding-left: 2rem;
}
.step-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 0.4rem;
}
.step-output {
background: var(--vp-c-bg-alt);
border-radius: 4px;
padding: 6px 10px;
overflow-x: auto;
}
.step-output code {
font-size: 0.75rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
word-break: break-all;
}
.step-arrow {
text-align: center;
height: 24px;
line-height: 24px;
color: var(--vp-c-text-3);
font-size: 1rem;
}
.step-arrow span {
opacity: 0;
transition: opacity 0.3s;
}
.step-arrow span.visible {
opacity: 0.5;
}
.final-result {
margin-top: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border: 1px solid #10b981;
border-radius: 8px;
}
.result-title {
font-weight: 600;
color: #10b981;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.vector-viz {
display: flex;
align-items: flex-end;
gap: 4px;
height: 80px;
padding: 0 4px;
}
.vec-bar {
flex: 1;
min-width: 0;
border-radius: 2px 2px 0 0;
position: relative;
transition: height 0.3s;
}
.vec-val {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
font-size: 0.6rem;
color: var(--vp-c-text-3);
font-family: var(--vp-font-family-mono);
white-space: nowrap;
}
.vec-note {
margin: 0.75rem 0 0;
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
@media (max-width: 640px) {
.pipeline-demo {
padding: 1rem;
}
.input-area {
flex-direction: column;
align-items: stretch;
}
.run-btn {
width: 100%;
}
}
</style>
@@ -0,0 +1,401 @@
<!--
VectorDatabaseDemo.vue
向量数据库对比组件
用途
交互式对比主流向量数据库的特性适用场景和架构差异
交互功能
- 点击卡片查看详情
- 对比不同数据库的核心指标
- 场景推荐
-->
<template>
<div class="vdb-demo">
<div class="demo-header">
<h4>主流向量数据库对比</h4>
<p class="desc">点击卡片查看详细信息了解不同向量数据库的特点与适用场景</p>
</div>
<div class="db-grid">
<div
v-for="db in databases"
:key="db.name"
class="db-card"
:class="{ active: selected === db.name }"
@click="selected = selected === db.name ? null : db.name"
>
<div class="card-header">
<span class="db-icon" :style="{ background: db.color }">{{ db.icon }}</span>
<div>
<div class="db-name">{{ db.name }}</div>
<div class="db-type">{{ db.type }}</div>
</div>
</div>
<div class="card-tags">
<span
v-for="tag in db.tags"
:key="tag"
class="tag"
>{{ tag }}</span>
</div>
<div v-if="selected === db.name" class="card-detail">
<div class="detail-row">
<span class="detail-label">开源协议</span>
<span class="detail-val">{{ db.license }}</span>
</div>
<div class="detail-row">
<span class="detail-label">索引算法</span>
<span class="detail-val">{{ db.index }}</span>
</div>
<div class="detail-row">
<span class="detail-label">最大维度</span>
<span class="detail-val">{{ db.maxDim }}</span>
</div>
<div class="detail-row">
<span class="detail-label">适用场景</span>
<span class="detail-val">{{ db.useCase }}</span>
</div>
<p class="detail-desc">{{ db.description }}</p>
</div>
<div class="card-metrics">
<div class="metric">
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: db.perf + '%', background: db.color }"></div>
</div>
<span class="metric-label">性能</span>
</div>
<div class="metric">
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: db.ease + '%', background: db.color }"></div>
</div>
<span class="metric-label">易用性</span>
</div>
<div class="metric">
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: db.scale + '%', background: db.color }"></div>
</div>
<span class="metric-label">扩展性</span>
</div>
</div>
</div>
</div>
<div class="scenario-section">
<h5>场景推荐</h5>
<div class="scenario-grid">
<div
v-for="s in scenarios"
:key="s.title"
class="scenario-card"
>
<div class="scenario-icon">{{ s.icon }}</div>
<div class="scenario-title">{{ s.title }}</div>
<div class="scenario-rec">{{ s.recommend }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selected = ref(null)
const databases = [
{
name: 'Pinecone',
type: '全托管云服务',
icon: 'P',
color: '#3b82f6',
tags: ['云原生', 'Serverless'],
license: '商业',
index: 'Proprietary ANN',
maxDim: '20,000',
useCase: '快速上线的 AI 应用',
description: '全托管向量数据库,无需运维,按用量付费。适合初创团队和快速原型开发。',
perf: 85,
ease: 95,
scale: 80
},
{
name: 'Milvus',
type: '开源分布式',
icon: 'M',
color: '#10b981',
tags: ['开源', '分布式', '高性能'],
license: 'Apache 2.0',
index: 'IVF / HNSW / DiskANN',
maxDim: '32,768',
useCase: '大规模企业级检索',
description: '支持十亿级向量的分布式数据库,提供丰富的索引类型和混合查询能力。',
perf: 95,
ease: 65,
scale: 95
},
{
name: 'Weaviate',
type: '开源 AI 原生',
icon: 'W',
color: '#8b5cf6',
tags: ['开源', 'GraphQL', '模块化'],
license: 'BSD-3',
index: 'HNSW',
maxDim: '65,536',
useCase: '语义搜索与多模态',
description: '内置向量化模块,支持文本、图像等多模态数据的自动嵌入和检索。',
perf: 80,
ease: 85,
scale: 80
},
{
name: 'Chroma',
type: '轻量级嵌入式',
icon: 'C',
color: '#f59e0b',
tags: ['开源', '轻量', 'Python'],
license: 'Apache 2.0',
index: 'HNSW',
maxDim: '无限制',
useCase: '本地开发与 RAG 原型',
description: '极简 API 设计,几行代码即可集成。非常适合 LangChain / LlamaIndex 生态。',
perf: 60,
ease: 98,
scale: 40
},
{
name: 'pgvector',
type: 'PostgreSQL 扩展',
icon: 'pg',
color: '#ef4444',
tags: ['SQL', 'PostgreSQL', '扩展'],
license: 'PostgreSQL',
index: 'IVFFlat / HNSW',
maxDim: '16,000',
useCase: '已有 PG 基础设施的团队',
description: '在现有 PostgreSQL 中添加向量能力,无需引入新的数据库。支持 SQL 混合查询。',
perf: 65,
ease: 80,
scale: 60
}
]
const scenarios = [
{ icon: '&#x1F680;', title: '快速原型', recommend: 'Chroma / Pinecone' },
{ icon: '&#x1F3E2;', title: '企业级部署', recommend: 'Milvus / Weaviate' },
{ icon: '&#x1F4BE;', title: '已有 PG 数据库', recommend: 'pgvector' },
{ icon: '&#x1F916;', title: 'RAG 应用', recommend: 'Chroma / Weaviate' }
]
</script>
<style scoped>
.vdb-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.db-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.db-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.db-card:hover {
border-color: var(--vp-c-text-3);
}
.db-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.6rem;
}
.db-icon {
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 0.8rem;
flex-shrink: 0;
}
.db-name {
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.db-type {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.6rem;
}
.tag {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 3px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-3);
}
.card-detail {
border-top: 1px solid var(--vp-c-divider);
padding-top: 0.6rem;
margin-bottom: 0.6rem;
}
.detail-row {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
padding: 3px 0;
}
.detail-label {
color: var(--vp-c-text-3);
}
.detail-val {
color: var(--vp-c-text-1);
font-weight: 500;
text-align: right;
}
.detail-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin: 0.5rem 0 0;
line-height: 1.5;
}
.card-metrics {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric {
display: flex;
align-items: center;
gap: 6px;
}
.metric-bar-wrap {
flex: 1;
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
overflow: hidden;
}
.metric-bar {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.metric-label {
font-size: 0.7rem;
color: var(--vp-c-text-3);
width: 40px;
text-align: right;
}
.scenario-section h5 {
margin: 0 0 0.75rem;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 0.5rem;
}
.scenario-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
}
.scenario-icon {
font-size: 1.5rem;
margin-bottom: 4px;
}
.scenario-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 2px;
}
.scenario-rec {
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: 500;
}
@media (max-width: 640px) {
.db-grid {
grid-template-columns: 1fr;
}
.scenario-grid {
grid-template-columns: repeat(2, 1fr);
}
.vdb-demo {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,443 @@
<!--
VectorIndexDemo.vue
向量索引策略可视化组件
用途
展示暴力搜索与近似最近邻(ANN)搜索的对比可视化不同索引策略的工作原理
交互功能
- 切换暴力搜索和ANN搜索模式
- 点击查询点触发搜索动画
- 展示搜索过程中访问的节点数量对比
-->
<template>
<div class="index-demo">
<div class="demo-header">
<h4>向量索引策略对比</h4>
<p class="desc">对比暴力搜索与近似最近邻搜索的效率差异</p>
</div>
<div class="controls">
<button
v-for="mode in modes"
:key="mode.key"
class="mode-btn"
:class="{ active: activeMode === mode.key }"
@click="switchMode(mode.key)"
>
{{ mode.label }}
</button>
<button class="search-btn" @click="runSearch">
{{ searching ? '搜索中...' : '开始搜索' }}
</button>
</div>
<div class="canvas-area">
<svg viewBox="0 0 500 380" class="index-svg">
<!-- 数据点 -->
<circle
v-for="(p, i) in points"
:key="'p' + i"
:cx="p.x" :cy="p.y" r="5"
:fill="pointColor(i)"
:opacity="pointOpacity(i)"
stroke="#fff"
stroke-width="0.8"
class="data-pt"
/>
<!-- ANN 分区线 ( ANN 模式) -->
<template v-if="activeMode === 'ann'">
<line
v-for="(line, i) in partitionLines"
:key="'line' + i"
:x1="line.x1" :y1="line.y1"
:x2="line.x2" :y2="line.y2"
stroke="var(--vp-c-brand)"
stroke-width="1"
stroke-dasharray="4 3"
opacity="0.3"
/>
</template>
<!-- 查询点 -->
<g>
<circle
:cx="query.x" :cy="query.y" r="9"
fill="none" stroke="#ef4444" stroke-width="2.5"
/>
<circle :cx="query.x" :cy="query.y" r="3" fill="#ef4444" />
<text
:x="query.x + 14" :y="query.y + 4"
fill="#ef4444" font-size="12" font-weight="600"
>查询点</text>
</g>
<!-- 搜索连线 -->
<line
v-for="(idx, i) in visitedOrder"
:key="'visit' + i"
:x1="query.x" :y1="query.y"
:x2="points[idx].x" :y2="points[idx].y"
:stroke="resultIndices.includes(idx) ? '#10b981' : '#94a3b8'"
:stroke-width="resultIndices.includes(idx) ? 2 : 0.8"
:opacity="resultIndices.includes(idx) ? 0.8 : 0.25"
/>
<!-- 结果高亮 -->
<circle
v-for="idx in resultIndices"
:key="'res' + idx"
:cx="points[idx].x" :cy="points[idx].y" r="8"
fill="none" stroke="#10b981" stroke-width="2.5"
/>
</svg>
</div>
<!-- 统计面板 -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">数据点总数</div>
<div class="stat-val">{{ points.length }}</div>
</div>
<div class="stat-card">
<div class="stat-label">访问节点数</div>
<div class="stat-val" :class="{ good: visitedOrder.length < points.length }">
{{ visitedOrder.length }}
</div>
</div>
<div class="stat-card">
<div class="stat-label">搜索效率</div>
<div class="stat-val efficiency">
{{ points.length > 0 ? ((visitedOrder.length / points.length) * 100).toFixed(0) : 0 }}%
</div>
</div>
<div class="stat-card">
<div class="stat-label">找到最近 K </div>
<div class="stat-val">{{ resultIndices.length }}</div>
</div>
</div>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>策略</th>
<th>时间复杂度</th>
<th>精确度</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr :class="{ 'row-active': activeMode === 'brute' }">
<td>暴力搜索</td>
<td><code>O(n)</code></td>
<td>100%</td>
<td>小数据集 (&lt;10K)</td>
</tr>
<tr :class="{ 'row-active': activeMode === 'ann' }">
<td>ANN (IVF)</td>
<td><code>O(n/k)</code></td>
<td>~95%</td>
<td>大数据集 (>100K)</td>
</tr>
<tr>
<td>HNSW</td>
<td><code>O(log n)</code></td>
<td>~98%</td>
<td>高性能检索</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const activeMode = ref('brute')
const searching = ref(false)
const visitedOrder = ref([])
const resultIndices = ref([])
const modes = [
{ key: 'brute', label: '暴力搜索' },
{ key: 'ann', label: 'ANN 近似搜索' }
]
const query = reactive({ x: 250, y: 190 })
// 生成随机数据点
function generatePoints() {
const pts = []
const rng = (seed) => {
let s = seed
return () => { s = (s * 16807) % 2147483647; return s / 2147483647 }
}
const rand = rng(42)
for (let i = 0; i < 60; i++) {
pts.push({
x: 40 + rand() * 420,
y: 30 + rand() * 320
})
}
return pts
}
const points = ref(generatePoints())
const partitionLines = [
{ x1: 250, y1: 10, x2: 250, y2: 370 },
{ x1: 30, y1: 190, x2: 470, y2: 190 },
{ x1: 140, y1: 10, x2: 140, y2: 370 },
{ x1: 360, y1: 10, x2: 360, y2: 370 }
]
function dist(a, b) {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
}
function switchMode(mode) {
activeMode.value = mode
visitedOrder.value = []
resultIndices.value = []
}
function runSearch() {
if (searching.value) return
searching.value = true
visitedOrder.value = []
resultIndices.value = []
const K = 3
const allDists = points.value.map((p, i) => ({ i, d: dist(query, p) }))
allDists.sort((a, b) => a.d - b.d)
const trueTopK = allDists.slice(0, K).map((x) => x.i)
if (activeMode.value === 'brute') {
// 暴力搜索:逐个访问
const order = allDists.map((x) => x.i)
let step = 0
const timer = setInterval(() => {
if (step < order.length) {
visitedOrder.value = order.slice(0, step + 1)
step++
} else {
clearInterval(timer)
resultIndices.value = trueTopK
searching.value = false
}
}, 30)
} else {
// ANN:只搜索查询点所在分区附近
const qPartX = query.x < 140 ? 0 : query.x < 250 ? 1 : query.x < 360 ? 2 : 3
const qPartY = query.y < 190 ? 0 : 1
const nearby = points.value
.map((p, i) => {
const pPartX = p.x < 140 ? 0 : p.x < 250 ? 1 : p.x < 360 ? 2 : 3
const pPartY = p.y < 190 ? 0 : 1
const samePart = Math.abs(pPartX - qPartX) <= 1 && pPartY === qPartY
return { i, d: dist(query, p), samePart }
})
.filter((x) => x.samePart)
.sort((a, b) => a.d - b.d)
const order = nearby.map((x) => x.i)
let step = 0
const timer = setInterval(() => {
if (step < order.length) {
visitedOrder.value = order.slice(0, step + 1)
step++
} else {
clearInterval(timer)
resultIndices.value = nearby.slice(0, K).map((x) => x.i)
searching.value = false
}
}, 50)
}
}
function pointColor(i) {
if (resultIndices.value.includes(i)) return '#10b981'
if (visitedOrder.value.includes(i)) return '#3b82f6'
return '#94a3b8'
}
function pointOpacity(i) {
if (resultIndices.value.includes(i)) return 1
if (visitedOrder.value.includes(i)) return 0.8
return 0.4
}
</script>
<style scoped>
.index-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.mode-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.mode-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.search-btn {
padding: 6px 16px;
border: 1px solid #10b981;
border-radius: 6px;
background: #10b981;
color: #fff;
cursor: pointer;
font-size: 0.85rem;
margin-left: auto;
transition: all 0.2s;
}
.search-btn:hover {
background: #059669;
}
.canvas-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
margin-bottom: 1rem;
overflow: hidden;
}
.index-svg {
width: 100%;
height: auto;
display: block;
}
.data-pt {
transition: fill 0.3s, opacity 0.3s;
}
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.stat-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
text-align: center;
}
.stat-label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 2px;
}
.stat-val {
font-size: 1.2rem;
font-weight: 700;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.stat-val.good {
color: #10b981;
}
.stat-val.efficiency {
color: var(--vp-c-brand);
}
.comparison-table {
overflow-x: auto;
}
.comparison-table table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.comparison-table th,
.comparison-table td {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
text-align: left;
}
.comparison-table th {
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
font-weight: 600;
}
.comparison-table td {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.row-active {
background: var(--vp-c-brand-soft) !important;
}
.row-active td {
background: var(--vp-c-brand-soft) !important;
border-color: var(--vp-c-brand);
}
code {
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
background: var(--vp-c-bg);
padding: 2px 4px;
border-radius: 3px;
}
@media (max-width: 640px) {
.stats-row {
grid-template-columns: repeat(2, 1fr);
}
.index-demo {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,398 @@
<!--
VectorSimilarityDemo.vue
向量相似度交互演示组件
用途
交互式展示余弦相似度和欧氏距离的计算过程用户可拖动点观察相似度变化
交互功能
- 拖动两个向量端点
- 实时计算余弦相似度和欧氏距离
- 切换度量方式查看差异
-->
<template>
<div class="similarity-demo">
<div class="demo-header">
<h4>向量相似度计算器</h4>
<p class="desc">拖动向量端点观察不同相似度指标的实时变化</p>
</div>
<div class="metric-tabs">
<button
v-for="m in metrics"
:key="m.key"
class="metric-btn"
:class="{ active: activeMetric === m.key }"
@click="activeMetric = m.key"
>
{{ m.label }}
</button>
</div>
<div class="canvas-area">
<svg
ref="svgEl"
viewBox="0 0 460 360"
class="sim-svg"
@mousemove="onDrag"
@mouseup="stopDrag"
@mouseleave="stopDrag"
>
<!-- 网格 -->
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="var(--vp-c-divider)" stroke-width="0.5" opacity="0.5" />
</pattern>
</defs>
<rect x="30" y="10" width="400" height="320" fill="url(#grid)" />
<!-- 坐标轴 -->
<line x1="230" y1="10" x2="230" y2="330" stroke="var(--vp-c-divider)" stroke-width="1" />
<line x1="30" y1="170" x2="430" y2="170" stroke="var(--vp-c-divider)" stroke-width="1" />
<!-- 夹角弧线 (余弦相似度模式) -->
<path
v-if="activeMetric === 'cosine'"
:d="anglePath"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="1.5"
stroke-dasharray="3 2"
opacity="0.6"
/>
<!-- 距离线 (欧氏距离模式) -->
<line
v-if="activeMetric === 'euclidean'"
:x1="vecA.x" :y1="vecA.y"
:x2="vecB.x" :y2="vecB.y"
stroke="#ef4444"
stroke-width="2"
stroke-dasharray="6 3"
opacity="0.7"
/>
<!-- 向量 A -->
<line x1="230" y1="170" :x2="vecA.x" :y2="vecA.y" stroke="#3b82f6" stroke-width="2.5" />
<polygon :points="arrowHead(230, 170, vecA.x, vecA.y)" fill="#3b82f6" />
<circle
:cx="vecA.x" :cy="vecA.y" r="10"
fill="#3b82f6" stroke="#fff" stroke-width="2"
class="drag-handle"
@mousedown.prevent="startDrag('A')"
/>
<text :x="vecA.x + 14" :y="vecA.y - 8" fill="#3b82f6" font-size="13" font-weight="600">A</text>
<!-- 向量 B -->
<line x1="230" y1="170" :x2="vecB.x" :y2="vecB.y" stroke="#10b981" stroke-width="2.5" />
<polygon :points="arrowHead(230, 170, vecB.x, vecB.y)" fill="#10b981" />
<circle
:cx="vecB.x" :cy="vecB.y" r="10"
fill="#10b981" stroke="#fff" stroke-width="2"
class="drag-handle"
@mousedown.prevent="startDrag('B')"
/>
<text :x="vecB.x + 14" :y="vecB.y - 8" fill="#10b981" font-size="13" font-weight="600">B</text>
<!-- 原点标记 -->
<circle cx="230" cy="170" r="3" fill="var(--vp-c-text-3)" />
</svg>
</div>
<!-- 结果面板 -->
<div class="results">
<div class="result-card" :class="{ highlight: activeMetric === 'cosine' }">
<div class="result-label">余弦相似度</div>
<div class="result-value">{{ cosineSim.toFixed(4) }}</div>
<div class="result-bar">
<div class="bar-fill cosine-bar" :style="{ width: ((cosineSim + 1) / 2 * 100) + '%' }"></div>
</div>
<div class="result-range">-1 (相反) ~ 1 (相同)</div>
</div>
<div class="result-card" :class="{ highlight: activeMetric === 'euclidean' }">
<div class="result-label">欧氏距离</div>
<div class="result-value">{{ euclideanDist.toFixed(2) }}</div>
<div class="result-bar">
<div class="bar-fill euclidean-bar" :style="{ width: Math.min(euclideanDist / 5 * 100, 100) + '%' }"></div>
</div>
<div class="result-range">0 (完全重合) ~ &#x221E; (无穷远)</div>
</div>
<div class="result-card">
<div class="result-label">点积</div>
<div class="result-value">{{ dotProduct.toFixed(2) }}</div>
<div class="result-hint">dot(A, B) = |A||B|cos&#x3B8;</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">&#x1F4A1;</span>
<strong>余弦相似度</strong>只关注方向不关注长度适合文本语义比较<strong>欧氏距离</strong>同时考虑方向和大小适合需要绝对距离的场景
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeMetric = ref('cosine')
const dragging = ref(null)
const metrics = [
{ key: 'cosine', label: '余弦相似度' },
{ key: 'euclidean', label: '欧氏距离' }
]
const vecA = ref({ x: 350, y: 80 })
const vecB = ref({ x: 370, y: 250 })
const svgEl = ref(null)
function toVec(p) {
return { x: (p.x - 230) / 100, y: (170 - p.y) / 100 }
}
const cosineSim = computed(() => {
const a = toVec(vecA.value)
const b = toVec(vecB.value)
const dot = a.x * b.x + a.y * b.y
const magA = Math.sqrt(a.x * a.x + a.y * a.y)
const magB = Math.sqrt(b.x * b.x + b.y * b.y)
if (magA === 0 || magB === 0) return 0
return dot / (magA * magB)
})
const euclideanDist = computed(() => {
const a = toVec(vecA.value)
const b = toVec(vecB.value)
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
})
const dotProduct = computed(() => {
const a = toVec(vecA.value)
const b = toVec(vecB.value)
return a.x * b.x + a.y * b.y
})
const anglePath = computed(() => {
const r = 40
const ax = vecA.value.x - 230
const ay = vecA.value.y - 170
const bx = vecB.value.x - 230
const by = vecB.value.y - 170
const angA = Math.atan2(ay, ax)
const angB = Math.atan2(by, bx)
const x1 = 230 + r * Math.cos(angA)
const y1 = 170 + r * Math.sin(angA)
const x2 = 230 + r * Math.cos(angB)
const y2 = 170 + r * Math.sin(angB)
const diff = angB - angA
const large = Math.abs(diff) > Math.PI ? 1 : 0
const sweep = diff > 0 ? 1 : 0
return `M ${x1} ${y1} A ${r} ${r} 0 ${large} ${sweep} ${x2} ${y2}`
})
function arrowHead(x1, y1, x2, y2) {
const dx = x2 - x1
const dy = y2 - y1
const len = Math.sqrt(dx * dx + dy * dy)
if (len < 1) return ''
const ux = dx / len
const uy = dy / len
const px = -uy
const py = ux
const tipX = x2
const tipY = y2
const s = 8
const p1x = tipX - ux * s + px * s * 0.4
const p1y = tipY - uy * s + py * s * 0.4
const p2x = tipX - ux * s - px * s * 0.4
const p2y = tipY - uy * s - py * s * 0.4
return `${tipX},${tipY} ${p1x},${p1y} ${p2x},${p2y}`
}
function startDrag(which) {
dragging.value = which
}
function stopDrag() {
dragging.value = null
}
function onDrag(e) {
if (!dragging.value || !svgEl.value) return
const svg = svgEl.value
const rect = svg.getBoundingClientRect()
const scaleX = 460 / rect.width
const scaleY = 360 / rect.height
const x = Math.max(30, Math.min(430, (e.clientX - rect.left) * scaleX))
const y = Math.max(10, Math.min(330, (e.clientY - rect.top) * scaleY))
if (dragging.value === 'A') {
vecA.value = { x, y }
} else {
vecB.value = { x, y }
}
}
</script>
<style scoped>
.similarity-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.metric-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.metric-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.metric-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.canvas-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
margin-bottom: 1rem;
overflow: hidden;
}
.sim-svg {
width: 100%;
height: auto;
display: block;
user-select: none;
}
.drag-handle {
cursor: grab;
}
.drag-handle:active {
cursor: grabbing;
}
.results {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
}
.result-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
text-align: center;
transition: border-color 0.2s;
}
.result-card.highlight {
border-color: var(--vp-c-brand);
}
.result-label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.result-value {
font-size: 1.4rem;
font-weight: 700;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.result-bar {
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
margin: 6px 0 4px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.cosine-bar {
background: #3b82f6;
}
.euclidean-bar {
background: #ef4444;
}
.result-range {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.result-hint {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-top: 6px;
font-family: var(--vp-font-family-mono);
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box p {
margin: 0;
}
.info-box .icon {
margin-right: 4px;
}
@media (max-width: 640px) {
.results {
grid-template-columns: 1fr;
}
.similarity-demo {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,444 @@
<!--
AlertEscalationDemo.vue
告警升级流程演示展示告警如何根据严重程度和时间逐级升级
-->
<template>
<div class="alert-escalation-demo">
<div class="header">
<div class="title">告警升级流程 (Alert Escalation)</div>
<div class="subtitle">选择一个场景观察告警如何逐级升级</div>
</div>
<div class="scenario-select">
<button
v-for="s in scenarios"
:key="s.id"
:class="['scenario-btn', { active: activeScenario === s.id }]"
@click="startScenario(s.id)"
>
{{ s.name }}
</button>
</div>
<div class="escalation-flow">
<div
v-for="(step, index) in escalationSteps"
:key="step.id"
:class="[
'esc-step',
{
active: currentStep === index,
completed: currentStep > index,
pending: currentStep < index
}
]"
>
<div class="esc-left">
<div class="esc-icon" :style="{ background: step.color }">
{{ step.icon }}
</div>
<div v-if="index < escalationSteps.length - 1" class="esc-line">
<div
class="esc-line-fill"
:class="{ filled: currentStep > index }"
></div>
</div>
</div>
<div class="esc-content">
<div class="esc-header">
<span class="esc-title">{{ step.title }}</span>
<span class="esc-time">{{ step.time }}</span>
</div>
<div class="esc-desc">{{ step.desc }}</div>
<div v-if="step.action && currentStep >= index" class="esc-action">
{{ step.action }}
</div>
</div>
</div>
</div>
<div v-if="activeScenario" class="timer-bar">
<div class="timer-label">
升级进度 {{ currentStep + 1 }} / {{ escalationSteps.length }}
</div>
<div class="timer-track">
<div
class="timer-fill"
:style="{
width: ((currentStep + 1) / escalationSteps.length) * 100 + '%'
}"
></div>
</div>
<div class="timer-controls">
<button
class="ctrl-btn"
@click="prevStep"
:disabled="currentStep <= 0"
>
上一级
</button>
<button
class="ctrl-btn"
@click="nextStep"
:disabled="currentStep >= escalationSteps.length - 1"
>
下一级升级
</button>
</div>
</div>
<div class="rule-box">
<div class="rule-title">升级规则说明</div>
<div class="rules">
<div class="rule-item">
<span class="rule-dot" style="background: #22c55e"></span>
<span>P3/P4 告警仅通知值班工程师无需升级</span>
</div>
<div class="rule-item">
<span class="rule-dot" style="background: #eab308"></span>
<span>P2 告警15 分钟未响应则升级至团队负责人</span>
</div>
<div class="rule-item">
<span class="rule-dot" style="background: #f59e0b"></span>
<span>P1 告警5 分钟未响应升级30 分钟未解决升级至总监</span>
</div>
<div class="rule-item">
<span class="rule-dot" style="background: #ef4444"></span>
<span>P0 告警立即通知全链路15 分钟未缓解升级至 VP/CTO</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeScenario = ref(null)
const currentStep = ref(0)
const scenarios = [
{ id: 'p0', name: 'P0 数据库宕机' },
{ id: 'p1', name: 'P1 接口超时' },
{ id: 'p2', name: 'P2 性能下降' }
]
const scenarioSteps = {
p0: [
{
id: 1,
icon: '📡',
color: '#3b82f6',
title: '监控系统检测',
time: 'T+0s',
desc: 'Prometheus 检测到数据库连接池耗尽,所有查询超时',
action: '自动触发 P0 级别告警'
},
{
id: 2,
icon: '📱',
color: '#f59e0b',
title: '值班工程师',
time: 'T+30s',
desc: '电话 + 短信 + 即时通讯同时通知值班 DBA',
action: '值班工程师确认告警,开始排查'
},
{
id: 3,
icon: '👥',
color: '#ef4444',
title: '团队负责人',
time: 'T+5min',
desc: '自动升级至数据库团队负责人和后端团队负责人',
action: '团队负责人召集紧急会议'
},
{
id: 4,
icon: '🎖️',
color: '#8b5cf6',
title: '技术总监',
time: 'T+15min',
desc: '问题未缓解,自动升级至技术总监',
action: '总监协调跨团队资源,启动应急预案'
},
{
id: 5,
icon: '🏢',
color: '#1e293b',
title: 'VP / CTO',
time: 'T+30min',
desc: '重大事故升级至高管层,准备对外沟通',
action: 'CTO 决策是否启动灾备切换'
}
],
p1: [
{
id: 1,
icon: '📡',
color: '#3b82f6',
title: '监控系统检测',
time: 'T+0s',
desc: 'API 网关检测到 P99 延迟超过 3 秒阈值',
action: '触发 P1 级别告警'
},
{
id: 2,
icon: '📱',
color: '#f59e0b',
title: '值班工程师',
time: 'T+1min',
desc: '即时通讯 + 短信通知值班后端工程师',
action: '工程师开始查看监控面板和日志'
},
{
id: 3,
icon: '👥',
color: '#ef4444',
title: '团队负责人',
time: 'T+15min',
desc: '15 分钟未解决,自动升级至团队负责人',
action: '负责人评估是否需要更多人力支援'
},
{
id: 4,
icon: '🎖️',
color: '#8b5cf6',
title: '技术总监',
time: 'T+30min',
desc: '30 分钟未缓解,升级至技术总监',
action: '总监决定是否升级为 P0'
}
],
p2: [
{
id: 1,
icon: '📡',
color: '#3b82f6',
title: '监控系统检测',
time: 'T+0s',
desc: '检测到页面加载时间从 1.2s 上升到 2.8s',
action: '触发 P2 级别告警'
},
{
id: 2,
icon: '📱',
color: '#eab308',
title: '值班工程师',
time: 'T+5min',
desc: '即时通讯通知值班前端工程师',
action: '工程师确认问题,记录工单'
},
{
id: 3,
icon: '👥',
color: '#f59e0b',
title: '团队负责人',
time: 'T+30min',
desc: '30 分钟未响应时升级至团队负责人',
action: '负责人安排当天修复'
}
]
}
const escalationSteps = computed(() => {
if (!activeScenario.value) return scenarioSteps.p0
return scenarioSteps[activeScenario.value]
})
const startScenario = (id) => {
activeScenario.value = id
currentStep.value = 0
}
const nextStep = () => {
if (currentStep.value < escalationSteps.value.length - 1) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
</script>
<style scoped>
.alert-escalation-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1.5rem; }
.title { font-weight: 700; font-size: 1.1rem; margin-bottom: 0.25rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.scenario-select {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.scenario-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.scenario-btn:hover { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.scenario-btn.active { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.escalation-flow {
display: flex;
flex-direction: column;
margin-bottom: 1.5rem;
}
.esc-step {
display: flex;
gap: 1rem;
opacity: 0.4;
transition: all 0.3s;
}
.esc-step.active,
.esc-step.completed { opacity: 1; }
.esc-left {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.esc-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: #fff;
z-index: 1;
}
.esc-line {
width: 3px;
flex: 1;
min-height: 20px;
background: var(--vp-c-divider);
margin: 4px 0;
}
.esc-line-fill {
width: 100%;
height: 0;
background: var(--vp-c-brand);
transition: height 0.5s;
}
.esc-line-fill.filled { height: 100%; }
.esc-content {
padding-bottom: 1rem;
flex: 1;
}
.esc-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.25rem;
}
.esc-title { font-weight: 600; font-size: 0.95rem; }
.esc-time { font-size: 0.8rem; color: var(--vp-c-text-3); font-family: monospace; }
.esc-desc { font-size: 0.85rem; color: var(--vp-c-text-2); margin-bottom: 0.3rem; }
.esc-action {
font-size: 0.85rem;
padding: 0.4rem 0.6rem;
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.08);
border-radius: 4px;
border-left: 3px solid var(--vp-c-brand);
color: var(--vp-c-text-1);
}
.timer-bar {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.timer-label { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.5rem; }
.timer-track {
height: 6px;
background: var(--vp-c-divider);
border-radius: 3px;
margin-bottom: 0.75rem;
overflow: hidden;
}
.timer-fill {
height: 100%;
background: var(--vp-c-brand);
border-radius: 3px;
transition: width 0.3s;
}
.timer-controls { display: flex; gap: 0.5rem; }
.ctrl-btn {
padding: 0.4rem 0.8rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.rule-box {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.rule-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.75rem; }
.rules { display: flex; flex-direction: column; gap: 0.5rem; }
.rule-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
}
.rule-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
@media (max-width: 768px) {
.scenario-select { flex-direction: column; }
.scenario-btn { width: 100%; }
}
</style>
@@ -0,0 +1,356 @@
<!--
IncidentCommandDemo.vue
事故指挥体系演示展示事故响应中的角色分工和协作关系
-->
<template>
<div class="incident-command-demo">
<div class="header">
<div class="title">事故指挥体系 (Incident Command System)</div>
<div class="subtitle">点击角色卡片了解各角色的职责和协作关系</div>
</div>
<div class="org-chart">
<div class="org-level org-top">
<div
:class="['role-card', 'commander', { active: activeRole === 'ic' }]"
@click="selectRole('ic')"
>
<div class="role-icon">🎖</div>
<div class="role-name">事故指挥官</div>
<div class="role-eng">Incident Commander</div>
</div>
</div>
<div class="org-connector">
<div class="connector-line"></div>
</div>
<div class="org-level org-middle">
<div
v-for="role in middleRoles"
:key="role.id"
:class="['role-card', { active: activeRole === role.id }]"
@click="selectRole(role.id)"
>
<div class="role-icon">{{ role.icon }}</div>
<div class="role-name">{{ role.name }}</div>
<div class="role-eng">{{ role.eng }}</div>
</div>
</div>
</div>
<div v-if="currentRole" class="role-detail">
<div class="detail-header" :style="{ background: currentRole.color }">
<span class="detail-icon">{{ currentRole.icon }}</span>
<span class="detail-name">{{ currentRole.name }}</span>
</div>
<div class="detail-body">
<div class="detail-section">
<div class="section-label">核心职责</div>
<div class="responsibilities">
<div
v-for="(r, i) in currentRole.responsibilities"
:key="i"
class="resp-item"
>
<span class="resp-num">{{ i + 1 }}</span>
<span>{{ r }}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-label">关键能力</div>
<div class="skills">
<span
v-for="skill in currentRole.skills"
:key="skill"
class="skill-tag"
>
{{ skill }}
</span>
</div>
</div>
<div class="detail-section">
<div class="section-label">常见话术</div>
<div class="quote-box">
"{{ currentRole.quote }}"
</div>
</div>
</div>
</div>
<div class="scenario-box">
<div class="scenario-title">模拟场景支付系统 P0 事故</div>
<div class="scenario-timeline">
<div
v-for="(event, i) in scenarioEvents"
:key="i"
class="event-item"
>
<span class="event-time">{{ event.time }}</span>
<span
class="event-role"
:style="{ background: event.color }"
>
{{ event.role }}
</span>
<span class="event-text">{{ event.text }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeRole = ref('ic')
const allRoles = {
ic: {
id: 'ic',
icon: '🎖️',
name: '事故指挥官',
eng: 'Incident Commander',
color: '#8b5cf6',
responsibilities: [
'统筹协调整个事故响应过程',
'做出关键决策(回滚、切流、降级等)',
'确保各角色高效协作,避免混乱',
'控制事故响应节奏,定时同步进展'
],
skills: ['全局视野', '决策能力', '沟通协调', '压力管理'],
quote: '当前状态:支付服务不可用。运维组排查数据库,后端组准备回滚方案,通讯组每 10 分钟同步一次。'
},
comm: {
id: 'comm',
icon: '📢',
name: '通讯协调员',
eng: 'Communications Lead',
color: '#3b82f6',
responsibilities: [
'对内:定时向管理层和相关团队通报进展',
'对外:更新状态页面,通知受影响客户',
'记录事故时间线,为复盘提供素材',
'过滤噪音信息,确保指挥官专注决策'
],
skills: ['文字表达', '信息整理', '多方沟通', '时间管理'],
quote: '状态更新:我们已识别到支付服务异常,团队正在紧急处理中,预计 30 分钟内恢复。'
},
ops: {
id: 'ops',
icon: '🔧',
name: '运维负责人',
eng: 'Operations Lead',
color: '#ef4444',
responsibilities: [
'执行具体的技术操作(回滚、重启、扩容等)',
'监控系统指标变化,判断操作效果',
'管理基础设施层面的应急响应',
'向指挥官汇报技术层面的进展'
],
skills: ['系统运维', '故障排查', '脚本自动化', '监控分析'],
quote: '数据库主节点 CPU 100%,正在执行主从切换,预计 2 分钟完成。'
},
dev: {
id: 'dev',
icon: '💻',
name: '开发负责人',
eng: 'Development Lead',
color: '#22c55e',
responsibilities: [
'分析代码层面的问题根因',
'准备和执行代码级别的修复或回滚',
'评估变更风险,提供技术方案',
'协调开发团队成员参与排查'
],
skills: ['代码分析', '快速调试', '风险评估', '版本管理'],
quote: '定位到问题:昨天上线的批量查询没有加分页,导致全表扫描拖垮数据库。准备回滚到上一版本。'
}
}
const middleRoles = [
allRoles.comm,
allRoles.ops,
allRoles.dev
]
const currentRole = computed(() => {
return allRoles[activeRole.value] || null
})
const selectRole = (id) => {
activeRole.value = id
}
const scenarioEvents = [
{ time: '14:02', role: '监控', color: '#3b82f6', text: '支付成功率从 99.9% 骤降至 12%,触发 P0 告警' },
{ time: '14:03', role: '指挥官', color: '#8b5cf6', text: '确认 P0 事故,开启事故频道,召集各角色' },
{ time: '14:05', role: '通讯', color: '#3b82f6', text: '通知管理层,更新状态页为"服务降级"' },
{ time: '14:08', role: '运维', color: '#ef4444', text: '发现数据库主节点 CPU 100%,连接池耗尽' },
{ time: '14:10', role: '开发', color: '#22c55e', text: '定位到昨日上线的慢查询是根因' },
{ time: '14:12', role: '指挥官', color: '#8b5cf6', text: '决策:立即回滚昨日变更 + 数据库主从切换' },
{ time: '14:15', role: '运维', color: '#ef4444', text: '数据库主从切换完成,连接恢复' },
{ time: '14:18', role: '开发', color: '#22c55e', text: '代码回滚部署完成' },
{ time: '14:20', role: '通讯', color: '#3b82f6', text: '支付成功率恢复至 99.8%,通知各方服务恢复' }
]
</script>
<style scoped>
.incident-command-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1.5rem; }
.title { font-weight: 700; font-size: 1.1rem; margin-bottom: 0.25rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.org-chart {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1.5rem;
}
.org-level { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
.org-connector {
display: flex;
justify-content: center;
padding: 0.5rem 0;
}
.connector-line {
width: 2px;
height: 24px;
background: var(--vp-c-divider);
}
.role-card {
padding: 0.75rem 1rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
cursor: pointer;
text-align: center;
transition: all 0.2s;
min-width: 130px;
}
.role-card:hover { border-color: var(--vp-c-brand); transform: translateY(-2px); }
.role-card.active { border-color: var(--vp-c-brand); box-shadow: 0 2px 12px rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.15); }
.role-card.commander { border-width: 3px; }
.role-icon { font-size: 1.5rem; margin-bottom: 0.25rem; }
.role-name { font-weight: 600; font-size: 0.9rem; }
.role-eng { font-size: 0.75rem; color: var(--vp-c-text-3); }
.role-detail {
background: var(--vp-c-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: #fff;
}
.detail-icon { font-size: 1.3rem; }
.detail-name { font-weight: 700; font-size: 1rem; }
.detail-body { padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
.detail-section { display: flex; flex-direction: column; gap: 0.3rem; }
.section-label { font-weight: 600; font-size: 0.85rem; color: var(--vp-c-text-2); }
.responsibilities { display: flex; flex-direction: column; gap: 0.3rem; }
.resp-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.85rem;
}
.resp-num {
width: 20px; height: 20px; border-radius: 50%;
background: var(--vp-c-bg-soft);
display: flex; align-items: center; justify-content: center;
font-size: 0.7rem; font-weight: 700; flex-shrink: 0;
}
.skills { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.skill-tag {
padding: 0.15rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
}
.quote-box {
font-size: 0.85rem;
padding: 0.6rem 0.8rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
font-style: italic;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.scenario-box {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.scenario-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.75rem; }
.scenario-timeline { display: flex; flex-direction: column; gap: 0.4rem; }
.event-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.82rem;
padding: 0.3rem 0;
border-bottom: 1px solid var(--vp-c-divider);
}
.event-item:last-child { border-bottom: none; }
.event-time {
font-family: monospace;
font-size: 0.8rem;
color: var(--vp-c-text-3);
min-width: 40px;
}
.event-role {
padding: 0.1rem 0.4rem;
border-radius: 4px;
color: #fff;
font-size: 0.75rem;
font-weight: 600;
min-width: 45px;
text-align: center;
}
.event-text { color: var(--vp-c-text-1); }
@media (max-width: 768px) {
.org-level { flex-direction: column; align-items: center; }
.event-item { flex-wrap: wrap; }
}
</style>
@@ -0,0 +1,471 @@
<!--
IncidentTimelineDemo.vue
事故响应时间线演示展示从发现到复盘的完整事故响应流程
-->
<template>
<div class="incident-timeline-demo">
<div class="header">
<div class="title">事故响应时间线 (Incident Timeline)</div>
<div class="subtitle">点击各阶段了解每个环节的关键动作</div>
</div>
<div class="timeline">
<div class="timeline-track">
<div
class="timeline-progress"
:style="{ width: progressWidth }"
></div>
</div>
<div class="timeline-nodes">
<div
v-for="(phase, index) in phases"
:key="phase.id"
:class="[
'timeline-node',
{
active: activePhase === phase.id,
completed: completedPhases.includes(phase.id)
}
]"
@click="selectPhase(phase.id)"
>
<div class="node-dot">
<span v-if="completedPhases.includes(phase.id)">&#10003;</span>
<span v-else>{{ index + 1 }}</span>
</div>
<div class="node-label">{{ phase.name }}</div>
<div class="node-time">{{ phase.timeHint }}</div>
</div>
</div>
</div>
<div v-if="currentPhase" class="phase-detail">
<div class="phase-header" :style="{ background: currentPhase.color }">
<span class="phase-icon">{{ currentPhase.icon }}</span>
<span class="phase-name">{{ currentPhase.name }}</span>
<span class="phase-duration">{{ currentPhase.duration }}</span>
</div>
<div class="phase-body">
<div class="phase-desc">{{ currentPhase.description }}</div>
<div class="phase-actions">
<div class="actions-title">关键动作</div>
<div
v-for="(action, i) in currentPhase.actions"
:key="i"
class="action-item"
>
<span class="action-bullet">{{ i + 1 }}</span>
<span>{{ action }}</span>
</div>
</div>
<div class="phase-roles">
<span class="roles-label">参与角色</span>
<span
v-for="role in currentPhase.roles"
:key="role"
class="role-tag"
>
{{ role }}
</span>
</div>
</div>
</div>
<div class="auto-controls">
<button class="play-btn" @click="autoPlay" :disabled="isPlaying">
{{ isPlaying ? '播放中...' : '自动演示完整流程' }}
</button>
<button class="reset-btn" @click="resetAll">重置</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activePhase = ref(null)
const completedPhases = ref([])
const isPlaying = ref(false)
const phases = [
{
id: 'detect',
name: '发现',
timeHint: 'T+0',
icon: '🔍',
color: '#ef4444',
duration: '目标 < 5 分钟',
description:
'通过监控告警、用户反馈或自动化检测发现系统异常。越早发现,损失越小。',
actions: [
'监控系统触发告警(CPU、延迟、错误率等)',
'值班人员收到通知并确认',
'初步判断影响范围',
'在事故频道发出第一条通报'
],
roles: ['值班工程师', '监控系统']
},
{
id: 'triage',
name: '分级',
timeHint: 'T+5min',
icon: '📋',
color: '#f59e0b',
duration: '目标 < 10 分钟',
description:
'快速评估事故严重程度,确定优先级(P0-P4),决定响应规模和升级路径。',
actions: [
'评估用户影响面(多少用户受影响?)',
'确定业务影响(核心功能是否不可用?)',
'分配事故等级(P0/P1/P2/P3/P4',
'根据等级启动对应的响应流程'
],
roles: ['值班工程师', '事故指挥官']
},
{
id: 'mitigate',
name: '止血',
timeHint: 'T+15min',
icon: '🚑',
color: '#3b82f6',
duration: '目标 < 1 小时',
description:
'采取紧急措施恢复服务,优先止血而非根治。回滚、降级、限流都是常见手段。',
actions: [
'回滚最近的变更(代码、配置、基础设施)',
'启用降级方案或备用系统',
'实施限流保护核心链路',
'持续监控恢复进度并通报状态'
],
roles: ['事故指挥官', '运维工程师', '开发工程师']
},
{
id: 'resolve',
name: '解决',
timeHint: 'T+1h',
icon: '🔧',
color: '#22c55e',
duration: '视复杂度而定',
description:
'在服务恢复后,定位根本原因并实施永久修复,确保同类问题不再发生。',
actions: [
'深入分析日志、监控数据定位根因',
'编写并审核修复代码',
'在预发布环境验证修复效果',
'灰度发布修复,确认问题彻底解决'
],
roles: ['开发工程师', '架构师', 'QA 工程师']
},
{
id: 'postmortem',
name: '复盘',
timeHint: 'T+48h',
icon: '📝',
color: '#8b5cf6',
duration: '事故后 48 小时内',
description:
'召开无责复盘会议,分析根因,提炼经验教训,制定改进措施防止再次发生。',
actions: [
'撰写事故复盘报告(时间线、影响、根因)',
'召开复盘会议,全员参与讨论',
'使用"五个为什么"深挖根本原因',
'制定并跟踪改进行动项(Action Items'
],
roles: ['事故指挥官', '全体相关人员', '管理层']
}
]
const currentPhase = computed(() => {
if (!activePhase.value) return null
return phases.find((p) => p.id === activePhase.value)
})
const progressWidth = computed(() => {
if (completedPhases.value.length === 0 && !activePhase.value) return '0%'
const activeIndex = phases.findIndex((p) => p.id === activePhase.value)
if (activeIndex === -1) {
const lastCompleted = completedPhases.value.length
return `${(lastCompleted / phases.length) * 100}%`
}
return `${((activeIndex + 0.5) / phases.length) * 100}%`
})
const selectPhase = (id) => {
activePhase.value = id
}
const autoPlay = async () => {
isPlaying.value = true
completedPhases.value = []
activePhase.value = null
for (let i = 0; i < phases.length; i++) {
activePhase.value = phases[i].id
await new Promise((r) => setTimeout(r, 1800))
completedPhases.value.push(phases[i].id)
}
isPlaying.value = false
}
const resetAll = () => {
activePhase.value = null
completedPhases.value = []
isPlaying.value = false
}
</script>
<style scoped>
.incident-timeline-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.timeline {
position: relative;
margin-bottom: 1.5rem;
}
.timeline-track {
position: absolute;
top: 16px;
left: 5%;
right: 5%;
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
}
.timeline-progress {
height: 100%;
background: var(--vp-c-brand);
border-radius: 2px;
transition: width 0.5s ease;
}
.timeline-nodes {
display: flex;
justify-content: space-between;
position: relative;
}
.timeline-node {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
flex: 1;
transition: all 0.2s;
}
.node-dot {
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--vp-c-bg);
border: 3px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.8rem;
transition: all 0.3s;
z-index: 1;
}
.timeline-node.active .node-dot {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: scale(1.2);
box-shadow: 0 0 0 4px rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.2);
}
.timeline-node.completed .node-dot {
border-color: #22c55e;
background: #22c55e;
color: #fff;
}
.node-label {
margin-top: 0.5rem;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
.node-time {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
}
.phase-detail {
background: var(--vp-c-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 1rem;
border: 1px solid var(--vp-c-divider);
}
.phase-header {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: #fff;
}
.phase-icon {
font-size: 1.3rem;
}
.phase-name {
font-weight: 700;
font-size: 1rem;
flex: 1;
}
.phase-duration {
background: rgba(255, 255, 255, 0.2);
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.8rem;
}
.phase-body {
padding: 1rem;
}
.phase-desc {
font-size: 0.9rem;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
line-height: 1.6;
}
.actions-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.action-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.85rem;
margin-bottom: 0.4rem;
color: var(--vp-c-text-2);
}
.action-bullet {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--vp-c-bg-soft);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
.phase-roles {
margin-top: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.roles-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.role-tag {
padding: 0.15rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.auto-controls {
display: flex;
gap: 0.5rem;
}
.play-btn,
.reset-btn {
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
border: 1px solid var(--vp-c-divider);
transition: all 0.2s;
}
.play-btn {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.play-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
background: var(--vp-c-bg);
}
.reset-btn:hover {
border-color: var(--vp-c-brand);
}
@media (max-width: 768px) {
.timeline-nodes {
flex-direction: column;
gap: 0.75rem;
}
.timeline-track {
display: none;
}
.timeline-node {
flex-direction: row;
gap: 0.75rem;
}
.node-label {
margin-top: 0;
}
.node-time {
margin-top: 0;
margin-left: auto;
}
}
</style>
@@ -0,0 +1,412 @@
<!--
PostmortemDemo.vue
事后复盘演示交互式展示"五个为什么"分析法和复盘报告模板
-->
<template>
<div class="postmortem-demo">
<div class="header">
<div class="title">事后复盘五个为什么 (5 Whys Analysis)</div>
<div class="subtitle">点击"继续追问"层层深入挖掘根本原因</div>
</div>
<div class="case-select">
<button
v-for="c in cases"
:key="c.id"
:class="['case-btn', { active: activeCase === c.id }]"
@click="selectCase(c.id)"
>
{{ c.name }}
</button>
</div>
<div v-if="currentCase" class="whys-chain">
<div
v-for="(why, index) in visibleWhys"
:key="index"
class="why-item"
>
<div class="why-header">
<span class="why-badge">
{{ index === 0 ? '现象' : '第 ' + index + ' 个为什么' }}
</span>
<span class="why-depth">
深度 {{ index }} / {{ currentCase.whys.length - 1 }}
</span>
</div>
<div class="why-question" v-if="index > 0">
为什么{{ currentCase.whys[index - 1].answer }}
</div>
<div class="why-answer">
<span class="answer-icon">{{ index === currentCase.whys.length - 1 && revealedCount >= currentCase.whys.length ? '🎯' : '💡' }}</span>
<span>{{ why.answer }}</span>
</div>
<div
v-if="index < visibleWhys.length - 1"
class="why-arrow"
>
继续追问
</div>
</div>
<div class="why-controls" v-if="revealedCount < currentCase.whys.length">
<button class="ask-btn" @click="revealNext">
继续追问为什么
</button>
</div>
<div v-else class="root-cause-box">
<div class="root-label">根本原因已找到</div>
<div class="root-content">{{ currentCase.rootCause }}</div>
<div class="root-actions">
<div class="actions-label">改进措施</div>
<div
v-for="(action, i) in currentCase.actions"
:key="i"
class="action-item"
>
<span class="action-check">&#10003;</span>
<span>{{ action }}</span>
</div>
</div>
</div>
</div>
<div class="template-box">
<div class="template-title">复盘报告模板</div>
<div class="template-sections">
<div
v-for="(section, i) in templateSections"
:key="i"
class="template-item"
:class="{ expanded: expandedSection === i }"
@click="expandedSection = expandedSection === i ? -1 : i"
>
<div class="template-item-header">
<span class="template-num">{{ i + 1 }}</span>
<span class="template-name">{{ section.name }}</span>
<span class="template-toggle">
{{ expandedSection === i ? '' : '+' }}
</span>
</div>
<div v-if="expandedSection === i" class="template-item-body">
{{ section.desc }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeCase = ref('payment')
const revealedCount = ref(1)
const expandedSection = ref(-1)
const casesData = {
payment: {
id: 'payment',
name: '支付系统宕机',
whys: [
{ answer: '支付系统在高峰期完全不可用,持续 18 分钟' },
{ answer: '数据库连接池被耗尽,所有新请求排队超时' },
{ answer: '一条慢查询占用连接长达 30 秒不释放' },
{ answer: '新上线的对账功能执行了全表扫描,没有使用索引' },
{ answer: '代码审查时没有检查 SQL 执行计划,也没有慢查询测试环节' }
],
rootCause: '研发流程缺陷:代码审查清单中缺少 SQL 性能审查项,CI/CD 流水线中没有慢查询检测环节。',
actions: [
'代码审查清单增加"SQL 执行计划检查"必选项',
'CI 流水线增加慢查询自动检测(阈值 100ms)',
'数据库连接池增加单查询超时限制(5s 强制断开)',
'建立大表变更审批流程'
]
},
deploy: {
id: 'deploy',
name: '部署导致服务中断',
whys: [
{ answer: '新版本部署后,用户登录功能完全失效,持续 25 分钟' },
{ answer: '新版本的认证服务无法连接 Redis 缓存集群' },
{ answer: '部署脚本使用了错误的 Redis 集群地址(指向了测试环境)' },
{ answer: '环境配置是硬编码在部署脚本中的,没有使用配置中心' },
{ answer: '团队没有统一的配置管理规范,每个服务自行管理配置' }
],
rootCause: '基础设施缺陷:缺乏统一的配置管理平台和规范,环境配置散落在各处,容易出错且难以审计。',
actions: [
'引入配置中心(如 Consul/Nacos),统一管理所有环境配置',
'部署流水线增加配置校验步骤(连通性检查)',
'禁止在代码和脚本中硬编码环境地址',
'建立部署前 Checklist,包含配置确认环节'
]
}
}
const cases = [
{ id: 'payment', name: '支付系统宕机' },
{ id: 'deploy', name: '部署导致服务中断' }
]
const currentCase = computed(() => casesData[activeCase.value] || null)
const visibleWhys = computed(() => {
if (!currentCase.value) return []
return currentCase.value.whys.slice(0, revealedCount.value)
})
const selectCase = (id) => {
activeCase.value = id
revealedCount.value = 1
}
const revealNext = () => {
if (currentCase.value && revealedCount.value < currentCase.value.whys.length) {
revealedCount.value++
}
}
const templateSections = [
{ name: '事故概述', desc: '简要描述事故发生的时间、持续时长、影响范围和严重程度。例如:"2024年3月15日 14:02-14:20,支付服务完全不可用,影响约 12 万笔交易。"' },
{ name: '时间线', desc: '按时间顺序记录从发现到解决的每一个关键事件,精确到分钟。包括:告警触发、人员响应、排查过程、修复操作、服务恢复等。' },
{ name: '影响评估', desc: '量化事故影响:受影响用户数、失败请求数、经济损失估算、SLA 影响等。用数据说话,避免模糊描述。' },
{ name: '根因分析', desc: '使用"五个为什么"等方法深入分析根本原因。区分直接原因(触发因素)和根本原因(系统性缺陷)。' },
{ name: '改进措施', desc: '列出具体的改进行动项,每项必须有负责人和截止日期。分为短期(本周)、中期(本月)、长期(本季度)三个层次。' },
{ name: '经验教训', desc: '总结哪些做得好(值得保持)、哪些做得不好(需要改进)、哪些是意外发现(新的风险点)。' }
]
</script>
<style scoped>
.postmortem-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1.5rem; }
.title { font-weight: 700; font-size: 1.1rem; margin-bottom: 0.25rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.case-select {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.case-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.case-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.case-btn.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.whys-chain {
margin-bottom: 1.5rem;
}
.why-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 0.25rem;
}
.why-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.4rem;
}
.why-badge {
font-weight: 700;
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
background: var(--vp-c-brand);
color: #fff;
border-radius: 4px;
}
.why-depth {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.why-question {
font-size: 0.85rem;
color: var(--vp-c-text-2);
font-style: italic;
margin-bottom: 0.3rem;
padding-left: 0.5rem;
border-left: 2px solid var(--vp-c-divider);
}
.why-answer {
display: flex;
align-items: flex-start;
gap: 0.4rem;
font-size: 0.9rem;
line-height: 1.5;
}
.answer-icon { flex-shrink: 0; }
.why-arrow {
text-align: center;
color: var(--vp-c-text-3);
font-size: 0.8rem;
padding: 0.25rem 0;
}
.why-controls {
text-align: center;
margin-top: 0.75rem;
}
.ask-btn {
padding: 0.6rem 1.5rem;
background: var(--vp-c-brand);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.2s;
}
.ask-btn:hover { opacity: 0.9; transform: translateY(-1px); }
.root-cause-box {
background: rgba(34, 197, 94, 0.08);
border: 2px solid #22c55e;
border-radius: 10px;
padding: 1rem;
margin-top: 0.75rem;
}
.root-label {
font-weight: 700;
font-size: 0.95rem;
color: #22c55e;
margin-bottom: 0.5rem;
}
.root-content {
font-size: 0.9rem;
line-height: 1.6;
margin-bottom: 0.75rem;
}
.actions-label {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.4rem;
}
.action-item {
display: flex;
align-items: flex-start;
gap: 0.4rem;
font-size: 0.85rem;
margin-bottom: 0.3rem;
}
.action-check {
color: #22c55e;
font-weight: 700;
flex-shrink: 0;
}
.template-box {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.template-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.template-sections {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.template-item {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
}
.template-item:hover {
border-color: var(--vp-c-brand);
}
.template-item.expanded {
border-color: var(--vp-c-brand);
}
.template-item-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
}
.template-num {
width: 22px; height: 22px; border-radius: 50%;
background: var(--vp-c-bg-soft);
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 700; flex-shrink: 0;
}
.template-name {
flex: 1;
font-weight: 600;
font-size: 0.9rem;
}
.template-toggle {
font-size: 1.1rem;
color: var(--vp-c-text-3);
font-weight: 700;
}
.template-item-body {
padding: 0 0.75rem 0.6rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@media (max-width: 768px) {
.case-select { flex-direction: column; }
.case-btn { width: 100%; }
}
</style>
@@ -0,0 +1,483 @@
<!--
SeverityLevelDemo.vue
事故严重程度分级演示交互式展示 P0-P4 各级别的定义示例和响应要求
-->
<template>
<div class="severity-level-demo">
<div class="header">
<div class="title">事故严重程度分级 (Severity Levels)</div>
<div class="subtitle">点击各级别了解对应的响应要求和真实案例</div>
</div>
<div class="level-tabs">
<button
v-for="level in levels"
:key="level.id"
:class="['level-tab', level.id, { active: activeLevel === level.id }]"
@click="activeLevel = level.id"
>
<span class="tab-badge">{{ level.id.toUpperCase() }}</span>
<span class="tab-name">{{ level.shortName }}</span>
</button>
</div>
<div v-if="current" class="level-detail">
<div class="detail-header" :style="{ background: current.color }">
<div class="detail-level">{{ current.id.toUpperCase() }}</div>
<div class="detail-name">{{ current.name }}</div>
</div>
<div class="detail-body">
<div class="detail-section">
<div class="section-label">定义</div>
<div class="section-content">{{ current.definition }}</div>
</div>
<div class="detail-section">
<div class="section-label">响应时间</div>
<div class="section-content response-time">
{{ current.responseTime }}
</div>
</div>
<div class="detail-section">
<div class="section-label">通知方式</div>
<div class="channels">
<span
v-for="ch in current.channels"
:key="ch"
class="channel-tag"
>
{{ ch }}
</span>
</div>
</div>
<div class="detail-section">
<div class="section-label">真实案例</div>
<div class="examples">
<div
v-for="(ex, i) in current.examples"
:key="i"
class="example-item"
>
{{ ex }}
</div>
</div>
</div>
<div class="detail-section">
<div class="section-label">响应要求</div>
<div class="requirements">
<div
v-for="(req, i) in current.requirements"
:key="i"
class="req-item"
>
<span class="req-check">&#10003;</span>
<span>{{ req }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title">各级别对比一览</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>级别</th>
<th>用户影响</th>
<th>响应时间</th>
<th>值班要求</th>
</tr>
</thead>
<tbody>
<tr
v-for="level in levels"
:key="level.id"
:class="{ highlight: activeLevel === level.id }"
@click="activeLevel = level.id"
>
<td>
<span class="table-badge" :class="level.id">
{{ level.id.toUpperCase() }}
</span>
</td>
<td>{{ level.userImpact }}</td>
<td>{{ level.responseTime }}</td>
<td>{{ level.oncallReq }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeLevel = ref('p0')
const levels = [
{
id: 'p0',
shortName: '致命',
name: '致命事故 (Critical)',
color: '#ef4444',
definition: '核心业务完全不可用,大面积用户受影响,造成严重经济损失或数据丢失风险。',
responseTime: '立即响应,5 分钟内到位',
userImpact: '全部用户',
oncallReq: '全员到位',
channels: ['电话', '短信', '即时通讯', '邮件'],
examples: [
'主数据库宕机,所有读写请求失败',
'支付系统完全不可用,用户无法下单',
'用户数据大规模泄露'
],
requirements: [
'事故指挥官必须在 5 分钟内就位',
'每 15 分钟向管理层通报进展',
'所有相关团队取消休假立即支援',
'事后 24 小时内完成复盘报告'
]
},
{
id: 'p1',
shortName: '严重',
name: '严重事故 (Major)',
color: '#f59e0b',
definition: '核心功能部分受损,大量用户体验降级,但系统未完全不可用。',
responseTime: '15 分钟内响应',
userImpact: '大量用户',
oncallReq: '核心团队',
channels: ['即时通讯', '短信', '邮件'],
examples: [
'搜索功能返回结果严重延迟(>5s)',
'部分地区用户无法登录',
'订单处理队列严重积压'
],
requirements: [
'值班工程师 15 分钟内开始排查',
'每 30 分钟通报一次进展',
'必要时升级为 P0',
'事后 48 小时内完成复盘'
]
},
{
id: 'p2',
shortName: '中等',
name: '中等事故 (Moderate)',
color: '#eab308',
definition: '非核心功能异常,部分用户受影响,不影响主要业务流程。',
responseTime: '1 小时内响应',
userImpact: '部分用户',
oncallReq: '值班工程师',
channels: ['即时通讯', '邮件'],
examples: [
'用户头像加载失败',
'报表导出功能超时',
'非关键页面 CSS 样式错乱'
],
requirements: [
'值班工程师在工作时间内处理',
'当天给出修复方案',
'不需要全员响应',
'在周报中记录'
]
},
{
id: 'p3',
shortName: '轻微',
name: '轻微问题 (Minor)',
color: '#84cc16',
definition: '边缘功能小问题,极少数用户受影响,不影响正常使用。',
responseTime: '当天确认,本周处理',
userImpact: '极少用户',
oncallReq: '正常排期',
channels: ['邮件', '工单系统'],
examples: [
'某个按钮在特定浏览器下对齐偏移',
'日志中出现非关键性警告',
'文案有错别字'
],
requirements: [
'记录到缺陷跟踪系统',
'纳入正常迭代排期',
'不需要紧急响应',
'修复后正常发布'
]
},
{
id: 'p4',
shortName: '建议',
name: '改进建议 (Suggestion)',
color: '#64748b',
definition: '非故障类问题,属于优化建议或技术债务,不影响任何用户。',
responseTime: '按优先级排期',
userImpact: '无直接影响',
oncallReq: '无需值班',
channels: ['工单系统'],
examples: [
'代码中存在可优化的性能瓶颈',
'依赖库版本过旧需要升级',
'监控覆盖率不足需要补充'
],
requirements: [
'记录到技术债务清单',
'季度规划时评估优先级',
'作为团队改进项跟踪',
'无时间压力'
]
}
]
const current = computed(() => {
return levels.find((l) => l.id === activeLevel.value)
})
</script>
<style scoped>
.severity-level-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.level-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.level-tab {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
.level-tab:hover {
border-color: var(--vp-c-text-3);
}
.level-tab.active.p0 { border-color: #ef4444; background: rgba(239,68,68,0.08); }
.level-tab.active.p1 { border-color: #f59e0b; background: rgba(245,158,11,0.08); }
.level-tab.active.p2 { border-color: #eab308; background: rgba(234,179,8,0.08); }
.level-tab.active.p3 { border-color: #84cc16; background: rgba(132,204,22,0.08); }
.level-tab.active.p4 { border-color: #64748b; background: rgba(100,116,139,0.08); }
.tab-badge {
font-weight: 700;
font-size: 0.8rem;
padding: 0.1rem 0.4rem;
border-radius: 4px;
color: #fff;
}
.p0 .tab-badge { background: #ef4444; }
.p1 .tab-badge { background: #f59e0b; }
.p2 .tab-badge { background: #eab308; }
.p3 .tab-badge { background: #84cc16; }
.p4 .tab-badge { background: #64748b; }
.tab-name {
font-weight: 500;
}
.level-detail {
background: var(--vp-c-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: #fff;
}
.detail-level {
font-weight: 800;
font-size: 1.2rem;
}
.detail-name {
font-weight: 600;
font-size: 1rem;
}
.detail-body {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-section {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.section-label {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.section-content {
font-size: 0.9rem;
line-height: 1.6;
}
.response-time {
font-weight: 700;
color: var(--vp-c-brand);
}
.channels {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.channel-tag {
padding: 0.15rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
}
.examples {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.example-item {
font-size: 0.85rem;
padding: 0.3rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
border-left: 3px solid var(--vp-c-divider);
}
.requirements {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.req-item {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.85rem;
}
.req-check {
color: #22c55e;
font-weight: 700;
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.table-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
th {
text-align: left;
padding: 0.5rem;
border-bottom: 2px solid var(--vp-c-divider);
font-weight: 600;
color: var(--vp-c-text-2);
}
td {
padding: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
tr.highlight {
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.06);
}
tr {
cursor: pointer;
transition: background 0.2s;
}
tr:hover {
background: var(--vp-c-bg-soft);
}
.table-badge {
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-weight: 700;
font-size: 0.75rem;
color: #fff;
}
.table-badge.p0 { background: #ef4444; }
.table-badge.p1 { background: #f59e0b; }
.table-badge.p2 { background: #eab308; }
.table-badge.p3 { background: #84cc16; }
.table-badge.p4 { background: #64748b; }
@media (max-width: 768px) {
.level-tabs {
flex-direction: column;
}
.level-tab {
width: 100%;
justify-content: center;
}
}
</style>
@@ -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>
@@ -0,0 +1,333 @@
<template>
<div class="finetuning-pipeline-demo">
<div class="pipeline-header">
<h4>微调流水线演示</h4>
<p class="subtitle">点击每个阶段了解微调的完整流程</p>
</div>
<div class="pipeline-steps">
<div
v-for="(step, index) in steps"
:key="step.id"
class="pipeline-step"
:class="{ active: activeStep === index, completed: index < activeStep }"
@click="setStep(index)"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-label">{{ step.label }}</div>
<div v-if="index < steps.length - 1" class="step-arrow">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</div>
<div class="step-detail" v-if="activeStep >= 0">
<div class="detail-title">
{{ steps[activeStep].icon }} {{ steps[activeStep].label }}
</div>
<p class="detail-desc">{{ steps[activeStep].description }}</p>
<div class="detail-points">
<div v-for="(point, i) in steps[activeStep].points" :key="i" class="point-item">
<span class="point-bullet">{{ i + 1 }}</span>
<span>{{ point }}</span>
</div>
</div>
<div class="detail-example" v-if="steps[activeStep].example">
<div class="example-label">示例</div>
<code>{{ steps[activeStep].example }}</code>
</div>
</div>
<div class="pipeline-controls">
<button class="ctrl-btn" :disabled="activeStep <= 0" @click="prevStep">上一步</button>
<span class="step-indicator">{{ activeStep + 1 }} / {{ steps.length }}</span>
<button class="ctrl-btn primary" :disabled="activeStep >= steps.length - 1" @click="nextStep">下一步</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeStep = ref(0)
const steps = [
{
id: 'base',
icon: '🧠',
label: '选择基座模型',
description: '微调的第一步是选择一个合适的预训练基座模型。基座模型已经在海量数据上学习了通用的语言能力,我们要做的是在此基础上进行"专业化训练"。',
points: [
'根据任务需求选择模型规模(7B、13B、70B 等)',
'考虑开源许可证(Apache 2.0、Llama 许可等)',
'评估模型的基础能力是否匹配目标场景',
'常见选择:Llama、Qwen、Mistral、DeepSeek 等'
],
example: 'model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-7B")'
},
{
id: 'data',
icon: '📊',
label: '准备训练数据',
description: '高质量的训练数据是微调成功的关键。数据的质量远比数量重要——1000 条精心标注的数据,往往胜过 10 万条噪声数据。',
points: [
'收集与目标任务相关的数据样本',
'清洗数据:去重、过滤低质量内容',
'格式化为模型要求的输入格式(如 instruction-response 对)',
'划分训练集、验证集(通常 9:1)'
],
example: '{"instruction": "翻译成英文", "input": "你好世界", "output": "Hello World"}'
},
{
id: 'train',
icon: '⚙️',
label: '执行微调训练',
description: '使用准备好的数据对模型进行训练。现代微调通常采用参数高效方法(如 LoRA),只更新模型的一小部分参数,大幅降低计算成本。',
points: [
'配置训练超参数(学习率、批次大小、训练轮数)',
'选择微调策略(全量微调 / LoRA / QLoRA',
'监控训练损失曲线,防止过拟合',
'通常需要 1-4 个 GPU,训练数小时到数天'
],
example: 'trainer = SFTTrainer(model, train_dataset, peft_config=lora_config)'
},
{
id: 'eval',
icon: '📈',
label: '评估与测试',
description: '训练完成后,需要全面评估模型的表现。不仅要看自动化指标,更要进行人工评测,确保模型在真实场景中表现良好。',
points: [
'在验证集上计算损失和困惑度(Perplexity',
'使用任务特定指标(BLEU、ROUGE、准确率等)',
'人工评测:流畅度、准确性、安全性',
'与基座模型对比,确认微调带来了提升'
],
example: 'eval_results = trainer.evaluate(eval_dataset)'
},
{
id: 'deploy',
icon: '🚀',
label: '部署上线',
description: '将微调好的模型部署到生产环境,对外提供服务。部署前通常需要进行模型优化(量化、蒸馏等)以降低推理成本。',
points: [
'导出模型权重,合并 LoRA 适配器',
'应用量化技术压缩模型体积',
'选择部署方案(API 服务、边缘部署等)',
'配置监控和日志,持续跟踪线上表现'
],
example: 'model.merge_and_unload().save_pretrained("my-finetuned-model")'
}
]
function setStep(index) {
activeStep.value = index
}
function prevStep() {
if (activeStep.value > 0) activeStep.value--
}
function nextStep() {
if (activeStep.value < steps.length - 1) activeStep.value++
}
</script>
<style scoped>
.finetuning-pipeline-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.pipeline-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 20px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.pipeline-steps {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.pipeline-step {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
transition: all 0.2s;
}
.step-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.pipeline-step.active .step-icon {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
transform: scale(1.1);
}
.pipeline-step.completed .step-icon {
border-color: #10b981;
background: #d1fae5;
}
.step-label {
font-size: 12px;
color: var(--vp-c-text-2);
max-width: 64px;
text-align: center;
line-height: 1.3;
}
.pipeline-step.active .step-label {
color: var(--vp-c-brand-1);
font-weight: 600;
}
.step-arrow {
color: var(--vp-c-text-3);
display: flex;
align-items: center;
margin: 0 2px;
}
.step-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
border: 1px solid var(--vp-c-divider);
}
.detail-title {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.detail-desc {
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0 0 12px;
}
.detail-points {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.point-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.point-bullet {
min-width: 20px;
height: 20px;
border-radius: 50%;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
}
.detail-example {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
}
.example-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-example code {
font-size: 12px;
color: var(--vp-c-brand-1);
word-break: break-all;
}
.pipeline-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.ctrl-btn {
padding: 6px 16px;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.ctrl-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.ctrl-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.ctrl-btn.primary {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.ctrl-btn.primary:hover:not(:disabled) {
opacity: 0.9;
}
.step-indicator {
font-size: 13px;
color: var(--vp-c-text-3);
}
</style>
@@ -0,0 +1,512 @@
<template>
<div class="lora-demo">
<div class="demo-header">
<h4>LoRA 低秩适配原理演示</h4>
<p class="subtitle">理解 LoRA 如何用极少参数实现高效微调</p>
</div>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-btn"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>{{ tab.label }}</button>
</div>
<!-- 核心原理 -->
<div v-if="activeTab === 'principle'" class="tab-content">
<div class="matrix-visual">
<div class="matrix-row">
<div class="matrix-box frozen">
<div class="matrix-label">原始权重 W</div>
<div class="matrix-size">{{ matrixSize }}x{{ matrixSize }}</div>
<div class="matrix-grid">
<div v-for="i in 16" :key="i" class="cell frozen-cell"></div>
</div>
<div class="param-count">{{ (matrixSize * matrixSize).toLocaleString() }} 参数</div>
<div class="status-badge frozen-badge">冻结不动</div>
</div>
<div class="plus-sign">+</div>
<div class="matrix-box trainable">
<div class="matrix-label">LoRA 适配器</div>
<div class="lora-decompose">
<div class="small-matrix a-matrix">
<div class="sm-label">A</div>
<div class="sm-size">{{ matrixSize }}x{{ loraRank }}</div>
<div class="sm-grid">
<div v-for="i in 8" :key="i" class="cell a-cell"></div>
</div>
</div>
<div class="multiply-sign">x</div>
<div class="small-matrix b-matrix">
<div class="sm-label">B</div>
<div class="sm-size">{{ loraRank }}x{{ matrixSize }}</div>
<div class="sm-grid">
<div v-for="i in 8" :key="i" class="cell b-cell"></div>
</div>
</div>
</div>
<div class="param-count lora-count">{{ loraParams.toLocaleString() }} 参数</div>
<div class="status-badge train-badge">可训练</div>
</div>
</div>
</div>
<div class="savings-bar">
<div class="savings-label">参数节省比例</div>
<div class="bar-track">
<div class="bar-fill" :style="{ width: savingsPercent + '%' }"></div>
</div>
<div class="savings-value">节省 {{ savingsPercent.toFixed(1) }}% 参数</div>
</div>
<div class="rank-control">
<label>LoRA (Rank): <strong>{{ loraRank }}</strong></label>
<input type="range" min="1" max="64" v-model.number="loraRank" />
<div class="rank-hints">
<span>秩越小 = 参数越少训练越快</span>
<span>秩越大 = 表达力越强效果越好</span>
</div>
</div>
</div>
<!-- 直觉类比 -->
<div v-if="activeTab === 'analogy'" class="tab-content">
<div class="analogy-card">
<div class="analogy-icon">🎨</div>
<div class="analogy-text">
<p><strong>想象你有一幅巨大的油画预训练模型</strong></p>
<p>传统微调就像把整幅画重新画一遍费时费力还可能破坏原作的精髓</p>
<p> LoRA 的做法是<strong>在原画上覆盖一层薄薄的透明贴纸</strong>只在贴纸上做修改原画完好无损贴纸又轻又薄随时可以换</p>
</div>
</div>
<div class="comparison-table">
<div class="comp-row header">
<div class="comp-cell">对比维度</div>
<div class="comp-cell">全量微调</div>
<div class="comp-cell highlight">LoRA 微调</div>
</div>
<div v-for="row in comparisonRows" :key="row.dim" class="comp-row">
<div class="comp-cell dim">{{ row.dim }}</div>
<div class="comp-cell">{{ row.full }}</div>
<div class="comp-cell highlight">{{ row.lora }}</div>
</div>
</div>
</div>
<!-- 实际应用 -->
<div v-if="activeTab === 'usage'" class="tab-content">
<div class="usage-steps">
<div v-for="(step, i) in usageSteps" :key="i" class="usage-step">
<div class="usage-num">{{ i + 1 }}</div>
<div class="usage-body">
<div class="usage-title">{{ step.title }}</div>
<div class="usage-code">
<code>{{ step.code }}</code>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('principle')
const matrixSize = ref(4096)
const loraRank = ref(8)
const tabs = [
{ id: 'principle', label: '核心原理' },
{ id: 'analogy', label: '直觉类比' },
{ id: 'usage', label: '实际应用' }
]
const loraParams = computed(() => {
return matrixSize.value * loraRank.value + loraRank.value * matrixSize.value
})
const fullParams = computed(() => matrixSize.value * matrixSize.value)
const savingsPercent = computed(() => {
return ((1 - loraParams.value / fullParams.value) * 100)
})
const comparisonRows = [
{ dim: '训练参数量', full: '100%(数十亿)', lora: '0.1%~1%(数百万)' },
{ dim: '显存需求', full: '4x A100 80GB', lora: '1x RTX 4090 24GB' },
{ dim: '训练时间', full: '数天~数周', lora: '数小时~1天' },
{ dim: '存储开销', full: '完整模型副本(~14GB', lora: '适配器文件(~几十MB' },
{ dim: '多任务切换', full: '需要多个完整模型', lora: '共享基座 + 切换适配器' },
{ dim: '训练效果', full: '理论上限最高', lora: '接近全量微调(90%+' }
]
const usageSteps = [
{
title: '配置 LoRA 参数',
code: `lora_config = LoraConfig(\n r=8, # 秩\n lora_alpha=16, # 缩放因子\n target_modules=["q_proj", "v_proj"],\n lora_dropout=0.05\n)`
},
{
title: '应用到模型',
code: `model = get_peft_model(base_model, lora_config)\nmodel.print_trainable_parameters()\n# 可训练参数: 4,194,304 / 6,738,415,616 (0.06%)`
},
{
title: '训练完成后合并',
code: `merged_model = model.merge_and_unload()\nmerged_model.save_pretrained("my-model")`
}
]
</script>
<style scoped>
.lora-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 16px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.tab-btn {
padding: 6px 16px;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.tab-btn.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.tab-content {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.matrix-visual {
margin-bottom: 20px;
}
.matrix-row {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.matrix-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
text-align: center;
border: 2px solid var(--vp-c-divider);
min-width: 160px;
}
.matrix-box.frozen {
border-color: #94a3b8;
}
.matrix-box.trainable {
border-color: var(--vp-c-brand-1);
}
.matrix-label {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: var(--vp-c-text-1);
}
.matrix-size {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.matrix-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2px;
margin: 0 auto 8px;
max-width: 80px;
}
.cell {
width: 16px;
height: 16px;
border-radius: 2px;
}
.frozen-cell {
background: #cbd5e1;
}
.param-count {
font-size: 12px;
color: var(--vp-c-text-2);
margin-bottom: 6px;
}
.lora-count {
color: var(--vp-c-brand-1);
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.frozen-badge {
background: #e2e8f0;
color: #64748b;
}
.train-badge {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.plus-sign, .multiply-sign {
font-size: 24px;
font-weight: 700;
color: var(--vp-c-text-3);
}
.lora-decompose {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 8px 0;
}
.small-matrix {
text-align: center;
}
.sm-label {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-brand-1);
}
.sm-size {
font-size: 10px;
color: var(--vp-c-text-3);
}
.sm-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px;
margin: 4px auto;
max-width: 40px;
}
.a-cell {
background: #818cf8;
}
.b-cell {
background: #f472b6;
}
.savings-bar {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 16px;
}
.savings-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 6px;
}
.bar-track {
height: 8px;
background: var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand-1), #10b981);
border-radius: 4px;
transition: width 0.3s;
}
.savings-value {
font-size: 13px;
font-weight: 600;
color: #10b981;
}
.rank-control {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 12px 16px;
}
.rank-control label {
font-size: 13px;
color: var(--vp-c-text-2);
}
.rank-control input[type="range"] {
width: 100%;
margin: 8px 0;
}
.rank-hints {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--vp-c-text-3);
}
.analogy-card {
display: flex;
gap: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid var(--vp-c-brand-1);
}
.analogy-icon {
font-size: 36px;
flex-shrink: 0;
}
.analogy-text p {
margin: 0 0 8px;
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.7;
}
.comparison-table {
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
}
.comp-row {
display: grid;
grid-template-columns: 1fr 1.2fr 1.2fr;
}
.comp-row.header {
background: var(--vp-c-bg-alt);
font-weight: 600;
font-size: 13px;
}
.comp-cell {
padding: 10px 12px;
font-size: 13px;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.comp-cell.dim {
font-weight: 600;
color: var(--vp-c-text-1);
}
.comp-cell.highlight {
background: var(--vp-c-brand-soft);
}
.usage-steps {
display: flex;
flex-direction: column;
gap: 16px;
}
.usage-step {
display: flex;
gap: 12px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
}
.usage-num {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--vp-c-brand-1);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.usage-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.usage-code {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 10px;
}
.usage-code code {
font-size: 12px;
color: var(--vp-c-brand-1);
white-space: pre-wrap;
}
</style>
@@ -0,0 +1,345 @@
<template>
<div class="quantization-demo">
<div class="demo-header">
<h4>模型量化演示</h4>
<p class="subtitle">拖动滑块直观感受不同精度下的模型体积速度与质量变化</p>
</div>
<div class="precision-selector">
<div
v-for="(p, i) in precisions"
:key="p.id"
class="precision-card"
:class="{ active: activePrecision === i }"
@click="activePrecision = i"
>
<div class="prec-badge" :style="{ background: p.color }">{{ p.label }}</div>
<div class="prec-bits">{{ p.bits }} bit</div>
</div>
</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon">💾</div>
<div class="metric-label">模型体积</div>
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: currentPrecision.sizePercent + '%', background: currentPrecision.color }"></div>
</div>
<div class="metric-value">{{ currentPrecision.size }}</div>
</div>
<div class="metric-card">
<div class="metric-icon"></div>
<div class="metric-label">推理速度</div>
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: currentPrecision.speedPercent + '%', background: '#10b981' }"></div>
</div>
<div class="metric-value">{{ currentPrecision.speed }}</div>
</div>
<div class="metric-card">
<div class="metric-icon">🎯</div>
<div class="metric-label">输出质量</div>
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: currentPrecision.qualityPercent + '%', background: '#818cf8' }"></div>
</div>
<div class="metric-value">{{ currentPrecision.quality }}</div>
</div>
<div class="metric-card">
<div class="metric-icon">🖥</div>
<div class="metric-label">显存需求</div>
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: currentPrecision.vramPercent + '%', background: '#f59e0b' }"></div>
</div>
<div class="metric-value">{{ currentPrecision.vram }}</div>
</div>
</div>
<div class="detail-section">
<div class="detail-title">{{ currentPrecision.label }} 详解</div>
<p class="detail-desc">{{ currentPrecision.description }}</p>
<div class="bit-visual">
<div class="bit-label">单个参数存储示意</div>
<div class="bit-row">
<div
v-for="i in currentPrecision.bits"
:key="i"
class="bit-cell"
:style="{ background: currentPrecision.color }"
>{{ i % 2 === 0 ? '1' : '0' }}</div>
</div>
<div class="bit-info">每个参数占用 {{ currentPrecision.bits }} = {{ currentPrecision.bytes }} 字节</div>
</div>
<div class="use-case">
<span class="use-label">适用场景</span>
<span>{{ currentPrecision.useCase }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activePrecision = ref(0)
const precisions = [
{
id: 'fp32',
label: 'FP32',
bits: 32,
bytes: 4,
color: '#ef4444',
size: '~28 GB (7B 模型)',
sizePercent: 100,
speed: '1x (基准)',
speedPercent: 25,
quality: '100% (无损)',
qualityPercent: 100,
vram: '~32 GB',
vramPercent: 100,
description: 'FP32(32位浮点数)是模型训练时的默认精度。每个参数用 32 位存储,精度最高但体积最大。通常只在训练阶段使用,推理时很少直接使用 FP32。',
useCase: '模型训练、科研实验、精度敏感的任务'
},
{
id: 'fp16',
label: 'FP16',
bits: 16,
bytes: 2,
color: '#f59e0b',
size: '~14 GB (7B 模型)',
sizePercent: 50,
speed: '2x',
speedPercent: 50,
quality: '~99.5%',
qualityPercent: 99,
vram: '~16 GB',
vramPercent: 50,
description: 'FP16(16位浮点数)将精度减半,模型体积直接缩小一半。在绝大多数场景下,FP16 的输出质量与 FP32 几乎无差别,是目前最主流的推理精度。',
useCase: '标准推理部署、GPU 服务器、大多数生产环境'
},
{
id: 'int8',
label: 'INT8',
bits: 8,
bytes: 1,
color: '#10b981',
size: '~7 GB (7B 模型)',
sizePercent: 25,
speed: '3-4x',
speedPercent: 75,
quality: '~98%',
qualityPercent: 96,
vram: '~8 GB',
vramPercent: 25,
description: 'INT8(8位整数)量化将浮点数映射为整数,体积仅为 FP32 的四分之一。质量损失很小,但推理速度显著提升。适合在消费级 GPU 上运行大模型。',
useCase: '消费级 GPU 部署(RTX 4090)、成本敏感场景'
},
{
id: 'int4',
label: 'INT4',
bits: 4,
bytes: 0.5,
color: '#818cf8',
size: '~3.5 GB (7B 模型)',
sizePercent: 12.5,
speed: '5-6x',
speedPercent: 90,
quality: '~93-95%',
qualityPercent: 90,
vram: '~4 GB',
vramPercent: 12.5,
description: 'INT4(4位整数)是目前最激进的量化方案。模型体积压缩到 FP32 的八分之一,甚至可以在笔记本电脑上运行 7B 模型。质量有一定损失,但对于大多数应用仍然可用。',
useCase: '笔记本/手机端部署、边缘计算、离线场景'
}
]
const currentPrecision = computed(() => precisions[activePrecision.value])
</script>
<style scoped>
.quantization-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 20px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.precision-selector {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.precision-card {
flex: 1;
min-width: 80px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 12px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.precision-card.active {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.prec-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
color: #fff;
font-size: 12px;
font-weight: 700;
margin-bottom: 4px;
}
.prec-bits {
font-size: 11px;
color: var(--vp-c-text-3);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 20px;
}
@media (max-width: 640px) {
.metrics-grid {
grid-template-columns: 1fr;
}
}
.metric-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 14px;
}
.metric-icon {
font-size: 18px;
margin-bottom: 4px;
}
.metric-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.metric-bar-wrap {
height: 6px;
background: var(--vp-c-divider);
border-radius: 3px;
overflow: hidden;
margin-bottom: 6px;
}
.metric-bar {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease;
}
.metric-value {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.detail-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--vp-c-divider);
}
.detail-title {
font-size: 15px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.detail-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0 0 16px;
}
.bit-visual {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
}
.bit-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.bit-row {
display: flex;
gap: 2px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.bit-cell {
width: 20px;
height: 20px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 10px;
font-weight: 600;
font-family: monospace;
}
.bit-info {
font-size: 12px;
color: var(--vp-c-text-3);
}
.use-case {
font-size: 13px;
color: var(--vp-c-text-2);
padding: 8px 12px;
background: var(--vp-c-brand-soft);
border-radius: 6px;
}
.use-label {
font-weight: 600;
color: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,332 @@
<template>
<div class="model-serving-demo">
<div class="demo-header">
<h4>模型服务架构演示</h4>
<p class="subtitle">点击不同部署方案对比其特点与适用场景</p>
</div>
<div class="serving-options">
<div
v-for="(opt, i) in options"
:key="opt.id"
class="option-card"
:class="{ active: activeOption === i }"
@click="activeOption = i"
>
<div class="opt-icon">{{ opt.icon }}</div>
<div class="opt-name">{{ opt.name }}</div>
<div class="opt-brief">{{ opt.brief }}</div>
</div>
</div>
<div class="option-detail" v-if="currentOption">
<div class="detail-header">
<span class="detail-icon">{{ currentOption.icon }}</span>
<span class="detail-name">{{ currentOption.name }}</span>
</div>
<p class="detail-desc">{{ currentOption.description }}</p>
<div class="arch-flow">
<div class="flow-label">架构流程</div>
<div class="flow-steps">
<div v-for="(node, i) in currentOption.flow" :key="i" class="flow-node">
<div class="node-box">{{ node }}</div>
<div v-if="i < currentOption.flow.length - 1" class="flow-arrow-h"></div>
</div>
</div>
</div>
<div class="specs-grid">
<div v-for="spec in currentOption.specs" :key="spec.label" class="spec-item">
<div class="spec-label">{{ spec.label }}</div>
<div class="spec-value">{{ spec.value }}</div>
<div class="spec-bar-wrap">
<div class="spec-bar" :style="{ width: spec.score + '%', background: spec.color }"></div>
</div>
</div>
</div>
<div class="tools-section">
<div class="tools-label">常用工具</div>
<div class="tools-list">
<span v-for="tool in currentOption.tools" :key="tool" class="tool-tag">{{ tool }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeOption = ref(0)
const options = [
{
id: 'api',
icon: '🌐',
name: 'API 服务',
brief: '最常见的在线部署方式',
description: '将模型封装为 RESTful API 或 gRPC 服务,通过 HTTP 请求调用。适合需要实时响应的在线应用,如聊天机器人、智能客服、内容生成等。是目前最主流的部署方式。',
flow: ['客户端请求', '负载均衡', '推理服务器', 'GPU 推理', '返回结果'],
specs: [
{ label: '响应延迟', value: '100ms - 2s', score: 70, color: '#10b981' },
{ label: '并发能力', value: '高(可水平扩展)', score: 85, color: '#818cf8' },
{ label: '部署成本', value: '中高(需 GPU 服务器)', score: 50, color: '#f59e0b' },
{ label: '运维复杂度', value: '中等', score: 55, color: '#ef4444' }
],
tools: ['vLLM', 'TGI', 'Triton', 'FastAPI', 'Ollama']
},
{
id: 'edge',
icon: '📱',
name: '边缘部署',
brief: '在终端设备上本地运行',
description: '将量化后的模型部署到手机、笔记本、嵌入式设备等终端上,无需网络连接即可运行。适合隐私敏感、离线场景或需要极低延迟的应用。',
flow: ['模型量化', '格式转换', '设备加载', '本地推理', '即时输出'],
specs: [
{ label: '响应延迟', value: '50ms - 5s', score: 60, color: '#10b981' },
{ label: '并发能力', value: '低(单设备)', score: 20, color: '#818cf8' },
{ label: '部署成本', value: '低(无服务器费用)', score: 90, color: '#f59e0b' },
{ label: '运维复杂度', value: '低', score: 85, color: '#ef4444' }
],
tools: ['llama.cpp', 'MLC LLM', 'ONNX Runtime', 'MediaPipe']
},
{
id: 'batch',
icon: '📦',
name: '批量处理',
brief: '离线批量推理大量数据',
description: '将大量请求收集后统一处理,不要求实时响应。适合数据标注、文档摘要、批量翻译等离线任务。通过批处理可以最大化 GPU 利用率,显著降低单条推理成本。',
flow: ['数据队列', '批量收集', 'GPU 批推理', '结果存储', '异步通知'],
specs: [
{ label: '响应延迟', value: '分钟~小时级', score: 20, color: '#10b981' },
{ label: '吞吐量', value: '极高(批处理优化)', score: 95, color: '#818cf8' },
{ label: '部署成本', value: '低(GPU 利用率高)', score: 85, color: '#f59e0b' },
{ label: '运维复杂度', value: '中等', score: 55, color: '#ef4444' }
],
tools: ['Ray Serve', 'Spark', 'Celery', 'AWS Batch']
}
]
const currentOption = computed(() => options[activeOption.value])
</script>
<style scoped>
.model-serving-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 20px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.serving-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
}
@media (max-width: 640px) {
.serving-options {
grid-template-columns: 1fr;
}
}
.option-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.option-card.active {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
}
.opt-icon {
font-size: 28px;
margin-bottom: 6px;
}
.opt-name {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.opt-brief {
font-size: 11px;
color: var(--vp-c-text-3);
}
.option-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.detail-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.detail-icon {
font-size: 22px;
}
.detail-name {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.detail-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0 0 16px;
}
.arch-flow {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 14px;
margin-bottom: 16px;
}
.flow-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.flow-steps {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
justify-content: center;
}
.flow-node {
display: flex;
align-items: center;
gap: 4px;
}
.node-box {
padding: 6px 12px;
border-radius: 6px;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.flow-arrow-h {
color: var(--vp-c-text-3);
font-size: 16px;
}
.specs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
@media (max-width: 640px) {
.specs-grid {
grid-template-columns: 1fr;
}
}
.spec-item {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 10px 12px;
}
.spec-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.spec-value {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 6px;
}
.spec-bar-wrap {
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
overflow: hidden;
}
.spec-bar {
height: 100%;
border-radius: 2px;
transition: width 0.5s ease;
}
.tools-section {
border-top: 1px solid var(--vp-c-divider);
padding-top: 12px;
}
.tools-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.tools-list {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tool-tag {
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
}
</style>
@@ -0,0 +1,369 @@
<template>
<div class="training-data-demo">
<div class="demo-header">
<h4>训练数据格式演示</h4>
<p class="subtitle">切换不同格式了解微调数据的组织方式</p>
</div>
<div class="format-tabs">
<button
v-for="fmt in formats"
:key="fmt.id"
class="fmt-btn"
:class="{ active: activeFormat === fmt.id }"
@click="activeFormat = fmt.id"
>
<span class="fmt-icon">{{ fmt.icon }}</span>
<span>{{ fmt.label }}</span>
</button>
</div>
<div class="format-detail">
<div class="format-info">
<div class="info-title">{{ currentFormat.label }}</div>
<p class="info-desc">{{ currentFormat.description }}</p>
<div class="info-tags">
<span class="tag" v-for="tag in currentFormat.tags" :key="tag">{{ tag }}</span>
</div>
</div>
<div class="data-preview">
<div class="preview-header">
<span class="preview-label">数据样例</span>
<button class="switch-btn" @click="nextExample">
换一条
</button>
</div>
<div class="json-block">
<div v-for="(line, i) in currentExample" :key="i" class="json-line">
<span class="json-key" v-if="line.key">{{ line.key }}</span>
<span class="json-colon" v-if="line.key">: </span>
<span :class="'json-value ' + (line.type || '')">{{ line.value }}</span>
</div>
</div>
</div>
<div class="quality-tips">
<div class="tips-title">数据质量要点</div>
<div class="tips-list">
<div v-for="(tip, i) in currentFormat.tips" :key="i" class="tip-item">
<span class="tip-check"></span>
<span>{{ tip }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeFormat = ref('instruction')
const exampleIndex = ref(0)
const formats = [
{
id: 'instruction',
icon: '📝',
label: '指令跟随',
description: '最常见的微调数据格式。每条数据包含一个指令(instruction)、可选的输入(input)和期望的输出(output)。适合训练通用助手类模型。',
tags: ['通用助手', 'ChatGPT 风格', '最常用'],
tips: [
'指令要清晰明确,避免歧义',
'输出要完整、准确、格式规范',
'覆盖多种任务类型(翻译、摘要、问答等)',
'数据量建议:1,000 ~ 50,000 条'
],
examples: [
[
{ key: '"instruction"', value: '"请将以下中文翻译成英文"', type: 'string' },
{ key: '"input"', value: '"人工智能正在改变世界"', type: 'string' },
{ key: '"output"', value: '"AI is changing the world"', type: 'string' }
],
[
{ key: '"instruction"', value: '"用一句话总结以下段落"', type: 'string' },
{ key: '"input"', value: '"深度学习是机器学习的一个分支..."', type: 'string' },
{ key: '"output"', value: '"深度学习通过多层神经网络自动学习数据特征"', type: 'string' }
],
[
{ key: '"instruction"', value: '"解释什么是 API"', type: 'string' },
{ key: '"input"', value: '""', type: 'string' },
{ key: '"output"', value: '"API 是应用程序编程接口,它定义了..."', type: 'string' }
]
]
},
{
id: 'conversation',
icon: '💬',
label: '多轮对话',
description: '模拟真实的多轮对话场景。每条数据包含一组对话消息,包括系统提示、用户消息和助手回复。适合训练聊天机器人。',
tags: ['聊天机器人', '多轮交互', '上下文理解'],
tips: [
'对话要自然流畅,符合真实交互模式',
'保持角色一致性(系统提示贯穿始终)',
'包含上下文引用和追问场景',
'数据量建议:5,000 ~ 100,000 条对话'
],
examples: [
[
{ key: '"messages"', value: '[', type: 'bracket' },
{ key: ' {"role"', value: '"system", "content": "你是一个编程助手"}', type: 'string' },
{ key: ' {"role"', value: '"user", "content": "Python 怎么读取文件?"}', type: 'string' },
{ key: ' {"role"', value: '"assistant", "content": "使用 open() 函数..."}', type: 'string' },
{ key: '', value: ']', type: 'bracket' }
],
[
{ key: '"messages"', value: '[', type: 'bracket' },
{ key: ' {"role"', value: '"system", "content": "你是一个医疗顾问"}', type: 'string' },
{ key: ' {"role"', value: '"user", "content": "感冒了怎么办?"}', type: 'string' },
{ key: ' {"role"', value: '"assistant", "content": "建议多休息多喝水..."}', type: 'string' },
{ key: ' {"role"', value: '"user", "content": "需要吃药吗?"}', type: 'string' },
{ key: ' {"role"', value: '"assistant", "content": "如果症状较轻..."}', type: 'string' },
{ key: '', value: ']', type: 'bracket' }
]
]
},
{
id: 'classification',
icon: '🏷️',
label: '分类标注',
description: '用于训练文本分类任务。每条数据包含输入文本和对应的类别标签。适合情感分析、意图识别、内容审核等场景。',
tags: ['情感分析', '意图识别', '内容审核'],
tips: [
'类别标签要统一规范,避免拼写差异',
'各类别样本数量尽量均衡',
'包含边界案例和易混淆样本',
'数据量建议:每个类别至少 100 条'
],
examples: [
[
{ key: '"text"', value: '"这家餐厅的菜品非常好吃,服务也很周到"', type: 'string' },
{ key: '"label"', value: '"positive"', type: 'label' }
],
[
{ key: '"text"', value: '"等了一个小时还没上菜,太失望了"', type: 'string' },
{ key: '"label"', value: '"negative"', type: 'label' }
],
[
{ key: '"text"', value: '"餐厅环境一般,价格中等"', type: 'string' },
{ key: '"label"', value: '"neutral"', type: 'label' }
]
]
}
]
const currentFormat = computed(() => {
return formats.find(f => f.id === activeFormat.value)
})
const currentExample = computed(() => {
const examples = currentFormat.value.examples
return examples[exampleIndex.value % examples.length]
})
function nextExample() {
exampleIndex.value++
}
</script>
<style scoped>
.training-data-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 16px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.format-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.fmt-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.fmt-btn.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.fmt-icon {
font-size: 16px;
}
.format-detail {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.format-info {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.info-title {
font-size: 15px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 6px;
}
.info-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0 0 10px;
}
.info-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag {
padding: 2px 10px;
border-radius: 10px;
font-size: 11px;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.data-preview {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid var(--vp-c-divider);
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.preview-label {
font-size: 12px;
color: var(--vp-c-text-3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.switch-btn {
padding: 4px 10px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 12px;
}
.switch-btn:hover {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.json-block {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
font-family: 'Fira Code', monospace;
}
.json-line {
font-size: 12px;
line-height: 1.8;
}
.json-key {
color: #818cf8;
}
.json-colon {
color: var(--vp-c-text-3);
}
.json-value.string {
color: #10b981;
}
.json-value.label {
color: #f59e0b;
font-weight: 600;
}
.json-value.bracket {
color: var(--vp-c-text-3);
}
.quality-tips {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
border-left: 3px solid #10b981;
}
.tips-title {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.tip-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.tip-check {
color: #10b981;
font-weight: 600;
flex-shrink: 0;
}
</style>
@@ -0,0 +1,439 @@
<!--
ChunkingStrategyDemo.vue
文本分块策略交互演示
用途
展示不同的文本分块策略固定大小按句子语义递归
用户可以输入文本并观察不同策略如何切分
交互功能
- 输入自定义文本或使用预设文本
- 切换不同分块策略
- 可视化展示分块结果与边界
-->
<template>
<div class="chunking-demo">
<div class="input-section">
<div class="section-header">
<span class="section-title">输入文本</span>
<button
class="preset-btn"
@click="usePreset"
>
使用示例文本
</button>
</div>
<textarea
v-model="inputText"
class="text-input"
rows="4"
placeholder="请输入要分块的文本,或点击「使用示例文本」..."
/>
</div>
<div class="strategy-selector">
<button
v-for="s in strategies"
:key="s.id"
:class="['strategy-btn', { active: currentStrategy === s.id }]"
@click="currentStrategy = s.id"
>
<span class="strategy-icon">{{ s.icon }}</span>
<span class="strategy-name">{{ s.name }}</span>
</button>
</div>
<div class="strategy-info">
<div class="info-title">{{ activeStrategy.name }}</div>
<div class="info-desc">{{ activeStrategy.desc }}</div>
<div class="info-params">
<span
v-for="(p, i) in activeStrategy.params"
:key="i"
class="param-tag"
>
{{ p }}
</span>
</div>
</div>
<div class="result-section">
<div class="result-header">
分块结果
<span class="chunk-count"> {{ chunks.length }} 个块</span>
</div>
<div class="chunks-container">
<div
v-for="(chunk, i) in chunks"
:key="i"
class="chunk-item"
:style="{ borderLeftColor: chunkColors[i % chunkColors.length] }"
>
<div class="chunk-meta">
<span
class="chunk-index"
:style="{ background: chunkColors[i % chunkColors.length] }"
>
#{{ i + 1 }}
</span>
<span class="chunk-size">{{ chunk.length }} 字符</span>
</div>
<div class="chunk-text">{{ chunk }}</div>
</div>
<div
v-if="chunks.length === 0"
class="empty-hint"
>
请输入文本后查看分块结果
</div>
</div>
</div>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>策略</th>
<th>优点</th>
<th>缺点</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr
v-for="s in strategies"
:key="s.id"
:class="{ highlight: currentStrategy === s.id }"
>
<td class="strategy-cell">{{ s.icon }} {{ s.name }}</td>
<td>{{ s.pros }}</td>
<td>{{ s.cons }}</td>
<td>{{ s.useCase }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const chunkColors = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']
const presetText = '人工智能(AI)是计算机科学的一个分支,致力于创建能够模拟人类智能的系统。机器学习是 AI 的核心方法之一,它让计算机能够从数据中学习规律。深度学习是机器学习的子集,使用多层神经网络来处理复杂任务。自然语言处理(NLP)使计算机能够理解和生成人类语言。大语言模型(LLM)如 GPT 和 Claude 通过海量文本训练,具备了强大的语言理解和生成能力。RAG(检索增强生成)技术通过在生成前检索相关文档,显著提升了 LLM 回答的准确性和时效性。向量数据库是 RAG 系统的关键组件,它能高效存储和检索文本的向量表示。'
const inputText = ref('')
const currentStrategy = ref('fixed')
const strategies = [
{
id: 'fixed',
name: '固定大小',
icon: '📏',
desc: '按照固定的字符数切分文本,是最简单直接的分块方式。通常会设置一定的重叠区域(overlap),避免在切分边界丢失上下文。',
params: ['块大小: 80 字符', '重叠: 20 字符'],
pros: '实现简单,块大小均匀',
cons: '可能在句子中间截断',
useCase: '结构化程度低的长文本'
},
{
id: 'sentence',
name: '按句子',
icon: '📝',
desc: '以句号、问号、感叹号等标点作为分隔符,按完整句子进行切分。保证每个块都是语义完整的句子集合。',
params: ['每块: 2-3 句', '分隔符: 。?!'],
pros: '保持句子完整性',
cons: '块大小不均匀',
useCase: '文章、报告等自然文本'
},
{
id: 'semantic',
name: '语义分块',
icon: '🧠',
desc: '根据文本的语义相似度进行分块。当相邻句子的语义差异超过阈值时,在此处切分。能更好地保持主题的连贯性。',
params: ['相似度阈值: 0.7', '最小块: 50 字符'],
pros: '主题连贯,语义完整',
cons: '计算成本高,需要嵌入模型',
useCase: '多主题混合的复杂文档'
},
{
id: 'recursive',
name: '递归分块',
icon: '🔄',
desc: '使用多级分隔符递归切分:先按段落分,段落太长则按句子分,句子太长则按固定大小分。LangChain 的默认策略。',
params: ['分隔符: \\n\\n → 。→ 固定', '目标: 80 字符'],
pros: '兼顾结构与大小',
cons: '实现较复杂',
useCase: '通用场景,推荐默认选择'
}
]
const activeStrategy = computed(() => strategies.find((s) => s.id === currentStrategy.value))
const chunks = computed(() => {
const text = inputText.value.trim()
if (!text) return []
switch (currentStrategy.value) {
case 'fixed':
return chunkFixed(text, 80, 20)
case 'sentence':
return chunkBySentence(text, 3)
case 'semantic':
return chunkSemantic(text)
case 'recursive':
return chunkRecursive(text, 80)
default:
return []
}
})
function chunkFixed(text, size, overlap) {
const result = []
let start = 0
while (start < text.length) {
result.push(text.slice(start, start + size))
start += size - overlap
}
return result
}
function chunkBySentence(text, perChunk) {
const sentences = text.split(/(?<=[。?!.?!])/).filter((s) => s.trim())
const result = []
for (let i = 0; i < sentences.length; i += perChunk) {
result.push(sentences.slice(i, i + perChunk).join(''))
}
return result
}
function chunkSemantic(text) {
const sentences = text.split(/(?<=[。?!.?!])/).filter((s) => s.trim())
const result = []
let current = ''
const keywords = ['AI', 'LLM', 'RAG', 'NLP', '机器学习', '深度学习', '向量']
let prevKeywords = new Set()
for (const s of sentences) {
const curKeywords = new Set(keywords.filter((k) => s.includes(k)))
const overlap = [...curKeywords].filter((k) => prevKeywords.has(k)).length
const similarity = prevKeywords.size > 0 ? overlap / Math.max(prevKeywords.size, curKeywords.size) : 1
if (current && similarity < 0.5 && current.length > 50) {
result.push(current)
current = s
} else {
current += s
}
prevKeywords = curKeywords
}
if (current) result.push(current)
return result
}
function chunkRecursive(text, target) {
const paragraphs = text.split(/\n\n+/).filter((p) => p.trim())
const result = []
for (const para of paragraphs) {
if (para.length <= target) {
result.push(para)
} else {
const sentences = para.split(/(?<=[。?!.?!])/).filter((s) => s.trim())
let current = ''
for (const s of sentences) {
if ((current + s).length > target && current) {
result.push(current)
current = s
} else {
current += s
}
}
if (current) result.push(current)
}
}
return result
}
function usePreset() {
inputText.value = presetText
}
</script>
<style scoped>
.chunking-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.section-title {
font-weight: 600;
font-size: 14px;
}
.preset-btn {
padding: 4px 12px;
border: 1px solid var(--vp-c-brand-1);
border-radius: 6px;
background: transparent;
color: var(--vp-c-brand-1);
cursor: pointer;
font-size: 12px;
}
.text-input {
width: 100%;
padding: 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 13px;
line-height: 1.6;
resize: vertical;
box-sizing: border-box;
}
.strategy-selector {
display: flex;
gap: 8px;
margin: 16px 0;
flex-wrap: wrap;
}
.strategy-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.strategy-btn.active {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.strategy-icon {
font-size: 16px;
}
.strategy-info {
padding: 14px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
margin-bottom: 16px;
}
.info-title {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-brand-1);
margin-bottom: 6px;
}
.info-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 8px;
}
.info-params {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.param-tag {
padding: 2px 10px;
border-radius: 4px;
background: var(--vp-c-bg-soft);
font-size: 12px;
color: var(--vp-c-text-2);
font-family: monospace;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 14px;
margin-bottom: 10px;
}
.chunk-count {
font-size: 12px;
color: var(--vp-c-text-3);
font-weight: 400;
}
.chunks-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.chunk-item {
padding: 10px 12px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid;
}
.chunk-meta {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.chunk-index {
padding: 1px 8px;
border-radius: 4px;
color: #fff;
font-size: 11px;
font-weight: 600;
}
.chunk-size {
font-size: 11px;
color: var(--vp-c-text-3);
}
.chunk-text {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.5;
word-break: break-all;
}
.empty-hint {
text-align: center;
padding: 20px;
color: var(--vp-c-text-3);
font-size: 13px;
}
.comparison-table {
margin-top: 16px;
overflow-x: auto;
}
.comparison-table table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.comparison-table th,
.comparison-table td {
padding: 8px 10px;
border: 1px solid var(--vp-c-divider);
text-align: left;
}
.comparison-table th {
background: var(--vp-c-bg);
font-weight: 600;
}
.comparison-table tr.highlight {
background: var(--vp-c-brand-soft);
}
.strategy-cell {
white-space: nowrap;
}
</style>
@@ -0,0 +1,417 @@
<!--
RAGArchitectureDemo.vue
RAG 架构演进交互演示
用途
展示三种 RAG 架构Naive RAGAdvanced RAGModular RAG
用户可以切换查看不同架构的流程图和特点
交互功能
- 切换三种架构
- 查看每种架构的流程节点
- 对比各架构的优劣
-->
<template>
<div class="rag-arch-demo">
<div class="arch-tabs">
<button
v-for="(arch, i) in architectures"
:key="i"
:class="['arch-tab', { active: currentArch === i }]"
@click="currentArch = i"
>
<span class="tab-badge">{{ arch.badge }}</span>
<span class="tab-name">{{ arch.name }}</span>
</button>
</div>
<div class="arch-desc">
{{ activeArch.desc }}
</div>
<div class="flow-diagram">
<div
v-for="(node, j) in activeArch.nodes"
:key="j"
class="flow-node-wrapper"
>
<div
:class="['flow-node', node.type]"
@click="selectedNode = selectedNode === j ? null : j"
>
<div class="node-icon">{{ node.icon }}</div>
<div class="node-label">{{ node.label }}</div>
</div>
<div
v-if="j < activeArch.nodes.length - 1"
class="flow-connector"
>
<span class="connector-arrow"></span>
<span
v-if="node.connectorLabel"
class="connector-label"
>{{ node.connectorLabel }}</span>
</div>
</div>
</div>
<div
v-if="selectedNode !== null"
class="node-detail"
>
<div class="node-detail-title">
{{ activeArch.nodes[selectedNode].icon }}
{{ activeArch.nodes[selectedNode].label }}
</div>
<div class="node-detail-desc">
{{ activeArch.nodes[selectedNode].detail }}
</div>
</div>
<div
v-else
class="node-hint"
>
点击流程节点查看详细说明
</div>
<div class="arch-features">
<div class="feature-title">架构特点</div>
<div class="feature-grid">
<div
v-for="(f, i) in activeArch.features"
:key="i"
class="feature-item"
>
<span class="feature-icon">{{ f.icon }}</span>
<span class="feature-text">{{ f.text }}</span>
</div>
</div>
</div>
<div class="evolution-bar">
<div class="evo-title">架构演进路线</div>
<div class="evo-track">
<div
v-for="(arch, i) in architectures"
:key="i"
:class="['evo-node', { active: currentArch >= i }]"
>
<div class="evo-dot" />
<div class="evo-label">{{ arch.name }}</div>
<div class="evo-year">{{ arch.year }}</div>
</div>
<div class="evo-line">
<div
class="evo-line-fill"
:style="{ width: (currentArch / (architectures.length - 1)) * 100 + '%' }"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const currentArch = ref(0)
const selectedNode = ref(null)
watch(currentArch, () => { selectedNode.value = null })
const architectures = [
{
name: 'Naive RAG',
badge: 'v1',
year: '2023',
desc: '最基础的 RAG 架构,流程简单直接:索引 → 检索 → 生成。适合快速原型验证,但在复杂场景下效果有限。',
nodes: [
{ icon: '📄', label: '文档加载', type: 'input', detail: '将原始文档(PDF、网页、数据库等)加载到系统中,进行基本的文本提取和清洗。', connectorLabel: '' },
{ icon: '✂️', label: '文本分块', type: 'process', detail: '将长文档按固定大小切分为较小的文本块(chunk),通常 200-500 个 token。', connectorLabel: '' },
{ icon: '🔢', label: '向量化', type: 'process', detail: '使用嵌入模型将每个文本块转化为向量,存入向量数据库。', connectorLabel: '' },
{ icon: '🔍', label: '检索', type: 'process', detail: '用户提问时,将问题向量化后在向量数据库中搜索最相似的文本块。', connectorLabel: '' },
{ icon: '🤖', label: '生成', type: 'output', detail: '将检索到的文本块与问题拼接为 Prompt,交给 LLM 生成回答。' }
],
features: [
{ icon: '✅', text: '实现简单,上手快' },
{ icon: '✅', text: '适合结构化知识库' },
{ icon: '⚠️', text: '检索质量依赖分块策略' },
{ icon: '❌', text: '无法处理复杂查询' }
]
},
{
name: 'Advanced RAG',
badge: 'v2',
year: '2024',
desc: '在 Naive RAG 基础上增加了查询优化和检索后处理,显著提升检索质量和生成准确性。',
nodes: [
{ icon: '💬', label: '用户查询', type: 'input', detail: '接收用户的原始问题。', connectorLabel: '' },
{ icon: '🔄', label: '查询改写', type: 'enhance', detail: '使用 LLM 对原始查询进行改写、扩展或分解。例如将模糊问题改写为更精确的检索查询,或生成多个子查询。', connectorLabel: '' },
{ icon: '🔍', label: '混合检索', type: 'process', detail: '同时使用向量检索(语义)和关键词检索(BM25),融合两者的结果,兼顾语义理解和精确匹配。', connectorLabel: '' },
{ icon: '📊', label: '重排序', type: 'enhance', detail: '使用交叉编码器对检索结果进行精细排序,过滤掉不相关的文档片段。', connectorLabel: '' },
{ icon: '📋', label: '上下文压缩', type: 'enhance', detail: '从检索到的文档中提取与问题最相关的部分,去除冗余信息,节省上下文窗口。', connectorLabel: '' },
{ icon: '🤖', label: '生成', type: 'output', detail: '基于优化后的上下文生成高质量回答。' }
],
features: [
{ icon: '✅', text: '查询改写提升检索召回率' },
{ icon: '✅', text: '混合检索兼顾语义和关键词' },
{ icon: '✅', text: '重排序显著提升精度' },
{ icon: '⚠️', text: '流程较长,延迟增加' }
]
},
{
name: 'Modular RAG',
badge: 'v3',
year: '2025',
desc: '将 RAG 拆解为可插拔的模块,支持灵活组合和路由。可根据查询类型动态选择最优流程。',
nodes: [
{ icon: '💬', label: '用户查询', type: 'input', detail: '接收用户的原始问题。', connectorLabel: '' },
{ icon: '🧭', label: '路由判断', type: 'enhance', detail: '分析查询意图,决定走哪条处理路径:简单问题直接回答,复杂问题走检索流程,多步问题走分解流程。', connectorLabel: '' },
{ icon: '🔀', label: '查询转换', type: 'enhance', detail: '根据路由结果选择:HyDE(假设文档嵌入)、Step-back(退一步提问)、子问题分解等策略。', connectorLabel: '' },
{ icon: '🔍', label: '自适应检索', type: 'process', detail: '根据查询特征自动选择检索策略:向量检索、图检索、SQL 检索或多路检索融合。', connectorLabel: '' },
{ icon: '🔄', label: '自我反思', type: 'enhance', detail: 'LLM 评估检索结果是否充分,不充分则触发二次检索或调整检索策略(Self-RAG / CRAG)。', connectorLabel: '' },
{ icon: '🤖', label: '生成', type: 'output', detail: '基于充分验证的上下文生成最终回答,并附带置信度评分。' }
],
features: [
{ icon: '✅', text: '模块化设计,灵活可扩展' },
{ icon: '✅', text: '自适应路由,智能选择策略' },
{ icon: '✅', text: '自我反思机制提升可靠性' },
{ icon: '⚠️', text: '系统复杂度高,需要精心调优' }
]
}
]
const activeArch = computed(() => architectures[currentArch.value])
</script>
<style scoped>
.rag-arch-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.arch-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.arch-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.arch-tab.active {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
}
.tab-badge {
padding: 1px 6px;
border-radius: 4px;
background: var(--vp-c-divider);
font-size: 11px;
font-weight: 700;
}
.arch-tab.active .tab-badge {
background: var(--vp-c-brand-1);
color: #fff;
}
.tab-name {
font-weight: 600;
}
.arch-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 16px;
padding: 12px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.flow-diagram {
display: flex;
align-items: center;
gap: 0;
overflow-x: auto;
padding: 12px 0;
margin-bottom: 12px;
}
.flow-node-wrapper {
display: flex;
align-items: center;
flex-shrink: 0;
}
.flow-node {
padding: 10px 14px;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
text-align: center;
cursor: pointer;
transition: all 0.2s;
min-width: 70px;
}
.flow-node:hover {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
}
.flow-node.input {
border-color: #3b82f6;
background: #eff6ff;
}
.flow-node.output {
border-color: #10b981;
background: #ecfdf5;
}
.flow-node.enhance {
border-color: #f59e0b;
background: #fffbeb;
}
.flow-node.process {
border-color: #8b5cf6;
background: #f5f3ff;
}
.node-icon {
font-size: 20px;
margin-bottom: 2px;
}
.node-label {
font-size: 11px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.flow-connector {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 4px;
}
.connector-arrow {
font-size: 16px;
color: var(--vp-c-text-3);
}
.connector-label {
font-size: 10px;
color: var(--vp-c-text-3);
}
.node-detail {
padding: 12px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-brand-1);
margin-bottom: 16px;
}
.node-detail-title {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-brand-1);
margin-bottom: 6px;
}
.node-detail-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.node-hint {
text-align: center;
padding: 12px;
color: var(--vp-c-text-3);
font-size: 13px;
margin-bottom: 16px;
}
.feature-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
margin-bottom: 16px;
}
.feature-item {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 13px;
}
.feature-icon {
flex-shrink: 0;
}
.feature-text {
color: var(--vp-c-text-2);
}
.evolution-bar {
padding: 16px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.evo-title {
font-weight: 600;
font-size: 13px;
margin-bottom: 16px;
text-align: center;
}
.evo-track {
display: flex;
justify-content: space-between;
position: relative;
padding: 0 20px;
}
.evo-node {
text-align: center;
z-index: 1;
opacity: 0.4;
transition: opacity 0.3s;
}
.evo-node.active {
opacity: 1;
}
.evo-dot {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--vp-c-divider);
margin: 0 auto 6px;
transition: background 0.3s;
}
.evo-node.active .evo-dot {
background: var(--vp-c-brand-1);
}
.evo-label {
font-size: 12px;
font-weight: 600;
}
.evo-year {
font-size: 11px;
color: var(--vp-c-text-3);
}
.evo-line {
position: absolute;
top: 6px;
left: 20px;
right: 20px;
height: 2px;
background: var(--vp-c-divider);
}
.evo-line-fill {
height: 100%;
background: var(--vp-c-brand-1);
transition: width 0.5s;
}
</style>
@@ -0,0 +1,349 @@
<!--
RAGPipelineDemo.vue
RAG 完整流程可视化演示
用途
展示 RAG 的核心流程用户提问 检索 上下文组装 LLM 生成 返回结果
用户可以逐步点击观察每个阶段的数据流动
交互功能
- 点击"下一步"逐步推进流程
- 每个阶段高亮并展示说明
- 可选择不同的示例问题
-->
<template>
<div class="rag-pipeline-demo">
<div class="query-selector">
<span class="label">选择问题</span>
<button
v-for="(q, i) in queries"
:key="i"
:class="['query-btn', { active: currentQuery === i }]"
@click="selectQuery(i)"
>
{{ q.short }}
</button>
</div>
<div class="pipeline">
<div
v-for="(stage, i) in stages"
:key="i"
:class="['stage', { active: currentStep >= i, current: currentStep === i }]"
>
<div class="stage-icon">{{ stage.icon }}</div>
<div class="stage-name">{{ stage.name }}</div>
<div
v-if="currentStep >= i"
class="stage-content"
>
{{ getStageContent(i) }}
</div>
<div
v-if="i < stages.length - 1"
:class="['arrow', { active: currentStep > i }]"
>
</div>
</div>
</div>
<div class="detail-panel">
<div class="detail-title">{{ stages[currentStep]?.name }} 详细说明</div>
<div class="detail-desc">{{ stages[currentStep]?.desc }}</div>
<div
v-if="currentStep >= 1 && currentStep <= 2"
class="retrieved-docs"
>
<div class="doc-title">检索到的文档片段</div>
<div
v-for="(doc, i) in queries[currentQuery].docs"
:key="i"
:class="['doc-item', { visible: currentStep >= 2 }]"
>
<span class="doc-score">相关度 {{ doc.score }}</span>
<span class="doc-text">{{ doc.text }}</span>
</div>
</div>
</div>
<div class="controls">
<button
class="ctrl-btn"
:disabled="currentStep <= 0"
@click="prevStep"
>
上一步
</button>
<span class="step-indicator">{{ currentStep + 1 }} / {{ stages.length }}</span>
<button
class="ctrl-btn primary"
:disabled="currentStep >= stages.length - 1"
@click="nextStep"
>
下一步
</button>
<button
class="ctrl-btn"
@click="reset"
>
重置
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const stages = [
{
name: '用户提问',
icon: '💬',
desc: '用户向系统提出一个自然语言问题。这个问题会被转化为向量表示,用于后续的语义检索。'
},
{
name: '语义检索',
icon: '🔍',
desc: '系统将问题编码为向量,在向量数据库中搜索语义最相近的文档片段。通常使用余弦相似度或点积来衡量相关性。'
},
{
name: '上下文组装',
icon: '📋',
desc: '将检索到的 Top-K 文档片段与原始问题拼接,构造成一个完整的 Prompt。这个 Prompt 会告诉 LLM:"请根据以下参考资料回答问题"。'
},
{
name: 'LLM 生成',
icon: '🤖',
desc: '大语言模型接收组装好的 Prompt,基于检索到的上下文信息生成回答。因为有了真实的参考资料,模型的回答更加准确、可靠。'
},
{
name: '返回结果',
icon: '✅',
desc: '系统将 LLM 生成的回答返回给用户。高级系统还会附带引用来源,方便用户验证答案的可靠性。'
}
]
const queries = [
{
short: '公司年假政策',
question: '我们公司的年假政策是什么?',
docs: [
{ score: '0.95', text: '员工入职满一年后享有 10 天带薪年假,满五年后增至 15 天。' },
{ score: '0.87', text: '年假需提前 3 个工作日申请,经直属主管审批后生效。' },
{ score: '0.72', text: '未使用的年假可结转至次年第一季度,逾期作废。' }
],
answer: '根据公司规定,入职满一年享有 10 天带薪年假,满五年增至 15 天。需提前 3 个工作日申请并经主管审批,未用年假可结转至次年 Q1。'
},
{
short: 'API 限流规则',
question: '我们的 API 限流规则是怎样的?',
docs: [
{ score: '0.93', text: '免费用户每分钟限 60 次请求,付费用户限 600 次。' },
{ score: '0.85', text: '超出限流后返回 HTTP 429 状态码,需等待 60 秒后重试。' },
{ score: '0.68', text: '企业版用户可申请自定义限流配额,最高支持每分钟 10000 次。' }
],
answer: '免费用户每分钟限 60 次请求,付费用户 600 次。超限返回 429 状态码,需等 60 秒。企业版可申请最高 10000 次/分钟的自定义配额。'
}
]
const currentQuery = ref(0)
const currentStep = ref(0)
function selectQuery(i) {
currentQuery.value = i
currentStep.value = 0
}
function getStageContent(i) {
const q = queries[currentQuery.value]
if (i === 0) return q.question
if (i === 1) return `找到 ${q.docs.length} 个相关片段`
if (i === 2) return '问题 + 参考资料 → Prompt'
if (i === 3) return '基于上下文生成回答...'
if (i === 4) return q.answer
return ''
}
function nextStep() {
if (currentStep.value < stages.length - 1) currentStep.value++
}
function prevStep() {
if (currentStep.value > 0) currentStep.value--
}
function reset() {
currentStep.value = 0
}
</script>
<style scoped>
.rag-pipeline-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.query-selector {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.query-selector .label {
font-size: 14px;
color: var(--vp-c-text-2);
}
.query-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.query-btn.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.pipeline {
display: flex;
align-items: flex-start;
gap: 4px;
overflow-x: auto;
padding: 12px 0;
}
.stage {
flex: 1;
min-width: 100px;
text-align: center;
padding: 12px 8px;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
opacity: 0.5;
transition: all 0.3s;
position: relative;
}
.stage.active {
opacity: 1;
}
.stage.current {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-1-rgb, 100, 108, 255), 0.15);
}
.stage-icon {
font-size: 24px;
margin-bottom: 4px;
}
.stage-name {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.stage-content {
font-size: 11px;
color: var(--vp-c-text-2);
margin-top: 6px;
line-height: 1.4;
}
.arrow {
position: absolute;
right: -16px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
color: var(--vp-c-divider);
z-index: 1;
transition: color 0.3s;
}
.arrow.active {
color: var(--vp-c-brand-1);
}
.detail-panel {
margin-top: 16px;
padding: 16px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
color: var(--vp-c-brand-1);
}
.detail-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.retrieved-docs {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--vp-c-divider);
}
.doc-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
}
.doc-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
margin-bottom: 4px;
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-size: 12px;
opacity: 0;
transition: opacity 0.3s;
}
.doc-item.visible {
opacity: 1;
}
.doc-score {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
}
.doc-text {
color: var(--vp-c-text-2);
}
.controls {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-top: 16px;
}
.ctrl-btn {
padding: 6px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.ctrl-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.ctrl-btn.primary {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.step-indicator {
font-size: 13px;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,409 @@
<!--
RAGvsFineTuningDemo.vue
RAG vs 微调对比演示
用途
并排对比 RAG 和微调两种方案的优劣势
帮助用户理解何时选择哪种方案
交互功能
- 切换不同维度的对比
- 场景选择器根据需求推荐方案
-->
<template>
<div class="rag-vs-ft-demo">
<div class="toggle-bar">
<button
:class="['toggle-btn', { active: view === 'compare' }]"
@click="view = 'compare'"
>
维度对比
</button>
<button
:class="['toggle-btn', { active: view === 'scenario' }]"
@click="view = 'scenario'"
>
场景推荐
</button>
</div>
<div
v-if="view === 'compare'"
class="compare-view"
>
<div class="compare-header">
<div class="col-label rag-label">RAG 检索增强生成</div>
<div class="col-label vs-label">VS</div>
<div class="col-label ft-label">Fine-tuning 微调</div>
</div>
<div
v-for="(dim, i) in dimensions"
:key="i"
class="compare-row"
>
<div class="dim-name">{{ dim.name }}</div>
<div class="dim-content">
<div class="rag-side">
<div class="score-bar">
<div
class="score-fill rag-fill"
:style="{ width: dim.ragScore + '%' }"
/>
</div>
<div class="side-text">{{ dim.ragText }}</div>
</div>
<div class="dim-icon">{{ dim.icon }}</div>
<div class="ft-side">
<div class="score-bar">
<div
class="score-fill ft-fill"
:style="{ width: dim.ftScore + '%' }"
/>
</div>
<div class="side-text">{{ dim.ftText }}</div>
</div>
</div>
</div>
</div>
<div
v-if="view === 'scenario'"
class="scenario-view"
>
<div class="scenario-question">你的需求是什么</div>
<div class="scenario-grid">
<div
v-for="(s, i) in scenarios"
:key="i"
:class="['scenario-card', { selected: selectedScenario === i }]"
@click="selectedScenario = i"
>
<div class="scenario-icon">{{ s.icon }}</div>
<div class="scenario-name">{{ s.name }}</div>
<div class="scenario-desc">{{ s.desc }}</div>
<div :class="['recommendation', s.recommend]">
{{ s.recommend === 'rag' ? '推荐 RAG' : s.recommend === 'ft' ? '推荐微调' : '两者结合' }}
</div>
</div>
</div>
<div
v-if="selectedScenario !== null"
class="scenario-detail"
>
<div class="detail-title">{{ scenarios[selectedScenario].name }} 详细分析</div>
<div class="detail-reason">{{ scenarios[selectedScenario].reason }}</div>
</div>
</div>
<div class="summary-box">
<div class="summary-title">一句话总结</div>
<div class="summary-text">
RAG 像是给模型配了一个<strong>实时更新的参考书库</strong>适合知识频繁变化的场景
微调像是让模型<strong>上了一门专业课</strong>适合需要特定风格或领域深度的场景
实际项目中两者常常结合使用
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const view = ref('compare')
const selectedScenario = ref(null)
const dimensions = [
{
name: '知识更新速度',
icon: '⚡',
ragScore: 95,
ragText: '实时更新,修改文档即生效',
ftScore: 25,
ftText: '需要重新训练,周期长'
},
{
name: '实施成本',
icon: '💰',
ragScore: 80,
ragText: '搭建检索系统,成本适中',
ftScore: 35,
ftText: '需要 GPU 资源和标注数据'
},
{
name: '回答风格控制',
icon: '🎨',
ragScore: 40,
ragText: '依赖 Prompt 工程',
ftScore: 90,
ftText: '可深度定制输出风格'
},
{
name: '幻觉控制',
icon: '🎯',
ragScore: 85,
ragText: '有据可查,可追溯来源',
ftScore: 50,
ftText: '仍可能产生幻觉'
},
{
name: '推理延迟',
icon: '⏱️',
ragScore: 55,
ragText: '需要额外的检索步骤',
ftScore: 85,
ftText: '直接生成,无额外开销'
},
{
name: '私有数据安全',
icon: '🔒',
ragScore: 90,
ragText: '数据留在本地,不进入模型',
ftScore: 45,
ftText: '数据融入模型权重'
}
]
const scenarios = [
{
icon: '📚',
name: '企业知识库问答',
desc: '内部文档、政策、FAQ 等频繁更新的知识',
recommend: 'rag',
reason: '企业知识库的内容更新频繁,使用 RAG 可以在文档更新后立即生效,无需重新训练。同时数据留在本地,满足企业数据安全要求。'
},
{
icon: '🏥',
name: '医疗报告生成',
desc: '需要严格遵循特定格式和术语的专业文档',
recommend: 'ft',
reason: '医疗报告有严格的格式要求和专业术语规范,微调可以让模型深度学习这些模式,生成更符合行业标准的内容。'
},
{
icon: '💬',
name: '客服对话系统',
desc: '需要准确回答产品问题,同时保持品牌语调',
recommend: 'both',
reason: '客服系统需要 RAG 来检索最新的产品信息和解决方案,同时需要微调来保持一致的品牌语调和对话风格。两者结合效果最佳。'
},
{
icon: '📰',
name: '实时新闻摘要',
desc: '需要基于最新信息生成摘要',
recommend: 'rag',
reason: '新闻内容实时变化,RAG 可以检索最新的新闻源并生成摘要,而微调无法跟上信息更新的速度。'
},
{
icon: '✍️',
name: '特定风格写作',
desc: '模仿特定作者或品牌的写作风格',
recommend: 'ft',
reason: '写作风格是一种内化的模式,通过微调让模型学习大量风格样本,能更自然地模仿目标风格,RAG 难以实现这种深层次的风格迁移。'
},
{
icon: '🔬',
name: '科研文献助手',
desc: '基于海量论文回答学术问题',
recommend: 'rag',
reason: '科研文献数量庞大且持续增长,RAG 可以动态检索相关论文片段,并提供引用来源,便于研究者验证和追溯。'
}
]
</script>
<style scoped>
.rag-vs-ft-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.toggle-bar {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.toggle-btn {
padding: 8px 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.toggle-btn.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.compare-header {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 16px;
}
.col-label {
font-weight: 600;
font-size: 14px;
padding: 6px 16px;
border-radius: 6px;
}
.rag-label {
background: #dbeafe;
color: #2563eb;
}
.vs-label {
color: var(--vp-c-text-3);
font-size: 16px;
}
.ft-label {
background: #fce7f3;
color: #db2777;
}
.compare-row {
margin-bottom: 14px;
padding: 12px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.dim-name {
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
text-align: center;
}
.dim-content {
display: flex;
align-items: center;
gap: 12px;
}
.rag-side,
.ft-side {
flex: 1;
}
.dim-icon {
font-size: 20px;
flex-shrink: 0;
}
.score-bar {
height: 6px;
background: var(--vp-c-bg-soft);
border-radius: 3px;
margin-bottom: 4px;
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease;
}
.rag-fill {
background: #3b82f6;
}
.ft-fill {
background: #ec4899;
}
.side-text {
font-size: 11px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.scenario-question {
font-size: 15px;
font-weight: 600;
margin-bottom: 12px;
text-align: center;
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px;
margin-bottom: 16px;
}
.scenario-card {
padding: 14px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.scenario-card.selected {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 3px rgba(100, 108, 255, 0.15);
}
.scenario-icon {
font-size: 28px;
margin-bottom: 6px;
}
.scenario-name {
font-weight: 600;
font-size: 13px;
margin-bottom: 4px;
}
.scenario-desc {
font-size: 11px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
line-height: 1.4;
}
.recommendation {
display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.recommendation.rag {
background: #dbeafe;
color: #2563eb;
}
.recommendation.ft {
background: #fce7f3;
color: #db2777;
}
.recommendation.both {
background: #f0fdf4;
color: #16a34a;
}
.scenario-detail {
padding: 14px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-brand-1);
margin-bottom: 6px;
}
.detail-reason {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.summary-box {
margin-top: 16px;
padding: 14px;
border-radius: 8px;
background: var(--vp-c-brand-soft);
border: 1px solid var(--vp-c-brand-1);
}
.summary-title {
font-weight: 600;
font-size: 13px;
color: var(--vp-c-brand-1);
margin-bottom: 6px;
}
.summary-text {
font-size: 13px;
color: var(--vp-c-text-1);
line-height: 1.6;
}
</style>
@@ -0,0 +1,549 @@
<!--
RetrievalDemo.vue
检索过程可视化演示
用途
展示 RAG 中的检索流程查询编码 向量搜索 重排序 Top-K 选择
用户可以输入查询观察检索过程
交互功能
- 选择示例查询
- 观察向量相似度计算过程
- 查看重排序效果
-->
<template>
<div class="retrieval-demo">
<div class="query-section">
<span class="label">选择查询</span>
<div class="query-options">
<button
v-for="(q, i) in queries"
:key="i"
:class="['q-btn', { active: currentQuery === i }]"
@click="selectQuery(i)"
>
{{ q.text }}
</button>
</div>
</div>
<div class="process-steps">
<div
v-for="(step, i) in steps"
:key="i"
:class="['step', { active: currentStep >= i, current: currentStep === i }]"
@click="currentStep = i"
>
<div class="step-num">{{ i + 1 }}</div>
<div class="step-name">{{ step.name }}</div>
</div>
</div>
<div class="step-detail">
<div class="step-title">{{ steps[currentStep].name }}</div>
<div class="step-desc">{{ steps[currentStep].desc }}</div>
</div>
<!-- Step 1: Query Embedding -->
<div v-if="currentStep === 0" class="embedding-viz">
<div class="embed-label">查询文本</div>
<div class="embed-text">{{ queries[currentQuery].text }}</div>
<div class="embed-arrow"> 嵌入模型编码</div>
<div class="embed-label">查询向量</div>
<div class="vector-display">
<span
v-for="(v, i) in queries[currentQuery].vector"
:key="i"
class="vector-val"
>{{ v }}</span>
</div>
</div>
<!-- Step 2: Vector Search -->
<div v-if="currentStep === 1" class="search-viz">
<div class="doc-list">
<div
v-for="(doc, i) in activeQuery.candidates"
:key="i"
class="doc-row"
>
<div class="doc-text-col">{{ doc.text }}</div>
<div class="similarity-col">
<div class="sim-bar-bg">
<div
class="sim-bar-fill"
:style="{
width: (doc.similarity * 100) + '%',
background: getSimColor(doc.similarity)
}"
/>
</div>
<span class="sim-value">{{ doc.similarity.toFixed(2) }}</span>
</div>
</div>
</div>
</div>
<!-- Step 3: Re-ranking -->
<div v-if="currentStep === 2" class="rerank-viz">
<div class="rerank-columns">
<div class="rerank-col">
<div class="col-title">初始排序向量相似度</div>
<div
v-for="(doc, i) in sortedBySimilarity"
:key="'init-' + i"
class="rerank-item"
>
<span class="rank-badge">#{{ i + 1 }}</span>
<span class="rerank-text">{{ doc.text }}</span>
<span class="rerank-score">{{ doc.similarity.toFixed(2) }}</span>
</div>
</div>
<div class="rerank-arrow-col"></div>
<div class="rerank-col">
<div class="col-title">重排序后交叉编码器</div>
<div
v-for="(doc, i) in reranked"
:key="'re-' + i"
class="rerank-item"
>
<span class="rank-badge highlight">#{{ i + 1 }}</span>
<span class="rerank-text">{{ doc.text }}</span>
<span class="rerank-score">{{ doc.rerankScore.toFixed(2) }}</span>
</div>
</div>
</div>
</div>
<!-- Step 4: Top-K Selection -->
<div v-if="currentStep === 3" class="topk-viz">
<div class="topk-setting">
<span>Top-K </span>
<button
v-for="k in [1, 2, 3]"
:key="k"
:class="['k-btn', { active: topK === k }]"
@click="topK = k"
>
K = {{ k }}
</button>
</div>
<div class="topk-results">
<div
v-for="(doc, i) in topKResults"
:key="i"
:class="['topk-item', { selected: i < topK }]"
>
<span class="topk-rank">#{{ i + 1 }}</span>
<span class="topk-text">{{ doc.text }}</span>
<span
v-if="i < topK"
class="topk-badge"
>已选中</span>
</div>
</div>
</div>
<div class="nav-controls">
<button
class="nav-btn"
:disabled="currentStep <= 0"
@click="currentStep--"
>
上一步
</button>
<button
class="nav-btn primary"
:disabled="currentStep >= steps.length - 1"
@click="currentStep++"
>
下一步
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentQuery = ref(0)
const currentStep = ref(0)
const topK = ref(2)
const steps = [
{ name: '查询编码', desc: '将用户的自然语言查询通过嵌入模型(如 text-embedding-ada-002)转化为高维向量表示。这个向量捕捉了查询的语义信息。' },
{ name: '向量搜索', desc: '在向量数据库中计算查询向量与所有文档向量的余弦相似度,找出语义最接近的候选文档。' },
{ name: '重排序', desc: '使用交叉编码器(Cross-Encoder)对候选文档进行精细排序。交叉编码器同时考虑查询和文档的交互信息,排序更准确。' },
{ name: 'Top-K 选择', desc: '从重排序后的结果中选取前 K 个最相关的文档片段,作为 LLM 生成回答的上下文。K 值的选择需要平衡准确性和上下文长度。' }
]
const queries = [
{
text: '如何申请年假?',
vector: [0.12, -0.45, 0.78, 0.33, -0.21, 0.56, 0.89, -0.14],
candidates: [
{ text: '员工年假申请需提前 3 个工作日提交审批流程', similarity: 0.94, rerankScore: 0.97 },
{ text: '年假天数根据工龄计算:1-5年10天,5年以上15天', similarity: 0.88, rerankScore: 0.91 },
{ text: '病假需提供医院开具的诊断证明', similarity: 0.62, rerankScore: 0.35 },
{ text: '未使用的年假可折算为工资补偿', similarity: 0.79, rerankScore: 0.82 },
{ text: '公司茶水间提供免费咖啡和零食', similarity: 0.15, rerankScore: 0.05 }
]
},
{
text: 'Redis 缓存穿透怎么解决?',
vector: [0.67, 0.23, -0.89, 0.45, 0.11, -0.34, 0.72, 0.56],
candidates: [
{ text: '缓存穿透可通过布隆过滤器拦截不存在的 key', similarity: 0.96, rerankScore: 0.98 },
{ text: '对空值也进行缓存,设置较短的 TTL', similarity: 0.89, rerankScore: 0.93 },
{ text: '缓存雪崩是指大量 key 同时过期导致数据库压力骤增', similarity: 0.71, rerankScore: 0.42 },
{ text: 'Redis 支持主从复制和哨兵模式实现高可用', similarity: 0.58, rerankScore: 0.28 },
{ text: '接口限流可以使用令牌桶或漏桶算法', similarity: 0.43, rerankScore: 0.15 }
]
}
]
const activeQuery = computed(() => queries[currentQuery.value])
const sortedBySimilarity = computed(() =>
[...activeQuery.value.candidates].sort((a, b) => b.similarity - a.similarity)
)
const reranked = computed(() =>
[...activeQuery.value.candidates].sort((a, b) => b.rerankScore - a.rerankScore)
)
const topKResults = computed(() => reranked.value)
function selectQuery(i) {
currentQuery.value = i
currentStep.value = 0
}
function getSimColor(sim) {
if (sim >= 0.8) return '#10b981'
if (sim >= 0.5) return '#f59e0b'
return '#ef4444'
}
</script>
<style scoped>
.retrieval-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.query-section {
margin-bottom: 16px;
}
.query-section .label {
font-size: 13px;
color: var(--vp-c-text-2);
display: block;
margin-bottom: 8px;
}
.query-options {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.q-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.q-btn.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.process-steps {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.step {
flex: 1;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
opacity: 0.5;
transition: all 0.2s;
}
.step.active { opacity: 1; }
.step.current {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
}
.step-num {
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.step.current .step-num {
background: var(--vp-c-brand-1);
color: #fff;
}
.step-name { font-size: 12px; }
.step-detail {
padding: 12px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
margin-bottom: 16px;
}
.step-title {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-brand-1);
margin-bottom: 4px;
}
.step-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
/* Embedding visualization */
.embedding-viz {
text-align: center;
padding: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
margin-bottom: 16px;
}
.embed-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.embed-text {
font-size: 15px;
font-weight: 600;
color: var(--vp-c-brand-1);
margin-bottom: 8px;
}
.embed-arrow {
font-size: 13px;
color: var(--vp-c-text-2);
margin: 8px 0;
}
.vector-display {
display: flex;
gap: 6px;
justify-content: center;
flex-wrap: wrap;
}
.vector-val {
padding: 3px 8px;
border-radius: 4px;
background: var(--vp-c-bg-soft);
font-family: monospace;
font-size: 12px;
color: var(--vp-c-text-2);
}
/* Search visualization */
.search-viz { margin-bottom: 16px; }
.doc-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.doc-row {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.doc-text-col {
flex: 1;
font-size: 13px;
color: var(--vp-c-text-1);
}
.similarity-col {
display: flex;
align-items: center;
gap: 8px;
min-width: 140px;
}
.sim-bar-bg {
flex: 1;
height: 6px;
background: var(--vp-c-bg-soft);
border-radius: 3px;
overflow: hidden;
}
.sim-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.5s;
}
.sim-value {
font-family: monospace;
font-size: 12px;
color: var(--vp-c-text-2);
min-width: 32px;
}
/* Reranking visualization */
.rerank-viz { margin-bottom: 16px; }
.rerank-columns {
display: flex;
gap: 12px;
align-items: flex-start;
}
.rerank-col { flex: 1; }
.rerank-arrow-col {
display: flex;
align-items: center;
font-size: 24px;
color: var(--vp-c-brand-1);
padding-top: 40px;
}
.col-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
text-align: center;
}
.rerank-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
margin-bottom: 4px;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-size: 12px;
}
.rank-badge {
padding: 1px 6px;
border-radius: 4px;
background: var(--vp-c-divider);
font-size: 11px;
font-weight: 600;
flex-shrink: 0;
}
.rank-badge.highlight {
background: var(--vp-c-brand-1);
color: #fff;
}
.rerank-text {
flex: 1;
color: var(--vp-c-text-2);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rerank-score {
font-family: monospace;
color: var(--vp-c-text-3);
flex-shrink: 0;
}
/* Top-K visualization */
.topk-viz { margin-bottom: 16px; }
.topk-setting {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 13px;
}
.k-btn {
padding: 4px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.k-btn.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.topk-results {
display: flex;
flex-direction: column;
gap: 6px;
}
.topk-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
font-size: 13px;
transition: all 0.3s;
opacity: 0.5;
}
.topk-item.selected {
border-color: var(--vp-c-brand-1);
opacity: 1;
background: var(--vp-c-brand-soft);
}
.topk-rank {
font-weight: 600;
font-size: 12px;
color: var(--vp-c-text-3);
}
.topk-text {
flex: 1;
color: var(--vp-c-text-1);
}
.topk-badge {
padding: 2px 8px;
border-radius: 4px;
background: var(--vp-c-brand-1);
color: #fff;
font-size: 11px;
}
/* Navigation */
.nav-controls {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 16px;
}
.nav-btn {
padding: 6px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.nav-btn.primary {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
</style>
+140
View File
@@ -120,6 +120,7 @@ import AdderChainDemo from './components/appendix/computer-fundamentals/AdderCha
import CompleteAdderDemo from './components/appendix/computer-fundamentals/CompleteAdderDemo.vue'
import FunctionalUnitDemo from './components/appendix/computer-fundamentals/FunctionalUnitDemo.vue'
import CpuArchitectureDemo from './components/appendix/computer-fundamentals/CpuArchitectureDemo.vue'
import MinCpuDemo from './components/appendix/computer-fundamentals/MinCpuDemo.vue'
import RegisterDemo from './components/appendix/computer-fundamentals/RegisterDemo.vue'
import FlipFlopDemo from './components/appendix/computer-fundamentals/FlipFlopDemo.vue'
// import EvolutionFlowDemo from './components/appendix/computer-fundamentals/EvolutionFlowDemo.vue'
@@ -171,6 +172,26 @@ import GraphStructureDemo from './components/appendix/computer-fundamentals/Grap
import LanguageTypeModelDemo from './components/appendix/computer-fundamentals/LanguageTypeModelDemo.vue'
import CompilationPracticeDemo from './components/appendix/computer-fundamentals/CompilationPracticeDemo.vue'
// Vibe Coding Fullstack Components
import DeveloperSkillShiftDemo from './components/appendix/computer-fundamentals/DeveloperSkillShiftDemo.vue'
import ComputerFieldMapDemo from './components/appendix/computer-fundamentals/ComputerFieldMapDemo.vue'
import FrontendTriadDemo from './components/appendix/computer-fundamentals/FrontendTriadDemo.vue'
import FrontendFrameworkDemo from './components/appendix/computer-fundamentals/FrontendFrameworkDemo.vue'
import BackendCoreDemo from './components/appendix/computer-fundamentals/BackendCoreDemo.vue'
import ProgrammingLanguageMapDemo from './components/appendix/computer-fundamentals/ProgrammingLanguageMapDemo.vue'
import LanguageSelectionDemo from './components/appendix/computer-fundamentals/LanguageSelectionDemo.vue'
import FullstackSkillDemo from './components/appendix/computer-fundamentals/FullstackSkillDemo.vue'
import AIvsTraditionalDemo from './components/appendix/computer-fundamentals/AIvsTraditionalDemo.vue'
import CareerPathDemo from './components/appendix/computer-fundamentals/CareerPathDemo.vue'
import LearningStrategyDemo from './components/appendix/computer-fundamentals/LearningStrategyDemo.vue'
import PowerOnDemo from './components/appendix/computer-fundamentals/PowerOnDemo.vue'
import BootProcessDemo from './components/appendix/computer-fundamentals/BootProcessDemo.vue'
import DesktopDemo from './components/appendix/computer-fundamentals/DesktopDemo.vue'
import BrowserArchitectureDemo from './components/appendix/computer-fundamentals/BrowserArchitectureDemo.vue'
import URLRequestDemo from './components/appendix/computer-fundamentals/URLRequestDemo.vue'
import RenderingDemo from './components/appendix/computer-fundamentals/RenderingDemo.vue'
import FullProcessDemo from './components/appendix/computer-fundamentals/FullProcessDemo.vue'
// Data Encoding Components
import GarbledTextDemo from './components/appendix/data-encoding/GarbledTextDemo.vue'
import CharacterEncodingExplorer from './components/appendix/data-encoding/CharacterEncodingExplorer.vue'
@@ -668,6 +689,55 @@ import ABTestingDemo from './components/appendix/data/ABTestingDemo.vue'
import DataAnalysisDemo from './components/appendix/data/DataAnalysisDemo.vue'
import DataTrackingDemo from './components/appendix/data/DataTrackingDemo.vue'
// RAG Components
import RAGPipelineDemo from './components/appendix/rag/RAGPipelineDemo.vue'
import ChunkingStrategyDemo from './components/appendix/rag/ChunkingStrategyDemo.vue'
import RetrievalDemo from './components/appendix/rag/RetrievalDemo.vue'
import RAGArchitectureDemo from './components/appendix/rag/RAGArchitectureDemo.vue'
import RAGvsFineTuningDemo from './components/appendix/rag/RAGvsFineTuningDemo.vue'
// Embedding & Vector Components
import EmbeddingConceptDemo from './components/appendix/embedding-vector/EmbeddingConceptDemo.vue'
import VectorSimilarityDemo from './components/appendix/embedding-vector/VectorSimilarityDemo.vue'
import VectorIndexDemo from './components/appendix/embedding-vector/VectorIndexDemo.vue'
import VectorDatabaseDemo from './components/appendix/embedding-vector/VectorDatabaseDemo.vue'
import EmbeddingPipelineDemo from './components/appendix/embedding-vector/EmbeddingPipelineDemo.vue'
// AI Native App Components
import AINativeArchDemo from './components/appendix/ai-native-app/AINativeArchDemo.vue'
import AIDesignPrincipleDemo from './components/appendix/ai-native-app/AIDesignPrincipleDemo.vue'
import PromptDesignDemo from './components/appendix/ai-native-app/PromptDesignDemo.vue'
import AIUXPatternDemo from './components/appendix/ai-native-app/AIUXPatternDemo.vue'
import AIAppFlowDemo from './components/appendix/ai-native-app/AIAppFlowDemo.vue'
// Infrastructure as Code Components
import IaCConceptDemo from './components/appendix/infrastructure-as-code/IaCConceptDemo.vue'
import TerraformWorkflowDemo from './components/appendix/infrastructure-as-code/TerraformWorkflowDemo.vue'
import IaCToolComparisonDemo from './components/appendix/infrastructure-as-code/IaCToolComparisonDemo.vue'
import ConfigDriftDemo from './components/appendix/infrastructure-as-code/ConfigDriftDemo.vue'
import IaCBestPracticeDemo from './components/appendix/infrastructure-as-code/IaCBestPracticeDemo.vue'
// DNS & HTTPS Components
import DnsResolutionDemo from './components/appendix/dns-https/DnsResolutionDemo.vue'
import DnsRecordTypeDemo from './components/appendix/dns-https/DnsRecordTypeDemo.vue'
import HttpsHandshakeDemo from './components/appendix/dns-https/HttpsHandshakeDemo.vue'
import CertificateChainDemo from './components/appendix/dns-https/CertificateChainDemo.vue'
import DnsHttpsComparisonDemo from './components/appendix/dns-https/DnsHttpsComparisonDemo.vue'
// Model Finetuning Components
import FinetuningPipelineDemo from './components/appendix/model-finetuning/FinetuningPipelineDemo.vue'
import TrainingDataDemo from './components/appendix/model-finetuning/TrainingDataDemo.vue'
import LoRADemo from './components/appendix/model-finetuning/LoRADemo.vue'
import ModelQuantizationDemo from './components/appendix/model-finetuning/ModelQuantizationDemo.vue'
import ModelServingDemo from './components/appendix/model-finetuning/ModelServingDemo.vue'
// Incident Response Components
import SeverityLevelDemo from './components/appendix/incident-response/SeverityLevelDemo.vue'
import IncidentTimelineDemo from './components/appendix/incident-response/IncidentTimelineDemo.vue'
import IncidentCommandDemo from './components/appendix/incident-response/IncidentCommandDemo.vue'
import AlertEscalationDemo from './components/appendix/incident-response/AlertEscalationDemo.vue'
import PostmortemDemo from './components/appendix/incident-response/PostmortemDemo.vue'
export default {
extends: DefaultTheme,
Layout,
@@ -787,6 +857,7 @@ export default {
app.component('CompleteAdderDemo', CompleteAdderDemo)
app.component('FunctionalUnitDemo', FunctionalUnitDemo)
app.component('CpuArchitectureDemo', CpuArchitectureDemo)
app.component('MinCpuDemo', MinCpuDemo)
app.component('RegisterDemo', RegisterDemo)
app.component('FlipFlopDemo', FlipFlopDemo)
// app.component('EvolutionFlowDemo', EvolutionFlowDemo)
@@ -844,6 +915,26 @@ export default {
app.component('LanguageTypeModelDemo', LanguageTypeModelDemo)
app.component('CompilationPracticeDemo', CompilationPracticeDemo)
// Vibe Coding Fullstack Components Registration
app.component('DeveloperSkillShiftDemo', DeveloperSkillShiftDemo)
app.component('ComputerFieldMapDemo', ComputerFieldMapDemo)
app.component('FrontendTriadDemo', FrontendTriadDemo)
app.component('FrontendFrameworkDemo', FrontendFrameworkDemo)
app.component('BackendCoreDemo', BackendCoreDemo)
app.component('ProgrammingLanguageMapDemo', ProgrammingLanguageMapDemo)
app.component('LanguageSelectionDemo', LanguageSelectionDemo)
app.component('FullstackSkillDemo', FullstackSkillDemo)
app.component('AIvsTraditionalDemo', AIvsTraditionalDemo)
app.component('CareerPathDemo', CareerPathDemo)
app.component('LearningStrategyDemo', LearningStrategyDemo)
app.component('PowerOnDemo', PowerOnDemo)
app.component('BootProcessDemo', BootProcessDemo)
app.component('DesktopDemo', DesktopDemo)
app.component('BrowserArchitectureDemo', BrowserArchitectureDemo)
app.component('URLRequestDemo', URLRequestDemo)
app.component('RenderingDemo', RenderingDemo)
app.component('FullProcessDemo', FullProcessDemo)
// Data Encoding Components Registration
app.component('GarbledTextDemo', GarbledTextDemo)
app.component('CharacterEncodingExplorer', CharacterEncodingExplorer)
@@ -1353,6 +1444,55 @@ export default {
app.component('LicenseComparisonDemo', LicenseComparisonDemo)
app.component('TechRadarDemo', TechRadarDemo)
app.component('DecisionMatrixDemo', DecisionMatrixDemo)
// RAG Components Registration
app.component('RAGPipelineDemo', RAGPipelineDemo)
app.component('ChunkingStrategyDemo', ChunkingStrategyDemo)
app.component('RetrievalDemo', RetrievalDemo)
app.component('RAGArchitectureDemo', RAGArchitectureDemo)
app.component('RAGvsFineTuningDemo', RAGvsFineTuningDemo)
// Embedding & Vector Components Registration
app.component('EmbeddingConceptDemo', EmbeddingConceptDemo)
app.component('VectorSimilarityDemo', VectorSimilarityDemo)
app.component('VectorIndexDemo', VectorIndexDemo)
app.component('VectorDatabaseDemo', VectorDatabaseDemo)
app.component('EmbeddingPipelineDemo', EmbeddingPipelineDemo)
// AI Native App Components Registration
app.component('AINativeArchDemo', AINativeArchDemo)
app.component('AIDesignPrincipleDemo', AIDesignPrincipleDemo)
app.component('PromptDesignDemo', PromptDesignDemo)
app.component('AIUXPatternDemo', AIUXPatternDemo)
app.component('AIAppFlowDemo', AIAppFlowDemo)
// Infrastructure as Code Components Registration
app.component('IaCConceptDemo', IaCConceptDemo)
app.component('TerraformWorkflowDemo', TerraformWorkflowDemo)
app.component('IaCToolComparisonDemo', IaCToolComparisonDemo)
app.component('ConfigDriftDemo', ConfigDriftDemo)
app.component('IaCBestPracticeDemo', IaCBestPracticeDemo)
// DNS & HTTPS Components Registration
app.component('DnsResolutionDemo', DnsResolutionDemo)
app.component('DnsRecordTypeDemo', DnsRecordTypeDemo)
app.component('HttpsHandshakeDemo', HttpsHandshakeDemo)
app.component('CertificateChainDemo', CertificateChainDemo)
app.component('DnsHttpsComparisonDemo', DnsHttpsComparisonDemo)
// Model Finetuning Components Registration
app.component('FinetuningPipelineDemo', FinetuningPipelineDemo)
app.component('TrainingDataDemo', TrainingDataDemo)
app.component('LoRADemo', LoRADemo)
app.component('ModelQuantizationDemo', ModelQuantizationDemo)
app.component('ModelServingDemo', ModelServingDemo)
// Incident Response Components Registration
app.component('SeverityLevelDemo', SeverityLevelDemo)
app.component('IncidentTimelineDemo', IncidentTimelineDemo)
app.component('IncidentCommandDemo', IncidentCommandDemo)
app.component('AlertEscalationDemo', AlertEscalationDemo)
app.component('PostmortemDemo', PostmortemDemo)
},
setup() {
const route = useRoute()