/**
* 三所实例共用 UI:复盘详情、盈亏着色等。
*/
(function (global) {
"use strict";
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function pnlClassFromValue(val) {
const n = Number(String(val == null ? "" : val).replace(/[^\d.-]/g, ""));
if (!Number.isFinite(n) || n === 0) return "";
return n > 0 ? "pnl-profit" : "pnl-loss";
}
function formatPnlSpan(val, suffix) {
const sfx = suffix == null ? "U" : suffix;
const cls = pnlClassFromValue(val);
const text = escapeHtml(val == null || val === "" ? "-" : val) + sfx;
return cls ? `${text}` : text;
}
function buildJournalDetailHtml(o, formatExitLine) {
const moodTags =
Array.isArray(o.mood_issues) && o.mood_issues.length
? o.mood_issues.join(",")
: o.mood_issues || "无";
const exitText =
typeof formatExitLine === "function" ? formatExitLine(o) : o.exit_reason || "无";
const lines = [
`币种/周期:${escapeHtml(o.coin || "-")} ${escapeHtml(o.tf || "-")}`,
`开仓时间:${escapeHtml(o.open_datetime || "-")}`,
`平仓时间:${escapeHtml(o.close_datetime || "-")}`,
`持仓时长:${escapeHtml(o.hold_duration || "-")}`,
`盈亏:${formatPnlSpan(o.pnl)}`,
`开仓类型:${escapeHtml(o.entry_reason || "无")}`,
`平仓/离场:${escapeHtml(exitText)}`,
`预期RR:${escapeHtml(o.expect_rr || "-")}`,
`实际RR:${escapeHtml(o.real_rr || "-")}`,
`保本后盯盘:${escapeHtml(o.post_breakeven_stare || "-")}`,
`心态标签:${escapeHtml(moodTags)}`,
`备注:${escapeHtml(o.note || "无")}`,
];
return lines.join("
");
}
function resolveJournalImages(o) {
if (Array.isArray(o.images) && o.images.length) return o.images;
if (o.image) return [{ tf: "", file: o.image }];
return [];
}
function setJournalDetailImages(o) {
const grid = document.getElementById("detailImages");
const legacyImg = document.getElementById("detailImage");
const images = resolveJournalImages(o || {});
if (grid) {
if (!images.length) {
grid.innerHTML = "";
grid.style.display = "none";
} else {
grid.innerHTML = images
.map(function (img) {
const tf = String(img.tf || "").trim();
const file = String(img.file || "").trim();
if (!file) return "";
const label = tf ? escapeHtml(tf) : "截图";
const src = "/static/images/" + encodeURIComponent(file).replace(/%2F/g, "/");
return (
'
' +
'
' +
label +
"" +
'

' +
"
"
);
})
.join("");
grid.style.display = "grid";
}
if (legacyImg) {
legacyImg.src = "";
legacyImg.style.display = "none";
}
return;
}
if (legacyImg) {
if (images.length === 1) {
legacyImg.src = "/static/images/" + images[0].file;
legacyImg.style.display = "block";
} else {
legacyImg.src = "";
legacyImg.style.display = "none";
}
}
}
function clearJournalDetailImages() {
const grid = document.getElementById("detailImages");
if (grid) {
grid.innerHTML = "";
grid.style.display = "none";
}
const legacyImg = document.getElementById("detailImage");
if (legacyImg) {
legacyImg.src = "";
legacyImg.style.display = "none";
}
}
function setJournalDetailBody(o, formatExitLine) {
const body = document.getElementById("detailBody");
if (!body) return;
body.classList.remove("md-review", "trade-record-detail-wrap");
body.classList.add("journal-detail-meta");
body.innerHTML = buildJournalDetailHtml(o, formatExitLine);
}
function openJournalDetailModal(id, journalCache, formatExitLine) {
const o = journalCache && journalCache[id];
if (!o) return;
const titleEl = document.getElementById("detailTitle");
if (titleEl) {
titleEl.innerText = `交易复盘详情|${o.coin || "-"} ${o.tf || "-"}`;
}
setJournalDetailBody(o, formatExitLine);
clearDetailActions();
setJournalDetailImages(o);
if (typeof setDetailModalFullscreen === "function") {
setDetailModalFullscreen(false);
}
const modal = document.getElementById("detailModal");
if (modal) modal.style.display = "flex";
}
function isMobileCompactRecords() {
if (typeof window === "undefined" || !window.matchMedia) return false;
return window.matchMedia("(max-width: 720px)").matches;
}
function inferJournalDirection(o) {
const text = String((o && o.entry_reason) || "");
if (/做空|空头|short/i.test(text)) {
return { text: "做空", cls: "direction-short" };
}
if (/做多|多头|long/i.test(text)) {
return { text: "做多", cls: "direction-long" };
}
return null;
}
function renderJournalListHtml(data) {
if (!data || !data.length) return "";
const mobile = isMobileCompactRecords();
return data
.map(function (o) {
if (mobile) {
const dir = inferJournalDirection(o);
const pnlCls = pnlClassFromValue(o.pnl);
const dirHtml = dir
? `${escapeHtml(dir.text)}`
: `-`;
const id = escapeHtml(o.id);
return `
`;
}
const moodTags = (o.mood_issues || []).join(",") || "无";
const id = escapeHtml(o.id);
return `
${escapeHtml(o.coin || "-")} ${escapeHtml(o.tf || "-")} | 盈亏:${escapeHtml(o.pnl == null || o.pnl === "" ? "-" : o.pnl)}U
开:${escapeHtml(o.open_datetime || "-")} 平:${escapeHtml(o.close_datetime || "-")} 持仓:${escapeHtml(o.hold_duration || "-")}
心态标签:${escapeHtml(moodTags)}
`;
})
.join("");
}
function parseTradeRecordRow(tr) {
const cells = tr.querySelectorAll("td");
if (cells.length < 14) return null;
const dirBadge = cells[2].querySelector(".badge");
return {
rowId: tr.id,
symbol: cells[0].textContent.trim(),
type: cells[1].textContent.trim(),
directionHtml: (dirBadge ? dirBadge.outerHTML : cells[2].innerHTML).trim(),
directionText: cells[2].textContent.trim(),
trigger: cells[3].textContent.trim(),
stopLoss: cells[4].textContent.trim(),
takeProfit: cells[5].textContent.trim(),
margin: cells[6].textContent.trim(),
leverage: cells[7].textContent.trim(),
holdMinutes: cells[8].textContent.trim(),
openedAt: cells[9].textContent.trim(),
closedAt: cells[10].textContent.trim(),
pnlHtml: cells[11].innerHTML.trim(),
pnlText: cells[11].textContent.trim(),
resultHtml: cells[12].innerHTML.trim(),
resultText: cells[12].textContent.trim(),
actionsHtml: cells[13].innerHTML,
};
}
function renderMobileTradeRow(tr) {
const row = parseTradeRecordRow(tr);
if (!row) return "";
const pnlCls = pnlClassFromValue(row.pnlText);
return ``;
}
function tradeDetailRow(label, valueHtml) {
return `${escapeHtml(label)}${valueHtml}
`;
}
function buildTradeRecordDetailHtml(row) {
return `${
tradeDetailRow("品种", escapeHtml(row.symbol)) +
tradeDetailRow("类型", escapeHtml(row.type)) +
tradeDetailRow("方向", row.directionHtml) +
tradeDetailRow("成交价", escapeHtml(row.trigger)) +
tradeDetailRow("止损(开仓)", escapeHtml(row.stopLoss)) +
tradeDetailRow("止盈", escapeHtml(row.takeProfit)) +
tradeDetailRow("基数", escapeHtml(row.margin)) +
tradeDetailRow("杠杆", escapeHtml(row.leverage)) +
tradeDetailRow("持仓分钟", escapeHtml(row.holdMinutes)) +
tradeDetailRow("开仓时间", escapeHtml(row.openedAt)) +
tradeDetailRow("平仓时间", escapeHtml(row.closedAt)) +
tradeDetailRow("盈亏U", row.pnlHtml) +
tradeDetailRow("结果", row.resultHtml)
}
`;
}
function clearDetailActions() {
const el = document.getElementById("detailActions");
if (el) {
el.innerHTML = "";
el.style.display = "none";
}
}
function setDetailActionsHtml(html) {
let el = document.getElementById("detailActions");
if (!el) {
const panel = document.querySelector("#detailModal .panel");
if (!panel) return;
el = document.createElement("div");
el.id = "detailActions";
el.className = "detail-actions";
const body = document.getElementById("detailBody");
if (body && body.parentNode === panel) {
panel.insertBefore(el, body.nextSibling);
} else {
panel.appendChild(el);
}
}
el.innerHTML = html || "";
el.style.display = html ? "flex" : "none";
}
function promptReviewEntryReason(options, currentValue) {
const opts = Array.isArray(options) ? options : [];
const cur = String(currentValue == null ? "" : currentValue).trim();
return new Promise(function (resolve) {
const backdrop = document.createElement("div");
backdrop.className = "review-entry-reason-backdrop open";
const modal = document.createElement("div");
modal.className = "review-entry-reason-modal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
const title = document.createElement("h3");
title.textContent = "开仓类型";
modal.appendChild(title);
const hint = document.createElement("p");
hint.className = "review-entry-reason-hint";
hint.textContent = "请选择下拉选项之一;选「不改该项」则保留原值。";
modal.appendChild(hint);
const select = document.createElement("select");
select.className = "review-entry-reason-select";
const emptyOpt = document.createElement("option");
emptyOpt.value = "";
emptyOpt.textContent = "(不改该项)";
select.appendChild(emptyOpt);
const seen = new Set([""]);
if (cur && opts.indexOf(cur) < 0) {
const curOpt = document.createElement("option");
curOpt.value = cur;
curOpt.textContent = cur + "(当前)";
select.appendChild(curOpt);
seen.add(cur);
}
opts.forEach(function (opt) {
const v = String(opt || "").trim();
if (!v || seen.has(v)) return;
const o = document.createElement("option");
o.value = v;
o.textContent = v;
select.appendChild(o);
seen.add(v);
});
if (cur) select.value = cur;
modal.appendChild(select);
const actions = document.createElement("div");
actions.className = "review-entry-reason-actions";
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.className = "review-entry-reason-cancel";
cancelBtn.textContent = "取消";
const okBtn = document.createElement("button");
okBtn.type = "button";
okBtn.className = "review-entry-reason-ok";
okBtn.textContent = "确定";
actions.appendChild(cancelBtn);
actions.appendChild(okBtn);
modal.appendChild(actions);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
function cleanup(result) {
document.removeEventListener("keydown", onKey);
backdrop.remove();
resolve(result);
}
function onKey(ev) {
if (ev.key === "Escape") cleanup(null);
}
cancelBtn.addEventListener("click", function () {
cleanup(null);
});
backdrop.addEventListener("click", function (ev) {
if (ev.target === backdrop) cleanup(null);
});
okBtn.addEventListener("click", function () {
cleanup(select.value);
});
document.addEventListener("keydown", onKey);
select.focus();
});
}
function openTradeRecordDetailModal(tr) {
const row = parseTradeRecordRow(tr);
if (!row) return;
const titleEl = document.getElementById("detailTitle");
if (titleEl) {
titleEl.innerText = `交易记录|${row.symbol}`;
}
const body = document.getElementById("detailBody");
if (body) {
body.classList.remove("md-review", "journal-detail-meta");
body.classList.add("trade-record-detail-wrap");
body.innerHTML = buildTradeRecordDetailHtml(row);
}
setDetailActionsHtml(
`${row.actionsHtml}
`
);
const imgEl = document.getElementById("detailImage");
if (imgEl) {
imgEl.src = "";
imgEl.style.display = "none";
}
if (typeof setDetailModalFullscreen === "function") {
setDetailModalFullscreen(false);
}
const modal = document.getElementById("detailModal");
if (modal) modal.style.display = "flex";
}
global.InstanceUI = {
escapeHtml: escapeHtml,
pnlClassFromValue: pnlClassFromValue,
formatPnlSpan: formatPnlSpan,
buildJournalDetailHtml: buildJournalDetailHtml,
setJournalDetailBody: setJournalDetailBody,
openJournalDetailModal: openJournalDetailModal,
isMobileCompactRecords: isMobileCompactRecords,
inferJournalDirection: inferJournalDirection,
renderJournalListHtml: renderJournalListHtml,
parseTradeRecordRow: parseTradeRecordRow,
renderMobileTradeRow: renderMobileTradeRow,
buildTradeRecordDetailHtml: buildTradeRecordDetailHtml,
openTradeRecordDetailModal: openTradeRecordDetailModal,
clearDetailActions: clearDetailActions,
clearJournalDetailImages: clearJournalDetailImages,
setJournalDetailImages: setJournalDetailImages,
promptReviewEntryReason: promptReviewEntryReason,
};
})(typeof window !== "undefined" ? window : globalThis);