diff --git a/lib/hub/hub_strategy_lib.py b/lib/hub/hub_strategy_lib.py index 6cb6e0b..7b2ee43 100644 --- a/lib/hub/hub_strategy_lib.py +++ b/lib/hub/hub_strategy_lib.py @@ -218,6 +218,217 @@ def strategy_meta_payload() -> dict[str, Any]: return {"ok": True, "exchanges": tabs} +def _checklist_html(checklist: dict[str, Any]) -> str: + groups = checklist.get("groups") or [] + parts = [f"

{escape_html(str(checklist.get('title') or '开仓检查清单'))}

"] + for grp in groups: + if not isinstance(grp, dict): + continue + gtitle = escape_html(str(grp.get("title") or "")) + parts.append(f"

{gtitle}

") + footnotes = checklist.get("footnotes") or [] + if footnotes: + parts.append("") + return "\n".join(parts) + + +def _print_document_css() -> str: + return """ +body { + margin: 0; + padding: 36px 28px 48px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + font-size: 14px; + line-height: 1.65; + color: #1a1a1a; + background: #fff; +} +.doc { + max-width: 720px; + margin: 0 auto; +} +.doc-head { + margin-bottom: 28px; + padding-bottom: 14px; + border-bottom: 1px solid #e5e5e5; +} +.doc-head h1 { + margin: 0 0 8px; + font-size: 1.55rem; + font-weight: 600; + line-height: 1.3; +} +.doc-meta { + margin: 0; + color: #666; + font-size: 0.85rem; +} +.doc-body h2 { + font-size: 1.12rem; + margin: 1.6em 0 0.55em; + font-weight: 600; +} +.doc-body h3 { + font-size: 1rem; + margin: 1.2em 0 0.45em; + font-weight: 600; +} +.doc-body h2:first-child, +.doc-body h3:first-child { + margin-top: 0; +} +.doc-body p { + margin: 0.65em 0; +} +.doc-body table { + border-collapse: collapse; + width: 100%; + font-size: 0.92rem; + margin: 10px 0 14px; +} +.doc-body th, +.doc-body td { + border: 1px solid #d8d8d8; + padding: 8px 10px; + text-align: left; + vertical-align: top; +} +.doc-body blockquote { + margin: 12px 0; + padding: 8px 14px; + border-left: 4px solid #c8c8c8; + color: #444; + background: #fafafa; +} +.doc-body pre, +.doc-body code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.88em; +} +.doc-body pre { + padding: 10px 12px; + background: #f6f6f6; + border: 1px solid #e8e8e8; + border-radius: 4px; + overflow-x: auto; +} +.doc-body hr { + border: none; + border-top: 1px solid #e5e5e5; + margin: 1.4em 0; +} +.checklist { + list-style: none; + margin: 0 0 16px; + padding: 0; +} +.checklist li { + margin: 7px 0; + padding: 0; +} +.box { + display: inline-block; + width: 1.05em; + margin-right: 6px; + font-size: 1.05em; + line-height: 1.2; +} +.footnotes { + margin: 18px 0 0; + padding-left: 20px; + color: #666; + font-size: 0.85rem; +} +.doc-foot { + margin-top: 28px; + padding-top: 12px; + border-top: 1px dashed #ddd; + color: #888; + font-size: 0.78rem; +} +@media print { + body { padding: 0; } + .doc { max-width: none; } + .doc-head { break-after: avoid; } + .doc-body h2, .doc-body h3 { break-after: avoid; } + .checklist li { break-inside: avoid; } +} +""" + + +def _print_auto_script() -> str: + return """""" + + +def build_print_html(exchange_key: str, part: str = "doc") -> str: + """part: doc | checklist""" + payload = load_strategy_payload(exchange_key) + key = payload["exchange_key"] + label = payload["label"] + title = payload["title"] + version = payload.get("version") or "" + checklist = payload.get("checklist") or {} + now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M") + part = (part or "doc").strip().lower() + css = _print_document_css() + + if part == "checklist": + cl_title = str(checklist.get("title") or "开仓检查清单") + page_title = f"{label} · {cl_title}" + body = f"""
+
+

{escape_html(cl_title)}

+

{escape_html(label)} · {escape_html(version)} · 打印 {escape_html(now)}

+
+
{_checklist_html(checklist)}
+
""" + elif part == "doc": + page_title = f"{label} · 策略说明" + source = payload.get("md_source") or "" + body = f"""
+
+

{escape_html(title)}

+

{escape_html(label)} · {escape_html(version)} · 打印 {escape_html(now)}

+
+
{payload.get("strategy_html") or ""}
+ +
""" + else: + raise KeyError(part) + + return f""" + + + + +{escape_html(page_title)} + + + +{body} +{_print_auto_script()} + +""" + + def build_export_html(exchange_key: str) -> str: payload = load_strategy_payload(exchange_key) key = payload["exchange_key"] @@ -225,23 +436,8 @@ def build_export_html(exchange_key: str) -> str: title = payload["title"] version = payload.get("version") or "" checklist = payload.get("checklist") or {} - groups = checklist.get("groups") or [] now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M") - - checklist_html_parts = [ - f"

{escape_html(str(checklist.get('title') or '开仓检查清单'))}

" - ] - for grp in groups: - if not isinstance(grp, dict): - continue - gtitle = escape_html(str(grp.get("title") or "")) - checklist_html_parts.append(f"

{gtitle}

") - checklist_html = "\n".join(checklist_html_parts) + checklist_html = _checklist_html(checklist) return f""" diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 3c4da00..5e6c240 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -80,6 +80,7 @@ from lib.hub.hub_entry_plan_lib import ( ) from lib.hub.hub_strategy_lib import ( build_export_html, + build_print_html, load_strategy_payload, strategy_meta_payload, ) @@ -2985,6 +2986,19 @@ def api_strategy_export(exchange_key: str): ) +@app.get("/api/strategy/{exchange_key}/print") +def api_strategy_print(exchange_key: str, part: str = "doc"): + key = exchange_key.strip().lower() + p = (part or "doc").strip().lower() + if p not in ("doc", "checklist"): + return JSONResponse({"ok": False, "msg": "part must be doc or checklist"}, status_code=400) + try: + html = build_print_html(key, p) + except KeyError: + return JSONResponse({"ok": False, "msg": "unknown exchange or part"}, status_code=404) + return HTMLResponse(content=html) + + @app.get("/api/entry-plans/meta") def api_entry_plans_meta(): init_entry_plan_db() diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 07d9a72..837b951 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -7545,12 +7545,6 @@ body.funds-fullscreen-open { .strategy-col-head .strategy-col-title { margin: 0; } -.strategy-col-title-print { - display: none; - margin: 0 0 14px; - font-size: 0.95rem; - color: var(--text-soft); -} .strategy-col-print { flex-shrink: 0; padding: 4px 10px; @@ -7660,17 +7654,6 @@ body.funds-fullscreen-open { color: var(--muted); font-size: 0.85rem; } -.strategy-print-header { - margin-bottom: 12px; -} -.strategy-print-header.hidden { - display: none; -} -.strategy-print-meta { - margin: 4px 0 0; - font-size: 0.85rem; - color: var(--muted); -} @media (max-width: 960px) { .strategy-layout { @@ -7685,39 +7668,3 @@ body.funds-fullscreen-open { } } -@media print { - body.hub-strategy-printing .app-bg, - body.hub-strategy-printing .app-header, - body.hub-strategy-printing .no-print, - body.hub-strategy-printing .page:not(#page-strategy) { - display: none !important; - } - body.hub-strategy-printing #page-strategy { - display: block !important; - } - body.hub-strategy-printing .strategy-print-header { - display: block !important; - } - body.hub-strategy-printing .strategy-col-title-print { - display: block !important; - } - body.hub-strategy-printing .strategy-doc-body { - max-height: none; - overflow: visible; - } - body.hub-strategy-print-doc .strategy-checklist-card { - display: none !important; - } - body.hub-strategy-print-checklist .strategy-doc-card { - display: none !important; - } - body.hub-strategy-printing .strategy-layout { - grid-template-columns: 1fr; - } - body.hub-strategy-printing .card { - break-inside: avoid; - box-shadow: none; - border: 1px solid #ccc; - } -} - diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index e2a1189..395e6ae 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -868,26 +868,20 @@
-
-
+

策略说明

-

策略说明

-
+

开仓检查清单

-

开仓检查清单

    @@ -1160,7 +1154,7 @@ - + diff --git a/manual_trading_hub/static/strategy.js b/manual_trading_hub/static/strategy.js index d657afd..c714291 100644 --- a/manual_trading_hub/static/strategy.js +++ b/manual_trading_hub/static/strategy.js @@ -12,12 +12,8 @@ const docCard = page.querySelector(".strategy-doc-card"); const checklistCard = page.querySelector(".strategy-checklist-card"); const checklistTitle = document.getElementById("strategy-checklist-title"); - const checklistTitlePrint = document.getElementById("strategy-checklist-title-print"); const checklistBody = document.getElementById("strategy-checklist-body"); const footnotesEl = document.getElementById("strategy-checklist-footnotes"); - const printTitle = document.getElementById("strategy-print-title"); - const printMeta = document.getElementById("strategy-print-meta"); - const printHeader = document.getElementById("strategy-print-header"); const btnPrintDoc = document.getElementById("strategy-btn-print-doc"); const btnPrintChecklist = document.getElementById("strategy-btn-print-checklist"); const btnDownload = document.getElementById("strategy-btn-download"); @@ -87,7 +83,6 @@ const cl = checklist || {}; const title = cl.title || "开仓检查清单"; if (checklistTitle) checklistTitle.textContent = title; - if (checklistTitlePrint) checklistTitlePrint.textContent = title; if (!checklistBody) return; const groups = cl.groups || []; if (!groups.length) { @@ -117,12 +112,6 @@ docSource.textContent = `文档:${data.md_source || ""}${ver}`; } renderChecklist(data.checklist); - if (printTitle) printTitle.textContent = data.title || data.label || ""; - if (printMeta) { - const ver = data.version ? ` · ${data.version}` : ""; - printMeta.textContent = `${data.label || activeKey}${ver}`; - } - if (printHeader) printHeader.classList.remove("hidden"); scheduleHeightSync(); } @@ -154,22 +143,12 @@ } function printSection(mode) { - document.body.classList.remove("hub-strategy-print-doc", "hub-strategy-print-checklist"); - document.body.classList.add("hub-strategy-printing"); - if (mode === "doc") document.body.classList.add("hub-strategy-print-doc"); - if (mode === "checklist") document.body.classList.add("hub-strategy-print-checklist"); - window.print(); - window.addEventListener( - "afterprint", - () => { - document.body.classList.remove( - "hub-strategy-printing", - "hub-strategy-print-doc", - "hub-strategy-print-checklist" - ); - }, - { once: true } - ); + const part = mode === "checklist" ? "checklist" : "doc"; + const url = `/api/strategy/${encodeURIComponent(activeKey)}/print?part=${encodeURIComponent(part)}`; + const w = window.open(url, "_blank", "noopener,noreferrer"); + if (!w) { + if (statusEl) statusEl.textContent = "请允许弹出窗口以打开打印预览"; + } } function bindActions() { diff --git a/tests/test_hub_strategy_lib.py b/tests/test_hub_strategy_lib.py index 8475fca..3f3ab79 100644 --- a/tests/test_hub_strategy_lib.py +++ b/tests/test_hub_strategy_lib.py @@ -7,6 +7,7 @@ from lib.hub.hub_strategy_lib import ( load_strategy_payload, strategy_meta_payload, build_export_html, + build_print_html, ) @@ -34,6 +35,15 @@ class TestHubStrategyLib(unittest.TestCase): self.assertIn("Gate", html) self.assertIn("☐", html) + def test_print_html_doc_and_checklist(self): + doc = build_print_html("binance", "doc") + cl = build_print_html("binance", "checklist") + self.assertIn("window.print", doc) + self.assertIn("window.print", cl) + self.assertIn("币安", doc) + self.assertIn("开仓检查清单", cl) + self.assertIn("☐", cl) + if __name__ == "__main__": unittest.main()