2026-01-18 12:21:49 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
DeadLetterQueueDemo.vue
|
|
|
|
|
|
死信队列演示 - 处理失败消息
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="dlq-demo">
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="demo-header">
|
|
|
|
|
|
<span class="icon">🚑</span>
|
|
|
|
|
|
<span class="title">死信队列</span>
|
|
|
|
|
|
<span class="subtitle">消息的"急救站" - 处理失败消息</span>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<div class="control">
|
|
|
|
|
|
<label>失败率:</label>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<input
|
|
|
|
|
|
v-model.number="failureRate"
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
max="100"
|
|
|
|
|
|
step="10"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<span class="value">{{ failureRate }}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="control">
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<label>最大重试:</label>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<input
|
|
|
|
|
|
v-model.number="maxRetries"
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
max="5"
|
|
|
|
|
|
step="1"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<span class="value">{{ maxRetries }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="demo-content">
|
|
|
|
|
|
<div class="flow-container">
|
|
|
|
|
|
<div class="main-queue-section">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
📦 主队列
|
|
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<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 }"
|
|
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="msg-id">
|
|
|
|
|
|
#{{ msg.id }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="msg.retries > 0"
|
|
|
|
|
|
class="msg-retries"
|
|
|
|
|
|
>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
重试: {{ msg.retries }}/{{ maxRetries }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="mainQueue.length === 0"
|
|
|
|
|
|
class="empty"
|
|
|
|
|
|
>
|
|
|
|
|
|
队列为空
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="mainQueue.length > 3"
|
|
|
|
|
|
class="more"
|
|
|
|
|
|
>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
还有 {{ mainQueue.length - 3 }} 条...
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<button
|
|
|
|
|
|
class="add-btn"
|
|
|
|
|
|
:disabled="processing"
|
|
|
|
|
|
@click="addMessage"
|
|
|
|
|
|
>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
+ 添加消息
|
|
|
|
|
|
</button>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="processing-section">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
⚙️ 消费处理
|
|
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="processor-box">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="processor-icon"
|
|
|
|
|
|
:class="{ active: processing }"
|
|
|
|
|
|
>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
{{ processing ? '⚙️' : '💤' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="processor-status">
|
|
|
|
|
|
{{ processing ? '处理中...' : '空闲' }}
|
|
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="currentMessage"
|
|
|
|
|
|
class="current-msg"
|
|
|
|
|
|
>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
处理: #{{ currentMessage.id }}
|
|
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="lastResult"
|
|
|
|
|
|
class="last-result"
|
|
|
|
|
|
:class="lastResult.type"
|
|
|
|
|
|
>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
{{ lastResult.message }}
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="dlq-section">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
⚠️ 死信队列
|
|
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="queue-box dead-letter">
|
|
|
|
|
|
<div class="queue-header">
|
|
|
|
|
|
<span>失败消息</span>
|
|
|
|
|
|
<span class="count">{{ deadLetterQueue.length }} 条</span>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="message-list">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="msg in deadLetterQueue.slice(0, 2)"
|
|
|
|
|
|
:key="msg.id"
|
|
|
|
|
|
class="message-item failed"
|
|
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="msg-id">
|
|
|
|
|
|
#{{ msg.id }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="msg-error">
|
|
|
|
|
|
{{ msg.error }}
|
|
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="deadLetterQueue.length === 0"
|
|
|
|
|
|
class="empty"
|
|
|
|
|
|
>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
无失败消息
|
|
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="deadLetterQueue.length > 2"
|
|
|
|
|
|
class="more"
|
|
|
|
|
|
>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
还有 {{ deadLetterQueue.length - 2 }} 条...
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<button
|
|
|
|
|
|
class="retry-btn"
|
|
|
|
|
|
:disabled="deadLetterQueue.length === 0"
|
2026-02-18 17:38:10 +08:00
|
|
|
|
@click="retryDeadLetters"
|
2026-02-14 12:14:07 +08:00
|
|
|
|
>
|
|
|
|
|
|
🔄 重试死信
|
|
|
|
|
|
</button>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="stats">
|
|
|
|
|
|
<div class="stat-card">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="stat-label">
|
|
|
|
|
|
总消息数
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-value">
|
|
|
|
|
|
{{ totalMessages }}
|
|
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card success">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="stat-label">
|
|
|
|
|
|
成功处理
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-value">
|
|
|
|
|
|
{{ successCount }}
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="stat-card warning">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="stat-label">
|
|
|
|
|
|
进入死信
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-value">
|
|
|
|
|
|
{{ deadLetterCount }}
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
<div class="stat-card">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="stat-label">
|
|
|
|
|
|
成功率
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-value">
|
|
|
|
|
|
{{ successRate }}%
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-14 12:14:07 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="info-box">
|
|
|
|
|
|
<span class="icon">💡</span>
|
|
|
|
|
|
<strong>核心思想:</strong>失败消息进入死信队列,避免阻塞正常消息,可后续人工介入或自动重试
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
const failureRate = ref(30)
|
|
|
|
|
|
const maxRetries = ref(3)
|
|
|
|
|
|
const processing = ref(false)
|
|
|
|
|
|
const currentMessage = ref(null)
|
|
|
|
|
|
const lastResult = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
let messageId = 0
|
|
|
|
|
|
const mainQueue = ref([])
|
|
|
|
|
|
const deadLetterQueue = ref([])
|
|
|
|
|
|
const successCount = ref(0)
|
|
|
|
|
|
|
|
|
|
|
|
const totalMessages = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
successCount.value + deadLetterQueue.value.length + mainQueue.value.length
|
|
|
|
|
|
)
|
|
|
|
|
|
const deadLetterCount = computed(() => deadLetterQueue.value.length)
|
|
|
|
|
|
const successRate = computed(() => {
|
|
|
|
|
|
if (totalMessages.value === 0) return 0
|
|
|
|
|
|
return Math.round((successCount.value / totalMessages.value) * 100)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
let addMessage = () => {
|
|
|
|
|
|
messageId++
|
|
|
|
|
|
mainQueue.value.push({
|
|
|
|
|
|
id: messageId,
|
|
|
|
|
|
retries: 0,
|
|
|
|
|
|
processing: false
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const processNext = () => {
|
|
|
|
|
|
if (mainQueue.value.length === 0 || processing.value) {
|
|
|
|
|
|
processing.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
let msg = mainQueue.value[0]
|
2026-01-18 12:21:49 +08:00
|
|
|
|
msg.processing = true
|
|
|
|
|
|
processing.value = true
|
|
|
|
|
|
currentMessage.value = msg
|
|
|
|
|
|
lastResult.value = null
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const shouldFail = Math.random() * 100 < failureRate.value
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldFail) {
|
|
|
|
|
|
msg.retries++
|
|
|
|
|
|
msg.processing = false
|
|
|
|
|
|
|
|
|
|
|
|
if (msg.retries >= maxRetries.value) {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
// 超过最大重试次数,进入死信队列
|
2026-01-18 12:21:49 +08:00
|
|
|
|
mainQueue.value.shift()
|
|
|
|
|
|
deadLetterQueue.value.push({
|
|
|
|
|
|
id: msg.id,
|
|
|
|
|
|
error: `重试 ${msg.retries} 次后仍失败`
|
|
|
|
|
|
})
|
|
|
|
|
|
lastResult.value = {
|
|
|
|
|
|
type: 'error',
|
|
|
|
|
|
message: `❌ 消息 #${msg.id} 进入死信队列`
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 重新入队
|
|
|
|
|
|
lastResult.value = {
|
|
|
|
|
|
type: 'warning',
|
2026-02-14 12:14:07 +08:00
|
|
|
|
message: `⚠️ 消息 #${msg.id} 处理失败,重试 ${msg.retries}/${maxRetries.value}`
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(processNext, 500)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 成功处理
|
|
|
|
|
|
mainQueue.value.shift()
|
|
|
|
|
|
successCount.value++
|
|
|
|
|
|
msg.processing = false
|
|
|
|
|
|
currentMessage.value = null
|
|
|
|
|
|
lastResult.value = {
|
|
|
|
|
|
type: 'success',
|
|
|
|
|
|
message: `✅ 消息 #${msg.id} 处理成功`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(processNext, 300)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const retryDeadLetters = () => {
|
|
|
|
|
|
const failed = deadLetterQueue.value.splice(0)
|
|
|
|
|
|
failed.forEach((msg) => {
|
|
|
|
|
|
msg.retries = 0
|
|
|
|
|
|
mainQueue.value.push(msg)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!processing.value && mainQueue.value.length > 0) {
|
|
|
|
|
|
processNext()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 自动开始处理
|
|
|
|
|
|
const startProcessing = () => {
|
|
|
|
|
|
if (!processing.value && mainQueue.value.length > 0) {
|
|
|
|
|
|
processNext()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听队列变化
|
|
|
|
|
|
const checkAndProcess = () => {
|
|
|
|
|
|
startProcessing()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加消息后自动开始处理
|
|
|
|
|
|
const originalAddMessage = addMessage
|
|
|
|
|
|
const addMessageWithAutoProcess = () => {
|
|
|
|
|
|
originalAddMessage()
|
|
|
|
|
|
checkAndProcess()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 覆盖 addMessage 方法
|
|
|
|
|
|
addMessage = addMessageWithAutoProcess
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.dlq-demo {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
.demo-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
margin-bottom: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
.demo-header .icon {
|
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.demo-header .title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
.demo-header .subtitle {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
color: var(--vp-c-text-2);
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
margin-left: 0.5rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
|
gap: 1rem;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
margin-bottom: 1rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.5rem;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.85rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control input[type='range'] {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control .value {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
min-width: 3rem;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
.demo-content {
|
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 12:21:49 +08:00
|
|
|
|
.flow-container {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
|
gap: 1rem;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
margin-bottom: 1rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
text-align: center;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
margin-bottom: 0.5rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.queue-box {
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 2px solid var(--vp-c-divider);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.queue-box.main-queue {
|
|
|
|
|
|
border-color: var(--vp-c-brand);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.queue-box.dead-letter {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
border-color: var(--vp-c-danger);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.queue-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-list {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
max-height: 150px;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
|
2026-01-18 12:21:49 +08:00
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
margin-bottom: 0.4rem;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-item.processing {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
border: 1px solid var(--vp-c-warning);
|
|
|
|
|
|
background: var(--vp-c-warning-soft);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-item.failed {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
border: 1px solid var(--vp-c-danger);
|
|
|
|
|
|
background: var(--vp-c-danger-soft);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.msg-id {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.msg-retries {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
color: var(--vp-c-warning);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.msg-error {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
color: var(--vp-c-danger);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
.empty, .more {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
text-align: center;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
padding: 1rem 0.5rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
color: var(--vp-c-text-3);
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-btn,
|
|
|
|
|
|
.retry-btn {
|
|
|
|
|
|
width: 100%;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
padding: 0.5rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
border: none;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-weight: 600;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
margin-top: 0.5rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-btn {
|
|
|
|
|
|
background: var(--vp-c-brand);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-btn:hover:not(:disabled) {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
opacity: 0.9;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-btn:disabled {
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.retry-btn {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
background: var(--vp-c-warning);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.retry-btn:hover:not(:disabled) {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
opacity: 0.8;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.processor-box {
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 2px solid var(--vp-c-divider);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
text-align: center;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
min-height: 150px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.processor-icon {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 2rem;
|
|
|
|
|
|
margin-bottom: 0.5rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.processor-icon.active {
|
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.processor-status {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.8rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.current-msg {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.last-result {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
margin-top: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.last-result.success {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
background: var(--vp-c-success);
|
|
|
|
|
|
color: white;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.last-result.warning {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
background: var(--vp-c-warning-soft);
|
|
|
|
|
|
color: var(--vp-c-warning-dark);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.last-result.error {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
background: var(--vp-c-danger-soft);
|
|
|
|
|
|
color: var(--vp-c-danger-dark);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats {
|
|
|
|
|
|
display: grid;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
2026-01-18 12:21:49 +08:00
|
|
|
|
gap: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
background: var(--vp-c-bg);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
text-align: center;
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card.success {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
border-color: var(--vp-c-success);
|
|
|
|
|
|
background: var(--vp-c-success-soft);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card.warning {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
border-color: var(--vp-c-danger);
|
|
|
|
|
|
background: var(--vp-c-danger-soft);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 0.7rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
margin-bottom: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
2026-02-14 12:14:07 +08:00
|
|
|
|
font-size: 1.1rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
.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;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
display: flex;
|
2026-02-14 12:14:07 +08:00
|
|
|
|
gap: 0.25rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 12:14:07 +08:00
|
|
|
|
.info-box .icon {
|
|
|
|
|
|
flex-shrink: 0;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
|
from {
|
|
|
|
|
|
transform: rotate(0deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
transform: rotate(360deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|