Files
toolbox/resources/js/components/message-sync/EventConsumerSync.vue
2025-12-02 10:16:32 +08:00

374 lines
15 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.
<template>
<div class="p-6">
<!-- 页面标题 -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">事件消费者同步对比</h1>
<p class="text-gray-600 mt-2">对比CRM和Agent的事件消费者消息找出缺失的消息</p>
</div>
<!-- 查询条件 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">查询条件</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">开始时间</label>
<input
v-model="startTime"
type="datetime-local"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">结束时间</label>
<input
v-model="endTime"
type="datetime-local"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">消息名称 (可选)</label>
<input
v-model="messageName"
type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="输入要查询的消息名称,留空则查询所有"
/>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">排除的消息名称 (每行一个)</label>
<textarea
v-model="excludeMessagesText"
rows="4"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="输入要排除的消息名称,每行一个"
></textarea>
</div>
<div class="flex space-x-4">
<button
@click="compareSync"
:disabled="loading"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
>
<svg v-if="loading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
对比同步状态
</button>
<button
@click="exportMissing"
:disabled="loading || !compareResult"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
>
<svg v-if="loading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
导出缺失消息
</button>
</div>
</div>
<!-- 错误信息 -->
<div v-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div class="flex">
<svg class="w-5 h-5 text-red-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="text-sm font-medium text-red-800">错误</h3>
<p class="text-sm text-red-700 mt-1">{{ error }}</p>
</div>
</div>
</div>
<!-- 对比结果 -->
<div v-if="compareResult" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">对比结果</h2>
<!-- 统计信息 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-blue-50 rounded-lg p-4">
<div class="text-2xl font-bold text-blue-600">{{ compareResult.crm_total }}</div>
<div class="text-sm text-blue-600">CRM消息总数</div>
</div>
<div class="bg-green-50 rounded-lg p-4">
<div class="text-2xl font-bold text-green-600">{{ compareResult.agent_total }}</div>
<div class="text-sm text-green-600">Agent消息总数</div>
</div>
<div class="bg-red-50 rounded-lg p-4">
<div class="text-2xl font-bold text-red-600">{{ compareResult.missing_count }}</div>
<div class="text-sm text-red-600">缺失消息数</div>
</div>
<div class="bg-purple-50 rounded-lg p-4">
<div class="text-2xl font-bold text-purple-600">{{ compareResult.sync_rate }}%</div>
<div class="text-sm text-purple-600">同步率</div>
</div>
</div>
<!-- 按Topic统计缺失消息 -->
<div v-if="compareResult.missing_by_topic && compareResult.missing_by_topic.length > 0" class="mb-6">
<h3 class="text-lg font-semibold text-gray-700 mb-4">按Topic统计缺失消息</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Topic名称</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">缺失数量</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">占比</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="item in compareResult.missing_by_topic" :key="item.topic">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ item.topic }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">
{{ item.count }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ ((item.count / compareResult.missing_count) * 100).toFixed(2) }}%
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 缺失消息列表 -->
<div v-if="compareResult.missing_messages.length > 0">
<h3 class="text-lg font-semibold text-gray-700 mb-4">缺失的消息详情</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">消息ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">消息名称</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">消息体</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">创建时间</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="msg in compareResult.missing_messages" :key="msg.msg_id">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">{{ msg.msg_id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ msg.event_name }}</td>
<td class="px-6 py-4 text-sm">
<button
@click="showDetail(msg)"
class="text-blue-600 hover:text-blue-800 hover:underline"
>
查看详情
</button>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDateTime(msg.created) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-else class="bg-green-50 border border-green-200 rounded-lg p-4">
<p class="text-green-700">所有消息都已同步到Agent</p>
</div>
</div>
<!-- 消息详情弹窗 -->
<div v-if="showDetailModal" class="fixed inset-0 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-96 overflow-y-auto">
<div class="sticky top-0 bg-gray-50 border-b border-gray-200 px-6 py-4 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-900">消息详情</h3>
<button
@click="showDetailModal = false"
class="text-gray-400 hover:text-gray-600"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="px-6 py-4">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">消息ID</label>
<p class="text-sm text-gray-900 font-mono break-all">{{ selectedMessage?.msg_id }}</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">消息名称</label>
<p class="text-sm text-gray-900">{{ selectedMessage?.event_name }}</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">消息体</label>
<div class="bg-gray-50 rounded p-3 max-h-48 overflow-y-auto">
<pre class="text-sm text-gray-900 whitespace-pre-wrap break-words">{{ selectedMessage?.msg_body }}</pre>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">创建时间</label>
<p class="text-sm text-gray-900">{{ formatDateTime(selectedMessage?.created) }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">更新时间</label>
<p class="text-sm text-gray-900">{{ formatDateTime(selectedMessage?.updated) }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'EventConsumerSync',
data() {
return {
startTime: '',
endTime: '',
messageName: '',
excludeMessagesText: '',
compareResult: null,
loading: false,
error: '',
showDetailModal: false,
selectedMessage: null
}
},
computed: {
excludeMessages() {
return this.excludeMessagesText
.split('\n')
.map(msg => msg.trim())
.filter(msg => msg.length > 0);
}
},
methods: {
async compareSync() {
this.loading = true;
this.error = '';
try {
const payload = {
exclude_messages: this.excludeMessages
};
if (this.startTime) {
payload.start_time = this.formatDateTimeForAPI(this.startTime);
}
if (this.endTime) {
payload.end_time = this.formatDateTimeForAPI(this.endTime);
}
if (this.messageName.trim()) {
payload.message_name = this.messageName.trim();
}
const response = await fetch('/api/message-sync/compare-event-consumer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.success) {
this.compareResult = data.data;
} else {
this.error = data.message;
}
} catch (error) {
this.error = '网络请求失败: ' + error.message;
} finally {
this.loading = false;
}
},
exportMissing() {
if (!this.compareResult || this.compareResult.missing_messages.length === 0) {
alert('没有缺失的消息可以导出');
return;
}
try {
// 准备导出数据
const exportData = {
export_time: new Date().toLocaleString(),
query_conditions: {
start_time: this.startTime || '未设置',
end_time: this.endTime || '未设置',
message_name: this.messageName || '未设置',
exclude_messages: this.excludeMessages.length > 0 ? this.excludeMessages : []
},
summary: {
crm_total: this.compareResult.crm_total,
agent_total: this.compareResult.agent_total,
missing_count: this.compareResult.missing_count,
sync_rate: this.compareResult.sync_rate + '%'
},
missing_messages: this.compareResult.missing_messages.map(msg => ({
msg_id: msg.msg_id,
event_name: msg.event_name,
msg_body: msg.msg_body,
created: msg.created,
updated: msg.updated
}))
};
// 生成JSON文件
const jsonString = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
// 生成文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, -5);
const filename = `missing_messages_${timestamp}.json`;
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
alert(`成功导出 ${this.compareResult.missing_messages.length} 条缺失消息`);
} catch (error) {
this.error = '导出失败: ' + error.message;
}
},
showDetail(message) {
this.selectedMessage = message;
this.showDetailModal = true;
},
formatDateTime(dateString) {
if (!dateString) return '-';
return new Date(dateString).toLocaleString();
},
formatDateTimeForAPI(dateTimeLocal) {
if (!dateTimeLocal) return null;
const date = new Date(dateTimeLocal);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
}
}
</script>