Files
test-repo/docs/.vitepress/theme/components/appendix/queue-design/MessageQueueComponentsDemo.vue
T
sanbuphy d35211071a style: update border-radius and padding values across components
- standardize border-radius from 8px to 6px for consistent styling
- adjust padding values from 1rem to 0.75rem for better visual hierarchy
- remove redundant overflow-y properties for cleaner code
2026-02-14 20:23:34 +08:00

562 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
MessageQueueComponentsDemo.vue
消息队列三要素可视化 - 生产者/Broker/消费者
-->
<template>
<div class="mq-components-demo">
<div class="header">
<div class="title">消息队列的三要素</div>
<div class="subtitle">生产者消息代理消费者的关系</div>
</div>
<div class="components-flow">
<div class="component producer">
<div class="comp-header">
<div class="comp-icon">📤</div>
<div class="comp-name">生产者 Producer</div>
</div>
<div class="comp-content">
<div class="comp-desc">发送消息的一方</div>
<div class="comp-example">例子订单服务</div>
<button
class="action-btn"
@click="produceMessage"
:disabled="producing"
>
{{ producing ? '发送中...' : '发送消息' }}
</button>
</div>
</div>
<div class="arrow" :class="{ active: messageInTransit }">
{{ messageInTransit ? '📨' : '→' }}
</div>
<div class="component broker">
<div class="comp-header">
<div class="comp-icon">📦</div>
<div class="comp-name">消息代理 Broker</div>
</div>
<div class="comp-content">
<div class="comp-desc">存储和转发消息</div>
<div class="comp-example">例子RabbitMQ, Kafka</div>
<div class="broker-storage">
<div class="storage-label">消息存储</div>
<div class="storage-box">
<transition-group name="message">
<div
v-for="msg in brokerMessages"
:key="msg.id"
class="broker-msg"
>
消息 #{{ msg.id }}
</div>
</transition-group>
<div v-if="brokerMessages.length === 0" class="empty">
暂无消息
</div>
</div>
</div>
</div>
</div>
<div class="arrow" :class="{ active: consuming }">
{{ consuming ? '📨' : '→' }}
</div>
<div class="component consumer">
<div class="comp-header">
<div class="comp-icon">📥</div>
<div class="comp-name">消费者 Consumer</div>
</div>
<div class="comp-content">
<div class="comp-desc">接收并处理消息</div>
<div class="comp-example">例子库存服务</div>
<button
class="action-btn consume"
@click="consumeMessage"
:disabled="brokerMessages.length === 0 || consuming"
>
{{ consuming ? '处理中...' : '消费消息' }}
</button>
<div v-if="lastConsumed" class="last-consumed">
已处理: #{{ lastConsumed }}
</div>
</div>
</div>
</div>
<div class="component-details">
<div class="detail-card producer">
<div class="detail-title">📤 生产者 (Producer)</div>
<div class="detail-content">
<div class="detail-item">
<strong>职责</strong>创建并发送消息到 Broker
</div>
<div class="detail-item">
<strong>特点</strong>发送后立即返回不等待处理完成
</div>
<div class="detail-item">
<strong>例子</strong>
<ul>
<li>订单服务下单成功后发送消息</li>
<li>用户服务用户注册后发送消息</li>
<li>支付服务支付完成后发送消息</li>
</ul>
</div>
</div>
</div>
<div class="detail-card broker">
<div class="detail-title">📦 消息代理 (Broker)</div>
<div class="detail-content">
<div class="detail-item">
<strong>职责</strong>存储转发管理消息
</div>
<div class="detail-item">
<strong>特点</strong>
<ul>
<li>消息持久化防止丢失</li>
<li>消息确认机制ACK</li>
<li>支持多种消息模式</li>
</ul>
</div>
<div class="detail-item">
<strong>常见实现</strong>
RabbitMQ, Kafka, RocketMQ, Redis Stream
</div>
</div>
</div>
<div class="detail-card consumer">
<div class="detail-title">📥 消费者 (Consumer)</div>
<div class="detail-content">
<div class="detail-item">
<strong>职责</strong> Broker 接收并处理消息
</div>
<div class="detail-item">
<strong>特点</strong>
<ul>
<li>可以单机或集群部署</li>
<li>处理失败可以重试</li>
<li>处理完成后发送 ACK</li>
</ul>
</div>
<div class="detail-item">
<strong>例子</strong>
<ul>
<li>库存服务扣减库存</li>
<li>短信服务发送通知</li>
<li>积分服务增加积分</li>
</ul>
</div>
</div>
</div>
</div>
<div class="message-flow">
<div class="flow-title">🔄 完整的消息流程</div>
<div class="flow-steps">
<div class="flow-step">
<div class="step-num">1</div>
<div class="step-content">
<div class="step-title">生产者发送消息</div>
<div class="step-desc">订单服务创建订单后发送"订单创建"消息</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<div class="step-num">2</div>
<div class="step-content">
<div class="step-title">Broker 存储消息</div>
<div class="step-desc">消息队列接收并存储消息持久化到磁盘</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<div class="step-num">3</div>
<div class="step-content">
<div class="step-title">消费者拉取消息</div>
<div class="step-desc">库存服务从队列中拉取消息</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<div class="step-num">4</div>
<div class="step-content">
<div class="step-title">处理业务逻辑</div>
<div class="step-desc">扣减库存创建出库记录</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<div class="step-num">5</div>
<div class="step-content">
<div class="step-title">发送 ACK</div>
<div class="step-desc">告诉 Broker 消息处理成功可以删除</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const producing = ref(false)
const consuming = ref(false)
const messageInTransit = ref(false)
const brokerMessages = ref([])
const lastConsumed = ref(null)
let messageId = 0
const produceMessage = () => {
if (producing.value) return
producing.value = true
messageInTransit.value = true
setTimeout(() => {
messageId++
brokerMessages.value.push({ id: messageId })
producing.value = false
messageInTransit.value = false
}, 500)
}
const consumeMessage = () => {
if (consuming.value || brokerMessages.value.length === 0) return
consuming.value = true
const msg = brokerMessages.value.shift()
setTimeout(() => {
consuming.value = false
lastConsumed.value = msg.id
setTimeout(() => {
lastConsumed.value = null
}, 2000)
}, 1000)
}
</script>
<style scoped>
.mq-components-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin-top: 0.25rem;
}
.components-flow {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
align-items: center;
margin-bottom: 2rem;
}
.component {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 0.75rem;
text-align: center;
transition: all 0.3s;
}
.component.producer {
border-color: #3b82f6;
}
.component.broker {
border-color: #8b5cf6;
}
.component.consumer {
border-color: #22c55e;
}
.comp-header {
margin-bottom: 0.75rem;
}
.comp-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.comp-name {
font-weight: 600;
font-size: 0.9rem;
}
.comp-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.comp-example {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 0.75rem;
}
.action-btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-btn {
background: #3b82f6;
color: white;
}
.action-btn.consume {
background: #22c55e;
color: white;
}
.arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
text-align: center;
transition: all 0.3s;
}
.arrow.active {
color: var(--vp-c-brand);
animation: bounce 0.5s ease;
}
.broker-storage {
margin-top: 0.75rem;
}
.storage-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.storage-box {
min-height: 80px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.broker-msg {
padding: 0.35rem 0.5rem;
background: white;
border-radius: 4px;
font-size: 0.75rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
animation: slideIn 0.3s ease;
}
.empty {
text-align: center;
color: var(--vp-c-text-3);
font-size: 0.75rem;
padding: 1rem 0;
}
.last-consumed {
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: #dcfce7;
color: #166534;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.component-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.detail-card {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.detail-card.producer {
border-left: 4px solid #3b82f6;
}
.detail-card.broker {
border-left: 4px solid #8b5cf6;
}
.detail-card.consumer {
border-left: 4px solid #22c55e;
}
.detail-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.detail-content {
font-size: 0.85rem;
line-height: 1.6;
}
.detail-item {
margin-bottom: 0.75rem;
}
.detail-item:last-child {
margin-bottom: 0;
}
.detail-item ul {
margin: 0.35rem 0 0 1rem;
padding: 0;
}
.detail-item li {
margin-bottom: 0.25rem;
color: var(--vp-c-text-2);
}
.message-flow {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.flow-title {
font-weight: 600;
margin-bottom: 1rem;
}
.flow-steps {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: flex-start;
}
.flow-step {
display: flex;
gap: 0.5rem;
flex: 1;
min-width: 150px;
}
.step-num {
width: 24px;
height: 24px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-title {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.25rem;
}
.step-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.flow-arrow {
font-size: 1.2rem;
color: var(--vp-c-text-3);
align-self: center;
padding: 0 0.25rem;
}
.message-enter-active {
transition: all 0.3s ease;
}
.message-enter-from {
opacity: 0;
transform: translateX(-10px);
}
@keyframes bounce {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(5px);
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>