diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 863a7b8..57c664c 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -8936,6 +8936,7 @@ _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") _MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js") _OPTIONS_PANEL_JS = os.path.join(_REPO_STATIC_DIR, "options_panel.js") +_OPTIONS_EXPIRY_COUNTDOWN_JS = os.path.join(_REPO_STATIC_DIR, "options_expiry_countdown.js") _OPTIONS_SETTINGS_JS = os.path.join(_REPO_STATIC_DIR, "options_settings.js") @@ -8967,6 +8968,13 @@ def static_options_panel_js(): return send_file(_OPTIONS_PANEL_JS, mimetype="application/javascript; charset=utf-8") +@app.route("/static/options_expiry_countdown.js") +def static_options_expiry_countdown_js(): + if not os.path.isfile(_OPTIONS_EXPIRY_COUNTDOWN_JS): + return Response("not found", status=404, mimetype="text/plain; charset=utf-8") + return send_file(_OPTIONS_EXPIRY_COUNTDOWN_JS, mimetype="application/javascript; charset=utf-8") + + @app.route("/static/options_settings.js") def static_options_settings_js(): if not os.path.isfile(_OPTIONS_SETTINGS_JS): diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index cb92e6a..642f245 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -2890,6 +2890,16 @@ html[data-theme="light"] .options-estimate-row { .opt-pos-card { margin-bottom: 10px; } +.opt-expiry-cd { + font-variant-numeric: tabular-nums; + font-weight: 600; +} +.opt-expiry-cd--urgent { + color: #ffb347; +} +.opt-expiry-cd--expired { + color: var(--muted); +} .settings-card--compact .options-settings-section { margin-bottom: 8px; } diff --git a/lib/common/static/options_expiry_countdown.js b/lib/common/static/options_expiry_countdown.js new file mode 100644 index 0000000..118aab1 --- /dev/null +++ b/lib/common/static/options_expiry_countdown.js @@ -0,0 +1,58 @@ +/** + * 期权到期倒计时(实例期权页 + 中控监控/看板共用) + */ +(function (global) { + function normalizeExpMs(v) { + if (v == null || v === "") return null; + var n = Number(v); + if (!Number.isFinite(n) || n <= 0) return null; + if (n < 1e12) n *= 1000; + return n; + } + + function formatCountdown(expMs, nowMs) { + var ms = normalizeExpMs(expMs); + if (ms == null) return "—"; + var now = nowMs != null ? nowMs : Date.now(); + var rem = Math.max(0, Math.floor((ms - now) / 1000)); + if (rem <= 0) return "已到期"; + var d = Math.floor(rem / 86400); + var h = Math.floor((rem % 86400) / 3600); + var m = Math.floor((rem % 3600) / 60); + var s = rem % 60; + var pad = function (x) { + return String(x).padStart(2, "0"); + }; + if (d > 0) return d + "天 " + pad(h) + ":" + pad(m) + ":" + pad(s); + return pad(h) + ":" + pad(m) + ":" + pad(s); + } + + function tick(root) { + var scope = root && root.querySelectorAll ? root : document; + var now = Date.now(); + scope.querySelectorAll("[data-opt-exp-ms]").forEach(function (el) { + var exp = el.getAttribute("data-opt-exp-ms"); + var text = formatCountdown(exp, now); + el.textContent = text; + var expMs = normalizeExpMs(exp); + el.classList.toggle("opt-expiry-cd--urgent", expMs != null && expMs - now > 0 && expMs - now < 3600000); + el.classList.toggle("opt-expiry-cd--expired", text === "已到期"); + }); + } + + var timer = null; + function ensureTimer() { + tick(); + if (timer) return; + timer = setInterval(function () { + tick(); + }, 1000); + } + + global.OptionsExpiryCountdown = { + normalizeExpMs: normalizeExpMs, + format: formatCountdown, + tick: tick, + ensureTimer: ensureTimer, + }; +})(typeof window !== "undefined" ? window : globalThis); diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 21bf756..b7cf16d 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -431,6 +431,8 @@ const upl = p.upl; const uplCls = upl > 0 ? "pos-pnl-profit" : upl < 0 ? "pos-pnl-loss" : ""; const sideCls = (p.opt_type || "").toUpperCase() === "P" ? "pos-side-short" : "pos-side-long"; + const expMs = p.exp_time_ms != null ? p.exp_time_ms : p.exp_time; + const expAttr = expMs != null && expMs !== "" ? String(expMs) : ""; return ( '