feat(appendix): 添加多个交互式演示组件,完善 AI/Infra 等章节内容
- 新增 Vibe Coding 全栈相关演示组件 (DeveloperSkillShift, FrontendTriad, BackendCore 等) - 新增 RAG 相关组件 (RAGPipeline, ChunkingStrategy, Retrieval 等) - 新增 Embedding & Vector 相关组件 (EmbeddingConcept, VectorSimilarity 等) - 新增 AI Native App 设计组件 (AINativeArch, PromptDesign 等) - 新增 Infrastructure as Code 组件 (IaCConcept, TerraformWorkflow 等) - 新增 DNS & HTTPS 演示组件 (DnsResolution, HttpsHandshake 等) - 新增 Model Finetuning 组件 (FinetuningPipeline 等) - 更新多个章节的 markdown 内容,集成交互式演示
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
<!--
|
||||
AlertEscalationDemo.vue
|
||||
告警升级流程演示:展示告警如何根据严重程度和时间逐级升级
|
||||
-->
|
||||
<template>
|
||||
<div class="alert-escalation-demo">
|
||||
<div class="header">
|
||||
<div class="title">告警升级流程 (Alert Escalation)</div>
|
||||
<div class="subtitle">选择一个场景,观察告警如何逐级升级</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-select">
|
||||
<button
|
||||
v-for="s in scenarios"
|
||||
:key="s.id"
|
||||
:class="['scenario-btn', { active: activeScenario === s.id }]"
|
||||
@click="startScenario(s.id)"
|
||||
>
|
||||
{{ s.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="escalation-flow">
|
||||
<div
|
||||
v-for="(step, index) in escalationSteps"
|
||||
:key="step.id"
|
||||
:class="[
|
||||
'esc-step',
|
||||
{
|
||||
active: currentStep === index,
|
||||
completed: currentStep > index,
|
||||
pending: currentStep < index
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div class="esc-left">
|
||||
<div class="esc-icon" :style="{ background: step.color }">
|
||||
{{ step.icon }}
|
||||
</div>
|
||||
<div v-if="index < escalationSteps.length - 1" class="esc-line">
|
||||
<div
|
||||
class="esc-line-fill"
|
||||
:class="{ filled: currentStep > index }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="esc-content">
|
||||
<div class="esc-header">
|
||||
<span class="esc-title">{{ step.title }}</span>
|
||||
<span class="esc-time">{{ step.time }}</span>
|
||||
</div>
|
||||
<div class="esc-desc">{{ step.desc }}</div>
|
||||
<div v-if="step.action && currentStep >= index" class="esc-action">
|
||||
{{ step.action }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeScenario" class="timer-bar">
|
||||
<div class="timer-label">
|
||||
升级进度:第 {{ currentStep + 1 }} / {{ escalationSteps.length }} 级
|
||||
</div>
|
||||
<div class="timer-track">
|
||||
<div
|
||||
class="timer-fill"
|
||||
:style="{
|
||||
width: ((currentStep + 1) / escalationSteps.length) * 100 + '%'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="timer-controls">
|
||||
<button
|
||||
class="ctrl-btn"
|
||||
@click="prevStep"
|
||||
:disabled="currentStep <= 0"
|
||||
>
|
||||
上一级
|
||||
</button>
|
||||
<button
|
||||
class="ctrl-btn"
|
||||
@click="nextStep"
|
||||
:disabled="currentStep >= escalationSteps.length - 1"
|
||||
>
|
||||
下一级升级
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-box">
|
||||
<div class="rule-title">升级规则说明</div>
|
||||
<div class="rules">
|
||||
<div class="rule-item">
|
||||
<span class="rule-dot" style="background: #22c55e"></span>
|
||||
<span>P3/P4 告警:仅通知值班工程师,无需升级</span>
|
||||
</div>
|
||||
<div class="rule-item">
|
||||
<span class="rule-dot" style="background: #eab308"></span>
|
||||
<span>P2 告警:15 分钟未响应则升级至团队负责人</span>
|
||||
</div>
|
||||
<div class="rule-item">
|
||||
<span class="rule-dot" style="background: #f59e0b"></span>
|
||||
<span>P1 告警:5 分钟未响应升级,30 分钟未解决升级至总监</span>
|
||||
</div>
|
||||
<div class="rule-item">
|
||||
<span class="rule-dot" style="background: #ef4444"></span>
|
||||
<span>P0 告警:立即通知全链路,15 分钟未缓解升级至 VP/CTO</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeScenario = ref(null)
|
||||
const currentStep = ref(0)
|
||||
|
||||
const scenarios = [
|
||||
{ id: 'p0', name: 'P0 数据库宕机' },
|
||||
{ id: 'p1', name: 'P1 接口超时' },
|
||||
{ id: 'p2', name: 'P2 性能下降' }
|
||||
]
|
||||
|
||||
const scenarioSteps = {
|
||||
p0: [
|
||||
{
|
||||
id: 1,
|
||||
icon: '📡',
|
||||
color: '#3b82f6',
|
||||
title: '监控系统检测',
|
||||
time: 'T+0s',
|
||||
desc: 'Prometheus 检测到数据库连接池耗尽,所有查询超时',
|
||||
action: '自动触发 P0 级别告警'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: '📱',
|
||||
color: '#f59e0b',
|
||||
title: '值班工程师',
|
||||
time: 'T+30s',
|
||||
desc: '电话 + 短信 + 即时通讯同时通知值班 DBA',
|
||||
action: '值班工程师确认告警,开始排查'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: '👥',
|
||||
color: '#ef4444',
|
||||
title: '团队负责人',
|
||||
time: 'T+5min',
|
||||
desc: '自动升级至数据库团队负责人和后端团队负责人',
|
||||
action: '团队负责人召集紧急会议'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: '🎖️',
|
||||
color: '#8b5cf6',
|
||||
title: '技术总监',
|
||||
time: 'T+15min',
|
||||
desc: '问题未缓解,自动升级至技术总监',
|
||||
action: '总监协调跨团队资源,启动应急预案'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: '🏢',
|
||||
color: '#1e293b',
|
||||
title: 'VP / CTO',
|
||||
time: 'T+30min',
|
||||
desc: '重大事故升级至高管层,准备对外沟通',
|
||||
action: 'CTO 决策是否启动灾备切换'
|
||||
}
|
||||
],
|
||||
p1: [
|
||||
{
|
||||
id: 1,
|
||||
icon: '📡',
|
||||
color: '#3b82f6',
|
||||
title: '监控系统检测',
|
||||
time: 'T+0s',
|
||||
desc: 'API 网关检测到 P99 延迟超过 3 秒阈值',
|
||||
action: '触发 P1 级别告警'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: '📱',
|
||||
color: '#f59e0b',
|
||||
title: '值班工程师',
|
||||
time: 'T+1min',
|
||||
desc: '即时通讯 + 短信通知值班后端工程师',
|
||||
action: '工程师开始查看监控面板和日志'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: '👥',
|
||||
color: '#ef4444',
|
||||
title: '团队负责人',
|
||||
time: 'T+15min',
|
||||
desc: '15 分钟未解决,自动升级至团队负责人',
|
||||
action: '负责人评估是否需要更多人力支援'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: '🎖️',
|
||||
color: '#8b5cf6',
|
||||
title: '技术总监',
|
||||
time: 'T+30min',
|
||||
desc: '30 分钟未缓解,升级至技术总监',
|
||||
action: '总监决定是否升级为 P0'
|
||||
}
|
||||
],
|
||||
p2: [
|
||||
{
|
||||
id: 1,
|
||||
icon: '📡',
|
||||
color: '#3b82f6',
|
||||
title: '监控系统检测',
|
||||
time: 'T+0s',
|
||||
desc: '检测到页面加载时间从 1.2s 上升到 2.8s',
|
||||
action: '触发 P2 级别告警'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: '📱',
|
||||
color: '#eab308',
|
||||
title: '值班工程师',
|
||||
time: 'T+5min',
|
||||
desc: '即时通讯通知值班前端工程师',
|
||||
action: '工程师确认问题,记录工单'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: '👥',
|
||||
color: '#f59e0b',
|
||||
title: '团队负责人',
|
||||
time: 'T+30min',
|
||||
desc: '30 分钟未响应时升级至团队负责人',
|
||||
action: '负责人安排当天修复'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const escalationSteps = computed(() => {
|
||||
if (!activeScenario.value) return scenarioSteps.p0
|
||||
return scenarioSteps[activeScenario.value]
|
||||
})
|
||||
|
||||
const startScenario = (id) => {
|
||||
activeScenario.value = id
|
||||
currentStep.value = 0
|
||||
}
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep.value < escalationSteps.value.length - 1) {
|
||||
currentStep.value++
|
||||
}
|
||||
}
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert-escalation-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
.scenario-select {
|
||||
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); }
|
||||
|
||||
.escalation-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.esc-step {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
opacity: 0.4;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.esc-step.active,
|
||||
.esc-step.completed { opacity: 1; }
|
||||
|
||||
.esc-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.esc-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
color: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.esc-line {
|
||||
width: 3px;
|
||||
flex: 1;
|
||||
min-height: 20px;
|
||||
background: var(--vp-c-divider);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.esc-line-fill {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
background: var(--vp-c-brand);
|
||||
transition: height 0.5s;
|
||||
}
|
||||
|
||||
.esc-line-fill.filled { height: 100%; }
|
||||
|
||||
.esc-content {
|
||||
padding-bottom: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.esc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.esc-title { font-weight: 600; font-size: 0.95rem; }
|
||||
.esc-time { font-size: 0.8rem; color: var(--vp-c-text-3); font-family: monospace; }
|
||||
.esc-desc { font-size: 0.85rem; color: var(--vp-c-text-2); margin-bottom: 0.3rem; }
|
||||
|
||||
.esc-action {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.08);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.timer-bar {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.timer-label { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
|
||||
.timer-track {
|
||||
height: 6px;
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timer-fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.timer-controls { display: flex; gap: 0.5rem; }
|
||||
|
||||
.ctrl-btn {
|
||||
padding: 0.4rem 0.8rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.rule-box {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.rule-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.75rem; }
|
||||
.rules { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
|
||||
.rule-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.rule-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.scenario-select { flex-direction: column; }
|
||||
.scenario-btn { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,356 @@
|
||||
<!--
|
||||
IncidentCommandDemo.vue
|
||||
事故指挥体系演示:展示事故响应中的角色分工和协作关系
|
||||
-->
|
||||
<template>
|
||||
<div class="incident-command-demo">
|
||||
<div class="header">
|
||||
<div class="title">事故指挥体系 (Incident Command System)</div>
|
||||
<div class="subtitle">点击角色卡片,了解各角色的职责和协作关系</div>
|
||||
</div>
|
||||
|
||||
<div class="org-chart">
|
||||
<div class="org-level org-top">
|
||||
<div
|
||||
:class="['role-card', 'commander', { active: activeRole === 'ic' }]"
|
||||
@click="selectRole('ic')"
|
||||
>
|
||||
<div class="role-icon">🎖️</div>
|
||||
<div class="role-name">事故指挥官</div>
|
||||
<div class="role-eng">Incident Commander</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="org-connector">
|
||||
<div class="connector-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="org-level org-middle">
|
||||
<div
|
||||
v-for="role in middleRoles"
|
||||
:key="role.id"
|
||||
:class="['role-card', { active: activeRole === role.id }]"
|
||||
@click="selectRole(role.id)"
|
||||
>
|
||||
<div class="role-icon">{{ role.icon }}</div>
|
||||
<div class="role-name">{{ role.name }}</div>
|
||||
<div class="role-eng">{{ role.eng }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentRole" class="role-detail">
|
||||
<div class="detail-header" :style="{ background: currentRole.color }">
|
||||
<span class="detail-icon">{{ currentRole.icon }}</span>
|
||||
<span class="detail-name">{{ currentRole.name }}</span>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div class="detail-section">
|
||||
<div class="section-label">核心职责</div>
|
||||
<div class="responsibilities">
|
||||
<div
|
||||
v-for="(r, i) in currentRole.responsibilities"
|
||||
:key="i"
|
||||
class="resp-item"
|
||||
>
|
||||
<span class="resp-num">{{ i + 1 }}</span>
|
||||
<span>{{ r }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-label">关键能力</div>
|
||||
<div class="skills">
|
||||
<span
|
||||
v-for="skill in currentRole.skills"
|
||||
:key="skill"
|
||||
class="skill-tag"
|
||||
>
|
||||
{{ skill }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-label">常见话术</div>
|
||||
<div class="quote-box">
|
||||
"{{ currentRole.quote }}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-box">
|
||||
<div class="scenario-title">模拟场景:支付系统 P0 事故</div>
|
||||
<div class="scenario-timeline">
|
||||
<div
|
||||
v-for="(event, i) in scenarioEvents"
|
||||
:key="i"
|
||||
class="event-item"
|
||||
>
|
||||
<span class="event-time">{{ event.time }}</span>
|
||||
<span
|
||||
class="event-role"
|
||||
:style="{ background: event.color }"
|
||||
>
|
||||
{{ event.role }}
|
||||
</span>
|
||||
<span class="event-text">{{ event.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeRole = ref('ic')
|
||||
|
||||
const allRoles = {
|
||||
ic: {
|
||||
id: 'ic',
|
||||
icon: '🎖️',
|
||||
name: '事故指挥官',
|
||||
eng: 'Incident Commander',
|
||||
color: '#8b5cf6',
|
||||
responsibilities: [
|
||||
'统筹协调整个事故响应过程',
|
||||
'做出关键决策(回滚、切流、降级等)',
|
||||
'确保各角色高效协作,避免混乱',
|
||||
'控制事故响应节奏,定时同步进展'
|
||||
],
|
||||
skills: ['全局视野', '决策能力', '沟通协调', '压力管理'],
|
||||
quote: '当前状态:支付服务不可用。运维组排查数据库,后端组准备回滚方案,通讯组每 10 分钟同步一次。'
|
||||
},
|
||||
comm: {
|
||||
id: 'comm',
|
||||
icon: '📢',
|
||||
name: '通讯协调员',
|
||||
eng: 'Communications Lead',
|
||||
color: '#3b82f6',
|
||||
responsibilities: [
|
||||
'对内:定时向管理层和相关团队通报进展',
|
||||
'对外:更新状态页面,通知受影响客户',
|
||||
'记录事故时间线,为复盘提供素材',
|
||||
'过滤噪音信息,确保指挥官专注决策'
|
||||
],
|
||||
skills: ['文字表达', '信息整理', '多方沟通', '时间管理'],
|
||||
quote: '状态更新:我们已识别到支付服务异常,团队正在紧急处理中,预计 30 分钟内恢复。'
|
||||
},
|
||||
ops: {
|
||||
id: 'ops',
|
||||
icon: '🔧',
|
||||
name: '运维负责人',
|
||||
eng: 'Operations Lead',
|
||||
color: '#ef4444',
|
||||
responsibilities: [
|
||||
'执行具体的技术操作(回滚、重启、扩容等)',
|
||||
'监控系统指标变化,判断操作效果',
|
||||
'管理基础设施层面的应急响应',
|
||||
'向指挥官汇报技术层面的进展'
|
||||
],
|
||||
skills: ['系统运维', '故障排查', '脚本自动化', '监控分析'],
|
||||
quote: '数据库主节点 CPU 100%,正在执行主从切换,预计 2 分钟完成。'
|
||||
},
|
||||
dev: {
|
||||
id: 'dev',
|
||||
icon: '💻',
|
||||
name: '开发负责人',
|
||||
eng: 'Development Lead',
|
||||
color: '#22c55e',
|
||||
responsibilities: [
|
||||
'分析代码层面的问题根因',
|
||||
'准备和执行代码级别的修复或回滚',
|
||||
'评估变更风险,提供技术方案',
|
||||
'协调开发团队成员参与排查'
|
||||
],
|
||||
skills: ['代码分析', '快速调试', '风险评估', '版本管理'],
|
||||
quote: '定位到问题:昨天上线的批量查询没有加分页,导致全表扫描拖垮数据库。准备回滚到上一版本。'
|
||||
}
|
||||
}
|
||||
|
||||
const middleRoles = [
|
||||
allRoles.comm,
|
||||
allRoles.ops,
|
||||
allRoles.dev
|
||||
]
|
||||
|
||||
const currentRole = computed(() => {
|
||||
return allRoles[activeRole.value] || null
|
||||
})
|
||||
|
||||
const selectRole = (id) => {
|
||||
activeRole.value = id
|
||||
}
|
||||
|
||||
const scenarioEvents = [
|
||||
{ time: '14:02', role: '监控', color: '#3b82f6', text: '支付成功率从 99.9% 骤降至 12%,触发 P0 告警' },
|
||||
{ time: '14:03', role: '指挥官', color: '#8b5cf6', text: '确认 P0 事故,开启事故频道,召集各角色' },
|
||||
{ time: '14:05', role: '通讯', color: '#3b82f6', text: '通知管理层,更新状态页为"服务降级"' },
|
||||
{ time: '14:08', role: '运维', color: '#ef4444', text: '发现数据库主节点 CPU 100%,连接池耗尽' },
|
||||
{ time: '14:10', role: '开发', color: '#22c55e', text: '定位到昨日上线的慢查询是根因' },
|
||||
{ time: '14:12', role: '指挥官', color: '#8b5cf6', text: '决策:立即回滚昨日变更 + 数据库主从切换' },
|
||||
{ time: '14:15', role: '运维', color: '#ef4444', text: '数据库主从切换完成,连接恢复' },
|
||||
{ time: '14:18', role: '开发', color: '#22c55e', text: '代码回滚部署完成' },
|
||||
{ time: '14:20', role: '通讯', color: '#3b82f6', text: '支付成功率恢复至 99.8%,通知各方服务恢复' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.incident-command-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
.org-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.org-level { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
|
||||
|
||||
.org-connector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.connector-line {
|
||||
width: 2px;
|
||||
height: 24px;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.role-card {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.role-card:hover { border-color: var(--vp-c-brand); transform: translateY(-2px); }
|
||||
.role-card.active { border-color: var(--vp-c-brand); box-shadow: 0 2px 12px rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.15); }
|
||||
.role-card.commander { border-width: 3px; }
|
||||
|
||||
.role-icon { font-size: 1.5rem; margin-bottom: 0.25rem; }
|
||||
.role-name { font-weight: 600; font-size: 0.9rem; }
|
||||
.role-eng { font-size: 0.75rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.role-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.detail-icon { font-size: 1.3rem; }
|
||||
.detail-name { font-weight: 700; font-size: 1rem; }
|
||||
|
||||
.detail-body { padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.detail-section { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.section-label { font-weight: 600; font-size: 0.85rem; color: var(--vp-c-text-2); }
|
||||
|
||||
.responsibilities { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
|
||||
.resp-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.resp-num {
|
||||
width: 20px; height: 20px; border-radius: 50%;
|
||||
background: var(--vp-c-bg-soft);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.7rem; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skills { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
||||
|
||||
.skill-tag {
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.quote-box {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.6rem 0.8rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
font-style: italic;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.scenario-box {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.scenario-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.75rem; }
|
||||
|
||||
.scenario-timeline { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
|
||||
.event-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.82rem;
|
||||
padding: 0.3rem 0;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.event-item:last-child { border-bottom: none; }
|
||||
|
||||
.event-time {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-3);
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.event-role {
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.event-text { color: var(--vp-c-text-1); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.org-level { flex-direction: column; align-items: center; }
|
||||
.event-item { flex-wrap: wrap; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,471 @@
|
||||
<!--
|
||||
IncidentTimelineDemo.vue
|
||||
事故响应时间线演示:展示从发现到复盘的完整事故响应流程
|
||||
-->
|
||||
<template>
|
||||
<div class="incident-timeline-demo">
|
||||
<div class="header">
|
||||
<div class="title">事故响应时间线 (Incident Timeline)</div>
|
||||
<div class="subtitle">点击各阶段,了解每个环节的关键动作</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="timeline-track">
|
||||
<div
|
||||
class="timeline-progress"
|
||||
:style="{ width: progressWidth }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="timeline-nodes">
|
||||
<div
|
||||
v-for="(phase, index) in phases"
|
||||
:key="phase.id"
|
||||
:class="[
|
||||
'timeline-node',
|
||||
{
|
||||
active: activePhase === phase.id,
|
||||
completed: completedPhases.includes(phase.id)
|
||||
}
|
||||
]"
|
||||
@click="selectPhase(phase.id)"
|
||||
>
|
||||
<div class="node-dot">
|
||||
<span v-if="completedPhases.includes(phase.id)">✓</span>
|
||||
<span v-else>{{ index + 1 }}</span>
|
||||
</div>
|
||||
<div class="node-label">{{ phase.name }}</div>
|
||||
<div class="node-time">{{ phase.timeHint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentPhase" class="phase-detail">
|
||||
<div class="phase-header" :style="{ background: currentPhase.color }">
|
||||
<span class="phase-icon">{{ currentPhase.icon }}</span>
|
||||
<span class="phase-name">{{ currentPhase.name }}</span>
|
||||
<span class="phase-duration">{{ currentPhase.duration }}</span>
|
||||
</div>
|
||||
<div class="phase-body">
|
||||
<div class="phase-desc">{{ currentPhase.description }}</div>
|
||||
<div class="phase-actions">
|
||||
<div class="actions-title">关键动作:</div>
|
||||
<div
|
||||
v-for="(action, i) in currentPhase.actions"
|
||||
:key="i"
|
||||
class="action-item"
|
||||
>
|
||||
<span class="action-bullet">{{ i + 1 }}</span>
|
||||
<span>{{ action }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="phase-roles">
|
||||
<span class="roles-label">参与角色:</span>
|
||||
<span
|
||||
v-for="role in currentPhase.roles"
|
||||
:key="role"
|
||||
class="role-tag"
|
||||
>
|
||||
{{ role }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auto-controls">
|
||||
<button class="play-btn" @click="autoPlay" :disabled="isPlaying">
|
||||
{{ isPlaying ? '播放中...' : '自动演示完整流程' }}
|
||||
</button>
|
||||
<button class="reset-btn" @click="resetAll">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activePhase = ref(null)
|
||||
const completedPhases = ref([])
|
||||
const isPlaying = ref(false)
|
||||
|
||||
const phases = [
|
||||
{
|
||||
id: 'detect',
|
||||
name: '发现',
|
||||
timeHint: 'T+0',
|
||||
icon: '🔍',
|
||||
color: '#ef4444',
|
||||
duration: '目标 < 5 分钟',
|
||||
description:
|
||||
'通过监控告警、用户反馈或自动化检测发现系统异常。越早发现,损失越小。',
|
||||
actions: [
|
||||
'监控系统触发告警(CPU、延迟、错误率等)',
|
||||
'值班人员收到通知并确认',
|
||||
'初步判断影响范围',
|
||||
'在事故频道发出第一条通报'
|
||||
],
|
||||
roles: ['值班工程师', '监控系统']
|
||||
},
|
||||
{
|
||||
id: 'triage',
|
||||
name: '分级',
|
||||
timeHint: 'T+5min',
|
||||
icon: '📋',
|
||||
color: '#f59e0b',
|
||||
duration: '目标 < 10 分钟',
|
||||
description:
|
||||
'快速评估事故严重程度,确定优先级(P0-P4),决定响应规模和升级路径。',
|
||||
actions: [
|
||||
'评估用户影响面(多少用户受影响?)',
|
||||
'确定业务影响(核心功能是否不可用?)',
|
||||
'分配事故等级(P0/P1/P2/P3/P4)',
|
||||
'根据等级启动对应的响应流程'
|
||||
],
|
||||
roles: ['值班工程师', '事故指挥官']
|
||||
},
|
||||
{
|
||||
id: 'mitigate',
|
||||
name: '止血',
|
||||
timeHint: 'T+15min',
|
||||
icon: '🚑',
|
||||
color: '#3b82f6',
|
||||
duration: '目标 < 1 小时',
|
||||
description:
|
||||
'采取紧急措施恢复服务,优先止血而非根治。回滚、降级、限流都是常见手段。',
|
||||
actions: [
|
||||
'回滚最近的变更(代码、配置、基础设施)',
|
||||
'启用降级方案或备用系统',
|
||||
'实施限流保护核心链路',
|
||||
'持续监控恢复进度并通报状态'
|
||||
],
|
||||
roles: ['事故指挥官', '运维工程师', '开发工程师']
|
||||
},
|
||||
{
|
||||
id: 'resolve',
|
||||
name: '解决',
|
||||
timeHint: 'T+1h',
|
||||
icon: '🔧',
|
||||
color: '#22c55e',
|
||||
duration: '视复杂度而定',
|
||||
description:
|
||||
'在服务恢复后,定位根本原因并实施永久修复,确保同类问题不再发生。',
|
||||
actions: [
|
||||
'深入分析日志、监控数据定位根因',
|
||||
'编写并审核修复代码',
|
||||
'在预发布环境验证修复效果',
|
||||
'灰度发布修复,确认问题彻底解决'
|
||||
],
|
||||
roles: ['开发工程师', '架构师', 'QA 工程师']
|
||||
},
|
||||
{
|
||||
id: 'postmortem',
|
||||
name: '复盘',
|
||||
timeHint: 'T+48h',
|
||||
icon: '📝',
|
||||
color: '#8b5cf6',
|
||||
duration: '事故后 48 小时内',
|
||||
description:
|
||||
'召开无责复盘会议,分析根因,提炼经验教训,制定改进措施防止再次发生。',
|
||||
actions: [
|
||||
'撰写事故复盘报告(时间线、影响、根因)',
|
||||
'召开复盘会议,全员参与讨论',
|
||||
'使用"五个为什么"深挖根本原因',
|
||||
'制定并跟踪改进行动项(Action Items)'
|
||||
],
|
||||
roles: ['事故指挥官', '全体相关人员', '管理层']
|
||||
}
|
||||
]
|
||||
|
||||
const currentPhase = computed(() => {
|
||||
if (!activePhase.value) return null
|
||||
return phases.find((p) => p.id === activePhase.value)
|
||||
})
|
||||
|
||||
const progressWidth = computed(() => {
|
||||
if (completedPhases.value.length === 0 && !activePhase.value) return '0%'
|
||||
const activeIndex = phases.findIndex((p) => p.id === activePhase.value)
|
||||
if (activeIndex === -1) {
|
||||
const lastCompleted = completedPhases.value.length
|
||||
return `${(lastCompleted / phases.length) * 100}%`
|
||||
}
|
||||
return `${((activeIndex + 0.5) / phases.length) * 100}%`
|
||||
})
|
||||
|
||||
const selectPhase = (id) => {
|
||||
activePhase.value = id
|
||||
}
|
||||
|
||||
const autoPlay = async () => {
|
||||
isPlaying.value = true
|
||||
completedPhases.value = []
|
||||
activePhase.value = null
|
||||
|
||||
for (let i = 0; i < phases.length; i++) {
|
||||
activePhase.value = phases[i].id
|
||||
await new Promise((r) => setTimeout(r, 1800))
|
||||
completedPhases.value.push(phases[i].id)
|
||||
}
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
const resetAll = () => {
|
||||
activePhase.value = null
|
||||
completedPhases.value = []
|
||||
isPlaying.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.incident-timeline-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.timeline-track {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 5%;
|
||||
right: 5%;
|
||||
height: 4px;
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.timeline-progress {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 2px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.timeline-nodes {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.node-dot {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg);
|
||||
border: 3px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.3s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-node.active .node-dot {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 0 0 4px rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.2);
|
||||
}
|
||||
|
||||
.timeline-node.completed .node-dot {
|
||||
border-color: #22c55e;
|
||||
background: #22c55e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.phase-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.phase-header {
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.phase-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.phase-name {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.phase-duration {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.phase-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.phase-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.actions-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.action-bullet {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg-soft);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.phase-roles {
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.roles-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.role-tag {
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auto-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.play-btn,
|
||||
.reset-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.play-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.timeline-nodes {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.timeline-track {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-node {
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.node-time {
|
||||
margin-top: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,412 @@
|
||||
<!--
|
||||
PostmortemDemo.vue
|
||||
事后复盘演示:交互式展示"五个为什么"分析法和复盘报告模板
|
||||
-->
|
||||
<template>
|
||||
<div class="postmortem-demo">
|
||||
<div class="header">
|
||||
<div class="title">事后复盘:五个为什么 (5 Whys Analysis)</div>
|
||||
<div class="subtitle">点击"继续追问",层层深入挖掘根本原因</div>
|
||||
</div>
|
||||
|
||||
<div class="case-select">
|
||||
<button
|
||||
v-for="c in cases"
|
||||
:key="c.id"
|
||||
:class="['case-btn', { active: activeCase === c.id }]"
|
||||
@click="selectCase(c.id)"
|
||||
>
|
||||
{{ c.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="currentCase" class="whys-chain">
|
||||
<div
|
||||
v-for="(why, index) in visibleWhys"
|
||||
:key="index"
|
||||
class="why-item"
|
||||
>
|
||||
<div class="why-header">
|
||||
<span class="why-badge">
|
||||
{{ index === 0 ? '现象' : '第 ' + index + ' 个为什么' }}
|
||||
</span>
|
||||
<span class="why-depth">
|
||||
深度 {{ index }} / {{ currentCase.whys.length - 1 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="why-question" v-if="index > 0">
|
||||
为什么{{ currentCase.whys[index - 1].answer }}?
|
||||
</div>
|
||||
<div class="why-answer">
|
||||
<span class="answer-icon">{{ index === currentCase.whys.length - 1 && revealedCount >= currentCase.whys.length ? '🎯' : '💡' }}</span>
|
||||
<span>{{ why.answer }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="index < visibleWhys.length - 1"
|
||||
class="why-arrow"
|
||||
>
|
||||
↓ 继续追问
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="why-controls" v-if="revealedCount < currentCase.whys.length">
|
||||
<button class="ask-btn" @click="revealNext">
|
||||
继续追问:为什么?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="root-cause-box">
|
||||
<div class="root-label">根本原因已找到</div>
|
||||
<div class="root-content">{{ currentCase.rootCause }}</div>
|
||||
<div class="root-actions">
|
||||
<div class="actions-label">改进措施:</div>
|
||||
<div
|
||||
v-for="(action, i) in currentCase.actions"
|
||||
:key="i"
|
||||
class="action-item"
|
||||
>
|
||||
<span class="action-check">✓</span>
|
||||
<span>{{ action }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="template-box">
|
||||
<div class="template-title">复盘报告模板</div>
|
||||
<div class="template-sections">
|
||||
<div
|
||||
v-for="(section, i) in templateSections"
|
||||
:key="i"
|
||||
class="template-item"
|
||||
:class="{ expanded: expandedSection === i }"
|
||||
@click="expandedSection = expandedSection === i ? -1 : i"
|
||||
>
|
||||
<div class="template-item-header">
|
||||
<span class="template-num">{{ i + 1 }}</span>
|
||||
<span class="template-name">{{ section.name }}</span>
|
||||
<span class="template-toggle">
|
||||
{{ expandedSection === i ? '−' : '+' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="expandedSection === i" class="template-item-body">
|
||||
{{ section.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeCase = ref('payment')
|
||||
const revealedCount = ref(1)
|
||||
const expandedSection = ref(-1)
|
||||
|
||||
const casesData = {
|
||||
payment: {
|
||||
id: 'payment',
|
||||
name: '支付系统宕机',
|
||||
whys: [
|
||||
{ answer: '支付系统在高峰期完全不可用,持续 18 分钟' },
|
||||
{ answer: '数据库连接池被耗尽,所有新请求排队超时' },
|
||||
{ answer: '一条慢查询占用连接长达 30 秒不释放' },
|
||||
{ answer: '新上线的对账功能执行了全表扫描,没有使用索引' },
|
||||
{ answer: '代码审查时没有检查 SQL 执行计划,也没有慢查询测试环节' }
|
||||
],
|
||||
rootCause: '研发流程缺陷:代码审查清单中缺少 SQL 性能审查项,CI/CD 流水线中没有慢查询检测环节。',
|
||||
actions: [
|
||||
'代码审查清单增加"SQL 执行计划检查"必选项',
|
||||
'CI 流水线增加慢查询自动检测(阈值 100ms)',
|
||||
'数据库连接池增加单查询超时限制(5s 强制断开)',
|
||||
'建立大表变更审批流程'
|
||||
]
|
||||
},
|
||||
deploy: {
|
||||
id: 'deploy',
|
||||
name: '部署导致服务中断',
|
||||
whys: [
|
||||
{ answer: '新版本部署后,用户登录功能完全失效,持续 25 分钟' },
|
||||
{ answer: '新版本的认证服务无法连接 Redis 缓存集群' },
|
||||
{ answer: '部署脚本使用了错误的 Redis 集群地址(指向了测试环境)' },
|
||||
{ answer: '环境配置是硬编码在部署脚本中的,没有使用配置中心' },
|
||||
{ answer: '团队没有统一的配置管理规范,每个服务自行管理配置' }
|
||||
],
|
||||
rootCause: '基础设施缺陷:缺乏统一的配置管理平台和规范,环境配置散落在各处,容易出错且难以审计。',
|
||||
actions: [
|
||||
'引入配置中心(如 Consul/Nacos),统一管理所有环境配置',
|
||||
'部署流水线增加配置校验步骤(连通性检查)',
|
||||
'禁止在代码和脚本中硬编码环境地址',
|
||||
'建立部署前 Checklist,包含配置确认环节'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{ id: 'payment', name: '支付系统宕机' },
|
||||
{ id: 'deploy', name: '部署导致服务中断' }
|
||||
]
|
||||
|
||||
const currentCase = computed(() => casesData[activeCase.value] || null)
|
||||
|
||||
const visibleWhys = computed(() => {
|
||||
if (!currentCase.value) return []
|
||||
return currentCase.value.whys.slice(0, revealedCount.value)
|
||||
})
|
||||
|
||||
const selectCase = (id) => {
|
||||
activeCase.value = id
|
||||
revealedCount.value = 1
|
||||
}
|
||||
|
||||
const revealNext = () => {
|
||||
if (currentCase.value && revealedCount.value < currentCase.value.whys.length) {
|
||||
revealedCount.value++
|
||||
}
|
||||
}
|
||||
|
||||
const templateSections = [
|
||||
{ name: '事故概述', desc: '简要描述事故发生的时间、持续时长、影响范围和严重程度。例如:"2024年3月15日 14:02-14:20,支付服务完全不可用,影响约 12 万笔交易。"' },
|
||||
{ name: '时间线', desc: '按时间顺序记录从发现到解决的每一个关键事件,精确到分钟。包括:告警触发、人员响应、排查过程、修复操作、服务恢复等。' },
|
||||
{ name: '影响评估', desc: '量化事故影响:受影响用户数、失败请求数、经济损失估算、SLA 影响等。用数据说话,避免模糊描述。' },
|
||||
{ name: '根因分析', desc: '使用"五个为什么"等方法深入分析根本原因。区分直接原因(触发因素)和根本原因(系统性缺陷)。' },
|
||||
{ name: '改进措施', desc: '列出具体的改进行动项,每项必须有负责人和截止日期。分为短期(本周)、中期(本月)、长期(本季度)三个层次。' },
|
||||
{ name: '经验教训', desc: '总结哪些做得好(值得保持)、哪些做得不好(需要改进)、哪些是意外发现(新的风险点)。' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.postmortem-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
.case-select {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.case-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;
|
||||
}
|
||||
|
||||
.case-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.case-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.whys-chain {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.why-item {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.why-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.why-badge {
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.why-depth {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.why-question {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-style: italic;
|
||||
margin-bottom: 0.3rem;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.why-answer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.answer-icon { flex-shrink: 0; }
|
||||
|
||||
.why-arrow {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.why-controls {
|
||||
text-align: center;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.ask-btn {
|
||||
padding: 0.6rem 1.5rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.ask-btn:hover { opacity: 0.9; transform: translateY(-1px); }
|
||||
|
||||
.root-cause-box {
|
||||
background: rgba(34, 197, 94, 0.08);
|
||||
border: 2px solid #22c55e;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.root-label {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
color: #22c55e;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.root-content {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.actions-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.action-check {
|
||||
color: #22c55e;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.template-box {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.template-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.template-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.template-item.expanded {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.template-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.template-num {
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
background: var(--vp-c-bg-soft);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.75rem; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.template-toggle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.template-item-body {
|
||||
padding: 0 0.75rem 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.case-select { flex-direction: column; }
|
||||
.case-btn { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,483 @@
|
||||
<!--
|
||||
SeverityLevelDemo.vue
|
||||
事故严重程度分级演示:交互式展示 P0-P4 各级别的定义、示例和响应要求
|
||||
-->
|
||||
<template>
|
||||
<div class="severity-level-demo">
|
||||
<div class="header">
|
||||
<div class="title">事故严重程度分级 (Severity Levels)</div>
|
||||
<div class="subtitle">点击各级别,了解对应的响应要求和真实案例</div>
|
||||
</div>
|
||||
|
||||
<div class="level-tabs">
|
||||
<button
|
||||
v-for="level in levels"
|
||||
:key="level.id"
|
||||
:class="['level-tab', level.id, { active: activeLevel === level.id }]"
|
||||
@click="activeLevel = level.id"
|
||||
>
|
||||
<span class="tab-badge">{{ level.id.toUpperCase() }}</span>
|
||||
<span class="tab-name">{{ level.shortName }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="level-detail">
|
||||
<div class="detail-header" :style="{ background: current.color }">
|
||||
<div class="detail-level">{{ current.id.toUpperCase() }}</div>
|
||||
<div class="detail-name">{{ current.name }}</div>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div class="detail-section">
|
||||
<div class="section-label">定义</div>
|
||||
<div class="section-content">{{ current.definition }}</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-label">响应时间</div>
|
||||
<div class="section-content response-time">
|
||||
{{ current.responseTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-label">通知方式</div>
|
||||
<div class="channels">
|
||||
<span
|
||||
v-for="ch in current.channels"
|
||||
:key="ch"
|
||||
class="channel-tag"
|
||||
>
|
||||
{{ ch }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-label">真实案例</div>
|
||||
<div class="examples">
|
||||
<div
|
||||
v-for="(ex, i) in current.examples"
|
||||
:key="i"
|
||||
class="example-item"
|
||||
>
|
||||
{{ ex }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-label">响应要求</div>
|
||||
<div class="requirements">
|
||||
<div
|
||||
v-for="(req, i) in current.requirements"
|
||||
:key="i"
|
||||
class="req-item"
|
||||
>
|
||||
<span class="req-check">✓</span>
|
||||
<span>{{ req }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<div class="table-title">各级别对比一览</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>级别</th>
|
||||
<th>用户影响</th>
|
||||
<th>响应时间</th>
|
||||
<th>值班要求</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="level in levels"
|
||||
:key="level.id"
|
||||
:class="{ highlight: activeLevel === level.id }"
|
||||
@click="activeLevel = level.id"
|
||||
>
|
||||
<td>
|
||||
<span class="table-badge" :class="level.id">
|
||||
{{ level.id.toUpperCase() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ level.userImpact }}</td>
|
||||
<td>{{ level.responseTime }}</td>
|
||||
<td>{{ level.oncallReq }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeLevel = ref('p0')
|
||||
|
||||
const levels = [
|
||||
{
|
||||
id: 'p0',
|
||||
shortName: '致命',
|
||||
name: '致命事故 (Critical)',
|
||||
color: '#ef4444',
|
||||
definition: '核心业务完全不可用,大面积用户受影响,造成严重经济损失或数据丢失风险。',
|
||||
responseTime: '立即响应,5 分钟内到位',
|
||||
userImpact: '全部用户',
|
||||
oncallReq: '全员到位',
|
||||
channels: ['电话', '短信', '即时通讯', '邮件'],
|
||||
examples: [
|
||||
'主数据库宕机,所有读写请求失败',
|
||||
'支付系统完全不可用,用户无法下单',
|
||||
'用户数据大规模泄露'
|
||||
],
|
||||
requirements: [
|
||||
'事故指挥官必须在 5 分钟内就位',
|
||||
'每 15 分钟向管理层通报进展',
|
||||
'所有相关团队取消休假立即支援',
|
||||
'事后 24 小时内完成复盘报告'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
shortName: '严重',
|
||||
name: '严重事故 (Major)',
|
||||
color: '#f59e0b',
|
||||
definition: '核心功能部分受损,大量用户体验降级,但系统未完全不可用。',
|
||||
responseTime: '15 分钟内响应',
|
||||
userImpact: '大量用户',
|
||||
oncallReq: '核心团队',
|
||||
channels: ['即时通讯', '短信', '邮件'],
|
||||
examples: [
|
||||
'搜索功能返回结果严重延迟(>5s)',
|
||||
'部分地区用户无法登录',
|
||||
'订单处理队列严重积压'
|
||||
],
|
||||
requirements: [
|
||||
'值班工程师 15 分钟内开始排查',
|
||||
'每 30 分钟通报一次进展',
|
||||
'必要时升级为 P0',
|
||||
'事后 48 小时内完成复盘'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
shortName: '中等',
|
||||
name: '中等事故 (Moderate)',
|
||||
color: '#eab308',
|
||||
definition: '非核心功能异常,部分用户受影响,不影响主要业务流程。',
|
||||
responseTime: '1 小时内响应',
|
||||
userImpact: '部分用户',
|
||||
oncallReq: '值班工程师',
|
||||
channels: ['即时通讯', '邮件'],
|
||||
examples: [
|
||||
'用户头像加载失败',
|
||||
'报表导出功能超时',
|
||||
'非关键页面 CSS 样式错乱'
|
||||
],
|
||||
requirements: [
|
||||
'值班工程师在工作时间内处理',
|
||||
'当天给出修复方案',
|
||||
'不需要全员响应',
|
||||
'在周报中记录'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
shortName: '轻微',
|
||||
name: '轻微问题 (Minor)',
|
||||
color: '#84cc16',
|
||||
definition: '边缘功能小问题,极少数用户受影响,不影响正常使用。',
|
||||
responseTime: '当天确认,本周处理',
|
||||
userImpact: '极少用户',
|
||||
oncallReq: '正常排期',
|
||||
channels: ['邮件', '工单系统'],
|
||||
examples: [
|
||||
'某个按钮在特定浏览器下对齐偏移',
|
||||
'日志中出现非关键性警告',
|
||||
'文案有错别字'
|
||||
],
|
||||
requirements: [
|
||||
'记录到缺陷跟踪系统',
|
||||
'纳入正常迭代排期',
|
||||
'不需要紧急响应',
|
||||
'修复后正常发布'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
shortName: '建议',
|
||||
name: '改进建议 (Suggestion)',
|
||||
color: '#64748b',
|
||||
definition: '非故障类问题,属于优化建议或技术债务,不影响任何用户。',
|
||||
responseTime: '按优先级排期',
|
||||
userImpact: '无直接影响',
|
||||
oncallReq: '无需值班',
|
||||
channels: ['工单系统'],
|
||||
examples: [
|
||||
'代码中存在可优化的性能瓶颈',
|
||||
'依赖库版本过旧需要升级',
|
||||
'监控覆盖率不足需要补充'
|
||||
],
|
||||
requirements: [
|
||||
'记录到技术债务清单',
|
||||
'季度规划时评估优先级',
|
||||
'作为团队改进项跟踪',
|
||||
'无时间压力'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => {
|
||||
return levels.find((l) => l.id === activeLevel.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.severity-level-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.level-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.level-tab:hover {
|
||||
border-color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.level-tab.active.p0 { border-color: #ef4444; background: rgba(239,68,68,0.08); }
|
||||
.level-tab.active.p1 { border-color: #f59e0b; background: rgba(245,158,11,0.08); }
|
||||
.level-tab.active.p2 { border-color: #eab308; background: rgba(234,179,8,0.08); }
|
||||
.level-tab.active.p3 { border-color: #84cc16; background: rgba(132,204,22,0.08); }
|
||||
.level-tab.active.p4 { border-color: #64748b; background: rgba(100,116,139,0.08); }
|
||||
|
||||
.tab-badge {
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.p0 .tab-badge { background: #ef4444; }
|
||||
.p1 .tab-badge { background: #f59e0b; }
|
||||
.p2 .tab-badge { background: #eab308; }
|
||||
.p3 .tab-badge { background: #84cc16; }
|
||||
.p4 .tab-badge { background: #64748b; }
|
||||
|
||||
.tab-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.level-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.detail-level {
|
||||
font-weight: 800;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.response-time {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.channels {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.channel-tag {
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.examples {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.example-item {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.requirements {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.req-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.req-check {
|
||||
color: #22c55e;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
tr.highlight {
|
||||
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.06);
|
||||
}
|
||||
|
||||
tr {
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.table-badge {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-badge.p0 { background: #ef4444; }
|
||||
.table-badge.p1 { background: #f59e0b; }
|
||||
.table-badge.p2 { background: #eab308; }
|
||||
.table-badge.p3 { background: #84cc16; }
|
||||
.table-badge.p4 { background: #64748b; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.level-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user