From 72dddc5106559de065c7815a73ecf7f9e222b2c7 Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 19 Jul 2026 16:23:37 +0800 Subject: [PATCH] Fix options review screenshots missing after journal upload hijack. Stop journal_upload_slots from binding options slots; resolve journal_* files from static/images root so existing reviews display again. Co-authored-by: Cursor --- lib/common/static/journal_upload_slots.js | 8 ++++++ lib/common/static/options_review.js | 23 +++++++++++++++-- lib/instance/templates/embed_shell.html | 2 +- lib/instance/templates/index.html | 2 +- lib/options/options_review_images_lib.py | 16 ++++++++---- lib/options/options_review_register.py | 8 +++++- .../templates/options_review_panel.html | 12 ++++++--- tests/test_options_review_lib.py | 25 +++++++++++++++++++ 8 files changed, 82 insertions(+), 14 deletions(-) diff --git a/lib/common/static/journal_upload_slots.js b/lib/common/static/journal_upload_slots.js index 8e43422..5153985 100644 --- a/lib/common/static/journal_upload_slots.js +++ b/lib/common/static/journal_upload_slots.js @@ -93,8 +93,16 @@ }); } + function isOptionsReviewSlot(input) { + if (!input) return false; + if (input.classList && input.classList.contains("or-upload-input")) return true; + return !!(input.closest && input.closest("#or-upload-slots, #options-review-root")); + } + function bindInput(input) { if (!input || input.dataset.journalSlotBound === "1") return; + // 期权复盘槽位由 options_review.js 处理,勿被合约复盘上传抢走 + if (isOptionsReviewSlot(input)) return; input.dataset.journalSlotBound = "1"; input.addEventListener("change", function () { var file = input.files && input.files[0]; diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js index 2c6ec72..a963eba 100644 --- a/lib/common/static/options_review.js +++ b/lib/common/static/options_review.js @@ -498,7 +498,12 @@ var slash = name.lastIndexOf("/"); if (slash >= 0) name = name.slice(slash + 1); if (!name) return ""; - return "/static/images/options_journal/" + encodeURIComponent(name); + // options_journal_* 在子目录;误走合约上传的 journal_* 在 static/images 根目录 + var base = + name.toLowerCase().indexOf("options_journal_") === 0 + ? "/static/images/options_journal/" + : "/static/images/"; + return base + encodeURIComponent(name); } function renderDetailImages(images) { @@ -940,7 +945,21 @@ ); if (hidden && img.file) { hidden.value = img.file; - if (status) status.textContent = "已有 " + img.file; + if (status) { + var src = optionsJournalImgSrc(img.file); + status.innerHTML = + '已有 ' + + escapeHtml(img.file) + + '
' +
+            escapeHtml(img.tf || '; + status.className = + "journal-upload-status or-upload-status journal-upload-status--ok"; + } } }); diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 53c910d..4368857 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -93,7 +93,7 @@ - + diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index ed256b1..2a766aa 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -456,7 +456,7 @@ - + diff --git a/lib/options/options_review_images_lib.py b/lib/options/options_review_images_lib.py index b1845a8..a0d0545 100644 --- a/lib/options/options_review_images_lib.py +++ b/lib/options/options_review_images_lib.py @@ -112,17 +112,23 @@ def images_json_dumps(items: Sequence[Mapping[str, str]]) -> Optional[str]: def options_review_image_paths(row: Any, upload_folder: str) -> List[str]: - upload_folder = os.path.abspath(upload_folder or "") + upload_root = os.path.abspath(upload_folder or "") + options_dir = options_review_upload_dir(upload_root) paths: List[str] = [] seen: set[str] = set() def _add(name: Optional[str]) -> None: if not name: return - p = os.path.abspath(os.path.join(upload_folder, str(name).strip())) - if os.path.isfile(p) and p not in seen: - seen.add(p) - paths.append(p) + base = os.path.basename(str(name).strip()) + if not base: + return + for folder in (options_dir, upload_root): + p = os.path.abspath(os.path.join(folder, base)) + if os.path.isfile(p) and p not in seen: + seen.add(p) + paths.append(p) + return try: keys = row.keys() if hasattr(row, "keys") else () diff --git a/lib/options/options_review_register.py b/lib/options/options_review_register.py index c73dbef..4dffe73 100644 --- a/lib/options/options_review_register.py +++ b/lib/options/options_review_register.py @@ -100,7 +100,13 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s safe = os.path.basename(filename or "") path = os.path.join(folder, safe) if not os.path.isfile(path): - return ("not found", 404) + # 兼容误走合约 journal 上传、落在 UPLOAD_FOLDER 根目录的文件 + root = os.path.abspath(cfg["upload_folder"] or "") + alt = os.path.join(root, safe) + if os.path.isfile(alt): + path = alt + else: + return ("not found", 404) return send_file(path) @app.route("/api/options/review/sync", methods=["POST"]) diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index 73135c5..13c7aca 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -68,6 +68,11 @@ .or-pager{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:.74rem} .or-list-loading{opacity:.55;pointer-events:none;transition:opacity .12s ease} .or-trades-table-wrap,.or-reviewed-table-wrap{min-height:9.5rem} + .or-slot-thumb{ + display:block;margin-top:6px;max-width:160px;max-height:90px; + object-fit:contain;border-radius:4px;border:1px solid rgba(127,127,127,.3); + background:rgba(0,0,0,.2);cursor:zoom-in; + } {# 1. 交易记录(含 Tab/筛选,固定约5行) #} @@ -176,13 +181,12 @@ -
{% for tf in ['5m', '15m', '1h', '4h'] %}
{{ tf }} - - + +
{% endfor %} @@ -261,4 +265,4 @@
- + diff --git a/tests/test_options_review_lib.py b/tests/test_options_review_lib.py index 13cd123..5f0f5ee 100644 --- a/tests/test_options_review_lib.py +++ b/tests/test_options_review_lib.py @@ -284,6 +284,31 @@ class OptionsReviewTests(unittest.TestCase): self.assertEqual(float(row["realized_pnl_total"]), 3.2) self.assertEqual(row["source_type"], SOURCE_OPTION) + def test_image_paths_resolve_legacy_journal_root(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + # 误存到 UPLOAD 根目录的 journal_* + legacy = root / "journal_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_5m.png" + legacy.write_bytes(b"img") + # 正常 options_journal 子目录 + sub = root / "options_journal" + sub.mkdir() + modern = sub / "options_journal_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_5m.png" + modern.write_bytes(b"img2") + from lib.options.options_review_images_lib import options_review_image_paths + + class Row: + images_json = ( + '[{"tf":"5m","file":"journal_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_5m.png"},' + '{"tf":"5m","file":"options_journal_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_5m.png"}]' + ) + image = None + + paths = options_review_image_paths(Row(), str(root)) + self.assertEqual(len(paths), 2) + self.assertTrue(any(p.endswith(legacy.name) for p in paths)) + self.assertTrue(any(p.endswith(modern.name) for p in paths)) + def test_image_namespace(self): with tempfile.TemporaryDirectory() as tmp: folder = options_review_upload_dir(tmp)