diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index e6bd72e..7fb3f6e 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -9352,29 +9352,28 @@ def del_order(id): @app.route("/add_journal", methods=["POST"]) @login_required def add_journal(): + from lib.instance.journal_chart_async_lib import journal_ajax_or_flash_error + d = request.form order_type_norm = normalize_journal_order_type(d.get("order_type")) if not order_type_norm: - flash("请选择下单类型") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择下单类型", redirect_fn=_redirect_records) direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint")) if not direction_norm: - flash("请选择方向") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择方向", redirect_fn=_redirect_records) entry_reason_norm = normalize_journal_entry_reason( d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False ) if not entry_reason_norm: - flash("请选择开仓类型") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择开仓类型", redirect_fn=_redirect_records) early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger")) early_exit_note = str(d.get("early_exit_note") or "").strip() if not early_exit_trigger: - flash("请选择离场触发") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择离场触发", redirect_fn=_redirect_records) if early_exit_trigger == "手动平仓" and not early_exit_note: - flash("手工平仓必须填写补充说明") - return _redirect_records() + return journal_ajax_or_flash_error( + request, "手工平仓必须填写补充说明", redirect_fn=_redirect_records + ) if early_exit_trigger != "手动平仓": early_exit_note = "" # 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」 @@ -9407,14 +9406,14 @@ def add_journal(): want_exchange_chart = ( not has_manual_uploads + and ORDER_CHART_ENABLED and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes") ) - chart_msg = None - if want_exchange_chart and ORDER_CHART_ENABLED: + chart_job = None + if want_exchange_chart: coin = (d.get("coin") or "").strip().upper() symbol_guess = normalize_symbol_input(coin) or coin exchange_symbol = normalize_exchange_symbol(symbol_guess) - title_prefix = f"{symbol_guess} journal {entry_id[:8]}" journal_tfs = parse_journal_chart_timeframes( d.get("journal_chart_tf1"), d.get("journal_chart_tf2"), @@ -9422,36 +9421,21 @@ def add_journal(): ) journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT) chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor")) - marker_payload = { - "entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")), - "exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")), - "entry_price": d.get("entry_price_hint"), - "exit_price": d.get("exit_price_hint"), - "stop_loss_price": d.get("stop_loss_hint"), - "chart_anchor": chart_anchor, - "now_ts_ms": int(app_now().timestamp() * 1000), + chart_job = { + "exchange_symbol": exchange_symbol, + "title_prefix": f"{symbol_guess} journal {entry_id[:8]}", + "journal_tfs": journal_tfs, + "journal_limit": journal_limit, + "marker_payload": { + "entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")), + "exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")), + "entry_price": d.get("entry_price_hint"), + "exit_price": d.get("exit_price_hint"), + "stop_loss_price": d.get("stop_loss_hint"), + "chart_anchor": chart_anchor, + "now_ts_ms": int(app_now().timestamp() * 1000), + }, } - try: - chart_fname = f"journal_{entry_id}.png" - saved = generate_multi_timeframe_chart_png( - exchange_symbol, - title_prefix, - timeframes=journal_tfs, - limit=journal_limit, - out_dir=app.config["UPLOAD_FOLDER"], - filename=chart_fname, - filename_prefix="journal", - marker_payload=marker_payload, - marker_timeframes={x.strip().lower() for x in journal_tfs}, - layout="vertical", - ) - if saved: - image_filename = saved - chart_msg = f"已生成复盘K线图({'/'.join(journal_tfs)} 各{journal_limit}根):/static/images/{saved}" - else: - chart_msg = "已勾选自动生成K线图,但生成失败(返回空).请检查 Pillow 是否安装,Binance 网络/代理是否正常." - except Exception as e: - chart_msg = f"自动生成K线图失败:{str(e)}" conn = get_db() conn.execute( @@ -9488,10 +9472,38 @@ def add_journal(): ) conn.commit() conn.close() - if chart_msg: - flash(f"交易复盘记录已保存.{chart_msg}") + if chart_job: + from lib.instance.journal_chart_async_lib import schedule_journal_exchange_chart + + schedule_journal_exchange_chart( + entry_id=entry_id, + exchange_symbol=chart_job["exchange_symbol"], + title_prefix=chart_job["title_prefix"], + journal_tfs=chart_job["journal_tfs"], + journal_limit=chart_job["journal_limit"], + marker_payload=chart_job["marker_payload"], + upload_folder=app.config["UPLOAD_FOLDER"], + generate_chart_fn=generate_multi_timeframe_chart_png, + get_db_fn=get_db, + ) + msg = ( + "交易复盘记录已保存.K 线图后台生成中" + f"({'/'.join(chart_job['journal_tfs'])} 各{chart_job['journal_limit']}根),稍后刷新列表可见." + ) else: - flash("交易复盘记录已保存") + msg = "交易复盘记录已保存" + from lib.instance.journal_chart_async_lib import request_wants_journal_ajax + + if request_wants_journal_ajax(request): + return jsonify( + { + "ok": True, + "id": entry_id, + "msg": msg, + "chart_pending": bool(chart_job), + } + ) + flash(msg) return _redirect_records() diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index e8c8ca0..07b89af 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -9190,29 +9190,28 @@ def del_order(id): @app.route("/add_journal", methods=["POST"]) @login_required def add_journal(): + from lib.instance.journal_chart_async_lib import journal_ajax_or_flash_error + d = request.form order_type_norm = normalize_journal_order_type(d.get("order_type")) if not order_type_norm: - flash("请选择下单类型") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择下单类型", redirect_fn=_redirect_records) direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint")) if not direction_norm: - flash("请选择方向") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择方向", redirect_fn=_redirect_records) entry_reason_norm = normalize_journal_entry_reason( d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False ) if not entry_reason_norm: - flash("请选择开仓类型") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择开仓类型", redirect_fn=_redirect_records) early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger")) early_exit_note = str(d.get("early_exit_note") or "").strip() if not early_exit_trigger: - flash("请选择离场触发") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择离场触发", redirect_fn=_redirect_records) if early_exit_trigger == "手动平仓" and not early_exit_note: - flash("手工平仓必须填写补充说明") - return _redirect_records() + return journal_ajax_or_flash_error( + request, "手工平仓必须填写补充说明", redirect_fn=_redirect_records + ) if early_exit_trigger != "手动平仓": early_exit_note = "" # 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」 @@ -9245,14 +9244,14 @@ def add_journal(): want_exchange_chart = ( not has_manual_uploads + and ORDER_CHART_ENABLED and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes") ) - chart_msg = None - if want_exchange_chart and ORDER_CHART_ENABLED: + chart_job = None + if want_exchange_chart: coin = (d.get("coin") or "").strip().upper() symbol_guess = normalize_symbol_input(coin) or coin exchange_symbol = normalize_exchange_symbol(symbol_guess) - title_prefix = f"{symbol_guess} journal {entry_id[:8]}" journal_tfs = parse_journal_chart_timeframes( d.get("journal_chart_tf1"), d.get("journal_chart_tf2"), @@ -9260,36 +9259,21 @@ def add_journal(): ) journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT) chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor")) - marker_payload = { - "entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")), - "exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")), - "entry_price": d.get("entry_price_hint"), - "exit_price": d.get("exit_price_hint"), - "stop_loss_price": d.get("stop_loss_hint"), - "chart_anchor": chart_anchor, - "now_ts_ms": int(app_now().timestamp() * 1000), + chart_job = { + "exchange_symbol": exchange_symbol, + "title_prefix": f"{symbol_guess} journal {entry_id[:8]}", + "journal_tfs": journal_tfs, + "journal_limit": journal_limit, + "marker_payload": { + "entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")), + "exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")), + "entry_price": d.get("entry_price_hint"), + "exit_price": d.get("exit_price_hint"), + "stop_loss_price": d.get("stop_loss_hint"), + "chart_anchor": chart_anchor, + "now_ts_ms": int(app_now().timestamp() * 1000), + }, } - try: - chart_fname = f"journal_{entry_id}.png" - saved = generate_multi_timeframe_chart_png( - exchange_symbol, - title_prefix, - timeframes=journal_tfs, - limit=journal_limit, - out_dir=app.config["UPLOAD_FOLDER"], - filename=chart_fname, - filename_prefix="journal", - marker_payload=marker_payload, - marker_timeframes={x.strip().lower() for x in journal_tfs}, - layout="vertical", - ) - if saved: - image_filename = saved - chart_msg = f"已生成复盘K线图({'/'.join(journal_tfs)} 各{journal_limit}根):/static/images/{saved}" - else: - chart_msg = "已勾选自动生成K线图,但生成失败(返回空).请检查 Pillow 是否安装,Gate 网络/代理是否正常." - except Exception as e: - chart_msg = f"自动生成K线图失败:{str(e)}" conn = get_db() conn.execute( @@ -9326,10 +9310,38 @@ def add_journal(): ) conn.commit() conn.close() - if chart_msg: - flash(f"交易复盘记录已保存.{chart_msg}") + if chart_job: + from lib.instance.journal_chart_async_lib import schedule_journal_exchange_chart + + schedule_journal_exchange_chart( + entry_id=entry_id, + exchange_symbol=chart_job["exchange_symbol"], + title_prefix=chart_job["title_prefix"], + journal_tfs=chart_job["journal_tfs"], + journal_limit=chart_job["journal_limit"], + marker_payload=chart_job["marker_payload"], + upload_folder=app.config["UPLOAD_FOLDER"], + generate_chart_fn=generate_multi_timeframe_chart_png, + get_db_fn=get_db, + ) + msg = ( + "交易复盘记录已保存.K 线图后台生成中" + f"({'/'.join(chart_job['journal_tfs'])} 各{chart_job['journal_limit']}根),稍后刷新列表可见." + ) else: - flash("交易复盘记录已保存") + msg = "交易复盘记录已保存" + from lib.instance.journal_chart_async_lib import request_wants_journal_ajax + + if request_wants_journal_ajax(request): + return jsonify( + { + "ok": True, + "id": entry_id, + "msg": msg, + "chart_pending": bool(chart_job), + } + ) + flash(msg) return _redirect_records() diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 4c678fb..36ba081 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -8857,29 +8857,28 @@ def del_order(id): @app.route("/add_journal", methods=["POST"]) @login_required def add_journal(): + from lib.instance.journal_chart_async_lib import journal_ajax_or_flash_error + d = request.form order_type_norm = normalize_journal_order_type(d.get("order_type")) if not order_type_norm: - flash("请选择下单类型") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择下单类型", redirect_fn=_redirect_records) direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint")) if not direction_norm: - flash("请选择方向") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择方向", redirect_fn=_redirect_records) entry_reason_norm = normalize_journal_entry_reason( d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False ) if not entry_reason_norm: - flash("请选择开仓类型") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择开仓类型", redirect_fn=_redirect_records) early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger")) early_exit_note = str(d.get("early_exit_note") or "").strip() if not early_exit_trigger: - flash("请选择离场触发") - return _redirect_records() + return journal_ajax_or_flash_error(request, "请选择离场触发", redirect_fn=_redirect_records) if early_exit_trigger == "手动平仓" and not early_exit_note: - flash("手工平仓必须填写补充说明") - return _redirect_records() + return journal_ajax_or_flash_error( + request, "手工平仓必须填写补充说明", redirect_fn=_redirect_records + ) if early_exit_trigger != "手动平仓": early_exit_note = "" # 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」 @@ -8912,14 +8911,14 @@ def add_journal(): want_exchange_chart = ( not has_manual_uploads + and ORDER_CHART_ENABLED and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes") ) - chart_msg = None - if want_exchange_chart and ORDER_CHART_ENABLED: + chart_job = None + if want_exchange_chart: coin = (d.get("coin") or "").strip().upper() symbol_guess = normalize_symbol_input(coin) or coin exchange_symbol = normalize_okx_symbol(symbol_guess) - title_prefix = f"{symbol_guess} journal {entry_id[:8]}" journal_tfs = parse_journal_chart_timeframes( d.get("journal_chart_tf1"), d.get("journal_chart_tf2"), @@ -8927,36 +8926,21 @@ def add_journal(): ) journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT) chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor")) - marker_payload = { - "entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")), - "exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")), - "entry_price": d.get("entry_price_hint"), - "exit_price": d.get("exit_price_hint"), - "stop_loss_price": d.get("stop_loss_hint"), - "chart_anchor": chart_anchor, - "now_ts_ms": int(app_now().timestamp() * 1000), + chart_job = { + "exchange_symbol": exchange_symbol, + "title_prefix": f"{symbol_guess} journal {entry_id[:8]}", + "journal_tfs": journal_tfs, + "journal_limit": journal_limit, + "marker_payload": { + "entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")), + "exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")), + "entry_price": d.get("entry_price_hint"), + "exit_price": d.get("exit_price_hint"), + "stop_loss_price": d.get("stop_loss_hint"), + "chart_anchor": chart_anchor, + "now_ts_ms": int(app_now().timestamp() * 1000), + }, } - try: - chart_fname = f"journal_{entry_id}.png" - saved = generate_multi_timeframe_chart_png( - exchange_symbol, - title_prefix, - timeframes=journal_tfs, - limit=journal_limit, - out_dir=app.config["UPLOAD_FOLDER"], - filename=chart_fname, - filename_prefix="journal", - marker_payload=marker_payload, - marker_timeframes={x.strip().lower() for x in journal_tfs}, - layout="vertical", - ) - if saved: - image_filename = saved - chart_msg = f"已生成复盘K线图({'/'.join(journal_tfs)} 各{journal_limit}根):/static/images/{saved}" - else: - chart_msg = "已勾选自动生成K线图,但生成失败(返回空).请检查 Pillow 是否安装,OKX 网络/代理是否正常." - except Exception as e: - chart_msg = f"自动生成K线图失败:{str(e)}" conn = get_db() conn.execute( @@ -8993,10 +8977,38 @@ def add_journal(): ) conn.commit() conn.close() - if chart_msg: - flash(f"交易复盘记录已保存.{chart_msg}") + if chart_job: + from lib.instance.journal_chart_async_lib import schedule_journal_exchange_chart + + schedule_journal_exchange_chart( + entry_id=entry_id, + exchange_symbol=chart_job["exchange_symbol"], + title_prefix=chart_job["title_prefix"], + journal_tfs=chart_job["journal_tfs"], + journal_limit=chart_job["journal_limit"], + marker_payload=chart_job["marker_payload"], + upload_folder=app.config["UPLOAD_FOLDER"], + generate_chart_fn=generate_multi_timeframe_chart_png, + get_db_fn=get_db, + ) + msg = ( + "交易复盘记录已保存.K 线图后台生成中" + f"({'/'.join(chart_job['journal_tfs'])} 各{chart_job['journal_limit']}根),稍后刷新列表可见." + ) else: - flash("交易复盘记录已保存") + msg = "交易复盘记录已保存" + from lib.instance.journal_chart_async_lib import request_wants_journal_ajax + + if request_wants_journal_ajax(request): + return jsonify( + { + "ok": True, + "id": entry_id, + "msg": msg, + "chart_pending": bool(chart_job), + } + ) + flash(msg) return _redirect_records() diff --git a/lib/common/static/journal_form_save.js b/lib/common/static/journal_form_save.js new file mode 100644 index 0000000..822ec09 --- /dev/null +++ b/lib/common/static/journal_form_save.js @@ -0,0 +1,142 @@ +/** + * 复盘表单 AJAX 保存:避免整页刷新卡顿;配合 FormSubmitGuard 即时反馈. + */ +(function (global) { + "use strict"; + + function $(id) { + return document.getElementById(id); + } + + function toast(msg) { + if (!msg) return; + try { + if (global.InstanceTheme && typeof InstanceTheme.toast === "function") { + InstanceTheme.toast(msg); + return; + } + } catch (_) {} + try { + alert(msg); + } catch (_) {} + } + + function refreshLists() { + if (typeof global.loadJournals === "function") { + try { + global.loadJournals(); + } catch (_) {} + } else if (global.RecordsReviewPage && typeof RecordsReviewPage.loadJournals === "function") { + try { + RecordsReviewPage.loadJournals(); + } catch (_) {} + } + } + + function resetJournalForm(form) { + if (!form) return; + form.reset(); + ["risk-amount-hint", "entry-price-hint", "stop-loss-hint", "exit-price-hint", "direction-hint"].forEach( + function (id) { + var el = $(id); + if (el) el.value = ""; + } + ); + if (global.JournalUploadSlots && typeof JournalUploadSlots.reset === "function") { + JournalUploadSlots.reset(form); + } + if (typeof global.syncEarlyExitNoteRequired === "function") { + try { + global.syncEarlyExitNoteRequired(); + } catch (_) {} + } + if (global.RecordsReviewPage && typeof RecordsReviewPage.hideJournalCard === "function") { + try { + RecordsReviewPage.hideJournalCard(); + } catch (_) {} + } + } + + function onSuccess(form, data) { + var msg = (data && data.msg) || "交易复盘记录已保存"; + toast(msg); + resetJournalForm(form); + refreshLists(); + if (data && data.chart_pending) { + // K 线后台生成,数秒后再刷一次列表拿缩略图 + setTimeout(refreshLists, 4000); + setTimeout(refreshLists, 12000); + } + } + + function bind(form) { + if (!form || form.dataset.journalAjaxBound === "1") return; + form.dataset.journalAjaxBound = "1"; + form.addEventListener("submit", function (ev) { + if (typeof global.validateJournalEntryReason === "function") { + if (!global.validateJournalEntryReason()) { + ev.preventDefault(); + return; + } + } + ev.preventDefault(); + if (global.FormSubmitGuard && FormSubmitGuard.isLocked(form)) return; + if (global.FormSubmitGuard) FormSubmitGuard.lock(form, "保存中…"); + + var fd = new FormData(form); + fd.set("ajax", "1"); + var action = form.getAttribute("action") || "/add_journal"; + + fetch(action, { + method: "POST", + body: fd, + credentials: "same-origin", + headers: { + "X-Requested-With": "XMLHttpRequest", + Accept: "application/json", + }, + }) + .then(function (res) { + var ct = (res.headers.get("content-type") || "").toLowerCase(); + if (ct.indexOf("application/json") >= 0) { + return res.json().then(function (data) { + return { ok: res.ok, data: data, redirected: false }; + }); + } + // 旧后端未返回 JSON 时走整页逻辑 + if (res.redirected || res.ok) { + global.location.reload(); + return { ok: true, data: null, redirected: true }; + } + throw new Error("save failed"); + }) + .then(function (result) { + if (result.redirected) return; + if (!result.ok || !result.data || result.data.ok === false) { + var err = + (result.data && (result.data.msg || result.data.error)) || "保存失败"; + throw new Error(err); + } + onSuccess(form, result.data); + }) + .catch(function (err) { + toast((err && err.message) || "保存失败,请稍后重试"); + }) + .finally(function () { + if (global.FormSubmitGuard) FormSubmitGuard.unlock(form); + }); + }); + } + + function init() { + bind($("journal-form")); + } + + global.JournalFormSave = { init: init, bind: bind }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(typeof window !== "undefined" ? window : globalThis); diff --git a/lib/hub/hub_bridge.py b/lib/hub/hub_bridge.py index 8f682e5..4cfb1a5 100644 --- a/lib/hub/hub_bridge.py +++ b/lib/hub/hub_bridge.py @@ -66,6 +66,7 @@ def install_instance_theme_static(app) -> None: "manual_order_rr_preview.js": "application/javascript; charset=utf-8", "symbol_live_price.js": "application/javascript; charset=utf-8", "journal_upload_slots.js": "application/javascript; charset=utf-8", + "journal_form_save.js": "application/javascript; charset=utf-8", "strategy_roll.js": "application/javascript; charset=utf-8", "instance_page.css": "text/css; charset=utf-8", "instance_embed.js": "application/javascript; charset=utf-8", diff --git a/lib/instance/journal_chart_async_lib.py b/lib/instance/journal_chart_async_lib.py new file mode 100644 index 0000000..bdd01d1 --- /dev/null +++ b/lib/instance/journal_chart_async_lib.py @@ -0,0 +1,93 @@ +"""复盘自动 K 线:后台线程生成,避免 /add_journal 同步卡住.""" + +from __future__ import annotations + +import logging +import threading +from typing import Any, Callable + +logger = logging.getLogger(__name__) + + +def schedule_journal_exchange_chart( + *, + entry_id: str, + exchange_symbol: str, + title_prefix: str, + journal_tfs: list[str], + journal_limit: int, + marker_payload: dict[str, Any], + upload_folder: str, + generate_chart_fn: Callable[..., str | None], + get_db_fn: Callable[[], Any], +) -> None: + """提交后立即返回;线程内画图并写回 journal_entries.image.""" + entry_id = str(entry_id or "").strip() + if not entry_id or not callable(generate_chart_fn) or not callable(get_db_fn): + return + + tfs = [str(x).strip() for x in (journal_tfs or []) if str(x).strip()] + if not tfs: + return + + def _run() -> None: + try: + chart_fname = f"journal_{entry_id}.png" + saved = generate_chart_fn( + exchange_symbol, + title_prefix, + timeframes=tfs, + limit=journal_limit, + out_dir=upload_folder, + filename=chart_fname, + filename_prefix="journal", + marker_payload=marker_payload, + marker_timeframes={x.lower() for x in tfs}, + layout="vertical", + ) + if not saved: + logger.warning("journal chart async empty entry_id=%s", entry_id) + return + conn = get_db_fn() + try: + conn.execute( + "UPDATE journal_entries SET image=? WHERE id=?", + (saved, entry_id), + ) + conn.commit() + finally: + conn.close() + except Exception: + logger.exception("journal chart async failed entry_id=%s", entry_id) + + threading.Thread( + target=_run, + name=f"journal-chart-{entry_id[:8]}", + daemon=True, + ).start() + + +def request_wants_journal_ajax(request: Any) -> bool: + """XHR / Accept:json / form ajax=1 → 返回 JSON,避免整页刷新.""" + xrw = str(getattr(request, "headers", {}).get("X-Requested-With") or "").lower() + if xrw == "xmlhttprequest": + return True + form = getattr(request, "form", None) + if form is not None: + raw = str(form.get("ajax") or "").strip().lower() + if raw in ("1", "true", "yes", "on"): + return True + accept = str(getattr(request, "headers", {}).get("Accept") or "").lower() + if "application/json" in accept and accept.strip().startswith("application/json"): + return True + return False + + +def journal_ajax_or_flash_error(request: Any, msg: str, *, redirect_fn: Callable[[], Any]): + """校验失败:AJAX 返回 JSON,否则 flash + 跳转.""" + from flask import flash, jsonify + + if request_wants_journal_ajax(request): + return jsonify({"ok": False, "msg": msg}), 400 + flash(msg) + return redirect_fn() diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index ecbc41f..62827b7 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1318,13 +1318,6 @@ if(window.ManualOrderRrPreview){ refreshAccountSnapshot(); if (window.AccountRiskBadge) AccountRiskBadge.startTicker(); -const _journalFormEl = document.getElementById("journal-form"); -if(_journalFormEl){ - _journalFormEl.addEventListener("submit", function(ev){ - if(!validateJournalEntryReason()) ev.preventDefault(); - }); -} - const addOrderForm = document.getElementById("add-order-form"); if(addOrderForm){ addOrderForm.addEventListener("submit", function(ev){ diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 2e36a8f..c0c2152 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -97,6 +97,7 @@ + +