|
|
@@ -3,109 +3,167 @@
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
- <title>套利流程监控</title>
|
|
|
- <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
|
|
|
+ <title>套利流程监控 (Tailwind)</title>
|
|
|
+ <script src="https://cdn.tailwindcss.com"></script>
|
|
|
<style>
|
|
|
body {
|
|
|
font-family: sans-serif;
|
|
|
- padding: 20px;
|
|
|
- background-color: #f4f7f6; /* Lighter background for the page */
|
|
|
+ background-color: #f3f4f6; /* Tailwind bg-gray-100 */
|
|
|
}
|
|
|
- .task-card {
|
|
|
- margin-bottom: 15px;
|
|
|
+ .modal {
|
|
|
+ transition: opacity 0.25s ease;
|
|
|
}
|
|
|
- .task-card .card-header {
|
|
|
- padding: 0;
|
|
|
- }
|
|
|
- .task-card .card-header button {
|
|
|
- color: #333;
|
|
|
- }
|
|
|
- .task-card .card-header button:hover {
|
|
|
- background-color: #e9ecef;
|
|
|
+ .modal-active {
|
|
|
+ overflow-x: hidden;
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
|
- .state-flow-table th, .state-flow-table td {
|
|
|
- font-size: 0.9em;
|
|
|
- vertical-align: middle;
|
|
|
+ /* MODAL CONTENT: 调整最大高度和溢出处理 */
|
|
|
+ .modal-content {
|
|
|
+ max-height: calc(100vh - 4rem); /* 模态框内容最大高度,留出上下边距 */
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
|
- .status-success { color: green; font-weight: bold; }
|
|
|
- .status-fail { color: red; font-weight: bold; }
|
|
|
- .status-pending { color: orange; }
|
|
|
- .status-info { color: blue; }
|
|
|
-
|
|
|
.log-msg {
|
|
|
white-space: pre-wrap;
|
|
|
word-break: break-all;
|
|
|
- max-height: 150px;
|
|
|
+ /* max-height is now set via inline style in JS for easier adjustment */
|
|
|
overflow-y: auto;
|
|
|
- font-size: 0.85em;
|
|
|
- background-color: #fdfdfd;
|
|
|
- padding: 8px;
|
|
|
- border: 1px solid #eee;
|
|
|
- border-radius: 3px;
|
|
|
- margin-top: 5px;
|
|
|
+ font-size: 0.82em; /* Slightly larger log font */
|
|
|
+ line-height: 1.45; /* Increased line height for log messages */
|
|
|
+ background-color: #f9fafb;
|
|
|
+ padding: 6px 8px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-top: 3px;
|
|
|
}
|
|
|
- .section-title {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 1rem;
|
|
|
- }
|
|
|
- .section-title h2 {
|
|
|
- margin-bottom: 0;
|
|
|
- margin-right: 0.5rem;
|
|
|
- }
|
|
|
- .button-group {
|
|
|
- margin-bottom: 1.5rem; /* Increased margin for button group */
|
|
|
+ .table-responsive-container {
|
|
|
+ overflow-x: auto;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
}
|
|
|
+ .status-success { color: #10b981; font-weight: 600; } /* Tailwind green-600 semibold */
|
|
|
+ .status-fail { color: #ef4444; font-weight: 600; } /* Tailwind red-500 semibold */
|
|
|
+ .status-pending { color: #f59e0b; font-weight: 500; } /* Tailwind amber-500 medium */
|
|
|
+ .status-info { color: #3b82f6; font-weight: 500; } /* Tailwind blue-500 medium */
|
|
|
</style>
|
|
|
</head>
|
|
|
-<body>
|
|
|
- <div class="container-fluid">
|
|
|
- <h1 class="mb-4">套利流程监控</h1>
|
|
|
- <div class="button-group">
|
|
|
- <button id="refreshButton" class="btn btn-primary mr-2">手动刷新</button>
|
|
|
- <button id="toggleAutoRefreshButton" class="btn btn-secondary">暂停自动刷新</button>
|
|
|
+<body class="text-gray-800">
|
|
|
+ <div class="container mx-auto p-4 md:p-6">
|
|
|
+ <!-- Header and Tabs remain the same -->
|
|
|
+ <header class="mb-6">
|
|
|
+ <h1 class="text-3xl font-bold text-gray-700">套利流程监控</h1>
|
|
|
+ <div class="mt-4 space-x-2">
|
|
|
+ <button id="refreshButton" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition duration-150">手动刷新</button>
|
|
|
+ <button id="toggleAutoRefreshButton" class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition duration-150">暂停自动刷新</button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="mb-4 border-b border-gray-200">
|
|
|
+ <ul class="flex flex-wrap -mb-px text-sm font-medium text-center" id="myTab" role="tablist">
|
|
|
+ <li class="mr-2" role="presentation">
|
|
|
+ <button class="tab-button inline-block p-4 border-b-2 rounded-t-lg" id="processing-tab" type="button" role="tab" aria-controls="processing" aria-selected="true">
|
|
|
+ 正在处理 <span id="processing-count-badge" class="ml-1 px-2 py-0.5 bg-blue-100 text-blue-800 text-xs font-semibold rounded-full">0</span>
|
|
|
+ </button>
|
|
|
+ </li>
|
|
|
+ <li class="mr-2" role="presentation">
|
|
|
+ <button class="tab-button inline-block p-4 border-b-2 rounded-t-lg border-transparent hover:text-gray-600 hover:border-gray-300" id="history-tab" type="button" role="tab" aria-controls="history" aria-selected="false">
|
|
|
+ 历史记录 <span id="history-count-badge" class="ml-1 px-2 py-0.5 bg-gray-100 text-gray-800 text-xs font-semibold rounded-full">0</span>
|
|
|
+ </button>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
</div>
|
|
|
-
|
|
|
- <div class="row">
|
|
|
- <div class="col-md-6">
|
|
|
- <div class="section-title">
|
|
|
- <h2>正在处理</h2>
|
|
|
- <span id="processing-count" class="badge badge-info badge-pill" style="font-size: 1rem;">0</span>
|
|
|
- </div>
|
|
|
- <div id="processing-list" class="accordion">
|
|
|
- <p>加载中...</p>
|
|
|
+
|
|
|
+ <div id="tabContent">
|
|
|
+ <div class="tab-pane p-1 bg-white rounded-lg shadow" id="processing" role="tabpanel" aria-labelledby="processing-tab">
|
|
|
+ <div class="table-responsive-container">
|
|
|
+ <table class="min-w-full divide-y divide-gray-200">
|
|
|
+ <thead class="bg-gray-50">
|
|
|
+ <tr>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">交易对</th>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">当前状态</th>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">利润/阈值</th>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">创建时间</th>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody id="processing-list" class="bg-white divide-y divide-gray-200">
|
|
|
+ <tr><td colspan="5" class="text-center p-4 text-gray-500">加载中...</td></tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="col-md-6">
|
|
|
- <div class="section-title">
|
|
|
- <h2>历史记录</h2>
|
|
|
- <span id="history-count" class="badge badge-secondary badge-pill" style="font-size: 1rem;">0</span>
|
|
|
- </div>
|
|
|
- <div id="history-list" class="accordion">
|
|
|
- <p>加载中...</p>
|
|
|
+ <div class="tab-pane hidden p-1 bg-white rounded-lg shadow" id="history" role="tabpanel" aria-labelledby="history-tab">
|
|
|
+ <div class="table-responsive-container">
|
|
|
+ <table class="min-w-full divide-y divide-gray-200">
|
|
|
+ <thead class="bg-gray-50">
|
|
|
+ <tr>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">交易对</th>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最终状态</th>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">利润/阈值</th>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">创建时间</th>
|
|
|
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody id="history-list" class="bg-white divide-y divide-gray-200">
|
|
|
+ <tr><td colspan="5" class="text-center p-4 text-gray-500">加载中...</td></tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- Modal: INCREASED MAX-WIDTH, PADDING, and added outer padding for small screens -->
|
|
|
+ <div id="taskDetailModal" class="modal fixed inset-0 bg-gray-600 bg-opacity-75 overflow-y-auto h-full w-full flex items-center justify-center hidden z-50 p-4 sm:p-6 md:p-8">
|
|
|
+ <div class="modal-content relative mx-auto p-5 sm:p-6 border w-full max-w-xl md:max-w-3xl lg:max-w-5xl shadow-xl rounded-lg bg-white">
|
|
|
+ <div class="flex justify-between items-center pb-4 border-b border-gray-200">
|
|
|
+ <h3 class="text-xl sm:text-2xl font-semibold text-gray-800">任务详情</h3>
|
|
|
+ <button id="closeModalButton" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center">
|
|
|
+ <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
|
|
+ <span class="sr-only">关闭</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div id="modalBody" class="pt-4 text-sm leading-relaxed space-y-3">
|
|
|
+ <!-- Modal content will be injected here -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<script>
|
|
|
+ // Constants and global variables (API_BASE_URL, etc.) remain the same
|
|
|
const API_BASE_URL = 'http://localhost:5002';
|
|
|
- const REFRESH_INTERVAL_MS = 10000; // 自动刷新间隔:10秒
|
|
|
+ const REFRESH_INTERVAL_MS = 10000;
|
|
|
let autoRefreshIntervalId = null;
|
|
|
let isAutoRefreshPaused = false;
|
|
|
+ let allTasksData = {};
|
|
|
|
|
|
const refreshButton = document.getElementById('refreshButton');
|
|
|
const toggleAutoRefreshButton = document.getElementById('toggleAutoRefreshButton');
|
|
|
+ const processingTab = document.getElementById('processing-tab');
|
|
|
+ const historyTab = document.getElementById('history-tab');
|
|
|
+ const processingPane = document.getElementById('processing');
|
|
|
+ const historyPane = document.getElementById('history');
|
|
|
+ const processingCountBadge = document.getElementById('processing-count-badge');
|
|
|
+ const historyCountBadge = document.getElementById('history-count-badge');
|
|
|
+ const taskDetailModal = document.getElementById('taskDetailModal');
|
|
|
+ const modalBody = document.getElementById('modalBody');
|
|
|
+ const closeModalButton = document.getElementById('closeModalButton');
|
|
|
|
|
|
- // ... (getStatusClass, status_includes_any, getStatusBadgeClass, formatStateFlow, createTaskCard 函数保持不变) ...
|
|
|
- function getStatusClass(status) { // For text color in stateFlow
|
|
|
+ // Helper functions (getStatusTextClass, getStatusBadgeClasses, status_includes_any) remain the same
|
|
|
+ function getStatusTextClass(status) {
|
|
|
status = status ? status.toLowerCase() : 'info';
|
|
|
if (status.includes('success') || status.includes('completed')) return 'status-success';
|
|
|
if (status.includes('fail') || status.includes('failed') || status.includes('error')) return 'status-fail';
|
|
|
- if (status.includes('pending') || status.includes('waiting') || status.includes('buying') || status_includes_any(status, ['received', 'processing', 'starting', 'pending_start'])) return 'status-pending';
|
|
|
+ if (status_includes_any(status, ['pending', 'waiting', 'buying', 'received', 'processing', 'starting', 'pending_start'])) return 'status-pending';
|
|
|
return 'status-info';
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ function getStatusBadgeClasses(status) {
|
|
|
+ status = status ? status.toLowerCase() : 'info';
|
|
|
+ if (status.includes('success') || status.includes('completed')) return 'bg-green-100 text-green-800';
|
|
|
+ if (status.includes('fail') || status.includes('failed') || status.includes('error')) return 'bg-red-100 text-red-800';
|
|
|
+ if (status_includes_any(status, ['pending', 'waiting', 'buying', 'received', 'processing', 'starting', 'pending_start'])) return 'bg-yellow-100 text-yellow-800';
|
|
|
+ return 'bg-blue-100 text-blue-800';
|
|
|
+ }
|
|
|
+
|
|
|
function status_includes_any(status, keywords) {
|
|
|
for (const keyword of keywords) {
|
|
|
if (status.includes(keyword)) return true;
|
|
|
@@ -113,197 +171,254 @@
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- function getStatusBadgeClass(status) { // For Bootstrap badge component in card header
|
|
|
- status = status ? status.toLowerCase() : 'info';
|
|
|
- if (status.includes('success') || status.includes('completed')) return 'badge-success';
|
|
|
- if (status.includes('fail') || status.includes('failed') || status.includes('error')) return 'badge-danger';
|
|
|
- if (status_includes_any(status, ['pending', 'waiting', 'buying', 'received', 'processing', 'starting', 'pending_start'])) return 'badge-warning';
|
|
|
- return 'badge-info';
|
|
|
- }
|
|
|
-
|
|
|
- function formatStateFlow(stateFlow) {
|
|
|
+ // MODIFIED: formatStateFlowForModal - Adjusted column widths and log-msg max-height
|
|
|
+ function formatStateFlowForModal(stateFlow) {
|
|
|
if (!stateFlow || stateFlow.length === 0) {
|
|
|
- return '<p><em>无状态流转记录。</em></p>';
|
|
|
+ return '<p class="text-gray-500 italic">无状态流转记录。</p>';
|
|
|
}
|
|
|
+ // Increased padding, adjusted font, and widths for better readability
|
|
|
let tableHtml = `
|
|
|
- <table class="table table-sm table-hover table-bordered state-flow-table mt-2">
|
|
|
- <thead class="thead-light">
|
|
|
- <tr>
|
|
|
- <th>时间戳</th>
|
|
|
- <th>状态名</th>
|
|
|
- <th>消息</th>
|
|
|
- <th>结果</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
+ <div class="overflow-x-auto border border-gray-200 rounded-md shadow-sm">
|
|
|
+ <table class="min-w-full divide-y divide-gray-200 text-xs sm:text-sm">
|
|
|
+ <thead class="bg-gray-100">
|
|
|
+ <tr>
|
|
|
+ <th class="px-3 py-2.5 sm:px-4 text-left font-semibold text-gray-600 uppercase tracking-wider" style="min-width: 130px;">时间戳</th>
|
|
|
+ <th class="px-3 py-2.5 sm:px-4 text-left font-semibold text-gray-600 uppercase tracking-wider" style="min-width: 100px;">状态名</th>
|
|
|
+ <th class="px-3 py-2.5 sm:px-4 text-left font-semibold text-gray-600 uppercase tracking-wider" style="min-width: 250px;">消息</th>
|
|
|
+ <th class="px-3 py-2.5 sm:px-4 text-left font-semibold text-gray-600 uppercase tracking-wider" style="min-width: 80px;">结果</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="bg-white divide-y divide-gray-200">
|
|
|
`;
|
|
|
stateFlow.forEach(state => {
|
|
|
let actualStatus = state.status;
|
|
|
- if (Array.isArray(state.status)) {
|
|
|
- actualStatus = state.status[state.status.length-1];
|
|
|
+ if (Array.isArray(state.status)) {
|
|
|
+ actualStatus = state.status[state.status.length - 1];
|
|
|
}
|
|
|
+ // Increased log-msg max-height
|
|
|
tableHtml += `
|
|
|
<tr>
|
|
|
- <td style="min-width: 140px;">${state.timestamp || 'N/A'}</td>
|
|
|
- <td>${state.stateName || 'N/A'}</td>
|
|
|
- <td><div class="log-msg">${state.msg || ''}</div></td>
|
|
|
- <td class="${getStatusClass(actualStatus)}">${actualStatus || 'N/A'}</td>
|
|
|
+ <td class="px-3 py-2 sm:px-4 whitespace-nowrap text-gray-700">${state.timestamp || 'N/A'}</td>
|
|
|
+ <td class="px-3 py-2 sm:px-4 text-gray-700">${state.stateName || 'N/A'}</td>
|
|
|
+ <td class="px-3 py-2 sm:px-4 text-gray-700">
|
|
|
+ <div class="log-msg" style="max-height: 150px;">${state.msg || ''}</div>
|
|
|
+ </td>
|
|
|
+ <td class="px-3 py-2 sm:px-4 ${getStatusTextClass(actualStatus)}">${actualStatus || 'N/A'}</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
});
|
|
|
- tableHtml += '</tbody></table>';
|
|
|
+ tableHtml += '</tbody></table></div>';
|
|
|
return tableHtml;
|
|
|
}
|
|
|
|
|
|
- function createTaskCard(task, listIdForAccordion) {
|
|
|
- const safeId = `task-${task.id.replace(/[^a-zA-Z0-9-_]/g, '')}`;
|
|
|
- const headingId = `heading-${safeId}`;
|
|
|
- const collapseId = `collapse-${safeId}`;
|
|
|
-
|
|
|
- const card = document.createElement('div');
|
|
|
- card.className = 'card task-card shadow-sm';
|
|
|
-
|
|
|
- const cardHeader = document.createElement('div');
|
|
|
- cardHeader.className = 'card-header';
|
|
|
- cardHeader.id = headingId;
|
|
|
- cardHeader.innerHTML = `
|
|
|
- <h2 class="mb-0">
|
|
|
- <button class="btn btn-light btn-block text-left p-3" type="button" data-toggle="collapse" data-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
|
|
|
- <div class="d-flex justify-content-between align-items-center w-100">
|
|
|
- <span style="font-size: 1.1rem; font-weight: bold; color: #007bff;">${task.symbol || 'N/A'}</span>
|
|
|
- <span class="badge ${getStatusBadgeClass(task.currentState)} p-2" style="font-size: 0.8rem;">${task.currentState || 'N/A'}</span>
|
|
|
- </div>
|
|
|
- <div style="font-size: 0.85em;" class="text-muted mt-2">
|
|
|
- <span>创建: ${task.creationTime || 'N/A'}</span>
|
|
|
- <span class="mx-2">|</span>
|
|
|
- <span>利润: ${task.profit || 'N/A'} (阈值: ${task.profitLimit || 'N/A'})</span>
|
|
|
- </div>
|
|
|
- </button>
|
|
|
- </h2>
|
|
|
+ // createTableRow function remains the same
|
|
|
+ function createTableRow(task) {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = 'hover:bg-gray-50 transition duration-150';
|
|
|
+ const profitText = task.profit && task.profitLimit ? `${task.profit} (${task.profitLimit})` : (task.profit || 'N/A');
|
|
|
+
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">${task.symbol || 'N/A'}</td>
|
|
|
+ <td class="px-4 py-3 whitespace-nowrap text-sm">
|
|
|
+ <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-4 py-3 whitespace-nowrap text-sm text-gray-500 hidden md:table-cell">${profitText}</td>
|
|
|
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 hidden sm:table-cell">${task.creationTime || 'N/A'}</td>
|
|
|
+ <td class="px-4 py-3 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>
|
|
|
`;
|
|
|
+ return tr;
|
|
|
+ }
|
|
|
|
|
|
- const collapseDiv = document.createElement('div');
|
|
|
- collapseDiv.id = collapseId;
|
|
|
- collapseDiv.className = 'collapse';
|
|
|
- collapseDiv.setAttribute('aria-labelledby', headingId);
|
|
|
- collapseDiv.setAttribute('data-parent', `#${listIdForAccordion}`);
|
|
|
-
|
|
|
- collapseDiv.innerHTML = `
|
|
|
- <div class="card-body">
|
|
|
- <p class="mb-1"><small class="text-muted">ID: ${task.id}</small></p>
|
|
|
- <hr class="my-2">
|
|
|
- <p class="mb-1"><strong>来源代币:</strong> ${task.fromTokenAmountHuman || 'N/A'} ${task.fromToken || ''}</p>
|
|
|
- <p><strong>目标代币:</strong> ${task.toTokenAmountHuman || 'N/A'} ${task.toToken || ''}</p>
|
|
|
- <h6 class="mt-3">状态流转:</h6>
|
|
|
- ${formatStateFlow(task.stateFlow)}
|
|
|
- </div>
|
|
|
- `;
|
|
|
+ // renderTable and fetchDataForTab functions remain the same
|
|
|
+ function renderTable(data, targetTableBodyId, countBadgeElement) {
|
|
|
+ const tableBody = document.getElementById(targetTableBodyId);
|
|
|
+ if (!tableBody || !countBadgeElement) return;
|
|
|
|
|
|
- card.appendChild(cardHeader);
|
|
|
- card.appendChild(collapseDiv);
|
|
|
- return card;
|
|
|
- }
|
|
|
+ tableBody.innerHTML = '';
|
|
|
|
|
|
- async function fetchData(endpoint, targetElementId, countElementId) {
|
|
|
- const targetElement = document.getElementById(targetElementId);
|
|
|
- const countElement = document.getElementById(countElementId);
|
|
|
- if (!targetElement || !countElement) {
|
|
|
- console.error(`Element not found: ${targetElementId} or ${countElementId}`);
|
|
|
- return;
|
|
|
+ if (data && data.length > 0) {
|
|
|
+ data.sort((a, b) => (a.creationTime || '0') < (b.creationTime || '0') ? 1 : -1);
|
|
|
+ data.forEach(task => {
|
|
|
+ allTasksData[task.id] = task;
|
|
|
+ tableBody.appendChild(createTableRow(task));
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-gray-500">没有任务。</td></tr>`;
|
|
|
}
|
|
|
-
|
|
|
- // 如果不是首次加载 (即 targetElement.innerHTML 为 '加载中...'),则不显示 "加载中..." 文本,避免闪烁
|
|
|
- const isInitialLoad = targetElement.querySelector('p') && targetElement.querySelector('p').textContent.includes('加载中');
|
|
|
- if (isInitialLoad) {
|
|
|
- targetElement.innerHTML = '<p class="text-muted"><em>加载中...</em></p>';
|
|
|
+ countBadgeElement.textContent = data.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function fetchDataForTab(endpoint, tableBodyId, countBadgeElement) {
|
|
|
+ const tableBody = document.getElementById(tableBodyId);
|
|
|
+ const isInitialLoad = tableBody.querySelector('td[colspan="5"]');
|
|
|
+ if (isInitialLoad) {
|
|
|
+ tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-gray-500">加载中...</td></tr>`;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`);
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
- }
|
|
|
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
const data = await response.json();
|
|
|
-
|
|
|
- if (data && data.length > 0) {
|
|
|
- data.sort((a, b) => {
|
|
|
- const timeA = a.creationTime || '0';
|
|
|
- const timeB = b.creationTime || '0';
|
|
|
- if (timeA < timeB) { return 1; }
|
|
|
- if (timeA > timeB) { return -1; }
|
|
|
- return 0;
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- targetElement.innerHTML = '';
|
|
|
- if (data && data.length > 0) {
|
|
|
- data.forEach(task => {
|
|
|
- targetElement.appendChild(createTaskCard(task, targetElementId));
|
|
|
- });
|
|
|
- } else {
|
|
|
- targetElement.innerHTML = '<p class="text-muted"><em>没有任务。</em></p>';
|
|
|
- }
|
|
|
- countElement.textContent = data.length;
|
|
|
-
|
|
|
+ renderTable(data, tableBodyId, countBadgeElement);
|
|
|
} catch (error) {
|
|
|
console.error(`获取 ${endpoint} 数据失败:`, error);
|
|
|
- if (isInitialLoad) { // 只有在初始加载失败时才替换整个内容
|
|
|
- targetElement.innerHTML = `<p class="text-danger">加载数据失败: ${error.message}</p>`;
|
|
|
+ if (isInitialLoad) {
|
|
|
+ tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败: ${error.message}</td></tr>`;
|
|
|
} else {
|
|
|
- console.warn("获取更新数据失败,保留旧数据。") // 或者可以加一个小的错误提示
|
|
|
+ console.warn("获取更新数据失败,保留旧数据。")
|
|
|
}
|
|
|
- countElement.textContent = 'ERR';
|
|
|
+ countBadgeElement.textContent = 'ERR';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // refreshAllData, startAutoRefresh, stopAutoRefresh functions remain the same
|
|
|
function refreshAllData() {
|
|
|
- console.log("Refreshing data..."); // 用于调试
|
|
|
- fetchData('/processing', 'processing-list', 'processing-count');
|
|
|
- fetchData('/history', 'history-list', 'history-count');
|
|
|
+ console.log("Refreshing data...");
|
|
|
+ fetchDataForTab('/processing', 'processing-list', processingCountBadge);
|
|
|
+ fetchDataForTab('/history', 'history-list', historyCountBadge);
|
|
|
}
|
|
|
|
|
|
function startAutoRefresh() {
|
|
|
- if (autoRefreshIntervalId) { // Clear existing interval if any
|
|
|
- clearInterval(autoRefreshIntervalId);
|
|
|
- }
|
|
|
+ if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId);
|
|
|
autoRefreshIntervalId = setInterval(refreshAllData, REFRESH_INTERVAL_MS);
|
|
|
isAutoRefreshPaused = false;
|
|
|
toggleAutoRefreshButton.textContent = '暂停自动刷新';
|
|
|
- toggleAutoRefreshButton.classList.remove('btn-success');
|
|
|
- toggleAutoRefreshButton.classList.add('btn-secondary');
|
|
|
- console.log("自动刷新已启动。 Interval ID:", autoRefreshIntervalId);
|
|
|
+ toggleAutoRefreshButton.classList.remove('bg-green-500', 'hover:bg-green-600');
|
|
|
+ toggleAutoRefreshButton.classList.add('bg-gray-500', 'hover:bg-gray-600');
|
|
|
+ console.log("自动刷新已启动。");
|
|
|
}
|
|
|
|
|
|
function stopAutoRefresh() {
|
|
|
- if (autoRefreshIntervalId) {
|
|
|
- clearInterval(autoRefreshIntervalId);
|
|
|
- autoRefreshIntervalId = null; // Set to null to indicate it's stopped
|
|
|
- console.log("自动刷新已暂停。");
|
|
|
- }
|
|
|
+ if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId);
|
|
|
+ autoRefreshIntervalId = null;
|
|
|
isAutoRefreshPaused = true;
|
|
|
toggleAutoRefreshButton.textContent = '恢复自动刷新';
|
|
|
- toggleAutoRefreshButton.classList.remove('btn-secondary');
|
|
|
- toggleAutoRefreshButton.classList.add('btn-success');
|
|
|
+ toggleAutoRefreshButton.classList.remove('bg-gray-500', 'hover:bg-gray-600');
|
|
|
+ toggleAutoRefreshButton.classList.add('bg-green-500', 'hover:bg-green-600');
|
|
|
+ console.log("自动刷新已暂停。");
|
|
|
}
|
|
|
|
|
|
- refreshButton.addEventListener('click', refreshAllData);
|
|
|
+ // Tab switching logic remains the same
|
|
|
+ function switchTab(activeTab, activePane, inactiveTab, inactivePane) {
|
|
|
+ activeTab.setAttribute('aria-selected', 'true');
|
|
|
+ activeTab.classList.remove('border-transparent', 'hover:text-gray-600', 'hover:border-gray-300');
|
|
|
+ activeTab.classList.add('border-blue-500', 'text-blue-600');
|
|
|
+
|
|
|
+ inactiveTab.setAttribute('aria-selected', 'false');
|
|
|
+ inactiveTab.classList.remove('border-blue-500', 'text-blue-600');
|
|
|
+ inactiveTab.classList.add('border-transparent', 'hover:text-gray-600', 'hover:border-gray-300');
|
|
|
+
|
|
|
+ activePane.classList.remove('hidden');
|
|
|
+ inactivePane.classList.add('hidden');
|
|
|
+ }
|
|
|
+
|
|
|
+ processingTab.addEventListener('click', () => switchTab(processingTab, processingPane, historyTab, historyPane));
|
|
|
+ historyTab.addEventListener('click', () => switchTab(historyTab, historyPane, processingTab, processingPane));
|
|
|
+
|
|
|
+ // MODIFIED: openModal - Basic info section uses a more structured layout (definition list style)
|
|
|
+ function openModal(taskId) {
|
|
|
+ const task = allTasksData[taskId];
|
|
|
+ if (!task) {
|
|
|
+ console.error("Task data not found for ID:", taskId);
|
|
|
+ modalBody.innerHTML = '<p class="text-red-500">无法加载任务详情。</p>';
|
|
|
+ taskDetailModal.classList.remove('hidden');
|
|
|
+ document.body.classList.add('modal-active');
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
+ // Improved basic info layout: using a definition list style with grid for responsiveness
|
|
|
+ let basicInfoHtml = '<dl class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-4">'; // gap-y-4 added for more vertical space
|
|
|
+
|
|
|
+ const infoMap = new Map([
|
|
|
+ ["ID", task.id], // ID is often long, let it span more columns if needed.
|
|
|
+ ["交易对", task.symbol || 'N/A'],
|
|
|
+ ["当前状态", `<span class="${getStatusTextClass(task.currentState)} font-semibold">${task.currentState || 'N/A'}</span>`],
|
|
|
+ ["来源代币", `${task.fromTokenAmountHuman || 'N/A'} ${task.fromToken || ''}`],
|
|
|
+ ["目标代币", `${task.toTokenAmountHuman || 'N/A'} ${task.toToken || ''}`],
|
|
|
+ ["创建时间", task.creationTime || 'N/A'],
|
|
|
+ ["利润", task.profit || 'N/A'],
|
|
|
+ ["利润阈值", task.profitLimit || 'N/A']
|
|
|
+ ]);
|
|
|
+
|
|
|
+ infoMap.forEach((value, label) => {
|
|
|
+ let itemClass = "sm:col-span-1"; // Default span
|
|
|
+ if (label === "ID") { // ID can span more
|
|
|
+ itemClass = "sm:col-span-2 lg:col-span-3 break-all"; // break-all for very long IDs
|
|
|
+ } else if (label === "来源代币" || label === "目标代币") { // These might also be long
|
|
|
+ itemClass = "sm:col-span-1 lg:col-span-1 break-words";
|
|
|
+ }
|
|
|
+
|
|
|
+ basicInfoHtml += `
|
|
|
+ <div class="${itemClass}">
|
|
|
+ <dt class="text-xs font-medium text-gray-500 uppercase">${label}</dt>
|
|
|
+ <dd class="mt-1 text-gray-900">${value}</dd>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+ basicInfoHtml += '</dl>';
|
|
|
+
|
|
|
+ modalBody.innerHTML = `
|
|
|
+ ${basicInfoHtml}
|
|
|
+ <hr class="my-4 sm:my-6">
|
|
|
+ <div>
|
|
|
+ <h4 class="text-base sm:text-lg font-semibold text-gray-800 mb-2">状态流转:</h4>
|
|
|
+ ${formatStateFlowForModal(task.stateFlow)}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ taskDetailModal.classList.remove('hidden');
|
|
|
+ document.body.classList.add('modal-active');
|
|
|
+ }
|
|
|
+
|
|
|
+ // closeModal, event listeners for modal, and initial setup remain the same
|
|
|
+ function closeModal() {
|
|
|
+ taskDetailModal.classList.add('hidden');
|
|
|
+ document.body.classList.remove('modal-active');
|
|
|
+ }
|
|
|
+
|
|
|
+ closeModalButton.addEventListener('click', closeModal);
|
|
|
+ taskDetailModal.addEventListener('click', (event) => {
|
|
|
+ if (event.target === taskDetailModal) {
|
|
|
+ closeModal();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ document.addEventListener('keydown', (event) => {
|
|
|
+ if (event.key === 'Escape' && !taskDetailModal.classList.contains('hidden')) {
|
|
|
+ closeModal();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ document.getElementById('tabContent').addEventListener('click', function(event) {
|
|
|
+ if (event.target.classList.contains('view-details-btn')) {
|
|
|
+ const taskId = event.target.dataset.taskId;
|
|
|
+ openModal(taskId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ refreshButton.addEventListener('click', refreshAllData);
|
|
|
toggleAutoRefreshButton.addEventListener('click', () => {
|
|
|
if (isAutoRefreshPaused) {
|
|
|
startAutoRefresh();
|
|
|
- refreshAllData(); // Resume and immediately refresh
|
|
|
+ refreshAllData();
|
|
|
} else {
|
|
|
stopAutoRefresh();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
- refreshAllData(); // Initial load
|
|
|
- startAutoRefresh(); // Start auto-refresh by default
|
|
|
+ processingTab.setAttribute('aria-selected', 'true');
|
|
|
+ processingTab.classList.add('border-blue-500', 'text-blue-600');
|
|
|
+ processingPane.classList.remove('hidden');
|
|
|
+
|
|
|
+ historyTab.classList.add('border-transparent', 'hover:text-gray-600', 'hover:border-gray-300');
|
|
|
+ historyPane.classList.add('hidden');
|
|
|
+
|
|
|
+ refreshAllData();
|
|
|
+ startAutoRefresh();
|
|
|
});
|
|
|
</script>
|
|
|
- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
|
|
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
|
|
|
- <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
|
|
</body>
|
|
|
-</html>
|
|
|
+</html>
|