Add personal license agreement and rename product section to tradable symbols.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 02:52:45 +08:00
parent 7b60f0dce5
commit ab9987e4c7
85 changed files with 18772 additions and 18235 deletions
+27 -23
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+57 -53
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
})();
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+46 -42
View File
@@ -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();
});
});
})();