feat: 档案统计独立卡片、共用交易日历与四所统计页日历
内照明心统计表移至顶部卡片,右侧为日历/图表/交易记录;日历样式适配浅深主题,四所统计分析页同步展示按月盈亏日历。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -77,9 +77,7 @@
|
||||
let chartExchangeSymbol = "";
|
||||
let chartMarketType = "swap";
|
||||
let searchTimer = null;
|
||||
let calendarYear = 0;
|
||||
let calendarMonth = 0;
|
||||
let calendarDays = {};
|
||||
let calendarWidget = null;
|
||||
let selectedCalendarDay = "";
|
||||
|
||||
function esc(s) {
|
||||
@@ -508,133 +506,54 @@
|
||||
);
|
||||
}
|
||||
|
||||
function calendarMonthLabel(y, m) {
|
||||
return y + " 年 " + m + " 月";
|
||||
}
|
||||
|
||||
function ensureCalendarMonthFromUI() {
|
||||
if (calendarYear > 0 && calendarMonth > 0) return;
|
||||
function calendarRefDate() {
|
||||
let ref = tradingDay || (elTradingDay && elTradingDay.value) || "";
|
||||
if (!ref && dateFrom) ref = dateFrom;
|
||||
if (!ref) {
|
||||
const now = new Date();
|
||||
calendarYear = now.getFullYear();
|
||||
calendarMonth = now.getMonth() + 1;
|
||||
return;
|
||||
}
|
||||
const p = String(ref).slice(0, 10).split("-");
|
||||
calendarYear = parseInt(p[0], 10) || new Date().getFullYear();
|
||||
calendarMonth = parseInt(p[1], 10) || new Date().getMonth() + 1;
|
||||
return ref || new Date();
|
||||
}
|
||||
|
||||
function renderCalendar() {
|
||||
if (!elCalendar || !elCalTitle) return;
|
||||
ensureCalendarMonthFromUI();
|
||||
elCalTitle.textContent = calendarMonthLabel(calendarYear, calendarMonth);
|
||||
const first = new Date(calendarYear, calendarMonth - 1, 1);
|
||||
const lastDay = new Date(calendarYear, calendarMonth, 0).getDate();
|
||||
const startWd = first.getDay();
|
||||
const weekdays = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
let html =
|
||||
'<div class="archive-cal-weekdays">' +
|
||||
weekdays.map(function (w) {
|
||||
return '<span class="archive-cal-wd">' + w + "</span>";
|
||||
}).join("") +
|
||||
"</div><div class=\"archive-cal-grid\">";
|
||||
for (let i = 0; i < startWd; i++) {
|
||||
html += '<span class="archive-cal-cell archive-cal-pad"></span>';
|
||||
}
|
||||
for (let d = 1; d <= lastDay; d++) {
|
||||
const dayStr =
|
||||
calendarYear +
|
||||
"-" +
|
||||
String(calendarMonth).padStart(2, "0") +
|
||||
"-" +
|
||||
String(d).padStart(2, "0");
|
||||
const info = calendarDays[dayStr];
|
||||
const hasTrade = info && info.open_count > 0;
|
||||
const sick = info && info.has_sick;
|
||||
const pnl = hasTrade ? Number(info.pnl_total) : null;
|
||||
const cnt = hasTrade ? info.open_count : 0;
|
||||
const cls =
|
||||
"archive-cal-cell" +
|
||||
(hasTrade ? " has-trade" : "") +
|
||||
(sick ? " is-sick-day" : "") +
|
||||
(selectedCalendarDay === dayStr ? " is-selected" : "") +
|
||||
(pnl != null && pnl > 0.0001 ? " pnl-pos" : pnl != null && pnl < -0.0001 ? " pnl-neg" : "");
|
||||
let body = '<span class="archive-cal-day-num">' + d + "</span>";
|
||||
if (hasTrade) {
|
||||
const pnlTxt = (pnl >= 0 ? "+" : "") + pnl.toFixed(1);
|
||||
body +=
|
||||
'<span class="archive-cal-pnl">' +
|
||||
esc(pnlTxt) +
|
||||
"</span>" +
|
||||
'<span class="archive-cal-cnt">' +
|
||||
cnt +
|
||||
"笔</span>";
|
||||
if (sick) body += '<span class="archive-cal-sick-tag">犯病</span>';
|
||||
}
|
||||
html +=
|
||||
'<button type="button" class="' +
|
||||
cls +
|
||||
'" data-day="' +
|
||||
dayStr +
|
||||
'" data-sick="' +
|
||||
(sick ? "1" : "0") +
|
||||
'"' +
|
||||
(hasTrade ? "" : " disabled") +
|
||||
">" +
|
||||
body +
|
||||
"</button>";
|
||||
}
|
||||
html += "</div>";
|
||||
elCalendar.innerHTML = html;
|
||||
elCalendar.querySelectorAll(".archive-cal-cell[data-day]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const day = btn.getAttribute("data-day");
|
||||
if (!day) return;
|
||||
function ensureCalendarWidget() {
|
||||
if (calendarWidget || !window.TradeStatsCalendar || !elCalendar) return calendarWidget;
|
||||
calendarWidget = new TradeStatsCalendar({
|
||||
gridEl: elCalendar,
|
||||
titleEl: elCalTitle,
|
||||
prevBtn: elCalPrev,
|
||||
nextBtn: elCalNext,
|
||||
showSick: true,
|
||||
buildQuery: function (year, month) {
|
||||
const q = new URLSearchParams();
|
||||
q.set("year", String(year));
|
||||
q.set("month", String(month));
|
||||
const ex = (elExchange && elExchange.value) || "";
|
||||
if (ex) q.set("exchange_key", ex);
|
||||
return q;
|
||||
},
|
||||
fetchFn: async function (q) {
|
||||
const r = await apiFetch("/api/archive/calendar?" + q.toString());
|
||||
return r.json();
|
||||
},
|
||||
parseResponse: function (data) {
|
||||
if (!data || !data.ok) return {};
|
||||
return data.days || {};
|
||||
},
|
||||
onDayClick: function (day, sick) {
|
||||
selectedCalendarDay = day;
|
||||
setPeriodMode("today");
|
||||
if (elTradingDay) elTradingDay.value = day;
|
||||
if (elFilterSick) {
|
||||
elFilterSick.checked = btn.getAttribute("data-sick") === "1";
|
||||
}
|
||||
if (elFilterSick) elFilterSick.checked = sick;
|
||||
syncPeriodUI();
|
||||
void loadDailyTrades();
|
||||
renderCalendar();
|
||||
});
|
||||
},
|
||||
});
|
||||
calendarWidget.ensureMonth(calendarRefDate());
|
||||
return calendarWidget;
|
||||
}
|
||||
|
||||
async function loadCalendar() {
|
||||
ensureCalendarMonthFromUI();
|
||||
const q = new URLSearchParams();
|
||||
q.set("year", String(calendarYear));
|
||||
q.set("month", String(calendarMonth));
|
||||
const ex = (elExchange && elExchange.value) || "";
|
||||
if (ex) q.set("exchange_key", ex);
|
||||
try {
|
||||
const r = await apiFetch("/api/archive/calendar?" + q.toString());
|
||||
const data = await r.json();
|
||||
if (!data.ok) return;
|
||||
calendarDays = data.days || {};
|
||||
renderCalendar();
|
||||
} catch (e) {
|
||||
console.warn("[archive calendar]", e);
|
||||
}
|
||||
}
|
||||
|
||||
function shiftCalendarMonth(delta) {
|
||||
ensureCalendarMonthFromUI();
|
||||
calendarMonth += delta;
|
||||
if (calendarMonth > 12) {
|
||||
calendarMonth = 1;
|
||||
calendarYear += 1;
|
||||
} else if (calendarMonth < 1) {
|
||||
calendarMonth = 12;
|
||||
calendarYear -= 1;
|
||||
}
|
||||
void loadCalendar();
|
||||
const cal = ensureCalendarWidget();
|
||||
if (!cal) return;
|
||||
cal.selectedDay = selectedCalendarDay;
|
||||
await cal.load();
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
@@ -1527,7 +1446,10 @@
|
||||
syncPeriodUI();
|
||||
dailyTrades = j.trades || [];
|
||||
dailyStats = j.stats || { open_count: 0, by_exchange: {} };
|
||||
if (periodMode === "today" && tradingDay) selectedCalendarDay = tradingDay;
|
||||
if (periodMode === "today" && tradingDay) {
|
||||
selectedCalendarDay = tradingDay;
|
||||
if (calendarWidget) calendarWidget.selectedDay = tradingDay;
|
||||
}
|
||||
renderStats();
|
||||
renderTrades();
|
||||
void loadCalendar();
|
||||
@@ -1600,8 +1522,6 @@
|
||||
void loadCalendar();
|
||||
});
|
||||
}
|
||||
if (elCalPrev) elCalPrev.addEventListener("click", function () { shiftCalendarMonth(-1); });
|
||||
if (elCalNext) elCalNext.addEventListener("click", function () { shiftCalendarMonth(1); });
|
||||
if (elPeriodTabs) {
|
||||
elPeriodTabs.addEventListener("click", function (ev) {
|
||||
const btn = ev.target.closest(".archive-period-btn");
|
||||
|
||||
Reference in New Issue
Block a user