Fix strategy checklist print closing too early.
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>
This commit is contained in:
+40
-18
@@ -218,16 +218,22 @@ def strategy_meta_payload() -> dict[str, Any]:
|
|||||||
return {"ok": True, "exchanges": tabs}
|
return {"ok": True, "exchanges": tabs}
|
||||||
|
|
||||||
|
|
||||||
def _checklist_html(checklist: dict[str, Any]) -> str:
|
def _checklist_html(checklist: dict[str, Any], *, include_title: bool = True) -> str:
|
||||||
groups = checklist.get("groups") or []
|
groups = checklist.get("groups") or []
|
||||||
parts = [f"<h2>{escape_html(str(checklist.get('title') or '开仓检查清单'))}</h2>"]
|
parts: list[str] = []
|
||||||
|
if include_title:
|
||||||
|
parts.append(f"<h2>{escape_html(str(checklist.get('title') or '开仓检查清单'))}</h2>")
|
||||||
for grp in groups:
|
for grp in groups:
|
||||||
if not isinstance(grp, dict):
|
if not isinstance(grp, dict):
|
||||||
continue
|
continue
|
||||||
gtitle = escape_html(str(grp.get("title") or ""))
|
gtitle = escape_html(str(grp.get("title") or ""))
|
||||||
parts.append(f"<h3>{gtitle}</h3><ul class=\"checklist\">")
|
parts.append(f"<h3>{gtitle}</h3><ul class=\"checklist\">")
|
||||||
for item in grp.get("items") or []:
|
for item in grp.get("items") or []:
|
||||||
parts.append(f"<li><span class=\"box\" aria-hidden=\"true\">☐</span> {escape_html(str(item))}</li>")
|
# 用 CSS 方框代替 ☐,避免部分打印机/预览吞掉 Unicode 勾选符
|
||||||
|
parts.append(
|
||||||
|
f"<li><span class=\"box\" aria-hidden=\"true\"></span>"
|
||||||
|
f"<span class=\"item-text\">{escape_html(str(item))}</span></li>"
|
||||||
|
)
|
||||||
parts.append("</ul>")
|
parts.append("</ul>")
|
||||||
footnotes = checklist.get("footnotes") or []
|
footnotes = checklist.get("footnotes") or []
|
||||||
if footnotes:
|
if footnotes:
|
||||||
@@ -330,15 +336,28 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.checklist li {
|
.checklist li {
|
||||||
margin: 7px 0;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.box {
|
.box {
|
||||||
|
flex: 0 0 auto;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.05em;
|
width: 0.95em;
|
||||||
margin-right: 6px;
|
height: 0.95em;
|
||||||
font-size: 1.05em;
|
margin-top: 0.2em;
|
||||||
line-height: 1.2;
|
border: 1.5px solid #333;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
.item-text {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.footnotes {
|
.footnotes {
|
||||||
margin: 18px 0 0;
|
margin: 18px 0 0;
|
||||||
@@ -364,16 +383,18 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
def _print_auto_script() -> str:
|
def _print_auto_script() -> str:
|
||||||
|
# 不在 afterprint 里 window.close(): Firefox 会在打开打印框时就触发 afterprint,
|
||||||
|
# 短页(检查清单)更容易被立刻关掉,表现为「打不开/打不出来」;策略长文相对不易复现.
|
||||||
return """<script>
|
return """<script>
|
||||||
(function () {
|
(function () {
|
||||||
function done() {
|
function go() {
|
||||||
try { window.close(); } catch (e) {}
|
try { window.focus(); } catch (e) {}
|
||||||
|
window.setTimeout(function () {
|
||||||
|
try { window.print(); } catch (e) {}
|
||||||
|
}, 250);
|
||||||
}
|
}
|
||||||
window.addEventListener("afterprint", done, { once: true });
|
if (document.readyState === "complete") go();
|
||||||
window.addEventListener("load", function () {
|
else window.addEventListener("load", go, { once: true });
|
||||||
window.setTimeout(function () { window.print(); }, 120);
|
|
||||||
window.setTimeout(done, 60000);
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
</script>"""
|
</script>"""
|
||||||
|
|
||||||
@@ -398,7 +419,7 @@ def build_print_html(exchange_key: str, part: str = "doc") -> str:
|
|||||||
<h1>{escape_html(cl_title)}</h1>
|
<h1>{escape_html(cl_title)}</h1>
|
||||||
<p class="doc-meta">{escape_html(label)} · {escape_html(version)} · 打印 {escape_html(now)}</p>
|
<p class="doc-meta">{escape_html(label)} · {escape_html(version)} · 打印 {escape_html(now)}</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="doc-body">{_checklist_html(checklist)}</div>
|
<div class="doc-body">{_checklist_html(checklist, include_title=False)}</div>
|
||||||
</article>"""
|
</article>"""
|
||||||
elif part == "doc":
|
elif part == "doc":
|
||||||
page_title = f"{label} · 策略说明"
|
page_title = f"{label} · 策略说明"
|
||||||
@@ -453,8 +474,9 @@ h1{{font-size:1.35rem;margin:0 0 8px}}
|
|||||||
.strategy-doc table{{border-collapse:collapse;width:100%;font-size:.88rem}}
|
.strategy-doc table{{border-collapse:collapse;width:100%;font-size:.88rem}}
|
||||||
.strategy-doc th,.strategy-doc td{{border:1px solid #ccc;padding:6px 8px;text-align:left}}
|
.strategy-doc th,.strategy-doc td{{border:1px solid #ccc;padding:6px 8px;text-align:left}}
|
||||||
.checklist ul{{list-style:none;padding:0;margin:0}}
|
.checklist ul{{list-style:none;padding:0;margin:0}}
|
||||||
.checklist li{{margin:8px 0;padding-left:0}}
|
.checklist li{{display:flex;align-items:flex-start;gap:8px;margin:8px 0}}
|
||||||
.box{{display:inline-block;width:1em;margin-right:6px}}
|
.box{{flex:0 0 auto;display:inline-block;width:.95em;height:.95em;margin-top:.2em;border:1.5px solid #333;border-radius:2px;background:#fff;box-sizing:border-box}}
|
||||||
|
.item-text{{flex:1 1 auto}}
|
||||||
@media print{{body{{margin:12mm}}}}
|
@media print{{body{{margin:12mm}}}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1374,7 +1374,7 @@
|
|||||||
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
|
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
|
||||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
||||||
<script src="/assets/dashboard.js?v=20260717-dash-kpi-merge"></script>
|
<script src="/assets/dashboard.js?v=20260717-dash-kpi-merge"></script>
|
||||||
<script src="/assets/strategy.js?v=3"></script>
|
<script src="/assets/strategy.js?v=4"></script>
|
||||||
<script src="/assets/help.js?v=1"></script>
|
<script src="/assets/help.js?v=1"></script>
|
||||||
<script src="/assets/logs.js?v=1"></script>
|
<script src="/assets/logs.js?v=1"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||||
|
|||||||
@@ -142,12 +142,41 @@
|
|||||||
renderTabs();
|
renderTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
function printSection(mode) {
|
async function printSection(mode) {
|
||||||
const part = mode === "checklist" ? "checklist" : "doc";
|
const part = mode === "checklist" ? "checklist" : "doc";
|
||||||
const url = `/api/strategy/${encodeURIComponent(activeKey)}/print?part=${encodeURIComponent(part)}`;
|
const url = `/api/strategy/${encodeURIComponent(activeKey)}/print?part=${encodeURIComponent(part)}`;
|
||||||
const w = window.open(url, "_blank", "noopener,noreferrer");
|
// 同步打开空白页保留用户手势;再 fetch 写入,避免 noopener 丢句柄 / 短页误关窗
|
||||||
|
const w = window.open("about:blank", "_blank");
|
||||||
if (!w) {
|
if (!w) {
|
||||||
if (statusEl) statusEl.textContent = "请允许弹出窗口以打开打印预览";
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,16 +33,19 @@ class TestHubStrategyLib(unittest.TestCase):
|
|||||||
def test_export_html_contains_checklist(self):
|
def test_export_html_contains_checklist(self):
|
||||||
html = build_export_html("gate")
|
html = build_export_html("gate")
|
||||||
self.assertIn("Gate", html)
|
self.assertIn("Gate", html)
|
||||||
self.assertIn("☐", html)
|
self.assertIn('class="box"', html)
|
||||||
|
self.assertNotIn("☐", html)
|
||||||
|
|
||||||
def test_print_html_doc_and_checklist(self):
|
def test_print_html_doc_and_checklist(self):
|
||||||
doc = build_print_html("binance", "doc")
|
doc = build_print_html("binance", "doc")
|
||||||
cl = build_print_html("binance", "checklist")
|
cl = build_print_html("binance", "checklist")
|
||||||
self.assertIn("window.print", doc)
|
self.assertIn("window.print", doc)
|
||||||
self.assertIn("window.print", cl)
|
self.assertIn("window.print", cl)
|
||||||
|
self.assertNotIn("window.close", cl)
|
||||||
self.assertIn("币安", doc)
|
self.assertIn("币安", doc)
|
||||||
self.assertIn("开仓检查清单", cl)
|
self.assertIn("开仓检查清单", cl)
|
||||||
self.assertIn("☐", cl)
|
self.assertIn('class="box"', cl)
|
||||||
|
self.assertIn("本账户仅做多", cl)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user