778a215ad7
Stop auto-closing the print tab on afterprint (short checklist pages were especially fragile), load print HTML via fetch into a blank window, and use CSS checkboxes instead of Unicode ballot boxes. Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
7.1 KiB
JavaScript
212 lines
7.1 KiB
JavaScript
/**
|
|
* 策略说明:三所 MD + 开仓检查清单 JSON.
|
|
*/
|
|
(function () {
|
|
const page = document.getElementById("page-strategy");
|
|
if (!page) return;
|
|
|
|
const tabsEl = document.getElementById("strategy-tabs");
|
|
const statusEl = document.getElementById("strategy-load-status");
|
|
const docBody = document.getElementById("strategy-doc-body");
|
|
const docSource = document.getElementById("strategy-doc-source");
|
|
const docCard = page.querySelector(".strategy-doc-card");
|
|
const checklistCard = page.querySelector(".strategy-checklist-card");
|
|
const checklistTitle = document.getElementById("strategy-checklist-title");
|
|
const checklistBody = document.getElementById("strategy-checklist-body");
|
|
const footnotesEl = document.getElementById("strategy-checklist-footnotes");
|
|
const btnPrintDoc = document.getElementById("strategy-btn-print-doc");
|
|
const btnPrintChecklist = document.getElementById("strategy-btn-print-checklist");
|
|
const btnDownload = document.getElementById("strategy-btn-download");
|
|
|
|
let activeKey = "binance";
|
|
let tabsMeta = [];
|
|
let cache = {};
|
|
let bound = false;
|
|
let heightSyncRaf = 0;
|
|
|
|
async function apiFetch(url, opts) {
|
|
const r = await fetch(url, { credentials: "same-origin", ...(opts || {}) });
|
|
const ct = (r.headers.get("content-type") || "").toLowerCase();
|
|
if (ct.includes("application/json")) {
|
|
const data = await r.json();
|
|
if (!r.ok) throw new Error((data && data.msg) || r.statusText || "请求失败");
|
|
return data;
|
|
}
|
|
if (!r.ok) throw new Error(r.statusText || "请求失败");
|
|
return r;
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function syncDocCardHeight() {
|
|
if (!docCard || !checklistCard || window.matchMedia("(max-width: 960px)").matches) {
|
|
if (docCard) docCard.style.height = "";
|
|
return;
|
|
}
|
|
docCard.style.height = `${checklistCard.offsetHeight}px`;
|
|
}
|
|
|
|
function scheduleHeightSync() {
|
|
if (heightSyncRaf) cancelAnimationFrame(heightSyncRaf);
|
|
heightSyncRaf = requestAnimationFrame(() => {
|
|
heightSyncRaf = 0;
|
|
syncDocCardHeight();
|
|
});
|
|
}
|
|
|
|
function renderTabs() {
|
|
if (!tabsEl) return;
|
|
tabsEl.innerHTML = tabsMeta
|
|
.map(
|
|
(t) =>
|
|
`<button type="button" class="strategy-tab${t.key === activeKey ? " is-active" : ""}" role="tab" aria-selected="${t.key === activeKey}" data-key="${esc(t.key)}">${esc(t.label)}</button>`
|
|
)
|
|
.join("");
|
|
tabsEl.querySelectorAll(".strategy-tab").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const key = btn.getAttribute("data-key");
|
|
if (!key || key === activeKey) return;
|
|
activeKey = key;
|
|
renderTabs();
|
|
void loadExchange(key);
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderChecklist(checklist) {
|
|
const cl = checklist || {};
|
|
const title = cl.title || "开仓检查清单";
|
|
if (checklistTitle) checklistTitle.textContent = title;
|
|
if (!checklistBody) return;
|
|
const groups = cl.groups || [];
|
|
if (!groups.length) {
|
|
checklistBody.innerHTML = '<p class="strategy-empty">暂无检查清单</p>';
|
|
} else {
|
|
checklistBody.innerHTML = groups
|
|
.map((grp) => {
|
|
const items = (grp.items || [])
|
|
.map((item) => `<li><span class="strategy-check-box" aria-hidden="true">☐</span>${esc(item)}</li>`)
|
|
.join("");
|
|
return `<div class="strategy-check-group"><h4>${esc(grp.title || "")}</h4><ul>${items}</ul></div>`;
|
|
})
|
|
.join("");
|
|
}
|
|
if (footnotesEl) {
|
|
const notes = cl.footnotes || [];
|
|
footnotesEl.innerHTML = notes.map((n) => `<li>${esc(n)}</li>`).join("");
|
|
footnotesEl.classList.toggle("hidden", !notes.length);
|
|
}
|
|
scheduleHeightSync();
|
|
}
|
|
|
|
function renderPayload(data) {
|
|
if (docBody) docBody.innerHTML = data.strategy_html || "";
|
|
if (docSource) {
|
|
const ver = data.version ? ` · ${data.version}` : "";
|
|
docSource.textContent = `文档:${data.md_source || ""}${ver}`;
|
|
}
|
|
renderChecklist(data.checklist);
|
|
scheduleHeightSync();
|
|
}
|
|
|
|
async function loadExchange(key) {
|
|
if (statusEl) statusEl.textContent = "加载中…";
|
|
try {
|
|
let data = cache[key];
|
|
if (!data) {
|
|
data = await apiFetch(`/api/strategy/${encodeURIComponent(key)}`);
|
|
cache[key] = data;
|
|
}
|
|
renderPayload(data);
|
|
if (statusEl) statusEl.textContent = "";
|
|
} catch (e) {
|
|
if (statusEl) statusEl.textContent = String(e);
|
|
if (docBody) docBody.innerHTML = "";
|
|
if (checklistBody) checklistBody.innerHTML = "";
|
|
scheduleHeightSync();
|
|
}
|
|
}
|
|
|
|
async function loadMeta() {
|
|
const meta = await apiFetch("/api/strategy/meta");
|
|
tabsMeta = meta.exchanges || [];
|
|
if (tabsMeta.length && !tabsMeta.some((t) => t.key === activeKey)) {
|
|
activeKey = tabsMeta[0].key;
|
|
}
|
|
renderTabs();
|
|
}
|
|
|
|
async function printSection(mode) {
|
|
const part = mode === "checklist" ? "checklist" : "doc";
|
|
const url = `/api/strategy/${encodeURIComponent(activeKey)}/print?part=${encodeURIComponent(part)}`;
|
|
// 同步打开空白页保留用户手势;再 fetch 写入,避免 noopener 丢句柄 / 短页误关窗
|
|
const w = window.open("about:blank", "_blank");
|
|
if (!w) {
|
|
if (statusEl) statusEl.textContent = "请允许弹出窗口以打开打印预览";
|
|
return;
|
|
}
|
|
try {
|
|
w.document.write(
|
|
"<!DOCTYPE html><title>打印准备中…</title><body style=\"font-family:sans-serif;padding:24px;color:#222\">正在加载打印内容…</body>"
|
|
);
|
|
w.document.close();
|
|
} catch (_) {}
|
|
try {
|
|
const r = await fetch(url, { credentials: "same-origin" });
|
|
const html = await r.text();
|
|
if (!r.ok) {
|
|
throw new Error((html && html.slice(0, 120)) || r.statusText || "加载打印页失败");
|
|
}
|
|
w.document.open();
|
|
w.document.write(html);
|
|
w.document.close();
|
|
if (statusEl) statusEl.textContent = "";
|
|
} catch (e) {
|
|
const msg = String(e && e.message ? e.message : e);
|
|
try {
|
|
w.document.open();
|
|
w.document.write(
|
|
`<!DOCTYPE html><body style="font-family:sans-serif;padding:24px;color:#222">打印页加载失败: ${esc(msg)}</body>`
|
|
);
|
|
w.document.close();
|
|
} catch (_) {}
|
|
if (statusEl) statusEl.textContent = msg;
|
|
}
|
|
}
|
|
|
|
function bindActions() {
|
|
if (bound) return;
|
|
bound = true;
|
|
if (btnPrintDoc) btnPrintDoc.addEventListener("click", () => printSection("doc"));
|
|
if (btnPrintChecklist) btnPrintChecklist.addEventListener("click", () => printSection("checklist"));
|
|
if (btnDownload) {
|
|
btnDownload.addEventListener("click", () => {
|
|
window.location.href = `/api/strategy/${encodeURIComponent(activeKey)}/export`;
|
|
});
|
|
}
|
|
window.addEventListener("resize", scheduleHeightSync);
|
|
}
|
|
|
|
async function init() {
|
|
bindActions();
|
|
try {
|
|
await loadMeta();
|
|
await loadExchange(activeKey);
|
|
} catch (e) {
|
|
if (statusEl) statusEl.textContent = String(e);
|
|
}
|
|
}
|
|
|
|
function destroy() {
|
|
window.removeEventListener("resize", scheduleHeightSync);
|
|
}
|
|
|
|
window.hubStrategyPage = { init, destroy };
|
|
})();
|