Options: show order form as viewport modal on select.

Stop inserting the panel under the strike row; mount backdrop on body with cancel/Esc close.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-20 08:06:07 +08:00
parent 19b46d19fd
commit 547eedeec4
5 changed files with 98 additions and 259 deletions
+39 -170
View File
@@ -91,16 +91,17 @@
}
function parkOrderPanel() {
stopPendingOrdersPoll();
const panel = orderPanel();
const host = orderPanelHost();
// 把整块 host(含面板)移回原位,再删行内 tr,避免 tbody 重绘销毁下单 DOM
if (host && orderPanelHome && host.parentElement !== orderPanelHome) {
orderPanelHome.appendChild(host);
} else if (panel && host && panel.parentElement !== host) {
host.appendChild(panel);
// 弹窗挂到 body;关闭后收回原位,绝不插入期权链表格
if (panel && host && panel.parentElement !== host) host.appendChild(panel);
if (host) {
host.hidden = true;
host.setAttribute("aria-hidden", "true");
if (orderPanelHome && host.parentElement !== orderPanelHome) {
orderPanelHome.appendChild(host);
}
}
if (host) host.hidden = true;
if (panel) panel.style.display = "none";
const inline = document.querySelector(".opt-order-inline-row");
if (inline) inline.remove();
@@ -121,32 +122,27 @@
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]') ||
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-call-inst="' + CSS.escape(instId) + '"]') ||
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-put-inst="' + CSS.escape(instId) + '"]');
if (!row) {
syncPickButtons(null);
return false;
}
document.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.toggle("opt-row-selected", r === row);
r.classList.toggle("opt-row-selected", !!row && r === row);
});
syncPickButtons(instId);
const oldInline = document.querySelector(".opt-order-inline-row");
if (oldInline) oldInline.remove();
if (panel.parentElement !== host) host.appendChild(panel);
const tr = document.createElement("tr");
tr.className = "opt-order-inline-row";
const td = document.createElement("td");
td.colSpan = strikeTableColspan();
td.appendChild(host);
tr.appendChild(td);
row.after(tr);
// 挂到 body,避免被卡片 overflow 裁成「行内展开」
if (host.parentElement !== document.body) document.body.appendChild(host);
host.hidden = false;
host.setAttribute("aria-hidden", "false");
panel.style.display = "";
tr.scrollIntoView({ behavior: "smooth", block: "nearest" });
refreshPendingOrders();
startPendingOrdersPoll();
return true;
}
function closeOrderDialog() {
state.selectedInst = null;
state.orderQuote = null;
parkOrderPanel();
}
function fmtPendingAge(sec) {
if (sec == null || Number.isNaN(Number(sec))) return "—";
let s = Math.max(0, Math.round(Number(sec)));
@@ -276,14 +272,15 @@
function updateSizeInputs() {
const mode = currentSizeMode();
const sheetsEl = document.getElementById("opt-sheets-amount");
const ethEl = document.getElementById("opt-eth-amount");
if (sheetsEl) sheetsEl.style.display = mode === "sheets" ? "" : "none";
if (ethEl) ethEl.style.display = mode === "eth_amount" ? "" : "none";
}
function hardenOrderAutofill() {
const note = document.getElementById("opt-signal-note");
if (!note) return;
function wipeNote() {
// 浏览器常把用户名(dekun)填进备注框
if (/^[a-z][a-z0-9._-]{1,31}$/i.test(String(note.value || "").trim())) {
note.value = "";
}
@@ -299,123 +296,6 @@
setTimeout(wipeNote, 800);
}
function hideEthOrderDialog() {
const bd = document.getElementById("opt-eth-order-backdrop");
if (bd) bd.hidden = true;
}
function openEthOrderDialog() {
if (!state.selectedInst) {
alert("请先选择合约");
const sheetsRadio = document.querySelector('input[name="opt-size-mode"][value="sheets"]');
if (sheetsRadio) {
sheetsRadio.checked = true;
updateSizeInputs();
}
return;
}
const bd = document.getElementById("opt-eth-order-backdrop");
const instEl = document.getElementById("opt-eth-order-inst");
const amountEl = document.getElementById("opt-eth-order-amount");
const preview = document.getElementById("opt-eth-order-preview");
const msg = document.getElementById("opt-eth-order-msg");
if (instEl) instEl.textContent = state.selectedInst;
if (amountEl) {
amountEl.value = document.getElementById("opt-eth-amount").value || "";
amountEl.focus();
}
if (preview) preview.textContent = "输入币数量后显示张数与权利金估算";
if (msg) {
msg.textContent = "";
msg.classList.remove("opt-error");
}
if (bd) bd.hidden = false;
refreshEthOrderPreview();
}
async function refreshEthOrderPreview() {
const preview = document.getElementById("opt-eth-order-preview");
const amountEl = document.getElementById("opt-eth-order-amount");
if (!preview || !amountEl || !state.selectedInst) return;
const eth = parseFloat(amountEl.value);
if (!Number.isFinite(eth) || eth <= 0) {
preview.textContent = "输入币数量后显示张数与权利金估算";
return;
}
preview.textContent = "估算中…";
try {
const url =
"/api/options/quote?inst_id=" +
encodeURIComponent(state.selectedInst) +
"&mode=eth_amount&eth_amount=" +
encodeURIComponent(String(eth));
const d = await apiJson(url);
if (!d || !d.ok) {
preview.textContent = (d && d.msg) || "报价失败";
return;
}
const sz = d.sizing || {};
if (sz.ok === false) {
preview.textContent = sz.msg || "张数无效";
return;
}
const sheets = sz.sheets != null ? sz.sheets : "—";
const prem = sz.total_premium != null ? sz.total_premium : null;
const premTxt = prem != null ? fmt(prem, 2) + " USDC" : "—";
preview.textContent = "约 " + sheets + " 张 · 预估权利金 " + premTxt;
} catch (e) {
preview.textContent = "报价失败";
}
}
async function submitEthOrderDialog() {
const amountEl = document.getElementById("opt-eth-order-amount");
const msg = document.getElementById("opt-eth-order-msg");
const submitBtn = document.getElementById("opt-eth-order-submit");
const eth = amountEl ? parseFloat(amountEl.value) : NaN;
if (!Number.isFinite(eth) || eth <= 0) {
if (msg) {
msg.textContent = "请输入有效币数量";
msg.classList.add("opt-error");
}
return;
}
const hidden = document.getElementById("opt-eth-amount");
if (hidden) hidden.value = String(eth);
const ethRadio = document.querySelector('input[name="opt-size-mode"][value="eth_amount"]');
if (ethRadio) ethRadio.checked = true;
updateSizeInputs();
if (submitBtn) submitBtn.disabled = true;
try {
if (state.selectedInst) {
await selectContract(state.selectedInst, null, true);
}
const ok = await openPosition();
if (ok) {
hideEthOrderDialog();
setOptionsPosTab("pending");
} else if (msg) {
const orderMsg = document.getElementById("opt-order-msg");
msg.textContent = (orderMsg && orderMsg.textContent) || "下单失败";
msg.classList.add("opt-error");
}
} finally {
if (submitBtn) submitBtn.disabled = false;
}
}
function cancelEthOrderDialog() {
hideEthOrderDialog();
const sheetsRadio = document.querySelector('input[name="opt-size-mode"][value="sheets"]');
if (sheetsRadio) {
sheetsRadio.checked = true;
updateSizeInputs();
}
const hidden = document.getElementById("opt-eth-amount");
if (hidden) hidden.value = "";
if (state.selectedInst) selectContract(state.selectedInst, null, true);
}
function quoteUrl(instId) {
const mode = currentSizeMode();
let url = "/api/options/quote?inst_id=" + encodeURIComponent(instId) + "&mode=" + mode;
@@ -1288,6 +1168,8 @@
startPendingOrdersPoll();
refreshAllPositions();
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
closeOrderDialog();
setOptionsPosTab("pending");
return true;
}
alert(d.msg || "下单失败");
@@ -2191,44 +2073,31 @@
document.querySelectorAll('input[name="opt-size-mode"]').forEach(function (r) {
r.addEventListener("change", function () {
const mode = currentSizeMode();
updateSizeInputs();
if (mode === "eth_amount") {
openEthOrderDialog();
return;
}
if (state.selectedInst) selectContract(state.selectedInst, null, true);
});
});
const ethBackdrop = document.getElementById("opt-eth-order-backdrop");
const ethCancel = document.getElementById("opt-eth-order-cancel");
const ethSubmit = document.getElementById("opt-eth-order-submit");
const ethAmount = document.getElementById("opt-eth-order-amount");
if (ethCancel) ethCancel.addEventListener("click", cancelEthOrderDialog);
if (ethSubmit) ethSubmit.addEventListener("click", submitEthOrderDialog);
if (ethAmount) {
ethAmount.addEventListener("input", refreshEthOrderPreview);
ethAmount.addEventListener("change", refreshEthOrderPreview);
ethAmount.addEventListener("keydown", function (ev) {
if (ev.key === "Enter") {
ev.preventDefault();
submitEthOrderDialog();
}
function bindOrderDialogChrome() {
const host = orderPanelHost();
const closeBtn = document.getElementById("opt-order-close-btn");
const cancelBtn = document.getElementById("opt-order-cancel-btn");
if (closeBtn) closeBtn.addEventListener("click", closeOrderDialog);
if (cancelBtn) cancelBtn.addEventListener("click", closeOrderDialog);
if (host) {
host.addEventListener("click", function (ev) {
if (ev.target === host) closeOrderDialog();
});
}
document.addEventListener("keydown", function (ev) {
if (ev.key !== "Escape") return;
const h = orderPanelHost();
if (h && !h.hidden) closeOrderDialog();
});
}
if (ethBackdrop) {
ethBackdrop.addEventListener("click", function (ev) {
if (ev.target === ethBackdrop) cancelEthOrderDialog();
});
}
document.addEventListener("keydown", function (ev) {
if (ev.key !== "Escape") return;
const bd = document.getElementById("opt-eth-order-backdrop");
if (bd && !bd.hidden) cancelEthOrderDialog();
});
bindOrderDialogChrome();
["opt-sheets-amount", "opt-target-idx"].forEach(function (id) {
["opt-sheets-amount", "opt-eth-amount", "opt-target-idx"].forEach(function (id) {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener("change", function () {