0eba9e87e9
- 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>
928 lines
20 KiB
Vue
928 lines
20 KiB
Vue
<!--
|
||
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>
|