/** * 三所 /records:交易记录分页 + 复盘表单显隐 + 复盘/AI 列表分页(soft,每页5). */ (function (global) { "use strict"; var PAGE_SIZE = 5; var tradesPage = 0; var tradesPages = 1; var journalsAll = []; var journalsPage = 0; var journalsPages = 1; var reviewsAll = []; var reviewsPage = 0; var reviewsPages = 1; var tradesCache = {}; var booted = false; function $(id) { return document.getElementById(id); } function esc(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function listQs() { if (typeof global.listWindowQueryString === "function") { return global.listWindowQueryString() || ""; } return ""; } function fmtNum(v, digits) { if (v == null || v === "") return "—"; var n = Number(v); if (!Number.isFinite(n)) return esc(v); return n.toFixed(digits == null ? 2 : digits); } /** 优先用后端交易所精度字符串;否则回退量级格式(与 formatPriceForInput 一致). */ function fmtPx(display, raw) { if (display != null && display !== "") return esc(display); if (raw == null || raw === "") return "—"; var n = Number(raw); if (!Number.isFinite(n)) return esc(raw); var av = Math.abs(n); var d; if (av >= 10000) d = 2; else if (av >= 100) d = 3; else if (av >= 1) d = 4; else if (av >= 0.01) d = 6; else if (av >= 0.0001) d = 8; else d = 10; var text = n.toFixed(d); if (text.indexOf(".") >= 0) text = text.replace(/\.?0+$/, ""); return text; } function fmtTime(s) { if (!s) return "—"; return esc(String(s).slice(0, 16)); } function resultBadge(result) { var er = String(result || "").trim(); if (["止盈", "保本止盈", "移动止盈"].indexOf(er) >= 0) { return '' + esc(er) + ""; } if (["止损", "强制清仓", "手动平仓"].indexOf(er) >= 0) { return '' + esc(er) + ""; } if (er === "时间平仓") return '' + esc(er) + ""; return '' + esc(er || "-") + ""; } function pnlClass(v) { var n = Number(v); if (!Number.isFinite(n) || n === 0) return ""; return n > 0 ? "pnl-profit" : "pnl-loss"; } function beginSoft(wrapId, soft) { var wrap = $(wrapId); if (!wrap) return null; if (soft) { if (!wrap.style.minHeight) { wrap.style.minHeight = Math.max(wrap.offsetHeight, 1) + "px"; } wrap.classList.add("rr-list-loading"); } else { wrap.classList.remove("rr-list-loading"); wrap.style.minHeight = ""; } return wrap; } function endSoft(wrap) { if (!wrap) return; wrap.classList.remove("rr-list-loading"); wrap.style.minHeight = ""; } function updatePager(kind) { var map = { trades: { page: tradesPage, pages: tradesPages, label: "rr-trades-page-label", prev: "rr-trades-prev", next: "rr-trades-next", }, journals: { page: journalsPage, pages: journalsPages, label: "rr-journals-page-label", prev: "rr-journals-prev", next: "rr-journals-next", }, reviews: { page: reviewsPage, pages: reviewsPages, label: "rr-reviews-page-label", prev: "rr-reviews-prev", next: "rr-reviews-next", }, }; var m = map[kind]; if (!m) return; var label = $(m.label); var prev = $(m.prev); var next = $(m.next); if (label) label.textContent = "第 " + (m.page + 1) + " / " + m.pages + " 页"; if (prev) prev.disabled = m.page <= 0; if (next) next.disabled = m.page + 1 >= m.pages; } function fillPayload(t) { return { symbol: t.symbol, monitor_type: t.monitor_type, key_signal_type: t.key_signal_type || "", direction: t.direction, trigger_price: t.trigger_price, stop_loss: t.display_open_stop_loss || t.initial_stop_loss || t.stop_loss, take_profit: t.effective_take_profit || t.take_profit, opened_at: t.effective_opened_at, closed_at: t.effective_closed_at, pnl_amount: t.effective_pnl_amount, result: t.effective_result, risk_amount: t.risk_amount, effective_entry_reason: t.effective_entry_reason || "", }; } function editPayload(t) { return { id: t.id, opened_at: t.effective_opened_at, closed_at: t.effective_closed_at, stop_loss: t.effective_stop_loss || t.initial_stop_loss || t.stop_loss, take_profit: t.effective_take_profit || t.take_profit, pnl_amount: t.effective_pnl_amount, result: t.effective_result, miss_reason: t.effective_miss_reason, effective_entry_reason: t.effective_entry_reason || "", }; } function renderTradesRows(rows) { var tbody = $("rr-trades-tbody"); if (!tbody) return; tradesCache = {}; if (!rows || !rows.length) { tbody.innerHTML = '暂无交易记录'; return; } tbody.innerHTML = rows .map(function (t) { tradesCache[t.id] = t; var mon = esc(t.monitor_type || ""); if (t.key_signal_type) mon += " · " + esc(t.key_signal_type); var stopShow = t.display_open_stop_loss || t.initial_stop_loss || t.stop_loss; var tpShow = t.effective_take_profit || t.take_profit; var pnl = t.effective_pnl_amount; var pnlSrc = ""; if (t.display_pnl_source === "exchange") { pnlSrc = '所'; } else if (t.display_pnl_source !== "reviewed") { pnlSrc = '估'; } var dirCls = t.direction === "long" ? "direction-long" : "direction-short"; var dirTxt = t.direction === "long" ? "做多" : "做空"; var margin = t.margin_capital != null && t.margin_capital !== "" ? fmtNum(t.margin_capital, 2) : "-"; return ( '' + "" + esc(t.symbol) + "" + "" + mon + "" + "" + esc(t.effective_entry_reason || "-") + "" + '' + dirTxt + "" + "" + fmtPx(t.trigger_price_display, t.trigger_price) + "" + "" + fmtPx(t.stop_loss_display, stopShow) + "" + "" + fmtPx(t.take_profit_display, tpShow) + "" + "" + margin + "" + "" + esc(t.leverage != null ? t.leverage : "-") + "" + "" + esc(t.effective_hold_minutes || 0) + "" + "" + fmtTime(t.effective_opened_at) + "" + "" + fmtTime(t.effective_closed_at || t.created_at) + "" + '' + fmtNum(pnl, 2) + "" + pnlSrc + "" + "" + resultBadge(t.effective_result) + "" + "" + ' ' + ' ' + '' + "" + "" ); }) .join(""); tbody.querySelectorAll(".rr-fill-btn").forEach(function (btn) { btn.addEventListener("click", function () { var id = btn.getAttribute("data-id"); var t = tradesCache[id]; if (!t) return; showJournalCard(); if (typeof global.fillJournalFromTrade === "function") { global.fillJournalFromTrade(fillPayload(t)); } }); }); tbody.querySelectorAll(".review-edit-btn").forEach(function (btn) { btn.addEventListener("click", function () { var id = btn.getAttribute("data-id"); var t = tradesCache[id]; if (!t) return; if (typeof global.editTradeRecordReview === "function") { global.editTradeRecordReview(editPayload(t)); } }); }); if (typeof global.toggleReviewMode === "function") { global.toggleReviewMode(); } } function loadTradeRecords(opts) { opts = opts || {}; var soft = !!opts.soft; var tbody = $("rr-trades-tbody"); if (!tbody) return; var wrap = beginSoft("rr-trades-wrap", soft); if (!soft) { tbody.innerHTML = '加载中…'; } var qs = listQs(); var p = new URLSearchParams(qs || ""); p.set("limit", String(PAGE_SIZE)); p.set("offset", String(tradesPage * PAGE_SIZE)); fetch("/api/trade_records?" + p.toString(), { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data || !data.ok) { tbody.innerHTML = '加载失败'; endSoft(wrap); return; } tradesPages = Math.max(1, Number(data.pages) || 1); if (tradesPage >= tradesPages) { tradesPage = Math.max(0, tradesPages - 1); updatePager("trades"); if (Number(data.total || 0) > 0) { loadTradeRecords(opts); return; } } updatePager("trades"); renderTradesRows(data.items || []); endSoft(wrap); }) .catch(function () { tbody.innerHTML = '加载失败'; endSoft(wrap); }); } function renderJournalsPage(soft) { var box = $("journal-list"); if (!box) return; var wrap = beginSoft("journal-list-wrap", soft); var total = journalsAll.length; journalsPages = Math.max(1, Math.ceil(total / PAGE_SIZE) || 1); if (journalsPage >= journalsPages) journalsPage = Math.max(0, journalsPages - 1); updatePager("journals"); var hint = $("rr-journals-hint"); if (hint) { hint.textContent = total > 0 ? "已保存的复盘(共" + total + "条,每页5条)." : "已保存的复盘(每页5条)."; } var slice = journalsAll.slice( journalsPage * PAGE_SIZE, journalsPage * PAGE_SIZE + PAGE_SIZE ); if (global.InstanceUI && typeof InstanceUI.renderJournalListHtml === "function") { var html = InstanceUI.renderJournalListHtml(slice); box.innerHTML = html || "
暂无数据
"; } else { box.innerHTML = "
暂无数据
"; } endSoft(wrap); } function renderReviewsPage(soft) { var box = $("review-list"); if (!box) return; var wrap = beginSoft("review-list-wrap", soft); var total = reviewsAll.length; reviewsPages = Math.max(1, Math.ceil(total / PAGE_SIZE) || 1); if (reviewsPage >= reviewsPages) reviewsPage = Math.max(0, reviewsPages - 1); updatePager("reviews"); var slice = reviewsAll.slice( reviewsPage * PAGE_SIZE, reviewsPage * PAGE_SIZE + PAGE_SIZE ); if (!slice.length) { box.innerHTML = "
暂无数据
"; endSoft(wrap); return; } var html = ""; slice.forEach(function (r) { if (global.reviewCache) global.reviewCache[r.id] = r; var preview = (r.content || "").replace(/\s+/g, " ").trim(); var shortText = preview.length > 90 ? preview.slice(0, 90) + "..." : preview; html += '
' + "
" + (r.review_type === "daily" ? "日复盘" : "周复盘") + " | " + esc(r.target_date) + "
" + '
' + esc(r.created_at || "") + "
" + '
' + esc(shortText || "(空)") + "
" + '
' + '" + '" + '导出MD' + '" + "
"; }); box.innerHTML = html; endSoft(wrap); } function loadJournalsPaged() { var qs = listQs(); fetch("/api/journals" + (qs ? "?" + qs : ""), { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { journalsAll = Array.isArray(data) ? data : []; if (global.journalCache) { Object.keys(global.journalCache).forEach(function (k) { delete global.journalCache[k]; }); journalsAll.forEach(function (o) { global.journalCache[o.id] = o; }); } journalsPage = 0; renderJournalsPage(false); }); } function loadReviewsPaged() { var qs = listQs(); fetch("/api/reviews" + (qs ? "?" + qs : ""), { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { reviewsAll = Array.isArray(data) ? data : []; if (global.reviewCache) { Object.keys(global.reviewCache).forEach(function (k) { delete global.reviewCache[k]; }); } else { global.reviewCache = {}; } reviewsAll.forEach(function (r) { global.reviewCache[r.id] = r; }); reviewsPage = 0; renderReviewsPage(false); }); } function showJournalCard() { var card = $("journal-card"); if (card) card.classList.remove("hidden"); var hint = $("rr-journal-fill-hint"); if (hint) hint.style.display = ""; } function hideJournalCard() { var card = $("journal-card"); if (card) card.classList.add("hidden"); var hint = $("rr-journal-fill-hint"); if (hint) hint.style.display = "none"; } function patchFillJournalFromTrade() { var prev = global.fillJournalFromTrade; if (typeof prev !== "function") return; if (prev.__rrPatched) return; global.fillJournalFromTrade = function (t) { showJournalCard(); prev(t); var hint = $("rr-journal-fill-hint"); if (hint) hint.style.display = ""; }; global.fillJournalFromTrade.__rrPatched = true; } function patchDeleteTradeRecord() { var prev = global.deleteTradeRecord; if (typeof prev !== "function") return; if (prev.__rrPatched) return; global.deleteTradeRecord = function (id) { if (!confirm("确定删除这条交易记录?")) return; fetch("/delete_trade_record/" + id, { method: "POST", credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { if (data && data.ok) { loadTradeRecords({ soft: true }); return; } if (typeof prev === "function") { /* fallthrough reload */ } global.location.href = (global.location.pathname || "/records") + "?_ts=" + Date.now(); }) .catch(function () { global.location.href = (global.location.pathname || "/records") + "?_ts=" + Date.now(); }); }; global.deleteTradeRecord.__rrPatched = true; } function bindPagers() { var tp = $("rr-trades-prev"); var tn = $("rr-trades-next"); var jp = $("rr-journals-prev"); var jn = $("rr-journals-next"); var rp = $("rr-reviews-prev"); var rn = $("rr-reviews-next"); var hideBtn = $("rr-journal-hide-btn"); if (tp) { tp.addEventListener("click", function (ev) { ev.preventDefault(); if (tradesPage <= 0) return; tradesPage -= 1; updatePager("trades"); loadTradeRecords({ soft: true }); }); } if (tn) { tn.addEventListener("click", function (ev) { ev.preventDefault(); if (tradesPage + 1 >= tradesPages) return; tradesPage += 1; updatePager("trades"); loadTradeRecords({ soft: true }); }); } if (jp) { jp.addEventListener("click", function (ev) { ev.preventDefault(); if (journalsPage <= 0) return; journalsPage -= 1; renderJournalsPage(true); }); } if (jn) { jn.addEventListener("click", function (ev) { ev.preventDefault(); if (journalsPage + 1 >= journalsPages) return; journalsPage += 1; renderJournalsPage(true); }); } if (rp) { rp.addEventListener("click", function (ev) { ev.preventDefault(); if (reviewsPage <= 0) return; reviewsPage -= 1; renderReviewsPage(true); }); } if (rn) { rn.addEventListener("click", function (ev) { ev.preventDefault(); if (reviewsPage + 1 >= reviewsPages) return; reviewsPage += 1; renderReviewsPage(true); }); } if (hideBtn) { hideBtn.addEventListener("click", function (ev) { ev.preventDefault(); hideJournalCard(); }); } } function init(opts) { opts = opts || {}; if (!$("records-panel-root")) return; var tbody = $("rr-trades-tbody"); var stuckLoading = !!tbody && tbody.querySelectorAll("tr").length <= 1 && /加载中/.test(String(tbody.textContent || "")); if (booted) { if (opts.refresh || stuckLoading) { loadTradeRecords({ soft: !stuckLoading }); loadJournalsPaged(); loadReviewsPaged(); } patchFillJournalFromTrade(); patchDeleteTradeRecord(); if (global.JournalFormSave && typeof global.JournalFormSave.init === "function") { global.JournalFormSave.init(); } return; } booted = true; if (!global.journalCache) global.journalCache = {}; if (!global.reviewCache) global.reviewCache = {}; global.loadJournals = loadJournalsPaged; global.loadReviews = loadReviewsPaged; global.loadTradeRecords = loadTradeRecords; patchFillJournalFromTrade(); patchDeleteTradeRecord(); bindPagers(); updatePager("trades"); updatePager("journals"); updatePager("reviews"); if (global.JournalFormSave && typeof global.JournalFormSave.init === "function") { global.JournalFormSave.init(); } loadTradeRecords({ soft: false }); loadJournalsPaged(); loadReviewsPaged(); } global.RecordsReviewPage = { init: init, loadTradeRecords: loadTradeRecords, loadJournals: loadJournalsPaged, loadReviews: loadReviewsPaged, showJournalCard: showJournalCard, hideJournalCard: hideJournalCard, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })(typeof window !== "undefined" ? window : globalThis);