Files
qihuo/modules/web/static/js/calendar.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

319 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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();
})();