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:
sanbuphy
2026-02-06 03:34:50 +08:00
parent e8bba6f7c0
commit 7c70c37072
171 changed files with 69830 additions and 6689 deletions
@@ -0,0 +1,580 @@
<template>
<div class="availability-zone-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="normal">正常运行</el-radio-button>
<el-radio-button label="az-failure"> AZ 故障</el-radio-button>
<el-radio-button label="maintenance">维护模式</el-radio-button>
<el-radio-button label="scaling">弹性扩容</el-radio-button>
</el-radio-group>
<el-switch
v-model="showTraffic"
active-text="显示流量"
style="margin-left: 20px"
/>
</div>
<!-- 架构图 -->
<div class="architecture-container">
<!-- 流量入口层 -->
<div class="layer entry-layer">
<div class="layer-title">🚪 流量入口层</div>
<div class="entry-components">
<div class="component dns">
<div class="component-icon">📖</div>
<div class="component-name">DNS 解析</div>
</div>
<div class="arrow"></div>
<div class="component cdn">
<div class="component-icon">🌐</div>
<div class="component-name">CDN 加速</div>
</div>
<div class="arrow"></div>
<div class="component waf">
<div class="component-icon">🛡</div>
<div class="component-name">WAF 防护</div>
</div>
</div>
</div>
<!-- 流量分发层 -->
<div class="layer distribution-layer">
<div class="layer-title"> 流量分发层 (SLB)</div>
<div class="slb-cluster"
:class="{ 'failover-active': viewMode === 'az-failure' }">
<div class="slb-instance primary"
:class="{ failed: viewMode === 'az-failure' }">
<div class="instance-header">
<span class="status-indicator"
:class="viewMode === 'az-failure' ? 'offline' : 'online'"></span>
<span class="instance-name">SLB-A ()</span>
</div>
<div class="instance-meta">可用区 A</div>
<!-- 流量动画 -->
<div class="traffic-flow" v-if="showTraffic && viewMode !== 'az-failure'">
<div class="flow-dot"></div>
</div>
</div>
<div class="failover-arrow" v-if="viewMode === 'az-failure'">
<span class="failover-text">故障转移</span>
<div class="arrow-line"></div>
</div>
<div class="slb-instance secondary"
:class="{ 'taking-over': viewMode === 'az-failure' }">
<div class="instance-header">
<span class="status-indicator"
:class="viewMode === 'az-failure' ? 'online' : 'standby'"></span>
<span class="instance-name">SLB-B ()</span>
</div>
<div class="instance-meta">可用区 B</div>
<div class="traffic-flow" v-if="showTraffic && viewMode === 'az-failure'">
<div class="flow-dot"></div>
</div>
</div>
</div>
</div>
<!-- 可用区层 -->
<div class="layer azs-layer">
<div class="layer-title">🏢 可用区层 (Multi-AZ)</div>
<div class="azs-grid">
<div
v-for="az in availabilityZones"
:key="az.id"
class="az-card"
:class="{
'az-a': az.id === 'az-a',
'az-b': az.id === 'az-b',
'az-c': az.id === 'az-c',
'degraded': viewMode === 'az-failure' && az.id === 'az-a',
'scaling': viewMode === 'scaling'
}"
>
<div class="az-header">
<div class="az-title">
<span class="az-name">{{ az.name }}</span>
<span class="az-id">{{ az.id }}</span>
</div>
<div class="az-status">
<span class="status-badge"
:class="getAzStatusClass(az)">
{{ getAzStatusText(az) }}
</span>
</div>
</div>
<div class="az-resources">
<div
v-for="resource in az.resources"
:key="resource.type"
class="resource-item"
>
<span class="resource-icon">{{ resource.icon }}</span>
<span class="resource-name">{{ resource.name }}</span>
<span class="resource-count">{{ resource.count }}</span>
</div>
</div>
<!-- 维护模式遮罩 -->
<div class="maintenance-overlay" v-if="viewMode === 'maintenance' && az.id === 'az-a'">
<div class="overlay-content">
<div class="overlay-icon">🔧</div>
<div class="overlay-text">维护中</div>
</div>
</div>
<!-- 弹性扩容动画 -->
<div class="scaling-indicator" v-if="viewMode === 'scaling'">
<div class="scaling-dot"></div>
<div class="scaling-text">扩容中</div>
</div>
</div>
</div>
</div>
</div>
<!-- 状态说明 -->
<div class="status-legend">
<div class="legend-title">状态说明</div>
<div class="legend-items">
<div class="legend-item">
<span class="legend-dot healthy"></span>
<span>健康运行</span>
</div>
<div class="legend-item">
<span class="legend-dot standby"></span>
<span>待机中</span>
</div>
<div class="legend-item">
<span class="legend-dot degraded"></span>
<span>降级/故障</span>
</div>
<div class="legend-item">
<span class="legend-dot maintenance"></span>
<span>维护中</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('normal')
const showTraffic = ref(false)
const availabilityZones = [
{
id: 'az-a',
name: '可用区 A',
resources: [
{ type: 'ecs', name: 'ECS 实例', icon: '🖥️', count: 8 },
{ type: 'rds', name: 'RDS 主库', icon: '🗄️', count: 1 },
{ type: 'redis', name: 'Redis 主库', icon: '📦', count: 1 },
{ type: 'slb', name: 'SLB 主', icon: '⚖️', count: 1 }
]
},
{
id: 'az-b',
name: '可用区 B',
resources: [
{ type: 'ecs', name: 'ECS 实例', icon: '🖥️', count: 6 },
{ type: 'rds', name: 'RDS 备库', icon: '🗄️', count: 1 },
{ type: 'redis', name: 'Redis 备库', icon: '📦', count: 1 },
{ type: 'slb', name: 'SLB 备', icon: '⚖️', count: 1 }
]
},
{
id: 'az-c',
name: '可用区 C',
resources: [
{ type: 'ecs', name: 'ECS 实例', icon: '🖥️', count: 4 },
{ type: 'slb', name: 'SLB 备', icon: '⚖️', count: 1 }
]
}
]
const getAzStatusClass = (az) => {
switch (viewMode.value) {
case 'az-failure':
return az.id === 'az-a' ? 'degraded' : 'healthy'
case 'maintenance':
return az.id === 'az-a' ? 'maintenance' : 'standby'
default:
return 'healthy'
}
}
const getAzStatusText = (az) => {
switch (viewMode.value) {
case 'az-failure':
return az.id === 'az-a' ? '故障中' : '接管中'
case 'maintenance':
return az.id === 'az-a' ? '维护中' : '待机中'
default:
return '正常运行'
}
}
</script>
<style scoped>
.availability-zone-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
}
.architecture-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.layer {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.layer-title {
font-size: 14px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Entry Layer */
.entry-components {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.component {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 16px;
background: #f5f7fa;
border-radius: 8px;
min-width: 80px;
}
.component-icon {
font-size: 24px;
}
.component-name {
font-size: 12px;
color: #606266;
font-weight: 500;
}
.arrow {
font-size: 20px;
color: #c0c4cc;
font-weight: bold;
}
/* AZs Layer */
.azs-layer {
background: transparent;
box-shadow: none;
padding: 0;
}
.azs-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.az-card {
background: white;
border-radius: 12px;
padding: 16px;
border: 2px solid #e4e7ed;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.az-card.az-a {
border-left: 4px solid #409eff;
}
.az-card.az-b {
border-left: 4px solid #67c23a;
}
.az-card.az-c {
border-left: 4px solid #e6a23c;
}
.az-card.degraded {
border-color: #f56c6c;
background: #fef0f0;
}
.az-card.maintenance {
border-color: #909399;
background: #f4f4f5;
}
.az-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.az-title {
display: flex;
align-items: center;
gap: 8px;
}
.az-name {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.az-id {
font-size: 11px;
padding: 2px 6px;
background: #f0f2f5;
border-radius: 4px;
color: #909399;
}
.status-badge {
font-size: 11px;
padding: 3px 8px;
border-radius: 10px;
font-weight: 500;
}
.status-badge.healthy {
background: #e1f3d8;
color: #67c23a;
}
.status-badge.standby {
background: #f4f4f5;
color: #909399;
}
.status-badge.degraded {
background: #fde2e2;
color: #f56c6c;
}
.status-badge.maintenance {
background: #e9e9eb;
color: #909399;
}
.az-resources {
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: #f5f7fa;
border-radius: 6px;
transition: all 0.2s;
}
.resource-item:hover {
background: #ecf5ff;
}
.resource-icon {
font-size: 16px;
}
.resource-name {
flex: 1;
font-size: 13px;
color: #606266;
}
.resource-count {
font-size: 12px;
padding: 2px 8px;
background: #409eff;
color: white;
border-radius: 10px;
font-weight: 500;
}
/* Maintenance Overlay */
.maintenance-overlay {
position: absolute;
inset: 0;
background: rgba(144, 147, 153, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.overlay-content {
text-align: center;
color: white;
}
.overlay-icon {
font-size: 48px;
margin-bottom: 8px;
}
.overlay-text {
font-size: 18px;
font-weight: 600;
}
/* Scaling Indicator */
.scaling-indicator {
position: absolute;
top: 12px;
right: 12px;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: #e6a23c;
border-radius: 12px;
}
.scaling-dot {
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
animation: pulse 1s infinite;
}
.scaling-text {
font-size: 11px;
color: white;
font-weight: 500;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.1); }
}
/* Status Legend */
.status-legend {
margin-top: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.legend-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.legend-items {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #606266;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.legend-dot.healthy {
background: #67c23a;
}
.legend-dot.standby {
background: #909399;
}
.legend-dot.degraded {
background: #f56c6c;
}
.legend-dot.maintenance {
background: #c0c4cc;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.entry-components {
flex-direction: column;
}
.arrow {
transform: rotate(90deg);
}
.slb-cluster {
flex-direction: column;
}
.failover-arrow {
transform: rotate(90deg);
margin: 12px 0;
}
}
</style>
@@ -0,0 +1,803 @@
<template>
<div class="compute-topology-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="overview">概览</el-radio-button>
<el-radio-button label="vm">虚拟机</el-radio-button>
<el-radio-button label="container">容器</el-radio-button>
<el-radio-button label="serverless">无服务器</el-radio-button>
</el-radio-group>
<el-switch v-model="showMetrics" active-text="显示指标" style="margin-left: 20px" />
</div>
<!-- 计算架构图 -->
<div class="compute-architecture">
<!-- 物理基础设施层 -->
<div class="layer physical-layer" v-if="viewMode === 'overview' || viewMode === 'vm'">
<div class="layer-header">
<span class="layer-icon">🏭</span>
<span class="layer-title">物理基础设施</span>
</div>
<div class="layer-content">
<div class="server-rack" v-for="rack in serverRacks" :key="rack.id">
<div class="rack-header">{{ rack.name }}</div>
<div class="rack-servers">
<div v-for="server in rack.servers" :key="server.id" class="server-node">
<div class="server-indicator" :class="server.status"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 虚拟化层 -->
<div class="layer virtualization-layer" v-if="viewMode === 'overview' || viewMode === 'vm'">
<div class="layer-header">
<span class="layer-icon">🔧</span>
<span class="layer-title">虚拟化层</span>
</div>
<div class="layer-content">
<div class="hypervisor-cluster">
<div class="hypervisor" v-for="hv in hypervisors" :key="hv.id">
<div class="hv-header">
<span class="hv-icon">🔨</span>
<span class="hv-name">{{ hv.name }}</span>
</div>
<div class="vms-list">
<div v-for="vm in hv.vms" :key="vm.id" class="vm-item">
<div class="vm-info">
<span class="vm-icon">💻</span>
<span class="vm-name">{{ vm.name }}</span>
</div>
<div class="vm-metrics" v-if="showMetrics">
<div class="metric">
<div class="metric-bar">
<div class="metric-fill" :style="{ width: vm.cpu + '%' }"></div>
</div>
<span class="metric-label">CPU</span>
</div>
<div class="metric">
<div class="metric-bar">
<div class="metric-fill memory" :style="{ width: vm.memory + '%' }"></div>
</div>
<span class="metric-label">内存</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 容器层 -->
<div class="layer container-layer" v-if="viewMode === 'overview' || viewMode === 'container'">
<div class="layer-header">
<span class="layer-icon">📦</span>
<span class="layer-title">容器编排层 (Kubernetes)</span>
</div>
<div class="layer-content">
<div class="k8s-cluster">
<!-- 控制平面 -->
<div class="control-plane">
<div class="cp-title">控制平面</div>
<div class="cp-components">
<div class="cp-comp" v-for="comp in controlPlaneComponents" :key="comp.name">
<div class="comp-icon">{{ comp.icon }}</div>
<div class="comp-name">{{ comp.name }}</div>
</div>
</div>
</div>
<!-- 工作节点 -->
<div class="worker-nodes">
<div class="nodes-title">工作节点</div>
<div class="nodes-grid">
<div class="node" v-for="node in workerNodes" :key="node.name">
<div class="node-header">
<span class="node-icon">🔧</span>
<span class="node-name">{{ node.name }}</span>
<span class="node-status" :class="node.status"></span>
</div>
<div class="pods-list">
<div class="pod" v-for="pod in node.pods" :key="pod.name">
<div class="pod-color" :style="{ background: pod.color }"></div>
<span class="pod-name">{{ pod.name }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 无服务器层 -->
<div class="layer serverless-layer" v-if="viewMode === 'overview' || viewMode === 'serverless'">
<div class="layer-header">
<span class="layer-icon"></span>
<span class="layer-title">无服务器计算 (Function Compute)</span>
</div>
<div class="layer-content">
<div class="serverless-arch">
<!-- 触发器 -->
<div class="triggers-section">
<div class="section-title">触发器</div>
<div class="triggers-list">
<div class="trigger" v-for="trigger in triggers" :key="trigger.name">
<div class="trigger-icon">{{ trigger.icon }}</div>
<div class="trigger-name">{{ trigger.name }}</div>
</div>
</div>
</div>
<!-- 函数计算 -->
<div class="functions-section">
<div class="section-title">函数计算实例</div>
<div class="functions-list">
<div class="function-card" v-for="func in functions" :key="func.name">
<div class="func-header">
<span class="func-icon"></span>
<span class="func-name">{{ func.name }}</span>
</div>
<div class="func-metrics" v-if="showMetrics">
<div class="metric-row">
<span class="metric-label">并发</span>
<div class="concurrency-bar">
<div class="concurrency-fill" :style="{ width: (func.concurrency / 100 * 100) + '%' }"></div>
</div>
<span class="metric-value">{{ func.concurrency }}</span>
</div>
<div class="metric-row">
<span class="metric-label">冷启动</span>
<span class="metric-value">{{ func.coldStart }}ms</span>
</div>
</div>
</div>
</div>
</div>
<!-- 后端服务 -->
<div class="backend-section">
<div class="section-title">后端服务</div>
<div class="backend-services">
<div class="service" v-for="svc in backendServices" :key="svc.name">
<div class="service-icon">{{ svc.icon }}</div>
<div class="service-name">{{ svc.name }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 说明 -->
<div class="architecture-legend">
<div class="legend-title">计算资源类型说明</div>
<div class="legend-items">
<div class="legend-item">
<span class="legend-icon">🔧</span>
<span class="legend-text">虚拟机 (ECS)完整 OS 控制适合传统应用</span>
</div>
<div class="legend-item">
<span class="legend-icon">📦</span>
<span class="legend-text">容器 (K8s)轻量级隔离适合微服务</span>
</div>
<div class="legend-item">
<span class="legend-icon"></span>
<span class="legend-text">无服务器 (FC)事件驱动按需付费</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('overview')
const showMetrics = ref(false)
// 物理服务器
const serverRacks = [
{
id: 'rack-1',
name: '机柜 A',
servers: Array(8).fill(null).map((_, i) => ({
id: `srv-a-${i}`,
status: i < 6 ? 'online' : 'offline'
}))
},
{
id: 'rack-2',
name: '机柜 B',
servers: Array(8).fill(null).map((_, i) => ({
id: `srv-b-${i}`,
status: i < 7 ? 'online' : 'standby'
}))
}
]
// 虚拟化层
const hypervisors = [
{
id: 'hv-1',
name: 'Hypervisor-01',
vms: [
{ id: 'vm-1', name: 'Web-01', cpu: 45, memory: 60 },
{ id: 'vm-2', name: 'Web-02', cpu: 32, memory: 45 },
{ id: 'vm-3', name: 'App-01', cpu: 67, memory: 78 }
]
},
{
id: 'hv-2',
name: 'Hypervisor-02',
vms: [
{ id: 'vm-4', name: 'Web-03', cpu: 28, memory: 35 },
{ id: 'vm-5', name: 'DB-01', cpu: 82, memory: 88 },
{ id: 'vm-6', name: 'Cache-01', cpu: 45, memory: 55 }
]
}
]
// K8s 控制平面
const controlPlaneComponents = [
{ name: 'API Server', icon: '🔌' },
{ name: 'etcd', icon: '📚' },
{ name: 'Scheduler', icon: '📅' },
{ name: 'Controller', icon: '🎮' }
]
// 工作节点
const workerNodes = [
{
name: 'Node-1',
status: 'ready',
pods: [
{ name: 'frontend-1', color: '#409eff' },
{ name: 'frontend-2', color: '#409eff' },
{ name: 'api-1', color: '#67c23a' }
]
},
{
name: 'Node-2',
status: 'ready',
pods: [
{ name: 'api-2', color: '#67c23a' },
{ name: 'worker-1', color: '#e6a23c' },
{ name: 'cache-1', color: '#f56c6c' }
]
},
{
name: 'Node-3',
status: 'ready',
pods: [
{ name: 'api-3', color: '#67c23a' },
{ name: 'worker-2', color: '#e6a23c' }
]
}
]
// Serverless 触发器
const triggers = [
{ name: 'HTTP 请求', icon: '🌐' },
{ name: '定时任务', icon: '⏰' },
{ name: 'OSS 事件', icon: '📦' },
{ name: '消息队列', icon: '📨' }
]
// 函数列表
const functions = [
{ name: 'user-service', runtime: 'Node.js', concurrency: 45, coldStart: 120 },
{ name: 'order-processor', runtime: 'Python', concurrency: 32, coldStart: 85 },
{ name: 'image-resizer', runtime: 'Go', concurrency: 18, coldStart: 45 }
]
// 后端服务
const backendServices = [
{ name: 'API 网关', icon: '🚪' },
{ name: '对象存储', icon: '🪣' },
{ name: '数据库', icon: '🗄️' },
{ name: '缓存', icon: '⚡' }
]
</script>
<style scoped>
.compute-topology-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.compute-architecture {
display: flex;
flex-direction: column;
gap: 16px;
}
.layer {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.layer-title {
font-size: 14px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Physical Layer */
.layer-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.server-rack {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
}
.rack-header {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
}
.rack-servers {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
}
.server-node {
aspect-ratio: 1;
background: #dcdfe6;
border-radius: 4px;
position: relative;
}
.server-indicator {
position: absolute;
inset: 2px;
border-radius: 2px;
}
.server-indicator.online {
background: #67c23a;
}
.server-indicator.offline {
background: #f56c6c;
}
.server-indicator.standby {
background: #e6a23c;
}
/* Hypervisor Layer */
.hypervisor-cluster {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.hypervisor {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
}
.hv-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.hv-icon {
font-size: 18px;
}
.hv-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.vms-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.vm-item {
background: white;
border-radius: 6px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.vm-info {
display: flex;
align-items: center;
gap: 6px;
}
.vm-icon {
font-size: 14px;
}
.vm-name {
font-size: 13px;
color: #606266;
font-weight: 500;
}
.vm-metrics {
display: flex;
flex-direction: column;
gap: 6px;
}
.metric {
display: flex;
align-items: center;
gap: 6px;
}
.metric-bar {
flex: 1;
height: 4px;
background: #e4e7ed;
border-radius: 2px;
overflow: hidden;
}
.metric-fill {
height: 100%;
background: #409eff;
border-radius: 2px;
transition: width 0.3s;
}
.metric-fill.memory {
background: #67c23a;
}
.metric-label {
font-size: 11px;
color: #909399;
width: 40px;
}
/* Container Layer */
.k8s-cluster {
display: flex;
flex-direction: column;
gap: 16px;
}
.control-plane {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 12px;
}
.cp-title {
font-size: 13px;
font-weight: 600;
color: #0369a1;
margin-bottom: 10px;
}
.cp-components {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.cp-comp {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: white;
border-radius: 6px;
border: 1px solid #e0f2fe;
}
.comp-icon {
font-size: 14px;
}
.comp-name {
font-size: 12px;
color: #0c4a6e;
}
.worker-nodes {
display: flex;
flex-direction: column;
gap: 12px;
}
.nodes-title {
font-size: 13px;
font-weight: 600;
color: #606266;
}
.nodes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.node {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
}
.node-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e2e8f0;
}
.node-icon {
font-size: 14px;
}
.node-name {
flex: 1;
font-size: 13px;
font-weight: 500;
color: #334155;
}
.node-status {
width: 8px;
height: 8px;
border-radius: 50%;
}
.node-status.ready {
background: #22c55e;
}
.pods-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.pod {
display: flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: white;
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.pod-color {
width: 8px;
height: 8px;
border-radius: 50%;
}
.pod-name {
font-size: 11px;
color: #64748b;
}
/* Serverless Layer */
.serverless-arch {
display: flex;
flex-direction: column;
gap: 16px;
}
.triggers-section,
.functions-section,
.backend-section {
background: #fafafa;
border-radius: 8px;
padding: 12px;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 10px;
}
.triggers-list,
.backend-services {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.trigger,
.service {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: white;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.trigger-icon,
.service-icon {
font-size: 16px;
}
.trigger-name,
.service-name {
font-size: 12px;
color: #606266;
}
.functions-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.function-card {
background: white;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
}
.func-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f2f5;
}
.func-icon {
font-size: 14px;
}
.func-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.func-metrics {
display: flex;
flex-direction: column;
gap: 8px;
}
.metric-row {
display: flex;
align-items: center;
gap: 8px;
}
.metric-row .metric-label {
width: 60px;
font-size: 11px;
}
.concurrency-bar {
flex: 1;
height: 4px;
background: #e4e7ed;
border-radius: 2px;
overflow: hidden;
}
.concurrency-fill {
height: 100%;
background: #67c23a;
border-radius: 2px;
transition: width 0.3s;
}
.metric-value {
font-size: 11px;
color: #909399;
width: 40px;
text-align: right;
}
/* Status Legend */
.architecture-legend {
margin-top: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.legend-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.legend-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: #f5f7fa;
border-radius: 6px;
}
.legend-icon {
font-size: 20px;
}
.legend-text {
font-size: 13px;
color: #606266;
line-height: 1.4;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.hypervisor-cluster,
.nodes-grid,
.functions-list {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,764 @@
<template>
<div class="disaster-recovery-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="drMode" size="small">
<el-radio-button label="same-city">同城双活</el-radio-button>
<el-radio-button label="remote">异地灾备</el-radio-button>
<el-radio-button label="three-center">两地三中心</el-radio-button>
<el-radio-button label="switchover">故障切换</el-radio-button>
</el-radio-group>
<el-switch v-model="showRPO" active-text="显示 RPO/RTO" style="margin-left: 20px" />
</div>
<!-- 灾备架构图 -->
<div class="dr-architecture">
<!-- 生产中心 -->
<div class="dr-center production" :class="{ degraded: drMode === 'switchover' && switchoverStep >= 2 }">
<div class="center-header">
<div class="center-badge production">生产</div>
<div class="center-title">生产中心 (Region A)</div>
<div class="center-location">📍 北京</div>
</div>
<div class="center-content">
<!-- 可用区 A -->
<div class="az-block" :class="{ failed: drMode === 'switchover' && switchoverStep >= 1 }">
<div class="az-header">
<span class="az-name">可用区 A</span>
<span class="az-status" :class="getAzStatus('A')">{{ getAzStatusText('A') }}</span>
</div>
<div class="az-resources">
<div class="resource-group">
<div class="group-title">计算</div>
<div class="resource-tags">
<span class="tag">ECS × 8</span>
<span class="tag primary">SLB </span>
</div>
</div>
<div class="resource-group">
<div class="group-title">数据库</div>
<div class="resource-tags">
<span class="tag primary">RDS </span>
<span class="tag">Redis </span>
</div>
</div>
</div>
</div>
<!-- 可用区 B (同城灾备) -->
<div class="az-block standby" v-if="drMode !== 'remote'">
<div class="az-header">
<span class="az-name">可用区 B</span>
<span class="az-status standby">热备</span>
</div>
<div class="az-resources">
<div class="resource-group">
<div class="group-title">计算</div>
<div class="resource-tags">
<span class="tag">ECS × 6</span>
<span class="tag standby">SLB </span>
</div>
</div>
<div class="resource-group">
<div class="group-title">数据库</div>
<div class="resource-tags">
<span class="tag standby">RDS </span>
<span class="tag">Redis </span>
</div>
</div>
</div>
</div>
</div>
<!-- RPO/RTO 指示器 -->
<div class="rpo-indicator" v-if="showRPO">
<div class="rpo-item">
<span class="rpo-label">RPO</span>
<span class="rpo-value">{{ getRPO() }}</span>
</div>
<div class="rpo-item">
<span class="rpo-label">RTO</span>
<span class="rpo-value">{{ getRTO() }}</span>
</div>
</div>
</div>
<!-- 复制链路 -->
<div class="replication-links">
<div class="link-group same-city" v-if="drMode === 'same-city' || drMode === 'three-center'">
<div class="link-line"></div>
<div class="link-label">同步复制</div>
<div class="link-bandwidth">延迟 &lt; 5ms</div>
</div>
<div class="link-group remote" v-if="drMode === 'remote' || drMode === 'three-center'">
<div class="link-line async"></div>
<div class="link-label">异步复制</div>
<div class="link-bandwidth">RPO 5s</div>
</div>
</div>
<!-- 灾备中心 -->
<div class="dr-center disaster-recovery" :class="{ active: drMode === 'switchover' && switchoverStep >= 2 }">
<div class="center-header">
<div class="center-badge dr">灾备</div>
<div class="center-title">灾备中心 (Region B)</div>
<div class="center-location">📍 {{ drMode === 'same-city' ? '北京 (可用区 C)' : '上海' }}</div>
</div>
<div class="center-content">
<div class="az-block dr-standby" :class="{ promoted: drMode === 'switchover' && switchoverStep >= 3 }">
<div class="az-header">
<span class="az-name">{{ drMode === 'same-city' ? '可用区 C' : '可用区 A' }}</span>
<span class="az-status" :class="getDrAzStatus()">{{ getDrAzStatusText() }}</span>
</div>
<div class="az-resources">
<div class="resource-group">
<div class="group-title">计算</div>
<div class="resource-tags">
<span class="tag">ECS × 4</span>
<span :class="['tag', drMode === 'switchover' && switchoverStep >= 3 ? 'primary' : 'standby']">
SLB {{ drMode === 'switchover' && switchoverStep >= 3 ? '主' : '备' }}
</span>
</div>
</div>
<div class="resource-group">
<div class="group-title">数据库</div>
<div class="resource-tags">
<span :class="['tag', drMode === 'switchover' && switchoverStep >= 3 ? 'primary' : 'standby']">
RDS {{ drMode === 'switchover' && switchoverStep >= 3 ? '主' : '备' }}
</span>
<span class="tag">Redis </span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 切换进度 (仅在故障切换模式显示) -->
<div class="switchover-progress" v-if="drMode === 'switchover'">
<div class="progress-title">故障切换进度</div>
<el-steps :active="switchoverStep" finish-status="success">
<el-step title="检测故障" description="监控系统发现可用区 A 故障" />
<el-step title="停止写入" description="切离主库,暂停业务写入" />
<el-step title="提升备库" description="灾备中心数据库提升为主库" />
<el-step title="流量切换" description="DNS 切换到灾备中心 SLB" />
<el-step title="恢复服务" description="业务在灾备中心正常运行" />
</el-steps>
<div class="progress-actions">
<el-button @click="prevStep" :disabled="switchoverStep === 0">上一步</el-button>
<el-button type="primary" @click="nextStep" :disabled="switchoverStep === 5">下一步</el-button>
<el-button @click="resetSwitchover">重置</el-button>
</div>
</div>
<!-- 架构对比表 -->
<div class="comparison-section">
<div class="comparison-title">📊 灾备架构方案对比</div>
<div class="comparison-table">
<div class="table-header">
<div class="header-cell">对比维度</div>
<div class="header-cell">同城双活</div>
<div class="header-cell">异地灾备</div>
<div class="header-cell">两地三中心</div>
</div>
<div class="table-row" v-for="row in drComparisonData" :key="row.dimension">
<div class="cell dimension">{{ row.dimension }}</div>
<div class="cell">{{ row.sameCity }}</div>
<div class="cell">{{ row.remote }}</div>
<div class="cell highlight">{{ row.threeCenter }}</div>
</div>
</div>
</div>
<!-- RPO/RTO 说明 -->
<div class="rpo-rto-explanation">
<div class="explanation-title">💡 RPO RTO 说明</div>
<div class="explanation-grid">
<div class="explanation-card">
<div class="card-icon"></div>
<div class="card-title">RPO (恢复点目标)</div>
<div class="card-desc">可接受的数据丢失量即最后一次备份到故障发生的时间间隔</div>
<div class="card-example">示例RPO = 5意味着最多丢失5秒的数据</div>
</div>
<div class="explanation-card">
<div class="card-icon">🔄</div>
<div class="card-title">RTO (恢复时间目标)</div>
<div class="card-desc">从故障发生到业务恢复所需的最长时间</div>
<div class="card-example">示例RTO = 30分钟意味着30分钟内必须恢复服务</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const drMode = ref('same-city')
const showRPO = ref(false)
const switchoverStep = ref(0)
// 获取可用区状态
const getAzStatus = (az) => {
if (drMode.value === 'switchover' && switchoverStep.value >= 1 && az === 'A') {
return 'failed'
}
return 'running'
}
const getAzStatusText = (az) => {
if (drMode.value === 'switchover' && switchoverStep.value >= 1 && az === 'A') {
return '故障'
}
return '运行中'
}
const getDrAzStatus = () => {
if (drMode.value === 'switchover' && switchoverStep.value >= 3) {
return 'promoted'
}
return 'standby'
}
const getDrAzStatusText = () => {
if (drMode.value === 'switchover' && switchoverStep.value >= 3) {
return '主库'
}
return '冷备'
}
const getRPO = () => {
switch (drMode.value) {
case 'same-city': return '0 (同步复制)'
case 'remote': return '~5s (异步复制)'
case 'three-center': return '0 (同城) / ~5s (异地)'
default: return '-'
}
}
const getRTO = () => {
switch (drMode.value) {
case 'same-city': return '~5min'
case 'remote': return '~30min'
case 'three-center': return '~5min (同城) / ~30min (异地)'
default: return '-'
}
}
const nextStep = () => {
if (switchoverStep.value < 5) {
switchoverStep.value++
}
}
const prevStep = () => {
if (switchoverStep.value > 0) {
switchoverStep.value--
}
}
const resetSwitchover = () => {
switchoverStep.value = 0
}
// 灾备对比数据
const drComparisonData = [
{ dimension: '部署成本', sameCity: '中等', remote: '较低', threeCenter: '高' },
{ dimension: '运维复杂度', sameCity: '中等', remote: '低', threeCenter: '高' },
{ dimension: '数据保护', sameCity: 'RPO=0', remote: 'RPO~5s', threeCenter: '最全面' },
{ dimension: '恢复速度', sameCity: '~5分钟', remote: '~30分钟', threeCenter: '分层恢复' },
{ dimension: '适用场景', sameCity: '金融核心', remote: '中小企业', threeCenter: '大型核心' }
]
</script>
<style scoped>
.disaster-recovery-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.dr-architecture {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
.dr-center {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border: 2px solid transparent;
transition: all 0.3s;
}
.dr-center.production {
border-color: #409eff;
}
.dr-center.production.degraded {
border-color: #f56c6c;
background: #fef0f0;
}
.dr-center.disaster-recovery {
border-color: #67c23a;
}
.dr-center.disaster-recovery.active {
border-color: #409eff;
background: #ecf5ff;
}
.center-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.center-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
color: white;
}
.center-badge.production {
background: #409eff;
}
.center-badge.dr {
background: #67c23a;
}
.center-title {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
}
.center-location {
font-size: 13px;
color: #909399;
}
.center-content {
display: flex;
flex-direction: column;
gap: 12px;
}
/* AZ Block */
.az-block {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
border-left: 4px solid #409eff;
transition: all 0.3s;
}
.az-block.failed {
border-left-color: #f56c6c;
background: #fef0f0;
}
.az-block.standby {
border-left-color: #67c23a;
background: #f0f9eb;
}
.az-block.dr-standby {
border-left-color: #e6a23c;
background: #fdf6ec;
}
.az-block.dr-standby.promoted {
border-left-color: #409eff;
background: #ecf5ff;
}
.az-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.az-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.az-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.az-status.running {
background: #e1f3d8;
color: #67c23a;
}
.az-status.failed {
background: #fde2e2;
color: #f56c6c;
}
.az-status.standby {
background: #e1f3d8;
color: #67c23a;
}
.az-status.promoted {
background: #ecf5ff;
color: #409eff;
}
.az-resources {
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-group {
display: flex;
align-items: center;
gap: 8px;
}
.group-title {
font-size: 12px;
color: #909399;
width: 50px;
}
.resource-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1;
}
.tag {
font-size: 11px;
padding: 2px 8px;
background: #e4e7ed;
border-radius: 10px;
color: #606266;
}
.tag.primary {
background: #409eff;
color: white;
}
.tag.standby {
background: #67c23a;
color: white;
}
/* RPO Indicator */
.rpo-indicator {
display: flex;
gap: 16px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #dcdfe6;
}
.rpo-item {
display: flex;
align-items: center;
gap: 6px;
}
.rpo-label {
font-size: 11px;
color: #909399;
text-transform: uppercase;
}
.rpo-value {
font-size: 13px;
font-weight: 600;
color: #409eff;
}
/* Replication Links */
.replication-links {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
}
.link-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
width: 100%;
}
.link-line {
width: 80%;
height: 3px;
background: linear-gradient(90deg, #409eff, #67c23a);
border-radius: 2px;
position: relative;
}
.link-line::before,
.link-line::after {
content: '';
position: absolute;
top: 50%;
width: 8px;
height: 8px;
background: #409eff;
border-radius: 50%;
transform: translateY(-50%);
}
.link-line::before {
left: -4px;
}
.link-line::after {
right: -4px;
background: #67c23a;
}
.link-line.async {
background: linear-gradient(90deg, #409eff, #e6a23c);
background-image: repeating-linear-gradient(
90deg,
transparent,
transparent 10px,
rgba(255, 255, 255, 0.3) 10px,
rgba(255, 255, 255, 0.3) 20px
);
}
.link-label {
font-size: 12px;
font-weight: 600;
color: #606266;
}
.link-bandwidth {
font-size: 11px;
color: #909399;
}
/* Switchover Progress */
.switchover-progress {
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.progress-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
}
.progress-actions {
margin-top: 20px;
display: flex;
gap: 12px;
justify-content: center;
}
/* Comparison Section */
.comparison-section {
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.comparison-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
}
.comparison-table {
overflow-x: auto;
}
.table-header {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 1px;
background: #e4e7ed;
border-radius: 8px 8px 0 0;
overflow: hidden;
}
.header-cell {
padding: 12px;
background: #f5f7fa;
font-size: 13px;
font-weight: 600;
color: #606266;
text-align: center;
}
.table-row {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 1px;
background: #e4e7ed;
border-bottom: 1px solid #e4e7ed;
}
.table-row:last-child {
border-radius: 0 0 8px 8px;
overflow: hidden;
border-bottom: none;
}
.cell {
padding: 10px 12px;
background: white;
font-size: 12px;
color: #606266;
text-align: center;
}
.cell.dimension {
text-align: left;
font-weight: 500;
color: #303133;
background: #fafafa;
}
.cell.highlight {
font-weight: 600;
color: #67c23a;
}
/* RPO/RTO Explanation */
.rpo-rto-explanation {
margin-top: 20px;
padding: 20px;
background: #f0f9ff;
border-radius: 12px;
border-left: 4px solid #409eff;
}
.explanation-title {
font-size: 16px;
font-weight: 600;
color: #0969da;
margin-bottom: 16px;
}
.explanation-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.explanation-card {
background: white;
border-radius: 8px;
padding: 16px;
}
.card-icon {
font-size: 32px;
margin-bottom: 8px;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.card-desc {
font-size: 13px;
color: #606266;
line-height: 1.5;
margin-bottom: 8px;
}
.card-example {
font-size: 12px;
color: #909399;
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.center-content {
flex-direction: column;
}
.comparison-table {
font-size: 11px;
}
.table-header,
.table-row {
grid-template-columns: 80px repeat(3, 1fr);
}
}
</style>
@@ -0,0 +1,651 @@
<template>
<div class="network-flow-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="flowMode" size="small">
<el-radio-button label="inbound">入向流量</el-radio-button>
<el-radio-button label="outbound">出向流量</el-radio-button>
<el-radio-button label="east-west">东西向流量</el-radio-button>
<el-radio-button label="full">完整拓扑</el-radio-button>
</el-radio-group>
<el-switch v-model="showMetrics" active-text="显示流量数据" style="margin-left: 20px" />
</div>
<!-- 网络拓扑图 -->
<div class="network-topology">
<!-- 互联网区域 -->
<div class="zone internet-zone" v-if="showInternet">
<div class="zone-header">
<span class="zone-icon">🌐</span>
<span class="zone-title">互联网 (Internet)</span>
</div>
<div class="zone-content">
<div class="internet-entities">
<div class="entity" v-for="entity in internetEntities" :key="entity.name">
<div class="entity-icon">{{ entity.icon }}</div>
<div class="entity-name">{{ entity.name }}</div>
</div>
</div>
</div>
</div>
<!-- 流量箭头 -->
<div class="flow-arrows" v-if="showFlowArrows">
<div class="arrow-container">
<div class="flow-line" :class="flowMode"></div>
<div class="flow-particles" v-if="showMetrics">
<div class="particle" v-for="n in 5" :key="n"
:style="{ animationDelay: (n * 0.5) + 's' }"></div>
</div>
</div>
<div class="flow-stats" v-if="showMetrics">
<div class="stat-item">
<div class="stat-label">带宽</div>
<div class="stat-value">2.5 Gbps</div>
</div>
<div class="stat-item">
<div class="stat-label">流量</div>
<div class="stat-value">1.2 TB/</div>
</div>
<div class="stat-item">
<div class="stat-label">延迟</div>
<div class="stat-value">15 ms</div>
</div>
</div>
</div>
<!-- VPC 区域 -->
<div class="zone vpc-zone">
<div class="zone-header">
<span class="zone-icon">🏠</span>
<span class="zone-title">VPC 网络 (172.16.0.0/12)</span>
</div>
<div class="zone-content">
<!-- 网络设备层 -->
<div class="network-devices">
<div class="device" v-for="device in networkDevices" :key="device.name"
:class="device.type">
<div class="device-icon">{{ device.icon }}</div>
<div class="device-name">{{ device.name }}</div>
<div class="device-stats" v-if="showMetrics">
<div class="stat">
<span class="stat-label">吞吐</span>
<span class="stat-value">{{ device.throughput }}</span>
</div>
<div class="stat">
<span class="stat-label">并发</span>
<span class="stat-value">{{ device.connections }}</span>
</div>
</div>
</div>
</div>
<!-- 子网层 -->
<div class="subnets-container">
<div class="subnet" v-for="subnet in subnets" :key="subnet.name"
:class="subnet.type">
<div class="subnet-header">
<span class="subnet-type-icon">{{ subnet.type === 'public' ? '🌐' : '🔒' }}</span>
<span class="subnet-name">{{ subnet.name }}</span>
<span class="subnet-cidr">{{ subnet.cidr }}</span>
</div>
<div class="subnet-resources">
<div class="resource" v-for="resource in subnet.resources" :key="resource.name">
<div class="resource-icon">{{ resource.icon }}</div>
<div class="resource-info">
<div class="resource-name">{{ resource.name }}</div>
<div class="resource-ip">{{ resource.ip }}</div>
</div>
<div class="resource-traffic" v-if="showMetrics">
<div class="traffic-in">
<span class="traffic-label"></span>
<span class="traffic-value">{{ resource.inTraffic }}</span>
</div>
<div class="traffic-out">
<span class="traffic-label"></span>
<span class="traffic-value">{{ resource.outTraffic }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 图例说明 -->
<div class="network-legend">
<div class="legend-title">流量类型说明</div>
<div class="legend-items">
<div class="legend-item">
<span class="legend-color inbound"></span>
<span>入向流量用户 服务器</span>
</div>
<div class="legend-item">
<span class="legend-color outbound"></span>
<span>出向流量服务器 外部</span>
</div>
<div class="legend-item">
<span class="legend-color east-west"></span>
<span>东西向流量服务间通信</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const flowMode = ref('inbound')
const showMetrics = ref(false)
// 显示互联网
const showInternet = computed(() => {
return ['inbound', 'outbound', 'full'].includes(flowMode.value)
})
// 显示流量箭头
const showFlowArrows = computed(() => {
return ['inbound', 'outbound', 'east-west', 'full'].includes(flowMode.value)
})
// 互联网实体
const internetEntities = [
{ name: '移动用户', icon: '📱' },
{ name: 'PC 用户', icon: '💻' },
{ name: '企业网络', icon: '🏢' },
{ name: '第三方 API', icon: '🔗' }
]
// 网络设备
const networkDevices = [
{ name: 'Internet Gateway', icon: '🌐', type: 'igw', throughput: '10 Gbps', connections: '10M' },
{ name: 'NAT Gateway', icon: '🔄', type: 'nat', throughput: '5 Gbps', connections: '1M' },
{ name: 'Load Balancer', icon: '⚖️', type: 'slb', throughput: '8 Gbps', connections: '500K' },
{ name: 'VPN Gateway', icon: '🔒', type: 'vpn', throughput: '1 Gbps', connections: '1K' }
]
// 子网
const subnets = [
{
name: 'Public-Subnet-A',
type: 'public',
cidr: '172.16.1.0/24',
resources: [
{ name: 'Nginx-LB-01', icon: '⚖️', ip: '172.16.1.10', inTraffic: '850 MB/s', outTraffic: '2.1 GB/s' },
{ name: 'Nginx-LB-02', icon: '⚖️', ip: '172.16.1.11', inTraffic: '780 MB/s', outTraffic: '1.9 GB/s' },
{ name: 'Bastion-Host', icon: '🔧', ip: '172.16.1.20', inTraffic: '5 MB/s', outTraffic: '12 MB/s' }
]
},
{
name: 'Private-Subnet-A',
type: 'private',
cidr: '172.16.2.0/24',
resources: [
{ name: 'App-Server-01', icon: '💻', ip: '172.16.2.10', inTraffic: '450 MB/s', outTraffic: '320 MB/s' },
{ name: 'App-Server-02', icon: '💻', ip: '172.16.2.11', inTraffic: '420 MB/s', outTraffic: '290 MB/s' },
{ name: 'App-Server-03', icon: '💻', ip: '172.16.2.12', inTraffic: '380 MB/s', outTraffic: '260 MB/s' }
]
},
{
name: 'Data-Subnet-A',
type: 'private',
cidr: '172.16.3.0/24',
resources: [
{ name: 'MySQL-Primary', icon: '🗄️', ip: '172.16.3.10', inTraffic: '120 MB/s', outTraffic: '180 MB/s' },
{ name: 'MySQL-Replica', icon: '🗄️', ip: '172.16.3.11', inTraffic: '80 MB/s', outTraffic: '95 MB/s' },
{ name: 'Redis-Cluster', icon: '⚡', ip: '172.16.3.20', inTraffic: '45 MB/s', outTraffic: '68 MB/s' }
]
}
]
</script>
<style scoped>
.network-flow-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.network-topology {
display: flex;
flex-direction: column;
gap: 16px;
}
.zone {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.zone-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.zone-icon {
font-size: 20px;
}
.zone-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
/* Internet Zone */
.internet-entities {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
}
.entity {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 16px;
background: #f5f7fa;
border-radius: 8px;
min-width: 80px;
}
.entity-icon {
font-size: 24px;
}
.entity-name {
font-size: 12px;
color: #606266;
}
/* Flow Arrows */
.flow-arrows {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px;
}
.arrow-container {
position: relative;
width: 100%;
height: 40px;
}
.flow-line {
position: absolute;
top: 50%;
left: 10%;
right: 10%;
height: 4px;
background: #e4e7ed;
border-radius: 2px;
transform: translateY(-50%);
}
.flow-line.inbound {
background: linear-gradient(to right, #409eff, #67c23a);
}
.flow-line.outbound {
background: linear-gradient(to left, #409eff, #e6a23c);
}
.flow-line.east-west {
background: linear-gradient(to right, #67c23a, #409eff, #67c23a);
}
.flow-line.full {
background: linear-gradient(90deg, #409eff, #67c23a, #e6a23c, #f56c6c);
}
.flow-particles {
position: absolute;
inset: 0;
}
.particle {
position: absolute;
top: 50%;
left: 10%;
width: 8px;
height: 8px;
background: #409eff;
border-radius: 50%;
transform: translateY(-50%);
animation: flow 2s linear infinite;
}
@keyframes flow {
0% {
left: 10%;
opacity: 1;
}
100% {
left: 90%;
opacity: 0;
}
}
.flow-stats {
display: flex;
gap: 24px;
justify-content: center;
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 11px;
color: #909399;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #409eff;
}
/* VPC Zone */
.vpc-zone {
border: 2px solid #409eff;
}
/* Network Devices */
.network-devices {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px dashed #e4e7ed;
}
.device {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
min-width: 100px;
border: 2px solid transparent;
transition: all 0.3s;
}
.device:hover {
border-color: #409eff;
transform: translateY(-2px);
}
.device.igw {
border-color: #409eff;
background: #ecf5ff;
}
.device.nat {
border-color: #67c23a;
background: #f0f9eb;
}
.device.slb {
border-color: #e6a23c;
background: #fdf6ec;
}
.device.vpn {
border-color: #909399;
background: #f4f4f5;
}
.device-icon {
font-size: 24px;
}
.device-name {
font-size: 12px;
font-weight: 500;
color: #303133;
}
.device-stats {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
padding-top: 6px;
border-top: 1px solid #e4e7ed;
}
.stat {
display: flex;
justify-content: space-between;
font-size: 10px;
}
.stat-label {
color: #909399;
}
.stat-value {
color: #409eff;
font-weight: 500;
}
/* Subnets */
.subnets-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.subnet {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
border-left: 4px solid;
}
.subnet.public {
border-left-color: #409eff;
}
.subnet.private {
border-left-color: #67c23a;
}
.subnet-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.subnet-type-icon {
font-size: 14px;
}
.subnet-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.subnet-cidr {
font-size: 11px;
padding: 2px 8px;
background: #e4e7ed;
border-radius: 10px;
color: #606266;
font-family: monospace;
}
.subnet-resources {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.resource {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: white;
border-radius: 6px;
border: 1px solid #e4e7ed;
flex: 1;
min-width: 200px;
}
.resource-icon {
font-size: 18px;
}
.resource-info {
flex: 1;
}
.resource-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.resource-ip {
font-size: 11px;
color: #909399;
font-family: monospace;
}
.resource-traffic {
display: flex;
flex-direction: column;
gap: 2px;
padding-left: 8px;
border-left: 1px solid #e4e7ed;
}
.traffic-in,
.traffic-out {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
}
.traffic-label {
color: #909399;
font-weight: 600;
}
.traffic-value {
color: #409eff;
font-weight: 500;
}
/* Network Legend */
.network-legend {
margin-top: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.legend-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.legend-items {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #606266;
}
.legend-color {
width: 20px;
height: 4px;
border-radius: 2px;
}
.legend-color.inbound {
background: linear-gradient(to right, #409eff, #67c23a);
}
.legend-color.outbound {
background: linear-gradient(to right, #409eff, #e6a23c);
}
.legend-color.east-west {
background: linear-gradient(to right, #67c23a, #409eff, #67c23a);
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.network-devices {
justify-content: center;
}
.resource {
min-width: 100%;
}
.flow-stats {
flex-direction: column;
gap: 12px;
}
}
</style>
@@ -0,0 +1,400 @@
<template>
<div class="resource-topology-demo">
<div class="controls">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="overview">全景视图</el-radio-button>
<el-radio-button label="compute">计算视角</el-radio-button>
<el-radio-button label="network">网络视角</el-radio-button>
<el-radio-button label="storage">存储视角</el-radio-button>
</el-radio-group>
</div>
<div class="topology-container" ref="topologyRef">
<!-- 云服务商层 -->
<div class="layer cloud-provider">
<div class="layer-title"> 云服务商</div>
<div class="provider-grid">
<div v-for="provider in providers" :key="provider.name"
class="provider-card"
:class="{ active: selectedProvider === provider.name }"
@click="selectProvider(provider.name)">
<div class="provider-icon">{{ provider.icon }}</div>
<div class="provider-name">{{ provider.name }}</div>
</div>
</div>
</div>
<!-- 连接箭头 -->
<div class="connection-arrow"></div>
<!-- 地域/可用区层 -->
<div class="layer region-layer">
<div class="layer-title">🌍 地域 & 可用区</div>
<div class="region-grid">
<div v-for="region in regions" :key="region.id"
class="region-card"
:class="{ active: selectedRegion === region.id }"
@click="selectRegion(region.id)">
<div class="region-name">{{ region.name }}</div>
<div class="region-azs">
<span v-for="az in region.azs" :key="az" class="az-badge">{{ az }}</span>
</div>
</div>
</div>
</div>
<!-- 连接箭头 -->
<div class="connection-arrow"></div>
<!-- 资源拓扑层 -->
<div class="layer resource-layer">
<div class="layer-title">🎯 资源拓扑</div>
<div class="resource-grid">
<!-- 计算资源 -->
<div class="resource-category" :class="{ highlight: viewMode === 'compute' || viewMode === 'overview' }">
<div class="category-title">💻 计算</div>
<div class="resource-list">
<div v-for="item in computeResources" :key="item.name" class="resource-item">
<span class="resource-icon">{{ item.icon }}</span>
<span class="resource-name">{{ item.name }}</span>
</div>
</div>
</div>
<!-- 网络资源 -->
<div class="resource-category" :class="{ highlight: viewMode === 'network' || viewMode === 'overview' }">
<div class="category-title">🌐 网络</div>
<div class="resource-list">
<div v-for="item in networkResources" :key="item.name" class="resource-item">
<span class="resource-icon">{{ item.icon }}</span>
<span class="resource-name">{{ item.name }}</span>
</div>
</div>
</div>
<!-- 存储资源 -->
<div class="resource-category" :class="{ highlight: viewMode === 'storage' || viewMode === 'overview' }">
<div class="category-title">💾 存储</div>
<div class="resource-list">
<div v-for="item in storageResources" :key="item.name" class="resource-item">
<span class="resource-icon">{{ item.icon }}</span>
<span class="resource-name">{{ item.name }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 信息面板 -->
<div class="info-panel" v-if="showInfo">
<div class="info-header">
<h4>💡 拓扑说明</h4>
<el-button type="text" @click="showInfo = false">关闭</el-button>
</div>
<div class="info-content">
<p><strong>当前视图</strong>{{ viewModeText }}</p>
<p><strong>选中云商</strong>{{ selectedProvider || '未选择' }}</p>
<p><strong>选中地域</strong>{{ selectedRegion || '未选择' }}</p>
<p class="tip">💡 提示点击云服务商和地域可以查看不同组合的资源拓扑</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const viewMode = ref('overview')
const selectedProvider = ref('阿里云')
const selectedRegion = ref('cn-beijing')
const showInfo = ref(true)
const providers = [
{ name: '阿里云', icon: '☁️' },
{ name: '腾讯云', icon: '🌟' },
{ name: '华为云', icon: '🔥' },
{ name: 'AWS', icon: '📦' }
]
const regions = [
{ id: 'cn-beijing', name: '华北2 (北京)', azs: ['A', 'B', 'C', 'D', 'E'] },
{ id: 'cn-shanghai', name: '华东2 (上海)', azs: ['A', 'B', 'C', 'D', 'E', 'F'] },
{ id: 'cn-shenzhen', name: '华南1 (深圳)', azs: ['A', 'B', 'C', 'D'] },
{ id: 'cn-hangzhou', name: '华东1 (杭州)', azs: ['A', 'B', 'C', 'D', 'E', 'F', 'G'] }
]
const computeResources = [
{ name: '云服务器 ECS', icon: '🖥️' },
{ name: '容器服务 K8s', icon: '📦' },
{ name: '函数计算 FC', icon: '⚡' },
{ name: '裸金属服务器', icon: '🔧' }
]
const networkResources = [
{ name: '专有网络 VPC', icon: '🕸️' },
{ name: '负载均衡 SLB', icon: '⚖️' },
{ name: '弹性公网 IP', icon: '🌍' },
{ name: 'VPN 网关', icon: '🔒' }
]
const storageResources = [
{ name: '对象存储 OSS', icon: '🪣' },
{ name: '块存储 EBS', icon: '💽' },
{ name: '文件存储 NAS', icon: '📁' },
{ name: '日志服务 SLS', icon: '📋' }
]
const viewModeText = computed(() => {
const map = {
overview: '全景视图 - 查看完整资源拓扑',
compute: '计算视角 - 聚焦计算资源',
network: '网络视角 - 聚焦网络资源',
storage: '存储视角 - 聚焦存储资源'
}
return map[viewMode.value]
})
const selectProvider = (name) => {
selectedProvider.value = name
}
const selectRegion = (id) => {
selectedRegion.value = id
}
</script>
<style scoped>
.resource-topology-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.controls {
margin-bottom: 20px;
text-align: center;
}
.topology-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.layer {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.layer-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid #e4e7ed;
}
.connection-arrow {
text-align: center;
font-size: 24px;
color: #909399;
padding: 8px 0;
}
/* Provider Grid */
.provider-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.provider-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
border: 2px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.provider-card:hover {
border-color: #409eff;
transform: translateY(-2px);
}
.provider-card.active {
border-color: #409eff;
background: #ecf5ff;
}
.provider-icon {
font-size: 32px;
margin-bottom: 8px;
}
.provider-name {
font-size: 14px;
font-weight: 500;
color: #606266;
}
/* Region Grid */
.region-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.region-card {
padding: 12px;
border: 2px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.region-card:hover {
border-color: #67c23a;
}
.region-card.active {
border-color: #67c23a;
background: #f0f9eb;
}
.region-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.region-azs {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.az-badge {
padding: 2px 6px;
background: #e4e7ed;
border-radius: 4px;
font-size: 11px;
color: #606266;
}
.region-card.active .az-badge {
background: #67c23a;
color: white;
}
/* Resource Grid */
.resource-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.resource-category {
padding: 16px;
border: 2px solid #e4e7ed;
border-radius: 8px;
transition: all 0.3s;
}
.resource-category.highlight {
border-color: #409eff;
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
}
.category-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.resource-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f5f7fa;
border-radius: 6px;
transition: all 0.2s;
}
.resource-item:hover {
background: #ecf5ff;
transform: translateX(4px);
}
.resource-icon {
font-size: 18px;
}
.resource-name {
font-size: 13px;
color: #606266;
}
/* Info Panel */
.info-panel {
margin-top: 20px;
padding: 16px;
background: #f0f9eb;
border-radius: 8px;
border-left: 4px solid #67c23a;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.info-header h4 {
margin: 0;
color: #67c23a;
}
.info-content p {
margin: 8px 0;
color: #606266;
font-size: 14px;
}
.info-content .tip {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #dcdfe6;
color: #909399;
font-size: 13px;
}
@media (max-width: 768px) {
.provider-grid,
.region-grid {
grid-template-columns: repeat(2, 1fr);
}
.resource-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,875 @@
<template>
<div class="storage-topology-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="overview">存储概览</el-radio-button>
<el-radio-button label="object">对象存储</el-radio-button>
<el-radio-button label="block">块存储</el-radio-button>
<el-radio-button label="file">文件存储</el-radio-button>
</el-radio-group>
<el-switch v-model="showDetails" active-text="显示详情" style="margin-left: 20px" />
</div>
<!-- 存储架构图 -->
<div class="storage-architecture">
<!-- 应用接入层 -->
<div class="layer access-layer">
<div class="layer-title">🔌 应用接入层</div>
<div class="access-methods">
<div class="method-card" v-for="method in accessMethods" :key="method.name"
@mouseenter="hoverMethod = method.name" @mouseleave="hoverMethod = null">
<div class="method-icon">{{ method.icon }}</div>
<div class="method-name">{{ method.name }}</div>
<div class="method-desc">{{ method.description }}</div>
<div class="method-tooltip" v-if="hoverMethod === method.name && showDetails">
<div v-for="detail in method.details" :key="detail">{{ detail }}</div>
</div>
</div>
</div>
</div>
<!-- 存储网关层 -->
<div class="layer gateway-layer">
<div class="layer-title">🚪 存储网关层</div>
<div class="gateways-grid">
<div class="gateway-card" v-for="gateway in storageGateways" :key="gateway.name"
:class="gateway.type">
<div class="gateway-header">
<span class="gateway-icon">{{ gateway.icon }}</span>
<span class="gateway-name">{{ gateway.name }}</span>
</div>
<div class="gateway-metrics" v-if="showDetails">
<div class="metric">
<span class="metric-label">TPS</span>
<span class="metric-value">{{ gateway.tps }}</span>
</div>
<div class="metric">
<span class="metric-label">延迟</span>
<span class="metric-value">{{ gateway.latency }}ms</span>
</div>
</div>
</div>
</div>
</div>
<!-- 存储服务层 -->
<div class="layer storage-services-layer">
<div class="layer-title">💾 存储服务层</div>
<div class="storage-types-grid">
<!-- 对象存储 -->
<div class="storage-type-card object-storage"
:class="{ active: viewMode === 'object' || viewMode === 'overview' }">
<div class="storage-header">
<div class="storage-icon">🪣</div>
<div class="storage-title">对象存储 OSS</div>
</div>
<div class="storage-desc">适合存储图片视频日志等非结构化数据</div>
<div class="storage-features">
<div class="feature">
<span class="feature-icon"></span>
<span>海量存储</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>低成本</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>CDN 加速</span>
</div>
</div>
<div class="storage-buckets" v-if="showDetails">
<div class="bucket" v-for="bucket in buckets" :key="bucket.name">
<div class="bucket-info">
<span class="bucket-name">{{ bucket.name }}</span>
<span class="bucket-objects">{{ bucket.objects }} 个对象</span>
</div>
<div class="bucket-size">{{ bucket.size }}</div>
</div>
</div>
</div>
<!-- 块存储 -->
<div class="storage-type-card block-storage"
:class="{ active: viewMode === 'block' || viewMode === 'overview' }">
<div class="storage-header">
<div class="storage-icon">💽</div>
<div class="storage-title">块存储 EBS</div>
</div>
<div class="storage-desc">为云服务器提供高性能低延迟的数据块存储</div>
<div class="storage-features">
<div class="feature">
<span class="feature-icon"></span>
<span>高性能 SSD</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>快照备份</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>弹性扩容</span>
</div>
</div>
<div class="volumes-list" v-if="showDetails">
<div class="volume" v-for="vol in volumes" :key="vol.id">
<div class="volume-info">
<div class="volume-header">
<span class="volume-name">{{ vol.name }}</span>
<span class="volume-type" :class="vol.type">{{ vol.type }}</span>
</div>
<div class="volume-meta">
<span>{{ vol.size }}</span>
<span></span>
<span>挂载到: {{ vol.attachedTo }}</span>
</div>
</div>
<div class="volume-iops" v-if="vol.iops">
<div class="iops-label">IOPS</div>
<div class="iops-value">{{ vol.iops }}</div>
</div>
</div>
</div>
</div>
<!-- 文件存储 -->
<div class="storage-type-card file-storage"
:class="{ active: viewMode === 'file' || viewMode === 'overview' }">
<div class="storage-header">
<div class="storage-icon">📁</div>
<div class="storage-title">文件存储 NAS</div>
</div>
<div class="storage-desc">为多个计算节点提供共享文件系统访问</div>
<div class="storage-features">
<div class="feature">
<span class="feature-icon"></span>
<span>共享访问</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>POSIX 兼容</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>自动扩容</span>
</div>
</div>
<div class="filesystems-list" v-if="showDetails">
<div class="filesystem" v-for="fs in filesystems" :key="fs.name">
<div class="fs-header">
<div class="fs-info">
<span class="fs-name">{{ fs.name }}</span>
<span class="fs-protocol" :class="fs.protocol">{{ fs.protocol }}</span>
</div>
<div class="fs-capacity">
<span class="capacity-used">{{ fs.used }}</span>
<span class="capacity-total">/ {{ fs.total }}</span>
</div>
</div>
<div class="fs-capacity-bar">
<div class="capacity-progress" :style="{ width: fs.percentage + '%' }"
:class="{ warning: fs.percentage > 80, danger: fs.percentage > 90 }"></div>
</div>
<div class="fs-mounts">
<div class="mounts-label">挂载点</div>
<div class="mounts-list">
<span class="mount-point" v-for="mount in fs.mounts" :key="mount">{{ mount }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 存储选型对比 -->
<div class="comparison-section">
<div class="comparison-title">📊 存储类型选型对比</div>
<div class="comparison-table">
<div class="table-header">
<div class="header-cell">特性</div>
<div class="header-cell object">对象存储</div>
<div class="header-cell block">块存储</div>
<div class="header-cell file">文件存储</div>
</div>
<div class="table-row" v-for="row in comparisonData" :key="row.feature">
<div class="cell feature">{{ row.feature }}</div>
<div class="cell" :class="{ highlight: row.object === '优秀' || row.object === '强' }">{{ row.object }}</div>
<div class="cell" :class="{ highlight: row.block === '优秀' || row.block === '强' }">{{ row.block }}</div>
<div class="cell" :class="{ highlight: row.file === '优秀' || row.file === '强' }">{{ row.file }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('overview')
const showDetails = ref(false)
const hoverMethod = ref(null)
// 接入方式
const accessMethods = [
{
name: 'API/SDK',
icon: '🔧',
description: '通过编程接口访问存储',
details: ['支持 RESTful API', '提供多语言 SDK', '支持批量操作', '可编程访问控制']
},
{
name: '挂载访问',
icon: '🔗',
description: '像本地磁盘一样使用',
details: ['支持 NFS 协议', '支持 SMB 协议', 'POSIX 兼容', '透明访问']
},
{
name: '控制台',
icon: '🖥️',
description: '通过 Web 界面管理',
details: ['可视化操作', '权限管理', '监控报表', '日志审计']
}
]
// 存储网关
const storageGateways = [
{ name: '对象网关', icon: '🪣', type: 'object', tps: '10000', latency: '5' },
{ name: '块网关', icon: '💽', type: 'block', tps: '50000', latency: '1' },
{ name: '文件网关', icon: '📁', type: 'file', tps: '8000', latency: '3' }
]
// 存储桶
const buckets = [
{ name: 'images-bucket', protocol: 'S3', used: '2.5 TB', total: '10 TB', percentage: 25, mounts: ['CDN 加速', '图片处理'] },
{ name: 'logs-bucket', protocol: 'S3', used: '850 GB', total: '5 TB', percentage: 17, mounts: ['日志归档', '数据分析'] },
{ name: 'backup-bucket', protocol: 'S3', used: '4.2 TB', total: '5 TB', percentage: 84, mounts: ['自动备份', '跨区域复制'] }
]
// 云盘
const volumes = [
{ name: 'data-disk-01', type: 'ESSD', size: '500 GB', used: '320 GB', percentage: 64, attachedTo: 'DB-Server-01', iops: 50000 },
{ name: 'data-disk-02', type: 'SSD', size: '200 GB', used: '145 GB', percentage: 72, attachedTo: 'App-Server-02', iops: 25000 },
{ name: 'log-disk-01', type: 'SATA', size: '1 TB', used: '680 GB', percentage: 68, attachedTo: 'Log-Server-01', iops: 5000 }
]
// 文件系统
const filesystems = [
{ name: 'shared-data', protocol: 'NFS', used: '1.2 TB', total: '5 TB', percentage: 24, mounts: ['/mnt/shared'] },
{ name: 'dev-env', protocol: 'NFS', used: '450 GB', total: '2 TB', percentage: 22, mounts: ['/mnt/dev'] },
{ name: 'team-share', protocol: 'SMB', used: '890 GB', total: '3 TB', percentage: 30, mounts: ['\\\\nas\\team'] }
]
// 对比数据
const comparisonData = [
{ feature: '访问协议', object: 'HTTP/HTTPS', block: 'iSCSI/NVMe', file: 'NFS/SMB' },
{ feature: '性能', object: '高吞吐', block: '低延迟', file: '中等' },
{ feature: '数据共享', object: '弱', block: '不支持', file: '强' },
{ feature: '容量扩展', object: '强', block: '中等', file: '中等' },
{ feature: '成本', object: '低', block: '高', file: '中等' },
{ feature: '典型场景', object: '图片/视频/备份', block: '数据库/应用', file: '共享文件/开发' }
]
</script>
<style scoped>
.storage-topology-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.storage-architecture {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
.layer {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.layer-title {
font-size: 14px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Access Layer */
.access-methods {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.method-card {
background: #f5f7fa;
border-radius: 8px;
padding: 16px;
text-align: center;
position: relative;
cursor: pointer;
transition: all 0.3s;
}
.method-card:hover {
background: #ecf5ff;
transform: translateY(-2px);
}
.method-icon {
font-size: 32px;
margin-bottom: 8px;
}
.method-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.method-desc {
font-size: 12px;
color: #909399;
}
.method-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 10px 14px;
border-radius: 8px;
font-size: 12px;
z-index: 10;
margin-bottom: 8px;
white-space: nowrap;
}
/* Gateway Layer */
.gateways-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.gateway-card {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
border-left: 4px solid;
}
.gateway-card.object {
border-left-color: #409eff;
}
.gateway-card.block {
border-left-color: #67c23a;
}
.gateway-card.file {
border-left-color: #e6a23c;
}
.gateway-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.gateway-icon {
font-size: 20px;
}
.gateway-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.gateway-metrics {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.metric-label {
color: #909399;
}
.metric-value {
color: #409eff;
font-weight: 500;
}
/* Storage Types Grid */
.storage-types-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 16px;
}
.storage-type-card {
background: #f5f7fa;
border-radius: 12px;
padding: 16px;
border: 2px solid transparent;
transition: all 0.3s;
}
.storage-type-card:hover,
.storage-type-card.active {
border-color: #409eff;
background: #ecf5ff;
}
.storage-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.storage-icon {
font-size: 28px;
}
.storage-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.storage-desc {
font-size: 12px;
color: #606266;
margin-bottom: 12px;
line-height: 1.5;
}
.storage-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.feature {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #606266;
}
.feature-icon {
color: #67c23a;
}
/* Buckets List */
.storage-buckets {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
}
.bucket {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background: white;
border-radius: 6px;
}
.bucket-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.bucket-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.bucket-objects {
font-size: 11px;
color: #909399;
}
.bucket-size {
font-size: 12px;
color: #409eff;
font-weight: 500;
}
/* Volumes List */
.volumes-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
}
.volume {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: white;
border-radius: 6px;
}
.volume-info {
flex: 1;
}
.volume-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.volume-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.volume-type {
font-size: 10px;
padding: 1px 6px;
border-radius: 10px;
text-transform: uppercase;
}
.volume-type.essd {
background: #409eff;
color: white;
}
.volume-type.ssd {
background: #67c23a;
color: white;
}
.volume-type.sata {
background: #909399;
color: white;
}
.volume-meta {
font-size: 11px;
color: #909399;
}
.volume-meta span {
margin: 0 4px;
}
.volume-iops {
text-align: center;
padding-left: 12px;
border-left: 1px solid #e4e7ed;
}
.iops-label {
font-size: 10px;
color: #909399;
text-transform: uppercase;
}
.iops-value {
font-size: 14px;
font-weight: 600;
color: #409eff;
}
/* Filesystems List */
.filesystems-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
}
.filesystem {
background: white;
border-radius: 6px;
padding: 12px;
}
.fs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.fs-info {
display: flex;
align-items: center;
gap: 8px;
}
.fs-name {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.fs-protocol {
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.fs-protocol.nfs {
background: #409eff;
color: white;
}
.fs-protocol.smb {
background: #67c23a;
color: white;
}
.fs-capacity {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
}
.capacity-used {
color: #303133;
font-weight: 500;
}
.capacity-total {
color: #909399;
}
.fs-capacity-bar {
height: 4px;
background: #e4e7ed;
border-radius: 2px;
margin-bottom: 8px;
overflow: hidden;
}
.capacity-progress {
height: 100%;
background: #67c23a;
border-radius: 2px;
transition: width 0.3s;
}
.capacity-progress.warning {
background: #e6a23c;
}
.capacity-progress.danger {
background: #f56c6c;
}
.fs-mounts {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mounts-label {
font-size: 12px;
color: #909399;
}
.mounts-list {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.mount-point {
font-size: 11px;
padding: 2px 8px;
background: #ecf5ff;
color: #409eff;
border-radius: 10px;
}
/* Comparison Section */
.comparison-section {
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.comparison-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
}
.comparison-table {
overflow-x: auto;
}
.table-header {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 1px;
background: #e4e7ed;
border-radius: 8px 8px 0 0;
overflow: hidden;
}
.header-cell {
padding: 12px;
background: #f5f7fa;
font-size: 13px;
font-weight: 600;
color: #606266;
text-align: center;
}
.header-cell.object {
background: #ecf5ff;
color: #409eff;
}
.header-cell.block {
background: #f0f9eb;
color: #67c23a;
}
.header-cell.file {
background: #fdf6ec;
color: #e6a23c;
}
.table-row {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 1px;
background: #e4e7ed;
border-bottom: 1px solid #e4e7ed;
}
.table-row:last-child {
border-radius: 0 0 8px 8px;
overflow: hidden;
border-bottom: none;
}
.cell {
padding: 10px 12px;
background: white;
font-size: 12px;
color: #606266;
text-align: center;
}
.cell.feature {
text-align: left;
font-weight: 500;
color: #303133;
background: #fafafa;
}
.cell.highlight {
font-weight: 600;
color: #67c23a;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.storage-types-grid {
grid-template-columns: 1fr;
}
.comparison-table {
font-size: 11px;
}
.table-header,
.table-row {
grid-template-columns: 80px repeat(3, 1fr);
}
.header-cell,
.cell {
padding: 6px 8px;
}
}
</style>
@@ -0,0 +1,508 @@
<template>
<div class="subnet-design-demo">
<!-- 控制面板 -->
<div class="control-panel">
<div class="panel-section">
<span class="panel-label">VPC 网段</span>
<el-radio-group v-model="vpcCidr" size="small">
<el-radio-button label="172.16.0.0/12">172.16.0.0/12</el-radio-button>
<el-radio-button label="10.0.0.0/8">10.0.0.0/8</el-radio-button>
<el-radio-button label="192.168.0.0/16">192.168.0.0/16</el-radio-button>
</el-radio-group>
</div>
<div class="panel-section">
<span class="panel-label">子网划分</span>
<el-slider v-model="subnetBits" :min="2" :max="4" show-stops :marks="{2: '/24', 3: '/25', 4: '/26'}" style="width: 200px;" />
</div>
<el-switch v-model="showCalculation" active-text="显示计算过程" style="margin-left: 20px;" />
</div>
<!-- 网段可视化 -->
<div class="network-visualization">
<div class="vpc-block">
<div class="vpc-header">
<span class="vpc-name">VPC 网段</span>
<span class="vpc-cidr">{{ vpcCidr }}</span>
<span class="vpc-stats">可用 IP: {{ totalIps.toLocaleString() }} </span>
</div>
<div class="subnet-grid">
<div
v-for="(subnet, index) in subnets"
:key="index"
class="subnet-cell"
:class="[subnet.type, { active: selectedSubnet === index }]"
@click="selectSubnet(index)"
@mouseenter="hoverSubnet = index"
@mouseleave="hoverSubnet = null"
>
<div class="cell-header">
<span class="cell-type">{{ subnet.type === 'public' ? '🌐' : '🔒' }}</span>
<span class="cell-name">{{ subnet.name }}</span>
</div>
<div class="cell-cidr">{{ subnet.cidr }}</div>
<div class="cell-stats">
<span class="ip-count">{{ subnet.ipCount }} IP</span>
<span class="az-badge">{{ subnet.az }}</span>
</div>
<!-- 悬停提示 -->
<div class="cell-tooltip" v-if="hoverSubnet === index && showCalculation">
<div class="tooltip-row">
<span>网段范围</span>
<code>{{ subnet.range }}</code>
</div>
<div class="tooltip-row">
<span>可用 IP</span>
<span>{{ subnet.usableIps }} </span>
</div>
<div class="tooltip-row">
<span>预留 IP</span>
<span>网络地址 + 广播地址 + 网关</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 网段计算说明 -->
<div class="calculation-panel" v-if="showCalculation">
<h4>📐 子网划分计算说明</h4>
<div class="calc-section">
<h5>1. 基础概念</h5>
<div class="concept-grid">
<div class="concept-item">
<span class="concept-label">CIDR 表示法</span>
<code>/24</code> 表示网络位占 24 主机位 8
</div>
<div class="concept-item">
<span class="concept-label"> IP </span>
<code>2^(32-24) = 256</code>
</div>
<div class="concept-item">
<span class="concept-label">可用 IP </span>
<code>256 - 3 = 253</code> 减去网络广播网关地址
</div>
</div>
</div>
<div class="calc-section">
<h5>2. 当前配置计算</h5>
<div class="calc-result">
<div class="result-item">
<span class="result-label">VPC 网段</span>
<code class="result-value">{{ vpcCidr }}</code>
</div>
<div class="result-item">
<span class="result-label">子网掩码</span>
<code class="result-value">/{{ subnetMask }}</code>
</div>
<div class="result-item">
<span class="result-label">子网数量</span>
<code class="result-value">{{ subnets.length }} </code>
</div>
<div class="result-item">
<span class="result-label">每个子网 IP </span>
<code class="result-value">{{ ipsPerSubnet }} </code>
</div>
</div>
</div>
</div>
<!-- 最佳实践提示 -->
<div class="tips-panel">
<h4>💡 子网设计最佳实践</h4>
<div class="tips-grid">
<div class="tip-item">
<div class="tip-icon">🎯</div>
<div class="tip-content">
<h5>预留足够 IP</h5>
<p>每个子网至少预留 20% IP 作为扩容缓冲</p>
</div>
</div>
<div class="tip-item">
<div class="tip-icon">🔒</div>
<div class="tip-content">
<h5>公网私网分离</h5>
<p>核心数据放在私网子网通过 NAT 访问外网</p>
</div>
</div>
<div class="tip-item">
<div class="tip-icon">🌐</div>
<div class="tip-content">
<h5> AZ 部署</h5>
<p>同一 VPC 的不同子网放在不同可用区</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const vpcCidr = ref('172.16.0.0/12')
const subnetBits = ref(2)
const showCalculation = ref(false)
const selectedSubnet = ref(null)
const hoverSubnet = ref(null)
const subnetMask = computed(() => {
const baseMask = parseInt(vpcCidr.value.split('/')[1])
return baseMask + subnetBits.value
})
const ipsPerSubnet = computed(() => {
return Math.pow(2, 32 - subnetMask.value)
})
const totalIps = computed(() => {
const mask = parseInt(vpcCidr.value.split('/')[1])
return Math.pow(2, 32 - mask)
})
const subnets = computed(() => {
const baseCidr = vpcCidr.value.split('/')[0]
const octets = baseCidr.split('.').map(Number)
const count = Math.pow(2, subnetBits.value)
const result = []
for (let i = 0; i < count; i++) {
const thirdOctet = octets[2] + Math.floor(i / 256)
const fourthOctet = i % 256
const cidr = `${octets[0]}.${octets[1]}.${thirdOctet}.${fourthOctet}/${subnetMask.value}`
const startIp = `${octets[0]}.${octets[1]}.${thirdOctet}.${fourthOctet}`
const endIp = `${octets[0]}.${octets[1]}.${thirdOctet + Math.floor((ipsPerSubnet.value - 1) / 256)}.${(fourthOctet + ipsPerSubnet.value - 1) % 256}`
result.push({
name: `子网-${String.fromCharCode(65 + i)}`,
cidr,
type: i % 2 === 0 ? 'public' : 'private',
ipCount: ipsPerSubnet.value,
az: `可用区 ${String.fromCharCode(65 + (i % 3))}`,
range: `${startIp} - ${endIp}`,
usableIps: ipsPerSubnet.value - 3
})
}
return result
})
const selectSubnet = (index) => {
selectedSubnet.value = selectedSubnet.value === index ? null : index
}
</script>
<style scoped>
.subnet-design-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
}
.panel-section {
display: flex;
align-items: center;
gap: 8px;
}
.panel-label {
font-size: 13px;
color: #606266;
font-weight: 500;
}
/* Network Visualization */
.network-visualization {
margin-bottom: 20px;
}
.vpc-block {
background: white;
border-radius: 12px;
padding: 20px;
border: 2px solid #409eff;
}
.vpc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.vpc-name {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.vpc-cidr {
font-size: 13px;
padding: 4px 8px;
background: #ecf5ff;
color: #409eff;
border-radius: 4px;
font-family: monospace;
}
.vpc-stats {
font-size: 12px;
color: #909399;
}
/* Subnet Grid */
.subnet-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
}
.subnet-cell {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.subnet-cell:hover {
border-color: #c0c4cc;
transform: translateY(-2px);
}
.subnet-cell.active {
border-color: #409eff;
background: #ecf5ff;
}
.subnet-cell.public {
border-left: 4px solid #409eff;
}
.subnet-cell.private {
border-left: 4px solid #67c23a;
}
.cell-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.cell-type {
font-size: 14px;
}
.cell-name {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.cell-cidr {
font-size: 11px;
color: #606266;
font-family: monospace;
margin-bottom: 8px;
}
.cell-stats {
display: flex;
justify-content: space-between;
align-items: center;
}
.ip-count {
font-size: 11px;
color: #909399;
}
.az-badge {
font-size: 10px;
padding: 2px 6px;
background: #e4e7ed;
border-radius: 10px;
color: #606266;
}
/* Cell Tooltip */
.cell-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 10px 14px;
border-radius: 8px;
font-size: 12px;
z-index: 10;
margin-bottom: 8px;
white-space: nowrap;
}
.tooltip-row {
display: flex;
justify-content: space-between;
gap: 12px;
margin: 3px 0;
}
/* Calculation Panel */
.calculation-panel {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.calculation-panel h4 {
margin: 0 0 16px 0;
color: #303133;
font-size: 16px;
}
.calc-section {
margin-bottom: 20px;
}
.calc-section:last-child {
margin-bottom: 0;
}
.calc-section h5 {
margin: 0 0 12px 0;
color: #606266;
font-size: 14px;
}
.concept-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px;
}
.concept-item {
background: #f5f7fa;
padding: 12px;
border-radius: 6px;
font-size: 13px;
color: #606266;
}
.concept-label {
font-weight: 600;
color: #303133;
}
.calc-result {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: #f5f7fa;
border-radius: 6px;
}
.result-label {
font-size: 13px;
color: #606266;
}
.result-value {
font-size: 13px;
color: #409eff;
font-weight: 600;
font-family: monospace;
}
/* Tips Panel */
.tips-panel {
background: #f0f9eb;
border-radius: 8px;
padding: 20px;
border-left: 4px solid #67c23a;
}
.tips-panel h4 {
margin: 0 0 16px 0;
color: #67c23a;
font-size: 16px;
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.tip-item {
display: flex;
gap: 12px;
background: white;
padding: 12px;
border-radius: 6px;
}
.tip-icon {
font-size: 24px;
flex-shrink: 0;
}
.tip-content h5 {
margin: 0 0 4px 0;
font-size: 14px;
color: #303133;
}
.tip-content p {
margin: 0;
font-size: 12px;
color: #606266;
line-height: 1.5;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.panel-section {
flex-direction: column;
align-items: flex-start;
}
.subnet-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,529 @@
<template>
<div class="vpc-architecture-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="full">完整架构</el-radio-button>
<el-radio-button label="public">公网访问</el-radio-button>
<el-radio-button label="private">私网隔离</el-radio-button>
<el-radio-button label="hybrid">混合云</el-radio-button>
</el-radio-group>
<el-switch
v-model="showDetails"
active-text="显示详情"
style="margin-left: 20px"
/>
</div>
<!-- VPC 架构图 -->
<div class="vpc-container">
<!-- 外部互联网 -->
<div class="internet-zone" v-if="showInternet">
<div class="zone-header">
<span class="zone-icon">🌐</span>
<span class="zone-title">互联网 (Internet)</span>
</div>
<div class="zone-content">
<div class="internet-user">
<div class="user-avatar">👤</div>
<div class="user-label">用户</div>
</div>
<div class="internet-user">
<div class="user-avatar">🏢</div>
<div class="user-label">企业</div>
</div>
</div>
</div>
<!-- 连接箭头 -->
<div class="connection-flow" v-if="showInternet">
<div class="flow-line"></div>
<div class="flow-devices">
<div class="device" v-for="device in borderDevices" :key="device.name"
:class="device.type"
@mouseenter="hoverDevice = device.name"
@mouseleave="hoverDevice = null"
003e
<div class="device-icon">{{ device.icon }}</div>
<div class="device-name">{{ device.name }}</div>
<div class="device-tooltip" v-if="hoverDevice === device.name && showDetails">
{{ device.description }}
</div>
</div>
</div>
</div>
<!-- VPC 主体 -->
<div class="vpc-zone">
<div class="vpc-header">
<div class="vpc-title">
<span class="vpc-icon">🏠</span>
<span>专有网络 VPC</span>
<span class="vpc-id">vpc-2ze7p8w7c9d6x5y4</span>
</div>
<div class="vpc-meta">
<span class="meta-item">📍 华北2 (北京)</span>
<span class="meta-item">🌐 172.16.0.0/12</span>
</div>
</div>
<div class="vpc-content">
<!-- 可用区 1 -->
<div class="az-container">
<div class="az-header">
<span class="az-name">可用区 A</span>
<span class="az-status online">在线</span>
</div>
<div class="subnets">
<div class="subnet public"
@mouseenter="hoverSubnet = 'public-a'"
@mouseleave="hoverSubnet = null">
<div class="subnet-header">
<span class="subnet-type">🌐 公网子网</span>
<span class="subnet-cidr">172.16.1.0/24</span>
</div>
<div class="subnet-resources">
<div class="resource-tag">🖥 ECS × 2</div>
<div class="resource-tag"> SLB</div>
<div class="resource-tag">🌐 NAT</div>
</div>
<div class="subnet-tooltip" v-if="hoverSubnet === 'public-a' && showDetails">
公网子网可直接访问互联网部署对外服务
</div>
</div>
<div class="subnet private"
@mouseenter="hoverSubnet = 'private-a'"
@mouseleave="hoverSubnet = null">
<div class="subnet-header">
<span class="subnet-type">🔒 私网子网</span>
<span class="subnet-cidr">172.16.2.0/24</span>
</div>
<div class="subnet-resources">
<div class="resource-tag">🖥 ECS × 4</div>
<div class="resource-tag">🗄 RDS</div>
<div class="resource-tag">📦 Redis</div>
</div>
<div class="subnet-tooltip" v-if="hoverSubnet === 'private-a' && showDetails">
私网子网无法直接访问互联网部署核心服务
</div>
</div>
</div>
</div>
<!-- 可用区 2 -->
<div class="az-container">
<div class="az-header">
<span class="az-name">可用区 B</span>
<span class="az-status online">在线</span>
</div>
<div class="subnets">
<div class="subnet public">
<div class="subnet-header">
<span class="subnet-type">🌐 公网子网</span>
<span class="subnet-cidr">172.16.3.0/24</span>
</div>
<div class="subnet-resources">
<div class="resource-tag">🖥 ECS × 2</div>
<div class="resource-tag"> SLB</div>
</div>
</div>
<div class="subnet private">
<div class="subnet-header">
<span class="subnet-type">🔒 私网子网</span>
<span class="subnet-cidr">172.16.4.0/24</span>
</div>
<div class="subnet-resources">
<div class="resource-tag">🖥 ECS × 4</div>
<div class="resource-tag">🗄 RDS Slave</div>
<div class="resource-tag">📦 Redis Slave</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('full')
const showDetails = ref(false)
const hoverDevice = ref(null)
const hoverSubnet = ref(null)
const showInternet = computed(() => {
return ['full', 'public', 'hybrid'].includes(viewMode.value)
})
const borderDevices = [
{
name: '边界路由器',
icon: '📡',
type: 'router',
description: '连接VPC与互联网的核心路由设备'
},
{
name: 'NAT网关',
icon: '🔄',
type: 'nat',
description: '实现私网资源访问互联网的地址转换'
},
{
name: '负载均衡',
icon: '⚖️',
type: 'slb',
description: '分发公网流量到多台后端服务器'
}
]
</script>
<style scoped>
.vpc-architecture-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
}
.vpc-container {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Internet Zone */
.internet-zone {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 12px;
padding: 16px;
border: 2px solid #90caf9;
}
.zone-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.zone-icon {
font-size: 20px;
}
.zone-title {
font-size: 16px;
font-weight: 600;
color: #1565c0;
}
.zone-content {
display: flex;
gap: 16px;
justify-content: center;
}
.internet-user {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.user-avatar {
font-size: 32px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.user-label {
font-size: 12px;
color: #546e7a;
}
/* Connection Flow */
.connection-flow {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.flow-line {
width: 4px;
height: 24px;
background: linear-gradient(to bottom, #90caf9, #4caf50);
border-radius: 2px;
}
.flow-devices {
display: flex;
gap: 24px;
justify-content: center;
}
.device {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 16px;
background: white;
border-radius: 8px;
border: 2px solid #e0e0e0;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.device:hover {
border-color: #409eff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.device.router {
border-color: #ff9800;
}
.device.nat {
border-color: #9c27b0;
}
.device.slb {
border-color: #2196f3;
}
.device-icon {
font-size: 24px;
}
.device-name {
font-size: 12px;
font-weight: 500;
color: #424242;
}
.device-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 8px 12px;
background: #333;
color: white;
font-size: 12px;
border-radius: 6px;
white-space: nowrap;
z-index: 10;
margin-bottom: 8px;
}
.device-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #333;
}
/* VPC Zone */
.vpc-zone {
background: white;
border-radius: 12px;
padding: 20px;
border: 2px solid #409eff;
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.1);
}
.vpc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.vpc-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.vpc-icon {
font-size: 20px;
}
.vpc-id {
font-size: 12px;
color: #909399;
font-weight: normal;
margin-left: 8px;
}
.vpc-meta {
display: flex;
gap: 16px;
}
.meta-item {
font-size: 13px;
color: #606266;
}
.vpc-content {
display: flex;
flex-direction: column;
gap: 16px;
}
/* AZ Container */
.az-container {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
}
.az-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.az-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.az-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.az-status.online {
background: #e1f3d8;
color: #67c23a;
}
.subnets {
display: flex;
flex-direction: column;
gap: 8px;
}
.subnet {
background: white;
border-radius: 6px;
padding: 10px;
border: 2px solid #e4e7ed;
transition: all 0.3s;
position: relative;
}
.subnet:hover {
transform: translateX(4px);
}
.subnet.public {
border-left: 4px solid #409eff;
}
.subnet.private {
border-left: 4px solid #67c23a;
}
.subnet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.subnet-type {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.subnet-cidr {
font-size: 11px;
padding: 2px 6px;
background: #f0f2f5;
border-radius: 4px;
color: #606266;
font-family: monospace;
}
.subnet-resources {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.resource-tag {
font-size: 11px;
padding: 3px 8px;
background: #ecf5ff;
border-radius: 4px;
color: #409eff;
}
.subnet-tooltip {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 8px;
padding: 8px 12px;
background: #333;
color: white;
font-size: 12px;
border-radius: 6px;
z-index: 10;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.vpc-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.vpc-meta {
flex-wrap: wrap;
}
}
</style>