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:
@@ -0,0 +1,778 @@
|
||||
<template>
|
||||
<div class="auto-scaling-demo">
|
||||
<div class="header">
|
||||
<div class="title">自动扩缩容</div>
|
||||
<div class="subtitle">基于CPU、内存、QPS的智能弹性伸缩</div>
|
||||
</div>
|
||||
|
||||
<!-- 指标选择器 -->
|
||||
<div class="metric-selector">
|
||||
<div class="selector-label">扩容指标:</div>
|
||||
<div class="selector-buttons">
|
||||
<button
|
||||
v-for="metric in metrics"
|
||||
:key="metric.key"
|
||||
class="metric-btn"
|
||||
:class="{ active: currentMetric === metric.key }"
|
||||
@click="currentMetric = metric.key"
|
||||
>
|
||||
<span class="btn-icon">{{ metric.icon }}</span>
|
||||
<span class="btn-name">{{ metric.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 监控仪表盘 -->
|
||||
<div class="monitoring-dashboard">
|
||||
<div class="dashboard-header">
|
||||
<span class="dashboard-title">实时监控</span>
|
||||
<span class="refresh-indicator">
|
||||
<span class="live-dot"></span>
|
||||
实时
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="metrics-grid">
|
||||
<!-- CPU使用率 -->
|
||||
<div class="metric-card" :class="{ warning: cpuUsage > 70, danger: cpuUsage > 90 }">
|
||||
<div class="metric-header">
|
||||
<span class="metric-icon">💻</span>
|
||||
<span class="metric-name">CPU使用率</span>
|
||||
</div>
|
||||
<div class="metric-value">
|
||||
<span class="value-number">{{ cpuUsage }}</span>
|
||||
<span class="value-unit">%</span>
|
||||
</div>
|
||||
<div class="metric-progress">
|
||||
<div class="progress-bar" :style="{ width: cpuUsage + '%', background: getUsageColor(cpuUsage) }"></div>
|
||||
</div>
|
||||
<div class="metric-threshold">
|
||||
<span>扩容阈值: 70%</span>
|
||||
<span>缩容阈值: 30%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内存使用率 -->
|
||||
<div class="metric-card" :class="{ warning: memoryUsage > 75, danger: memoryUsage > 90 }">
|
||||
<div class="metric-header">
|
||||
<span class="metric-icon">🧠</span>
|
||||
<span class="metric-name">内存使用率</span>
|
||||
</div>
|
||||
<div class="metric-value">
|
||||
<span class="value-number">{{ memoryUsage }}</span>
|
||||
<span class="value-unit">%</span>
|
||||
</div>
|
||||
<div class="metric-progress">
|
||||
<div class="progress-bar" :style="{ width: memoryUsage + '%', background: getUsageColor(memoryUsage) }"></div>
|
||||
</div>
|
||||
<div class="metric-threshold">
|
||||
<span>扩容阈值: 75%</span>
|
||||
<span>缩容阈值: 40%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QPS -->
|
||||
<div class="metric-card">
|
||||
<div class="metric-header">
|
||||
<span class="metric-icon">⚡</span>
|
||||
<span class="metric-name">QPS</span>
|
||||
</div>
|
||||
<div class="metric-value">
|
||||
<span class="value-number">{{ currentQPS }}</span>
|
||||
<span class="value-unit">req/s</span>
|
||||
</div>
|
||||
<div class="metric-chart">
|
||||
<svg viewBox="0 0 200 40" class="sparkline">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="2"
|
||||
:points="qpsSparklinePoints"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="metric-threshold">
|
||||
<span>扩容阈值: 1000/s</span>
|
||||
<span>目标: 800/s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实例数量 -->
|
||||
<div class="metric-card instances">
|
||||
<div class="metric-header">
|
||||
<span class="metric-icon">🖥️</span>
|
||||
<span class="metric-name">运行实例</span>
|
||||
</div>
|
||||
<div class="instances-display">
|
||||
<div class="instance-count">
|
||||
<span class="count-number">{{ currentInstances }}</span>
|
||||
<span class="count-label">个实例</span>
|
||||
</div>
|
||||
<div class="instance-range">
|
||||
<span>最小: {{ minInstances }}</span>
|
||||
<span>最大: {{ maxInstances }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instance-visual">
|
||||
<div
|
||||
v-for="i in maxInstances"
|
||||
:key="i"
|
||||
class="instance-dot"
|
||||
:class="{ active: i <= currentInstances, scaling: isScaling && i === currentInstances + 1 }"
|
||||
>
|
||||
{{ i }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="scaleReason" class="scale-reason">
|
||||
<span class="reason-icon">{{ scaleReason.includes('扩容') ? '📈' : '📉' }}</span>
|
||||
<span class="reason-text">{{ scaleReason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 扩缩容历史 -->
|
||||
<div class="scaling-history">
|
||||
<div class="history-header">
|
||||
<span class="history-title">扩缩容历史</span>
|
||||
<span class="history-count">最近 5 次操作</span>
|
||||
</div>
|
||||
<div class="history-list">
|
||||
<div
|
||||
v-for="(record, index) in scalingHistory"
|
||||
:key="index"
|
||||
class="history-item"
|
||||
:class="{ scaleOut: record.type === '缩容' }"
|
||||
>
|
||||
<div class="item-icon">{{ record.type === '扩容' ? '📈' : '📉' }}</div>
|
||||
<div class="item-details">
|
||||
<div class="item-action">
|
||||
{{ record.type }}: {{ record.from }} → {{ record.to }} 实例
|
||||
</div>
|
||||
<div class="item-reason">{{ record.reason }}</div>
|
||||
</div>
|
||||
<div class="item-time">{{ record.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最佳实践 -->
|
||||
<div class="best-practices">
|
||||
<div class="practices-title">自动扩缩容最佳实践</div>
|
||||
<div class="practices-grid">
|
||||
<div class="practice-card">
|
||||
<div class="practice-icon">⏱️</div>
|
||||
<div class="practice-title">冷却时间</div>
|
||||
<div class="practice-desc">
|
||||
设置适当的冷却时间(通常3-5分钟),避免扩缩容操作过于频繁导致的震荡
|
||||
</div>
|
||||
</div>
|
||||
<div class="practice-card">
|
||||
<div class="practice-icon">📊</div>
|
||||
<div class="practice-title">多指标综合</div>
|
||||
<div class="practice-desc">
|
||||
不要依赖单一指标,结合CPU、内存、QPS、连接数等多维度进行综合判断
|
||||
</div>
|
||||
</div>
|
||||
<div class="practice-card">
|
||||
<div class="practice-icon">🎯</div>
|
||||
<div class="practice-title">目标利用率</div>
|
||||
<div class="practice-desc">
|
||||
设置合理的资源目标利用率(如70%),预留足够的缓冲应对突发流量
|
||||
</div>
|
||||
</div>
|
||||
<div class="practice-card">
|
||||
<div class="practice-icon">⚡</div>
|
||||
<div class="practice-title">快速扩容</div>
|
||||
<div class="practice-desc">
|
||||
扩容操作应该比缩容更激进,确保系统能快速应对流量增长
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
const currentMetric = ref('cpu')
|
||||
const cpuUsage = ref(45)
|
||||
const memoryUsage = ref(60)
|
||||
const currentQPS = ref(650)
|
||||
const currentInstances = ref(3)
|
||||
const minInstances = ref(2)
|
||||
const maxInstances = ref(10)
|
||||
const isScaling = ref(false)
|
||||
const scaleReason = ref('')
|
||||
|
||||
const metrics = [
|
||||
{ key: 'cpu', name: 'CPU 使用率', icon: '💻' },
|
||||
{ key: 'memory', name: '内存使用率', icon: '🧠' },
|
||||
{ key: 'qps', name: 'QPS', icon: '⚡' }
|
||||
]
|
||||
|
||||
// QPS 历史数据
|
||||
const qpsHistory = ref(Array(20).fill(650))
|
||||
|
||||
// 计算 QPS 折线图的点
|
||||
const qpsSparklinePoints = computed(() => {
|
||||
const max = Math.max(...qpsHistory.value)
|
||||
const min = Math.min(...qpsHistory.value)
|
||||
const range = max - min || 1
|
||||
return qpsHistory.value.map((qps, index) => {
|
||||
const x = (index / (qpsHistory.value.length - 1)) * 200
|
||||
const y = 40 - ((qps - min) / range) * 35 - 2.5
|
||||
return `${x},${y}`
|
||||
}).join(' ')
|
||||
})
|
||||
|
||||
// 获取颜色
|
||||
const getUsageColor = (usage) => {
|
||||
if (usage > 90) return '#ef4444'
|
||||
if (usage > 70) return '#f59e0b'
|
||||
return '#22c55e'
|
||||
}
|
||||
|
||||
// 扩缩容历史
|
||||
const scalingHistory = ref([
|
||||
{ type: '扩容', from: 2, to: 3, reason: 'CPU使用率超过70%', time: '10:23' },
|
||||
{ type: '缩容', from: 4, to: 3, reason: 'CPU使用率低于30%', time: '09:15' },
|
||||
{ type: '扩容', from: 3, to: 4, reason: 'QPS达到1000/s', time: '08:42' },
|
||||
{ type: '扩容', from: 2, to: 3, reason: '内存使用率超过75%', time: '07:30' },
|
||||
{ type: '缩容', from: 5, to: 4, reason: '流量下降', time: '06:20' }
|
||||
])
|
||||
|
||||
// 模拟指标变化
|
||||
let simulationInterval
|
||||
const startSimulation = () => {
|
||||
simulationInterval = setInterval(() => {
|
||||
// 模拟 CPU 波动
|
||||
const cpuChange = (Math.random() - 0.5) * 10
|
||||
cpuUsage.value = Math.max(20, Math.min(95, cpuUsage.value + cpuChange))
|
||||
|
||||
// 模拟内存波动
|
||||
const memChange = (Math.random() - 0.5) * 8
|
||||
memoryUsage.value = Math.max(30, Math.min(90, memoryUsage.value + memChange))
|
||||
|
||||
// 模拟 QPS 波动
|
||||
const qpsChange = Math.floor((Math.random() - 0.5) * 50)
|
||||
currentQPS.value = Math.max(200, Math.min(1200, currentQPS.value + qpsChange))
|
||||
qpsHistory.value.shift()
|
||||
qpsHistory.value.push(currentQPS.value)
|
||||
|
||||
// 根据指标触发扩缩容逻辑
|
||||
checkScalingLogic()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 检查扩缩容逻辑
|
||||
const checkScalingLogic = () => {
|
||||
if (isScaling.value) return
|
||||
|
||||
let shouldScale = false
|
||||
let newCount = currentInstances.value
|
||||
let reason = ''
|
||||
|
||||
// 扩容检查
|
||||
if (cpuUsage.value > 75 || memoryUsage.value > 75 || currentQPS.value > 900) {
|
||||
if (currentInstances.value < maxInstances.value) {
|
||||
shouldScale = true
|
||||
newCount = currentInstances.value + 1
|
||||
if (cpuUsage.value > 75) reason = 'CPU使用率超过75%'
|
||||
else if (memoryUsage.value > 75) reason = '内存使用率超过75%'
|
||||
else reason = 'QPS超过900/s'
|
||||
}
|
||||
}
|
||||
// 缩容检查
|
||||
else if (cpuUsage.value < 35 && memoryUsage.value < 40 && currentQPS.value < 400) {
|
||||
if (currentInstances.value > minInstances.value) {
|
||||
shouldScale = true
|
||||
newCount = currentInstances.value - 1
|
||||
reason = '资源使用率低于阈值'
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldScale) {
|
||||
triggerScaling(newCount, reason)
|
||||
}
|
||||
}
|
||||
|
||||
// 触发扩缩容
|
||||
const triggerScaling = (newCount, reason) => {
|
||||
isScaling.value = true
|
||||
scaleReason.value = `${newCount > currentInstances.value ? '扩容' : '缩容'}中: ${reason}`
|
||||
|
||||
setTimeout(() => {
|
||||
currentInstances.value = newCount
|
||||
isScaling.value = false
|
||||
scaleReason.value = ''
|
||||
|
||||
// 添加到历史
|
||||
scalingHistory.value.unshift({
|
||||
type: newCount > currentInstances.value ? '扩容' : '缩容',
|
||||
from: newCount > currentInstances.value ? newCount - 1 : newCount + 1,
|
||||
to: newCount,
|
||||
reason,
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
})
|
||||
scalingHistory.value = scalingHistory.value.slice(0, 5)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startSimulation()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(simulationInterval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auto-scaling-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Metric Selector */
|
||||
.metric-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.selector-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.selector-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.metric-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.metric-btn:hover {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
}
|
||||
|
||||
.metric-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Monitoring Dashboard */
|
||||
.monitoring-dashboard {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.refresh-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #22c55e;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.metric-card.warning {
|
||||
border-color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.05);
|
||||
}
|
||||
|
||||
.metric-card.danger {
|
||||
border-color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.metric-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.value-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.metric-progress {
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.metric-threshold {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.metric-chart {
|
||||
height: 40px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sparkline {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Instances Card */
|
||||
.metric-card.instances {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metric-card.instances {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
.instances-display {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.instance-count {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.count-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.instance-range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.instance-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.instance-dot {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 50%;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.instance-dot.active {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 4px var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.instance-dot.scaling {
|
||||
animation: scaleIn 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||
50% { transform: scale(1.1); opacity: 1; }
|
||||
}
|
||||
|
||||
.scale-reason {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.reason-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Scaling History */
|
||||
.scaling-history {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.history-count {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.history-item.scaleOut {
|
||||
border-left: 3px solid #22c55e;
|
||||
}
|
||||
|
||||
.history-item:not(.scaleOut) {
|
||||
border-left: 3px solid #f59e0b;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-action {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.item-reason {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.item-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Best Practices */
|
||||
.best-practices {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.practices-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.practices-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.practices-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.practice-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.practice-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.practice-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.practice-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,924 @@
|
||||
<template>
|
||||
<div class="blue-green-deployment-demo">
|
||||
<div class="header">
|
||||
<div class="title">蓝绿部署</div>
|
||||
<div class="subtitle">零停机发布的经典策略,两套环境瞬间切换</div>
|
||||
</div>
|
||||
|
||||
<!-- 部署状态控制 -->
|
||||
<div class="deployment-control">
|
||||
<div class="status-display">
|
||||
<div class="status-item" :class="{ active: currentEnv === 'blue' }">
|
||||
<div class="status-icon">🔵</div>
|
||||
<div class="status-label">蓝环境</div>
|
||||
<div class="status-version">v{{ blueVersion }}</div>
|
||||
<div class="status-traffic">
|
||||
{{ currentEnv === 'blue' ? '100%' : '0%' }} 流量
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="switch-control">
|
||||
<button
|
||||
class="switch-btn"
|
||||
@click="toggleEnvironment"
|
||||
:disabled="isSwitching"
|
||||
:class="{ switching: isSwitching }"
|
||||
>
|
||||
<span v-if="!isSwitching">
|
||||
{{ currentEnv === 'blue' ? '切换到绿环境 →' : '← 切换到蓝环境' }}
|
||||
</span>
|
||||
<span v-else class="switching-text">
|
||||
<span class="spinner"></span>
|
||||
切换中...
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="progress-bar" v-if="isSwitching">
|
||||
<div class="progress-fill" :style="{ width: switchProgress + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-item" :class="{ active: currentEnv === 'green' }">
|
||||
<div class="status-icon">🟢</div>
|
||||
<div class="status-label">绿环境</div>
|
||||
<div class="status-version">v{{ greenVersion }}</div>
|
||||
<div class="status-traffic">
|
||||
{{ currentEnv === 'green' ? '100%' : '0%' }} 流量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 架构可视化 -->
|
||||
<div class="architecture-view">
|
||||
<div class="layer users">
|
||||
<div class="layer-title">用户流量</div>
|
||||
<div class="users-row">
|
||||
<div v-for="i in 5" :key="i" class="user-avatar" :class="{ active: isUserActive(i) }">
|
||||
👤
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-down">↓</div>
|
||||
|
||||
<div class="layer load-balancer">
|
||||
<div class="lb-box">
|
||||
<div class="lb-icon">⚖️</div>
|
||||
<div class="lb-info">
|
||||
<div class="lb-title">负载均衡器</div>
|
||||
<div class="lb-status">
|
||||
当前指向:
|
||||
<span class="env-badge" :class="currentEnv">
|
||||
{{ currentEnv === 'blue' ? '🔵 蓝环境' : '🟢 绿环境' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-down">↓</div>
|
||||
|
||||
<div class="layer environments">
|
||||
<div class="env-row">
|
||||
<!-- 蓝环境 -->
|
||||
<div class="env-box" :class="{ active: currentEnv === 'blue', standby: currentEnv === 'green' }">
|
||||
<div class="env-header">
|
||||
<span class="env-icon">🔵</span>
|
||||
<span class="env-name">蓝环境</span>
|
||||
<span class="env-badge version">v{{ blueVersion }}</span>
|
||||
</div>
|
||||
<div class="env-content">
|
||||
<div class="server-list">
|
||||
<div v-for="i in 3" :key="i" class="server-item" :class="{ busy: isServerBusy('blue', i) }">
|
||||
<span class="server-icon">🖥️</span>
|
||||
<span class="server-name">B{{ i }}</span>
|
||||
<span class="server-status" :class="getServerStatus('blue', i)">
|
||||
{{ getServerStatus('blue', i) === 'healthy' ? '●' : '○' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="env-footer">
|
||||
<div class="traffic-indicator">
|
||||
<span class="indicator-label">流量:</span>
|
||||
<span class="indicator-value" :class="{ active: currentEnv === 'blue' }">
|
||||
{{ currentEnv === 'blue' ? '100%' : '0%' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-badge" :class="currentEnv === 'blue' ? 'active' : 'standby'">
|
||||
{{ currentEnv === 'blue' ? '生产环境' : '待命' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绿环境 -->
|
||||
<div class="env-box" :class="{ active: currentEnv === 'green', standby: currentEnv === 'blue' }">
|
||||
<div class="env-header">
|
||||
<span class="env-icon">🟢</span>
|
||||
<span class="env-name">绿环境</span>
|
||||
<span class="env-badge version">v{{ greenVersion }}</span>
|
||||
</div>
|
||||
<div class="env-content">
|
||||
<div class="server-list">
|
||||
<div v-for="i in 3" :key="i" class="server-item" :class="{ busy: isServerBusy('green', i) }">
|
||||
<span class="server-icon">🖥️</span>
|
||||
<span class="server-name">G{{ i }}</span>
|
||||
<span class="server-status" :class="getServerStatus('green', i)">
|
||||
{{ getServerStatus('green', i) === 'healthy' ? '●' : '○' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="env-footer">
|
||||
<div class="traffic-indicator">
|
||||
<span class="indicator-label">流量:</span>
|
||||
<span class="indicator-value" :class="{ active: currentEnv === 'green' }">
|
||||
{{ currentEnv === 'green' ? '100%' : '0%' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-badge" :class="currentEnv === 'green' ? 'active' : 'standby'">
|
||||
{{ currentEnv === 'green' ? '生产环境' : '待命' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 部署流程说明 -->
|
||||
<div class="deployment-process">
|
||||
<div class="process-title">蓝绿部署流程</div>
|
||||
<div class="process-steps">
|
||||
<div class="step" :class="{ active: deploymentStep >= 1 }">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">绿环境部署</div>
|
||||
<div class="step-desc">在绿环境部署新版本,进行冒烟测试</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-arrow">→</div>
|
||||
<div class="step" :class="{ active: deploymentStep >= 2 }">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">切换流量</div>
|
||||
<div class="step-desc">将负载均衡器指向绿环境,流量瞬间切换</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-arrow">→</div>
|
||||
<div class="step" :class="{ active: deploymentStep >= 3 }">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">监控观察</div>
|
||||
<div class="step-desc">观察绿环境运行状态,确认无异常</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-arrow">→</div>
|
||||
<div class="step" :class="{ active: deploymentStep >= 4 }">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">蓝环境升级</div>
|
||||
<div class="step-desc">在蓝环境部署新版本,为下次切换做准备</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 优缺点分析 -->
|
||||
<div class="pros-cons-analysis">
|
||||
<div class="analysis-title">蓝绿部署优缺点</div>
|
||||
<div class="analysis-grid">
|
||||
<div class="analysis-card pros">
|
||||
<div class="card-header">
|
||||
<span class="header-icon">✅</span>
|
||||
<span class="header-title">优点</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="feature-list">
|
||||
<li class="feature-item">
|
||||
<span class="item-title">零停机时间:</span>
|
||||
<span class="item-desc">流量切换在毫秒级完成,用户无感知</span>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<span class="item-title">快速回滚:</span>
|
||||
<span class="item-desc">发现问题可立即切回原环境,风险可控</span>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<span class="item-title">完整的预发布测试:</span>
|
||||
<span class="item-desc">新环境可完整测试后再接管流量</span>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<span class="item-title">数据一致性:</span>
|
||||
<span class="item-desc">无需处理新旧版本同时运行时的兼容问题</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analysis-card cons">
|
||||
<div class="card-header">
|
||||
<span class="header-icon">❌</span>
|
||||
<span class="header-title">缺点</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="feature-list">
|
||||
<li class="feature-item">
|
||||
<span class="item-title">资源成本高:</span>
|
||||
<span class="item-desc">需要同时维护两套完整环境,服务器成本翻倍</span>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<span class="item-title">数据库兼容性挑战:</span>
|
||||
<span class="item-desc">如果涉及数据库Schema变更,需要特别处理兼容性</span>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<span class="item-title">预热问题:</span>
|
||||
<span class="item-desc">新环境启动后可能需要时间预热缓存、连接池等</span>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<span class="item-title">不适合有状态服务:</span>
|
||||
<span class="item-desc">对于长连接、会话保持要求高的场景处理复杂</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
const currentEnv = ref('blue')
|
||||
const blueVersion = ref('1.0.0')
|
||||
const greenVersion = ref('1.1.0')
|
||||
const isSwitching = ref(false)
|
||||
const switchProgress = ref(0)
|
||||
const deploymentStep = ref(4)
|
||||
|
||||
// 加权服务器数据
|
||||
const weightedServers = ref([
|
||||
{ id: 1, name: 'Server 1', specs: '16核 64GB NVMe', ip: '10.0.1.10', weight: 5, status: 'healthy' },
|
||||
{ id: 2, name: 'Server 2', specs: '8核 32GB SSD', ip: '10.0.1.11', weight: 3, status: 'healthy' },
|
||||
{ id: 3, name: 'Server 3', specs: '4核 16GB SSD', ip: '10.0.1.12', weight: 2, status: 'healthy' }
|
||||
])
|
||||
|
||||
const totalTraffic = ref(1000)
|
||||
|
||||
const getTotalWeight = () => {
|
||||
return weightedServers.value.reduce((sum, s) => sum + s.weight, 0)
|
||||
}
|
||||
|
||||
const getAllocationPercentage = (weight) => {
|
||||
const total = getTotalWeight()
|
||||
return total > 0 ? (weight / total) * 100 : 0
|
||||
}
|
||||
|
||||
const getWeightColor = (index) => {
|
||||
const colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6']
|
||||
return colors[index % colors.length]
|
||||
}
|
||||
|
||||
// 流量流动画
|
||||
const trafficFlows = ref([])
|
||||
|
||||
const generateTrafficFlows = () => {
|
||||
const colors = ['#3b82f6', '#22c55e', '#f59e0b']
|
||||
trafficFlows.value = Array.from({ length: 12 }, (_, i) => ({
|
||||
delay: i * 0.2,
|
||||
color: colors[Math.floor(Math.random() * colors.length)]
|
||||
}))
|
||||
}
|
||||
|
||||
const isUserActive = (index) => {
|
||||
return index <= 3 || (currentEnv.value === 'green' && index > 3)
|
||||
}
|
||||
|
||||
const isServerBusy = (env, index) => {
|
||||
return (currentEnv.value === env && index <= 2)
|
||||
}
|
||||
|
||||
const getServerStatus = (env, index) => {
|
||||
return 'healthy'
|
||||
}
|
||||
|
||||
const toggleEnvironment = async () => {
|
||||
if (isSwitching.value) return
|
||||
|
||||
isSwitching.value = true
|
||||
switchProgress.value = 0
|
||||
|
||||
// 模拟切换进度
|
||||
const interval = setInterval(() => {
|
||||
switchProgress.value += 10
|
||||
if (switchProgress.value >= 100) {
|
||||
clearInterval(interval)
|
||||
currentEnv.value = currentEnv.value === 'blue' ? 'green' : 'blue'
|
||||
isSwitching.value = false
|
||||
switchProgress.value = 0
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
generateTrafficFlows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.blue-green-deployment-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Deployment Control */
|
||||
.deployment-control {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-display {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.status-display {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.status-item {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.status-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
opacity: 1;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.status-version {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-traffic {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.switch-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.switch-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.switch-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.switch-btn.switching {
|
||||
background: linear-gradient(135deg, #6b7280, #9ca3af);
|
||||
}
|
||||
|
||||
.switching-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6, #22c55e);
|
||||
border-radius: 3px;
|
||||
transition: width 0.1s;
|
||||
}
|
||||
|
||||
/* Architecture View */
|
||||
.architecture-view {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.layer {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.layer-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.users-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 50%;
|
||||
font-size: 1.25rem;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.user-avatar.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
/* Load Balancer */
|
||||
.load-balancer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lb-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.lb-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.lb-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lb-status {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.9;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.env-badge {
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.env-badge.blue {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
color: #bfdbfe;
|
||||
}
|
||||
|
||||
.env-badge.green {
|
||||
background: rgba(34, 197, 94, 0.3);
|
||||
color: #bbf7d0;
|
||||
}
|
||||
|
||||
/* Environments */
|
||||
.env-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.env-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.env-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.env-box.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
opacity: 1;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.env-box.standby {
|
||||
border-color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.env-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.env-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.env-badge.version {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.env-content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.server-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.server-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.server-item.busy {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.server-status {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.server-status.healthy {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.env-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.traffic-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.indicator-label {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.indicator-value {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.indicator-value.active {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.status-badge.standby {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Deployment Process */
|
||||
.deployment-process {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.process-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.process-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step-arrow {
|
||||
font-size: 1.25rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
/* Pros Cons Analysis */
|
||||
.pros-cons-analysis {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.analysis-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.analysis-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.analysis-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.analysis-card.pros {
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.analysis-card.cons {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.analysis-card.pros .card-header {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.analysis-card.cons .card-header {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.feature-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,686 @@
|
||||
<template>
|
||||
<div class="canary-release-demo">
|
||||
<div class="header">
|
||||
<div class="title">金丝雀发布</div>
|
||||
<div class="subtitle">灰度发布策略,小流量先行验证新版本</div>
|
||||
</div>
|
||||
|
||||
<!-- 流量分配控制 -->
|
||||
<div class="traffic-control">
|
||||
<div class="control-header">
|
||||
<span class="control-title">流量分配比例</span>
|
||||
<span class="control-hint">拖动滑块调整新旧版本流量占比</span>
|
||||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
<div class="version-labels">
|
||||
<span class="version-label stable">
|
||||
<span class="dot blue"></span>
|
||||
稳定版 v{{ stableVersion }}
|
||||
</span>
|
||||
<span class="percentage stable">{{ 100 - canaryPercentage }}%</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="canaryPercentage"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
class="traffic-slider"
|
||||
/>
|
||||
|
||||
<div class="version-labels">
|
||||
<span class="version-label canary">
|
||||
<span class="dot yellow"></span>
|
||||
金丝雀 v{{ canaryVersion }}
|
||||
</span>
|
||||
<span class="percentage canary">{{ canaryPercentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预设按钮 -->
|
||||
<div class="preset-buttons">
|
||||
<button
|
||||
v-for="preset in trafficPresets"
|
||||
:key="preset.value"
|
||||
class="preset-btn"
|
||||
:class="{ active: canaryPercentage === preset.value }"
|
||||
@click="canaryPercentage = preset.value"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可视化流量 -->
|
||||
<div class="traffic-visualization">
|
||||
<div class="viz-header">
|
||||
<span class="viz-title">实时流量模拟</span>
|
||||
<span class="viz-stats">
|
||||
总请求: {{ totalRequests }} |
|
||||
稳定版: {{ stableRequests }} |
|
||||
金丝雀: {{ canaryRequests }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="traffic-pipeline">
|
||||
<div class="pipeline-stage">
|
||||
<div class="stage-label">用户请求</div>
|
||||
<div class="request-bubbles">
|
||||
<div
|
||||
v-for="(req, index) in requestQueue"
|
||||
:key="index"
|
||||
class="request-bubble"
|
||||
:class="{ canary: req.isCanary }"
|
||||
:style="{ animationDelay: req.delay + 's' }"
|
||||
>
|
||||
{{ req.isCanary ? 'C' : 'S' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pipeline-arrow">→</div>
|
||||
|
||||
<div class="pipeline-stage">
|
||||
<div class="stage-label">负载均衡器</div>
|
||||
<div class="lb-diagram">
|
||||
<div class="lb-icon">⚖️</div>
|
||||
<div class="routing-logic">
|
||||
<div class="logic-line">
|
||||
<span class="logic-label">Canary:</span>
|
||||
<span class="logic-value">{{ canaryPercentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pipeline-arrow">→</div>
|
||||
|
||||
<div class="pipeline-stage">
|
||||
<div class="stage-label">后端服务</div>
|
||||
<div class="backend-pods">
|
||||
<div class="pod-group stable">
|
||||
<div class="pod-label">稳定版 v{{ stableVersion }}</div>
|
||||
<div class="pods-row">
|
||||
<div
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="pod"
|
||||
:class="{ active: hasTrafficToStable }"
|
||||
>
|
||||
<span class="pod-icon">📦</span>
|
||||
<span class="pod-name">S{{ i }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pod-group canary">
|
||||
<div class="pod-label">金丝雀 v{{ canaryVersion }}</div>
|
||||
<div class="pods-row">
|
||||
<div
|
||||
v-for="i in 2"
|
||||
:key="i"
|
||||
class="pod"
|
||||
:class="{ active: hasTrafficToCanary }"
|
||||
>
|
||||
<span class="pod-icon">🧪</span>
|
||||
<span class="pod-name">C{{ i }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 金丝雀发布策略 -->
|
||||
<div class="canary-strategy">
|
||||
<div class="strategy-title">金丝雀发布最佳实践</div>
|
||||
|
||||
<div class="strategy-grid">
|
||||
<div class="strategy-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">📊</span>
|
||||
<span class="card-title">渐进式放量</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="strategy-list">
|
||||
<li>1% → 5% → 10% → 25% → 50% → 100%</li>
|
||||
<li>每个阶段观察至少15-30分钟</li>
|
||||
<li>关键指标:错误率、延迟、吞吐量</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">🎯</span>
|
||||
<span class="card-title">精准用户选择</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="strategy-list">
|
||||
<li>内部员工/测试用户先行</li>
|
||||
<li>按地域:选择特定区域用户</li>
|
||||
<li>按用户属性:VIP用户或普通用户</li>
|
||||
<li>按设备类型:iOS/Android/Web</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">🛡️</span>
|
||||
<span class="card-title">自动回滚机制</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="strategy-list">
|
||||
<li>错误率超过阈值自动回滚</li>
|
||||
<li>P99延迟异常触发告警</li>
|
||||
<li>关键业务指标下降自动回滚</li>
|
||||
<li>一键回滚:30秒内恢复旧版本</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">📈</span>
|
||||
<span class="card-title">监控与指标</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="strategy-list">
|
||||
<li>基础设施:CPU、内存、磁盘、网络</li>
|
||||
<li>应用指标:QPS、错误率、延迟分布</li>
|
||||
<li>业务指标:转化率、订单量、收入</li>
|
||||
<li>用户体验:页面加载时间、交互延迟</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const canaryPercentage = ref(10)
|
||||
const stableVersion = ref('1.0.0')
|
||||
const canaryVersion = ref('1.1.0')
|
||||
const currentEnv = ref('blue')
|
||||
const isSwitching = ref(false)
|
||||
const blueVersion = ref('1.0.0')
|
||||
const greenVersion = ref('1.1.0')
|
||||
const switchProgress = ref(0)
|
||||
const deploymentStep = ref(4)
|
||||
|
||||
const trafficPresets = [
|
||||
{ label: '1%', value: 1 },
|
||||
{ label: '5%', value: 5 },
|
||||
{ label: '10%', value: 10 },
|
||||
{ label: '25%', value: 25 },
|
||||
{ label: '50%', value: 50 },
|
||||
{ label: '100%', value: 100 }
|
||||
]
|
||||
|
||||
// 请求队列
|
||||
const requestQueue = ref([])
|
||||
const totalRequests = ref(0)
|
||||
const stableRequests = ref(0)
|
||||
const canaryRequests = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const hasTrafficToStable = computed(() => canaryPercentage.value < 100)
|
||||
const hasTrafficToCanary = computed(() => canaryPercentage.value > 0)
|
||||
|
||||
// 生成请求
|
||||
const generateRequests = () => {
|
||||
const isCanary = Math.random() * 100 < canaryPercentage.value
|
||||
const request = {
|
||||
isCanary,
|
||||
delay: Math.random() * 2
|
||||
}
|
||||
requestQueue.value.push(request)
|
||||
if (requestQueue.value.length > 10) {
|
||||
requestQueue.value.shift()
|
||||
}
|
||||
|
||||
// 更新统计
|
||||
totalRequests.value++
|
||||
if (isCanary) {
|
||||
canaryRequests.value++
|
||||
} else {
|
||||
stableRequests.value++
|
||||
}
|
||||
}
|
||||
|
||||
let requestInterval
|
||||
onMounted(() => {
|
||||
requestInterval = setInterval(generateRequests, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(requestInterval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.canary-release-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Traffic Control */
|
||||
.traffic-control {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.control-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.control-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.control-hint {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.version-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.version-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.version-label.stable {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.version-label.canary {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot.blue {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.dot.yellow {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.percentage.stable {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.percentage.canary {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.traffic-slider {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: linear-gradient(90deg, #3b82f6 0%, #3b82f6 v-bind('(100 - canaryPercentage) + "%"'), #f59e0b v-bind('(100 - canaryPercentage) + "%"'), #f59e0b 100%);
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.traffic-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: white;
|
||||
border: 3px solid var(--vp-c-brand);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.preset-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
}
|
||||
|
||||
.preset-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Traffic Visualization */
|
||||
.traffic-visualization {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.viz-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.viz-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.viz-stats {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.traffic-pipeline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.pipeline-stage {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stage-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.request-bubbles {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.request-bubble {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
animation: bubbleFlow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.request-bubble.canary {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
@keyframes bubbleFlow {
|
||||
0%, 100% { transform: translateY(0); opacity: 1; }
|
||||
50% { transform: translateY(-5px); opacity: 0.8; }
|
||||
}
|
||||
|
||||
.pipeline-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lb-diagram {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.lb-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.routing-logic {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.logic-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.logic-label {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.logic-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.backend-pods {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.pod-group {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.pod-group.stable {
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.pod-group.canary {
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.pod-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pods-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pod {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
padding: 0.4rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
min-width: 40px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.pod.active {
|
||||
opacity: 1;
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 2px var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.pod.busy {
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.pod-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.pod-name {
|
||||
font-size: 0.6rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Canary Strategy */
|
||||
.canary-strategy {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.strategy-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.strategy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.strategy-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.strategy-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.strategy-list {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.strategy-list li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,691 @@
|
||||
<template>
|
||||
<div class="health-check-demo">
|
||||
<div class="header">
|
||||
<div class="title">健康检查机制</div>
|
||||
<div class="subtitle">主动探测、被动感知与智能阈值</div>
|
||||
</div>
|
||||
|
||||
<!-- 模式选择器 -->
|
||||
<div class="mode-selector">
|
||||
<button
|
||||
v-for="mode in modes"
|
||||
:key="mode.key"
|
||||
class="mode-btn"
|
||||
:class="{ active: currentMode === mode.key }"
|
||||
@click="currentMode = mode.key"
|
||||
>
|
||||
<span class="mode-icon">{{ mode.icon }}</span>
|
||||
<span class="mode-name">{{ mode.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 可视化展示区 -->
|
||||
<div class="visualization-area">
|
||||
<!-- 负载均衡器 -->
|
||||
<div class="lb-node">
|
||||
<div class="lb-icon">⚖️</div>
|
||||
<div class="lb-label">负载均衡器</div>
|
||||
<div class="lb-status">{{ currentModeData.label }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接线和健康检查标记 -->
|
||||
<div class="connections-layer">
|
||||
<div
|
||||
v-for="(server, index) in servers"
|
||||
:key="index"
|
||||
class="connection-line"
|
||||
:class="{
|
||||
healthy: server.status === 'healthy',
|
||||
unhealthy: server.status === 'unhealthy',
|
||||
checking: server.status === 'checking'
|
||||
}"
|
||||
>
|
||||
<div class="health-packet" v-if="server.showPacket">{{ server.packetType }}</div>
|
||||
<div class="health-indicator">
|
||||
<span v-if="server.status === 'healthy'">✅</span>
|
||||
<span v-else-if="server.status === 'unhealthy'">❌</span>
|
||||
<span v-else">🔄</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 后端服务器 -->
|
||||
<div class="servers-grid">
|
||||
<div
|
||||
v-for="(server, index) in servers"
|
||||
:key="index"
|
||||
class="server-card"
|
||||
:class="{
|
||||
healthy: server.status === 'healthy',
|
||||
unhealthy: server.status === 'unhealthy',
|
||||
checking: server.status === 'checking'
|
||||
}"
|
||||
>
|
||||
<div class="server-header">
|
||||
<div class="server-icon">🖥️</div>
|
||||
<div class="server-info">
|
||||
<div class="server-name">Server {{ index + 1 }}</div>
|
||||
<div class="server-ip">{{ server.ip }}</div>
|
||||
</div>
|
||||
<div class="status-badge" :class="server.status">
|
||||
{{ server.status === 'healthy' ? '健康' : server.status === 'unhealthy' ? '故障' : '检查中' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="server-metrics">
|
||||
<div class="metric">
|
||||
<div class="metric-label">响应时间</div>
|
||||
<div class="metric-value" :class="{ warning: server.responseTime > 100 }">{{ server.responseTime }}ms</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">失败率</div>
|
||||
<div class="metric-value" :class="{ danger: server.errorRate > 5 }">{{ server.errorRate }}%</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">连续成功</div>
|
||||
<div class="metric-value">{{ server.consecutiveSuccess }}/3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 检查机制详情 -->
|
||||
<div class="mechanism-details">
|
||||
<div class="detail-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">{{ currentModeData.icon }}</span>
|
||||
<span class="card-title">{{ currentModeData.name }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="description">{{ currentModeData.description }}</p>
|
||||
|
||||
<div class="config-section">
|
||||
<div class="section-title">关键配置参数</div>
|
||||
<div class="config-grid">
|
||||
<div
|
||||
v-for="param in currentModeData.params"
|
||||
:key="param.name"
|
||||
class="config-item"
|
||||
>
|
||||
<div class="config-name">{{ param.name }}</div>
|
||||
<div class="config-value">{{ param.value }}</div>
|
||||
<div class="config-desc">{{ param.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<div class="pros-cons-title">✅ 优点</div>
|
||||
<ul>
|
||||
<li v-for="pro in currentModeData.pros" :key="pro">{{ pro }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<div class="pros-cons-title">❌ 缺点</div>
|
||||
<ul>
|
||||
<li v-for="con in currentModeData.cons" :key="con">{{ con }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const currentMode = ref('active')
|
||||
|
||||
const modes = [
|
||||
{
|
||||
key: 'active',
|
||||
name: '主动健康检查',
|
||||
icon: '🔍',
|
||||
label: 'Probing'
|
||||
},
|
||||
{
|
||||
key: 'passive',
|
||||
name: '被动健康检查',
|
||||
icon: '👁️',
|
||||
label: 'Observing'
|
||||
},
|
||||
{
|
||||
key: 'threshold',
|
||||
name: '阈值判定',
|
||||
icon: '📊',
|
||||
label: 'Threshold'
|
||||
}
|
||||
]
|
||||
|
||||
const modeDetails = {
|
||||
active: {
|
||||
name: '主动健康检查',
|
||||
icon: '🔍',
|
||||
label: '定期主动探测',
|
||||
description: '负载均衡器主动向后端服务器发送探测请求(如HTTP /health、TCP握手等),根据响应判断服务器健康状态。这是最常用的健康检查方式。',
|
||||
params: [
|
||||
{ name: '检查间隔', value: '5s', desc: '两次检查之间的时间间隔' },
|
||||
{ name: '超时时间', value: '3s', desc: '等待响应的最大时间' },
|
||||
{ name: '健康阈值', value: '2', desc: '判定为健康所需的连续成功次数' },
|
||||
{ name: '不健康阈值', value: '3', desc: '判定为不健康所需的连续失败次数' }
|
||||
],
|
||||
pros: [
|
||||
'检测结果准确可靠,能真实反映服务状态',
|
||||
'可以精确配置检查参数和阈值',
|
||||
'不依赖实际业务流量,无流量时也能检测'
|
||||
],
|
||||
cons: [
|
||||
'产生额外的探测流量和系统开销',
|
||||
'检查间隔期间发生的故障不能立即发现',
|
||||
'需要后端服务提供健康检查端点'
|
||||
]
|
||||
},
|
||||
passive: {
|
||||
name: '被动健康检查',
|
||||
icon: '👁️',
|
||||
label: '观察实际流量',
|
||||
description: '负载均衡器通过监控实际业务流量的响应情况来判断后端健康状态。不发送额外的探测请求,而是分析真实请求的响应时间、状态码等指标。',
|
||||
params: [
|
||||
{ name: '采样窗口', value: '60s', desc: '统计响应时间的时间窗口' },
|
||||
{ name: '错误阈值', value: '10%', desc: '可接受的最大错误率' },
|
||||
{ name: '延迟阈值', value: '500ms', desc: '可接受的最大平均延迟' },
|
||||
{ name: '最小样本', value: '100', desc: '判定所需的最小请求数' }
|
||||
],
|
||||
pros: [
|
||||
'不产生额外的探测流量',
|
||||
'能反映真实业务场景下的服务状态',
|
||||
'对无法提供健康检查端点的服务也有效'
|
||||
],
|
||||
cons: [
|
||||
'需要足够的流量样本才能判定',
|
||||
'低流量时可能无法及时发现问题',
|
||||
'检测结果受业务流量特征影响较大'
|
||||
]
|
||||
},
|
||||
threshold: {
|
||||
name: '阈值判定机制',
|
||||
icon: '📊',
|
||||
label: '多维度阈值',
|
||||
description: '结合多种指标(响应时间、错误率、连接数、CPU/内存使用率等)设置阈值,进行综合判定。支持动态阈值调整,适应不同负载场景。',
|
||||
params: [
|
||||
{ name: '响应时间P99', value: '200ms', desc: '99%请求的响应时间阈值' },
|
||||
{ name: '错误率', value: '1%', desc: '可接受的最大错误比例' },
|
||||
{ name: '连接数', value: '1000', desc: '最大并发连接数限制' },
|
||||
{ name: 'CPU使用率', value: '80%', desc: '服务器CPU使用率阈值' }
|
||||
],
|
||||
pros: [
|
||||
'多维度综合判定,结果更全面准确',
|
||||
'可根据业务特点灵活配置阈值',
|
||||
'支持动态阈值调整,适应负载变化'
|
||||
],
|
||||
cons: [
|
||||
'配置复杂,需要深入理解各项指标',
|
||||
'阈值设置不当可能导致误判',
|
||||
'需要持续调优以达到最佳效果'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const currentModeData = computed(() => modeDetails[currentMode.value])
|
||||
|
||||
// 模拟服务器数据
|
||||
const servers = ref([
|
||||
{ ip: '10.0.1.10', status: 'healthy', responseTime: 25, errorRate: 0.1, consecutiveSuccess: 5, showPacket: false, packetType: '' },
|
||||
{ ip: '10.0.1.11', status: 'healthy', responseTime: 30, errorRate: 0.2, consecutiveSuccess: 4, showPacket: false, packetType: '' },
|
||||
{ ip: '10.0.1.12', status: 'unhealthy', responseTime: 3500, errorRate: 15, consecutiveSuccess: 0, showPacket: false, packetType: '' }
|
||||
])
|
||||
|
||||
// 模拟健康检查动画
|
||||
let healthCheckInterval
|
||||
let packetInterval
|
||||
|
||||
const simulateHealthCheck = () => {
|
||||
// 随机选择一个服务器发送健康检查包
|
||||
const serverIndex = Math.floor(Math.random() * servers.value.length)
|
||||
const server = servers.value[serverIndex]
|
||||
|
||||
server.showPacket = true
|
||||
server.packetType = currentMode.value === 'active' ? 'GET /health' : currentMode.value === 'passive' ? 'Observing' : 'Metrics'
|
||||
|
||||
setTimeout(() => {
|
||||
server.showPacket = false
|
||||
|
||||
// 模拟检查结果
|
||||
if (server.status === 'healthy') {
|
||||
server.consecutiveSuccess = Math.min(server.consecutiveSuccess + 1, 5)
|
||||
server.responseTime = Math.floor(Math.random() * 50) + 20
|
||||
} else if (server.status === 'unhealthy') {
|
||||
server.consecutiveSuccess = 0
|
||||
server.responseTime = 3000 + Math.floor(Math.random() * 2000)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 启动健康检查模拟
|
||||
healthCheckInterval = setInterval(() => {
|
||||
simulateHealthCheck()
|
||||
}, 2000)
|
||||
|
||||
// 轮播显示活跃服务器
|
||||
packetInterval = setInterval(() => {
|
||||
const healthyServers = servers.value.filter(s => s.status === 'healthy')
|
||||
if (healthyServers.length > 0) {
|
||||
const randomServer = healthyServers[Math.floor(Math.random() * healthyServers.length)]
|
||||
activeServer.value = servers.value.indexOf(randomServer)
|
||||
}
|
||||
}, 1500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(healthCheckInterval)
|
||||
clearInterval(packetInterval)
|
||||
})
|
||||
|
||||
const activeServer = ref(0)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.health-check-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Mode Selector */
|
||||
.mode-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mode-selector {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.mode-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Visualization Area */
|
||||
.visualization-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.lb-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.lb-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.lb-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lb-status {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.9;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Connections Layer */
|
||||
.connections-layer {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.connection-line.healthy {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.connection-line.unhealthy {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.connection-line.checking {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.health-packet {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
font-size: 0.7rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
animation: packetMove 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes packetMove {
|
||||
0% { transform: translateY(0); opacity: 1; }
|
||||
100% { transform: translateY(30px); opacity: 0; }
|
||||
}
|
||||
|
||||
.health-indicator {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* Servers Grid */
|
||||
.servers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.servers-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.server-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.server-card.healthy {
|
||||
border-color: #22c55e;
|
||||
background: rgba(34, 197, 94, 0.05);
|
||||
}
|
||||
|
||||
.server-card.unhealthy {
|
||||
border-color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.server-card.checking {
|
||||
border-color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.05);
|
||||
}
|
||||
|
||||
.server-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.server-ip {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.healthy {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.status-badge.unhealthy {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-badge.checking {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.server-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.metric-value.warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.metric-value.danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Mechanism Details */
|
||||
.mechanism-details {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.config-item {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.config-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.config-value {
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.config-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.pros-cons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pros-cons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.pros-cons-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pros ul,
|
||||
.cons ul {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.pros li,
|
||||
.cons li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,680 @@
|
||||
<template>
|
||||
<div class="load-balancer-types-demo">
|
||||
<div class="header">
|
||||
<div class="title">负载均衡器类型</div>
|
||||
<div class="subtitle">从四层到七层,从硬件到软件的演进</div>
|
||||
</div>
|
||||
|
||||
<!-- 层级选择器 -->
|
||||
<div class="layer-selector">
|
||||
<button
|
||||
v-for="layer in layers"
|
||||
:key="layer.key"
|
||||
class="layer-btn"
|
||||
:class="{ active: currentLayer === layer.key }"
|
||||
@click="currentLayer = layer.key"
|
||||
>
|
||||
<span class="layer-icon">{{ layer.icon }}</span>
|
||||
<span class="layer-name">{{ layer.name }}</span>
|
||||
<span class="layer-tag">{{ layer.tag }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 架构对比图 -->
|
||||
<div class="architecture-comparison">
|
||||
<div class="comparison-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">传统架构</span>
|
||||
<span class="panel-badge single">单点</span>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div class="single-server">
|
||||
<div class="server-icon">🖥️</div>
|
||||
<div class="server-label">Web Server</div>
|
||||
<div class="server-load">
|
||||
<div class="load-bar" :style="{ width: '95%' }"></div>
|
||||
</div>
|
||||
<div class="load-text">负载: 95% 🔥</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-arrow">→</div>
|
||||
|
||||
<div class="comparison-panel highlighted">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">负载均衡架构</span>
|
||||
<span class="panel-badge distributed">分布式</span>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div class="lb-layer">
|
||||
<div class="lb-node">
|
||||
<span class="lb-icon">⚖️</span>
|
||||
<span class="lb-label">{{ currentLayerData.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="servers-layer">
|
||||
<div
|
||||
v-for="(server, index) in servers"
|
||||
:key="index"
|
||||
class="server-node"
|
||||
:class="{ active: activeServer === index }"
|
||||
>
|
||||
<div class="server-icon-small">🖥️</div>
|
||||
<div class="server-id">S{{ index + 1 }}</div>
|
||||
<div class="server-load-mini">
|
||||
<div class="load-bar-mini" :style="{ width: server.load + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息面板 -->
|
||||
<div class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ currentLayerData.icon }}</span>
|
||||
<span class="detail-title">{{ currentLayerData.name }}</span>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="detail-section">
|
||||
<div class="section-title">工作原理</div>
|
||||
<p class="section-desc">{{ currentLayerData.principle }}</p>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-title">典型产品</div>
|
||||
<div class="product-tags">
|
||||
<span v-for="product in currentLayerData.products" :key="product" class="product-tag">
|
||||
{{ product }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-title">适用场景</div>
|
||||
<ul class="scenario-list">
|
||||
<li v-for="scenario in currentLayerData.scenarios" :key="scenario">{{ scenario }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能对比 -->
|
||||
<div class="performance-comparison">
|
||||
<div class="comparison-title">性能对比一览</div>
|
||||
<div class="comparison-table">
|
||||
<div class="table-header">
|
||||
<div class="th">类型</div>
|
||||
<div class="th">处理层</div>
|
||||
<div class="th">性能</div>
|
||||
<div class="th">灵活性</div>
|
||||
<div class="th">成本</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="row in comparisonData"
|
||||
:key="row.type"
|
||||
class="table-row"
|
||||
:class="{ active: currentLayer === row.key }"
|
||||
>
|
||||
<div class="td type">{{ row.type }}</div>
|
||||
<div class="td">{{ row.layer }}</div>
|
||||
<div class="td">
|
||||
<div class="rating-bar">
|
||||
<div class="rating-fill" :style="{ width: row.performance + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="td">
|
||||
<div class="rating-bar">
|
||||
<div class="rating-fill" :style="{ width: row.flexibility + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="td cost">{{ row.cost }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const currentLayer = ref('l4')
|
||||
const activeServer = ref(0)
|
||||
|
||||
const layers = [
|
||||
{
|
||||
key: 'hardware',
|
||||
name: '硬件负载均衡',
|
||||
icon: '🏗️',
|
||||
tag: 'F5/A10'
|
||||
},
|
||||
{
|
||||
key: 'l4',
|
||||
name: '四层负载均衡',
|
||||
icon: '📦',
|
||||
tag: 'L4'
|
||||
},
|
||||
{
|
||||
key: 'l7',
|
||||
name: '七层负载均衡',
|
||||
icon: '🌐',
|
||||
tag: 'L7'
|
||||
},
|
||||
{
|
||||
key: 'software',
|
||||
name: '软件负载均衡',
|
||||
icon: '💻',
|
||||
tag: '开源'
|
||||
}
|
||||
]
|
||||
|
||||
const layerDetails = {
|
||||
hardware: {
|
||||
name: '硬件负载均衡器',
|
||||
icon: '🏗️',
|
||||
label: 'F5 BIG-IP',
|
||||
principle: '专用硬件设备,通过ASIC芯片实现高性能流量转发。独立于服务器部署,具备高可靠性和丰富的企业级功能。',
|
||||
products: ['F5 BIG-IP', 'A10 Thunder', 'Citrix ADC', 'Radware'],
|
||||
scenarios: ['金融核心系统', '电信级应用', '需要硬件SSL卸载的场景', '高合规要求环境']
|
||||
},
|
||||
l4: {
|
||||
name: '四层负载均衡 (L4)',
|
||||
icon: '📦',
|
||||
label: 'L4 Load Balancer',
|
||||
principle: '基于传输层信息(IP地址+端口)进行流量分发。不关心应用层内容,只做"快递分拣",因此性能极高。',
|
||||
products: ['LVS (Linux Virtual Server)', 'HAProxy (TCP模式)', 'AWS NLB', 'Azure Load Balancer'],
|
||||
scenarios: ['需要极高吞吐量的场景', 'TCP/UDP流量分发', '不需要内容识别的场景', '微服务间通信']
|
||||
},
|
||||
l7: {
|
||||
name: '七层负载均衡 (L7)',
|
||||
icon: '🌐',
|
||||
label: 'L7 Load Balancer',
|
||||
principle: '基于应用层内容(HTTP头、URL、Cookie等)进行智能路由。可以理解"快递内容",实现更精细的流量控制。',
|
||||
products: ['Nginx', 'HAProxy (HTTP模式)', 'Envoy', 'AWS ALB', 'Traefik'],
|
||||
scenarios: ['基于URL路径路由', 'A/B测试和灰度发布', '基于Cookie的会话保持', 'HTTPS终结和证书管理']
|
||||
},
|
||||
software: {
|
||||
name: '软件负载均衡方案',
|
||||
icon: '💻',
|
||||
label: 'Software LB',
|
||||
principle: '运行在通用服务器上的负载均衡软件,灵活可定制。从开源方案到云原生方案,选择丰富。',
|
||||
products: ['Nginx / OpenResty', 'HAProxy', 'Envoy Proxy', 'Kong', 'Spring Cloud Gateway'],
|
||||
scenarios: ['成本敏感场景', '需要深度定制的环境', '云原生/K8s环境', '快速迭代开发']
|
||||
}
|
||||
}
|
||||
|
||||
const currentLayerData = computed(() => layerDetails[currentLayer.value])
|
||||
|
||||
const servers = ref([
|
||||
{ load: 30 },
|
||||
{ load: 45 },
|
||||
{ load: 25 }
|
||||
])
|
||||
|
||||
const comparisonData = [
|
||||
{
|
||||
key: 'hardware',
|
||||
type: '硬件负载均衡',
|
||||
layer: 'L4/L7',
|
||||
performance: 95,
|
||||
flexibility: 40,
|
||||
cost: '$$$$$'
|
||||
},
|
||||
{
|
||||
key: 'l4',
|
||||
type: '四层负载均衡',
|
||||
layer: 'L4 (传输层)',
|
||||
performance: 90,
|
||||
flexibility: 50,
|
||||
cost: '$$'
|
||||
},
|
||||
{
|
||||
key: 'l7',
|
||||
type: '七层负载均衡',
|
||||
layer: 'L7 (应用层)',
|
||||
performance: 70,
|
||||
flexibility: 90,
|
||||
cost: '$$$'
|
||||
},
|
||||
{
|
||||
key: 'software',
|
||||
type: '软件负载均衡',
|
||||
layer: 'L4/L7',
|
||||
performance: 75,
|
||||
flexibility: 95,
|
||||
cost: '$'
|
||||
}
|
||||
]
|
||||
|
||||
// 自动轮播演示
|
||||
let demoInterval
|
||||
const startDemo = () => {
|
||||
demoInterval = setInterval(() => {
|
||||
activeServer.value = (activeServer.value + 1) % servers.value.length
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 组件挂载时启动演示
|
||||
onMounted(() => {
|
||||
startDemo()
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
clearInterval(demoInterval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.load-balancer-types-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Layer Selector */
|
||||
.layer-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.layer-selector {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.layer-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.layer-btn:hover {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
}
|
||||
|
||||
.layer-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.layer-tag {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Architecture Comparison */
|
||||
.architecture-comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.architecture-comparison {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.comparison-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.comparison-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comparison-panel.highlighted {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 2px var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.panel-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-badge.single {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.panel-badge.distributed {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Single Server */
|
||||
.single-server {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.server-label {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.server-load {
|
||||
width: 150px;
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.load-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #22c55e, #f59e0b, #ef4444);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.load-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* LB Layer */
|
||||
.lb-layer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.lb-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lb-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Servers Layer */
|
||||
.servers-layer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.server-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.server-node.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.server-icon-small {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.server-id {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.server-load-mini {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.load-bar-mini {
|
||||
height: 100%;
|
||||
background: #22c55e;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.comparison-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Detail Panel */
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.product-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.product-tag {
|
||||
font-size: 0.8rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.scenario-list {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.scenario-list li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Performance Comparison */
|
||||
.performance-comparison {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.comparison-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr 1fr 1fr 0.8fr;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr 1fr 1fr 0.8fr;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.8rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-row.active {
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.td.type {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.td.cost {
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.rating-bar {
|
||||
width: 60px;
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rating-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #22c55e, #3b82f6);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,597 @@
|
||||
<template>
|
||||
<div class="multi-region-demo">
|
||||
<div class="header">
|
||||
<div class="title">多区域部署</div>
|
||||
<div class="subtitle">异地多活架构,就近服务与容灾备份</div>
|
||||
</div>
|
||||
|
||||
<!-- 全球地图 -->
|
||||
<div class="world-map">
|
||||
<div class="map-header">
|
||||
<span class="map-title">全球部署视图</span>
|
||||
<span class="map-legend">
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot active"></span>
|
||||
主节点
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot standby"></span>
|
||||
备节点
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="map-container">
|
||||
<!-- 简化的世界地图 -->
|
||||
<div class="map-bg">
|
||||
<!-- 亚洲 -->
|
||||
<div class="continent asia">
|
||||
<div
|
||||
v-for="region in asiaRegions"
|
||||
:key="region.id"
|
||||
class="region-node"
|
||||
:class="{
|
||||
active: region.isPrimary,
|
||||
standby: !region.isPrimary,
|
||||
selected: selectedRegion === region.id
|
||||
}"
|
||||
:style="{ top: region.y + '%', left: region.x + '%' }"
|
||||
@click="selectedRegion = region.id"
|
||||
>
|
||||
<div class="node-icon">{{ region.isPrimary ? '📡' : '📶' }}</div>
|
||||
<div class="node-label">{{ region.name }}</div>
|
||||
<div class="node-delay">{{ region.delay }}ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 欧洲 -->
|
||||
<div class="continent europe">
|
||||
<div
|
||||
v-for="region in europeRegions"
|
||||
:key="region.id"
|
||||
class="region-node"
|
||||
:class="{
|
||||
active: region.isPrimary,
|
||||
standby: !region.isPrimary,
|
||||
selected: selectedRegion === region.id
|
||||
}"
|
||||
:style="{ top: region.y + '%', left: region.x + '%' }"
|
||||
@click="selectedRegion = region.id"
|
||||
>
|
||||
<div class="node-icon">{{ region.isPrimary ? '📡' : '📶' }}</div>
|
||||
<div class="node-label">{{ region.name }}</div>
|
||||
<div class="node-delay">{{ region.delay }}ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 北美 -->
|
||||
<div class="continent north-america">
|
||||
<div
|
||||
v-for="region in northAmericaRegions"
|
||||
:key="region.id"
|
||||
class="region-node"
|
||||
:class="{
|
||||
active: region.isPrimary,
|
||||
standby: !region.isPrimary,
|
||||
selected: selectedRegion === region.id
|
||||
}"
|
||||
:style="{ top: region.y + '%', left: region.x + '%' }"
|
||||
@click="selectedRegion = region.id"
|
||||
>
|
||||
<div class="node-icon">{{ region.isPrimary ? '📡' : '📶' }}</div>
|
||||
<div class="node-label">{{ region.name }}</div>
|
||||
<div class="node-delay">{{ region.delay }}ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接线路 -->
|
||||
<svg class="connection-lines" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="3" markerHeight="3" refX="2" refY="1.5" orient="auto">
|
||||
<polygon points="0 0, 3 1.5, 0 3" fill="var(--vp-c-brand)" />
|
||||
</marker>
|
||||
</defs>
|
||||
<line
|
||||
v-for="(line, index) in connectionLines"
|
||||
:key="index"
|
||||
:x1="line.x1"
|
||||
:y1="line.y1"
|
||||
:x2="line.x2"
|
||||
:y2="line.y2"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="0.3"
|
||||
stroke-dasharray="2 1"
|
||||
marker-end="url(#arrowhead)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 区域详情 -->
|
||||
<div class="region-details" v-if="selectedRegionData">
|
||||
<div class="details-header">
|
||||
<div class="region-title">
|
||||
<span class="region-icon">{{ selectedRegionData.isPrimary ? '📡' : '📶' }}</span>
|
||||
<span class="region-name">{{ selectedRegionData.name }}</span>
|
||||
<span class="region-badge" :class="{ primary: selectedRegionData.isPrimary, standby: !selectedRegionData.isPrimary }">
|
||||
{{ selectedRegionData.isPrimary ? '主节点' : '备节点' }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="close-btn" @click="selectedRegion = null">×</button>
|
||||
</div>
|
||||
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">延迟</div>
|
||||
<div class="detail-value">{{ selectedRegionData.delay }}ms</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">在线实例</div>
|
||||
<div class="detail-value">{{ selectedRegionData.instances }}个</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">当前QPS</div>
|
||||
<div class="detail-value">{{ selectedRegionData.qps }}/s</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">数据同步延迟</div>
|
||||
<div class="detail-value">{{ selectedRegionData.syncDelay }}ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details-actions">
|
||||
<button class="action-btn primary" v-if="!selectedRegionData.isPrimary">
|
||||
提升为主节点
|
||||
</button>
|
||||
<button class="action-btn danger" v-if="selectedRegionData.isPrimary">
|
||||
切换流量
|
||||
</button>
|
||||
<button class="action-btn">
|
||||
查看日志
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 架构优势 -->
|
||||
<div class="architecture-benefits">
|
||||
<div class="benefits-title">多区域部署优势</div>
|
||||
<div class="benefits-grid">
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">⚡</div>
|
||||
<div class="benefit-title">就近服务</div>
|
||||
<div class="benefit-desc">用户请求自动路由到最近的区域,降低网络延迟,提升访问速度</div>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🛡️</div>
|
||||
<div class="benefit-title">容灾备份</div>
|
||||
<div class="benefit-desc">单区域故障时自动切换流量,确保服务高可用,数据多副本保存</div>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🌍</div>
|
||||
<div class="benefit-title">全球覆盖</div>
|
||||
<div class="benefit-desc">支持跨区域部署,满足不同地区的合规要求和数据主权法规</div>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">📈</div>
|
||||
<div class="benefit-title">负载均衡</div>
|
||||
<div class="benefit-desc">跨区域流量调度,避免单点过载,实现全局资源优化配置</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 当前选中的指标
|
||||
const currentMetric = ref('cpu')
|
||||
const selectedRegion = ref(null)
|
||||
|
||||
// 亚洲区域数据
|
||||
const asiaRegions = ref([
|
||||
{ id: 'bj', name: '北京', x: 75, y: 35, isPrimary: true, delay: 20, instances: 5, qps: 2500, syncDelay: 10 },
|
||||
{ id: 'sh', name: '上海', x: 80, y: 45, isPrimary: false, delay: 25, instances: 3, qps: 1500, syncDelay: 15 },
|
||||
{ id: 'sg', name: '新加坡', x: 72, y: 65, isPrimary: false, delay: 45, instances: 2, qps: 800, syncDelay: 25 }
|
||||
])
|
||||
|
||||
// 欧洲区域数据
|
||||
const europeRegions = ref([
|
||||
{ id: 'fr', name: '法兰克福', x: 48, y: 30, isPrimary: true, delay: 120, instances: 4, qps: 1800, syncDelay: 20 },
|
||||
{ id: 'uk', name: '伦敦', x: 45, y: 25, isPrimary: false, delay: 130, instances: 2, qps: 900, syncDelay: 30 }
|
||||
])
|
||||
|
||||
// 北美区域数据
|
||||
const northAmericaRegions = ref([
|
||||
{ id: 'usw', name: '硅谷', x: 15, y: 38, isPrimary: true, delay: 150, instances: 6, qps: 3200, syncDelay: 25 },
|
||||
{ id: 'use', name: '弗吉尼亚', x: 28, y: 35, isPrimary: false, delay: 160, instances: 3, qps: 1400, syncDelay: 35 }
|
||||
])
|
||||
|
||||
// 连接线数据
|
||||
const connectionLines = ref([
|
||||
// 北京-上海
|
||||
{ x1: 75, y1: 35, x2: 80, y2: 45 },
|
||||
// 北京-新加坡
|
||||
{ x1: 75, y1: 35, x2: 72, y2: 65 },
|
||||
// 法兰克福-伦敦
|
||||
{ x1: 48, y1: 30, x2: 45, y2: 25 },
|
||||
// 硅谷-弗吉尼亚
|
||||
{ x1: 15, y1: 38, x2: 28, y2: 35 },
|
||||
// 跨洲连接
|
||||
{ x1: 75, y1: 35, x2: 48, y2: 30 },
|
||||
{ x1: 48, y1: 30, x2: 15, y2: 38 }
|
||||
])
|
||||
|
||||
// 选中区域详情
|
||||
const selectedRegionData = computed(() => {
|
||||
if (!selectedRegion.value) return null
|
||||
const allRegions = [
|
||||
...asiaRegions.value,
|
||||
...europeRegions.value,
|
||||
...northAmericaRegions.value
|
||||
]
|
||||
return allRegions.find(r => r.id === selectedRegion.value)
|
||||
})
|
||||
|
||||
// 获取使用率颜色
|
||||
const getUsageColor = (usage) => {
|
||||
if (usage > 90) return '#ef4444'
|
||||
if (usage > 70) return '#f59e0b'
|
||||
return '#22c55e'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.multi-region-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* World Map */
|
||||
.world-map {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.map-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.map-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-dot.active {
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.legend-dot.standby {
|
||||
background: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.map-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 50%;
|
||||
background: linear-gradient(135deg, #f0f4f8, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.continent {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.region-node {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
min-width: 60px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.region-node:hover {
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.region-node.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.region-node.active .node-icon {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.region-node.standby {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.region-node.selected {
|
||||
box-shadow: 0 0 0 3px var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.node-delay {
|
||||
font-size: 0.6rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.connection-lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Region Details */
|
||||
.region-details {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.details-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.region-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.region-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.region-name {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.region-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.region-badge.primary {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.region-badge.standby {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 1.25rem;
|
||||
color: var(--vp-c-text-2);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
border-color: var(--vp-c-danger);
|
||||
color: var(--vp-c-danger);
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.details-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.details-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: var(--vp-c-danger);
|
||||
color: white;
|
||||
border-color: var(--vp-c-danger);
|
||||
}
|
||||
|
||||
/* Architecture Benefits */
|
||||
.architecture-benefits {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.benefits-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.benefits-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.benefit-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.benefit-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.benefit-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,938 @@
|
||||
<template>
|
||||
<div class="session-persistence-demo">
|
||||
<div class="header">
|
||||
<div class="title">会话保持机制</div>
|
||||
<div class="subtitle">Cookie、IP哈希与粘性会话的技术对比</div>
|
||||
</div>
|
||||
|
||||
<!-- 场景选择 -->
|
||||
<div class="scenario-selector">
|
||||
<div class="scenario-label">应用场景:</div>
|
||||
<div class="scenario-buttons">
|
||||
<button
|
||||
v-for="scenario in scenarios"
|
||||
:key="scenario.key"
|
||||
class="scenario-btn"
|
||||
:class="{ active: currentScenario === scenario.key }"
|
||||
@click="currentScenario = scenario.key"
|
||||
>
|
||||
{{ scenario.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 机制选择器 -->
|
||||
<div class="mechanism-selector">
|
||||
<button
|
||||
v-for="mech in mechanisms"
|
||||
:key="mech.key"
|
||||
class="mechanism-btn"
|
||||
:class="{ active: currentMechanism === mech.key }"
|
||||
@click="currentMechanism = mech.key"
|
||||
>
|
||||
<span class="mechanism-icon">{{ mech.icon }}</span>
|
||||
<span class="mechanism-name">{{ mech.name }}</span>
|
||||
<span class="mechanism-tag">{{ mech.tag }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 可视化演示区 -->
|
||||
<div class="demo-stage">
|
||||
<!-- 用户层 -->
|
||||
<div class="user-layer">
|
||||
<div class="user-avatars">
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="user-avatar"
|
||||
:class="{ active: activeUser === user.id }"
|
||||
@click="activeUser = user.id"
|
||||
>
|
||||
<div class="avatar-icon">{{ user.avatar }}</div>
|
||||
<div class="user-name">{{ user.name }}</div>
|
||||
<div v-if="hasSessionCookie" class="cookie-badge">🍪</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 请求流程 -->
|
||||
<div class="request-flow">
|
||||
<div class="flow-step">
|
||||
<div class="step-label">请求</div>
|
||||
<div class="step-arrow">↓</div>
|
||||
</div>
|
||||
|
||||
<!-- 负载均衡器 -->
|
||||
<div class="lb-box">
|
||||
<div class="lb-header">
|
||||
<span class="lb-icon">⚖️</span>
|
||||
<span class="lb-title">负载均衡器</span>
|
||||
</div>
|
||||
<div class="lb-mechanism">
|
||||
<div class="mechanism-display">
|
||||
<span class="display-icon">{{ currentMechanismData.icon }}</span>
|
||||
<div class="display-info">
|
||||
<div class="display-name">{{ currentMechanismData.name }}</div>
|
||||
<div class="display-desc">{{ currentMechanismData.shortDesc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 会话表 -->
|
||||
<div v-if="currentMechanism === 'cookie' || currentMechanism === 'sticky'" class="session-table">
|
||||
<div class="table-title">会话映射表</div>
|
||||
<div class="table-rows">
|
||||
<div v-for="mapping in sessionMappings" :key="mapping.session" class="table-row">
|
||||
<span class="session-id">{{ mapping.session }}</span>
|
||||
<span class="mapping-arrow">→</span>
|
||||
<span class="server-name">{{ mapping.server }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- IP哈希环 -->
|
||||
<div v-if="currentMechanism === 'iphash'" class="hash-ring">
|
||||
<div class="ring-title">IP哈希环</div>
|
||||
<div class="ring-visual">
|
||||
<div
|
||||
v-for="(server, index) in hashRingServers"
|
||||
:key="index"
|
||||
class="ring-segment"
|
||||
:style="getSegmentStyle(index)"
|
||||
:title="server"
|
||||
>
|
||||
{{ server.slice(-1) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hash-formula">
|
||||
<code>server = hash(client_ip) % server_count</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="step-arrow">↓</div>
|
||||
</div>
|
||||
|
||||
<!-- 后端服务器 -->
|
||||
<div class="backend-servers">
|
||||
<div
|
||||
v-for="server in backendServers"
|
||||
:key="server.id"
|
||||
class="backend-server"
|
||||
:class="{ target: isTargetServer(server.id) }"
|
||||
>
|
||||
<div class="server-icon">🖥️</div>
|
||||
<div class="server-info">
|
||||
<div class="server-name">{{ server.name }}</div>
|
||||
<div class="server-ip">{{ server.ip }}</div>
|
||||
</div>
|
||||
<div class="server-status" :class="server.status">
|
||||
{{ server.status === 'healthy' ? '✓' : '✗' }}
|
||||
</div>
|
||||
<div v-if="isTargetServer(server.id)" class="selected-indicator">
|
||||
选中
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 响应流程 -->
|
||||
<div class="response-flow" v-if="currentMechanism === 'cookie'">
|
||||
<div class="flow-step">
|
||||
<div class="step-arrow">↑</div>
|
||||
</div>
|
||||
<div class="set-cookie-box">
|
||||
<div class="cookie-header">
|
||||
<span class="cookie-icon">🍪</span>
|
||||
<span class="cookie-title">Set-Cookie 响应头</span>
|
||||
</div>
|
||||
<div class="cookie-content">
|
||||
<code>SERVERID=srv001; Path=/; HttpOnly</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 机制对比表 -->
|
||||
<div class="mechanism-comparison">
|
||||
<div class="comparison-title">三种会话保持机制对比</div>
|
||||
<div class="comparison-grid">
|
||||
<div class="comparison-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">🍪</span>
|
||||
<span class="card-title">Cookie 插入</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="feature-list">
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon good">✓</span>
|
||||
<span>不受客户端IP变化影响</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon good">✓</span>
|
||||
<span>首次请求即可保持会话</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon bad">✗</span>
|
||||
<span>客户端需支持Cookie</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon bad">✗</span>
|
||||
<span>存在Cookie被禁用的风险</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">#️⃣</span>
|
||||
<span class="card-title">IP Hash</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="feature-list">
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon good">✓</span>
|
||||
<span>无需客户端支持任何机制</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon good">✓</span>
|
||||
<span>无状态,LB重启不影响会话</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon bad">✗</span>
|
||||
<span>客户端IP变化会丢失会话</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon bad">✗</span>
|
||||
<span>难以做到真正的负载均衡</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">📝</span>
|
||||
<span class="card-title">粘性会话</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="feature-list">
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon good">✓</span>
|
||||
<span>结合Cookie和IP两种方式优势</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon good">✓</span>
|
||||
<span>支持会话复制和故障转移</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon bad">✗</span>
|
||||
<span>实现复杂,需要应用支持</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon bad">✗</span>
|
||||
<span>会话复制带来性能开销</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const currentMode = ref('active')
|
||||
const currentScenario = ref('shopping')
|
||||
const currentMechanism = ref('cookie')
|
||||
const activeUser = ref(1)
|
||||
|
||||
const modes = [
|
||||
{ key: 'active', name: '主动检查', icon: '🔍' },
|
||||
{ key: 'passive', name: '被动感知', icon: '👁️' },
|
||||
{ key: 'threshold', name: '阈值判定', icon: '📊' }
|
||||
]
|
||||
|
||||
const scenarios = [
|
||||
{ key: 'shopping', name: '购物车' },
|
||||
{ key: 'login', name: '登录状态' },
|
||||
{ key: 'websocket', name: '实时通信' }
|
||||
]
|
||||
|
||||
const mechanisms = [
|
||||
{ key: 'cookie', name: 'Cookie插入', icon: '🍪', tag: '应用层' },
|
||||
{ key: 'iphash', name: 'IP哈希', icon: '#️⃣', tag: '传输层' },
|
||||
{ key: 'sticky', name: '粘性会话', icon: '📝', tag: '会话层' }
|
||||
]
|
||||
|
||||
const currentMechanismData = computed(() => {
|
||||
const data = {
|
||||
cookie: {
|
||||
name: 'Cookie 插入',
|
||||
icon: '🍪',
|
||||
label: 'Set-Cookie',
|
||||
shortDesc: '通过HTTP Cookie保持会话',
|
||||
description: '负载均衡器在第一次响应时向客户端设置Cookie(如SERVERID=srv001),后续请求携带此Cookie,LB根据Cookie值将请求路由到对应后端服务器。'
|
||||
},
|
||||
iphash: {
|
||||
name: 'IP 哈希',
|
||||
icon: '#️⃣',
|
||||
label: 'IP Hash',
|
||||
shortDesc: '基于客户端IP计算哈希',
|
||||
description: '通过对客户端IP地址进行哈希计算(如hash(client_ip) % server_count),确定请求应该路由到哪台后端服务器。同一IP的请求总是落到同一台服务器。'
|
||||
},
|
||||
sticky: {
|
||||
name: '粘性会话',
|
||||
icon: '📝',
|
||||
label: 'Sticky Session',
|
||||
shortDesc: '服务端维护会话映射表',
|
||||
description: '负载均衡器在内存中维护会话映射表(session_id -> server),首次请求建立映射关系,后续相同会话ID的请求都路由到同一服务器。支持会话复制实现高可用。'
|
||||
}
|
||||
}
|
||||
return data[currentMechanism.value]
|
||||
})
|
||||
|
||||
const users = [
|
||||
{ id: 1, name: '用户A', avatar: '👤', ip: '192.168.1.100' },
|
||||
{ id: 2, name: '用户B', avatar: '👥', ip: '192.168.1.101' },
|
||||
{ id: 3, name: '用户C', avatar: '👨💼', ip: '192.168.1.102' }
|
||||
]
|
||||
|
||||
const sessionMappings = [
|
||||
{ session: 'sess_abc123', server: 'Server 1' },
|
||||
{ session: 'sess_def456', server: 'Server 2' },
|
||||
{ session: 'sess_ghi789', server: 'Server 1' }
|
||||
]
|
||||
|
||||
const hashRingServers = ['Server 1', 'Server 2', 'Server 3', 'Server 4']
|
||||
|
||||
const backendServers = [
|
||||
{ id: 1, name: 'Server 1', ip: '10.0.1.10', status: 'healthy' },
|
||||
{ id: 2, name: 'Server 2', ip: '10.0.1.11', status: 'healthy' },
|
||||
{ id: 3, name: 'Server 3', ip: '10.0.1.12', status: 'unhealthy' }
|
||||
]
|
||||
|
||||
const hasSessionCookie = computed(() => {
|
||||
return currentMechanism.value === 'cookie' && activeUser.value > 0
|
||||
})
|
||||
|
||||
const isTargetServer = (serverId) => {
|
||||
// 模拟根据机制选择目标服务器
|
||||
if (currentMechanism.value === 'iphash') {
|
||||
return serverId === ((activeUser.value + serverId) % 3) + 1
|
||||
}
|
||||
return serverId === 1 || (activeUser.value === serverId)
|
||||
}
|
||||
|
||||
const getSegmentStyle = (index) => {
|
||||
const colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444']
|
||||
const rotation = index * 90
|
||||
return {
|
||||
background: colors[index],
|
||||
transform: `rotate(${rotation}deg) translateY(-20px)`
|
||||
}
|
||||
}
|
||||
|
||||
// 轮播演示
|
||||
let demoInterval
|
||||
onMounted(() => {
|
||||
demoInterval = setInterval(() => {
|
||||
activeUser.value = (activeUser.value % 3) + 1
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(demoInterval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.session-persistence-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Scenario Selector */
|
||||
.scenario-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.scenario-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.scenario-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.scenario-btn {
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.scenario-btn:hover {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
}
|
||||
|
||||
.scenario-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Mechanism Selector */
|
||||
.mechanism-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mechanism-selector {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mechanism-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mechanism-btn:hover {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
}
|
||||
|
||||
.mechanism-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.mechanism-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.mechanism-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.mechanism-tag {
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Demo Stage */
|
||||
.demo-stage {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* User Layer */
|
||||
.user-layer {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.user-avatars {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.user-avatar.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.cookie-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
font-size: 1rem;
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-3px); }
|
||||
}
|
||||
|
||||
/* Request Flow */
|
||||
.request-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* LB Box */
|
||||
.lb-box {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.lb-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.lb-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.lb-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.lb-mechanism {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mechanism-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.display-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.display-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.display-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* Session Table */
|
||||
.session-table {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.table-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.session-id {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.mapping-arrow {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
/* Hash Ring */
|
||||
.hash-ring {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.ring-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ring-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.ring-segment {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hash-formula {
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hash-formula code {
|
||||
font-size: 0.75rem;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Backend Servers */
|
||||
.backend-servers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.backend-servers {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.backend-server {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.backend-server.target {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
box-shadow: 0 0 0 3px var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.server-ip {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.server-status {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.server-status.healthy {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.server-status.unhealthy {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.selected-indicator {
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
font-size: 0.65rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Response Flow - Set Cookie */
|
||||
.response-flow {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.set-cookie-box {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.cookie-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.cookie-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.cookie-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.cookie-content {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
color: #78350f;
|
||||
}
|
||||
|
||||
/* Mechanism Comparison */
|
||||
.mechanism-comparison {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.comparison-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.comparison-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comparison-card .card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.comparison-card .card-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.comparison-card .card-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.comparison-card .card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 0.6rem;
|
||||
font-weight: bold;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.feature-icon.good {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.feature-icon.bad {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,709 @@
|
||||
<template>
|
||||
<div class="weighted-routing-demo">
|
||||
<div class="header">
|
||||
<div class="title">加权路由策略</div>
|
||||
<div class="subtitle">按性能、成本、地理位置智能分配流量</div>
|
||||
</div>
|
||||
|
||||
<!-- 策略选择器 -->
|
||||
<div class="strategy-selector">
|
||||
<div class="strategy-label">加权策略:</div>
|
||||
<div class="strategy-buttons">
|
||||
<button
|
||||
v-for="strategy in strategies"
|
||||
:key="strategy.key"
|
||||
class="strategy-btn"
|
||||
:class="{ active: currentStrategy === strategy.key }"
|
||||
@click="currentStrategy = strategy.key"
|
||||
>
|
||||
<span class="btn-icon">{{ strategy.icon }}</span>
|
||||
<span class="btn-name">{{ strategy.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可视化区域 -->
|
||||
<div class="visualization">
|
||||
<!-- 流量进入 -->
|
||||
<div class="traffic-incoming">
|
||||
<div class="traffic-label">总流量</div>
|
||||
<div class="traffic-value">{{ totalTraffic }} req/s</div>
|
||||
<div class="traffic-slider">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="totalTraffic"
|
||||
min="100"
|
||||
max="10000"
|
||||
step="100"
|
||||
/>
|
||||
<div class="slider-labels">
|
||||
<span>100</span>
|
||||
<span>5000</span>
|
||||
<span>10000</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 权重分配可视化 -->
|
||||
<div class="weight-allocation">
|
||||
<div class="allocation-title">权重分配</div>
|
||||
<div class="allocation-bars">
|
||||
<div
|
||||
v-for="(server, index) in weightedServers"
|
||||
:key="server.id"
|
||||
class="allocation-item"
|
||||
>
|
||||
<div class="server-info">
|
||||
<div class="server-icon">🖥️</div>
|
||||
<div class="server-details">
|
||||
<div class="server-name">{{ server.name }}</div>
|
||||
<div class="server-specs">{{ server.specs }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="weight-bar-container">
|
||||
<div
|
||||
class="weight-bar"
|
||||
:style="{
|
||||
width: getAllocationPercentage(server.weight) + '%',
|
||||
background: getWeightColor(index)
|
||||
}"
|
||||
>
|
||||
<span class="weight-value">{{ Math.round(getAllocationPercentage(server.weight)) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="traffic-assigned">
|
||||
{{ Math.round((totalTraffic * server.weight) / getTotalWeight()) }} req/s
|
||||
</div>
|
||||
<div class="weight-control">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="server.weight"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
class="weight-slider"
|
||||
/>
|
||||
<span class="weight-label">权重: {{ server.weight }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实时流量动画 -->
|
||||
<div class="traffic-animation">
|
||||
<div class="animation-title">实时流量</div>
|
||||
<div class="traffic-flows">
|
||||
<div
|
||||
v-for="(flow, index) in trafficFlows"
|
||||
:key="index"
|
||||
class="flow-item"
|
||||
:style="{ animationDelay: flow.delay + 's' }"
|
||||
>
|
||||
<div class="flow-packet" :style="{ background: flow.color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-indicators">
|
||||
<div
|
||||
v-for="(server, index) in weightedServers"
|
||||
:key="server.id"
|
||||
class="indicator"
|
||||
:style="{ background: getWeightColor(index) }"
|
||||
>
|
||||
{{ server.name.slice(-1) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 策略详情对比 -->
|
||||
<div class="strategy-comparison">
|
||||
<div class="comparison-title">加权策略对比</div>
|
||||
<div class="comparison-grid">
|
||||
<div
|
||||
v-for="strategy in strategies"
|
||||
:key="strategy.key"
|
||||
class="strategy-card"
|
||||
:class="{ active: currentStrategy === strategy.key }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="card-icon">{{ strategy.icon }}</span>
|
||||
<span class="card-name">{{ strategy.name }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">{{ strategy.description }}</p>
|
||||
<div class="use-cases">
|
||||
<div class="use-case-title">适用场景:</div>
|
||||
<ul>
|
||||
<li v-for="useCase in strategy.useCases" :key="useCase">{{ useCase }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const currentStrategy = ref('performance')
|
||||
const totalTraffic = ref(1000)
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
key: 'performance',
|
||||
name: '按性能加权',
|
||||
icon: '⚡',
|
||||
description: '根据后端服务器的处理能力(CPU、内存、I/O性能)分配权重,高性能服务器承担更多流量。',
|
||||
useCases: [
|
||||
'混合部署环境(新老服务器混用)',
|
||||
'异构硬件环境',
|
||||
'需要最大化整体吞吐量的场景'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'cost',
|
||||
name: '按成本加权',
|
||||
icon: '💰',
|
||||
description: '根据服务器成本(按需实例vs预留实例、不同地域成本)分配权重,优先使用低成本资源。',
|
||||
useCases: [
|
||||
'云环境中的成本优化',
|
||||
'跨地域部署的流量调度',
|
||||
'预留实例与按需实例混合使用'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'geo',
|
||||
name: '按地理位置',
|
||||
icon: '🌍',
|
||||
description: '根据用户的地理位置,将请求路由到最近的数据中心,减少网络延迟。',
|
||||
useCases: [
|
||||
'全球化的应用服务',
|
||||
'对延迟敏感的应用(游戏、金融交易)',
|
||||
'CDN与源站之间的智能路由'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const weightedServers = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Server 1',
|
||||
specs: '8核 32GB SSD',
|
||||
ip: '10.0.1.10',
|
||||
weight: 5,
|
||||
status: 'healthy'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Server 2',
|
||||
specs: '4核 16GB SSD',
|
||||
ip: '10.0.1.11',
|
||||
weight: 3,
|
||||
status: 'healthy'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Server 3',
|
||||
specs: '2核 8GB HDD',
|
||||
ip: '10.0.1.12',
|
||||
weight: 2,
|
||||
status: 'healthy'
|
||||
}
|
||||
])
|
||||
|
||||
const getTotalWeight = () => {
|
||||
return weightedServers.value.reduce((sum, s) => sum + s.weight, 0)
|
||||
}
|
||||
|
||||
const getAllocationPercentage = (weight) => {
|
||||
const total = getTotalWeight()
|
||||
return total > 0 ? (weight / total) * 100 : 0
|
||||
}
|
||||
|
||||
const getWeightColor = (index) => {
|
||||
const colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6']
|
||||
return colors[index % colors.length]
|
||||
}
|
||||
|
||||
// 流量流动画
|
||||
const trafficFlows = ref([])
|
||||
|
||||
const generateTrafficFlows = () => {
|
||||
const colors = ['#3b82f6', '#22c55e', '#f59e0b']
|
||||
trafficFlows.value = Array.from({ length: 12 }, (_, i) => ({
|
||||
delay: i * 0.2,
|
||||
color: colors[Math.floor(Math.random() * colors.length)]
|
||||
}))
|
||||
}
|
||||
|
||||
// 目标服务器计算
|
||||
const isTargetServer = (serverId) => {
|
||||
// 模拟根据权重选择
|
||||
const server = weightedServers.value.find(s => s.id === serverId)
|
||||
if (!server) return false
|
||||
return server.weight >= 4
|
||||
}
|
||||
|
||||
// 根据策略调整服务器规格和权重
|
||||
const updateServersByStrategy = () => {
|
||||
if (currentStrategy.value === 'performance') {
|
||||
weightedServers.value = [
|
||||
{ id: 1, name: 'Server 1', specs: '16核 64GB NVMe', ip: '10.0.1.10', weight: 8, status: 'healthy' },
|
||||
{ id: 2, name: 'Server 2', specs: '8核 32GB SSD', ip: '10.0.1.11', weight: 4, status: 'healthy' },
|
||||
{ id: 3, name: 'Server 3', specs: '4核 16GB SSD', ip: '10.0.1.12', weight: 2, status: 'healthy' }
|
||||
]
|
||||
} else if (currentStrategy.value === 'cost') {
|
||||
weightedServers.value = [
|
||||
{ id: 1, name: 'Server 1', specs: '预留实例 (低成本)', ip: '10.0.1.10', weight: 7, status: 'healthy' },
|
||||
{ id: 2, name: 'Server 2', specs: '预留实例 (低成本)', ip: '10.0.1.11', weight: 7, status: 'healthy' },
|
||||
{ id: 3, name: 'Server 3', specs: '按需实例 (高成本)', ip: '10.0.1.12', weight: 2, status: 'healthy' }
|
||||
]
|
||||
} else if (currentStrategy.value === 'geo') {
|
||||
weightedServers.value = [
|
||||
{ id: 1, name: '北京节点', specs: '服务华北用户', ip: '10.0.1.10', weight: 5, status: 'healthy' },
|
||||
{ id: 2, name: '上海节点', specs: '服务华东用户', ip: '10.0.1.11', weight: 5, status: 'healthy' },
|
||||
{ id: 3, name: '广州节点', specs: '服务华南用户', ip: '10.0.1.12', weight: 5, status: 'healthy' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
generateTrafficFlows()
|
||||
// 监听策略变化更新服务器
|
||||
watch(currentStrategy, () => {
|
||||
updateServersByStrategy()
|
||||
}, { immediate: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.weighted-routing-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Strategy Selector */
|
||||
.strategy-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.strategy-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.strategy-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.strategy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.strategy-btn:hover {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
}
|
||||
|
||||
.strategy-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Visualization */
|
||||
.visualization {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Traffic Incoming */
|
||||
.traffic-incoming {
|
||||
text-align: center;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.traffic-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.traffic-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.traffic-slider {
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.traffic-slider input {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.traffic-slider input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.slider-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Weight Allocation */
|
||||
.weight-allocation {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.allocation-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.allocation-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.allocation-item {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.server-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.server-specs {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.weight-bar-container {
|
||||
height: 24px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.weight-bar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 0.75rem;
|
||||
transition: width 0.3s ease;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.weight-value {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.traffic-assigned {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.75rem;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.weight-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.weight-slider {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.weight-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.weight-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Traffic Animation */
|
||||
.traffic-animation {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.animation-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.traffic-flows {
|
||||
height: 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.flow-item {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
animation: flowMove 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes flowMove {
|
||||
0% {
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.flow-packet {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.server-indicators {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Strategy Comparison */
|
||||
.strategy-comparison {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.comparison-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.strategy-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.strategy-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.use-cases {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.use-case-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.use-cases ul {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.use-cases li {
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user