feat: update docs and components, fix DLQ demo bug
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user