|
@@ -4,7 +4,9 @@
|
|
|
<meta charset="UTF-8">
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>价格与价差监控 MUBARAK/USDT</title>
|
|
<title>价格与价差监控 MUBARAK/USDT</title>
|
|
|
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <!-- 引入 Chart.js -->
|
|
|
|
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
|
+ <!-- 引入 chartjs-plugin-zoom 插件 -->
|
|
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
|
|
|
<style>
|
|
<style>
|
|
|
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
|
|
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
|
|
|
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
|
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
|
@@ -14,22 +16,52 @@
|
|
|
th { background-color: #e9e9e9; }
|
|
th { background-color: #e9e9e9; }
|
|
|
.price-up { color: green; }
|
|
.price-up { color: green; }
|
|
|
.price-down { color: red; }
|
|
.price-down { color: red; }
|
|
|
- .error-message { color: #c00; font-style: italic; font-size: 0.9em; } /* Renamed from .error */
|
|
|
|
|
|
|
+ .error-message { color: #c00; font-style: italic; font-size: 0.9em; }
|
|
|
.status-cell { /* For status messages, not just errors */ }
|
|
.status-cell { /* For status messages, not just errors */ }
|
|
|
.timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 15px; }
|
|
.timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 15px; }
|
|
|
.chart-container {
|
|
.chart-container {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
- height: 40vh; /* Responsive height */
|
|
|
|
|
- width: 80vw; /* Responsive width */
|
|
|
|
|
- margin: auto; /* Center the chart */
|
|
|
|
|
- margin-bottom: 30px;
|
|
|
|
|
|
|
+ height: 45vh; /* Slightly increased height for better zoom interaction */
|
|
|
|
|
+ width: 90vw;
|
|
|
|
|
+ margin: auto;
|
|
|
|
|
+ margin-bottom: 10px; /* Reduced bottom margin */
|
|
|
|
|
+ }
|
|
|
|
|
+ .controls-container { /* Container for buttons */
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .control-button {
|
|
|
|
|
+ background-color: #007bff;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ padding: 8px 15px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ margin: 0 5px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .control-button:hover {
|
|
|
|
|
+ background-color: #0056b3;
|
|
|
|
|
+ }
|
|
|
|
|
+ .pause-button-active {
|
|
|
|
|
+ background-color: #ffc107; /* Yellow when active (paused) */
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ }
|
|
|
|
|
+ .pause-button-active:hover {
|
|
|
|
|
+ background-color: #e0a800;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
<div class="container">
|
|
<div class="container">
|
|
|
<h1>MUBARAK/USDT 价格监控</h1>
|
|
<h1>MUBARAK/USDT 价格监控</h1>
|
|
|
-
|
|
|
|
|
|
|
+ <div class="controls-container">
|
|
|
|
|
+ <button id="pause-resume-button" class="control-button">暂停刷新</button>
|
|
|
|
|
+ </div>
|
|
|
<table>
|
|
<table>
|
|
|
<thead>
|
|
<thead>
|
|
|
<tr>
|
|
<tr>
|
|
@@ -63,6 +95,9 @@
|
|
|
<div class="chart-container">
|
|
<div class="chart-container">
|
|
|
<canvas id="priceHistoryChart"></canvas>
|
|
<canvas id="priceHistoryChart"></canvas>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="controls-container">
|
|
|
|
|
+ <button id="reset-price-zoom-button" class="control-button">重置缩放</button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="container">
|
|
<div class="container">
|
|
@@ -70,12 +105,20 @@
|
|
|
<div class="chart-container">
|
|
<div class="chart-container">
|
|
|
<canvas id="diffHistoryChart"></canvas>
|
|
<canvas id="diffHistoryChart"></canvas>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="controls-container">
|
|
|
|
|
+ <button id="reset-diff-zoom-button" class="control-button">重置缩放</button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
let priceChartInstance = null;
|
|
let priceChartInstance = null;
|
|
|
let diffChartInstance = null;
|
|
let diffChartInstance = null;
|
|
|
- const MAX_CHART_POINTS = 60; // 与后端 MAX_HISTORY_POINTS 对应或稍小
|
|
|
|
|
|
|
+ // const MAX_CHART_POINTS = 60; // MAX_HISTORY_POINTS is defined in backend
|
|
|
|
|
+
|
|
|
|
|
+ let dataUpdateIntervalID = null;
|
|
|
|
|
+ let isPaused = false;
|
|
|
|
|
+ const REFRESH_INTERVAL_MS = 5000; // 5 seconds
|
|
|
|
|
+ const pauseResumeButton = document.getElementById('pause-resume-button');
|
|
|
|
|
|
|
|
function formatPrice(priceStr) {
|
|
function formatPrice(priceStr) {
|
|
|
if (priceStr === null || priceStr === undefined || priceStr === "N/A") return "N/A";
|
|
if (priceStr === null || priceStr === undefined || priceStr === "N/A") return "N/A";
|
|
@@ -87,14 +130,14 @@
|
|
|
return new Chart(ctx, {
|
|
return new Chart(ctx, {
|
|
|
type: 'line',
|
|
type: 'line',
|
|
|
data: {
|
|
data: {
|
|
|
- labels: initialLabels, // 时间戳
|
|
|
|
|
- datasets: datasetsConfig // e.g., [{ label: 'OpenOcean', data: [], borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false }, ...]
|
|
|
|
|
|
|
+ labels: initialLabels,
|
|
|
|
|
+ datasets: datasetsConfig
|
|
|
},
|
|
},
|
|
|
options: {
|
|
options: {
|
|
|
responsive: true,
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
maintainAspectRatio: false,
|
|
|
animation: {
|
|
animation: {
|
|
|
- duration: 200 // 平滑过渡
|
|
|
|
|
|
|
+ duration: 150 // slightly faster animation
|
|
|
},
|
|
},
|
|
|
scales: {
|
|
scales: {
|
|
|
x: {
|
|
x: {
|
|
@@ -102,17 +145,35 @@
|
|
|
},
|
|
},
|
|
|
y: {
|
|
y: {
|
|
|
title: { display: true, text: chartType === 'price' ? '价格 (USDT)' : '价差 (%)' },
|
|
title: { display: true, text: chartType === 'price' ? '价格 (USDT)' : '价差 (%)' },
|
|
|
- beginAtZero: chartType === 'diff' ? false : undefined // 价差可能为负
|
|
|
|
|
|
|
+ beginAtZero: chartType === 'diff' ? false : undefined
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
plugins: {
|
|
plugins: {
|
|
|
legend: { position: 'top' },
|
|
legend: { position: 'top' },
|
|
|
- title: { display: true, text: titleText }
|
|
|
|
|
|
|
+ title: { display: true, text: titleText },
|
|
|
|
|
+ zoom: { // Zoom plugin configuration
|
|
|
|
|
+ pan: {
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ mode: 'xy', // Allow panning in x and y directions
|
|
|
|
|
+ threshold: 5, // Pixels before pan starts
|
|
|
|
|
+ },
|
|
|
|
|
+ zoom: {
|
|
|
|
|
+ wheel: {
|
|
|
|
|
+ enabled: true, // Enable zooming with mouse wheel
|
|
|
|
|
+ },
|
|
|
|
|
+ pinch: {
|
|
|
|
|
+ enabled: true // Enable zooming with pinch gesture (touch devices)
|
|
|
|
|
+ },
|
|
|
|
|
+ drag: { // Enable drag-to-zoom (box select)
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ backgroundColor: 'rgba(0,123,255,0.25)'
|
|
|
|
|
+ },
|
|
|
|
|
+ mode: 'xy', // Allow zooming in x and y directions
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
elements: {
|
|
elements: {
|
|
|
- point:{
|
|
|
|
|
- radius: 2 // Smaller points
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ point:{ radius: 2 }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
@@ -123,14 +184,16 @@
|
|
|
newDatasetsData.forEach((datasetData, index) => {
|
|
newDatasetsData.forEach((datasetData, index) => {
|
|
|
chartInstance.data.datasets[index].data = datasetData;
|
|
chartInstance.data.datasets[index].data = datasetData;
|
|
|
});
|
|
});
|
|
|
- chartInstance.update('quiet'); // 'quiet' to prevent re-animation on every update
|
|
|
|
|
|
|
+ chartInstance.update('quiet');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function updateDisplayAndCharts() {
|
|
function updateDisplayAndCharts() {
|
|
|
|
|
+ // If paused by user, don't fetch new data unless it's an initial call or manual resume
|
|
|
|
|
+ // This check is implicitly handled by not having setInterval running when paused.
|
|
|
|
|
+
|
|
|
fetch('/data')
|
|
fetch('/data')
|
|
|
.then(response => response.json())
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
.then(data => {
|
|
|
- // --- 更新表格数据 ---
|
|
|
|
|
const current = data.current;
|
|
const current = data.current;
|
|
|
document.getElementById('oo-price').textContent = formatPrice(current.oo_price);
|
|
document.getElementById('oo-price').textContent = formatPrice(current.oo_price);
|
|
|
document.getElementById('gate-price').textContent = formatPrice(current.gate_price);
|
|
document.getElementById('gate-price').textContent = formatPrice(current.gate_price);
|
|
@@ -139,7 +202,7 @@
|
|
|
if (current.difference_percentage && current.difference_percentage !== "N/A") {
|
|
if (current.difference_percentage && current.difference_percentage !== "N/A") {
|
|
|
diffEl.textContent = current.difference_percentage;
|
|
diffEl.textContent = current.difference_percentage;
|
|
|
const diffValue = parseFloat(current.difference_percentage.replace('%', ''));
|
|
const diffValue = parseFloat(current.difference_percentage.replace('%', ''));
|
|
|
- if (!isNaN(diffValue)) { // Ensure it's a number before comparing
|
|
|
|
|
|
|
+ if (!isNaN(diffValue)) {
|
|
|
if (diffValue > 0) diffEl.className = 'price-up';
|
|
if (diffValue > 0) diffEl.className = 'price-up';
|
|
|
else if (diffValue < 0) diffEl.className = 'price-down';
|
|
else if (diffValue < 0) diffEl.className = 'price-down';
|
|
|
else diffEl.className = '';
|
|
else diffEl.className = '';
|
|
@@ -161,26 +224,12 @@
|
|
|
|
|
|
|
|
document.getElementById('last-updated').textContent = current.last_updated || "N/A";
|
|
document.getElementById('last-updated').textContent = current.last_updated || "N/A";
|
|
|
|
|
|
|
|
- // --- 初始化或更新图表 ---
|
|
|
|
|
const history = data.history;
|
|
const history = data.history;
|
|
|
|
|
|
|
|
- // 价格历史图表
|
|
|
|
|
const priceCtx = document.getElementById('priceHistoryChart').getContext('2d');
|
|
const priceCtx = document.getElementById('priceHistoryChart').getContext('2d');
|
|
|
const priceDatasets = [
|
|
const priceDatasets = [
|
|
|
- {
|
|
|
|
|
- label: 'OpenOcean',
|
|
|
|
|
- data: history.prices.oo,
|
|
|
|
|
- borderColor: 'rgb(75, 192, 192)',
|
|
|
|
|
- tension: 0.1,
|
|
|
|
|
- fill: false
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: 'Gate.io',
|
|
|
|
|
- data: history.prices.gate,
|
|
|
|
|
- borderColor: 'rgb(255, 99, 132)',
|
|
|
|
|
- tension: 0.1,
|
|
|
|
|
- fill: false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ { label: 'OpenOcean', data: history.prices.oo, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false },
|
|
|
|
|
+ { label: 'Gate.io', data: history.prices.gate, borderColor: 'rgb(255, 99, 132)', tension: 0.1, fill: false }
|
|
|
];
|
|
];
|
|
|
if (!priceChartInstance) {
|
|
if (!priceChartInstance) {
|
|
|
priceChartInstance = initializeChart(priceCtx, 'price', priceDatasets, history.prices.labels, '价格历史 (MUBARAK/USDT)');
|
|
priceChartInstance = initializeChart(priceCtx, 'price', priceDatasets, history.prices.labels, '价格历史 (MUBARAK/USDT)');
|
|
@@ -188,37 +237,60 @@
|
|
|
updateChartData(priceChartInstance, history.prices.labels, [history.prices.oo, history.prices.gate]);
|
|
updateChartData(priceChartInstance, history.prices.labels, [history.prices.oo, history.prices.gate]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 价差历史图表
|
|
|
|
|
const diffCtx = document.getElementById('diffHistoryChart').getContext('2d');
|
|
const diffCtx = document.getElementById('diffHistoryChart').getContext('2d');
|
|
|
- const diffDatasets = [
|
|
|
|
|
- {
|
|
|
|
|
- label: '价差百分比 (OO vs Gate)',
|
|
|
|
|
- data: history.difference.values,
|
|
|
|
|
- borderColor: 'rgb(54, 162, 235)',
|
|
|
|
|
- tension: 0.1,
|
|
|
|
|
- fill: false
|
|
|
|
|
- }
|
|
|
|
|
- ];
|
|
|
|
|
- if (!diffChartInstance) {
|
|
|
|
|
|
|
+ const diffDatasets = [{ label: '价差百分比 (OO vs Gate)', data: history.difference.values, borderColor: 'rgb(54, 162, 235)', tension: 0.1, fill: false }];
|
|
|
|
|
+ if (!diffChartInstance) {
|
|
|
diffChartInstance = initializeChart(diffCtx, 'diff', diffDatasets, history.difference.labels, '价差百分比历史');
|
|
diffChartInstance = initializeChart(diffCtx, 'diff', diffDatasets, history.difference.labels, '价差百分比历史');
|
|
|
} else {
|
|
} else {
|
|
|
updateChartData(diffChartInstance, history.difference.labels, [history.difference.values]);
|
|
updateChartData(diffChartInstance, history.difference.labels, [history.difference.values]);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
})
|
|
})
|
|
|
.catch(error => {
|
|
.catch(error => {
|
|
|
console.error('Error fetching data:', error);
|
|
console.error('Error fetching data:', error);
|
|
|
- // Handle display errors if fetch fails
|
|
|
|
|
document.getElementById('oo-price').textContent = '错误';
|
|
document.getElementById('oo-price').textContent = '错误';
|
|
|
document.getElementById('gate-price').textContent = '错误';
|
|
document.getElementById('gate-price').textContent = '错误';
|
|
|
document.getElementById('diff-percentage').textContent = '无法获取数据';
|
|
document.getElementById('diff-percentage').textContent = '无法获取数据';
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 首次加载数据并初始化图表
|
|
|
|
|
|
|
+ function togglePauseResume() {
|
|
|
|
|
+ isPaused = !isPaused;
|
|
|
|
|
+ if (isPaused) {
|
|
|
|
|
+ clearInterval(dataUpdateIntervalID);
|
|
|
|
|
+ dataUpdateIntervalID = null; // Clear the ID
|
|
|
|
|
+ pauseResumeButton.textContent = '继续刷新';
|
|
|
|
|
+ pauseResumeButton.classList.add('pause-button-active');
|
|
|
|
|
+ console.log("Data refresh PAUSED");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ pauseResumeButton.textContent = '暂停刷新';
|
|
|
|
|
+ pauseResumeButton.classList.remove('pause-button-active');
|
|
|
|
|
+ updateDisplayAndCharts(); // Refresh immediately upon resuming
|
|
|
|
|
+ dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
|
|
|
|
|
+ console.log("Data refresh RESUMED");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- Event Listeners ---
|
|
|
|
|
+ pauseResumeButton.addEventListener('click', togglePauseResume);
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('reset-price-zoom-button').addEventListener('click', () => {
|
|
|
|
|
+ if (priceChartInstance) {
|
|
|
|
|
+ priceChartInstance.resetZoom();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('reset-diff-zoom-button').addEventListener('click', () => {
|
|
|
|
|
+ if (diffChartInstance) {
|
|
|
|
|
+ diffChartInstance.resetZoom();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Initial data load and start interval
|
|
|
updateDisplayAndCharts();
|
|
updateDisplayAndCharts();
|
|
|
- // 每5秒更新一次数据和图表 (与后端更新频率匹配)
|
|
|
|
|
- setInterval(updateDisplayAndCharts, 5000);
|
|
|
|
|
|
|
+ if (!isPaused) { // Start interval only if not initially paused (though it's false by default)
|
|
|
|
|
+ dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
</script>
|
|
</script>
|
|
|
</body>
|
|
</body>
|
|
|
-</html>
|
|
|
|
|
|
|
+</html>
|