feat: enhance demo components with consistent styling and info boxes
- Add standardized header and info box components to all demo files - Improve visual consistency with theme colors and spacing - Add max-height and overflow-y for better content containment - Update package.json build script with --force flag - Add .gitignore entries for REFACTORING files - Fix table formatting in audio-intro.md
This commit is contained in:
@@ -4,9 +4,10 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="coupling-demo">
|
||||
<div class="header">
|
||||
<div class="title">系统解耦:从紧耦合到松耦合</div>
|
||||
<div class="subtitle">观察同步调用与异步消息的区别</div>
|
||||
<div class="demo-header">
|
||||
<span class="icon">🔗</span>
|
||||
<span class="title">系统解耦</span>
|
||||
<span class="subtitle">从紧耦合到松耦合</span>
|
||||
</div>
|
||||
|
||||
<div class="mode-switch">
|
||||
@@ -26,7 +27,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-content">
|
||||
<!-- 紧耦合模式 -->
|
||||
<div v-if="!useAsync" class="synchronous-mode">
|
||||
<div class="scenario">
|
||||
@@ -65,7 +66,7 @@
|
||||
<div class="problem-list">
|
||||
<div class="problem-item">
|
||||
<span class="icon">⚠️</span>
|
||||
<span><strong>依赖性强:</strong>通知服务宕机,订单创建失败</span>
|
||||
<span><strong>依赖性强:</strong>通知服务宕机,订单创建失败</span>
|
||||
</div>
|
||||
<div class="problem-item">
|
||||
<span class="icon">⚠️</span>
|
||||
@@ -111,7 +112,7 @@
|
||||
<div class="consumer-box" :class="{ failed: consumerFailed }">
|
||||
<div class="consumer-name">短信服务</div>
|
||||
<div class="consumer-status">
|
||||
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
|
||||
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="consumer-box">
|
||||
@@ -122,10 +123,6 @@
|
||||
<div class="consumer-name">积分服务</div>
|
||||
<div class="consumer-status">运行中</div>
|
||||
</div>
|
||||
<div class="consumer-box new">
|
||||
<div class="consumer-name">数据分析</div>
|
||||
<div class="consumer-status">新增 ✨</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +134,7 @@
|
||||
<div class="benefit-item">
|
||||
<span class="icon">✅</span>
|
||||
<span
|
||||
><strong>响应快:</strong>订单服务只耗时 50ms(发送消息)</span
|
||||
><strong>响应快:</strong>订单服务只耗时 50ms(发送消息)</span
|
||||
>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
@@ -153,41 +150,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-summary">
|
||||
<div class="summary-title">📊 对比总结</div>
|
||||
<div class="summary-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>维度</th>
|
||||
<th>紧耦合 (同步)</th>
|
||||
<th>松耦合 (异步)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>服务依赖</td>
|
||||
<td class="bad">强依赖,一个挂全挂</td>
|
||||
<td class="good">弱依赖,独立运行</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>响应时间</td>
|
||||
<td class="bad">1200ms(串行执行)</td>
|
||||
<td class="good">50ms(只发消息)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>扩展性</td>
|
||||
<td class="bad">修改订单服务代码</td>
|
||||
<td class="good">增加新消费者即可</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>可用性</td>
|
||||
<td class="bad">90%(任一服务故障)</td>
|
||||
<td class="good">99.9%(独立故障域)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>同步调用强依赖、响应慢;异步消息解耦、响应快、易扩展
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -203,7 +168,7 @@ const messageInQueue = ref(false)
|
||||
const syncCalls = ref([
|
||||
{ id: 1, service: '调用库存服务', active: false, status: '处理中...' },
|
||||
{ id: 2, service: '调用积分服务', active: false, status: '处理中...' },
|
||||
{ id: 3, service: '调用通知服务', active: false, status: '失败!订单回滚' }
|
||||
{ id: 3, service: '调用通知服务', active: false, status: '失败!订单回滚' }
|
||||
])
|
||||
|
||||
const testSyncCall = () => {
|
||||
@@ -232,42 +197,52 @@ const testAsyncCall = () => {
|
||||
<style scoped>
|
||||
.coupling-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1rem;
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
.demo-header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
@@ -277,18 +252,18 @@ const testAsyncCall = () => {
|
||||
|
||||
.mode-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
margin-bottom: 1.5rem;
|
||||
.demo-content {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -296,55 +271,55 @@ const testAsyncCall = () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.service-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
min-width: 180px;
|
||||
min-width: 140px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.service-box.failed {
|
||||
border-color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: var(--vp-c-danger);
|
||||
background: var(--vp-c-danger-soft);
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.service-desc {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: #ef4444;
|
||||
background: var(--vp-c-danger);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arrows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.sync-call {
|
||||
@@ -357,7 +332,7 @@ const testAsyncCall = () => {
|
||||
}
|
||||
|
||||
.sync-call.active {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
background: var(--vp-c-danger-soft);
|
||||
}
|
||||
|
||||
.call-line {
|
||||
@@ -367,96 +342,91 @@ const testAsyncCall = () => {
|
||||
}
|
||||
|
||||
.sync-call.active .call-line {
|
||||
background: #ef4444;
|
||||
background: var(--vp-c-danger);
|
||||
}
|
||||
|
||||
.call-label {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.call-status {
|
||||
font-size: 0.75rem;
|
||||
color: #ef4444;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mq-bridge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.mq-box {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
min-width: 140px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.mq-icon {
|
||||
font-size: 2rem;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mq-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.msg-indicator {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
background: var(--vp-c-success);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.consumers-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.consumer-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.consumer-box.failed {
|
||||
border-color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.consumer-box.new {
|
||||
border-color: #22c55e;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-color: var(--vp-c-warning);
|
||||
background: var(--vp-c-warning-soft);
|
||||
}
|
||||
|
||||
.consumer-name {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.consumer-status {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@@ -464,8 +434,8 @@ const testAsyncCall = () => {
|
||||
.benefit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.problem-item,
|
||||
@@ -473,92 +443,66 @@ const testAsyncCall = () => {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.problem-item {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
background: var(--vp-c-danger-soft);
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
background: var(--vp-c-success-soft);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.test-btn.fail {
|
||||
background: #ef4444;
|
||||
background: var(--vp-c-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.fail:hover {
|
||||
background: #dc2626;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.test-btn.success {
|
||||
background: #22c55e;
|
||||
background: var(--vp-c-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.success:hover {
|
||||
background: #16a34a;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.comparison-summary {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.summary-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bad {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.good {
|
||||
color: #16a34a;
|
||||
.info-box .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,133 +4,132 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="dlq-demo">
|
||||
<div class="header">
|
||||
<div class="title">死信队列:消息的"急救站"</div>
|
||||
<div class="subtitle">处理无法消费的消息,避免阻塞队列</div>
|
||||
<div class="demo-header">
|
||||
<span class="icon">🚑</span>
|
||||
<span class="title">死信队列</span>
|
||||
<span class="subtitle">消息的"急救站" - 处理失败消息</span>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<label>失败率:</label>
|
||||
<input v-model="failureRate" type="range" min="0" max="100" step="10" />
|
||||
<input v-model.number="failureRate" type="range" min="0" max="100" step="10" />
|
||||
<span class="value">{{ failureRate }}%</span>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label>最大重试次数:</label>
|
||||
<input v-model="maxRetries" type="range" min="1" max="5" step="1" />
|
||||
<label>最大重试:</label>
|
||||
<input v-model.number="maxRetries" type="range" min="1" max="5" step="1" />
|
||||
<span class="value">{{ maxRetries }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-container">
|
||||
<div class="main-queue-section">
|
||||
<div class="section-title">📦 主队列</div>
|
||||
<div class="queue-box main-queue">
|
||||
<div class="queue-header">
|
||||
<span>正常消息队列</span>
|
||||
<span class="count">{{ mainQueue.length }} 条</span>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<div
|
||||
v-for="msg in mainQueue"
|
||||
:key="msg.id"
|
||||
class="message-item"
|
||||
:class="{ processing: msg.processing }"
|
||||
>
|
||||
<div class="msg-id">#{{ msg.id }}</div>
|
||||
<div class="msg-retries" v-if="msg.retries > 0">
|
||||
重试: {{ msg.retries }}/{{ maxRetries }}
|
||||
<div class="demo-content">
|
||||
<div class="flow-container">
|
||||
<div class="main-queue-section">
|
||||
<div class="section-title">📦 主队列</div>
|
||||
<div class="queue-box main-queue">
|
||||
<div class="queue-header">
|
||||
<span>正常消息队列</span>
|
||||
<span class="count">{{ mainQueue.length }} 条</span>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<div
|
||||
v-for="msg in mainQueue.slice(0, 3)"
|
||||
:key="msg.id"
|
||||
class="message-item"
|
||||
:class="{ processing: msg.processing }"
|
||||
>
|
||||
<div class="msg-id">#{{ msg.id }}</div>
|
||||
<div class="msg-retries" v-if="msg.retries > 0">
|
||||
重试: {{ msg.retries }}/{{ maxRetries }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mainQueue.length === 0" class="empty">队列为空</div>
|
||||
<div v-else-if="mainQueue.length > 3" class="more">
|
||||
还有 {{ mainQueue.length - 3 }} 条...
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mainQueue.length === 0" class="empty">队列为空</div>
|
||||
</div>
|
||||
<button class="add-btn" @click="addMessage" :disabled="processing">
|
||||
+ 添加消息
|
||||
</button>
|
||||
</div>
|
||||
<button class="add-btn" @click="addMessage" :disabled="processing">
|
||||
+ 添加消息
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="processing-section">
|
||||
<div class="section-title">⚙️ 消费处理</div>
|
||||
<div class="processor-box">
|
||||
<div class="processor-icon" :class="{ active: processing }">
|
||||
{{ processing ? '⚙️' : '💤' }}
|
||||
</div>
|
||||
<div class="processor-status">
|
||||
{{ processing ? '处理中...' : '空闲' }}
|
||||
</div>
|
||||
<div v-if="currentMessage" class="current-msg">
|
||||
处理: #{{ currentMessage.id }}
|
||||
</div>
|
||||
<div v-if="lastResult" class="last-result" :class="lastResult.type">
|
||||
{{ lastResult.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dlq-section">
|
||||
<div class="section-title">⚠️ 死信队列</div>
|
||||
<div class="queue-box dead-letter">
|
||||
<div class="queue-header">
|
||||
<span>失败消息</span>
|
||||
<span class="count">{{ deadLetterQueue.length }} 条</span>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<div
|
||||
v-for="msg in deadLetterQueue"
|
||||
:key="msg.id"
|
||||
class="message-item failed"
|
||||
>
|
||||
<div class="msg-id">#{{ msg.id }}</div>
|
||||
<div class="msg-error">{{ msg.error }}</div>
|
||||
<div class="processing-section">
|
||||
<div class="section-title">⚙️ 消费处理</div>
|
||||
<div class="processor-box">
|
||||
<div class="processor-icon" :class="{ active: processing }">
|
||||
{{ processing ? '⚙️' : '💤' }}
|
||||
</div>
|
||||
<div v-if="deadLetterQueue.length === 0" class="empty">
|
||||
无失败消息
|
||||
<div class="processor-status">
|
||||
{{ processing ? '处理中...' : '空闲' }}
|
||||
</div>
|
||||
<div v-if="currentMessage" class="current-msg">
|
||||
处理: #{{ currentMessage.id }}
|
||||
</div>
|
||||
<div v-if="lastResult" class="last-result" :class="lastResult.type">
|
||||
{{ lastResult.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="retry-btn"
|
||||
@click="retryDeadLetters"
|
||||
:disabled="deadLetterQueue.length === 0"
|
||||
>
|
||||
🔄 重试死信
|
||||
</button>
|
||||
|
||||
<div class="dlq-section">
|
||||
<div class="section-title">⚠️ 死信队列</div>
|
||||
<div class="queue-box dead-letter">
|
||||
<div class="queue-header">
|
||||
<span>失败消息</span>
|
||||
<span class="count">{{ deadLetterQueue.length }} 条</span>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<div
|
||||
v-for="msg in deadLetterQueue.slice(0, 2)"
|
||||
:key="msg.id"
|
||||
class="message-item failed"
|
||||
>
|
||||
<div class="msg-id">#{{ msg.id }}</div>
|
||||
<div class="msg-error">{{ msg.error }}</div>
|
||||
</div>
|
||||
<div v-if="deadLetterQueue.length === 0" class="empty">
|
||||
无失败消息
|
||||
</div>
|
||||
<div v-else-if="deadLetterQueue.length > 2" class="more">
|
||||
还有 {{ deadLetterQueue.length - 2 }} 条...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="retry-btn"
|
||||
@click="retryDeadLetters"
|
||||
:disabled="deadLetterQueue.length === 0"
|
||||
>
|
||||
🔄 重试死信
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总消息数</div>
|
||||
<div class="stat-value">{{ totalMessages }}</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-label">成功处理</div>
|
||||
<div class="stat-value">{{ successCount }}</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-label">进入死信</div>
|
||||
<div class="stat-value">{{ deadLetterCount }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">成功率</div>
|
||||
<div class="stat-value">{{ successRate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总消息数</div>
|
||||
<div class="stat-value">{{ totalMessages }}</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-label">成功处理</div>
|
||||
<div class="stat-value">{{ successCount }}</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-label">进入死信</div>
|
||||
<div class="stat-value">{{ deadLetterCount }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">成功率</div>
|
||||
<div class="stat-value">{{ successRate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="explanation">
|
||||
<div class="exp-title">💡 死信队列的作用</div>
|
||||
<div class="exp-content">
|
||||
<div class="exp-item">
|
||||
<strong>1. 隔离异常消息:</strong>失败消息不会阻塞正常消息的处理
|
||||
</div>
|
||||
<div class="exp-item">
|
||||
<strong>2. 保留失败记录:</strong>可以后续人工介入或自动重试
|
||||
</div>
|
||||
<div class="exp-item">
|
||||
<strong>3. 系统保护:</strong>避免因持续失败导致消费者崩溃
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>失败消息进入死信队列,避免阻塞正常消息,可后续人工介入或自动重试
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -174,7 +173,7 @@ const processNext = () => {
|
||||
return
|
||||
}
|
||||
|
||||
let msg = mainQueue[0]
|
||||
let msg = mainQueue.value[0]
|
||||
msg.processing = true
|
||||
processing.value = true
|
||||
currentMessage.value = msg
|
||||
@@ -188,7 +187,7 @@ const processNext = () => {
|
||||
msg.processing = false
|
||||
|
||||
if (msg.retries >= maxRetries.value) {
|
||||
// 超过最大重试次数,进入死信队列
|
||||
// 超过最大重试次数,进入死信队列
|
||||
mainQueue.value.shift()
|
||||
deadLetterQueue.value.push({
|
||||
id: msg.id,
|
||||
@@ -202,7 +201,7 @@ const processNext = () => {
|
||||
// 重新入队
|
||||
lastResult.value = {
|
||||
type: 'warning',
|
||||
message: `⚠️ 消息 #${msg.id} 处理失败,重试 ${msg.retries}/${maxRetries.value}`
|
||||
message: `⚠️ 消息 #${msg.id} 处理失败,重试 ${msg.retries}/${maxRetries.value}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,39 +261,48 @@ addMessage = addMessageWithAutoProcess
|
||||
.dlq-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);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1rem;
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
.demo-header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.control input[type='range'] {
|
||||
@@ -307,25 +315,29 @@ addMessage = addMessageWithAutoProcess
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.flow-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
text-align: center;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -334,7 +346,7 @@ addMessage = addMessageWithAutoProcess
|
||||
}
|
||||
|
||||
.queue-box.dead-letter {
|
||||
border-color: #ef4444;
|
||||
border-color: var(--vp-c-danger);
|
||||
}
|
||||
|
||||
.queue-header {
|
||||
@@ -343,12 +355,12 @@ addMessage = addMessageWithAutoProcess
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
max-height: 200px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -361,17 +373,17 @@ addMessage = addMessageWithAutoProcess
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.message-item.processing {
|
||||
border: 1px solid #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid var(--vp-c-warning);
|
||||
background: var(--vp-c-warning-soft);
|
||||
}
|
||||
|
||||
.message-item.failed {
|
||||
border: 1px solid #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid var(--vp-c-danger);
|
||||
background: var(--vp-c-danger-soft);
|
||||
}
|
||||
|
||||
.msg-id {
|
||||
@@ -379,32 +391,32 @@ addMessage = addMessageWithAutoProcess
|
||||
}
|
||||
|
||||
.msg-retries {
|
||||
font-size: 0.7rem;
|
||||
color: #f59e0b;
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-warning);
|
||||
}
|
||||
|
||||
.msg-error {
|
||||
font-size: 0.7rem;
|
||||
color: #ef4444;
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-danger);
|
||||
}
|
||||
|
||||
.empty {
|
||||
.empty, .more {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
padding: 1rem 0.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.add-btn,
|
||||
.retry-btn {
|
||||
width: 100%;
|
||||
padding: 0.6rem;
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
@@ -414,8 +426,7 @@ addMessage = addMessageWithAutoProcess
|
||||
}
|
||||
|
||||
.add-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.add-btn:disabled {
|
||||
@@ -424,21 +435,21 @@ addMessage = addMessageWithAutoProcess
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #f59e0b;
|
||||
background: var(--vp-c-warning);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.retry-btn:hover:not(:disabled) {
|
||||
background: #d97706;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.processor-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
min-height: 200px;
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -446,8 +457,8 @@ addMessage = addMessageWithAutoProcess
|
||||
}
|
||||
|
||||
.processor-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.processor-icon.active {
|
||||
@@ -455,97 +466,87 @@ addMessage = addMessageWithAutoProcess
|
||||
}
|
||||
|
||||
.processor-status {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.current-msg {
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.last-result {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.last-result.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
background: var(--vp-c-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.last-result.warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
background: var(--vp-c-warning-soft);
|
||||
color: var(--vp-c-warning-dark);
|
||||
}
|
||||
|
||||
.last-result.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
background: var(--vp-c-danger-soft);
|
||||
color: var(--vp-c-danger-dark);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
border-color: #22c55e;
|
||||
background: rgba(34, 197, 94, 0.05);
|
||||
border-color: var(--vp-c-success);
|
||||
background: var(--vp-c-success-soft);
|
||||
}
|
||||
|
||||
.stat-card.warning {
|
||||
border-color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
border-color: var(--vp-c-danger);
|
||||
background: var(--vp-c-danger-soft);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.3rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.exp-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.exp-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.exp-item {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
<!--
|
||||
DecouplingDemo.vue
|
||||
系统解耦演示 - 同步 vs 异步对比
|
||||
-->
|
||||
<template>
|
||||
<div class="decoupling-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🔗</span>
|
||||
<span class="title">系统解耦演示</span>
|
||||
<span class="subtitle">从紧耦合到松耦合的演进</span>
|
||||
</div>
|
||||
|
||||
<div class="mode-switch">
|
||||
<button
|
||||
class="mode-btn"
|
||||
:class="{ active: !useAsync }"
|
||||
@click="useAsync = false"
|
||||
>
|
||||
🔗 紧耦合 (同步)
|
||||
</button>
|
||||
<button
|
||||
class="mode-btn"
|
||||
:class="{ active: useAsync }"
|
||||
@click="useAsync = true"
|
||||
>
|
||||
🔓 松耦合 (异步)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 紧耦合模式 -->
|
||||
<div v-if="!useAsync" class="synchronous-mode">
|
||||
<div class="scenario">
|
||||
<div class="scenario-title">❌ 紧耦合的致命问题</div>
|
||||
<div class="flow-diagram">
|
||||
<div class="service-box order">
|
||||
<div class="service-name">订单服务</div>
|
||||
<div class="service-desc">创建订单</div>
|
||||
</div>
|
||||
|
||||
<div class="arrows">
|
||||
<div
|
||||
v-for="call in syncCalls"
|
||||
:key="call.id"
|
||||
class="sync-call"
|
||||
:class="{ active: call.active }"
|
||||
>
|
||||
<div class="call-line"></div>
|
||||
<div class="call-label">{{ call.service }}</div>
|
||||
<div v-if="call.active" class="call-status">
|
||||
{{ call.status }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="service-box notification"
|
||||
:class="{ failed: notificationFailed }"
|
||||
>
|
||||
<div class="service-name">通知服务</div>
|
||||
<div class="service-desc">发送短信/邮件</div>
|
||||
<div v-if="notificationFailed" class="error-msg">
|
||||
服务宕机 ❌
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="problem-list">
|
||||
<div class="problem-item">
|
||||
<span class="icon">⚠️</span>
|
||||
<span
|
||||
><strong>依赖性强:</strong>通知服务宕机,订单创建失败</span
|
||||
>
|
||||
</div>
|
||||
<div class="problem-item">
|
||||
<span class="icon">⚠️</span>
|
||||
<span
|
||||
><strong>响应慢:</strong>总耗时 = 300ms + 500ms + 400ms =
|
||||
1200ms</span
|
||||
>
|
||||
</div>
|
||||
<div class="problem-item">
|
||||
<span class="icon">⚠️</span>
|
||||
<span
|
||||
><strong>扩展难:</strong>增加新服务需要修改订单代码</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="test-btn fail" @click="testSyncCall">
|
||||
模拟通知服务故障
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 松耦合模式 -->
|
||||
<div v-else class="asynchronous-mode">
|
||||
<div class="scenario">
|
||||
<div class="scenario-title">✅ 松耦合的核心优势</div>
|
||||
<div class="flow-diagram">
|
||||
<div class="service-box order">
|
||||
<div class="service-name">订单服务</div>
|
||||
<div class="service-desc">创建订单 + 发送消息</div>
|
||||
</div>
|
||||
|
||||
<div class="mq-bridge">
|
||||
<div class="mq-box">
|
||||
<div class="mq-icon">📨</div>
|
||||
<div class="mq-label">消息队列</div>
|
||||
<div v-if="messageInQueue" class="msg-indicator">
|
||||
消息已发送
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
</div>
|
||||
|
||||
<div class="consumers-group">
|
||||
<div class="consumer-box" :class="{ failed: consumerFailed }">
|
||||
<div class="consumer-name">短信服务</div>
|
||||
<div class="consumer-status">
|
||||
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="consumer-box">
|
||||
<div class="consumer-name">邮件服务</div>
|
||||
<div class="consumer-status">运行中</div>
|
||||
</div>
|
||||
<div class="consumer-box">
|
||||
<div class="consumer-name">积分服务</div>
|
||||
<div class="consumer-status">运行中</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-list">
|
||||
<div class="benefit-item">
|
||||
<span class="icon">✅</span>
|
||||
<span
|
||||
><strong>独立运行:</strong>通知服务宕机不影响订单创建</span
|
||||
>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<span class="icon">✅</span>
|
||||
<span
|
||||
><strong>响应快:</strong>订单服务只耗时 50ms(发送消息)</span
|
||||
>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<span class="icon">✅</span>
|
||||
<span
|
||||
><strong>易扩展:</strong>增加新消费者无需修改订单代码</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="test-btn success" @click="testAsyncCall">
|
||||
发送订单消息
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>同步调用强依赖、响应慢;异步消息解耦、响应快、易扩展
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const useAsync = ref(false)
|
||||
const notificationFailed = ref(false)
|
||||
const consumerFailed = ref(false)
|
||||
const messageInQueue = ref(false)
|
||||
|
||||
const syncCalls = ref([
|
||||
{ id: 1, service: '调用库存服务', active: false, status: '处理中...' },
|
||||
{ id: 2, service: '调用积分服务', active: false, status: '处理中...' },
|
||||
{
|
||||
id: 3,
|
||||
service: '调用通知服务',
|
||||
active: false,
|
||||
status: '失败!订单回滚'
|
||||
}
|
||||
])
|
||||
|
||||
const testSyncCall = () => {
|
||||
notificationFailed.value = true
|
||||
|
||||
syncCalls.value.forEach((call, index) => {
|
||||
setTimeout(() => {
|
||||
call.active = true
|
||||
if (index === syncCalls.value.length - 1) {
|
||||
setTimeout(() => {
|
||||
call.active = false
|
||||
}, 2000)
|
||||
}
|
||||
}, index * 800)
|
||||
})
|
||||
}
|
||||
|
||||
const testAsyncCall = () => {
|
||||
messageInQueue.value = true
|
||||
setTimeout(() => {
|
||||
messageInQueue.value = false
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.decoupling-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header .icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-diagram {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.service-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
min-width: 160px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.service-box.failed {
|
||||
border-color: var(--vp-c-danger);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.service-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--vp-c-danger);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arrows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.sync-call {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sync-call.active {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.call-line {
|
||||
width: 2px;
|
||||
height: 24px;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.sync-call.active .call-line {
|
||||
background: var(--vp-c-danger);
|
||||
}
|
||||
|
||||
.call-label {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.call-status {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mq-bridge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mq-box {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.mq-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mq-label {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.msg-indicator {
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--vp-c-success);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.consumers-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.consumer-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.consumer-box.failed {
|
||||
border-color: var(--vp-c-warning);
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.consumer-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.consumer-status {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.problem-list,
|
||||
.benefit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.problem-item,
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.problem-item {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.test-btn.fail {
|
||||
background: var(--vp-c-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.fail:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.test-btn.success {
|
||||
background: var(--vp-c-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.success:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +1,881 @@
|
||||
<!--
|
||||
IdempotenceDemo.vue
|
||||
幂等性演示 - 重复消费处理
|
||||
-->
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="idempotence-demo">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<span class="icon">🔄</span>
|
||||
<span class="title">幂等性演示</span>
|
||||
<span class="subtitle">保证重复消费不会产生副作用</span>
|
||||
</div>
|
||||
|
||||
<div class="scenario-switch">
|
||||
<button
|
||||
class="scenario-btn"
|
||||
:class="{ active: scenario === 'transfer' }"
|
||||
@click="scenario = 'transfer'"
|
||||
>
|
||||
💰 银行转账
|
||||
</button>
|
||||
<button
|
||||
class="scenario-btn"
|
||||
:class="{ active: scenario === 'elevator' }"
|
||||
@click="scenario = 'elevator'"
|
||||
>
|
||||
🛗 电梯按钮
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
幂等性演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
<!-- 银行转账场景 -->
|
||||
<div v-if="scenario === 'transfer'" class="transfer-scenario">
|
||||
<div class="scenario-header">
|
||||
<div class="title">❌ 非幂等操作: 银行转账</div>
|
||||
<div class="subtitle">重复消费会导致多次扣款</div>
|
||||
</div>
|
||||
|
||||
<div class="account-system">
|
||||
<div class="account-card sender">
|
||||
<div class="account-name">发送方</div>
|
||||
<div class="account-balance">
|
||||
余额: ¥<span class="balance-amount">{{ senderBalance }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transfer-flow">
|
||||
<div class="flow-animation" :class="{ active: isTransferring }">
|
||||
<div class="money-icon">💰</div>
|
||||
<div class="flow-label">转账 ¥100</div>
|
||||
</div>
|
||||
<div class="retry-info" v-if="retryCount > 0">
|
||||
<div class="retry-badge">重试 {{ retryCount }} 次</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-card receiver">
|
||||
<div class="account-name">接收方</div>
|
||||
<div class="account-balance">
|
||||
余额: ¥<span class="balance-amount">{{ receiverBalance }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<div class="control-row">
|
||||
<div class="control-item">
|
||||
<label>幂等性保护</label>
|
||||
<div class="toggle-switch">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: useIdempotence }"
|
||||
@click="useIdempotence = !useIdempotence"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
<span class="toggle-label">{{ useIdempotence ? '已启用' : '未启用' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="simulateTransfer"
|
||||
:disabled="isTransferring"
|
||||
>
|
||||
{{ isTransferring ? '处理中...' : '模拟重复消费' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="idempotence-info" v-if="useIdempotence">
|
||||
<div class="info-item">
|
||||
<span class="info-icon">🔑</span>
|
||||
<span class="info-text">每笔交易有唯一ID,重复请求被自动过滤</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-log">
|
||||
<div class="log-header">处理日志</div>
|
||||
<div class="log-list">
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
class="log-item"
|
||||
:class="log.type"
|
||||
>
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
<div v-if="logs.length === 0" class="log-empty">
|
||||
暂无日志,点击按钮开始模拟
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-box">
|
||||
<div class="comparison-item bad">
|
||||
<div class="comp-header">❌ 无幂等保护</div>
|
||||
<div class="comp-body">
|
||||
<div class="comp-result">扣款 ¥{{ (retryCount + 1) * 100 }}</div>
|
||||
<div class="comp-desc">重复消费造成多次扣款</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comparison-item good">
|
||||
<div class="comp-header">✅ 有幂等保护</div>
|
||||
<div class="comp-body">
|
||||
<div class="comp-result">扣款 ¥100</div>
|
||||
<div class="comp-desc">重复请求被过滤,只扣一次</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 电梯按钮场景 -->
|
||||
<div v-else class="elevator-scenario">
|
||||
<div class="scenario-header">
|
||||
<div class="title">✅ 天然幂等操作: 电梯按钮</div>
|
||||
<div class="subtitle">无论按多少次,电梯只响应一次</div>
|
||||
</div>
|
||||
|
||||
<div class="elevator-system">
|
||||
<div class="elevator-panel">
|
||||
<div class="panel-title">电梯按钮面板</div>
|
||||
<div class="button-grid">
|
||||
<button
|
||||
v-for="floor in floors"
|
||||
:key="floor"
|
||||
class="floor-btn"
|
||||
:class="{ active: selectedFloor === floor }"
|
||||
@click="pressFloor(floor)"
|
||||
>
|
||||
{{ floor }}F
|
||||
</button>
|
||||
</div>
|
||||
<div class="press-count">
|
||||
<span class="count-label">按钮按了</span>
|
||||
<span class="count-value">{{ pressCount }}</span>
|
||||
<span class="count-label">次</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="elevator-shaft">
|
||||
<div class="floor-marks">
|
||||
<div
|
||||
v-for="floor in floors"
|
||||
:key="floor"
|
||||
class="floor-mark"
|
||||
:class="{ current: elevatorFloor === floor }"
|
||||
>
|
||||
<span class="floor-num">{{ floor }}F</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="elevator-car" :style="{ bottom: elevatorPosition }">
|
||||
<div class="car-icon">🛗</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<div class="control-item">
|
||||
<label>快速连按3次</label>
|
||||
<button class="action-btn" @click="pressMultipleTimes">
|
||||
🚀 连续点击
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<span class="info-icon">💡</span>
|
||||
虽然按了{{ pressCount }}次,但电梯只响应一次请求
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="explanation-box">
|
||||
<div class="explanation-title">为什么电梯按钮是幂等的?</div>
|
||||
<div class="explanation-list">
|
||||
<div class="explanation-item">
|
||||
<span class="icon">✅</span>
|
||||
<span>状态只切换一次: 停靠 → 已选中</span>
|
||||
</div>
|
||||
<div class="explanation-item">
|
||||
<span class="icon">✅</span>
|
||||
<span>重复请求不改变目标楼层</span>
|
||||
</div>
|
||||
<div class="explanation-item">
|
||||
<span class="icon">✅</span>
|
||||
<span>无需额外的幂等性保护机制</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="principle-box">
|
||||
<div class="principle-icon">🎯</div>
|
||||
<div class="principle-content">
|
||||
<strong>幂等性核心原则:</strong>
|
||||
{{ scenario === 'transfer'
|
||||
? '为每条消息生成唯一ID,处理前检查是否已处理,避免重复操作'
|
||||
: '设计操作时确保重复执行和执行一次的效果相同' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const title = ref('幂等性演示')
|
||||
const description = ref('展示消息消费中的幂等性问题,以及如何通过幂等性设计保证消息处理的正确性')
|
||||
// 场景切换
|
||||
const scenario = ref('transfer')
|
||||
|
||||
// 转账场景
|
||||
const senderBalance = ref(1000)
|
||||
const receiverBalance = ref(500)
|
||||
const isTransferring = ref(false)
|
||||
const useIdempotence = ref(false)
|
||||
const retryCount = ref(0)
|
||||
const logs = ref([])
|
||||
|
||||
const addLog = (message, type = 'info') => {
|
||||
const now = new Date()
|
||||
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
|
||||
logs.value.unshift({ time, message, type })
|
||||
}
|
||||
|
||||
const simulateTransfer = () => {
|
||||
if (isTransferring.value) return
|
||||
|
||||
isTransferring.value = true
|
||||
retryCount.value = 0
|
||||
logs.value = []
|
||||
|
||||
const originalSenderBalance = senderBalance.value
|
||||
const originalReceiverBalance = receiverBalance.value
|
||||
|
||||
addLog('收到转账请求: ¥100', 'info')
|
||||
|
||||
// 模拟重复消费
|
||||
const processTransfer = (attempt) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
retryCount.value = attempt
|
||||
|
||||
if (useIdempotence.value) {
|
||||
if (attempt === 0) {
|
||||
senderBalance.value = originalSenderBalance - 100
|
||||
receiverBalance.value = originalReceiverBalance + 100
|
||||
addLog(`第${attempt + 1}次处理: 成功转账 ¥100`, 'success')
|
||||
addLog('幂等性检查: 唯一ID已记录,后续请求被过滤', 'info')
|
||||
} else {
|
||||
addLog(`第${attempt + 1}次处理: 重复请求,已忽略`, 'warning')
|
||||
}
|
||||
} else {
|
||||
senderBalance.value -= 100
|
||||
receiverBalance.value += 100
|
||||
addLog(`第${attempt + 1}次处理: 转账 ¥100`, attempt === 0 ? 'success' : 'error')
|
||||
}
|
||||
|
||||
if (attempt < 2) {
|
||||
setTimeout(() => processTransfer(attempt + 1), 1000)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
isTransferring.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
resolve()
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
processTransfer(0)
|
||||
}
|
||||
|
||||
// 电梯场景
|
||||
const floors = [1, 2, 3, 4, 5]
|
||||
const selectedFloor = ref(null)
|
||||
const elevatorFloor = ref(1)
|
||||
const pressCount = ref(0)
|
||||
|
||||
const elevatorPosition = computed(() => {
|
||||
return ((elevatorFloor.value - 1) / 4) * 100 + '%'
|
||||
})
|
||||
|
||||
const pressFloor = (floor) => {
|
||||
pressCount.value++
|
||||
selectedFloor.value = floor
|
||||
|
||||
setTimeout(() => {
|
||||
elevatorFloor.value = floor
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const pressMultipleTimes = () => {
|
||||
const targetFloor = Math.floor(Math.random() * 5) + 1
|
||||
let count = 0
|
||||
const interval = setInterval(() => {
|
||||
pressFloor(targetFloor)
|
||||
count++
|
||||
if (count >= 3) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.idempotence-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
.demo-header .icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.scenario-switch {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scenario-btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.scenario-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.scenario-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.scenario-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scenario-header .title {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.scenario-header .subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
.account-system {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-bottom: 20px;
|
||||
padding: 24px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.account-card {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.account-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.account-balance {
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.transfer-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.flow-animation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.flow-animation.active {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.money-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.flow-label {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.retry-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.retry-badge {
|
||||
padding: 4px 10px;
|
||||
background: var(--vp-c-warning);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.control-item label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
background: var(--vp-c-divider);
|
||||
border: none;
|
||||
border-radius: 13px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.toggle-btn.active .toggle-slider {
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 20px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.idempotence-info {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-log {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.log-item.success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.log-item.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.log-item.warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--vp-c-text-3);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.log-empty {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.comparison-box {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comparison-item.bad {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.comparison-item.good {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
.comp-header {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.comp-result {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.comp-desc {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.elevator-system {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
padding: 24px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.elevator-panel {
|
||||
flex: 1;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.floor-btn {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.floor-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.floor-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.press-count {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.count-value {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.elevator-shaft {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
height: 300px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.floor-marks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.floor-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.floor-num {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.floor-mark.current .floor-num {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.elevator-car {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transition: bottom 0.5s ease;
|
||||
}
|
||||
|
||||
.car-icon {
|
||||
font-size: 32px;
|
||||
animation: bounce 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.info-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.explanation-box {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.explanation-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.explanation-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.explanation-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.explanation-item .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.principle-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.principle-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.principle-content {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,207 @@
|
||||
<!--
|
||||
ReliabilityDemo.vue
|
||||
消息可靠性演示 - 三道防线
|
||||
-->
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="reliability-demo">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<span class="icon">🛡️</span>
|
||||
<span class="title">消息可靠性演示</span>
|
||||
<span class="subtitle">三道防线保证消息不丢失</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
消息可靠性演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
|
||||
<div class="defense-system">
|
||||
<!-- 防线1: 生产者确认 -->
|
||||
<div class="defense-line">
|
||||
<div class="defense-header">
|
||||
<div class="defense-badge line1">防线 1</div>
|
||||
<div class="defense-title">生产者确认 (Producer ACK)</div>
|
||||
</div>
|
||||
<div class="defense-content">
|
||||
<div class="flow-diagram">
|
||||
<div class="component producer">
|
||||
<div class="comp-icon">📤</div>
|
||||
<div class="comp-label">生产者</div>
|
||||
<div class="comp-desc">发送消息</div>
|
||||
</div>
|
||||
|
||||
<div class="message-flow">
|
||||
<div class="msg-item" :class="{ active: step === 1 }">
|
||||
<div class="msg-icon">📨</div>
|
||||
<div class="msg-label">消息</div>
|
||||
<div v-if="step === 1" class="msg-status">
|
||||
{{ ackStatus }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ack-item" :class="{ active: step === 2 }">
|
||||
<div class="ack-icon">✓</div>
|
||||
<div class="ack-label">ACK确认</div>
|
||||
<div v-if="step === 2" class="ack-status">
|
||||
{{ ackMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component broker">
|
||||
<div class="comp-icon">📦</div>
|
||||
<div class="comp-label">Broker</div>
|
||||
<div class="comp-desc">接收并存储</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<div class="control-item">
|
||||
<label>发送消息</label>
|
||||
<button class="action-btn" @click="sendWithAck" :disabled="step > 0">
|
||||
发送并等待确认
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<span class="info-icon">💡</span>
|
||||
如果没收到ACK,生产者会重试或记录本地日志
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防线2: Broker持久化 -->
|
||||
<div class="defense-line">
|
||||
<div class="defense-header">
|
||||
<div class="defense-badge line2">防线 2</div>
|
||||
<div class="defense-title">Broker持久化</div>
|
||||
</div>
|
||||
<div class="defense-content">
|
||||
<div class="storage-diagram">
|
||||
<div class="storage-container">
|
||||
<div class="storage-option" :class="{ active: storageType === 'memory' }">
|
||||
<div class="option-icon">⚡</div>
|
||||
<div class="option-label">内存存储</div>
|
||||
<div class="option-desc">速度快,但重启丢失</div>
|
||||
<div class="option-risk">❌ 高风险</div>
|
||||
</div>
|
||||
|
||||
<div class="vs-divider">vs</div>
|
||||
|
||||
<div class="storage-option recommended" :class="{ active: storageType === 'disk' }">
|
||||
<div class="option-icon">💾</div>
|
||||
<div class="option-label">磁盘存储</div>
|
||||
<div class="option-desc">落盘保证不丢失</div>
|
||||
<div class="option-risk">✅ 推荐</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="replication-info">
|
||||
<div class="replication-title">
|
||||
<span class="icon">🔄</span>
|
||||
多副本同步
|
||||
</div>
|
||||
<div class="replication-detail">
|
||||
消息同步到3个节点,即使1个节点宕机也不丢数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<div class="control-item">
|
||||
<label>存储方式</label>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: storageType === 'memory' }"
|
||||
@click="storageType = 'memory'"
|
||||
>
|
||||
内存
|
||||
</button>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: storageType === 'disk' }"
|
||||
@click="storageType = 'disk'"
|
||||
>
|
||||
磁盘
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-text" :class="{ warning: storageType === 'memory' }">
|
||||
<span class="info-icon">{{ storageType === 'disk' ? '✅' : '⚠️' }}</span>
|
||||
{{ storageType === 'disk' ? '消息已落盘,安全可靠' : '消息仅在内存,重启丢失' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防线3: 消费者确认 -->
|
||||
<div class="defense-line">
|
||||
<div class="defense-header">
|
||||
<div class="defense-badge line3">防线 3</div>
|
||||
<div class="defense-title">消费者确认 (Consumer ACK)</div>
|
||||
</div>
|
||||
<div class="defense-content">
|
||||
<div class="consumer-flow">
|
||||
<div class="flow-step" :class="{ active: consumerStep >= 1 }">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">拉取消息</div>
|
||||
<div class="step-desc">从Broker获取消息</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow" :class="{ active: consumerStep >= 1 }">→</div>
|
||||
|
||||
<div class="flow-step" :class="{ active: consumerStep >= 2 }">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">处理消息</div>
|
||||
<div class="step-desc">执行业务逻辑</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow" :class="{ active: consumerStep >= 2 }">→</div>
|
||||
|
||||
<div class="flow-step" :class="{ active: consumerStep >= 3 }">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">手动ACK</div>
|
||||
<div class="step-desc">确认处理完成</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ack-comparison">
|
||||
<div class="ack-option">
|
||||
<div class="ack-type">自动 ACK</div>
|
||||
<div class="ack-desc">高效但可能丢消息</div>
|
||||
<div class="ack-risk">⚠️ 不推荐</div>
|
||||
</div>
|
||||
|
||||
<div class="ack-option recommended">
|
||||
<div class="ack-type">手动 ACK</div>
|
||||
<div class="ack-desc">可靠,处理完才确认</div>
|
||||
<div class="ack-risk">✅ 推荐</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<div class="control-item">
|
||||
<label>模拟消费</label>
|
||||
<button class="action-btn" @click="simulateConsume" :disabled="consumerStep > 0">
|
||||
开始消费流程
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<span class="info-icon">💡</span>
|
||||
如果处理失败,不发送ACK,Broker会重新投递
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-box">
|
||||
<div class="summary-icon">🎯</div>
|
||||
<div class="summary-content">
|
||||
<strong>三道防线,缺一不可:</strong>生产者确认 → Broker持久化 → 消费者确认
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,36 +209,500 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('消息可靠性演示')
|
||||
const description = ref('展示消息队列如何保证消息的可靠传输,包括消息确认、持久化、重试机制等')
|
||||
// 防线1: 生产者确认
|
||||
const step = ref(0)
|
||||
const ackStatus = ref('')
|
||||
const ackMessage = ref('')
|
||||
|
||||
// 防线2: 存储方式
|
||||
const storageType = ref('disk')
|
||||
|
||||
// 防线3: 消费者确认
|
||||
const consumerStep = ref(0)
|
||||
|
||||
const sendWithAck = () => {
|
||||
step.value = 1
|
||||
ackStatus.value = '发送中...'
|
||||
|
||||
setTimeout(() => {
|
||||
step.value = 2
|
||||
ackStatus.value = '已发送'
|
||||
ackMessage.value = '收到ACK,消息安全'
|
||||
|
||||
setTimeout(() => {
|
||||
step.value = 0
|
||||
ackStatus.value = ''
|
||||
ackMessage.value = ''
|
||||
}, 3000)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const simulateConsume = () => {
|
||||
consumerStep.value = 1
|
||||
|
||||
setTimeout(() => {
|
||||
consumerStep.value = 2
|
||||
setTimeout(() => {
|
||||
consumerStep.value = 3
|
||||
setTimeout(() => {
|
||||
consumerStep.value = 0
|
||||
}, 3000)
|
||||
}, 1500)
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.reliability-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
.demo-header .icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.defense-system {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.defense-line {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.defense-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.defense-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.defense-badge.line1 {
|
||||
background: #3b82f6
|
||||
}
|
||||
|
||||
.defense-badge.line2 {
|
||||
background: #f59e0b
|
||||
}
|
||||
|
||||
.defense-badge.line3 {
|
||||
background: #22c55e
|
||||
}
|
||||
|
||||
.defense-title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.defense-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.flow-diagram {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 12px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.comp-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.comp-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.comp-desc {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
.message-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.msg-item,
|
||||
.ack-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.msg-item.active {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.ack-item.active {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.msg-icon,
|
||||
.ack-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.msg-label,
|
||||
.ack-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.msg-status,
|
||||
.ack-status {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.storage-diagram {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.storage-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.storage-option {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.storage-option.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.storage-option.recommended {
|
||||
border-color: var(--vp-c-success);
|
||||
}
|
||||
|
||||
.storage-option.recommended.active {
|
||||
background: rgba(34, 197, 94, 0.05);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.option-risk {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vs-divider {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.replication-info {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.replication-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.replication-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.replication-detail {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.consumer-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
min-width: 100px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.flow-step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-divider);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.flow-arrow.active {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.ack-comparison {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ack-option {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ack-option.recommended {
|
||||
border-color: var(--vp-c-success);
|
||||
background: rgba(34, 197, 94, 0.05);
|
||||
}
|
||||
|
||||
.ack-type {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ack-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ack-risk {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.control-item label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 20px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
padding: 8px 16px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.info-text.warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.summary-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user