|
|
@@ -6,6 +6,7 @@
|
|
|
<title>{{ target_asset }}/SOL 多平台价格监控 (Plotly)</title>
|
|
|
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
|
|
|
<style>
|
|
|
+ /* CSS样式与之前相同,此处省略以保持简洁 */
|
|
|
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; }
|
|
|
h1, h2 { text-align: center; color: #333; margin-top:10px; margin-bottom:15px; }
|
|
|
@@ -29,6 +30,7 @@
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
+ <!-- HTML 结构与之前相同,此处省略 -->
|
|
|
<div class="container">
|
|
|
<h1 id="main-title">{{ target_asset }}/SOL 多平台价格监控</h1>
|
|
|
<div class="controls-container">
|
|
|
@@ -116,13 +118,16 @@
|
|
|
|
|
|
let pricePlotInitialized = false;
|
|
|
let diffPlotInitialized = false;
|
|
|
+ let isSyncingLayout = false; // Flag to prevent event loops
|
|
|
|
|
|
- function formatPriceForTable(priceStr, precision = 8) { // Default precision 8 for SOL prices
|
|
|
+ // --- Helper functions (formatPriceForTable, formatPercentageForTable) ---
|
|
|
+ // (与之前版本相同)
|
|
|
+ function formatPriceForTable(priceStr, precision = 8) {
|
|
|
if (priceStr === null || priceStr === undefined || String(priceStr).toLowerCase() === "n/a") return "N/A";
|
|
|
const price = parseFloat(priceStr);
|
|
|
if (isNaN(price)) return "N/A";
|
|
|
if (price === 0) return (0).toFixed(precision);
|
|
|
- if (Math.abs(price) < 1e-9 && price !== 0) return price.toExponential(3); // Very small numbers
|
|
|
+ if (Math.abs(price) < 1e-9 && price !== 0) return price.toExponential(3);
|
|
|
return price.toFixed(precision);
|
|
|
}
|
|
|
function formatPercentageForTable(percStr) {
|
|
|
@@ -133,7 +138,45 @@
|
|
|
return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`;
|
|
|
}
|
|
|
|
|
|
- async function updateTableData() {
|
|
|
+ // --- Function to sync X-axis between two Plotly charts ---
|
|
|
+ function syncPlotlyXAxes(sourceDiv, targetDiv, eventData) {
|
|
|
+ if (isSyncingLayout) return; // Prevent infinite loop
|
|
|
+ isSyncingLayout = true;
|
|
|
+
|
|
|
+ const update = {};
|
|
|
+ if (eventData && eventData['xaxis.range[0]'] !== undefined) {
|
|
|
+ // Zoom/Pan event
|
|
|
+ update['xaxis.range[0]'] = eventData['xaxis.range[0]'];
|
|
|
+ update['xaxis.range[1]'] = eventData['xaxis.range[1]'];
|
|
|
+ } else if (eventData && eventData['xaxis.autorange']) {
|
|
|
+ // Double-click to reset zoom (autorange)
|
|
|
+ update['xaxis.autorange'] = true;
|
|
|
+ } else {
|
|
|
+ // Fallback: try to get range from source layout if eventData is not specific enough
|
|
|
+ // This might not always be reliable if eventData is empty or non-standard
|
|
|
+ if (sourceDiv.layout && sourceDiv.layout.xaxis && sourceDiv.layout.xaxis.range) {
|
|
|
+ update['xaxis.range[0]'] = sourceDiv.layout.xaxis.range[0];
|
|
|
+ update['xaxis.range[1]'] = sourceDiv.layout.xaxis.range[1];
|
|
|
+ } else {
|
|
|
+ isSyncingLayout = false;
|
|
|
+ return; // Not enough info to sync
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Only apply if a valid update is constructed
|
|
|
+ if (Object.keys(update).length > 0) {
|
|
|
+ Plotly.relayout(targetDiv, update).then(() => {
|
|
|
+ isSyncingLayout = false;
|
|
|
+ }).catch(e => {
|
|
|
+ console.error("Error syncing layout:", e);
|
|
|
+ isSyncingLayout = false;
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ isSyncingLayout = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function updateTableData() { /* (与之前版本相同) */
|
|
|
if (isPaused) return;
|
|
|
try {
|
|
|
const response = await fetch('/table-data');
|
|
|
@@ -143,7 +186,7 @@
|
|
|
document.getElementById('oo-rfc-sol-price').textContent = formatPriceForTable(data.oo_rfc_sol_price);
|
|
|
document.getElementById('gate-spot-rfc-sol-price').textContent = formatPriceForTable(data.gate_spot_rfc_sol_price);
|
|
|
document.getElementById('gate-futures-rfc-sol-price').textContent = formatPriceForTable(data.gate_futures_rfc_sol_price);
|
|
|
- document.getElementById('sol-usdt-price-binance').textContent = formatPriceForTable(data.sol_usdt_price_binance, 4); // SOL/USDT with 4 decimals
|
|
|
+ document.getElementById('sol-usdt-price-binance').textContent = formatPriceForTable(data.sol_usdt_price_binance, 4);
|
|
|
|
|
|
document.getElementById('oo-status').textContent = data.oo_error || '正常';
|
|
|
document.getElementById('gate-spot-status').textContent = data.gate_spot_error || '正常';
|
|
|
@@ -169,12 +212,8 @@
|
|
|
if (!isNaN(val)) { el.className = val > 0 ? 'price-up' : (val < 0 ? 'price-down' : ''); }
|
|
|
else { el.className = ''; }
|
|
|
});
|
|
|
-
|
|
|
document.getElementById('last-updated').textContent = data.last_updated || "N/A";
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('Error fetching table data:', error);
|
|
|
- }
|
|
|
+ } catch (error) { console.error('Error fetching table data:', error); }
|
|
|
}
|
|
|
|
|
|
async function updatePlotlyCharts() {
|
|
|
@@ -184,15 +223,40 @@
|
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
const chartData = await response.json();
|
|
|
|
|
|
+ const currentPriceXRange = priceChartDiv.layout ? priceChartDiv.layout.xaxis.range : undefined;
|
|
|
+ const currentDiffXRange = diffChartDiv.layout ? diffChartDiv.layout.xaxis.range : undefined;
|
|
|
+
|
|
|
if (chartData.price_chart && chartData.price_chart.data && chartData.price_chart.layout) {
|
|
|
+ // Preserve X-axis range if user has zoomed/panned, unless paused and it's first draw
|
|
|
+ if (pricePlotInitialized && !isPaused && currentPriceXRange) {
|
|
|
+ // chartData.price_chart.layout.xaxis.range = currentPriceXRange; // This might reset if autorange is true
|
|
|
+ // Let Plotly.react handle it, it preserves zoom by default if data length doesn't change drastically or x values change
|
|
|
+ }
|
|
|
Plotly.react(priceChartDiv, chartData.price_chart.data, chartData.price_chart.layout, {responsive: true});
|
|
|
- if (!pricePlotInitialized) pricePlotInitialized = true;
|
|
|
+ if (!pricePlotInitialized) {
|
|
|
+ pricePlotInitialized = true;
|
|
|
+ // Add event listener AFTER first plot
|
|
|
+ priceChartDiv.on('plotly_relayout', (eventData) => {
|
|
|
+ console.log('Price chart relayout:', eventData);
|
|
|
+ syncPlotlyXAxes(priceChartDiv, diffChartDiv, eventData);
|
|
|
+ });
|
|
|
+ }
|
|
|
priceChartStatusDiv.textContent = `价格图表更新于: ${new Date().toLocaleTimeString()}`;
|
|
|
} else { priceChartStatusDiv.textContent = "错误: 价格图表数据无效。"; }
|
|
|
|
|
|
if (chartData.diff_chart && chartData.diff_chart.data && chartData.diff_chart.layout) {
|
|
|
+ if (diffPlotInitialized && !isPaused && currentDiffXRange) {
|
|
|
+ // chartData.diff_chart.layout.xaxis.range = currentDiffXRange;
|
|
|
+ }
|
|
|
Plotly.react(diffChartDiv, chartData.diff_chart.data, chartData.diff_chart.layout, {responsive: true});
|
|
|
- if (!diffPlotInitialized) diffPlotInitialized = true;
|
|
|
+ if (!diffPlotInitialized) {
|
|
|
+ diffPlotInitialized = true;
|
|
|
+ // Add event listener AFTER first plot
|
|
|
+ diffChartDiv.on('plotly_relayout', (eventData) => {
|
|
|
+ console.log('Diff chart relayout:', eventData);
|
|
|
+ syncPlotlyXAxes(diffChartDiv, priceChartDiv, eventData);
|
|
|
+ });
|
|
|
+ }
|
|
|
diffChartStatusDiv.textContent = `价差图表更新于: ${new Date().toLocaleTimeString()}`;
|
|
|
} else { diffChartStatusDiv.textContent = "错误: 价差图表数据无效。"; }
|
|
|
|
|
|
@@ -203,7 +267,7 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- function togglePauseResume() {
|
|
|
+ function togglePauseResume() { /* (与之前版本相同) */
|
|
|
isPaused = !isPaused;
|
|
|
if (isPaused) {
|
|
|
if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID); dataUpdateIntervalID = null;
|
|
|
@@ -216,6 +280,7 @@
|
|
|
}
|
|
|
pauseResumeButton.addEventListener('click', togglePauseResume);
|
|
|
|
|
|
+ // --- Initial Load ---
|
|
|
updateTableData(); updatePlotlyCharts();
|
|
|
if (!isPaused) {
|
|
|
dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs);
|