Files
test-repo/docs/.vitepress/theme/components/appendix/gateway-proxy/LoadBalancingDemo.vue
T
sanbuphy 0eba9e87e9 fix(eslint): reduce warnings in GitHub Actions deployment
- Disable formatting rules (handled by Prettier)
- Relaxed strict Vue/JS rules for demo code compatibility
- Fix syntax errors in ApiPlayground and VoiceCloningDemo
- Fix duplicate else-if condition in ApiPlayground
- Fix Promise executor async pattern in AutoregressiveAudioDemo
- Add TypeScript file support to ESLint config

Warnings reduced from 295 to 251 problems.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-18 17:38:10 +08:00

928 lines
20 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
LoadBalancingDemo.vue
负载均衡 - 轮询/加权/最少连接/IP哈希
-->
<template>
<div class="load-balancing-demo">
<div class="header">
<div class="title">
负载均衡"压力"均匀分摊到多台服务器
</div>
<div class="subtitle">
想象成银行的取号系统把客户均匀分配到各个窗口避免某个窗口排长队
</div>
</div>
<div class="strategy-selector">
<div class="selector-title">
选择负载均衡策略
</div>
<div class="strategy-tabs">
<button
v-for="strategy in strategies"
:key="strategy.id"
:class="['strategy-tab', { active: currentStrategy === strategy.id }]"
@click="changeStrategy(strategy.id)"
>
<span class="tab-icon">{{ strategy.icon }}</span>
<span class="tab-name">{{ strategy.name }}</span>
<span
v-if="strategy.badge"
class="tab-badge"
>{{ strategy.badge }}</span>
</button>
</div>
</div>
<div class="simulation-area">
<div class="sim-header">
<div class="sim-title">
🎮 负载均衡模拟器
</div>
<div class="sim-controls">
<button
class="sim-btn"
:disabled="isSimulating"
@click="startSimulation"
>
{{ isSimulating ? '运行中...' : '▶ 开始模拟' }}
</button>
<button
class="sim-btn reset"
@click="resetSimulation"
>
重置
</button>
</div>
</div>
<div class="strategy-explanation">
<div class="exp-icon">
💡
</div>
<div class="exp-content">
<div class="exp-title">
{{ currentStrategyData.name }} - {{ currentStrategyData.shortDesc }}
</div>
<div class="exp-desc">
{{ currentStrategyData.fullDesc }}
</div>
</div>
</div>
<div class="servers-pool">
<div class="pool-header">
<div class="pool-title">
🏢 后端服务器集群
</div>
<div class="pool-config">
<label>服务器数量:</label>
<input
v-model="serverCount"
type="range"
min="2"
max="6"
:disabled="isSimulating"
>
<span>{{ serverCount }} </span>
</div>
</div>
<div class="servers-grid">
<div
v-for="server in servers"
:key="server.id"
:class="['server-card', { active: server.active, overloaded: server.load > 80 }]"
:style="{ borderColor: server.color }"
>
<div class="server-header">
<div class="server-icon">
🖥
</div>
<div class="server-name">
{{ server.name }}
</div>
<div
class="server-status"
:style="{ background: server.color }"
>
{{ server.load }}%
</div>
</div>
<div class="server-metrics">
<div class="metric">
<span class="metric-label">请求数:</span>
<span class="metric-value">{{ server.requests }}</span>
</div>
<div class="metric">
<span class="metric-label">权重:</span>
<input
v-if="currentStrategy === 'weighted'"
v-model.number="server.weight"
type="number"
min="1"
max="10"
:disabled="isSimulating"
class="weight-input"
>
<span v-else>{{ server.weight }}</span>
</div>
</div>
<div class="load-bar">
<div
class="load-fill"
:style="{ width: server.load + '%', background: server.color }"
/>
</div>
<div class="recent-requests">
<div class="req-label">
最近请求:
</div>
<div class="req-list">
<span
v-for="(req, idx) in server.recentRequests"
:key="idx"
class="req-badge"
:style="{ background: req.color }"
>
{{ req.id }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="request-queue">
<div class="queue-header">
<div class="queue-title">
📨 请求队列
</div>
<div class="queue-stats">
<span>总请求: {{ totalRequests }}</span>
<span>待处理: {{ pendingRequests.length }}</span>
</div>
</div>
<div class="queue-items">
<div
v-for="req in displayedRequests"
:key="req.id"
:class="['queue-item', req.status]"
>
<span class="req-id">#{{ req.id }}</span>
<span class="req-arrow"></span>
<span
v-if="req.assignedServer"
class="req-target"
:style="{ color: req.serverColor }"
>
{{ req.assignedServer }}
</span>
<span
v-else
class="req-status"
>{{ req.statusText }}</span>
</div>
</div>
</div>
<div class="strategy-stats">
<div class="stats-title">
📊 负载分布统计
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">
{{ avgLoad }}%
</div>
<div class="stat-label">
平均负载
</div>
</div>
<div class="stat-card">
<div class="stat-value">
{{ maxLoad }}%
</div>
<div class="stat-label">
最高负载
</div>
</div>
<div class="stat-card">
<div class="stat-value">
{{ loadStdDev }}
</div>
<div class="stat-label">
负载标准差
</div>
</div>
<div class="stat-card">
<div class="stat-value">
{{ mostBusyServer || '-' }}
</div>
<div class="stat-label">
最忙服务器
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive, watch } from 'vue'
// 负载均衡策略
const strategies = [
{
id: 'roundrobin',
icon: '🔄',
name: '轮询',
badge: '默认',
shortDesc: '挨个分发,雨露均沾',
fullDesc: '按照服务器列表的顺序,依次将请求分配给每台服务器。就像银行叫号,1号窗口完事了到2号,2号完事了到3号,轮着来。'
},
{
id: 'weighted',
icon: '⚖️',
name: '加权轮询',
badge: '',
shortDesc: '性能好的多干活',
fullDesc: '给每台服务器设置一个权重值,性能强的服务器权重高,分配到的请求就多。就像团队里能力强的人多分担点任务。'
},
{
id: 'leastconn',
icon: '🔌',
name: '最少连接',
badge: '',
shortDesc: '谁闲找谁',
fullDesc: '将新请求分配给当前活跃连接数最少的服务器。就像食堂打饭,看哪个窗口排队的人少就去哪个。'
},
{
id: 'iphash',
icon: '🔢',
name: 'IP 哈希',
badge: '',
shortDesc: '同一用户永远去同一台',
fullDesc: '根据客户端 IP 地址计算哈希值,将同一 IP 的请求永远分配到同一台服务器。适用于需要保持会话状态的场景(如购物车)。'
}
]
const currentStrategy = ref('roundrobin')
const isSimulating = ref(false)
const serverCount = ref(4)
const currentIndex = ref(0)
const currentStrategyData = computed(() => strategies.find(s => s.id === currentStrategy.value))
// 生成服务器列表
const generateServers = (count) => {
const colors = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']
const names = ['Server-A', 'Server-B', 'Server-C', 'Server-D', 'Server-E', 'Server-F']
return Array.from({ length: count }, (_, i) => ({
id: i,
name: names[i] || `Server-${i + 1}`,
color: colors[i % colors.length],
requests: 0,
load: Math.floor(Math.random() * 40) + 10,
weight: Math.floor(Math.random() * 5) + 1,
connections: Math.floor(Math.random() * 20),
active: false,
recentRequests: []
}))
}
const servers = ref(generateServers(serverCount.value))
// 请求队列
const requestQueue = ref([])
const totalRequests = ref(0)
const pendingRequests = computed(() => requestQueue.value.filter(r => r.status === 'pending'))
const displayedRequests = computed(() => requestQueue.value.slice(0, 10))
// 选择服务器的算法
const selectServer = (requestId, clientIP) => {
let selectedIndex = 0
switch (currentStrategy.value) {
case 'roundrobin':
selectedIndex = currentIndex.value % servers.value.length
currentIndex.value++
break
case 'weighted':
const totalWeight = servers.value.reduce((sum, s) => sum + s.weight, 0)
let random = Math.random() * totalWeight
for (let i = 0; i < servers.value.length; i++) {
random -= servers.value[i].weight
if (random <= 0) {
selectedIndex = i
break
}
}
break
case 'leastconn':
selectedIndex = servers.value.reduce((minIdx, s, i, arr) =>
s.connections < arr[minIdx].connections ? i : minIdx, 0)
break
case 'iphash':
const hash = clientIP.split('.').reduce((h, octet) => (h * 31 + parseInt(octet)) & 0xffffffff, 0)
selectedIndex = hash % servers.value.length
break
}
return servers.value[selectedIndex]
}
// 模拟请求
const simulateRequest = async () => {
const reqId = totalRequests.value + 1
const clientIP = `192.168.1.${Math.floor(Math.random() * 255) + 1}`
const request = {
id: reqId,
clientIP,
status: 'pending',
statusText: '等待分配...',
assignedServer: null,
serverColor: null
}
requestQueue.value.unshift(request)
totalRequests.value++
// 模拟分配延迟
await new Promise(resolve => setTimeout(resolve, 300))
const server = selectServer(reqId, clientIP)
request.assignedServer = server.name
request.serverColor = server.color
request.status = 'assigned'
request.statusText = '已分配'
// 更新服务器状态
server.requests++
server.connections++
server.load = Math.min(100, server.load + Math.floor(Math.random() * 10) + 5)
server.active = true
server.recentRequests.unshift({ id: reqId, color: '#22c55e' })
if (server.recentRequests.length > 5) server.recentRequests.pop()
setTimeout(() => {
server.connections = Math.max(0, server.connections - 1)
if (server.connections === 0) server.active = false
}, 2000)
}
// 开始模拟
const startSimulation = async () => {
isSimulating.value = true
for (let i = 0; i < 20; i++) {
if (!isSimulating.value) break
simulateRequest()
await new Promise(resolve => setTimeout(resolve, 400))
}
isSimulating.value = false
}
// 重置模拟
const resetSimulation = () => {
isSimulating.value = false
servers.value = generateServers(serverCount.value)
requestQueue.value = []
totalRequests.value = 0
currentIndex.value = 0
}
// 切换策略
const changeStrategy = (id) => {
currentStrategy.value = id
resetSimulation()
}
// 统计计算
const avgLoad = computed(() => {
if (servers.value.length === 0) return 0
return Math.round(servers.value.reduce((sum, s) => sum + s.load, 0) / servers.value.length)
})
const maxLoad = computed(() => {
if (servers.value.length === 0) return 0
return Math.max(...servers.value.map(s => s.load))
})
const loadStdDev = computed(() => {
if (servers.value.length === 0) return 0
const avg = avgLoad.value
const variance = servers.value.reduce((sum, s) => sum + Math.pow(s.load - avg, 2), 0) / servers.value.length
return Math.sqrt(variance).toFixed(1)
})
const mostBusyServer = computed(() => {
if (servers.value.length === 0) return null
return servers.value.reduce((max, s) => s.load > max.load ? s : max, servers.value[0]).name
})
// 监听服务器数量变化
watch(serverCount, (newVal) => {
if (!isSimulating.value) {
servers.value = generateServers(newVal)
}
})
</script>
<style scoped>
.load-balancing-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;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.strategy-selector {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.selector-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
text-align: center;
color: var(--vp-c-text-1);
}
.strategy-tabs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.strategy-tab {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.strategy-tab:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.strategy-tab.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.2);
}
.tab-icon {
font-size: 1.75rem;
}
.tab-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.tab-badge {
position: absolute;
top: -6px;
right: -6px;
background: #22c55e;
color: white;
font-size: 0.65rem;
font-weight: 700;
padding: 0.15rem 0.4rem;
border-radius: 999px;
}
.simulation-area {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.sim-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.sim-title {
font-weight: 700;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.sim-controls {
display: flex;
gap: 0.5rem;
}
.sim-btn {
padding: 0.6rem 1.25rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.sim-btn:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
transform: translateY(-1px);
}
.sim-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.sim-btn.reset {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.strategy-explanation {
background: linear-gradient(135deg, rgba(var(--vp-c-brand-rgb), 0.1), rgba(var(--vp-c-brand-rgb), 0.05));
border: 2px solid var(--vp-c-brand);
border-radius: 10px;
padding: 0.75rem;
margin-bottom: 1.5rem;
display: flex;
gap: 1rem;
align-items: flex-start;
}
.exp-icon {
font-size: 1.5rem;
}
.exp-content {
flex: 1;
}
.exp-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 0.25rem;
color: var(--vp-c-text-1);
}
.exp-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.servers-pool {
margin-bottom: 1.5rem;
}
.pool-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.75rem;
}
.pool-title {
font-weight: 700;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.pool-config {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.pool-config input[type="range"] {
width: 120px;
}
.pool-config span {
min-width: 50px;
font-weight: 600;
}
.servers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.server-card {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 0.75rem;
transition: all 0.3s;
}
.server-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.server-card.active {
box-shadow: 0 0 0 3px currentColor;
}
.server-card.overloaded {
background: #fef2f2;
border-color: #ef4444;
}
.server-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.server-icon {
font-size: 1.25rem;
}
.server-name {
flex: 1;
font-weight: 700;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.server-status {
padding: 0.25rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
color: white;
}
.server-metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.metric {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
}
.metric-label {
color: var(--vp-c-text-2);
}
.metric-value {
font-weight: 700;
color: var(--vp-c-text-1);
}
.weight-input {
width: 50px;
padding: 0.1rem 0.25rem;
font-size: 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
.load-bar {
height: 8px;
background: var(--vp-c-bg-alt);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.75rem;
}
.load-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
}
.recent-requests {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
}
.req-label {
color: var(--vp-c-text-2);
white-space: nowrap;
}
.req-list {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
}
.req-badge {
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: 600;
color: white;
}
.request-queue {
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 0.75rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.queue-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.queue-title {
font-weight: 700;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.queue-stats {
display: flex;
gap: 1rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.queue-items {
display: flex;
flex-direction: column;
gap: 0.25rem;
max-height: 200px;
}
.queue-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.6rem;
background: white;
border-radius: 5px;
font-size: 0.8rem;
border-left: 3px solid var(--vp-c-divider);
}
.queue-item.pending {
border-left-color: #f59e0b;
background: #fffbeb;
}
.queue-item.assigned {
border-left-color: #22c55e;
background: #f0fdf4;
}
.req-id {
font-weight: 700;
color: var(--vp-c-text-1);
min-width: 40px;
}
.req-arrow {
color: var(--vp-c-text-2);
}
.req-target {
font-weight: 700;
}
.req-status {
color: var(--vp-c-text-2);
font-style: italic;
}
.strategy-stats {
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.stats-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
text-align: center;
color: var(--vp-c-text-1);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.stat-card {
background: white;
border-radius: 6px;
padding: 0.75rem;
text-align: center;
border: 1px solid var(--vp-c-divider);
}
.stat-value {
font-weight: 700;
font-size: 1.25rem;
color: var(--vp-c-brand);
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
@media (max-width: 768px) {
.strategy-tabs {
grid-template-columns: 1fr;
}
.auth-tabs {
grid-template-columns: 1fr;
}
.servers-grid {
grid-template-columns: 1fr;
}
.server-metrics {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>