index.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>价格与价差监控 MUBARAK/USDT</title>
  7. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  8. <!-- 引入 chartjs-plugin-zoom 插件 -->
  9. <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
  10. <style>
  11. body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
  12. .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
  13. h1, h2 { text-align: center; color: #333; }
  14. table { width: 100%; border-collapse: collapse; margin-top: 20px; margin-bottom: 20px; }
  15. th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
  16. th { background-color: #e9e9e9; }
  17. .price-up { color: green; }
  18. .price-down { color: red; }
  19. .error-message { color: #c00; font-style: italic; font-size: 0.9em; }
  20. .status-cell { /* For status messages, not just errors */ }
  21. .timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 15px; }
  22. .chart-container {
  23. position: relative;
  24. height: 45vh; /* Slightly increased height for better zoom interaction */
  25. width: 90vw;
  26. margin: auto;
  27. margin-bottom: 10px; /* Reduced bottom margin */
  28. }
  29. .controls-container { /* Container for buttons */
  30. text-align: center;
  31. margin-bottom: 20px;
  32. margin-top: 5px;
  33. }
  34. .control-button {
  35. background-color: #007bff;
  36. color: white;
  37. border: none;
  38. padding: 8px 15px;
  39. text-align: center;
  40. text-decoration: none;
  41. display: inline-block;
  42. font-size: 14px;
  43. border-radius: 5px;
  44. cursor: pointer;
  45. margin: 0 5px;
  46. }
  47. .control-button:hover {
  48. background-color: #0056b3;
  49. }
  50. .pause-button-active {
  51. background-color: #ffc107; /* Yellow when active (paused) */
  52. color: #333;
  53. }
  54. .pause-button-active:hover {
  55. background-color: #e0a800;
  56. }
  57. </style>
  58. </head>
  59. <body>
  60. <div class="container">
  61. <h1>MUBARAK/USDT 价格监控</h1>
  62. <div class="controls-container">
  63. <button id="pause-resume-button" class="control-button">暂停刷新</button>
  64. </div>
  65. <table>
  66. <thead>
  67. <tr>
  68. <th>平台</th>
  69. <th>价格 (USDT/MUBARAK)</th>
  70. <th>状态/错误</th>
  71. </tr>
  72. </thead>
  73. <tbody>
  74. <tr>
  75. <td>OpenOcean (BSC)</td>
  76. <td id="oo-price">加载中...</td>
  77. <td id="oo-status" class="status-cell"></td>
  78. </tr>
  79. <tr>
  80. <td>Gate.io (Spot)</td>
  81. <td id="gate-price">加载中...</td>
  82. <td id="gate-status" class="status-cell"></td>
  83. </tr>
  84. <tr>
  85. <td><b>价差百分比</b></td>
  86. <td id="diff-percentage" colspan="2">计算中...</td>
  87. </tr>
  88. </tbody>
  89. </table>
  90. <div class="timestamp">最后更新: <span id="last-updated">N/A</span></div>
  91. </div>
  92. <div class="container">
  93. <h2>价格历史曲线</h2>
  94. <div class="chart-container">
  95. <canvas id="priceHistoryChart"></canvas>
  96. </div>
  97. <div class="controls-container">
  98. <button id="reset-price-zoom-button" class="control-button">重置缩放</button>
  99. </div>
  100. </div>
  101. <div class="container">
  102. <h2>价差百分比历史曲线</h2>
  103. <div class="chart-container">
  104. <canvas id="diffHistoryChart"></canvas>
  105. </div>
  106. <div class="controls-container">
  107. <button id="reset-diff-zoom-button" class="control-button">重置缩放</button>
  108. </div>
  109. </div>
  110. <script>
  111. let priceChartInstance = null;
  112. let diffChartInstance = null;
  113. // const MAX_CHART_POINTS = 60; // MAX_HISTORY_POINTS is defined in backend
  114. let dataUpdateIntervalID = null;
  115. let isPaused = false;
  116. const REFRESH_INTERVAL_MS = 5000; // 5 seconds
  117. const pauseResumeButton = document.getElementById('pause-resume-button');
  118. function formatPrice(priceStr) {
  119. if (priceStr === null || priceStr === undefined || priceStr === "N/A") return "N/A";
  120. const price = parseFloat(priceStr);
  121. return isNaN(price) ? "N/A" : price.toFixed(6);
  122. }
  123. function initializeChart(ctx, chartType, datasetsConfig, initialLabels, titleText) {
  124. return new Chart(ctx, {
  125. type: 'line',
  126. data: {
  127. labels: initialLabels,
  128. datasets: datasetsConfig
  129. },
  130. options: {
  131. responsive: true,
  132. maintainAspectRatio: false,
  133. animation: {
  134. duration: 150 // slightly faster animation
  135. },
  136. scales: {
  137. x: {
  138. title: { display: true, text: '时间' }
  139. },
  140. y: {
  141. title: { display: true, text: chartType === 'price' ? '价格 (USDT)' : '价差 (%)' },
  142. beginAtZero: chartType === 'diff' ? false : undefined
  143. }
  144. },
  145. plugins: {
  146. legend: { position: 'top' },
  147. title: { display: true, text: titleText },
  148. zoom: { // Zoom plugin configuration
  149. pan: {
  150. enabled: true,
  151. mode: 'xy', // Allow panning in x and y directions
  152. threshold: 5, // Pixels before pan starts
  153. },
  154. zoom: {
  155. wheel: {
  156. enabled: true, // Enable zooming with mouse wheel
  157. },
  158. pinch: {
  159. enabled: true // Enable zooming with pinch gesture (touch devices)
  160. },
  161. drag: { // Enable drag-to-zoom (box select)
  162. enabled: true,
  163. backgroundColor: 'rgba(0,123,255,0.25)'
  164. },
  165. mode: 'xy', // Allow zooming in x and y directions
  166. }
  167. }
  168. },
  169. elements: {
  170. point:{ radius: 2 }
  171. }
  172. }
  173. });
  174. }
  175. function updateChartData(chartInstance, newLabels, newDatasetsData) {
  176. chartInstance.data.labels = newLabels;
  177. newDatasetsData.forEach((datasetData, index) => {
  178. chartInstance.data.datasets[index].data = datasetData;
  179. });
  180. chartInstance.update('quiet');
  181. }
  182. function updateDisplayAndCharts() {
  183. // If paused by user, don't fetch new data unless it's an initial call or manual resume
  184. // This check is implicitly handled by not having setInterval running when paused.
  185. fetch('/data')
  186. .then(response => response.json())
  187. .then(data => {
  188. const current = data.current;
  189. document.getElementById('oo-price').textContent = formatPrice(current.oo_price);
  190. document.getElementById('gate-price').textContent = formatPrice(current.gate_price);
  191. const diffEl = document.getElementById('diff-percentage');
  192. if (current.difference_percentage && current.difference_percentage !== "N/A") {
  193. diffEl.textContent = current.difference_percentage;
  194. const diffValue = parseFloat(current.difference_percentage.replace('%', ''));
  195. if (!isNaN(diffValue)) {
  196. if (diffValue > 0) diffEl.className = 'price-up';
  197. else if (diffValue < 0) diffEl.className = 'price-down';
  198. else diffEl.className = '';
  199. } else {
  200. diffEl.className = '';
  201. }
  202. } else {
  203. diffEl.textContent = current.difference_percentage || "N/A";
  204. diffEl.className = '';
  205. }
  206. const ooStatusEl = document.getElementById('oo-status');
  207. ooStatusEl.textContent = current.oo_error ? `错误: ${current.oo_error}` : '正常';
  208. ooStatusEl.className = current.oo_error ? 'status-cell error-message' : 'status-cell ';
  209. const gateStatusEl = document.getElementById('gate-status');
  210. gateStatusEl.textContent = current.gate_error ? `错误: ${current.gate_error}` : '正常';
  211. gateStatusEl.className = current.gate_error ? 'status-cell error-message' : 'status-cell ';
  212. document.getElementById('last-updated').textContent = current.last_updated || "N/A";
  213. const history = data.history;
  214. const priceCtx = document.getElementById('priceHistoryChart').getContext('2d');
  215. const priceDatasets = [
  216. { label: 'OpenOcean', data: history.prices.oo, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false },
  217. { label: 'Gate.io', data: history.prices.gate, borderColor: 'rgb(255, 99, 132)', tension: 0.1, fill: false }
  218. ];
  219. if (!priceChartInstance) {
  220. priceChartInstance = initializeChart(priceCtx, 'price', priceDatasets, history.prices.labels, '价格历史 (MUBARAK/USDT)');
  221. } else {
  222. updateChartData(priceChartInstance, history.prices.labels, [history.prices.oo, history.prices.gate]);
  223. }
  224. const diffCtx = document.getElementById('diffHistoryChart').getContext('2d');
  225. const diffDatasets = [{ label: '价差百分比 (OO vs Gate)', data: history.difference.values, borderColor: 'rgb(54, 162, 235)', tension: 0.1, fill: false }];
  226. if (!diffChartInstance) {
  227. diffChartInstance = initializeChart(diffCtx, 'diff', diffDatasets, history.difference.labels, '价差百分比历史');
  228. } else {
  229. updateChartData(diffChartInstance, history.difference.labels, [history.difference.values]);
  230. }
  231. })
  232. .catch(error => {
  233. console.error('Error fetching data:', error);
  234. document.getElementById('oo-price').textContent = '错误';
  235. document.getElementById('gate-price').textContent = '错误';
  236. document.getElementById('diff-percentage').textContent = '无法获取数据';
  237. });
  238. }
  239. function togglePauseResume() {
  240. isPaused = !isPaused;
  241. if (isPaused) {
  242. clearInterval(dataUpdateIntervalID);
  243. dataUpdateIntervalID = null; // Clear the ID
  244. pauseResumeButton.textContent = '继续刷新';
  245. pauseResumeButton.classList.add('pause-button-active');
  246. console.log("Data refresh PAUSED");
  247. } else {
  248. pauseResumeButton.textContent = '暂停刷新';
  249. pauseResumeButton.classList.remove('pause-button-active');
  250. updateDisplayAndCharts(); // Refresh immediately upon resuming
  251. dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
  252. console.log("Data refresh RESUMED");
  253. }
  254. }
  255. // --- Event Listeners ---
  256. pauseResumeButton.addEventListener('click', togglePauseResume);
  257. document.getElementById('reset-price-zoom-button').addEventListener('click', () => {
  258. if (priceChartInstance) {
  259. priceChartInstance.resetZoom();
  260. }
  261. });
  262. document.getElementById('reset-diff-zoom-button').addEventListener('click', () => {
  263. if (diffChartInstance) {
  264. diffChartInstance.resetZoom();
  265. }
  266. });
  267. // Initial data load and start interval
  268. updateDisplayAndCharts();
  269. if (!isPaused) { // Start interval only if not initially paused (though it's false by default)
  270. dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
  271. }
  272. </script>
  273. </body>
  274. </html>