Restructure options review into pending trades, form, and reviewed detail.

Show five pending trades per page, reveal the upload form only after clicking 复盘, and list saved reviews with image detail above stats.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 10:50:12 +08:00
parent 01ca035c2a
commit 8842f0b869
3 changed files with 406 additions and 90 deletions
+334 -58
View File
@@ -1,10 +1,10 @@
/**
* OKX 期权复盘:三类 Tab + 列表点选 + 下方多周期(5m/15m/1h/4h)复盘表单.
* OKX 期权复盘:待复盘交易(5行) → 点复盘出表单 → 复盘记录详情 → 统计.
*/
(function (global) {
"use strict";
var SLOT_TFS = ["5m", "15m", "1h", "4h"];
var PAGE_SIZE = 5;
var TAB_LABELS = {
option_spot: "期权交易记录",
options_options: "期期对冲记录",
@@ -27,11 +27,22 @@
var currentTradeId = null;
var draftId = "";
var tradesCache = {};
var reviewedCache = {};
var tradesPage = 0;
var tradesHasMore = false;
function $(id) {
return document.getElementById(id);
}
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function fmtPnl(v) {
if (v == null || v === "") return "—";
var n = Number(v);
@@ -54,6 +65,23 @@
return s;
}
function tradeTitle(t) {
if (!t) return "—";
if (t.source_type === "option_spot") return t.inst_id || "—";
return (
(t.underlying || "") +
(t.direction ? " " + t.direction : "") +
(t.plan_close_reason ? " · " + t.plan_close_reason : "")
);
}
function pnlStyle(v) {
var n = Number(v);
if (n > 0) return "color:#3dd68c";
if (n < 0) return "color:#f07178";
return "";
}
function newDraftId() {
if (global.crypto && typeof global.crypto.randomUUID === "function") {
return global.crypto.randomUUID().replace(/-/g, "");
@@ -63,23 +91,21 @@
return s;
}
function qs() {
function baseQs() {
var p = new URLSearchParams();
p.set("source_type", activeSource);
var uly = ($("or-filter-uly") || {}).value || "";
var opt = ($("or-filter-opt") || {}).value || "";
var reviewed = ($("or-filter-reviewed") || {}).value || "";
var strategy = (($("or-filter-strategy") || {}).value || "").trim();
var from = ($("or-filter-from") || {}).value || "";
var to = ($("or-filter-to") || {}).value || "";
if (uly) p.set("underlying", uly);
if (opt) p.set("opt_type", opt);
if (reviewed) p.set("reviewed", reviewed);
if (strategy) p.set("strategy_tag", strategy);
if (from) p.set("closed_from", from.replace("T", " ") + ":00");
if (to) p.set("closed_to", to.replace("T", " ") + ":00");
if (($("or-include-hedge-legs") || {}).checked) p.set("include_hedge_legs", "1");
return p.toString();
return p;
}
function setSyncStatus(text) {
@@ -90,6 +116,7 @@
function reloadAll() {
setSyncStatus("读取本地记录…");
loadTrades();
loadReviewed();
loadStats();
}
@@ -170,6 +197,7 @@
function setActiveTab(source) {
activeSource = source || "option_spot";
tradesPage = 0;
document.querySelectorAll(".or-tab").forEach(function (btn) {
btn.classList.toggle("active", btn.getAttribute("data-source") === activeSource);
});
@@ -177,45 +205,49 @@
if (title) title.textContent = TAB_LABELS[activeSource] || "记录";
applyFormPresets(activeSource);
hideJournalForm();
hideDetail();
reloadAll();
}
function updateTradesPager() {
var label = $("or-trades-page-label");
var prev = $("or-trades-prev");
var next = $("or-trades-next");
if (label) label.textContent = "第 " + (tradesPage + 1) + " 页";
if (prev) prev.disabled = tradesPage <= 0;
if (next) next.disabled = !tradesHasMore;
}
function loadTrades() {
var tbody = $("or-trades-tbody");
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="8" class="muted">加载中…</td></tr>';
fetch("/api/options/review/trades?" + qs(), { credentials: "same-origin" })
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载中…</td></tr>';
var p = baseQs();
p.set("reviewed", "0");
p.set("limit", String(PAGE_SIZE));
p.set("offset", String(tradesPage * PAGE_SIZE));
fetch("/api/options/review/trades?" + p.toString(), { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
setSyncStatus("本地记录已加载");
if (!data.ok) {
tbody.innerHTML = '<tr><td colspan="8" class="muted">加载失败</td></tr>';
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
return;
}
var rows = data.trades || [];
tradesHasMore = rows.length >= PAGE_SIZE;
tradesCache = {};
updateTradesPager();
if (!rows.length) {
tbody.innerHTML =
'<tr><td colspan="8" class="muted">暂无本地已平记录(期权页平仓后或对冲计划结束后会出现在此)</td></tr>';
'<tr><td colspan="6" class="muted">暂无待复盘记录</td></tr>';
return;
}
tbody.innerHTML = rows
.map(function (t) {
tradesCache[t.id] = t;
var title =
t.source_type === "option_spot"
? t.inst_id || "—"
: (t.underlying || "") +
(t.direction ? " " + t.direction : "") +
(t.plan_close_reason ? " · " + t.plan_close_reason : "");
var pnlClass =
Number(t.realized_pnl_total) > 0
? "color:#3dd68c"
: Number(t.realized_pnl_total) < 0
? "color:#f07178"
: "";
var active = currentTradeId === t.id ? " or-row-active" : "";
return (
'<tr class="or-trade-row' +
@@ -224,41 +256,39 @@
t.id +
'">' +
"<td><span class=\"or-badge\">" +
(t.source_label || t.source_type) +
escapeHtml(t.source_label || t.source_type) +
"</span></td>" +
"<td>" +
title +
escapeHtml(tradeTitle(t)) +
"</td>" +
'<td style="' +
pnlClass +
pnlStyle(t.realized_pnl_total) +
'">' +
fmtPnl(t.realized_pnl_total) +
"</td>" +
'<td class="muted" style="font-size:12px">' +
(t.opened_at || "—") +
escapeHtml(t.opened_at || "—") +
"<br>" +
(t.closed_at || "—") +
escapeHtml(t.closed_at || "—") +
"</td>" +
"<td>" +
fmtHold(t.hold_seconds) +
"</td>" +
"<td>" +
(t.strategy_tag || "—") +
"</td>" +
"<td>" +
(t.reviewed ? "已复盘" : "待复盘") +
"</td>" +
'<td><button type="button" class="btn-secondary or-hide-btn" data-id="' +
'<td><button type="button" class="btn or-review-btn" data-id="' +
t.id +
'" style="font-size:.72rem;padding:2px 8px">复盘</button> ' +
'<button type="button" class="btn-secondary or-hide-btn" data-id="' +
t.id +
'" style="font-size:.72rem;padding:2px 8px">删除</button></td>' +
"</tr>"
);
})
.join("");
tbody.querySelectorAll(".or-trade-row").forEach(function (tr) {
tr.addEventListener("click", function (ev) {
if (ev.target && ev.target.closest && ev.target.closest(".or-hide-btn")) return;
openJournalForm(Number(tr.getAttribute("data-id")));
tbody.querySelectorAll(".or-review-btn").forEach(function (btn) {
btn.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
openJournalForm(Number(btn.getAttribute("data-id")));
});
});
tbody.querySelectorAll(".or-hide-btn").forEach(function (btn) {
@@ -270,10 +300,217 @@
});
})
.catch(function () {
tbody.innerHTML = '<tr><td colspan="8" class="muted">加载失败</td></tr>';
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
});
}
function loadReviewed() {
var tbody = $("or-reviewed-tbody");
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载中…</td></tr>';
var p = baseQs();
p.set("reviewed", "1");
p.set("limit", "100");
p.set("offset", "0");
fetch("/api/options/review/trades?" + p.toString(), { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
if (!data.ok) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
return;
}
var rows = data.trades || [];
reviewedCache = {};
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">暂无复盘记录</td></tr>';
return;
}
tbody.innerHTML = rows
.map(function (t) {
reviewedCache[t.id] = t;
return (
'<tr class="or-reviewed-row" data-id="' +
t.id +
'">' +
"<td><span class=\"or-badge\">" +
escapeHtml(t.source_label || t.source_type) +
"</span></td>" +
"<td>" +
escapeHtml(tradeTitle(t)) +
"</td>" +
'<td style="' +
pnlStyle(t.realized_pnl_total) +
'">' +
fmtPnl(t.realized_pnl_total) +
"</td>" +
"<td>" +
escapeHtml(t.strategy_tag || "—") +
"</td>" +
"<td>" +
escapeHtml(t.result_tag || "—") +
"</td>" +
'<td class="muted" style="font-size:12px">' +
escapeHtml(t.reviewed_at || "—") +
"</td>" +
"</tr>"
);
})
.join("");
tbody.querySelectorAll(".or-reviewed-row").forEach(function (tr) {
tr.addEventListener("click", function () {
openDetail(Number(tr.getAttribute("data-id")));
});
});
})
.catch(function () {
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
});
}
function hideDetail() {
var panel = $("or-detail-panel");
if (panel) panel.classList.add("hidden");
}
function openDetail(tradeId) {
var panel = $("or-detail-panel");
if (!panel) return;
panel.classList.remove("hidden");
($("or-detail-title") || {}).textContent = "加载中…";
($("or-detail-meta") || {}).innerHTML = "";
($("or-detail-text") || {}).innerHTML = "";
($("or-detail-images") || {}).innerHTML = "";
panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
fetch("/api/options/review/trades/" + tradeId, { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
if (!data.ok || !data.trade) {
($("or-detail-title") || {}).textContent = "加载失败";
return;
}
renderDetail(data.trade);
})
.catch(function () {
($("or-detail-title") || {}).textContent = "加载失败";
});
}
function renderDetail(t) {
var e = t.entry || {};
reviewedCache[t.id] = t;
($("or-detail-title") || {}).textContent =
"复盘详情 · " + (t.source_label || "") + " · " + tradeTitle(t);
var editBtn = $("or-detail-edit-btn");
if (editBtn) editBtn.setAttribute("data-id", String(t.id));
var meta = $("or-detail-meta");
if (meta) {
var cells = [
["标的", t.underlying || "—"],
["合约/计划", tradeTitle(t)],
["盈亏", fmtPnl(t.realized_pnl_total)],
["持有", fmtHold(t.hold_seconds)],
["开仓", t.opened_at || "—"],
["平仓", t.closed_at || "—"],
["策略", e.strategy_tag || "—"],
["方向", e.direction_view || "—"],
["结果", e.result_tag || "—"],
["离场", e.exit_reason || "—"],
["按计划", e.followed_plan || "—"],
["入场逻辑", e.entry_logic || "—"],
];
if (t.is_hedge) {
cells.push(["永续盈亏", fmtPnl(t.realized_pnl_perp)]);
cells.push(["期权盈亏", fmtPnl(t.realized_pnl_options)]);
}
meta.innerHTML = cells
.map(function (pair) {
return (
"<div><div class=\"muted\" style=\"font-size:11px\">" +
escapeHtml(pair[0]) +
"</div><div>" +
escapeHtml(pair[1]) +
"</div></div>"
);
})
.join("");
}
var text = $("or-detail-text");
if (text) {
var lines = [];
if (e.mistake_tags) lines.push("<div><b>心理标签</b>" + escapeHtml(e.mistake_tags) + "</div>");
if (e.note) lines.push("<div><b>备注</b>" + escapeHtml(e.note).replace(/\n/g, "<br>") + "</div>");
if (t.legs && t.legs.length) {
lines.push(
"<div style=\"margin-top:6px\"><b>计划腿</b></div><table class=\"options-strike-table\"><thead><tr><th>腿</th><th>合约</th><th>盈亏</th><th>原因</th></tr></thead><tbody>" +
t.legs
.map(function (leg) {
return (
"<tr><td>" +
escapeHtml(leg.leg_role || "") +
"</td><td>" +
escapeHtml(leg.inst_id || leg.symbol || "") +
"</td><td>" +
fmtPnl(leg.realized_pnl) +
"</td><td>" +
escapeHtml(leg.close_reason || "") +
"</td></tr>"
);
})
.join("") +
"</tbody></table>"
);
}
text.innerHTML = lines.join("") || '<div class="muted">无额外备注</div>';
}
var imagesHost = $("or-detail-images");
if (imagesHost) {
var images = e.images || [];
if (!images.length) {
imagesHost.innerHTML = '<div class="muted">无截图</div>';
} else {
imagesHost.innerHTML = images
.map(function (img) {
var file = String(img.file || "").trim();
if (!file) return "";
var src = "/static/images/options_journal/" + encodeURIComponent(file).replace(/%2F/g, "/");
var label = escapeHtml(img.tf || "截图");
return (
'<div class="or-detail-img-cell">' +
'<span class="or-detail-img-label">' +
label +
"</span>" +
'<img class="or-detail-img-thumb" src="' +
src +
'" alt="' +
label +
'" data-src="' +
src +
'">' +
"</div>"
);
})
.join("");
imagesHost.querySelectorAll("img").forEach(function (img) {
img.addEventListener("click", function () {
if (typeof global.showImage === "function") {
global.showImage(img.getAttribute("data-src"));
} else {
global.open(img.getAttribute("data-src"), "_blank");
}
});
});
}
}
}
function renderGroup(title, items) {
if (!items || !items.length) {
return (
@@ -288,7 +525,7 @@
return (
'<div style="display:flex;justify-content:space-between;gap:8px;font-size:13px">' +
"<span>" +
g.key +
escapeHtml(g.key) +
" · " +
g.count +
"笔</span>" +
@@ -314,7 +551,7 @@
var kpi = $("or-kpi");
var groups = $("or-stats-groups");
if (!kpi || !groups) return;
fetch("/api/options/review/stats?" + qs(), { credentials: "same-origin" })
fetch("/api/options/review/stats?" + baseQs().toString(), { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
@@ -372,7 +609,7 @@
input.dataset.orBound = "1";
input.addEventListener("change", function () {
var file = input.files && input.files[0];
var row = input.closest(".or-upload-row");
var row = input.closest(".journal-upload-row");
var status = row && row.querySelector(".or-upload-status");
var hidden = row && row.querySelector(".or-upload-hidden");
if (!file) {
@@ -456,7 +693,7 @@
function hideJournalForm() {
currentTradeId = null;
var card = $("or-journal-card");
if (card) card.classList.add("or-journal-idle");
if (card) card.classList.add("hidden");
document.querySelectorAll(".or-trade-row").forEach(function (tr) {
tr.classList.remove("or-row-active");
});
@@ -478,8 +715,7 @@
resetUploadSlots();
var summary = $("or-journal-summary");
if (summary) {
summary.textContent =
"点下方列表一笔记录自动填入;截图槽位与合约复盘相同(5m / 15m / 1h / 4h).";
summary.textContent = "截图槽位与合约复盘相同(5m / 15m / 1h / 4h).";
}
var legsHost = $("or-legs-host");
if (legsHost) legsHost.innerHTML = "";
@@ -490,7 +726,7 @@
currentTradeId = tradeId;
var card = $("or-journal-card");
if (!card) return;
card.classList.remove("or-journal-idle");
card.classList.remove("hidden");
card.scrollIntoView({ behavior: "smooth", block: "start" });
document.querySelectorAll(".or-trade-row").forEach(function (tr) {
tr.classList.toggle("or-row-active", Number(tr.getAttribute("data-id")) === tradeId);
@@ -551,7 +787,6 @@
: "");
}
// 已有截图回填状态(文件名保留,新上传会换 draft)
(e.images || []).forEach(function (img) {
var hidden = document.querySelector(
'#or-upload-slots .or-upload-hidden[data-tf="' + img.tf + '"]'
@@ -574,13 +809,13 @@
.map(function (leg) {
return (
"<tr><td>" +
(leg.leg_role || "") +
escapeHtml(leg.leg_role || "") +
"</td><td>" +
(leg.inst_id || leg.symbol || "") +
escapeHtml(leg.inst_id || leg.symbol || "") +
"</td><td>" +
fmtPnl(leg.realized_pnl) +
"</td><td>" +
(leg.close_reason || "") +
escapeHtml(leg.close_reason || "") +
"</td></tr>"
);
})
@@ -594,7 +829,7 @@
function hideTrade(tradeId) {
if (!tradeId) return;
if (!confirm("从复盘列表删除并隐藏?刷新后也不会再出现.")) return;
if (!confirm("从复盘列表删除并隐藏?刷新后也不会再出现.")) return;
fetch("/api/options/review/trades/" + tradeId, {
method: "DELETE",
credentials: "same-origin",
@@ -618,7 +853,7 @@
function saveEntry() {
var tradeId = Number(($("or-trade-id") || {}).value || 0);
if (!tradeId) {
alert("请先在下方列表点选一笔本地记录");
alert("请先点击交易记录中的「复盘」");
return;
}
var strategy = (($("or-f-strategy") || {}).value || "").trim();
@@ -654,7 +889,9 @@
return;
}
($("or-save-status") || {}).textContent = "已保存";
hideJournalForm();
reloadAll();
openDetail(tradeId);
})
.catch(function () {
($("or-save-status") || {}).textContent = "保存失败";
@@ -664,7 +901,7 @@
function deleteEntry() {
var tradeId = Number(($("or-trade-id") || {}).value || 0);
if (!tradeId) return;
if (!confirm("删除该条复盘内容与图片?")) return;
if (!confirm("删除该条复盘内容与图片?交易记录会回到待复盘列表.")) return;
fetch("/api/options/review/entry/" + tradeId, {
method: "DELETE",
credentials: "same-origin",
@@ -674,6 +911,7 @@
})
.then(function () {
hideJournalForm();
hideDetail();
reloadAll();
});
}
@@ -689,18 +927,56 @@
var saveBtn = $("or-save-btn");
var clearBtn = $("or-clear-btn");
var delBtn = $("or-del-btn");
var prevBtn = $("or-trades-prev");
var nextBtn = $("or-trades-next");
var detailClose = $("or-detail-close-btn");
var detailEdit = $("or-detail-edit-btn");
if (reloadBtn) reloadBtn.addEventListener("click", reloadAll);
if (saveBtn) saveBtn.addEventListener("click", saveEntry);
if (clearBtn) clearBtn.addEventListener("click", hideJournalForm);
if (delBtn) delBtn.addEventListener("click", deleteEntry);
["or-filter-uly", "or-filter-opt", "or-filter-reviewed", "or-include-hedge-legs"].forEach(
function (id) {
var el = $(id);
if (el) el.addEventListener("change", reloadAll);
if (prevBtn) {
prevBtn.addEventListener("click", function () {
if (tradesPage <= 0) return;
tradesPage -= 1;
loadTrades();
});
}
if (nextBtn) {
nextBtn.addEventListener("click", function () {
if (!tradesHasMore) return;
tradesPage += 1;
loadTrades();
});
}
if (detailClose) detailClose.addEventListener("click", hideDetail);
if (detailEdit) {
detailEdit.addEventListener("click", function () {
var id = Number(detailEdit.getAttribute("data-id") || 0);
if (id) openJournalForm(id);
});
}
["or-filter-uly", "or-filter-opt", "or-include-hedge-legs"].forEach(function (id) {
var el = $(id);
if (el) {
el.addEventListener("change", function () {
tradesPage = 0;
reloadAll();
});
}
);
});
["or-filter-strategy", "or-filter-from", "or-filter-to"].forEach(function (id) {
var el = $(id);
if (el) {
el.addEventListener("change", function () {
tradesPage = 0;
reloadAll();
});
}
});
bindUploadSlots();
hideJournalForm();
hideDetail();
setActiveTab("option_spot");
}