Add shared records review pagination for all three exchanges.

Paginate trade records, journals, and AI history at 5 per page with soft in-card flips; hide the journal form until 填入复盘.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 11:52:02 +08:00
parent 57f8d6761c
commit d9b8069906
14 changed files with 999 additions and 361 deletions
+6 -2
View File
@@ -103,8 +103,12 @@
global.initStrategyRollForm();
}
if (tab === "records") {
if (!revisit && typeof global.loadJournals === "function") global.loadJournals();
if (!revisit && typeof global.loadReviews === "function") global.loadReviews();
if (global.RecordsReviewPage && typeof global.RecordsReviewPage.init === "function") {
global.RecordsReviewPage.init({ refresh: !!revisit });
} else {
if (!revisit && typeof global.loadJournals === "function") global.loadJournals();
if (!revisit && typeof global.loadReviews === "function") global.loadReviews();
}
if (global.InstanceTheme && typeof global.InstanceTheme.initReviewEditModeSync === "function") {
global.InstanceTheme.initReviewEditModeSync();
} else if (typeof global.toggleReviewMode === "function") {
+604
View File
@@ -0,0 +1,604 @@
/**
* 三所 /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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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);
}
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 '<span class="badge profit">' + esc(er) + "</span>";
}
if (["止损", "强制清仓", "手动平仓"].indexOf(er) >= 0) {
return '<span class="badge loss">' + esc(er) + "</span>";
}
if (er === "时间平仓") return '<span class="badge miss">' + esc(er) + "</span>";
return '<span class="badge">' + esc(er || "-") + "</span>";
}
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 = '<tr><td colspan="15" class="muted">暂无交易记录</td></tr>';
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 = '<span style="font-size:.68rem;color:#6ab88a">所</span>';
} else if (t.display_pnl_source !== "reviewed") {
pnlSrc = '<span style="font-size:.68rem;color:#8892b0">估</span>';
}
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 (
'<tr id="trade-row-' +
esc(t.id) +
'">' +
"<td>" +
esc(t.symbol) +
"</td>" +
"<td>" +
mon +
"</td>" +
"<td>" +
esc(t.effective_entry_reason || "-") +
"</td>" +
'<td><span class="badge ' +
dirCls +
'">' +
dirTxt +
"</span></td>" +
"<td>" +
fmtNum(t.trigger_price, 4) +
"</td>" +
"<td>" +
fmtNum(stopShow, 4) +
"</td>" +
"<td>" +
fmtNum(tpShow, 4) +
"</td>" +
"<td>" +
margin +
"</td>" +
"<td>" +
esc(t.leverage != null ? t.leverage : "-") +
"</td>" +
"<td>" +
esc(t.effective_hold_minutes || 0) +
"</td>" +
"<td>" +
fmtTime(t.effective_opened_at) +
"</td>" +
"<td>" +
fmtTime(t.effective_closed_at || t.created_at) +
"</td>" +
'<td><span class="' +
pnlClass(pnl) +
'">' +
fmtNum(pnl, 2) +
"</span>" +
pnlSrc +
"</td>" +
"<td>" +
resultBadge(t.effective_result) +
"</td>" +
"<td>" +
'<button type="button" class="table-del rr-fill-btn" style="background:#1f3a5a;color:#8fc8ff;margin-right:6px" data-id="' +
esc(t.id) +
'">填入复盘</button> ' +
'<button type="button" class="table-del review-edit-btn" style="background:#1f3a5a;color:#8fc8ff;margin-right:6px" data-id="' +
esc(t.id) +
'" disabled>核对修改</button> ' +
'<button type="button" class="table-del" onclick="deleteTradeRecord(' +
Number(t.id) +
')">删除</button>' +
"</td>" +
"</tr>"
);
})
.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 = '<tr><td colspan="15" class="muted">加载中…</td></tr>';
}
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 = '<tr><td colspan="15" class="muted">加载失败</td></tr>';
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 = '<tr><td colspan="15" class="muted">加载失败</td></tr>';
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 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 || "<div class='journal-empty-msg'>暂无数据</div>";
} else {
box.innerHTML = "<div class='journal-empty-msg'>暂无数据</div>";
}
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 = "<div class='entry'>暂无数据</div>";
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 +=
'<div class="entry">' +
"<div><strong>" +
(r.review_type === "daily" ? "日复盘" : "周复盘") +
"</strong> | " +
esc(r.target_date) +
"</div>" +
'<div style="font-size:12px;color:#9aa">' +
esc(r.created_at || "") +
"</div>" +
'<div style="margin-top:4px;color:#c9d2ff">' +
esc(shortText || "(空)") +
"</div>" +
'<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px">' +
'<button type="button" class="btn-del" style="border:none;cursor:pointer;background:#1f3a5a;color:#8fc8ff" onclick="openReviewDetail(\'' +
esc(r.id) +
"', false)\">查看</button>" +
'<button type="button" class="btn-del" style="border:none;cursor:pointer;background:#1f3a5a;color:#8fc8ff" onclick="openReviewDetail(\'' +
esc(r.id) +
"', true)\">全屏</button>" +
'<a class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff" href="/export/review_md/' +
esc(r.id) +
'">导出MD</a>' +
'<button type="button" class="btn-del" onclick="deleteReview(\'' +
esc(r.id) +
"')\">删除</button>" +
"</div></div>";
});
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;
if (booted) {
if (opts.refresh) {
loadTradeRecords({ soft: true });
loadJournalsPaged();
loadReviewsPaged();
}
patchFillJournalFromTrade();
patchDeleteTradeRecord();
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");
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);