e5a586f903
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>
319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||
* 交易日历 — 按日汇总平仓与复盘
|
||
*/
|
||
(function () {
|
||
var calYear = 0;
|
||
var calMonth = 0;
|
||
var selectedDate = '';
|
||
|
||
function pad2(n) {
|
||
return n < 10 ? '0' + n : String(n);
|
||
}
|
||
|
||
function todayIso() {
|
||
var d = new Date();
|
||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
||
}
|
||
|
||
function fmtNum(v) {
|
||
if (v === null || v === undefined || v === '') return '-';
|
||
var n = Number(v);
|
||
if (isNaN(n)) return String(v);
|
||
return Number.isInteger(n) ? String(n) : n.toFixed(2);
|
||
}
|
||
|
||
function fmtMoney(v) {
|
||
if (v === null || v === undefined) return '-';
|
||
return fmtNum(v) + ' 元';
|
||
}
|
||
|
||
function pnlClass(v) {
|
||
if (v > 0) return 'is-profit';
|
||
if (v < 0) return 'is-loss';
|
||
return 'is-flat';
|
||
}
|
||
|
||
function fmtPnlShort(v) {
|
||
if (v === null || v === undefined) return '-';
|
||
var n = Number(v);
|
||
if (isNaN(n)) return '-';
|
||
var s = Number.isInteger(n) ? String(n) : n.toFixed(0);
|
||
return (n > 0 ? '+' : '') + s;
|
||
}
|
||
|
||
function fmtTime(v) {
|
||
if (!v) return '-';
|
||
return String(v).replace('T', ' ').slice(0, 16);
|
||
}
|
||
|
||
function fmtTags(item) {
|
||
var tags = item.behavior_tags || '';
|
||
if (item.is_emotion) {
|
||
return tags ? '情绪单 · ' + tags : '情绪单';
|
||
}
|
||
return tags || '';
|
||
}
|
||
|
||
function setCalendarTitle() {
|
||
var title = document.getElementById('trade-cal-title');
|
||
if (!title) return;
|
||
if (window.qihuoLunar && qihuoLunar.monthTitle) {
|
||
title.textContent = qihuoLunar.monthTitle(calYear, calMonth);
|
||
} else {
|
||
title.textContent = '公历 ' + calYear + '年' + calMonth + '月';
|
||
}
|
||
}
|
||
|
||
function lunarCellHtml(iso) {
|
||
if (!window.qihuoLunar) return '';
|
||
var text = qihuoLunar.cellLunarText(iso);
|
||
var info = qihuoLunar.fromIso(iso);
|
||
var cls = 'trade-cal-day-lunar';
|
||
if (info.lunarDay === 1) cls += ' is-month-start';
|
||
return '<span class="' + cls + '">' + text + '</span>';
|
||
}
|
||
|
||
function renderCalendar(data) {
|
||
var grid = document.getElementById('trade-cal-grid');
|
||
if (!grid) return;
|
||
setCalendarTitle();
|
||
var html = '';
|
||
var pad = data.weekday_start || 0;
|
||
var i;
|
||
for (i = 0; i < pad; i++) {
|
||
html += '<div class="trade-cal-cell is-empty"></div>';
|
||
}
|
||
(data.days || []).forEach(function (day) {
|
||
var dayNum = day.date.slice(8, 10).replace(/^0/, '');
|
||
var classes = ['trade-cal-cell'];
|
||
if (day.count > 0) classes.push('is-clickable');
|
||
if (day.date === todayIso()) classes.push('is-today');
|
||
if (day.date === selectedDate) classes.push('is-selected');
|
||
if (day.has_emotion) classes.push('is-emotion');
|
||
html += '<div class="' + classes.join(' ') + '" data-date="' + day.date + '" role="button" tabindex="' + (day.count > 0 ? '0' : '-1') + '">';
|
||
html += '<div class="trade-cal-day-head">';
|
||
html += '<span class="trade-cal-day-solar">' + dayNum + '</span>';
|
||
html += lunarCellHtml(day.date);
|
||
html += '</div>';
|
||
if (day.count > 0) {
|
||
html += '<div class="trade-cal-meta"><div class="trade-cal-count">' + day.count + ' 笔</div>';
|
||
html += '<div class="trade-cal-pnl ' + pnlClass(day.total_net) + '">' + fmtPnlShort(day.total_net) + '</div>';
|
||
if (day.has_emotion) {
|
||
html += '<span class="trade-cal-emotion">情绪' + (day.emotion_count > 1 ? '×' + day.emotion_count : '') + '</span>';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
});
|
||
grid.innerHTML = html;
|
||
}
|
||
|
||
function loadCalendar() {
|
||
fetch('/api/stats/calendar?year=' + calYear + '&month=' + calMonth, { credentials: 'same-origin' })
|
||
.then(function (r) {
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
return r.json();
|
||
})
|
||
.then(function (data) {
|
||
renderCalendar(data);
|
||
if (selectedDate && selectedDate.slice(0, 7) === calYear + '-' + pad2(calMonth)) {
|
||
loadDayDetail(selectedDate, false);
|
||
} else {
|
||
hideDayDetail();
|
||
}
|
||
})
|
||
.catch(function () {
|
||
var grid = document.getElementById('trade-cal-grid');
|
||
if (grid) grid.innerHTML = '<div class="text-muted" style="grid-column:1/-1">日历加载失败</div>';
|
||
});
|
||
}
|
||
|
||
function hideDayDetail() {
|
||
var panel = document.getElementById('trade-cal-day-detail');
|
||
if (panel) panel.hidden = true;
|
||
}
|
||
|
||
function renderDayDetail(data) {
|
||
var panel = document.getElementById('trade-cal-day-detail');
|
||
var title = document.getElementById('trade-cal-day-detail-title');
|
||
var summary = document.getElementById('trade-cal-day-summary');
|
||
var list = document.getElementById('trade-cal-day-list');
|
||
if (!panel || !list) return;
|
||
|
||
var label = data.date.replace(/-/g, '/');
|
||
var lunarPart = window.qihuoLunar ? '(' + qihuoLunar.daySubtitle(data.date) + ')' : '';
|
||
if (title) title.textContent = label + lunarPart + ' 交易记录';
|
||
if (summary) {
|
||
var parts = [data.count + ' 笔', '净盈亏 ' + fmtMoney(data.total_net)];
|
||
if (data.emotion_count) parts.push('情绪单 ' + data.emotion_count);
|
||
summary.textContent = parts.join(' · ');
|
||
}
|
||
|
||
list.innerHTML = '';
|
||
if (!data.items || !data.items.length) {
|
||
list.innerHTML = '<div class="text-muted">当日无平仓记录</div>';
|
||
panel.hidden = false;
|
||
return;
|
||
}
|
||
|
||
data.items.forEach(function (item) {
|
||
var card = document.createElement('div');
|
||
card.className = 'trade-cal-day-item' + (item.is_emotion ? ' is-emotion' : '');
|
||
|
||
var head = document.createElement('div');
|
||
head.className = 'trade-cal-day-item-head';
|
||
var sym = document.createElement('div');
|
||
sym.className = 'trade-cal-day-item-symbol';
|
||
sym.textContent = (item.symbol || item.symbol_code || '-') + ' · ' + (item.direction || '-');
|
||
var pnl = document.createElement('div');
|
||
pnl.className = 'trade-cal-day-item-pnl ' + pnlClass(item.pnl_net);
|
||
pnl.textContent = fmtMoney(item.pnl_net);
|
||
head.appendChild(sym);
|
||
head.appendChild(pnl);
|
||
|
||
var meta = document.createElement('div');
|
||
meta.className = 'trade-cal-day-item-meta';
|
||
var badges = '';
|
||
if (item.source === 'review') {
|
||
badges += '<span class="trade-cal-badge review">复盘</span>';
|
||
}
|
||
if (item.is_emotion) {
|
||
badges += '<span class="trade-cal-badge emotion">情绪单</span>';
|
||
}
|
||
var metaParts = [
|
||
badges,
|
||
'平仓 ' + fmtTime(item.close_time),
|
||
item.lots != null ? item.lots + ' 手' : '',
|
||
item.entry_price != null ? '开 ' + item.entry_price : '',
|
||
item.close_price != null ? '平 ' + item.close_price : '',
|
||
];
|
||
if (item.source === 'review' && item.open_type) metaParts.push(item.open_type);
|
||
if (item.source === 'review' && item.exit_trigger) metaParts.push('出场: ' + item.exit_trigger);
|
||
if (item.result) metaParts.push(item.result);
|
||
meta.innerHTML = metaParts.filter(Boolean).join(' · ');
|
||
|
||
card.appendChild(head);
|
||
card.appendChild(meta);
|
||
|
||
var tags = fmtTags(item);
|
||
if (tags) {
|
||
var tagEl = document.createElement('div');
|
||
tagEl.className = 'trade-cal-day-item-notes';
|
||
tagEl.textContent = tags;
|
||
card.appendChild(tagEl);
|
||
}
|
||
if (item.notes) {
|
||
var notes = document.createElement('div');
|
||
notes.className = 'trade-cal-day-item-notes';
|
||
notes.textContent = item.notes;
|
||
card.appendChild(notes);
|
||
}
|
||
if (item.screenshot) {
|
||
var shot = document.createElement('div');
|
||
shot.className = 'trade-cal-day-item-shot';
|
||
shot.innerHTML = '<img src="/uploads/' + item.screenshot + '" alt="复盘截图">';
|
||
card.appendChild(shot);
|
||
}
|
||
list.appendChild(card);
|
||
});
|
||
panel.hidden = false;
|
||
}
|
||
|
||
function loadDayDetail(dateStr, scroll) {
|
||
if (scroll === undefined) scroll = true;
|
||
selectedDate = dateStr;
|
||
document.querySelectorAll('.trade-cal-cell.is-selected').forEach(function (el) {
|
||
el.classList.remove('is-selected');
|
||
});
|
||
var cell = document.querySelector('.trade-cal-cell[data-date="' + dateStr + '"]');
|
||
if (cell) cell.classList.add('is-selected');
|
||
|
||
fetch('/api/stats/calendar/day?date=' + encodeURIComponent(dateStr), { credentials: 'same-origin' })
|
||
.then(function (r) {
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
return r.json();
|
||
})
|
||
.then(function (data) {
|
||
renderDayDetail(data);
|
||
if (scroll) {
|
||
var panel = document.getElementById('trade-cal-day-detail');
|
||
if (panel) panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
}
|
||
})
|
||
.catch(function () {
|
||
var panel = document.getElementById('trade-cal-day-detail');
|
||
var list = document.getElementById('trade-cal-day-list');
|
||
if (panel && list) {
|
||
list.innerHTML = '<div class="text-muted">加载失败</div>';
|
||
panel.hidden = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
function shiftCalendarMonth(delta) {
|
||
calMonth += delta;
|
||
if (calMonth > 12) {
|
||
calMonth = 1;
|
||
calYear += 1;
|
||
} else if (calMonth < 1) {
|
||
calMonth = 12;
|
||
calYear -= 1;
|
||
}
|
||
loadCalendar();
|
||
}
|
||
|
||
function bindCalendar() {
|
||
var grid = document.getElementById('trade-cal-grid');
|
||
if (!grid || grid.dataset.tradeCalBound) return;
|
||
grid.dataset.tradeCalBound = '1';
|
||
|
||
grid.addEventListener('click', function (e) {
|
||
var cell = e.target.closest('.trade-cal-cell.is-clickable');
|
||
if (!cell) return;
|
||
loadDayDetail(cell.getAttribute('data-date'));
|
||
});
|
||
grid.addEventListener('keydown', function (e) {
|
||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||
var cell = e.target.closest('.trade-cal-cell.is-clickable');
|
||
if (!cell) return;
|
||
e.preventDefault();
|
||
loadDayDetail(cell.getAttribute('data-date'));
|
||
});
|
||
|
||
var prev = document.getElementById('trade-cal-prev');
|
||
var next = document.getElementById('trade-cal-next');
|
||
var todayBtn = document.getElementById('trade-cal-today');
|
||
if (prev) prev.addEventListener('click', function () { shiftCalendarMonth(-1); });
|
||
if (next) next.addEventListener('click', function () { shiftCalendarMonth(1); });
|
||
if (todayBtn) todayBtn.addEventListener('click', function () {
|
||
var d = new Date();
|
||
calYear = d.getFullYear();
|
||
calMonth = d.getMonth() + 1;
|
||
loadCalendar();
|
||
});
|
||
}
|
||
|
||
function bootCalendarPage() {
|
||
if (!document.getElementById('trade-cal-grid')) return;
|
||
var d = new Date();
|
||
calYear = d.getFullYear();
|
||
calMonth = d.getMonth() + 1;
|
||
bindCalendar();
|
||
loadCalendar();
|
||
|
||
var params = new URLSearchParams(window.location.search);
|
||
var dateParam = params.get('date');
|
||
if (dateParam && /^\d{4}-\d{2}-\d{2}$/.test(dateParam)) {
|
||
var parts = dateParam.split('-');
|
||
calYear = parseInt(parts[0], 10);
|
||
calMonth = parseInt(parts[1], 10);
|
||
loadCalendar();
|
||
setTimeout(function () { loadDayDetail(dateParam); }, 300);
|
||
}
|
||
}
|
||
|
||
if (window.qihuoPageBoot) window.qihuoPageBoot(bootCalendarPage, '#trade-cal-grid');
|
||
else if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootCalendarPage);
|
||
else bootCalendarPage();
|
||
})();
|