Speed up journal save with AJAX and async K-line charts.
Defer exchange chart generation to a background thread and save via XHR so fill-from-trade no longer blocks on full page reload. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
@@ -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){
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
<script src="/static/time_close_ui.js?v=3"></script>
|
||||
<script src="/static/ai_review_render.js?v=2"></script>
|
||||
<script src="/static/form_submit_guard.js?v=2"></script>
|
||||
<script src="/static/journal_form_save.js?v=1"></script>
|
||||
<script>
|
||||
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
|
||||
const ORDER_ENTRY_MODEL_CATEGORIES = {{ entry_model_categories | tojson }};
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
<script src="/static/time_close_ui.js?v=3"></script>
|
||||
<script src="/static/ai_review_render.js?v=2"></script>
|
||||
<script src="/static/form_submit_guard.js?v=2"></script>
|
||||
<script src="/static/journal_form_save.js?v=1"></script>
|
||||
<script>
|
||||
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
|
||||
const ORDER_ENTRY_MODEL_CATEGORIES = {{ entry_model_categories | tojson }};
|
||||
@@ -1823,13 +1824,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){
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="journal_exchange_chart" value="true">
|
||||
保存时自动生成 K 线图并作为截图
|
||||
保存后后台生成 K 线图并作为截图(不阻塞保存)
|
||||
</label>
|
||||
<label style="font-size:.82rem;color:#9aa">周期1</label>
|
||||
<select name="journal_chart_tf1" style="min-width:72px">
|
||||
|
||||
Reference in New Issue
Block a user