修改
This commit is contained in:
@@ -36,6 +36,7 @@ if _REPO_ROOT not in sys.path:
|
|||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
from ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
||||||
|
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from fib_key_monitor_lib import (
|
from fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
calc_fib_plan,
|
calc_fib_plan,
|
||||||
@@ -6476,6 +6477,12 @@ def add_key():
|
|||||||
return redirect("/key_monitor")
|
return redirect("/key_monitor")
|
||||||
mt = (d.get("type") or "").strip()
|
mt = (d.get("type") or "").strip()
|
||||||
direction_sel = (d.get("direction") or "").strip().lower()
|
direction_sel = (d.get("direction") or "").strip().lower()
|
||||||
|
dup_msg = check_duplicate_submit(
|
||||||
|
session, submit_scope_add_key(symbol, mt, direction_sel or "watch")
|
||||||
|
)
|
||||||
|
if dup_msg:
|
||||||
|
flash(dup_msg)
|
||||||
|
return redirect("/key_monitor")
|
||||||
if mt in KEY_MONITOR_RS_TYPES:
|
if mt in KEY_MONITOR_RS_TYPES:
|
||||||
direction_sel = KEY_DIRECTION_WATCH
|
direction_sel = KEY_DIRECTION_WATCH
|
||||||
elif direction_sel not in ("long", "short"):
|
elif direction_sel not in ("long", "short"):
|
||||||
@@ -6622,6 +6629,11 @@ def add_order():
|
|||||||
conn.close()
|
conn.close()
|
||||||
flash("symbol 不能为空")
|
flash("symbol 不能为空")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction))
|
||||||
|
if dup_msg:
|
||||||
|
conn.close()
|
||||||
|
flash(dup_msg)
|
||||||
|
return redirect("/trade")
|
||||||
ok, reason = precheck_risk(conn, symbol, direction)
|
ok, reason = precheck_risk(conn, symbol, direction)
|
||||||
if not ok:
|
if not ok:
|
||||||
if "已达最大持仓数" in reason:
|
if "已达最大持仓数" in reason:
|
||||||
@@ -7517,7 +7529,9 @@ def api_reviews():
|
|||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
_AI_REVIEW_RENDER_JS = os.path.join(os.path.dirname(BASE_DIR), "static", "ai_review_render.js")
|
_REPO_STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
||||||
|
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
||||||
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/ai_review_render.js")
|
@app.route("/static/ai_review_render.js")
|
||||||
@@ -7527,6 +7541,13 @@ def static_ai_review_render_js():
|
|||||||
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
|
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/static/form_submit_guard.js")
|
||||||
|
def static_form_submit_guard_js():
|
||||||
|
if not os.path.isfile(_FORM_SUBMIT_GUARD_JS):
|
||||||
|
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
|
||||||
|
return send_file(_FORM_SUBMIT_GUARD_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/export/review_md/<rid>")
|
@app.route("/export/review_md/<rid>")
|
||||||
@login_required
|
@login_required
|
||||||
def export_review_md(rid):
|
def export_review_md(rid):
|
||||||
|
|||||||
@@ -61,6 +61,8 @@
|
|||||||
.pnl-profit{color:#4cd97f;font-weight:600}
|
.pnl-profit{color:#4cd97f;font-weight:600}
|
||||||
.pnl-loss{color:#ff6666;font-weight:600}
|
.pnl-loss{color:#ff6666;font-weight:600}
|
||||||
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
||||||
|
form.is-form-submitting{opacity:.88;pointer-events:none}
|
||||||
|
form.is-form-submitting button[type=submit],form.is-form-submitting input[type=submit]{cursor:wait}
|
||||||
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
||||||
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
||||||
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
||||||
@@ -862,6 +864,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/ai_review_render.js?v=1"></script>
|
<script src="/static/ai_review_render.js?v=1"></script>
|
||||||
|
<script src="/static/form_submit_guard.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
||||||
@@ -1586,27 +1589,35 @@ const keyForm = document.getElementById("key-form");
|
|||||||
if(keyForm){
|
if(keyForm){
|
||||||
keyForm.addEventListener("submit", (e)=>{
|
keyForm.addEventListener("submit", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
|
||||||
const symbolEl = keyForm.querySelector('[name="symbol"]');
|
const symbolEl = keyForm.querySelector('[name="symbol"]');
|
||||||
const symbol = (symbolEl ? symbolEl.value : "").trim();
|
const symbol = (symbolEl ? symbolEl.value : "").trim();
|
||||||
if(!symbol){
|
if(!symbol){
|
||||||
alert("请先输入交易对");
|
alert("请先输入交易对");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
|
||||||
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
|
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
|
||||||
.then(r=>r.json().then(d=>({status:r.status, data:d})))
|
.then(r=>r.json().then(d=>({status:r.status, data:d})))
|
||||||
.then(({status,data})=>{
|
.then(({status,data})=>{
|
||||||
if(status >= 400 || !data.ok){
|
if(status >= 400 || !data.ok){
|
||||||
alert((data && data.msg) || "日成交量排名读取失败");
|
alert((data && data.msg) || "日成交量排名读取失败");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rankMax = data.rank_max || 30;
|
const rankMax = data.rank_max || 30;
|
||||||
if(!data.in_top30){
|
if(!data.in_top30){
|
||||||
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前${rankMax},已拦截。`);
|
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前${rankMax},已拦截。`);
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
keyForm.submit();
|
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
|
||||||
|
else keyForm.submit();
|
||||||
})
|
})
|
||||||
.catch(()=>alert("日成交量排名检查失败,请稍后重试"));
|
.catch(()=>{
|
||||||
|
alert("日成交量排名检查失败,请稍后重试");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
||||||
@@ -1734,8 +1745,10 @@ function cancelExchangeTpsl(orderId, role){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function allowManualOrderSubmit(form){
|
function allowManualOrderSubmit(form){
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(form) && form.dataset.rrOk !== "1") return;
|
||||||
form.dataset.rrOk = "1";
|
form.dataset.rrOk = "1";
|
||||||
form.submit();
|
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(form, "开仓提交中…");
|
||||||
|
else form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
let latestAvailableUsdt = null;
|
let latestAvailableUsdt = null;
|
||||||
@@ -1950,23 +1963,32 @@ if(addOrderForm){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
||||||
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
||||||
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
|
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
|
||||||
if(mode === "pct"){
|
if(mode === "pct"){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
const rr = calcClientRrFromPct(
|
const rr = calcClientRrFromPct(
|
||||||
(document.getElementById("order-sl-pct")||{}).value,
|
(document.getElementById("order-sl-pct")||{}).value,
|
||||||
(document.getElementById("order-tp-pct")||{}).value
|
(document.getElementById("order-tp-pct")||{}).value
|
||||||
);
|
);
|
||||||
if(rejectManualOrderRr(rr)) return;
|
if(rejectManualOrderRr(rr)){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
allowManualOrderSubmit(addOrderForm);
|
allowManualOrderSubmit(addOrderForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sl = Number((document.getElementById("order-sl")||{}).value);
|
const sl = Number((document.getElementById("order-sl")||{}).value);
|
||||||
const tp = Number((document.getElementById("order-tp")||{}).value);
|
const tp = Number((document.getElementById("order-tp")||{}).value);
|
||||||
let entry = sl;
|
let entry = sl;
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
if(!symbol){
|
if(!symbol){
|
||||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return;
|
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
allowManualOrderSubmit(addOrderForm);
|
allowManualOrderSubmit(addOrderForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1975,10 +1997,16 @@ if(addOrderForm){
|
|||||||
.then(data=>{
|
.then(data=>{
|
||||||
const px = data.last_price || data.price;
|
const px = data.last_price || data.price;
|
||||||
if(px) entry = Number(px);
|
if(px) entry = Number(px);
|
||||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return;
|
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
allowManualOrderSubmit(addOrderForm);
|
allowManualOrderSubmit(addOrderForm);
|
||||||
})
|
})
|
||||||
.catch(()=>{ alert("无法校验盈亏比,请稍后重试"); });
|
.catch(()=>{
|
||||||
|
alert("无法校验盈亏比,请稍后重试");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ if _REPO_ROOT not in sys.path:
|
|||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
from ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
||||||
|
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from fib_key_monitor_lib import (
|
from fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
KEY_ENTRY_REASON_BY_SIGNAL,
|
KEY_ENTRY_REASON_BY_SIGNAL,
|
||||||
@@ -6526,6 +6527,13 @@ def add_key():
|
|||||||
flash("symbol 不能为空")
|
flash("symbol 不能为空")
|
||||||
return redirect("/key_monitor")
|
return redirect("/key_monitor")
|
||||||
mt = (d.get("type") or "").strip()
|
mt = (d.get("type") or "").strip()
|
||||||
|
direction_pre = (d.get("direction") or "").strip().lower()
|
||||||
|
dup_msg = check_duplicate_submit(
|
||||||
|
session, submit_scope_add_key(symbol, mt, direction_pre or "watch")
|
||||||
|
)
|
||||||
|
if dup_msg:
|
||||||
|
flash(dup_msg)
|
||||||
|
return redirect("/key_monitor")
|
||||||
direction_sel = (d.get("direction") or "").strip().lower()
|
direction_sel = (d.get("direction") or "").strip().lower()
|
||||||
if mt in KEY_MONITOR_RS_TYPES:
|
if mt in KEY_MONITOR_RS_TYPES:
|
||||||
direction_sel = KEY_DIRECTION_WATCH
|
direction_sel = KEY_DIRECTION_WATCH
|
||||||
@@ -6696,6 +6704,11 @@ def add_order():
|
|||||||
conn.close()
|
conn.close()
|
||||||
flash("symbol 不能为空")
|
flash("symbol 不能为空")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction))
|
||||||
|
if dup_msg:
|
||||||
|
conn.close()
|
||||||
|
flash(dup_msg)
|
||||||
|
return redirect("/trade")
|
||||||
ok, reason = precheck_risk(conn, symbol, direction)
|
ok, reason = precheck_risk(conn, symbol, direction)
|
||||||
if not ok:
|
if not ok:
|
||||||
if "已达最大持仓数" in reason:
|
if "已达最大持仓数" in reason:
|
||||||
@@ -7597,7 +7610,9 @@ def api_reviews():
|
|||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
_AI_REVIEW_RENDER_JS = os.path.join(os.path.dirname(BASE_DIR), "static", "ai_review_render.js")
|
_REPO_STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
||||||
|
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
||||||
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/ai_review_render.js")
|
@app.route("/static/ai_review_render.js")
|
||||||
@@ -7607,6 +7622,13 @@ def static_ai_review_render_js():
|
|||||||
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
|
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/static/form_submit_guard.js")
|
||||||
|
def static_form_submit_guard_js():
|
||||||
|
if not os.path.isfile(_FORM_SUBMIT_GUARD_JS):
|
||||||
|
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
|
||||||
|
return send_file(_FORM_SUBMIT_GUARD_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/export/review_md/<rid>")
|
@app.route("/export/review_md/<rid>")
|
||||||
@login_required
|
@login_required
|
||||||
def export_review_md(rid):
|
def export_review_md(rid):
|
||||||
|
|||||||
@@ -61,6 +61,8 @@
|
|||||||
.pnl-profit{color:#4cd97f;font-weight:600}
|
.pnl-profit{color:#4cd97f;font-weight:600}
|
||||||
.pnl-loss{color:#ff6666;font-weight:600}
|
.pnl-loss{color:#ff6666;font-weight:600}
|
||||||
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
||||||
|
form.is-form-submitting{opacity:.88;pointer-events:none}
|
||||||
|
form.is-form-submitting button[type=submit],form.is-form-submitting input[type=submit]{cursor:wait}
|
||||||
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
||||||
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
||||||
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
||||||
@@ -862,6 +864,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/ai_review_render.js?v=1"></script>
|
<script src="/static/ai_review_render.js?v=1"></script>
|
||||||
|
<script src="/static/form_submit_guard.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
||||||
@@ -1586,27 +1589,35 @@ const keyForm = document.getElementById("key-form");
|
|||||||
if(keyForm){
|
if(keyForm){
|
||||||
keyForm.addEventListener("submit", (e)=>{
|
keyForm.addEventListener("submit", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
|
||||||
const symbolEl = keyForm.querySelector('[name="symbol"]');
|
const symbolEl = keyForm.querySelector('[name="symbol"]');
|
||||||
const symbol = (symbolEl ? symbolEl.value : "").trim();
|
const symbol = (symbolEl ? symbolEl.value : "").trim();
|
||||||
if(!symbol){
|
if(!symbol){
|
||||||
alert("请先输入交易对");
|
alert("请先输入交易对");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
|
||||||
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
|
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
|
||||||
.then(r=>r.json().then(d=>({status:r.status, data:d})))
|
.then(r=>r.json().then(d=>({status:r.status, data:d})))
|
||||||
.then(({status,data})=>{
|
.then(({status,data})=>{
|
||||||
if(status >= 400 || !data.ok){
|
if(status >= 400 || !data.ok){
|
||||||
alert((data && data.msg) || "日成交量排名读取失败");
|
alert((data && data.msg) || "日成交量排名读取失败");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rankMax = data.rank_max || 30;
|
const rankMax = data.rank_max || 30;
|
||||||
if(!data.in_top30){
|
if(!data.in_top30){
|
||||||
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前${rankMax},已拦截。`);
|
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前${rankMax},已拦截。`);
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
keyForm.submit();
|
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
|
||||||
|
else keyForm.submit();
|
||||||
})
|
})
|
||||||
.catch(()=>alert("日成交量排名检查失败,请稍后重试"));
|
.catch(()=>{
|
||||||
|
alert("日成交量排名检查失败,请稍后重试");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
||||||
@@ -1734,8 +1745,10 @@ function cancelExchangeTpsl(orderId, role){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function allowManualOrderSubmit(form){
|
function allowManualOrderSubmit(form){
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(form) && form.dataset.rrOk !== "1") return;
|
||||||
form.dataset.rrOk = "1";
|
form.dataset.rrOk = "1";
|
||||||
form.submit();
|
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(form, "开仓提交中…");
|
||||||
|
else form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
let latestAvailableUsdt = null;
|
let latestAvailableUsdt = null;
|
||||||
@@ -1950,23 +1963,32 @@ if(addOrderForm){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
||||||
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
||||||
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
|
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
|
||||||
if(mode === "pct"){
|
if(mode === "pct"){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
const rr = calcClientRrFromPct(
|
const rr = calcClientRrFromPct(
|
||||||
(document.getElementById("order-sl-pct")||{}).value,
|
(document.getElementById("order-sl-pct")||{}).value,
|
||||||
(document.getElementById("order-tp-pct")||{}).value
|
(document.getElementById("order-tp-pct")||{}).value
|
||||||
);
|
);
|
||||||
if(rejectManualOrderRr(rr)) return;
|
if(rejectManualOrderRr(rr)){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
allowManualOrderSubmit(addOrderForm);
|
allowManualOrderSubmit(addOrderForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sl = Number((document.getElementById("order-sl")||{}).value);
|
const sl = Number((document.getElementById("order-sl")||{}).value);
|
||||||
const tp = Number((document.getElementById("order-tp")||{}).value);
|
const tp = Number((document.getElementById("order-tp")||{}).value);
|
||||||
let entry = sl;
|
let entry = sl;
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
if(!symbol){
|
if(!symbol){
|
||||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return;
|
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
allowManualOrderSubmit(addOrderForm);
|
allowManualOrderSubmit(addOrderForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1975,10 +1997,16 @@ if(addOrderForm){
|
|||||||
.then(data=>{
|
.then(data=>{
|
||||||
const px = data.last_price || data.price;
|
const px = data.last_price || data.price;
|
||||||
if(px) entry = Number(px);
|
if(px) entry = Number(px);
|
||||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return;
|
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
allowManualOrderSubmit(addOrderForm);
|
allowManualOrderSubmit(addOrderForm);
|
||||||
})
|
})
|
||||||
.catch(()=>{ alert("无法校验盈亏比,请稍后重试"); });
|
.catch(()=>{
|
||||||
|
alert("无法校验盈亏比,请稍后重试");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ if _REPO_ROOT not in sys.path:
|
|||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
from ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
||||||
|
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from journal_chart_lib import (
|
from journal_chart_lib import (
|
||||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
JOURNAL_CHART_DEFAULT_TF1,
|
JOURNAL_CHART_DEFAULT_TF1,
|
||||||
@@ -5825,6 +5826,14 @@ def add_key():
|
|||||||
if not symbol:
|
if not symbol:
|
||||||
flash("symbol 不能为空")
|
flash("symbol 不能为空")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
mt = (d.get("type") or "").strip()
|
||||||
|
direction_pre = (d.get("direction") or "long").strip().lower()
|
||||||
|
dup_msg = check_duplicate_submit(
|
||||||
|
session, submit_scope_add_key(symbol, mt, direction_pre)
|
||||||
|
)
|
||||||
|
if dup_msg:
|
||||||
|
flash(dup_msg)
|
||||||
|
return redirect("/")
|
||||||
rank, total = _daily_volume_rank(symbol)
|
rank, total = _daily_volume_rank(symbol)
|
||||||
if rank is None:
|
if rank is None:
|
||||||
flash("日成交量排名读取失败,请稍后重试")
|
flash("日成交量排名读取失败,请稍后重试")
|
||||||
@@ -5852,6 +5861,11 @@ def add_order():
|
|||||||
conn.close()
|
conn.close()
|
||||||
flash("symbol 不能为空")
|
flash("symbol 不能为空")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction))
|
||||||
|
if dup_msg:
|
||||||
|
conn.close()
|
||||||
|
flash(dup_msg)
|
||||||
|
return redirect("/trade")
|
||||||
ok, reason = precheck_risk(conn, symbol, direction)
|
ok, reason = precheck_risk(conn, symbol, direction)
|
||||||
if not ok:
|
if not ok:
|
||||||
if "已达最大持仓数" in reason:
|
if "已达最大持仓数" in reason:
|
||||||
@@ -7004,7 +7018,9 @@ def api_reviews():
|
|||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
_AI_REVIEW_RENDER_JS = os.path.join(os.path.dirname(BASE_DIR), "static", "ai_review_render.js")
|
_REPO_STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
||||||
|
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
||||||
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/ai_review_render.js")
|
@app.route("/static/ai_review_render.js")
|
||||||
@@ -7014,6 +7030,13 @@ def static_ai_review_render_js():
|
|||||||
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
|
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/static/form_submit_guard.js")
|
||||||
|
def static_form_submit_guard_js():
|
||||||
|
if not os.path.isfile(_FORM_SUBMIT_GUARD_JS):
|
||||||
|
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
|
||||||
|
return send_file(_FORM_SUBMIT_GUARD_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/export/review_md/<rid>")
|
@app.route("/export/review_md/<rid>")
|
||||||
@login_required
|
@login_required
|
||||||
def export_review_md(rid):
|
def export_review_md(rid):
|
||||||
|
|||||||
@@ -62,6 +62,8 @@
|
|||||||
.pnl-loss{color:#ff6666;font-weight:600}
|
.pnl-loss{color:#ff6666;font-weight:600}
|
||||||
.pnl-neutral{color:#cfd3ef;font-weight:600}
|
.pnl-neutral{color:#cfd3ef;font-weight:600}
|
||||||
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
||||||
|
form.is-form-submitting{opacity:.88;pointer-events:none}
|
||||||
|
form.is-form-submitting button[type=submit],form.is-form-submitting input[type=submit]{cursor:wait}
|
||||||
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
||||||
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
||||||
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
||||||
@@ -318,7 +320,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<button type="submit">手动划转</button>
|
<button type="submit">手动划转</button>
|
||||||
</form>
|
</form>
|
||||||
<form action="/add_order" method="post" class="form-row">
|
<form id="add-order-form" action="/add_order" method="post" class="form-row">
|
||||||
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required>
|
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required>
|
||||||
<select id="order-direction" name="direction" required>
|
<select id="order-direction" name="direction" required>
|
||||||
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
||||||
@@ -701,6 +703,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/ai_review_render.js?v=1"></script>
|
<script src="/static/ai_review_render.js?v=1"></script>
|
||||||
|
<script src="/static/form_submit_guard.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
||||||
@@ -1392,26 +1395,48 @@ const keyForm = document.getElementById("key-form");
|
|||||||
if(keyForm){
|
if(keyForm){
|
||||||
keyForm.addEventListener("submit", (e)=>{
|
keyForm.addEventListener("submit", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
|
||||||
const symbolEl = keyForm.querySelector('[name="symbol"]');
|
const symbolEl = keyForm.querySelector('[name="symbol"]');
|
||||||
const symbol = (symbolEl ? symbolEl.value : "").trim();
|
const symbol = (symbolEl ? symbolEl.value : "").trim();
|
||||||
if(!symbol){
|
if(!symbol){
|
||||||
alert("请先输入交易对");
|
alert("请先输入交易对");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
|
||||||
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
|
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
|
||||||
.then(r=>r.json().then(d=>({status:r.status, data:d})))
|
.then(r=>r.json().then(d=>({status:r.status, data:d})))
|
||||||
.then(({status,data})=>{
|
.then(({status,data})=>{
|
||||||
if(status >= 400 || !data.ok){
|
if(status >= 400 || !data.ok){
|
||||||
alert((data && data.msg) || "日成交量排名读取失败");
|
alert((data && data.msg) || "日成交量排名读取失败");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(!data.in_top30){
|
if(!data.in_top30){
|
||||||
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前30,已拦截。`);
|
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前30,已拦截。`);
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
keyForm.submit();
|
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
|
||||||
|
else keyForm.submit();
|
||||||
})
|
})
|
||||||
.catch(()=>alert("日成交量排名检查失败,请稍后重试"));
|
.catch(()=>{
|
||||||
|
alert("日成交量排名检查失败,请稍后重试");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const addOrderForm = document.getElementById("add-order-form");
|
||||||
|
if(addOrderForm){
|
||||||
|
addOrderForm.addEventListener("submit", function(ev){
|
||||||
|
if(addOrderForm.dataset.submitOnce === "1"){
|
||||||
|
addOrderForm.dataset.submitOnce = "0";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ev.preventDefault();
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||||
|
addOrderForm.dataset.submitOnce = "1";
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(addOrderForm, "开仓提交中…");
|
||||||
|
else addOrderForm.submit();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ if _REPO_ROOT not in sys.path:
|
|||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
from ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
||||||
|
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from fib_key_monitor_lib import (
|
from fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
calc_fib_plan,
|
calc_fib_plan,
|
||||||
@@ -5926,6 +5927,12 @@ def add_key():
|
|||||||
return redirect("/key_monitor")
|
return redirect("/key_monitor")
|
||||||
mt = (d.get("type") or "").strip()
|
mt = (d.get("type") or "").strip()
|
||||||
direction_sel = (d.get("direction") or "").strip().lower()
|
direction_sel = (d.get("direction") or "").strip().lower()
|
||||||
|
dup_msg = check_duplicate_submit(
|
||||||
|
session, submit_scope_add_key(symbol, mt, direction_sel or "watch")
|
||||||
|
)
|
||||||
|
if dup_msg:
|
||||||
|
flash(dup_msg)
|
||||||
|
return redirect("/key_monitor")
|
||||||
if mt in KEY_MONITOR_RS_TYPES:
|
if mt in KEY_MONITOR_RS_TYPES:
|
||||||
direction_sel = KEY_DIRECTION_WATCH
|
direction_sel = KEY_DIRECTION_WATCH
|
||||||
elif direction_sel not in ("long", "short"):
|
elif direction_sel not in ("long", "short"):
|
||||||
@@ -6056,6 +6063,11 @@ def add_order():
|
|||||||
conn.close()
|
conn.close()
|
||||||
flash("symbol 不能为空")
|
flash("symbol 不能为空")
|
||||||
return redirect("/trade")
|
return redirect("/trade")
|
||||||
|
dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction))
|
||||||
|
if dup_msg:
|
||||||
|
conn.close()
|
||||||
|
flash(dup_msg)
|
||||||
|
return redirect("/trade")
|
||||||
ok, reason = precheck_risk(conn, symbol, direction)
|
ok, reason = precheck_risk(conn, symbol, direction)
|
||||||
if not ok:
|
if not ok:
|
||||||
if "已达最大持仓数" in reason or "一次只能持有一个仓位" in reason:
|
if "已达最大持仓数" in reason or "一次只能持有一个仓位" in reason:
|
||||||
@@ -6912,7 +6924,9 @@ def api_reviews():
|
|||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
_AI_REVIEW_RENDER_JS = os.path.join(os.path.dirname(BASE_DIR), "static", "ai_review_render.js")
|
_REPO_STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
||||||
|
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
||||||
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/ai_review_render.js")
|
@app.route("/static/ai_review_render.js")
|
||||||
@@ -6922,6 +6936,13 @@ def static_ai_review_render_js():
|
|||||||
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
|
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/static/form_submit_guard.js")
|
||||||
|
def static_form_submit_guard_js():
|
||||||
|
if not os.path.isfile(_FORM_SUBMIT_GUARD_JS):
|
||||||
|
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
|
||||||
|
return send_file(_FORM_SUBMIT_GUARD_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/export/review_md/<rid>")
|
@app.route("/export/review_md/<rid>")
|
||||||
@login_required
|
@login_required
|
||||||
def export_review_md(rid):
|
def export_review_md(rid):
|
||||||
|
|||||||
@@ -61,6 +61,8 @@
|
|||||||
.pnl-profit{color:#4cd97f;font-weight:600}
|
.pnl-profit{color:#4cd97f;font-weight:600}
|
||||||
.pnl-loss{color:#ff6666;font-weight:600}
|
.pnl-loss{color:#ff6666;font-weight:600}
|
||||||
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
||||||
|
form.is-form-submitting{opacity:.88;pointer-events:none}
|
||||||
|
form.is-form-submitting button[type=submit],form.is-form-submitting input[type=submit]{cursor:wait}
|
||||||
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
||||||
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
||||||
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
||||||
@@ -871,6 +873,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/ai_review_render.js?v=1"></script>
|
<script src="/static/ai_review_render.js?v=1"></script>
|
||||||
|
<script src="/static/form_submit_guard.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
||||||
@@ -1595,28 +1598,36 @@ const keyForm = document.getElementById("key-form");
|
|||||||
if(keyForm){
|
if(keyForm){
|
||||||
keyForm.addEventListener("submit", (e)=>{
|
keyForm.addEventListener("submit", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
|
||||||
const symbolEl = keyForm.querySelector('[name="symbol"]');
|
const symbolEl = keyForm.querySelector('[name="symbol"]');
|
||||||
const symbol = (symbolEl ? symbolEl.value : "").trim();
|
const symbol = (symbolEl ? symbolEl.value : "").trim();
|
||||||
if(!symbol){
|
if(!symbol){
|
||||||
alert("请先输入交易对");
|
alert("请先输入交易对");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
|
||||||
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
|
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
|
||||||
.then(r=>r.json().then(d=>({status:r.status, data:d})))
|
.then(r=>r.json().then(d=>({status:r.status, data:d})))
|
||||||
.then(({status,data})=>{
|
.then(({status,data})=>{
|
||||||
if(status >= 400 || !data.ok){
|
if(status >= 400 || !data.ok){
|
||||||
alert((data && data.msg) || "日成交量排名读取失败");
|
alert((data && data.msg) || "日成交量排名读取失败");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rankMax = data.rank_max || 30;
|
const rankMax = data.rank_max || 30;
|
||||||
const inTop = data.in_top != null ? data.in_top : data.in_top30;
|
const inTop = data.in_top != null ? data.in_top : data.in_top30;
|
||||||
if(data.rank == null || !inTop){
|
if(data.rank == null || !inTop){
|
||||||
alert(`${data.symbol} 当前24h成交额排名 ${data.rank == null ? "—" : data.rank}/${data.total},不在前${rankMax},已拦截。`);
|
alert(`${data.symbol} 当前24h成交额排名 ${data.rank == null ? "—" : data.rank}/${data.total},不在前${rankMax},已拦截。`);
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
keyForm.submit();
|
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
|
||||||
|
else keyForm.submit();
|
||||||
})
|
})
|
||||||
.catch(()=>alert("日成交量排名检查失败,请稍后重试"));
|
.catch(()=>{
|
||||||
|
alert("日成交量排名检查失败,请稍后重试");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
||||||
@@ -1744,8 +1755,10 @@ function cancelExchangeTpsl(orderId, role){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function allowManualOrderSubmit(form){
|
function allowManualOrderSubmit(form){
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(form) && form.dataset.rrOk !== "1") return;
|
||||||
form.dataset.rrOk = "1";
|
form.dataset.rrOk = "1";
|
||||||
form.submit();
|
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(form, "开仓提交中…");
|
||||||
|
else form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
let latestAvailableUsdt = null;
|
let latestAvailableUsdt = null;
|
||||||
@@ -1992,15 +2005,20 @@ if(addOrderForm){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
||||||
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
||||||
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
|
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
|
||||||
if(mode === "pct"){
|
if(mode === "pct"){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
const rr = calcClientRrFromPct(
|
const rr = calcClientRrFromPct(
|
||||||
(document.getElementById("order-sl-pct")||{}).value,
|
(document.getElementById("order-sl-pct")||{}).value,
|
||||||
(document.getElementById("order-tp-pct")||{}).value
|
(document.getElementById("order-tp-pct")||{}).value
|
||||||
);
|
);
|
||||||
if(rejectManualOrderRr(rr)) return;
|
if(rejectManualOrderRr(rr)){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
allowManualOrderSubmit(addOrderForm);
|
allowManualOrderSubmit(addOrderForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2010,6 +2028,7 @@ if(addOrderForm){
|
|||||||
alert("请先填写币种,以便按市价校验盈亏比");
|
alert("请先填写币种,以便按市价校验盈亏比");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
fetch(`/api/order_defaults?symbol=${encodeURIComponent(symbol)}&direction=${encodeURIComponent(direction)}`)
|
fetch(`/api/order_defaults?symbol=${encodeURIComponent(symbol)}&direction=${encodeURIComponent(direction)}`)
|
||||||
.then(r=>r.json())
|
.then(r=>r.json())
|
||||||
.then(data=>{
|
.then(data=>{
|
||||||
@@ -2017,12 +2036,19 @@ if(addOrderForm){
|
|||||||
const entry = Number(px);
|
const entry = Number(px);
|
||||||
if(!Number.isFinite(entry) || entry <= 0){
|
if(!Number.isFinite(entry) || entry <= 0){
|
||||||
alert("无法获取市价,请稍后重试");
|
alert("无法获取市价,请稍后重试");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return;
|
|
||||||
allowManualOrderSubmit(addOrderForm);
|
allowManualOrderSubmit(addOrderForm);
|
||||||
})
|
})
|
||||||
.catch(()=>{ alert("无法校验盈亏比,请稍后重试"); });
|
.catch(()=>{
|
||||||
|
alert("无法校验盈亏比,请稍后重试");
|
||||||
|
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""防重复提交:Flask session 短窗口去重(下单 / 关键位等)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SUBMIT_GUARD_TTL = 90.0
|
||||||
|
|
||||||
|
|
||||||
|
def _prune_locks(locks: dict, now: float) -> dict:
|
||||||
|
return {k: float(v) for k, v in (locks or {}).items() if float(v) > now}
|
||||||
|
|
||||||
|
|
||||||
|
def check_duplicate_submit(
|
||||||
|
session: Any,
|
||||||
|
scope: str,
|
||||||
|
*,
|
||||||
|
ttl: float = DEFAULT_SUBMIT_GUARD_TTL,
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
同一 scope 在 ttl 秒内仅允许通过一次。
|
||||||
|
返回提示文案表示应拒绝;返回 None 表示可继续处理。
|
||||||
|
"""
|
||||||
|
scope = (scope or "").strip()
|
||||||
|
if not scope:
|
||||||
|
return None
|
||||||
|
now = time.time()
|
||||||
|
locks = _prune_locks(session.get("_form_submit_guard") or {}, now)
|
||||||
|
if scope in locks:
|
||||||
|
return "请求正在处理或刚提交过,请勿重复点击(请等待页面刷新后再试)"
|
||||||
|
locks[scope] = now + float(ttl)
|
||||||
|
session["_form_submit_guard"] = locks
|
||||||
|
try:
|
||||||
|
session.modified = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def submit_scope_add_order(symbol: str, direction: str) -> str:
|
||||||
|
sym = (symbol or "").strip().upper()
|
||||||
|
d = (direction or "").strip().lower()
|
||||||
|
return f"add_order:{sym}:{d}"
|
||||||
|
|
||||||
|
|
||||||
|
def submit_scope_add_key(symbol: str, monitor_type: str, direction: str) -> str:
|
||||||
|
sym = (symbol or "").strip().upper()
|
||||||
|
mt = (monitor_type or "").strip()
|
||||||
|
d = (direction or "").strip().lower() or "watch"
|
||||||
|
return f"add_key:{sym}:{mt}:{d}"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 表单提交防重复:网络慢时禁用按钮并显示「提交中」。
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function submitButtons(form) {
|
||||||
|
if (!form) return [];
|
||||||
|
return Array.prototype.slice.call(
|
||||||
|
form.querySelectorAll('button[type="submit"], input[type="submit"]')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockForm(form, label) {
|
||||||
|
if (!form) return false;
|
||||||
|
if (form.dataset.submitGuard === "locked") return false;
|
||||||
|
form.dataset.submitGuard = "locked";
|
||||||
|
form.classList.add("is-form-submitting");
|
||||||
|
submitButtons(form).forEach(function (btn) {
|
||||||
|
if (btn.dataset.submitGuardOrig === undefined) {
|
||||||
|
btn.dataset.submitGuardOrig =
|
||||||
|
btn.tagName === "BUTTON" ? btn.textContent : btn.value;
|
||||||
|
}
|
||||||
|
btn.disabled = true;
|
||||||
|
if (label) {
|
||||||
|
if (btn.tagName === "BUTTON") btn.textContent = label;
|
||||||
|
else btn.value = label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlockForm(form) {
|
||||||
|
if (!form) return;
|
||||||
|
delete form.dataset.submitGuard;
|
||||||
|
form.classList.remove("is-form-submitting");
|
||||||
|
submitButtons(form).forEach(function (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
var orig = btn.dataset.submitGuardOrig;
|
||||||
|
if (orig !== undefined) {
|
||||||
|
if (btn.tagName === "BUTTON") btn.textContent = orig;
|
||||||
|
else btn.value = orig;
|
||||||
|
delete btn.dataset.submitGuardOrig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLocked(form) {
|
||||||
|
return !!(form && form.dataset.submitGuard === "locked");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已通过前端校验,发起最终 POST(页面将跳转) */
|
||||||
|
function nativeSubmitOnce(form, label) {
|
||||||
|
if (!form) return;
|
||||||
|
lockForm(form, label || "提交中…");
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.FormSubmitGuard = {
|
||||||
|
lock: lockForm,
|
||||||
|
unlock: unlockForm,
|
||||||
|
isLocked: isLocked,
|
||||||
|
nativeSubmitOnce: nativeSubmitOnce,
|
||||||
|
};
|
||||||
|
})(typeof window !== "undefined" ? window : this);
|
||||||
Reference in New Issue
Block a user