Add archive tabs for trades vs review quotes.

Switch hub 内照明心 to a single-column tab layout and auto-show that day's closed trades under the quote form for AI review context.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 13:41:13 +08:00
parent afb771fef1
commit 7a0d015416
4 changed files with 282 additions and 39 deletions
+172 -1
View File
@@ -34,6 +34,11 @@
const elQuoteDate = document.getElementById("archive-quote-date");
const elQuoteContent = document.getElementById("archive-quote-content");
const elQuoteSubmit = document.getElementById("archive-quote-submit");
const elContentTabs = document.getElementById("archive-content-tabs");
const elPanelTrades = document.getElementById("archive-panel-trades");
const elPanelQuotes = document.getElementById("archive-panel-quotes");
const elQuoteDayTradesMeta = document.getElementById("archive-quote-day-trades-meta");
const elQuoteDayTradesBody = document.getElementById("archive-quote-day-trades-body");
const elChartSection = document.getElementById("archive-chart-section");
const elChartTitle = document.getElementById("archive-chart-title");
const elTfTabs = document.getElementById("archive-tf-tabs");
@@ -65,6 +70,10 @@
let quotes = [];
let selectedQuoteId = null;
let editingQuoteId = null;
let archiveContentTab = "trades";
let quoteDayTrades = [];
let quoteDayTradesDay = "";
let quoteDayTradesReq = 0;
let dailyTrades = [];
let tradesPage = 0;
let dailyStats = { open_count: 0, by_exchange: {} };
@@ -547,9 +556,11 @@
selectedCalendarDay = day;
setPeriodMode("today");
if (elTradingDay) elTradingDay.value = day;
if (elQuoteDate) elQuoteDate.value = day;
if (elFilterSick) elFilterSick.checked = false;
syncPeriodUI();
void loadDailyTrades();
if (archiveContentTab === "quotes") void loadQuoteDayTrades();
},
});
calendarWidget.ensureMonth(calendarRefDate());
@@ -943,6 +954,146 @@
elQuoteSubmit.textContent = editingQuoteId ? "修改保存" : "添加语录";
}
function syncQuoteDateFromTradingDay(force) {
if (!elQuoteDate) return;
const day =
(elTradingDay && elTradingDay.value) ||
selectedCalendarDay ||
tradingDay ||
"";
if (!day) return;
if (force || !elQuoteDate.value) {
elQuoteDate.value = day;
}
}
function setArchiveContentTab(tab) {
const next = tab === "quotes" ? "quotes" : "trades";
archiveContentTab = next;
const showQuotes = next === "quotes";
if (elContentTabs) {
elContentTabs.querySelectorAll(".archive-content-tab").forEach(function (btn) {
const on = btn.getAttribute("data-archive-tab") === next;
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-selected", on ? "true" : "false");
});
}
if (elPanelTrades) {
elPanelTrades.classList.toggle("is-active", !showQuotes);
elPanelTrades.hidden = showQuotes;
}
if (elPanelQuotes) {
elPanelQuotes.classList.toggle("is-active", showQuotes);
elPanelQuotes.hidden = !showQuotes;
}
if (showQuotes) {
syncQuoteDateFromTradingDay(false);
void loadQuoteDayTrades();
} else {
requestAnimationFrame(syncTradesLayout);
}
}
function quoteTagLabel(t) {
if (!t) return "—";
if (t.behavior_tag_from_journal || t.behavior_tag === "sick") return "犯病";
if (t.behavior_tag === "emotion") return "情绪";
return "—";
}
function renderQuoteDayTrades() {
if (!elQuoteDayTradesBody) return;
const day = quoteDayTradesDay || (elQuoteDate && elQuoteDate.value) || "";
if (elQuoteDayTradesMeta) {
elQuoteDayTradesMeta.textContent = day
? day + " · " + quoteDayTrades.length + " 笔"
: "";
}
if (!day) {
elQuoteDayTradesBody.innerHTML =
'<p class="archive-empty">选择日期后自动带上该日已平仓记录.</p>';
return;
}
if (!quoteDayTrades.length) {
elQuoteDayTradesBody.innerHTML =
'<p class="archive-empty">该日暂无已平仓记录.</p>';
return;
}
elQuoteDayTradesBody.innerHTML =
'<table class="archive-quote-day-trades-table"><thead><tr>' +
"<th>交易所</th><th>合约</th><th>方向</th><th>开仓</th><th>平仓</th><th>盈亏</th><th>标签</th>" +
"</tr></thead><tbody>" +
quoteDayTrades
.map(function (t) {
return (
"<tr>" +
"<td>" +
esc(tradeRowExchange(t)) +
"</td>" +
"<td>" +
esc(t.symbol || "—") +
"</td>" +
"<td>" +
esc(t.direction || "—") +
"</td>" +
'<td class="archive-dt">' +
fmtDt(t.opened_at) +
"</td>" +
'<td class="archive-dt">' +
fmtDt(t.closed_at) +
"</td>" +
'<td class="' +
pnlClass(t.pnl_amount) +
'">' +
fmtPnl(t.pnl_amount) +
"</td>" +
"<td>" +
esc(quoteTagLabel(t)) +
"</td></tr>"
);
})
.join("") +
"</tbody></table>";
}
async function loadQuoteDayTrades() {
if (!elQuoteDayTradesBody) return;
const day = elQuoteDate && String(elQuoteDate.value || "").trim();
if (!day) {
quoteDayTrades = [];
quoteDayTradesDay = "";
renderQuoteDayTrades();
return;
}
const req = ++quoteDayTradesReq;
const q = new URLSearchParams();
q.set("period", "today");
q.set("trading_day", day);
elQuoteDayTradesBody.innerHTML = '<p class="archive-empty">加载当日已平仓…</p>';
if (elQuoteDayTradesMeta) elQuoteDayTradesMeta.textContent = day;
try {
const r = await apiFetch("/api/archive/daily-trades?" + q.toString());
const j = await r.json();
if (req !== quoteDayTradesReq) return;
if (!r.ok) {
quoteDayTrades = [];
quoteDayTradesDay = day;
elQuoteDayTradesBody.innerHTML =
'<p class="archive-empty">' + esc(j.detail || "加载失败") + "</p>";
return;
}
quoteDayTrades = j.trades || [];
quoteDayTradesDay = day;
renderQuoteDayTrades();
} catch (_) {
if (req !== quoteDayTradesReq) return;
quoteDayTrades = [];
quoteDayTradesDay = day;
elQuoteDayTradesBody.innerHTML =
'<p class="archive-empty">加载失败</p>';
}
}
function resetQuoteForm() {
editingQuoteId = null;
if (elQuoteContent) elQuoteContent.value = "";
@@ -959,6 +1110,7 @@
elQuoteContent.focus();
}
updateQuoteSubmitBtn();
void loadQuoteDayTrades();
}
function selectQuote(id) {
@@ -1839,6 +1991,7 @@
renderStats();
renderTrades();
void loadCalendar();
if (archiveContentTab === "quotes") void loadQuoteDayTrades();
setStatus(
(periodLabel || tradingDay || "当日") +
" · 列表 " +
@@ -1932,7 +2085,12 @@
loadDailyTrades();
});
}
if (elTradingDay) elTradingDay.addEventListener("change", loadDailyTrades);
if (elTradingDay) {
elTradingDay.addEventListener("change", function () {
if (elQuoteDate && elTradingDay.value) elQuoteDate.value = elTradingDay.value;
void loadDailyTrades();
});
}
if (elDateFrom) elDateFrom.addEventListener("change", loadDailyTrades);
if (elDateTo) elDateTo.addEventListener("change", loadDailyTrades);
[elFilterProfit, elFilterLoss, elFilterSick].forEach(function (el) {
@@ -1967,6 +2125,18 @@
});
}
if (elQuoteForm) elQuoteForm.addEventListener("submit", submitQuoteForm);
if (elQuoteDate) {
elQuoteDate.addEventListener("change", function () {
void loadQuoteDayTrades();
});
}
if (elContentTabs) {
elContentTabs.addEventListener("click", function (ev) {
const btn = ev.target.closest(".archive-content-tab");
if (!btn) return;
setArchiveContentTab(btn.getAttribute("data-archive-tab") || "trades");
});
}
if (elTfTabs) {
elTfTabs.addEventListener("click", function (ev) {
const btn = ev.target.closest(".archive-tf-btn");
@@ -1999,6 +2169,7 @@
syncPeriodUI();
syncTradesLayout();
bindEvents();
setArchiveContentTab("trades");
inited = true;
}
await loadMeta();