feat: add comprehensive backend topics and fix build issues
## 新增内容 ### 附录文档扩展 - 扩展前端项目架构文档 (frontend-project-architecture.md) - 扩展后端项目架构文档 (backend-project-architecture.md) - 扩展数据治理文档 (data-governance.md) - 扩展数据可视化文档 (data-visualization.md) - 扩展分布式系统文档 (distributed-systems.md) - 扩展高可用文档 (high-availability.md) - 扩展单体到微服务文档 (monolith-to-microservices.md) - 扩展系统设计方法论文档 (system-design-methodology.md) - 扩展 Docker 容器文档 (docker-containers.md) - 扩展 Kubernetes 文档 (kubernetes.md) - 扩展 Linux 基础文档 (linux-basics.md) - 扩展神经网络文档 (neural-networks.md) ### 新增交互式组件 - 数据治理组件: DataQualityDemo, DataGovernanceFrameworkDemo, DataLineageDemo - 数据可视化组件: ChartTypeSelectorDemo, DashboardLayoutDemo - 分布式系统组件: CAPTheoremDemo, ConsistencyModelsDemo, DistributedChallengesDemo - 高可用组件: AvailabilityCalculatorDemo, FailoverStrategyDemo - 系统设计组件: SystemDesignStepsDemo, CapacityEstimationDemo - Docker 容器组件: DockerArchitectureDemo, DockerLifecycleDemo - Kubernetes 组件: K8sArchitectureDemo, K8sWorkloadsDemo - Linux 基础组件: LinuxFileSystemDemo, LinuxCommandDemo, LinuxPermissionsDemo - 神经网络组件: NeuronDemo, NetworkLayersDemo, NetworkArchitectureDemo - 单体到微服务组件: ArchEvolutionDemo - 项目架构组件: ProjectArchitectureComparisonDemo - 附录导航组件: AppendixFlowMap ### 英文版重构 - 将 en-us 目录重命名为 en - 更新相关配置和组件中的语言代码 ## Bug 修复 - 修复 index.js 中重复的组件导入语句导致的 build 失败 - 恢复被注释的 InvertedIndexDemo 和 SearchRelevanceDemo 导入 - 修复 HomeFeatures.vue 中 en-us 与 config.mjs 中 en 不一致导致的语言切换问题 ## 其他改进 - 添加构建脚本 (scripts/build.mjs) - 更新依赖版本
This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { withBase } from 'vitepress'
|
||||
|
||||
const categories = [
|
||||
{
|
||||
id: 'computer-fundamentals',
|
||||
name: '计算机基础',
|
||||
icon: '💻',
|
||||
color: '#10b981',
|
||||
bgGradient: 'linear-gradient(135deg, #10b98115, #10b98108)',
|
||||
articles: [
|
||||
{ title: 'Vibe Coding 全栈开发', path: '/zh-cn/appendix/1-computer-fundamentals/vibe-coding-fullstack' },
|
||||
{ title: '从晶体管到 CPU', path: '/zh-cn/appendix/1-computer-fundamentals/transistor-to-cpu' },
|
||||
{ title: '操作系统', path: '/zh-cn/appendix/1-computer-fundamentals/operating-systems' },
|
||||
{ title: '数据结构', path: '/zh-cn/appendix/1-computer-fundamentals/data-structures' },
|
||||
{ title: '算法思维入门', path: '/zh-cn/appendix/1-computer-fundamentals/algorithm-thinking' },
|
||||
{ title: '编程语言图谱', path: '/zh-cn/appendix/1-computer-fundamentals/programming-languages' },
|
||||
{ title: '网络基础', path: '/zh-cn/appendix/1-computer-fundamentals/computer-networks' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'development-tools',
|
||||
name: '开发工具',
|
||||
icon: '🔧',
|
||||
color: '#3b82f6',
|
||||
bgGradient: 'linear-gradient(135deg, #3b82f615, #3b82f608)',
|
||||
articles: [
|
||||
{ title: 'IDE 基础', path: '/zh-cn/appendix/2-development-tools/ide-basics' },
|
||||
{ title: '命令行与 Shell', path: '/zh-cn/appendix/2-development-tools/command-line-shell' },
|
||||
{ title: 'Git 版本控制', path: '/zh-cn/appendix/2-development-tools/git-version-control' },
|
||||
{ title: '环境变量与 PATH', path: '/zh-cn/appendix/2-development-tools/environment-path' },
|
||||
{ title: '包管理器', path: '/zh-cn/appendix/2-development-tools/package-managers' },
|
||||
{ title: '调试的艺术', path: '/zh-cn/appendix/2-development-tools/debugging-art/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'browser-frontend',
|
||||
name: '浏览器与前端',
|
||||
icon: '🌍',
|
||||
color: '#f59e0b',
|
||||
bgGradient: 'linear-gradient(135deg, #f59e0b15, #f59e0b08)',
|
||||
articles: [
|
||||
{ title: 'JavaScript 深入', path: '/zh-cn/appendix/3-browser-and-frontend/javascript-deep-dive' },
|
||||
{ title: 'TypeScript', path: '/zh-cn/appendix/3-browser-and-frontend/typescript' },
|
||||
{ title: '浏览器是一个操作系统', path: '/zh-cn/appendix/3-browser-and-frontend/browser-as-os' },
|
||||
{ title: '浏览器渲染管道', path: '/zh-cn/appendix/3-browser-and-frontend/browser-as-os-rendering' },
|
||||
{ title: '前端框架对比', path: '/zh-cn/appendix/3-browser-and-frontend/frontend-frameworks' },
|
||||
{ title: '前端工程化', path: '/zh-cn/appendix/3-browser-and-frontend/frontend-engineering' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'server-backend',
|
||||
name: '服务端与后端',
|
||||
icon: '⚙️',
|
||||
color: '#8b5cf6',
|
||||
bgGradient: 'linear-gradient(135deg, #8b5cf615, #8b5cf608)',
|
||||
articles: [
|
||||
{ title: '后端语言对比', path: '/zh-cn/appendix/4-server-and-backend/backend-languages' },
|
||||
{ title: 'HTTP 协议', path: '/zh-cn/appendix/4-server-and-backend/http-protocol' },
|
||||
{ title: 'API 设计哲学', path: '/zh-cn/appendix/4-server-and-backend/api-design' },
|
||||
{ title: 'Web 框架的本质', path: '/zh-cn/appendix/4-server-and-backend/web-frameworks' },
|
||||
{ title: '认证与授权', path: '/zh-cn/appendix/4-server-and-backend/auth-authorization' },
|
||||
{ title: '缓存策略', path: '/zh-cn/appendix/4-server-and-backend/caching' },
|
||||
{ title: '消息队列', path: '/zh-cn/appendix/4-server-and-backend/message-queues' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
name: '数据',
|
||||
icon: '📊',
|
||||
color: '#ec4899',
|
||||
bgGradient: 'linear-gradient(135deg, #ec489915, #ec489908)',
|
||||
articles: [
|
||||
{ title: 'SQL', path: '/zh-cn/appendix/5-data/sql' },
|
||||
{ title: '数据库原理', path: '/zh-cn/appendix/5-data/database-fundamentals' },
|
||||
{ title: '数据模型全景', path: '/zh-cn/appendix/5-data/data-models' },
|
||||
{ title: '数据分析基础', path: '/zh-cn/appendix/5-data/data-analysis' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'architecture',
|
||||
name: '架构设计',
|
||||
icon: '🏗️',
|
||||
color: '#14b8a6',
|
||||
bgGradient: 'linear-gradient(135deg, #14b8a615, #14b8a608)',
|
||||
articles: [
|
||||
{ title: '从单体到微服务', path: '/zh-cn/appendix/6-architecture-and-system-design/monolith-to-microservices' },
|
||||
{ title: '分布式系统', path: '/zh-cn/appendix/6-architecture-and-system-design/distributed-systems' },
|
||||
{ title: '高可用与容灾', path: '/zh-cn/appendix/6-architecture-and-system-design/high-availability' },
|
||||
{ title: '系统设计方法论', path: '/zh-cn/appendix/6-architecture-and-system-design/system-design-methodology' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'infrastructure',
|
||||
name: '基础设施',
|
||||
icon: '☁️',
|
||||
color: '#06b6d4',
|
||||
bgGradient: 'linear-gradient(135deg, #06b6d415, #06b6d408)',
|
||||
articles: [
|
||||
{ title: 'Linux 基础', path: '/zh-cn/appendix/7-infrastructure-and-operations/linux-basics' },
|
||||
{ title: 'Docker 容器化', path: '/zh-cn/appendix/7-infrastructure-and-operations/docker-containers' },
|
||||
{ title: 'Kubernetes', path: '/zh-cn/appendix/7-infrastructure-and-operations/kubernetes' },
|
||||
{ title: 'CI/CD 自动化', path: '/zh-cn/appendix/7-infrastructure-and-operations/ci-cd' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
name: '人工智能',
|
||||
icon: '🤖',
|
||||
color: '#f97316',
|
||||
bgGradient: 'linear-gradient(135deg, #f9731615, #f9731608)',
|
||||
articles: [
|
||||
{ title: 'AI 简史', path: '/zh-cn/appendix/8-artificial-intelligence/ai-history' },
|
||||
{ title: '神经网络', path: '/zh-cn/appendix/8-artificial-intelligence/neural-networks' },
|
||||
{ title: 'Transformer', path: '/zh-cn/appendix/8-artificial-intelligence/transformer-attention' },
|
||||
{ title: '大语言模型原理', path: '/zh-cn/appendix/8-artificial-intelligence/llm-principles' },
|
||||
{ title: 'RAG 架构', path: '/zh-cn/appendix/8-artificial-intelligence/rag' },
|
||||
{ title: 'AI Agent', path: '/zh-cn/appendix/8-artificial-intelligence/ai-agents' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'engineering',
|
||||
name: '工程素养',
|
||||
icon: '✨',
|
||||
color: '#a855f7',
|
||||
bgGradient: 'linear-gradient(135deg, #a855f715, #a855f708)',
|
||||
articles: [
|
||||
{ title: '设计模式', path: '/zh-cn/appendix/9-engineering-excellence/design-patterns' },
|
||||
{ title: '代码质量与重构', path: '/zh-cn/appendix/9-engineering-excellence/code-quality-refactoring' },
|
||||
{ title: '测试策略', path: '/zh-cn/appendix/9-engineering-excellence/testing-strategies' },
|
||||
{ title: '技术写作', path: '/zh-cn/appendix/9-engineering-excellence/technical-writing' },
|
||||
{ title: '开源协作', path: '/zh-cn/appendix/9-engineering-excellence/open-source-collaboration' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const activeCategory = ref(null)
|
||||
const hoveredArticle = ref(null)
|
||||
|
||||
const toggleCategory = (id) => {
|
||||
activeCategory.value = activeCategory.value === id ? null : id
|
||||
}
|
||||
|
||||
const articleCount = categories.reduce((sum, cat) => sum + cat.articles.length, 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="appendix-bento">
|
||||
<div class="bento-header">
|
||||
<h3 class="bento-title">探索附录</h3>
|
||||
<p class="bento-subtitle">9 个主题方向 · {{ articleCount }} 篇文章</p>
|
||||
</div>
|
||||
|
||||
<div class="bento-grid">
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
class="bento-card"
|
||||
:class="{ active: activeCategory === category.id }"
|
||||
:style="{
|
||||
'--card-color': category.color,
|
||||
'--card-bg': category.bgGradient
|
||||
}"
|
||||
@click="toggleCategory(category.id)"
|
||||
>
|
||||
<div class="card-icon">{{ category.icon }}</div>
|
||||
<div class="card-content">
|
||||
<h4 class="card-title">{{ category.name }}</h4>
|
||||
<p class="card-count">{{ category.articles.length }} 篇</p>
|
||||
</div>
|
||||
|
||||
<Transition name="pop">
|
||||
<div v-if="activeCategory === category.id" class="card-articles">
|
||||
<a
|
||||
v-for="article in category.articles"
|
||||
:key="article.path"
|
||||
:href="withBase(article.path)"
|
||||
class="article-item"
|
||||
@mouseenter="hoveredArticle = article.path"
|
||||
@mouseleave="hoveredArticle = null"
|
||||
>
|
||||
<span class="article-dot"></span>
|
||||
<span class="article-title">{{ article.title }}</span>
|
||||
<span class="article-arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="card-indicator" v-if="activeCategory !== category.id">
|
||||
<span>{{ category.articles.length }} 篇 →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.appendix-bento {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.bento-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.bento-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 0.25rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.bento-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bento-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.bento-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.bento-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.bento-card {
|
||||
position: relative;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bento-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--card-bg);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.bento-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bento-card:hover {
|
||||
border-color: var(--card-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.bento-card.active {
|
||||
border-color: var(--card-color);
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.bento-card.active::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.card-count {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-indicator {
|
||||
position: absolute;
|
||||
bottom: 1.25rem;
|
||||
right: 1.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-3);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bento-card:hover .card-indicator {
|
||||
color: var(--card-color);
|
||||
}
|
||||
|
||||
.card-articles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 15px;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.article-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.article-item:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.article-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--card-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
flex: 1;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.article-arrow {
|
||||
opacity: 0;
|
||||
color: var(--card-color);
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.article-item:hover .article-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.pop-enter-active {
|
||||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.pop-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pop-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.pop-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
@@ -220,7 +220,7 @@ const i18n = {
|
||||
btn: '>_ Start'
|
||||
}
|
||||
},
|
||||
'en-us': {
|
||||
'en': {
|
||||
nav: {
|
||||
title: 'Easy-Vibe Tutorial',
|
||||
home: 'Home',
|
||||
@@ -231,27 +231,27 @@ const i18n = {
|
||||
start: 'Start Learning'
|
||||
},
|
||||
stage1: {
|
||||
cat: 'Stage 1 · Novice & PM',
|
||||
cat: 'Stage 1 · Getting Started',
|
||||
title: 'Zero to Hero, <br><span class="highlight">Be Your Own PM.</span>',
|
||||
sub: 'No CS background needed. Just speak your idea, and AI will turn it into high-fidelity web prototypes.',
|
||||
sub: 'No CS background needed. Just speak your ideas—AI will turn them into high-fidelity web prototypes.',
|
||||
cards: [
|
||||
{
|
||||
title: 'AI Product Manager',
|
||||
desc: 'From idea to prototype, just by speaking.',
|
||||
sub: 'Non-tech friendly',
|
||||
link: '/en-us/stage-0/'
|
||||
title: 'Learning Map',
|
||||
desc: 'Understand the complete learning path from zero to full-stack development.',
|
||||
sub: 'All Ages Friendly',
|
||||
link: '/en/stage-0/0.1-learning-map/'
|
||||
},
|
||||
{
|
||||
title: 'Gamified Intro',
|
||||
desc: 'Build Snake, Tetris, and break the fear of code.',
|
||||
sub: 'Learn by playing',
|
||||
link: '/en-us/stage-0/'
|
||||
desc: 'Experience the magic of AI programming by building games like Snake.',
|
||||
sub: 'Learn by Playing',
|
||||
link: '/en/stage-0/0.2-ai-capabilities-through-games/'
|
||||
},
|
||||
{
|
||||
title: 'Vibe Coding',
|
||||
desc: 'Master the core of AI coding: Prompt Engineering & Context.',
|
||||
desc: 'Master the core of AI coding: From product ideas to interactive prototypes.',
|
||||
sub: 'Core Mindset',
|
||||
link: '/en-us/stage-0/'
|
||||
link: '/en/stage-0/'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -259,25 +259,25 @@ const i18n = {
|
||||
cat: 'Stage 2 · Junior/Mid Dev',
|
||||
title:
|
||||
'Go Full Stack, <br><span class="highlight">Build Real Apps.</span>',
|
||||
sub: 'Master frontend-backend separation. Build commercial-grade projects with DB, API, and complex interactions.',
|
||||
sub: 'Master frontend-backend separation and build commercial-grade projects with DB and API.',
|
||||
cards: [
|
||||
{
|
||||
title: 'Full Stack',
|
||||
headline: 'Frontend & Backend.',
|
||||
desc: 'From DB design to API and components, build a modern web app completely.',
|
||||
link: '/en-us/stage-2/'
|
||||
link: '/zh-cn/stage-2/assignments/2.1-fullstack-app/'
|
||||
},
|
||||
{
|
||||
title: 'Real Projects',
|
||||
headline: 'No Toy Code.',
|
||||
desc: 'Deep dive into Auth, Storage, File Uploads and core business logic.',
|
||||
link: '/en-us/stage-2/'
|
||||
link: '/zh-cn/stage-2/backend/2.2-database-supabase/chapter5/'
|
||||
},
|
||||
{
|
||||
title: 'Deployment',
|
||||
headline: 'Show the World.',
|
||||
desc: 'Server config, DNS, CI/CD. The last mile of product delivery.',
|
||||
link: '/en-us/stage-2/'
|
||||
link: '/zh-cn/stage-2/backend/2.5-zeabur-deployment/extra6/'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -290,22 +290,22 @@ const i18n = {
|
||||
{
|
||||
title: 'WeChat Mini-app',
|
||||
desc: 'Cross-platform dev, reaching millions of users.',
|
||||
link: '/en-us/stage-3/'
|
||||
link: '/zh-cn/stage-3/cross-platform/3.3-wechat-miniprogram/'
|
||||
},
|
||||
{
|
||||
title: 'AI Native Apps',
|
||||
desc: 'RAG, Agent. Explore the limits of LLMs.',
|
||||
link: '/en-us/stage-3/'
|
||||
link: '/zh-cn/stage-3/ai-advanced/3.a1-rag-introduction/'
|
||||
},
|
||||
{
|
||||
title: 'Complex Arch',
|
||||
desc: 'High concurrency, High availability architecture design.',
|
||||
link: '/en-us/stage-3/'
|
||||
desc: 'Architecture design for high concurrency and stable AI tasks.',
|
||||
link: '/zh-cn/stage-3/core-skills/3.2-long-running-tasks/'
|
||||
},
|
||||
{
|
||||
title: 'Personal Brand',
|
||||
desc: 'Build your own website and academic blog.',
|
||||
link: '/en-us/stage-3/'
|
||||
link: '/zh-cn/stage-3/personal-brand/3.7-personal-website-blog/'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -318,62 +318,62 @@ const i18n = {
|
||||
{
|
||||
title: 'AI History',
|
||||
desc: 'Milestones in AI evolution.',
|
||||
link: '/en-us/appendix/ai-evolution'
|
||||
link: '/zh-cn/appendix/8-artificial-intelligence/ai-history'
|
||||
},
|
||||
{
|
||||
title: 'Prompt Eng',
|
||||
desc: 'Master AI communication skills.',
|
||||
link: '/en-us/appendix/prompt-engineering'
|
||||
link: '/zh-cn/appendix/8-artificial-intelligence/prompt-engineering'
|
||||
},
|
||||
{
|
||||
title: 'LLM Intro',
|
||||
desc: 'Understanding Large Language Models.',
|
||||
link: '/en-us/appendix/llm-intro'
|
||||
link: '/zh-cn/appendix/8-artificial-intelligence/llm-principles'
|
||||
},
|
||||
{
|
||||
title: 'AI Agents',
|
||||
desc: 'Autonomous decision-making AI.',
|
||||
link: '/en-us/appendix/agent-intro'
|
||||
link: '/zh-cn/appendix/8-artificial-intelligence/ai-agents'
|
||||
},
|
||||
{
|
||||
title: 'Web Basics',
|
||||
desc: 'HTML/CSS/JS fundamentals.',
|
||||
link: '/en-us/appendix/web-basics'
|
||||
link: '/zh-cn/appendix/3-browser-and-frontend/javascript-deep-dive'
|
||||
},
|
||||
{
|
||||
title: 'Frontend Evo',
|
||||
desc: 'Evolution of frontend tech stack.',
|
||||
link: '/en-us/appendix/frontend-evolution'
|
||||
link: '/zh-cn/appendix/3-browser-and-frontend/frontend-frameworks'
|
||||
},
|
||||
{
|
||||
title: 'Backend Arch',
|
||||
desc: 'From monolith to microservices.',
|
||||
link: '/en-us/appendix/backend-evolution'
|
||||
link: '/zh-cn/appendix/4-server-and-backend/backend-layered-architecture'
|
||||
},
|
||||
{
|
||||
title: 'Backend Lang',
|
||||
desc: 'Choosing the right tech stack.',
|
||||
link: '/en-us/appendix/backend-languages'
|
||||
link: '/zh-cn/appendix/4-server-and-backend/backend-languages'
|
||||
},
|
||||
{
|
||||
title: 'Database',
|
||||
desc: 'Core principles of data storage.',
|
||||
link: '/en-us/appendix/database-intro'
|
||||
link: '/zh-cn/appendix/5-data/database-fundamentals'
|
||||
},
|
||||
{
|
||||
title: 'API Design',
|
||||
desc: 'Designing robust interfaces.',
|
||||
link: '/en-us/appendix/api-intro'
|
||||
link: '/zh-cn/appendix/4-server-and-backend/api-intro'
|
||||
},
|
||||
{
|
||||
title: 'Git',
|
||||
desc: 'Version control mastery.',
|
||||
link: '/en-us/appendix/git-intro'
|
||||
link: '/zh-cn/appendix/2-development-tools/git-version-control'
|
||||
},
|
||||
{
|
||||
title: 'Networks',
|
||||
desc: 'Protocols and communication.',
|
||||
link: '/en-us/appendix/computer-networks'
|
||||
link: '/zh-cn/appendix/1-computer-fundamentals/computer-networks'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1374,12 +1374,12 @@ const i18n = {
|
||||
import { computed } from 'vue'
|
||||
const t = computed(() => {
|
||||
const code = lang.value ? lang.value.toLowerCase() : 'zh-cn'
|
||||
return i18n[code] || i18n['en-us']
|
||||
return i18n[code] || i18n['en']
|
||||
})
|
||||
|
||||
const locales = [
|
||||
{ code: 'zh-cn', text: '简体中文' },
|
||||
{ code: 'en-us', text: 'English' },
|
||||
{ code: 'en', text: 'English' },
|
||||
{ code: 'ja-jp', text: '日本語' },
|
||||
{ code: 'zh-tw', text: '繁體中文' },
|
||||
{ code: 'ko-kr', text: '한국어' },
|
||||
@@ -1699,7 +1699,7 @@ const appendixCards = [
|
||||
</div>
|
||||
<a
|
||||
class="buy-btn"
|
||||
:href="withBase('/zh-cn/stage-0/0.1-learning-map/')"
|
||||
:href="withBase(t.stage1.cards[0].link)"
|
||||
>{{ t.footer.btn }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+24
-8
@@ -155,7 +155,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
|
||||
const stage = ref(0)
|
||||
const expandedOp = ref(-1)
|
||||
@@ -313,15 +313,26 @@ function getDeviceStatus(i) {
|
||||
return '等待'
|
||||
}
|
||||
|
||||
const postTimer = ref(null)
|
||||
const hwTimer = ref(null)
|
||||
const bootTimer = ref(null)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (postTimer.value) clearInterval(postTimer.value)
|
||||
if (hwTimer.value) clearInterval(hwTimer.value)
|
||||
if (bootTimer.value) clearInterval(bootTimer.value)
|
||||
})
|
||||
|
||||
// POST 自检动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (postTimer.value) clearInterval(postTimer.value)
|
||||
if (newStage === 1) {
|
||||
currentCheck.value = 0
|
||||
const interval = setInterval(() => {
|
||||
postTimer.value = setInterval(() => {
|
||||
if (currentCheck.value < postItems.length) {
|
||||
currentCheck.value++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
if (postTimer.value) clearInterval(postTimer.value)
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
@@ -329,15 +340,16 @@ watch(() => stage.value, (newStage) => {
|
||||
|
||||
// 硬件初始化动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (hwTimer.value) clearInterval(hwTimer.value)
|
||||
if (newStage === 2) {
|
||||
activeHw.value = 0
|
||||
hwProgress.value = 0
|
||||
const interval = setInterval(() => {
|
||||
hwTimer.value = setInterval(() => {
|
||||
if (hwProgress.value < 100) {
|
||||
hwProgress.value += 5
|
||||
activeHw.value = Math.floor(hwProgress.value / 20) % hardwareItems.length
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
if (hwTimer.value) clearInterval(hwTimer.value)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
@@ -345,11 +357,12 @@ watch(() => stage.value, (newStage) => {
|
||||
|
||||
// 启动设备搜索动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (bootTimer.value) clearInterval(bootTimer.value)
|
||||
if (newStage === 3) {
|
||||
currentDevice.value = 0
|
||||
foundDevice.value = -1
|
||||
let device = 0
|
||||
const interval = setInterval(() => {
|
||||
bootTimer.value = setInterval(() => {
|
||||
if (device < bootDevices.length) {
|
||||
currentDevice.value = device
|
||||
// 假设第一个设备(硬盘)可启动
|
||||
@@ -357,11 +370,11 @@ watch(() => stage.value, (newStage) => {
|
||||
setTimeout(() => {
|
||||
foundDevice.value = device
|
||||
}, 400)
|
||||
clearInterval(interval)
|
||||
if (bootTimer.value) clearInterval(bootTimer.value)
|
||||
}
|
||||
device++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
if (bootTimer.value) clearInterval(bootTimer.value)
|
||||
}
|
||||
}, 800)
|
||||
}
|
||||
@@ -387,6 +400,9 @@ function reset() {
|
||||
hwProgress.value = 0
|
||||
currentDevice.value = 0
|
||||
foundDevice.value = -1
|
||||
if (postTimer.value) clearInterval(postTimer.value)
|
||||
if (hwTimer.value) clearInterval(hwTimer.value)
|
||||
if (bootTimer.value) clearInterval(bootTimer.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+5
-1
@@ -47,12 +47,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
const selected = ref(0)
|
||||
const visibleSteps = ref(0)
|
||||
let timer = null
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
|
||||
function selectMode(i) {
|
||||
selected.value = i
|
||||
visibleSteps.value = 0
|
||||
|
||||
+11
-3
@@ -451,7 +451,10 @@ function toggleAuto() {
|
||||
} else {
|
||||
autoRunning.value = true
|
||||
autoTimer = setInterval(() => {
|
||||
if (done.value) { stopAuto(); return }
|
||||
if (done.value) {
|
||||
stopAuto()
|
||||
return
|
||||
}
|
||||
advance()
|
||||
}, 900)
|
||||
}
|
||||
@@ -459,7 +462,10 @@ function toggleAuto() {
|
||||
|
||||
function stopAuto() {
|
||||
autoRunning.value = false
|
||||
if (autoTimer) { clearInterval(autoTimer); autoTimer = null }
|
||||
if (autoTimer) {
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
@@ -476,7 +482,9 @@ function reset() {
|
||||
currentPhase.value = -1
|
||||
}
|
||||
|
||||
onUnmounted(stopAuto)
|
||||
onUnmounted(() => {
|
||||
stopAuto()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -61,12 +61,17 @@ const updateClock = () => {
|
||||
}
|
||||
|
||||
const runSequence = () => {
|
||||
if (phaseTimer) clearTimeout(phaseTimer)
|
||||
phase.value = 0
|
||||
const delays = [1500, 1500, 1800]
|
||||
let i = 0
|
||||
const next = () => {
|
||||
if (i < delays.length) {
|
||||
phaseTimer = setTimeout(() => { phase.value = i + 1; i++; next() }, delays[i])
|
||||
phaseTimer = setTimeout(() => {
|
||||
phase.value = i + 1
|
||||
i++
|
||||
next()
|
||||
}, delays[i])
|
||||
}
|
||||
}
|
||||
next()
|
||||
@@ -77,6 +82,7 @@ onMounted(() => {
|
||||
timer = setInterval(updateClock, 30000)
|
||||
runSequence()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
if (phaseTimer) clearTimeout(phaseTimer)
|
||||
|
||||
@@ -165,7 +165,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
+16
-6
@@ -418,23 +418,31 @@ onMounted(() => {
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
const blTimer = ref(null)
|
||||
const kernelTimer = ref(null)
|
||||
const svcTimer = ref(null)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
if (blTimer.value) clearInterval(blTimer.value)
|
||||
if (kernelTimer.value) clearInterval(kernelTimer.value)
|
||||
if (svcTimer.value) clearInterval(svcTimer.value)
|
||||
})
|
||||
|
||||
// 引导程序动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (blTimer.value) clearInterval(blTimer.value)
|
||||
if (newStage === 1) {
|
||||
blStep.value = -1
|
||||
blCodeLine.value = -1
|
||||
let step = 0
|
||||
const interval = setInterval(() => {
|
||||
blTimer.value = setInterval(() => {
|
||||
if (step < blSteps.length) {
|
||||
blStep.value = step
|
||||
blCodeLine.value = step + 1
|
||||
step++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
if (blTimer.value) clearInterval(blTimer.value)
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
@@ -442,14 +450,15 @@ watch(() => stage.value, (newStage) => {
|
||||
|
||||
// 内核加载动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (kernelTimer.value) clearInterval(kernelTimer.value)
|
||||
if (newStage === 2) {
|
||||
kernelProgress.value = 0
|
||||
kernelName.value = Math.random() > 0.5 ? 'ntoskrnl.exe' : 'vmlinuz'
|
||||
const interval = setInterval(() => {
|
||||
kernelTimer.value = setInterval(() => {
|
||||
if (kernelProgress.value < 100) {
|
||||
kernelProgress.value += 4
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
if (kernelTimer.value) clearInterval(kernelTimer.value)
|
||||
}
|
||||
}, 80)
|
||||
}
|
||||
@@ -457,13 +466,14 @@ watch(() => stage.value, (newStage) => {
|
||||
|
||||
// 服务启动动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (svcTimer.value) clearInterval(svcTimer.value)
|
||||
if (newStage === 3) {
|
||||
svcProgress.value = 0
|
||||
const interval = setInterval(() => {
|
||||
svcTimer.value = setInterval(() => {
|
||||
if (svcProgress.value < 100) {
|
||||
svcProgress.value += 3
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
if (svcTimer.value) clearInterval(svcTimer.value)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
@@ -99,8 +99,8 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
clearTimeout(switchTimer)
|
||||
if (timer) clearInterval(timer)
|
||||
if (switchTimer) clearTimeout(switchTimer)
|
||||
})
|
||||
|
||||
const currentTask = computed(() => processes.value[currentIdx.value])
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
+10
-4
@@ -123,13 +123,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
const activeAlgo = ref('linear')
|
||||
const targetNumber = ref(7)
|
||||
const foundIndex = ref(-1)
|
||||
const searchStep = ref(-1)
|
||||
const searching = ref(false)
|
||||
const searchTimer = ref(null)
|
||||
|
||||
const numbers = ref([3, 7, 2, 9, 5, 1, 8, 4, 6, 10])
|
||||
|
||||
@@ -140,24 +141,29 @@ const binaryRight = ref(9)
|
||||
const binaryMid = ref(4)
|
||||
const binaryFoundIndex = ref(-1)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (searchTimer.value) clearInterval(searchTimer.value)
|
||||
})
|
||||
|
||||
const startLinearSearch = () => {
|
||||
if (searchTimer.value) clearInterval(searchTimer.value)
|
||||
searching.value = true
|
||||
searchStep.value = -1
|
||||
foundIndex.value = -1
|
||||
|
||||
let step = 0
|
||||
const interval = setInterval(() => {
|
||||
searchTimer.value = setInterval(() => {
|
||||
if (step < numbers.value.length) {
|
||||
searchStep.value = step
|
||||
if (numbers.value[step] === targetNumber.value) {
|
||||
foundIndex.value = step
|
||||
searching.value = false
|
||||
clearInterval(interval)
|
||||
if (searchTimer.value) clearInterval(searchTimer.value)
|
||||
}
|
||||
step++
|
||||
} else {
|
||||
searching.value = false
|
||||
clearInterval(interval)
|
||||
if (searchTimer.value) clearInterval(searchTimer.value)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
+10
-4
@@ -123,24 +123,30 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
const activeType = ref('serial')
|
||||
const dataBits = ref(['1', '0', '1', '1', '0', '0', '1', '0'])
|
||||
const dataBits = ref([1, 0, 1, 1, 0, 0, 1, 0])
|
||||
const receivedBits = ref(['-', '-', '-', '-', '-', '-', '-', '-'])
|
||||
const sendingBit = ref(null)
|
||||
const timer = ref(null)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer.value) clearInterval(timer.value)
|
||||
})
|
||||
|
||||
const startTransmission = () => {
|
||||
if (timer.value) clearInterval(timer.value)
|
||||
if (activeType.value === 'serial') {
|
||||
receivedBits.value = ['-', '-', '-', '-', '-', '-', '-', '-']
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
timer.value = setInterval(() => {
|
||||
if (i < dataBits.value.length) {
|
||||
sendingBit.value = i
|
||||
receivedBits.value[i] = dataBits.value[i]
|
||||
i++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
if (timer.value) clearInterval(timer.value)
|
||||
sendingBit.value = null
|
||||
}
|
||||
}, 300)
|
||||
|
||||
@@ -61,6 +61,7 @@ const steps = [
|
||||
]
|
||||
|
||||
const autoPlay = () => {
|
||||
if (timer) clearInterval(timer)
|
||||
current.value = -1
|
||||
playing.value = true
|
||||
let i = 0
|
||||
@@ -68,7 +69,7 @@ const autoPlay = () => {
|
||||
current.value = i
|
||||
i++
|
||||
if (i >= steps.length) {
|
||||
clearInterval(timer)
|
||||
if (timer) clearInterval(timer)
|
||||
playing.value = false
|
||||
}
|
||||
}, 800)
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
<!--
|
||||
DataGovernanceFrameworkDemo.vue
|
||||
数据治理框架演示:展示数据治理的核心流程
|
||||
-->
|
||||
<template>
|
||||
<div class="governance-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据治理框架</div>
|
||||
<div class="subtitle">点击各阶段查看详情</div>
|
||||
</div>
|
||||
|
||||
<div class="pipeline">
|
||||
<div
|
||||
v-for="(stage, i) in stages"
|
||||
:key="stage.key"
|
||||
:class="['stage', { active: activeStage === stage.key }]"
|
||||
@click="activeStage = stage.key"
|
||||
>
|
||||
<div class="stage-num">{{ i + 1 }}</div>
|
||||
<div class="stage-name">{{ stage.name }}</div>
|
||||
<div v-if="i < stages.length - 1" class="arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="stage-detail">
|
||||
<div class="detail-title">{{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="activities">
|
||||
<div v-for="(act, i) in current.activities" :key="i" class="activity">
|
||||
<span class="act-icon">{{ act.icon }}</span>
|
||||
<div>
|
||||
<div class="act-name">{{ act.name }}</div>
|
||||
<div class="act-desc">{{ act.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeStage = ref('define')
|
||||
|
||||
const stages = [
|
||||
{
|
||||
key: 'define',
|
||||
name: '定义标准',
|
||||
desc: '制定数据标准、命名规范、数据字典',
|
||||
activities: [
|
||||
{ icon: '📖', name: '数据字典', desc: '定义每个字段的含义、类型、取值范围' },
|
||||
{ icon: '📏', name: '命名规范', desc: '统一字段命名:snake_case、驼峰、前缀约定' },
|
||||
{ icon: '🏷️', name: '分类分级', desc: '按敏感度分级:公开、内部、机密、绝密' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'collect',
|
||||
name: '采集接入',
|
||||
desc: '规范数据采集流程,确保源头质量',
|
||||
activities: [
|
||||
{ icon: '🔌', name: '接入规范', desc: '定义数据接入的格式、协议、频率要求' },
|
||||
{ icon: '✅', name: '入库校验', desc: '数据写入前进行格式、完整性、合规性校验' },
|
||||
{ icon: '📝', name: '血缘记录', desc: '记录数据来源、加工链路、依赖关系' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'store',
|
||||
name: '存储管理',
|
||||
desc: '合理存储数据,控制成本和访问权限',
|
||||
activities: [
|
||||
{ icon: '🗄️', name: '分层存储', desc: 'ODS → DWD → DWS → ADS 数仓分层' },
|
||||
{ icon: '🔒', name: '权限控制', desc: '按角色和数据分级控制读写权限' },
|
||||
{ icon: '♻️', name: '生命周期', desc: '热数据 → 温数据 → 冷数据 → 归档/删除' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'use',
|
||||
name: '使用消费',
|
||||
desc: '让数据安全、高效地被业务使用',
|
||||
activities: [
|
||||
{ icon: '🔍', name: '数据目录', desc: '提供可搜索的数据资产目录,降低找数成本' },
|
||||
{ icon: '🎭', name: '脱敏处理', desc: '对敏感字段进行掩码、加密、泛化处理' },
|
||||
{ icon: '📊', name: '质量监控', desc: '持续监控数据质量指标,异常时告警' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'retire',
|
||||
name: '归档销毁',
|
||||
desc: '按合规要求归档或安全销毁数据',
|
||||
activities: [
|
||||
{ icon: '📦', name: '归档策略', desc: '超过保留期的数据迁移到低成本存储' },
|
||||
{ icon: '🗑️', name: '安全删除', desc: '按 GDPR/个保法要求彻底删除用户数据' },
|
||||
{ icon: '📋', name: '审计日志', desc: '记录数据删除操作,满足合规审计要求' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => stages.find(s => s.key === activeStage.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.governance-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.pipeline { display: flex; align-items: center; gap: 0.25rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.stage {
|
||||
display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px; cursor: pointer; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider); transition: all 0.2s; font-size: 0.85rem;
|
||||
}
|
||||
.stage:hover { border-color: var(--vp-c-brand); }
|
||||
.stage.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.stage-num { width: 20px; height: 20px; border-radius: 50%; background: var(--vp-c-brand); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; }
|
||||
.stage-name { font-weight: 600; }
|
||||
.arrow { color: var(--vp-c-text-3); margin-left: 0.25rem; }
|
||||
.stage-detail { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); }
|
||||
.detail-title { font-weight: 700; font-size: 1rem; margin-bottom: 0.25rem; }
|
||||
.detail-desc { color: var(--vp-c-text-2); font-size: 0.85rem; margin-bottom: 0.75rem; }
|
||||
.activities { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.activity { display: flex; gap: 0.5rem; padding: 0.5rem; border-radius: 6px; background: var(--vp-c-bg-soft); }
|
||||
.act-icon { font-size: 1.2rem; }
|
||||
.act-name { font-weight: 600; font-size: 0.85rem; }
|
||||
.act-desc { font-size: 0.78rem; color: var(--vp-c-text-2); }
|
||||
@media (max-width: 640px) { .pipeline { flex-direction: column; align-items: stretch; } .arrow { display: none; } }
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
DataLineageDemo.vue
|
||||
数据血缘追踪演示:展示数据从源头到消费的流转路径
|
||||
-->
|
||||
<template>
|
||||
<div class="lineage-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据血缘追踪</div>
|
||||
<div class="subtitle">点击任意节点,查看上下游依赖关系</div>
|
||||
</div>
|
||||
|
||||
<div class="lineage-graph">
|
||||
<div v-for="(layer, li) in layers" :key="li" class="layer">
|
||||
<div class="layer-label">{{ layer.label }}</div>
|
||||
<div class="layer-nodes">
|
||||
<div
|
||||
v-for="node in layer.nodes"
|
||||
:key="node.id"
|
||||
:class="['node', { active: activeNode === node.id, upstream: upstreamIds.includes(node.id), downstream: downstreamIds.includes(node.id) }]"
|
||||
@click="selectNode(node.id)"
|
||||
>
|
||||
<div class="node-icon">{{ node.icon }}</div>
|
||||
<div class="node-name">{{ node.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeNode && activeInfo" class="info-panel">
|
||||
<div class="info-title">{{ activeInfo.name }}</div>
|
||||
<div class="info-row"><span class="info-label">上游依赖:</span>{{ activeInfo.upstreamNames || '无(数据源头)' }}</div>
|
||||
<div class="info-row"><span class="info-label">下游消费:</span>{{ activeInfo.downstreamNames || '无(最终消费)' }}</div>
|
||||
<div class="info-row"><span class="info-label">负责人:</span>{{ activeInfo.owner }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeNode = ref(null)
|
||||
|
||||
const nodes = {
|
||||
mysql_user: { name: 'MySQL 用户表', icon: '🗄️', upstream: [], downstream: ['ods_user'], owner: '业务开发组' },
|
||||
mysql_order: { name: 'MySQL 订单表', icon: '🗄️', upstream: [], downstream: ['ods_order'], owner: '业务开发组' },
|
||||
log_click: { name: '点击日志', icon: '📝', upstream: [], downstream: ['ods_click'], owner: '前端团队' },
|
||||
ods_user: { name: 'ODS 用户', icon: '📥', upstream: ['mysql_user'], downstream: ['dwd_user'], owner: '数据工程师' },
|
||||
ods_order: { name: 'ODS 订单', icon: '📥', upstream: ['mysql_order'], downstream: ['dwd_order'], owner: '数据工程师' },
|
||||
ods_click: { name: 'ODS 点击', icon: '📥', upstream: ['log_click'], downstream: ['dwd_click'], owner: '数据工程师' },
|
||||
dwd_user: { name: 'DWD 用户明细', icon: '🔧', upstream: ['ods_user'], downstream: ['dws_user_profile'], owner: '数据开发' },
|
||||
dwd_order: { name: 'DWD 订单明细', icon: '🔧', upstream: ['ods_order'], downstream: ['dws_gmv'], owner: '数据开发' },
|
||||
dwd_click: { name: 'DWD 点击明细', icon: '🔧', upstream: ['ods_click'], downstream: ['dws_user_profile'], owner: '数据开发' },
|
||||
dws_user_profile: { name: 'DWS 用户画像', icon: '📊', upstream: ['dwd_user', 'dwd_click'], downstream: ['ads_report'], owner: '数据分析师' },
|
||||
dws_gmv: { name: 'DWS GMV 汇总', icon: '📊', upstream: ['dwd_order'], downstream: ['ads_report'], owner: '数据分析师' },
|
||||
ads_report: { name: 'ADS 经营报表', icon: '📈', upstream: ['dws_user_profile', 'dws_gmv'], downstream: [], owner: '数据产品' }
|
||||
}
|
||||
|
||||
const layers = [
|
||||
{ label: '数据源', nodes: [{ id: 'mysql_user', ...nodes.mysql_user }, { id: 'mysql_order', ...nodes.mysql_order }, { id: 'log_click', ...nodes.log_click }] },
|
||||
{ label: 'ODS 层', nodes: [{ id: 'ods_user', ...nodes.ods_user }, { id: 'ods_order', ...nodes.ods_order }, { id: 'ods_click', ...nodes.ods_click }] },
|
||||
{ label: 'DWD 层', nodes: [{ id: 'dwd_user', ...nodes.dwd_user }, { id: 'dwd_order', ...nodes.dwd_order }, { id: 'dwd_click', ...nodes.dwd_click }] },
|
||||
{ label: 'DWS 层', nodes: [{ id: 'dws_user_profile', ...nodes.dws_user_profile }, { id: 'dws_gmv', ...nodes.dws_gmv }] },
|
||||
{ label: 'ADS 层', nodes: [{ id: 'ads_report', ...nodes.ads_report }] }
|
||||
]
|
||||
|
||||
function getAllUpstream(id, visited = new Set()) {
|
||||
if (visited.has(id)) return []
|
||||
visited.add(id)
|
||||
const node = nodes[id]
|
||||
if (!node) return []
|
||||
let result = [...node.upstream]
|
||||
node.upstream.forEach(uid => { result = result.concat(getAllUpstream(uid, visited)) })
|
||||
return result
|
||||
}
|
||||
|
||||
function getAllDownstream(id, visited = new Set()) {
|
||||
if (visited.has(id)) return []
|
||||
visited.add(id)
|
||||
const node = nodes[id]
|
||||
if (!node) return []
|
||||
let result = [...node.downstream]
|
||||
node.downstream.forEach(did => { result = result.concat(getAllDownstream(did, visited)) })
|
||||
return result
|
||||
}
|
||||
|
||||
const upstreamIds = computed(() => activeNode.value ? getAllUpstream(activeNode.value) : [])
|
||||
const downstreamIds = computed(() => activeNode.value ? getAllDownstream(activeNode.value) : [])
|
||||
|
||||
const activeInfo = computed(() => {
|
||||
if (!activeNode.value || !nodes[activeNode.value]) return null
|
||||
const n = nodes[activeNode.value]
|
||||
return {
|
||||
...n,
|
||||
upstreamNames: n.upstream.map(id => nodes[id]?.name).join('、'),
|
||||
downstreamNames: n.downstream.map(id => nodes[id]?.name).join('、')
|
||||
}
|
||||
})
|
||||
|
||||
function selectNode(id) {
|
||||
activeNode.value = activeNode.value === id ? null : id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lineage-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.lineage-graph { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.layer { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.layer-label {
|
||||
min-width: 60px; font-size: 0.75rem; font-weight: 700;
|
||||
color: var(--vp-c-text-3); text-align: right;
|
||||
}
|
||||
.layer-nodes { display: flex; gap: 0.5rem; flex-wrap: wrap; flex: 1; }
|
||||
.node {
|
||||
display: flex; align-items: center; gap: 0.3rem; padding: 0.4rem 0.6rem;
|
||||
border-radius: 6px; cursor: pointer; font-size: 0.78rem;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); transition: all 0.2s;
|
||||
}
|
||||
.node:hover { border-color: var(--vp-c-brand); }
|
||||
.node.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.1); font-weight: 700; }
|
||||
.node.upstream { border-color: #f59e0b; background: rgba(245,158,11,0.08); }
|
||||
.node.downstream { border-color: #22c55e; background: rgba(34,197,94,0.08); }
|
||||
.node-icon { font-size: 1rem; }
|
||||
.node-name { white-space: nowrap; }
|
||||
.info-panel {
|
||||
background: var(--vp-c-bg); border-radius: 8px; padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.info-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.5rem; }
|
||||
.info-row { font-size: 0.82rem; margin-bottom: 0.25rem; }
|
||||
.info-label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
@media (max-width: 640px) { .layer { flex-direction: column; align-items: flex-start; } .layer-label { text-align: left; } }
|
||||
</style>
|
||||
@@ -0,0 +1,266 @@
|
||||
<!--
|
||||
DataQualityDemo.vue
|
||||
数据质量维度演示:展示数据质量的六个核心维度
|
||||
-->
|
||||
<template>
|
||||
<div class="data-quality-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据质量检测器</div>
|
||||
<div class="subtitle">点击不同维度,查看数据质量问题示例</div>
|
||||
</div>
|
||||
|
||||
<div class="dimensions">
|
||||
<div
|
||||
v-for="dim in dimensions"
|
||||
:key="dim.key"
|
||||
:class="['dim-card', { active: activeDim === dim.key }]"
|
||||
@click="activeDim = dim.key"
|
||||
>
|
||||
<div class="dim-icon">{{ dim.icon }}</div>
|
||||
<div class="dim-name">{{ dim.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentDim" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ currentDim.icon }}</span>
|
||||
<span class="detail-title">{{ currentDim.name }}</span>
|
||||
<span class="detail-desc">{{ currentDim.desc }}</span>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<div class="example bad">
|
||||
<div class="example-label bad-label">问题数据</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in currentDim.badData.cols" :key="col">{{ col }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in currentDim.badData.rows" :key="i">
|
||||
<td
|
||||
v-for="(cell, j) in row"
|
||||
:key="j"
|
||||
:class="{ 'cell-error': cell.error }"
|
||||
>{{ cell.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="example good">
|
||||
<div class="example-label good-label">治理后</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in currentDim.goodData.cols" :key="col">{{ col }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in currentDim.goodData.rows" :key="i">
|
||||
<td v-for="(cell, j) in row" :key="j">{{ cell.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quality-score">
|
||||
<div class="score-label">质量评分</div>
|
||||
<div class="score-bar-bg">
|
||||
<div
|
||||
class="score-bar-fill"
|
||||
:style="{ width: currentDim.score + '%', background: scoreColor(currentDim.score) }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="score-value">{{ currentDim.score }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeDim = ref('completeness')
|
||||
|
||||
const dimensions = [
|
||||
{
|
||||
key: 'completeness', name: '完整性', icon: '📋',
|
||||
desc: '数据是否存在缺失值',
|
||||
score: 72,
|
||||
badData: {
|
||||
cols: ['用户ID', '姓名', '邮箱', '手机号'],
|
||||
rows: [
|
||||
[{ value: '001' }, { value: '张三' }, { value: 'zhang@mail.com' }, { value: '138xxxx1234' }],
|
||||
[{ value: '002' }, { value: '李四' }, { value: '', error: true }, { value: '', error: true }],
|
||||
[{ value: '003' }, { value: '', error: true }, { value: 'wang@mail.com' }, { value: '139xxxx5678' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['用户ID', '姓名', '邮箱', '手机号'],
|
||||
rows: [
|
||||
[{ value: '001' }, { value: '张三' }, { value: 'zhang@mail.com' }, { value: '138xxxx1234' }],
|
||||
[{ value: '002' }, { value: '李四' }, { value: 'li@mail.com' }, { value: '137xxxx9012' }],
|
||||
[{ value: '003' }, { value: '王五' }, { value: 'wang@mail.com' }, { value: '139xxxx5678' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'accuracy', name: '准确性', icon: '🎯',
|
||||
desc: '数据值是否正确反映真实情况',
|
||||
score: 65,
|
||||
badData: {
|
||||
cols: ['订单ID', '金额', '日期', '状态'],
|
||||
rows: [
|
||||
[{ value: 'ORD-101' }, { value: '-50.00', error: true }, { value: '2025-01-15' }, { value: '已完成' }],
|
||||
[{ value: 'ORD-102' }, { value: '299.00' }, { value: '2025-13-01', error: true }, { value: '已发货' }],
|
||||
[{ value: 'ORD-103' }, { value: '1500.00' }, { value: '2025-02-28' }, { value: '已退款', error: true }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['订单ID', '金额', '日期', '状态'],
|
||||
rows: [
|
||||
[{ value: 'ORD-101' }, { value: '50.00' }, { value: '2025-01-15' }, { value: '已完成' }],
|
||||
[{ value: 'ORD-102' }, { value: '299.00' }, { value: '2025-01-13' }, { value: '已发货' }],
|
||||
[{ value: 'ORD-103' }, { value: '1500.00' }, { value: '2025-02-28' }, { value: '已完成' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'consistency', name: '一致性', icon: '🔗',
|
||||
desc: '同一数据在不同系统中是否一致',
|
||||
score: 58,
|
||||
badData: {
|
||||
cols: ['来源', '用户名', '手机号', '地址'],
|
||||
rows: [
|
||||
[{ value: 'CRM' }, { value: '张三' }, { value: '13812341234' }, { value: '北京市朝阳区' }],
|
||||
[{ value: '订单系统' }, { value: '张三丰', error: true }, { value: '13812341234' }, { value: '北京朝阳', error: true }],
|
||||
[{ value: '客服系统' }, { value: '张三' }, { value: '13899999999', error: true }, { value: '北京市朝阳区' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['来源', '用户名', '手机号', '地址'],
|
||||
rows: [
|
||||
[{ value: 'CRM' }, { value: '张三' }, { value: '13812341234' }, { value: '北京市朝阳区' }],
|
||||
[{ value: '订单系统' }, { value: '张三' }, { value: '13812341234' }, { value: '北京市朝阳区' }],
|
||||
[{ value: '客服系统' }, { value: '张三' }, { value: '13812341234' }, { value: '北京市朝阳区' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'timeliness', name: '时效性', icon: '⏰',
|
||||
desc: '数据是否及时更新',
|
||||
score: 80,
|
||||
badData: {
|
||||
cols: ['商品ID', '价格', '库存', '更新时间'],
|
||||
rows: [
|
||||
[{ value: 'SKU-001' }, { value: '¥299' }, { value: '50' }, { value: '2024-06-01', error: true }],
|
||||
[{ value: 'SKU-002' }, { value: '¥599' }, { value: '0', error: true }, { value: '2024-03-15', error: true }],
|
||||
[{ value: 'SKU-003' }, { value: '¥199' }, { value: '200' }, { value: '2025-02-20' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['商品ID', '价格', '库存', '更新时间'],
|
||||
rows: [
|
||||
[{ value: 'SKU-001' }, { value: '¥259' }, { value: '35' }, { value: '2025-02-25' }],
|
||||
[{ value: 'SKU-002' }, { value: '¥549' }, { value: '12' }, { value: '2025-02-25' }],
|
||||
[{ value: 'SKU-003' }, { value: '¥199' }, { value: '180' }, { value: '2025-02-25' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'uniqueness', name: '唯一性', icon: '🔑',
|
||||
desc: '数据是否存在重复记录',
|
||||
score: 70,
|
||||
badData: {
|
||||
cols: ['用户ID', '姓名', '邮箱', '注册时间'],
|
||||
rows: [
|
||||
[{ value: '001' }, { value: '张三' }, { value: 'zhang@mail.com' }, { value: '2025-01-01' }],
|
||||
[{ value: '005' }, { value: '张三', error: true }, { value: 'zhang@mail.com', error: true }, { value: '2025-01-15', error: true }],
|
||||
[{ value: '002' }, { value: '李四' }, { value: 'li@mail.com' }, { value: '2025-01-10' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['用户ID', '姓名', '邮箱', '注册时间'],
|
||||
rows: [
|
||||
[{ value: '001' }, { value: '张三' }, { value: 'zhang@mail.com' }, { value: '2025-01-01' }],
|
||||
[{ value: '002' }, { value: '李四' }, { value: 'li@mail.com' }, { value: '2025-01-10' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'validity', name: '有效性', icon: '✅',
|
||||
desc: '数据是否符合预定义的格式和规则',
|
||||
score: 75,
|
||||
badData: {
|
||||
cols: ['字段', '值', '规则'],
|
||||
rows: [
|
||||
[{ value: '邮箱' }, { value: 'not-an-email', error: true }, { value: '需包含@' }],
|
||||
[{ value: '年龄' }, { value: '-5', error: true }, { value: '0~150' }],
|
||||
[{ value: '手机号' }, { value: '1234', error: true }, { value: '11位数字' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['字段', '值', '规则'],
|
||||
rows: [
|
||||
[{ value: '邮箱' }, { value: 'user@mail.com' }, { value: '需包含@' }],
|
||||
[{ value: '年龄' }, { value: '28' }, { value: '0~150' }],
|
||||
[{ value: '手机号' }, { value: '13812345678' }, { value: '11位数字' }]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const currentDim = computed(() => dimensions.find(d => d.key === activeDim.value))
|
||||
|
||||
function scoreColor(score) {
|
||||
if (score >= 80) return '#22c55e'
|
||||
if (score >= 60) return '#f59e0b'
|
||||
return '#ef4444'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-quality-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.dimensions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||
.dim-card {
|
||||
display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
cursor: pointer; font-size: 0.85rem; transition: all 0.2s;
|
||||
}
|
||||
.dim-card:hover { border-color: var(--vp-c-brand); }
|
||||
.dim-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.dim-icon { font-size: 1.1rem; }
|
||||
.dim-name { font-weight: 600; }
|
||||
.detail-panel {
|
||||
padding: 1rem; border-radius: 8px; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider); margin-bottom: 1rem;
|
||||
}
|
||||
.detail-header { margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.detail-icon { font-size: 1.2rem; }
|
||||
.detail-title { font-weight: 700; font-size: 1rem; }
|
||||
.detail-desc { color: var(--vp-c-text-2); font-size: 0.85rem; }
|
||||
.example-section { display: flex; gap: 1rem; margin-bottom: 1rem; }
|
||||
.example { flex: 1; }
|
||||
.example-label { font-weight: 600; font-size: 0.8rem; margin-bottom: 0.4rem; padding: 0.2rem 0.5rem; border-radius: 4px; display: inline-block; }
|
||||
.bad-label { background: rgba(239,68,68,0.1); color: #ef4444; }
|
||||
.good-label { background: rgba(34,197,94,0.1); color: #22c55e; }
|
||||
.data-table { width: 100%; border-collapse: collapse; font-size: 0.75rem; }
|
||||
.data-table th { background: var(--vp-c-bg-soft); padding: 0.3rem 0.4rem; text-align: left; font-weight: 600; border-bottom: 1px solid var(--vp-c-divider); }
|
||||
.data-table td { padding: 0.3rem 0.4rem; border-bottom: 1px solid var(--vp-c-divider); }
|
||||
.cell-error { background: rgba(239,68,68,0.1); color: #ef4444; font-weight: 600; }
|
||||
.quality-score { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.score-label { font-weight: 600; font-size: 0.85rem; white-space: nowrap; }
|
||||
.score-bar-bg { flex: 1; height: 10px; background: var(--vp-c-bg-soft); border-radius: 5px; overflow: hidden; }
|
||||
.score-bar-fill { height: 100%; border-radius: 5px; transition: width 0.4s; }
|
||||
.score-value { font-weight: 700; font-size: 0.9rem; font-family: var(--vp-font-family-mono); min-width: 40px; }
|
||||
@media (max-width: 640px) { .example-section { flex-direction: column; } }
|
||||
</style>
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
<!--
|
||||
ChartTypeSelectorDemo.vue
|
||||
图表类型选择器:根据数据特征推荐合适的图表类型
|
||||
-->
|
||||
<template>
|
||||
<div class="chart-selector-demo">
|
||||
<div class="header">
|
||||
<div class="title">图表类型选择器</div>
|
||||
<div class="subtitle">选择你的数据目的,查看推荐的图表类型</div>
|
||||
</div>
|
||||
|
||||
<div class="purposes">
|
||||
<div
|
||||
v-for="p in purposes"
|
||||
:key="p.key"
|
||||
:class="['purpose-card', { active: activePurpose === p.key }]"
|
||||
@click="activePurpose = p.key"
|
||||
>
|
||||
<div class="purpose-icon">{{ p.icon }}</div>
|
||||
<div class="purpose-name">{{ p.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentPurpose" class="charts-panel">
|
||||
<div class="panel-title">{{ currentPurpose.name }}:推荐图表</div>
|
||||
<div class="chart-list">
|
||||
<div
|
||||
v-for="chart in currentPurpose.charts"
|
||||
:key="chart.name"
|
||||
class="chart-item"
|
||||
>
|
||||
<div class="chart-visual">{{ chart.visual }}</div>
|
||||
<div class="chart-info">
|
||||
<div class="chart-name">{{ chart.name }}</div>
|
||||
<div class="chart-desc">{{ chart.desc }}</div>
|
||||
<div class="chart-example">示例:{{ chart.example }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activePurpose = ref('comparison')
|
||||
|
||||
const purposes = [
|
||||
{
|
||||
key: 'comparison',
|
||||
name: '比较',
|
||||
icon: '📊',
|
||||
charts: [
|
||||
{ name: '柱状图', visual: '▐▐▐', desc: '比较不同类别的数值大小', example: '各部门销售额对比' },
|
||||
{ name: '分组柱状图', visual: '▐▐ ▐▐', desc: '多维度分组比较', example: '各季度各产品线收入' },
|
||||
{ name: '雷达图', visual: '◇', desc: '多维度综合对比', example: '候选人能力评估' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'trend',
|
||||
name: '趋势',
|
||||
icon: '📈',
|
||||
charts: [
|
||||
{ name: '折线图', visual: '╱╲╱', desc: '展示数据随时间的变化趋势', example: '月度用户增长曲线' },
|
||||
{ name: '面积图', visual: '▓▓▓', desc: '强调趋势下的累积量', example: '各渠道流量占比变化' },
|
||||
{ name: '阶梯图', visual: '┐└┐', desc: '展示离散时间点的变化', example: '价格调整历史' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'proportion',
|
||||
name: '占比',
|
||||
icon: '🍩',
|
||||
charts: [
|
||||
{ name: '饼图', visual: '◔', desc: '展示各部分占整体的比例', example: '市场份额分布' },
|
||||
{ name: '环形图', visual: '◎', desc: '饼图的变体,中间可放数字', example: '预算使用率' },
|
||||
{ name: '堆叠柱状图', visual: '▐▐▐', desc: '展示各部分的组成和总量', example: '各地区各品类销售构成' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'distribution',
|
||||
name: '分布',
|
||||
icon: '🔔',
|
||||
charts: [
|
||||
{ name: '直方图', visual: '▁▃▇▃▁', desc: '展示数据的频率分布', example: '用户年龄分布' },
|
||||
{ name: '散点图', visual: '· ·· ·', desc: '展示两个变量的关系', example: '广告投入 vs 销售额' },
|
||||
{ name: '箱线图', visual: '├─┤', desc: '展示数据的中位数、四分位数和异常值', example: '各城市房价分布' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'relation',
|
||||
name: '关系',
|
||||
icon: '🕸️',
|
||||
charts: [
|
||||
{ name: '桑基图', visual: '≋≋≋', desc: '展示流量或能量的流向', example: '用户转化漏斗' },
|
||||
{ name: '网络图', visual: '⊙─⊙', desc: '展示节点之间的关联关系', example: '社交关系网络' },
|
||||
{ name: '热力图', visual: '▓▒░', desc: '用颜色深浅表示数值大小', example: '各时段各页面访问量' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const currentPurpose = computed(() => purposes.find(p => p.key === activePurpose.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-selector-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.purposes { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.purpose-card {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
|
||||
padding: 0.6rem; border-radius: 8px; cursor: pointer;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); transition: all 0.2s;
|
||||
}
|
||||
.purpose-card:hover { border-color: var(--vp-c-brand); }
|
||||
.purpose-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.purpose-icon { font-size: 1.3rem; }
|
||||
.purpose-name { font-size: 0.8rem; font-weight: 600; }
|
||||
.panel-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.75rem; }
|
||||
.charts-panel { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); }
|
||||
.chart-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.chart-item { display: flex; gap: 0.75rem; padding: 0.5rem; border-radius: 6px; background: var(--vp-c-bg-soft); }
|
||||
.chart-visual { font-size: 1.2rem; min-width: 50px; display: flex; align-items: center; justify-content: center; font-family: var(--vp-font-family-mono); color: var(--vp-c-brand); }
|
||||
.chart-name { font-weight: 600; font-size: 0.85rem; }
|
||||
.chart-desc { font-size: 0.78rem; color: var(--vp-c-text-2); }
|
||||
.chart-example { font-size: 0.75rem; color: var(--vp-c-text-3); font-style: italic; }
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!--
|
||||
DashboardLayoutDemo.vue
|
||||
仪表盘布局演示:展示仪表盘的常见布局模式
|
||||
-->
|
||||
<template>
|
||||
<div class="dashboard-demo">
|
||||
<div class="header">
|
||||
<div class="title">仪表盘布局模式</div>
|
||||
<div class="subtitle">点击查看不同类型的仪表盘布局</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-tabs">
|
||||
<div
|
||||
v-for="layout in layouts"
|
||||
:key="layout.key"
|
||||
:class="['tab', { active: activeLayout === layout.key }]"
|
||||
@click="activeLayout = layout.key"
|
||||
>
|
||||
{{ layout.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="layout-preview">
|
||||
<div class="preview-title">{{ current.name }}</div>
|
||||
<div class="preview-desc">{{ current.desc }}</div>
|
||||
<div :class="['mock-dashboard', current.key]">
|
||||
<div
|
||||
v-for="(widget, i) in current.widgets"
|
||||
:key="i"
|
||||
:class="['widget', widget.type]"
|
||||
>
|
||||
<div class="widget-label">{{ widget.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="use-case">适用场景:{{ current.useCase }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeLayout = ref('overview')
|
||||
|
||||
const layouts = [
|
||||
{
|
||||
key: 'overview',
|
||||
name: '全局概览型',
|
||||
desc: '顶部核心指标卡片 + 中间趋势图 + 底部明细表',
|
||||
useCase: '管理层日报、运营大盘',
|
||||
widgets: [
|
||||
{ type: 'kpi', label: 'DAU 12.5万' },
|
||||
{ type: 'kpi', label: '收入 ¥85万' },
|
||||
{ type: 'kpi', label: '转化率 3.2%' },
|
||||
{ type: 'kpi', label: '客单价 ¥268' },
|
||||
{ type: 'chart-wide', label: '趋势折线图' },
|
||||
{ type: 'table', label: '明细数据表' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'comparison',
|
||||
name: '对比分析型',
|
||||
desc: '左右对比布局,适合 A/B 测试或同环比分析',
|
||||
useCase: 'A/B 测试报告、竞品分析',
|
||||
widgets: [
|
||||
{ type: 'half', label: '实验组指标' },
|
||||
{ type: 'half', label: '对照组指标' },
|
||||
{ type: 'chart-wide', label: '差异对比图' },
|
||||
{ type: 'table', label: '统计显著性检验' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'drill',
|
||||
name: '下钻分析型',
|
||||
desc: '从汇总到明细逐层下钻,支持交互式探索',
|
||||
useCase: '销售分析、用户行为分析',
|
||||
widgets: [
|
||||
{ type: 'chart-wide', label: '全国销售地图(点击省份下钻)' },
|
||||
{ type: 'half', label: '省份排名柱状图' },
|
||||
{ type: 'half', label: '城市明细饼图' },
|
||||
{ type: 'table', label: '门店级明细表' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'realtime',
|
||||
name: '实时监控型',
|
||||
desc: '大屏展示,数据自动刷新,适合投屏',
|
||||
useCase: '双十一大屏、服务器监控',
|
||||
widgets: [
|
||||
{ type: 'big-number', label: '实时 GMV ¥1.2亿' },
|
||||
{ type: 'half', label: '订单量实时曲线' },
|
||||
{ type: 'half', label: '地域热力图' },
|
||||
{ type: 'kpi', label: '支付成功率' },
|
||||
{ type: 'kpi', label: '平均响应时间' },
|
||||
{ type: 'kpi', label: '在线用户数' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => layouts.find(l => l.key === activeLayout.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.layout-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.tab {
|
||||
padding: 0.4rem 0.75rem; border-radius: 6px; cursor: pointer;
|
||||
font-size: 0.85rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab:hover { border-color: var(--vp-c-brand); }
|
||||
.tab.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); font-weight: 600; }
|
||||
.layout-preview { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); }
|
||||
.preview-title { font-weight: 700; font-size: 0.95rem; }
|
||||
.preview-desc { color: var(--vp-c-text-2); font-size: 0.82rem; margin-bottom: 0.75rem; }
|
||||
.mock-dashboard { display: grid; gap: 0.4rem; margin-bottom: 0.75rem; grid-template-columns: repeat(4, 1fr); }
|
||||
.widget {
|
||||
padding: 0.5rem; border-radius: 6px; text-align: center;
|
||||
font-size: 0.75rem; font-weight: 600; border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
.widget.kpi { background: rgba(var(--vp-c-brand-rgb), 0.06); grid-column: span 1; }
|
||||
.widget.chart-wide { background: rgba(34,197,94,0.06); grid-column: span 4; min-height: 50px; }
|
||||
.widget.table { background: rgba(245,158,11,0.06); grid-column: span 4; }
|
||||
.widget.half { background: rgba(99,102,241,0.06); grid-column: span 2; min-height: 40px; }
|
||||
.widget.big-number { background: rgba(239,68,68,0.06); grid-column: span 4; min-height: 40px; font-size: 0.9rem; }
|
||||
.widget-label { color: var(--vp-c-text-2); }
|
||||
.use-case { font-size: 0.82rem; color: var(--vp-c-text-3); }
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<!--
|
||||
CAPTheoremDemo.vue
|
||||
CAP 定理交互演示:展示一致性、可用性、分区容错性的权衡
|
||||
-->
|
||||
<template>
|
||||
<div class="cap-demo">
|
||||
<div class="header">
|
||||
<div class="title">CAP 定理交互演示</div>
|
||||
<div class="subtitle">点击选择两个属性,查看对应的系统类型</div>
|
||||
</div>
|
||||
|
||||
<div class="triangle">
|
||||
<div
|
||||
v-for="item in capItems"
|
||||
:key="item.key"
|
||||
:class="['cap-node', { active: selected.includes(item.key) }]"
|
||||
@click="toggle(item.key)"
|
||||
>
|
||||
<div class="cap-letter">{{ item.letter }}</div>
|
||||
<div class="cap-name">{{ item.name }}</div>
|
||||
<div class="cap-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="result" class="result-panel">
|
||||
<div class="result-title">{{ result.type }}</div>
|
||||
<div class="result-desc">{{ result.desc }}</div>
|
||||
<div class="result-examples">
|
||||
<span class="label">典型系统:</span>{{ result.examples }}
|
||||
</div>
|
||||
<div class="result-tradeoff">
|
||||
<span class="label">放弃了:</span>{{ result.sacrifice }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="hint">请选择两个属性查看结果</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selected = ref(['C', 'A'])
|
||||
|
||||
const capItems = [
|
||||
{ key: 'C', letter: 'C', name: '一致性', desc: '所有节点看到相同的数据' },
|
||||
{ key: 'A', letter: 'A', name: '可用性', desc: '每个请求都能得到响应' },
|
||||
{ key: 'P', letter: 'P', name: '分区容错', desc: '网络分区时系统仍能运行' }
|
||||
]
|
||||
|
||||
const combinations = {
|
||||
'CA': {
|
||||
type: 'CA 系统(放弃分区容错)',
|
||||
desc: '在没有网络分区的情况下,同时保证一致性和可用性。但在分布式环境中,网络分区是不可避免的,所以纯 CA 系统在实际分布式场景中很少见。',
|
||||
examples: '单机 MySQL、PostgreSQL(单节点)',
|
||||
sacrifice: '分区容错性(P)— 网络故障时系统不可用'
|
||||
},
|
||||
'CP': {
|
||||
type: 'CP 系统(放弃可用性)',
|
||||
desc: '网络分区时优先保证数据一致性,可能拒绝部分请求。适合对数据正确性要求极高的场景。',
|
||||
examples: 'ZooKeeper、etcd、HBase、MongoDB(强一致模式)',
|
||||
sacrifice: '可用性(A)— 分区时部分请求会被拒绝或超时'
|
||||
},
|
||||
'AP': {
|
||||
type: 'AP 系统(放弃强一致性)',
|
||||
desc: '网络分区时优先保证可用性,允许数据暂时不一致(最终一致性)。适合对可用性要求高、能容忍短暂不一致的场景。',
|
||||
examples: 'Cassandra、DynamoDB、DNS、CDN',
|
||||
sacrifice: '强一致性(C)— 不同节点可能短暂返回不同数据'
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(key) {
|
||||
const idx = selected.value.indexOf(key)
|
||||
if (idx >= 0) {
|
||||
selected.value = selected.value.filter(k => k !== key)
|
||||
} else {
|
||||
if (selected.value.length >= 2) {
|
||||
selected.value = [selected.value[1], key]
|
||||
} else {
|
||||
selected.value = [...selected.value, key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = computed(() => {
|
||||
if (selected.value.length !== 2) return null
|
||||
const combo = [...selected.value].sort().join('')
|
||||
return combinations[combo] || null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cap-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.triangle { display: flex; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; justify-content: center; }
|
||||
.cap-node {
|
||||
flex: 1; min-width: 120px; max-width: 200px; padding: 0.75rem; border-radius: 8px;
|
||||
cursor: pointer; text-align: center; background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider); transition: all 0.2s;
|
||||
}
|
||||
.cap-node:hover { border-color: var(--vp-c-brand); }
|
||||
.cap-node.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.08); }
|
||||
.cap-letter { font-size: 1.5rem; font-weight: 800; color: var(--vp-c-brand); }
|
||||
.cap-name { font-weight: 700; font-size: 0.9rem; margin: 0.2rem 0; }
|
||||
.cap-desc { font-size: 0.75rem; color: var(--vp-c-text-2); }
|
||||
.result-panel {
|
||||
background: var(--vp-c-bg); border-radius: 8px; padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.result-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.5rem; }
|
||||
.result-desc { font-size: 0.82rem; color: var(--vp-c-text-2); margin-bottom: 0.5rem; }
|
||||
.result-examples, .result-tradeoff { font-size: 0.82rem; margin-bottom: 0.25rem; }
|
||||
.label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
.hint { text-align: center; color: var(--vp-c-text-3); font-size: 0.85rem; padding: 1rem; }
|
||||
</style>
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
<!--
|
||||
ConsistencyModelsDemo.vue
|
||||
一致性模型演示:展示强一致性、最终一致性、因果一致性的区别
|
||||
-->
|
||||
<template>
|
||||
<div class="consistency-demo">
|
||||
<div class="header">
|
||||
<div class="title">一致性模型对比</div>
|
||||
<div class="subtitle">点击查看不同一致性模型的行为差异</div>
|
||||
</div>
|
||||
|
||||
<div class="model-tabs">
|
||||
<div
|
||||
v-for="m in models"
|
||||
:key="m.key"
|
||||
:class="['tab', { active: activeModel === m.key }]"
|
||||
@click="activeModel = m.key"
|
||||
>
|
||||
{{ m.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="model-detail">
|
||||
<div class="model-name">{{ current.name }}</div>
|
||||
<div class="model-desc">{{ current.desc }}</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div v-for="(step, i) in current.steps" :key="i" class="step">
|
||||
<div class="step-time">T{{ i + 1 }}</div>
|
||||
<div class="step-nodes">
|
||||
<div
|
||||
v-for="(node, ni) in step.nodes"
|
||||
:key="ni"
|
||||
:class="['node', node.status]"
|
||||
>
|
||||
<div class="node-label">{{ node.name }}</div>
|
||||
<div class="node-value">{{ node.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-desc">{{ step.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="model-tradeoff">
|
||||
<span class="label">权衡:</span>{{ current.tradeoff }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeModel = ref('strong')
|
||||
|
||||
const models = [
|
||||
{
|
||||
key: 'strong',
|
||||
name: '强一致性',
|
||||
desc: '写入成功后,所有节点立即返回最新值。像单机数据库一样的体验。',
|
||||
tradeoff: '延迟高(需要等所有节点确认),可用性低(节点故障时可能阻塞)',
|
||||
steps: [
|
||||
{ nodes: [{ name: '节点A', value: 'v1', status: 'ok' }, { name: '节点B', value: 'v1', status: 'ok' }, { name: '节点C', value: 'v1', status: 'ok' }], desc: '初始状态,所有节点数据一致' },
|
||||
{ nodes: [{ name: '节点A', value: 'v2 ✍️', status: 'writing' }, { name: '节点B', value: '同步中...', status: 'syncing' }, { name: '节点C', value: '同步中...', status: 'syncing' }], desc: '客户端写入 v2,等待所有节点确认' },
|
||||
{ nodes: [{ name: '节点A', value: 'v2', status: 'ok' }, { name: '节点B', value: 'v2', status: 'ok' }, { name: '节点C', value: 'v2', status: 'ok' }], desc: '所有节点确认后才返回成功,读任意节点都是 v2' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'eventual',
|
||||
name: '最终一致性',
|
||||
desc: '写入后不等所有节点同步,数据最终会一致,但中间可能读到旧值。',
|
||||
tradeoff: '延迟低、可用性高,但可能短暂读到旧数据',
|
||||
steps: [
|
||||
{ nodes: [{ name: '节点A', value: 'v1', status: 'ok' }, { name: '节点B', value: 'v1', status: 'ok' }, { name: '节点C', value: 'v1', status: 'ok' }], desc: '初始状态' },
|
||||
{ nodes: [{ name: '节点A', value: 'v2 ✍️', status: 'writing' }, { name: '节点B', value: 'v1', status: 'stale' }, { name: '节点C', value: 'v1', status: 'stale' }], desc: '写入 A 后立即返回成功,B/C 还是旧值' },
|
||||
{ nodes: [{ name: '节点A', value: 'v2', status: 'ok' }, { name: '节点B', value: 'v2', status: 'ok' }, { name: '节点C', value: 'v1→v2', status: 'syncing' }], desc: '后台异步同步,逐渐达到一致' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'causal',
|
||||
name: '因果一致性',
|
||||
desc: '有因果关系的操作保证顺序,无因果关系的操作可以乱序。介于强一致和最终一致之间。',
|
||||
tradeoff: '比强一致性延迟低,比最终一致性更可预测',
|
||||
steps: [
|
||||
{ nodes: [{ name: '用户A', value: '发帖: "你好"', status: 'ok' }, { name: '用户B', value: '看到帖子', status: 'ok' }, { name: '用户C', value: '看到帖子', status: 'ok' }], desc: '用户 A 发帖' },
|
||||
{ nodes: [{ name: '用户A', value: '发帖: "你好"', status: 'ok' }, { name: '用户B', value: '回复: "嗨!"', status: 'writing' }, { name: '用户C', value: '看到帖子', status: 'ok' }], desc: '用户 B 回复(因果依赖于 A 的帖子)' },
|
||||
{ nodes: [{ name: '用户A', value: '看到回复', status: 'ok' }, { name: '用户B', value: '回复: "嗨!"', status: 'ok' }, { name: '用户C', value: '先看到帖子再看到回复', status: 'ok' }], desc: '所有人都先看到帖子再看到回复(因果顺序保证)' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => models.find(m => m.key === activeModel.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.consistency-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.model-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.tab {
|
||||
padding: 0.4rem 0.75rem; border-radius: 6px; cursor: pointer;
|
||||
font-size: 0.85rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab:hover { border-color: var(--vp-c-brand); }
|
||||
.tab.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); font-weight: 600; }
|
||||
.model-detail { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); }
|
||||
.model-name { font-weight: 700; font-size: 0.95rem; }
|
||||
.model-desc { color: var(--vp-c-text-2); font-size: 0.82rem; margin-bottom: 0.75rem; }
|
||||
.timeline { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.75rem; }
|
||||
.step { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
.step-time { font-weight: 700; font-size: 0.8rem; color: var(--vp-c-brand); min-width: 28px; }
|
||||
.step-nodes { display: flex; gap: 0.4rem; flex: 1; }
|
||||
.node {
|
||||
padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 0.72rem;
|
||||
border: 1px solid var(--vp-c-divider); flex: 1; text-align: center;
|
||||
}
|
||||
.node.ok { background: rgba(34,197,94,0.08); border-color: #22c55e; }
|
||||
.node.writing { background: rgba(var(--vp-c-brand-rgb),0.08); border-color: var(--vp-c-brand); }
|
||||
.node.syncing { background: rgba(245,158,11,0.08); border-color: #f59e0b; }
|
||||
.node.stale { background: rgba(239,68,68,0.08); border-color: #ef4444; }
|
||||
.node-label { font-weight: 600; }
|
||||
.node-value { color: var(--vp-c-text-2); }
|
||||
.step-desc { font-size: 0.75rem; color: var(--vp-c-text-3); width: 100%; margin-top: 0.15rem; }
|
||||
.model-tradeoff { font-size: 0.82rem; }
|
||||
.label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
@media (max-width: 640px) { .step { flex-direction: column; } .step-nodes { width: 100%; } }
|
||||
</style>
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
<!--
|
||||
DistributedChallengesDemo.vue
|
||||
分布式系统常见挑战交互演示
|
||||
-->
|
||||
<template>
|
||||
<div class="challenges-demo">
|
||||
<div class="header">
|
||||
<div class="title">分布式系统八大挑战</div>
|
||||
<div class="subtitle">点击查看每个挑战的详情和应对策略</div>
|
||||
</div>
|
||||
|
||||
<div class="challenge-grid">
|
||||
<div
|
||||
v-for="c in challenges"
|
||||
:key="c.key"
|
||||
:class="['challenge-card', { active: activeChallenge === c.key }]"
|
||||
@click="activeChallenge = activeChallenge === c.key ? null : c.key"
|
||||
>
|
||||
<div class="challenge-icon">{{ c.icon }}</div>
|
||||
<div class="challenge-name">{{ c.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-panel">
|
||||
<div class="detail-title">{{ current.icon }} {{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="detail-scenario">
|
||||
<span class="label">场景举例:</span>{{ current.scenario }}
|
||||
</div>
|
||||
<div class="detail-solution">
|
||||
<span class="label">应对策略:</span>
|
||||
<ul class="solution-list">
|
||||
<li v-for="(s, i) in current.solutions" :key="i">{{ s }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeChallenge = ref('network')
|
||||
|
||||
const challenges = [
|
||||
{
|
||||
key: 'network',
|
||||
name: '网络不可靠',
|
||||
icon: '🔌',
|
||||
desc: '分布式系统的节点通过网络通信,而网络随时可能丢包、延迟、断开。这是分布式系统最根本的挑战——你永远不能假设网络是可靠的。',
|
||||
scenario: '服务 A 调用服务 B,请求发出后 3 秒没收到响应。是 B 没收到?还是 B 处理了但响应丢了?A 无法区分。',
|
||||
solutions: [
|
||||
'超时 + 重试:设置合理超时,失败后重试(需保证幂等性)',
|
||||
'心跳检测:定期发送心跳包检测连接是否存活',
|
||||
'断路器模式:连续失败后暂停调用,避免雪崩'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'clock',
|
||||
name: '时钟不同步',
|
||||
icon: '⏰',
|
||||
desc: '每台机器的物理时钟都有微小偏差(时钟漂移),即使用 NTP 同步也只能精确到毫秒级。在分布式系统中,你不能依赖物理时钟来判断事件的先后顺序。',
|
||||
scenario: '节点 A 在 10:00:00.001 写入数据,节点 B 在 10:00:00.002 写入数据。但 B 的时钟快了 5ms,实际上 B 先写的。',
|
||||
solutions: [
|
||||
'逻辑时钟(Lamport Clock):用递增计数器代替物理时钟',
|
||||
'向量时钟(Vector Clock):每个节点维护一个向量,追踪因果关系',
|
||||
'TrueTime(Google Spanner):用 GPS + 原子钟提供有界误差的时间'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'partition',
|
||||
name: '网络分区',
|
||||
icon: '✂️',
|
||||
desc: '网络分区是指部分节点之间无法通信,但各自仍在运行。这时系统必须在一致性和可用性之间做选择(CAP 定理)。',
|
||||
scenario: '数据中心 A 和 B 之间的光纤被挖断,两边的服务各自运行,但数据开始分叉。',
|
||||
solutions: [
|
||||
'CP 策略:分区时拒绝写入,保证一致性(如 ZooKeeper)',
|
||||
'AP 策略:分区时允许写入,事后合并冲突(如 DynamoDB)',
|
||||
'多数派写入:只要多数节点确认就算成功'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'consistency',
|
||||
name: '数据一致性',
|
||||
icon: '🔄',
|
||||
desc: '多个副本之间如何保持数据一致?强一致性性能差,最终一致性可能读到旧数据。没有银弹,只有权衡。',
|
||||
scenario: '用户在节点 A 修改了头像,但刷新页面时请求被路由到节点 B,看到的还是旧头像。',
|
||||
solutions: [
|
||||
'读写同一节点:写入后的读请求路由到同一节点',
|
||||
'读修复(Read Repair):读取时检测不一致并修复',
|
||||
'反熵协议:后台定期比对副本,修复差异'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'failure',
|
||||
name: '部分失败',
|
||||
icon: '💥',
|
||||
desc: '分布式系统中,部分节点可能失败而其他节点正常运行。系统需要在部分失败的情况下继续提供服务。',
|
||||
scenario: '5 个节点的集群中有 2 个节点宕机,系统需要判断:是继续服务还是停止?剩余节点的数据是否完整?',
|
||||
solutions: [
|
||||
'冗余副本:数据存多份,单点故障不影响可用性',
|
||||
'故障检测:通过心跳和超时机制快速发现故障节点',
|
||||
'自动故障转移:检测到主节点故障后自动切换到备节点'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'split-brain',
|
||||
name: '脑裂问题',
|
||||
icon: '🧠',
|
||||
desc: '当网络分区导致集群分成两部分时,两边都认为自己是"主",各自接受写入,导致数据冲突。这就是脑裂。',
|
||||
scenario: '主从架构中,主节点和从节点之间网络断开,从节点以为主节点挂了,自己升级为主。现在有两个主节点同时写入。',
|
||||
solutions: [
|
||||
'多数派选举:只有获得多数票的节点才能成为主节点',
|
||||
'Fencing Token:旧主节点的写入请求会被存储层拒绝',
|
||||
'仲裁节点:引入第三方节点来裁决谁是真正的主'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'ordering',
|
||||
name: '事件排序',
|
||||
icon: '📋',
|
||||
desc: '在分布式系统中,不同节点上发生的事件没有全局统一的顺序。如何确定"谁先谁后"是一个根本性难题。',
|
||||
scenario: '两个用户同时编辑同一个文档,节点 A 收到"删除第 3 行",节点 B 收到"修改第 3 行"。最终结果取决于执行顺序。',
|
||||
solutions: [
|
||||
'全序广播(Total Order Broadcast):所有节点以相同顺序处理消息',
|
||||
'CRDT(无冲突复制数据类型):数据结构本身保证合并无冲突',
|
||||
'OT(操作转换):Google Docs 使用的协作编辑算法'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'transaction',
|
||||
name: '分布式事务',
|
||||
icon: '🔐',
|
||||
desc: '跨多个节点的操作如何保证原子性?要么全部成功,要么全部回滚。这比单机事务复杂得多。',
|
||||
scenario: '电商下单:扣库存在服务 A,扣余额在服务 B,创建订单在服务 C。如果扣余额失败,库存需要回滚。',
|
||||
solutions: [
|
||||
'2PC(两阶段提交):协调者先问所有参与者能否提交,再统一提交',
|
||||
'Saga 模式:每个步骤有对应的补偿操作,失败时逐步回滚',
|
||||
'TCC(Try-Confirm-Cancel):预留资源 → 确认 → 取消'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() =>
|
||||
challenges.find(c => c.key === activeChallenge.value)
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.challenges-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.challenge-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.challenge-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
padding: 0.6rem 0.4rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.challenge-card:hover { border-color: var(--vp-c-brand); }
|
||||
.challenge-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.05);
|
||||
}
|
||||
.challenge-icon { font-size: 1.3rem; }
|
||||
.challenge-name { font-size: 0.75rem; font-weight: 600; text-align: center; }
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.4rem; }
|
||||
.detail-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.detail-scenario {
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(245, 158, 11, 0.06);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.detail-solution { font-size: 0.82rem; }
|
||||
.solution-list {
|
||||
margin: 0.3rem 0 0 1.2rem;
|
||||
padding: 0;
|
||||
}
|
||||
.solution-list li {
|
||||
margin-bottom: 0.2rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
</style>
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
<!--
|
||||
DockerArchitectureDemo.vue
|
||||
Docker 架构对比演示:虚拟机 vs 容器
|
||||
-->
|
||||
<template>
|
||||
<div class="docker-arch-demo">
|
||||
<div class="header">
|
||||
<div class="title">虚拟机 vs 容器</div>
|
||||
<div class="subtitle">点击切换查看两种虚拟化方式的架构差异</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="['tab-btn', { active: activeTab === tab.key }]"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="arch-view">
|
||||
<div class="layers">
|
||||
<div
|
||||
v-for="(layer, i) in currentLayers"
|
||||
:key="i"
|
||||
:class="['layer', layer.type]"
|
||||
>
|
||||
<div class="layer-label">{{ layer.label }}</div>
|
||||
<div v-if="layer.items" class="layer-items">
|
||||
<div v-for="(item, j) in layer.items" :key="j" class="layer-item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison">
|
||||
<div v-for="(item, i) in currentInfo" :key="i" class="info-row">
|
||||
<span class="info-label">{{ item.label }}</span>
|
||||
<span :class="['info-value', item.highlight ? 'highlight' : '']">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeTab = ref('container')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'vm', label: '虚拟机' },
|
||||
{ key: 'container', label: '容器' }
|
||||
]
|
||||
|
||||
const vmLayers = [
|
||||
{ label: '应用 A / 应用 B / 应用 C', type: 'app', items: ['App A + Bins/Libs', 'App B + Bins/Libs', 'App C + Bins/Libs'] },
|
||||
{ label: '客户操作系统(Guest OS)', type: 'os', items: ['Ubuntu', 'CentOS', 'Debian'] },
|
||||
{ label: 'Hypervisor(VMware / KVM)', type: 'hypervisor' },
|
||||
{ label: '宿主操作系统(Host OS)', type: 'host' },
|
||||
{ label: '物理硬件', type: 'hardware' }
|
||||
]
|
||||
|
||||
const containerLayers = [
|
||||
{ label: '应用 A / 应用 B / 应用 C', type: 'app', items: ['App A + Bins/Libs', 'App B + Bins/Libs', 'App C + Bins/Libs'] },
|
||||
{ label: 'Docker Engine', type: 'docker' },
|
||||
{ label: '宿主操作系统(Host OS)', type: 'host' },
|
||||
{ label: '物理硬件', type: 'hardware' }
|
||||
]
|
||||
|
||||
const vmInfo = [
|
||||
{ label: '启动速度', value: '分钟级', highlight: false },
|
||||
{ label: '资源占用', value: '每个 VM 需要完整 OS(GB 级)', highlight: false },
|
||||
{ label: '隔离性', value: '强(硬件级隔离)', highlight: true },
|
||||
{ label: '密度', value: '单机通常 10-20 个 VM', highlight: false },
|
||||
{ label: '镜像大小', value: 'GB 级', highlight: false }
|
||||
]
|
||||
|
||||
const containerInfo = [
|
||||
{ label: '启动速度', value: '秒级', highlight: true },
|
||||
{ label: '资源占用', value: '共享宿主 OS 内核(MB 级)', highlight: true },
|
||||
{ label: '隔离性', value: '较强(进程级隔离)', highlight: false },
|
||||
{ label: '密度', value: '单机可运行数百个容器', highlight: true },
|
||||
{ label: '镜像大小', value: 'MB 级', highlight: true }
|
||||
]
|
||||
|
||||
const currentLayers = computed(() => activeTab.value === 'vm' ? vmLayers : containerLayers)
|
||||
const currentInfo = computed(() => activeTab.value === 'vm' ? vmInfo : containerInfo)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.docker-arch-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.tab-btn {
|
||||
padding: 0.4rem 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab-btn:hover { border-color: var(--vp-c-brand); }
|
||||
.tab-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.arch-view {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.arch-view { grid-template-columns: 1fr; }
|
||||
}
|
||||
.layers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.layer {
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.layer.app { background: rgba(59, 130, 246, 0.12); color: var(--vp-c-text-1); }
|
||||
.layer.os { background: rgba(245, 158, 11, 0.12); color: var(--vp-c-text-1); }
|
||||
.layer.hypervisor { background: rgba(239, 68, 68, 0.12); color: var(--vp-c-text-1); }
|
||||
.layer.docker { background: rgba(6, 182, 212, 0.15); color: var(--vp-c-text-1); }
|
||||
.layer.host { background: rgba(34, 197, 94, 0.12); color: var(--vp-c-text-1); }
|
||||
.layer.hardware { background: rgba(107, 114, 128, 0.12); color: var(--vp-c-text-2); }
|
||||
.layer-label { margin-bottom: 0.25rem; }
|
||||
.layer-items {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.layer-item {
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.comparison {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.info-label { color: var(--vp-c-text-2); font-weight: 500; }
|
||||
.info-value { font-weight: 600; }
|
||||
.info-value.highlight { color: var(--vp-c-brand); }
|
||||
</style>
|
||||
@@ -0,0 +1,178 @@
|
||||
<!--
|
||||
DockerLifecycleDemo.vue
|
||||
Docker 生命周期演示:镜像构建到容器运行的流程
|
||||
-->
|
||||
<template>
|
||||
<div class="docker-lifecycle-demo">
|
||||
<div class="header">
|
||||
<div class="title">Docker 生命周期</div>
|
||||
<div class="subtitle">点击每个阶段查看详细说明</div>
|
||||
</div>
|
||||
|
||||
<div class="stages">
|
||||
<div
|
||||
v-for="(stage, i) in stages"
|
||||
:key="stage.key"
|
||||
:class="['stage-card', { active: activeStage === stage.key }]"
|
||||
@click="activeStage = stage.key"
|
||||
>
|
||||
<div class="stage-icon">{{ stage.icon }}</div>
|
||||
<div class="stage-name">{{ stage.name }}</div>
|
||||
<div v-if="i < stages.length - 1" class="arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-panel">
|
||||
<div class="detail-title">{{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="command-block">
|
||||
<div class="cmd-label">常用命令</div>
|
||||
<div v-for="(cmd, i) in current.commands" :key="i" class="cmd-item">
|
||||
<code>{{ cmd.cmd }}</code>
|
||||
<span class="cmd-desc">{{ cmd.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeStage = ref('write')
|
||||
|
||||
const stages = [
|
||||
{
|
||||
key: 'write',
|
||||
name: '编写 Dockerfile',
|
||||
icon: '📝',
|
||||
desc: 'Dockerfile 是构建镜像的"配方",定义了从基础镜像开始,如何一步步构建出你的应用环境。每条指令创建一个镜像层(Layer),Docker 会缓存这些层以加速后续构建。',
|
||||
commands: [
|
||||
{ cmd: 'FROM node:18-alpine', desc: '指定基础镜像' },
|
||||
{ cmd: 'WORKDIR /app', desc: '设置工作目录' },
|
||||
{ cmd: 'COPY package*.json ./', desc: '复制依赖文件(利用缓存)' },
|
||||
{ cmd: 'RUN npm install', desc: '安装依赖' },
|
||||
{ cmd: 'COPY . .', desc: '复制应用代码' },
|
||||
{ cmd: 'EXPOSE 3000', desc: '声明端口' },
|
||||
{ cmd: 'CMD ["node", "server.js"]', desc: '启动命令' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'build',
|
||||
name: '构建镜像',
|
||||
icon: '🔨',
|
||||
desc: 'docker build 命令读取 Dockerfile,逐层执行指令,最终生成一个不可变的镜像(Image)。镜像是只读的模板,包含运行应用所需的一切:代码、运行时、库、环境变量。',
|
||||
commands: [
|
||||
{ cmd: 'docker build -t myapp:1.0 .', desc: '构建并打标签' },
|
||||
{ cmd: 'docker images', desc: '查看本地镜像列表' },
|
||||
{ cmd: 'docker image prune', desc: '清理无用镜像' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'push',
|
||||
name: '推送仓库',
|
||||
icon: '☁️',
|
||||
desc: '将构建好的镜像推送到镜像仓库(Registry),如 Docker Hub、阿里云 ACR、AWS ECR。团队成员和部署环境可以从仓库拉取镜像,实现"一次构建,到处运行"。',
|
||||
commands: [
|
||||
{ cmd: 'docker tag myapp:1.0 registry/myapp:1.0', desc: '给镜像打远程标签' },
|
||||
{ cmd: 'docker push registry/myapp:1.0', desc: '推送到仓库' },
|
||||
{ cmd: 'docker pull registry/myapp:1.0', desc: '从仓库拉取' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'run',
|
||||
name: '运行容器',
|
||||
icon: '▶️',
|
||||
desc: '容器是镜像的运行实例。一个镜像可以启动多个容器,每个容器有独立的文件系统、网络和进程空间。容器是轻量级的,启动只需秒级。',
|
||||
commands: [
|
||||
{ cmd: 'docker run -d -p 3000:3000 myapp:1.0', desc: '后台运行并映射端口' },
|
||||
{ cmd: 'docker ps', desc: '查看运行中的容器' },
|
||||
{ cmd: 'docker logs <container>', desc: '查看容器日志' },
|
||||
{ cmd: 'docker exec -it <container> sh', desc: '进入容器终端' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'manage',
|
||||
name: '管理容器',
|
||||
icon: '⚙️',
|
||||
desc: '容器运行后需要监控、停止、重启或删除。Docker Compose 可以管理多个容器的编排,定义服务间的依赖关系和网络。',
|
||||
commands: [
|
||||
{ cmd: 'docker stop <container>', desc: '停止容器' },
|
||||
{ cmd: 'docker restart <container>', desc: '重启容器' },
|
||||
{ cmd: 'docker rm <container>', desc: '删除容器' },
|
||||
{ cmd: 'docker compose up -d', desc: '用 Compose 启动多服务' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => stages.find(s => s.key === activeStage.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.docker-lifecycle-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.stages {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stage-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.stage-card:hover { border-color: var(--vp-c-brand); }
|
||||
.stage-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.05);
|
||||
}
|
||||
.stage-icon { font-size: 1.1rem; }
|
||||
.stage-name { font-size: 0.8rem; font-weight: 600; }
|
||||
.arrow { color: var(--vp-c-text-3); font-size: 0.9rem; margin: 0 0.1rem; }
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.4rem; }
|
||||
.detail-desc { font-size: 0.82rem; color: var(--vp-c-text-2); margin-bottom: 0.75rem; line-height: 1.6; }
|
||||
.command-block {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
}
|
||||
.cmd-label { font-weight: 600; font-size: 0.78rem; margin-bottom: 0.4rem; color: var(--vp-c-text-2); }
|
||||
.cmd-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.cmd-item code {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.cmd-desc { color: var(--vp-c-text-3); }
|
||||
</style>
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<!--
|
||||
AvailabilityCalculatorDemo.vue
|
||||
可用性计算器:展示不同 SLA 级别对应的停机时间
|
||||
-->
|
||||
<template>
|
||||
<div class="availability-demo">
|
||||
<div class="header">
|
||||
<div class="title">可用性等级计算器</div>
|
||||
<div class="subtitle">点击查看不同"几个 9"对应的停机时间</div>
|
||||
</div>
|
||||
|
||||
<div class="sla-cards">
|
||||
<div
|
||||
v-for="sla in slaLevels"
|
||||
:key="sla.nines"
|
||||
:class="['sla-card', { active: activeSla === sla.nines }]"
|
||||
@click="activeSla = sla.nines"
|
||||
>
|
||||
<div class="sla-nines">{{ sla.label }}</div>
|
||||
<div class="sla-percent">{{ sla.percent }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-panel">
|
||||
<div class="detail-title">{{ current.label }}({{ current.percent }})</div>
|
||||
<div class="downtime-grid">
|
||||
<div class="downtime-item">
|
||||
<div class="dt-label">每年停机</div>
|
||||
<div class="dt-value">{{ current.yearly }}</div>
|
||||
</div>
|
||||
<div class="downtime-item">
|
||||
<div class="dt-label">每月停机</div>
|
||||
<div class="dt-value">{{ current.monthly }}</div>
|
||||
</div>
|
||||
<div class="downtime-item">
|
||||
<div class="dt-label">每周停机</div>
|
||||
<div class="dt-value">{{ current.weekly }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-examples">
|
||||
<span class="label">典型场景:</span>{{ current.examples }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeSla = ref('3')
|
||||
|
||||
const slaLevels = [
|
||||
{ nines: '2', label: '2 个 9', percent: '99%', yearly: '3.65 天', monthly: '7.3 小时', weekly: '1.68 小时', examples: '内部工具、非关键系统' },
|
||||
{ nines: '3', label: '3 个 9', percent: '99.9%', yearly: '8.76 小时', monthly: '43.8 分钟', weekly: '10.1 分钟', examples: '普通 Web 应用、企业系统' },
|
||||
{ nines: '4', label: '4 个 9', percent: '99.99%', yearly: '52.6 分钟', monthly: '4.38 分钟', weekly: '1.01 分钟', examples: '电商平台、SaaS 服务' },
|
||||
{ nines: '5', label: '5 个 9', percent: '99.999%', yearly: '5.26 分钟', monthly: '26.3 秒', weekly: '6.05 秒', examples: '金融交易、电信核心网' }
|
||||
]
|
||||
|
||||
const current = computed(() => slaLevels.find(s => s.nines === activeSla.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.availability-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.sla-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.sla-card {
|
||||
padding: 0.6rem; border-radius: 8px; cursor: pointer; text-align: center;
|
||||
background: var(--vp-c-bg); border: 2px solid var(--vp-c-divider); transition: all 0.2s;
|
||||
}
|
||||
.sla-card:hover { border-color: var(--vp-c-brand); }
|
||||
.sla-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.08); }
|
||||
.sla-nines { font-weight: 800; font-size: 1.1rem; color: var(--vp-c-brand); }
|
||||
.sla-percent { font-size: 0.8rem; color: var(--vp-c-text-2); }
|
||||
.detail-panel { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); }
|
||||
.detail-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.75rem; }
|
||||
.downtime-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; margin-bottom: 0.75rem; }
|
||||
.downtime-item { text-align: center; padding: 0.5rem; border-radius: 6px; background: var(--vp-c-bg-soft); }
|
||||
.dt-label { font-size: 0.75rem; color: var(--vp-c-text-3); }
|
||||
.dt-value { font-weight: 700; font-size: 0.9rem; color: var(--vp-c-brand); }
|
||||
.detail-examples { font-size: 0.82rem; }
|
||||
.label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<!--
|
||||
FailoverStrategyDemo.vue
|
||||
故障转移策略演示:展示主备、主主、多活等高可用架构
|
||||
-->
|
||||
<template>
|
||||
<div class="failover-demo">
|
||||
<div class="header">
|
||||
<div class="title">故障转移策略对比</div>
|
||||
<div class="subtitle">点击查看不同高可用架构的工作方式</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-tabs">
|
||||
<div
|
||||
v-for="s in strategies"
|
||||
:key="s.key"
|
||||
:class="['tab', { active: activeStrategy === s.key }]"
|
||||
@click="activeStrategy = s.key"
|
||||
>
|
||||
{{ s.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="strategy-detail">
|
||||
<div class="strategy-name">{{ current.name }}</div>
|
||||
<div class="strategy-desc">{{ current.desc }}</div>
|
||||
|
||||
<div class="arch-visual">
|
||||
<div
|
||||
v-for="(node, i) in current.nodes"
|
||||
:key="i"
|
||||
:class="['arch-node', node.role]"
|
||||
>
|
||||
<div class="node-role">{{ node.label }}</div>
|
||||
<div class="node-status">{{ node.status }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<div class="pc-title">优点</div>
|
||||
<div v-for="p in current.pros" :key="p" class="pc-item good">{{ p }}</div>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<div class="pc-title">缺点</div>
|
||||
<div v-for="c in current.cons" :key="c" class="pc-item bad">{{ c }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeStrategy = ref('active-standby')
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
key: 'active-standby',
|
||||
name: '主备模式',
|
||||
desc: '一个主节点处理所有请求,备节点待命。主节点故障时,备节点接管。',
|
||||
nodes: [
|
||||
{ label: '主节点', status: '处理请求', role: 'primary' },
|
||||
{ label: '备节点', status: '待命同步', role: 'standby' }
|
||||
],
|
||||
pros: ['架构简单,易于理解', '数据一致性好保证'],
|
||||
cons: ['备节点资源浪费', '切换有短暂中断(秒级)']
|
||||
},
|
||||
{
|
||||
key: 'active-active',
|
||||
name: '主主模式',
|
||||
desc: '两个节点都处理请求,互相同步数据。任一节点故障,另一个继续服务。',
|
||||
nodes: [
|
||||
{ label: '节点 A', status: '处理请求', role: 'primary' },
|
||||
{ label: '节点 B', status: '处理请求', role: 'primary' }
|
||||
],
|
||||
pros: ['资源利用率高', '无切换中断'],
|
||||
cons: ['数据冲突处理复杂', '需要解决写冲突']
|
||||
},
|
||||
{
|
||||
key: 'multi-az',
|
||||
name: '多可用区',
|
||||
desc: '在同一地域的不同数据中心部署,防止单个机房故障。',
|
||||
nodes: [
|
||||
{ label: 'AZ-1 主', status: '读写', role: 'primary' },
|
||||
{ label: 'AZ-2 从', status: '只读', role: 'secondary' },
|
||||
{ label: 'AZ-3 从', status: '只读', role: 'secondary' }
|
||||
],
|
||||
pros: ['机房级容灾', '读性能可扩展'],
|
||||
cons: ['跨 AZ 延迟(1-2ms)', '成本增加']
|
||||
},
|
||||
{
|
||||
key: 'multi-region',
|
||||
name: '异地多活',
|
||||
desc: '在不同地域部署完整的服务,每个地域独立处理本地流量。',
|
||||
nodes: [
|
||||
{ label: '北京', status: '独立服务', role: 'primary' },
|
||||
{ label: '上海', status: '独立服务', role: 'primary' },
|
||||
{ label: '广州', status: '独立服务', role: 'primary' }
|
||||
],
|
||||
pros: ['地域级容灾', '就近访问延迟低'],
|
||||
cons: ['架构极其复杂', '数据同步挑战大']
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => strategies.find(s => s.key === activeStrategy.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.failover-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.strategy-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.tab {
|
||||
padding: 0.4rem 0.75rem; border-radius: 6px; cursor: pointer;
|
||||
font-size: 0.85rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab:hover { border-color: var(--vp-c-brand); }
|
||||
.tab.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); font-weight: 600; }
|
||||
.strategy-detail { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); }
|
||||
.strategy-name { font-weight: 700; font-size: 0.95rem; }
|
||||
.strategy-desc { color: var(--vp-c-text-2); font-size: 0.82rem; margin-bottom: 0.75rem; }
|
||||
.arch-visual { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; flex-wrap: wrap; justify-content: center; }
|
||||
.arch-node {
|
||||
padding: 0.5rem 0.75rem; border-radius: 6px; text-align: center;
|
||||
border: 1px dashed var(--vp-c-divider); min-width: 90px;
|
||||
}
|
||||
.arch-node.primary { background: rgba(var(--vp-c-brand-rgb), 0.08); border-color: var(--vp-c-brand); }
|
||||
.arch-node.standby { background: rgba(245,158,11,0.08); border-color: #f59e0b; }
|
||||
.arch-node.secondary { background: rgba(99,102,241,0.08); border-color: #6366f1; }
|
||||
.node-role { font-weight: 700; font-size: 0.82rem; }
|
||||
.node-status { font-size: 0.72rem; color: var(--vp-c-text-2); }
|
||||
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
|
||||
.pc-title { font-weight: 700; font-size: 0.82rem; margin-bottom: 0.3rem; }
|
||||
.pc-item { font-size: 0.78rem; padding: 0.2rem 0; }
|
||||
.pc-item.good::before { content: '+ '; color: #22c55e; font-weight: 700; }
|
||||
.pc-item.bad::before { content: '- '; color: #ef4444; font-weight: 700; }
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<!--
|
||||
K8sArchitectureDemo.vue
|
||||
Kubernetes 架构演示:控制平面与工作节点
|
||||
-->
|
||||
<template>
|
||||
<div class="k8s-arch-demo">
|
||||
<div class="header">
|
||||
<div class="title">Kubernetes 架构</div>
|
||||
<div class="subtitle">点击组件查看详细说明</div>
|
||||
</div>
|
||||
|
||||
<div class="arch-layout">
|
||||
<div class="plane control-plane">
|
||||
<div class="plane-title">控制平面(Control Plane)</div>
|
||||
<div class="components">
|
||||
<div
|
||||
v-for="c in controlPlane"
|
||||
:key="c.key"
|
||||
:class="['comp-card', { active: active === c.key }]"
|
||||
@click="active = c.key"
|
||||
>
|
||||
<div class="comp-name">{{ c.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plane worker-plane">
|
||||
<div class="plane-title">工作节点(Worker Node)× N</div>
|
||||
<div class="components">
|
||||
<div
|
||||
v-for="c in workerNode"
|
||||
:key="c.key"
|
||||
:class="['comp-card', { active: active === c.key }]"
|
||||
@click="active = c.key"
|
||||
>
|
||||
<div class="comp-name">{{ c.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-panel">
|
||||
<div class="detail-title">{{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="detail-analogy">
|
||||
<span class="label">类比:</span>{{ current.analogy }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const active = ref('api-server')
|
||||
|
||||
const controlPlane = [
|
||||
{
|
||||
key: 'api-server',
|
||||
name: 'API Server',
|
||||
desc: 'Kubernetes 的"前门",所有操作(kubectl、Dashboard、内部组件)都通过 API Server 进行。它负责认证、授权、准入控制,是集群的唯一入口。',
|
||||
analogy: '公司前台,所有访客和快递都要经过前台登记'
|
||||
},
|
||||
{
|
||||
key: 'etcd',
|
||||
name: 'etcd',
|
||||
desc: '分布式键值存储,保存集群的所有状态数据:Pod 信息、Service 配置、Secret 等。它是集群的"记忆",丢失 etcd 数据等于丢失整个集群。',
|
||||
analogy: '公司的档案室,记录所有员工信息和规章制度'
|
||||
},
|
||||
{
|
||||
key: 'scheduler',
|
||||
name: 'Scheduler',
|
||||
desc: '负责将新创建的 Pod 分配到合适的节点上。它会考虑资源需求、亲和性规则、污点容忍等因素,做出最优调度决策。',
|
||||
analogy: 'HR 部门,根据岗位需求把新员工分配到合适的部门'
|
||||
},
|
||||
{
|
||||
key: 'controller',
|
||||
name: 'Controller Manager',
|
||||
desc: '运行各种控制器(Deployment、ReplicaSet、Job 等),持续监控集群状态,确保实际状态与期望状态一致。如果 Pod 挂了,控制器会自动重建。',
|
||||
analogy: '各部门经理,确保每个部门的人员配置符合编制要求'
|
||||
}
|
||||
]
|
||||
|
||||
const workerNode = [
|
||||
{
|
||||
key: 'kubelet',
|
||||
name: 'kubelet',
|
||||
desc: '每个节点上的"代理人",负责管理本节点上的 Pod 生命周期。它接收 API Server 的指令,调用容器运行时创建/销毁容器,并上报节点状态。',
|
||||
analogy: '每个工位上的组长,负责管理组员的日常工作'
|
||||
},
|
||||
{
|
||||
key: 'kube-proxy',
|
||||
name: 'kube-proxy',
|
||||
desc: '负责实现 Service 的网络规则,将访问 Service 的流量转发到对应的 Pod。它维护节点上的 iptables/IPVS 规则,实现负载均衡。',
|
||||
analogy: '公司的电话总机,把外部来电转接到正确的分机'
|
||||
},
|
||||
{
|
||||
key: 'runtime',
|
||||
name: '容器运行时',
|
||||
desc: '实际运行容器的组件,如 containerd、CRI-O。kubelet 通过 CRI(容器运行时接口)与它交互,它负责拉取镜像、创建和管理容器。',
|
||||
analogy: '实际干活的工人,按照指令完成具体的生产任务'
|
||||
}
|
||||
]
|
||||
|
||||
const allComponents = [...controlPlane, ...workerNode]
|
||||
const current = computed(() => allComponents.find(c => c.key === active.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.k8s-arch-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.arch-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.arch-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
.plane {
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.control-plane { background: rgba(59, 130, 246, 0.06); }
|
||||
.worker-plane { background: rgba(34, 197, 94, 0.06); }
|
||||
.plane-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.components {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.comp-card {
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.comp-card:hover { border-color: var(--vp-c-brand); }
|
||||
.comp-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.08);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.4rem; }
|
||||
.detail-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.detail-analogy {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
</style>
|
||||
@@ -0,0 +1,235 @@
|
||||
<!--
|
||||
K8sWorkloadsDemo.vue
|
||||
Kubernetes 工作负载演示:Pod、Deployment、Service 等核心资源
|
||||
-->
|
||||
<template>
|
||||
<div class="k8s-workloads-demo">
|
||||
<div class="header">
|
||||
<div class="title">K8s 核心资源</div>
|
||||
<div class="subtitle">点击资源类型查看说明和 YAML 示例</div>
|
||||
</div>
|
||||
|
||||
<div class="resource-tabs">
|
||||
<button
|
||||
v-for="r in resources"
|
||||
:key="r.key"
|
||||
:class="['res-btn', { active: activeRes === r.key }]"
|
||||
@click="activeRes = r.key"
|
||||
>
|
||||
{{ r.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<div class="detail-title">{{ current.name }}</div>
|
||||
<div class="detail-badge">{{ current.category }}</div>
|
||||
</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="yaml-block">
|
||||
<div class="yaml-label">YAML 示例</div>
|
||||
<pre class="yaml-code"><code>{{ current.yaml }}</code></pre>
|
||||
</div>
|
||||
<div v-if="current.tips" class="tips">
|
||||
<span class="tip-label">要点:</span>{{ current.tips }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeRes = ref('pod')
|
||||
|
||||
const resources = [
|
||||
{
|
||||
key: 'pod',
|
||||
name: 'Pod',
|
||||
category: '最小调度单元',
|
||||
desc: 'Pod 是 K8s 中最小的部署单元,包含一个或多个紧密关联的容器。同一 Pod 内的容器共享网络和存储,可以通过 localhost 互相通信。',
|
||||
yaml: `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: my-app:1.0
|
||||
ports:
|
||||
- containerPort: 3000`,
|
||||
tips: '生产环境中很少直接创建 Pod,通常通过 Deployment 管理。'
|
||||
},
|
||||
{
|
||||
key: 'deployment',
|
||||
name: 'Deployment',
|
||||
category: '工作负载',
|
||||
desc: 'Deployment 管理 Pod 的副本数、滚动更新和回滚。你声明"我要 3 个副本运行 v1.0",Deployment 控制器会确保始终有 3 个健康的 Pod 在运行。',
|
||||
yaml: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: my-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: my-app
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: my-app:1.0`,
|
||||
tips: '更新镜像版本后,Deployment 会自动执行滚动更新,逐步替换旧 Pod。'
|
||||
},
|
||||
{
|
||||
key: 'service',
|
||||
name: 'Service',
|
||||
category: '网络',
|
||||
desc: 'Service 为一组 Pod 提供稳定的访问入口。Pod 的 IP 会变,但 Service 的 ClusterIP 和 DNS 名称不变。它通过 label selector 找到对应的 Pod,并做负载均衡。',
|
||||
yaml: `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: my-app-svc
|
||||
spec:
|
||||
selector:
|
||||
app: my-app
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
type: ClusterIP`,
|
||||
tips: 'ClusterIP(集群内访问)、NodePort(节点端口)、LoadBalancer(云负载均衡器)是三种常用类型。'
|
||||
},
|
||||
{
|
||||
key: 'configmap',
|
||||
name: 'ConfigMap',
|
||||
category: '配置',
|
||||
desc: 'ConfigMap 存储非敏感的配置数据(如数据库地址、功能开关),可以作为环境变量或文件挂载到 Pod 中。修改 ConfigMap 后可以不重建镜像就更新配置。',
|
||||
yaml: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: app-config
|
||||
data:
|
||||
DB_HOST: "db.example.com"
|
||||
LOG_LEVEL: "info"`,
|
||||
tips: '敏感数据(密码、密钥)应该用 Secret 而不是 ConfigMap。'
|
||||
},
|
||||
{
|
||||
key: 'ingress',
|
||||
name: 'Ingress',
|
||||
category: '网络',
|
||||
desc: 'Ingress 管理集群的外部 HTTP/HTTPS 访问入口,支持基于域名和路径的路由规则。它是集群的"反向代理",通常配合 Nginx Ingress Controller 使用。',
|
||||
yaml: `apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: my-ingress
|
||||
spec:
|
||||
rules:
|
||||
- host: app.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: my-app-svc
|
||||
port:
|
||||
number: 80`,
|
||||
tips: 'Ingress 需要 Ingress Controller 才能工作,它本身只是路由规则的声明。'
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => resources.find(r => r.key === activeRes.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.k8s-workloads-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.resource-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.res-btn {
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.res-btn:hover { border-color: var(--vp-c-brand); }
|
||||
.res-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.detail-title { font-weight: 700; font-size: 0.95rem; }
|
||||
.detail-badge {
|
||||
font-size: 0.68rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.1);
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
.detail-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.yaml-block {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.yaml-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.yaml-code {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.tips {
|
||||
font-size: 0.78rem;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.tip-label { font-weight: 600; }
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<!--
|
||||
LinuxCommandDemo.vue
|
||||
Linux 常用命令分类演示
|
||||
-->
|
||||
<template>
|
||||
<div class="linux-cmd-demo">
|
||||
<div class="header">
|
||||
<div class="title">Linux 命令速查</div>
|
||||
<div class="subtitle">按分类查看常用命令及示例</div>
|
||||
</div>
|
||||
|
||||
<div class="categories">
|
||||
<button
|
||||
v-for="cat in categories"
|
||||
:key="cat.key"
|
||||
:class="['cat-btn', { active: activeCat === cat.key }]"
|
||||
@click="activeCat = cat.key"
|
||||
>
|
||||
{{ cat.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="cmd-list">
|
||||
<div v-for="(cmd, i) in current.commands" :key="i" class="cmd-card">
|
||||
<div class="cmd-header">
|
||||
<code class="cmd-name">{{ cmd.name }}</code>
|
||||
<span class="cmd-brief">{{ cmd.brief }}</span>
|
||||
</div>
|
||||
<div class="cmd-example">
|
||||
<code>{{ cmd.example }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeCat = ref('file')
|
||||
|
||||
const categories = [
|
||||
{
|
||||
key: 'file',
|
||||
label: '文件操作',
|
||||
commands: [
|
||||
{ name: 'ls', brief: '列出文件和目录', example: 'ls -la /home' },
|
||||
{ name: 'cd', brief: '切换目录', example: 'cd /var/log' },
|
||||
{ name: 'cp', brief: '复制文件', example: 'cp -r src/ backup/' },
|
||||
{ name: 'mv', brief: '移动/重命名', example: 'mv old.txt new.txt' },
|
||||
{ name: 'rm', brief: '删除文件', example: 'rm -rf dist/' },
|
||||
{ name: 'mkdir', brief: '创建目录', example: 'mkdir -p src/components' },
|
||||
{ name: 'find', brief: '查找文件', example: 'find . -name "*.js" -type f' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'text',
|
||||
label: '文本处理',
|
||||
commands: [
|
||||
{ name: 'cat', brief: '查看文件内容', example: 'cat config.json' },
|
||||
{ name: 'grep', brief: '搜索文本', example: 'grep -rn "ERROR" /var/log/' },
|
||||
{ name: 'head/tail', brief: '查看文件头/尾', example: 'tail -f app.log' },
|
||||
{ name: 'awk', brief: '文本列处理', example: "awk '{print $1, $3}' data.txt" },
|
||||
{ name: 'sed', brief: '流式文本替换', example: "sed -i 's/old/new/g' file.txt" },
|
||||
{ name: 'wc', brief: '统计行/词/字符数', example: 'wc -l *.js' },
|
||||
{ name: 'sort | uniq', brief: '排序去重', example: 'sort data.txt | uniq -c' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'process',
|
||||
label: '进程管理',
|
||||
commands: [
|
||||
{ name: 'ps', brief: '查看进程', example: 'ps aux | grep node' },
|
||||
{ name: 'top/htop', brief: '实时监控', example: 'top -o %CPU' },
|
||||
{ name: 'kill', brief: '终止进程', example: 'kill -9 12345' },
|
||||
{ name: 'nohup', brief: '后台运行', example: 'nohup node app.js &' },
|
||||
{ name: 'lsof', brief: '查看打开的文件', example: 'lsof -i :3000' },
|
||||
{ name: 'systemctl', brief: '管理系统服务', example: 'systemctl restart nginx' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'network',
|
||||
label: '网络工具',
|
||||
commands: [
|
||||
{ name: 'curl', brief: '发送 HTTP 请求', example: 'curl -X POST -d "data" url' },
|
||||
{ name: 'ping', brief: '测试连通性', example: 'ping -c 4 google.com' },
|
||||
{ name: 'ss/netstat', brief: '查看网络连接', example: 'ss -tlnp' },
|
||||
{ name: 'dig', brief: 'DNS 查询', example: 'dig example.com' },
|
||||
{ name: 'ssh', brief: '远程登录', example: 'ssh user@server -p 22' },
|
||||
{ name: 'scp', brief: '远程复制文件', example: 'scp file.txt user@server:/tmp/' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => categories.find(c => c.key === activeCat.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.linux-cmd-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.categories {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.cat-btn {
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.cat-btn:hover { border-color: var(--vp-c-brand); }
|
||||
.cat-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.cmd-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.cmd-card {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.7rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.cmd-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.cmd-name {
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.08);
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cmd-brief {
|
||||
font-size: 0.78rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.cmd-example code {
|
||||
font-size: 0.73rem;
|
||||
color: var(--vp-c-text-3);
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<!--
|
||||
LinuxFileSystemDemo.vue
|
||||
Linux 文件系统层级演示
|
||||
-->
|
||||
<template>
|
||||
<div class="linux-fs-demo">
|
||||
<div class="header">
|
||||
<div class="title">Linux 文件系统层级</div>
|
||||
<div class="subtitle">点击目录查看用途说明</div>
|
||||
</div>
|
||||
|
||||
<div class="tree">
|
||||
<div
|
||||
v-for="dir in dirs"
|
||||
:key="dir.path"
|
||||
:class="['dir-item', { active: activeDir === dir.path }]"
|
||||
@click="activeDir = dir.path"
|
||||
>
|
||||
<span class="dir-icon">{{ dir.icon }}</span>
|
||||
<span class="dir-path">{{ dir.path }}</span>
|
||||
<span class="dir-brief">{{ dir.brief }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-panel">
|
||||
<div class="detail-title">{{ current.path }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div v-if="current.examples.length" class="examples">
|
||||
<div class="ex-label">常见内容:</div>
|
||||
<div class="ex-list">
|
||||
<span v-for="(ex, i) in current.examples" :key="i" class="ex-tag">{{ ex }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeDir = ref('/')
|
||||
|
||||
const dirs = [
|
||||
{ path: '/', icon: '📁', brief: '根目录', desc: '整个文件系统的起点,所有目录和文件都从这里开始。Linux 中一切皆文件,所有设备、进程信息都以文件形式存在于这棵目录树中。', examples: [] },
|
||||
{ path: '/bin', icon: '⚙️', brief: '基础命令', desc: '存放系统启动和单用户模式下必需的基础命令二进制文件。这些命令所有用户都可以使用。', examples: ['ls', 'cp', 'mv', 'cat', 'grep', 'chmod'] },
|
||||
{ path: '/etc', icon: '📋', brief: '配置文件', desc: '存放系统和应用的配置文件。几乎所有软件的配置都在这里,修改配置是 Linux 运维的日常。', examples: ['nginx.conf', 'hosts', 'passwd', 'ssh/sshd_config', 'crontab'] },
|
||||
{ path: '/home', icon: '🏠', brief: '用户目录', desc: '普通用户的家目录。每个用户在这里有一个以用户名命名的子目录,存放个人文件和配置。', examples: ['/home/alice', '/home/bob', '~/.bashrc', '~/.ssh/'] },
|
||||
{ path: '/var', icon: '📊', brief: '可变数据', desc: '存放运行时会变化的数据:日志、缓存、邮件、数据库文件等。排查问题时经常需要查看这里的日志。', examples: ['/var/log/', '/var/cache/', '/var/lib/mysql/', '/var/www/'] },
|
||||
{ path: '/tmp', icon: '🗑️', brief: '临时文件', desc: '存放临时文件,系统重启后通常会被清空。所有用户都有写权限,适合存放不需要持久化的中间文件。', examples: ['编译中间文件', '下载缓存', '会话临时数据'] },
|
||||
{ path: '/usr', icon: '📦', brief: '用户程序', desc: '存放用户安装的程序、库和文档。可以理解为 "Unix System Resources",是最大的目录之一。', examples: ['/usr/bin/', '/usr/lib/', '/usr/local/', '/usr/share/'] },
|
||||
{ path: '/proc', icon: '🔍', brief: '进程信息', desc: '虚拟文件系统,不占磁盘空间。内核将进程和系统信息以文件形式暴露在这里,是监控和调试的重要数据源。', examples: ['/proc/cpuinfo', '/proc/meminfo', '/proc/[pid]/status'] },
|
||||
{ path: '/dev', icon: '🔌', brief: '设备文件', desc: '存放设备文件。Linux 中硬件设备也是文件,通过读写这些文件与硬件交互。', examples: ['/dev/sda', '/dev/null', '/dev/zero', '/dev/tty'] }
|
||||
]
|
||||
|
||||
const current = computed(() => dirs.find(d => d.path === activeDir.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.linux-fs-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.dir-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.dir-item:hover { border-color: var(--vp-c-divider); }
|
||||
.dir-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.05);
|
||||
}
|
||||
.dir-icon { font-size: 0.9rem; }
|
||||
.dir-path { font-weight: 700; font-family: var(--vp-font-family-mono); min-width: 60px; }
|
||||
.dir-brief { color: var(--vp-c-text-3); font-size: 0.78rem; }
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.detail-desc { font-size: 0.82rem; color: var(--vp-c-text-2); margin-bottom: 0.5rem; line-height: 1.6; }
|
||||
.examples { margin-top: 0.4rem; }
|
||||
.ex-label { font-size: 0.75rem; font-weight: 600; color: var(--vp-c-text-3); margin-bottom: 0.3rem; }
|
||||
.ex-list { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||
.ex-tag {
|
||||
font-size: 0.72rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<!--
|
||||
LinuxPermissionsDemo.vue
|
||||
Linux 权限系统演示
|
||||
-->
|
||||
<template>
|
||||
<div class="linux-perm-demo">
|
||||
<div class="header">
|
||||
<div class="title">Linux 权限解读器</div>
|
||||
<div class="subtitle">输入权限字符串或数字,查看含义</div>
|
||||
</div>
|
||||
|
||||
<div class="input-row">
|
||||
<div class="input-group">
|
||||
<label>权限数字(如 755)</label>
|
||||
<input v-model="permNum" type="text" maxlength="3" placeholder="755" @input="onNumInput" />
|
||||
</div>
|
||||
<div class="perm-string">{{ permString }}</div>
|
||||
</div>
|
||||
|
||||
<div class="perm-grid">
|
||||
<div v-for="(group, gi) in groups" :key="gi" class="perm-group">
|
||||
<div class="group-label">{{ group.label }}</div>
|
||||
<div class="bits">
|
||||
<label v-for="(bit, bi) in group.bits" :key="bi" class="bit-label">
|
||||
<input type="checkbox" v-model="bit.on" @change="onBitChange" />
|
||||
<span :class="['bit-char', bit.char]">{{ bit.char }}</span>
|
||||
<span class="bit-name">{{ bit.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="examples">
|
||||
<div class="ex-title">常见权限组合</div>
|
||||
<div class="ex-grid">
|
||||
<div v-for="ex in examples" :key="ex.num" class="ex-item" @click="setPermNum(ex.num)">
|
||||
<code>{{ ex.num }}</code>
|
||||
<span class="ex-desc">{{ ex.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
|
||||
const groups = reactive([
|
||||
{
|
||||
label: '所有者(Owner)',
|
||||
bits: [
|
||||
{ char: 'r', name: '读', on: true },
|
||||
{ char: 'w', name: '写', on: true },
|
||||
{ char: 'x', name: '执行', on: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '所属组(Group)',
|
||||
bits: [
|
||||
{ char: 'r', name: '读', on: true },
|
||||
{ char: 'w', name: '写', on: false },
|
||||
{ char: 'x', name: '执行', on: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '其他人(Others)',
|
||||
bits: [
|
||||
{ char: 'r', name: '读', on: true },
|
||||
{ char: 'w', name: '写', on: false },
|
||||
{ char: 'x', name: '执行', on: true }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const permNum = ref('755')
|
||||
|
||||
const permString = computed(() => {
|
||||
return '-' + groups.map(g =>
|
||||
g.bits.map(b => b.on ? b.char : '-').join('')
|
||||
).join('')
|
||||
})
|
||||
|
||||
function bitsToNum() {
|
||||
return groups.map(g => {
|
||||
let n = 0
|
||||
if (g.bits[0].on) n += 4
|
||||
if (g.bits[1].on) n += 2
|
||||
if (g.bits[2].on) n += 1
|
||||
return n
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function onBitChange() {
|
||||
permNum.value = bitsToNum()
|
||||
}
|
||||
|
||||
function onNumInput() {
|
||||
const s = permNum.value.replace(/[^0-7]/g, '').slice(0, 3)
|
||||
permNum.value = s
|
||||
if (s.length === 3) {
|
||||
s.split('').forEach((ch, gi) => {
|
||||
const n = parseInt(ch)
|
||||
groups[gi].bits[0].on = !!(n & 4)
|
||||
groups[gi].bits[1].on = !!(n & 2)
|
||||
groups[gi].bits[2].on = !!(n & 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setPermNum(num) {
|
||||
permNum.value = num
|
||||
onNumInput()
|
||||
}
|
||||
|
||||
const examples = [
|
||||
{ num: '644', desc: '普通文件(owner 读写,其他只读)' },
|
||||
{ num: '755', desc: '可执行文件/目录(owner 全权限)' },
|
||||
{ num: '600', desc: '私密文件(仅 owner 读写)' },
|
||||
{ num: '777', desc: '完全开放(不推荐)' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.linux-perm-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.input-group input {
|
||||
padding: 0.4rem 0.6rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 1.1rem;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-weight: 700;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.perm-string {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.perm-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.perm-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.perm-group {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.group-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.bits { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.bit-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bit-char {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-weight: 700;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.bit-char.r { color: #22c55e; }
|
||||
.bit-char.w { color: #f59e0b; }
|
||||
.bit-char.x { color: #ef4444; }
|
||||
.bit-name { color: var(--vp-c-text-3); font-size: 0.75rem; }
|
||||
.examples {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.ex-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.ex-grid { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
||||
.ex-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.ex-item:hover { background: var(--vp-c-bg-soft); }
|
||||
.ex-item code {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.ex-desc { color: var(--vp-c-text-3); }
|
||||
</style>
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
<!--
|
||||
ArchEvolutionDemo.vue
|
||||
架构演进演示:展示从单体到微服务的演进过程
|
||||
-->
|
||||
<template>
|
||||
<div class="arch-evolution-demo">
|
||||
<div class="header">
|
||||
<div class="title">架构演进路径</div>
|
||||
<div class="subtitle">点击查看每个阶段的架构特点</div>
|
||||
</div>
|
||||
|
||||
<div class="stages">
|
||||
<div
|
||||
v-for="(stage, i) in stages"
|
||||
:key="stage.key"
|
||||
:class="['stage-card', { active: activeStage === stage.key }]"
|
||||
@click="activeStage = stage.key"
|
||||
>
|
||||
<div class="stage-num">{{ i + 1 }}</div>
|
||||
<div class="stage-name">{{ stage.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="stage-detail">
|
||||
<div class="detail-name">{{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
|
||||
<div class="arch-visual">
|
||||
<div
|
||||
v-for="(box, i) in current.boxes"
|
||||
:key="i"
|
||||
:class="['arch-box', box.type]"
|
||||
>
|
||||
{{ box.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<span class="label">适用规模:</span>{{ current.scale }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">核心挑战:</span>{{ current.challenge }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeStage = ref('monolith')
|
||||
|
||||
const stages = [
|
||||
{
|
||||
key: 'monolith',
|
||||
name: '单体架构',
|
||||
desc: '所有功能打包在一个应用中,共享一个数据库。简单直接,适合早期快速迭代。',
|
||||
scale: '团队 < 10 人,日活 < 10 万',
|
||||
challenge: '代码耦合严重,一个模块的 Bug 可能拖垮整个系统',
|
||||
boxes: [
|
||||
{ label: '用户模块', type: 'module' },
|
||||
{ label: '订单模块', type: 'module' },
|
||||
{ label: '支付模块', type: 'module' },
|
||||
{ label: '商品模块', type: 'module' },
|
||||
{ label: '单体应用(一个进程)', type: 'container' },
|
||||
{ label: 'MySQL', type: 'db' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'modular',
|
||||
name: '模块化单体',
|
||||
desc: '在单体内部按业务域划分模块,模块间通过接口通信。是微服务的前置步骤。',
|
||||
scale: '团队 10-30 人',
|
||||
challenge: '模块边界容易被打破,需要纪律性',
|
||||
boxes: [
|
||||
{ label: '用户域', type: 'domain' },
|
||||
{ label: '订单域', type: 'domain' },
|
||||
{ label: '支付域', type: 'domain' },
|
||||
{ label: '内部 API 边界', type: 'boundary' },
|
||||
{ label: 'MySQL', type: 'db' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'soa',
|
||||
name: '服务化(SOA)',
|
||||
desc: '按业务能力拆分为独立服务,通过 ESB 或 API 网关通信。每个服务可以独立部署。',
|
||||
scale: '团队 30-100 人',
|
||||
challenge: '服务间调用链变长,需要服务治理',
|
||||
boxes: [
|
||||
{ label: '用户服务', type: 'service' },
|
||||
{ label: '订单服务', type: 'service' },
|
||||
{ label: '支付服务', type: 'service' },
|
||||
{ label: 'API 网关', type: 'gateway' },
|
||||
{ label: '各自数据库', type: 'db' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'microservices',
|
||||
name: '微服务架构',
|
||||
desc: '更细粒度的服务拆分,每个服务独立开发、部署、扩缩容。配合容器化和 K8s。',
|
||||
scale: '团队 100+ 人,日活百万+',
|
||||
challenge: '分布式复杂性、数据一致性、运维成本',
|
||||
boxes: [
|
||||
{ label: '用户服务', type: 'service' },
|
||||
{ label: '认证服务', type: 'service' },
|
||||
{ label: '订单服务', type: 'service' },
|
||||
{ label: '库存服务', type: 'service' },
|
||||
{ label: '支付服务', type: 'service' },
|
||||
{ label: '通知服务', type: 'service' },
|
||||
{ label: 'API Gateway + Service Mesh', type: 'gateway' },
|
||||
{ label: '独立数据库 x N', type: 'db' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => stages.find(s => s.key === activeStage.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.arch-evolution-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.stages { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.stage-card {
|
||||
display: flex; align-items: center; gap: 0.4rem;
|
||||
padding: 0.4rem 0.75rem; border-radius: 6px; cursor: pointer;
|
||||
font-size: 0.85rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.stage-card:hover { border-color: var(--vp-c-brand); }
|
||||
.stage-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); font-weight: 600; }
|
||||
.stage-num {
|
||||
width: 20px; height: 20px; border-radius: 50%; background: var(--vp-c-brand);
|
||||
color: white; font-size: 0.7rem; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.stage-detail { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); }
|
||||
.detail-name { font-weight: 700; font-size: 0.95rem; }
|
||||
.detail-desc { color: var(--vp-c-text-2); font-size: 0.82rem; margin-bottom: 0.75rem; }
|
||||
.arch-visual { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.75rem; }
|
||||
.arch-box {
|
||||
padding: 0.35rem 0.6rem; border-radius: 4px; font-size: 0.72rem; font-weight: 600;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
.arch-box.module { background: rgba(var(--vp-c-brand-rgb), 0.06); }
|
||||
.arch-box.domain { background: rgba(99,102,241,0.06); }
|
||||
.arch-box.service { background: rgba(34,197,94,0.06); }
|
||||
.arch-box.gateway { background: rgba(245,158,11,0.06); width: 100%; text-align: center; }
|
||||
.arch-box.container { background: rgba(239,68,68,0.06); width: 100%; text-align: center; }
|
||||
.arch-box.boundary { background: rgba(156,163,175,0.06); width: 100%; text-align: center; }
|
||||
.arch-box.db { background: rgba(139,92,246,0.06); }
|
||||
.detail-row { font-size: 0.82rem; margin-bottom: 0.25rem; }
|
||||
.label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
</style>
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
<!--
|
||||
NetworkArchitectureDemo.vue
|
||||
神经网络架构对比演示
|
||||
-->
|
||||
<template>
|
||||
<div class="net-arch-demo">
|
||||
<div class="header">
|
||||
<div class="title">常见神经网络架构</div>
|
||||
<div class="subtitle">点击查看不同网络架构的特点和应用</div>
|
||||
</div>
|
||||
|
||||
<div class="arch-tabs">
|
||||
<button
|
||||
v-for="arch in architectures"
|
||||
:key="arch.key"
|
||||
:class="['arch-btn', { active: activeArch === arch.key }]"
|
||||
@click="activeArch = arch.key"
|
||||
>
|
||||
{{ arch.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<div class="detail-title">{{ current.name }}({{ current.abbr }})</div>
|
||||
<div class="detail-year">{{ current.year }}</div>
|
||||
</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
|
||||
<div class="structure">
|
||||
<div class="struct-label">网络结构</div>
|
||||
<div class="struct-visual">
|
||||
<span v-for="(layer, i) in current.layers" :key="i" class="layer-tag">
|
||||
{{ layer }}
|
||||
<span v-if="i < current.layers.length - 1" class="layer-arrow">→</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="apps">
|
||||
<div class="apps-label">典型应用</div>
|
||||
<div class="apps-list">
|
||||
<span v-for="(app, i) in current.applications" :key="i" class="app-tag">{{ app }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-idea">
|
||||
<span class="idea-label">核心思想:</span>{{ current.keyIdea }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeArch = ref('ffn')
|
||||
|
||||
const architectures = [
|
||||
{
|
||||
key: 'ffn',
|
||||
name: '前馈神经网络',
|
||||
abbr: 'FNN',
|
||||
year: '1958',
|
||||
desc: '最基础的神经网络结构,数据从输入层经过隐藏层到输出层,单向流动,没有循环。每一层的每个神经元与下一层的所有神经元相连(全连接)。',
|
||||
layers: ['输入层', '隐藏层 ×N', '输出层'],
|
||||
applications: ['分类', '回归', '函数逼近'],
|
||||
keyIdea: '通过多层非线性变换,将输入映射到输出。层数越多,能表达的函数越复杂。'
|
||||
},
|
||||
{
|
||||
key: 'cnn',
|
||||
name: '卷积神经网络',
|
||||
abbr: 'CNN',
|
||||
year: '1998',
|
||||
desc: '专为处理网格状数据(如图像)设计。通过卷积核在输入上滑动提取局部特征,池化层降低维度,最后全连接层做分类。参数共享大幅减少了参数量。',
|
||||
layers: ['输入', '卷积层', '池化层', '...', '全连接层', '输出'],
|
||||
applications: ['图像分类', '目标检测', '人脸识别', '医学影像'],
|
||||
keyIdea: '局部感受野 + 参数共享。卷积核只关注局部区域,同一个卷积核在整张图上共享参数。'
|
||||
},
|
||||
{
|
||||
key: 'rnn',
|
||||
name: '循环神经网络',
|
||||
abbr: 'RNN/LSTM',
|
||||
year: '1997',
|
||||
desc: '专为处理序列数据设计。隐藏状态会传递到下一个时间步,让网络具有"记忆"能力。LSTM 通过门控机制解决了长序列中的梯度消失问题。',
|
||||
layers: ['输入序列', '循环层(含记忆)', '...', '输出序列'],
|
||||
applications: ['机器翻译', '语音识别', '时间序列预测', '文本生成'],
|
||||
keyIdea: '引入时间维度的循环连接,让网络能处理变长序列并保持上下文记忆。'
|
||||
},
|
||||
{
|
||||
key: 'transformer',
|
||||
name: 'Transformer',
|
||||
abbr: 'Transformer',
|
||||
year: '2017',
|
||||
desc: '用自注意力机制替代循环结构,可以并行处理整个序列。每个位置都能直接关注序列中的任意其他位置,解决了 RNN 的长距离依赖问题。是 GPT、BERT 等大模型的基础。',
|
||||
layers: ['输入嵌入', '位置编码', '多头注意力', '前馈网络', '...×N', '输出'],
|
||||
applications: ['ChatGPT', 'BERT', '机器翻译', '代码生成', '图像生成'],
|
||||
keyIdea: '自注意力(Self-Attention):让序列中的每个元素都能"看到"其他所有元素,计算相关性权重。'
|
||||
},
|
||||
{
|
||||
key: 'gan',
|
||||
name: '生成对抗网络',
|
||||
abbr: 'GAN',
|
||||
year: '2014',
|
||||
desc: '由生成器和判别器两个网络对抗训练。生成器试图生成以假乱真的数据,判别器试图区分真假。两者博弈的结果是生成器越来越强。',
|
||||
layers: ['随机噪声', '生成器', '生成数据', '判别器', '真/假'],
|
||||
applications: ['图像生成', '风格迁移', '超分辨率', '数据增强'],
|
||||
keyIdea: '对抗训练:生成器和判别器互相博弈,共同进步,最终生成器能产生逼真的数据。'
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => architectures.find(a => a.key === activeArch.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.net-arch-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.arch-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.arch-btn {
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.arch-btn:hover { border-color: var(--vp-c-brand); }
|
||||
.arch-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.detail-title { font-weight: 700; font-size: 0.95rem; }
|
||||
.detail-year {
|
||||
font-size: 0.72rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.1);
|
||||
color: var(--vp-c-brand);
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.detail-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.structure, .apps {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.struct-label, .apps-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.struct-visual {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.layer-tag {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.layer-arrow {
|
||||
color: var(--vp-c-text-3);
|
||||
margin: 0 0.1rem;
|
||||
}
|
||||
.apps-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.app-tag {
|
||||
font-size: 0.72rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.key-idea {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.idea-label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!--
|
||||
NetworkLayersDemo.vue
|
||||
神经网络层类型交互演示
|
||||
-->
|
||||
<template>
|
||||
<div class="layers-demo">
|
||||
<div class="header">
|
||||
<div class="title">神经网络常见层类型</div>
|
||||
<div class="subtitle">点击查看各层的作用和参数</div>
|
||||
</div>
|
||||
|
||||
<div class="layer-tabs">
|
||||
<button v-for="l in layers" :key="l.key"
|
||||
:class="['tab-btn', { active: activeLayer === l.key }]"
|
||||
@click="activeLayer = activeLayer === l.key ? null : l.key">
|
||||
{{ l.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="layer-detail">
|
||||
<div class="detail-name">{{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="detail-section">
|
||||
<span class="section-label">核心参数:</span>
|
||||
<code v-for="(p, i) in current.params" :key="i" class="param-tag">{{ p }}</code>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<span class="section-label">典型用途:</span>
|
||||
<span class="usage-text">{{ current.usage }}</span>
|
||||
</div>
|
||||
<div class="detail-code">
|
||||
<code>{{ current.code }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeLayer = ref('dense')
|
||||
|
||||
const layers = [
|
||||
{
|
||||
key: 'dense',
|
||||
name: '全连接层',
|
||||
desc: '每个神经元与上一层所有神经元相连。最基础的层类型,用于学习输入特征的组合。',
|
||||
params: ['units(神经元数)', 'activation(激活函数)'],
|
||||
usage: '分类、回归任务的输出层,以及简单特征提取',
|
||||
code: 'Dense(128, activation="relu")'
|
||||
},
|
||||
{
|
||||
key: 'conv',
|
||||
name: '卷积层',
|
||||
desc: '用滑动窗口(卷积核)扫描输入,提取局部特征。参数共享大幅减少参数量,是图像处理的核心。',
|
||||
params: ['filters(卷积核数)', 'kernel_size(核大小)', 'stride(步长)'],
|
||||
usage: '图像分类、目标检测、图像分割',
|
||||
code: 'Conv2D(64, kernel_size=3, stride=1, padding=1)'
|
||||
},
|
||||
{
|
||||
key: 'rnn',
|
||||
name: '循环层',
|
||||
desc: '具有"记忆"能力,能处理序列数据。每个时间步的输出会作为下一步的输入,形成循环。',
|
||||
params: ['hidden_size(隐藏维度)', 'num_layers(层数)'],
|
||||
usage: '文本生成、语音识别、时间序列预测',
|
||||
code: 'LSTM(hidden_size=256, num_layers=2)'
|
||||
},
|
||||
{
|
||||
key: 'attention',
|
||||
name: '注意力层',
|
||||
desc: '让模型学会"关注"输入中最重要的部分。Transformer 的核心,彻底改变了 NLP 领域。',
|
||||
params: ['embed_dim(嵌入维度)', 'num_heads(注意力头数)'],
|
||||
usage: 'GPT、BERT 等大语言模型,机器翻译',
|
||||
code: 'MultiHeadAttention(embed_dim=512, num_heads=8)'
|
||||
},
|
||||
{
|
||||
key: 'norm',
|
||||
name: '归一化层',
|
||||
desc: '将数据标准化到合理范围,加速训练收敛,缓解梯度消失/爆炸问题。',
|
||||
params: ['num_features(特征数)'],
|
||||
usage: '几乎所有深度网络中都会使用,通常跟在卷积或全连接层后面',
|
||||
code: 'BatchNorm2d(64) / LayerNorm(512)'
|
||||
},
|
||||
{
|
||||
key: 'dropout',
|
||||
name: 'Dropout 层',
|
||||
desc: '训练时随机"关闭"一部分神经元,防止网络过度依赖某些特征,是最常用的正则化手段。',
|
||||
params: ['p(丢弃概率,通常 0.1~0.5)'],
|
||||
usage: '防止过拟合,提升模型泛化能力',
|
||||
code: 'Dropout(p=0.3)'
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => layers.find(l => l.key === activeLayer.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layers-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.layer-tabs { display: flex; gap: 0.4rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.tab-btn {
|
||||
padding: 0.35rem 0.7rem; border-radius: 6px; cursor: pointer;
|
||||
font-size: 0.8rem; font-weight: 600; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider); transition: all 0.2s;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.tab-btn:hover { border-color: var(--vp-c-brand); }
|
||||
.tab-btn.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); color: var(--vp-c-text-1); }
|
||||
.layer-detail {
|
||||
background: var(--vp-c-bg); border-radius: 8px; padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-name { font-weight: 700; font-size: 0.95rem; color: var(--vp-c-brand); margin-bottom: 0.3rem; }
|
||||
.detail-desc { font-size: 0.82rem; color: var(--vp-c-text-2); margin-bottom: 0.6rem; line-height: 1.5; }
|
||||
.detail-section { font-size: 0.8rem; margin-bottom: 0.4rem; display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; }
|
||||
.section-label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
.param-tag {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.08); padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px; font-size: 0.72rem;
|
||||
}
|
||||
.usage-text { color: var(--vp-c-text-2); }
|
||||
.detail-code {
|
||||
margin-top: 0.5rem; padding: 0.5rem 0.7rem; background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px; font-family: var(--vp-font-family-mono); font-size: 0.75rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<!--
|
||||
NeuronDemo.vue
|
||||
神经元结构演示:展示单个神经元的工作原理
|
||||
-->
|
||||
<template>
|
||||
<div class="neuron-demo">
|
||||
<div class="header">
|
||||
<div class="title">神经元工作原理</div>
|
||||
<div class="subtitle">调整输入和权重,观察神经元的输出变化</div>
|
||||
</div>
|
||||
|
||||
<div class="neuron-layout">
|
||||
<div class="inputs-col">
|
||||
<div class="col-label">输入 × 权重</div>
|
||||
<div v-for="(inp, i) in inputs" :key="i" class="input-row">
|
||||
<div class="input-pair">
|
||||
<label>x{{ i + 1 }}</label>
|
||||
<input v-model.number="inp.value" type="range" min="-1" max="1" step="0.1" />
|
||||
<span class="val">{{ inp.value.toFixed(1) }}</span>
|
||||
</div>
|
||||
<span class="multiply">×</span>
|
||||
<div class="input-pair">
|
||||
<label>w{{ i + 1 }}</label>
|
||||
<input v-model.number="inp.weight" type="range" min="-2" max="2" step="0.1" />
|
||||
<span class="val">{{ inp.weight.toFixed(1) }}</span>
|
||||
</div>
|
||||
<span class="equals">=</span>
|
||||
<span class="partial">{{ (inp.value * inp.weight).toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="output-col">
|
||||
<div class="sum-box">
|
||||
<div class="sum-label">加权求和 + 偏置({{ bias.toFixed(1) }})</div>
|
||||
<div class="sum-value">{{ weightedSum.toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="arrow">↓</div>
|
||||
<div class="activation-box">
|
||||
<div class="act-label">激活函数: {{ activationName }}</div>
|
||||
<div class="act-value">{{ activationOutput.toFixed(4) }}</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="control-row">
|
||||
<label>偏置 b</label>
|
||||
<input v-model.number="bias" type="range" min="-2" max="2" step="0.1" />
|
||||
<span class="val">{{ bias.toFixed(1) }}</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<label>激活函数</label>
|
||||
<select v-model="activation">
|
||||
<option value="sigmoid">Sigmoid</option>
|
||||
<option value="relu">ReLU</option>
|
||||
<option value="tanh">Tanh</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
|
||||
const inputs = reactive([
|
||||
{ value: 0.5, weight: 0.8 },
|
||||
{ value: -0.3, weight: 1.2 },
|
||||
{ value: 0.7, weight: -0.5 }
|
||||
])
|
||||
|
||||
const bias = ref(0.1)
|
||||
const activation = ref('sigmoid')
|
||||
|
||||
const activationName = computed(() => {
|
||||
const names = { sigmoid: 'Sigmoid', relu: 'ReLU', tanh: 'Tanh' }
|
||||
return names[activation.value]
|
||||
})
|
||||
|
||||
const weightedSum = computed(() => {
|
||||
return inputs.reduce((sum, inp) => sum + inp.value * inp.weight, 0) + bias.value
|
||||
})
|
||||
|
||||
const activationOutput = computed(() => {
|
||||
const z = weightedSum.value
|
||||
switch (activation.value) {
|
||||
case 'sigmoid': return 1 / (1 + Math.exp(-z))
|
||||
case 'relu': return Math.max(0, z)
|
||||
case 'tanh': return Math.tanh(z)
|
||||
default: return z
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.neuron-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.neuron-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.neuron-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
.col-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.input-pair {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.input-pair label {
|
||||
font-weight: 600;
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
min-width: 18px;
|
||||
}
|
||||
.input-pair input[type="range"] { width: 60px; }
|
||||
.val {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.72rem;
|
||||
min-width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
.multiply, .equals {
|
||||
color: var(--vp-c-text-3);
|
||||
font-weight: 600;
|
||||
}
|
||||
.partial {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
.output-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.sum-box, .activation-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.8rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.sum-label, .act-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.sum-value, .act-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.arrow { color: var(--vp-c-text-3); font-size: 1.2rem; }
|
||||
.controls { width: 100%; margin-top: 0.5rem; }
|
||||
.control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.control-row label {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 55px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.control-row select {
|
||||
padding: 0.2rem 0.4rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.78rem;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
<!--
|
||||
CapacityEstimationDemo.vue
|
||||
容量估算计算器:信封背面估算交互演示
|
||||
-->
|
||||
<template>
|
||||
<div class="capacity-demo">
|
||||
<div class="header">
|
||||
<div class="title">信封背面估算器</div>
|
||||
<div class="subtitle">输入基础数据,自动计算系统容量需求</div>
|
||||
</div>
|
||||
|
||||
<div class="inputs">
|
||||
<div class="input-group">
|
||||
<label>日活用户(万)</label>
|
||||
<input v-model.number="dau" type="number" min="1" max="100000" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>人均请求数/天</label>
|
||||
<input v-model.number="reqPerUser" type="number" min="1" max="1000" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>单次响应大小(KB)</label>
|
||||
<input v-model.number="responseSize" type="number" min="0.1" max="1000" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>峰值系数</label>
|
||||
<input v-model.number="peakFactor" type="number" min="1" max="10" step="0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results">
|
||||
<div class="result-card">
|
||||
<div class="result-label">日请求量</div>
|
||||
<div class="result-value">{{ formatNumber(dailyRequests) }}</div>
|
||||
</div>
|
||||
<div class="result-card">
|
||||
<div class="result-label">平均 QPS</div>
|
||||
<div class="result-value">{{ formatNumber(avgQps) }}</div>
|
||||
</div>
|
||||
<div class="result-card">
|
||||
<div class="result-label">峰值 QPS</div>
|
||||
<div class="result-value">{{ formatNumber(peakQps) }}</div>
|
||||
</div>
|
||||
<div class="result-card">
|
||||
<div class="result-label">日带宽</div>
|
||||
<div class="result-value">{{ formatBandwidth(dailyBandwidth) }}</div>
|
||||
</div>
|
||||
<div class="result-card">
|
||||
<div class="result-label">峰值带宽</div>
|
||||
<div class="result-value">{{ formatBandwidth(peakBandwidthPerSec) }}/s</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reference">
|
||||
<div class="ref-title">常用估算参考值</div>
|
||||
<div class="ref-grid">
|
||||
<div class="ref-item" v-for="r in references" :key="r.label">
|
||||
<span class="ref-label">{{ r.label }}</span>
|
||||
<span class="ref-value">{{ r.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const dau = ref(100)
|
||||
const reqPerUser = ref(20)
|
||||
const responseSize = ref(5)
|
||||
const peakFactor = ref(3)
|
||||
|
||||
const dailyRequests = computed(() => dau.value * 10000 * reqPerUser.value)
|
||||
const avgQps = computed(() => Math.round(dailyRequests.value / 86400))
|
||||
const peakQps = computed(() => Math.round(avgQps.value * peakFactor.value))
|
||||
const dailyBandwidth = computed(() => dailyRequests.value * responseSize.value * 1024)
|
||||
const peakBandwidthPerSec = computed(() => peakQps.value * responseSize.value * 1024)
|
||||
|
||||
const references = [
|
||||
{ label: '1 天', value: '86,400 秒' },
|
||||
{ label: '1 月', value: '≈ 250 万秒' },
|
||||
{ label: 'QPS 1000', value: '≈ 1 台 8 核服务器' },
|
||||
{ label: '1 亿/天', value: '≈ 1,200 QPS' },
|
||||
{ label: 'MySQL 单机', value: '≈ 5,000 QPS' },
|
||||
{ label: 'Redis 单机', value: '≈ 100,000 QPS' }
|
||||
]
|
||||
|
||||
function formatNumber(n) {
|
||||
if (n >= 1e8) return (n / 1e8).toFixed(1) + ' 亿'
|
||||
if (n >= 1e4) return (n / 1e4).toFixed(1) + ' 万'
|
||||
return n.toLocaleString()
|
||||
}
|
||||
|
||||
function formatBandwidth(bytes) {
|
||||
if (bytes >= 1e12) return (bytes / 1e12).toFixed(1) + ' TB'
|
||||
if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + ' GB'
|
||||
if (bytes >= 1e6) return (bytes / 1e6).toFixed(1) + ' MB'
|
||||
if (bytes >= 1e3) return (bytes / 1e3).toFixed(1) + ' KB'
|
||||
return bytes + ' B'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.capacity-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.results {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.result-card {
|
||||
padding: 0.6rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.result-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.result-value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.reference {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.ref-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.ref-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.ref-item {
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
.ref-label { color: var(--vp-c-text-2); }
|
||||
.ref-value { font-weight: 600; font-family: var(--vp-font-family-mono); }
|
||||
</style>
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
<!--
|
||||
SystemDesignStepsDemo.vue
|
||||
系统设计步骤交互演示:展示系统设计面试/实战的标准流程
|
||||
-->
|
||||
<template>
|
||||
<div class="design-steps-demo">
|
||||
<div class="header">
|
||||
<div class="title">系统设计四步法</div>
|
||||
<div class="subtitle">点击每个步骤查看详细内容</div>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<div
|
||||
v-for="(step, i) in steps"
|
||||
:key="step.key"
|
||||
:class="['step-card', { active: activeStep === step.key }]"
|
||||
@click="activeStep = step.key"
|
||||
>
|
||||
<div class="step-number">{{ i + 1 }}</div>
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-time">{{ step.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-panel">
|
||||
<div class="detail-title">{{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="checklist">
|
||||
<div v-for="(item, i) in current.checklist" :key="i" class="check-item">
|
||||
<span class="check-icon">✓</span>
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-example">
|
||||
<span class="label">示例(设计短链服务):</span>
|
||||
<div class="example-text">{{ current.example }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeStep = ref('requirements')
|
||||
|
||||
const steps = [
|
||||
{
|
||||
key: 'requirements',
|
||||
name: '需求澄清',
|
||||
time: '~5 分钟',
|
||||
desc: '不要急着画架构图。先搞清楚:系统要解决什么问题?用户规模多大?有哪些核心功能?有哪些非功能需求?',
|
||||
checklist: [
|
||||
'核心功能有哪些?(MVP 范围)',
|
||||
'用户规模?DAU / QPS 预估',
|
||||
'读写比例?读多写少还是写多读少?',
|
||||
'数据量级?需要存多少数据?',
|
||||
'可用性要求?几个 9?',
|
||||
'延迟要求?P99 要多少毫秒?'
|
||||
],
|
||||
example: '短链服务:生成短链(写)+ 重定向(读),读写比约 100:1,日均 1 亿次重定向,短链永不过期。'
|
||||
},
|
||||
{
|
||||
key: 'estimation',
|
||||
name: '容量估算',
|
||||
time: '~5 分钟',
|
||||
desc: '用"信封背面估算"(Back-of-envelope estimation)快速计算系统需要的资源量级,为后续架构决策提供数据支撑。',
|
||||
checklist: [
|
||||
'QPS 估算:日请求量 / 86400',
|
||||
'存储估算:单条数据大小 × 总量',
|
||||
'带宽估算:QPS × 单次响应大小',
|
||||
'缓存估算:热点数据量(通常 20% 数据承载 80% 请求)',
|
||||
'峰值估算:平均 QPS × 峰值系数(通常 2-5 倍)'
|
||||
],
|
||||
example: '1 亿次/天 ≈ 1200 QPS,峰值 ≈ 3600 QPS。每条短链 100 字节,5 年 = 1.8 亿条 ≈ 18GB。缓存热点 20% ≈ 3.6GB,一台 Redis 足够。'
|
||||
},
|
||||
{
|
||||
key: 'design',
|
||||
name: '架构设计',
|
||||
time: '~15 分钟',
|
||||
desc: '画出核心组件和数据流。先画最简单的版本(单机),再根据需求逐步演进(加缓存、分库分表、CDN 等)。',
|
||||
checklist: [
|
||||
'API 设计:定义核心接口的输入输出',
|
||||
'数据模型:设计核心表结构',
|
||||
'核心组件:Web 服务、数据库、缓存、消息队列',
|
||||
'数据流:请求从用户到数据库的完整路径',
|
||||
'读写分离:读路径和写路径分开考虑'
|
||||
],
|
||||
example: '写路径:客户端 → API → 生成短码(Base62) → 写入 MySQL + Redis。读路径:客户端 → CDN → API → Redis 查询 → 302 重定向。'
|
||||
},
|
||||
{
|
||||
key: 'deep-dive',
|
||||
name: '深入优化',
|
||||
time: '~10 分钟',
|
||||
desc: '针对系统的瓶颈和关键问题进行深入讨论。这是展示技术深度的环节。',
|
||||
checklist: [
|
||||
'如何保证短码唯一性?(哈希冲突处理)',
|
||||
'如何应对热点?(缓存、CDN)',
|
||||
'如何水平扩展?(分库分表策略)',
|
||||
'如何保证高可用?(主备、多可用区)',
|
||||
'如何监控和告警?(关键指标)',
|
||||
'安全考虑?(防刷、恶意链接检测)'
|
||||
],
|
||||
example: '短码生成:用分布式 ID 生成器(Snowflake)+ Base62 编码,避免哈希冲突。热点短链用多级缓存(本地缓存 + Redis + CDN)。'
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => steps.find(s => s.key === activeStep.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.design-steps-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: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.steps { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
.step-card {
|
||||
padding: 0.6rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.step-card:hover { border-color: var(--vp-c-brand); }
|
||||
.step-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.05);
|
||||
}
|
||||
.step-number {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.step-name { font-weight: 600; font-size: 0.85rem; }
|
||||
.step-time {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.detail-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.checklist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.check-item {
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.check-icon {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.detail-example {
|
||||
font-size: 0.82rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.example-text {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
</style>
|
||||
+136
-44
@@ -206,15 +206,15 @@ import LearningStrategyDemo from './components/appendix/computer-fundamentals/Le
|
||||
import VibeCodingFlowDemo from './components/appendix/computer-fundamentals/VibeCodingFlowDemo.vue'
|
||||
import PowerOnDemo from './components/appendix/computer-fundamentals/PowerOnDemo.vue'
|
||||
import BootProcessDemo from './components/appendix/computer-fundamentals/BootProcessDemo.vue'
|
||||
import BiosUefiDemo from './components/appendix/computer-fundamentals/BiosUefiDemo.vue'
|
||||
import BiosUefiInteractiveDemo from './components/appendix/computer-fundamentals/BiosUefiInteractiveDemo.vue'
|
||||
import AppLaunchDemo from './components/appendix/computer-fundamentals/AppLaunchDemo.vue'
|
||||
import DesktopDemo from './components/appendix/computer-fundamentals/DesktopDemo.vue'
|
||||
import OSBootInteractiveDemo from './components/appendix/computer-fundamentals/OSBootInteractiveDemo.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'
|
||||
// import BiosUefiDemo from './components/appendix/computer-fundamentals/BiosUefiDemo.vue'
|
||||
// import BiosUefiInteractiveDemo from './components/appendix/computer-fundamentals/BiosUefiInteractiveDemo.vue'
|
||||
// import AppLaunchDemo from './components/appendix/computer-fundamentals/AppLaunchDemo.vue'
|
||||
// import DesktopDemo from './components/appendix/computer-fundamentals/DesktopDemo.vue'
|
||||
// import OSBootInteractiveDemo from './components/appendix/computer-fundamentals/OSBootInteractiveDemo.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'
|
||||
@@ -762,27 +762,73 @@ import IncidentCommandDemo from './components/appendix/incident-response/Inciden
|
||||
import AlertEscalationDemo from './components/appendix/incident-response/AlertEscalationDemo.vue'
|
||||
import PostmortemDemo from './components/appendix/incident-response/PostmortemDemo.vue'
|
||||
|
||||
// Async Task Queues Components
|
||||
import AsyncTaskFlowDemo from './components/appendix/async-task-queues/AsyncTaskFlowDemo.vue'
|
||||
import TaskWorkerDemo from './components/appendix/async-task-queues/TaskWorkerDemo.vue'
|
||||
import TaskRetryDemo from './components/appendix/async-task-queues/TaskRetryDemo.vue'
|
||||
import AsyncComparisonDemo from './components/appendix/async-task-queues/AsyncComparisonDemo.vue'
|
||||
// // Async Task Queues Components
|
||||
// import AsyncTaskFlowDemo from './components/appendix/async-task-queues/AsyncTaskFlowDemo.vue'
|
||||
// import TaskWorkerDemo from './components/appendix/async-task-queues/TaskWorkerDemo.vue'
|
||||
// import TaskRetryDemo from './components/appendix/async-task-queues/TaskRetryDemo.vue'
|
||||
// import AsyncComparisonDemo from './components/appendix/async-task-queues/AsyncComparisonDemo.vue'
|
||||
|
||||
// File Storage Components
|
||||
import FileStorageTypeDemo from './components/appendix/file-storage/FileStorageTypeDemo.vue'
|
||||
import FileUploadFlowDemo from './components/appendix/file-storage/FileUploadFlowDemo.vue'
|
||||
import CDNAccelerationDemo from './components/appendix/file-storage/CDNAccelerationDemo.vue'
|
||||
// // File Storage Components
|
||||
// import FileStorageTypeDemo from './components/appendix/file-storage/FileStorageTypeDemo.vue'
|
||||
// import FileUploadFlowDemo from './components/appendix/file-storage/FileUploadFlowDemo.vue'
|
||||
// import CDNAccelerationDemo from './components/appendix/file-storage/CDNAccelerationDemo.vue'
|
||||
|
||||
// Rate Limiting Components
|
||||
import RateLimitAlgorithmDemo from './components/appendix/rate-limiting/RateLimitAlgorithmDemo.vue'
|
||||
import BackpressureDemo from './components/appendix/rate-limiting/BackpressureDemo.vue'
|
||||
// // Rate Limiting Components
|
||||
// import RateLimitAlgorithmDemo from './components/appendix/rate-limiting/RateLimitAlgorithmDemo.vue'
|
||||
// import BackpressureDemo from './components/appendix/rate-limiting/BackpressureDemo.vue'
|
||||
|
||||
// Search Engines Components Registration
|
||||
import InvertedIndexDemo from './components/appendix/search-engines/InvertedIndexDemo.vue'
|
||||
import SearchRelevanceDemo from './components/appendix/search-engines/SearchRelevanceDemo.vue'
|
||||
|
||||
// Monolith to Microservices Components
|
||||
import ArchEvolutionDemo from './components/appendix/monolith-to-microservices/ArchEvolutionDemo.vue'
|
||||
|
||||
// High Availability Components
|
||||
import AvailabilityCalculatorDemo from './components/appendix/high-availability/AvailabilityCalculatorDemo.vue'
|
||||
import FailoverStrategyDemo from './components/appendix/high-availability/FailoverStrategyDemo.vue'
|
||||
|
||||
// Distributed Systems Components
|
||||
import CAPTheoremDemo from './components/appendix/distributed-systems/CAPTheoremDemo.vue'
|
||||
import ConsistencyModelsDemo from './components/appendix/distributed-systems/ConsistencyModelsDemo.vue'
|
||||
import DistributedChallengesDemo from './components/appendix/distributed-systems/DistributedChallengesDemo.vue'
|
||||
|
||||
// System Design Methodology Components
|
||||
import SystemDesignStepsDemo from './components/appendix/system-design-methodology/SystemDesignStepsDemo.vue'
|
||||
import CapacityEstimationDemo from './components/appendix/system-design-methodology/CapacityEstimationDemo.vue'
|
||||
|
||||
// Data Visualization Components
|
||||
import ChartTypeSelectorDemo from './components/appendix/data-visualization/ChartTypeSelectorDemo.vue'
|
||||
import DashboardLayoutDemo from './components/appendix/data-visualization/DashboardLayoutDemo.vue'
|
||||
|
||||
// Data Governance Components
|
||||
import DataQualityDemo from './components/appendix/data-governance/DataQualityDemo.vue'
|
||||
import DataGovernanceFrameworkDemo from './components/appendix/data-governance/DataGovernanceFrameworkDemo.vue'
|
||||
import DataLineageDemo from './components/appendix/data-governance/DataLineageDemo.vue'
|
||||
|
||||
// Linux Basics Components
|
||||
import LinuxFileSystemDemo from './components/appendix/linux-basics/LinuxFileSystemDemo.vue'
|
||||
import LinuxCommandDemo from './components/appendix/linux-basics/LinuxCommandDemo.vue'
|
||||
import LinuxPermissionsDemo from './components/appendix/linux-basics/LinuxPermissionsDemo.vue'
|
||||
|
||||
// Docker Containers Components
|
||||
import DockerArchitectureDemo from './components/appendix/docker-containers/DockerArchitectureDemo.vue'
|
||||
import DockerLifecycleDemo from './components/appendix/docker-containers/DockerLifecycleDemo.vue'
|
||||
|
||||
// Kubernetes Components
|
||||
import K8sArchitectureDemo from './components/appendix/kubernetes/K8sArchitectureDemo.vue'
|
||||
import K8sWorkloadsDemo from './components/appendix/kubernetes/K8sWorkloadsDemo.vue'
|
||||
|
||||
// Neural Networks Components
|
||||
import NeuronDemo from './components/appendix/neural-networks/NeuronDemo.vue'
|
||||
import NetworkLayersDemo from './components/appendix/neural-networks/NetworkLayersDemo.vue'
|
||||
import NetworkArchitectureDemo from './components/appendix/neural-networks/NetworkArchitectureDemo.vue'
|
||||
|
||||
// Project Architecture Components
|
||||
import ArchitectureComparisonDemo from './components/appendix/project-architecture/ArchitectureComparisonDemo.vue'
|
||||
import ProjectArchitectureComparisonDemo from './components/appendix/project-architecture/ArchitectureComparisonDemo.vue'
|
||||
|
||||
// Appendix Navigation Component
|
||||
import AppendixFlowMap from './components/AppendixFlowMap.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
@@ -995,15 +1041,15 @@ export default {
|
||||
app.component('VibeCodingFlowDemo', VibeCodingFlowDemo)
|
||||
app.component('PowerOnDemo', PowerOnDemo)
|
||||
app.component('BootProcessDemo', BootProcessDemo)
|
||||
app.component('BiosUefiDemo', BiosUefiDemo)
|
||||
app.component('BiosUefiInteractiveDemo', BiosUefiInteractiveDemo)
|
||||
app.component('AppLaunchDemo', AppLaunchDemo)
|
||||
app.component('DesktopDemo', DesktopDemo)
|
||||
app.component('OSBootInteractiveDemo', OSBootInteractiveDemo)
|
||||
app.component('BrowserArchitectureDemo', BrowserArchitectureDemo)
|
||||
app.component('URLRequestDemo', URLRequestDemo)
|
||||
app.component('RenderingDemo', RenderingDemo)
|
||||
app.component('FullProcessDemo', FullProcessDemo)
|
||||
// app.component('BiosUefiDemo', BiosUefiDemo)
|
||||
// app.component('BiosUefiInteractiveDemo', BiosUefiInteractiveDemo)
|
||||
// app.component('AppLaunchDemo', AppLaunchDemo)
|
||||
// app.component('DesktopDemo', DesktopDemo)
|
||||
// app.component('OSBootInteractiveDemo', OSBootInteractiveDemo)
|
||||
// app.component('BrowserArchitectureDemo', BrowserArchitectureDemo)
|
||||
// app.component('URLRequestDemo', URLRequestDemo)
|
||||
// app.component('RenderingDemo', RenderingDemo)
|
||||
// app.component('FullProcessDemo', FullProcessDemo)
|
||||
|
||||
// Data Encoding Components Registration
|
||||
app.component('GarbledTextDemo', GarbledTextDemo)
|
||||
@@ -1564,27 +1610,73 @@ export default {
|
||||
app.component('AlertEscalationDemo', AlertEscalationDemo)
|
||||
app.component('PostmortemDemo', PostmortemDemo)
|
||||
|
||||
// Async Task Queues Components Registration
|
||||
app.component('AsyncTaskFlowDemo', AsyncTaskFlowDemo)
|
||||
app.component('TaskWorkerDemo', TaskWorkerDemo)
|
||||
app.component('TaskRetryDemo', TaskRetryDemo)
|
||||
app.component('AsyncComparisonDemo', AsyncComparisonDemo)
|
||||
// // Async Task Queues Components Registration
|
||||
// app.component('AsyncTaskFlowDemo', AsyncTaskFlowDemo)
|
||||
// app.component('TaskWorkerDemo', TaskWorkerDemo)
|
||||
// app.component('TaskRetryDemo', TaskRetryDemo)
|
||||
// app.component('AsyncComparisonDemo', AsyncComparisonDemo)
|
||||
|
||||
// File Storage Components Registration
|
||||
app.component('FileStorageTypeDemo', FileStorageTypeDemo)
|
||||
app.component('FileUploadFlowDemo', FileUploadFlowDemo)
|
||||
app.component('CDNAccelerationDemo', CDNAccelerationDemo)
|
||||
// // File Storage Components Registration
|
||||
// app.component('FileStorageTypeDemo', FileStorageTypeDemo)
|
||||
// app.component('FileUploadFlowDemo', FileUploadFlowDemo)
|
||||
// app.component('CDNAccelerationDemo', CDNAccelerationDemo)
|
||||
|
||||
// Rate Limiting Components Registration
|
||||
app.component('RateLimitAlgorithmDemo', RateLimitAlgorithmDemo)
|
||||
app.component('BackpressureDemo', BackpressureDemo)
|
||||
// // Rate Limiting Components Registration
|
||||
// app.component('RateLimitAlgorithmDemo', RateLimitAlgorithmDemo)
|
||||
// app.component('BackpressureDemo', BackpressureDemo)
|
||||
|
||||
// Search Engines Components Registration
|
||||
app.component('InvertedIndexDemo', InvertedIndexDemo)
|
||||
app.component('SearchRelevanceDemo', SearchRelevanceDemo)
|
||||
|
||||
// Data Visualization Components Registration
|
||||
app.component('ChartTypeSelectorDemo', ChartTypeSelectorDemo)
|
||||
app.component('DashboardLayoutDemo', DashboardLayoutDemo)
|
||||
|
||||
// Data Governance Components Registration
|
||||
app.component('DataQualityDemo', DataQualityDemo)
|
||||
app.component('DataGovernanceFrameworkDemo', DataGovernanceFrameworkDemo)
|
||||
app.component('DataLineageDemo', DataLineageDemo)
|
||||
|
||||
// Distributed Systems Components Registration
|
||||
app.component('CAPTheoremDemo', CAPTheoremDemo)
|
||||
app.component('ConsistencyModelsDemo', ConsistencyModelsDemo)
|
||||
app.component('DistributedChallengesDemo', DistributedChallengesDemo)
|
||||
|
||||
// High Availability Components Registration
|
||||
app.component('AvailabilityCalculatorDemo', AvailabilityCalculatorDemo)
|
||||
app.component('FailoverStrategyDemo', FailoverStrategyDemo)
|
||||
|
||||
// Monolith to Microservices Components Registration
|
||||
app.component('ArchEvolutionDemo', ArchEvolutionDemo)
|
||||
|
||||
// System Design Methodology Components Registration
|
||||
app.component('SystemDesignStepsDemo', SystemDesignStepsDemo)
|
||||
app.component('CapacityEstimationDemo', CapacityEstimationDemo)
|
||||
|
||||
// Docker Containers Components Registration
|
||||
app.component('DockerArchitectureDemo', DockerArchitectureDemo)
|
||||
app.component('DockerLifecycleDemo', DockerLifecycleDemo)
|
||||
|
||||
// Linux Basics Components Registration
|
||||
app.component('LinuxFileSystemDemo', LinuxFileSystemDemo)
|
||||
app.component('LinuxCommandDemo', LinuxCommandDemo)
|
||||
app.component('LinuxPermissionsDemo', LinuxPermissionsDemo)
|
||||
|
||||
// Kubernetes Components Registration
|
||||
app.component('K8sArchitectureDemo', K8sArchitectureDemo)
|
||||
app.component('K8sWorkloadsDemo', K8sWorkloadsDemo)
|
||||
|
||||
// Neural Networks Components Registration
|
||||
app.component('NeuronDemo', NeuronDemo)
|
||||
app.component('NetworkLayersDemo', NetworkLayersDemo)
|
||||
app.component('NetworkArchitectureDemo', NetworkArchitectureDemo)
|
||||
|
||||
// Project Architecture Components Registration
|
||||
app.component('ArchitectureComparisonDemo', ArchitectureComparisonDemo)
|
||||
app.component('ProjectArchitectureComparisonDemo', ProjectArchitectureComparisonDemo)
|
||||
|
||||
// Appendix Navigation Component Registration
|
||||
app.component('AppendixFlowMap', AppendixFlowMap)
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
|
||||
Reference in New Issue
Block a user