Files
test-repo/docs/.vitepress/theme/components/appendix/incident-response/PostmortemDemo.vue
T
sanbuphy 3af119a598 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 内容,集成交互式演示
2026-02-24 18:22:58 +08:00

413 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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">&#10003;</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>