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:
sanbuphy
2026-02-24 18:22:58 +08:00
parent b5a55811cc
commit 3af119a598
86 changed files with 20311 additions and 340 deletions
@@ -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)">&#10003;</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">&#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>
@@ -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">&#10003;</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>