Files
toolbox/resources/js/components/message-sync/MessageSync.vue

312 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.
<template>
<div class="p-4">
<!-- 页面标题 -->
<div class="mb-3 flex items-center justify-between">
<div>
<h1 class="text-lg font-bold text-gray-900">消息同步</h1>
<p class="text-xs text-gray-500 mt-0.5">输入消息ID通过Mono服务重新消费并分发消息</p>
</div>
<button
@click="testConnection"
:disabled="loading.test"
class="px-3 py-1.5 text-xs bg-gray-100 text-gray-600 rounded hover:bg-gray-200 disabled:opacity-50 flex items-center"
>
<svg v-if="loading.test" class="animate-spin -ml-0.5 mr-1.5 h-3 w-3" 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 v-if="error" class="bg-red-50 border border-red-200 rounded px-3 py-2 mb-3 flex items-start text-sm">
<svg class="w-4 h-4 text-red-400 mr-1.5 mt-0.5 shrink-0" 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>
<span class="text-red-700">{{ error }}</span>
</div>
<!-- 输入区域 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-3">
<div class="flex gap-4">
<div class="flex-1">
<div class="flex items-center justify-between mb-1.5">
<label class="text-xs font-medium text-gray-600">消息ID每行一个</label>
<span class="text-xs text-gray-400">{{ messageIdsList.length }} </span>
</div>
<textarea
v-model="messageIdsText"
rows="6"
class="w-full border border-gray-300 rounded px-2.5 py-1.5 text-sm font-mono focus:ring-1 focus:ring-blue-500 focus:border-blue-500 resize-none"
placeholder="af7e5ca7-2779-0e9e-93d1-68c79ceffd9033&#10;bf8f6db8-3880-1f0f-a4e2-79d8adf00144"
></textarea>
</div>
<div class="flex flex-col gap-2 pt-6">
<button
@click="queryMessages"
:disabled="loading.query || messageIdsList.length === 0"
class="px-4 py-1.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center whitespace-nowrap"
>
<svg v-if="loading.query" class="animate-spin -ml-0.5 mr-1.5 h-3.5 w-3.5 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="syncMessages"
:disabled="loading.sync || messageIdsList.length === 0"
class="px-4 py-1.5 text-sm bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center whitespace-nowrap"
>
<svg v-if="loading.sync" class="animate-spin -ml-0.5 mr-1.5 h-3.5 w-3.5 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>
<!-- 查询结果 -->
<div v-if="queryResults" class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-3">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold text-gray-700">查询结果</h2>
<div class="flex gap-4 text-xs">
<span class="text-blue-600">请求 <b>{{ queryResults.stats.total_requested }}</b></span>
<span class="text-green-600">找到 <b>{{ queryResults.stats.total_found }}</b></span>
<span class="text-red-600">缺失 <b>{{ queryResults.stats.total_missing }}</b></span>
<span class="text-purple-600">类型 <b>{{ Object.keys(queryResults.stats.event_types).length }}</b></span>
</div>
</div>
<!-- 消息列表 -->
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead>
<tr class="border-b border-gray-200 text-xs text-gray-500">
<th class="text-left py-2 pr-3 font-medium">消息ID</th>
<th class="text-left py-2 pr-3 font-medium">事件类型</th>
<th class="text-left py-2 pr-3 font-medium">跟踪ID</th>
<th class="text-left py-2 pr-3 font-medium">时间</th>
<th class="text-left py-2 font-medium w-12"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="message in queryResults.messages" :key="message.msg_id" class="hover:bg-gray-50">
<td class="py-1.5 pr-3 font-mono text-xs text-gray-900">{{ message.msg_id }}</td>
<td class="py-1.5 pr-3 text-gray-700">{{ message.event_type }}</td>
<td class="py-1.5 pr-3 font-mono text-xs text-gray-400">{{ message.trace_id }}</td>
<td class="py-1.5 pr-3 text-xs text-gray-500 whitespace-nowrap">{{ formatTimestamp(message.timestamp) }}</td>
<td class="py-1.5">
<button @click="showMessageDetail(message)" class="text-blue-500 hover:text-blue-700 text-xs">详情</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 同步结果 -->
<div v-if="syncResults" class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold text-gray-700">同步结果</h2>
<div class="flex gap-4 text-xs">
<span class="text-blue-600">总计 <b>{{ syncResults.summary.total }}</b></span>
<span class="text-green-600">成功 <b>{{ syncResults.summary.success }}</b></span>
<span class="text-red-600">失败 <b>{{ syncResults.summary.failure }}</b></span>
</div>
</div>
<!-- 同步结果列表 -->
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead>
<tr class="border-b border-gray-200 text-xs text-gray-500">
<th class="text-left py-2 pr-3 font-medium">消息ID</th>
<th class="text-left py-2 pr-3 font-medium w-16">状态</th>
<th class="text-left py-2 pr-3 font-medium">响应</th>
<th class="text-left py-2 font-medium w-12"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="result in syncResults.results" :key="result.msg_id" class="hover:bg-gray-50">
<td class="py-1.5 pr-3 font-mono text-xs text-gray-900">{{ result.msg_id }}</td>
<td class="py-1.5 pr-3">
<span v-if="result.success" class="inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">成功</span>
<span v-else class="inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">失败</span>
</td>
<td class="py-1.5 pr-3 text-xs text-gray-500 max-w-md truncate">
{{ result.success ? '消息消费成功' : result.error }}
</td>
<td class="py-1.5">
<button @click="showSyncDetail(result)" class="text-blue-500 hover:text-blue-700 text-xs">详情</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 详情模态框 -->
<div v-if="showDetailModal" class="fixed inset-0 overflow-y-auto h-full w-full z-50" @click.self="closeDetailModal">
<div class="relative top-16 mx-auto p-4 border w-11/12 md:w-2/3 lg:w-1/2 shadow-lg rounded-lg bg-white">
<div class="flex justify-between items-center mb-3">
<h3 class="text-sm font-semibold text-gray-900">详细信息</h3>
<button @click="closeDetailModal" class="text-gray-400 hover:text-gray-600">
<svg class="w-5 h-5" 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="max-h-[70vh] overflow-y-auto">
<pre class="bg-gray-50 p-3 rounded text-xs font-mono overflow-x-auto leading-relaxed">{{ JSON.stringify(selectedDetail, null, 2) }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'MessageSync',
data() {
return {
messageIdsText: '',
queryResults: null,
syncResults: null,
loading: {
query: false,
sync: false,
test: false
},
error: '',
showDetailModal: false,
selectedDetail: null
}
},
computed: {
messageIdsList() {
return this.messageIdsText
.split('\n')
.map(id => id.trim())
.filter(id => id.length > 0);
}
},
methods: {
async queryMessages() {
if (this.messageIdsList.length === 0) {
this.error = '请输入至少一个消息ID';
return;
}
this.loading.query = true;
this.error = '';
this.queryResults = null;
try {
const response = await fetch('/api/message-sync/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
message_ids: this.messageIdsList
})
});
const data = await response.json();
if (data.success) {
this.queryResults = data.data;
} else {
this.error = data.message;
}
} catch (error) {
this.error = '网络请求失败: ' + error.message;
} finally {
this.loading.query = false;
}
},
async syncMessages() {
if (this.messageIdsList.length === 0) {
this.error = '请输入至少一个消息ID';
return;
}
this.loading.sync = true;
this.error = '';
this.syncResults = null;
try {
const response = await fetch('/api/message-sync/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
message_ids: this.messageIdsList
})
});
const data = await response.json();
if (data.success) {
this.syncResults = data.data;
} else {
this.error = data.message;
}
} catch (error) {
this.error = '网络请求失败: ' + error.message;
} finally {
this.loading.sync = false;
}
},
async testConnection() {
this.loading.test = true;
this.error = '';
try {
const response = await fetch('/api/message-sync/test-connection');
const data = await response.json();
if (data.success) {
alert('数据库连接测试成功!');
} else {
this.error = data.message;
}
} catch (error) {
this.error = '网络请求失败: ' + error.message;
} finally {
this.loading.test = false;
}
},
showMessageDetail(message) {
this.selectedDetail = message;
this.showDetailModal = true;
},
showSyncDetail(result) {
this.selectedDetail = result;
this.showDetailModal = true;
},
closeDetailModal() {
this.showDetailModal = false;
this.selectedDetail = null;
},
formatTimestamp(timestamp) {
if (!timestamp) return '-';
return new Date(timestamp * 1000).toLocaleString();
}
}
}
</script>