Files
qihuo/static/js/market.js
T
dekun 6f3ac3deb6 新增行情K线页,支持分时与多周期图表
扩展新浪K线拉取与合成逻辑,提供 ECharts 交互图表及实时报价 API。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 17:07:04 +08:00

298 lines
12 KiB
JavaScript

(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('<br/>');
},
},
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();
}
});
})();