Files
test-repo/docs/.vitepress/theme/components/appendix/load-balancing/MultiRegionDemo.vue
T
sanbuphy 7c70c37072 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.
2026-02-06 03:34:50 +08:00

598 lines
14 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="multi-region-demo">
<div class="header">
<div class="title">多区域部署</div>
<div class="subtitle">异地多活架构就近服务与容灾备份</div>
</div>
<!-- 全球地图 -->
<div class="world-map">
<div class="map-header">
<span class="map-title">全球部署视图</span>
<span class="map-legend">
<span class="legend-item">
<span class="legend-dot active"></span>
主节点
</span>
<span class="legend-item">
<span class="legend-dot standby"></span>
备节点
</span>
</span>
</div>
<div class="map-container">
<!-- 简化的世界地图 -->
<div class="map-bg">
<!-- 亚洲 -->
<div class="continent asia">
<div
v-for="region in asiaRegions"
:key="region.id"
class="region-node"
:class="{
active: region.isPrimary,
standby: !region.isPrimary,
selected: selectedRegion === region.id
}"
:style="{ top: region.y + '%', left: region.x + '%' }"
@click="selectedRegion = region.id"
>
<div class="node-icon">{{ region.isPrimary ? '📡' : '📶' }}</div>
<div class="node-label">{{ region.name }}</div>
<div class="node-delay">{{ region.delay }}ms</div>
</div>
</div>
<!-- 欧洲 -->
<div class="continent europe">
<div
v-for="region in europeRegions"
:key="region.id"
class="region-node"
:class="{
active: region.isPrimary,
standby: !region.isPrimary,
selected: selectedRegion === region.id
}"
:style="{ top: region.y + '%', left: region.x + '%' }"
@click="selectedRegion = region.id"
>
<div class="node-icon">{{ region.isPrimary ? '📡' : '📶' }}</div>
<div class="node-label">{{ region.name }}</div>
<div class="node-delay">{{ region.delay }}ms</div>
</div>
</div>
<!-- 北美 -->
<div class="continent north-america">
<div
v-for="region in northAmericaRegions"
:key="region.id"
class="region-node"
:class="{
active: region.isPrimary,
standby: !region.isPrimary,
selected: selectedRegion === region.id
}"
:style="{ top: region.y + '%', left: region.x + '%' }"
@click="selectedRegion = region.id"
>
<div class="node-icon">{{ region.isPrimary ? '📡' : '📶' }}</div>
<div class="node-label">{{ region.name }}</div>
<div class="node-delay">{{ region.delay }}ms</div>
</div>
</div>
</div>
<!-- 连接线路 -->
<svg class="connection-lines" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<marker id="arrowhead" markerWidth="3" markerHeight="3" refX="2" refY="1.5" orient="auto">
<polygon points="0 0, 3 1.5, 0 3" fill="var(--vp-c-brand)" />
</marker>
</defs>
<line
v-for="(line, index) in connectionLines"
:key="index"
:x1="line.x1"
:y1="line.y1"
:x2="line.x2"
:y2="line.y2"
stroke="var(--vp-c-brand)"
stroke-width="0.3"
stroke-dasharray="2 1"
marker-end="url(#arrowhead)"
/>
</svg>
</div>
</div>
<!-- 区域详情 -->
<div class="region-details" v-if="selectedRegionData">
<div class="details-header">
<div class="region-title">
<span class="region-icon">{{ selectedRegionData.isPrimary ? '📡' : '📶' }}</span>
<span class="region-name">{{ selectedRegionData.name }}</span>
<span class="region-badge" :class="{ primary: selectedRegionData.isPrimary, standby: !selectedRegionData.isPrimary }">
{{ selectedRegionData.isPrimary ? '主节点' : '备节点' }}
</span>
</div>
<button class="close-btn" @click="selectedRegion = null">×</button>
</div>
<div class="details-grid">
<div class="detail-item">
<div class="detail-label">延迟</div>
<div class="detail-value">{{ selectedRegionData.delay }}ms</div>
</div>
<div class="detail-item">
<div class="detail-label">在线实例</div>
<div class="detail-value">{{ selectedRegionData.instances }}</div>
</div>
<div class="detail-item">
<div class="detail-label">当前QPS</div>
<div class="detail-value">{{ selectedRegionData.qps }}/s</div>
</div>
<div class="detail-item">
<div class="detail-label">数据同步延迟</div>
<div class="detail-value">{{ selectedRegionData.syncDelay }}ms</div>
</div>
</div>
<div class="details-actions">
<button class="action-btn primary" v-if="!selectedRegionData.isPrimary">
提升为主节点
</button>
<button class="action-btn danger" v-if="selectedRegionData.isPrimary">
切换流量
</button>
<button class="action-btn">
查看日志
</button>
</div>
</div>
<!-- 架构优势 -->
<div class="architecture-benefits">
<div class="benefits-title">多区域部署优势</div>
<div class="benefits-grid">
<div class="benefit-card">
<div class="benefit-icon"></div>
<div class="benefit-title">就近服务</div>
<div class="benefit-desc">用户请求自动路由到最近的区域降低网络延迟提升访问速度</div>
</div>
<div class="benefit-card">
<div class="benefit-icon">🛡</div>
<div class="benefit-title">容灾备份</div>
<div class="benefit-desc">单区域故障时自动切换流量确保服务高可用数据多副本保存</div>
</div>
<div class="benefit-card">
<div class="benefit-icon">🌍</div>
<div class="benefit-title">全球覆盖</div>
<div class="benefit-desc">支持跨区域部署满足不同地区的合规要求和数据主权法规</div>
</div>
<div class="benefit-card">
<div class="benefit-icon">📈</div>
<div class="benefit-title">负载均衡</div>
<div class="benefit-desc">跨区域流量调度避免单点过载实现全局资源优化配置</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
// 当前选中的指标
const currentMetric = ref('cpu')
const selectedRegion = ref(null)
// 亚洲区域数据
const asiaRegions = ref([
{ id: 'bj', name: '北京', x: 75, y: 35, isPrimary: true, delay: 20, instances: 5, qps: 2500, syncDelay: 10 },
{ id: 'sh', name: '上海', x: 80, y: 45, isPrimary: false, delay: 25, instances: 3, qps: 1500, syncDelay: 15 },
{ id: 'sg', name: '新加坡', x: 72, y: 65, isPrimary: false, delay: 45, instances: 2, qps: 800, syncDelay: 25 }
])
// 欧洲区域数据
const europeRegions = ref([
{ id: 'fr', name: '法兰克福', x: 48, y: 30, isPrimary: true, delay: 120, instances: 4, qps: 1800, syncDelay: 20 },
{ id: 'uk', name: '伦敦', x: 45, y: 25, isPrimary: false, delay: 130, instances: 2, qps: 900, syncDelay: 30 }
])
// 北美区域数据
const northAmericaRegions = ref([
{ id: 'usw', name: '硅谷', x: 15, y: 38, isPrimary: true, delay: 150, instances: 6, qps: 3200, syncDelay: 25 },
{ id: 'use', name: '弗吉尼亚', x: 28, y: 35, isPrimary: false, delay: 160, instances: 3, qps: 1400, syncDelay: 35 }
])
// 连接线数据
const connectionLines = ref([
// 北京-上海
{ x1: 75, y1: 35, x2: 80, y2: 45 },
// 北京-新加坡
{ x1: 75, y1: 35, x2: 72, y2: 65 },
// 法兰克福-伦敦
{ x1: 48, y1: 30, x2: 45, y2: 25 },
// 硅谷-弗吉尼亚
{ x1: 15, y1: 38, x2: 28, y2: 35 },
// 跨洲连接
{ x1: 75, y1: 35, x2: 48, y2: 30 },
{ x1: 48, y1: 30, x2: 15, y2: 38 }
])
// 选中区域详情
const selectedRegionData = computed(() => {
if (!selectedRegion.value) return null
const allRegions = [
...asiaRegions.value,
...europeRegions.value,
...northAmericaRegions.value
]
return allRegions.find(r => r.id === selectedRegion.value)
})
// 获取使用率颜色
const getUsageColor = (usage) => {
if (usage > 90) return '#ef4444'
if (usage > 70) return '#f59e0b'
return '#22c55e'
}
</script>
<style scoped>
.multi-region-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
/* World Map */
.world-map {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.map-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.map-title {
font-weight: 600;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.map-legend {
display: flex;
gap: 1rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.legend-dot.active {
background: var(--vp-c-brand);
}
.legend-dot.standby {
background: var(--vp-c-text-3);
}
.map-container {
position: relative;
width: 100%;
padding-bottom: 50%;
background: linear-gradient(135deg, #f0f4f8, #e2e8f0);
border-radius: 8px;
overflow: hidden;
}
.map-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.continent {
position: absolute;
width: 100%;
height: 100%;
}
.region-node {
position: absolute;
transform: translate(-50%, -50%);
background: white;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.5rem;
text-align: center;
cursor: pointer;
transition: all 0.3s;
min-width: 60px;
z-index: 10;
}
.region-node:hover {
transform: translate(-50%, -50%) scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.region-node.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.region-node.active .node-icon {
color: var(--vp-c-brand);
}
.region-node.standby {
opacity: 0.7;
}
.region-node.selected {
box-shadow: 0 0 0 3px var(--vp-c-brand-soft);
}
.node-icon {
font-size: 1.25rem;
margin-bottom: 0.2rem;
}
.node-label {
font-size: 0.7rem;
font-weight: 600;
color: var(--vp-c-text-1);
white-space: nowrap;
}
.node-delay {
font-size: 0.6rem;
color: var(--vp-c-text-2);
}
.connection-lines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
/* Region Details */
.region-details {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.details-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.region-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.region-icon {
font-size: 1.5rem;
}
.region-name {
font-weight: 600;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.region-badge {
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.region-badge.primary {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
}
.region-badge.standby {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.close-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 1.25rem;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
}
.close-btn:hover {
border-color: var(--vp-c-danger);
color: var(--vp-c-danger);
}
.details-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.details-grid {
grid-template-columns: 1fr;
}
}
.detail-item {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
}
.detail-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-value {
font-size: 1.25rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.details-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.action-btn {
padding: 0.5rem 1rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
border-color: var(--vp-c-brand);
}
.action-btn.primary {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.action-btn.danger {
background: var(--vp-c-danger);
color: white;
border-color: var(--vp-c-danger);
}
/* Architecture Benefits */
.architecture-benefits {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
}
.benefits-title {
font-weight: 600;
font-size: 1rem;
text-align: center;
margin-bottom: 1rem;
color: var(--vp-c-text-1);
}
.benefits-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
@media (max-width: 768px) {
.benefits-grid {
grid-template-columns: 1fr;
}
}
.benefit-card {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
text-align: center;
}
.benefit-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.benefit-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.benefit-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
</style>