/** * 策略说明:策略正文(目录 h2 + 章节标识) + 执行清单 Tab. */ (function () { const page = document.getElementById("page-strategy"); if (!page) return; const tabsEl = document.getElementById("strategy-tabs"); const viewTabsEl = document.getElementById("strategy-view-tabs"); const statusEl = document.getElementById("strategy-load-status"); const docBody = document.getElementById("strategy-doc-body"); const docSource = document.getElementById("strategy-doc-source"); const docToc = document.getElementById("strategy-doc-toc"); const panelDoc = document.getElementById("strategy-panel-doc"); const panelChecklist = document.getElementById("strategy-panel-checklist"); 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 btnPrintChecklistInline = document.getElementById("strategy-btn-print-checklist-inline"); const btnDownload = document.getElementById("strategy-btn-download"); let activeKey = "playbook_v2"; let activeView = "doc"; let tabsMeta = []; let cache = {}; let bound = false; let scrollSpyObs = null; 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, """); } function slugify(text) { return String(text || "") .trim() .toLowerCase() .replace(/\s+/g, "-") .replace(/[^\w\u4e00-\u9fff\-]+/g, "") .replace(/\-+/g, "-") .replace(/^\-|\-$/g, "") .slice(0, 48); } function setView(view) { activeView = view === "checklist" ? "checklist" : "doc"; if (panelDoc) panelDoc.classList.toggle("hidden", activeView !== "doc"); if (panelChecklist) panelChecklist.classList.toggle("hidden", activeView !== "checklist"); if (viewTabsEl) { viewTabsEl.querySelectorAll(".strategy-view-tab").forEach((btn) => { const on = btn.getAttribute("data-view") === activeView; btn.classList.toggle("is-active", on); btn.setAttribute("aria-selected", on ? "true" : "false"); }); } } function renderExchangeTabs() { if (!tabsEl) return; if (!tabsMeta.length) { tabsEl.innerHTML = ""; if (statusEl) statusEl.textContent = "当前无可显示的策略页签(可在系统设置·显示与导航中开启)"; if (docBody) docBody.innerHTML = ""; if (docToc) docToc.innerHTML = ""; if (checklistBody) checklistBody.innerHTML = ""; return; } tabsEl.innerHTML = tabsMeta .map( (t) => `` ) .join(""); tabsEl.querySelectorAll(".strategy-tab").forEach((btn) => { btn.addEventListener("click", () => { const key = btn.getAttribute("data-key"); if (!key || key === activeKey) return; activeKey = key; renderExchangeTabs(); void loadExchange(key); }); }); } function setActiveToc(id) { if (!docToc) return; docToc.querySelectorAll(".strategy-toc-item").forEach((a) => { a.classList.toggle("is-active", a.getAttribute("data-id") === id); }); } function bindScrollSpy(heads) { if (scrollSpyObs) { scrollSpyObs.disconnect(); scrollSpyObs = null; } if (!docBody || !heads.length || typeof IntersectionObserver === "undefined") return; const visible = new Map(); scrollSpyObs = new IntersectionObserver( (entries) => { entries.forEach((en) => { if (en.isIntersecting) visible.set(en.target.id, en.intersectionRatio); else visible.delete(en.target.id); }); let bestId = ""; let bestRatio = -1; visible.forEach((ratio, id) => { if (ratio > bestRatio) { bestRatio = ratio; bestId = id; } }); if (bestId) setActiveToc(bestId); }, { root: docBody, rootMargin: "-8% 0px -70% 0px", threshold: [0, 0.25, 0.5, 1] } ); heads.forEach((h) => scrollSpyObs.observe(h)); } function sectionTag(title) { const t = String(title || ""); if (/总原则|原则/.test(t)) return "原则"; if (/账户|定位|分工/.test(t)) return "账户"; if (/开仓类型|开仓|入场|反转|顺势|波段|假破|结构|对冲|方向单/.test(t)) return "入场"; if (/周期/.test(t)) return "周期"; if (/方向/.test(t)) return "方向"; if (/纪律|出场|笔数|节奏|止损|次数/.test(t)) return "纪律"; if (/持仓|离场|强平|到期/.test(t)) return "离场"; if (/资金|杠杆|计仓|仓位|预算/.test(t)) return "仓位"; if (/系统|字段|对接/.test(t)) return "系统"; if (/修订|记录/.test(t)) return "版本"; if (/边界|关系|行情状态/.test(t)) return "边界"; if (/选择|如何/.test(t)) return "选择"; return "章节"; } function buildDocToc() { if (!docBody || !docToc) return; const heads = Array.from(docBody.querySelectorAll("h2")); if (!heads.length) { docToc.innerHTML = '

暂无目录

'; return; } const used = {}; const items = []; heads.forEach((el, idx) => { const num = String(idx + 1).padStart(2, "0"); const text = (el.textContent || "").trim(); const tag = sectionTag(text); let base = "st-" + num + "-" + slugify(text); if (!base || base === "st-" + num + "-") base = "st-" + num; let id = base; let n = 2; while (used[id] || document.getElementById(id)) { id = base + "-" + n; n += 1; } used[id] = true; el.id = id; if (!el.querySelector(".strategy-sec-mark")) { const mark = document.createElement("span"); mark.className = "strategy-sec-mark"; mark.setAttribute("aria-hidden", "true"); mark.innerHTML = `${esc(num)}` + `${esc(tag)}`; el.insertBefore(mark, el.firstChild); } items.push({ num, id, text, tag }); }); Array.from(docBody.querySelectorAll("h3")).forEach((el, idx) => { if (el.id) return; let base = "st-h3-" + (idx + 1) + "-" + slugify(el.textContent); if (!base || base.endsWith("-")) base = "st-h3-" + (idx + 1); let id = base; let n = 2; while (document.getElementById(id)) { id = base + "-" + n; n += 1; } el.id = id; }); docToc.innerHTML = items .map( (it) => `` + `${esc(it.num)}` + `${esc(it.tag)}` + `${esc(it.text)}` ) .join(""); docToc.querySelectorAll(".strategy-toc-item").forEach((a) => { a.addEventListener("click", (ev) => { ev.preventDefault(); const id = a.getAttribute("data-id"); const target = id ? document.getElementById(id) : null; if (!target || !docBody.contains(target)) return; setActiveToc(id); target.scrollIntoView({ behavior: "smooth", block: "start" }); }); }); if (items[0]) setActiveToc(items[0].id); bindScrollSpy(heads); } 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 = '

暂无检查清单

'; } else { checklistBody.innerHTML = groups .map((grp) => { const items = (grp.items || []) .map((item) => `
  • ${esc(item)}
  • `) .join(""); return `

    ${esc(grp.title || "")}

    `; }) .join(""); } if (footnotesEl) { const notes = cl.footnotes || []; footnotesEl.innerHTML = notes.map((n) => `
  • ${esc(n)}
  • `).join(""); footnotesEl.classList.toggle("hidden", !notes.length); } } function renderPayload(data) { if (docBody) docBody.innerHTML = data.strategy_html || ""; if (docSource) { const ver = data.version ? ` · ${data.version}` : ""; docSource.textContent = `文档:${data.md_source || ""}${ver}`; } buildDocToc(); renderChecklist(data.checklist); } 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 (docToc) docToc.innerHTML = ""; if (checklistBody) checklistBody.innerHTML = ""; } } 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; } renderExchangeTabs(); } async function reloadMeta() { try { await loadMeta(); if (tabsMeta.length) await loadExchange(activeKey); } catch (e) { if (statusEl) statusEl.textContent = String(e); } } async function printSection(mode) { const part = mode === "checklist" ? "checklist" : "doc"; const url = `/api/strategy/${encodeURIComponent(activeKey)}/print?part=${encodeURIComponent(part)}`; const w = window.open("about:blank", "_blank"); if (!w) { if (statusEl) statusEl.textContent = "请允许弹出窗口以打开打印预览"; return; } try { w.document.write( "打印准备中…正在加载打印内容…" ); 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( `打印页加载失败: ${esc(msg)}` ); w.document.close(); } catch (_) {} if (statusEl) statusEl.textContent = msg; } } function bindActions() { if (bound) return; bound = true; if (viewTabsEl) { viewTabsEl.querySelectorAll(".strategy-view-tab").forEach((btn) => { btn.addEventListener("click", () => setView(btn.getAttribute("data-view"))); }); } if (btnPrintDoc) btnPrintDoc.addEventListener("click", () => printSection("doc")); if (btnPrintChecklist) btnPrintChecklist.addEventListener("click", () => printSection("checklist")); if (btnPrintChecklistInline) btnPrintChecklistInline.addEventListener("click", () => printSection("checklist")); if (btnDownload) { btnDownload.addEventListener("click", () => { window.location.href = `/api/strategy/${encodeURIComponent(activeKey)}/export`; }); } } async function init() { bindActions(); setView(activeView); try { await loadMeta(); if (tabsMeta.length) await loadExchange(activeKey); else if (statusEl) statusEl.textContent = "当前无可显示的策略页签(可在系统设置·显示与导航中开启)"; } catch (e) { if (statusEl) statusEl.textContent = String(e); } } function destroy() { if (scrollSpyObs) { scrollSpyObs.disconnect(); scrollSpyObs = null; } } window.hubStrategyPage = { init, destroy, reloadMeta }; })();