fix: inline options order panel below selected row with clear active states

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 09:16:04 +08:00
parent c1647822fb
commit 38f6bc240e
3 changed files with 154 additions and 42 deletions
+32 -1
View File
@@ -2372,9 +2372,40 @@ html[data-theme="light"] .settings-export-link {
.options-chain-toolbar .btn-secondary.active,
.opt-uly-btn.active,
.opt-type-btn.active {
border-color: #4a7cff;
border-color: #5b8cff;
color: #cfe0ff;
background: rgba(74, 124, 255, 0.28);
box-shadow: inset 0 0 0 1px rgba(120, 160, 255, 0.45);
}
.opt-pick-btn.active {
border-color: #5b8cff;
color: #fff;
background: rgba(74, 124, 255, 0.45);
box-shadow: inset 0 0 0 1px rgba(140, 175, 255, 0.6);
}
.opt-strike-row.opt-row-selected td {
background: rgba(74, 124, 255, 0.1);
}
.opt-strike-row.opt-row-selected td:first-child {
box-shadow: inset 3px 0 0 #5b8cff;
}
.opt-order-inline-row td {
padding: 14px 16px !important;
background: rgba(74, 124, 255, 0.07);
border-top: 1px solid rgba(74, 124, 255, 0.25);
border-bottom: 1px solid rgba(74, 124, 255, 0.25);
}
.opt-order-panel-inner {
border-radius: 8px;
}
.opt-order-panel-inner .opt-order-title {
margin: 0 0 10px;
font-size: 1rem;
color: #9ec0ff;
}
.opt-order-panel-host {
display: none;
}
.options-order-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+100 -20
View File
@@ -22,11 +22,63 @@
return r.json();
}
function scrollToOrderPanel() {
const panel = document.getElementById("opt-order-panel");
if (panel && panel.style.display !== "none") {
panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
function orderPanel() {
return document.getElementById("opt-order-panel");
}
function orderPanelHost() {
return document.getElementById("opt-order-panel-host");
}
function parkOrderPanel() {
const panel = orderPanel();
const host = orderPanelHost();
const inline = document.querySelector(".opt-order-inline-row");
if (inline) inline.remove();
if (panel && host && panel.parentElement !== host) {
host.appendChild(panel);
host.hidden = true;
}
if (panel) panel.style.display = "none";
document.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.remove("opt-row-selected");
});
document.querySelectorAll(".opt-pick-btn").forEach(function (b) {
b.classList.remove("active");
b.disabled = false;
if (b.dataset.origText) b.textContent = b.dataset.origText;
});
}
function placeOrderPanelAfter(instId) {
const panel = orderPanel();
if (!panel || !instId) return;
const row = document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]');
if (!row) return;
document.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.toggle("opt-row-selected", r === row);
});
document.querySelectorAll(".opt-pick-btn").forEach(function (b) {
const on = b.getAttribute("data-inst") === instId;
b.classList.toggle("active", on);
if (!b.dataset.origText) b.dataset.origText = b.textContent;
if (on) b.textContent = "已选";
});
const oldInline = document.querySelector(".opt-order-inline-row");
if (oldInline) oldInline.remove();
const tr = document.createElement("tr");
tr.className = "opt-order-inline-row";
const td = document.createElement("td");
td.colSpan = 6;
td.appendChild(panel);
tr.appendChild(td);
row.after(tr);
panel.style.display = "";
orderPanelHost().hidden = true;
tr.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
function currentSizeMode() {
@@ -107,9 +159,12 @@
function renderStrikes() {
const tbody = document.getElementById("opt-strike-tbody");
const expMs = document.getElementById("opt-exp-select").value;
const prevSelected = state.selectedInst;
parkOrderPanel();
tbody.innerHTML = "";
if (!expMs || !state.chain) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">请选择到期日</td></tr>';
state.selectedInst = null;
return;
}
const exp = (state.chain.expiries || []).find(function (e) {
@@ -121,10 +176,14 @@
});
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">该到期日暂无报价</td></tr>';
state.selectedInst = null;
return;
}
let stillVisible = false;
list.forEach(function (c) {
const tr = document.createElement("tr");
tr.className = "opt-strike-row";
tr.setAttribute("data-inst", c.inst_id);
if (c.moneyness) tr.classList.add("opt-row-" + c.moneyness);
tr.innerHTML =
"<td>" + c.strike + "</td>" +
@@ -137,28 +196,30 @@
'<button type="button" class="btn-primary opt-buy-btn" data-inst="' + c.inst_id + '">买入</button>' +
"</td>";
tbody.appendChild(tr);
if (c.inst_id === prevSelected) stillVisible = true;
});
tbody.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
selectContract(btn.getAttribute("data-inst"));
selectContract(btn.getAttribute("data-inst"), btn);
});
});
tbody.querySelectorAll(".opt-buy-btn").forEach(function (btn) {
btn.addEventListener("click", async function () {
await selectContract(btn.getAttribute("data-inst"));
await selectContract(btn.getAttribute("data-inst"), null);
await openPosition();
});
});
if (stillVisible && prevSelected) {
selectContract(prevSelected, null, true);
} else {
state.selectedInst = null;
}
}
async function selectContract(instId) {
state.selectedInst = instId;
const d = await apiJson(quoteUrl(instId));
const panel = document.getElementById("opt-order-panel");
panel.style.display = "";
document.getElementById("opt-order-inst").textContent = instId;
document.getElementById("opt-order-ask").textContent = fmt(d.ask, 4);
function fillOrderPanel(d) {
const sz = d.sizing || {};
document.getElementById("opt-order-inst").textContent = d.inst_id || state.selectedInst || "";
document.getElementById("opt-order-ask").textContent = fmt(d.ask, 4);
document.getElementById("opt-order-sheets").textContent = sz.sheets != null ? sz.sheets : "—";
document.getElementById("opt-order-eth").textContent = sz.eth_amount != null ? sz.eth_amount : "—";
document.getElementById("opt-order-premium").textContent = sz.total_premium != null ? fmt(sz.total_premium, 4) + " USDC" : "—";
@@ -173,8 +234,28 @@
msgEl.textContent = "";
msgEl.classList.remove("opt-error");
}
scrollToOrderPanel();
return d;
}
async function selectContract(instId, pickBtn, silent) {
state.selectedInst = instId;
placeOrderPanelAfter(instId);
if (pickBtn) {
pickBtn.disabled = true;
if (!pickBtn.dataset.origText) pickBtn.dataset.origText = "选择";
pickBtn.textContent = "加载…";
}
try {
const d = await apiJson(quoteUrl(instId));
fillOrderPanel(d);
return d;
} finally {
if (pickBtn) {
pickBtn.disabled = false;
pickBtn.textContent = "已选";
pickBtn.classList.add("active");
}
if (!silent) placeOrderPanelAfter(instId);
}
}
async function loadChain() {
@@ -184,6 +265,8 @@
return;
}
state.chain = d;
state.selectedInst = null;
parkOrderPanel();
renderExpiries();
renderStrikes();
}
@@ -313,7 +396,7 @@
document.querySelectorAll('input[name="opt-size-mode"]').forEach(function (r) {
r.addEventListener("change", function () {
updateSizeInputs();
if (state.selectedInst) selectContract(state.selectedInst);
if (state.selectedInst) selectContract(state.selectedInst, null, true);
});
});
@@ -321,10 +404,7 @@
const el = document.getElementById(id);
if (!el) return;
el.addEventListener("change", function () {
if (state.selectedInst) selectContract(state.selectedInst);
});
el.addEventListener("input", function () {
if (state.selectedInst) selectContract(state.selectedInst);
if (state.selectedInst) selectContract(state.selectedInst, null, true);
});
});
+22 -21
View File
@@ -84,27 +84,28 @@
</tbody>
</table>
</div>
</div>
<div class="options-section card-nested" id="opt-order-panel" style="display:none">
<h3>下单</h3>
<div id="opt-order-inst" class="options-order-inst"></div>
<div class="options-order-grid">
<div><span class="k">卖一(每1币)</span><span id="opt-order-ask" class="v"></span></div>
<div><span class="k">张数</span><span id="opt-order-sheets" class="v"></span></div>
<div><span class="k">ETH/BTC 数量</span><span id="opt-order-eth" class="v"></span></div>
<div><span class="k">预估权利金</span><span id="opt-order-premium" class="v"></span></div>
<div id="opt-order-panel-host" class="opt-order-panel-host" hidden aria-hidden="true">
<div id="opt-order-panel" class="opt-order-panel-inner" style="display:none">
<h3 class="opt-order-title">下单</h3>
<div id="opt-order-inst" class="options-order-inst"></div>
<div class="options-order-grid">
<div><span class="k">卖一(每1币)</span><span id="opt-order-ask" class="v"></span></div>
<div><span class="k">张数</span><span id="opt-order-sheets" class="v"></span></div>
<div><span class="k">ETH/BTC 数量</span><span id="opt-order-eth" class="v"></span></div>
<div><span class="k">预估权利金</span><span id="opt-order-premium" class="v"></span></div>
</div>
<div class="form-row options-order-mode-row">
<label><input type="radio" name="opt-size-mode" value="sheets" checked> 指定张数</label>
<input type="number" id="opt-sheets-amount" min="1" step="1" value="1" placeholder="张数">
<label><input type="radio" name="opt-size-mode" value="budget_full"> 按单笔上限打满</label>
<label><input type="radio" name="opt-size-mode" value="eth_amount"> 指定币数量</label>
<input type="number" id="opt-eth-amount" min="0.01" step="0.01" placeholder="如 0.5" style="display:none">
<input type="text" id="opt-signal-note" placeholder="备注(关键位说明)">
<button type="button" class="btn-primary" id="opt-open-btn">限价买入 @ 卖一</button>
</div>
<div id="opt-order-msg" class="muted"></div>
</div>
</div>
<div class="form-row options-order-mode-row">
<label><input type="radio" name="opt-size-mode" value="sheets" checked> 指定张数</label>
<input type="number" id="opt-sheets-amount" min="1" step="1" value="1" placeholder="张数">
<label><input type="radio" name="opt-size-mode" value="budget_full"> 按单笔上限打满</label>
<label><input type="radio" name="opt-size-mode" value="eth_amount"> 指定币数量</label>
<input type="number" id="opt-eth-amount" min="0.01" step="0.01" placeholder="如 0.5" style="display:none">
<input type="text" id="opt-signal-note" placeholder="备注(关键位说明)">
<button type="button" class="btn-primary" id="opt-open-btn">限价买入 @ 卖一</button>
</div>
<div id="opt-order-msg" class="muted"></div>
</div>
<div class="options-section">
@@ -131,4 +132,4 @@
</div>
</div>
</div>
<script src="/static/options_panel.js?v=3"></script>
<script src="/static/options_panel.js?v=4"></script>