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()
|
||||
|
||||
Reference in New Issue
Block a user