|
|
@@ -3,419 +3,271 @@
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
- <title>套利流程监控 (Tailwind)</title>
|
|
|
+ <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>
|
|
|
<style>
|
|
|
- body {
|
|
|
- font-family: sans-serif;
|
|
|
- background-color: #f3f4f6; /* Tailwind bg-gray-100 */
|
|
|
- }
|
|
|
- .modal {
|
|
|
- transition: opacity 0.25s ease;
|
|
|
- }
|
|
|
- .modal-active {
|
|
|
- overflow-x: hidden;
|
|
|
- overflow-y: auto;
|
|
|
- }
|
|
|
- /* MODAL CONTENT: 调整最大高度和溢出处理 */
|
|
|
- .modal-content {
|
|
|
- max-height: calc(100vh - 4rem); /* 模态框内容最大高度,留出上下边距 */
|
|
|
- overflow-y: auto;
|
|
|
- }
|
|
|
- .log-msg {
|
|
|
- white-space: pre-wrap;
|
|
|
- word-break: break-all;
|
|
|
- /* max-height is now set via inline style in JS for easier adjustment */
|
|
|
- /* overflow-y: auto; */
|
|
|
- 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;
|
|
|
- }
|
|
|
- .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 */
|
|
|
+ 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; }
|
|
|
+ .modal-active { overflow: hidden; }
|
|
|
+ .modal-content { height: calc(100vh - 2rem); width: calc(100vw - 2rem); max-width: none; max-height: none; overflow-y: auto; }
|
|
|
+ .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; }
|
|
|
</style>
|
|
|
</head>
|
|
|
-<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>
|
|
|
+<body class="text-gray-800 antialiased">
|
|
|
+ <div class="container mx-auto px-2 py-4 sm:px-4 sm:py-6">
|
|
|
+ <header class="mb-4">
|
|
|
+ <h1 class="text-2xl font-bold text-gray-700">套利流程监控</h1>
|
|
|
+ <div class="mt-3 space-x-2">
|
|
|
+ <button id="refreshButton" class="px-3 py-1.5 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition">手动刷新</button>
|
|
|
+ <button id="toggleAutoRefreshButton" class="px-3 py-1.5 text-sm bg-gray-500 text-white rounded hover:bg-gray-600 transition">暂停自动刷新</button>
|
|
|
+ <a id="downloadExcelLink" href="http://localhost:1888/download_history_excel" target="_blank" class="inline-block">
|
|
|
+ <button class="px-3 py-1.5 text-sm bg-green-600 text-white rounded hover:bg-green-700 transition">下载历史(Excel)</button>
|
|
|
+ </a>
|
|
|
</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>
|
|
|
+ <li class="mr-2" role="presentation"><button class="tab-button inline-block p-3 border-b-2 rounded-t-lg" id="processing-tab" type="button">正在处理 <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-3 border-b-2 rounded-t-lg border-transparent hover:text-gray-600 hover:border-gray-300" id="history-tab" type="button">历史记录 <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 id="tabContent">
|
|
|
- <div class="tab-pane p-1 bg-white rounded-lg shadow" id="processing" role="tabpanel" aria-labelledby="processing-tab">
|
|
|
+ <div class="tab-pane bg-white rounded-lg shadow-sm" id="processing" role="tabpanel">
|
|
|
<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 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>
|
|
|
+ <tbody id="processing-list" class="divide-y divide-gray-200"></tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <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 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>
|
|
|
</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>
|
|
|
+ <!-- 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">
|
|
|
+ <h3 class="text-lg sm:text-xl 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>
|
|
|
+ <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><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>
|
|
|
</button>
|
|
|
</div>
|
|
|
- <div id="modalBody" class="pt-4 text-sm leading-relaxed space-y-3">
|
|
|
- <!-- Modal content will be injected here -->
|
|
|
- </div>
|
|
|
+ <div id="modalBody" class="pt-3 text-sm leading-relaxed space-y-3"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
- // Constants and global variables (API_BASE_URL, etc.) remain the same
|
|
|
- const API_BASE_URL = 'http://localhost:1888';
|
|
|
- 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');
|
|
|
-
|
|
|
- // 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_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;
|
|
|
+ document.addEventListener('DOMContentLoaded', () => {
|
|
|
+ const API_BASE_URL = 'http://localhost:1888';
|
|
|
+ const REFRESH_INTERVAL_MS = 10000;
|
|
|
+ let autoRefreshIntervalId = null;
|
|
|
+ let isAutoRefreshPaused = false;
|
|
|
+ let allTasksData = {};
|
|
|
+ let historyDataTable = null;
|
|
|
+
|
|
|
+ const refreshButton = document.getElementById('refreshButton');
|
|
|
+ const toggleAutoRefreshButton = document.getElementById('toggleAutoRefreshButton');
|
|
|
+ const tabButtons = document.querySelectorAll('.tab-button');
|
|
|
+ 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');
|
|
|
+
|
|
|
+ // --- Helper Functions ---
|
|
|
+ function getStatusTextClass(status) {
|
|
|
+ status = status ? status.toLowerCase() : '';
|
|
|
+ if (status_includes_any(status, ['fail', 'error'])) return 'status-fail';
|
|
|
+ if (status_includes_any(status, ['success', 'completed'])) return 'status-success';
|
|
|
+ if (status_includes_any(status, ['pending', 'waiting', 'processing'])) return 'status-pending';
|
|
|
+ return 'status-info';
|
|
|
}
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // MODIFIED: formatStateFlowForModal - Adjusted column widths and log-msg max-height
|
|
|
- function formatStateFlowForModal(stateFlow) {
|
|
|
- if (!stateFlow || stateFlow.length === 0) {
|
|
|
- return '<p class="text-gray-500 italic">无状态流转记录。</p>';
|
|
|
+ function getStatusBadgeClasses(status) {
|
|
|
+ status = status ? status.toLowerCase() : '';
|
|
|
+ if (status_includes_any(status, ['fail', 'error'])) return 'bg-red-100 text-red-800';
|
|
|
+ if (status_includes_any(status, ['success', 'completed'])) return 'bg-green-100 text-green-800';
|
|
|
+ if (status_includes_any(status, ['pending', 'waiting', 'processing'])) return 'bg-yellow-100 text-yellow-800';
|
|
|
+ return 'bg-blue-100 text-blue-800';
|
|
|
}
|
|
|
- // Increased padding, adjusted font, and widths for better readability
|
|
|
- let tableHtml = `
|
|
|
- <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];
|
|
|
- }
|
|
|
- // Increased log-msg max-height
|
|
|
- tableHtml += `
|
|
|
- <tr>
|
|
|
- <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">${state.msg || ''}</div>
|
|
|
- </td>
|
|
|
- <td class="px-3 py-2 sm:px-4 ${getStatusTextClass(actualStatus)}">${actualStatus || 'N/A'}</td>
|
|
|
- </tr>
|
|
|
- `;
|
|
|
- });
|
|
|
- tableHtml += '</tbody></table></div>';
|
|
|
- return tableHtml;
|
|
|
- }
|
|
|
-
|
|
|
- // 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.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;
|
|
|
- }
|
|
|
-
|
|
|
- // renderTable and fetchDataForTab functions remain the same
|
|
|
- function renderTable(data, targetTableBodyId, countBadgeElement) {
|
|
|
- const tableBody = document.getElementById(targetTableBodyId);
|
|
|
- if (!tableBody || !countBadgeElement) return;
|
|
|
-
|
|
|
- tableBody.innerHTML = '';
|
|
|
-
|
|
|
- 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));
|
|
|
+ function status_includes_any(status, keywords) { for (const k of keywords) if (status.includes(k)) return true; return false; }
|
|
|
+ function formatStateFlowForModal(stateFlow) {
|
|
|
+ if (!stateFlow || stateFlow.length === 0) return '<p class="text-gray-500 italic">无状态流转记录。</p>';
|
|
|
+ let tableHtml = '<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"><thead class="bg-gray-100"><tr><th class="px-2 py-2 text-left font-semibold text-gray-600">时间戳</th><th class="px-2 py-2 text-left font-semibold text-gray-600">状态名</th><th class="px-2 py-2 text-left font-semibold text-gray-600">消息</th><th class="px-2 py-2 text-left font-semibold text-gray-600">结果</th></tr></thead><tbody class="bg-white divide-y divide-gray-200">';
|
|
|
+ stateFlow.forEach(state => {
|
|
|
+ let actualStatus = Array.isArray(state.status) ? state.status[state.status.length - 1] : state.status;
|
|
|
+ tableHtml += `<tr><td class="px-2 py-2 whitespace-nowrap text-gray-700">${state.timestamp || 'N/A'}</td><td class="px-2 py-2 text-gray-800 font-medium">${state.stateName || 'N/A'}</td><td class="px-2 py-2 text-gray-700"><div class="log-msg">${state.msg || ''}</div></td><td class="px-2 py-2 font-semibold ${getStatusTextClass(actualStatus)}">${actualStatus || 'N/A'}</td></tr>`;
|
|
|
});
|
|
|
- } else {
|
|
|
- tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-gray-500">没有任务。</td></tr>`;
|
|
|
- }
|
|
|
- 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>`;
|
|
|
+ return tableHtml += '</tbody></table></div>';
|
|
|
}
|
|
|
|
|
|
- try {
|
|
|
- const response = await fetch(`${API_BASE_URL}${endpoint}`);
|
|
|
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
- const data = await response.json();
|
|
|
- renderTable(data, tableBodyId, countBadgeElement);
|
|
|
- } catch (error) {
|
|
|
- console.error(`获取 ${endpoint} 数据失败:`, error);
|
|
|
- if (isInitialLoad) {
|
|
|
- tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败: ${error.message}</td></tr>`;
|
|
|
+ // --- Table Rendering Functions ---
|
|
|
+ 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.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);
|
|
|
+ });
|
|
|
} else {
|
|
|
- console.warn("获取更新数据失败,保留旧数据。")
|
|
|
+ tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-gray-500">没有正在处理的任务。</td></tr>`;
|
|
|
}
|
|
|
- countBadgeElement.textContent = 'ERR';
|
|
|
+ processingCountBadge.textContent = data.length;
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- // refreshAllData, startAutoRefresh, stopAutoRefresh functions remain the same
|
|
|
- function refreshAllData() {
|
|
|
- console.log("Refreshing data...");
|
|
|
- fetchDataForTab('/processing', 'processing-list', processingCountBadge);
|
|
|
- fetchDataForTab('/history', 'history-list', historyCountBadge);
|
|
|
- }
|
|
|
-
|
|
|
- function startAutoRefresh() {
|
|
|
- if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId);
|
|
|
- autoRefreshIntervalId = setInterval(refreshAllData, REFRESH_INTERVAL_MS);
|
|
|
- isAutoRefreshPaused = false;
|
|
|
- toggleAutoRefreshButton.textContent = '暂停自动刷新';
|
|
|
- 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;
|
|
|
- isAutoRefreshPaused = true;
|
|
|
- toggleAutoRefreshButton.textContent = '恢复自动刷新';
|
|
|
- toggleAutoRefreshButton.classList.remove('bg-gray-500', 'hover:bg-gray-600');
|
|
|
- toggleAutoRefreshButton.classList.add('bg-green-500', 'hover:bg-green-600');
|
|
|
- console.log("自动刷新已暂停。");
|
|
|
- }
|
|
|
-
|
|
|
- // 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');
|
|
|
- }
|
|
|
+ function renderHistoryDataTable(data) {
|
|
|
+ const tableContainer = document.getElementById('history-table');
|
|
|
+
|
|
|
+ if (historyDataTable && !document.body.contains(historyDataTable.table)) {
|
|
|
+ historyDataTable = null;
|
|
|
+ }
|
|
|
+ if (historyDataTable) {
|
|
|
+ historyDataTable.destroy();
|
|
|
+ }
|
|
|
|
|
|
- processingTab.addEventListener('click', () => switchTab(processingTab, processingPane, historyTab, historyPane));
|
|
|
- historyTab.addEventListener('click', () => switchTab(historyTab, historyPane, processingTab, processingPane));
|
|
|
+ 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;
|
|
|
+ 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>`
|
|
|
+ ];
|
|
|
+ });
|
|
|
|
|
|
- // 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;
|
|
|
+ historyCountBadge.textContent = data.length;
|
|
|
+
|
|
|
+ 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');
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- // 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";
|
|
|
+ // --- Main Logic Functions ---
|
|
|
+ async function fetchData() {
|
|
|
+ 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);
|
|
|
+ document.getElementById('processing-list').innerHTML = `<tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败</td></tr>`;
|
|
|
+ document.getElementById('history-table').innerHTML = `<tbody><tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败</td></tr></tbody>`;
|
|
|
+ processingCountBadge.textContent = 'ERR';
|
|
|
+ historyCountBadge.textContent = 'ERR';
|
|
|
}
|
|
|
-
|
|
|
- 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();
|
|
|
}
|
|
|
- });
|
|
|
+ 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; }
|
|
|
+ function switchTab(activeTab) {
|
|
|
+ tabButtons.forEach(tab => {
|
|
|
+ tab.classList.remove('border-blue-500', 'text-blue-600');
|
|
|
+ tab.classList.add('border-transparent','hover:text-gray-600', 'hover:border-gray-300');
|
|
|
+ const paneId = tab.id.replace('-tab', '');
|
|
|
+ document.getElementById(paneId).classList.add('hidden');
|
|
|
+ });
|
|
|
+ activeTab.classList.add('border-blue-500', 'text-blue-600');
|
|
|
+ activeTab.classList.remove('border-transparent','hover:text-gray-600', 'hover:border-gray-300');
|
|
|
|
|
|
- document.getElementById('tabContent').addEventListener('click', function(event) {
|
|
|
- if (event.target.classList.contains('view-details-btn')) {
|
|
|
- const taskId = event.target.dataset.taskId;
|
|
|
- openModal(taskId);
|
|
|
+ const paneId = activeTab.id.replace('-tab', '');
|
|
|
+ document.getElementById(paneId).classList.remove('hidden');
|
|
|
}
|
|
|
- });
|
|
|
-
|
|
|
- refreshButton.addEventListener('click', refreshAllData);
|
|
|
- toggleAutoRefreshButton.addEventListener('click', () => {
|
|
|
- if (isAutoRefreshPaused) {
|
|
|
- startAutoRefresh();
|
|
|
- refreshAllData();
|
|
|
- } else {
|
|
|
- stopAutoRefresh();
|
|
|
+ function openModal(taskId) {
|
|
|
+ const task = allTasksData[taskId];
|
|
|
+ if (!task) { modalBody.innerHTML = '<p class="text-red-500">无法加载任务详情。</p>'; return; }
|
|
|
+ let basicInfoHtml = '<dl class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-4 gap-y-3 text-xs">';
|
|
|
+ const infoMap = new Map([
|
|
|
+ ["交易对", `<b>${task.symbol || 'N/A'}</b>`],
|
|
|
+ ["状态", `<span class="font-semibold ${getStatusTextClass(task.currentState)}">${task.currentState || 'N/A'}</span>`],
|
|
|
+ ["利润", task.profit !== null && !isNaN(parseFloat(task.profit)) ? `<b>${parseFloat(task.profit).toFixed(4)}</b>` : 'N/A'],
|
|
|
+ ["ID", `<span class="text-gray-500 break-all">${task.id || 'N/A'}</span>`],
|
|
|
+ ["创建时间", task.creationTime || 'N/A'],
|
|
|
+ ["来源", `${task.fromTokenAmountHuman || ''} ${task.fromToken || ''}`],
|
|
|
+ ["目标", `${task.toTokenAmountHuman || ''} ${task.toToken || ''}`],
|
|
|
+ ]);
|
|
|
+ infoMap.forEach((value, label) => {
|
|
|
+ let itemClass = "sm:col-span-1";
|
|
|
+ if (label === "ID") itemClass = "col-span-2 sm:col-span-3 lg:col-span-4";
|
|
|
+ basicInfoHtml += `<div class="${itemClass}"><dt class="font-medium text-gray-500">${label}</dt><dd class="mt-1 text-gray-900">${value}</dd></div>`;
|
|
|
+ });
|
|
|
+ modalBody.innerHTML = `${basicInfoHtml}</dl><hr class="my-3"><h4 class="text-base font-semibold text-gray-800 mb-2">状态流转:</h4>${formatStateFlowForModal(task.stateFlow)}`;
|
|
|
+ taskDetailModal.classList.remove('hidden');
|
|
|
+ document.body.classList.add('modal-active');
|
|
|
}
|
|
|
- });
|
|
|
-
|
|
|
- document.addEventListener('DOMContentLoaded', () => {
|
|
|
- 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');
|
|
|
+ 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)));
|
|
|
+ closeModalButton.addEventListener('click', closeModal);
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
+ // --- Initial Load ---
|
|
|
+ switchTab(document.getElementById('processing-tab'));
|
|
|
refreshAllData();
|
|
|
startAutoRefresh();
|
|
|
});
|