(function () { var chartEl = document.getElementById('market-chart'); var emptyEl = document.getElementById('market-chart-empty'); var wrapEl = chartEl && chartEl.parentElement; var chart = null; var currentPeriod = '15m'; var quoteTimer = null; function getSymbol() { var hidden = document.getElementById('market-symbol-hidden'); var input = document.getElementById('market-symbol-input'); if (hidden && hidden.value) return hidden.value.trim(); if (input && input.value) return input.value.trim(); return ''; } function getMarketCodes() { return { symbol: getSymbol(), market_code: (document.getElementById('market-market-code') || {}).value || '', sina_code: (document.getElementById('market-sina-code') || {}).value || '', }; } function themeColors() { var dark = document.documentElement.getAttribute('data-theme') !== 'light'; return { bg: dark ? '#0a0c14' : '#f4f7fc', text: dark ? '#a8b0c8' : '#5c6578', title: dark ? '#e8eaf6' : '#1a2233', grid: dark ? '#1a2038' : '#e2e8f0', up: dark ? '#4cd97f' : '#15803d', down: dark ? '#ff6b7a' : '#dc2626', line: dark ? '#4cc2ff' : '#2563eb', area: dark ? 'rgba(76,194,255,0.12)' : 'rgba(37,99,235,0.1)', }; } function initChart() { if (!chartEl || !window.echarts) return; chart = echarts.init(chartEl); window.addEventListener('resize', function () { if (chart) chart.resize(); }); document.addEventListener('click', function (e) { if (e.target.closest('[data-theme-pick]')) { setTimeout(function () { if (chart && lastData) renderChart(lastData); }, 100); } }); } var lastData = null; function renderChart(data) { if (!chart) return; lastData = data; var c = themeColors(); var bars = data.bars || []; var times = bars.map(function (b) { return b.time; }); var isLine = data.chart_type === 'line' || data.period === 'timeshare'; if (isLine) { var closes = bars.map(function (b) { return b.close; }); var vols = bars.map(function (b) { return b.volume; }); chart.setOption({ backgroundColor: c.bg, animation: false, tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } }, axisPointer: { link: [{ xAxisIndex: 'all' }] }, grid: [ { left: 56, right: 16, top: 40, height: '58%' }, { left: 56, right: 16, top: '72%', height: '18%' }, ], xAxis: [ { type: 'category', data: times, gridIndex: 0, axisLabel: { color: c.text, fontSize: 10 }, axisLine: { lineStyle: { color: c.grid } } }, { type: 'category', data: times, gridIndex: 1, axisLabel: { show: false }, axisLine: { lineStyle: { color: c.grid } } }, ], yAxis: [ { scale: true, gridIndex: 0, splitLine: { lineStyle: { color: c.grid } }, axisLabel: { color: c.text } }, { scale: true, gridIndex: 1, splitLine: { show: false }, axisLabel: { color: c.text, fontSize: 10 }, splitNumber: 2 }, ], series: [ { name: '价格', type: 'line', data: closes, smooth: true, showSymbol: false, lineStyle: { width: 1.5, color: c.line }, areaStyle: { color: c.area }, xAxisIndex: 0, yAxisIndex: 0, }, { name: '成交量', type: 'bar', data: vols, itemStyle: { color: c.area }, xAxisIndex: 1, yAxisIndex: 1, }, ], }, true); } else { var candle = bars.map(function (b) { return [b.open, b.close, b.low, b.high]; }); var vols = bars.map(function (b, i) { var up = b.close >= b.open; return { value: b.volume, itemStyle: { color: up ? c.up : c.down, opacity: 0.65 }, }; }); chart.setOption({ backgroundColor: c.bg, animation: false, tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, formatter: function (params) { var idx = params[0] && params[0].dataIndex; if (idx == null || !bars[idx]) return ''; var b = bars[idx]; return [ b.time, '开 ' + b.open, '高 ' + b.high, '低 ' + b.low, '收 ' + b.close, '量 ' + b.volume, ].join('
'); }, }, axisPointer: { link: [{ xAxisIndex: 'all' }] }, grid: [ { left: 56, right: 16, top: 40, height: '58%' }, { left: 56, right: 16, top: '72%', height: '18%' }, ], xAxis: [ { type: 'category', data: times, gridIndex: 0, axisLabel: { color: c.text, fontSize: 10 }, axisLine: { lineStyle: { color: c.grid } } }, { type: 'category', data: times, gridIndex: 1, axisLabel: { show: false }, axisLine: { lineStyle: { color: c.grid } } }, ], yAxis: [ { scale: true, gridIndex: 0, splitLine: { lineStyle: { color: c.grid } }, axisLabel: { color: c.text } }, { scale: true, gridIndex: 1, splitLine: { show: false }, axisLabel: { color: c.text, fontSize: 10 }, splitNumber: 2 }, ], series: [ { name: 'K线', type: 'candlestick', data: candle, itemStyle: { color: c.up, color0: c.down, borderColor: c.up, borderColor0: c.down, }, xAxisIndex: 0, yAxisIndex: 0, }, { name: '成交量', type: 'bar', data: vols, xAxisIndex: 1, yAxisIndex: 1, }, ], }, true); } var title = (data.chart_symbol || data.symbol || '') + ' · ' + periodLabel(data.period); chart.setOption({ title: { text: title, left: 12, top: 8, textStyle: { color: c.title, fontSize: 13, fontWeight: 600 } } }); } function periodLabel(key) { var tabs = document.querySelectorAll('.period-tab'); for (var i = 0; i < tabs.length; i++) { if (tabs[i].getAttribute('data-period') === key) return tabs[i].textContent; } return key; } function setLoading(on) { var btn = document.getElementById('market-load-btn'); if (btn) { btn.disabled = on; btn.textContent = on ? '加载中…' : '查看'; } if (emptyEl && on) { emptyEl.textContent = '加载中…'; emptyEl.style.display = 'flex'; } } function loadKline() { var symbol = getSymbol(); if (!symbol) { alert('请先选择或输入合约代码'); return; } setLoading(true); if (wrapEl) wrapEl.classList.remove('has-data'); var url = '/api/kline?symbol=' + encodeURIComponent(symbol) + '&period=' + encodeURIComponent(currentPeriod); fetch(url) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, data: j }; }); }) .then(function (res) { if (!res.ok) { throw new Error(res.data.error || '加载失败'); } if (wrapEl) wrapEl.classList.add('has-data'); renderChart(res.data); updateQuoteMeta(res.data); startQuotePoll(); }) .catch(function (err) { if (emptyEl) { emptyEl.textContent = err.message || '加载失败'; emptyEl.style.display = 'flex'; } if (wrapEl) wrapEl.classList.remove('has-data'); }) .finally(function () { setLoading(false); }); } function updateQuoteMeta(data) { var meta = document.getElementById('market-quote-meta'); if (meta) { meta.textContent = data.count ? ('共 ' + data.count + ' 根 · ' + periodLabel(data.period)) : ''; } var nameEl = document.getElementById('market-quote-name'); var hiddenName = document.getElementById('market-symbol-name'); if (nameEl) { nameEl.textContent = (hiddenName && hiddenName.value) || data.symbol || '—'; } } function loadQuote() { var codes = getMarketCodes(); if (!codes.symbol) return; var q = 'symbol=' + encodeURIComponent(codes.symbol); if (codes.market_code) q += '&market_code=' + encodeURIComponent(codes.market_code); if (codes.sina_code) q += '&sina_code=' + encodeURIComponent(codes.sina_code); fetch('/api/market_quote?' + q) .then(function (r) { return r.json(); }) .then(function (data) { var priceEl = document.getElementById('market-quote-price'); var nameEl = document.getElementById('market-quote-name'); if (nameEl && data.name) nameEl.textContent = data.name + ' ' + (data.symbol || ''); if (priceEl) { priceEl.textContent = data.price != null ? Number(data.price).toFixed(2) : '—'; } }) .catch(function () { /* ignore */ }); } function startQuotePoll() { if (quoteTimer) clearInterval(quoteTimer); loadQuote(); quoteTimer = setInterval(loadQuote, 5000); } function bindPeriodTabs() { var tabs = document.getElementById('market-period-tabs'); if (!tabs) return; tabs.addEventListener('click', function (e) { var btn = e.target.closest('.period-tab'); if (!btn) return; tabs.querySelectorAll('.period-tab').forEach(function (el) { el.classList.remove('active'); }); btn.classList.add('active'); currentPeriod = btn.getAttribute('data-period') || '15m'; if (getSymbol()) loadKline(); }); } document.addEventListener('DOMContentLoaded', function () { initChart(); bindPeriodTabs(); var active = document.querySelector('.period-tab.active'); if (active) currentPeriod = active.getAttribute('data-period') || '15m'; var loadBtn = document.getElementById('market-load-btn'); if (loadBtn) loadBtn.addEventListener('click', loadKline); var hidden = document.getElementById('market-symbol-hidden'); var input = document.getElementById('market-symbol-input'); if (hidden && hidden.value) { if (input && !input.value) input.value = hidden.value; loadKline(); } }); })();