a268a93027
Add shared lib/UI for midnight force-close indicator on instance and hub pages, and document Gate intraday 0-point exit alignment. Co-authored-by: Cursor <cursoragent@cursor.com>
195 lines
6.9 KiB
JavaScript
195 lines
6.9 KiB
JavaScript
/**
|
|
* 时间平仓 + 整点强制清仓:表单开关 + 持仓/顶栏倒计时。
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
function pad2(n) {
|
|
return n < 10 ? "0" + n : String(n);
|
|
}
|
|
|
|
function formatCountdown(sec) {
|
|
const s = Math.max(0, parseInt(sec, 10) || 0);
|
|
const h = Math.floor(s / 3600);
|
|
const m = Math.floor((s % 3600) / 60);
|
|
const r = s % 60;
|
|
return pad2(h) + ":" + pad2(m) + ":" + pad2(r);
|
|
}
|
|
|
|
function isForceCloseActive(wrap) {
|
|
if (!wrap) return false;
|
|
const raw =
|
|
wrap.dataset.forceCloseActive ||
|
|
wrap.getAttribute("data-force-close-active") ||
|
|
"";
|
|
return raw === "1" || raw === "true";
|
|
}
|
|
|
|
function bindTimeCloseForm(checkboxId, selectId, wrapId) {
|
|
const cb = document.getElementById(checkboxId);
|
|
const sel = document.getElementById(selectId);
|
|
const wrap = wrapId ? document.getElementById(wrapId) : null;
|
|
if (!cb || !sel) return;
|
|
function sync() {
|
|
const on = !!cb.checked;
|
|
sel.disabled = false;
|
|
sel.tabIndex = 0;
|
|
if (wrap) wrap.classList.toggle("is-disabled", !on);
|
|
}
|
|
sel.addEventListener("mousedown", function (ev) {
|
|
ev.stopPropagation();
|
|
});
|
|
sel.addEventListener("click", function (ev) {
|
|
ev.stopPropagation();
|
|
});
|
|
cb.addEventListener("change", sync);
|
|
sync();
|
|
}
|
|
|
|
function paintCountdownEl(cd, rem, active) {
|
|
if (!cd) return;
|
|
if (active) {
|
|
cd.textContent = "执行中";
|
|
return;
|
|
}
|
|
cd.textContent = Number.isFinite(rem) ? formatCountdown(rem) : "--:--:--";
|
|
}
|
|
|
|
function paintOrderTimeClose(order) {
|
|
if (!order || order.id == null) return;
|
|
const wrap = document.getElementById("order-time-close-wrap-" + order.id);
|
|
const cd = document.getElementById("order-time-close-cd-" + order.id);
|
|
if (!wrap || !cd) return;
|
|
const enabled = !!(order.time_close_enabled || order.time_close_at_ms);
|
|
if (!enabled) {
|
|
wrap.style.display = "none";
|
|
return;
|
|
}
|
|
wrap.style.display = "";
|
|
const hours = order.time_close_hours;
|
|
const label = order.time_close_label || (hours ? "时间平仓 " + hours + "h" : "时间平仓");
|
|
const labelEl = wrap.querySelector(".pos-time-close-label");
|
|
if (labelEl) labelEl.textContent = label;
|
|
let rem =
|
|
order.time_close_remaining_sec != null
|
|
? Number(order.time_close_remaining_sec)
|
|
: null;
|
|
if ((rem == null || !Number.isFinite(rem)) && order.time_close_at_ms) {
|
|
rem = Math.max(0, Math.floor((Number(order.time_close_at_ms) - Date.now()) / 1000));
|
|
}
|
|
paintCountdownEl(cd, rem, false);
|
|
wrap.dataset.closeAtMs = order.time_close_at_ms ? String(order.time_close_at_ms) : "";
|
|
}
|
|
|
|
function paintOrderForceClose(order) {
|
|
if (!order || order.id == null) return;
|
|
const wrap = document.getElementById("order-force-close-wrap-" + order.id);
|
|
const cd = document.getElementById("order-force-close-cd-" + order.id);
|
|
if (!wrap || !cd) return;
|
|
const enabled = !!order.force_close_enabled;
|
|
if (!enabled) {
|
|
wrap.style.display = "none";
|
|
return;
|
|
}
|
|
wrap.style.display = "";
|
|
const label = order.force_close_label || "强制清仓";
|
|
const labelEl = wrap.querySelector(".pos-force-close-label");
|
|
if (labelEl) labelEl.textContent = label;
|
|
let rem =
|
|
order.force_close_remaining_sec != null
|
|
? Number(order.force_close_remaining_sec)
|
|
: null;
|
|
const atMs = order.force_close_at_ms;
|
|
if ((rem == null || !Number.isFinite(rem)) && atMs) {
|
|
rem = Math.max(0, Math.floor((Number(atMs) - Date.now()) / 1000));
|
|
}
|
|
const active = !!order.force_close_active;
|
|
paintCountdownEl(cd, rem, active);
|
|
wrap.dataset.forceCloseAtMs = atMs ? String(atMs) : "";
|
|
wrap.dataset.forceCloseActive = active ? "1" : "0";
|
|
}
|
|
|
|
function paintForceCloseHeader(state) {
|
|
const wrap = document.getElementById("force-close-header-badge");
|
|
if (!wrap) return;
|
|
if (!state || !state.enabled) {
|
|
wrap.style.display = "none";
|
|
return;
|
|
}
|
|
wrap.style.display = "";
|
|
const label = state.label || "强制清仓";
|
|
const labelPrefix = label + " 已开启 · ";
|
|
let prefixNode = wrap.querySelector(".force-close-header-prefix");
|
|
if (!prefixNode) {
|
|
wrap.textContent = "";
|
|
prefixNode = document.createElement("span");
|
|
prefixNode.className = "force-close-header-prefix";
|
|
prefixNode.textContent = labelPrefix;
|
|
wrap.appendChild(prefixNode);
|
|
const cd = document.createElement("span");
|
|
cd.className = "force-close-header-cd";
|
|
wrap.appendChild(cd);
|
|
} else {
|
|
prefixNode.textContent = labelPrefix;
|
|
}
|
|
const cd = wrap.querySelector(".force-close-header-cd");
|
|
let rem = state.remaining_sec != null ? Number(state.remaining_sec) : null;
|
|
if ((rem == null || !Number.isFinite(rem)) && state.next_at_ms) {
|
|
rem = Math.max(0, Math.floor((Number(state.next_at_ms) - Date.now()) / 1000));
|
|
}
|
|
paintCountdownEl(cd, rem, !!state.active);
|
|
wrap.dataset.forceCloseAtMs = state.next_at_ms ? String(state.next_at_ms) : "";
|
|
wrap.dataset.forceCloseActive = state.active ? "1" : "0";
|
|
}
|
|
|
|
function tickLocalCountdowns() {
|
|
document.querySelectorAll("[data-close-at-ms]").forEach(function (wrap) {
|
|
const closeAtRaw = wrap.dataset.closeAtMs || wrap.getAttribute("data-close-at-ms") || "";
|
|
const cd = wrap.querySelector(".pos-time-close-cd");
|
|
if (!cd) return;
|
|
const closeAt = Number(closeAtRaw);
|
|
if (!closeAt) return;
|
|
const rem = Math.max(0, Math.floor((closeAt - Date.now()) / 1000));
|
|
cd.textContent = formatCountdown(rem);
|
|
});
|
|
document.querySelectorAll("[data-force-close-at-ms]").forEach(function (wrap) {
|
|
const closeAtRaw =
|
|
wrap.dataset.forceCloseAtMs || wrap.getAttribute("data-force-close-at-ms") || "";
|
|
const cd = wrap.querySelector(".pos-force-close-cd, .force-close-header-cd");
|
|
if (!cd) return;
|
|
const closeAt = Number(closeAtRaw);
|
|
if (!closeAt) return;
|
|
const rem = Math.max(0, Math.floor((closeAt - Date.now()) / 1000));
|
|
paintCountdownEl(cd, rem, isForceCloseActive(wrap));
|
|
});
|
|
}
|
|
|
|
function paintOrders(orders) {
|
|
(orders || []).forEach(function (order) {
|
|
paintOrderTimeClose(order);
|
|
paintOrderForceClose(order);
|
|
});
|
|
}
|
|
|
|
function syncKeyTimeCloseVisibility(show) {
|
|
const wrap = document.getElementById("key-time-close-wrap");
|
|
if (!wrap) return;
|
|
wrap.style.display = show ? "inline-flex" : "none";
|
|
}
|
|
|
|
global.TimeCloseUI = {
|
|
bindTimeCloseForm: bindTimeCloseForm,
|
|
paintOrderTimeClose: paintOrderTimeClose,
|
|
paintOrderForceClose: paintOrderForceClose,
|
|
paintForceCloseHeader: paintForceCloseHeader,
|
|
paintOrders: paintOrders,
|
|
tickLocalCountdowns: tickLocalCountdowns,
|
|
syncKeyTimeCloseVisibility: syncKeyTimeCloseVisibility,
|
|
formatCountdown: formatCountdown,
|
|
};
|
|
|
|
if (!global.__timeCloseCountdownTimer) {
|
|
global.__timeCloseCountdownTimer = setInterval(tickLocalCountdowns, 1000);
|
|
}
|
|
})(typeof window !== "undefined" ? window : globalThis);
|