feat: update docs and components, fix DLQ demo bug

This commit is contained in:
sanbuphy
2026-01-18 12:21:49 +08:00
parent 26ed39e1eb
commit e41063a1cd
159 changed files with 54236 additions and 2525 deletions
@@ -0,0 +1,501 @@
<!--
AlertFlowDemo.vue
告警流程演示展示从监控指标异常到告警通知的完整流程
-->
<template>
<div class="alert-flow-demo">
<div class="header">
<div class="title">告警流程 (Alerting Flow)</div>
<div class="subtitle">从发现异常到通知运维的自动化流程</div>
</div>
<div class="controls">
<button
v-for="scenario in scenarios"
:key="scenario.id"
:class="['scenario-btn', { active: activeScenario === scenario.id }]"
@click="triggerScenario(scenario.id)"
>
{{ scenario.name }}
</button>
</div>
<div class="flow-steps">
<div
v-for="(step, index) in steps"
:key="step.id"
:class="[
'flow-step',
{ active: step.active, completed: step.completed }
]"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div v-if="step.details" class="step-details">{{ step.details }}</div>
</div>
<div v-if="index < steps.length - 1" class="step-arrow"></div>
</div>
</div>
<div class="alert-info" v-if="currentAlert">
<div class="alert-header" :class="'level-' + currentAlert.level">
<span class="alert-icon"></span>
<span class="alert-title">告警详情</span>
<span class="alert-level">{{ currentAlert.levelName }}</span>
</div>
<div class="alert-body">
<div class="alert-row">
<span class="label">告警名称</span>
<span class="value">{{ currentAlert.name }}</span>
</div>
<div class="alert-row">
<span class="label">触发时间</span>
<span class="value">{{ currentAlert.time }}</span>
</div>
<div class="alert-row">
<span class="label">当前值</span>
<span class="value critical">{{ currentAlert.currentValue }}</span>
</div>
<div class="alert-row">
<span class="label">阈值</span>
<span class="value">{{ currentAlert.threshold }}</span>
</div>
<div class="alert-row">
<span class="label">通知渠道</span>
<span class="value">{{ currentAlert.channels.join(', ') }}</span>
</div>
</div>
</div>
<div class="level-guide">
<div class="guide-title">告警级别说明</div>
<div class="levels">
<div class="level-item">
<span class="level-badge p0">P0</span>
<span>最高优先级立即处理如核心服务宕机</span>
</div>
<div class="level-item">
<span class="level-badge p1">P1</span>
<span>高优先级30分钟内处理如部分功能异常</span>
</div>
<div class="level-item">
<span class="level-badge p2">P2</span>
<span>中优先级当天处理如性能下降</span>
</div>
<div class="level-item">
<span class="level-badge p3">P3</span>
<span>低优先级本周处理如资源使用率偏高</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeScenario = ref(null)
const currentAlert = ref(null)
const scenarios = [
{ id: 'cpu', name: 'CPU 过载告警' },
{ id: 'latency', name: '响应延迟告警' },
{ id: 'error', name: '错误率飙升告警' },
{ id: 'disk', name: '磁盘空间不足告警' }
]
const steps = ref([
{
id: 'monitor',
title: '监控采集',
desc: 'Prometheus 每隔 15s 采集一次指标',
active: false,
completed: false
},
{
id: 'rule',
title: '规则评估',
desc: 'Alertmanager 评估是否满足告警条件',
active: false,
completed: false
},
{
id: 'group',
title: '告警分组',
desc: '相似告警合并,避免轰炸',
active: false,
completed: false
},
{
id: 'silence',
title: '静默判断',
desc: '检查是否在静默时间(如维护窗口)',
active: false,
completed: false
},
{
id: 'route',
title: '路由分发',
desc: '根据标签分发到不同接收器',
active: false,
completed: false
},
{
id: 'notify',
title: '发送通知',
desc: '通过钉钉/邮件/短信通知值班人员',
active: false,
completed: false
}
])
const scenarioData = {
cpu: {
name: 'CPU 使用率过高',
level: 'p1',
levelName: 'P1 - 高优先级',
currentValue: '92%',
threshold: '> 85%',
channels: ['钉钉', '短信', '邮件']
},
latency: {
name: 'API 响应延迟过高',
level: 'p0',
levelName: 'P0 - 最高优先级',
currentValue: '2350ms',
threshold: '> 1000ms',
channels: ['钉钉', '短信', '电话']
},
error: {
name: '错误率异常升高',
level: 'p0',
levelName: 'P0 - 最高优先级',
currentValue: '8.5%',
threshold: '> 5%',
channels: ['钉钉', '短信', '电话', '邮件']
},
disk: {
name: '磁盘空间不足',
level: 'p2',
levelName: 'P2 - 中优先级',
currentValue: '88%',
threshold: '> 85%',
channels: ['钉钉', '邮件']
}
}
const triggerScenario = async (scenarioId) => {
activeScenario.value = scenarioId
currentAlert.value = null
// 重置所有步骤
steps.value.forEach((step) => {
step.active = false
step.completed = false
})
// 逐步执行流程
for (let i = 0; i < steps.value.length; i++) {
steps.value[i].active = true
await new Promise((resolve) => setTimeout(resolve, 600))
steps.value[i].active = false
steps.value[i].completed = true
// 最后一步时显示告警详情
if (i === steps.value.length - 1) {
const data = scenarioData[scenarioId]
currentAlert.value = {
...data,
time: new Date().toLocaleString('zh-CN')
}
}
}
}
</script>
<style scoped>
.alert-flow-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;
}
.controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.scenario-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.scenario-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.scenario-btn.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.flow-step {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.flow-step.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.05);
}
.flow-step.completed {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.05);
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.9rem;
flex-shrink: 0;
}
.flow-step.active .step-number {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.flow-step.completed .step-number {
border-color: #22c55e;
color: #22c55e;
}
.step-content {
flex: 1;
}
.step-title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.25rem;
}
.step-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.step-details {
margin-top: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.step-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.alert-info {
background: var(--vp-c-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 1.5rem;
}
.alert-header {
padding: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: #fff;
}
.alert-header.level-p0 {
background: #ef4444;
}
.alert-header.level-p1 {
background: #f59e0b;
}
.alert-header.level-p2 {
background: #eab308;
}
.alert-icon {
font-size: 1.5rem;
}
.alert-title {
font-weight: 700;
font-size: 1rem;
flex: 1;
}
.alert-level {
background: rgba(255, 255, 255, 0.2);
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
}
.alert-body {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.alert-row {
display: flex;
font-size: 0.9rem;
}
.label {
color: var(--vp-c-text-2);
min-width: 100px;
}
.value {
font-weight: 600;
color: var(--vp-c-text-1);
}
.value.critical {
color: #ef4444;
}
.level-guide {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.guide-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.levels {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.level-item {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.85rem;
}
.level-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: 700;
font-size: 0.75rem;
color: #fff;
min-width: 40px;
text-align: center;
}
.level-badge.p0 {
background: #ef4444;
}
.level-badge.p1 {
background: #f59e0b;
}
.level-badge.p2 {
background: #eab308;
}
.level-badge.p3 {
background: #84cc16;
}
@media (max-width: 768px) {
.flow-steps {
gap: 0.5rem;
}
.flow-step {
flex-direction: column;
align-items: flex-start;
}
.step-arrow {
transform: rotate(90deg);
align-self: center;
}
.controls {
flex-direction: column;
}
.scenario-btn {
width: 100%;
}
}
</style>
@@ -0,0 +1,485 @@
<!--
CapacityPlanningDemo.vue
容量规划计算器帮助理解如何评估系统容量需求
-->
<template>
<div class="capacity-demo">
<div class="header">
<div class="title">容量规划计算器 (Capacity Planning)</div>
<div class="subtitle">估算系统需要多少台服务器才能满足需求</div>
</div>
<div class="calculator">
<div class="input-section">
<div class="section-title">📊 业务指标</div>
<div class="input-grid">
<div class="input-group">
<label>日活用户 (DAU)</label>
<input v-model.number="dau" type="number" min="1" step="1000" />
<span class="unit"></span>
</div>
<div class="input-group">
<label>人均请求/</label>
<input v-model.number="requestsPerUser" type="number" min="1" />
<span class="unit"></span>
</div>
<div class="input-group">
<label>高峰时段占比</label>
<input v-model.number="peakRatio" type="number" min="1" max="100" />
<span class="unit">%</span>
</div>
<div class="input-group">
<label>单机 QPS 能力</label>
<input v-model.number="serverQps" type="number" min="1" />
<span class="unit">/</span>
</div>
<div class="input-group">
<label>冗余系数</label>
<input
v-model.number="redundancy"
type="number"
min="1"
max="3"
step="0.1"
/>
<span class="unit"></span>
</div>
</div>
<div class="tips">
💡
<span class="tip-text"
>通常高峰期流量是平均流量的 2-3 建议预留 50-100%
冗余应对突发流量</span
>
</div>
</div>
<div class="output-section">
<div class="section-title">📈 容量评估结果</div>
<div class="result-card">
<div class="result-label">日均总请求量</div>
<div class="result-value">
{{ totalRequests.toLocaleString() }} /
</div>
</div>
<div class="result-card highlight">
<div class="result-label">高峰期 QPS (目标)</div>
<div class="result-value">{{ targetQPS.toLocaleString() }} /</div>
</div>
<div class="result-card">
<div class="result-label">理论所需服务器</div>
<div class="result-value">{{ minServers }} </div>
</div>
<div class="result-card highlight">
<div class="result-label">推荐配置 (含冗余)</div>
<div class="result-value large">{{ recommendedServers }} </div>
</div>
<div class="cost-estimate">
<div class="cost-title">💰 月成本估算 (云服务器)</div>
<div class="cost-options">
<div
class="cost-option"
v-for="option in costOptions"
:key="option.name"
>
<div class="option-name">{{ option.name }}</div>
<div class="option-price">
¥{{ option.price.toLocaleString() }}/
</div>
</div>
</div>
</div>
</div>
</div>
<div class="planning-tips">
<div class="tips-title">🎯 容量规划要点</div>
<div class="tips-grid">
<div class="tip-card">
<div class="tip-icon">1</div>
<div class="tip-title">以峰值为核心</div>
<div class="tip-desc">
不能按平均流量规划必须按高峰期流量通常是平均的 2-3 来准备
</div>
</div>
<div class="tip-card">
<div class="tip-icon">2</div>
<div class="tip-title">预留冗余空间</div>
<div class="tip-desc">
至少预留 50% 冗余用于应对突发流量服务器故障维护窗口
</div>
</div>
<div class="tip-card">
<div class="tip-icon">3</div>
<div class="tip-title">定期压测验证</div>
<div class="tip-desc">
每季度进行压力测试验证实际容量是否满足预估
</div>
</div>
<div class="tip-card">
<div class="tip-icon">4</div>
<div class="tip-title">弹性扩缩容</div>
<div class="tip-desc">
结合云服务的自动扩缩容在高峰期自动增加实例
</div>
</div>
</div>
</div>
<div class="formula-section">
<div class="formula-title">📐 计算公式</div>
<div class="formula-list">
<div class="formula-item">
<span class="formula-label">日均请求量</span>
<span class="formula-math">DAU × 人均请求次数</span>
</div>
<div class="formula-item">
<span class="formula-label">平均 QPS</span>
<span class="formula-math">日均请求量 ÷ 86400 </span>
</div>
<div class="formula-item">
<span class="formula-label">高峰 QPS</span>
<span class="formula-math">平均 QPS × 高峰系数 (2-3 )</span>
</div>
<div class="formula-item">
<span class="formula-label">所需服务器</span>
<span class="formula-math">高峰 QPS × 冗余系数 ÷ 单机 QPS</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const dau = ref(100000)
const requestsPerUser = ref(50)
const peakRatio = ref(30)
const serverQps = ref(2000)
const redundancy = ref(1.5)
const totalRequests = computed(() => {
return Math.round(dau.value * requestsPerUser.value)
})
const avgQPS = computed(() => {
return Math.round(totalRequests.value / 86400)
})
const peakQPS = computed(() => {
const avg = avgQPS.value
const peak = avg * (1 + peakRatio.value / 100)
return Math.round(peak)
})
const targetQPS = computed(() => {
return peakQPS.value
})
const minServers = computed(() => {
return Math.ceil(targetQPS.value / serverQps.value)
})
const recommendedServers = computed(() => {
const min = minServers.value
return Math.ceil(min * redundancy.value)
})
const costOptions = computed(() => {
const servers = recommendedServers.value
return [
{
name: '阿里云 (4核8G)',
price: servers * 300
},
{
name: '腾讯云 (4核8G)',
price: servers * 280
},
{
name: 'AWS (t3.xlarge)',
price: servers * 500
}
]
})
</script>
<style scoped>
.capacity-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;
}
.calculator {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.input-section,
.output-section {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
border: 1px solid var(--vp-c-divider);
}
.section-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.input-grid {
display: flex;
flex-direction: column;
gap: 1rem;
}
.input-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.input-group label {
min-width: 120px;
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.input-group input {
flex: 1;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.9rem;
background: var(--vp-c-bg-soft);
}
.input-group .unit {
font-size: 0.85rem;
color: var(--vp-c-text-2);
min-width: 40px;
}
.tips {
margin-top: 1rem;
padding: 0.75rem;
background: rgba(var(--vp-c-brand-rgb), 0.05);
border-radius: 6px;
font-size: 0.85rem;
display: flex;
gap: 0.5rem;
align-items: flex-start;
}
.tip-text {
color: var(--vp-c-text-2);
line-height: 1.5;
}
.result-card {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.result-card.highlight {
background: rgba(var(--vp-c-brand-rgb), 0.1);
border: 1px solid var(--vp-c-brand);
}
.result-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.result-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.result-value.large {
font-size: 1.8rem;
color: var(--vp-c-brand);
}
.cost-estimate {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
.cost-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.cost-options {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.cost-option {
display: flex;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-size: 0.85rem;
}
.option-name {
color: var(--vp-c-text-1);
}
.option-price {
font-weight: 700;
color: var(--vp-c-brand);
}
.planning-tips {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.tips-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
}
.tip-card {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
}
.tip-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.tip-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.tip-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.formula-section {
background: rgba(var(--vp-c-brand-rgb), 0.05);
border-radius: 10px;
padding: 1.25rem;
border: 1px solid var(--vp-c-brand);
}
.formula-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.formula-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.formula-item {
display: flex;
gap: 1rem;
font-size: 0.9rem;
}
.formula-label {
min-width: 120px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.formula-math {
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
@media (max-width: 768px) {
.calculator {
grid-template-columns: 1fr;
}
.input-group {
flex-wrap: wrap;
}
.input-group label {
min-width: 100%;
margin-bottom: 0.25rem;
}
.input-group input {
min-width: 150px;
}
.tips-grid {
grid-template-columns: 1fr;
}
.formula-item {
flex-direction: column;
gap: 0.25rem;
}
}
</style>
@@ -0,0 +1,463 @@
<!--
IncidentResponseDemo.vue
故障响应流程演示展示从故障发现到复盘的完整流程
-->
<template>
<div class="incident-demo">
<div class="header">
<div class="title">故障响应流程 (Incident Response)</div>
<div class="subtitle">专业团队如何处理线上故障</div>
</div>
<div class="timeline">
<div
v-for="(phase, index) in phases"
:key="phase.id"
:class="[
'phase',
{ active: activePhase === index, completed: activePhase > index }
]"
@click="activePhase = index"
>
<div class="phase-marker">{{ index + 1 }}</div>
<div class="phase-content">
<div class="phase-title">{{ phase.title }}</div>
<div class="phase-time">{{ phase.time }}</div>
<div class="phase-desc">{{ phase.desc }}</div>
<div v-if="activePhase === index" class="phase-actions">
<div class="action-title">关键动作</div>
<ul class="action-list">
<li v-for="action in phase.actions" :key="action">
{{ action }}
</li>
</ul>
<div v-if="phase.tools" class="tools-section">
<div class="tools-title">常用工具</div>
<div class="tools-list">
<span
v-for="tool in phase.tools"
:key="tool"
class="tool-tag"
>{{ tool }}</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="incident-meta" v-if="activePhase === phases.length - 1">
<div class="meta-title">📋 故障复盘报告 (Post-mortem)</div>
<div class="meta-content">
<div class="meta-section">
<div class="meta-label">故障等级</div>
<div class="meta-value level-p1">P1 - 高优先级</div>
</div>
<div class="meta-section">
<div class="meta-label">影响范围</div>
<div class="meta-value"> 15% 用户无法访问订单服务</div>
</div>
<div class="meta-section">
<div class="meta-label">故障时长</div>
<div class="meta-value">23 分钟</div>
</div>
<div class="meta-section">
<div class="meta-label">根本原因</div>
<div class="meta-value">数据库连接池配置过小高峰期连接耗尽</div>
</div>
<div class="meta-section">
<div class="meta-label">改进措施</div>
<div class="meta-value">
1. 增加连接池大小至 200
<br />
2. 添加连接池监控告警
<br />
3. 优化慢查询减少连接占用时间
</div>
</div>
</div>
</div>
<div class="best-practices">
<div class="practice-title">🎯 故障处理最佳实践</div>
<div class="practice-grid">
<div class="practice-card">
<div class="practice-icon"></div>
<div class="practice-name">快速响应</div>
<div class="practice-desc">
建立 15 分钟响应机制P0 故障立即电话通知
</div>
</div>
<div class="practice-card">
<div class="practice-icon">📢</div>
<div class="practice-name">信息同步</div>
<div class="practice-desc">
定期向用户和内部同步故障进展避免猜测
</div>
</div>
<div class="practice-card">
<div class="practice-icon">🔍</div>
<div class="practice-name">保留现场</div>
<div class="practice-desc">
故障现场数据日志监控完整留存便于分析
</div>
</div>
<div class="practice-card">
<div class="practice-icon">📝</div>
<div class="practice-name">blameless 文化</div>
<div class="practice-desc">
复盘对事不对人聚焦流程改进而非个人责任
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activePhase = ref(0)
const phases = [
{
id: 'detect',
title: '故障发现',
time: 'T+0 分钟',
desc: '监控系统自动发现异常指标',
actions: [
'监控检测到订单服务错误率从 0.1% 飙升到 8.5%',
'Alertmanager 立即触发 P1 告警',
'值班人员收到钉钉和短信通知'
],
tools: ['Prometheus', 'Grafana', 'Alertmanager']
},
{
id: 'respond',
title: '快速响应',
time: 'T+3 分钟',
desc: '值班人员确认故障并启动应急流程',
actions: [
'登录监控面板确认故障范围',
'创建线上故障 War Room 会议',
'通知相关开发人员和运维人员'
],
tools: ['钉钉/飞书', 'Zoom/腾讯会议']
},
{
id: 'diagnose',
title: '故障定位',
time: 'T+8 分钟',
desc: '通过日志和追踪系统分析根因',
actions: [
'查看应用日志,发现大量 "Connection pool exhausted" 错误',
'通过链路追踪定位到数据库查询耗时异常',
'检查数据库监控,发现连接池已满'
],
tools: ['ELK', 'Jaeger/Zipkin', 'Arthas', 'tcpdump']
},
{
id: 'fix',
title: '故障修复',
time: 'T+18 分钟',
desc: '实施临时解决方案恢复服务',
actions: [
'紧急扩容数据库连接池从 50 到 200',
'重启应用服务使配置生效',
'监控显示错误率逐渐下降到正常水平'
],
tools: ['K8s Dashboard', 'kubectl', 'ansible']
},
{
id: 'verify',
title: '恢复验证',
time: 'T+21 分钟',
desc: '确认服务完全恢复正常',
actions: [
'监控指标全部回到正常范围',
'执行冒烟测试验证核心功能',
'观察 5 分钟无异常,宣布故障结束'
],
tools: ['Postman', '自动化测试平台']
},
{
id: 'postmortem',
title: '故障复盘',
time: 'T+48 小时',
desc: '总结经验教训,制定改进计划',
actions: [
'召开复盘会议,整理故障时间线',
'编写 Post-mortem 报告',
'跟进改进措施落实情况'
],
tools: ['Confluence/Notion', 'JIRA']
}
]
</script>
<style scoped>
.incident-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;
}
.timeline {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.phase {
display: flex;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 10px;
border: 2px solid var(--vp-c-divider);
cursor: pointer;
transition: all 0.3s;
}
.phase:hover {
border-color: var(--vp-c-brand);
}
.phase.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.05);
}
.phase.completed {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.02);
}
.phase-marker {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1rem;
flex-shrink: 0;
}
.phase.active .phase-marker {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
}
.phase.completed .phase-marker {
border-color: #22c55e;
color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
.phase-content {
flex: 1;
}
.phase-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 0.25rem;
}
.phase-time {
font-size: 0.85rem;
color: var(--vp-c-brand);
font-weight: 600;
margin-bottom: 0.5rem;
}
.phase-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
}
.phase-actions {
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin-top: 0.75rem;
}
.action-title {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.5rem;
}
.action-list {
margin: 0;
padding-left: 1.25rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.action-list li {
margin-bottom: 0.25rem;
}
.tools-section {
margin-top: 0.75rem;
}
.tools-title {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.5rem;
}
.tools-list {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.tool-tag {
padding: 0.25rem 0.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.75rem;
font-family: var(--vp-font-family-mono);
}
.incident-meta {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.meta-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.meta-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.meta-section {
display: flex;
gap: 1rem;
font-size: 0.9rem;
}
.meta-label {
min-width: 100px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.meta-value {
flex: 1;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.meta-value.level-p1 {
color: #f59e0b;
font-weight: 700;
}
.best-practices {
background: rgba(var(--vp-c-brand-rgb), 0.05);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-brand);
}
.practice-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.practice-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.practice-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.practice-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.practice-name {
font-weight: 700;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.practice-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
@media (max-width: 768px) {
.phase {
flex-direction: column;
}
.phase-marker {
width: 32px;
height: 32px;
}
.meta-section {
flex-direction: column;
gap: 0.25rem;
}
.meta-label {
min-width: auto;
}
.practice-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,446 @@
<!--
MonitoringDashboardDemo.vue
监控面板演示展示基础设施应用业务三个层次的监控指标
-->
<template>
<div class="monitoring-dashboard">
<div class="header">
<div class="title">实时监控面板 (Monitoring Dashboard)</div>
<div class="subtitle">运维的"眼睛" - 让系统状态一目了然</div>
</div>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="dashboard-content">
<!-- 基础设施监控 -->
<div v-if="activeTab === 'infra'" class="metrics-grid">
<div
v-for="metric in infraMetrics"
:key="metric.name"
class="metric-card"
>
<div class="metric-header">
<span class="metric-name">{{ metric.name }}</span>
<span class="metric-value"
>{{ metric.value }}{{ metric.unit }}</span
>
</div>
<div class="metric-chart">
<div
class="chart-bar"
:style="{
width: metric.value + '%',
background: getColor(metric.value, metric.threshold)
}"
></div>
</div>
<div
class="metric-status"
:class="getStatus(metric.value, metric.threshold)"
>
{{ getStatusText(metric.value, metric.threshold) }}
</div>
</div>
</div>
<!-- 应用监控 -->
<div v-if="activeTab === 'app'" class="metrics-grid">
<div class="metric-card large">
<div class="metric-header">
<span class="metric-name">QPS (每秒请求数)</span>
<span class="metric-value">{{ qps }}</span>
</div>
<div class="qps-chart">
<div
v-for="(height, index) in qpsHistory"
:key="index"
class="qps-bar"
:style="{ height: height + '%' }"
></div>
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-name">平均响应时间</span>
<span class="metric-value">{{ latency }} ms</span>
</div>
<div
class="metric-status"
:class="latency > 500 ? 'critical' : 'normal'"
>
{{ latency > 500 ? '需要优化' : '正常' }}
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-name">错误率</span>
<span class="metric-value">{{ errorRate }}%</span>
</div>
<div
class="metric-status"
:class="errorRate > 1 ? 'critical' : 'normal'"
>
{{ errorRate > 1 ? '告警' : '正常' }}
</div>
</div>
</div>
<!-- 业务监控 -->
<div v-if="activeTab === 'business'" class="metrics-grid">
<div
v-for="metric in businessMetrics"
:key="metric.name"
class="metric-card"
>
<div class="metric-header">
<span class="metric-name">{{ metric.name }}</span>
<span class="metric-value">{{ metric.value }}</span>
</div>
<div class="trend" :class="metric.trend">
{{
metric.trend === 'up'
? '📈 上升'
: metric.trend === 'down'
? '📉 下降'
: '➡️ 持平'
}}
</div>
<div class="metric-desc">{{ metric.desc }}</div>
</div>
</div>
</div>
<div class="legend">
<div class="item">
<span class="dot normal"></span>
<span>正常 (Normal)</span>
</div>
<div class="item">
<span class="dot warning"></span>
<span>警告 (Warning)</span>
</div>
<div class="item">
<span class="dot critical"></span>
<span>严重 (Critical)</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const activeTab = ref('infra')
const tabs = [
{ id: 'infra', name: '基础设施' },
{ id: 'app', name: '应用监控' },
{ id: 'business', name: '业务监控' }
]
const infraMetrics = ref([
{ name: 'CPU 使用率', value: 45, unit: '%', threshold: 80 },
{ name: '内存使用率', value: 62, unit: '%', threshold: 85 },
{ name: '磁盘使用率', value: 78, unit: '%', threshold: 90 },
{ name: '网络带宽', value: 34, unit: '%', threshold: 80 },
{ name: '磁盘 I/O', value: 55, unit: '%', threshold: 70 },
{ name: '负载均衡', value: 42, unit: '%', threshold: 75 }
])
const qps = ref(1250)
const latency = ref(180)
const errorRate = ref(0.12)
const qpsHistory = ref([
40, 55, 45, 60, 50, 65, 70, 60, 75, 80, 70, 85, 90, 80, 95, 100
])
const businessMetrics = ref([
{
name: '在线用户数',
value: '12,458',
trend: 'up',
desc: '当前实时在线用户'
},
{
name: '订单量/小时',
value: '856',
trend: 'up',
desc: '过去一小时的订单数'
},
{
name: '支付成功率',
value: '98.5%',
trend: 'stable',
desc: '支付成功的比例'
},
{
name: 'DAU (日活)',
value: '45,621',
trend: 'up',
desc: '今日活跃用户数'
}
])
let interval = null
const getColor = (value, threshold) => {
if (value >= threshold) return '#ef4444'
if (value >= threshold * 0.8) return '#f59e0b'
return '#22c55e'
}
const getStatus = (value, threshold) => {
if (value >= threshold) return 'critical'
if (value >= threshold * 0.8) return 'warning'
return 'normal'
}
const getStatusText = (value, threshold) => {
if (value >= threshold) return '严重'
if (value >= threshold * 0.8) return '警告'
return '正常'
}
const updateMetrics = () => {
// 更新基础设施指标
infraMetrics.value = infraMetrics.value.map((metric) => ({
...metric,
value: Math.max(0, Math.min(100, metric.value + (Math.random() - 0.5) * 10))
}))
// 更新应用指标
qps.value = Math.round(1200 + Math.random() * 200)
latency.value = Math.round(150 + Math.random() * 100)
errorRate.value = Math.max(
0,
Math.round((0.1 + Math.random() * 0.3) * 100) / 100
)
// 更新 QPS 历史图表
qpsHistory.value.shift()
qpsHistory.value.push(Math.round(40 + Math.random() * 60))
}
onMounted(() => {
interval = setInterval(updateMetrics, 2000)
})
onUnmounted(() => {
if (interval) clearInterval(interval)
})
</script>
<style scoped>
.monitoring-dashboard {
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;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 0.5rem;
}
.tab {
padding: 0.5rem 1rem;
border: none;
background: none;
cursor: pointer;
font-size: 0.9rem;
color: var(--vp-c-text-2);
border-radius: 6px;
transition: all 0.2s;
}
.tab:hover {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.tab.active {
background: var(--vp-c-brand);
color: #fff;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.metric-card {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.metric-card.large {
grid-column: span 2;
}
.metric-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.metric-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.metric-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--vp-c-brand);
}
.metric-chart {
height: 8px;
background: var(--vp-c-bg-soft);
border-radius: 999px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.chart-bar {
height: 100%;
transition: width 0.5s ease;
}
.metric-status {
font-size: 0.85rem;
font-weight: 600;
}
.metric-status.normal {
color: #22c55e;
}
.metric-status.warning {
color: #f59e0b;
}
.metric-status.critical {
color: #ef4444;
}
.qps-chart {
display: flex;
align-items: flex-end;
gap: 2px;
height: 80px;
margin-top: 0.5rem;
}
.qps-bar {
flex: 1;
background: var(--vp-c-brand);
border-radius: 2px 2px 0 0;
min-height: 10px;
transition: height 0.3s ease;
}
.trend {
font-size: 0.85rem;
margin: 0.5rem 0;
font-weight: 600;
}
.trend.up {
color: #22c55e;
}
.trend.down {
color: #ef4444;
}
.trend.stable {
color: var(--vp-c-text-2);
}
.metric-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.legend {
display: flex;
gap: 1.5rem;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
}
.item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot.normal {
background: #22c55e;
}
.dot.warning {
background: #f59e0b;
}
.dot.critical {
background: #ef4444;
}
@media (max-width: 768px) {
.metrics-grid {
grid-template-columns: 1fr;
}
.metric-card.large {
grid-column: span 1;
}
}
</style>
@@ -0,0 +1,647 @@
<!--
TraceVisualizationDemo.vue
链路追踪可视化展示分布式系统中的请求调用链路
-->
<template>
<div class="trace-demo">
<div class="header">
<div class="title">分布式链路追踪 (Distributed Tracing)</div>
<div class="subtitle">一个请求在微服务间流转的完整路径</div>
</div>
<div class="controls">
<button
:class="['scenario-btn', { active: scenario === 'normal' }]"
@click="setScenario('normal')"
>
正常流程
</button>
<button
:class="['scenario-btn', { active: scenario === 'slow' }]"
@click="setScenario('slow')"
>
性能瓶颈
</button>
<button
:class="['scenario-btn', { active: scenario === 'error' }]"
@click="setScenario('error')"
>
错误追踪
</button>
</div>
<div class="trace-info">
<div class="info-item">
<span class="label">Trace ID</span>
<span class="value">{{ traceId }}</span>
</div>
<div class="info-item">
<span class="label">总耗时</span>
<span class="value">{{ totalDuration }}ms</span>
</div>
<div class="info-item">
<span class="label">调用服务数</span>
<span class="value">{{ spans.length }}</span>
</div>
</div>
<div class="spans-container">
<div class="time-ruler">
<div
v-for="tick in timeTicks"
:key="tick"
class="tick"
:style="{ left: tick + '%' }"
>
{{ tick }}ms
</div>
</div>
<div class="spans">
<div v-for="(span, index) in spans" :key="span.id" class="span-row">
<div class="span-service">{{ span.service }}</div>
<div class="span-timeline">
<div
class="span-bar"
:class="{
error: span.status === 'error',
warning: span.duration > 200,
success: span.status === 'success'
}"
:style="{
left: (span.startTime / totalDuration) * 100 + '%',
width: Math.max(5, (span.duration / totalDuration) * 100) + '%'
}"
>
<div class="span-details">
<div class="span-name">{{ span.name }}</div>
<div class="span-time">{{ span.duration }}ms</div>
</div>
</div>
</div>
<div class="span-status">
<span v-if="span.status === 'error'" class="status-error"></span>
<span v-else-if="span.duration > 200" class="status-warning"
></span
>
<span v-else class="status-success"></span>
</div>
</div>
</div>
</div>
<div class="span-detail" v-if="selectedSpan">
<div class="detail-header">Span 详情</div>
<div class="detail-body">
<div class="detail-row">
<span class="label">服务名</span>
<span class="value">{{ selectedSpan.service }}</span>
</div>
<div class="detail-row">
<span class="label">操作</span>
<span class="value">{{ selectedSpan.name }}</span>
</div>
<div class="detail-row">
<span class="label">耗时</span>
<span class="value">{{ selectedSpan.duration }}ms</span>
</div>
<div class="detail-row">
<span class="label">状态</span>
<span class="value" :class="selectedSpan.status">{{
selectedSpan.status
}}</span>
</div>
<div v-if="selectedSpan.error" class="detail-row">
<span class="label">错误信息</span>
<span class="value error">{{ selectedSpan.error }}</span>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<span class="color-box success"></span>
<span>正常 (200ms)</span>
</div>
<div class="legend-item">
<span class="color-box warning"></span>
<span>慢调用 (>200ms)</span>
</div>
<div class="legend-item">
<span class="color-box error"></span>
<span>错误</span>
</div>
</div>
<div class="tips">
<div class="tip-title">💡 观察要点</div>
<ul class="tip-list">
<li>点击"性能瓶颈"查看数据库查询慢导致的延迟</li>
<li>点击"错误追踪"查看库存服务异常如何影响整个链路</li>
<li>每个 Span 都有唯一的 Span ID通过 Trace ID 关联</li>
<li>时间条越长表示该服务耗时越长</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const scenario = ref('normal')
const selectedSpan = ref(null)
const traceId = ref('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
const spansData = {
normal: [
{
id: 1,
service: 'API Gateway',
name: 'POST /api/order/create',
startTime: 0,
duration: 450,
status: 'success'
},
{
id: 2,
service: 'User Service',
name: '验证用户身份',
startTime: 10,
duration: 45,
status: 'success'
},
{
id: 3,
service: 'Product Service',
name: '查询商品信息',
startTime: 70,
duration: 85,
status: 'success'
},
{
id: 4,
service: 'Inventory Service',
name: '扣减库存',
startTime: 175,
duration: 120,
status: 'success'
},
{
id: 5,
service: 'Payment Service',
name: '创建支付订单',
startTime: 310,
duration: 95,
status: 'success'
},
{
id: 6,
service: 'Order Service',
name: '保存订单记录',
startTime: 420,
duration: 25,
status: 'success'
}
],
slow: [
{
id: 1,
service: 'API Gateway',
name: 'POST /api/order/create',
startTime: 0,
duration: 1250,
status: 'success'
},
{
id: 2,
service: 'User Service',
name: '验证用户身份',
startTime: 10,
duration: 45,
status: 'success'
},
{
id: 3,
service: 'Product Service',
name: '查询商品信息',
startTime: 70,
duration: 85,
status: 'success'
},
{
id: 4,
service: 'Inventory Service',
name: '扣减库存',
startTime: 175,
duration: 520,
status: 'success'
},
{
id: 5,
service: 'Database',
name: 'UPDATE inventory SET count = count - 1',
startTime: 200,
duration: 480,
status: 'success'
},
{
id: 6,
service: 'Payment Service',
name: '创建支付订单',
startTime: 710,
duration: 95,
status: 'success'
},
{
id: 7,
service: 'Order Service',
name: '保存订单记录',
startTime: 820,
duration: 25,
status: 'success'
}
],
error: [
{
id: 1,
service: 'API Gateway',
name: 'POST /api/order/create',
startTime: 0,
duration: 280,
status: 'success'
},
{
id: 2,
service: 'User Service',
name: '验证用户身份',
startTime: 10,
duration: 45,
status: 'success'
},
{
id: 3,
service: 'Product Service',
name: '查询商品信息',
startTime: 70,
duration: 85,
status: 'success'
},
{
id: 4,
service: 'Inventory Service',
name: '扣减库存',
startTime: 175,
duration: 55,
status: 'error',
error: '库存不足: product_id=12345, required=10, available=5'
},
{
id: 5,
service: 'Order Service',
name: '回滚订单创建',
startTime: 240,
duration: 35,
status: 'success'
}
]
}
const spans = computed(() => spansData[scenario.value])
const totalDuration = computed(() => {
const maxEnd = spans.value.reduce((max, span) => {
return Math.max(max, span.startTime + span.duration)
}, 0)
return Math.ceil(maxEnd / 50) * 50 // 向上取整到 50ms
})
const timeTicks = computed(() => {
const ticks = []
for (let i = 0; i <= totalDuration.value; i += totalDuration.value / 10) {
ticks.push(Math.round(i))
}
return ticks
})
const setScenario = (s) => {
scenario.value = s
selectedSpan.value = null
}
</script>
<style scoped>
.trace-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;
}
.controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.scenario-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.scenario-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.scenario-btn.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.trace-info {
display: flex;
gap: 2rem;
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
flex-wrap: wrap;
}
.info-item {
display: flex;
gap: 0.5rem;
font-size: 0.9rem;
}
.label {
color: var(--vp-c-text-2);
font-weight: 600;
}
.value {
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.spans-container {
position: relative;
margin-bottom: 1.5rem;
}
.time-ruler {
position: relative;
height: 30px;
border-bottom: 1px solid var(--vp-c-divider);
margin-bottom: 1rem;
}
.tick {
position: absolute;
top: 0;
transform: translateX(-50%);
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.spans {
position: relative;
}
.span-row {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.75rem;
padding: 0.5rem;
border-radius: 6px;
transition: background 0.2s;
cursor: pointer;
}
.span-row:hover {
background: var(--vp-c-bg);
}
.span-service {
min-width: 140px;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.span-timeline {
flex: 1;
position: relative;
height: 40px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.span-bar {
position: absolute;
top: 4px;
bottom: 4px;
border-radius: 4px;
display: flex;
align-items: center;
padding: 0 0.5rem;
transition: all 0.3s;
cursor: pointer;
}
.span-bar.success {
background: linear-gradient(90deg, #22c55e, #16a34a);
}
.span-bar.warning {
background: linear-gradient(90deg, #f59e0b, #d97706);
}
.span-bar.error {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.span-bar:hover {
transform: scaleY(1.1);
filter: brightness(1.1);
}
.span-details {
display: flex;
align-items: center;
gap: 0.5rem;
color: #fff;
font-size: 0.8rem;
font-weight: 600;
white-space: nowrap;
}
.span-status {
min-width: 30px;
text-align: center;
font-size: 1.2rem;
}
.status-success {
color: #22c55e;
}
.status-warning {
color: #f59e0b;
}
.status-error {
color: #ef4444;
}
.span-detail {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.detail-body {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.detail-row {
display: flex;
font-size: 0.9rem;
}
.detail-row .label {
min-width: 100px;
color: var(--vp-c-text-2);
}
.detail-row .value {
font-weight: 600;
color: var(--vp-c-text-1);
}
.detail-row .value.success {
color: #22c55e;
}
.detail-row .value.error {
color: #ef4444;
}
.legend {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
font-size: 0.85rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.color-box {
width: 16px;
height: 16px;
border-radius: 4px;
}
.color-box.success {
background: #22c55e;
}
.color-box.warning {
background: #f59e0b;
}
.color-box.error {
background: #ef4444;
}
.tips {
background: rgba(var(--vp-c-brand-rgb), 0.05);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-brand);
}
.tip-title {
font-weight: 700;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.tip-list {
margin: 0;
padding-left: 1.25rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.tip-list li {
margin-bottom: 0.25rem;
}
@media (max-width: 768px) {
.span-row {
flex-wrap: wrap;
}
.span-service {
min-width: 100%;
margin-bottom: 0.25rem;
}
.span-timeline {
min-width: 200px;
}
.controls {
flex-direction: column;
}
.scenario-btn {
width: 100%;
}
}
</style>