+393
-93
@@ -1,13 +1,16 @@
|
||||
(function () {
|
||||
var chartEl = document.getElementById('market-chart');
|
||||
var emptyEl = document.getElementById('market-chart-empty');
|
||||
var wrapEl = chartEl && chartEl.parentElement;
|
||||
var wrapEl = document.getElementById('market-chart-wrap');
|
||||
var chart = null;
|
||||
var currentPeriod = '15m';
|
||||
var klineSource = null;
|
||||
var streamActive = false;
|
||||
var reconnectTimer = null;
|
||||
var lastData = null;
|
||||
var lastPrevClose = null;
|
||||
var chartOpts = { prevClose: false, ma: false, gapDay: false };
|
||||
var dataZoomBound = false;
|
||||
|
||||
function getSymbol() {
|
||||
var hidden = document.getElementById('market-symbol-hidden');
|
||||
@@ -38,6 +41,9 @@
|
||||
area: dark ? 'rgba(76,194,255,0.12)' : 'rgba(37,99,235,0.1)',
|
||||
slider: dark ? '#1e2640' : '#cbd5e1',
|
||||
sliderFill: dark ? '#4cc2ff' : '#2563eb',
|
||||
ma21: dark ? '#ffb347' : '#d97706',
|
||||
ma55: dark ? '#c084fc' : '#7c3aed',
|
||||
prevClose: dark ? '#fbbf24' : '#b45309',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,6 +63,148 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
function calcMA(period, closes) {
|
||||
var result = [];
|
||||
for (var i = 0; i < closes.length; i++) {
|
||||
if (i < period - 1) {
|
||||
result.push(null);
|
||||
continue;
|
||||
}
|
||||
var sum = 0;
|
||||
for (var j = 0; j < period; j++) sum += closes[i - j];
|
||||
result.push(+(sum / period).toFixed(4));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function findMaCrosses(ma21, ma55) {
|
||||
var points = [];
|
||||
for (var i = 1; i < ma21.length; i++) {
|
||||
if (ma21[i] == null || ma55[i] == null || ma21[i - 1] == null || ma55[i - 1] == null) continue;
|
||||
var prev = ma21[i - 1] - ma55[i - 1];
|
||||
var curr = ma21[i] - ma55[i];
|
||||
if (prev <= 0 && curr > 0) points.push({ i: i, type: 'golden' });
|
||||
if (prev >= 0 && curr < 0) points.push({ i: i, type: 'death' });
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
function getDefaultZoomStart() {
|
||||
return currentPeriod === 'timeshare' ? 60 : 75;
|
||||
}
|
||||
|
||||
function getZoomRange() {
|
||||
if (!chart) return { start: getDefaultZoomStart(), end: 100 };
|
||||
var opt = chart.getOption();
|
||||
if (opt && opt.dataZoom && opt.dataZoom.length) {
|
||||
var z = opt.dataZoom[0];
|
||||
if (z.start != null && z.end != null) return { start: z.start, end: z.end };
|
||||
}
|
||||
return { start: getDefaultZoomStart(), end: 100 };
|
||||
}
|
||||
|
||||
function visibleIndices(bars, zoom) {
|
||||
var n = bars.length;
|
||||
if (!n) return { start: 0, end: 0 };
|
||||
var start = Math.floor(n * zoom.start / 100);
|
||||
var end = Math.min(n - 1, Math.ceil(n * zoom.end / 100) - 1);
|
||||
if (end < start) end = start;
|
||||
return { start: start, end: end };
|
||||
}
|
||||
|
||||
function computeVisibleHL(bars, startIdx, endIdx) {
|
||||
var maxH = -Infinity;
|
||||
var minL = Infinity;
|
||||
var maxI = startIdx;
|
||||
var minI = startIdx;
|
||||
for (var i = startIdx; i <= endIdx; i++) {
|
||||
var b = bars[i];
|
||||
if (!b) continue;
|
||||
if (b.high > maxH) { maxH = b.high; maxI = i; }
|
||||
if (b.low < minL) { minL = b.low; minI = i; }
|
||||
}
|
||||
if (maxH === -Infinity) return null;
|
||||
return { maxH: maxH, minL: minL, maxI: maxI, minI: minI };
|
||||
}
|
||||
|
||||
function buildHLMarkPoint(hl, c) {
|
||||
if (!hl) return { data: [] };
|
||||
return {
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
data: [
|
||||
{
|
||||
name: '高',
|
||||
xAxis: hl.maxI,
|
||||
yAxis: hl.maxH,
|
||||
value: hl.maxH.toFixed(2),
|
||||
itemStyle: { color: c.up },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '高 ' + hl.maxH.toFixed(2),
|
||||
color: c.up,
|
||||
fontSize: 11,
|
||||
position: 'top',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '低',
|
||||
xAxis: hl.minI,
|
||||
yAxis: hl.minL,
|
||||
value: hl.minL.toFixed(2),
|
||||
itemStyle: { color: c.down },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '低 ' + hl.minL.toFixed(2),
|
||||
color: c.down,
|
||||
fontSize: 11,
|
||||
position: 'bottom',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function buildMaCrossMarkPoint(crosses, bars, c) {
|
||||
if (!crosses.length) return { data: [] };
|
||||
return {
|
||||
symbol: 'pin',
|
||||
symbolSize: 28,
|
||||
data: crosses.map(function (p) {
|
||||
var b = bars[p.i];
|
||||
return {
|
||||
name: p.type === 'golden' ? '金叉' : '死叉',
|
||||
xAxis: p.i,
|
||||
yAxis: b ? b.close : 0,
|
||||
itemStyle: { color: p.type === 'golden' ? c.up : c.down },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: p.type === 'golden' ? '金叉' : '死叉',
|
||||
fontSize: 10,
|
||||
color: p.type === 'golden' ? c.up : c.down,
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildPrevCloseMarkLine(prevClose, c) {
|
||||
if (prevClose == null) return { data: [] };
|
||||
var v = Number(prevClose);
|
||||
return {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { color: c.prevClose, type: 'dashed', width: 1 },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '昨收 ' + v.toFixed(2),
|
||||
color: c.prevClose,
|
||||
fontSize: 10,
|
||||
},
|
||||
data: [{ yAxis: v }],
|
||||
};
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
if (!chartEl || !window.echarts) return;
|
||||
chart = echarts.init(chartEl);
|
||||
@@ -72,8 +220,28 @@
|
||||
});
|
||||
}
|
||||
|
||||
function bindDataZoomHL() {
|
||||
if (!chart || dataZoomBound) return;
|
||||
dataZoomBound = true;
|
||||
chart.on('dataZoom', function () {
|
||||
updateVisibleHLMark();
|
||||
});
|
||||
}
|
||||
|
||||
function updateVisibleHLMark() {
|
||||
if (!chart || !lastData || !lastData.bars) return;
|
||||
var bars = lastData.bars;
|
||||
var zoom = getZoomRange();
|
||||
var idx = visibleIndices(bars, zoom);
|
||||
var hl = computeVisibleHL(bars, idx.start, idx.end);
|
||||
var c = themeColors();
|
||||
chart.setOption({
|
||||
series: [{ id: 'main', markPoint: buildHLMarkPoint(hl, c) }],
|
||||
});
|
||||
}
|
||||
|
||||
function getDataZoom(c, preserve) {
|
||||
var defStart = currentPeriod === 'timeshare' ? 60 : 75;
|
||||
var defStart = getDefaultZoomStart();
|
||||
var zoom = [
|
||||
{
|
||||
type: 'inside',
|
||||
@@ -116,19 +284,61 @@
|
||||
return zoom;
|
||||
}
|
||||
|
||||
function mapSeriesData(bars, values, gapDay) {
|
||||
if (!gapDay) return values;
|
||||
return bars.map(function (b, i) {
|
||||
var v = values[i];
|
||||
if (v == null || b.timestamp == null) return v;
|
||||
return [b.timestamp, v];
|
||||
});
|
||||
}
|
||||
|
||||
function renderChart(data, preserveZoom) {
|
||||
if (!chart) return;
|
||||
lastData = data;
|
||||
if (data.prev_close != null) lastPrevClose = data.prev_close;
|
||||
|
||||
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';
|
||||
var gapDay = chartOpts.gapDay;
|
||||
var dataZoom = getDataZoom(c, preserveZoom);
|
||||
var zoom = preserveZoom ? getZoomRange() : { start: dataZoom[0].start, end: dataZoom[0].end };
|
||||
var vIdx = visibleIndices(bars, zoom);
|
||||
var hl = computeVisibleHL(bars, vIdx.start, vIdx.end);
|
||||
var closes = bars.map(function (b) { return b.close; });
|
||||
var ma21 = chartOpts.ma ? calcMA(21, closes) : null;
|
||||
var ma55 = chartOpts.ma ? calcMA(55, closes) : null;
|
||||
var crosses = chartOpts.ma ? findMaCrosses(ma21, ma55) : [];
|
||||
var prevCloseVal = lastPrevClose != null ? lastPrevClose : data.prev_close;
|
||||
var showPrev = chartOpts.prevClose && prevCloseVal != null;
|
||||
|
||||
var grids = [
|
||||
{ left: 56, right: 20, top: 44, height: '50%' },
|
||||
{ left: 56, right: 20, top: '66%', height: '14%' },
|
||||
{ left: 56, right: 24, top: 44, height: '50%' },
|
||||
{ left: 56, right: 24, top: '66%', height: '14%' },
|
||||
];
|
||||
|
||||
var xAxisType = gapDay ? 'time' : 'category';
|
||||
var xAxis0 = {
|
||||
type: xAxisType,
|
||||
gridIndex: 0,
|
||||
boundaryGap: gapDay ? false : true,
|
||||
axisLabel: { color: c.text, fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: c.grid } },
|
||||
};
|
||||
var xAxis1 = {
|
||||
type: xAxisType,
|
||||
gridIndex: 1,
|
||||
boundaryGap: gapDay ? false : true,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { lineStyle: { color: c.grid } },
|
||||
};
|
||||
if (!gapDay) {
|
||||
xAxis0.data = times;
|
||||
xAxis1.data = times;
|
||||
}
|
||||
|
||||
var base = {
|
||||
backgroundColor: c.bg,
|
||||
animation: false,
|
||||
@@ -136,98 +346,139 @@
|
||||
axisPointer: { link: [{ xAxisIndex: 'all' }] },
|
||||
dataZoom: dataZoom,
|
||||
grid: grids,
|
||||
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 } } },
|
||||
],
|
||||
xAxis: [xAxis0, xAxis1],
|
||||
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 },
|
||||
],
|
||||
};
|
||||
|
||||
if (isLine) {
|
||||
var closes = bars.map(function (b) { return b.close; });
|
||||
var vols = bars.map(function (b) { return b.volume; });
|
||||
chart.setOption(Object.assign(base, {
|
||||
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) {
|
||||
var up = b.close >= b.open;
|
||||
return {
|
||||
value: b.volume,
|
||||
itemStyle: { color: up ? c.up : c.down, opacity: 0.65 },
|
||||
};
|
||||
});
|
||||
chart.setOption(Object.assign(base, {
|
||||
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/>');
|
||||
},
|
||||
},
|
||||
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 series = [];
|
||||
var mainMark = {
|
||||
markPoint: buildHLMarkPoint(hl, c),
|
||||
};
|
||||
if (showPrev) mainMark.markLine = buildPrevCloseMarkLine(prevCloseVal, c);
|
||||
if (chartOpts.ma && crosses.length) {
|
||||
var crossMp = buildMaCrossMarkPoint(crosses, bars, c);
|
||||
mainMark.markPoint.data = mainMark.markPoint.data.concat(crossMp.data);
|
||||
}
|
||||
|
||||
if (isLine) {
|
||||
var lineData = mapSeriesData(bars, closes, gapDay);
|
||||
series.push({
|
||||
id: 'main',
|
||||
name: '价格',
|
||||
type: 'line',
|
||||
data: lineData,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { width: 1.5, color: c.line },
|
||||
areaStyle: { color: c.area },
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
markPoint: mainMark.markPoint,
|
||||
markLine: mainMark.markLine,
|
||||
});
|
||||
} else {
|
||||
var candle = bars.map(function (b) {
|
||||
if (gapDay && b.timestamp) {
|
||||
return [b.timestamp, b.open, b.close, b.low, b.high];
|
||||
}
|
||||
return [b.open, b.close, b.low, b.high];
|
||||
});
|
||||
series.push({
|
||||
id: 'main',
|
||||
name: 'K线',
|
||||
type: 'candlestick',
|
||||
data: candle,
|
||||
itemStyle: {
|
||||
color: c.up,
|
||||
color0: c.down,
|
||||
borderColor: c.up,
|
||||
borderColor0: c.down,
|
||||
},
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
markPoint: mainMark.markPoint,
|
||||
markLine: mainMark.markLine,
|
||||
});
|
||||
}
|
||||
|
||||
if (chartOpts.ma && ma21 && ma55) {
|
||||
series.push({
|
||||
id: 'ma21',
|
||||
name: 'MA21',
|
||||
type: 'line',
|
||||
data: mapSeriesData(bars, ma21, gapDay),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { width: 1, color: c.ma21 },
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
});
|
||||
series.push({
|
||||
id: 'ma55',
|
||||
name: 'MA55',
|
||||
type: 'line',
|
||||
data: mapSeriesData(bars, ma55, gapDay),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { width: 1, color: c.ma55 },
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
});
|
||||
}
|
||||
|
||||
var vols = bars.map(function (b) {
|
||||
var up = b.close >= b.open;
|
||||
var val = {
|
||||
value: b.volume,
|
||||
itemStyle: { color: up ? c.up : c.down, opacity: 0.65 },
|
||||
};
|
||||
if (gapDay && b.timestamp) {
|
||||
return { value: [b.timestamp, b.volume], itemStyle: val.itemStyle };
|
||||
}
|
||||
return val;
|
||||
});
|
||||
series.push({
|
||||
id: 'volume',
|
||||
name: '成交量',
|
||||
type: 'bar',
|
||||
data: vols,
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
});
|
||||
|
||||
if (!isLine) {
|
||||
base.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];
|
||||
var lines = [b.time, '开 ' + b.open, '高 ' + b.high, '低 ' + b.low, '收 ' + b.close, '量 ' + b.volume];
|
||||
if (ma21 && ma21[idx] != null) lines.push('MA21 ' + ma21[idx]);
|
||||
if (ma55 && ma55[idx] != null) lines.push('MA55 ' + ma55[idx]);
|
||||
return lines.join('<br/>');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
chart.setOption(Object.assign(base, { series: series }), 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 } },
|
||||
legend: chartOpts.ma ? {
|
||||
data: ['MA21', 'MA55'],
|
||||
top: 8,
|
||||
right: 12,
|
||||
textStyle: { color: c.text, fontSize: 10 },
|
||||
} : { show: false },
|
||||
});
|
||||
|
||||
bindDataZoomHL();
|
||||
}
|
||||
|
||||
function periodLabel(key) {
|
||||
@@ -239,15 +490,11 @@
|
||||
}
|
||||
|
||||
function hideEmptyOverlay() {
|
||||
if (emptyEl) emptyEl.style.display = '';
|
||||
if (wrapEl) wrapEl.classList.add('has-data');
|
||||
}
|
||||
|
||||
function showEmptyOverlay(text) {
|
||||
if (emptyEl) {
|
||||
emptyEl.textContent = text;
|
||||
emptyEl.style.display = 'flex';
|
||||
}
|
||||
if (emptyEl) emptyEl.textContent = text;
|
||||
if (wrapEl) wrapEl.classList.remove('has-data');
|
||||
}
|
||||
|
||||
@@ -257,8 +504,14 @@
|
||||
btn.disabled = on;
|
||||
btn.textContent = on ? '连接中…' : '查看';
|
||||
}
|
||||
if (on) showEmptyOverlay('连接中…');
|
||||
else if (lastData) hideEmptyOverlay();
|
||||
if (!wrapEl) return;
|
||||
if (on && !lastData) {
|
||||
wrapEl.classList.add('loading');
|
||||
showEmptyOverlay('请选择合约并点击「查看」');
|
||||
} else {
|
||||
wrapEl.classList.remove('loading');
|
||||
if (lastData) hideEmptyOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
function updateRefreshHint(disconnected) {
|
||||
@@ -284,6 +537,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updatePrevCloseDisplay(val) {
|
||||
var prevEl = document.getElementById('market-quote-prev');
|
||||
if (!prevEl) return;
|
||||
if (val != null && !isNaN(Number(val))) {
|
||||
prevEl.textContent = '昨收 ' + Number(val).toFixed(2);
|
||||
} else {
|
||||
prevEl.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
function applyQuote(data) {
|
||||
var priceEl = document.getElementById('market-quote-price');
|
||||
var nameEl = document.getElementById('market-quote-name');
|
||||
@@ -291,6 +554,11 @@
|
||||
if (priceEl) {
|
||||
priceEl.textContent = data.price != null ? Number(data.price).toFixed(2) : '—';
|
||||
}
|
||||
if (data.prev_close != null) {
|
||||
lastPrevClose = data.prev_close;
|
||||
updatePrevCloseDisplay(data.prev_close);
|
||||
if (chartOpts.prevClose && lastData) renderChart(lastData, true);
|
||||
}
|
||||
}
|
||||
|
||||
function stopKlineStream() {
|
||||
@@ -340,6 +608,7 @@
|
||||
hideEmptyOverlay();
|
||||
renderChart(data, lastData !== null);
|
||||
updateQuoteMeta(data);
|
||||
if (data.prev_close != null) updatePrevCloseDisplay(data.prev_close);
|
||||
updateRefreshHint(false);
|
||||
setLoading(false);
|
||||
} catch (err) { /* ignore */ }
|
||||
@@ -392,8 +661,7 @@
|
||||
|
||||
function resetDataZoom() {
|
||||
if (!chart) return;
|
||||
var start = currentPeriod === 'timeshare' ? 60 : 75;
|
||||
chart.dispatchAction({ type: 'dataZoom', start: start, end: 100 });
|
||||
chart.dispatchAction({ type: 'dataZoom', start: getDefaultZoomStart(), end: 100 });
|
||||
}
|
||||
|
||||
function bindPeriodTabs() {
|
||||
@@ -418,10 +686,35 @@
|
||||
if (zoomReset) zoomReset.addEventListener('click', resetDataZoom);
|
||||
}
|
||||
|
||||
function bindChartOptions() {
|
||||
var prevCb = document.getElementById('chart-opt-prev-close');
|
||||
var maCb = document.getElementById('chart-opt-ma');
|
||||
var gapCb = document.getElementById('chart-opt-gap-day');
|
||||
if (prevCb) {
|
||||
prevCb.addEventListener('change', function () {
|
||||
chartOpts.prevClose = prevCb.checked;
|
||||
if (lastData) renderChart(lastData, true);
|
||||
});
|
||||
}
|
||||
if (maCb) {
|
||||
maCb.addEventListener('change', function () {
|
||||
chartOpts.ma = maCb.checked;
|
||||
if (lastData) renderChart(lastData, true);
|
||||
});
|
||||
}
|
||||
if (gapCb) {
|
||||
gapCb.addEventListener('change', function () {
|
||||
chartOpts.gapDay = gapCb.checked;
|
||||
if (lastData) renderChart(lastData, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initChart();
|
||||
bindPeriodTabs();
|
||||
bindZoomButtons();
|
||||
bindChartOptions();
|
||||
|
||||
var active = document.querySelector('.period-tab.active');
|
||||
if (active) currentPeriod = active.getAttribute('data-period') || '15m';
|
||||
@@ -431,6 +724,13 @@
|
||||
|
||||
var hidden = document.getElementById('market-symbol-hidden');
|
||||
var input = document.getElementById('market-symbol-input');
|
||||
if (input) {
|
||||
input.addEventListener('symbol-selected', function () {
|
||||
lastPrevClose = null;
|
||||
updatePrevCloseDisplay(null);
|
||||
loadKline(true);
|
||||
});
|
||||
}
|
||||
if (hidden && hidden.value) {
|
||||
if (input && !input.value) input.value = hidden.value;
|
||||
loadKline(true);
|
||||
|
||||
+96
-11
@@ -9,6 +9,16 @@
|
||||
return item.input_label || (item.name + ' ' + item.ths_code);
|
||||
}
|
||||
|
||||
function itemMatchesQuery(item, qLower) {
|
||||
if (!qLower) return true;
|
||||
var hay = (
|
||||
item.name + ' ' + item.ths_code + ' ' +
|
||||
(item.display || '') + ' ' + (item.contract || '') + ' ' +
|
||||
(item.exchange || '')
|
||||
).toLowerCase();
|
||||
return hay.indexOf(qLower) >= 0;
|
||||
}
|
||||
|
||||
function initSymbolInput(wrapper) {
|
||||
const input = wrapper.querySelector('.symbol-input');
|
||||
const hiddenThs = wrapper.querySelector('input[name="symbol"]');
|
||||
@@ -17,9 +27,12 @@
|
||||
const hiddenSina = wrapper.querySelector('input[name="sina_code"]');
|
||||
const dropdown = wrapper.querySelector('.symbol-dropdown');
|
||||
const selectedEl = wrapper.querySelector('.symbol-selected');
|
||||
const isMarketPicker = wrapper.classList.contains('market-symbol-wrap');
|
||||
let timer = null;
|
||||
let abortCtrl = null;
|
||||
const cache = new Map();
|
||||
let mainsCache = null;
|
||||
let mainsLoading = false;
|
||||
|
||||
function hideDropdown() {
|
||||
dropdown.classList.remove('show');
|
||||
@@ -34,6 +47,28 @@
|
||||
if (hiddenSina) hiddenSina.value = item.sina_code || '';
|
||||
selectedEl.textContent = formatSub(item);
|
||||
hideDropdown();
|
||||
if (isMarketPicker) {
|
||||
input.dispatchEvent(new CustomEvent('symbol-selected', { detail: item }));
|
||||
}
|
||||
}
|
||||
|
||||
function buildOptionEl(item) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'symbol-option';
|
||||
if (item.near_expiry) {
|
||||
div.classList.add('near-expiry');
|
||||
}
|
||||
var label = item.display || (item.name + ' ' + item.ths_code);
|
||||
if (item.near_expiry) {
|
||||
label += ' <span class="near-expiry-tag">临期</span>';
|
||||
}
|
||||
div.innerHTML = label +
|
||||
'<div class="sub">' + formatSub(item) + '</div>';
|
||||
div.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
selectItem(item);
|
||||
});
|
||||
return div;
|
||||
}
|
||||
|
||||
function renderItems(items) {
|
||||
@@ -42,20 +77,57 @@
|
||||
dropdown.innerHTML = '<div class="symbol-option">无匹配,可输入同花顺代码如 ag2608</div>';
|
||||
} else {
|
||||
items.forEach(function (item) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'symbol-option';
|
||||
div.innerHTML = item.display +
|
||||
'<div class="sub">' + formatSub(item) + '</div>';
|
||||
div.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
selectItem(item);
|
||||
});
|
||||
dropdown.appendChild(div);
|
||||
dropdown.appendChild(buildOptionEl(item));
|
||||
});
|
||||
}
|
||||
dropdown.classList.add('show');
|
||||
}
|
||||
|
||||
function renderGrouped(groups, filterQ) {
|
||||
dropdown.innerHTML = '';
|
||||
const qLower = (filterQ || '').trim().toLowerCase();
|
||||
let any = false;
|
||||
groups.forEach(function (group) {
|
||||
const items = group.items.filter(function (item) {
|
||||
return itemMatchesQuery(item, qLower);
|
||||
});
|
||||
if (!items.length) return;
|
||||
any = true;
|
||||
const head = document.createElement('div');
|
||||
head.className = 'symbol-group-head';
|
||||
head.textContent = group.category;
|
||||
dropdown.appendChild(head);
|
||||
items.forEach(function (item) {
|
||||
dropdown.appendChild(buildOptionEl(item));
|
||||
});
|
||||
});
|
||||
if (!any) {
|
||||
dropdown.innerHTML = '<div class="symbol-option">无匹配品种,可输入合约代码如 ag2608</div>';
|
||||
}
|
||||
dropdown.classList.add('show');
|
||||
}
|
||||
|
||||
function showMarketMains(filterQ) {
|
||||
if (mainsCache) {
|
||||
renderGrouped(mainsCache, filterQ);
|
||||
return;
|
||||
}
|
||||
if (mainsLoading) return;
|
||||
mainsLoading = true;
|
||||
fetch('/api/symbols/mains')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (groups) {
|
||||
mainsCache = groups;
|
||||
renderGrouped(groups, filterQ);
|
||||
})
|
||||
.catch(function () {
|
||||
hideDropdown();
|
||||
})
|
||||
.finally(function () {
|
||||
mainsLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function search(q) {
|
||||
if (cache.has(q)) {
|
||||
renderItems(cache.get(q));
|
||||
@@ -87,12 +159,20 @@
|
||||
selectedEl.textContent = '';
|
||||
const q = input.value.trim();
|
||||
if (!q) {
|
||||
hideDropdown();
|
||||
if (isMarketPicker) {
|
||||
showMarketMains('');
|
||||
} else {
|
||||
hideDropdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
search(q);
|
||||
if (isMarketPicker) {
|
||||
showMarketMains(q);
|
||||
} else {
|
||||
search(q);
|
||||
}
|
||||
}, 120);
|
||||
});
|
||||
|
||||
@@ -102,6 +182,10 @@
|
||||
|
||||
input.addEventListener('focus', function () {
|
||||
const q = input.value.trim();
|
||||
if (isMarketPicker) {
|
||||
showMarketMains(q);
|
||||
return;
|
||||
}
|
||||
if (q && !hiddenThs.value) {
|
||||
search(q);
|
||||
}
|
||||
@@ -113,6 +197,7 @@
|
||||
|
||||
document.querySelectorAll('form').forEach(function (form) {
|
||||
if (!form.querySelector('.symbol-wrap')) return;
|
||||
if (form.id === 'market-form') return;
|
||||
form.addEventListener('submit', function (e) {
|
||||
const ths = form.querySelector('input[name="symbol"]');
|
||||
const market = form.querySelector('input[name="market_code"]');
|
||||
|
||||
Reference in New Issue
Block a user