|
|
@@ -5,8 +5,7 @@
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>套利流程监控</title>
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
- <link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
|
|
|
- <script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest" type="text/javascript"></script>
|
|
|
+ <!-- 不再需要 simple-datatables 的 CSS 和 JS -->
|
|
|
<style>
|
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,"Helvetica Neue", Arial, "Noto Sans", sans-serif,"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: #f7fafc; }
|
|
|
.modal { transition: opacity 0.25s ease; }
|
|
|
@@ -15,15 +14,11 @@
|
|
|
.log-msg { white-space: pre-wrap; word-break: break-all; font-size: 0.75rem; line-height: 1.5; font-family: 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", monospace; background-color: #f9fafb; padding: 4px 6px; border: 1px solid #e5e7eb; border-radius: 4px; margin-top: 2px; }
|
|
|
.table-responsive-container { overflow-x: auto; }
|
|
|
.status-success { color: #10b981; } .status-fail { color: #ef4444; } .status-pending { color: #f59e0b; } .status-info { color: #3b82f6; }
|
|
|
- .datatable-wrapper { padding: 0; }
|
|
|
- .datatable-container { border: 1px solid #e2e8f0; border-radius: 0.375rem; }
|
|
|
- .datatable-top { padding: 0.5rem; }
|
|
|
- .datatable-input { padding: 0.3rem 0.6rem; font-size: 0.875rem; border-radius: 0.375rem; border: 1px solid #d1d5db; }
|
|
|
- .datatable-table { font-size: 0.875rem; }
|
|
|
- .datatable-table th, .datatable-table td { padding: 0.6rem 0.8rem; border-bottom: 1px solid #e2e8f0; }
|
|
|
- .datatable-bottom { display: none; }
|
|
|
- .datatable-sorter { color: #4b5563; text-decoration: none; }
|
|
|
- .datatable-sorter::before, .datatable-sorter::after { border-color: #9ca3af; }
|
|
|
+ /* 简单表格样式 */
|
|
|
+ .simple-table { font-size: 0.875rem; min-width: 100%; }
|
|
|
+ .simple-table th, .simple-table td { padding: 0.6rem 0.8rem; border-bottom: 1px solid #e2e8f0; text-align: left; }
|
|
|
+ .simple-table thead { background-color: #f9fafb; }
|
|
|
+ .simple-table th { font-medium text-xs text-gray-500 uppercase tracking-wider; }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body class="text-gray-800 antialiased">
|
|
|
@@ -49,21 +44,21 @@
|
|
|
<div id="tabContent">
|
|
|
<div class="tab-pane bg-white rounded-lg shadow-sm" id="processing" role="tabpanel">
|
|
|
<div class="table-responsive-container">
|
|
|
- <table class="min-w-full text-sm">
|
|
|
- <thead class="bg-gray-50"><tr><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">交易对</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">利润</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">创建时间</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead>
|
|
|
+ <!-- 使用新的简单表格样式 -->
|
|
|
+ <table class="simple-table">
|
|
|
+ <thead><tr><th>交易对</th><th>状态</th><th class="hidden md:table-cell">利润</th><th class="hidden sm:table-cell">创建时间</th><th>操作</th></tr></thead>
|
|
|
<tbody id="processing-list" class="divide-y divide-gray-200"></tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <!-- 历史记录容器,JS会填充内容 -->
|
|
|
<div class="tab-pane hidden bg-white rounded-lg shadow-sm" id="history" role="tabpanel">
|
|
|
- <div class="table-responsive-container">
|
|
|
- <table id="history-table" class="min-w-full"></table>
|
|
|
- </div>
|
|
|
+ <div id="history-container" class="table-responsive-container"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Modal -->
|
|
|
+ <!-- Modal (保持不变) -->
|
|
|
<div id="taskDetailModal" class="modal fixed inset-0 bg-gray-600 bg-opacity-75 h-full w-full flex items-center justify-center hidden z-50">
|
|
|
<div class="modal-content relative mx-auto p-4 sm:p-5 border w-full shadow-xl rounded-lg bg-white">
|
|
|
<div class="flex justify-between items-center pb-3 border-b border-gray-200 sticky top-0 bg-white z-10 -mt-4 -mx-4 sm:-mt-5 sm:-mx-5 px-4 sm:px-5 pt-4 sm:pt-5 rounded-t-lg">
|
|
|
@@ -79,12 +74,11 @@
|
|
|
<script>
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
const API_BASE_URL = 'http://localhost:1888';
|
|
|
- const REFRESH_INTERVAL_MS = 10000;
|
|
|
+ const REFRESH_INTERVAL_MS = 10000; // 自动刷新间隔改为10秒
|
|
|
let autoRefreshIntervalId = null;
|
|
|
let isAutoRefreshPaused = false;
|
|
|
let allTasksData = {};
|
|
|
- let historyDataTable = null;
|
|
|
- let isFetching = false; // <--- 在这里添加“刷新锁”变量
|
|
|
+ // 不再需要 isFetching 和 historyDataTable 变量
|
|
|
|
|
|
const refreshButton = document.getElementById('refreshButton');
|
|
|
const toggleAutoRefreshButton = document.getElementById('toggleAutoRefreshButton');
|
|
|
@@ -95,7 +89,7 @@
|
|
|
const modalBody = document.getElementById('modalBody');
|
|
|
const closeModalButton = document.getElementById('closeModalButton');
|
|
|
|
|
|
- // --- Helper Functions ---
|
|
|
+ // --- Helper Functions (保持不变) ---
|
|
|
function getStatusTextClass(status) {
|
|
|
status = status ? status.toLowerCase() : '';
|
|
|
if (status_includes_any(status, ['fail', 'error'])) return 'status-fail';
|
|
|
@@ -122,18 +116,24 @@
|
|
|
}
|
|
|
|
|
|
// --- Table Rendering Functions ---
|
|
|
+ // processing 表格渲染基本不变
|
|
|
function renderProcessingTable(data) {
|
|
|
const tableBody = document.getElementById('processing-list');
|
|
|
tableBody.innerHTML = '';
|
|
|
if (data && data.length > 0) {
|
|
|
- data.sort((a, b) => (a.creationTime || '0') < (b.creationTime || '0') ? 1 : -1);
|
|
|
+ data.sort((a, b) => new Date(b.creationTime.replace(/,/g, '.')) - new Date(a.creationTime.replace(/,/g, '.')));
|
|
|
data.forEach(task => {
|
|
|
allTasksData[task.id] = task;
|
|
|
- const tr = document.createElement('tr');
|
|
|
- tr.className = 'hover:bg-gray-50';
|
|
|
const profitText = task.profit !== null && !isNaN(parseFloat(task.profit)) ? `${parseFloat(task.profit).toFixed(4)}` : 'N/A';
|
|
|
- tr.innerHTML = `<td class="px-3 py-2 whitespace-nowrap font-medium text-gray-900">${task.symbol||'N/A'}</td><td class="px-3 py-2 whitespace-nowrap"><span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusBadgeClasses(task.currentState)}">${task.currentState||'N/A'}</span></td><td class="px-3 py-2 whitespace-nowrap text-gray-600 hidden md:table-cell">${profitText}</td><td class="px-3 py-2 whitespace-nowrap text-gray-600 hidden sm:table-cell">${task.creationTime||'N/A'}</td><td class="px-3 py-2 whitespace-nowrap text-sm font-medium"><button class="text-indigo-600 hover:text-indigo-900 view-details-btn" data-task-id="${task.id}">详情</button></td>`;
|
|
|
- tableBody.appendChild(tr);
|
|
|
+ const rowHtml = `
|
|
|
+ <tr class="hover:bg-gray-50">
|
|
|
+ <td class="px-3 py-2 whitespace-nowrap font-medium text-gray-900">${task.symbol||'N/A'}</td>
|
|
|
+ <td class="px-3 py-2 whitespace-nowrap"><span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusBadgeClasses(task.currentState)}">${task.currentState||'N/A'}</span></td>
|
|
|
+ <td class="px-3 py-2 whitespace-nowrap text-gray-600 hidden md:table-cell">${profitText}</td>
|
|
|
+ <td class="px-3 py-2 whitespace-nowrap text-gray-600 hidden sm:table-cell">${task.creationTime||'N/A'}</td>
|
|
|
+ <td class="px-3 py-2 whitespace-nowrap text-sm font-medium"><button class="text-indigo-600 hover:text-indigo-900 view-details-btn" data-task-id="${task.id}">详情</button></td>
|
|
|
+ </tr>`;
|
|
|
+ tableBody.innerHTML += rowHtml;
|
|
|
});
|
|
|
} else {
|
|
|
tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-gray-500">没有正在处理的任务。</td></tr>`;
|
|
|
@@ -141,111 +141,96 @@
|
|
|
processingCountBadge.textContent = data.length;
|
|
|
}
|
|
|
|
|
|
+ // 【【【核心修改】】】
|
|
|
+ // 重写历史记录表格渲染函数,不再使用 simple-datatables
|
|
|
function renderHistoryDataTable(data) {
|
|
|
- const tableContainer = document.getElementById('history-table');
|
|
|
-
|
|
|
- // 1. 如果 DataTable 实例已存在,则彻底销毁它并清理 DOM
|
|
|
- if (historyDataTable) {
|
|
|
- historyDataTable.destroy();
|
|
|
- // 关键步骤:手动清空容器,确保创建一个全新的、干净的环境
|
|
|
- tableContainer.innerHTML = '';
|
|
|
- }
|
|
|
+ const container = document.getElementById('history-container');
|
|
|
+ historyCountBadge.textContent = data.length;
|
|
|
|
|
|
- // 2. 准备数据
|
|
|
- // 如果没有数据,显示提示信息并提前退出
|
|
|
if (!data || data.length === 0) {
|
|
|
- tableContainer.innerHTML = '<thead><tr><th>交易对</th><th>状态</th><th>利润</th><th>创建时间</th><th>操作</th></tr></thead><tbody><tr><td colspan="5" class="text-center p-4 text-gray-500">没有历史记录。</td></tr></tbody>';
|
|
|
- historyCountBadge.textContent = 0;
|
|
|
- historyDataTable = null; // 确保实例也被清空
|
|
|
+ container.innerHTML = '<div class="text-center p-4 text-gray-500">没有历史记录。</div>';
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- // 将原始数据映射为表格需要的格式
|
|
|
- data.forEach(task => allTasksData[task.id] = task);
|
|
|
- const headings = ['交易对', '状态', '利润', '创建时间', '操作'];
|
|
|
- const tableData = data.map(task => {
|
|
|
- let creationTimeForSort = task.creationTime ? String(task.creationTime).replace(/,/g, '.') : "N/A";
|
|
|
- return [
|
|
|
- task.symbol || 'N/A',
|
|
|
- `<span class="text-xs font-semibold ${getStatusTextClass(task.currentState)}">${task.currentState || 'N/A'}</span>`,
|
|
|
- task.profit !== null && !isNaN(parseFloat(task.profit)) ? parseFloat(task.profit) : null,
|
|
|
- creationTimeForSort,
|
|
|
- `<button class="text-indigo-600 hover:text-indigo-900 view-details-btn" data-task-id="${task.id}">详情</button>`
|
|
|
- ];
|
|
|
- });
|
|
|
|
|
|
- historyCountBadge.textContent = data.length;
|
|
|
-
|
|
|
- // 3. 在清理过的容器上创建全新的 DataTable 实例
|
|
|
- historyDataTable = new simpleDatatables.DataTable(tableContainer, {
|
|
|
- data: { headings, data: tableData },
|
|
|
- paging: false,
|
|
|
- perPageSelect: false,
|
|
|
- searchable: true,
|
|
|
- labels: { placeholder: "搜索...", noRows: "未找到记录" },
|
|
|
- columns: [
|
|
|
- { select: 2, type: 'number' },
|
|
|
- { select: 3, type: 'date', format: "YYYY-MM-DD HH:mm:ss.SSS" },
|
|
|
- { select: 4, sortable: false }
|
|
|
- ]
|
|
|
- });
|
|
|
-
|
|
|
- // 确保每次创建后都进行排序
|
|
|
- historyDataTable.on('datatable.init', () => {
|
|
|
- historyDataTable.columns.sort(3, 'desc');
|
|
|
+ // 1. 手动排序:按创建时间倒序排列
|
|
|
+ data.sort((a, b) => {
|
|
|
+ // 为了防止 creationTime 不存在导致程序崩溃,提供一个默认的旧时间
|
|
|
+ const timeA = a.creationTime[0] || '1970-01-01 00:00:00,000';
|
|
|
+ const timeB = b.creationTime[0] || '1970-01-01 00:00:00,000';
|
|
|
+
|
|
|
+ // 将 'YYYY-MM-DD HH:mm:ss,SSS' 格式的字符串转换为可比较的 Date 对象
|
|
|
+ // .replace(',', '.') 是为了确保所有浏览器都能正确解析这个时间格式
|
|
|
+ const dateB = new Date(timeB.replace(',', '.'));
|
|
|
+ const dateA = new Date(timeA.replace(',', '.'));
|
|
|
+
|
|
|
+ // 返回 b - a 实现时间上的倒序排列(最新的在最前)
|
|
|
+ return dateB - dateA;
|
|
|
});
|
|
|
|
|
|
- // 对于重建的表格,需要手动触发一次排序,因为 'datatable.init' 可能仅在首次初始化时触发
|
|
|
- if (historyDataTable.initialized) {
|
|
|
- historyDataTable.columns.sort(3, 'desc');
|
|
|
- }
|
|
|
+ // 2. 生成行HTML
|
|
|
+ const rowsHtml = data.map(task => {
|
|
|
+ allTasksData[task.id] = task; // 存储任务数据以供弹窗使用
|
|
|
+ const profitText = task.profit !== null && !isNaN(parseFloat(task.profit)) ? `${parseFloat(task.profit).toFixed(4)}` : 'N/A';
|
|
|
+ return `
|
|
|
+ <tr class="hover:bg-gray-50">
|
|
|
+ <td>${task.symbol || 'N/A'}</td>
|
|
|
+ <td><span class="text-xs font-semibold ${getStatusTextClass(task.currentState)}">${task.currentState || 'N/A'}</span></td>
|
|
|
+ <td class="hidden md:table-cell">${profitText}</td>
|
|
|
+ <td class="hidden sm:table-cell">${task.creationTime || 'N/A'}</td>
|
|
|
+ <td><button class="text-indigo-600 hover:text-indigo-900 view-details-btn" data-task-id="${task.id}">详情</button></td>
|
|
|
+ </tr>`;
|
|
|
+ }).join('');
|
|
|
+
|
|
|
+ // 3. 组装完整的表格HTML并一次性写入DOM
|
|
|
+ const tableHtml = `
|
|
|
+ <table class="simple-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>交易对</th>
|
|
|
+ <th>状态</th>
|
|
|
+ <th class="hidden md:table-cell">利润</th>
|
|
|
+ <th class="hidden sm:table-cell">创建时间</th>
|
|
|
+ <th>操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="divide-y divide-gray-200">
|
|
|
+ ${rowsHtml}
|
|
|
+ </tbody>
|
|
|
+ </table>`;
|
|
|
+
|
|
|
+ container.innerHTML = tableHtml;
|
|
|
}
|
|
|
|
|
|
- // --- Main Logic Functions ---
|
|
|
+ // --- Main Logic Functions (简化) ---
|
|
|
async function fetchData() {
|
|
|
- // 检查锁:如果当前正在获取数据,则直接跳过本次刷新,防止冲突
|
|
|
- if (isFetching) {
|
|
|
- console.log("刷新任务正在进行中,跳过此次周期。");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 上锁:标记刷新任务开始
|
|
|
- isFetching = true;
|
|
|
-
|
|
|
try {
|
|
|
const [processingRes, historyRes] = await Promise.all([
|
|
|
fetch(`${API_BASE_URL}/processing`),
|
|
|
fetch(`${API_BASE_URL}/history`)
|
|
|
]);
|
|
|
|
|
|
- // 检查网络请求是否成功
|
|
|
if (!processingRes.ok) throw new Error(`Processing fetch failed: ${processingRes.status}`);
|
|
|
if (!historyRes.ok) throw new Error(`History fetch failed: ${historyRes.status}`);
|
|
|
|
|
|
const processingData = await processingRes.json();
|
|
|
const historyData = await historyRes.json();
|
|
|
|
|
|
- // 渲染是同步操作,在锁的保护下是安全的
|
|
|
renderProcessingTable(processingData);
|
|
|
renderHistoryDataTable(historyData);
|
|
|
} catch (error) {
|
|
|
console.error("获取数据失败:", error);
|
|
|
- // 即使出错,也要更新UI以反馈错误
|
|
|
document.getElementById('processing-list').innerHTML = `<tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败</td></tr>`;
|
|
|
- const historyTable = document.getElementById('history-table');
|
|
|
- if(historyDataTable) historyDataTable.destroy();
|
|
|
- historyTable.innerHTML = `<thead><tr><th>交易对</th><th>状态</th><th>利润</th><th>创建时间</th><th>操作</th></tr></thead><tbody><tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败</td></tr></tbody>`;
|
|
|
-
|
|
|
+ document.getElementById('history-container').innerHTML = `<div class="text-center p-4 text-red-500">加载数据失败</div>`;
|
|
|
processingCountBadge.textContent = 'ERR';
|
|
|
historyCountBadge.textContent = 'ERR';
|
|
|
- } finally {
|
|
|
- // 释放锁:无论成功还是失败,最后都要确保释放锁,以便下次刷新可以进行
|
|
|
- isFetching = false;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
function refreshAllData() { console.log("Refreshing data..."); fetchData(); }
|
|
|
function startAutoRefresh() { if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId); autoRefreshIntervalId = setInterval(refreshAllData, REFRESH_INTERVAL_MS); toggleAutoRefreshButton.textContent = '暂停自动刷新'; toggleAutoRefreshButton.classList.remove('bg-green-500');toggleAutoRefreshButton.classList.add('bg-gray-500'); isAutoRefreshPaused = false; }
|
|
|
function stopAutoRefresh() { if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId); autoRefreshIntervalId = null; toggleAutoRefreshButton.textContent = '恢复自动刷新'; toggleAutoRefreshButton.classList.remove('bg-gray-500');toggleAutoRefreshButton.classList.add('bg-green-500'); isAutoRefreshPaused = true; }
|
|
|
+
|
|
|
+ // --- Event Listeners and Initial Load (基本不变) ---
|
|
|
function switchTab(activeTab) {
|
|
|
tabButtons.forEach(tab => {
|
|
|
tab.classList.remove('border-blue-500', 'text-blue-600');
|
|
|
@@ -283,7 +268,6 @@
|
|
|
}
|
|
|
function closeModal() { taskDetailModal.classList.add('hidden'); document.body.classList.remove('modal-active'); }
|
|
|
|
|
|
- // --- Event Listeners ---
|
|
|
refreshButton.addEventListener('click', refreshAllData);
|
|
|
toggleAutoRefreshButton.addEventListener('click', () => isAutoRefreshPaused ? startAutoRefresh() : stopAutoRefresh());
|
|
|
tabButtons.forEach(tab => tab.addEventListener('click', () => switchTab(tab)));
|
|
|
@@ -291,16 +275,16 @@
|
|
|
taskDetailModal.addEventListener('click', e => (e.target === taskDetailModal) && closeModal());
|
|
|
document.addEventListener('keydown', e => (e.key === 'Escape' && !taskDetailModal.classList.contains('hidden')) && closeModal());
|
|
|
document.getElementById('tabContent').addEventListener('click', e => {
|
|
|
- if (e.target?.classList.contains('view-details-btn')) {
|
|
|
- openModal(e.target.dataset.taskId);
|
|
|
+ const button = e.target.closest('.view-details-btn');
|
|
|
+ if (button) {
|
|
|
+ openModal(button.dataset.taskId);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
- // --- Initial Load ---
|
|
|
switchTab(document.getElementById('processing-tab'));
|
|
|
refreshAllData();
|
|
|
startAutoRefresh();
|
|
|
});
|
|
|
</script>
|
|
|
</body>
|
|
|
-</html>
|
|
|
+</html>
|