feat(docs): add interactive demo components for technical appendices
Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation. Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
This commit is contained in:
+283
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="architecture-comparison-demo">
|
||||
<div class="demo-header">
|
||||
<h4>🏗️ 架构演进对比</h4>
|
||||
<p>四个时代的核心架构特征对比</p>
|
||||
</div>
|
||||
|
||||
<div class="comparison-grid">
|
||||
<div class="era-card" v-for="era in eras" :key="era.name" :class="{ active: selectedEra === era.name }" @click="selectedEra = era.name">
|
||||
<div class="era-icon">{{ era.icon }}</div>
|
||||
<div class="era-name">{{ era.name }}</div>
|
||||
<div class="era-year">{{ era.year }}</div>
|
||||
<div class="era-tag">{{ era.tag }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-panel" v-if="selectedEra">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ currentEra.icon }}</span>
|
||||
<h5>{{ currentEra.name }} ({{ currentEra.year }})</h5>
|
||||
</div>
|
||||
|
||||
<div class="detail-content">
|
||||
<div class="feature-section">
|
||||
<h6>🏗️ 架构特征</h6>
|
||||
<ul>
|
||||
<li v-for="(feat, i) in currentEra.features" :key="i">{{ feat }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h6>✅ 优点</h6>
|
||||
<ul>
|
||||
<li v-for="(pro, i) in currentEra.pros" :key="i">{{ pro }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h6>❌ 痛点</h6>
|
||||
<ul>
|
||||
<li v-for="(con, i) in currentEra.cons" :key="i">{{ con }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tech-stack">
|
||||
<h6>🔧 典型技术</h6>
|
||||
<div class="tech-tags">
|
||||
<span v-for="(tech, i) in currentEra.techs" :key="i" class="tech-tag">{{ tech }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selectedEra = ref('单体')
|
||||
|
||||
const eras = [
|
||||
{ name: '物理机', icon: '🖥️', year: '1990s', tag: '单机' },
|
||||
{ name: '单体', icon: '🏢', year: '2000s', tag: '集中' },
|
||||
{ name: '微服务', icon: '🏭', year: '2010s', tag: '分布' },
|
||||
{ name: 'Serverless', icon: '☁️', year: '2020s+', tag: '无服' }
|
||||
]
|
||||
|
||||
const eraDetails = {
|
||||
'物理机': {
|
||||
features: ['单机部署,无冗余', 'FTP 手动上传代码', '垂直扩展(买更强的机器)', '无服务治理概念'],
|
||||
pros: ['部署简单,无需复杂配置', '单机性能好,无网络延迟', '易于调试和排查问题'],
|
||||
cons: ['单点故障,服务不可用', '扩展困难,只能垂直扩容', '手动运维,效率低下'],
|
||||
techs: ['Apache/Nginx', 'CGI/Perl', 'FTP/SFTP', '物理服务器']
|
||||
},
|
||||
'单体': {
|
||||
features: ['单一代码库,统一技术栈', '共享数据库,事务一致性', '统一部署,整体发布', '进程内通信,无网络开销'],
|
||||
pros: ['开发简单,易于上手', '测试方便,本地启动即可', '部署简单,一个包搞定'],
|
||||
cons: ['代码耦合,牵一发而动全身', '技术栈单一,难以引入新技术', '团队扩张后协作困难'],
|
||||
techs: ['Spring/Django/Rails', 'Tomcat/Gunicorn', 'MySQL/PostgreSQL', 'Maven/Gradle']
|
||||
},
|
||||
'微服务': {
|
||||
features: ['服务拆分,独立部署', '技术栈异构,自由选择', '数据库独立,最终一致性', '服务间网络通信'],
|
||||
pros: ['服务独立,团队自治', '技术栈灵活,选择最适合的', '故障隔离,不影响全局'],
|
||||
cons: ['分布式复杂度,调试困难', '网络延迟,性能损耗', '运维成本激增'],
|
||||
techs: ['Docker/Kubernetes', 'gRPC/REST', 'Kafka/RabbitMQ', 'Prometheus/Grafana']
|
||||
},
|
||||
'Serverless': {
|
||||
features: ['函数粒度,事件驱动', '自动扩缩容,按需计费', '无服务器管理,平台托管', '冷启动,有延迟'],
|
||||
pros: ['无需运维,专注业务', '自动扩展,应对流量高峰', '按调用付费,成本低'],
|
||||
cons: ['冷启动延迟', '平台锁定,迁移困难', '调试困难,本地难复现'],
|
||||
techs: ['AWS Lambda', 'Vercel/Cloudflare', 'Supabase/Firebase', 'EventBridge']
|
||||
}
|
||||
}
|
||||
|
||||
const currentEra = computed(() => {
|
||||
const name = selectedEra.value
|
||||
return {
|
||||
icon: eras.find(e => e.name === name)?.icon || '🏗️',
|
||||
name,
|
||||
year: eras.find(e => e.name === name)?.year || '',
|
||||
...eraDetails[name]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.architecture-comparison-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.era-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.era-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.era-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.era-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.era-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.era-year {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.era-tag {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-header h5 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-section {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.feature-section h6 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.feature-section ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.feature-section li {
|
||||
margin-bottom: 0.4rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.feature-section li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tech-stack {
|
||||
grid-column: 1 / -1;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tech-stack h6 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.tech-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="container-docker-demo">
|
||||
<div class="demo-header">
|
||||
<h4>🐳 Docker 容器化演示</h4>
|
||||
<p>理解容器如何让应用"一次打包,到处运行"</p>
|
||||
</div>
|
||||
|
||||
<div class="docker-visualization">
|
||||
<div class="layer traditional" :class="{ active: showTraditional }" @click="showTraditional = true; showDocker = false">
|
||||
<h5>传统部署</h5>
|
||||
<div class="server-stack">
|
||||
<div class="layer-item app">应用 A</div>
|
||||
<div class="layer-item conflict" v-if="showConflict">依赖冲突!</div>
|
||||
<div class="layer-item deps">依赖库 v1.0</div>
|
||||
<div class="layer-item os">操作系统</div>
|
||||
<div class="layer-item hardware">物理服务器</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vs-divider">VS</div>
|
||||
|
||||
<div class="layer docker" :class="{ active: showDocker }" @click="showDocker = true; showTraditional = false">
|
||||
<h5>Docker 容器</h5>
|
||||
<div class="docker-stack">
|
||||
<div class="containers">
|
||||
<div class="container-box">
|
||||
<div class="container-app">应用 A</div>
|
||||
<div class="container-deps">依赖 v1.0</div>
|
||||
</div>
|
||||
<div class="container-box">
|
||||
<div class="container-app">应用 B</div>
|
||||
<div class="container-deps">依赖 v2.0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="docker-engine">Docker Engine</div>
|
||||
<div class="host-os">宿主机操作系统</div>
|
||||
<div class="hardware">物理服务器</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefits-grid">
|
||||
<div class="benefit-card" v-for="benefit in benefits" :key="benefit.title">
|
||||
<div class="benefit-icon">{{ benefit.icon }}</div>
|
||||
<div class="benefit-title">{{ benefit.title }}</div>
|
||||
<div class="benefit-desc">{{ benefit.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const showTraditional = ref(true)
|
||||
const showDocker = ref(false)
|
||||
const showConflict = ref(false)
|
||||
|
||||
const benefits = [
|
||||
{ icon: '📦', title: '环境一致性', desc: '开发、测试、生产环境完全一致,告别"在我机器上能跑"' },
|
||||
{ icon: '🚀', title: '快速部署', desc: '秒级启动,镜像分发,滚动更新无停机' },
|
||||
{ icon: '📊', title: '资源隔离', desc: 'CPU/内存限制,互不干扰,一台机器跑多个应用' },
|
||||
{ icon: '🔄', title: '版本管理', desc: '镜像版本化,随时回滚,灰度发布' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-docker-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.docker-visualization {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.layer {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.layer:hover,
|
||||
.layer.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.layer h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
text-align: center;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.server-stack,
|
||||
.docker-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
padding: 0.6rem;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.layer-item.app {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.layer-item.deps {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.layer-item.os,
|
||||
.layer-item.hardware {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.layer-item.conflict {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--vp-c-danger);
|
||||
font-weight: 600;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.containers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.container-box {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-app {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.container-deps {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.docker-engine {
|
||||
padding: 0.6rem;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.host-os,
|
||||
.hardware {
|
||||
padding: 0.6rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.vs-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.benefit-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.benefit-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.benefit-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.benefit-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.docker-visualization {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vs-divider {
|
||||
justify-content: center;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="deployment-flow-demo">
|
||||
<div class="demo-header">
|
||||
<h4>🚀 部署方式演进</h4>
|
||||
<p>从手工部署到自动化流水线的变化</p>
|
||||
</div>
|
||||
|
||||
<div class="flow-timeline">
|
||||
<div
|
||||
v-for="(step, idx) in steps"
|
||||
:key="idx"
|
||||
class="flow-step"
|
||||
:class="{ active: currentStep === idx }"
|
||||
@click="currentStep = idx"
|
||||
>
|
||||
<div class="step-connector" v-if="idx > 0">
|
||||
<div class="connector-line"></div>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-icon">{{ step.icon }}</div>
|
||||
<div class="step-era">{{ step.era }}</div>
|
||||
<div class="step-title">{{ step.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-detail" v-if="currentStep !== null">
|
||||
<h5>{{ steps[currentStep].title }}</h5>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">部署方式:</span>
|
||||
<span class="value">{{ steps[currentStep].deploy }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">耗时:</span>
|
||||
<span class="value">{{ steps[currentStep].time }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">风险:</span>
|
||||
<span class="value">{{ steps[currentStep].risk }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tools-list">
|
||||
<span class="tools-label">代表工具:</span>
|
||||
<span v-for="tool in steps[currentStep].tools" :key="tool" class="tool-tag">{{ tool }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const currentStep = ref(1)
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: '👤',
|
||||
era: '1990s',
|
||||
title: '手工部署',
|
||||
deploy: 'FTP 上传文件',
|
||||
time: '30分钟-2小时',
|
||||
risk: '人为错误率高',
|
||||
tools: ['FTP', 'SSH', 'SCP']
|
||||
},
|
||||
{
|
||||
icon: '📦',
|
||||
era: '2000s',
|
||||
title: '脚本部署',
|
||||
deploy: '自动化脚本',
|
||||
time: '10-30分钟',
|
||||
risk: '脚本维护成本',
|
||||
tools: ['Shell', 'Ansible', 'Puppet']
|
||||
},
|
||||
{
|
||||
icon: '🔄',
|
||||
era: '2010s',
|
||||
title: 'CI/CD 流水线',
|
||||
deploy: '自动化流水线',
|
||||
time: '5-15分钟',
|
||||
risk: '流水线配置复杂',
|
||||
tools: ['Jenkins', 'GitLab CI', 'GitHub Actions']
|
||||
},
|
||||
{
|
||||
icon: '🚀',
|
||||
era: '2020s+',
|
||||
title: 'GitOps',
|
||||
deploy: '声明式部署',
|
||||
time: '秒级',
|
||||
risk: '学习曲线陡峭',
|
||||
tools: ['ArgoCD', 'Flux', 'Kubernetes']
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-flow-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.flow-timeline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
position: absolute;
|
||||
left: -0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0.5rem;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.connector-line {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.flow-step:hover .step-content,
|
||||
.flow-step.active .step-content {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-era {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.step-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.step-detail h5 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tools-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tools-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.tool-tag {
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flow-timeline {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
flex: 0 0 calc(50% - 0.25rem);
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="evolution-intro-demo">
|
||||
<div class="intro-header">
|
||||
<h3>后端架构进化之旅</h3>
|
||||
<p>用一个餐厅的成长历程,理解后端架构的 30 年变迁</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline-cards">
|
||||
<div
|
||||
v-for="(stage, idx) in stages"
|
||||
:key="idx"
|
||||
class="stage-card"
|
||||
:class="{ active: currentStage === idx }"
|
||||
@click="currentStage = idx"
|
||||
>
|
||||
<div class="stage-era">{{ stage.era }}</div>
|
||||
<div class="stage-icon">{{ stage.icon }}</div>
|
||||
<div class="stage-name">{{ stage.name }}</div>
|
||||
<div class="stage-arch">{{ stage.arch }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stage-detail">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="currentStage" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ stages[currentStage].icon }}</span>
|
||||
<h4>{{ stages[currentStage].restaurant }}</h4>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="detail-section">
|
||||
<h5>🍽️ 餐厅场景</h5>
|
||||
<p>{{ stages[currentStage].scenario }}</p>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h5>💻 后端映射</h5>
|
||||
<p>{{ stages[currentStage].mapping }}</p>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h5>⚡ 核心痛点</h5>
|
||||
<ul>
|
||||
<li v-for="(pain, i) in stages[currentStage].pains" :key="i">{{ pain }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const currentStage = ref(0)
|
||||
|
||||
const stages = [
|
||||
{
|
||||
era: '1990s',
|
||||
icon: '🏠',
|
||||
name: '家庭小作坊',
|
||||
arch: '物理服务器',
|
||||
restaurant: '家庭小厨房',
|
||||
scenario: '一位厨师在一间小厨房里,亲自去菜市场买菜、洗菜、切菜、炒菜、上菜。客人多了就忙不过来,只能让客人排队等。',
|
||||
mapping: '一台物理服务器,处理所有请求:接收HTTP请求、读取文件、执行CGI脚本、返回响应。CPU和内存有限,请求多了只能排队。',
|
||||
pains: [
|
||||
'单机性能瓶颈:客人太多时,厨师根本忙不过来',
|
||||
'垂直扩展成本高:买更贵的机器就像换更大的厨房,治标不治本',
|
||||
'单点故障:厨师生病了,整个餐馆必须关门'
|
||||
]
|
||||
},
|
||||
{
|
||||
era: '2000s',
|
||||
icon: '🏢',
|
||||
name: '大型中央厨房',
|
||||
arch: '单体架构',
|
||||
restaurant: '连锁餐厅中央厨房',
|
||||
scenario: '建立了一个大型中央厨房,分工明确:有人专门洗菜、有人专门切菜、有人专门炒菜。但所有人都在一个大空间里工作,互相依赖。',
|
||||
mapping: '单体应用架构:所有功能模块(用户、订单、支付)都在同一个进程中运行,共享同一个数据库,部署在一个大应用服务器上。',
|
||||
pains: [
|
||||
'牵一发而动全身:切菜师傅切到手,整个厨房都要停下来',
|
||||
'技术债务累积:老员工(老代码)越来越多,新人很难接手',
|
||||
'部署风险高:更新一个菜品(功能)可能影响整个菜单(系统)'
|
||||
]
|
||||
},
|
||||
{
|
||||
era: '2010s',
|
||||
icon: '🏭',
|
||||
name: '专业化分工',
|
||||
arch: '微服务架构',
|
||||
restaurant: '餐饮集团多厨房',
|
||||
scenario: '把中央厨房拆分成多个专业厨房:一个专门做中餐、一个专门做西餐、一个专门做甜点。每个厨房独立运营,通过标准化流程协作。',
|
||||
mapping: '微服务架构:每个业务功能(用户服务、订单服务、支付服务)都是独立的进程,有自己的数据库,通过HTTP/gRPC通信。',
|
||||
pains: [
|
||||
'分布式复杂度:协调多个厨房比管理一个厨房难得多',
|
||||
'网络依赖:中餐厨房需要西餐厨房的原料时,可能网络延迟或故障',
|
||||
'运维成本激增:需要更多人手(运维工程师)来管理这么多厨房'
|
||||
]
|
||||
},
|
||||
{
|
||||
era: '2020s+',
|
||||
icon: '🍽️',
|
||||
name: '外卖平台',
|
||||
arch: 'Serverless',
|
||||
restaurant: '外卖/云厨房',
|
||||
scenario: '你不再自己开厨房,而是在外卖平台上注册。有订单时,平台调度附近的厨房为你制作食物。你只管设计菜品和推广,不用关心厨房在哪、有多少厨师。',
|
||||
mapping: 'Serverless架构:开发者只写业务代码(函数),不关心服务器在哪、有多少台、怎么扩容。云平台自动调度资源,按实际执行时间付费。',
|
||||
pains: [
|
||||
'冷启动延迟:第一家店接单时可能需要热身(冷启动),客人要等',
|
||||
'平台依赖:完全依赖外卖平台(云厂商),迁移成本高',
|
||||
'资源限制:不能做太复杂的菜品(函数有时长和内存限制)'
|
||||
]
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.evolution-intro-demo {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
color: #fff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.intro-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.intro-header h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.intro-header p {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stage-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 16px 12px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stage-card:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stage-card.active {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stage-era {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stage-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.stage-arch {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.stage-detail {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
animation: fadeIn 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.detail-header h4 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-section h5 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.detail-section p {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.detail-section ul {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.detail-section li {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.timeline-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stage-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<div class="kubernetes-demo">
|
||||
<div class="demo-header">
|
||||
<h4>☸️ Kubernetes 编排演示</h4>
|
||||
<p>观察 K8s 如何自动调度容器、实现负载均衡和故障恢复</p>
|
||||
</div>
|
||||
|
||||
<div class="k8s-architecture">
|
||||
<div class="control-plane">
|
||||
<div class="plane-title">控制平面 (Control Plane)</div>
|
||||
<div class="components">
|
||||
<div class="component" v-for="comp in controlPlane" :key="comp.name"
|
||||
:class="{ active: activeComponent === comp.name }"
|
||||
@click="activeComponent = comp.name">
|
||||
<div class="comp-icon">{{ comp.icon }}</div>
|
||||
<div class="comp-name">{{ comp.name }}</div>
|
||||
<div class="comp-desc">{{ comp.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="worker-nodes">
|
||||
<div class="plane-title">工作节点 (Worker Nodes)</div>
|
||||
<div class="nodes-container">
|
||||
<div class="node" v-for="node in workerNodes" :key="node.name"
|
||||
:class="{
|
||||
active: node.status === 'active',
|
||||
failed: node.status === 'failed',
|
||||
selected: selectedNode === node.name
|
||||
}"
|
||||
@click="selectNode(node.name)">
|
||||
<div class="node-header">
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
<span class="node-status" :class="node.status">{{ node.statusText }}</span>
|
||||
</div>
|
||||
<div class="node-resources">
|
||||
<div class="resource">
|
||||
<span class="res-label">CPU:</span>
|
||||
<div class="res-bar">
|
||||
<div class="res-fill" :style="{ width: node.cpu + '%' }" :class="{ high: node.cpu > 80 }"></div>
|
||||
</div>
|
||||
<span class="res-value">{{ node.cpu }}%</span>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<span class="res-label">内存:</span>
|
||||
<div class="res-bar">
|
||||
<div class="res-fill" :style="{ width: node.memory + '%' }" :class="{ high: node.memory > 80 }"></div>
|
||||
</div>
|
||||
<span class="res-value">{{ node.memory }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-pods">
|
||||
<div class="pods-label">运行 Pod: {{ node.pods }} 个</div>
|
||||
<div class="pods-grid">
|
||||
<div v-for="n in Math.min(node.pods, 8)" :key="n" class="pod-dot" :class="{
|
||||
running: node.status === 'active',
|
||||
pending: node.status === 'pending',
|
||||
failed: node.status === 'failed'
|
||||
}"></div>
|
||||
<div v-if="node.pods > 8" class="pod-more">+{{ node.pods - 8 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="k8s-controls">
|
||||
<button class="control-btn" @click="simulateScheduling" :disabled="isScheduling">{{ isScheduling ? '调度中...' : '🚀 模拟 Pod 调度' }}</button>
|
||||
<button class="control-btn" @click="simulateScaling" :disabled="isScaling">{{ isScaling ? '扩容中...' : '📈 自动扩容' }}</button>
|
||||
<button class="control-btn danger" @click="simulateFailure" :disabled="isFailing">{{ isFailing ? '故障注入中...' : '💥 模拟节点故障' }}</button>
|
||||
<button class="control-btn" @click="resetCluster">🔄 重置集群</button>
|
||||
</div>
|
||||
|
||||
<div class="k8s-logs" v-if="logs.length > 0">
|
||||
<div class="log-entry" v-for="(log, idx) in logs.slice(-5)" :key="idx" :class="log.level">
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-explanation">
|
||||
<h5>💡 Kubernetes 核心概念</h5>
|
||||
<ul>
|
||||
<li><strong>Pod</strong>:最小的部署单元,一个 Pod 可以包含一个或多个容器</li>
|
||||
<li><strong>Deployment</strong>:管理 Pod 的副本数量和滚动更新</li>
|
||||
<li><strong>Service</strong>:提供稳定的网络访问入口,实现负载均衡</li>
|
||||
<li><strong>Scheduler</strong>:根据资源需求和策略,自动将 Pod 调度到合适的节点</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const controlPlane = [
|
||||
{ name: 'API Server', icon: '🌐', desc: '集群的统一入口' },
|
||||
{ name: 'etcd', icon: '🗄️', desc: '分布式键值存储' },
|
||||
{ name: 'Scheduler', icon: '📋', desc: 'Pod 调度器' },
|
||||
{ name: 'Controller', icon: '🎮', desc: '控制器管理器' }
|
||||
]
|
||||
|
||||
const workerNodes = reactive([
|
||||
{
|
||||
name: 'Node-1',
|
||||
icon: '🖥️',
|
||||
status: 'active',
|
||||
statusText: '运行中',
|
||||
cpu: 45,
|
||||
memory: 60,
|
||||
pods: 5
|
||||
},
|
||||
{
|
||||
name: 'Node-2',
|
||||
icon: '🖥️',
|
||||
status: 'active',
|
||||
statusText: '运行中',
|
||||
cpu: 30,
|
||||
memory: 40,
|
||||
pods: 3
|
||||
},
|
||||
{
|
||||
name: 'Node-3',
|
||||
icon: '🖥️',
|
||||
status: 'pending',
|
||||
statusText: '准备中',
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
pods: 0
|
||||
}
|
||||
])
|
||||
|
||||
const activeComponent = ref(null)
|
||||
const selectedNode = ref(null)
|
||||
const isScheduling = ref(false)
|
||||
const isScaling = ref(false)
|
||||
const isFailing = ref(false)
|
||||
const logs = ref([])
|
||||
|
||||
const addLog = (message, level = 'info') => {
|
||||
const now = new Date()
|
||||
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
|
||||
logs.value.push({ time, message, level })
|
||||
if (logs.value.length > 20) logs.value.shift()
|
||||
}
|
||||
|
||||
const selectNode = (name) => {
|
||||
selectedNode.value = selectedNode.value === name ? null : name
|
||||
}
|
||||
|
||||
const simulateScheduling = async () => {
|
||||
isScheduling.value = true
|
||||
addLog('开始调度新 Pod...', 'info')
|
||||
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
addLog('Scheduler: 评估节点资源...', 'info')
|
||||
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
const targetNode = workerNodes.find(n => n.status === 'active' && n.cpu < 70)
|
||||
if (targetNode) {
|
||||
targetNode.pods++
|
||||
targetNode.cpu += 10
|
||||
addLog(`Pod 已调度到 ${targetNode.name}`, 'success')
|
||||
} else {
|
||||
addLog('警告: 没有合适的节点可调度', 'warning')
|
||||
}
|
||||
|
||||
isScheduling.value = false
|
||||
}
|
||||
|
||||
const simulateScaling = async () => {
|
||||
isScaling.value = true
|
||||
addLog('检测到高负载,开始水平扩容...', 'info')
|
||||
|
||||
const pendingNode = workerNodes.find(n => n.status === 'pending')
|
||||
if (pendingNode) {
|
||||
await new Promise(r => setTimeout(r, 1500))
|
||||
pendingNode.status = 'active'
|
||||
pendingNode.statusText = '运行中'
|
||||
pendingNode.cpu = 20
|
||||
pendingNode.memory = 30
|
||||
addLog(`${pendingNode.name} 已启动并加入集群`, 'success')
|
||||
} else {
|
||||
addLog('已达到最大节点数', 'warning')
|
||||
}
|
||||
|
||||
isScaling.value = false
|
||||
}
|
||||
|
||||
const simulateFailure = async () => {
|
||||
isFailing.value = true
|
||||
const targetNode = workerNodes.find(n => n.status === 'active')
|
||||
|
||||
if (targetNode) {
|
||||
addLog(`警告: ${targetNode.name} 失去连接!`, 'error')
|
||||
targetNode.status = 'failed'
|
||||
targetNode.statusText = '故障'
|
||||
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
addLog('Controller: 开始重新调度 Pod...', 'info')
|
||||
|
||||
await new Promise(r => setTimeout(r, 1500))
|
||||
const healthyNode = workerNodes.find(n => n.status === 'active' && n.name !== targetNode.name)
|
||||
if (healthyNode) {
|
||||
healthyNode.pods += targetNode.pods
|
||||
addLog(`Pod 已成功迁移到 ${healthyNode.name}`, 'success')
|
||||
}
|
||||
|
||||
targetNode.pods = 0
|
||||
targetNode.cpu = 0
|
||||
targetNode.memory = 0
|
||||
}
|
||||
|
||||
isFailing.value = false
|
||||
}
|
||||
|
||||
const resetCluster = () => {
|
||||
workerNodes.forEach((node, index) => {
|
||||
if (index < 2) {
|
||||
node.status = 'active'
|
||||
node.statusText = '运行中'
|
||||
node.cpu = 30 + index * 15
|
||||
node.memory = 40 + index * 20
|
||||
node.pods = 3 + index * 2
|
||||
} else {
|
||||
node.status = 'pending'
|
||||
node.statusText = '准备中'
|
||||
node.cpu = 0
|
||||
node.memory = 0
|
||||
node.pods = 0
|
||||
}
|
||||
})
|
||||
logs.value = []
|
||||
addLog('集群已重置', 'info')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-docker-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.docker-visualization {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.layer {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.layer:hover,
|
||||
.layer.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.layer h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
text-align: center;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.server-stack,
|
||||
.docker-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
padding: 0.6rem;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.layer-item.app {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.layer-item.deps {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.layer-item.os,
|
||||
.layer-item.hardware {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.layer-item.conflict {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--vp-c-danger);
|
||||
font-weight: 600;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.containers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.container-box {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-app {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.container-deps {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.docker-engine {
|
||||
padding: 0.6rem;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.host-os,
|
||||
.hardware {
|
||||
padding: 0.6rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.vs-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.benefit-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.benefit-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.benefit-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.benefit-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.docker-visualization {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vs-divider {
|
||||
justify-content: center;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<div class="microservices-demo">
|
||||
<div class="demo-header">
|
||||
<h4>🏭 微服务架构演示</h4>
|
||||
<p>观察多个独立服务如何协作,以及服务间通信方式</p>
|
||||
</div>
|
||||
|
||||
<div class="services-grid">
|
||||
<div
|
||||
v-for="service in services"
|
||||
:key="service.name"
|
||||
class="service-card"
|
||||
:class="{ active: activeService === service.name, failed: service.status === 'failed' }"
|
||||
@click="selectService(service.name)"
|
||||
>
|
||||
<div class="service-header">
|
||||
<span class="service-icon">{{ service.icon }}</span>
|
||||
<span class="service-name">{{ service.name }}</span>
|
||||
<span class="service-status" :class="service.status">{{ service.statusText }}</span>
|
||||
</div>
|
||||
<div class="service-details">
|
||||
<div class="detail-row">
|
||||
<span class="label">端口:</span>
|
||||
<span class="value">{{ service.port }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">数据库:</span>
|
||||
<span class="value">{{ service.database }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">依赖:</span>
|
||||
<span class="value deps">{{ service.dependencies.join(', ') || '无' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="communication-flow">
|
||||
<h5>服务间通信链路</h5>
|
||||
<div class="flow-visualization">
|
||||
<div class="flow-step" v-for="(step, idx) in flowSteps" :key="idx"
|
||||
:class="{ active: currentFlowStep === idx, completed: currentFlowStep > idx }">
|
||||
<div class="step-number">{{ idx + 1 }}</div>
|
||||
<div class="step-content">
|
||||
<div class="step-service">{{ step.service }}</div>
|
||||
<div class="step-action">{{ step.action }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-controls">
|
||||
<button class="flow-btn" @click="startFlow" :disabled="isFlowRunning">开始流程</button>
|
||||
<button class="flow-btn" @click="resetFlow">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const services = ref([
|
||||
{
|
||||
name: '用户服务',
|
||||
icon: '👤',
|
||||
status: 'healthy',
|
||||
statusText: '健康',
|
||||
port: '8081',
|
||||
database: 'MySQL',
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
name: '订单服务',
|
||||
icon: '📦',
|
||||
status: 'healthy',
|
||||
statusText: '健康',
|
||||
port: '8082',
|
||||
database: 'PostgreSQL',
|
||||
dependencies: ['用户服务']
|
||||
},
|
||||
{
|
||||
name: '支付服务',
|
||||
icon: '💳',
|
||||
status: 'healthy',
|
||||
statusText: '健康',
|
||||
port: '8083',
|
||||
database: 'MongoDB',
|
||||
dependencies: ['用户服务', '订单服务']
|
||||
},
|
||||
{
|
||||
name: '库存服务',
|
||||
icon: '🏭',
|
||||
status: 'healthy',
|
||||
statusText: '健康',
|
||||
port: '8084',
|
||||
database: 'Redis',
|
||||
dependencies: ['订单服务']
|
||||
}
|
||||
])
|
||||
|
||||
const activeService = ref(null)
|
||||
const currentFlowStep = ref(-1)
|
||||
const isFlowRunning = ref(false)
|
||||
|
||||
const flowSteps = [
|
||||
{ service: '用户服务', action: '验证用户身份' },
|
||||
{ service: '订单服务', action: '创建订单记录' },
|
||||
{ service: '库存服务', action: '检查库存数量' },
|
||||
{ service: '支付服务', action: '处理支付请求' },
|
||||
{ service: '订单服务', action: '更新订单状态' }
|
||||
]
|
||||
|
||||
const selectService = (name) => {
|
||||
activeService.value = activeService.value === name ? null : name
|
||||
}
|
||||
|
||||
const startFlow = async () => {
|
||||
isFlowRunning.value = true
|
||||
currentFlowStep.value = 0
|
||||
|
||||
for (let i = 0; i < flowSteps.length; i++) {
|
||||
currentFlowStep.value = i
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
}
|
||||
|
||||
isFlowRunning.value = false
|
||||
}
|
||||
|
||||
const resetFlow = () => {
|
||||
currentFlowStep.value = -1
|
||||
isFlowRunning.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.microservices-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.service-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.service-card.failed {
|
||||
border-color: var(--vp-c-danger);
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.service-status {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.service-status.healthy {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.service-status.failed {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.service-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value.deps {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.communication-flow {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.communication-flow h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.flow-visualization {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.flow-step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.flow-step.completed {
|
||||
border-color: var(--vp-c-success);
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.flow-step.active .step-number {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.flow-step.completed .step-number {
|
||||
background: var(--vp-c-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-service {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.step-action {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.flow-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flow-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.flow-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.flow-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.services-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.service-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<div class="monolith-demo">
|
||||
<div class="demo-header">
|
||||
<h4>🏢 单体架构演示</h4>
|
||||
<p>观察单体应用如何处理请求,以及模块间的依赖关系</p>
|
||||
</div>
|
||||
|
||||
<div class="monolith-diagram">
|
||||
<div class="monolith-box" :class="{ crashed: hasCrashed }">
|
||||
<div class="monolith-header">单体应用进程</div>
|
||||
<div class="modules-container">
|
||||
<div
|
||||
v-for="module in modules"
|
||||
:key="module.name"
|
||||
class="module-box"
|
||||
:class="{ active: activeModule === module.name, crashed: crashedModule === module.name }"
|
||||
@click="triggerModule(module.name)"
|
||||
>
|
||||
<div class="module-icon">{{ module.icon }}</div>
|
||||
<div class="module-name">{{ module.name }}</div>
|
||||
<div class="module-status" :class="module.status">{{ module.statusText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shared-db">
|
||||
<div class="db-icon">🗄️</div>
|
||||
<div class="db-label">共享数据库</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="request-flow">
|
||||
<div
|
||||
v-for="req in requests"
|
||||
:key="req.id"
|
||||
class="flow-request"
|
||||
:class="req.status"
|
||||
>
|
||||
<span class="req-type">{{ req.type }}</span>
|
||||
<span class="req-arrow">→</span>
|
||||
<span class="req-target">{{ req.target }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="control-btn" @click="simulateNormalRequest">正常请求</button>
|
||||
<button class="control-btn danger" @click="simulateCrash">模拟模块故障</button>
|
||||
<button class="control-btn" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-explanation">
|
||||
<h5>💡 单体架构的特点</h5>
|
||||
<ul>
|
||||
<li><strong>共享进程空间</strong>:所有模块在同一个进程中运行,内存共享</li>
|
||||
<li><strong>数据库耦合</strong>:所有模块共享同一个数据库,Schema变更影响全局</li>
|
||||
<li><strong>级联故障</strong>:一个模块崩溃可能导致整个进程挂掉(雪崩效应)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const modules = ref([
|
||||
{ name: '用户模块', icon: '👤', status: 'healthy', statusText: '健康' },
|
||||
{ name: '订单模块', icon: '📦', status: 'healthy', statusText: '健康' },
|
||||
{ name: '支付模块', icon: '💳', status: 'healthy', statusText: '健康' },
|
||||
{ name: '库存模块', icon: '🏭', status: 'healthy', statusText: '健康' }
|
||||
])
|
||||
|
||||
const requests = ref([])
|
||||
const hasCrashed = ref(false)
|
||||
const crashedModule = ref(null)
|
||||
const activeModule = ref(null)
|
||||
const requestId = ref(0)
|
||||
|
||||
const simulateNormalRequest = () => {
|
||||
const targets = ['用户模块', '订单模块', '支付模块', '库存模块']
|
||||
const target = targets[Math.floor(Math.random() * targets.length)]
|
||||
|
||||
activeModule.value = target
|
||||
requestId.value++
|
||||
|
||||
requests.value.push({
|
||||
id: requestId.value,
|
||||
type: 'GET',
|
||||
target: target,
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
activeModule.value = null
|
||||
if (requests.value.length > 5) {
|
||||
requests.value.shift()
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const simulateCrash = () => {
|
||||
const targetModule = '订单模块'
|
||||
hasCrashed.value = true
|
||||
crashedModule.value = targetModule
|
||||
|
||||
const module = modules.value.find(m => m.name === targetModule)
|
||||
if (module) {
|
||||
module.status = 'crashed'
|
||||
module.statusText = '已崩溃'
|
||||
}
|
||||
|
||||
// Cascade effect - other modules become unavailable
|
||||
setTimeout(() => {
|
||||
modules.value.forEach(m => {
|
||||
if (m.name !== targetModule) {
|
||||
m.status = 'affected'
|
||||
m.statusText = '受影响'
|
||||
}
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
hasCrashed.value = false
|
||||
crashedModule.value = null
|
||||
activeModule.value = null
|
||||
requests.value = []
|
||||
|
||||
modules.value.forEach(m => {
|
||||
m.status = 'healthy'
|
||||
m.statusText = '健康'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.monolith-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.monolith-diagram {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.monolith-box {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.monolith-box.crashed {
|
||||
border-color: var(--vp-c-danger);
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.monolith-header {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.modules-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.module-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.module-box:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.module-box.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.module-box.crashed {
|
||||
border-color: var(--vp-c-danger);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.module-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.module-status {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.module-status.healthy {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.module-status.crashed {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.module-status.affected {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.shared-db {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.db-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.db-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.request-flow {
|
||||
width: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.flow-request {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.flow-request.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.req-type {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.req-arrow {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.req-target {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.control-btn.danger {
|
||||
border-color: var(--vp-c-danger);
|
||||
color: var(--vp-c-danger);
|
||||
}
|
||||
|
||||
.control-btn.danger:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.demo-explanation {
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-explanation h5 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.demo-explanation ul {
|
||||
margin: 0 0 1rem 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-explanation li {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.demo-explanation li strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.monolith-diagram {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.request-flow {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<div class="physical-server-demo">
|
||||
<div class="demo-header">
|
||||
<h4>🖥️ 物理服务器时代演示</h4>
|
||||
<p>点击"发送请求",观察早期 CGI 服务器的处理瓶颈</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-stage">
|
||||
<div class="client-zone">
|
||||
<div class="zone-title">👤 用户浏览器</div>
|
||||
<div class="request-queue">
|
||||
<div
|
||||
v-for="(req, idx) in pendingRequests"
|
||||
:key="req.id"
|
||||
class="request-card"
|
||||
:style="{ animationDelay: idx * 0.1 + 's' }"
|
||||
>
|
||||
<span class="req-method">{{ req.method }}</span>
|
||||
<span class="req-path">{{ req.path }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="send-btn"
|
||||
:disabled="isProcessing"
|
||||
@click="sendRequest"
|
||||
>
|
||||
{{ isProcessing ? '处理中...' : '🚀 发起请求' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="connection-zone">
|
||||
<div class="network-line" :class="{ busy: isProcessing }">
|
||||
<div class="packets">
|
||||
<div
|
||||
v-for="pkt in packets"
|
||||
:key="pkt.id"
|
||||
class="packet"
|
||||
:class="pkt.type"
|
||||
:style="{ top: pkt.top + 'px' }"
|
||||
>
|
||||
{{ pkt.type === 'req' ? '📤' : '📥' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="latency-display" v-if="currentLatency > 0">
|
||||
⏱️ {{ currentLatency }}ms
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="server-zone">
|
||||
<div class="zone-title">🖥️ CGI 服务器</div>
|
||||
<div class="server-status">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="{ processing: isProcessing }"
|
||||
>
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">{{ serverStatus }}</span>
|
||||
</div>
|
||||
<div class="cpu-usage" v-if="isProcessing">
|
||||
<div class="cpu-bar">
|
||||
<div
|
||||
class="cpu-fill"
|
||||
:style="{ width: cpuUsage + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="cpu-text">CPU: {{ cpuUsage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="process-queue">
|
||||
<div
|
||||
v-for="proc in processQueue"
|
||||
:key="proc.id"
|
||||
class="process-item"
|
||||
>
|
||||
<span class="proc-name">{{ proc.name }}</span>
|
||||
<div class="proc-progress">
|
||||
<div
|
||||
class="proc-bar"
|
||||
:style="{ width: proc.progress + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-explanation">
|
||||
<h5>💡 早期的痛点在哪里?</h5>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>进程启动开销</strong>:每个请求都要启动新的 CGI
|
||||
进程,就像每来一个客人都要重新搭一个厨房
|
||||
</li>
|
||||
<li>
|
||||
<strong>资源无法复用</strong>:数据库连接每次都要重新建立,CPU
|
||||
频繁在进程间切换
|
||||
</li>
|
||||
<li>
|
||||
<strong>扩展困难</strong>:只能买更强的单机(垂直扩展),无法通过增加机器分担压力
|
||||
</li>
|
||||
</ul>
|
||||
<p class="demo-conclusion">
|
||||
这就是<strong>物理服务器 + CGI</strong>时代的核心问题:<span
|
||||
class="highlight"
|
||||
>进程级隔离带来了稳定性,但也带来了巨大的性能开销</span
|
||||
>。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const isProcessing = ref(false)
|
||||
const currentLatency = ref(0)
|
||||
const cpuUsage = ref(0)
|
||||
const packets = ref([])
|
||||
const pendingRequests = ref([])
|
||||
const processQueue = ref([])
|
||||
const requestCounter = ref(0)
|
||||
const packetCounter = ref(0)
|
||||
|
||||
const serverStatus = computed(() => {
|
||||
if (isProcessing.value) return '处理中...'
|
||||
return '等待请求'
|
||||
})
|
||||
|
||||
const sendRequest = async () => {
|
||||
if (isProcessing.value) return
|
||||
|
||||
isProcessing.value = true
|
||||
requestCounter.value++
|
||||
const requestId = requestCounter.value
|
||||
|
||||
// Add request to queue
|
||||
pendingRequests.value.push({
|
||||
id: requestId,
|
||||
method: 'GET',
|
||||
path: '/index.cgi'
|
||||
})
|
||||
|
||||
// Simulate network latency
|
||||
currentLatency.value = 0
|
||||
const latencyInterval = setInterval(() => {
|
||||
currentLatency.value += Math.floor(Math.random() * 50) + 20
|
||||
}, 100)
|
||||
|
||||
// Simulate packet
|
||||
const packetId = ++packetCounter.value
|
||||
packets.value.push({
|
||||
id: packetId,
|
||||
type: 'req',
|
||||
top: 20
|
||||
})
|
||||
|
||||
// Add process to queue
|
||||
processQueue.value.push({
|
||||
id: requestId,
|
||||
name: `CGI Process #${requestId}`,
|
||||
progress: 0
|
||||
})
|
||||
|
||||
// Simulate CPU usage fluctuation
|
||||
const cpuInterval = setInterval(() => {
|
||||
cpuUsage.value = Math.min(100, cpuUsage.value + Math.random() * 20 + 10)
|
||||
processQueue.value.forEach(p => {
|
||||
p.progress = Math.min(100, p.progress + Math.random() * 15 + 5)
|
||||
})
|
||||
}, 100)
|
||||
|
||||
// Simulate processing time
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
clearInterval(latencyInterval)
|
||||
clearInterval(cpuInterval)
|
||||
|
||||
// Cleanup
|
||||
pendingRequests.value = pendingRequests.value.filter(r => r.id !== requestId)
|
||||
packets.value = packets.value.filter(p => p.id !== packetId)
|
||||
processQueue.value = processQueue.value.filter(p => p.id !== requestId)
|
||||
|
||||
cpuUsage.value = 0
|
||||
currentLatency.value = 0
|
||||
isProcessing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.physical-server-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-stage {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.client-zone,
|
||||
.server-zone {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.zone-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.request-queue {
|
||||
min-height: 60px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.request-card {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.req-method {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.connection-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.network-line {
|
||||
width: 3px;
|
||||
height: 120px;
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.network-line.busy {
|
||||
opacity: 1;
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.latency-display {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.server-status {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-success);
|
||||
}
|
||||
|
||||
.status-indicator.processing .status-dot {
|
||||
background: var(--vp-c-danger);
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.cpu-usage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cpu-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cpu-fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-danger);
|
||||
border-radius: 3px;
|
||||
transition: width 0.1s ease;
|
||||
}
|
||||
|
||||
.cpu-text {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.process-queue {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.process-item {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.proc-name {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.proc-progress {
|
||||
height: 4px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.proc-bar {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 2px;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.demo-explanation {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-explanation h5 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.demo-explanation ul {
|
||||
margin: 0 0 1rem 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-explanation li {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.demo-explanation li strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-conclusion {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-stage {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.connection-zone {
|
||||
flex-direction: row;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.network-line {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="scaling-strategy-demo">
|
||||
<div class="demo-header">
|
||||
<h4>📈 扩展策略对比</h4>
|
||||
<p>垂直扩展 vs 水平扩展</p>
|
||||
</div>
|
||||
|
||||
<div class="strategies">
|
||||
<div class="strategy-card" :class="{ active: activeStrategy === 'vertical' }" @click="activeStrategy = 'vertical'">
|
||||
<div class="strategy-icon">📦</div>
|
||||
<div class="strategy-name">垂直扩展</div>
|
||||
<div class="strategy-desc">买更强的机器</div>
|
||||
<div class="visual-vertical">
|
||||
<div class="server" :class="{ scale: activeStrategy === 'vertical' }">
|
||||
<div class="cpu">CPU</div>
|
||||
<div class="memory">内存</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-card" :class="{ active: activeStrategy === 'horizontal' }" @click="activeStrategy = 'horizontal'">
|
||||
<div class="strategy-icon">🔄</div>
|
||||
<div class="strategy-name">水平扩展</div>
|
||||
<div class="strategy-desc">加更多机器</div>
|
||||
<div class="visual-horizontal">
|
||||
<div class="servers">
|
||||
<div class="server-mini" v-for="n in 4" :key="n" :class="{ active: activeStrategy === 'horizontal' && n <= serverCount }" :style="{ animationDelay: (n * 0.1) + 's' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<div class="table-row header">
|
||||
<span>维度</span>
|
||||
<span>垂直扩展</span>
|
||||
<span>水平扩展</span>
|
||||
</div>
|
||||
<div class="table-row" v-for="item in comparisonData" :key="item.dim">
|
||||
<span>{{ item.dim }}</span>
|
||||
<span :class="{ better: item.verticalBetter }">{{ item.vertical }}</span>
|
||||
<span :class="{ better: item.horizontalBetter }">{{ item.horizontal }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeStrategy = ref('horizontal')
|
||||
const serverCount = ref(3)
|
||||
|
||||
const comparisonData = [
|
||||
{ dim: '成本', vertical: '硬件贵', horizontal: '机器多', verticalBetter: false, horizontalBetter: true },
|
||||
{ dim: '上限', vertical: '有瓶颈', horizontal: '理论上无限', verticalBetter: false, horizontalBetter: true },
|
||||
{ dim: '复杂度', vertical: '简单', horizontal: '需要分布式', verticalBetter: true, horizontalBetter: false },
|
||||
{ dim: '数据', vertical: '一致性好', horizontal: '需要同步', verticalBetter: true, horizontalBetter: false }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scaling-strategy-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.strategies {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.strategy-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.strategy-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.strategy-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
.strategy-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.strategy-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.strategy-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.visual-vertical,
|
||||
.visual-horizontal {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.server {
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.server.scale {
|
||||
transform: scale(1.2);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.cpu, .memory {
|
||||
font-size: 0.5rem;
|
||||
padding: 1px 3px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 2px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.servers {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.server-mini {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 2px;
|
||||
opacity: 0.3;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.server-mini.active {
|
||||
opacity: 1;
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
animation: popIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes popIn {
|
||||
0% { transform: scale(0.8); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr 1.2fr;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table-row:not(.header):not(:last-child) {
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.table-row.header {
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.table-row span:first-child {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.better {
|
||||
color: var(--vp-c-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.strategies {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.comparison-table .table-row {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div class="serverless-demo">
|
||||
<div class="demo-header">
|
||||
<h4>⚡ Serverless 架构演示</h4>
|
||||
<p>观察 Serverless 如何按需执行函数、自动扩缩容</p>
|
||||
</div>
|
||||
|
||||
<div class="serverless-visualization">
|
||||
<div class="function-grid">
|
||||
<div v-for="func in functions" :key="func.name" class="function-card" :class="{ active: func.state === 'running', cold: func.state === 'cold', warming: func.state === 'warming' }" @click="triggerFunction(func.name)">
|
||||
<div class="function-icon">{{ func.icon }}</div>
|
||||
<div class="function-name">{{ func.name }}</div>
|
||||
<div class="function-state" :class="func.state">{{ stateText(func.state) }}</div>
|
||||
<div class="function-metrics" v-if="func.invocations > 0">
|
||||
<span>调用: {{ func.invocations }}</span>
|
||||
<span>平均: {{ func.avgDuration }}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auto-scaling-panel">
|
||||
<div class="scaling-title">自动扩缩容状态</div>
|
||||
<div class="scaling-metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-label">并发请求:</span>
|
||||
<span class="metric-value">{{ concurrentRequests }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">运行实例:</span>
|
||||
<span class="metric-value">{{ runningInstances }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">冷启动:</span>
|
||||
<span class="metric-value">{{ coldStarts }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scaling-chart">
|
||||
<div v-for="(point, idx) in scalingHistory" :key="idx" class="chart-bar" :style="{ height: point + '%' }" :class="{ high: point > 70 }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="traffic-simulator">
|
||||
<div class="simulator-title">流量模拟器</div>
|
||||
<div class="traffic-patterns">
|
||||
<button v-for="pattern in trafficPatterns" :key="pattern.name" class="pattern-btn" :class="{ active: currentPattern === pattern.name }" @click="applyPattern(pattern)">
|
||||
<span class="pattern-icon">{{ pattern.icon }}</span>
|
||||
<span class="pattern-name">{{ pattern.name }}</span>
|
||||
<span class="pattern-desc">{{ pattern.desc }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-explanation">
|
||||
<h5>💡 Serverless 核心特性</h5>
|
||||
<ul>
|
||||
<li><strong>按需执行</strong>:函数只在被调用时运行,不调用不产生费用</li>
|
||||
<li><strong>自动扩缩容</strong>:从 0 到数千实例自动扩展,无需人工干预</li>
|
||||
<li><strong>冷启动</strong>:长时间未调用后首次调用会有延迟,需要预热策略</li>
|
||||
<li><strong>事件驱动</strong>:响应 HTTP 请求、消息队列、定时任务等多种事件源</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const functions = reactive([
|
||||
{ name: '用户登录', icon: '🔐', state: 'cold', invocations: 0, avgDuration: 0 },
|
||||
{ name: '订单处理', icon: '📦', state: 'cold', invocations: 0, avgDuration: 0 },
|
||||
{ name: '图片处理', icon: '🖼️', state: 'cold', invocations: 0, avgDuration: 0 },
|
||||
{ name: '数据备份', icon: '💾', state: 'cold', invocations: 0, avgDuration: 0 }
|
||||
])
|
||||
|
||||
const concurrentRequests = ref(0)
|
||||
const runningInstances = ref(0)
|
||||
const coldStarts = ref(0)
|
||||
const scalingHistory = ref([10, 15, 20, 25, 30, 35, 40, 35, 30, 25, 20, 15])
|
||||
const currentPattern = ref(null)
|
||||
const isFlowRunning = ref(false)
|
||||
|
||||
const trafficPatterns = [
|
||||
{ name: '正常流量', icon: '📊', desc: '平稳的请求速率' },
|
||||
{ name: '突发流量', icon: '🚀', desc: '突然的流量激增' },
|
||||
{ name: '潮汐流量', icon: '🌊', desc: '周期性的高低峰' }
|
||||
]
|
||||
|
||||
const stateText = (state) => {
|
||||
const map = { cold: '冷状态', warming: '预热中', running: '运行中' }
|
||||
return map[state] || state
|
||||
}
|
||||
|
||||
const triggerFunction = async (name) => {
|
||||
const fn = functions.find(f => f.name === name)
|
||||
if (!fn) return
|
||||
|
||||
if (fn.state === 'cold') {
|
||||
fn.state = 'warming'
|
||||
coldStarts.value++
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
}
|
||||
|
||||
fn.state = 'running'
|
||||
fn.invocations++
|
||||
concurrentRequests.value++
|
||||
runningInstances.value++
|
||||
|
||||
const duration = Math.floor(Math.random() * 150) + 50
|
||||
fn.avgDuration = Math.floor((fn.avgDuration * (fn.invocations - 1) + duration) / fn.invocations)
|
||||
|
||||
await new Promise(r => setTimeout(r, duration))
|
||||
|
||||
concurrentRequests.value--
|
||||
if (concurrentRequests.value === 0) {
|
||||
runningInstances.value = 0
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (fn.invocations > 0) {
|
||||
fn.state = 'cold'
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const applyPattern = (pattern) => {
|
||||
currentPattern.value = pattern.name
|
||||
// 模拟流量模式
|
||||
if (pattern.name === '突发流量') {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
setTimeout(() => {
|
||||
const fn = functions[Math.floor(Math.random() * functions.length)]
|
||||
triggerFunction(fn.name)
|
||||
}, i * 200)
|
||||
}
|
||||
} else if (pattern.name === '潮汐流量') {
|
||||
const interval = setInterval(() => {
|
||||
const fn = functions[Math.floor(Math.random() * functions.length)]
|
||||
triggerFunction(fn.name)
|
||||
}, 500)
|
||||
setTimeout(() => clearInterval(interval), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
let interval
|
||||
onMounted(() => {
|
||||
interval = setInterval(() => {
|
||||
scalingHistory.value.shift()
|
||||
const last = scalingHistory.value[scalingHistory.value.length - 1]
|
||||
const variation = Math.floor(Math.random() * 20) - 10
|
||||
const next = Math.max(10, Math.min(90, last + variation))
|
||||
scalingHistory.value.push(next)
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.serverless-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.serverless-visualization {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.function-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.function-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.function-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.function-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.function-card.cold {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.function-card.warming {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.function-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.function-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.function-state {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.function-state.cold {
|
||||
background: rgba(156, 163, 175, 0.2);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.function-state.warming {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.function-state.running {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.function-metrics {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.auto-scaling-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.scaling-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scaling-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.scaling-chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
height: 60px;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
flex: 1;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 1px;
|
||||
transition: height 0.3s;
|
||||
min-height: 2px;
|
||||
}
|
||||
|
||||
.chart-bar.high {
|
||||
background: var(--vp-c-warning);
|
||||
}
|
||||
|
||||
.traffic-simulator {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.simulator-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.traffic-patterns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.pattern-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pattern-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.pattern-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.pattern-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.pattern-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.pattern-desc {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-explanation {
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-explanation h5 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.demo-explanation ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-explanation li {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.demo-explanation li strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.serverless-visualization {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.function-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.traffic-patterns {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="tech-stack-timeline-demo">
|
||||
<div class="demo-header">
|
||||
<h4>📚 技术栈演进时间线</h4>
|
||||
<p>每个时代的主流技术栈</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div
|
||||
v-for="(era, idx) in eras"
|
||||
:key="idx"
|
||||
class="era-section"
|
||||
:class="{ active: activeEra === idx }"
|
||||
@click="activeEra = idx"
|
||||
>
|
||||
<div class="era-marker">
|
||||
<div class="era-dot"></div>
|
||||
<div class="era-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="era-content">
|
||||
<div class="era-header">
|
||||
<span class="era-icon">{{ era.icon }}</span>
|
||||
<span class="era-name">{{ era.name }}</span>
|
||||
<span class="era-period">{{ era.period }}</span>
|
||||
</div>
|
||||
|
||||
<div class="tech-categories">
|
||||
<div class="category" v-for="(cat, cIdx) in era.categories" :key="cIdx">
|
||||
<div class="category-name">{{ cat.name }}</div>
|
||||
<div class="tech-tags">
|
||||
<span
|
||||
v-for="(tech, tIdx) in cat.techs"
|
||||
:key="tIdx"
|
||||
class="tech-tag"
|
||||
:class="{ highlight: tIdx === 0 }"
|
||||
>
|
||||
{{ tech }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeEra = ref(0)
|
||||
|
||||
const eras = [
|
||||
{
|
||||
icon: '🖥️',
|
||||
name: '物理机时代',
|
||||
period: '1990s',
|
||||
categories: [
|
||||
{ name: 'Web服务器', techs: ['Apache', 'Nginx', 'IIS'] },
|
||||
{ name: '后端语言', techs: ['Perl', 'PHP', 'ASP'] },
|
||||
{ name: '数据库', techs: ['MySQL', 'PostgreSQL', 'Oracle'] },
|
||||
{ name: '部署方式', techs: ['FTP', 'SSH', '手动'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: '🏢',
|
||||
name: '单体架构',
|
||||
period: '2000s',
|
||||
categories: [
|
||||
{ name: '后端框架', techs: ['Spring', 'Django', 'Rails', 'Laravel'] },
|
||||
{ name: '前端技术', techs: ['jQuery', 'Bootstrap', 'JSP'] },
|
||||
{ name: '数据库', techs: ['MySQL', 'Redis', 'MongoDB'] },
|
||||
{ name: '构建工具', techs: ['Maven', 'Gradle', 'Ant'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: '🏭',
|
||||
name: '微服务',
|
||||
period: '2010s',
|
||||
categories: [
|
||||
{ name: '容器化', techs: ['Docker', 'Kubernetes', 'Helm'] },
|
||||
{ name: '服务框架', techs: ['Spring Cloud', 'gRPC', 'Dubbo'] },
|
||||
{ name: '数据存储', techs: ['Redis', 'MongoDB', 'Kafka', 'ES'] },
|
||||
{ name: '可观测', techs: ['Prometheus', 'Grafana', 'Jaeger'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: '☁️',
|
||||
name: 'Serverless',
|
||||
period: '2020s+',
|
||||
categories: [
|
||||
{ name: '函数计算', techs: ['Lambda', 'Vercel', 'Cloudflare'] },
|
||||
{ name: 'BaaS', techs: ['Supabase', 'Firebase', 'Auth0'] },
|
||||
{ name: '前端框架', techs: ['Next.js', 'Nuxt', 'SvelteKit'] },
|
||||
{ name: '数据库', techs: ['PlanetScale', 'Neon', 'Turso'] }
|
||||
]
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tech-stack-timeline-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.era-section {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.era-section:hover,
|
||||
.era-section.active {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.era-marker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.era-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-divider);
|
||||
border: 2px solid var(--vp-c-bg);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.era-section.active .era-dot {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.era-line {
|
||||
flex: 1;
|
||||
width: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.era-content {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.5rem 0.5rem 0;
|
||||
}
|
||||
|
||||
.era-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.era-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.era-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.era-period {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tech-categories {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.category {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.tech-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.35rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 3px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.tech-tag.highlight {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tech-categories {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user