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:
@@ -573,6 +573,14 @@ export default defineConfig({
|
||||
text: '一、计算机是怎么回事',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: 'Vibe Coding 时代下的全栈开发',
|
||||
link: '/zh-cn/appendix/1-computer-fundamentals/vibe-coding-fullstack'
|
||||
},
|
||||
{
|
||||
text: '从按下电源到访问网站发生了什么',
|
||||
link: '/zh-cn/appendix/1-computer-fundamentals/power-on-to-web'
|
||||
},
|
||||
{
|
||||
text: '从晶体管到 CPU',
|
||||
link: '/zh-cn/appendix/1-computer-fundamentals/transistor-to-cpu'
|
||||
|
||||
@@ -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 思考时就能看到部分结果,大幅降低感知等待时间。技术上通过 SSE(Server-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>
|
||||
+373
@@ -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>
|
||||
+130
@@ -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>
|
||||
+164
@@ -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>
|
||||
+750
-302
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
+162
@@ -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>
|
||||
+190
@@ -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>
|
||||
+158
@@ -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>
|
||||
+134
@@ -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>
|
||||
+128
@@ -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>
|
||||
+134
@@ -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>
|
||||
+170
@@ -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">💡</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">✓</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 }">↓</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: '🚀', title: '快速原型', recommend: 'Chroma / Pinecone' },
|
||||
{ icon: '🏢', title: '企业级部署', recommend: 'Milvus / Weaviate' },
|
||||
{ icon: '💾', title: '已有 PG 数据库', recommend: 'pgvector' },
|
||||
{ icon: '🤖', 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>小数据集 (<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 (完全重合) ~ ∞ (无穷远)</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θ</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</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)">✓</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">✓</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">✓</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>
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="iac-best-practice-demo">
|
||||
<div class="demo-label">交互演示 ── IaC 最佳实践</div>
|
||||
|
||||
<div class="practice-tabs">
|
||||
<button
|
||||
v-for="(tab, i) in practices"
|
||||
:key="tab.key"
|
||||
:class="['practice-tab', { active: activeTab === i }]"
|
||||
@click="activeTab = i"
|
||||
>
|
||||
<span class="tab-icon">{{ tab.icon }}</span>
|
||||
<span class="tab-name">{{ tab.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="activeTab" class="practice-content">
|
||||
<div class="practice-header">
|
||||
<span class="practice-icon">{{ currentPractice.icon }}</span>
|
||||
<div>
|
||||
<div class="practice-title">{{ currentPractice.title }}</div>
|
||||
<div class="practice-subtitle">{{ currentPractice.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="do-dont-grid">
|
||||
<div class="do-card">
|
||||
<div class="card-label good-label">✅ 推荐做法</div>
|
||||
<div class="card-items">
|
||||
<div v-for="(item, i) in currentPractice.dos" :key="i" class="card-item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dont-card">
|
||||
<div class="card-label bad-label">❌ 反面模式</div>
|
||||
<div class="card-items">
|
||||
<div v-for="(item, i) in currentPractice.donts" :key="i" class="card-item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentPractice.code" class="code-example">
|
||||
<div class="code-header">
|
||||
<span>{{ currentPractice.codeTitle }}</span>
|
||||
</div>
|
||||
<pre class="code-body"><code>{{ currentPractice.code }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="maturity-bar">
|
||||
<div class="maturity-label">实践成熟度</div>
|
||||
<div class="maturity-track">
|
||||
<div
|
||||
v-for="(level, i) in maturityLevels"
|
||||
:key="i"
|
||||
:class="['maturity-segment', { filled: i <= currentPractice.maturity }]"
|
||||
>
|
||||
<span class="maturity-text">{{ level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeTab = ref(0)
|
||||
const maturityLevels = ['入门', '基础', '进阶', '成熟', '卓越']
|
||||
|
||||
const practices = [
|
||||
{
|
||||
key: 'vcs', icon: '📂', name: '版本控制',
|
||||
title: '实践一:基础设施代码纳入版本控制',
|
||||
subtitle: '像管理应用代码一样管理基础设施代码',
|
||||
dos: [
|
||||
'所有 .tf 文件提交到 Git 仓库',
|
||||
'使用分支策略(main / dev / feature)',
|
||||
'通过 Pull Request 进行代码审查',
|
||||
'在 CI 中自动运行 terraform plan'
|
||||
],
|
||||
donts: [
|
||||
'在本地执行 apply 后不提交代码',
|
||||
'直接在 main 分支上修改',
|
||||
'将 .tfstate 文件提交到 Git',
|
||||
'跳过 Code Review 直接部署'
|
||||
],
|
||||
codeTitle: '.gitignore 示例',
|
||||
code: `# 忽略本地状态文件
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
.terraform/
|
||||
|
||||
# 忽略敏感变量文件
|
||||
*.tfvars
|
||||
!example.tfvars`,
|
||||
maturity: 1
|
||||
},
|
||||
{
|
||||
key: 'modules', icon: '🧩', name: '模块化',
|
||||
title: '实践二:使用模块实现代码复用',
|
||||
subtitle: '避免复制粘贴,通过模块封装通用基础设施模式',
|
||||
dos: [
|
||||
'将通用模式抽取为可复用模块',
|
||||
'模块使用语义化版本号',
|
||||
'为模块编写 README 和使用示例',
|
||||
'通过 variables 暴露可配置参数'
|
||||
],
|
||||
donts: [
|
||||
'在多个项目中复制粘贴相同代码',
|
||||
'创建过于庞大的"万能"模块',
|
||||
'模块内硬编码环境特定的值',
|
||||
'不写文档直接发布模块'
|
||||
],
|
||||
codeTitle: '模块调用示例',
|
||||
code: `module "web_server" {
|
||||
source = "./modules/ec2-instance"
|
||||
version = "2.1.0"
|
||||
|
||||
instance_type = "t3.micro"
|
||||
environment = "production"
|
||||
app_name = "my-web-app"
|
||||
}`,
|
||||
maturity: 2
|
||||
},
|
||||
{
|
||||
key: 'state', icon: '💾', name: '状态管理',
|
||||
title: '实践三:远程状态存储与锁定',
|
||||
subtitle: 'State 文件是 IaC 的核心,必须安全可靠地管理',
|
||||
dos: [
|
||||
'使用远程后端(S3 + DynamoDB)',
|
||||
'启用状态文件加密',
|
||||
'配置状态锁防止并发冲突',
|
||||
'按环境/项目隔离状态文件'
|
||||
],
|
||||
donts: [
|
||||
'将 State 存储在本地文件系统',
|
||||
'多人共享同一个 State 无锁机制',
|
||||
'手动编辑 terraform.tfstate',
|
||||
'所有环境共用一个 State 文件'
|
||||
],
|
||||
codeTitle: '远程后端配置',
|
||||
code: `terraform {
|
||||
backend "s3" {
|
||||
bucket = "my-tf-state"
|
||||
key = "prod/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
encrypt = true
|
||||
dynamodb_table = "tf-lock"
|
||||
}
|
||||
}`,
|
||||
maturity: 2
|
||||
},
|
||||
{
|
||||
key: 'env', icon: '🌍', name: '环境管理',
|
||||
title: '实践四:多环境一致性管理',
|
||||
subtitle: '开发、测试、生产环境使用相同代码,仅参数不同',
|
||||
dos: [
|
||||
'使用 Workspace 或目录隔离环境',
|
||||
'通过 .tfvars 文件区分环境参数',
|
||||
'保持环境间代码结构完全一致',
|
||||
'先在 dev 验证,再推广到 prod'
|
||||
],
|
||||
donts: [
|
||||
'为每个环境维护独立的代码副本',
|
||||
'在代码中硬编码环境名称',
|
||||
'跳过测试环境直接部署生产',
|
||||
'不同环境使用不同的模块版本'
|
||||
],
|
||||
codeTitle: '多环境目录结构',
|
||||
code: `environments/
|
||||
├── dev/
|
||||
│ ├── main.tf # 引用相同模块
|
||||
│ └── dev.tfvars # 开发环境参数
|
||||
├── staging/
|
||||
│ ├── main.tf
|
||||
│ └── staging.tfvars
|
||||
└── prod/
|
||||
├── main.tf
|
||||
└── prod.tfvars`,
|
||||
maturity: 3
|
||||
}
|
||||
]
|
||||
|
||||
const currentPractice = computed(() => practices[activeTab.value])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iac-best-practice-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-label {
|
||||
font-size: 0.78rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.practice-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.practice-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.practice-tab.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.practice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.practice-icon { font-size: 1.5rem; }
|
||||
.practice-title { font-weight: 600; font-size: 0.95rem; }
|
||||
.practice-subtitle { font-size: 0.78rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.do-dont-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.do-dont-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.do-card, .dont-card {
|
||||
border-radius: 6px;
|
||||
padding: 0.7rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.do-card { background: #d1fae508; border-color: #6ee7b740; }
|
||||
.dont-card { background: #fee2e208; border-color: #fca5a540; }
|
||||
.card-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.good-label { color: #10b981; }
|
||||
.bad-label { color: #ef4444; }
|
||||
.card-items { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.card-item {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
padding-left: 0.5rem;
|
||||
border-left: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.code-example {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.code-header {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 4px 10px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.code-body {
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
padding: 0.8rem;
|
||||
font-size: 0.73rem;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.maturity-bar { margin-top: 0.5rem; }
|
||||
.maturity-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.maturity-track {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
.maturity-segment {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-text-3);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.maturity-segment.filled {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div class="iac-concept-demo">
|
||||
<div class="demo-label">交互演示 ── 手动运维 vs 基础设施即代码</div>
|
||||
|
||||
<div class="toggle-bar">
|
||||
<button
|
||||
v-for="mode in modes"
|
||||
:key="mode.key"
|
||||
:class="['toggle-btn', { active: current === mode.key }]"
|
||||
@click="current = mode.key"
|
||||
>
|
||||
{{ mode.icon }} {{ mode.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scene-container">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="current === 'manual'" key="manual" class="scene manual-scene">
|
||||
<div class="scene-title">手动运维流程</div>
|
||||
<div class="steps">
|
||||
<div
|
||||
v-for="(step, i) in manualSteps"
|
||||
:key="i"
|
||||
:class="['step-card', { done: manualProgress > i, current: manualProgress === i }]"
|
||||
>
|
||||
<div class="step-num">{{ i + 1 }}</div>
|
||||
<div class="step-icon">{{ step.icon }}</div>
|
||||
<div class="step-text">{{ step.text }}</div>
|
||||
<div class="step-risk">{{ step.risk }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-btn manual-btn" @click="advanceManual" :disabled="manualProgress >= manualSteps.length">
|
||||
{{ manualProgress >= manualSteps.length ? '全部完成(耗时约 2 小时)' : '点击控制台按钮...' }}
|
||||
</button>
|
||||
<div v-if="manualProgress >= manualSteps.length" class="result-box warning">
|
||||
手动操作完成,但存在风险:步骤不可重复、无法审计、容易遗漏配置。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else key="iac" class="scene iac-scene">
|
||||
<div class="scene-title">IaC 代码驱动流程</div>
|
||||
<div class="code-block">
|
||||
<div class="code-header">main.tf</div>
|
||||
<pre class="code-content"><code>{{ iacCode }}</code></pre>
|
||||
</div>
|
||||
<div class="iac-steps">
|
||||
<div
|
||||
v-for="(step, i) in iacSteps"
|
||||
:key="i"
|
||||
:class="['iac-step', { done: iacProgress > i, current: iacProgress === i }]"
|
||||
>
|
||||
<span class="iac-arrow" v-if="i > 0">→</span>
|
||||
<span class="iac-badge">{{ step.icon }}</span>
|
||||
<span class="iac-label">{{ step.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-btn iac-btn" @click="advanceIac" :disabled="iacProgress >= iacSteps.length">
|
||||
{{ iacProgress >= iacSteps.length ? '全部完成(耗时约 30 秒)' : '执行下一步' }}
|
||||
</button>
|
||||
<div v-if="iacProgress >= iacSteps.length" class="result-box success">
|
||||
代码即文档,可重复、可审计、可版本控制,团队协作无忧。
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>对比维度</th>
|
||||
<th>手动运维</th>
|
||||
<th>基础设施即代码</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in comparisonRows" :key="row.dim">
|
||||
<td class="dim-cell">{{ row.dim }}</td>
|
||||
<td class="bad-cell">{{ row.manual }}</td>
|
||||
<td class="good-cell">{{ row.iac }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const current = ref('manual')
|
||||
const manualProgress = ref(0)
|
||||
const iacProgress = ref(0)
|
||||
|
||||
const modes = [
|
||||
{ key: 'manual', icon: '🖱️', label: '手动运维' },
|
||||
{ key: 'iac', icon: '📝', label: '基础设施即代码' }
|
||||
]
|
||||
|
||||
const manualSteps = [
|
||||
{ icon: '🌐', text: '登录云控制台', risk: '需要记住密码' },
|
||||
{ icon: '🖥️', text: '手动创建服务器', risk: '配置可能遗漏' },
|
||||
{ icon: '🔧', text: '配置安全组规则', risk: '容易开放过多端口' },
|
||||
{ icon: '💾', text: '挂载存储卷', risk: '大小可能选错' },
|
||||
{ icon: '🔗', text: '配置负载均衡', risk: '路由规则易出错' },
|
||||
{ icon: '📋', text: '手动记录到文档', risk: '文档很快过时' }
|
||||
]
|
||||
|
||||
const iacSteps = [
|
||||
{ icon: '📝', text: 'Write(编写代码)' },
|
||||
{ icon: '🔍', text: 'Plan(预览变更)' },
|
||||
{ icon: '🚀', text: 'Apply(自动执行)' },
|
||||
{ icon: '✅', text: 'Done(状态记录)' }
|
||||
]
|
||||
|
||||
const iacCode = `resource "aws_instance" "web" {
|
||||
ami = "ami-0c55b159"
|
||||
instance_type = "t3.micro"
|
||||
|
||||
tags = {
|
||||
Name = "web-server"
|
||||
Env = "production"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_security_group" "web_sg" {
|
||||
ingress {
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
}`
|
||||
|
||||
const comparisonRows = [
|
||||
{ dim: '可重复性', manual: '每次操作可能不同', iac: '代码保证完全一致' },
|
||||
{ dim: '速度', manual: '分钟到小时级', iac: '秒到分钟级' },
|
||||
{ dim: '审计追踪', manual: '依赖人工记录', iac: 'Git 历史自动记录' },
|
||||
{ dim: '协作', manual: '口头传达、截图', iac: 'Code Review、PR 流程' },
|
||||
{ dim: '回滚', manual: '几乎不可能', iac: 'git revert 一键回滚' }
|
||||
]
|
||||
|
||||
function advanceManual() {
|
||||
if (manualProgress.value < manualSteps.length) {
|
||||
manualProgress.value++
|
||||
}
|
||||
}
|
||||
|
||||
function advanceIac() {
|
||||
if (iacProgress.value < iacSteps.length) {
|
||||
iacProgress.value++
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iac-concept-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-label {
|
||||
font-size: 0.78rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.toggle-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.toggle-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.toggle-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.scene-container { min-height: 200px; }
|
||||
.scene-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
.steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.step-card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
background: var(--vp-c-bg);
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.step-card.done { opacity: 1; border-color: #f59e0b; background: #fef3c710; }
|
||||
.step-card.current { opacity: 1; border-color: var(--vp-c-brand); box-shadow: 0 0 0 2px var(--vp-c-brand-light); }
|
||||
.step-num { font-size: 0.65rem; color: var(--vp-c-text-3); }
|
||||
.step-icon { font-size: 1.4rem; margin: 4px 0; }
|
||||
.step-text { font-size: 0.75rem; font-weight: 600; }
|
||||
.step-risk { font-size: 0.65rem; color: #ef4444; margin-top: 2px; }
|
||||
.action-btn {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.action-btn:disabled { opacity: 0.6; cursor: default; }
|
||||
.manual-btn { background: #fbbf24; color: #78350f; }
|
||||
.iac-btn { background: var(--vp-c-brand); color: #fff; }
|
||||
.code-block {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.code-header {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 4px 10px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.code-content {
|
||||
padding: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
.iac-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.iac-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.78rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
opacity: 0.4;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.iac-step.done { opacity: 1; border-color: #10b981; background: #d1fae510; }
|
||||
.iac-step.current { opacity: 1; border-color: var(--vp-c-brand); box-shadow: 0 0 0 2px var(--vp-c-brand-light); }
|
||||
.iac-arrow { color: var(--vp-c-text-3); font-size: 0.8rem; }
|
||||
.result-box {
|
||||
margin-top: 0.8rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
.result-box.warning { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
.result-box.success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
:root.dark .result-box.warning { background: #451a0320; color: #fcd34d; }
|
||||
:root.dark .result-box.success { background: #022c2220; color: #6ee7b7; }
|
||||
.comparison-table { margin-top: 1rem; overflow-x: auto; }
|
||||
.comparison-table table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
|
||||
.comparison-table th, .comparison-table td {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
text-align: center;
|
||||
}
|
||||
.comparison-table th { background: var(--vp-c-bg-alt); font-weight: 600; }
|
||||
.dim-cell { font-weight: 600; }
|
||||
.bad-cell { color: #ef4444; }
|
||||
.good-cell { color: #10b981; }
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
<template>
|
||||
<div class="iac-tool-comparison-demo">
|
||||
<div class="demo-label">交互演示 ── 主流 IaC 工具对比</div>
|
||||
|
||||
<div class="tool-selector">
|
||||
<span class="selector-hint">选择要对比的工具(至少选 2 个):</span>
|
||||
<div class="tool-chips">
|
||||
<button
|
||||
v-for="tool in tools"
|
||||
:key="tool.name"
|
||||
:class="['tool-chip', { selected: selectedTools.includes(tool.name) }]"
|
||||
:style="selectedTools.includes(tool.name) ? { background: tool.color, borderColor: tool.color, color: '#fff' } : {}"
|
||||
@click="toggleTool(tool.name)"
|
||||
>
|
||||
{{ tool.icon }} {{ tool.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedTools.length >= 2" class="comparison-grid">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="feature-col">特性</th>
|
||||
<th v-for="name in selectedTools" :key="name" class="tool-col">
|
||||
<span class="tool-header-icon">{{ getToolByName(name).icon }}</span>
|
||||
<span>{{ name }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="feature in features" :key="feature.key">
|
||||
<td class="feature-cell">{{ feature.label }}</td>
|
||||
<td v-for="name in selectedTools" :key="name" class="value-cell">
|
||||
<span :class="getCellClass(name, feature.key)">
|
||||
{{ getToolByName(name).features[feature.key] }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-hint">
|
||||
请至少选择 2 个工具进行对比
|
||||
</div>
|
||||
|
||||
<Transition name="fade">
|
||||
<div v-if="selectedDetail" class="detail-card">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ selectedDetail.icon }}</span>
|
||||
<span class="detail-name">{{ selectedDetail.name }}</span>
|
||||
<button class="close-btn" @click="detailName = ''">✕</button>
|
||||
</div>
|
||||
<p class="detail-desc">{{ selectedDetail.desc }}</p>
|
||||
<div class="detail-code">
|
||||
<div class="code-label">示例代码片段:</div>
|
||||
<pre class="code-block"><code>{{ selectedDetail.example }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="detail-hint" v-if="selectedTools.length >= 2 && !detailName">
|
||||
点击下方工具名称查看详细介绍和代码示例
|
||||
</div>
|
||||
<div class="tool-detail-btns" v-if="selectedTools.length >= 2">
|
||||
<button
|
||||
v-for="name in selectedTools"
|
||||
:key="name"
|
||||
:class="['detail-btn', { active: detailName === name }]"
|
||||
@click="detailName = detailName === name ? '' : name"
|
||||
>
|
||||
{{ getToolByName(name).icon }} {{ name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selectedTools = ref(['Terraform', 'CloudFormation'])
|
||||
const detailName = ref('')
|
||||
|
||||
const selectedDetail = computed(() => {
|
||||
if (!detailName.value) return null
|
||||
return tools.find(t => t.name === detailName.value)
|
||||
})
|
||||
|
||||
const features = [
|
||||
{ key: 'vendor', label: '厂商' },
|
||||
{ key: 'language', label: '配置语言' },
|
||||
{ key: 'style', label: '声明式/命令式' },
|
||||
{ key: 'multiCloud', label: '多云支持' },
|
||||
{ key: 'stateManagement', label: '状态管理' },
|
||||
{ key: 'learning', label: '学习曲线' },
|
||||
{ key: 'community', label: '社区生态' },
|
||||
{ key: 'bestFor', label: '最佳场景' }
|
||||
]
|
||||
|
||||
const tools = [
|
||||
{
|
||||
name: 'Terraform',
|
||||
icon: '🟣',
|
||||
color: '#7c3aed',
|
||||
features: {
|
||||
vendor: 'HashiCorp',
|
||||
language: 'HCL',
|
||||
style: '声明式',
|
||||
multiCloud: '原生多云',
|
||||
stateManagement: 'State 文件',
|
||||
learning: '中等',
|
||||
community: '非常活跃',
|
||||
bestFor: '多云/混合云'
|
||||
},
|
||||
desc: 'Terraform 是目前最流行的开源 IaC 工具,由 HashiCorp 开发。它使用自研的 HCL 语言,通过 Provider 机制支持几乎所有主流云平台。',
|
||||
example: `resource "aws_s3_bucket" "data" {
|
||||
bucket = "my-data-bucket"
|
||||
tags = { Env = "prod" }
|
||||
}
|
||||
|
||||
resource "aws_instance" "web" {
|
||||
ami = "ami-0c55b159"
|
||||
instance_type = "t3.micro"
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: 'CloudFormation',
|
||||
icon: '🟠',
|
||||
color: '#ea580c',
|
||||
features: {
|
||||
vendor: 'AWS',
|
||||
language: 'YAML / JSON',
|
||||
style: '声明式',
|
||||
multiCloud: '仅 AWS',
|
||||
stateManagement: 'AWS 托管',
|
||||
learning: '中等偏高',
|
||||
community: 'AWS 生态',
|
||||
bestFor: '纯 AWS 环境'
|
||||
},
|
||||
desc: 'CloudFormation 是 AWS 原生的 IaC 服务,与 AWS 服务深度集成。状态由 AWS 自动管理,无需额外维护 State 文件。',
|
||||
example: `Resources:
|
||||
WebServer:
|
||||
Type: AWS::EC2::Instance
|
||||
Properties:
|
||||
ImageId: ami-0c55b159
|
||||
InstanceType: t3.micro
|
||||
Tags:
|
||||
- Key: Name
|
||||
Value: web-server`
|
||||
},
|
||||
{
|
||||
name: 'Pulumi',
|
||||
icon: '🔵',
|
||||
color: '#2563eb',
|
||||
features: {
|
||||
vendor: 'Pulumi',
|
||||
language: 'TypeScript/Python/Go',
|
||||
style: '命令式 + 声明式',
|
||||
multiCloud: '原生多云',
|
||||
stateManagement: 'Pulumi Cloud / 自管',
|
||||
learning: '低(熟悉编程语言)',
|
||||
community: '快速增长',
|
||||
bestFor: '开发者友好场景'
|
||||
},
|
||||
desc: 'Pulumi 允许使用真正的编程语言(TypeScript、Python、Go 等)来定义基础设施,对开发者非常友好,支持条件判断、循环等编程特性。',
|
||||
example: `import * as aws from "@pulumi/aws"
|
||||
|
||||
const bucket = new aws.s3.Bucket("data", {
|
||||
tags: { Env: "prod" }
|
||||
})
|
||||
|
||||
const server = new aws.ec2.Instance("web", {
|
||||
ami: "ami-0c55b159",
|
||||
instanceType: "t3.micro",
|
||||
})`
|
||||
},
|
||||
{
|
||||
name: 'Ansible',
|
||||
icon: '🔴',
|
||||
color: '#dc2626',
|
||||
features: {
|
||||
vendor: 'Red Hat',
|
||||
language: 'YAML (Playbook)',
|
||||
style: '命令式',
|
||||
multiCloud: '通过模块支持',
|
||||
stateManagement: '无状态(幂等)',
|
||||
learning: '低',
|
||||
community: '非常活跃',
|
||||
bestFor: '配置管理 + 编排'
|
||||
},
|
||||
desc: 'Ansible 是一个无代理的自动化工具,擅长配置管理和应用部署。它通过 SSH 连接目标机器执行任务,无需安装客户端。',
|
||||
example: `- name: 部署 Web 服务器
|
||||
hosts: webservers
|
||||
tasks:
|
||||
- name: 安装 Nginx
|
||||
apt:
|
||||
name: nginx
|
||||
state: present
|
||||
- name: 启动服务
|
||||
service:
|
||||
name: nginx
|
||||
state: started`
|
||||
}
|
||||
]
|
||||
|
||||
function getToolByName(name) {
|
||||
return tools.find(t => t.name === name)
|
||||
}
|
||||
|
||||
function toggleTool(name) {
|
||||
const idx = selectedTools.value.indexOf(name)
|
||||
if (idx >= 0) {
|
||||
if (selectedTools.value.length > 2) {
|
||||
selectedTools.value.splice(idx, 1)
|
||||
}
|
||||
} else {
|
||||
selectedTools.value.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
function getCellClass(toolName, featureKey) {
|
||||
const val = getToolByName(toolName).features[featureKey]
|
||||
if (featureKey === 'multiCloud') {
|
||||
if (val.includes('原生多云')) return 'cell-good'
|
||||
if (val.includes('仅')) return 'cell-warn'
|
||||
return ''
|
||||
}
|
||||
if (featureKey === 'learning') {
|
||||
if (val === '低') return 'cell-good'
|
||||
if (val.includes('高')) return 'cell-warn'
|
||||
return ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iac-tool-comparison-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-label {
|
||||
font-size: 0.78rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.selector-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--vp-c-text-3);
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tool-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.tool-chip {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 20px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tool-chip:hover { transform: scale(1.05); }
|
||||
.comparison-grid { overflow-x: auto; margin-bottom: 1rem; }
|
||||
.comparison-grid table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.comparison-grid th,
|
||||
.comparison-grid td {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
text-align: center;
|
||||
}
|
||||
.comparison-grid th {
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-weight: 600;
|
||||
}
|
||||
.feature-col { text-align: left; min-width: 80px; }
|
||||
.feature-cell { font-weight: 600; text-align: left; }
|
||||
.tool-header-icon { margin-right: 4px; }
|
||||
.cell-good { color: #10b981; font-weight: 600; }
|
||||
.cell-warn { color: #f59e0b; }
|
||||
.empty-hint {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.detail-hint {
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tool-detail-btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.detail-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.detail-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.detail-card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.detail-icon { font-size: 1.2rem; }
|
||||
.detail-name { font-weight: 600; font-size: 1rem; flex: 1; }
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.detail-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.code-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.code-block {
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
padding: 0.8rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.73rem;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="terraform-workflow-demo">
|
||||
<div class="demo-label">交互演示 ── Terraform 工作流四阶段</div>
|
||||
|
||||
<div class="stage-nav">
|
||||
<div
|
||||
v-for="(stage, i) in stages"
|
||||
:key="stage.key"
|
||||
:class="['stage-tab', { active: currentStage === i, completed: i < currentStage }]"
|
||||
@click="goToStage(i)"
|
||||
>
|
||||
<span class="stage-icon">{{ stage.icon }}</span>
|
||||
<span class="stage-name">{{ stage.name }}</span>
|
||||
<span v-if="i < stages.length - 1" class="stage-arrow">→</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stage-content">
|
||||
<Transition name="slide" mode="out-in">
|
||||
<div :key="currentStage" class="stage-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-icon">{{ stages[currentStage].icon }}</span>
|
||||
<span class="panel-title">{{ stages[currentStage].title }}</span>
|
||||
</div>
|
||||
<p class="panel-desc">{{ stages[currentStage].desc }}</p>
|
||||
|
||||
<div class="terminal-block">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-dot red"></span>
|
||||
<span class="terminal-dot yellow"></span>
|
||||
<span class="terminal-dot green"></span>
|
||||
<span class="terminal-title">Terminal</span>
|
||||
</div>
|
||||
<div class="terminal-body">
|
||||
<div v-for="(line, li) in visibleLines" :key="li" class="terminal-line">
|
||||
<span :class="line.cls">{{ line.text }}</span>
|
||||
</div>
|
||||
<span v-if="isTyping" class="cursor-blink">_</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-points">
|
||||
<div v-for="(point, pi) in stages[currentStage].points" :key="pi" class="point-item">
|
||||
<span class="point-bullet">{{ point.icon }}</span>
|
||||
<span class="point-text">{{ point.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div class="nav-buttons">
|
||||
<button class="nav-btn" :disabled="currentStage === 0" @click="goToStage(currentStage - 1)">
|
||||
← 上一步
|
||||
</button>
|
||||
<span class="stage-indicator">{{ currentStage + 1 }} / {{ stages.length }}</span>
|
||||
<button class="nav-btn primary" :disabled="currentStage === stages.length - 1" @click="goToStage(currentStage + 1)">
|
||||
下一步 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const currentStage = ref(0)
|
||||
const typingIndex = ref(0)
|
||||
const isTyping = ref(false)
|
||||
|
||||
const stages = [
|
||||
{
|
||||
key: 'write', icon: '📝', name: 'Write', title: 'Write ── 编写基础设施代码',
|
||||
desc: '用声明式语言(HCL)描述你期望的基础设施状态。代码就是文档,可以提交到 Git 进行版本管理和 Code Review。',
|
||||
lines: [
|
||||
{ text: '$ vim main.tf', cls: 'cmd' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'resource "aws_instance" "app" {', cls: 'code' },
|
||||
{ text: ' ami = "ami-0c55b159"', cls: 'code' },
|
||||
{ text: ' instance_type = "t3.micro"', cls: 'code' },
|
||||
{ text: ' tags = { Name = "my-app" }', cls: 'code' },
|
||||
{ text: '}', cls: 'code' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: '# 文件已保存 ✓', cls: 'success' }
|
||||
],
|
||||
points: [
|
||||
{ icon: '📄', text: '使用 .tf 文件描述资源' },
|
||||
{ icon: '🔧', text: 'HCL 语法简洁易读' },
|
||||
{ icon: '📦', text: '支持模块化复用' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'plan', icon: '🔍', name: 'Plan', title: 'Plan ── 预览变更计划',
|
||||
desc: 'Terraform 会对比当前状态和期望状态,生成一份详细的执行计划。这一步不会做任何实际变更,是安全的"预演"。',
|
||||
lines: [
|
||||
{ text: '$ terraform plan', cls: 'cmd' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Terraform will perform the following actions:', cls: 'info' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: ' + aws_instance.app', cls: 'add' },
|
||||
{ text: ' ami: "ami-0c55b159"', cls: 'detail' },
|
||||
{ text: ' instance_type: "t3.micro"', cls: 'detail' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Plan: 1 to add, 0 to change, 0 to destroy.', cls: 'success' }
|
||||
],
|
||||
points: [
|
||||
{ icon: '🛡️', text: '变更前先预览,避免意外' },
|
||||
{ icon: '➕', text: '绿色 + 表示新增资源' },
|
||||
{ icon: '🔄', text: '~ 表示修改,- 表示删除' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'apply', icon: '🚀', name: 'Apply', title: 'Apply ── 执行变更',
|
||||
desc: '确认计划无误后,Terraform 调用云平台 API 创建/修改/删除资源,并将最终状态写入 State 文件。',
|
||||
lines: [
|
||||
{ text: '$ terraform apply', cls: 'cmd' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'aws_instance.app: Creating...', cls: 'info' },
|
||||
{ text: 'aws_instance.app: Still creating... [10s elapsed]', cls: 'info' },
|
||||
{ text: 'aws_instance.app: Creation complete after 32s', cls: 'success' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Apply complete! Resources: 1 added, 0 changed, 0 destroyed.', cls: 'success' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Outputs:', cls: 'info' },
|
||||
{ text: ' public_ip = "54.123.45.67"', cls: 'output' }
|
||||
],
|
||||
points: [
|
||||
{ icon: '☁️', text: '自动调用云平台 API' },
|
||||
{ icon: '💾', text: '状态保存到 terraform.tfstate' },
|
||||
{ icon: '📤', text: '输出关键信息(IP、域名等)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'destroy', icon: '🗑️', name: 'Destroy', title: 'Destroy ── 销毁资源',
|
||||
desc: '不再需要时,一条命令即可安全销毁所有资源。Terraform 会按照依赖关系的逆序逐一清理,避免残留。',
|
||||
lines: [
|
||||
{ text: '$ terraform destroy', cls: 'cmd' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Terraform will perform the following actions:', cls: 'info' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: ' - aws_instance.app', cls: 'remove' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Plan: 0 to add, 0 to change, 1 to destroy.', cls: 'warn' },
|
||||
{ text: 'aws_instance.app: Destroying...', cls: 'info' },
|
||||
{ text: 'aws_instance.app: Destruction complete after 15s', cls: 'success' },
|
||||
{ text: '', cls: '' },
|
||||
{ text: 'Destroy complete! Resources: 1 destroyed.', cls: 'success' }
|
||||
],
|
||||
points: [
|
||||
{ icon: '🧹', text: '按依赖逆序安全清理' },
|
||||
{ icon: '💰', text: '避免资源遗忘产生费用' },
|
||||
{ icon: '♻️', text: '环境可随时重建' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const visibleLines = computed(() => {
|
||||
return stages[currentStage.value].lines.slice(0, typingIndex.value)
|
||||
})
|
||||
|
||||
function goToStage(i) {
|
||||
currentStage.value = i
|
||||
}
|
||||
|
||||
watch(currentStage, () => {
|
||||
typingIndex.value = 0
|
||||
isTyping.value = true
|
||||
typeNext()
|
||||
})
|
||||
|
||||
function typeNext() {
|
||||
const total = stages[currentStage.value].lines.length
|
||||
if (typingIndex.value < total) {
|
||||
setTimeout(() => {
|
||||
typingIndex.value++
|
||||
typeNext()
|
||||
}, 120)
|
||||
} else {
|
||||
isTyping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize first stage
|
||||
typeNext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terraform-workflow-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-label {
|
||||
font-size: 0.78rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.stage-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.stage-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.stage-tab.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.stage-tab.completed {
|
||||
border-color: #10b981;
|
||||
background: #d1fae510;
|
||||
}
|
||||
.stage-arrow { color: var(--vp-c-text-3); margin: 0 2px; }
|
||||
.stage-content { min-height: 280px; }
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.panel-icon { font-size: 1.3rem; }
|
||||
.panel-title { font-weight: 600; font-size: 0.95rem; }
|
||||
.panel-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.8rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.terminal-block {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.terminal-header {
|
||||
background: #1e1e1e;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.terminal-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.terminal-dot.red { background: #ff5f57; }
|
||||
.terminal-dot.yellow { background: #febc2e; }
|
||||
.terminal-dot.green { background: #28c840; }
|
||||
.terminal-title {
|
||||
font-size: 0.7rem;
|
||||
color: #888;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.terminal-body {
|
||||
background: #1a1a2e;
|
||||
padding: 0.8rem;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
font-size: 0.73rem;
|
||||
line-height: 1.6;
|
||||
min-height: 160px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.terminal-line .cmd { color: #7dd3fc; }
|
||||
.terminal-line .code { color: #a5b4fc; }
|
||||
.terminal-line .info { color: #94a3b8; }
|
||||
.terminal-line .add { color: #4ade80; }
|
||||
.terminal-line .remove { color: #f87171; }
|
||||
.terminal-line .detail { color: #cbd5e1; padding-left: 1rem; }
|
||||
.terminal-line .success { color: #34d399; font-weight: 600; }
|
||||
.terminal-line .warn { color: #fbbf24; }
|
||||
.terminal-line .output { color: #c084fc; }
|
||||
.cursor-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
color: #7dd3fc;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
.key-points {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.point-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.nav-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-btn:disabled { opacity: 0.4; cursor: default; }
|
||||
.nav-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
|
||||
.stage-indicator { font-size: 0.75rem; color: var(--vp-c-text-3); }
|
||||
.slide-enter-active, .slide-leave-active { transition: all 0.3s ease; }
|
||||
.slide-enter-from { opacity: 0; transform: translateX(20px); }
|
||||
.slide-leave-to { opacity: 0; transform: translateX(-20px); }
|
||||
</style>
|
||||
+333
@@ -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 RAG、Advanced RAG、Modular 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>
|
||||
@@ -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()
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
# 算法思维入门
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
::: tip 前言
|
||||
**如何高效地解决问题?** 你可能遇到过这样的困惑:同一个问题,有人写的代码跑几秒就出结果,有人写的跑几分钟还在转。差别往往在于算法。本章带你理解算法的核心思维方式。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **问题拆解能力**:面对复杂问题,能想到用分治、递归等策略拆解,而不是一上来就写代码
|
||||
- **效率判断能力**:用大 O 表示法判断两种解法哪个更高效,而不是凭感觉猜测
|
||||
- **复杂度思维**:写代码前先估算数据规模和时间要求,选择合适的算法级别
|
||||
- **后续学习基础**:为高级数据结构、分布式系统、机器学习打下基础
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 二分查找 | 分治思想、O(log n) |
|
||||
| **第 2 章** | 排序算法 | 冒泡、快排、归并 |
|
||||
| **第 3 章** | 复杂度分析 | 时间复杂度、空间复杂度 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:算法是什么?
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将掌握从输入网址到页面显示的完整技术流程,理解浏览器与服务器如何协同工作。这些知识是后续学习 API、接口、网络安全等技术的基石,也是排查"网页打不开"、"加载慢"等日常问题的关键。
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | URL 解析 | 网址的结构和作用 |
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
# 什么是数据的编码与传输?
|
||||
|
||||
> 💡 **学习指南**:当你给朋友发一张照片、发一条微信,或者下载一个几 GB 的游戏时,这些信息是怎么穿过大半个地球、完好无损地出现在你的屏幕上的?
|
||||
>
|
||||
> 本章节会围绕一个经常困扰新手的问题展开:**为什么我收到的文件变成了乱码?**
|
||||
>
|
||||
> 顺着这个问题,我们将彻底揭开计算机底层最核心的三大基石:**编码、存储与传输**。
|
||||
::: tip 前言
|
||||
当你给朋友发一张照片、发一条微信,或者下载一个几 GB 的游戏时,这些信息是怎么穿过大半个地球、完好无损地出现在你的屏幕上的?本章节会围绕一个经常困扰新手的问题展开:**为什么我收到的文件变成了乱码?** 顺着这个问题,我们将彻底揭开计算机底层最核心的三大基石:**编码、存储与传输**。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **乱码排查能力**:遇到"文件打开是乱码"时,能从编码角度分析原因,而不是简单认为"文件坏了"
|
||||
- **跨平台意识**:处理数据交换时,知道为什么要关注编码格式和字节序
|
||||
- **编码世界观**:理解计算机如何用 0 和 1 表示世间万物——从文字到图像到复杂对象
|
||||
- **后续学习基础**:为网络协议、文件格式、序列化技术打下基础
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 字符编码 | ASCII、UTF-8、GBK |
|
||||
| **第 2 章** | 数据存储 | 二进制、字节序 |
|
||||
| **第 3 章** | 数据传输 | 序列化、压缩 |
|
||||
|
||||
在开始之前,我们需要先明确一个经常被新手忽略的物理事实:
|
||||
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
# 数据结构
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
::: tip 前言
|
||||
**如何高效地组织和存储数据?** 你可能遇到过这样的困惑:为什么有些程序处理几万条数据很快,有些处理几百条就卡住了?答案往往在于数据结构的选择。本章带你理解常见数据结构的特点和适用场景。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **选型决策能力**:知道什么时候用数组快速访问,什么时候用链表灵活插入
|
||||
- **性能分析视角**:能判断性能问题是数据结构选择不当,还是算法效率低下
|
||||
- **权衡思维**:理解"空间换时间"与"时间换空间",知道没有完美的数据结构
|
||||
- **后续学习基础**:为数据库、缓存系统、搜索引擎等技术打下基础
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 线性结构 | 数组、链表、栈、队列 |
|
||||
| **第 2 章** | 哈希结构 | 哈希表、冲突处理 |
|
||||
| **第 3 章** | 树形结构 | 二叉树、B树、堆 |
|
||||
| **第 4 章** | 图结构 | 有向图、无向图、遍历算法 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:数据结构是什么?
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
# 操作系统:给电脑请个"大管家"
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
::: tip 前言
|
||||
**有了完美的 CPU 和无限的内存,电脑就能直接用了吗?**
|
||||
在上一章,我们见证了晶体管如何组合成强大的 CPU。但即使你拥有最顶级的硬件,如果直接让它们工作,连在屏幕上显示一个字母都需要写几百行晦涩的机器指令。不仅麻烦,还极其危险——稍有差池,你的代码就可能把别人的数据覆盖掉。
|
||||
|
||||
为了解决这些噩梦,**操作系统(Operating System, 简称 OS)**诞生了。它是挡在你和冰冷硬件之间的一层最伟大的"软件"。本章我们将抛开深奥的代码,用通俗的比喻,看看这个"超级管家"是如何把杂乱无章的硬件调教得服服帖帖的。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **问题排查能力**:遇到"程序卡死"、"内存不足"时,能从操作系统层面分析原因
|
||||
- **术语理解深度**:理解"多进程"、"虚拟内存"、"文件权限"解决的是什么问题
|
||||
- **系统观思维**:理解程序不是孤立运行的,而是与操作系统、其他进程、硬件资源密切交互
|
||||
- **后续学习基础**:为并发编程、系统调优、容器技术打下基础
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 进程管理 | CPU 时分复用、时间片轮转 |
|
||||
| **第 2 章** | 内存管理 | 虚拟内存、分页机制 |
|
||||
| **第 3 章** | 文件系统 | 文件组织、目录结构 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:没有操作系统会怎样?
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
# 从按下电源到访问网站发生了什么
|
||||
|
||||
::: tip 前言
|
||||
你有没有想过,当你按下电脑电源键,到最终在浏览器中看到网页,这中间到底发生了什么?
|
||||
|
||||
这个过程涉及**硬件启动**、**操作系统加载**、**网络通信**等多个环节。理解这个过程,能帮助你建立对计算机系统的整体认知,也是成为全栈工程师的必经之路。
|
||||
:::
|
||||
|
||||
**你会学到什么?**
|
||||
|
||||
- 电脑从通电到显示桌面的完整过程
|
||||
- 操作系统是如何启动的
|
||||
- 浏览器是如何工作的
|
||||
- 当你访问一个 URL 时,网络请求是如何完成的
|
||||
|
||||
---
|
||||
|
||||
## 1. 按下电源:硬件的觉醒
|
||||
|
||||
### 1.1 电源启动
|
||||
|
||||
当你按下电源键,**电源单元(PSU)** 开始工作,把交流电(220V)转换成直流电(12V、5V、3.3V 等),为各个硬件部件供电。
|
||||
|
||||
```
|
||||
电源按钮 → 电源单元(PSU) → 直流电输出 → 供给主板各部件
|
||||
```
|
||||
|
||||
### 1.2 主板芯片组唤醒
|
||||
|
||||
电源稳定后,**主板芯片组**开始工作,它就像电脑的"总调度员",负责协调各个硬件部件。
|
||||
|
||||
### 1.3 CPU 复位
|
||||
|
||||
CPU 接收到复位信号后,把内部所有寄存器和缓存清零,从一个预设的地址开始执行指令。这个地址通常指向 **BIOS/UEFI** 芯片。
|
||||
|
||||
<PowerOnDemo />
|
||||
|
||||
---
|
||||
|
||||
## 2. BIOS/UEFI:硬件的自检
|
||||
|
||||
### 2.1 什么是 BIOS/UEFI?
|
||||
|
||||
**BIOS(Basic Input/Output System)** 是电脑启动后第一个运行的程序,存储在主板的一个**只读芯片**中。
|
||||
|
||||
**UEFI(Unified Extensible Firmware Interface)** 是 BIOS 的升级版,更安全、更现代。现在的电脑大多使用 UEFI。
|
||||
|
||||
### 2.2 BIOS/UEFI 做了什么?
|
||||
|
||||
1. **硬件自检(POST)**:检查内存、显卡、键盘等部件是否正常
|
||||
2. **初始化硬件**:设置硬件工作模式
|
||||
3. **启动顺序**:按照设定顺序,尝试从硬盘/U 盘/网络启动
|
||||
|
||||
```
|
||||
BIOS/UEFI 工作流程:
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. 硬件自检 (POST) │
|
||||
│ - 检查内存是否正常 │
|
||||
│ - 检查显卡是否正常 │
|
||||
│ - 检查键盘/鼠标是否正常 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 2. 初始化硬件 │
|
||||
│ - 设置硬件工作模式 │
|
||||
│ - 配置中断向量表 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 3. 寻找启动设备 │
|
||||
│ - 按启动顺序查找可启动设备 │
|
||||
│ - 读取启动扇区 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
如果发现问题,主板会发出**蜂鸣声**(不同次数代表不同错误)。
|
||||
|
||||
### 2.3 启动顺序
|
||||
|
||||
BIOS/UEFI 会按照设定的**启动顺序**查找启动设备:
|
||||
|
||||
1. 硬盘(最常见)
|
||||
2. U 盘/光盘(重装系统时用)
|
||||
3. 网络( PXE 启动,企业批量部署用)
|
||||
|
||||
找到第一个可启动设备后,读取它的**启动扇区(Boot Sector)**,把控制权交给操作系统。
|
||||
|
||||
---
|
||||
|
||||
## 3. 操作系统启动:从内核到桌面
|
||||
|
||||
### 3.1 什么是操作系统?
|
||||
|
||||
**操作系统(Operating System,简称 OS)** 是管理计算机硬件和软件资源的程序集合。它就像一个"大管家",帮我们管理内存、CPU、文件等资源,让我们不需要直接和硬件打交道。
|
||||
|
||||
常见的操作系统:
|
||||
|
||||
| 操作系统 | 特点 | 典型设备 |
|
||||
|---------|------|---------|
|
||||
| **Windows** | 生态丰富,兼容性好 | 桌面电脑、笔记本 |
|
||||
| **macOS** | 苹果生态,流畅稳定 | Mac 电脑 |
|
||||
| **Linux** | 开源免费,服务器首选 | 服务器、嵌入式设备 |
|
||||
| **Android** | 移动端 Linux | 手机、平板 |
|
||||
| **iOS** | 苹果移动端 | iPhone、iPad |
|
||||
|
||||
### 3.2 操作系统的启动过程
|
||||
|
||||
当你从硬盘启动时,操作系统的启动过程如下:
|
||||
|
||||
<BootProcessDemo />
|
||||
|
||||
#### 第一步:引导程序(Bootloader)
|
||||
|
||||
硬盘的第一个扇区存放着**引导程序(Bootloader)**,它的任务是把操作系统内核加载到内存中。
|
||||
|
||||
- **Windows**:Bootloader 叫 `bootmgr`
|
||||
- **Linux**:常见的引导程序有 `GRUB`、`rEFInd` 等
|
||||
|
||||
```
|
||||
引导程序工作流程:
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. 读取硬盘分区表 │
|
||||
│ 2. 找到操作系统分区 │
|
||||
│ 3. 加载操作系统内核到内存 │
|
||||
│ 4. 跳转到内核入口点 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 第二步:内核加载(Kernel)
|
||||
|
||||
操作系统**内核(Kernel)** 是操作系统的核心,负责管理内存、CPU、进程等核心功能。
|
||||
|
||||
```
|
||||
内核的主要功能:
|
||||
┌─────────────────────────────────────┐
|
||||
│ • 进程管理 - 创建/调度进程 │
|
||||
│ • 内存管理 - 分配/回收内存 │
|
||||
│ • 文件系统 - 管理文件存储 │
|
||||
│ • 设备驱动 - 控制硬件设备 │
|
||||
│ • 网络通信 - 处理网络协议 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 第三步:系统服务启动
|
||||
|
||||
内核加载后,会启动各种**系统服务**:
|
||||
|
||||
- **Windows 服务**:更新服务、安全中心、打印机服务
|
||||
- **Linux 服务**:SSH 服务、网络服务、图形界面(GNOME、KDE)
|
||||
|
||||
```
|
||||
Windows 启动过程:
|
||||
BIOS → MBR → bootmgr → winload.exe → ntoskrnl.exe → 系统服务 → 桌面
|
||||
|
||||
Linux 启动过程:
|
||||
BIOS → GRUB → vmlinuz (内核) → systemd → 系统服务 → 桌面环境
|
||||
```
|
||||
|
||||
#### 第四步:显示桌面
|
||||
|
||||
最后,操作系统启动**图形界面(GUI)**,显示桌面:
|
||||
|
||||
- **Windows**:explorer.exe(资源管理器)显示桌面
|
||||
- **Linux**:GNOME、KDE、XFCE 等桌面环境
|
||||
- **macOS**:Finder 显示桌面
|
||||
|
||||
```
|
||||
桌面出现的过程:
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. 显卡驱动加载 │
|
||||
│ 2. 显示服务器启动 │
|
||||
│ (Windows: Desktop Window Manager)│
|
||||
│ (Linux: X Server / Wayland) │
|
||||
│ 3. 桌面环境启动 │
|
||||
│ 4. 显示桌面背景和图标 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
<DesktopDemo />
|
||||
|
||||
---
|
||||
|
||||
## 4. 打开浏览器:应用程序的启动
|
||||
|
||||
### 4.1 应用程序的启动过程
|
||||
|
||||
当你双击浏览器图标时,操作系统会:
|
||||
|
||||
1. **查找可执行文件**:根据文件关联,找到浏览器的 `.exe`(Windows)或可执行文件
|
||||
2. **创建进程**:为浏览器创建一个新的**进程**
|
||||
3. **加载程序**:把浏览器的代码从硬盘加载到内存
|
||||
4. **初始化**:启动浏览器的主线程、渲染引擎、网络引擎等
|
||||
|
||||
```
|
||||
浏览器启动过程:
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. 双击图标 │
|
||||
│ 2. 操作系统查找浏览器可执行文件 │
|
||||
│ 3. 创建浏览器进程 │
|
||||
│ 4. 加载浏览器代码到内存 │
|
||||
│ 5. 初始化各模块(渲染、网络、JS) │
|
||||
│ 6. 显示浏览器窗口 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 浏览器的主要组成部分
|
||||
|
||||
现代浏览器是一个复杂的"操作系统",主要由以下部分组成:
|
||||
|
||||
| 模块 | 功能 |
|
||||
|-----|------|
|
||||
| **用户界面** | 地址栏、标签页、书签等 |
|
||||
| **浏览器引擎** | 协调 UI 和渲染引擎 |
|
||||
| **渲染引擎** | 解析 HTML/CSS,显示网页 |
|
||||
| **JavaScript 引擎** | 执行 JavaScript 代码 |
|
||||
| **网络模块** | 发送 HTTP 请求 |
|
||||
| **UI 后端** | 绘制基础 UI 组件 |
|
||||
| **数据存储** | Cookie、LocalStorage 等 |
|
||||
|
||||
<BrowserArchitectureDemo />
|
||||
|
||||
---
|
||||
|
||||
## 5. 访问 URL:网络请求的全过程
|
||||
|
||||
### 5.1 什么是 URL?
|
||||
|
||||
**URL(Uniform Resource Locator)** 是资源的地址,就像生活中的地址一样,用来定位互联网上的资源。
|
||||
|
||||
```
|
||||
URL 的结构:
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ https:// │ www.example.com │ /path/to/page │ ?query=1 │
|
||||
│ 协议 │ 域名 │ 路径 │ 查询 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **协议(Protocol)**:用什么方式访问(http、https、ftp 等)
|
||||
- **域名(Domain)**:服务器的地址
|
||||
- **路径(Path)**:资源在服务器上的位置
|
||||
- **查询(Query)**:额外的参数
|
||||
|
||||
### 5.2 访问 URL 的完整过程
|
||||
|
||||
当你访问 `https://www.example.com` 时,发生了这些事情:
|
||||
|
||||
<URLRequestDemo />
|
||||
|
||||
#### 第一步:URL 解析
|
||||
|
||||
浏览器首先**解析 URL**,提取出协议、域名、路径等信息。
|
||||
|
||||
```
|
||||
URL 解析过程:
|
||||
https://www.example.com/index.html
|
||||
↓
|
||||
协议: https
|
||||
域名: www.example.com
|
||||
路径: /index.html
|
||||
```
|
||||
|
||||
#### 第二步:DNS 解析
|
||||
|
||||
计算机通过网络访问服务器,但网络用的是 **IP 地址**(如 93.184.216.34),而不是域名。所以需要把域名转换成 IP 地址,这个过程叫 **DNS 解析**。
|
||||
|
||||
```
|
||||
DNS 解析流程:
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 浏览器缓存 → hosts 文件 → 本地 DNS 缓存 → DNS 服务器 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
实际过程:
|
||||
1. 浏览器检查缓存(最近访问过吗?)
|
||||
2. 操作系统检查 DNS 缓存
|
||||
3. 向 DNS 服务器发送查询请求
|
||||
4. DNS 服务器返回 IP 地址
|
||||
```
|
||||
|
||||
#### 第三步:建立 TCP 连接
|
||||
|
||||
拿到 IP 地址后,浏览器要与服务器建立 **TCP 连接**。TCP 是传输层协议,保证数据可靠传输。
|
||||
|
||||
```
|
||||
TCP 三次握手:
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 客户端 → 服务器:SYN(同步请求) │
|
||||
│ 服务器 → 客户端:SYN-ACK(确认并同步) │
|
||||
│ 客户端 → 服务器:ACK(确认) │
|
||||
│ ↓ │
|
||||
│ 连接建立完成! │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
如果是 **HTTPS**,还需要进行 **TLS/SSL 握手**,建立加密通道。
|
||||
|
||||
#### 第四步:发送 HTTP 请求
|
||||
|
||||
连接建立后,浏览器向服务器发送 **HTTP 请求**:
|
||||
|
||||
```
|
||||
HTTP 请求格式:
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ GET /index.html HTTP/1.1 │
|
||||
│ Host: www.example.com │
|
||||
│ User-Agent: Mozilla/5.0... │
|
||||
│ Accept: text/html │
|
||||
│ │
|
||||
│ (空行) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
常见的 HTTP 方法:
|
||||
|
||||
| 方法 | 含义 | 用途 |
|
||||
|-----|------|-----|
|
||||
| **GET** | 获取资源 | 浏览网页 |
|
||||
| **POST** | 提交数据 | 登录、提交表单 |
|
||||
| **PUT** | 上传资源 | 文件上传 |
|
||||
| **DELETE** | 删除资源 | 删除数据 |
|
||||
|
||||
#### 第五步:服务器处理请求
|
||||
|
||||
服务器(通常是 **Web 服务器** 如 Nginx、Apache)收到请求后:
|
||||
|
||||
1. **解析请求**:理解客户端想要什么
|
||||
2. **处理业务**:调用后端程序(如 Python、Node.js、Java)
|
||||
3. **查询数据库**:获取需要的数据
|
||||
4. **生成响应**:把数据组装成 HTML、JSON 等格式
|
||||
|
||||
```
|
||||
服务器处理流程:
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. Web 服务器接收请求 (Nginx/Apache) │
|
||||
│ 2. 根据路径找到对应的处理程序 │
|
||||
│ 3. 执行后端代码 (API、业务逻辑) │
|
||||
│ 4. 如需查询数据库,获取数据 │
|
||||
│ 5. 组装响应 (HTML/JSON/CSS/JS) │
|
||||
│ 6. 返回 HTTP 响应 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 第六步:返回 HTTP 响应
|
||||
|
||||
服务器返回 **HTTP 响应**,包含状态码、响应头和响应体:
|
||||
|
||||
```
|
||||
HTTP 响应格式:
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ HTTP/1.1 200 OK │
|
||||
│ Content-Type: text/html │
|
||||
│ Content-Length: 1234 │
|
||||
│ │
|
||||
│ <!DOCTYPE html> │
|
||||
│ <html>...</html> │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
常见的状态码:
|
||||
|
||||
| 状态码 | 含义 |
|
||||
|-------|------|
|
||||
| **200** | 成功 |
|
||||
| **301/302** | 重定向 |
|
||||
| **404** | 资源未找到 |
|
||||
| **500** | 服务器错误 |
|
||||
|
||||
#### 第七步:浏览器渲染页面
|
||||
|
||||
浏览器收到响应后,开始**渲染页面**:
|
||||
|
||||
<RenderingDemo />
|
||||
|
||||
1. **解析 HTML**:构建 DOM 树
|
||||
2. **解析 CSS**:计算样式,构建渲染树
|
||||
3. **执行 JavaScript**:执行页面中的 JS 代码
|
||||
4. **绘制页面**:把内容显示到屏幕上
|
||||
|
||||
```
|
||||
浏览器渲染过程:
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. HTML 解析 → DOM 树 │
|
||||
│ 2. CSS 解析 → 样式规则 │
|
||||
│ 3. DOM + CSS → 渲染树 │
|
||||
│ 4. 布局计算 → 每个元素的大小位置 │
|
||||
│ 5. 绘制 → 像素显示到屏幕 │
|
||||
│ 6. 合成 → 多层合并显示 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 完整流程回顾
|
||||
|
||||
让我们把整个过程串起来:
|
||||
|
||||
<FullProcessDemo />
|
||||
|
||||
```
|
||||
从按下电源到访问网站的完整流程:
|
||||
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 1. 按下电源 │
|
||||
│ └── 电源启动 → 主板唤醒 → CPU 复位 → 执行 BIOS/UEFI │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ 2. BIOS/UEFI 启动 │
|
||||
│ └── 硬件自检 → 寻找启动设备 → 读取引导程序 │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ 3. 操作系统启动 │
|
||||
│ └── 引导程序 → 加载内核 → 启动服务 → 显示桌面 │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ 4. 打开浏览器 │
|
||||
│ └── 双击图标 → 创建进程 → 加载程序 → 显示窗口 │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ 5. 访问 URL │
|
||||
│ └── URL 解析 → DNS 解析 → TCP 连接 → HTTP 请求 │
|
||||
│ → 服务器处理 → HTTP 响应 → 浏览器渲染 → 显示网页 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 知识地图
|
||||
|
||||
这一章涉及的知识领域:
|
||||
|
||||
```
|
||||
计算机系统概览
|
||||
├── 硬件基础
|
||||
│ ├── 电源 (PSU)
|
||||
│ ├── 主板芯片组
|
||||
│ └── CPU
|
||||
├── BIOS/UEFI
|
||||
│ ├── POST 自检
|
||||
│ ├── 启动顺序
|
||||
│ └── 引导程序
|
||||
├── 操作系统
|
||||
│ ├── 内核 (Kernel)
|
||||
│ ├── 系统服务
|
||||
│ └── 桌面环境
|
||||
├── 应用程序
|
||||
│ ├── 进程管理
|
||||
│ └── 程序加载
|
||||
└── 网络通信
|
||||
├── DNS 解析
|
||||
├── TCP/IP 协议
|
||||
├── HTTP 协议
|
||||
└── 浏览器渲染
|
||||
```
|
||||
|
||||
::: tip 继续学习
|
||||
如果你想深入了解某个环节,可以继续学习:
|
||||
|
||||
- **从晶体管到 CPU**:了解计算机硬件基础
|
||||
- **操作系统(进程/内存/文件系统)**:深入理解操作系统
|
||||
- **计算机网络**:深入理解网络协议
|
||||
:::
|
||||
@@ -1,6 +1,23 @@
|
||||
# 编程语言图谱
|
||||
|
||||
> 💡 **学习指南**:为什么有这么多编程语言?该学哪个?本章带你从"语言演化"到"编程范式"到"如何选择",建立对编程语言全景的理解。**结论先行:没有最好的语言,只有最适合场景的语言。**
|
||||
::: tip 前言
|
||||
为什么有这么多编程语言?该学哪个?本章带你从"语言演化"到"编程范式"到"如何选择",建立对编程语言全景的理解。**结论先行:没有最好的语言,只有最适合场景的语言。**
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **理性选型能力**:面对"学什么语言"时,能根据项目需求做出判断,而不是盲目跟风
|
||||
- **范式理解深度**:理解"面向对象"、"函数式编程"是不同的思维方式,而不仅仅是语法差异
|
||||
- **历史演进视角**:看到 70 多年语言演化——从手写 0 和 1 到自然语言生成代码
|
||||
- **后续学习基础**:为理解新语言设计理念、技术选型决策打下基础
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 语言演化 | 从机器语言到高级语言 |
|
||||
| **第 2 章** | 编程范式 | 命令式、面向对象、函数式 |
|
||||
| **第 3 章** | 语言选择 | 场景驱动的选型方法 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
# 从晶体管到 CPU
|
||||
|
||||
::: tip 核心问题
|
||||
::: tip 前言
|
||||
**计算机是怎么"思考"的?** 你可能知道 CPU 是电脑的"大脑",但这个大脑到底是怎么工作的?它怎么从一堆金属和塑料变成能执行程序、处理数据的智能设备?本章带你从最底层的晶体管开始,一步步理解 CPU 的构造原理。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **术语理解能力**:听到"CPU 主频"、"多核"、"指令集"不再一头雾水,能理解背后的物理原理
|
||||
- **代码执行视角**:看到一行代码如何经过取指、解码、执行、写回,最终变成屏幕上的像素点
|
||||
- **抽象层次思维**:理解每一层如何向上层提供服务,又如何隐藏下层的复杂性
|
||||
- **后续学习基础**:为计算机体系结构、嵌入式开发、性能优化打下基础
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 晶体管 | 数字世界的开关 |
|
||||
| **第 2 章** | 逻辑门 | 布尔运算的物理实现 |
|
||||
| **第 3 章** | 功能单元 | 加法器、寄存器、多路选择器 |
|
||||
| **第 4 章** | CPU 核心 | 取指、解码、执行、写回 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:从沙子到智能
|
||||
@@ -201,6 +217,8 @@
|
||||
- **内部总线 (Internal Bus)**:系统里的传送带,负责在各个模块之间搬运数据和信号。
|
||||
- **控制单元 (Control Unit)**:总指挥。它的使命就是从内存中读取用 0 和 1 组成的指令,解析出应该做什么,并向其他模块传达具体的控制信号,调度它们各司其职。
|
||||
|
||||
<MinCpuDemo />
|
||||
|
||||
### 4.2 CPU 是如何执行指令的?
|
||||
|
||||
不管写下的高级编程语言有多么复杂,最终都会变成内存中的一条条底层指令。CPU 执行任何指令的过程,本质上都在重复以下典型的四个步骤:
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
# 类型系统与编译原理入门
|
||||
|
||||
> 💡 **学习指南**:当你写下 `int x = 10 + 5;` 时,编译器是如何理解每个字符、检查类型是否正确、最终生成机器指令的?本章用两个核心概念——**类型系统**和**编译流程**——帮你理解编程语言背后的"翻译机制"。
|
||||
::: tip 前言
|
||||
当你写下 `int x = 10 + 5;` 时,编译器是如何理解每个字符、检查类型是否正确、最终生成机器指令的?本章用两个核心概念——**类型系统**和**编译流程**——帮你理解编程语言背后的"翻译机制"。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **问题诊断能力**:看到 `TypeError` 报错时,能快速定位是类型不匹配还是隐式转换惹的祸
|
||||
- **语言选择依据**:理解为什么 TypeScript 适合大型项目、Python 适合快速原型开发
|
||||
- **类型安全思维**:在写代码时就预见可能的类型错误,而不是等到运行时才发现
|
||||
- **后续学习基础**:为编译原理、语言设计等高级话题打下基础
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 类型系统 | 静态/动态类型、强/弱类型 |
|
||||
| **第 2 章** | 编译流程 | 词法分析、语法分析、代码生成 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
# Vibe Coding 时代下的全栈开发
|
||||
|
||||
::: tip 前言
|
||||
**什么是 Vibe Coding?** 简单说,就是"用自然语言写代码"——你用中文或英文描述想要什么,AI 帮你生成代码。这彻底改变了软件开发的游戏规则。
|
||||
|
||||
但这里有个关键问题:**AI 能帮你写代码,但 AI 不能替你思考。** 你仍然需要知道"要写什么"、"为什么这么写"、"怎么判断对错"。这正是本章要帮你建立的基础认知框架。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **领域全景认知**:知道前端、后端、AI 算法等方向分别做什么
|
||||
- **技术选型能力**:面对"学什么语言/框架"时,能做出理性判断
|
||||
- **成长路径清晰**:了解从零基础到 3-5 年经验工程师的技能演进
|
||||
- **Vibe Coding 思维**:理解在 AI 辅助时代,哪些能力变得更重要
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 计算机领域全景 | 前端、后端、移动端、AI、运维 |
|
||||
| **第 2 章** | 什么是前端 | 用户能感知的界面层 |
|
||||
| **第 3 章** | 什么是后端 | 幕后的服务器逻辑 |
|
||||
| **第 4 章** | 编程语言图谱 | 与计算机沟通的工具 |
|
||||
| **第 5 章** | 全栈工程师 | 前后端通吃的多面手 |
|
||||
| **第 6 章** | AI 算法工程师 | 让机器学会思考 |
|
||||
| **第 7 章** | 成长路径 | 从入门到精通的路线图 |
|
||||
|
||||
---
|
||||
|
||||
## 0. Vibe Coding:软件开发的新范式
|
||||
|
||||
### 0.1 什么是 Vibe Coding?
|
||||
|
||||
想象一下以前的软件开发:
|
||||
|
||||
```
|
||||
传统开发流程:
|
||||
你 → 学习语法 → 写代码 → 调试 → 查文档 → 修改 → 运行
|
||||
↑___________________反复循环___________________↓
|
||||
```
|
||||
|
||||
现在有了 AI 辅助:
|
||||
|
||||
```
|
||||
Vibe Coding 流程:
|
||||
你 → 用自然语言描述需求 → AI 生成代码 → 你审核修改 → 运行
|
||||
↑____________快速迭代____________↓
|
||||
```
|
||||
|
||||
**核心变化**:从"怎么写代码"变成"怎么描述需求"。
|
||||
|
||||
### 0.2 Vibe Coding 时代,什么能力更重要?
|
||||
|
||||
<DeveloperSkillShiftDemo />
|
||||
|
||||
::: tip 💡 关键洞察
|
||||
AI 能帮你写代码,但以下能力 AI 替代不了:
|
||||
- **判断力**:知道 AI 生成的代码对不对、好不好
|
||||
- **架构思维**:知道系统该怎么设计、模块该怎么划分
|
||||
- **领域知识**:理解业务逻辑,知道"要做什么"
|
||||
- **调试能力**:出问题时知道从哪里排查
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 计算机领域全景图
|
||||
|
||||
在深入各个方向之前,先建立一个全局认知。
|
||||
|
||||
<ComputerFieldMapDemo />
|
||||
|
||||
### 1.1 用"餐厅"比喻理解各领域
|
||||
|
||||
把一个软件系统想象成一家**餐厅**:
|
||||
|
||||
| 领域 | 餐厅角色 | 做什么 | 产出物 |
|
||||
|-----|---------|--------|--------|
|
||||
| **前端** | 装修 + 菜单 + 服务员 | 用户能看到、能交互的一切 | 网页、小程序、App 界面 |
|
||||
| **后端** | 厨房 + 仓库 | 处理业务逻辑、存储数据 | API、数据库、服务器程序 |
|
||||
| **移动端** | 外卖窗口 | 手机上的应用体验 | iOS/Android App |
|
||||
| **AI/算法** | 研发部 | 让系统变"聪明" | 推荐模型、图像识别、智能对话 |
|
||||
| **运维/DevOps** | 物业 + 安保 | 保证系统稳定运行 | 部署脚本、监控系统、安全防护 |
|
||||
| **数据工程** | 财务 + 分析师 | 数据采集、存储、分析 | 数据管道、报表、仪表盘 |
|
||||
|
||||
### 1.2 各领域的技术栈速览
|
||||
|
||||
不要被这些名词吓到,这里只是让你"见过"它们:
|
||||
|
||||
| 领域 | 核心语言 | 常用框架/工具 | 典型产出 |
|
||||
|-----|---------|--------------|---------|
|
||||
| 前端 | JavaScript, TypeScript | React, Vue, CSS | 网页、管理后台 |
|
||||
| 后端 | Node.js, Go, Java, Python | Express, Gin, Spring | API 服务 |
|
||||
| 移动端 | Swift, Kotlin, Dart | SwiftUI, Jetpack, Flutter | 手机 App |
|
||||
| AI/算法 | Python | PyTorch, TensorFlow | 模型、算法 |
|
||||
| 运维 | Shell, Python | Docker, Kubernetes | 部署方案 |
|
||||
|
||||
::: tip 💡 给新手的建议
|
||||
不要试图一次学完所有东西。先选一个方向深入,建立"根据地",再横向扩展。全栈不是"什么都懂一点",而是"有一个核心强项,其他方向能用"。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 什么是前端?
|
||||
|
||||
### 2.1 一句话定义
|
||||
|
||||
**前端 = 用户能直接看到、点击、交互的部分。**
|
||||
|
||||
当你打开一个网页:
|
||||
- 页面的布局、颜色、字体 → 前端
|
||||
- 点击按钮后的动画效果 → 前端
|
||||
- 表单输入、数据展示 → 前端
|
||||
- 页面怎么适配手机屏幕 → 前端
|
||||
|
||||
### 2.2 前端三件套
|
||||
|
||||
<FrontendTriadDemo />
|
||||
|
||||
**用"装修房子"来比喻**:
|
||||
|
||||
| 技术 | 装修角色 | 职责 |
|
||||
|-----|---------|------|
|
||||
| **HTML** | 房屋结构 | 墙在哪、门在哪、房间怎么划分 |
|
||||
| **CSS** | 装饰风格 | 墙什么颜色、家具怎么摆、灯光效果 |
|
||||
| **JavaScript** | 智能家居 | 开关灯、窗帘自动开合、安防系统 |
|
||||
|
||||
### 2.3 前端框架:为什么要用?
|
||||
|
||||
原生 HTML/CSS/JS 能写网页,为什么还要学 React、Vue 这些框架?
|
||||
|
||||
<FrontendFrameworkDemo />
|
||||
|
||||
**核心原因**:当页面变得复杂(比如淘宝、微信网页版),直接操作 DOM 会变得非常混乱。框架帮你"管理复杂性"。
|
||||
|
||||
### 2.4 前端工程师的一天
|
||||
|
||||
```
|
||||
9:00 查看设计稿,理解要做什么功能
|
||||
10:00 用 React/Vue 写组件代码
|
||||
12:00 午休
|
||||
14:00 和后端对接 API,调试数据展示
|
||||
16:00 修复 bug,优化页面性能
|
||||
18:00 代码评审,和团队讨论技术方案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 什么是后端?
|
||||
|
||||
### 3.1 一句话定义
|
||||
|
||||
**后端 = 用户看不到,但支撑整个系统运转的逻辑。**
|
||||
|
||||
当你网购下单:
|
||||
- 验证你的账号密码 → 后端
|
||||
- 检查商品库存 → 后端
|
||||
- 计算优惠价格 → 后端
|
||||
- 生成订单、扣款 → 后端
|
||||
- 通知仓库发货 → 后端
|
||||
|
||||
### 3.2 后端的核心职责
|
||||
|
||||
<BackendCoreDemo />
|
||||
|
||||
**用"餐厅厨房"来比喻**:
|
||||
|
||||
| 后端职责 | 厨房类比 | 具体内容 |
|
||||
|---------|---------|---------|
|
||||
| **API 设计** | 菜单设计 | 定义"用户能点什么菜"、"怎么点" |
|
||||
| **业务逻辑** | 烹饪过程 | 处理订单、计算价格、验证权限 |
|
||||
| **数据存储** | 仓库管理 | 把数据存进数据库、查询数据 |
|
||||
| **性能优化** | 厨房效率 | 缓存、异步处理、负载均衡 |
|
||||
| **安全防护** | 食品安全 | 防止 SQL 注入、权限控制 |
|
||||
|
||||
### 3.3 后端语言怎么选?
|
||||
|
||||
| 语言 | 特点 | 适合场景 |
|
||||
|-----|------|---------|
|
||||
| **Node.js** | 前端友好,JavaScript 全栈 | 中小型项目、快速原型 |
|
||||
| **Go** | 高性能、并发强 | 高并发服务、微服务架构 |
|
||||
| **Java** | 生态成熟、企业级 | 大型企业系统、银行 |
|
||||
| **Python** | 简洁、AI 生态好 | 数据处理、AI 服务 |
|
||||
|
||||
::: tip 💡 新手建议
|
||||
如果你已经会 JavaScript(前端基础),Node.js 是最自然的后端入门选择。一套语言,前后端都能写。
|
||||
:::
|
||||
|
||||
### 3.4 后端工程师的一天
|
||||
|
||||
```
|
||||
9:00 查看 API 需求文档
|
||||
10:00 设计数据库表结构
|
||||
11:00 写 API 接口代码
|
||||
14:00 和前端联调,修复接口问题
|
||||
16:00 优化慢查询,处理线上问题
|
||||
18:00 代码评审,写技术文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 编程语言图谱
|
||||
|
||||
### 4.1 编程语言是什么?
|
||||
|
||||
**编程语言 = 人类和计算机沟通的桥梁。**
|
||||
|
||||
计算机只认识 0 和 1,人类习惯说自然语言。编程语言是中间层:
|
||||
- 人类用编程语言写代码(比 0/1 好理解)
|
||||
- 计算机把编程语言翻译成机器指令
|
||||
|
||||
### 4.2 语言分类
|
||||
|
||||
<ProgrammingLanguageMapDemo />
|
||||
|
||||
**按运行方式分类**:
|
||||
|
||||
| 类型 | 原理 | 代表语言 | 特点 |
|
||||
|-----|------|---------|------|
|
||||
| **编译型** | 先翻译成机器码,再运行 | C, C++, Go, Rust | 运行快,编译慢 |
|
||||
| **解释型** | 边翻译边运行 | Python, JavaScript, Ruby | 开发快,运行慢 |
|
||||
| **字节码型** | 折中方案 | Java, Kotlin, C# | 平衡性能和开发效率 |
|
||||
|
||||
**按类型系统分类**:
|
||||
|
||||
| 类型 | 特点 | 代表语言 |
|
||||
|-----|------|---------|
|
||||
| **静态类型** | 变量类型写代码时确定 | Java, TypeScript, Go |
|
||||
| **动态类型** | 变量类型运行时确定 | Python, JavaScript, Ruby |
|
||||
| **强类型** | 类型检查严格,不自动转换 | Python, Java |
|
||||
| **弱类型** | 类型检查宽松,会自动转换 | JavaScript, PHP |
|
||||
|
||||
### 4.3 该学哪门语言?
|
||||
|
||||
<LanguageSelectionDemo />
|
||||
|
||||
::: tip 💡 选择原则
|
||||
没有"最好的语言",只有"最适合场景的语言"。新手建议:
|
||||
1. **先学一门,学深**:建立编程思维
|
||||
2. **再学第二门,对比**:理解语言设计差异
|
||||
3. **按需学习**:根据项目需求选择
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 全栈工程师:前后端通吃
|
||||
|
||||
### 5.1 什么是全栈?
|
||||
|
||||
**全栈工程师 = 能独立完成前端 + 后端开发的工程师。**
|
||||
|
||||
<FullstackSkillDemo />
|
||||
|
||||
### 5.2 全栈的优势
|
||||
|
||||
| 优势 | 说明 |
|
||||
|-----|------|
|
||||
| **独立完成项目** | 从需求到上线,一个人搞定 |
|
||||
| **沟通成本低** | 不需要前后端来回扯皮 |
|
||||
| **技术视野广** | 理解整个系统如何运作 |
|
||||
| **创业友好** | 快速验证想法,MVP 开发 |
|
||||
|
||||
### 5.3 全栈的挑战
|
||||
|
||||
| 挑战 | 说明 |
|
||||
|-----|------|
|
||||
| **深度 vs 广度** | 容易"什么都懂一点,什么都不精" |
|
||||
| **技术更新快** | 前后端技术都在快速演进 |
|
||||
| **精力分散** | 需要同时关注多个领域 |
|
||||
|
||||
### 5.4 全栈成长建议
|
||||
|
||||
```
|
||||
第 1 阶段:建立根据地
|
||||
└── 选一个方向深入(建议从前端或后端开始)
|
||||
└── 达到能独立完成项目的水平
|
||||
|
||||
第 2 阶段:横向扩展
|
||||
└── 学习另一个方向的基础
|
||||
└── 能完成简单的全栈项目
|
||||
|
||||
第 3 阶段:融会贯通
|
||||
└── 理解前后端如何协作
|
||||
└── 能设计完整的技术架构
|
||||
|
||||
第 4 阶段:持续精进
|
||||
└── 在某个领域保持深度
|
||||
└── 其他领域保持"能用"水平
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. AI 算法工程师:让机器学会思考
|
||||
|
||||
### 6.1 AI 工程师 vs 传统开发
|
||||
|
||||
<AIvsTraditionalDemo />
|
||||
|
||||
| 维度 | 传统开发 | AI 算法工程师 |
|
||||
|-----|---------|--------------|
|
||||
| **核心任务** | 实现确定性的业务逻辑 | 训练模型、优化算法 |
|
||||
| **思维方式** | "如果 A 则执行 B" | "让机器从数据中学习规律" |
|
||||
| **代码产出** | 功能模块、系统 | 模型、训练脚本 |
|
||||
| **调试方式** | 断点、日志 | 看指标、调超参 |
|
||||
| **成功标准** | 功能正确、无 bug | 准确率、召回率达标 |
|
||||
|
||||
### 6.2 AI 工程师的技能树
|
||||
|
||||
```
|
||||
AI 算法工程师
|
||||
│
|
||||
├── 数学基础
|
||||
│ ├── 线性代数(矩阵运算)
|
||||
│ ├── 概率统计(分布、期望)
|
||||
│ └── 微积分(梯度、优化)
|
||||
│
|
||||
├── 编程能力
|
||||
│ ├── Python(主力语言)
|
||||
│ ├── PyTorch / TensorFlow(深度学习框架)
|
||||
│ └── 数据处理(Pandas, NumPy)
|
||||
│
|
||||
├── 机器学习
|
||||
│ ├── 监督学习(分类、回归)
|
||||
│ ├── 无监督学习(聚类、降维)
|
||||
│ └── 模型评估方法
|
||||
│
|
||||
└── 深度学习
|
||||
├── 神经网络基础
|
||||
├── CNN(图像)
|
||||
├── RNN / Transformer(序列)
|
||||
└── 大模型(LLM)
|
||||
```
|
||||
|
||||
### 6.3 AI 工程师的一天
|
||||
|
||||
```
|
||||
9:00 查看模型训练结果,分析指标
|
||||
10:00 数据预处理,清洗训练数据
|
||||
12:00 午休
|
||||
14:00 调整模型结构,尝试新方案
|
||||
16:00 跑实验,对比不同方案效果
|
||||
18:00 写实验报告,和团队讨论下一步
|
||||
```
|
||||
|
||||
### 6.4 Vibe Coding 时代的 AI 工程师
|
||||
|
||||
AI 辅助开发对 AI 工程师的影响:
|
||||
|
||||
| 变化 | 说明 |
|
||||
|-----|------|
|
||||
| **代码生成** | AI 能生成训练脚本、数据处理代码 |
|
||||
| **论文阅读** | AI 能帮你总结论文要点 |
|
||||
| **实验记录** | AI 能帮你整理实验结果 |
|
||||
| **不变的是** | 对问题的理解、对结果的判断、对方向的把握 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 成长路径:从入门到精通
|
||||
|
||||
### 7.1 3-5 年成长路线图
|
||||
|
||||
<CareerPathDemo />
|
||||
|
||||
### 7.2 各阶段能力要求
|
||||
|
||||
| 阶段 | 时间 | 核心能力 | 典型产出 |
|
||||
|-----|------|---------|---------|
|
||||
| **入门** | 0-1 年 | 掌握一门语言 + 基础工具 | 能完成简单功能模块 |
|
||||
| **进阶** | 1-2 年 | 熟悉一个技术栈 + 工程化 | 能独立完成中型项目 |
|
||||
| **高级** | 2-3 年 | 深入一个领域 + 架构能力 | 能设计系统方案 |
|
||||
| **资深** | 3-5 年 | 技术深度 + 业务理解 + 团队协作 | 能主导大型项目 |
|
||||
|
||||
### 7.3 Vibe Coding 时代的学习策略
|
||||
|
||||
<LearningStrategyDemo />
|
||||
|
||||
::: tip 💡 核心建议
|
||||
1. **基础比工具重要**:语言特性、数据结构、算法思维是根基
|
||||
2. **实践比理论重要**:做项目是最好的学习方式
|
||||
3. **思考比记忆重要**:理解"为什么"比记住"怎么做"更有价值
|
||||
4. **AI 是工具不是拐杖**:用 AI 加速学习,不要用 AI 替代思考
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结:Vibe Coding 时代的核心竞争力
|
||||
|
||||
回顾本章,我们建立了计算机领域的全局认知:
|
||||
|
||||
1. **领域划分**:前端、后端、移动端、AI、运维、数据——各有侧重
|
||||
2. **技术选型**:没有最好的技术,只有最适合场景的技术
|
||||
3. **成长路径**:先深后广,建立根据地再横向扩展
|
||||
4. **AI 时代**:AI 能帮你写代码,但不能替你思考
|
||||
|
||||
### Vibe Coding 时代的三层能力
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 第 3 层:判断力(AI 替代不了) │
|
||||
│ - 知道什么是对的 │
|
||||
│ - 知道什么是好的 │
|
||||
│ - 知道该往哪个方向走 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 第 2 层:架构思维(AI 辅助) │
|
||||
│ - 系统设计能力 │
|
||||
│ - 模块划分能力 │
|
||||
│ - 技术选型能力 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 第 1 层:代码实现(AI 擅长) │
|
||||
│ - 语法编写 │
|
||||
│ - API 调用 │
|
||||
│ - 常见模式实现 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
@@ -0,0 +1,566 @@
|
||||
# 领域特定语言(DSL):后端世界中那些"不像代码的代码"
|
||||
|
||||
::: tip 前言
|
||||
在一个真实案例中,工程师 Armin 在新公司用 AI 构建了一套基础设施服务,总计约 4 万行代码(Go + YAML + Pulumi + SDK 胶水代码),其中超过 90% 由 AI 生成。这个案例中出现了许多初学者不熟悉的术语:YAML、Pulumi、HCL、Lua、SDK 胶水代码……它们既不是 Python,也不是 JavaScript,却在后端项目中无处不在。本文将从一个统一的视角——**领域特定语言(DSL)**——来系统地介绍这些技术。
|
||||
:::
|
||||
|
||||
**本文的学习目标**
|
||||
|
||||
在后端开发中,除了用通用编程语言(Python、Go、Java 等)编写的业务逻辑之外,还存在大量**用途各异、语法各异、但都不属于通用编程语言**的文件和代码。它们有一个共同的上位概念:**DSL(Domain-Specific Language,领域特定语言)**。
|
||||
|
||||
学完本文后,你将能够:
|
||||
|
||||
- 理解 DSL 与通用编程语言(GPL)的本质区别
|
||||
- 掌握 DSL 的分类体系:数据序列化格式、嵌入式脚本语言、基础设施定义语言
|
||||
- 区分 XML、JSON、YAML、TOML、CSV、Protobuf 等数据格式的适用场景
|
||||
- 理解 Lua 等嵌入式脚本语言的设计目的
|
||||
- 解释 Terraform(HCL)和 Pulumi 的原理与区别
|
||||
- 理解 OpenAPI 规范与 SDK 自动生成的工作原理
|
||||
- 判断哪些类型的代码适合交给 AI 生成
|
||||
|
||||
| 章节 | 主题 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | DSL 总论 | DSL vs GPL 的定义、分类体系与全景图 |
|
||||
| **第 2 章** | 数据序列化格式 | XML、JSON、YAML、TOML、CSV、Protobuf 等 |
|
||||
| **第 3 章** | 嵌入式脚本语言 | Lua 等语言的设计哲学与典型应用 |
|
||||
| **第 4 章** | 基础设施即代码 | Terraform(HCL)、Pulumi 的原理与对比 |
|
||||
| **第 5 章** | 胶水代码与 SDK 生成 | OpenAPI 规范与客户端代码自动生成 |
|
||||
| **第 6 章** | AI 与 DSL 的关系 | 为什么 AI 特别擅长生成 DSL 代码 |
|
||||
|
||||
---
|
||||
|
||||
## 1. DSL 总论:通用语言之外的另一个世界
|
||||
|
||||
### 1.1 什么是 DSL?
|
||||
|
||||
**DSL(Domain-Specific Language,领域特定语言)** 是为某个特定领域或特定任务设计的语言。与之相对的是 **GPL(General-Purpose Language,通用编程语言)**,如 Python、Java、Go、C++ 等——它们被设计为可以解决任意计算问题。
|
||||
|
||||
两者的核心区别:
|
||||
|
||||
| 维度 | GPL(通用编程语言) | DSL(领域特定语言) |
|
||||
|------|-------------------|-------------------|
|
||||
| **设计目标** | 解决任意计算问题 | 解决某个特定领域的问题 |
|
||||
| **表达范围** | 图灵完备,理论上可以计算任何东西 | 通常有意限制表达范围 |
|
||||
| **学习成本** | 较高,需要理解完整的语言体系 | 较低,只需理解该领域的概念 |
|
||||
| **典型代表** | Python、Java、Go、C++、JavaScript | SQL、HTML/CSS、正则表达式、YAML、HCL |
|
||||
|
||||
你其实早就在使用 DSL 了:
|
||||
|
||||
- **SQL** 是数据库查询领域的 DSL——你用 `SELECT * FROM users WHERE age > 18` 来查数据,而不是用 Python 手写遍历逻辑
|
||||
- **HTML/CSS** 是网页结构与样式领域的 DSL——你用标签和属性描述页面,而不是用 C++ 操作像素
|
||||
- **正则表达式** 是文本模式匹配领域的 DSL——你用 `\d{3}-\d{4}` 匹配电话号码,而不是手写字符比较循环
|
||||
|
||||
### 1.2 DSL 的分类
|
||||
|
||||
DSL 可以按照"是否具备图灵完备性"分为两大类:
|
||||
|
||||
**外部 DSL(External DSL)**
|
||||
|
||||
拥有独立的语法和解析器,不依附于任何通用编程语言。用户编写的代码由专用的解释器或编译器处理。
|
||||
|
||||
- 纯数据描述型:JSON、YAML、XML、TOML、CSV、Protobuf(不含任何逻辑)
|
||||
- 查询/操作型:SQL、GraphQL、正则表达式(有限的逻辑能力)
|
||||
- 领域建模型:HCL(Terraform)、Dockerfile、Nginx 配置语法(声明式描述特定领域的状态)
|
||||
|
||||
**内部 DSL(Internal DSL / Embedded DSL)**
|
||||
|
||||
寄生在某门通用编程语言内部,利用宿主语言的语法来构建领域专用的表达方式。代码本身是合法的宿主语言代码,但读起来像是一门专用语言。
|
||||
|
||||
- Pulumi(用 TypeScript/Python/Go 编写,但 API 设计得像声明式配置)
|
||||
- Ruby on Rails 的路由定义(`get '/users', to: 'users#index'`,合法的 Ruby 代码,但读起来像配置)
|
||||
- 测试框架中的断言语法(`expect(value).toBe(42)`,合法的 JavaScript,但读起来像自然语言)
|
||||
|
||||
### 1.3 后端项目中的 DSL 全景图
|
||||
|
||||
在一个典型的后端项目中,你会遇到以下几类 DSL:
|
||||
|
||||
```
|
||||
后端项目中的 DSL
|
||||
├── 数据序列化格式(描述数据结构)
|
||||
│ ├── 文本格式:JSON、YAML、XML、TOML、CSV、INI
|
||||
│ └── 二进制格式:Protobuf、MessagePack、Avro、BSON
|
||||
├── 嵌入式脚本语言(可编程的配置层)
|
||||
│ ├── Lua(游戏引擎、Nginx、Redis)
|
||||
│ ├── GDScript(Godot 引擎)
|
||||
│ └── Jsonnet(配置模板生成)
|
||||
├── 基础设施与运维 DSL(声明式描述系统状态)
|
||||
│ ├── HCL(Terraform)
|
||||
│ ├── Dockerfile / Docker Compose YAML
|
||||
│ └── Nginx / Apache 配置语法
|
||||
└── 接口描述语言(描述 API 契约)
|
||||
├── OpenAPI / Swagger
|
||||
├── Protocol Buffers(.proto 文件)
|
||||
└── GraphQL Schema
|
||||
```
|
||||
|
||||
理解了这张全景图,后续章节将逐一展开每个分支。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据序列化格式:用文本描述结构化数据
|
||||
|
||||
### 2.1 什么是数据序列化?
|
||||
|
||||
**序列化(Serialization)** 是指将内存中的数据结构(对象、字典、数组等)转换为一种可存储或可传输的文本/字节流的过程。反过来,从文本/字节流还原为内存中的数据结构,称为**反序列化(Deserialization)**。
|
||||
|
||||
数据序列化格式是 DSL 中最基础的一类——它们属于纯数据描述型外部 DSL,不具备任何逻辑能力,只负责静态地描述"值是什么"。
|
||||
|
||||
### 2.2 为什么需要这些格式?
|
||||
|
||||
假设你开发了一个后端服务,数据库地址为 `localhost:5432`。如果将这个地址硬编码在源代码中,本地开发没有问题,但部署到生产环境时,数据库地址变为 `db.prod.company.com:5432`,你就需要修改源代码并重新编译。
|
||||
|
||||
工程实践中的通用做法是:**将可变的参数从代码中分离出来,存放在独立的配置文件中。** 程序在启动时读取配置文件,根据其中的值来决定行为。
|
||||
|
||||
除了配置之外,数据序列化格式还广泛用于:系统间的数据交换(API 请求/响应)、数据持久化存储、跨语言通信等场景。
|
||||
|
||||
### 2.3 人类可读的文本格式
|
||||
|
||||
以下是工程中最常见的文本序列化格式,按历史顺序介绍。
|
||||
|
||||
**INI**
|
||||
|
||||
最早期的配置格式,起源于 Windows 系统。结构简单,由节(section)和键值对组成:
|
||||
|
||||
```ini
|
||||
[database]
|
||||
host = localhost
|
||||
port = 5432
|
||||
|
||||
[server]
|
||||
debug = true
|
||||
```
|
||||
|
||||
优点是可读性强。局限在于不支持嵌套结构和数组类型,无法表达复杂配置。目前主要出现在遗留系统和部分 Linux 配置中(如 `php.ini`、`my.cnf`)。
|
||||
|
||||
**CSV**
|
||||
|
||||
**CSV(Comma-Separated Values,逗号分隔值)** 是最简单的表格数据格式:
|
||||
|
||||
```csv
|
||||
name,age,city
|
||||
Alice,30,Beijing
|
||||
Bob,25,Shanghai
|
||||
```
|
||||
|
||||
每行是一条记录,字段之间用逗号分隔。CSV 广泛用于数据导入导出、电子表格交换、数据分析管道。它的局限是只能表达扁平的二维表格,不支持嵌套结构,且没有类型信息(所有值都是字符串)。
|
||||
|
||||
**XML**
|
||||
|
||||
**XML(eXtensible Markup Language,可扩展标记语言)** 诞生于 1998 年,曾经是数据交换的主流标准:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config>
|
||||
<database>
|
||||
<host>localhost</host>
|
||||
<port>5432</port>
|
||||
</database>
|
||||
<server>
|
||||
<debug>true</debug>
|
||||
<allowed_origins>
|
||||
<origin>https://example.com</origin>
|
||||
<origin>https://app.example.com</origin>
|
||||
</allowed_origins>
|
||||
</server>
|
||||
</config>
|
||||
```
|
||||
|
||||
XML 的表达力非常强,支持嵌套、属性、命名空间、Schema 验证等高级特性。但它的语法冗长——大量的开闭标签导致信噪比低,手动编写和阅读的体验较差。
|
||||
|
||||
XML 在以下领域仍然广泛使用:
|
||||
- Java 生态(Maven 的 `pom.xml`、Spring 配置、Android 布局文件)
|
||||
- 企业级 Web 服务(SOAP 协议)
|
||||
- 办公文档格式(`.docx`、`.xlsx` 本质上是 ZIP 压缩的 XML 文件集合)
|
||||
- RSS/Atom 订阅源、SVG 矢量图形
|
||||
|
||||
**JSON**
|
||||
|
||||
**JSON(JavaScript Object Notation)** 诞生于 2001 年,因其简洁性迅速取代 XML 成为 Web API 数据交换的事实标准:
|
||||
|
||||
```json
|
||||
{
|
||||
"database": {
|
||||
"host": "localhost",
|
||||
"port": 5432
|
||||
},
|
||||
"server": {
|
||||
"debug": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
优点是结构清晰,几乎所有编程语言都有原生解析支持。主要缺点是**不支持注释**,且大量的括号和引号在手动编写时容易出错。JSON 同时也是前端项目配置的标准格式(`package.json`、`tsconfig.json`)。
|
||||
|
||||
**YAML**
|
||||
|
||||
**YAML(YAML Ain't Markup Language)** 同样诞生于 2001 年,是目前后端和 DevOps 领域使用最广泛的配置格式。Docker Compose、Kubernetes、GitHub Actions 等工具均采用 YAML:
|
||||
|
||||
```yaml
|
||||
# 数据库配置
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
debug: true
|
||||
allowed_origins:
|
||||
- https://example.com
|
||||
- https://app.example.com
|
||||
```
|
||||
|
||||
优点是支持注释、语法简洁、可表达复杂嵌套结构。缺点是**依赖缩进来表示层级关系**,缩进错误会导致解析失败,这是初学者最常遇到的问题。
|
||||
|
||||
> 补充:YAML 的全称 "YAML Ain't Markup Language" 是一个递归缩写。
|
||||
|
||||
**TOML**
|
||||
|
||||
**TOML(Tom's Obvious Minimal Language)** 诞生于 2013 年,被 Rust 的包管理器 Cargo 和 Python 的 `pyproject.toml` 采用:
|
||||
|
||||
```toml
|
||||
[database]
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
|
||||
[server]
|
||||
debug = true
|
||||
allowed_origins = [
|
||||
"https://example.com",
|
||||
"https://app.example.com"
|
||||
]
|
||||
```
|
||||
|
||||
TOML 试图兼顾 INI 的简洁性和 YAML 的表达力,同时避免缩进敏感带来的问题。
|
||||
|
||||
### 2.4 二进制序列化格式
|
||||
|
||||
上述格式都是人类可读的文本。在对性能和体积有更高要求的场景中,还存在一类**二进制序列化格式**——它们牺牲可读性,换取更小的体积和更快的解析速度。
|
||||
|
||||
| 格式 | 开发方 | 特点 | 典型使用场景 |
|
||||
|------|-------|------|------------|
|
||||
| **Protocol Buffers (Protobuf)** | Google | 需要预定义 `.proto` Schema 文件,强类型,体积极小 | gRPC 通信、Google 内部服务、高性能微服务 |
|
||||
| **MessagePack** | 社区 | 类似 JSON 的二进制版本,无需 Schema | Redis 内部编码、跨语言高性能通信 |
|
||||
| **Avro** | Apache | 支持 Schema 演进,适合大数据场景 | Hadoop / Kafka 生态的数据序列化 |
|
||||
| **BSON** | MongoDB | JSON 的二进制扩展,支持更多数据类型 | MongoDB 数据库内部存储格式 |
|
||||
|
||||
以 Protocol Buffers 为例,需要先定义 Schema:
|
||||
|
||||
```protobuf
|
||||
// user.proto
|
||||
syntax = "proto3";
|
||||
|
||||
message User {
|
||||
string name = 1;
|
||||
int32 age = 2;
|
||||
string email = 3;
|
||||
}
|
||||
```
|
||||
|
||||
然后通过编译器(`protoc`)自动生成各语言的序列化/反序列化代码。这种"先定义 Schema,再生成代码"的模式与后文将介绍的 OpenAPI SDK 生成思路一致。
|
||||
|
||||
### 2.5 完整对比
|
||||
|
||||
| 格式 | 类型 | 诞生年代 | 可读性 | 支持注释 | 典型使用场景 |
|
||||
|------|------|---------|--------|---------|------------|
|
||||
| **INI** | 文本 | 1980s | 高 | ✅ | 系统配置、遗留项目 |
|
||||
| **CSV** | 文本 | 1972 | 高 | ❌ | 数据导入导出、表格交换 |
|
||||
| **XML** | 文本 | 1998 | 中 | ✅ | Java 生态、企业级 Web 服务、文档格式 |
|
||||
| **JSON** | 文本 | 2001 | 高 | ❌ | Web API 数据交换、前端配置 |
|
||||
| **YAML** | 文本 | 2001 | 高 | ✅ | Docker、K8s、CI/CD、后端服务配置 |
|
||||
| **TOML** | 文本 | 2013 | 高 | ✅ | Rust / Python 项目配置 |
|
||||
| **Protobuf** | 二进制 | 2008 | 无 | — | gRPC、高性能微服务通信 |
|
||||
| **MessagePack** | 二进制 | 2008 | 无 | — | 高性能跨语言通信 |
|
||||
| **Avro** | 二进制 | 2009 | 无 | — | Hadoop / Kafka 大数据管道 |
|
||||
| **BSON** | 二进制 | 2009 | 无 | — | MongoDB 内部存储 |
|
||||
|
||||
**要点**:所有这些格式的本质功能相同——**将结构化数据转换为可存储、可传输的形式**。文本格式优先考虑人类可读性和易编辑性;二进制格式优先考虑解析性能和传输体积。选择哪种格式取决于具体场景的需求权衡。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. 嵌入式脚本语言:可编程的配置层
|
||||
|
||||
### 3.1 概念定义
|
||||
|
||||
Python、JavaScript、Go 等语言是通用编程语言(General-Purpose Language),它们可以独立运行,构建完整的应用程序。
|
||||
|
||||
与之不同,还有一类语言**专门设计为嵌入到其他宿主程序中运行**,为宿主程序提供可编程的扩展能力。这类语言被称为**嵌入式脚本语言(Embedded Scripting Language)**。
|
||||
|
||||
它们解决的核心问题是:**当静态配置文件(YAML/JSON)的表达力不够,需要引入条件判断、循环等逻辑时,如何在不修改宿主程序源码的前提下实现动态行为。**
|
||||
|
||||
### 3.2 Lua:最具代表性的嵌入式脚本语言
|
||||
|
||||
Lua(葡萄牙语中"月亮"的意思)是一门极其轻量的脚本语言,整个解释器编译后仅几百 KB。它的设计目标不是独立运行,而是作为可嵌入的扩展层。
|
||||
|
||||
Lua 的典型应用场景:
|
||||
|
||||
- **游戏引擎**:《魔兽世界》的插件系统、《Roblox》的游戏脚本均使用 Lua。游戏引擎用 C/C++ 实现核心渲染和物理计算,将关卡逻辑、NPC 对话等频繁变动的部分交给 Lua 脚本。这样,策划人员修改游戏内容时不需要重新编译引擎。
|
||||
|
||||
- **Web 服务器**:OpenResty 将 Lua 嵌入 Nginx 内部,使运维人员可以用 Lua 脚本实现请求过滤、限流、鉴权等逻辑,而无需修改 Nginx 的 C 源码。
|
||||
|
||||
- **数据库**:Redis 支持将 Lua 脚本发送到服务端执行,用于实现需要原子性保证的复合操作(如"先读后写")。
|
||||
|
||||
以下是一段嵌入在 Nginx(OpenResty)中的 Lua 脚本示例:
|
||||
|
||||
```lua
|
||||
-- 功能:对 /api/secret 路径进行 token 鉴权
|
||||
local uri = ngx.var.uri
|
||||
local token = ngx.req.get_headers()["Authorization"]
|
||||
|
||||
if uri == "/api/secret" and token ~= "Bearer my-secret-token" then
|
||||
ngx.status = 403
|
||||
ngx.say("Access denied")
|
||||
return ngx.exit(403)
|
||||
end
|
||||
```
|
||||
|
||||
### 3.3 其他嵌入式脚本语言
|
||||
|
||||
| 语言 | 宿主环境 | 典型用途 |
|
||||
|------|---------|---------|
|
||||
| **Lua** | 游戏引擎、Nginx(OpenResty)、Redis | 游戏逻辑、网关策略、缓存操作 |
|
||||
| **VimScript / Lua** | Vim / Neovim 编辑器 | 编辑器插件开发 |
|
||||
| **Emacs Lisp** | Emacs 编辑器 | 编辑器行为自定义 |
|
||||
| **GDScript** | Godot 游戏引擎 | 游戏逻辑脚本 |
|
||||
| **Jsonnet** | Kubernetes 生态 / 配置生成工具 | 模板化生成大量相似的 JSON/YAML 配置 |
|
||||
|
||||
**要点**:嵌入式脚本语言在 DSL 分类中属于**内部 DSL 与外部 DSL 的交界地带**——它们是独立的语言(有自己的语法和解释器),但设计目标是嵌入宿主程序运行,而非独立构建应用。它们填补了"静态配置文件"(纯数据描述型 DSL)与"通用编程语言"(GPL)之间的空白:当配置需要表达逻辑(条件判断、循环、函数调用)时,嵌入一门轻量脚本语言是工程上的标准解决方案。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. 基础设施即代码(Infrastructure as Code)
|
||||
|
||||
### 4.1 什么是"基础设施"
|
||||
|
||||
在后端工程中,"基础设施"(Infrastructure)指的是应用程序运行所依赖的底层资源:
|
||||
|
||||
- 计算资源:服务器(虚拟机或容器)
|
||||
- 数据存储:数据库实例、对象存储桶
|
||||
- 网络:防火墙规则、负载均衡器、DNS 配置
|
||||
- 中间件:消息队列、缓存集群
|
||||
|
||||
在云计算时代,这些资源通过云服务商(如 AWS、阿里云、腾讯云)的控制台以图形界面的方式创建和管理。
|
||||
|
||||
### 4.2 手动管理的局限性
|
||||
|
||||
通过控制台手动操作在小规模项目中可行,但随着项目规模增长,会暴露以下问题:
|
||||
|
||||
1. **不可重复**:操作步骤没有记录,无法精确复现同一套环境
|
||||
2. **不可审计**:无法追溯"谁在什么时间修改了什么配置"
|
||||
3. **不可协作**:操作过程无法纳入版本控制,无法进行代码审查
|
||||
4. **容易出错**:手动操作在生产环境中存在误操作风险
|
||||
|
||||
**基础设施即代码(Infrastructure as Code,简称 IaC)** 的核心思想是:**用代码来声明式地定义基础设施资源,使其具备版本控制、自动化执行和可重复部署的能力。**
|
||||
|
||||
### 4.3 Terraform
|
||||
|
||||
Terraform 是目前使用最广泛的 IaC 工具,由 HashiCorp 公司开发。它使用专用的 **HCL(HashiCorp Configuration Language)** 语言。
|
||||
|
||||
Terraform 采用**声明式**范式:用户描述期望的最终状态,Terraform 自动计算从当前状态到目标状态所需的操作。
|
||||
|
||||
```hcl
|
||||
# 定义一台云服务器
|
||||
resource "aws_instance" "my_server" {
|
||||
ami = "ami-0c55b159cbfafe1f0" # 操作系统镜像
|
||||
instance_type = "t3.micro" # 实例规格
|
||||
|
||||
tags = {
|
||||
Name = "my-first-server"
|
||||
}
|
||||
}
|
||||
|
||||
# 定义一个 PostgreSQL 数据库实例
|
||||
resource "aws_db_instance" "my_database" {
|
||||
engine = "postgres"
|
||||
instance_class = "db.t3.micro"
|
||||
username = "admin"
|
||||
password = "please-use-secrets-manager"
|
||||
}
|
||||
```
|
||||
|
||||
执行流程:
|
||||
|
||||
```bash
|
||||
terraform plan # 预览将要执行的变更
|
||||
terraform apply # 确认并执行,自动在云平台创建资源
|
||||
```
|
||||
|
||||
### 4.4 Pulumi
|
||||
|
||||
Pulumi 提供了另一种思路:**直接使用通用编程语言(TypeScript、Python、Go 等)来定义基础设施**,而非学习专用的 HCL 语法。
|
||||
|
||||
同样的服务器定义,用 Pulumi + TypeScript 表达如下:
|
||||
|
||||
```typescript
|
||||
import * as aws from "@pulumi/aws";
|
||||
|
||||
const server = new aws.ec2.Instance("my-server", {
|
||||
ami: "ami-0c55b159cbfafe1f0",
|
||||
instanceType: "t3.micro",
|
||||
tags: { Name: "my-first-server" },
|
||||
});
|
||||
|
||||
const bucket = new aws.s3.Bucket("my-bucket", {
|
||||
acl: "private",
|
||||
});
|
||||
|
||||
export const serverIp = server.publicIp;
|
||||
```
|
||||
|
||||
由于使用的是通用编程语言,开发者可以利用循环、条件判断、函数抽象等语言特性来处理复杂的基础设施逻辑。
|
||||
|
||||
### 4.5 Terraform 与 Pulumi 的对比
|
||||
|
||||
| 维度 | Terraform | Pulumi |
|
||||
|------|-----------|--------|
|
||||
| **语言** | HCL(专用语言) | TypeScript / Python / Go 等通用语言 |
|
||||
| **学习成本** | 需要学习 HCL 语法 | 使用已掌握的编程语言,学习成本较低 |
|
||||
| **社区生态** | 非常成熟,几乎覆盖所有云服务商 | 快速增长中,但规模小于 Terraform |
|
||||
| **适用场景** | 运维团队主导的标准化基础设施管理 | 开发者主导的项目,需要复杂逻辑的场景 |
|
||||
| **AI 代码生成适配度** | 高(模式固定) | 很高(本质是通用编程语言代码) |
|
||||
|
||||
**要点**:IaC 工具中的 HCL 是一种典型的外部 DSL——它有独立的语法和解析器,专门用于声明式描述基础设施状态。而 Pulumi 则采用内部 DSL 的策略——用通用编程语言的语法来表达领域特定的概念。两者目标一致(将基础设施管理从手动操作转为代码驱动),路径不同(专用语言 vs 通用语言)。代码可以纳入 Git 版本控制、进行团队审查、自动化执行和回滚。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. 胶水代码与 SDK 自动生成
|
||||
|
||||
### 5.1 什么是胶水代码
|
||||
|
||||
在软件工程中,**胶水代码(Glue Code)** 指的是本身不包含业务逻辑,仅用于连接两个系统或模块的代码。
|
||||
|
||||
典型的胶水代码包括:
|
||||
|
||||
- 前端调用后端 API 时编写的 HTTP 请求代码(URL 拼接、请求头设置、响应解析)
|
||||
- 后端服务 A 调用服务 B 接口时编写的 HTTP 客户端代码
|
||||
- 不同编程语言之间的接口适配代码
|
||||
|
||||
这类代码的特征是:**高度重复、模式固定、但不可省略。**
|
||||
|
||||
### 5.2 OpenAPI 规范与代码自动生成
|
||||
|
||||
既然胶水代码具有高度的模式化特征,工程界的解决方案是:**先用标准格式描述 API 接口,再用工具自动生成客户端代码。**
|
||||
|
||||
**OpenAPI 规范**(前身为 Swagger)是描述 REST API 的行业标准。它使用 YAML 或 JSON 格式,精确定义 API 的路径、参数、请求体和响应结构:
|
||||
|
||||
```yaml
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: 邮件服务 API
|
||||
version: 1.0.0
|
||||
|
||||
paths:
|
||||
/emails:
|
||||
post:
|
||||
summary: 发送邮件
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
to:
|
||||
type: string
|
||||
example: "user@example.com"
|
||||
subject:
|
||||
type: string
|
||||
body:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 发送成功
|
||||
```
|
||||
|
||||
基于这份规范文件,使用 `openapi-generator` 等工具可以自动生成多种语言的客户端 SDK:
|
||||
|
||||
- **Python**:`client.emails.send(to="user@example.com", subject="Hi", body="Hello")`
|
||||
- **TypeScript**:`client.emails.send({ to: "user@example.com", subject: "Hi", body: "Hello" })`
|
||||
- **Go**:`client.Emails.Send(ctx, &SendEmailRequest{To: "user@example.com", ...})`
|
||||
|
||||
生成的 SDK 封装了 HTTP 请求的所有细节,调用方无需关心 URL 路径、请求方法、序列化格式等底层实现。
|
||||
|
||||
### 5.3 重新理解 Armin 的案例
|
||||
|
||||
回到本文开头的案例,现在可以准确理解其中每个组成部分:
|
||||
|
||||
| 组成部分 | 性质 | 说明 |
|
||||
|---------|------|------|
|
||||
| **Go** | 业务逻辑代码 | 邮件收发服务的核心功能实现 |
|
||||
| **YAML** | 配置文件 | 服务配置、CI/CD 流水线定义、OpenAPI 规范文件 |
|
||||
| **Pulumi** | 基础设施代码 | 用 Go/TypeScript 定义云资源(服务器、数据库、网络) |
|
||||
| **SDK 胶水代码** | 自动生成的客户端库 | 从 OpenAPI 规范自动生成的 Python 和 TypeScript SDK |
|
||||
|
||||
其中 YAML 配置、Pulumi 资源定义、SDK 胶水代码这三类均属于高度模式化、有明确规范约束的代码,这正是 AI 代码生成能力最强的领域。因此"4 万行代码中 90% 由 AI 生成"是合理的。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. AI 与 DSL 的关系
|
||||
|
||||
### 6.1 AI 代码生成的适用性分析
|
||||
|
||||
| 特征维度 | 适合 AI 生成 | 不适合 AI 生成 |
|
||||
|---------|-------------|---------------|
|
||||
| **模式化程度** | 高度重复,存在固定模板 | 需要创造性设计,无先例可循 |
|
||||
| **规范约束** | 有明确的 schema 或语法规范 | 需求模糊,边界不清晰 |
|
||||
| **上下文依赖** | 局部自洽,单个定义不依赖全局理解 | 需要理解整个系统的架构意图 |
|
||||
| **可验证性** | 可被工具自动校验(如 `terraform validate`) | 只能依靠人工判断设计合理性 |
|
||||
|
||||
本文介绍的四类技术——配置文件、嵌入式脚本、IaC 代码、SDK 胶水代码——均具备左列的特征。这解释了为什么 AI 在这些领域的代码生成效果显著优于业务逻辑代码。
|
||||
|
||||
### 6.2 评估框架
|
||||
|
||||
在判断某段代码是否适合交给 AI 生成时,可以参考以下三个标准:
|
||||
|
||||
1. **是否存在现成的规范或 schema?** —— 存在则 AI 友好
|
||||
2. **是否属于大量重复的模式?** —— 是则 AI 友好
|
||||
3. **生成结果能否被工具自动验证?** —— 能则 AI 友好
|
||||
|
||||
三项均满足的代码(如从 OpenAPI 规范生成 SDK、用 Terraform 批量定义同构资源),可以高度依赖 AI 生成。三项均不满足的代码(如设计一个新的分布式一致性协议),仍需要工程师自行完成。
|
||||
|
||||
---
|
||||
|
||||
## 7. 术语表
|
||||
|
||||
| 术语 | 全称 / 中文 | 定义 |
|
||||
|------|------------|------|
|
||||
| **DSL** | Domain-Specific Language / 领域特定语言 | 为特定领域设计的语言,与通用编程语言相对 |
|
||||
| **GPL** | General-Purpose Language / 通用编程语言 | 可解决任意计算问题的编程语言,如 Python、Java、Go |
|
||||
| **外部 DSL** | External DSL | 拥有独立语法和解析器的领域特定语言,如 SQL、HCL、YAML |
|
||||
| **内部 DSL** | Internal DSL / Embedded DSL | 寄生在通用编程语言内部、利用宿主语法构建的领域专用表达,如 Pulumi |
|
||||
| **数据序列化** | Data Serialization | 将内存中的数据结构转换为可存储或可传输的格式的过程 |
|
||||
| **INI** | Initialization | 最早期的键值对配置格式,起源于 Windows 系统 |
|
||||
| **CSV** | Comma-Separated Values / 逗号分隔值 | 用逗号分隔字段的纯文本表格格式 |
|
||||
| **XML** | eXtensible Markup Language / 可扩展标记语言 | 基于标签的文本数据格式,表达力强但语法冗长 |
|
||||
| **JSON** | JavaScript Object Notation | 基于键值对的轻量数据交换格式,Web API 的事实标准 |
|
||||
| **YAML** | YAML Ain't Markup Language | 基于缩进的配置文件格式,后端和 DevOps 领域广泛使用 |
|
||||
| **TOML** | Tom's Obvious Minimal Language | 显式语法的配置格式,Rust 和 Python 生态常用 |
|
||||
| **Protobuf** | Protocol Buffers | Google 开发的二进制序列化格式,需预定义 Schema,体积小、速度快 |
|
||||
| **MessagePack** | — | 类似 JSON 的二进制序列化格式,无需 Schema |
|
||||
| **Lua** | — | 轻量级嵌入式脚本语言,常用于游戏引擎、Web 服务器和数据库扩展 |
|
||||
| **IaC** | Infrastructure as Code / 基础设施即代码 | 用代码定义和管理云计算资源的工程实践 |
|
||||
| **Terraform** | — | HashiCorp 开发的 IaC 工具,使用 HCL 声明式语言 |
|
||||
| **HCL** | HashiCorp Configuration Language | Terraform 使用的专用配置语言 |
|
||||
| **Pulumi** | — | 支持通用编程语言的 IaC 工具 |
|
||||
| **OpenAPI** | — | 描述 REST API 接口的行业标准规范(前身为 Swagger) |
|
||||
| **SDK** | Software Development Kit / 软件开发工具包 | 封装了 API 调用细节的客户端库 |
|
||||
| **胶水代码** | Glue Code | 不含业务逻辑,仅用于连接两个系统的适配代码 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
后端工程中存在大量非业务逻辑代码。它们有一个共同的上位概念:**DSL(领域特定语言)**——为特定领域设计的、与通用编程语言相对的语言。
|
||||
|
||||
本文介绍的 DSL 可以归为四个类别:
|
||||
|
||||
1. **数据序列化格式**(XML / JSON / YAML / TOML / CSV / Protobuf 等)—— 纯数据描述型外部 DSL,将结构化数据转换为可存储、可传输的形式
|
||||
2. **嵌入式脚本语言**(Lua 等)—— 介于配置与通用语言之间,为宿主程序提供可编程的扩展能力
|
||||
3. **基础设施定义语言**(HCL / Dockerfile 等)—— 声明式外部 DSL,描述系统期望状态;Pulumi 则以内部 DSL 的方式实现同一目标
|
||||
4. **接口描述语言与胶水代码生成**(OpenAPI / .proto)—— 通过规范描述自动生成系统间的连接代码
|
||||
|
||||
理解 DSL 这一分类框架后,面对后端项目中各类"不像代码的代码"时,可以快速识别其性质:它属于哪类 DSL、解决什么领域的问题、为什么不用通用编程语言来写。
|
||||
|
||||
同时,由于 DSL 代码具有高度模式化、规范驱动、可自动验证的特征,它们也是当前 AI 代码生成技术最有效的应用领域。
|
||||
@@ -1,3 +1,173 @@
|
||||
# 域名、DNS 与 HTTPS
|
||||
|
||||
> 待实现
|
||||
::: tip 前言
|
||||
**当你在浏览器输入 `www.google.com` 并按下回车,背后发生了什么?** 这个看似简单的动作,背后涉及域名解析、DNS 查询、TLS 加密握手等一系列精密的协作过程。理解这些机制,是每个开发者的必修课——它直接关系到你的网站能不能被访问、数据会不会被窃取。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **DNS 原理**:理解域名如何被翻译成 IP 地址的完整过程
|
||||
- **记录类型**:掌握 A、CNAME、MX 等常见 DNS 记录的用途
|
||||
- **HTTPS 机制**:理解 TLS 握手如何建立安全连接
|
||||
- **证书体系**:了解数字证书的信任链和验证机制
|
||||
- **安全意识**:明白为什么 HTTPS 是现代 Web 的底线要求
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | DNS 解析 | 递归查询、迭代查询 |
|
||||
| **第 2 章** | DNS 记录 | A、CNAME、MX、TXT |
|
||||
| **第 3 章** | HTTPS 与 TLS | 握手过程、加密通信 |
|
||||
| **第 4 章** | 证书信任链 | CA、根证书、中间证书 |
|
||||
| **第 5 章** | HTTP vs HTTPS | 明文 vs 加密、安全对比 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:从域名到安全连接
|
||||
|
||||
互联网的通信基于 IP 地址(如 142.250.80.46),但人类记不住这些数字。于是我们发明了**域名系统(DNS)**——互联网的"电话簿",把人类可读的域名翻译成机器可读的 IP 地址。
|
||||
|
||||
但光能找到服务器还不够。如果通信内容是明文传输的,任何中间人都能窃听、篡改你的数据。**HTTPS** 就是解决这个问题的——它在 HTTP 之上加了一层 TLS 加密,确保数据在传输过程中的机密性和完整性。
|
||||
|
||||
::: tip 一次完整的网页访问
|
||||
1. **域名解析**:浏览器问 DNS "www.google.com 的 IP 是多少?",DNS 回答 "142.250.80.46"
|
||||
2. **TCP 连接**:浏览器与服务器建立 TCP 三次握手
|
||||
3. **TLS 握手**:双方协商加密算法、验证证书、交换密钥
|
||||
4. **加密通信**:所有 HTTP 数据通过加密通道传输
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. DNS 解析:互联网的"电话簿"
|
||||
|
||||
DNS(Domain Name System)的工作原理就像查电话簿:你知道对方的名字(域名),需要查到对方的电话号码(IP 地址)。但互联网的"电话簿"不是一本,而是一个分层的分布式系统。
|
||||
|
||||
<DnsResolutionDemo />
|
||||
|
||||
::: tip DNS 解析的四个步骤
|
||||
1. **浏览器缓存**:先查本地缓存,如果之前访问过这个域名,直接用缓存的 IP
|
||||
2. **递归解析器**:缓存没命中,请求发给 ISP 的递归解析器(如 8.8.8.8)
|
||||
3. **逐级查询**:递归解析器依次询问根域名服务器 → 顶级域服务器(.com)→ 权威域名服务器(google.com)
|
||||
4. **返回结果**:权威服务器返回最终 IP,递归解析器缓存结果并返回给浏览器
|
||||
:::
|
||||
|
||||
| 层级 | 服务器 | 职责 | 数量 |
|
||||
|------|-------|------|------|
|
||||
| 根域 | Root Server | 知道所有顶级域的地址 | 全球 13 组 |
|
||||
| 顶级域 | TLD Server | 管理 .com、.cn、.org 等 | 每个后缀一组 |
|
||||
| 权威域 | Authoritative | 存储具体域名的 DNS 记录 | 每个域名至少 2 个 |
|
||||
| 递归解析器 | Resolver | 代替用户完成整个查询过程 | ISP 或公共 DNS |
|
||||
|
||||
---
|
||||
|
||||
## 2. DNS 记录类型:域名背后的"配置表"
|
||||
|
||||
DNS 不只是把域名翻译成 IP。通过不同类型的 DNS 记录,你可以控制邮件投递、域名跳转、服务发现等多种行为。理解这些记录类型,是配置域名和排查网络问题的基础。
|
||||
|
||||
<DnsRecordTypeDemo />
|
||||
|
||||
| 记录类型 | 用途 | 示例 |
|
||||
|---------|------|------|
|
||||
| A | 域名 → IPv4 地址 | `example.com → 93.184.216.34` |
|
||||
| AAAA | 域名 → IPv6 地址 | `example.com → 2606:2800:220:1:...` |
|
||||
| CNAME | 域名 → 另一个域名(别名) | `www.example.com → example.com` |
|
||||
| MX | 指定邮件服务器 | `example.com → mail.example.com` |
|
||||
| TXT | 存储文本信息 | SPF 验证、域名所有权验证 |
|
||||
| NS | 指定权威域名服务器 | `example.com → ns1.example.com` |
|
||||
|
||||
::: tip 实际场景中的 DNS 配置
|
||||
- **部署网站**:添加 A 记录指向服务器 IP,或 CNAME 指向 CDN 域名
|
||||
- **配置邮箱**:添加 MX 记录指向邮件服务器,TXT 记录配置 SPF/DKIM 防垃圾邮件
|
||||
- **验证域名所有权**:云服务商要求你添加特定 TXT 记录来证明你拥有这个域名
|
||||
- **负载均衡**:同一域名配置多条 A 记录,DNS 轮询分发流量
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. HTTPS 与 TLS:给数据穿上"防弹衣"
|
||||
|
||||
HTTP 协议的数据是明文传输的——就像寄明信片,邮递员(中间人)可以随意阅读内容。HTTPS 在 HTTP 之上加了一层 TLS(Transport Layer Security)加密,相当于把明信片装进了密封信封。
|
||||
|
||||
TLS 握手是建立安全连接的关键步骤,它在正式传输数据之前,完成身份验证和密钥协商。
|
||||
|
||||
<HttpsHandshakeDemo />
|
||||
|
||||
::: tip TLS 1.3 握手的核心步骤
|
||||
1. **Client Hello**:客户端发送支持的加密算法列表和一个随机数
|
||||
2. **Server Hello**:服务器选择加密算法,返回数字证书和随机数
|
||||
3. **证书验证**:客户端验证服务器证书是否可信(检查 CA 签名、有效期、域名匹配)
|
||||
4. **密钥交换**:双方通过 ECDHE 算法协商出一个共享密钥(不在网络上传输密钥本身)
|
||||
5. **加密通信**:后续所有数据使用协商好的对称密钥加密传输
|
||||
:::
|
||||
|
||||
| 特性 | TLS 1.2 | TLS 1.3 |
|
||||
|------|---------|---------|
|
||||
| 握手往返次数 | 2-RTT | 1-RTT(首次)/ 0-RTT(恢复) |
|
||||
| 密钥交换 | RSA 或 ECDHE | 仅 ECDHE(前向安全) |
|
||||
| 加密算法 | 支持较多旧算法 | 仅保留安全算法 |
|
||||
| 性能 | 较慢 | 更快 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 证书信任链:凭什么相信这个网站?
|
||||
|
||||
TLS 握手中最关键的一步是"证书验证"。浏览器怎么判断一个网站的证书是真的,而不是攻击者伪造的?答案是**证书信任链**——一个层层背书的信任体系。
|
||||
|
||||
<CertificateChainDemo />
|
||||
|
||||
::: tip 证书信任链的三层结构
|
||||
1. **根证书(Root CA)**:由受信任的证书颁发机构签发,预装在操作系统和浏览器中。这是信任的"锚点"。
|
||||
2. **中间证书(Intermediate CA)**:由根 CA 签发,用于签发终端证书。根 CA 不直接签发网站证书,是为了安全隔离。
|
||||
3. **终端证书(Leaf Certificate)**:你的网站实际使用的证书,由中间 CA 签发,包含域名、公钥、有效期等信息。
|
||||
:::
|
||||
|
||||
| 证书类型 | 验证级别 | 颁发速度 | 适用场景 |
|
||||
|---------|---------|---------|---------|
|
||||
| DV(域名验证) | 仅验证域名所有权 | 分钟级 | 个人网站、博客 |
|
||||
| OV(组织验证) | 验证组织身份 | 数天 | 企业官网 |
|
||||
| EV(扩展验证) | 严格验证组织 | 数周 | 银行、金融机构 |
|
||||
| 通配符证书 | 覆盖所有子域名 | 视类型而定 | 多子域名场景 |
|
||||
|
||||
---
|
||||
|
||||
## 5. HTTP vs HTTPS:为什么加密是底线?
|
||||
|
||||
2024 年,全球超过 95% 的网页流量已经通过 HTTPS 传输。Chrome 浏览器会对 HTTP 网站标记"不安全"警告,搜索引擎也会降低 HTTP 网站的排名。HTTPS 不再是"可选项",而是现代 Web 的底线要求。
|
||||
|
||||
<DnsHttpsComparisonDemo />
|
||||
|
||||
| 维度 | HTTP | HTTPS |
|
||||
|------|------|-------|
|
||||
| 数据传输 | 明文,可被窃听 | 加密,无法被窃听 |
|
||||
| 身份验证 | 无,无法确认服务器身份 | 有,通过证书验证服务器 |
|
||||
| 数据完整性 | 无保护,可被篡改 | 有保护,篡改会被检测 |
|
||||
| 端口 | 80 | 443 |
|
||||
| SEO 影响 | 搜索排名降低 | 搜索排名加分 |
|
||||
| 浏览器表现 | 显示"不安全"警告 | 显示锁图标 |
|
||||
|
||||
::: tip 免费获取 HTTPS 证书
|
||||
**Let's Encrypt** 是一个免费、自动化的证书颁发机构,让任何网站都能零成本启用 HTTPS。配合 Certbot 工具,可以一键申请和自动续期证书。大多数云平台和 CDN 服务商也提供免费的 SSL 证书。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
域名、DNS 和 HTTPS 是互联网基础设施的三大支柱。DNS 让我们用人类可读的名字访问网站,HTTPS 确保通信过程安全可信。
|
||||
|
||||
回顾本章的关键要点:
|
||||
|
||||
1. **DNS 是分层系统**:根域 → 顶级域 → 权威域,逐级查询,缓存加速
|
||||
2. **记录类型各有用途**:A 记录指向 IP,CNAME 做别名,MX 管邮件,TXT 做验证
|
||||
3. **TLS 握手建立信任**:证书验证 + 密钥协商,TLS 1.3 只需 1-RTT
|
||||
4. **证书信任链**:根 CA → 中间 CA → 终端证书,层层背书
|
||||
5. **HTTPS 是底线**:免费证书(Let's Encrypt)让加密零门槛
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [How DNS Works](https://howdns.works/) - 漫画形式讲解 DNS 工作原理
|
||||
- [Let's Encrypt 文档](https://letsencrypt.org/docs/) - 免费 SSL 证书申请指南
|
||||
- [Cloudflare Learning Center](https://www.cloudflare.com/learning/dns/what-is-dns/) - DNS 和网络安全系统教程
|
||||
- [TLS 1.3 RFC 8446](https://datatracker.ietf.org/doc/html/rfc8446) - TLS 1.3 协议规范
|
||||
- [SSL Labs](https://www.ssllabs.com/ssltest/) - 在线检测网站 HTTPS 配置质量
|
||||
|
||||
@@ -1,3 +1,158 @@
|
||||
# 故障排查与应急响应
|
||||
|
||||
> 待实现
|
||||
::: tip 前言
|
||||
**凌晨三点,手机疯狂震动,线上服务全面瘫痪——你该怎么办?** 对于任何互联网团队来说,故障不是"会不会发生"的问题,而是"什么时候发生"的问题。优秀的团队不是不出故障,而是出了故障能快速响应、高效恢复,并从中学习避免重蹈覆辙。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **分级意识**:掌握 P0~P4 事故严重程度分级标准
|
||||
- **响应流程**:理解从发现到恢复的完整事故响应时间线
|
||||
- **组织协作**:了解事故指挥体系中的角色分工和协作机制
|
||||
- **告警体系**:掌握告警升级策略,确保关键问题不被遗漏
|
||||
- **复盘方法**:学会用"五个为什么"挖掘根因,写出有价值的复盘报告
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 严重程度分级 | P0~P4、影响范围评估 |
|
||||
| **第 2 章** | 响应时间线 | 发现→响应→恢复→复盘 |
|
||||
| **第 3 章** | 指挥体系 | IC、通信官、技术负责人 |
|
||||
| **第 4 章** | 告警升级 | 分级告警、逐级升级 |
|
||||
| **第 5 章** | 事后复盘 | 五个为什么、无责文化 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:故障是最好的老师
|
||||
|
||||
Netflix 有一个著名的工具叫 Chaos Monkey——它会随机杀掉生产环境的服务器。听起来疯狂,但背后的逻辑很清晰:**与其等故障找上门,不如主动制造故障来锻炼团队的应急能力**。
|
||||
|
||||
应急响应不是靠临场发挥,而是靠**流程、角色、工具**三位一体的体系化建设。就像消防队不是火灾发生时才组建的——他们平时就在训练、演练、维护装备。
|
||||
|
||||
::: tip 应急响应的四个核心要素
|
||||
- **快速发现**:完善的监控和告警体系,确保问题在用户感知之前被发现
|
||||
- **高效协作**:清晰的角色分工和沟通机制,避免混乱中的重复劳动
|
||||
- **快速恢复**:优先恢复服务,而不是优先找根因。先止血,再治病
|
||||
- **持续改进**:每次故障都是学习机会,通过复盘不断完善系统和流程
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 严重程度分级:不是所有故障都要"全员出动"
|
||||
|
||||
一个按钮颜色显示错误和整个支付系统瘫痪,显然不是同一个级别的问题。**事故分级**的目的是让团队用合适的力度响应合适级别的问题——既不过度反应浪费资源,也不轻视问题导致损失扩大。
|
||||
|
||||
<SeverityLevelDemo />
|
||||
|
||||
| 级别 | 名称 | 影响范围 | 响应要求 | 示例 |
|
||||
|------|------|---------|---------|------|
|
||||
| P0 | 致命 | 核心业务完全不可用 | 立即响应,全员待命 | 支付系统瘫痪、数据泄露 |
|
||||
| P1 | 严重 | 核心功能严重受损 | 15 分钟内响应 | 登录失败率 > 50%、API 大面积超时 |
|
||||
| P2 | 重要 | 部分功能异常 | 1 小时内响应 | 搜索结果不准确、部分页面 500 |
|
||||
| P3 | 一般 | 非核心功能异常 | 工作时间处理 | 头像加载失败、非关键通知延迟 |
|
||||
| P4 | 轻微 | 体验问题 | 排入迭代计划 | UI 错位、文案错误 |
|
||||
|
||||
::: tip 分级的关键原则
|
||||
- **影响用户数**:影响 100% 用户的 P2 可能比影响 1% 用户的 P1 更紧急
|
||||
- **业务损失**:直接影响收入的问题(支付、下单)优先级更高
|
||||
- **可降级处理**:如果有临时方案可以缓解影响,可以适当降级处理
|
||||
- **动态调整**:随着排查深入,级别可能上调或下调
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 响应时间线:从发现到复盘的完整流程
|
||||
|
||||
一次事故响应就像一场接力赛,每个阶段都有明确的目标和交接点。清晰的时间线能让团队在混乱中保持有序。
|
||||
|
||||
<IncidentTimelineDemo />
|
||||
|
||||
::: tip 事故响应的五个阶段
|
||||
1. **检测(Detection)**:通过监控告警、用户反馈或内部巡检发现异常。目标:尽早发现,缩短 MTTD(平均检测时间)。
|
||||
2. **响应(Response)**:确认事故、评估严重程度、召集响应团队、建立沟通频道。目标:快速组织起有效的响应力量。
|
||||
3. **缓解(Mitigation)**:采取临时措施恢复服务,如回滚部署、切换备用节点、限流降级。目标:先止血,恢复用户体验。
|
||||
4. **修复(Resolution)**:找到根本原因并彻底修复。目标:消除隐患,防止复发。
|
||||
5. **复盘(Postmortem)**:回顾整个过程,分析根因,制定改进措施。目标:从故障中学习,让系统更健壮。
|
||||
:::
|
||||
|
||||
| 指标 | 含义 | 优化方向 |
|
||||
|------|------|---------|
|
||||
| MTTD | 平均检测时间 | 完善监控覆盖、降低告警阈值 |
|
||||
| MTTR | 平均恢复时间 | 自动化恢复、预案演练 |
|
||||
| MTBF | 平均故障间隔 | 提升系统可靠性、消除单点故障 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 指挥体系:谁来指挥这场"战斗"?
|
||||
|
||||
大型事故中最怕的不是技术难题,而是**混乱**——十几个人同时在排查,没人知道别人在做什么,关键信息在各个群里碎片化传播。事故指挥体系(Incident Command System)就是为了解决这个问题。
|
||||
|
||||
<IncidentCommandDemo />
|
||||
|
||||
::: tip 三个核心角色
|
||||
1. **事故指挥官(Incident Commander, IC)**:整个事故响应的总负责人。负责决策、协调资源、把控节奏。IC 不一定是技术最强的人,但必须是最冷静、最有全局观的人。
|
||||
2. **通信官(Communication Lead)**:负责对外沟通——更新状态页、通知客户、同步管理层。让 IC 和技术人员专注于解决问题,不被沟通事务打断。
|
||||
3. **技术负责人(Tech Lead)**:负责技术层面的排查和修复。组织技术人员分工协作,向 IC 汇报进展和方案。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 告警升级:确保关键问题不被遗漏
|
||||
|
||||
告警系统是事故响应的"眼睛"。但告警太少会漏报,告警太多会导致"告警疲劳"——当每天收到几百条告警时,真正重要的那条很容易被淹没。**告警升级策略**就是解决这个问题的关键。
|
||||
|
||||
<AlertEscalationDemo />
|
||||
|
||||
::: tip 告警升级的三层机制
|
||||
1. **一线响应(L1)**:告警触发后,先通知值班工程师。如果 15 分钟内未确认,自动升级。
|
||||
2. **二线升级(L2)**:通知团队负责人和相关领域专家。如果 30 分钟内未缓解,继续升级。
|
||||
3. **三线升级(L3)**:通知技术总监和管理层,启动全面应急响应。
|
||||
:::
|
||||
|
||||
| 告警级别 | 通知方式 | 响应时限 | 升级条件 |
|
||||
|---------|---------|---------|---------|
|
||||
| Warning | IM 消息 | 工作时间处理 | 持续 30 分钟未恢复 |
|
||||
| Critical | 电话 + IM | 15 分钟内确认 | 未确认或未缓解 |
|
||||
| Fatal | 电话轰炸 + 短信 | 5 分钟内响应 | 自动升级至管理层 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 事后复盘:从故障中学习
|
||||
|
||||
事故恢复后,最重要的一步是**复盘(Postmortem)**。复盘不是为了追责,而是为了找到系统性的改进机会。Google、Meta 等公司都奉行"无责复盘"文化——关注"系统为什么允许这个错误发生",而不是"谁犯了这个错误"。
|
||||
|
||||
<PostmortemDemo />
|
||||
|
||||
::: tip "五个为什么"分析法
|
||||
从表面现象出发,连续追问"为什么",直到找到根本原因:
|
||||
1. **为什么服务挂了?** → 数据库连接池耗尽
|
||||
2. **为什么连接池耗尽?** → 慢查询占用连接不释放
|
||||
3. **为什么有慢查询?** → 缺少索引,全表扫描
|
||||
4. **为什么缺少索引?** → 新表上线时没有 DBA 审核
|
||||
5. **为什么没有审核?** → 没有强制的 SQL 审核流程
|
||||
|
||||
根因不是"某个人忘了加索引",而是"缺少 SQL 审核流程"。修复根因才能防止复发。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
故障排查与应急响应是每个技术团队的必备能力。它不是靠英雄主义的个人发挥,而是靠体系化的流程、清晰的角色分工和持续的复盘改进。
|
||||
|
||||
回顾本章的关键要点:
|
||||
|
||||
1. **分级响应**:P0~P4 分级确保用合适的力度应对合适级别的问题
|
||||
2. **时间线清晰**:检测→响应→缓解→修复→复盘,每个阶段目标明确
|
||||
3. **指挥体系**:IC + 通信官 + 技术负责人,分工协作避免混乱
|
||||
4. **告警升级**:分级告警 + 自动升级,确保关键问题不被遗漏
|
||||
5. **无责复盘**:用"五个为什么"挖掘根因,关注系统改进而非个人追责
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [Google SRE Book - Incident Response](https://sre.google/sre-book/managing-incidents/) - Google 的事故管理实践
|
||||
- [PagerDuty Incident Response Guide](https://response.pagerduty.com/) - PagerDuty 开源的应急响应指南
|
||||
- [Atlassian Incident Management](https://www.atlassian.com/incident-management) - Atlassian 的事故管理最佳实践
|
||||
- [Learning from Incidents](https://www.learningfromincidents.io/) - 从事故中学习的社区资源
|
||||
- [Chaos Engineering (O'Reilly)](https://www.oreilly.com/library/view/chaos-engineering/9781492043850/) - 混沌工程原理与实践
|
||||
|
||||
@@ -1,3 +1,173 @@
|
||||
# 基础设施即代码
|
||||
|
||||
> 待实现
|
||||
::: tip 前言
|
||||
**你有没有经历过这种噩梦:线上服务器挂了,但没人记得当初是怎么配置的?** 手动登录服务器、凭记忆敲命令、祈祷不要敲错——这就是传统运维的日常。基础设施即代码(Infrastructure as Code,IaC)彻底改变了这一切:用代码来定义和管理基础设施,让服务器配置像软件一样可版本控制、可复现、可审计。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **核心概念**:理解 IaC 是什么,为什么它是现代运维的基石
|
||||
- **工作流认知**:掌握 Terraform 的 Write → Plan → Apply → Destroy 四阶段流程
|
||||
- **工具选型**:了解 Terraform、Pulumi、CloudFormation 等主流工具的优劣
|
||||
- **风险意识**:理解配置漂移的危害和检测方法
|
||||
- **最佳实践**:掌握 IaC 项目的工程化管理方法
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | IaC 概念 | 手动运维 vs 代码化管理 |
|
||||
| **第 2 章** | Terraform 工作流 | Write → Plan → Apply |
|
||||
| **第 3 章** | 工具对比 | Terraform、Pulumi、CDK |
|
||||
| **第 4 章** | 配置漂移 | 检测、预防、修复 |
|
||||
| **第 5 章** | 最佳实践 | 模块化、状态管理、CI/CD |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:为什么基础设施也需要"源代码"?
|
||||
|
||||
想象你是一个厨师。如果每道菜都凭感觉做,今天放一勺盐、明天放两勺,味道永远不稳定。但如果你把配方写下来——精确到每种调料的克数——任何人都能复现同样的味道。
|
||||
|
||||
基础设施管理面临的是同样的问题。一台服务器的配置可能涉及操作系统、网络规则、安全组、存储卷、环境变量等几十个参数。手动配置不仅容易出错,而且**不可复现、不可审计、不可回滚**。
|
||||
|
||||
::: tip IaC 的核心价值
|
||||
- **可复现**:同一份代码,无论执行多少次,结果都一样(幂等性)
|
||||
- **可版本控制**:基础设施变更通过 Git 管理,谁改了什么、为什么改,一目了然
|
||||
- **可审计**:所有变更都有记录,满足合规要求
|
||||
- **可自动化**:通过 CI/CD 流水线自动部署,消除人为操作风险
|
||||
- **可协作**:团队成员通过 Pull Request 审查基础设施变更,就像审查代码一样
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. IaC 概念:从"手动点击"到"代码声明"
|
||||
|
||||
传统运维的工作方式是:登录云平台控制台,手动点击创建服务器、配置网络、设置安全组。这种方式在管理几台服务器时还能应付,但当规模扩大到几十、几百台时,就变成了噩梦。
|
||||
|
||||
IaC 的核心思想是:**用声明式代码描述你想要的基础设施状态,让工具自动帮你实现**。你不需要告诉工具"先创建 VPC,再创建子网,再创建安全组"(命令式),只需要说"我要一个这样的网络环境"(声明式),工具会自动计算出需要执行的步骤。
|
||||
|
||||
<IaCConceptDemo />
|
||||
|
||||
| 维度 | 手动运维 | 基础设施即代码 |
|
||||
|------|---------|--------------|
|
||||
| 操作方式 | 登录控制台点击 | 编写代码文件 |
|
||||
| 可复现性 | 依赖文档和记忆 | 代码即文档,100% 可复现 |
|
||||
| 变更追踪 | 无记录或记录不全 | Git 版本控制,完整历史 |
|
||||
| 协作方式 | 口头沟通、文档传递 | Pull Request 审查 |
|
||||
| 回滚能力 | 手动逆向操作 | git revert + 重新 apply |
|
||||
| 一致性 | 环境间差异大 | 开发/测试/生产完全一致 |
|
||||
|
||||
::: tip 声明式 vs 命令式
|
||||
- **声明式(Declarative)**:描述"我要什么",工具自动计算"怎么做"。Terraform、CloudFormation 采用这种方式。优点是幂等性好,缺点是灵活性受限。
|
||||
- **命令式(Imperative)**:描述"怎么做",一步步执行。Ansible、Shell 脚本采用这种方式。优点是灵活,缺点是难以保证幂等性。
|
||||
- **混合式**:Pulumi、AWS CDK 用通用编程语言编写,兼具声明式的状态管理和命令式的灵活性。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. Terraform 工作流:Write → Plan → Apply
|
||||
|
||||
Terraform 是目前最流行的 IaC 工具,由 HashiCorp 开发。它的工作流程清晰直观,分为四个阶段,就像软件开发的"编码→审查→部署→清理"。
|
||||
|
||||
<TerraformWorkflowDemo />
|
||||
|
||||
::: tip 四阶段工作流
|
||||
1. **Write(编写)**:用 HCL(HashiCorp Configuration Language)编写基础设施定义文件(.tf)。声明你需要的资源:服务器、数据库、网络等。
|
||||
2. **Plan(计划)**:运行 `terraform plan`,Terraform 会对比当前状态和目标状态,生成一份"执行计划"——告诉你它打算创建、修改、删除哪些资源。这是安全网,让你在真正执行前确认变更。
|
||||
3. **Apply(执行)**:确认计划无误后,运行 `terraform apply`,Terraform 按计划创建或修改资源。执行完成后,当前状态会保存到状态文件(terraform.tfstate)。
|
||||
4. **Destroy(销毁)**:不再需要时,运行 `terraform destroy` 清理所有资源,避免产生不必要的费用。
|
||||
:::
|
||||
|
||||
| 命令 | 作用 | 是否修改基础设施 | 使用场景 |
|
||||
|------|------|----------------|---------|
|
||||
| `terraform init` | 初始化项目,下载 Provider | 否 | 首次使用或添加新 Provider |
|
||||
| `terraform plan` | 预览变更,生成执行计划 | 否 | 每次变更前必须执行 |
|
||||
| `terraform apply` | 执行变更,创建/修改资源 | 是 | 确认 plan 后执行 |
|
||||
| `terraform destroy` | 销毁所有资源 | 是 | 清理测试环境、下线服务 |
|
||||
| `terraform state` | 查看/管理状态文件 | 视操作而定 | 状态迁移、资源导入 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 工具对比:选择适合你的 IaC 工具
|
||||
|
||||
IaC 领域有多种工具,各有侧重。选择工具时需要考虑团队技术栈、云平台、项目规模等因素。没有"最好"的工具,只有最适合你场景的工具。
|
||||
|
||||
<IaCToolComparisonDemo />
|
||||
|
||||
| 工具 | 语言 | 云平台支持 | 学习曲线 | 适用场景 |
|
||||
|------|------|-----------|---------|---------|
|
||||
| Terraform | HCL | 多云(AWS/Azure/GCP) | 中等 | 多云环境、团队协作 |
|
||||
| Pulumi | Python/TS/Go | 多云 | 低(熟悉编程语言) | 开发者友好、复杂逻辑 |
|
||||
| AWS CloudFormation | JSON/YAML | 仅 AWS | 中等 | 纯 AWS 环境 |
|
||||
| AWS CDK | Python/TS/Java | 仅 AWS | 低 | AWS + 编程语言偏好 |
|
||||
| Ansible | YAML | 多云 + 裸机 | 低 | 配置管理、混合环境 |
|
||||
|
||||
::: tip 如何选择?
|
||||
- **初创团队 / 单云**:CloudFormation(AWS)或对应云平台原生工具,生态集成最好
|
||||
- **多云 / 中大型团队**:Terraform,社区最大、Provider 最丰富、招聘最容易
|
||||
- **开发者主导的团队**:Pulumi 或 CDK,用熟悉的编程语言写基础设施,IDE 支持好
|
||||
- **需要配置管理**:Ansible,擅长服务器内部配置(安装软件、修改配置文件)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置漂移:无声的定时炸弹
|
||||
|
||||
配置漂移(Configuration Drift)是 IaC 实践中最隐蔽的敌人。它指的是**实际基础设施状态与代码定义的状态之间逐渐产生偏差**。
|
||||
|
||||
这种偏差通常是怎么产生的?有人为了"快速修复"一个线上问题,直接登录控制台手动改了安全组规则;有人为了调试,临时加大了某台服务器的配置但忘了改回来。这些"小改动"日积月累,最终导致代码和实际环境严重脱节。
|
||||
|
||||
<ConfigDriftDemo />
|
||||
|
||||
::: tip 配置漂移的危害
|
||||
1. **不可复现**:代码描述的环境和实际环境不一致,新建环境时会出问题
|
||||
2. **回滚失败**:以为回滚到上一版本就能恢复,但实际环境已经被手动修改过
|
||||
3. **安全隐患**:手动开放的端口、放宽的权限可能被遗忘,成为攻击入口
|
||||
4. **审计失效**:合规审计基于代码,但代码不反映真实状态
|
||||
:::
|
||||
|
||||
| 预防措施 | 说明 |
|
||||
|---------|------|
|
||||
| 禁止手动变更 | 通过 IAM 策略限制控制台操作权限 |
|
||||
| 定期漂移检测 | 定时运行 `terraform plan` 检查差异 |
|
||||
| 自动修复 | 检测到漂移后自动执行 apply 恢复一致性 |
|
||||
| 变更审计 | 开启 CloudTrail 等审计日志,追踪所有变更来源 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 最佳实践:让 IaC 项目可持续演进
|
||||
|
||||
IaC 代码和应用代码一样,需要良好的工程实践来保证可维护性。随着基础设施规模增长,没有章法的 IaC 代码会变成另一种形式的"技术债"。
|
||||
|
||||
<IaCBestPracticeDemo />
|
||||
|
||||
::: tip 六条核心最佳实践
|
||||
1. **模块化**:将可复用的基础设施抽象为模块(如 VPC 模块、数据库模块),避免复制粘贴。就像写函数一样,一处定义、多处调用。
|
||||
2. **环境隔离**:开发、测试、生产使用独立的状态文件和变量文件,通过 workspace 或目录结构隔离。
|
||||
3. **远程状态管理**:状态文件(tfstate)存储在远程后端(S3 + DynamoDB),支持团队协作和状态锁定,避免并发冲突。
|
||||
4. **敏感信息管理**:密码、密钥等敏感信息不要写在代码里,使用 Vault、AWS Secrets Manager 等工具管理。
|
||||
5. **CI/CD 集成**:将 terraform plan 集成到 PR 流程,apply 通过流水线自动执行,杜绝本地手动操作。
|
||||
6. **代码审查**:基础设施变更和应用代码一样需要 Code Review,尤其是涉及安全组、IAM 策略的变更。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
基础设施即代码是现代云原生运维的基石。它把"不可描述的手动操作"变成了"可版本控制的代码",让基础设施管理从"艺术"变成了"工程"。
|
||||
|
||||
回顾本章的关键要点:
|
||||
|
||||
1. **IaC 的本质**:用代码声明基础设施的期望状态,让工具自动实现
|
||||
2. **Terraform 工作流**:Write → Plan → Apply 三步走,Plan 是安全网
|
||||
3. **工具选型**:多云选 Terraform,单云选原生工具,开发者团队选 Pulumi
|
||||
4. **配置漂移**:最隐蔽的风险,需要通过流程和工具双重防护
|
||||
5. **工程化管理**:模块化、环境隔离、远程状态、CI/CD 集成缺一不可
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [Terraform 官方教程](https://developer.hashicorp.com/terraform/tutorials) - 从零开始学 Terraform
|
||||
- [Pulumi 文档](https://www.pulumi.com/docs/) - 用编程语言写基础设施
|
||||
- [AWS CDK Workshop](https://cdkworkshop.com/) - AWS CDK 实战教程
|
||||
- [Infrastructure as Code (O'Reilly)](https://www.oreilly.com/library/view/infrastructure-as-code/9781098114664/) - IaC 领域的经典书籍
|
||||
- [Spacelift Blog](https://spacelift.io/blog) - IaC 最佳实践和行业趋势
|
||||
|
||||
@@ -1,3 +1,164 @@
|
||||
# AI 原生应用设计
|
||||
|
||||
> 待实现
|
||||
::: tip 前言
|
||||
**为什么有些 AI 产品让人惊艳,而有些只是"套壳 ChatGPT"?** 差别不在于用了多强的模型,而在于产品是否从底层就围绕 AI 的特性来设计。AI 原生应用不是在传统应用上"加个聊天框",而是重新思考用户交互、系统架构和产品逻辑的全新范式。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **范式认知**:理解 AI 原生应用与传统应用的本质区别
|
||||
- **设计原则**:掌握 AI 原生产品设计的核心原则
|
||||
- **Prompt 工程**:了解如何设计高质量的 Prompt 来驱动 AI 能力
|
||||
- **交互模式**:认识 AI 时代的新型用户交互范式
|
||||
- **架构思维**:理解 AI 应用的请求处理流程和系统架构
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 架构对比 | 传统应用 vs AI 原生应用 |
|
||||
| **第 2 章** | 设计原则 | AI-First 思维、不确定性设计 |
|
||||
| **第 3 章** | Prompt 工程 | 系统提示词、模板设计 |
|
||||
| **第 4 章** | 交互模式 | 流式输出、多模态、Agent |
|
||||
| **第 5 章** | 请求流程 | AI 应用的完整生命周期 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:从"加个 AI"到"AI 原生"
|
||||
|
||||
过去几年,很多产品的 AI 化路径是这样的:有一个现成的应用,然后在某个角落加一个"AI 助手"按钮。这种做法就像在马车上装一个引擎——能跑,但远不如从头设计一辆汽车。
|
||||
|
||||
**AI 原生应用**是一种全新的产品思维:从第一行代码开始,就把 AI 作为核心能力来设计,而不是事后附加的功能。
|
||||
|
||||
::: tip 传统应用 vs AI 原生应用
|
||||
- **传统应用**:用户操作 → 确定性逻辑 → 确定性结果。每次点击"提交订单",流程完全一样。
|
||||
- **AI 原生应用**:用户意图 → AI 理解 → 概率性结果。同样的问题,每次回答可能略有不同。
|
||||
- **核心转变**:从"编写规则"到"描述意图",从"确定性"到"概率性",从"操作界面"到"对话界面"。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构对比:两种完全不同的世界
|
||||
|
||||
传统应用的架构是"请求-响应"模型:用户点击按钮,后端执行确定性逻辑,返回确定性结果。整个过程可预测、可测试、可复现。
|
||||
|
||||
AI 原生应用则引入了一个全新的角色——**大语言模型**。它像一个"智能中间层",接收自然语言输入,输出自然语言结果。这带来了架构上的根本性变化。
|
||||
|
||||
<AINativeArchDemo />
|
||||
|
||||
| 维度 | 传统应用 | AI 原生应用 |
|
||||
|------|---------|------------|
|
||||
| 输入方式 | 表单、按钮、下拉框 | 自然语言、图片、语音 |
|
||||
| 处理逻辑 | if-else、规则引擎 | LLM 推理、Prompt 驱动 |
|
||||
| 输出特性 | 确定性、可复现 | 概率性、每次可能不同 |
|
||||
| 延迟特征 | 毫秒级 | 秒级(需要流式输出) |
|
||||
| 错误处理 | 明确的错误码 | 幻觉、拒绝回答、答非所问 |
|
||||
| 成本模型 | 固定计算资源 | 按 token 计费,成本波动大 |
|
||||
|
||||
::: tip 架构演进的三个阶段
|
||||
1. **AI 增强型**:在现有应用中嵌入 AI 功能(如自动补全、智能推荐)
|
||||
2. **AI 协作型**:AI 作为核心交互方式,但仍有传统 UI 兜底(如 Notion AI、GitHub Copilot)
|
||||
3. **AI 原生型**:整个产品围绕 AI 构建,去掉 AI 产品就不成立(如 ChatGPT、Cursor、Midjourney)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计原则:AI 原生产品的"宪法"
|
||||
|
||||
设计 AI 原生应用不能照搬传统软件的设计思路。AI 的概率性、延迟性和不可预测性,要求我们建立一套全新的设计原则。
|
||||
|
||||
<AIDesignPrincipleDemo />
|
||||
|
||||
::: tip 五大核心设计原则
|
||||
1. **拥抱不确定性**:AI 的输出不是 100% 可靠的,产品设计必须考虑"AI 可能出错"的情况。提供编辑、重试、反馈机制,让用户始终拥有控制权。
|
||||
2. **渐进式信任**:不要一开始就让 AI 做高风险决策。先从低风险场景建立用户信任,再逐步扩展 AI 的自主权。
|
||||
3. **透明可解释**:让用户知道 AI 在做什么、为什么这么做。展示推理过程、引用来源、标注置信度。
|
||||
4. **人机协作**:AI 不是替代人,而是增强人。最好的设计是让 AI 做初稿,人做终审。
|
||||
5. **优雅降级**:当 AI 服务不可用或结果不理想时,产品仍然可用。永远有 Plan B。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. Prompt 工程:AI 应用的"编程语言"
|
||||
|
||||
在传统应用中,你用代码告诉计算机做什么。在 AI 原生应用中,你用 Prompt 告诉模型做什么。**Prompt 就是 AI 时代的编程语言**——写得好,AI 表现惊艳;写得差,AI 胡说八道。
|
||||
|
||||
<PromptDesignDemo />
|
||||
|
||||
::: tip Prompt 设计的四层结构
|
||||
1. **系统提示词(System Prompt)**:定义 AI 的角色、能力边界和行为规范。这是"宪法"级别的指令,用户看不到但始终生效。
|
||||
2. **上下文注入(Context)**:通过 RAG 检索到的相关文档、用户历史记录等,为 AI 提供回答所需的背景信息。
|
||||
3. **用户输入(User Message)**:用户的实际问题或指令。
|
||||
4. **输出格式约束(Format)**:指定 AI 的输出格式(JSON、Markdown、特定模板),确保结果可被程序解析。
|
||||
:::
|
||||
|
||||
| Prompt 技巧 | 说明 | 效果 |
|
||||
|------------|------|------|
|
||||
| 角色设定 | "你是一个资深前端工程师" | 提升专业领域回答质量 |
|
||||
| Few-shot 示例 | 给出 2-3 个输入输出示例 | 让模型理解期望的格式和风格 |
|
||||
| 思维链(CoT) | "请一步步思考" | 提升复杂推理的准确性 |
|
||||
| 输出约束 | "用 JSON 格式回答" | 确保输出可被程序解析 |
|
||||
| 负面指令 | "不要编造不确定的信息" | 减少幻觉和错误信息 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互模式:AI 时代的用户体验
|
||||
|
||||
AI 原生应用催生了一批全新的交互模式。传统应用的交互是"点击-等待-查看",而 AI 应用的交互更像是"对话-观察-调整"。
|
||||
|
||||
<AIUXPatternDemo />
|
||||
|
||||
::: tip 四种核心交互模式
|
||||
1. **流式输出(Streaming)**:AI 生成内容时逐字显示,而不是等全部生成完再展示。这大幅降低了用户的感知等待时间,也让用户可以在生成过程中判断方向是否正确。
|
||||
2. **多轮对话(Multi-turn)**:通过上下文记忆实现连续对话,用户可以逐步细化需求。关键挑战是上下文窗口管理和对话历史压缩。
|
||||
3. **多模态交互(Multimodal)**:支持文本、图片、语音、文件等多种输入方式,AI 也能输出图片、代码、表格等多种格式。
|
||||
4. **Agent 模式(Agentic)**:AI 不只是回答问题,而是自主规划、执行多步骤任务。用户给出目标,AI 自行拆解步骤并逐一完成。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 请求流程:一次 AI 调用的完整生命周期
|
||||
|
||||
当用户在 AI 应用中发送一条消息,背后发生了什么?理解这个完整流程,是构建可靠 AI 应用的基础。
|
||||
|
||||
<AIAppFlowDemo />
|
||||
|
||||
::: tip 请求处理的六个阶段
|
||||
1. **输入预处理**:校验用户输入、内容安全审核、敏感信息脱敏
|
||||
2. **上下文组装**:拼接系统提示词、检索相关文档(RAG)、加载对话历史
|
||||
3. **模型调用**:将组装好的 Prompt 发送给 LLM API,开启流式响应
|
||||
4. **输出后处理**:格式化输出、内容安全过滤、结构化数据提取
|
||||
5. **结果缓存**:对常见问题缓存结果,降低成本和延迟
|
||||
6. **监控记录**:记录 token 用量、响应时间、用户反馈,用于持续优化
|
||||
:::
|
||||
|
||||
| 阶段 | 关键考量 | 常见问题 |
|
||||
|------|---------|---------|
|
||||
| 输入预处理 | 注入攻击防护、长度限制 | Prompt 注入、越狱攻击 |
|
||||
| 上下文组装 | token 预算分配、信息优先级 | 上下文溢出、关键信息被截断 |
|
||||
| 模型调用 | 超时处理、重试策略、流式传输 | API 限流、网络超时 |
|
||||
| 输出后处理 | 格式校验、幻觉检测 | 输出格式不符预期 |
|
||||
| 缓存策略 | 语义缓存 vs 精确缓存 | 缓存命中率低 |
|
||||
| 监控告警 | 成本监控、质量评估 | token 成本失控 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
AI 原生应用设计不是简单地在传统应用上叠加 AI 功能,而是从架构、交互、工程实践等维度进行全面重构。
|
||||
|
||||
回顾本章的关键要点:
|
||||
|
||||
1. **架构转变**:从确定性逻辑到概率性推理,AI 原生应用需要全新的架构思维
|
||||
2. **设计原则**:拥抱不确定性、渐进式信任、透明可解释、人机协作、优雅降级
|
||||
3. **Prompt 是核心**:Prompt 工程是 AI 应用的"编程语言",直接决定产品质量
|
||||
4. **交互革新**:流式输出、多轮对话、多模态、Agent 模式重新定义了用户体验
|
||||
5. **全链路思维**:从输入预处理到监控告警,每个环节都需要针对 AI 特性专门设计
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [Google PAIR Guidelines](https://pair.withgoogle.com/) - Google 的人机交互 AI 设计指南
|
||||
- [OpenAI Prompt Engineering Guide](https://platform.openai.com/docs/guides/prompt-engineering) - 官方 Prompt 工程最佳实践
|
||||
- [Anthropic Prompt Engineering](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering) - Claude 的 Prompt 设计指南
|
||||
- [Nielsen Norman Group: AI UX](https://www.nngroup.com/topic/artificial-intelligence/) - AI 用户体验研究
|
||||
- [Building LLM Applications](https://www.oreilly.com/library/view/building-llm-powered/9781835462317/) - 构建 LLM 应用的实战指南
|
||||
|
||||
@@ -1,3 +1,193 @@
|
||||
# Embedding 与向量检索
|
||||
|
||||
> 待实现
|
||||
::: tip 前言
|
||||
**计算机怎么理解"猫和狗很像,但和汽车不像"这件事?** 对人类来说这是常识,但对计算机来说,"猫"、"狗"、"汽车"不过是三个毫无关联的字符串。Embedding(嵌入)技术就是解决这个问题的关键——它把文字变成数字向量,让计算机也能理解语义上的"远近亲疏"。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **直觉理解**:明白 Embedding 是什么,为什么"猫"和"狗"的向量会靠近
|
||||
- **相似度计算**:掌握余弦相似度、欧氏距离等核心度量方法
|
||||
- **索引原理**:理解向量数据库如何在百万级数据中毫秒级检索
|
||||
- **技术选型**:了解主流向量数据库的特点和适用场景
|
||||
- **端到端流程**:掌握从文本到向量到检索的完整 Pipeline
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | Embedding 概念 | 语义空间、向量表示 |
|
||||
| **第 2 章** | 相似度计算 | 余弦相似度、欧氏距离 |
|
||||
| **第 3 章** | 向量索引 | 暴力搜索 vs ANN |
|
||||
| **第 4 章** | 向量数据库 | Pinecone、Milvus、Chroma |
|
||||
| **第 5 章** | 端到端 Pipeline | 文本→向量→存储→查询 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:从文字到数字的桥梁
|
||||
|
||||
在自然语言处理的世界里,有一个根本性的挑战:**计算机只认识数字,不认识文字**。
|
||||
|
||||
早期的做法是给每个词分配一个编号(One-Hot 编码),比如"猫"=001,"狗"=010,"汽车"=100。但这样做有个致命问题:**所有词之间的距离都一样远**。"猫"到"狗"的距离和"猫"到"汽车"的距离完全相同——这显然不符合我们的直觉。
|
||||
|
||||
Embedding 的革命性在于:它把每个词映射到一个**稠密的低维向量空间**,让语义相近的词自然聚集在一起。在这个空间里,"猫"和"狗"靠得很近,而"汽车"则在远处——计算机终于能"理解"语义了。
|
||||
|
||||
::: tip 从 One-Hot 到 Embedding 的飞跃
|
||||
- **One-Hot**:维度 = 词表大小(可能几万维),每个向量只有一个 1,其余全是 0,稀疏且无语义
|
||||
- **Embedding**:维度通常 768~1536,每个数字都有意义,稠密且富含语义信息
|
||||
- **关键突破**:Word2Vec(2013)证明了"词的含义可以用它的上下文来定义",开启了 Embedding 时代
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. Embedding 概念:把文字变成坐标
|
||||
|
||||
Embedding 的核心思想可以用一句话概括:**用一组数字(向量)来表示一个词或句子的含义**。
|
||||
|
||||
想象一个二维坐标系。我们把"猫"放在坐标 (0.2, 0.7),"狗"放在 (0.3, 0.6),"汽车"放在 (0.9, 0.1)。你会发现"猫"和"狗"的坐标很接近,而"汽车"离它们很远。这就是 Embedding 的直觉——**语义相似度变成了空间距离**。
|
||||
|
||||
<EmbeddingConceptDemo />
|
||||
|
||||
::: tip Embedding 的三个关键特性
|
||||
1. **语义聚类**:相似含义的词会自动聚集在一起(动物一簇、食物一簇、科技一簇)
|
||||
2. **类比关系**:向量运算可以表达语义关系,经典例子:king - man + woman ≈ queen
|
||||
3. **维度含义**:每个维度隐式编码了某种语义特征(如"是否是动物"、"大小"、"情感倾向"等)
|
||||
:::
|
||||
|
||||
| 编码方式 | 维度 | 语义信息 | 典型应用 |
|
||||
|---------|------|---------|---------|
|
||||
| One-Hot | 词表大小(~50000) | 无 | 传统 NLP |
|
||||
| Word2Vec | 100~300 | 词级语义 | 词相似度、类比推理 |
|
||||
| BERT Embedding | 768 | 上下文语义 | 句子理解、问答 |
|
||||
| OpenAI text-embedding-3 | 1536~3072 | 深层语义 | RAG、语义搜索 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 相似度计算:向量之间有多"近"?
|
||||
|
||||
有了向量表示,下一个问题自然是:**怎么衡量两个向量有多相似?** 这就像在地图上衡量两个城市有多近——你可以量直线距离,也可以看方向是否一致。
|
||||
|
||||
<VectorSimilarityDemo />
|
||||
|
||||
::: tip 两种核心度量
|
||||
- **余弦相似度(Cosine Similarity)**:衡量两个向量的**方向**是否一致,值域 [-1, 1]。1 表示方向完全相同,0 表示正交(无关),-1 表示完全相反。文本语义比较的首选,因为它不受向量长度影响。
|
||||
- **欧氏距离(Euclidean Distance)**:衡量两个向量端点之间的**直线距离**,值域 [0, ∞)。0 表示完全重合,值越大越不相似。适合需要考虑"绝对大小"的场景。
|
||||
:::
|
||||
|
||||
| 度量方式 | 公式直觉 | 值域 | 适用场景 |
|
||||
|---------|---------|------|---------|
|
||||
| 余弦相似度 | 看方向,忽略长度 | [-1, 1] | 文本语义搜索、推荐系统 |
|
||||
| 欧氏距离 | 看端点直线距离 | [0, ∞) | 图像特征、聚类分析 |
|
||||
| 点积 | 方向 × 长度 | (-∞, +∞) | 归一化向量的快速计算 |
|
||||
| 曼哈顿距离 | 沿坐标轴走的距离 | [0, ∞) | 高维稀疏向量 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 向量索引:如何在百万向量中毫秒检索?
|
||||
|
||||
假设你有 100 万条文档,每条都转成了 1536 维的向量。用户提了一个问题,你需要找到最相似的 10 条。最直接的方法是逐一计算相似度——但这意味着要做 100 万次 1536 维的向量运算,太慢了。
|
||||
|
||||
这就是**向量索引**要解决的问题:**用空间换时间,通过预处理建立索引结构,让检索速度从 O(n) 降到近似 O(log n)**。
|
||||
|
||||
<VectorIndexDemo />
|
||||
|
||||
::: tip 暴力搜索 vs 近似最近邻(ANN)
|
||||
- **暴力搜索(Flat)**:逐一比较,100% 准确但速度慢。适合数据量小(< 10 万)的场景。
|
||||
- **IVF(倒排文件索引)**:先把向量空间划分成若干区域(聚类),查询时只搜索最近的几个区域。像是把图书馆按主题分区,找书时只去相关区域。
|
||||
- **HNSW(分层可导航小世界图)**:构建多层图结构,从粗粒度到细粒度逐层导航。像是先看世界地图定位到国家,再看省级地图,最后看街道地图。
|
||||
- **PQ(乘积量化)**:把高维向量压缩成短编码,牺牲少量精度换取大幅内存节省。适合超大规模数据集。
|
||||
:::
|
||||
|
||||
| 索引类型 | 构建速度 | 查询速度 | 召回率 | 内存占用 | 适用规模 |
|
||||
|---------|---------|---------|-------|---------|---------|
|
||||
| Flat(暴力) | 无需构建 | 慢 | 100% | 高 | < 10 万 |
|
||||
| IVF | 中等 | 快 | 95%+ | 中 | 10 万~1000 万 |
|
||||
| HNSW | 慢 | 很快 | 99%+ | 高 | 10 万~1000 万 |
|
||||
| PQ | 中等 | 快 | 90%+ | 很低 | > 1000 万 |
|
||||
| IVF-PQ | 中等 | 快 | 92%+ | 低 | > 1 亿 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 向量数据库:专为向量而生的存储引擎
|
||||
|
||||
有了向量和索引算法,你需要一个地方来存储和管理它们。传统数据库(MySQL、PostgreSQL)擅长处理结构化数据,但对高维向量的相似度搜索力不从心。**向量数据库**就是为这个场景专门设计的。
|
||||
|
||||
<VectorDatabaseDemo />
|
||||
|
||||
::: tip 向量数据库的核心能力
|
||||
1. **高效存储**:针对高维浮点向量优化的存储格式
|
||||
2. **ANN 检索**:内置多种近似最近邻索引算法(HNSW、IVF 等)
|
||||
3. **元数据过滤**:支持在向量搜索的同时按标签、时间等条件过滤
|
||||
4. **实时更新**:支持动态增删改向量,无需重建整个索引
|
||||
5. **水平扩展**:分布式架构支持亿级向量规模
|
||||
:::
|
||||
|
||||
| 数据库 | 类型 | 特点 | 适用场景 |
|
||||
|-------|------|------|---------|
|
||||
| Pinecone | 全托管云服务 | 零运维、开箱即用 | 快速原型、中小规模生产 |
|
||||
| Milvus | 开源分布式 | 高性能、可扩展 | 大规模生产环境 |
|
||||
| Chroma | 开源轻量 | 嵌入式、API 简洁 | 本地开发、小型项目 |
|
||||
| Weaviate | 开源云原生 | 内置向量化、GraphQL | 需要自动向量化的场景 |
|
||||
| Qdrant | 开源高性能 | Rust 实现、过滤强 | 需要复杂过滤的场景 |
|
||||
| pgvector | PG 扩展 | 复用现有 PG 基础设施 | 已有 PostgreSQL 的团队 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 端到端 Pipeline:从文本到检索的完整流程
|
||||
|
||||
理解了各个组件后,让我们把它们串起来,看看一个完整的向量检索系统是怎么工作的。
|
||||
|
||||
整个流程分为两条线:**离线写入**(把文档变成向量存起来)和**在线查询**(把问题变成向量去搜索)。
|
||||
|
||||
<EmbeddingPipelineDemo />
|
||||
|
||||
::: tip 离线写入流程
|
||||
1. **文档加载**:从各种来源(PDF、网页、数据库)读取原始文本
|
||||
2. **文本预处理**:清洗、去噪、标准化(去掉 HTML 标签、特殊字符等)
|
||||
3. **文本分块**:按策略将长文本切分为合适大小的片段(200~500 tokens)
|
||||
4. **向量化**:调用嵌入模型(如 OpenAI text-embedding-3-small)将每个片段转为向量
|
||||
5. **存入向量数据库**:将向量和原始文本、元数据一起写入数据库
|
||||
:::
|
||||
|
||||
::: tip 在线查询流程
|
||||
1. **接收查询**:用户输入自然语言问题
|
||||
2. **查询向量化**:用同一个嵌入模型将问题转为向量
|
||||
3. **相似度检索**:在向量数据库中搜索 Top-K 最相似的文档片段
|
||||
4. **后处理**:重排序、去重、元数据过滤
|
||||
5. **返回结果**:将最相关的文档片段返回给调用方(或交给 LLM 生成回答)
|
||||
:::
|
||||
|
||||
| 环节 | 关键选择 | 推荐方案 |
|
||||
|------|---------|---------|
|
||||
| 嵌入模型 | 精度 vs 成本 vs 速度 | OpenAI text-embedding-3-small(性价比高) |
|
||||
| 分块策略 | 粒度 vs 语义完整性 | 递归分块,200~500 tokens |
|
||||
| 向量数据库 | 规模 vs 运维成本 | 小项目用 Chroma,生产用 Pinecone/Milvus |
|
||||
| 相似度度量 | 语义 vs 精确 | 余弦相似度(文本场景首选) |
|
||||
| Top-K 值 | 召回率 vs 噪音 | 先检索 20 条,重排序后取 Top 5 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
Embedding 与向量检索是连接"人类语言"和"机器理解"的桥梁,也是 RAG、语义搜索、推荐系统等 AI 应用的基础设施。
|
||||
|
||||
回顾本章的关键要点:
|
||||
|
||||
1. **Embedding 的本质**:把文本映射到高维向量空间,让语义相似度变成空间距离
|
||||
2. **相似度度量**:余弦相似度关注方向(适合文本),欧氏距离关注绝对距离
|
||||
3. **索引是性能关键**:HNSW 和 IVF 让百万级向量的检索降到毫秒级
|
||||
4. **向量数据库选型**:小项目用 Chroma/pgvector,生产环境用 Pinecone/Milvus
|
||||
5. **端到端思维**:从文档加载到最终检索,每个环节的选择都会影响最终效果
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [OpenAI Embeddings 文档](https://platform.openai.com/docs/guides/embeddings) - 官方嵌入模型使用指南
|
||||
- [Pinecone Learning Center](https://www.pinecone.io/learn/) - 向量数据库和检索的系统教程
|
||||
- [FAISS Wiki](https://github.com/facebookresearch/faiss/wiki) - Facebook 开源的向量检索库文档
|
||||
- [Word2Vec 原始论文](https://arxiv.org/abs/1301.3781) - Embedding 时代的开山之作
|
||||
- [MTEB 排行榜](https://huggingface.co/spaces/mteb/leaderboard) - 嵌入模型性能对比排行榜
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,173 @@
|
||||
# 模型微调与部署
|
||||
|
||||
> 待实现
|
||||
::: tip 前言
|
||||
**大模型很强,但它不懂你的业务。** GPT-4 能写诗、能编程,但它不知道你公司的产品术语、不了解你行业的专业规范。微调(Fine-tuning)就是让通用大模型"学会"你的专业知识的过程——就像给一个博学的通才做岗前培训,让它变成你的领域专家。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **流程认知**:掌握从数据准备到模型上线的完整微调流水线
|
||||
- **数据工程**:了解微调数据的格式要求和质量标准
|
||||
- **高效微调**:理解 LoRA 等参数高效微调技术的原理和优势
|
||||
- **模型压缩**:掌握量化技术如何让大模型在消费级硬件上运行
|
||||
- **部署实践**:了解模型服务的主流架构和选型策略
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | 微调流水线 | 数据→训练→评估→部署 |
|
||||
| **第 2 章** | 训练数据 | 数据格式、质量控制 |
|
||||
| **第 3 章** | LoRA 微调 | 低秩适配、参数高效 |
|
||||
| **第 4 章** | 模型量化 | FP16、INT8、INT4 |
|
||||
| **第 5 章** | 模型部署 | 推理服务、API 网关 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:为什么需要微调?
|
||||
|
||||
大语言模型的训练分为两个阶段:**预训练**和**微调**。预训练是在海量通用数据上学习语言能力,微调是在特定任务数据上学习专业能力。
|
||||
|
||||
打个比方:预训练像是上大学——学习通识知识,什么都懂一点;微调像是入职培训——针对具体岗位学习专业技能。
|
||||
|
||||
::: tip 什么时候需要微调?
|
||||
- **特定输出格式**:需要模型始终以固定 JSON 格式输出
|
||||
- **专业领域知识**:医疗、法律、金融等领域的专业术语和规范
|
||||
- **语言风格迁移**:让模型用特定的语气、风格回答(如客服话术)
|
||||
- **小众语言支持**:提升模型在特定语言上的表现
|
||||
- **成本优化**:用小模型微调替代大模型调用,降低推理成本
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 微调流水线:从数据到上线的完整旅程
|
||||
|
||||
微调不是"把数据丢给模型就完事"。它是一个严谨的工程流程,每个环节都会影响最终效果。
|
||||
|
||||
<FinetuningPipelineDemo />
|
||||
|
||||
::: tip 微调的五个阶段
|
||||
1. **数据准备**:收集、清洗、标注训练数据,这是最耗时也最关键的环节
|
||||
2. **模型选择**:选择合适的基座模型(Base Model),如 Llama 3、Qwen、Mistral
|
||||
3. **训练配置**:设置学习率、batch size、epoch 数等超参数
|
||||
4. **训练执行**:在 GPU 上运行训练,监控 loss 曲线和评估指标
|
||||
5. **评估上线**:在测试集上评估效果,通过后部署为 API 服务
|
||||
:::
|
||||
|
||||
| 阶段 | 关键动作 | 常见陷阱 |
|
||||
|------|---------|---------|
|
||||
| 数据准备 | 清洗、去重、格式化 | 数据质量差导致模型"学坏" |
|
||||
| 模型选择 | 评估基座模型能力 | 模型太大训练不动,太小效果差 |
|
||||
| 训练配置 | 调整超参数 | 学习率过高导致灾难性遗忘 |
|
||||
| 训练执行 | 监控 loss 和指标 | 过拟合、训练不收敛 |
|
||||
| 评估上线 | A/B 测试、灰度发布 | 测试集泄漏导致评估虚高 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 训练数据:微调效果的天花板
|
||||
|
||||
在微调中有一句老话:**"Garbage in, garbage out"**。训练数据的质量直接决定了微调效果的上限。100 条高质量数据的效果,往往好过 10000 条低质量数据。
|
||||
|
||||
<TrainingDataDemo />
|
||||
|
||||
::: tip 微调数据的三种常见格式
|
||||
1. **指令格式(Instruction)**:最常用的格式,包含 instruction(指令)、input(输入)、output(期望输出)三个字段。适合训练模型遵循指令。
|
||||
2. **对话格式(Chat)**:多轮对话形式,包含 system、user、assistant 角色的消息列表。适合训练聊天机器人。
|
||||
3. **补全格式(Completion)**:简单的 prompt-completion 对,适合文本生成、代码补全等场景。
|
||||
:::
|
||||
|
||||
| 数据质量维度 | 说明 | 检查方法 |
|
||||
|------------|------|---------|
|
||||
| 准确性 | 答案必须正确无误 | 人工审核、专家校验 |
|
||||
| 一致性 | 相似问题的回答风格一致 | 抽样对比检查 |
|
||||
| 多样性 | 覆盖足够多的场景和变体 | 统计问题类型分布 |
|
||||
| 去重 | 避免重复样本导致过拟合 | 文本去重、语义去重 |
|
||||
| 数据量 | 通常 500~5000 条高质量数据即可 | 从少量开始,逐步增加 |
|
||||
|
||||
---
|
||||
|
||||
## 3. LoRA:用 1% 的参数实现 90% 的效果
|
||||
|
||||
全量微调(Full Fine-tuning)需要更新模型的所有参数——对于一个 70B 参数的模型,这意味着需要数百 GB 的显存和大量的 GPU 算力。对大多数团队来说,这不现实。
|
||||
|
||||
LoRA(Low-Rank Adaptation)提供了一个优雅的解决方案:**冻结原始模型参数,只训练一小组新增的低秩矩阵**。这些矩阵的参数量通常只有原模型的 0.1%~1%,但能达到接近全量微调的效果。
|
||||
|
||||
<LoRADemo />
|
||||
|
||||
::: tip LoRA 的核心思想
|
||||
原始模型的权重矩阵 W 是一个巨大的矩阵(如 4096×4096)。LoRA 不直接修改 W,而是在旁边加一个"旁路":W' = W + BA,其中 B 和 A 是两个小矩阵(如 4096×8 和 8×4096)。训练时只更新 B 和 A,原始 W 保持不变。
|
||||
- **秩(Rank)**:r 值越大,表达能力越强,但参数量也越多。通常 r=8~64 就够用
|
||||
- **合并部署**:训练完成后,可以把 BA 合并回 W,推理时零额外开销
|
||||
:::
|
||||
|
||||
| 微调方式 | 可训练参数 | 显存需求 | 训练速度 | 效果 |
|
||||
|---------|-----------|---------|---------|------|
|
||||
| 全量微调 | 100% | 极高 | 慢 | 最好 |
|
||||
| LoRA | 0.1%~1% | 低 | 快 | 接近全量 |
|
||||
| QLoRA | 0.1%~1% | 更低 | 中等 | 略低于 LoRA |
|
||||
| Prompt Tuning | < 0.01% | 极低 | 很快 | 有限 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 模型量化:让大模型"瘦身"
|
||||
|
||||
一个 70B 参数的模型,如果用 FP32(32 位浮点数)存储,需要 280GB 显存——没有几块顶级 GPU 根本跑不起来。量化(Quantization)技术通过降低数值精度来压缩模型体积,让大模型能在消费级硬件上运行。
|
||||
|
||||
<ModelQuantizationDemo />
|
||||
|
||||
::: tip 量化的核心权衡
|
||||
量化本质上是**精度换空间**的权衡。FP32 → FP16 几乎无损,INT8 有轻微损失,INT4 会有明显但通常可接受的质量下降。关键是找到你场景下的最佳平衡点。
|
||||
- **FP16(半精度)**:体积减半,质量几乎无损,是训练和推理的默认选择
|
||||
- **INT8(8 位整数)**:体积再减半,质量损失很小,适合大多数推理场景
|
||||
- **INT4(4 位整数)**:体积仅为 FP32 的 1/8,质量有一定损失,适合资源受限场景
|
||||
:::
|
||||
|
||||
| 精度 | 每参数字节 | 70B 模型体积 | 质量损失 | 适用场景 |
|
||||
|------|-----------|-------------|---------|---------|
|
||||
| FP32 | 4 字节 | ~280 GB | 无 | 训练基准 |
|
||||
| FP16 | 2 字节 | ~140 GB | 几乎无 | 标准训练和推理 |
|
||||
| INT8 | 1 字节 | ~70 GB | 很小 | 生产推理 |
|
||||
| INT4 | 0.5 字节 | ~35 GB | 可接受 | 边缘设备、本地部署 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 模型部署:从实验室到生产环境
|
||||
|
||||
模型训练好了,量化压缩了,最后一步是把它部署成可供调用的服务。模型部署不只是"把模型跑起来",还涉及并发处理、负载均衡、成本控制等工程问题。
|
||||
|
||||
<ModelServingDemo />
|
||||
|
||||
::: tip 三种主流部署方案
|
||||
1. **API 服务商**:直接使用 OpenAI、Anthropic 等厂商的 API。零运维,按 token 付费,适合快速验证和中小规模使用。
|
||||
2. **自托管推理服务**:用 vLLM、TGI 等框架在自己的 GPU 服务器上部署。成本可控,数据不出域,适合有隐私要求或大规模调用的场景。
|
||||
3. **Serverless 推理**:使用 AWS SageMaker、Replicate 等平台,按请求付费,自动扩缩容。适合流量波动大的场景。
|
||||
:::
|
||||
|
||||
| 部署方案 | 成本模型 | 延迟 | 运维复杂度 | 适用场景 |
|
||||
|---------|---------|------|-----------|---------|
|
||||
| API 服务商 | 按 token 计费 | 中等 | 零 | 快速原型、中小规模 |
|
||||
| vLLM 自部署 | GPU 租赁费用 | 低 | 高 | 大规模、隐私敏感 |
|
||||
| Serverless | 按请求计费 | 冷启动较高 | 低 | 流量波动大 |
|
||||
| 边缘部署 | 硬件一次性投入 | 极低 | 中 | 离线场景、IoT |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
模型微调与部署是让大模型从"通用工具"变成"专业助手"的关键环节。从数据准备到模型上线,每一步都需要工程化的思维和实践。
|
||||
|
||||
回顾本章的关键要点:
|
||||
|
||||
1. **微调是岗前培训**:让通用模型学会特定领域的知识和行为模式
|
||||
2. **数据质量决定上限**:100 条高质量数据胜过 10000 条低质量数据
|
||||
3. **LoRA 是效率之王**:用不到 1% 的参数实现接近全量微调的效果
|
||||
4. **量化是部署利器**:INT4 量化让 70B 模型在单卡上运行成为可能
|
||||
5. **部署方案因地制宜**:快速验证用 API,大规模用自部署,波动大用 Serverless
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [Hugging Face PEFT 文档](https://huggingface.co/docs/peft) - 参数高效微调库官方文档
|
||||
- [vLLM 文档](https://docs.vllm.ai/) - 高性能 LLM 推理引擎
|
||||
- [Unsloth](https://github.com/unslothai/unsloth) - 2x 加速的 LoRA 微调框架
|
||||
- [GGUF 格式说明](https://github.com/ggerganov/ggml/blob/master/docs/gguf.md) - llama.cpp 使用的量化模型格式
|
||||
- [OpenAI Fine-tuning Guide](https://platform.openai.com/docs/guides/fine-tuning) - OpenAI 官方微调指南
|
||||
|
||||
@@ -1,3 +1,161 @@
|
||||
# RAG 架构
|
||||
# RAG:检索增强生成
|
||||
|
||||
> 待实现
|
||||
::: tip 前言
|
||||
**为什么 ChatGPT 有时候会"一本正经地胡说八道"?** 大语言模型的知识来自训练数据,但训练数据有截止日期,也不包含你公司的内部文档。RAG(Retrieval-Augmented Generation,检索增强生成)就是解决这个问题的核心技术——让 AI 在回答之前,先去"查资料"。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
学完这章后,你将获得:
|
||||
|
||||
- **核心概念理解**:明白 RAG 是什么、为什么需要它,以及它如何解决大模型的"幻觉"问题
|
||||
- **完整流程认知**:掌握从文档加载、分块、向量化到检索、生成的端到端流程
|
||||
- **技术选型能力**:了解不同分块策略、检索方法的优劣,能根据场景做出选择
|
||||
- **架构演进视角**:理解 RAG 从 Naive 到 Advanced 再到 Modular 的演进路线
|
||||
- **实践决策能力**:知道什么时候该用 RAG、什么时候该用微调
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | RAG 基础流程 | 索引、检索、生成三阶段 |
|
||||
| **第 2 章** | 文本分块策略 | 固定分块、语义分块、递归分块 |
|
||||
| **第 3 章** | 检索技术 | 向量检索、关键词检索、混合检索 |
|
||||
| **第 4 章** | 架构演进 | Naive RAG → Advanced RAG → Modular RAG |
|
||||
| **第 5 章** | RAG vs 微调 | 两种方案的适用场景对比 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:为什么大模型需要"查资料"?
|
||||
|
||||
想象你是一个博学的教授,读过无数书籍。但如果有人问你"昨天公司的销售数据是多少",你肯定答不上来——因为这些信息不在你读过的书里。
|
||||
|
||||
大语言模型面临的就是同样的困境:
|
||||
|
||||
- **知识有截止日期**:GPT-4 的训练数据截止到某个时间点,之后发生的事它不知道
|
||||
- **缺乏私有知识**:你公司的内部文档、产品手册、客户数据,模型从未见过
|
||||
- **容易产生幻觉**:当模型不确定答案时,它倾向于"编造"一个看起来合理的回答
|
||||
|
||||
::: tip RAG 的核心思想
|
||||
RAG 的解决方案非常直觉:**在让模型回答之前,先帮它找到相关的参考资料**。就像开卷考试——你不需要记住所有知识,只需要知道去哪里找、怎么找。
|
||||
|
||||
RAG = 检索(Retrieval)+ 增强(Augmented)+ 生成(Generation)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. RAG 基础流程:索引、检索、生成
|
||||
|
||||
RAG 的工作流程可以分为两个阶段:**离线索引**和**在线查询**。
|
||||
|
||||
离线阶段就像图书馆的编目工作——把所有书籍分类、编号、上架,方便日后查找。在线阶段则是读者来图书馆查资料的过程——根据问题找到相关书籍,然后综合信息给出回答。
|
||||
|
||||
<RAGPipelineDemo />
|
||||
|
||||
::: tip 三个核心阶段
|
||||
1. **索引阶段(Indexing)**:将原始文档加载、清洗、分块,然后通过嵌入模型转化为向量,存入向量数据库。这是一次性的准备工作。
|
||||
2. **检索阶段(Retrieval)**:用户提问时,将问题也转化为向量,在向量数据库中搜索最相似的文档片段。
|
||||
3. **生成阶段(Generation)**:将检索到的文档片段和用户问题一起拼接为 Prompt,交给大模型生成最终回答。
|
||||
:::
|
||||
|
||||
| 阶段 | 输入 | 输出 | 关键技术 |
|
||||
|------|------|------|---------|
|
||||
| 索引 | 原始文档 | 向量数据库 | 文本分块、嵌入模型 |
|
||||
| 检索 | 用户问题 | Top-K 文档片段 | 向量相似度、重排序 |
|
||||
| 生成 | 问题 + 上下文 | 最终回答 | Prompt 工程、LLM |
|
||||
|
||||
---
|
||||
|
||||
## 2. 文本分块:把大象装进冰箱
|
||||
|
||||
文本分块是 RAG 中最容易被忽视、却对效果影响最大的环节。为什么需要分块?因为大模型的上下文窗口有限,我们不可能把整本书塞进去。更重要的是,**分块的质量直接决定了检索的质量**。
|
||||
|
||||
想象你在图书馆找一本书的某个知识点。如果整本书是一个"块",检索到了也没用——你还是得翻遍全书。但如果按章节甚至段落分块,就能精准定位到你需要的内容。
|
||||
|
||||
<ChunkingStrategyDemo />
|
||||
|
||||
::: tip 分块策略的选择
|
||||
- **固定大小分块**:按字符数或 token 数切分,简单粗暴但可能切断语义
|
||||
- **递归分块**:先按段落分,段落太长再按句子分,保持语义完整性
|
||||
- **语义分块**:用嵌入模型判断语义边界,相似度突变处切分
|
||||
- **文档结构分块**:利用 Markdown 标题、HTML 标签等结构信息分块
|
||||
|
||||
没有"最好"的分块策略,只有最适合你数据的策略。一般建议从递归分块开始,chunk 大小 200-500 tokens,overlap 10-20%。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 检索技术:如何找到最相关的内容?
|
||||
|
||||
分块完成后,下一个关键问题是:**用户提了一个问题,怎么从成千上万个文档片段中找到最相关的那几个?**
|
||||
|
||||
这就像在一个巨大的图书馆里找书。你可以按书名关键词搜索(关键词检索),也可以描述你想要的内容让图书管理员帮你找(语义检索),最好的方式是两者结合(混合检索)。
|
||||
|
||||
<RetrievalDemo />
|
||||
|
||||
| 检索方式 | 原理 | 优势 | 劣势 |
|
||||
|---------|------|------|------|
|
||||
| 关键词检索(BM25) | 基于词频和逆文档频率 | 精确匹配、速度快 | 无法理解语义、同义词失效 |
|
||||
| 向量检索 | 基于嵌入向量的余弦相似度 | 理解语义、支持模糊匹配 | 对专有名词不敏感 |
|
||||
| 混合检索 | 融合关键词和向量检索结果 | 兼顾精确和语义 | 需要调权重、复杂度高 |
|
||||
|
||||
::: tip 重排序(Reranking)
|
||||
检索到候选文档后,通常还需要一步"重排序"。初始检索追求召回率(尽量不遗漏),重排序追求精确率(把最相关的排到最前面)。常用的重排序模型有 Cohere Rerank、BGE Reranker 等,它们使用交叉编码器对 query-document 对进行精细打分。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 架构演进:从简单到智能
|
||||
|
||||
RAG 技术在短短两年内经历了三代演进,每一代都在解决上一代的痛点。
|
||||
|
||||
<RAGArchitectureDemo />
|
||||
|
||||
::: tip 三代 RAG 架构对比
|
||||
- **Naive RAG(2023)**:最基础的"索引→检索→生成"流程,实现简单但效果有限。问题包括:检索质量不稳定、无法处理复杂查询、容易引入噪音上下文。
|
||||
- **Advanced RAG(2024)**:在 Naive RAG 基础上增加了查询改写、混合检索、重排序、上下文压缩等优化环节,显著提升了检索精度和生成质量。
|
||||
- **Modular RAG(2025)**:将 RAG 拆解为可插拔的模块,支持路由判断、自适应检索、自我反思等高级能力。可根据查询类型动态选择最优处理流程。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. RAG vs 微调:该选哪个?
|
||||
|
||||
当你想让大模型掌握特定领域的知识时,通常有两条路:RAG 和微调(Fine-tuning)。它们不是互斥的,而是互补的。
|
||||
|
||||
打个比方:**微调像是让学生上培训班**,把知识内化到大脑里;**RAG 像是给学生发参考书**,考试时可以翻阅。两种方式各有优劣,关键看你的具体需求。
|
||||
|
||||
<RAGvsFineTuningDemo />
|
||||
|
||||
| 维度 | RAG | 微调 |
|
||||
|------|-----|------|
|
||||
| 知识更新 | 实时更新,改文档即可 | 需要重新训练 |
|
||||
| 成本 | 低(无需 GPU 训练) | 高(需要训练资源) |
|
||||
| 可解释性 | 高(可追溯来源) | 低(知识内化在权重中) |
|
||||
| 适用场景 | 知识库问答、文档检索 | 风格迁移、特定任务优化 |
|
||||
| 幻觉控制 | 较好(有参考依据) | 一般(仍可能幻觉) |
|
||||
|
||||
::: tip 实践建议
|
||||
大多数场景下,**先试 RAG**。RAG 的优势在于:不需要训练、知识可实时更新、回答可追溯来源。只有当你需要改变模型的"行为模式"(比如输出格式、语言风格、推理方式)时,才考虑微调。最强的方案往往是 **RAG + 微调** 的组合。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
RAG 是当前让大模型"落地"最实用的技术之一。它的核心价值在于:让模型的回答有据可查、知识可实时更新、幻觉可有效控制。
|
||||
|
||||
回顾本章的关键要点:
|
||||
|
||||
1. **RAG 解决的核心问题**:大模型知识过时、缺乏私有数据、容易幻觉
|
||||
2. **三阶段流程**:索引(离线准备)→ 检索(在线查找)→ 生成(综合回答)
|
||||
3. **分块是基础**:分块质量直接决定检索质量,选择合适的分块策略至关重要
|
||||
4. **检索是关键**:混合检索 + 重排序是目前效果最好的组合
|
||||
5. **架构在演进**:从 Naive RAG 到 Modular RAG,系统越来越智能和灵活
|
||||
6. **RAG 和微调互补**:大多数场景先试 RAG,需要改变模型行为时再考虑微调
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [LangChain RAG 教程](https://python.langchain.com/docs/tutorials/rag/) - 最流行的 RAG 框架实战指南
|
||||
- [LlamaIndex 文档](https://docs.llamaindex.ai/) - 专注于 RAG 的框架,提供丰富的数据连接器
|
||||
- [RAG Survey 论文](https://arxiv.org/abs/2312.10997) - 全面的 RAG 技术综述
|
||||
- [Chunking Strategies](https://www.pinecone.io/learn/chunking-strategies/) - Pinecone 的分块策略详解
|
||||
- [向量数据库对比](https://superlinked.com/vector-db-comparison) - 主流向量数据库的功能对比
|
||||
|
||||
@@ -214,7 +214,55 @@ function getPayAmount(employee) {
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
## 5. AI 助力:用大模型提升代码质量
|
||||
|
||||
大模型在代码质量领域已经非常实用,它可以充当你的"24 小时在线的代码审查员"。
|
||||
|
||||
### 5.1 识别代码坏味道
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 请审查以下代码,识别其中的代码坏味道(Code Smell),包括但不限于:
|
||||
> 过长函数、魔法数字、重复代码、过深嵌套、过长参数列表。
|
||||
> 对每个问题给出具体位置、问题描述和改进建议。
|
||||
>
|
||||
> [粘贴你的代码]
|
||||
> ```
|
||||
|
||||
### 5.2 自动重构
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 请对以下代码进行重构,要求:
|
||||
> 1. 不改变外部行为
|
||||
> 2. 使用提炼函数、卫语句替代嵌套等手法
|
||||
> 3. 改善命名,消除魔法数字
|
||||
> 4. 解释每一步重构的理由
|
||||
>
|
||||
> [粘贴你的代码]
|
||||
> ```
|
||||
|
||||
### 5.3 模拟 Code Review
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 请以资深开发者的视角审查这段代码,从以下维度给出反馈:
|
||||
> - 正确性:逻辑是否有 Bug?边界条件是否处理?
|
||||
> - 可读性:命名是否清晰?结构是否易懂?
|
||||
> - 性能:是否有明显的性能问题?
|
||||
> - 安全性:是否有注入或数据泄露风险?
|
||||
> 用"建议"而非"命令"的语气,给出改进方案。
|
||||
>
|
||||
> [粘贴你的代码]
|
||||
> ```
|
||||
|
||||
::: tip AI 使用建议
|
||||
AI 的重构建议需要你自己验证——跑测试确认行为没变。把 AI 当作"提建议的同事",而不是"无条件信任的权威"。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
回顾这一路,我们从识别问题到解决问题,建立了一套完整的代码质量改进体系:
|
||||
|
||||
|
||||
@@ -207,7 +207,55 @@ calculatePrice(100, 'svip') // 60
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
## 5. AI 助力:用大模型学习和应用设计模式
|
||||
|
||||
大模型可以帮你识别代码中适合使用设计模式的场景,并给出具体的重构方案。
|
||||
|
||||
### 5.1 识别适用模式
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 分析以下代码,判断是否存在可以用设计模式改进的地方。
|
||||
> 如果有,请说明:
|
||||
> 1. 当前代码的问题
|
||||
> 2. 推荐使用哪种设计模式
|
||||
> 3. 重构后的代码示例
|
||||
> 4. 为什么这个模式适合这个场景
|
||||
>
|
||||
> [粘贴你的代码]
|
||||
> ```
|
||||
|
||||
### 5.2 用具体场景学习模式
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 用一个"外卖点餐系统"的真实场景,分别演示以下设计模式的应用:
|
||||
> - 工厂模式:创建不同类型的订单
|
||||
> - 观察者模式:订单状态变化通知
|
||||
> - 策略模式:不同的配送费计算规则
|
||||
>
|
||||
> 用 JavaScript 代码示例,每个模式先展示不用模式的问题,
|
||||
> 再展示用模式后的改进。
|
||||
> ```
|
||||
|
||||
### 5.3 判断是否过度设计
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 审查以下代码,判断是否存在过度设计的问题。
|
||||
> 是否有不必要的抽象、用不到的设计模式、或过早的优化?
|
||||
> 如果有,请建议如何简化,遵循 KISS 原则。
|
||||
>
|
||||
> [粘贴你的代码]
|
||||
> ```
|
||||
|
||||
::: tip AI 使用建议
|
||||
让 AI 用你熟悉的业务场景来解释设计模式,比看抽象的 UML 图有效得多。但记住:AI 可能倾向于推荐更复杂的方案,你需要自己判断是否真的需要。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
1. **创建型模式**:解决"如何创建对象"的问题,让创建过程更灵活
|
||||
2. **结构型模式**:解决"如何组织代码"的问题,让结构更清晰
|
||||
|
||||
@@ -142,7 +142,55 @@ git commit -m "fix: 修复 README 中的安装命令拼写错误"
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
## 5. AI 助力:用大模型加速开源贡献
|
||||
|
||||
大模型可以帮你快速理解陌生代码库、写出高质量的 PR 描述、甚至辅助 Code Review。
|
||||
|
||||
### 5.1 快速理解陌生代码库
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 我刚 clone 了一个开源项目,请帮我分析以下目录结构,
|
||||
> 说明每个目录/文件的职责,以及代码的整体架构和数据流向。
|
||||
> 我想修复一个登录相关的 Bug,应该从哪里开始看?
|
||||
>
|
||||
> [粘贴 tree 命令输出或目录结构]
|
||||
> ```
|
||||
|
||||
### 5.2 写 PR 描述
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 根据以下 git diff,帮我写一份 Pull Request 描述,包括:
|
||||
> - 标题(简洁,说明改了什么)
|
||||
> - 改动说明(为什么改、改了什么)
|
||||
> - 测试方法(如何验证改动正确)
|
||||
> - 关联 Issue(如果有)
|
||||
> 用英文撰写,语气专业友好。
|
||||
>
|
||||
> [粘贴 git diff 输出]
|
||||
> ```
|
||||
|
||||
### 5.3 辅助翻译文档
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 将以下中文技术文档翻译为英文,要求:
|
||||
> 1. 技术术语使用业界通用的英文表达
|
||||
> 2. 代码注释和变量名不翻译
|
||||
> 3. 保持 Markdown 格式不变
|
||||
> 4. 语气自然流畅,不要机翻感
|
||||
>
|
||||
> [粘贴中文文档]
|
||||
> ```
|
||||
|
||||
::: tip AI 使用建议
|
||||
用 AI 写 PR 描述时,确保你自己理解了每一行改动。审查者可能会问你为什么这么改——如果你答不上来,说明你还没真正理解。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
1. **流程**:Fork → Branch → Commit → PR → Review → Merge
|
||||
2. **许可证**:MIT 最宽松,GPL 最严格,根据需求选择
|
||||
|
||||
@@ -148,7 +148,55 @@ Strict-Transport-Security: max-age=31536000
|
||||
|
||||
---
|
||||
|
||||
## 4. 总结
|
||||
## 4. AI 助力:用大模型提升安全防护
|
||||
|
||||
大模型可以充当你的"安全顾问",帮你审计代码漏洞、生成安全方案。
|
||||
|
||||
### 4.1 代码安全审计
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 请对以下代码进行安全审计,检查是否存在:
|
||||
> - XSS 漏洞(未转义的用户输入)
|
||||
> - SQL 注入(字符串拼接查询)
|
||||
> - CSRF 风险(缺少 Token 验证)
|
||||
> - 敏感数据泄露(硬编码密钥、明文密码)
|
||||
> 对每个问题给出风险等级、具体位置和修复方案。
|
||||
>
|
||||
> [粘贴你的代码]
|
||||
> ```
|
||||
|
||||
### 4.2 生成安全配置
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 我的项目使用 Express.js + PostgreSQL,即将部署上线。
|
||||
> 请生成一份完整的安全配置清单,包括:
|
||||
> - HTTP 安全头配置代码
|
||||
> - CORS 配置
|
||||
> - 数据库连接的安全设置
|
||||
> - 环境变量管理方案
|
||||
> 给出可直接使用的代码片段。
|
||||
> ```
|
||||
|
||||
### 4.3 解释漏洞原理
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 用一个具体的例子,解释 CSRF 攻击的完整流程:
|
||||
> 1. 攻击者如何构造恶意页面
|
||||
> 2. 为什么浏览器会自动携带 Cookie
|
||||
> 3. 服务端如何用 CSRF Token 防御
|
||||
> 用代码演示攻击和防御的完整过程。
|
||||
> ```
|
||||
|
||||
::: tip AI 使用建议
|
||||
AI 的安全审计不能替代专业的安全测试。把它当作第一道筛查,关键系统仍需专业安全团队审计。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
|
||||
1. **安全思维**:永不信任外部输入,最小权限,纵深防御
|
||||
2. **常见攻击**:XSS、SQL 注入、CSRF 是最高频的 Web 安全威胁
|
||||
|
||||
@@ -151,7 +151,58 @@ for (let i = items.length - 1; i >= 0; i--) { ... }
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
## 5. AI 助力:用大模型提升文档质量
|
||||
|
||||
大模型在技术写作领域几乎是"天赋异禀"——生成文档、改善表达、翻译内容都是它的强项。
|
||||
|
||||
### 5.1 生成 API 文档
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 根据以下 Express 路由代码,生成完整的 API 文档,包括:
|
||||
> - 端点路径和方法
|
||||
> - 请求参数(路径参数、查询参数、请求体)及类型
|
||||
> - 成功和错误的响应示例
|
||||
> - 使用 curl 的调用示例
|
||||
>
|
||||
> [粘贴你的路由代码]
|
||||
> ```
|
||||
|
||||
### 5.2 改善技术写作
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 请改善以下技术文档的表达,要求:
|
||||
> 1. 语言简洁清晰,去掉冗余表述
|
||||
> 2. 用主动语态替代被动语态
|
||||
> 3. 专业术语保持准确
|
||||
> 4. 添加必要的代码示例
|
||||
> 保持原意不变,只改善表达质量。
|
||||
>
|
||||
> [粘贴你的文档内容]
|
||||
> ```
|
||||
|
||||
### 5.3 生成 README
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 根据以下项目信息,生成一份高质量的 README.md:
|
||||
> - 项目名称:[名称]
|
||||
> - 一句话描述:[描述]
|
||||
> - 技术栈:[列出]
|
||||
> - 核心功能:[列出]
|
||||
>
|
||||
> 要求包含:项目简介、快速开始、功能特性、
|
||||
> 安装步骤(含代码)、使用示例、贡献指南、许可证。
|
||||
> ```
|
||||
|
||||
::: tip AI 使用建议
|
||||
AI 生成的文档要检查技术细节是否准确——它可能编造不存在的 API 参数或错误的返回值。始终对照实际代码验证。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
1. **类型匹配**:不同文档有不同的结构和写法
|
||||
2. **清晰优先**:具体、准确、面向读者
|
||||
|
||||
@@ -119,7 +119,55 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. 总结
|
||||
## 4. AI 助力:用大模型辅助技术选型
|
||||
|
||||
大模型可以帮你快速调研技术方案、对比优劣、生成决策报告。
|
||||
|
||||
### 4.1 技术方案对比
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 我需要为一个电商项目选择数据库,候选方案:
|
||||
> MySQL、PostgreSQL、MongoDB。
|
||||
> 项目特点:读多写少、需要复杂查询、数据量预计千万级。
|
||||
>
|
||||
> 请从以下维度对比三个方案:
|
||||
> 性能、生态、学习曲线、运维成本、扩展性。
|
||||
> 用表格形式呈现,并给出最终推荐和理由。
|
||||
> ```
|
||||
|
||||
### 4.2 生成架构决策记录(ADR)
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 帮我写一份架构决策记录(ADR),格式如下:
|
||||
> - 标题:选择 Vue 3 作为前端框架
|
||||
> - 背景:[项目背景和需求]
|
||||
> - 候选方案:React, Vue 3, Svelte
|
||||
> - 决策:Vue 3
|
||||
> - 理由:[基于团队能力、生态、性能等维度]
|
||||
> - 后果:[选择后的影响和风险]
|
||||
> ```
|
||||
|
||||
### 4.3 调研新技术
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 我在考虑是否在项目中引入 Bun 替代 Node.js,请帮我分析:
|
||||
> 1. Bun 相比 Node.js 的核心优势和劣势
|
||||
> 2. 当前生态成熟度(npm 兼容性、主流框架支持)
|
||||
> 3. 生产环境使用的风险点
|
||||
> 4. 适合和不适合使用 Bun 的场景
|
||||
> 给出客观评估,不要只说优点。
|
||||
> ```
|
||||
|
||||
::: tip AI 使用建议
|
||||
AI 的知识有时效性——它可能不了解最新版本的变化。对于快速迭代的技术,用 AI 做初步调研后,务必查阅官方文档确认最新信息。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
|
||||
1. **技术雷达**:了解技术的成熟度,区分采纳/试验/评估/暂缓
|
||||
2. **选型维度**:团队能力 > 社区生态 > 性能需求 > 维护状态
|
||||
|
||||
@@ -149,7 +149,53 @@ TDD 适合逻辑密集的代码(算法、业务规则、数据转换),但
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
## 5. AI 助力:用大模型提升测试效率
|
||||
|
||||
大模型在测试领域的能力已经非常强大——它可以帮你生成测试用例、发现边界条件、甚至写出完整的测试代码。
|
||||
|
||||
### 5.1 生成单元测试
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 请为以下函数编写单元测试,使用 Vitest 框架,要求:
|
||||
> 1. 遵循 AAA 模式(Arrange-Act-Assert)
|
||||
> 2. 覆盖正常路径、边界条件和错误路径
|
||||
> 3. 每个测试用例有清晰的中文描述
|
||||
>
|
||||
> [粘贴你的函数代码]
|
||||
> ```
|
||||
|
||||
### 5.2 发现边界条件
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 分析以下函数,列出所有可能的边界条件和极端输入场景,
|
||||
> 包括:空值、零、负数、超大数、特殊字符、并发情况等。
|
||||
> 对每个场景说明预期行为和可能的风险。
|
||||
>
|
||||
> [粘贴你的函数代码]
|
||||
> ```
|
||||
|
||||
### 5.3 从需求生成测试(TDD 辅助)
|
||||
|
||||
> **提示词**:
|
||||
> ```
|
||||
> 我要实现一个购物车模块,需求如下:
|
||||
> - 添加商品、删除商品、修改数量
|
||||
> - 自动计算总价(含折扣)
|
||||
> - 库存不足时提示错误
|
||||
>
|
||||
> 请按照 TDD 思路,先写出测试用例(不写实现),
|
||||
> 使用 Vitest,覆盖所有核心场景。
|
||||
> ```
|
||||
|
||||
::: tip AI 使用建议
|
||||
AI 生成的测试要检查断言是否有意义——避免 `expect(true).toBe(true)` 这种无效测试。好的测试应该在代码出错时真的能失败。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
1. **测试金字塔**:底层多、顶层少,平衡速度与真实度
|
||||
2. **单元测试**:遵循 FIRST 原则和 AAA 模式,测试核心逻辑
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
从晶体管到操作系统,深入了解计算机如何工作:
|
||||
<NavGrid>
|
||||
<NavCard
|
||||
href="/zh-cn/appendix/1-computer-fundamentals/vibe-coding-fullstack"
|
||||
title="Vibe Coding 时代下的全栈开发"
|
||||
description="AI 辅助时代,前端、后端、编程语言、全栈工程师的成长路径全景图"
|
||||
/>
|
||||
<NavCard
|
||||
href="/zh-cn/appendix/1-computer-fundamentals/transistor-to-cpu"
|
||||
title="从晶体管到 CPU"
|
||||
|
||||
Reference in New Issue
Block a user