Files
qihuo/modules/web/static/js/stats.js
T
dekun e5a586f903 Restructure into modules/ with single-process CTP and config/ layout.
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 14:42:16 +08:00

179 lines
6.7 KiB
JavaScript

/* 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 === 'max_loss' || key === 'max_profit') {
if (val === 0) return 'text-muted';
}
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') {
if (val < 0) return 'text-loss';
}
return '';
}
function fmtBreakdownVal(key, val) {
if ((key === 'max_loss' || key === 'max_profit') && (val === 0 || val === 0.0)) {
return '-';
}
return fmtNum(val);
}
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 = fmtBreakdownVal(col.key, 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', { credentials: 'same-origin' })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(applyData)
.catch(function () {
var updated = document.getElementById('stats-updated');
if (updated) updated.textContent = '加载失败,请刷新页面';
});
}
function bootStatsPage() {
if (!document.getElementById('stats-summary')) return;
var viewSel = document.getElementById('stats-view-select');
if (viewSel && !viewSel.dataset.statsBound) {
viewSel.dataset.statsBound = '1';
viewSel.addEventListener('change', function () {
renderBreakdown(this.value);
});
}
loadStats();
}
if (window.qihuoPageBoot) window.qihuoPageBoot(bootStatsPage, '#stats-summary');
else if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootStatsPage);
else bootStatsPage();
})();