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:
sanbuphy
2026-02-14 12:14:07 +08:00
parent cd2ce9e661
commit ebe2bf6109
70 changed files with 12307 additions and 10445 deletions
@@ -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>