Files

298 lines
6.0 KiB
Vue
Raw Permalink Normal View History

<!--
MonolithReleaseRiskDemo.vue
单体发布的影响面与风险演示
-->
<template>
<div class="release-demo">
<div class="header">
<div class="title">
单体发布牵一发而动全身
</div>
<div class="subtitle">
选择修改范围看看爆炸半径
</div>
</div>
<div class="content">
<div class="modules">
<div class="section-title">
本次改动涉及
</div>
<div class="module-grid">
<button
v-for="module in modules"
:key="module.key"
class="module-btn"
:class="{ active: module.active }"
@click="toggleModule(module.key)"
>
{{ module.label }}
</button>
</div>
<div class="slider">
<label>
改动规模<strong>{{ changeSizeLabel }}</strong>
</label>
<input
v-model="changeSize"
type="range"
min="1"
max="5"
step="1"
>
</div>
</div>
<div class="result">
<div class="risk-meter">
<div class="risk-title">
故障概率
</div>
<div class="risk-value">
{{ riskPercent }}%
</div>
<div class="meter">
<div
class="bar"
:style="{ width: riskPercent + '%' }"
/>
</div>
</div>
<button
class="deploy-btn"
@click="deployRelease"
>
模拟发布
</button>
<div
class="status"
:class="deployStatusClass"
>
{{ deployStatus }}
</div>
<div class="history">
<div class="section-title">
最近 3 次发布
</div>
<ul>
<li
v-for="(item, index) in deployHistory"
:key="index"
>
{{ item }}
</li>
<li v-if="deployHistory.length === 0">
暂无记录
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const modules = ref([
{ key: 'user', label: '用户', active: true },
{ key: 'order', label: '订单', active: true },
{ key: 'payment', label: '支付', active: false },
{ key: 'product', label: '商品', active: false }
])
const changeSize = ref(3)
const deployHistory = ref([])
const deployStatus = ref('等待发布...')
const deployStatusClass = ref('idle')
const activeModules = computed(
() => modules.value.filter((m) => m.active).length
)
const riskPercent = computed(() => {
const base = 8
const moduleRisk = activeModules.value * 12
const changeRisk = changeSize.value * 6
return Math.min(95, base + moduleRisk + changeRisk)
})
const changeSizeLabel = computed(() => {
const labels = ['很小', '小', '中等', '大', '特大']
return labels[changeSize.value - 1] || '中等'
})
const toggleModule = (key) => {
const target = modules.value.find((m) => m.key === key)
if (!target) return
target.active = !target.active
}
const deployRelease = () => {
const roll = Math.random() * 100
if (roll < riskPercent.value) {
deployStatus.value = `发布失败:全站回滚,用时 ${8 + changeSize.value * 4} 分钟`
deployStatusClass.value = 'fail'
} else {
deployStatus.value = '发布成功:流量切换完成'
deployStatusClass.value = 'success'
}
const summary = `${new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})} - ${deployStatus.value}`
deployHistory.value.unshift(summary)
deployHistory.value = deployHistory.value.slice(0, 3)
}
</script>
<style scoped>
.release-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.25rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.25rem;
}
.section-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.module-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
.module-btn {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
padding: 0.5rem;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
}
.module-btn.active {
background: rgba(59, 130, 246, 0.15);
border-color: #3b82f6;
color: #1d4ed8;
}
.slider label {
display: block;
margin: 1rem 0 0.5rem;
font-size: 0.9rem;
}
.slider input[type='range'] {
width: 100%;
}
.risk-meter {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.risk-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.risk-value {
font-size: 1.5rem;
font-weight: 700;
margin: 0.35rem 0 0.75rem;
}
.meter {
height: 8px;
background: var(--vp-c-bg-soft);
border-radius: 999px;
overflow: hidden;
}
.bar {
height: 100%;
background: linear-gradient(90deg, #f59e0b, #ef4444);
}
.deploy-btn {
margin-top: 1rem;
width: 100%;
border: none;
border-radius: 6px;
padding: 0.6rem;
background: var(--vp-c-brand);
color: #fff;
cursor: pointer;
font-weight: 600;
}
.status {
margin-top: 0.75rem;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
}
.status.success {
color: #16a34a;
border-color: rgba(22, 163, 74, 0.4);
}
.status.fail {
color: #ef4444;
border-color: rgba(239, 68, 68, 0.4);
}
.history {
margin-top: 1rem;
}
.history ul {
padding-left: 1rem;
margin: 0.25rem 0 0;
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
@media (max-width: 720px) {
.module-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>