feat(hub): redesign archive as inner-light-mind journal

Rename archive to 内照明心 with daily trade records by default, review quotes sidebar, on-demand chart, sick-row highlighting, and new daily-trades/quotes APIs.
This commit is contained in:
dekun
2026-06-11 17:43:45 +08:00
parent 65d2bc5e00
commit bb800b876b
5 changed files with 926 additions and 325 deletions
+350 -212
View File
@@ -1,5 +1,5 @@
/**
* 中控币种档案:列表筛选、交易时间线、永久 K 线(lightweight-charts
* 内照明心:复盘语录 + 当日交易记录 + 按需 K 线
*/
(function () {
const page = document.getElementById("page-archive");
@@ -9,14 +9,20 @@
const elFilterProfit = document.getElementById("archive-filter-profit");
const elFilterLoss = document.getElementById("archive-filter-loss");
const elFilterSick = document.getElementById("archive-filter-sick");
const elFilterEmotion = document.getElementById("archive-filter-emotion");
const elTradingDay = document.getElementById("archive-trading-day");
const elSearch = document.getElementById("archive-search");
const elBtnChartToggle = document.getElementById("archive-btn-chart-toggle");
const elBtnRefresh = document.getElementById("archive-btn-refresh");
const elBtnSync = document.getElementById("archive-btn-sync");
const elStatus = document.getElementById("archive-status");
const elList = document.getElementById("archive-list");
const elDetailPanel = document.getElementById("archive-detail-panel");
const elDetailTitle = document.getElementById("archive-detail-title");
const elDetailStats = document.getElementById("archive-detail-stats");
const elStats = document.getElementById("archive-stats");
const elQuotesList = document.getElementById("archive-quotes-list");
const elQuotesCount = document.getElementById("archive-quotes-count");
const elQuoteForm = document.getElementById("archive-quote-form");
const elQuoteDate = document.getElementById("archive-quote-date");
const elQuoteContent = document.getElementById("archive-quote-content");
const elChartSection = document.getElementById("archive-chart-section");
const elChartTitle = document.getElementById("archive-chart-title");
const elTfTabs = document.getElementById("archive-tf-tabs");
const elViewMode = document.getElementById("archive-view-mode");
const elJumpAt = document.getElementById("archive-jump-at");
@@ -36,7 +42,10 @@
const CHART_TZ_OFFSET_SEC = 8 * 60 * 60;
let meta = null;
let listRows = [];
let quotes = [];
let dailyTrades = [];
let dailyStats = { open_count: 0, by_exchange: {} };
let tradingDay = "";
let selected = null;
let trades = [];
let selectedTradeId = null;
@@ -47,6 +56,15 @@
let inited = false;
let markAuto = true;
let lastCandles = [];
let searchTimer = null;
function esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function loadMarkAutoPref() {
try {
@@ -89,8 +107,7 @@
function fmtPnl(v) {
const n = Number(v);
if (!Number.isFinite(n)) return "—";
const s = (n >= 0 ? "+" : "") + n.toFixed(2);
return s;
return (n >= 0 ? "+" : "") + n.toFixed(2);
}
function pad2(n) {
@@ -174,14 +191,10 @@
const raw = String(
tr.entry_type || tr.entry_reason || tr.reviewed_entry_reason || ""
).trim();
if (raw) {
return ENTRY_TYPE_LABELS[raw] || raw;
}
if (raw) return ENTRY_TYPE_LABELS[raw] || raw;
const mt = String(tr.monitor_type || "").trim();
if (mt && mt !== "下单监控") {
return ENTRY_TYPE_LABELS[mt] || mt;
}
return "—";
if (mt && mt !== "下单监控") return ENTRY_TYPE_LABELS[mt] || mt;
return mt || "—";
}
function reviewMark(tr) {
@@ -198,6 +211,36 @@
if (elStatus) elStatus.textContent = text || "";
}
function exchangeLabel(exKey) {
const key = String(exKey || "").toLowerCase();
const hit = (meta && meta.exchanges || []).find(function (ex) {
return String(ex.key || "").toLowerCase() === key;
});
return hit ? hit.name || hit.key : exKey || "—";
}
function isChartOpen() {
return !!(elChartSection && elChartSection.open);
}
function setChartOpen(on) {
if (!elChartSection) return;
elChartSection.open = !!on;
if (elBtnChartToggle) {
elBtnChartToggle.classList.toggle("is-active", !!on);
}
if (!on) destroyChart();
}
function updateChartTitle() {
if (!elChartTitle) return;
if (!selected) {
elChartTitle.textContent = "—";
return;
}
elChartTitle.textContent = selected.symbol + " · " + exchangeLabel(selected.exchange_key);
}
async function apiFetch(url, opts) {
const r = await fetch(url, opts);
if (r.status === 401) {
@@ -207,14 +250,15 @@
return r;
}
function queryListParams() {
function queryDailyParams() {
const q = new URLSearchParams();
if (elTradingDay && elTradingDay.value) q.set("trading_day", elTradingDay.value);
const ex = (elExchange && elExchange.value) || "";
if (ex) q.set("exchange_key", ex);
if (elFilterProfit && elFilterProfit.checked) q.set("filter_profit", "1");
if (elFilterLoss && elFilterLoss.checked) q.set("filter_loss", "1");
if (elFilterSick && elFilterSick.checked) q.set("filter_sick", "1");
if (elFilterEmotion && elFilterEmotion.checked) q.set("filter_emotion", "1");
if (elSearch && elSearch.value.trim()) q.set("search", elSearch.value.trim());
return q.toString();
}
@@ -231,52 +275,147 @@
if (cur) elExchange.value = cur;
}
function renderList() {
if (!elList) return;
if (!listRows.length) {
elList.innerHTML = '<p class="archive-empty">暂无档案数据。点击「同步交易与 K 线」从四所拉取。</p>';
function renderStats() {
if (!elStats) return;
const st = dailyStats || { open_count: 0, by_exchange: {} };
const parts = ["今日开仓 " + (st.open_count || 0) + " 次"];
const byEx = st.by_exchange || {};
Object.keys(byEx)
.sort()
.forEach(function (ex) {
parts.push(exchangeLabel(ex) + " " + byEx[ex]);
});
elStats.textContent = parts.join(" · ");
}
function quotePreview(text) {
const s = String(text || "").replace(/\s+/g, " ").trim();
if (!s) return "(空)";
return s.length > 36 ? s.slice(0, 36) + "…" : s;
}
function renderQuotes() {
if (!elQuotesList) return;
if (elQuotesCount) {
elQuotesCount.textContent = quotes.length ? quotes.length + " 条" : "";
}
if (!quotes.length) {
elQuotesList.innerHTML = '<p class="archive-empty">暂无复盘语录,可在上方添加。</p>';
return;
}
elList.innerHTML = listRows
.map(function (row) {
const active =
selected &&
selected.exchange_key === row.exchange_key &&
selected.symbol === row.symbol
? " is-active"
: "";
const seed = row.seed_complete ? "已建档" : "待种子";
elQuotesList.innerHTML = quotes
.map(function (q) {
return (
'<button type="button" class="archive-row' +
active +
'" data-ex="' +
row.exchange_key +
'" data-sym="' +
row.symbol +
'">' +
'<span class="archive-row-sym">' +
row.symbol +
'<details class="archive-quote-card">' +
'<summary class="archive-quote-summary">' +
'<span class="archive-quote-date">' +
esc(q.quote_date) +
"</span>" +
'<span class="archive-row-ex">' +
row.exchange_key +
'<span class="archive-quote-preview">' +
esc(quotePreview(q.content)) +
"</span>" +
'<span class="archive-row-stat">' +
row.trade_count +
" 笔 · " +
fmtPnl(row.total_pnl) +
" U</span>" +
'<span class="archive-row-meta">' +
seed +
"</span>" +
"</button>"
"</summary>" +
'<div class="archive-quote-body">' +
'<textarea class="archive-quote-edit" data-id="' +
q.id +
'" rows="4">' +
esc(q.content) +
"</textarea>" +
'<div class="archive-quote-actions">' +
'<button type="button" class="ghost archive-quote-save" data-id="' +
q.id +
'">保存</button>' +
'<button type="button" class="archive-del-btn archive-quote-del" data-id="' +
q.id +
'">删除</button>' +
"</div></div></details>"
);
})
.join("");
elList.querySelectorAll(".archive-row").forEach(function (btn) {
elQuotesList.querySelectorAll(".archive-quote-save").forEach(function (btn) {
btn.addEventListener("click", function () {
openDetail(btn.getAttribute("data-ex"), btn.getAttribute("data-sym"));
const id = btn.getAttribute("data-id");
const card = btn.closest(".archive-quote-card");
const ta = card && card.querySelector(".archive-quote-edit");
const dateEl = card && card.querySelector(".archive-quote-date");
if (!id || !ta) return;
void saveQuote(id, dateEl ? dateEl.textContent : "", ta.value, card);
});
});
elQuotesList.querySelectorAll(".archive-quote-del").forEach(function (btn) {
btn.addEventListener("click", function () {
void deleteQuote(btn.getAttribute("data-id"));
});
});
}
async function loadQuotes() {
const r = await apiFetch("/api/archive/quotes");
const j = await r.json();
quotes = j.quotes || [];
renderQuotes();
}
async function addQuote(ev) {
if (ev) ev.preventDefault();
const date = elQuoteDate && elQuoteDate.value;
const content = elQuoteContent && elQuoteContent.value.trim();
if (!date || !content) return;
const r = await apiFetch("/api/archive/quotes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quote_date: date, content: content }),
});
const j = await r.json();
if (!r.ok) {
setStatus(j.detail || "添加失败");
return;
}
if (elQuoteContent) elQuoteContent.value = "";
await loadQuotes();
setStatus("语录已添加");
}
async function saveQuote(id, quoteDate, content, cardEl) {
let card = cardEl;
if (!card && elQuotesList) {
const ta = elQuotesList.querySelector('.archive-quote-edit[data-id="' + id + '"]');
card = ta ? ta.closest(".archive-quote-card") : null;
}
const date =
quoteDate ||
(card &&
card.querySelector(".archive-quote-date") &&
card.querySelector(".archive-quote-date").textContent) ||
"";
const r = await apiFetch("/api/archive/quotes/" + id, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quote_date: date.trim(), content: content }),
});
const j = await r.json();
if (!r.ok) {
setStatus(j.detail || "保存失败");
return;
}
if (card) card.open = false;
await loadQuotes();
setStatus("语录已保存");
}
async function deleteQuote(id) {
if (!id || !window.confirm("确定删除这条复盘语录?")) return;
const r = await apiFetch("/api/archive/quotes/" + id, { method: "DELETE" });
if (!r.ok) {
const j = await r.json().catch(function () {
return {};
});
setStatus(j.detail || "删除失败");
return;
}
await loadQuotes();
setStatus("语录已删除");
}
function pickAnchorTrade() {
@@ -326,9 +465,7 @@
function anchorMsForTrade(tr) {
if (!tr) return null;
const mode = (elViewMode && elViewMode.value) || "hold";
if (mode === "entry") {
return tradeOpenMs(tr);
}
if (mode === "entry") return tradeOpenMs(tr);
return tradeCloseMs(tr) || tradeOpenMs(tr);
}
@@ -359,12 +496,8 @@
function isLongDirection(dir) {
const d = String(dir || "").trim().toLowerCase();
if (d === "short" || d === "空" || d === "sell" || d === "做空" || d === "shorts") {
return false;
}
if (d === "long" || d === "多" || d === "buy" || d === "做多" || d === "longs") {
return true;
}
if (d === "short" || d === "空" || d === "sell" || d === "做空" || d === "shorts") return false;
if (d === "long" || d === "多" || d === "buy" || d === "做多" || d === "longs") return true;
return true;
}
@@ -384,9 +517,7 @@
const openColor = openArrowColor(long, highlight);
let closeColor = highlight ? "#fbbf24" : "#f59e0b";
const pnl = Number(tr.pnl_amount);
if (!highlight && Number.isFinite(pnl) && pnl < -0.0001) {
closeColor = "#a855f7";
}
if (!highlight && Number.isFinite(pnl) && pnl < -0.0001) closeColor = "#a855f7";
const markers = [];
if (openMs) {
markers.push({
@@ -438,7 +569,6 @@
candleSeries.setMarkers(buildChartMarkers(lastCandles, timeframe));
}
/** 初始只聚焦持仓段;完整历史已加载,可向左拖动/滚轮缩小查看建仓前全局。 */
function focusInitialTradeView(candles, tr, tf) {
if (!chart || !candles.length || !tr) return;
const mode = (elViewMode && elViewMode.value) || "hold";
@@ -472,9 +602,7 @@
fromIdx = Math.max(0, openIdx - 10);
toIdx = Math.min(candles.length - 1, closeIdx + 14);
}
if (toIdx <= fromIdx) {
toIdx = Math.min(candles.length - 1, fromIdx + 80);
}
if (toIdx <= fromIdx) toIdx = Math.min(candles.length - 1, fromIdx + 80);
chart.timeScale().setVisibleLogicalRange({ from: fromIdx, to: toIdx + 4 });
}
@@ -533,9 +661,7 @@
priceFormat: { type: "volume" },
priceScaleId: "",
});
volumeSeries.priceScale().applyOptions({
scaleMargins: { top: 0.82, bottom: 0 },
});
volumeSeries.priceScale().applyOptions({ scaleMargins: { top: 0.82, bottom: 0 } });
new ResizeObserver(function () {
if (chart && elChartHost) {
chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight });
@@ -544,11 +670,21 @@
chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight });
}
async function loadSymbolTradesForChart(exKey, sym) {
const r = await apiFetch(
"/api/archive/detail?exchange_key=" +
encodeURIComponent(exKey) +
"&symbol=" +
encodeURIComponent(sym)
);
const j = await r.json();
trades = j.trades || [];
}
async function loadChart() {
if (!selected) return;
if (!selected || !isChartOpen()) return;
const tr = pickAnchorTrade();
const anchor = anchorMsForTrade(tr);
const jump = (elJumpAt && elJumpAt.value || "").trim();
const jump = (elJumpAt && elJumpAt.value) || "";
let openMs = null;
let closeMs = null;
if (markAuto && trades.length) {
@@ -571,7 +707,8 @@
params.set("closed_ms", String(closeMs));
} else {
params.set("bars", "200");
if (jump) params.set("at", jump);
const anchor = anchorMsForTrade(tr);
if (jump.trim()) params.set("at", jump.trim());
else if (anchor) params.set("anchor_ms", String(anchor));
}
setStatus("加载 K 线…");
@@ -604,66 +741,72 @@
} else if (candles.length > 10) {
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 });
}
const markHint = markAuto && trades.length > 1 ? " · 自动标注 " + trades.length + " 笔" : tr ? " · 已标注开/平" : "";
const histHint =
openMs && closeMs
? " · 建档30天历史 · 可拖动/滚轮缩放查看建仓前走势"
: "";
setStatus("K 线 " + candles.length + " 根 · " + timeframe + markHint + histHint);
updateChartTitle();
setStatus("K 线 " + candles.length + " 根 · " + timeframe);
}
async function openTradeChart(tr) {
if (!tr) return;
const exKey = tr.exchange_key;
const sym = tr.symbol;
if (!exKey || !sym) return;
selected = { exchange_key: exKey, symbol: sym };
selectedTradeId = String(tr.trade_id || tr.id);
setChartOpen(true);
await loadSymbolTradesForChart(exKey, sym);
await loadChart();
}
function renderTrades() {
if (!elTrades) return;
if (!trades.length) {
elTrades.innerHTML = '<p class="archive-empty">该币种暂无已平仓记录。</p>';
if (!dailyTrades.length) {
elTrades.innerHTML =
'<p class="archive-empty">该日暂无交易记录。可调整日期或点击「同步」拉取数据。</p>';
return;
}
elTrades.innerHTML =
'<table class="archive-trades-table"><thead><tr>' +
"<th>开仓类型</th><th>开仓时间</th><th>平仓时间</th><th>持仓时长</th>" +
"<th>交易所</th><th>开仓类型</th><th>开仓时间</th><th>平仓时间</th><th>持仓时长</th>" +
"<th>方向</th><th>结果</th><th>盈亏</th><th>标签</th><th>备注</th><th>操作</th>" +
"</tr></thead><tbody>" +
trades
dailyTrades
.map(function (t) {
const tid = t.trade_id || t.id;
const active = String(tid) === String(selectedTradeId) ? " is-active" : "";
const exKey = t.exchange_key || "";
const tag = t.behavior_tag || "";
const sick = tag === "sick";
const active = String(tid) === String(selectedTradeId) ? " is-active" : "";
const rev = reviewMark(t);
return (
'<tr class="archive-trade-row' +
active +
(sick ? " archive-trade-sick" : "") +
'" data-id="' +
tid +
'" data-ex="' +
esc(exKey) +
'">' +
'<td' +
(rev ? ' title="复盘记录"' : "") +
">" +
(rev ? '<span class="archive-review-mark">' + rev + "</span>" : "") +
fmtEntryType(t) +
"<td>" +
esc(exchangeLabel(exKey)) +
"</td>" +
'<td class="archive-dt"' +
(rev ? ' title="复盘记录"' : "") +
">" +
"<td>" +
(rev ? '<span class="archive-review-mark">' + rev + "</span>" : "") +
esc(fmtEntryType(t)) +
"</td>" +
'<td class="archive-dt">' +
fmtDt(t.opened_at) +
"</td>" +
'<td class="archive-dt"' +
(rev ? ' title="复盘记录"' : "") +
">" +
(rev ? '<span class="archive-review-mark">' + rev + "</span>" : "") +
'<td class="archive-dt">' +
fmtDt(t.closed_at) +
"</td>" +
'<td class="archive-hold"' +
(rev ? ' title="复盘记录"' : "") +
">" +
(rev ? '<span class="archive-review-mark">' + rev + "</span>" : "") +
'<td class="archive-hold">' +
fmtHoldMinutes(t) +
"</td>" +
"<td>" +
(t.direction || "—") +
esc(t.direction || "—") +
"</td>" +
"<td>" +
(t.result || "—") +
esc(t.result || "—") +
"</td>" +
'<td class="' +
pnlClass(t.pnl_amount) +
@@ -672,6 +815,8 @@
"</td>" +
'<td><select class="archive-tag-select" data-id="' +
tid +
'" data-ex="' +
esc(exKey) +
'">' +
'<option value=""' +
(tag === "" ? " selected" : "") +
@@ -685,13 +830,19 @@
"</select></td>" +
'<td><input class="archive-note-input" data-id="' +
tid +
'" data-ex="' +
esc(exKey) +
'" value="' +
String(t.note || "").replace(/"/g, "&quot;") +
esc(t.note || "") +
'" placeholder="备注" /></td>' +
'<td><button type="button" class="archive-del-btn" data-id="' +
'<td class="archive-actions-cell">' +
'<button type="button" class="ghost archive-chart-btn" data-id="' +
tid +
'" title="从档案移除(不影响实例复盘库)">删除</button></td>' +
"</tr>"
'">图表</button>' +
'<button type="button" class="archive-del-btn" data-id="' +
tid +
'">删除</button>' +
"</td></tr>"
);
})
.join("") +
@@ -700,48 +851,49 @@
elTrades.querySelectorAll(".archive-del-btn").forEach(function (btn) {
btn.addEventListener("click", function (ev) {
ev.stopPropagation();
void deleteTrade(btn.getAttribute("data-id"));
const row = btn.closest(".archive-trade-row");
void deleteTrade(btn.getAttribute("data-id"), row && row.getAttribute("data-ex"));
});
});
elTrades.querySelectorAll(".archive-trade-row").forEach(function (row) {
row.addEventListener("click", function (ev) {
if (
ev.target.closest("select") ||
ev.target.closest("input") ||
ev.target.closest(".archive-del-btn")
) {
return;
}
selectedTradeId = row.getAttribute("data-id");
renderTrades();
applyChartMarkers();
const trSel = pickAnchorTrade();
if (trSel && tradeOpenMs(trSel) && tradeCloseMs(trSel)) {
focusInitialTradeView(lastCandles, trSel, timeframe);
elTrades.querySelectorAll(".archive-chart-btn").forEach(function (btn) {
btn.addEventListener("click", function (ev) {
ev.stopPropagation();
const row = btn.closest(".archive-trade-row");
const tid = btn.getAttribute("data-id");
const tr = dailyTrades.find(function (t) {
return String(t.trade_id || t.id) === String(tid);
});
if (tr) void openTradeChart(tr);
else if (row) {
selectedTradeId = tid;
renderTrades();
}
});
});
elTrades.querySelectorAll(".archive-tag-select").forEach(function (sel) {
sel.addEventListener("change", function () {
saveOverlay(sel.getAttribute("data-id"), sel.value, null);
saveOverlay(sel.getAttribute("data-id"), sel.getAttribute("data-ex"), sel.value, null);
});
});
elTrades.querySelectorAll(".archive-note-input").forEach(function (inp) {
inp.addEventListener("change", function () {
const row = inp.closest(".archive-trade-row");
const tagSel = row && row.querySelector(".archive-tag-select");
saveOverlay(inp.getAttribute("data-id"), tagSel ? tagSel.value : "", inp.value);
saveOverlay(
inp.getAttribute("data-id"),
inp.getAttribute("data-ex"),
tagSel ? tagSel.value : "",
inp.value
);
});
});
}
async function deleteTrade(tradeId) {
if (!selected || tradeId == null) return;
if (!window.confirm("从币种档案移除该笔交易?(不影响交易所实例里的复盘记录)")) return;
const r = await apiFetch(
"/api/archive/trade/" + selected.exchange_key + "/" + tradeId,
{ method: "DELETE" }
);
async function deleteTrade(tradeId, exchangeKey) {
const exKey = exchangeKey || (selected && selected.exchange_key);
if (!exKey || tradeId == null) return;
if (!window.confirm("从档案移除该笔交易?(不影响交易所实例里的复盘记录)")) return;
const r = await apiFetch("/api/archive/trade/" + exKey + "/" + tradeId, { method: "DELETE" });
if (!r.ok) {
const j = await r.json().catch(function () {
return {};
@@ -749,82 +901,53 @@
setStatus(j.detail || j.msg || "删除失败");
return;
}
if (String(selectedTradeId) === String(tradeId)) {
selectedTradeId = null;
}
trades = trades.filter(function (t) {
return String(t.trade_id || t.id) !== String(tradeId);
});
renderTrades();
applyChartMarkers();
await loadList();
if (String(selectedTradeId) === String(tradeId)) selectedTradeId = null;
await loadDailyTrades();
setStatus("已移除 1 笔档案记录");
}
async function saveOverlay(tradeId, tag, note) {
if (!selected) return;
async function saveOverlay(tradeId, exchangeKey, tag, note) {
const exKey = exchangeKey || (selected && selected.exchange_key);
if (!exKey) return;
const body = { behavior_tag: tag || "", note: note != null ? note : undefined };
if (note == null) {
const row = elTrades.querySelector('.archive-trade-row[data-id="' + tradeId + '"]');
const inp = row && row.querySelector(".archive-note-input");
body.note = inp ? inp.value : "";
}
await apiFetch("/api/archive/trade/" + selected.exchange_key + "/" + tradeId, {
await apiFetch("/api/archive/trade/" + exKey + "/" + tradeId, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const tr = trades.find(function (t) {
const tr = dailyTrades.find(function (t) {
return String(t.trade_id || t.id) === String(tradeId);
});
if (tr) {
tr.behavior_tag = body.behavior_tag;
tr.note = body.note;
}
}
async function openDetail(exchangeKey, symbol) {
selected = { exchange_key: exchangeKey, symbol: symbol };
if (elDetailPanel) elDetailPanel.classList.remove("hidden");
const row = listRows.find(function (r) {
return r.exchange_key === exchangeKey && r.symbol === symbol;
});
if (elDetailTitle) {
elDetailTitle.textContent = symbol + " · " + exchangeKey;
}
if (elDetailStats && row) {
elDetailStats.textContent =
row.trade_count +
" 笔 · 胜 " +
row.win_count +
" / 负 " +
row.loss_count +
" · 合计 " +
fmtPnl(row.total_pnl) +
" U";
}
renderList();
setStatus("加载交易明细…");
const r = await apiFetch(
"/api/archive/detail?exchange_key=" +
encodeURIComponent(exchangeKey) +
"&symbol=" +
encodeURIComponent(symbol)
);
const j = await r.json();
trades = j.trades || [];
selectedTradeId = trades.length ? String(trades[0].trade_id || trades[0].id) : null;
renderTrades();
await loadChart();
}
async function loadList() {
setStatus("加载列表…");
const r = await apiFetch("/api/archive/list?" + queryListParams());
async function loadDailyTrades() {
setStatus("加载交易记录…");
const r = await apiFetch("/api/archive/daily-trades?" + queryDailyParams());
const j = await r.json();
listRows = j.rows || [];
renderList();
setStatus("共 " + listRows.length + " 个币种档案 · " + new Date().toLocaleTimeString());
if (!r.ok) {
setStatus(j.detail || "加载失败");
return;
}
tradingDay = j.trading_day || tradingDay;
if (elTradingDay && tradingDay && !elTradingDay.value) elTradingDay.value = tradingDay;
if (elQuoteDate && tradingDay && !elQuoteDate.value) elQuoteDate.value = tradingDay;
dailyTrades = j.trades || [];
dailyStats = j.stats || { open_count: 0, by_exchange: {} };
renderStats();
renderTrades();
setStatus(
(tradingDay || "当日") + " · " + dailyTrades.length + " 笔 · " + new Date().toLocaleTimeString()
);
}
async function loadMeta() {
@@ -850,14 +973,10 @@
const parts = ["同步完成 · " + okN + "/" + (j.exchanges || 0) + " 所"];
results.forEach(function (row) {
const label = row.exchange_key || row.name || "?";
if (row.ok === false) {
parts.push(label + " 失败: " + (row.msg || "未知错误"));
} else {
let line =
label + " " + (row.trade_count != null ? row.trade_count : row.trades || 0) + " 笔";
if (row.trades_removed > 0) {
line += " 清" + row.trades_removed;
}
if (row.ok === false) parts.push(label + " 失败: " + (row.msg || "未知错误"));
else {
let line = label + " " + (row.trade_count != null ? row.trade_count : row.trades || 0) + " 笔";
if (row.trades_removed > 0) line += " 清" + row.trades_removed;
parts.push(line);
}
});
@@ -866,27 +985,50 @@
async function syncAll() {
setStatus("同步中(可能需数分钟)…");
elBtnSync && (elBtnSync.disabled = true);
if (elBtnSync) elBtnSync.disabled = true;
try {
const r = await apiFetch("/api/archive/sync", { method: "POST" });
const j = await r.json();
setStatus(formatSyncSummary(j));
await loadList();
if (selected) await openDetail(selected.exchange_key, selected.symbol);
await loadDailyTrades();
await loadQuotes();
if (isChartOpen() && selected) await loadChart();
} catch (e) {
setStatus(String(e));
} finally {
elBtnSync && (elBtnSync.disabled = false);
if (elBtnSync) elBtnSync.disabled = false;
}
}
function bindEvents() {
if (elBtnRefresh) elBtnRefresh.addEventListener("click", loadList);
if (elBtnRefresh) elBtnRefresh.addEventListener("click", loadDailyTrades);
if (elBtnSync) elBtnSync.addEventListener("click", syncAll);
if (elExchange) elExchange.addEventListener("change", loadList);
[elFilterProfit, elFilterLoss, elFilterSick, elFilterEmotion].forEach(function (el) {
if (el) el.addEventListener("change", loadList);
if (elExchange) elExchange.addEventListener("change", loadDailyTrades);
if (elTradingDay) elTradingDay.addEventListener("change", loadDailyTrades);
[elFilterProfit, elFilterLoss, elFilterSick].forEach(function (el) {
if (el) el.addEventListener("change", loadDailyTrades);
});
if (elSearch) {
elSearch.addEventListener("input", function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(loadDailyTrades, 320);
});
}
if (elBtnChartToggle) {
elBtnChartToggle.addEventListener("click", function () {
const next = !isChartOpen();
setChartOpen(next);
if (next && selected) void loadChart();
});
}
if (elChartSection) {
elChartSection.addEventListener("toggle", function () {
if (elBtnChartToggle) elBtnChartToggle.classList.toggle("is-active", elChartSection.open);
if (elChartSection.open && selected) void loadChart();
else if (!elChartSection.open) destroyChart();
});
}
if (elQuoteForm) elQuoteForm.addEventListener("submit", addQuote);
if (elTfTabs) {
elTfTabs.addEventListener("click", function (ev) {
const btn = ev.target.closest(".archive-tf-btn");
@@ -908,24 +1050,20 @@
loadChart();
});
}
if (elBtnJump) {
elBtnJump.addEventListener("click", function () {
loadChart();
});
}
if (elBtnJump) elBtnJump.addEventListener("click", loadChart);
}
async function init() {
if (!document.getElementById("page-archive") || document.getElementById("page-archive").classList.contains("hidden")) {
return;
}
if (!page || page.classList.contains("hidden")) return;
if (!inited) {
loadMarkAutoPref();
setChartOpen(false);
bindEvents();
inited = true;
}
await loadMeta();
await loadList();
await loadQuotes();
await loadDailyTrades();
}
function destroy() {