fdbbde08df
Co-authored-by: Cursor <cursoragent@cursor.com>
1362 lines
44 KiB
JavaScript
1362 lines
44 KiB
JavaScript
/**
|
||
* OKX 期权复盘:待复盘交易(5行) → 点复盘出表单 → 复盘记录详情 → 统计.
|
||
*/
|
||
(function (global) {
|
||
"use strict";
|
||
|
||
var PAGE_SIZE = 5;
|
||
var TAB_LABELS = {
|
||
option_spot: "期权交易记录",
|
||
options_options: "期期对冲记录",
|
||
perp_options: "永期对冲记录",
|
||
};
|
||
var FORM_PRESETS = {
|
||
option_spot: {
|
||
strategy: ["顺势", "反转"],
|
||
direction: ["多", "空"],
|
||
entry: ["假突破", "结构突破"],
|
||
},
|
||
hedge: {
|
||
strategy: ["横盘", "趋势"],
|
||
direction: ["多", "空"],
|
||
entry: ["横盘博弈方向", "趋势对冲止损"],
|
||
},
|
||
};
|
||
var RESULT_OPTIONS = ["盈利", "亏损", "持平"];
|
||
var activeSource = "option_spot";
|
||
var currentTradeId = null;
|
||
var draftId = "";
|
||
var tradesCache = {};
|
||
var reviewedCache = {};
|
||
var tradesPage = 0;
|
||
var tradesPages = 1;
|
||
var reviewedPage = 0;
|
||
var reviewedPages = 1;
|
||
|
||
function $(id) {
|
||
return document.getElementById(id);
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s == null ? "" : s)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function fmtPnl(v) {
|
||
if (v == null || v === "") return "—";
|
||
var n = Number(v);
|
||
if (Number.isNaN(n)) return "—";
|
||
return (n >= 0 ? "+" : "") + n.toFixed(2);
|
||
}
|
||
|
||
function fmtHold(sec) {
|
||
if (sec == null) return "—";
|
||
var s = Math.max(0, Number(sec) || 0);
|
||
if (s < 3600) return Math.round(s / 60) + "m";
|
||
if (s < 86400) return (s / 3600).toFixed(1) + "h";
|
||
return (s / 86400).toFixed(1) + "d";
|
||
}
|
||
|
||
function toLocalInput(ts) {
|
||
if (!ts) return "";
|
||
var s = String(ts).trim().replace(" ", "T");
|
||
if (s.length >= 16) return s.slice(0, 16);
|
||
return s;
|
||
}
|
||
|
||
function closeReasonLabel(r) {
|
||
var map = {
|
||
perp_tp: "永续止盈",
|
||
perp_sl: "永续止损",
|
||
oo_expiry_loss: "期期到期亏损",
|
||
oo_expiry_win: "期期到期盈利",
|
||
target_win_leg: "期期平盈利腿",
|
||
target_up_win_leg: "期期上破·平盈利腿",
|
||
target_down_win_leg: "期期下破·平盈利腿",
|
||
oo_rest_closing: "期期全平·清残腿中",
|
||
oo_rest_closed: "期期全平·两腿已平",
|
||
orphaned_after_tp: "止盈后持有至到期",
|
||
orphaned_option_expiry: "残腿到期",
|
||
hold_to_expiry: "持有至到期",
|
||
expiry: "到期",
|
||
manual: "人工结束",
|
||
partial_fail: "半腿失败",
|
||
cancelled: "已取消",
|
||
tp: "止盈",
|
||
sl: "止损",
|
||
};
|
||
var key = String(r || "").trim();
|
||
if (!key) return "—";
|
||
return map[key] || key;
|
||
}
|
||
|
||
function legRoleLabel(role) {
|
||
var map = {
|
||
perp: "永续腿",
|
||
option_hedge: "保险期权",
|
||
option_a: "期期腿A",
|
||
option_b: "期期腿B",
|
||
};
|
||
var key = String(role || "").trim();
|
||
if (!key) return "—";
|
||
return map[key] || key;
|
||
}
|
||
|
||
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 ? " · " + closeReasonLabel(t.plan_close_reason) : "")
|
||
);
|
||
}
|
||
|
||
function pnlClass(v) {
|
||
var n = Number(v);
|
||
if (n > 0) return "pos-pnl-profit";
|
||
if (n < 0) return "pos-pnl-loss";
|
||
return "";
|
||
}
|
||
|
||
function resultClass(tag) {
|
||
var t = String(tag || "").trim();
|
||
if (t === "盈利") return "pos-pnl-profit";
|
||
if (t === "亏损") return "pos-pnl-loss";
|
||
return "";
|
||
}
|
||
|
||
function tradeContractLabel(t) {
|
||
if (!t) return "—";
|
||
if (t.source_type === "option_spot") return t.inst_id || t.underlying || "—";
|
||
return t.underlying || "—";
|
||
}
|
||
|
||
function newDraftId() {
|
||
if (global.crypto && typeof global.crypto.randomUUID === "function") {
|
||
return global.crypto.randomUUID().replace(/-/g, "");
|
||
}
|
||
var s = "";
|
||
for (var i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
|
||
return s;
|
||
}
|
||
|
||
function baseQs() {
|
||
var p = new URLSearchParams();
|
||
p.set("source_type", activeSource);
|
||
var uly = ($("or-filter-uly") || {}).value || "";
|
||
var opt = ($("or-filter-opt") || {}).value || "";
|
||
var q = (($("or-filter-q") || $("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 (q) p.set("q", q);
|
||
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;
|
||
}
|
||
|
||
function setSyncStatus(text) {
|
||
var el = $("or-sync-status");
|
||
if (el) el.textContent = text || "";
|
||
}
|
||
|
||
function reloadAll() {
|
||
setSyncStatus("读取本地记录…");
|
||
loadTrades({ sync: true });
|
||
loadReviewed({ sync: false });
|
||
loadStats();
|
||
}
|
||
|
||
function isHedgeSource(sourceType) {
|
||
return sourceType === "options_options" || sourceType === "perp_options";
|
||
}
|
||
|
||
function fillSelect(el, options, placeholder) {
|
||
if (!el) return;
|
||
var keep = el.value;
|
||
el.innerHTML = "";
|
||
var first = document.createElement("option");
|
||
first.value = "";
|
||
first.textContent = placeholder || "";
|
||
el.appendChild(first);
|
||
(options || []).forEach(function (v) {
|
||
var opt = document.createElement("option");
|
||
opt.value = v;
|
||
opt.textContent = v;
|
||
el.appendChild(opt);
|
||
});
|
||
if (keep) setSelectValue(el, keep);
|
||
}
|
||
|
||
function setSelectValue(el, value) {
|
||
if (!el) return;
|
||
var v = value == null ? "" : String(value);
|
||
if (!v) {
|
||
el.value = "";
|
||
return;
|
||
}
|
||
var found = false;
|
||
for (var i = 0; i < el.options.length; i++) {
|
||
if (el.options[i].value === v) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
var opt = document.createElement("option");
|
||
opt.value = v;
|
||
opt.textContent = v;
|
||
el.appendChild(opt);
|
||
}
|
||
el.value = v;
|
||
}
|
||
|
||
function applyFormPresets(sourceType) {
|
||
var preset = isHedgeSource(sourceType) ? FORM_PRESETS.hedge : FORM_PRESETS.option_spot;
|
||
fillSelect($("or-f-strategy"), preset.strategy, "策略标签");
|
||
fillSelect($("or-f-direction"), preset.direction, "方向判断");
|
||
fillSelect($("or-f-entry"), preset.entry, "入场逻辑");
|
||
fillSelect($("or-f-result"), RESULT_OPTIONS, "结果标签");
|
||
}
|
||
|
||
function autoDirection(t) {
|
||
if (!t) return "";
|
||
if (isHedgeSource(t.source_type)) {
|
||
var d = String(t.direction || "").trim().toLowerCase();
|
||
if (d === "long" || d === "buy" || d === "多") return "多";
|
||
if (d === "short" || d === "sell" || d === "空") return "空";
|
||
return "";
|
||
}
|
||
var ot = String(t.opt_type || "").trim().toUpperCase();
|
||
if (ot === "C" || ot === "CALL") return "多";
|
||
if (ot === "P" || ot === "PUT") return "空";
|
||
return "";
|
||
}
|
||
|
||
function autoResultTag(pnl) {
|
||
if (pnl == null || pnl === "") return "";
|
||
var n = Number(pnl);
|
||
if (Number.isNaN(n)) return "";
|
||
if (n > 0) return "盈利";
|
||
if (n < 0) return "亏损";
|
||
return "持平";
|
||
}
|
||
|
||
function tradeModeFromDom() {
|
||
var tabs = document.querySelector(".or-tabs");
|
||
return (tabs && tabs.getAttribute("data-okx-trade-mode")) || "options";
|
||
}
|
||
|
||
function defaultSourceForMode(mode) {
|
||
if (mode === "options_options") return "options_options";
|
||
if (mode === "perp_options") return "perp_options";
|
||
return "option_spot";
|
||
}
|
||
|
||
function setActiveTab(source) {
|
||
var mode = tradeModeFromDom();
|
||
var allowed = defaultSourceForMode(mode);
|
||
activeSource = source || allowed;
|
||
if (activeSource !== allowed) activeSource = allowed;
|
||
tradesPage = 0;
|
||
reviewedPage = 0;
|
||
document.querySelectorAll(".or-tab").forEach(function (btn) {
|
||
btn.classList.toggle("active", btn.getAttribute("data-source") === activeSource);
|
||
});
|
||
var title = $("or-list-title");
|
||
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) + " / " + tradesPages + " 页";
|
||
}
|
||
if (prev) prev.disabled = tradesPage <= 0;
|
||
if (next) next.disabled = tradesPage + 1 >= tradesPages;
|
||
}
|
||
|
||
function updateReviewedPager() {
|
||
var label = $("or-reviewed-page-label");
|
||
var prev = $("or-reviewed-prev");
|
||
var next = $("or-reviewed-next");
|
||
if (label) {
|
||
label.textContent = "第 " + (reviewedPage + 1) + " / " + reviewedPages + " 页";
|
||
}
|
||
if (prev) prev.disabled = reviewedPage <= 0;
|
||
if (next) next.disabled = reviewedPage + 1 >= reviewedPages;
|
||
}
|
||
|
||
function applyPagerMeta(data, kind) {
|
||
var pages = Number(data.pages || 1);
|
||
if (!pages || pages < 1) pages = 1;
|
||
var clamped = false;
|
||
if (kind === "trades") {
|
||
tradesPages = pages;
|
||
if (tradesPage >= tradesPages) {
|
||
tradesPage = Math.max(0, tradesPages - 1);
|
||
clamped = true;
|
||
}
|
||
updateTradesPager();
|
||
} else {
|
||
reviewedPages = pages;
|
||
if (reviewedPage >= reviewedPages) {
|
||
reviewedPage = Math.max(0, reviewedPages - 1);
|
||
clamped = true;
|
||
}
|
||
updateReviewedPager();
|
||
}
|
||
return clamped;
|
||
}
|
||
|
||
function beginListLoad(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("or-list-loading");
|
||
} else {
|
||
wrap.classList.remove("or-list-loading");
|
||
wrap.style.minHeight = "";
|
||
}
|
||
return wrap;
|
||
}
|
||
|
||
function endListLoad(wrap) {
|
||
if (!wrap) return;
|
||
wrap.classList.remove("or-list-loading");
|
||
wrap.style.minHeight = "";
|
||
}
|
||
|
||
function loadTrades(opts) {
|
||
opts = opts || {};
|
||
var doSync = opts.sync !== false;
|
||
var soft = !!opts.soft;
|
||
var tbody = $("or-trades-tbody");
|
||
if (!tbody) return;
|
||
var wrap = beginListLoad("or-trades-wrap", soft);
|
||
if (!soft) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="muted">加载中…</td></tr>';
|
||
}
|
||
var p = baseQs();
|
||
// 交易记录保留已复盘条目,不再只显示待复盘
|
||
p.set("limit", String(PAGE_SIZE));
|
||
p.set("offset", String(tradesPage * PAGE_SIZE));
|
||
if (!doSync) p.set("sync", "0");
|
||
fetch("/api/options/review/trades?" + p.toString(), { credentials: "same-origin" })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (data) {
|
||
if (doSync) setSyncStatus("本地记录已加载");
|
||
if (!data.ok) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="muted">加载失败</td></tr>';
|
||
endListLoad(wrap);
|
||
return;
|
||
}
|
||
if (applyPagerMeta(data, "trades") && Number(data.total || 0) > 0) {
|
||
loadTrades(opts);
|
||
return;
|
||
}
|
||
var rows = data.trades || [];
|
||
tradesCache = {};
|
||
if (!rows.length) {
|
||
tbody.innerHTML =
|
||
'<tr><td colspan="7" class="muted">暂无交易记录</td></tr>';
|
||
endListLoad(wrap);
|
||
return;
|
||
}
|
||
tbody.innerHTML = rows
|
||
.map(function (t) {
|
||
tradesCache[t.id] = t;
|
||
var active = currentTradeId === t.id ? " or-row-active" : "";
|
||
var reviewed = !!t.reviewed;
|
||
var actionBtn = reviewed
|
||
? '<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 or-review-btn" data-id="' +
|
||
t.id +
|
||
'" style="font-size:.72rem;padding:2px 8px">复盘</button>';
|
||
var badgeExtra = reviewed
|
||
? ' <span class="or-badge" style="background:rgba(61,214,140,.2)">已复盘</span>'
|
||
: "";
|
||
return (
|
||
'<tr class="or-trade-row' +
|
||
active +
|
||
'" data-id="' +
|
||
t.id +
|
||
'">' +
|
||
"<td><span class=\"or-badge\">" +
|
||
escapeHtml(t.source_label || t.source_type) +
|
||
"</span>" +
|
||
badgeExtra +
|
||
"</td>" +
|
||
"<td>" +
|
||
escapeHtml(tradeTitle(t)) +
|
||
"</td>" +
|
||
'<td class="' +
|
||
pnlClass(t.realized_pnl_total) +
|
||
'">' +
|
||
fmtPnl(t.realized_pnl_total) +
|
||
"</td>" +
|
||
'<td class="muted" style="font-size:12px;white-space:nowrap">' +
|
||
escapeHtml(t.opened_at || "—") +
|
||
"</td>" +
|
||
'<td class="muted" style="font-size:12px;white-space:nowrap">' +
|
||
escapeHtml(t.closed_at || "—") +
|
||
"</td>" +
|
||
"<td>" +
|
||
fmtHold(t.hold_seconds) +
|
||
"</td>" +
|
||
"<td>" +
|
||
actionBtn +
|
||
" " +
|
||
'<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-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) {
|
||
btn.addEventListener("click", function (ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
hideTrade(Number(btn.getAttribute("data-id")));
|
||
});
|
||
});
|
||
endListLoad(wrap);
|
||
})
|
||
.catch(function () {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="muted">加载失败</td></tr>';
|
||
endListLoad(wrap);
|
||
});
|
||
}
|
||
|
||
function loadReviewed(opts) {
|
||
opts = opts || {};
|
||
var doSync = opts.sync === true;
|
||
var soft = !!opts.soft;
|
||
var tbody = $("or-reviewed-tbody");
|
||
if (!tbody) return;
|
||
var wrap = beginListLoad("or-reviewed-wrap", soft);
|
||
if (!soft) {
|
||
tbody.innerHTML = '<tr><td colspan="11" class="muted">加载中…</td></tr>';
|
||
}
|
||
var p = baseQs();
|
||
p.set("reviewed", "1");
|
||
p.set("limit", String(PAGE_SIZE));
|
||
p.set("offset", String(reviewedPage * PAGE_SIZE));
|
||
if (!doSync) p.set("sync", "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="11" class="muted">加载失败</td></tr>';
|
||
endListLoad(wrap);
|
||
return;
|
||
}
|
||
if (applyPagerMeta(data, "reviewed") && Number(data.total || 0) > 0) {
|
||
loadReviewed(opts);
|
||
return;
|
||
}
|
||
var rows = data.trades || [];
|
||
reviewedCache = {};
|
||
if (!rows.length) {
|
||
tbody.innerHTML = '<tr><td colspan="11" class="muted">暂无复盘记录</td></tr>';
|
||
endListLoad(wrap);
|
||
return;
|
||
}
|
||
tbody.innerHTML = rows
|
||
.map(function (t) {
|
||
reviewedCache[t.id] = t;
|
||
var entry = t.entry || {};
|
||
var direction = t.direction_view || entry.direction_view || "";
|
||
var entryLogic = t.entry_logic || entry.entry_logic || "";
|
||
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(tradeContractLabel(t)) +
|
||
"</td>" +
|
||
"<td>" +
|
||
escapeHtml(direction || "—") +
|
||
"</td>" +
|
||
'<td class="' +
|
||
pnlClass(t.realized_pnl_total) +
|
||
'">' +
|
||
fmtPnl(t.realized_pnl_total) +
|
||
"</td>" +
|
||
'<td class="muted" style="font-size:12px;white-space:nowrap">' +
|
||
escapeHtml(t.opened_at || "—") +
|
||
"</td>" +
|
||
'<td class="muted" style="font-size:12px;white-space:nowrap">' +
|
||
escapeHtml(t.closed_at || "—") +
|
||
"</td>" +
|
||
"<td>" +
|
||
escapeHtml(fmtHold(t.hold_seconds)) +
|
||
"</td>" +
|
||
"<td>" +
|
||
escapeHtml(t.strategy_tag || "—") +
|
||
"</td>" +
|
||
"<td>" +
|
||
escapeHtml(entryLogic || "—") +
|
||
"</td>" +
|
||
'<td class="' +
|
||
resultClass(t.result_tag) +
|
||
'">' +
|
||
escapeHtml(t.result_tag || "—") +
|
||
"</td>" +
|
||
'<td class="muted" style="font-size:12px;white-space:nowrap">' +
|
||
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")));
|
||
});
|
||
});
|
||
endListLoad(wrap);
|
||
})
|
||
.catch(function () {
|
||
tbody.innerHTML = '<tr><td colspan="11" class="muted">加载失败</td></tr>';
|
||
endListLoad(wrap);
|
||
});
|
||
}
|
||
|
||
function hideLightbox() {
|
||
var box = $("or-img-lightbox");
|
||
if (box) box.hidden = true;
|
||
var img = $("or-img-lightbox-img");
|
||
if (img) img.src = "";
|
||
}
|
||
|
||
function showLightbox(src) {
|
||
var url = String(src || "").trim();
|
||
if (!url) return;
|
||
var box = $("or-img-lightbox");
|
||
var img = $("or-img-lightbox-img");
|
||
if (box && img) {
|
||
img.src = url;
|
||
box.hidden = false;
|
||
return;
|
||
}
|
||
if (typeof global.showImage === "function") {
|
||
global.showImage(url);
|
||
} else if (typeof window.showImage === "function") {
|
||
window.showImage(url);
|
||
} else {
|
||
global.open(url, "_blank");
|
||
}
|
||
}
|
||
|
||
function hideDetail() {
|
||
hideLightbox();
|
||
var backdrop = $("or-detail-backdrop");
|
||
if (backdrop) backdrop.hidden = true;
|
||
}
|
||
|
||
function openDetail(tradeId) {
|
||
var backdrop = $("or-detail-backdrop");
|
||
if (!backdrop) return;
|
||
backdrop.hidden = false;
|
||
($("or-detail-title") || {}).textContent = "加载中…";
|
||
($("or-detail-meta") || {}).innerHTML = "";
|
||
($("or-detail-text") || {}).innerHTML = "";
|
||
($("or-detail-images") || {}).innerHTML = "";
|
||
|
||
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 optionsJournalImgSrc(file) {
|
||
var name = String(file || "").trim().replace(/\\/g, "/");
|
||
var slash = name.lastIndexOf("/");
|
||
if (slash >= 0) name = name.slice(slash + 1);
|
||
if (!name) return "";
|
||
// options_journal_* 在子目录;误走合约上传的 journal_* 在 static/images 根目录
|
||
var base =
|
||
name.toLowerCase().indexOf("options_journal_") === 0
|
||
? "/static/images/options_journal/"
|
||
: "/static/images/";
|
||
return base + encodeURIComponent(name);
|
||
}
|
||
|
||
function renderDetailImages(images) {
|
||
var imagesHost = $("or-detail-images");
|
||
if (!imagesHost) return;
|
||
var byTf = {};
|
||
(images || []).forEach(function (img) {
|
||
var tf = String((img && img.tf) || "").trim();
|
||
var file = String((img && img.file) || "").trim();
|
||
if (!file) return;
|
||
var key = tf || "_";
|
||
byTf[key] = file;
|
||
});
|
||
var order = ["5m", "15m", "1h", "4h"];
|
||
var keys = order.slice();
|
||
Object.keys(byTf).forEach(function (k) {
|
||
if (keys.indexOf(k) < 0) keys.push(k);
|
||
});
|
||
var cells = keys
|
||
.map(function (tf) {
|
||
var file = byTf[tf];
|
||
if (!file) {
|
||
if (order.indexOf(tf) < 0) return "";
|
||
return (
|
||
'<div class="or-detail-img-cell">' +
|
||
'<span class="or-detail-img-label">' +
|
||
escapeHtml(tf) +
|
||
"</span>" +
|
||
'<div class="or-detail-img-miss">未上传</div>' +
|
||
"</div>"
|
||
);
|
||
}
|
||
var src = optionsJournalImgSrc(file);
|
||
var label = escapeHtml(tf === "_" ? "截图" : 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 +
|
||
'" loading="lazy">' +
|
||
"</div>"
|
||
);
|
||
})
|
||
.filter(Boolean);
|
||
if (!cells.length) {
|
||
imagesHost.innerHTML = '<div class="muted">无截图</div>';
|
||
return;
|
||
}
|
||
imagesHost.innerHTML = cells.join("");
|
||
imagesHost.querySelectorAll("img").forEach(function (img) {
|
||
img.addEventListener("error", function () {
|
||
var cell = img.closest(".or-detail-img-cell");
|
||
if (!cell) return;
|
||
var label = cell.querySelector(".or-detail-img-label");
|
||
var tf = label ? label.textContent : "截图";
|
||
cell.innerHTML =
|
||
'<span class="or-detail-img-label">' +
|
||
escapeHtml(tf) +
|
||
"</span>" +
|
||
'<div class="or-detail-img-miss">文件缺失或无法加载</div>';
|
||
});
|
||
img.addEventListener("click", function () {
|
||
var src = img.getAttribute("data-src") || img.src;
|
||
showLightbox(src);
|
||
});
|
||
});
|
||
}
|
||
|
||
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) {
|
||
var cls = "";
|
||
if (pair[0] === "盈亏" || pair[0] === "永续盈亏" || pair[0] === "期权盈亏") {
|
||
cls = pnlClass(t.realized_pnl_total);
|
||
if (pair[0] === "永续盈亏") cls = pnlClass(t.realized_pnl_perp);
|
||
if (pair[0] === "期权盈亏") cls = pnlClass(t.realized_pnl_options);
|
||
} else if (pair[0] === "结果") {
|
||
cls = resultClass(e.result_tag);
|
||
}
|
||
return (
|
||
"<div><div class=\"muted\" style=\"font-size:11px\">" +
|
||
escapeHtml(pair[0]) +
|
||
'</div><div class="' +
|
||
cls +
|
||
'">' +
|
||
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(legRoleLabel(leg.leg_role)) +
|
||
"</td><td>" +
|
||
escapeHtml(leg.inst_id || leg.symbol || "") +
|
||
"</td><td class=\"" +
|
||
pnlClass(leg.realized_pnl) +
|
||
"\">" +
|
||
fmtPnl(leg.realized_pnl) +
|
||
"</td><td>" +
|
||
escapeHtml(closeReasonLabel(leg.close_reason)) +
|
||
"</td></tr>"
|
||
);
|
||
})
|
||
.join("") +
|
||
"</tbody></table>"
|
||
);
|
||
}
|
||
text.innerHTML = lines.join("") || '<div class="muted">无额外备注</div>';
|
||
}
|
||
|
||
var imagesHost = $("or-detail-images");
|
||
if (imagesHost) {
|
||
renderDetailImages(e.images || []);
|
||
}
|
||
}
|
||
|
||
function renderGroup(title, items) {
|
||
if (!items || !items.length) return "";
|
||
var lines = items
|
||
.slice(0, 8)
|
||
.map(function (g) {
|
||
var keyLabel =
|
||
title === "对冲结束原因" ? closeReasonLabel(g.key) : String(g.key || "");
|
||
return (
|
||
'<div class="or-stat-row">' +
|
||
'<span class="or-stat-key">' +
|
||
escapeHtml(keyLabel) +
|
||
" · " +
|
||
g.count +
|
||
"笔</span>" +
|
||
'<span class="or-stat-val">' +
|
||
fmtPnl(g.pnl_sum) +
|
||
" / 胜" +
|
||
(g.win_rate || 0) +
|
||
"%</span>" +
|
||
"</div>"
|
||
);
|
||
})
|
||
.join("");
|
||
return (
|
||
'<div class="or-stat-card"><div class="or-stat-card-title">' +
|
||
title +
|
||
"</div>" +
|
||
lines +
|
||
"</div>"
|
||
);
|
||
}
|
||
|
||
function loadStats() {
|
||
var kpi = $("or-kpi");
|
||
var groups = $("or-stats-groups");
|
||
if (!kpi || !groups) return;
|
||
fetch("/api/options/review/stats?" + baseQs().toString(), { credentials: "same-origin" })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (data) {
|
||
if (!data.ok) return;
|
||
var k = data.kpi || {};
|
||
kpi.innerHTML = [
|
||
["笔数", k.total],
|
||
["已复盘率", (k.review_rate || 0) + "%"],
|
||
["胜率", (k.win_rate || 0) + "%"],
|
||
["累计盈亏", fmtPnl(k.pnl_sum)],
|
||
["平均盈亏", fmtPnl(k.avg_pnl)],
|
||
["平均持有", fmtHold(k.avg_hold_sec)],
|
||
]
|
||
.map(function (pair) {
|
||
var cls = "";
|
||
if (pair[0] === "累计盈亏") cls = pnlClass(k.pnl_sum);
|
||
if (pair[0] === "平均盈亏") cls = pnlClass(k.avg_pnl);
|
||
return (
|
||
'<div class="or-kpi-tile"><div class="or-kpi-label">' +
|
||
pair[0] +
|
||
'</div><div class="or-kpi-value' +
|
||
(cls ? " " + cls : "") +
|
||
'">' +
|
||
pair[1] +
|
||
"</div></div>"
|
||
);
|
||
})
|
||
.join("");
|
||
var html = [
|
||
renderGroup("按类型", data.by_source_type),
|
||
renderGroup("按标的", data.by_underlying),
|
||
renderGroup("按策略", data.by_strategy),
|
||
renderGroup("对冲结束原因", data.by_close_reason),
|
||
renderGroup("持有周期", data.by_hold_bucket),
|
||
renderGroup("Call/Put", data.by_opt_type),
|
||
]
|
||
.filter(Boolean)
|
||
.join("");
|
||
groups.innerHTML = html || '<div class="muted" style="font-size:.76rem">暂无分组数据</div>';
|
||
})
|
||
.catch(function () {});
|
||
}
|
||
|
||
function resetUploadSlots() {
|
||
draftId = newDraftId();
|
||
var draftEl = $("or-draft-id");
|
||
if (draftEl) draftEl.value = draftId;
|
||
document.querySelectorAll("#or-upload-slots .or-upload-hidden").forEach(function (el) {
|
||
el.value = "";
|
||
});
|
||
document.querySelectorAll("#or-upload-slots .or-upload-input").forEach(function (el) {
|
||
el.value = "";
|
||
});
|
||
document.querySelectorAll("#or-upload-slots .or-upload-status").forEach(function (el) {
|
||
el.textContent = "";
|
||
});
|
||
}
|
||
|
||
function bindUploadSlots() {
|
||
document.querySelectorAll("#or-upload-slots .or-upload-input").forEach(function (input) {
|
||
if (input.dataset.orBound === "1") return;
|
||
input.dataset.orBound = "1";
|
||
input.addEventListener("change", function () {
|
||
var file = input.files && input.files[0];
|
||
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) {
|
||
if (hidden) hidden.value = "";
|
||
if (status) {
|
||
status.textContent = "";
|
||
status.className = "journal-upload-status or-upload-status";
|
||
}
|
||
return;
|
||
}
|
||
if (!draftId) draftId = newDraftId();
|
||
if (status) {
|
||
status.textContent = "上传中…";
|
||
status.className = "journal-upload-status or-upload-status journal-upload-status--pending";
|
||
}
|
||
var fd = new FormData();
|
||
fd.append("draft_id", draftId);
|
||
fd.append("tf", input.getAttribute("data-tf") || "");
|
||
fd.append("file", file);
|
||
fetch("/api/options/review/upload_slot", {
|
||
method: "POST",
|
||
body: fd,
|
||
credentials: "same-origin",
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (data) {
|
||
if (!data.ok) throw new Error(data.error || "fail");
|
||
if (hidden) hidden.value = data.file;
|
||
if (status) {
|
||
status.textContent = "上传成功 " + data.file;
|
||
status.className = "journal-upload-status or-upload-status journal-upload-status--ok";
|
||
}
|
||
input.value = "";
|
||
})
|
||
.catch(function () {
|
||
if (hidden) hidden.value = "";
|
||
if (status) {
|
||
status.textContent = "上传失败";
|
||
status.className = "journal-upload-status or-upload-status journal-upload-status--err";
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function setMoodTags(raw) {
|
||
var set = {};
|
||
String(raw || "")
|
||
.split(/[,,]/)
|
||
.map(function (x) {
|
||
return x.trim();
|
||
})
|
||
.filter(Boolean)
|
||
.forEach(function (x) {
|
||
set[x] = true;
|
||
});
|
||
document.querySelectorAll(".or-mood").forEach(function (cb) {
|
||
cb.checked = !!set[cb.value];
|
||
});
|
||
}
|
||
|
||
function collectMoodTags() {
|
||
var out = [];
|
||
document.querySelectorAll(".or-mood:checked").forEach(function (cb) {
|
||
out.push(cb.value);
|
||
});
|
||
return out.join(",");
|
||
}
|
||
|
||
function collectImages() {
|
||
var out = [];
|
||
document.querySelectorAll("#or-upload-slots .or-upload-hidden").forEach(function (el) {
|
||
var file = (el.value || "").trim();
|
||
if (file) out.push({ tf: el.getAttribute("data-tf") || "", file: file });
|
||
});
|
||
return out;
|
||
}
|
||
|
||
function hideJournalForm() {
|
||
currentTradeId = null;
|
||
var card = $("or-journal-card");
|
||
if (card) card.classList.add("hidden");
|
||
document.querySelectorAll(".or-trade-row").forEach(function (tr) {
|
||
tr.classList.remove("or-row-active");
|
||
});
|
||
($("or-trade-id") || {}).value = "";
|
||
($("or-f-open") || {}).value = "";
|
||
($("or-f-close") || {}).value = "";
|
||
($("or-f-coin") || {}).value = "";
|
||
($("or-f-inst") || {}).value = "";
|
||
($("or-f-pnl") || {}).value = "";
|
||
($("or-f-hold") || {}).value = "";
|
||
($("or-f-strategy") || {}).value = "";
|
||
($("or-f-direction") || {}).value = "";
|
||
($("or-f-exit") || {}).value = "";
|
||
($("or-f-followed") || {}).value = "";
|
||
($("or-f-result") || {}).value = "";
|
||
($("or-f-entry") || {}).value = "";
|
||
($("or-f-note") || {}).value = "";
|
||
setMoodTags("");
|
||
resetUploadSlots();
|
||
var summary = $("or-journal-summary");
|
||
if (summary) {
|
||
summary.textContent = "截图槽位与合约复盘相同(5m / 15m / 1h / 4h).";
|
||
}
|
||
var legsHost = $("or-legs-host");
|
||
if (legsHost) legsHost.innerHTML = "";
|
||
($("or-save-status") || {}).textContent = "";
|
||
}
|
||
|
||
function openJournalForm(tradeId) {
|
||
currentTradeId = tradeId;
|
||
var card = $("or-journal-card");
|
||
if (!card) return;
|
||
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);
|
||
});
|
||
resetUploadSlots();
|
||
bindUploadSlots();
|
||
($("or-save-status") || {}).textContent = "加载中…";
|
||
|
||
fetch("/api/options/review/trades/" + tradeId, { credentials: "same-origin" })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (data) {
|
||
if (!data.ok || !data.trade) {
|
||
($("or-save-status") || {}).textContent = "加载失败";
|
||
return;
|
||
}
|
||
fillForm(data.trade);
|
||
($("or-save-status") || {}).textContent = "已选中 #" + tradeId;
|
||
})
|
||
.catch(function () {
|
||
($("or-save-status") || {}).textContent = "加载失败";
|
||
});
|
||
}
|
||
|
||
function fillForm(t) {
|
||
var e = t.entry || {};
|
||
applyFormPresets(t.source_type || activeSource);
|
||
($("or-trade-id") || {}).value = String(t.id || "");
|
||
($("or-f-open") || {}).value = toLocalInput(t.opened_at);
|
||
($("or-f-close") || {}).value = toLocalInput(t.closed_at);
|
||
($("or-f-coin") || {}).value = t.underlying || "";
|
||
($("or-f-inst") || {}).value =
|
||
t.source_type === "option_spot"
|
||
? t.inst_id || ""
|
||
: (t.source_label || "") +
|
||
(t.plan_close_reason ? " · " + closeReasonLabel(t.plan_close_reason) : "");
|
||
($("or-f-pnl") || {}).value = fmtPnl(t.realized_pnl_total);
|
||
($("or-f-hold") || {}).value = fmtHold(t.hold_seconds);
|
||
setSelectValue($("or-f-strategy"), e.strategy_tag || "");
|
||
setSelectValue($("or-f-direction"), e.direction_view || autoDirection(t));
|
||
($("or-f-exit") || {}).value = e.exit_reason || closeReasonLabel(t.plan_close_reason) || "";
|
||
($("or-f-followed") || {}).value = e.followed_plan || "";
|
||
setSelectValue($("or-f-result"), e.result_tag || autoResultTag(t.realized_pnl_total));
|
||
setSelectValue($("or-f-entry"), e.entry_logic || "");
|
||
($("or-f-note") || {}).value = e.note || "";
|
||
setMoodTags(e.mistake_tags);
|
||
|
||
var summary = $("or-journal-summary");
|
||
if (summary) {
|
||
summary.textContent =
|
||
(t.source_label || "") +
|
||
" · " +
|
||
(t.inst_id || t.underlying || "#" + t.id) +
|
||
" · 盈亏 " +
|
||
fmtPnl(t.realized_pnl_total) +
|
||
(t.is_hedge
|
||
? " (永续 " + fmtPnl(t.realized_pnl_perp) + " / 期权 " + fmtPnl(t.realized_pnl_options) + ")"
|
||
: "");
|
||
}
|
||
|
||
(e.images || []).forEach(function (img) {
|
||
var hidden = document.querySelector(
|
||
'#or-upload-slots .or-upload-hidden[data-tf="' + img.tf + '"]'
|
||
);
|
||
var status = document.querySelector(
|
||
'#or-upload-slots .or-upload-status[data-tf="' + img.tf + '"]'
|
||
);
|
||
if (hidden && img.file) {
|
||
hidden.value = img.file;
|
||
if (status) {
|
||
var src = optionsJournalImgSrc(img.file);
|
||
status.innerHTML =
|
||
'已有 <a href="' +
|
||
src +
|
||
'" target="_blank" rel="noopener">' +
|
||
escapeHtml(img.file) +
|
||
'</a><br><img class="or-slot-thumb" src="' +
|
||
src +
|
||
'" alt="' +
|
||
escapeHtml(img.tf || "") +
|
||
'" loading="lazy">';
|
||
status.className =
|
||
"journal-upload-status or-upload-status journal-upload-status--ok";
|
||
}
|
||
}
|
||
});
|
||
|
||
var legsHost = $("or-legs-host");
|
||
if (legsHost) {
|
||
if (t.legs && t.legs.length) {
|
||
legsHost.innerHTML =
|
||
"<h3>计划腿</h3><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(legRoleLabel(leg.leg_role)) +
|
||
"</td><td>" +
|
||
escapeHtml(leg.inst_id || leg.symbol || "") +
|
||
"</td><td>" +
|
||
fmtPnl(leg.realized_pnl) +
|
||
"</td><td>" +
|
||
escapeHtml(closeReasonLabel(leg.close_reason)) +
|
||
"</td></tr>"
|
||
);
|
||
})
|
||
.join("") +
|
||
"</tbody></table>";
|
||
} else {
|
||
legsHost.innerHTML = "";
|
||
}
|
||
}
|
||
}
|
||
|
||
function hideTrade(tradeId) {
|
||
if (!tradeId) return;
|
||
if (!confirm("从待复盘列表删除并隐藏?刷新后也不会再出现.")) return;
|
||
fetch("/api/options/review/trades/" + tradeId, {
|
||
method: "DELETE",
|
||
credentials: "same-origin",
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (data) {
|
||
if (!data.ok) {
|
||
alert(data.msg || "删除失败");
|
||
return;
|
||
}
|
||
if (currentTradeId === tradeId) hideJournalForm();
|
||
reloadAll();
|
||
})
|
||
.catch(function () {
|
||
alert("删除失败");
|
||
});
|
||
}
|
||
|
||
function saveEntry() {
|
||
var tradeId = Number(($("or-trade-id") || {}).value || 0);
|
||
if (!tradeId) {
|
||
alert("请先点击交易记录中的「复盘」");
|
||
return;
|
||
}
|
||
var strategy = (($("or-f-strategy") || {}).value || "").trim();
|
||
if (!strategy) {
|
||
alert("请选择策略标签");
|
||
return;
|
||
}
|
||
var payload = {
|
||
trade_id: tradeId,
|
||
strategy_tag: strategy,
|
||
direction_view: ($("or-f-direction") || {}).value || "",
|
||
exit_reason: ($("or-f-exit") || {}).value || "",
|
||
followed_plan: ($("or-f-followed") || {}).value || "",
|
||
result_tag: ($("or-f-result") || {}).value || "",
|
||
mistake_tags: collectMoodTags(),
|
||
entry_logic: ($("or-f-entry") || {}).value || "",
|
||
note: ($("or-f-note") || {}).value || "",
|
||
images: collectImages(),
|
||
};
|
||
($("or-save-status") || {}).textContent = "保存中…";
|
||
fetch("/api/options/review/entry", {
|
||
method: "POST",
|
||
credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (data) {
|
||
if (!data.ok) {
|
||
($("or-save-status") || {}).textContent = data.msg || "保存失败";
|
||
return;
|
||
}
|
||
($("or-save-status") || {}).textContent = "已保存";
|
||
hideJournalForm();
|
||
reloadAll();
|
||
openDetail(tradeId);
|
||
})
|
||
.catch(function () {
|
||
($("or-save-status") || {}).textContent = "保存失败";
|
||
});
|
||
}
|
||
|
||
function deleteEntry() {
|
||
var tradeId = Number(($("or-trade-id") || {}).value || 0);
|
||
if (!tradeId) return;
|
||
if (!confirm("删除该条复盘内容与图片?交易记录会回到待复盘列表.")) return;
|
||
fetch("/api/options/review/entry/" + tradeId, {
|
||
method: "DELETE",
|
||
credentials: "same-origin",
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function () {
|
||
hideJournalForm();
|
||
hideDetail();
|
||
reloadAll();
|
||
});
|
||
}
|
||
|
||
function init() {
|
||
if (!$("options-review-root")) return;
|
||
document.querySelectorAll(".or-tab").forEach(function (btn) {
|
||
btn.addEventListener("click", function () {
|
||
setActiveTab(btn.getAttribute("data-source"));
|
||
});
|
||
});
|
||
var reloadBtn = $("or-reload-btn");
|
||
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 reviewedPrev = $("or-reviewed-prev");
|
||
var reviewedNext = $("or-reviewed-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);
|
||
if (prevBtn) {
|
||
prevBtn.addEventListener("click", function (ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
if (tradesPage <= 0) return;
|
||
tradesPage -= 1;
|
||
updateTradesPager();
|
||
loadTrades({ sync: false, soft: true });
|
||
});
|
||
}
|
||
if (nextBtn) {
|
||
nextBtn.addEventListener("click", function (ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
if (tradesPage + 1 >= tradesPages) return;
|
||
tradesPage += 1;
|
||
updateTradesPager();
|
||
loadTrades({ sync: false, soft: true });
|
||
});
|
||
}
|
||
if (reviewedPrev) {
|
||
reviewedPrev.addEventListener("click", function (ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
if (reviewedPage <= 0) return;
|
||
reviewedPage -= 1;
|
||
updateReviewedPager();
|
||
loadReviewed({ sync: false, soft: true });
|
||
});
|
||
}
|
||
if (reviewedNext) {
|
||
reviewedNext.addEventListener("click", function (ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
if (reviewedPage + 1 >= reviewedPages) return;
|
||
reviewedPage += 1;
|
||
updateReviewedPager();
|
||
loadReviewed({ sync: false, soft: true });
|
||
});
|
||
}
|
||
if (detailClose) detailClose.addEventListener("click", hideDetail);
|
||
if (detailEdit) {
|
||
detailEdit.addEventListener("click", function () {
|
||
var id = Number(detailEdit.getAttribute("data-id") || 0);
|
||
if (id) {
|
||
hideDetail();
|
||
openJournalForm(id);
|
||
}
|
||
});
|
||
}
|
||
var detailBackdrop = $("or-detail-backdrop");
|
||
if (detailBackdrop) {
|
||
detailBackdrop.addEventListener("click", function (ev) {
|
||
if (ev.target === detailBackdrop) hideDetail();
|
||
});
|
||
}
|
||
var lightbox = $("or-img-lightbox");
|
||
if (lightbox) {
|
||
lightbox.addEventListener("click", function () {
|
||
hideLightbox();
|
||
});
|
||
}
|
||
document.addEventListener("keydown", function (ev) {
|
||
if (ev.key !== "Escape") return;
|
||
var lb = $("or-img-lightbox");
|
||
if (lb && !lb.hidden) {
|
||
hideLightbox();
|
||
return;
|
||
}
|
||
var bd = $("or-detail-backdrop");
|
||
if (bd && !bd.hidden) hideDetail();
|
||
});
|
||
["or-filter-uly", "or-filter-opt", "or-include-hedge-legs"].forEach(function (id) {
|
||
var el = $(id);
|
||
if (el) {
|
||
el.addEventListener("change", function () {
|
||
tradesPage = 0;
|
||
reviewedPage = 0;
|
||
reloadAll();
|
||
});
|
||
}
|
||
});
|
||
["or-filter-q", "or-filter-strategy", "or-filter-from", "or-filter-to"].forEach(function (id) {
|
||
var el = $(id);
|
||
if (el) {
|
||
el.addEventListener("change", function () {
|
||
tradesPage = 0;
|
||
reviewedPage = 0;
|
||
reloadAll();
|
||
});
|
||
}
|
||
});
|
||
bindUploadSlots();
|
||
hideJournalForm();
|
||
hideDetail();
|
||
hardenSearchAutofill();
|
||
setActiveTab(defaultSourceForMode(tradeModeFromDom()));
|
||
}
|
||
|
||
function hardenSearchAutofill() {
|
||
var qEl = $("or-filter-q");
|
||
if (!qEl) return;
|
||
function wipe() {
|
||
qEl.value = "";
|
||
}
|
||
wipe();
|
||
qEl.addEventListener("focus", function () {
|
||
qEl.removeAttribute("readonly");
|
||
});
|
||
qEl.addEventListener("blur", function () {
|
||
if (!qEl.value) qEl.setAttribute("readonly", "readonly");
|
||
});
|
||
// 密码管理器常延后写入用户名,加载后再清两次
|
||
setTimeout(wipe, 200);
|
||
setTimeout(wipe, 800);
|
||
}
|
||
|
||
global.OptionsReview = {
|
||
init: init,
|
||
openJournalForm: openJournalForm,
|
||
hideJournalForm: hideJournalForm,
|
||
};
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
} else {
|
||
init();
|
||
}
|
||
})(typeof window !== "undefined" ? window : globalThis);
|