Add personal license agreement and rename product section to tradable symbols.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+27
-23
@@ -1,23 +1,27 @@
|
||||
(function () {
|
||||
var form = document.getElementById('contract-search-form');
|
||||
if (!form) return;
|
||||
|
||||
var wrap = form.querySelector('.symbol-wrap');
|
||||
var hidden = wrap && wrap.querySelector('input[name="symbol"]');
|
||||
var visible = form.querySelector('#contract-symbol-input');
|
||||
|
||||
// 带 symbol 参数进入时,显示合约代码
|
||||
if (hidden && hidden.value && visible && !visible.value) {
|
||||
visible.value = hidden.value;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function () {
|
||||
if (!hidden || !visible) return;
|
||||
var v = visible.value.trim();
|
||||
// 若未从下拉选择,尝试用输入框内容(支持直接输入 rb2510)
|
||||
if (!hidden.value && v) {
|
||||
var m = v.match(/([A-Za-z]+\d{3,4})/);
|
||||
hidden.value = m ? m[1] : v;
|
||||
}
|
||||
});
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var form = document.getElementById('contract-search-form');
|
||||
if (!form) return;
|
||||
|
||||
var wrap = form.querySelector('.symbol-wrap');
|
||||
var hidden = wrap && wrap.querySelector('input[name="symbol"]');
|
||||
var visible = form.querySelector('#contract-symbol-input');
|
||||
|
||||
// 带 symbol 参数进入时,显示合约代码
|
||||
if (hidden && hidden.value && visible && !visible.value) {
|
||||
visible.value = hidden.value;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function () {
|
||||
if (!hidden || !visible) return;
|
||||
var v = visible.value.trim();
|
||||
// 若未从下拉选择,尝试用输入框内容(支持直接输入 rb2510)
|
||||
if (!hidden.value && v) {
|
||||
var m = v.match(/([A-Za-z]+\d{3,4})/);
|
||||
hidden.value = m ? m[1] : v;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
+127
-123
@@ -1,123 +1,127 @@
|
||||
(function () {
|
||||
var el = document.getElementById('equity-curve-chart');
|
||||
var raw = window.__EQUITY_CURVE__;
|
||||
if (!el || !raw || !raw.length || !window.LightweightCharts) return;
|
||||
|
||||
var chart = null;
|
||||
var series = null;
|
||||
var chartData = [];
|
||||
|
||||
function cssVar(name, fallback) {
|
||||
var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
return v || fallback;
|
||||
}
|
||||
|
||||
function themeColors() {
|
||||
return {
|
||||
bg: 'transparent',
|
||||
text: cssVar('--text-muted', '#7a82a0'),
|
||||
grid: cssVar('--table-border', 'rgba(76,194,255,.1)'),
|
||||
border: cssVar('--card-border', 'rgba(76,194,255,.22)'),
|
||||
line: cssVar('--accent', '#4cc2ff'),
|
||||
};
|
||||
}
|
||||
|
||||
function parseTime(s) {
|
||||
if (!s) return null;
|
||||
var t = String(s).trim().replace(' ', 'T');
|
||||
if (t.length === 16) t += ':00';
|
||||
var d = new Date(t);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
return Math.floor(d.getTime() / 1000);
|
||||
}
|
||||
|
||||
function buildData() {
|
||||
var data = [];
|
||||
var lastTs = 0;
|
||||
raw.forEach(function (p) {
|
||||
var ts = parseTime(p.time);
|
||||
if (ts == null) return;
|
||||
if (ts <= lastTs) ts = lastTs + 1;
|
||||
lastTs = ts;
|
||||
data.push({ time: ts, value: Number(p.value) });
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
function applyChartTheme() {
|
||||
if (!chart || !series) return;
|
||||
var c = themeColors();
|
||||
chart.applyOptions({
|
||||
layout: {
|
||||
background: { type: 'solid', color: c.bg },
|
||||
textColor: c.text,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: c.grid },
|
||||
horzLines: { color: c.grid },
|
||||
},
|
||||
rightPriceScale: { borderColor: c.border },
|
||||
timeScale: { borderColor: c.border },
|
||||
});
|
||||
series.applyOptions({ color: c.line });
|
||||
}
|
||||
|
||||
function renderChart() {
|
||||
chartData = buildData();
|
||||
if (!chartData.length) {
|
||||
el.innerHTML = '<p class="text-muted" style="padding:1rem">暂无资金曲线数据</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var c = themeColors();
|
||||
if (chart) {
|
||||
chart.remove();
|
||||
chart = null;
|
||||
series = null;
|
||||
}
|
||||
|
||||
chart = LightweightCharts.createChart(el, {
|
||||
width: el.clientWidth || 800,
|
||||
height: 220,
|
||||
layout: {
|
||||
background: { type: 'solid', color: c.bg },
|
||||
textColor: c.text,
|
||||
fontSize: 11,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: c.grid },
|
||||
horzLines: { color: c.grid },
|
||||
},
|
||||
rightPriceScale: { borderColor: c.border },
|
||||
timeScale: { borderColor: c.border, timeVisible: true, secondsVisible: false },
|
||||
});
|
||||
series = chart.addLineSeries({
|
||||
color: c.line,
|
||||
lineWidth: 2,
|
||||
priceFormat: { type: 'price', precision: 2, minMove: 0.01 },
|
||||
});
|
||||
series.setData(chartData);
|
||||
chart.timeScale().fitContent();
|
||||
}
|
||||
|
||||
renderChart();
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
if (chart) chart.applyOptions({ width: el.clientWidth || 800 });
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.closest('[data-theme-pick]')) {
|
||||
setTimeout(applyChartTheme, 50);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
var obs = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (m) {
|
||||
if (m.attributeName === 'data-theme') applyChartTheme();
|
||||
});
|
||||
});
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
}
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var el = document.getElementById('equity-curve-chart');
|
||||
var raw = window.__EQUITY_CURVE__;
|
||||
if (!el || !raw || !raw.length || !window.LightweightCharts) return;
|
||||
|
||||
var chart = null;
|
||||
var series = null;
|
||||
var chartData = [];
|
||||
|
||||
function cssVar(name, fallback) {
|
||||
var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
return v || fallback;
|
||||
}
|
||||
|
||||
function themeColors() {
|
||||
return {
|
||||
bg: 'transparent',
|
||||
text: cssVar('--text-muted', '#7a82a0'),
|
||||
grid: cssVar('--table-border', 'rgba(76,194,255,.1)'),
|
||||
border: cssVar('--card-border', 'rgba(76,194,255,.22)'),
|
||||
line: cssVar('--accent', '#4cc2ff'),
|
||||
};
|
||||
}
|
||||
|
||||
function parseTime(s) {
|
||||
if (!s) return null;
|
||||
var t = String(s).trim().replace(' ', 'T');
|
||||
if (t.length === 16) t += ':00';
|
||||
var d = new Date(t);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
return Math.floor(d.getTime() / 1000);
|
||||
}
|
||||
|
||||
function buildData() {
|
||||
var data = [];
|
||||
var lastTs = 0;
|
||||
raw.forEach(function (p) {
|
||||
var ts = parseTime(p.time);
|
||||
if (ts == null) return;
|
||||
if (ts <= lastTs) ts = lastTs + 1;
|
||||
lastTs = ts;
|
||||
data.push({ time: ts, value: Number(p.value) });
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
function applyChartTheme() {
|
||||
if (!chart || !series) return;
|
||||
var c = themeColors();
|
||||
chart.applyOptions({
|
||||
layout: {
|
||||
background: { type: 'solid', color: c.bg },
|
||||
textColor: c.text,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: c.grid },
|
||||
horzLines: { color: c.grid },
|
||||
},
|
||||
rightPriceScale: { borderColor: c.border },
|
||||
timeScale: { borderColor: c.border },
|
||||
});
|
||||
series.applyOptions({ color: c.line });
|
||||
}
|
||||
|
||||
function renderChart() {
|
||||
chartData = buildData();
|
||||
if (!chartData.length) {
|
||||
el.innerHTML = '<p class="text-muted" style="padding:1rem">暂无资金曲线数据</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var c = themeColors();
|
||||
if (chart) {
|
||||
chart.remove();
|
||||
chart = null;
|
||||
series = null;
|
||||
}
|
||||
|
||||
chart = LightweightCharts.createChart(el, {
|
||||
width: el.clientWidth || 800,
|
||||
height: 220,
|
||||
layout: {
|
||||
background: { type: 'solid', color: c.bg },
|
||||
textColor: c.text,
|
||||
fontSize: 11,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: c.grid },
|
||||
horzLines: { color: c.grid },
|
||||
},
|
||||
rightPriceScale: { borderColor: c.border },
|
||||
timeScale: { borderColor: c.border, timeVisible: true, secondsVisible: false },
|
||||
});
|
||||
series = chart.addLineSeries({
|
||||
color: c.line,
|
||||
lineWidth: 2,
|
||||
priceFormat: { type: 'price', precision: 2, minMove: 0.01 },
|
||||
});
|
||||
series.setData(chartData);
|
||||
chart.timeScale().fitContent();
|
||||
}
|
||||
|
||||
renderChart();
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
if (chart) chart.applyOptions({ width: el.clientWidth || 800 });
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.closest('[data-theme-pick]')) {
|
||||
setTimeout(applyChartTheme, 50);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
var obs = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (m) {
|
||||
if (m.attributeName === 'data-theme') applyChartTheme();
|
||||
});
|
||||
});
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
}
|
||||
})();
|
||||
|
||||
+38
-34
@@ -1,34 +1,38 @@
|
||||
(function () {
|
||||
var keyTimer = null;
|
||||
|
||||
function fmtDist(v) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
return Number(v).toFixed(2);
|
||||
}
|
||||
|
||||
function pollKeyPrices() {
|
||||
var list = document.getElementById('key-monitor-list');
|
||||
if (!list || !list.querySelector('.key-item')) return;
|
||||
|
||||
fetch('/api/key_prices')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (rows) {
|
||||
rows.forEach(function (row) {
|
||||
var el = list.querySelector('.key-item[data-key-id="' + row.id + '"]');
|
||||
if (!el) return;
|
||||
var priceEl = el.querySelector('.live-price');
|
||||
var upEl = el.querySelector('.dist-up');
|
||||
var downEl = el.querySelector('.dist-down');
|
||||
if (priceEl) priceEl.textContent = row.price != null ? row.price : '--';
|
||||
if (upEl) upEl.textContent = fmtDist(row.dist_upper);
|
||||
if (downEl) downEl.textContent = fmtDist(row.dist_lower);
|
||||
});
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
pollKeyPrices();
|
||||
keyTimer = setInterval(pollKeyPrices, 1000);
|
||||
});
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var keyTimer = null;
|
||||
|
||||
function fmtDist(v) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
return Number(v).toFixed(2);
|
||||
}
|
||||
|
||||
function pollKeyPrices() {
|
||||
var list = document.getElementById('key-monitor-list');
|
||||
if (!list || !list.querySelector('.key-item')) return;
|
||||
|
||||
fetch('/api/key_prices')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (rows) {
|
||||
rows.forEach(function (row) {
|
||||
var el = list.querySelector('.key-item[data-key-id="' + row.id + '"]');
|
||||
if (!el) return;
|
||||
var priceEl = el.querySelector('.live-price');
|
||||
var upEl = el.querySelector('.dist-up');
|
||||
var downEl = el.querySelector('.dist-down');
|
||||
if (priceEl) priceEl.textContent = row.price != null ? row.price : '--';
|
||||
if (upEl) upEl.textContent = fmtDist(row.dist_upper);
|
||||
if (downEl) downEl.textContent = fmtDist(row.dist_lower);
|
||||
});
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
pollKeyPrices();
|
||||
keyTimer = setInterval(pollKeyPrices, 1000);
|
||||
});
|
||||
})();
|
||||
|
||||
+660
-656
File diff suppressed because it is too large
Load Diff
+57
-53
@@ -1,53 +1,57 @@
|
||||
(function () {
|
||||
var toggle = document.getElementById('nav-toggle');
|
||||
var nav = document.getElementById('site-nav');
|
||||
var backdrop = document.getElementById('nav-backdrop');
|
||||
if (!toggle || !nav) return;
|
||||
|
||||
function openNav() {
|
||||
nav.classList.add('open');
|
||||
if (backdrop) {
|
||||
backdrop.hidden = false;
|
||||
backdrop.classList.add('show');
|
||||
}
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeNav() {
|
||||
nav.classList.remove('open');
|
||||
if (backdrop) {
|
||||
backdrop.classList.remove('show');
|
||||
backdrop.hidden = true;
|
||||
}
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function isMobileNav() {
|
||||
return window.matchMedia('(max-width: 767px)').matches;
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', function () {
|
||||
if (nav.classList.contains('open')) closeNav();
|
||||
else openNav();
|
||||
});
|
||||
|
||||
if (backdrop) {
|
||||
backdrop.addEventListener('click', closeNav);
|
||||
}
|
||||
|
||||
nav.querySelectorAll('a').forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
if (isMobileNav()) closeNav();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
if (!isMobileNav()) closeNav();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') closeNav();
|
||||
});
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var toggle = document.getElementById('nav-toggle');
|
||||
var nav = document.getElementById('site-nav');
|
||||
var backdrop = document.getElementById('nav-backdrop');
|
||||
if (!toggle || !nav) return;
|
||||
|
||||
function openNav() {
|
||||
nav.classList.add('open');
|
||||
if (backdrop) {
|
||||
backdrop.hidden = false;
|
||||
backdrop.classList.add('show');
|
||||
}
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeNav() {
|
||||
nav.classList.remove('open');
|
||||
if (backdrop) {
|
||||
backdrop.classList.remove('show');
|
||||
backdrop.hidden = true;
|
||||
}
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function isMobileNav() {
|
||||
return window.matchMedia('(max-width: 767px)').matches;
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', function () {
|
||||
if (nav.classList.contains('open')) closeNav();
|
||||
else openNav();
|
||||
});
|
||||
|
||||
if (backdrop) {
|
||||
backdrop.addEventListener('click', closeNav);
|
||||
}
|
||||
|
||||
nav.querySelectorAll('a').forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
if (isMobileNav()) closeNav();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
if (!isMobileNav()) closeNav();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') closeNav();
|
||||
});
|
||||
})();
|
||||
|
||||
+49
-45
@@ -1,45 +1,49 @@
|
||||
(function () {
|
||||
var timer = null;
|
||||
|
||||
function fmtDist(v) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
return v.toFixed(2);
|
||||
}
|
||||
|
||||
function pollPrices() {
|
||||
var list = document.getElementById('plan-monitor-list');
|
||||
if (!list || !list.querySelector('.plan-item')) return;
|
||||
|
||||
fetch('/api/plan_prices')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (rows) {
|
||||
rows.forEach(function (row) {
|
||||
var el = list.querySelector('.plan-item[data-plan-id="' + row.id + '"]');
|
||||
if (!el) return;
|
||||
var priceEl = el.querySelector('.live-price');
|
||||
var distEl = el.querySelector('.live-dist');
|
||||
var upEl = el.querySelector('.dist-up');
|
||||
var downEl = el.querySelector('.dist-down');
|
||||
if (priceEl) {
|
||||
priceEl.textContent = row.price != null ? row.price : '--';
|
||||
}
|
||||
if (row.in_zone && distEl) {
|
||||
distEl.innerHTML = '<span class="text-profit" style="font-weight:600">在区间内</span>';
|
||||
} else if (distEl) {
|
||||
distEl.innerHTML =
|
||||
'距上<span class="dist-up">' + fmtDist(row.dist_upper) + '</span> ' +
|
||||
'距下<span class="dist-down">' + fmtDist(row.dist_lower) + '</span>';
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (timer) clearInterval(timer);
|
||||
pollPrices();
|
||||
timer = setInterval(pollPrices, 1000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', startPolling);
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var timer = null;
|
||||
|
||||
function fmtDist(v) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
return v.toFixed(2);
|
||||
}
|
||||
|
||||
function pollPrices() {
|
||||
var list = document.getElementById('plan-monitor-list');
|
||||
if (!list || !list.querySelector('.plan-item')) return;
|
||||
|
||||
fetch('/api/plan_prices')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (rows) {
|
||||
rows.forEach(function (row) {
|
||||
var el = list.querySelector('.plan-item[data-plan-id="' + row.id + '"]');
|
||||
if (!el) return;
|
||||
var priceEl = el.querySelector('.live-price');
|
||||
var distEl = el.querySelector('.live-dist');
|
||||
var upEl = el.querySelector('.dist-up');
|
||||
var downEl = el.querySelector('.dist-down');
|
||||
if (priceEl) {
|
||||
priceEl.textContent = row.price != null ? row.price : '--';
|
||||
}
|
||||
if (row.in_zone && distEl) {
|
||||
distEl.innerHTML = '<span class="text-profit" style="font-weight:600">在区间内</span>';
|
||||
} else if (distEl) {
|
||||
distEl.innerHTML =
|
||||
'距上<span class="dist-up">' + fmtDist(row.dist_upper) + '</span> ' +
|
||||
'距下<span class="dist-down">' + fmtDist(row.dist_lower) + '</span>';
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (timer) clearInterval(timer);
|
||||
pollPrices();
|
||||
timer = setInterval(pollPrices, 1000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', startPolling);
|
||||
})();
|
||||
|
||||
+79
-75
@@ -1,75 +1,79 @@
|
||||
(function () {
|
||||
var posTimer = null;
|
||||
|
||||
function fmtNum(v, digits) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
return Number(v).toFixed(digits === undefined ? 2 : digits);
|
||||
}
|
||||
|
||||
function buildPosCard(row) {
|
||||
var pnlClass = '';
|
||||
if (row.float_pnl > 0) pnlClass = 'pnl-pos';
|
||||
if (row.float_pnl < 0) pnlClass = 'pnl-neg';
|
||||
var pnlText = '--';
|
||||
if (row.float_pnl != null) {
|
||||
var sign = row.float_pnl >= 0 ? '+' : '';
|
||||
pnlText = sign + fmtNum(row.float_pnl) + '元';
|
||||
if (row.float_pct != null) {
|
||||
pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)';
|
||||
}
|
||||
}
|
||||
var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--';
|
||||
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
||||
|
||||
return (
|
||||
'<div class="pos-card" data-pos-id="' + row.id + '">' +
|
||||
'<div class="pos-card-head">' +
|
||||
'<div><div class="title">' + row.symbol + ' <span class="badge dir">' + row.direction + '</span></div></div>' +
|
||||
'<form method="post" action="/close_position/' + row.id + '" style="display:inline" onsubmit="return confirm(\'确认平仓?\')">' +
|
||||
'<button type="submit" class="btn-del pos-del">平仓</button></form>' +
|
||||
'</div>' +
|
||||
'<div class="pos-card-meta">来源 <strong>手动输入</strong> · 风险 <strong>' +
|
||||
fmtNum(row.risk_pct) + '%≈' + fmtNum(row.risk_amount) + '元</strong></div>' +
|
||||
'<div class="pos-metrics">' +
|
||||
'<div class="cell"><label>成交价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
||||
'<div class="cell"><label>止损</label><div>' + fmtNum(row.stop_loss) + '</div></div>' +
|
||||
'<div class="cell"><label>止盈</label><div>' + fmtNum(row.take_profit) + '</div></div>' +
|
||||
'<div class="cell"><label>盈亏比</label><div>' + rr + '</div></div>' +
|
||||
'<div class="cell"><label>标记价</label><div>' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '</div></div>' +
|
||||
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
||||
'<div class="cell"><label>预估手续费</label><div>' + fmtNum(row.est_fee) + '元</div></div>' +
|
||||
'<div class="cell ' + (row.est_pnl_net > 0 ? 'pnl-pos' : (row.est_pnl_net < 0 ? 'pnl-neg' : '')) + '">' +
|
||||
'<label>扣费后</label><div>' + (row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + '元' : '--') + '</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="pos-footer">' +
|
||||
'<span>保证金 ' + fmtNum(row.margin) + '元</span>' +
|
||||
'<span>仓位占比 ' + fmtNum(row.position_pct) + '%</span>' +
|
||||
'<span>开仓 ' + (openT || '--') + '</span>' +
|
||||
'<span>持仓 ' + (row.holding_duration || '--') + '</span>' +
|
||||
'<span>张数 ' + row.lots + '</span>' +
|
||||
'<span>手续费(估) ' + fmtNum(row.est_fee) + '元 (' + (row.est_fee_close_type || '') + ')</span>' +
|
||||
'</div></div>'
|
||||
);
|
||||
}
|
||||
|
||||
function pollPositions() {
|
||||
var list = document.getElementById('position-live-list');
|
||||
if (!list) return;
|
||||
|
||||
fetch('/api/position_live')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (rows) {
|
||||
if (!rows.length) {
|
||||
list.innerHTML = '<div class="empty-hint">暂无持仓,左侧录入后显示</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = rows.map(buildPosCard).join('');
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
pollPositions();
|
||||
posTimer = setInterval(pollPositions, 1000);
|
||||
});
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var posTimer = null;
|
||||
|
||||
function fmtNum(v, digits) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
return Number(v).toFixed(digits === undefined ? 2 : digits);
|
||||
}
|
||||
|
||||
function buildPosCard(row) {
|
||||
var pnlClass = '';
|
||||
if (row.float_pnl > 0) pnlClass = 'pnl-pos';
|
||||
if (row.float_pnl < 0) pnlClass = 'pnl-neg';
|
||||
var pnlText = '--';
|
||||
if (row.float_pnl != null) {
|
||||
var sign = row.float_pnl >= 0 ? '+' : '';
|
||||
pnlText = sign + fmtNum(row.float_pnl) + '元';
|
||||
if (row.float_pct != null) {
|
||||
pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)';
|
||||
}
|
||||
}
|
||||
var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--';
|
||||
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
||||
|
||||
return (
|
||||
'<div class="pos-card" data-pos-id="' + row.id + '">' +
|
||||
'<div class="pos-card-head">' +
|
||||
'<div><div class="title">' + row.symbol + ' <span class="badge dir">' + row.direction + '</span></div></div>' +
|
||||
'<form method="post" action="/close_position/' + row.id + '" style="display:inline" onsubmit="return confirm(\'确认平仓?\')">' +
|
||||
'<button type="submit" class="btn-del pos-del">平仓</button></form>' +
|
||||
'</div>' +
|
||||
'<div class="pos-card-meta">来源 <strong>手动输入</strong> · 风险 <strong>' +
|
||||
fmtNum(row.risk_pct) + '%≈' + fmtNum(row.risk_amount) + '元</strong></div>' +
|
||||
'<div class="pos-metrics">' +
|
||||
'<div class="cell"><label>成交价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
||||
'<div class="cell"><label>止损</label><div>' + fmtNum(row.stop_loss) + '</div></div>' +
|
||||
'<div class="cell"><label>止盈</label><div>' + fmtNum(row.take_profit) + '</div></div>' +
|
||||
'<div class="cell"><label>盈亏比</label><div>' + rr + '</div></div>' +
|
||||
'<div class="cell"><label>标记价</label><div>' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '</div></div>' +
|
||||
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
||||
'<div class="cell"><label>预估手续费</label><div>' + fmtNum(row.est_fee) + '元</div></div>' +
|
||||
'<div class="cell ' + (row.est_pnl_net > 0 ? 'pnl-pos' : (row.est_pnl_net < 0 ? 'pnl-neg' : '')) + '">' +
|
||||
'<label>扣费后</label><div>' + (row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + '元' : '--') + '</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="pos-footer">' +
|
||||
'<span>保证金 ' + fmtNum(row.margin) + '元</span>' +
|
||||
'<span>仓位占比 ' + fmtNum(row.position_pct) + '%</span>' +
|
||||
'<span>开仓 ' + (openT || '--') + '</span>' +
|
||||
'<span>持仓 ' + (row.holding_duration || '--') + '</span>' +
|
||||
'<span>张数 ' + row.lots + '</span>' +
|
||||
'<span>手续费(估) ' + fmtNum(row.est_fee) + '元 (' + (row.est_fee_close_type || '') + ')</span>' +
|
||||
'</div></div>'
|
||||
);
|
||||
}
|
||||
|
||||
function pollPositions() {
|
||||
var list = document.getElementById('position-live-list');
|
||||
if (!list) return;
|
||||
|
||||
fetch('/api/position_live')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (rows) {
|
||||
if (!rows.length) {
|
||||
list.innerHTML = '<div class="empty-hint">暂无持仓,左侧录入后显示</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = rows.map(buildPosCard).join('');
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
pollPositions();
|
||||
posTimer = setInterval(pollPositions, 1000);
|
||||
});
|
||||
})();
|
||||
|
||||
+93
-89
@@ -1,89 +1,93 @@
|
||||
(function () {
|
||||
var deferredPrompt = null;
|
||||
var installBtn = document.getElementById('pwa-install-btn');
|
||||
var iosHint = document.getElementById('pwa-ios-hint');
|
||||
|
||||
function isStandalone() {
|
||||
return window.matchMedia('(display-mode: standalone)').matches
|
||||
|| window.navigator.standalone === true;
|
||||
}
|
||||
|
||||
function isIOS() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
&& !window.MSStream;
|
||||
}
|
||||
|
||||
function isTouchDevice() {
|
||||
return window.matchMedia('(hover: none) and (pointer: coarse)').matches
|
||||
|| window.matchMedia('(max-width: 1024px)').matches;
|
||||
}
|
||||
|
||||
function updateThemeColor() {
|
||||
var meta = document.getElementById('meta-theme-color');
|
||||
if (!meta) return;
|
||||
var theme = document.documentElement.getAttribute('data-theme');
|
||||
meta.setAttribute('content', theme === 'light' ? '#e8eef8' : '#050508');
|
||||
}
|
||||
|
||||
function showInstallBtn() {
|
||||
if (installBtn && !isStandalone()) {
|
||||
installBtn.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showIosHint() {
|
||||
if (iosHint && isIOS() && !isStandalone()) {
|
||||
iosHint.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(function () { /* ignore */ });
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', function (e) {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
showInstallBtn();
|
||||
});
|
||||
|
||||
if (installBtn) {
|
||||
installBtn.addEventListener('click', function () {
|
||||
if (!deferredPrompt) {
|
||||
if (isIOS()) showIosHint();
|
||||
return;
|
||||
}
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.then(function () {
|
||||
deferredPrompt = null;
|
||||
installBtn.hidden = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('appinstalled', function () {
|
||||
deferredPrompt = null;
|
||||
if (installBtn) installBtn.hidden = true;
|
||||
if (iosHint) iosHint.classList.remove('show');
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
updateThemeColor();
|
||||
showIosHint();
|
||||
if (isStandalone()) {
|
||||
if (installBtn) installBtn.hidden = true;
|
||||
if (iosHint) iosHint.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
if (isTouchDevice() && installBtn && deferredPrompt) {
|
||||
showInstallBtn();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var pick = e.target.closest('[data-theme-pick]');
|
||||
if (pick) setTimeout(updateThemeColor, 80);
|
||||
});
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var deferredPrompt = null;
|
||||
var installBtn = document.getElementById('pwa-install-btn');
|
||||
var iosHint = document.getElementById('pwa-ios-hint');
|
||||
|
||||
function isStandalone() {
|
||||
return window.matchMedia('(display-mode: standalone)').matches
|
||||
|| window.navigator.standalone === true;
|
||||
}
|
||||
|
||||
function isIOS() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
&& !window.MSStream;
|
||||
}
|
||||
|
||||
function isTouchDevice() {
|
||||
return window.matchMedia('(hover: none) and (pointer: coarse)').matches
|
||||
|| window.matchMedia('(max-width: 1024px)').matches;
|
||||
}
|
||||
|
||||
function updateThemeColor() {
|
||||
var meta = document.getElementById('meta-theme-color');
|
||||
if (!meta) return;
|
||||
var theme = document.documentElement.getAttribute('data-theme');
|
||||
meta.setAttribute('content', theme === 'light' ? '#e8eef8' : '#050508');
|
||||
}
|
||||
|
||||
function showInstallBtn() {
|
||||
if (installBtn && !isStandalone()) {
|
||||
installBtn.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showIosHint() {
|
||||
if (iosHint && isIOS() && !isStandalone()) {
|
||||
iosHint.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(function () { /* ignore */ });
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', function (e) {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
showInstallBtn();
|
||||
});
|
||||
|
||||
if (installBtn) {
|
||||
installBtn.addEventListener('click', function () {
|
||||
if (!deferredPrompt) {
|
||||
if (isIOS()) showIosHint();
|
||||
return;
|
||||
}
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.then(function () {
|
||||
deferredPrompt = null;
|
||||
installBtn.hidden = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('appinstalled', function () {
|
||||
deferredPrompt = null;
|
||||
if (installBtn) installBtn.hidden = true;
|
||||
if (iosHint) iosHint.classList.remove('show');
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
updateThemeColor();
|
||||
showIosHint();
|
||||
if (isStandalone()) {
|
||||
if (installBtn) installBtn.hidden = true;
|
||||
if (iosHint) iosHint.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
if (isTouchDevice() && installBtn && deferredPrompt) {
|
||||
showInstallBtn();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var pick = e.target.closest('[data-theme-pick]');
|
||||
if (pick) setTimeout(updateThemeColor, 80);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
function parseNum(v) {
|
||||
var n = parseFloat(v);
|
||||
|
||||
+159
-155
@@ -1,155 +1,159 @@
|
||||
(function () {
|
||||
var cache = null;
|
||||
|
||||
function fmtNum(v, suffix) {
|
||||
if (v === null || v === undefined || v === '') return '-';
|
||||
var n = Number(v);
|
||||
if (isNaN(n)) return String(v);
|
||||
var s = Number.isInteger(n) ? String(n) : n.toFixed(2);
|
||||
return suffix ? s + suffix : s;
|
||||
}
|
||||
|
||||
function fmtMoney(v) {
|
||||
if (v === null || v === undefined) return '-';
|
||||
return fmtNum(v) + ' 元';
|
||||
}
|
||||
|
||||
function fmtPct(v) {
|
||||
if (v === null || v === undefined) return '-';
|
||||
return fmtNum(v) + '%';
|
||||
}
|
||||
|
||||
function setSummary(s) {
|
||||
var map = {
|
||||
total_trades: function () { return fmtNum(s.total_trades); },
|
||||
win_rate: function () { return fmtPct(s.win_rate); },
|
||||
avg_profit: function () { return fmtMoney(s.avg_profit); },
|
||||
avg_loss: function () { return fmtMoney(s.avg_loss); },
|
||||
profit_loss_ratio: function () { return fmtNum(s.profit_loss_ratio); },
|
||||
consecutive_losses: function () { return fmtNum(s.consecutive_losses); },
|
||||
max_drawdown: function () {
|
||||
var amt = fmtMoney(s.max_drawdown);
|
||||
var pct = s.max_drawdown_pct ? ' (' + fmtPct(s.max_drawdown_pct) + ')' : '';
|
||||
return amt + pct;
|
||||
},
|
||||
max_loss_amount: function () { return fmtMoney(s.max_loss_amount); },
|
||||
max_loss_pct: function () { return fmtPct(s.max_loss_pct); },
|
||||
max_profit_amount: function () { return fmtMoney(s.max_profit_amount); },
|
||||
max_profit_pct: function () { return fmtPct(s.max_profit_pct); },
|
||||
total_fee: function () { return fmtMoney(s.total_fee); },
|
||||
emotion_count: function () { return fmtNum(s.emotion_count); },
|
||||
emotion_ratio: function () { return fmtPct(s.emotion_ratio); },
|
||||
};
|
||||
document.querySelectorAll('#stats-summary [data-k]').forEach(function (el) {
|
||||
var key = el.getAttribute('data-k');
|
||||
el.textContent = map[key] ? map[key]() : '-';
|
||||
});
|
||||
}
|
||||
|
||||
function fillViewSelect(views, selected) {
|
||||
var sel = document.getElementById('stats-view-select');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = '';
|
||||
views.forEach(function (v) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = v.key;
|
||||
opt.textContent = v.label;
|
||||
if (v.key === selected) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function cellClass(key, val) {
|
||||
if (key === 'total_net' || key === 'max_profit' || key === 'avg_profit') {
|
||||
if (val > 0) return 'text-profit';
|
||||
if (val < 0) return 'text-loss';
|
||||
}
|
||||
if (key === 'max_loss' || key === 'avg_loss' || key === 'total_fee') {
|
||||
return 'text-loss';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderBreakdown(key) {
|
||||
if (!cache || !cache.breakdowns) return;
|
||||
var block = cache.breakdowns[key];
|
||||
var head = document.getElementById('stats-breakdown-head');
|
||||
var body = document.getElementById('stats-breakdown-body');
|
||||
if (!block || !head || !body) return;
|
||||
|
||||
head.innerHTML = '';
|
||||
block.columns.forEach(function (col) {
|
||||
var th = document.createElement('th');
|
||||
th.textContent = col.label;
|
||||
head.appendChild(th);
|
||||
});
|
||||
|
||||
body.innerHTML = '';
|
||||
if (!block.rows || !block.rows.length) {
|
||||
var tr = document.createElement('tr');
|
||||
var td = document.createElement('td');
|
||||
td.colSpan = block.columns.length;
|
||||
td.className = 'text-muted';
|
||||
td.textContent = '暂无数据';
|
||||
tr.appendChild(td);
|
||||
body.appendChild(tr);
|
||||
return;
|
||||
}
|
||||
|
||||
block.rows.forEach(function (row) {
|
||||
var tr = document.createElement('tr');
|
||||
block.columns.forEach(function (col) {
|
||||
var td = document.createElement('td');
|
||||
var val = row[col.key];
|
||||
if (col.key === 'win_rate') {
|
||||
td.textContent = fmtPct(val);
|
||||
} else if (col.key === 'label') {
|
||||
td.textContent = val || '-';
|
||||
} else if (typeof val === 'number') {
|
||||
td.textContent = fmtNum(val);
|
||||
td.className = cellClass(col.key, val);
|
||||
} else {
|
||||
td.textContent = val != null ? val : '-';
|
||||
}
|
||||
tr.appendChild(td);
|
||||
});
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function applyData(data) {
|
||||
cache = data;
|
||||
setSummary(data.summary || {});
|
||||
var views = data.views || [];
|
||||
var sel = document.getElementById('stats-view-select');
|
||||
var current = sel && sel.value ? sel.value : (views[0] && views[0].key);
|
||||
fillViewSelect(views, current);
|
||||
renderBreakdown(current);
|
||||
var updated = document.getElementById('stats-updated');
|
||||
if (updated) {
|
||||
updated.textContent = data.updated_at
|
||||
? '统计更新于 ' + data.updated_at.replace('T', ' ')
|
||||
: '统计已加载';
|
||||
}
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
fetch('/api/stats')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(applyData)
|
||||
.catch(function () {
|
||||
var updated = document.getElementById('stats-updated');
|
||||
if (updated) updated.textContent = '加载失败,请刷新页面';
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var viewSel = document.getElementById('stats-view-select');
|
||||
if (viewSel) {
|
||||
viewSel.addEventListener('change', function () {
|
||||
renderBreakdown(this.value);
|
||||
});
|
||||
}
|
||||
loadStats();
|
||||
});
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var cache = null;
|
||||
|
||||
function fmtNum(v, suffix) {
|
||||
if (v === null || v === undefined || v === '') return '-';
|
||||
var n = Number(v);
|
||||
if (isNaN(n)) return String(v);
|
||||
var s = Number.isInteger(n) ? String(n) : n.toFixed(2);
|
||||
return suffix ? s + suffix : s;
|
||||
}
|
||||
|
||||
function fmtMoney(v) {
|
||||
if (v === null || v === undefined) return '-';
|
||||
return fmtNum(v) + ' 元';
|
||||
}
|
||||
|
||||
function fmtPct(v) {
|
||||
if (v === null || v === undefined) return '-';
|
||||
return fmtNum(v) + '%';
|
||||
}
|
||||
|
||||
function setSummary(s) {
|
||||
var map = {
|
||||
total_trades: function () { return fmtNum(s.total_trades); },
|
||||
win_rate: function () { return fmtPct(s.win_rate); },
|
||||
avg_profit: function () { return fmtMoney(s.avg_profit); },
|
||||
avg_loss: function () { return fmtMoney(s.avg_loss); },
|
||||
profit_loss_ratio: function () { return fmtNum(s.profit_loss_ratio); },
|
||||
consecutive_losses: function () { return fmtNum(s.consecutive_losses); },
|
||||
max_drawdown: function () {
|
||||
var amt = fmtMoney(s.max_drawdown);
|
||||
var pct = s.max_drawdown_pct ? ' (' + fmtPct(s.max_drawdown_pct) + ')' : '';
|
||||
return amt + pct;
|
||||
},
|
||||
max_loss_amount: function () { return fmtMoney(s.max_loss_amount); },
|
||||
max_loss_pct: function () { return fmtPct(s.max_loss_pct); },
|
||||
max_profit_amount: function () { return fmtMoney(s.max_profit_amount); },
|
||||
max_profit_pct: function () { return fmtPct(s.max_profit_pct); },
|
||||
total_fee: function () { return fmtMoney(s.total_fee); },
|
||||
emotion_count: function () { return fmtNum(s.emotion_count); },
|
||||
emotion_ratio: function () { return fmtPct(s.emotion_ratio); },
|
||||
};
|
||||
document.querySelectorAll('#stats-summary [data-k]').forEach(function (el) {
|
||||
var key = el.getAttribute('data-k');
|
||||
el.textContent = map[key] ? map[key]() : '-';
|
||||
});
|
||||
}
|
||||
|
||||
function fillViewSelect(views, selected) {
|
||||
var sel = document.getElementById('stats-view-select');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = '';
|
||||
views.forEach(function (v) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = v.key;
|
||||
opt.textContent = v.label;
|
||||
if (v.key === selected) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function cellClass(key, val) {
|
||||
if (key === 'total_net' || key === 'max_profit' || key === 'avg_profit') {
|
||||
if (val > 0) return 'text-profit';
|
||||
if (val < 0) return 'text-loss';
|
||||
}
|
||||
if (key === 'max_loss' || key === 'avg_loss' || key === 'total_fee') {
|
||||
return 'text-loss';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderBreakdown(key) {
|
||||
if (!cache || !cache.breakdowns) return;
|
||||
var block = cache.breakdowns[key];
|
||||
var head = document.getElementById('stats-breakdown-head');
|
||||
var body = document.getElementById('stats-breakdown-body');
|
||||
if (!block || !head || !body) return;
|
||||
|
||||
head.innerHTML = '';
|
||||
block.columns.forEach(function (col) {
|
||||
var th = document.createElement('th');
|
||||
th.textContent = col.label;
|
||||
head.appendChild(th);
|
||||
});
|
||||
|
||||
body.innerHTML = '';
|
||||
if (!block.rows || !block.rows.length) {
|
||||
var tr = document.createElement('tr');
|
||||
var td = document.createElement('td');
|
||||
td.colSpan = block.columns.length;
|
||||
td.className = 'text-muted';
|
||||
td.textContent = '暂无数据';
|
||||
tr.appendChild(td);
|
||||
body.appendChild(tr);
|
||||
return;
|
||||
}
|
||||
|
||||
block.rows.forEach(function (row) {
|
||||
var tr = document.createElement('tr');
|
||||
block.columns.forEach(function (col) {
|
||||
var td = document.createElement('td');
|
||||
var val = row[col.key];
|
||||
if (col.key === 'win_rate') {
|
||||
td.textContent = fmtPct(val);
|
||||
} else if (col.key === 'label') {
|
||||
td.textContent = val || '-';
|
||||
} else if (typeof val === 'number') {
|
||||
td.textContent = fmtNum(val);
|
||||
td.className = cellClass(col.key, val);
|
||||
} else {
|
||||
td.textContent = val != null ? val : '-';
|
||||
}
|
||||
tr.appendChild(td);
|
||||
});
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function applyData(data) {
|
||||
cache = data;
|
||||
setSummary(data.summary || {});
|
||||
var views = data.views || [];
|
||||
var sel = document.getElementById('stats-view-select');
|
||||
var current = sel && sel.value ? sel.value : (views[0] && views[0].key);
|
||||
fillViewSelect(views, current);
|
||||
renderBreakdown(current);
|
||||
var updated = document.getElementById('stats-updated');
|
||||
if (updated) {
|
||||
updated.textContent = data.updated_at
|
||||
? '统计更新于 ' + data.updated_at.replace('T', ' ')
|
||||
: '统计已加载';
|
||||
}
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
fetch('/api/stats')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(applyData)
|
||||
.catch(function () {
|
||||
var updated = document.getElementById('stats-updated');
|
||||
if (updated) updated.textContent = '加载失败,请刷新页面';
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var viewSel = document.getElementById('stats-view-select');
|
||||
if (viewSel) {
|
||||
viewSel.addEventListener('change', function () {
|
||||
renderBreakdown(this.value);
|
||||
});
|
||||
}
|
||||
loadStats();
|
||||
});
|
||||
})();
|
||||
|
||||
+140
-136
@@ -1,136 +1,140 @@
|
||||
(function () {
|
||||
var trendPayload = null;
|
||||
|
||||
function jsonPost(url, body) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body || {})
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
function formData(form) {
|
||||
var fd = new FormData(form);
|
||||
var o = {};
|
||||
fd.forEach(function (v, k) { o[k] = v; });
|
||||
return o;
|
||||
}
|
||||
|
||||
function showPreview(el, text, ok) {
|
||||
if (!el) return;
|
||||
if (!text) {
|
||||
el.hidden = true;
|
||||
el.textContent = '';
|
||||
return;
|
||||
}
|
||||
el.hidden = false;
|
||||
el.textContent = text;
|
||||
el.style.color = ok === false ? 'var(--loss)' : '';
|
||||
}
|
||||
|
||||
function formatPlan(plan) {
|
||||
if (!plan) return '';
|
||||
var lines = [];
|
||||
if (plan.symbol) lines.push('品种:' + plan.symbol);
|
||||
if (plan.target_lots != null) lines.push('目标手数:' + plan.target_lots);
|
||||
if (plan.first_lots != null) lines.push('首仓:' + plan.first_lots + ' 手');
|
||||
if (plan.grid && plan.grid.length) {
|
||||
lines.push('补仓档位:' + plan.grid.map(function (g) { return g.price; }).join(' → '));
|
||||
}
|
||||
if (plan.message) lines.push(plan.message);
|
||||
return lines.length ? lines.join('\n') : JSON.stringify(plan, null, 2);
|
||||
}
|
||||
|
||||
function formatRoll(preview) {
|
||||
if (!preview) return '';
|
||||
var lines = [];
|
||||
if (preview.add_lots != null) lines.push('加仓手数:' + preview.add_lots);
|
||||
if (preview.new_stop_loss != null) lines.push('新止损:' + preview.new_stop_loss);
|
||||
if (preview.total_lots != null) lines.push('合计手数:' + preview.total_lots);
|
||||
if (preview.worst_loss != null) lines.push('最坏亏损:' + preview.worst_loss + ' 元');
|
||||
if (preview.message) lines.push(preview.message);
|
||||
return lines.length ? lines.join('\n') : JSON.stringify(preview, null, 2);
|
||||
}
|
||||
|
||||
var trendForm = document.getElementById('trend-form');
|
||||
var btnPreview = document.getElementById('btn-trend-preview');
|
||||
var btnExec = document.getElementById('btn-trend-exec');
|
||||
var previewEl = document.getElementById('trend-preview');
|
||||
|
||||
if (btnPreview && trendForm) {
|
||||
btnPreview.addEventListener('click', function () {
|
||||
btnPreview.disabled = true;
|
||||
jsonPost('/api/strategy/trend/preview', formData(trendForm)).then(function (d) {
|
||||
if (!d.ok) {
|
||||
showPreview(previewEl, d.error || '预览失败', false);
|
||||
btnExec.hidden = true;
|
||||
return;
|
||||
}
|
||||
trendPayload = formData(trendForm);
|
||||
showPreview(previewEl, formatPlan(d.plan), true);
|
||||
btnExec.hidden = false;
|
||||
}).finally(function () {
|
||||
btnPreview.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (btnExec) {
|
||||
btnExec.addEventListener('click', function () {
|
||||
if (!trendPayload) return;
|
||||
btnExec.disabled = true;
|
||||
btnExec.textContent = '执行中…';
|
||||
jsonPost('/api/strategy/trend/execute', trendPayload).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
}).finally(function () {
|
||||
btnExec.disabled = false;
|
||||
btnExec.textContent = '确认执行首仓';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var rollForm = document.getElementById('roll-form');
|
||||
var btnRollP = document.getElementById('btn-roll-preview');
|
||||
var btnRollE = document.getElementById('btn-roll-exec');
|
||||
var rollPrev = document.getElementById('roll-preview');
|
||||
if (btnRollP && rollForm) {
|
||||
btnRollP.addEventListener('click', function () {
|
||||
btnRollP.disabled = true;
|
||||
jsonPost('/api/strategy/roll/preview', formData(rollForm)).then(function (d) {
|
||||
if (!d.ok) {
|
||||
showPreview(rollPrev, d.error, false);
|
||||
btnRollE.hidden = true;
|
||||
return;
|
||||
}
|
||||
showPreview(rollPrev, formatRoll(d.preview), true);
|
||||
btnRollE.hidden = false;
|
||||
}).finally(function () {
|
||||
btnRollP.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (btnRollE && rollForm) {
|
||||
btnRollE.addEventListener('click', function () {
|
||||
btnRollE.disabled = true;
|
||||
btnRollE.textContent = '执行中…';
|
||||
jsonPost('/api/strategy/roll/execute', formData(rollForm)).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
}).finally(function () {
|
||||
btnRollE.disabled = false;
|
||||
btnRollE.textContent = '执行滚仓';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var btnStop = document.getElementById('btn-trend-stop');
|
||||
if (btnStop) {
|
||||
btnStop.addEventListener('click', function () {
|
||||
var pid = document.querySelector('#trend-stop-form input[name=plan_id]');
|
||||
jsonPost('/api/strategy/trend/stop', { plan_id: pid ? pid.value : 0 }).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var trendPayload = null;
|
||||
|
||||
function jsonPost(url, body) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body || {})
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
function formData(form) {
|
||||
var fd = new FormData(form);
|
||||
var o = {};
|
||||
fd.forEach(function (v, k) { o[k] = v; });
|
||||
return o;
|
||||
}
|
||||
|
||||
function showPreview(el, text, ok) {
|
||||
if (!el) return;
|
||||
if (!text) {
|
||||
el.hidden = true;
|
||||
el.textContent = '';
|
||||
return;
|
||||
}
|
||||
el.hidden = false;
|
||||
el.textContent = text;
|
||||
el.style.color = ok === false ? 'var(--loss)' : '';
|
||||
}
|
||||
|
||||
function formatPlan(plan) {
|
||||
if (!plan) return '';
|
||||
var lines = [];
|
||||
if (plan.symbol) lines.push('品种:' + plan.symbol);
|
||||
if (plan.target_lots != null) lines.push('目标手数:' + plan.target_lots);
|
||||
if (plan.first_lots != null) lines.push('首仓:' + plan.first_lots + ' 手');
|
||||
if (plan.grid && plan.grid.length) {
|
||||
lines.push('补仓档位:' + plan.grid.map(function (g) { return g.price; }).join(' → '));
|
||||
}
|
||||
if (plan.message) lines.push(plan.message);
|
||||
return lines.length ? lines.join('\n') : JSON.stringify(plan, null, 2);
|
||||
}
|
||||
|
||||
function formatRoll(preview) {
|
||||
if (!preview) return '';
|
||||
var lines = [];
|
||||
if (preview.add_lots != null) lines.push('加仓手数:' + preview.add_lots);
|
||||
if (preview.new_stop_loss != null) lines.push('新止损:' + preview.new_stop_loss);
|
||||
if (preview.total_lots != null) lines.push('合计手数:' + preview.total_lots);
|
||||
if (preview.worst_loss != null) lines.push('最坏亏损:' + preview.worst_loss + ' 元');
|
||||
if (preview.message) lines.push(preview.message);
|
||||
return lines.length ? lines.join('\n') : JSON.stringify(preview, null, 2);
|
||||
}
|
||||
|
||||
var trendForm = document.getElementById('trend-form');
|
||||
var btnPreview = document.getElementById('btn-trend-preview');
|
||||
var btnExec = document.getElementById('btn-trend-exec');
|
||||
var previewEl = document.getElementById('trend-preview');
|
||||
|
||||
if (btnPreview && trendForm) {
|
||||
btnPreview.addEventListener('click', function () {
|
||||
btnPreview.disabled = true;
|
||||
jsonPost('/api/strategy/trend/preview', formData(trendForm)).then(function (d) {
|
||||
if (!d.ok) {
|
||||
showPreview(previewEl, d.error || '预览失败', false);
|
||||
btnExec.hidden = true;
|
||||
return;
|
||||
}
|
||||
trendPayload = formData(trendForm);
|
||||
showPreview(previewEl, formatPlan(d.plan), true);
|
||||
btnExec.hidden = false;
|
||||
}).finally(function () {
|
||||
btnPreview.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (btnExec) {
|
||||
btnExec.addEventListener('click', function () {
|
||||
if (!trendPayload) return;
|
||||
btnExec.disabled = true;
|
||||
btnExec.textContent = '执行中…';
|
||||
jsonPost('/api/strategy/trend/execute', trendPayload).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
}).finally(function () {
|
||||
btnExec.disabled = false;
|
||||
btnExec.textContent = '确认执行首仓';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var rollForm = document.getElementById('roll-form');
|
||||
var btnRollP = document.getElementById('btn-roll-preview');
|
||||
var btnRollE = document.getElementById('btn-roll-exec');
|
||||
var rollPrev = document.getElementById('roll-preview');
|
||||
if (btnRollP && rollForm) {
|
||||
btnRollP.addEventListener('click', function () {
|
||||
btnRollP.disabled = true;
|
||||
jsonPost('/api/strategy/roll/preview', formData(rollForm)).then(function (d) {
|
||||
if (!d.ok) {
|
||||
showPreview(rollPrev, d.error, false);
|
||||
btnRollE.hidden = true;
|
||||
return;
|
||||
}
|
||||
showPreview(rollPrev, formatRoll(d.preview), true);
|
||||
btnRollE.hidden = false;
|
||||
}).finally(function () {
|
||||
btnRollP.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (btnRollE && rollForm) {
|
||||
btnRollE.addEventListener('click', function () {
|
||||
btnRollE.disabled = true;
|
||||
btnRollE.textContent = '执行中…';
|
||||
jsonPost('/api/strategy/roll/execute', formData(rollForm)).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
}).finally(function () {
|
||||
btnRollE.disabled = false;
|
||||
btnRollE.textContent = '执行滚仓';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var btnStop = document.getElementById('btn-trend-stop');
|
||||
if (btnStop) {
|
||||
btnStop.addEventListener('click', function () {
|
||||
var pid = document.querySelector('#trend-stop-form input[name=plan_id]');
|
||||
jsonPost('/api/strategy/trend/stop', { plan_id: pid ? pid.value : 0 }).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
+286
-282
@@ -1,282 +1,286 @@
|
||||
(function () {
|
||||
var recommendedGroupsCache = null;
|
||||
var recommendedGroupsPromise = null;
|
||||
|
||||
function loadRecommendedGroups() {
|
||||
if (recommendedGroupsCache) {
|
||||
return Promise.resolve(recommendedGroupsCache);
|
||||
}
|
||||
if (recommendedGroupsPromise) {
|
||||
return recommendedGroupsPromise;
|
||||
}
|
||||
recommendedGroupsPromise = fetch('/api/symbols/recommended')
|
||||
.then(function (r) {
|
||||
if (!r.ok) {
|
||||
throw new Error('HTTP ' + r.status);
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(function (groups) {
|
||||
recommendedGroupsCache = Array.isArray(groups) ? groups : [];
|
||||
return recommendedGroupsCache;
|
||||
})
|
||||
.catch(function () {
|
||||
recommendedGroupsCache = null;
|
||||
throw new Error('load failed');
|
||||
})
|
||||
.finally(function () {
|
||||
recommendedGroupsPromise = null;
|
||||
});
|
||||
return recommendedGroupsPromise;
|
||||
}
|
||||
|
||||
function formatSub(item) {
|
||||
var sub = '同花顺 ' + item.ths_code +
|
||||
(item.market_code ? ' · ' + item.market_code : '') +
|
||||
' · ' + (item.exchange || '');
|
||||
if (item.max_lots != null && item.max_lots > 0) {
|
||||
sub += ' · 最大 ' + item.max_lots + ' 手';
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
function formatInputLabel(item) {
|
||||
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 groupedHasMatch(groups, qLower) {
|
||||
if (!qLower) return true;
|
||||
return groups.some(function (group) {
|
||||
return group.items.some(function (item) {
|
||||
return itemMatchesQuery(item, qLower);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initSymbolInput(wrapper) {
|
||||
const input = wrapper.querySelector('.symbol-input');
|
||||
const hiddenThs = wrapper.querySelector('input[name="symbol"]')
|
||||
|| wrapper.querySelector('.symbol-ths-code');
|
||||
const hiddenName = wrapper.querySelector('input[name="symbol_name"]');
|
||||
const hiddenMarket = wrapper.querySelector('input[name="market_code"]');
|
||||
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');
|
||||
const useMainsPicker = isMarketPicker || wrapper.classList.contains('symbol-mains');
|
||||
let timer = null;
|
||||
let abortCtrl = null;
|
||||
const cache = new Map();
|
||||
let mainsCache = null;
|
||||
|
||||
function hideDropdown() {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
|
||||
function selectItem(item) {
|
||||
const label = formatInputLabel(item);
|
||||
input.value = label;
|
||||
if (hiddenThs) hiddenThs.value = item.ths_code;
|
||||
if (hiddenName) hiddenName.value = item.name;
|
||||
if (hiddenMarket) hiddenMarket.value = item.market_code || '';
|
||||
if (hiddenSina) hiddenSina.value = item.sina_code || '';
|
||||
if (selectedEl) selectedEl.textContent = formatSub(item);
|
||||
hideDropdown();
|
||||
input.dispatchEvent(new CustomEvent('symbol-selected', { detail: item, bubbles: true }));
|
||||
}
|
||||
|
||||
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) {
|
||||
dropdown.innerHTML = '';
|
||||
if (!items.length) {
|
||||
dropdown.innerHTML = '<div class="symbol-option">无匹配,可输入同花顺代码如 ag2608</div>';
|
||||
} else {
|
||||
items.forEach(function (item) {
|
||||
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, onEmpty) {
|
||||
const q = (filterQ || '').trim();
|
||||
const qLower = q.toLowerCase();
|
||||
if (mainsCache) {
|
||||
if (!q || groupedHasMatch(mainsCache, qLower)) {
|
||||
renderGrouped(mainsCache, q);
|
||||
return;
|
||||
}
|
||||
if (typeof onEmpty === 'function') {
|
||||
onEmpty(q);
|
||||
return;
|
||||
}
|
||||
renderGrouped(mainsCache, q);
|
||||
return;
|
||||
}
|
||||
dropdown.innerHTML = '<div class="symbol-option">正在加载推荐品种…</div>';
|
||||
dropdown.classList.add('show');
|
||||
loadRecommendedGroups()
|
||||
.then(function (groups) {
|
||||
mainsCache = groups;
|
||||
if (!groups.length) {
|
||||
dropdown.innerHTML =
|
||||
'<div class="symbol-option">当前资金下暂无推荐品种,可输入合约代码搜索</div>';
|
||||
dropdown.classList.add('show');
|
||||
return;
|
||||
}
|
||||
showMarketMains(filterQ, onEmpty);
|
||||
})
|
||||
.catch(function () {
|
||||
dropdown.innerHTML =
|
||||
'<div class="symbol-option">推荐品种加载失败,请刷新页面或输入合约代码搜索</div>';
|
||||
dropdown.classList.add('show');
|
||||
});
|
||||
}
|
||||
|
||||
function search(q) {
|
||||
if (cache.has(q)) {
|
||||
renderItems(cache.get(q));
|
||||
return;
|
||||
}
|
||||
if (abortCtrl) {
|
||||
abortCtrl.abort();
|
||||
}
|
||||
abortCtrl = new AbortController();
|
||||
fetch('/api/symbols/search?q=' + encodeURIComponent(q), {
|
||||
signal: abortCtrl.signal,
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (items) {
|
||||
cache.set(q, items);
|
||||
renderItems(items);
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err && err.name === 'AbortError') return;
|
||||
hideDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuery(q) {
|
||||
if (useMainsPicker) {
|
||||
showMarketMains(q, function (query) {
|
||||
search(query);
|
||||
});
|
||||
} else {
|
||||
search(q);
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
if (hiddenThs) hiddenThs.value = '';
|
||||
if (hiddenName) hiddenName.value = '';
|
||||
if (hiddenMarket) hiddenMarket.value = '';
|
||||
if (hiddenSina) hiddenSina.value = '';
|
||||
if (selectedEl) selectedEl.textContent = '';
|
||||
const q = input.value.trim();
|
||||
if (!q) {
|
||||
if (useMainsPicker) {
|
||||
showMarketMains('');
|
||||
} else {
|
||||
hideDropdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
handleQuery(q);
|
||||
}, 120);
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function () {
|
||||
setTimeout(hideDropdown, 150);
|
||||
});
|
||||
|
||||
input.addEventListener('focus', function () {
|
||||
const q = input.value.trim();
|
||||
if (useMainsPicker) {
|
||||
showMarketMains(q, function (query) {
|
||||
if (query) search(query);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (q && hiddenThs && !hiddenThs.value) {
|
||||
search(q);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('.symbol-wrap').forEach(initSymbolInput);
|
||||
|
||||
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"]')
|
||||
|| form.querySelector('.symbol-ths-code');
|
||||
const market = form.querySelector('input[name="market_code"]');
|
||||
if (ths && !ths.value.trim()) {
|
||||
e.preventDefault();
|
||||
alert('请从下拉列表选择品种');
|
||||
return;
|
||||
}
|
||||
if (market && !market.value.trim()) {
|
||||
e.preventDefault();
|
||||
alert('请从下拉列表选择品种(需含同花顺行情代码)');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var recommendedGroupsCache = null;
|
||||
var recommendedGroupsPromise = null;
|
||||
|
||||
function loadRecommendedGroups() {
|
||||
if (recommendedGroupsCache) {
|
||||
return Promise.resolve(recommendedGroupsCache);
|
||||
}
|
||||
if (recommendedGroupsPromise) {
|
||||
return recommendedGroupsPromise;
|
||||
}
|
||||
recommendedGroupsPromise = fetch('/api/symbols/recommended')
|
||||
.then(function (r) {
|
||||
if (!r.ok) {
|
||||
throw new Error('HTTP ' + r.status);
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(function (groups) {
|
||||
recommendedGroupsCache = Array.isArray(groups) ? groups : [];
|
||||
return recommendedGroupsCache;
|
||||
})
|
||||
.catch(function () {
|
||||
recommendedGroupsCache = null;
|
||||
throw new Error('load failed');
|
||||
})
|
||||
.finally(function () {
|
||||
recommendedGroupsPromise = null;
|
||||
});
|
||||
return recommendedGroupsPromise;
|
||||
}
|
||||
|
||||
function formatSub(item) {
|
||||
var sub = '同花顺 ' + item.ths_code +
|
||||
(item.market_code ? ' · ' + item.market_code : '') +
|
||||
' · ' + (item.exchange || '');
|
||||
if (item.max_lots != null && item.max_lots > 0) {
|
||||
sub += ' · 最大 ' + item.max_lots + ' 手';
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
function formatInputLabel(item) {
|
||||
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 groupedHasMatch(groups, qLower) {
|
||||
if (!qLower) return true;
|
||||
return groups.some(function (group) {
|
||||
return group.items.some(function (item) {
|
||||
return itemMatchesQuery(item, qLower);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initSymbolInput(wrapper) {
|
||||
const input = wrapper.querySelector('.symbol-input');
|
||||
const hiddenThs = wrapper.querySelector('input[name="symbol"]')
|
||||
|| wrapper.querySelector('.symbol-ths-code');
|
||||
const hiddenName = wrapper.querySelector('input[name="symbol_name"]');
|
||||
const hiddenMarket = wrapper.querySelector('input[name="market_code"]');
|
||||
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');
|
||||
const useMainsPicker = isMarketPicker || wrapper.classList.contains('symbol-mains');
|
||||
let timer = null;
|
||||
let abortCtrl = null;
|
||||
const cache = new Map();
|
||||
let mainsCache = null;
|
||||
|
||||
function hideDropdown() {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
|
||||
function selectItem(item) {
|
||||
const label = formatInputLabel(item);
|
||||
input.value = label;
|
||||
if (hiddenThs) hiddenThs.value = item.ths_code;
|
||||
if (hiddenName) hiddenName.value = item.name;
|
||||
if (hiddenMarket) hiddenMarket.value = item.market_code || '';
|
||||
if (hiddenSina) hiddenSina.value = item.sina_code || '';
|
||||
if (selectedEl) selectedEl.textContent = formatSub(item);
|
||||
hideDropdown();
|
||||
input.dispatchEvent(new CustomEvent('symbol-selected', { detail: item, bubbles: true }));
|
||||
}
|
||||
|
||||
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) {
|
||||
dropdown.innerHTML = '';
|
||||
if (!items.length) {
|
||||
dropdown.innerHTML = '<div class="symbol-option">无匹配,可输入同花顺代码如 ag2608</div>';
|
||||
} else {
|
||||
items.forEach(function (item) {
|
||||
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, onEmpty) {
|
||||
const q = (filterQ || '').trim();
|
||||
const qLower = q.toLowerCase();
|
||||
if (mainsCache) {
|
||||
if (!q || groupedHasMatch(mainsCache, qLower)) {
|
||||
renderGrouped(mainsCache, q);
|
||||
return;
|
||||
}
|
||||
if (typeof onEmpty === 'function') {
|
||||
onEmpty(q);
|
||||
return;
|
||||
}
|
||||
renderGrouped(mainsCache, q);
|
||||
return;
|
||||
}
|
||||
dropdown.innerHTML = '<div class="symbol-option">正在加载推荐品种…</div>';
|
||||
dropdown.classList.add('show');
|
||||
loadRecommendedGroups()
|
||||
.then(function (groups) {
|
||||
mainsCache = groups;
|
||||
if (!groups.length) {
|
||||
dropdown.innerHTML =
|
||||
'<div class="symbol-option">当前资金下暂无推荐品种,可输入合约代码搜索</div>';
|
||||
dropdown.classList.add('show');
|
||||
return;
|
||||
}
|
||||
showMarketMains(filterQ, onEmpty);
|
||||
})
|
||||
.catch(function () {
|
||||
dropdown.innerHTML =
|
||||
'<div class="symbol-option">推荐品种加载失败,请刷新页面或输入合约代码搜索</div>';
|
||||
dropdown.classList.add('show');
|
||||
});
|
||||
}
|
||||
|
||||
function search(q) {
|
||||
if (cache.has(q)) {
|
||||
renderItems(cache.get(q));
|
||||
return;
|
||||
}
|
||||
if (abortCtrl) {
|
||||
abortCtrl.abort();
|
||||
}
|
||||
abortCtrl = new AbortController();
|
||||
fetch('/api/symbols/search?q=' + encodeURIComponent(q), {
|
||||
signal: abortCtrl.signal,
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (items) {
|
||||
cache.set(q, items);
|
||||
renderItems(items);
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err && err.name === 'AbortError') return;
|
||||
hideDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuery(q) {
|
||||
if (useMainsPicker) {
|
||||
showMarketMains(q, function (query) {
|
||||
search(query);
|
||||
});
|
||||
} else {
|
||||
search(q);
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
if (hiddenThs) hiddenThs.value = '';
|
||||
if (hiddenName) hiddenName.value = '';
|
||||
if (hiddenMarket) hiddenMarket.value = '';
|
||||
if (hiddenSina) hiddenSina.value = '';
|
||||
if (selectedEl) selectedEl.textContent = '';
|
||||
const q = input.value.trim();
|
||||
if (!q) {
|
||||
if (useMainsPicker) {
|
||||
showMarketMains('');
|
||||
} else {
|
||||
hideDropdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
handleQuery(q);
|
||||
}, 120);
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function () {
|
||||
setTimeout(hideDropdown, 150);
|
||||
});
|
||||
|
||||
input.addEventListener('focus', function () {
|
||||
const q = input.value.trim();
|
||||
if (useMainsPicker) {
|
||||
showMarketMains(q, function (query) {
|
||||
if (query) search(query);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (q && hiddenThs && !hiddenThs.value) {
|
||||
search(q);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('.symbol-wrap').forEach(initSymbolInput);
|
||||
|
||||
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"]')
|
||||
|| form.querySelector('.symbol-ths-code');
|
||||
const market = form.querySelector('input[name="market_code"]');
|
||||
if (ths && !ths.value.trim()) {
|
||||
e.preventDefault();
|
||||
alert('请从下拉列表选择品种');
|
||||
return;
|
||||
}
|
||||
if (market && !market.value.trim()) {
|
||||
e.preventDefault();
|
||||
alert('请从下拉列表选择品种(需含同花顺行情代码)');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
+57
-53
@@ -1,53 +1,57 @@
|
||||
(function () {
|
||||
var KEY = 'qihuo-theme';
|
||||
|
||||
function updateButtons(theme) {
|
||||
document.querySelectorAll('[data-theme-pick]').forEach(function (btn) {
|
||||
var pick = btn.getAttribute('data-theme-pick');
|
||||
var on = pick === theme;
|
||||
btn.classList.toggle('active', on);
|
||||
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function apply(theme) {
|
||||
if (theme !== 'light' && theme !== 'dark') {
|
||||
theme = 'dark';
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
try {
|
||||
localStorage.setItem(KEY, theme);
|
||||
} catch (e) { /* ignore */ }
|
||||
updateButtons(theme);
|
||||
}
|
||||
|
||||
var saved = null;
|
||||
try {
|
||||
saved = localStorage.getItem(KEY);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
if (saved === 'light' || saved === 'dark') {
|
||||
apply(saved);
|
||||
} else {
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
apply(prefersLight ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-theme-pick]');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
apply(btn.getAttribute('data-theme-pick'));
|
||||
});
|
||||
|
||||
function syncButtons() {
|
||||
var cur = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
updateButtons(cur);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', syncButtons);
|
||||
} else {
|
||||
syncButtons();
|
||||
}
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var KEY = 'qihuo-theme';
|
||||
|
||||
function updateButtons(theme) {
|
||||
document.querySelectorAll('[data-theme-pick]').forEach(function (btn) {
|
||||
var pick = btn.getAttribute('data-theme-pick');
|
||||
var on = pick === theme;
|
||||
btn.classList.toggle('active', on);
|
||||
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function apply(theme) {
|
||||
if (theme !== 'light' && theme !== 'dark') {
|
||||
theme = 'dark';
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
try {
|
||||
localStorage.setItem(KEY, theme);
|
||||
} catch (e) { /* ignore */ }
|
||||
updateButtons(theme);
|
||||
}
|
||||
|
||||
var saved = null;
|
||||
try {
|
||||
saved = localStorage.getItem(KEY);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
if (saved === 'light' || saved === 'dark') {
|
||||
apply(saved);
|
||||
} else {
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
apply(prefersLight ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-theme-pick]');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
apply(btn.getAttribute('data-theme-pick'));
|
||||
});
|
||||
|
||||
function syncButtons() {
|
||||
var cur = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
updateButtons(cur);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', syncButtons);
|
||||
} else {
|
||||
syncButtons();
|
||||
}
|
||||
})();
|
||||
|
||||
+1487
-1483
File diff suppressed because it is too large
Load Diff
+46
-42
@@ -1,42 +1,46 @@
|
||||
(function () {
|
||||
var switchEl = document.getElementById('trade-edit-switch');
|
||||
if (!switchEl) return;
|
||||
|
||||
function setEditMode(on) {
|
||||
document.querySelectorAll('.cell-edit-hide').forEach(function (el) {
|
||||
el.style.display = on ? 'none' : '';
|
||||
});
|
||||
document.querySelectorAll('.cell-edit-show').forEach(function (el) {
|
||||
if (el.type === 'hidden') return;
|
||||
el.style.display = on ? '' : 'none';
|
||||
});
|
||||
document.querySelectorAll('.trade-save-btn').forEach(function (btn) {
|
||||
btn.disabled = !on;
|
||||
});
|
||||
}
|
||||
|
||||
switchEl.addEventListener('change', function () {
|
||||
setEditMode(switchEl.checked);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.trade-save-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var row = btn.closest('tr[data-trade-id]');
|
||||
if (!row) return;
|
||||
var id = row.getAttribute('data-trade-id');
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/update_trade/' + id;
|
||||
row.querySelectorAll('.cell-edit-show').forEach(function (el) {
|
||||
if (!el.name) return;
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = el.name;
|
||||
input.value = el.value;
|
||||
form.appendChild(input);
|
||||
});
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
})();
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var switchEl = document.getElementById('trade-edit-switch');
|
||||
if (!switchEl) return;
|
||||
|
||||
function setEditMode(on) {
|
||||
document.querySelectorAll('.cell-edit-hide').forEach(function (el) {
|
||||
el.style.display = on ? 'none' : '';
|
||||
});
|
||||
document.querySelectorAll('.cell-edit-show').forEach(function (el) {
|
||||
if (el.type === 'hidden') return;
|
||||
el.style.display = on ? '' : 'none';
|
||||
});
|
||||
document.querySelectorAll('.trade-save-btn').forEach(function (btn) {
|
||||
btn.disabled = !on;
|
||||
});
|
||||
}
|
||||
|
||||
switchEl.addEventListener('change', function () {
|
||||
setEditMode(switchEl.checked);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.trade-save-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var row = btn.closest('tr[data-trade-id]');
|
||||
if (!row) return;
|
||||
var id = row.getAttribute('data-trade-id');
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/update_trade/' + id;
|
||||
row.querySelectorAll('.cell-edit-show').forEach(function (el) {
|
||||
if (!el.name) return;
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = el.name;
|
||||
input.value = el.value;
|
||||
form.appendChild(input);
|
||||
});
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user