374 lines
15 KiB
Vue
374 lines
15 KiB
Vue
<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>
|
||
|