Files
test-repo/docs/.vitepress/theme/components/appendix/queue-design/PubSubDemo.vue
T

669 lines
14 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.
<!--
PubSubDemo.vue
发布订阅模式演示 - 一条消息多消费者
-->
<template>
<div class="pubsub-demo">
<div class="header">
<div class="title">发布订阅模式一条消息多处消费</div>
<div class="subtitle">发布一次事件多个订阅者独立处理</div>
</div>
<div class="main-flow">
<div class="publisher-section">
<div class="section-title">📤 发布者 Publisher</div>
<div class="event-selector">
<label>选择事件</label>
<select v-model="selectedEvent" @change="onEventChange">
<option value="order.created">订单创建成功</option>
<option value="user.registered">用户注册成功</option>
<option value="product.updated">商品信息更新</option>
</select>
</div>
<div class="event-details">
<div class="event-name">{{ eventDetails.name }}</div>
<div class="event-desc">{{ eventDetails.description }}</div>
</div>
<button
class="publish-btn"
@click="publishEvent"
:disabled="publishing"
>
{{ publishing ? '发布中...' : '🚀 发布事件' }}
</button>
</div>
<div class="topic-section">
<div class="section-title">📡 主题 Topic</div>
<div class="topic-box" :class="{ active: hasMessage }">
<div class="topic-icon">📨</div>
<div class="topic-name">{{ selectedEvent }}</div>
<div v-if="hasMessage" class="message-indicator">消息已发布</div>
</div>
<div class="topic-desc">所有订阅者都会收到这条消息</div>
</div>
<div class="subscribers-section">
<div class="section-title">📥 订阅者 Subscribers</div>
<div class="subscribers-grid">
<div
v-for="sub in currentSubscribers"
:key="sub.id"
class="subscriber-card"
:class="{ processing: sub.processing, completed: sub.completed }"
>
<div class="sub-icon">{{ sub.icon }}</div>
<div class="sub-name">{{ sub.name }}</div>
<div class="sub-action">{{ sub.action }}</div>
<div class="sub-status">
<span v-if="sub.processing"> 处理中...</span>
<span v-else-if="sub.completed"> 已完成</span>
<span v-else>💤 等待消息</span>
</div>
<div class="sub-count">已处理: {{ sub.count }} </div>
</div>
</div>
</div>
</div>
<div class="real-time-log">
<div class="log-header">
<div class="log-title">📋 实时日志</div>
<button class="clear-btn" @click="clearLog">清空</button>
</div>
<div class="log-content">
<div v-if="logs.length === 0" class="log-empty">暂无日志</div>
<div
v-for="(log, index) in logs"
:key="index"
class="log-entry"
:class="log.type"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
<div class="use-cases">
<div class="case-title">💡 典型应用场景</div>
<div class="case-grid">
<div class="case-card">
<div class="case-icon">🛒</div>
<div class="case-name">电商订单</div>
<div class="case-desc">
订单创建 库存服务积分服务通知服务数据仓库同时处理
</div>
</div>
<div class="case-card">
<div class="case-icon">👤</div>
<div class="case-name">用户注册</div>
<div class="case-desc">
用户注册 欢迎邮件短信验证发放优惠券创建用户画像
</div>
</div>
<div class="case-card">
<div class="case-icon">📊</div>
<div class="case-name">数据分析</div>
<div class="case-desc">
用户行为 推荐系统实时统计数据仓库风控系统
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedEvent = ref('order.created')
const publishing = ref(false)
const hasMessage = ref(false)
const logs = ref([])
const eventConfigs = {
'order.created': {
name: '订单创建成功',
description: '用户完成支付,订单创建成功',
subscribers: [
{
id: 1,
name: '库存服务',
icon: '📦',
action: '扣减库存',
processing: false,
completed: false,
count: 0
},
{
id: 2,
name: '积分服务',
icon: '💎',
action: '增加积分',
processing: false,
completed: false,
count: 0
},
{
id: 3,
name: '短信服务',
icon: '📱',
action: '发送短信',
processing: false,
completed: false,
count: 0
},
{
id: 4,
name: '邮件服务',
icon: '📧',
action: '发送邮件',
processing: false,
completed: false,
count: 0
},
{
id: 5,
name: '数据仓库',
icon: '📊',
action: '记录订单数据',
processing: false,
completed: false,
count: 0
}
]
},
'user.registered': {
name: '用户注册成功',
description: '新用户完成注册流程',
subscribers: [
{
id: 1,
name: '欢迎邮件',
icon: '📧',
action: '发送欢迎邮件',
processing: false,
completed: false,
count: 0
},
{
id: 2,
name: '短信验证',
icon: '📱',
action: '发送验证短信',
processing: false,
completed: false,
count: 0
},
{
id: 3,
name: '优惠券服务',
icon: '🎫',
action: '发放新用户券',
processing: false,
completed: false,
count: 0
},
{
id: 4,
name: '用户画像',
icon: '👤',
action: '创建用户档案',
processing: false,
completed: false,
count: 0
}
]
},
'product.updated': {
name: '商品信息更新',
description: '商家更新商品信息',
subscribers: [
{
id: 1,
name: '搜索服务',
icon: '🔍',
action: '更新搜索索引',
processing: false,
completed: false,
count: 0
},
{
id: 2,
name: '推荐服务',
icon: '⭐',
action: '更新推荐列表',
processing: false,
completed: false,
count: 0
},
{
id: 3,
name: '缓存服务',
icon: '⚡',
action: '刷新缓存',
processing: false,
completed: false,
count: 0
}
]
}
}
const subscribers = ref(
JSON.parse(JSON.stringify(eventConfigs['order.created'].subscribers))
)
const eventDetails = computed(() => {
return eventConfigs[selectedEvent.value]
})
const currentSubscribers = computed(() => {
return subscribers.value
})
const onEventChange = () => {
subscribers.value = JSON.parse(
JSON.stringify(eventConfigs[selectedEvent.value].subscribers)
)
hasMessage.value = false
}
const publishEvent = () => {
if (publishing.value) return
publishing.value = true
hasMessage.value = true
addLog('info', `📤 发布事件: ${eventDetails.value.name}`)
// 所有订阅者都收到消息
subscribers.value.forEach((sub, index) => {
setTimeout(() => {
sub.processing = true
sub.completed = false
addLog('info', `📥 ${sub.name} 开始处理`)
// 模拟处理时间
setTimeout(
() => {
sub.processing = false
sub.completed = true
sub.count++
addLog('success', `${sub.name} 处理完成: ${sub.action}`)
setTimeout(() => {
sub.completed = false
}, 2000)
},
1500 + Math.random() * 1000
)
}, index * 200)
})
setTimeout(() => {
publishing.value = false
setTimeout(() => {
hasMessage.value = false
}, 1000)
}, 3000)
}
const addLog = (type, message) => {
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({ type, time, message })
logs.value = logs.value.slice(0, 20)
}
const clearLog = () => {
logs.value = []
}
</script>
<style scoped>
.pubsub-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;
}
.main-flow {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.section-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
text-align: center;
margin-bottom: 0.75rem;
text-transform: uppercase;
}
.publisher-section,
.topic-section,
.subscribers-section {
display: flex;
flex-direction: column;
align-items: center;
}
.event-selector {
width: 100%;
margin-bottom: 1rem;
}
.event-selector label {
display: block;
font-size: 0.85rem;
margin-bottom: 0.4rem;
color: var(--vp-c-text-2);
}
.event-selector select {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 0.9rem;
}
.event-details {
text-align: center;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 8px;
width: 100%;
}
.event-name {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.35rem;
}
.event-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.publish-btn {
width: 100%;
padding: 0.75rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.publish-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.publish-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.topic-box {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
text-align: center;
min-width: 160px;
transition: all 0.3s;
}
.topic-box.active {
border-color: var(--vp-c-brand);
background: rgba(59, 130, 246, 0.1);
animation: pulse 2s infinite;
}
.topic-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.topic-name {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.5rem;
font-family: monospace;
}
.message-indicator {
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: #dcfce7;
color: #166534;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.topic-desc {
margin-top: 0.75rem;
font-size: 0.8rem;
color: var(--vp-c-text-3);
text-align: center;
}
.subscribers-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
}
.subscriber-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
transition: all 0.3s;
}
.subscriber-card.processing {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.05);
}
.subscriber-card.completed {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.05);
}
.sub-icon {
font-size: 1.5rem;
margin-bottom: 0.25rem;
text-align: center;
}
.sub-name {
font-size: 0.85rem;
font-weight: 600;
text-align: center;
margin-bottom: 0.25rem;
}
.sub-action {
font-size: 0.75rem;
color: var(--vp-c-text-2);
text-align: center;
margin-bottom: 0.35rem;
}
.sub-status {
font-size: 0.75rem;
text-align: center;
margin-bottom: 0.25rem;
}
.sub-count {
font-size: 0.7rem;
color: var(--vp-c-text-3);
text-align: center;
}
.real-time-log {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.log-title {
font-weight: 600;
font-size: 0.9rem;
}
.clear-btn {
padding: 0.35rem 0.75rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
}
.clear-btn:hover {
background: var(--vp-c-divider);
}
.log-content {
max-height: 250px;
overflow-y: auto;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.75rem;
font-size: 0.8rem;
}
.log-empty {
text-align: center;
color: var(--vp-c-text-3);
padding: 1rem;
}
.log-entry {
padding: 0.4rem 0;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
gap: 0.5rem;
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: var(--vp-c-text-3);
font-family: monospace;
}
.log-message {
flex: 1;
}
.log-entry.info .log-message {
color: var(--vp-c-text-1);
}
.log-entry.success .log-message {
color: #16a34a;
}
.use-cases {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.case-title {
font-weight: 600;
margin-bottom: 1rem;
}
.case-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.case-card {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.case-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.case-name {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.case-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
}
50% {
box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
}
}
</style>