feat: restructure OKX options UI with header funds, settings card, and dual-column layout

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 09:36:00 +08:00
parent 38f6bc240e
commit 090b9c6215
15 changed files with 619 additions and 225 deletions
+58
View File
@@ -2458,4 +2458,62 @@ html[data-theme="light"] .settings-export-link {
.options-order-mode-row input[type="number"] {
width: 88px;
}
.options-dual-grid {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 16px;
align-items: start;
}
@media (max-width: 1100px) {
.options-dual-grid {
grid-template-columns: 1fr;
}
}
.options-order-card h2,
.options-pos-card-wrap h2 {
margin: 0 0 10px;
}
.options-pos-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.options-pos-head h2 {
margin: 0;
}
.options-pos-tabs {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.opt-pos-tab.active {
border-color: #5b8cff;
color: #cfe0ff;
background: rgba(74, 124, 255, 0.28);
}
.options-pos-pane {
min-height: 200px;
}
.opt-pos-card {
margin-bottom: 10px;
}
.options-settings-block {
margin-bottom: 14px;
}
.options-settings-row {
flex-wrap: wrap;
gap: 8px;
}
.options-settings-hint {
font-size: 0.82rem;
margin: 0 0 8px;
}
.pos-pnl-profit {
color: #7ee787;
}
.pos-pnl-loss {
color: #ff8b8b;
}
+91 -91
View File
@@ -9,7 +9,7 @@
optType: "C",
chain: null,
selectedInst: null,
convertQuoteId: null,
posTab: "live",
};
function fmt(v, d) {
@@ -55,20 +55,17 @@
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 = "已选";
document.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
const on = btn.getAttribute("data-inst") === instId;
btn.classList.toggle("active", on);
if (!btn.dataset.origText) btn.dataset.origText = btn.textContent;
if (on) btn.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");
@@ -113,15 +110,8 @@
return '<span class="opt-moneyness opt-moneyness-' + m + '">' + label + "</span>";
}
async function refreshBalances() {
const d = await apiJson("/api/options/balances");
if (!d.ok) return;
document.getElementById("opt-funding-usdt").textContent = fmt(d.funding_usdt) + " U";
document.getElementById("opt-funding-usdc").textContent = fmt(d.funding_usdc) + " U";
document.getElementById("opt-trading-usdt").textContent = fmt(d.trading_usdt) + " U";
document.getElementById("opt-trading-usdc").textContent = fmt(d.trading_usdc) + " U";
document.getElementById("opt-trading-usdg").textContent = fmt(d.trading_usdg) + " U";
document.getElementById("opt-trade-budget").textContent = fmt(d.trade_budget) + " USDC";
function optTypeLabel(t) {
return (t || "").toUpperCase() === "P" ? "看跌 Put" : "看涨 Call";
}
function expLabel(ms) {
@@ -299,8 +289,8 @@
msgEl.textContent = d.ok ? "下单已提交,可在 OKX 委托中查看" : (d.msg || "失败");
msgEl.classList.toggle("opt-error", !d.ok);
if (d.ok) {
refreshBalances();
refreshPositions();
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
} else {
alert(d.msg || "下单失败");
}
@@ -309,6 +299,32 @@
}
}
function renderPositionCard(p) {
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";
return (
'<div class="pos-card opt-pos-card" data-inst="' + (p.inst_id || "") + '">' +
'<div class="pos-card-head">' +
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + '</strong>' +
'<span class="pos-side-badge ' + sideCls + '">' + optTypeLabel(p.opt_type) + "</span></div>" +
'<div class="pos-head-actions">' +
'<button type="button" class="btn-primary opt-close-btn" data-inst="' + p.inst_id + '">限价平仓</button>' +
"</div></div>" +
'<div class="pos-meta">' +
'<span class="pos-meta-item">行权价: ' + fmt(p.strike, 0) + "</span>" +
'<span class="pos-meta-item">张数: ' + fmt(p.pos, 0) + " · 币量 " + fmt(p.eth_amount, 4) + "</span>" +
"</div>" +
'<div class="pos-grid">' +
'<div class="pos-cell"><span class="pos-label">开仓均价</span><span class="pos-value">' + fmt(p.avg_px, 4) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">' + fmt(p.mark_px, 4) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">浮盈亏</span><span class="pos-value ' + uplCls + '">' + fmt(p.upl, 4) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
(p.upl_ratio_pct != null ? p.upl_ratio_pct + "%" : "—") + "</span></div>" +
"</div></div>"
);
}
async function closePosition(inst, btn) {
const q = await apiJson("/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=sheets&sheets=1");
if (!q.ok) {
@@ -334,7 +350,8 @@
alert(r.msg || "平仓失败");
}
refreshPositions();
refreshBalances();
refreshHistory();
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
} finally {
if (btn) btn.disabled = false;
}
@@ -342,33 +359,62 @@
async function refreshPositions() {
const d = await apiJson("/api/options/positions");
const tbody = document.getElementById("opt-positions-tbody");
tbody.innerHTML = "";
const wrap = document.getElementById("opt-pos-cards");
const empty = document.getElementById("opt-pos-empty");
const list = (d.ok && d.positions) || [];
wrap.innerHTML = "";
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="8" class="muted">暂无持仓</td></tr>';
empty.style.display = "";
return;
}
empty.style.display = "none";
list.forEach(function (p) {
const tr = document.createElement("tr");
tr.innerHTML =
"<td><code>" + (p.inst_id || "") + "</code></td>" +
"<td>" + fmt(p.pos, 0) + "</td>" +
"<td>" + fmt(p.eth_amount, 4) + "</td>" +
"<td>" + fmt(p.avg_px, 4) + "</td>" +
"<td>" + fmt(p.mark_px, 4) + "</td>" +
"<td>" + fmt(p.upl, 4) + "</td>" +
"<td>" + (p.upl_ratio_pct != null ? p.upl_ratio_pct + "%" : "—") + "</td>" +
'<td><button type="button" class="btn-primary opt-close-btn" data-inst="' + p.inst_id + '">限价平仓</button></td>';
tbody.appendChild(tr);
const div = document.createElement("div");
div.innerHTML = renderPositionCard(p);
wrap.appendChild(div.firstChild);
});
tbody.querySelectorAll(".opt-close-btn").forEach(function (btn) {
wrap.querySelectorAll(".opt-close-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
closePosition(btn.getAttribute("data-inst"), btn);
});
});
}
async function refreshHistory() {
const d = await apiJson("/api/options/history");
const tbody = document.getElementById("opt-history-tbody");
tbody.innerHTML = "";
const list = (d.ok && d.history) || [];
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">暂无历史记录</td></tr>';
return;
}
list.forEach(function (h) {
const tr = document.createElement("tr");
const prem = h.status === "closed" ? h.premium_received : h.premium_paid;
const pnl = h.realized_pnl;
const pnlTxt = pnl != null ? fmt(pnl, 4) : "—";
tr.innerHTML =
"<td><code>" + (h.inst_id || "") + "</code></td>" +
"<td>" + fmt(h.sheets, 0) + "</td>" +
"<td>" + fmt(prem, 4) + "</td>" +
"<td>" + (h.status === "closed" ? "已平" : "持仓中") + "</td>" +
"<td>" + pnlTxt + "</td>" +
"<td>" + (h.closed_at || h.created_at || "—") + "</td>";
tbody.appendChild(tr);
});
}
function switchPosTab(tab) {
state.posTab = tab;
document.querySelectorAll(".opt-pos-tab").forEach(function (b) {
b.classList.toggle("active", b.getAttribute("data-tab") === tab);
});
document.getElementById("opt-pos-live").hidden = tab !== "live";
document.getElementById("opt-pos-history").hidden = tab !== "history";
if (tab === "history") refreshHistory();
}
document.querySelectorAll(".opt-uly-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".opt-uly-btn").forEach(function (b) { b.classList.remove("active"); });
@@ -387,10 +433,18 @@
});
});
document.querySelectorAll(".opt-pos-tab").forEach(function (btn) {
btn.addEventListener("click", function () {
switchPosTab(btn.getAttribute("data-tab"));
});
});
document.getElementById("opt-exp-select").addEventListener("change", renderStrikes);
document.getElementById("opt-load-chain").addEventListener("click", loadChain);
document.getElementById("opt-refresh-balances").addEventListener("click", refreshBalances);
document.getElementById("opt-refresh-positions").addEventListener("click", refreshPositions);
document.getElementById("opt-refresh-positions").addEventListener("click", function () {
refreshPositions();
if (state.posTab === "history") refreshHistory();
});
document.getElementById("opt-open-btn").addEventListener("click", openPosition);
document.querySelectorAll('input[name="opt-size-mode"]').forEach(function (r) {
@@ -408,61 +462,7 @@
});
});
document.getElementById("opt-convert-quote-btn").addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-convert-amount").value);
if (!amount || amount <= 0) {
alert("请输入 USDT 数量");
return;
}
const d = await apiJson("/api/options/convert/quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: amount }),
});
const prev = document.getElementById("opt-convert-preview");
if (!d.ok) {
prev.textContent = d.msg || "询价失败";
state.convertQuoteId = null;
document.getElementById("opt-convert-exec-btn").disabled = true;
return;
}
state.convertQuoteId = d.quote_id;
prev.textContent =
"预估获得 " + fmt(d.base_sz, 6) + " USDC,汇率 " + fmt(d.cnvt_px, 6);
document.getElementById("opt-convert-exec-btn").disabled = false;
});
document.getElementById("opt-convert-exec-btn").addEventListener("click", async function () {
if (!state.convertQuoteId) return;
const amount = parseFloat(document.getElementById("opt-convert-amount").value);
const d = await apiJson("/api/options/convert/execute", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quote_id: state.convertQuoteId, rfq_sz: amount }),
});
document.getElementById("opt-convert-preview").textContent = d.ok ? "兑换成功" : (d.msg || "失败");
state.convertQuoteId = null;
document.getElementById("opt-convert-exec-btn").disabled = true;
refreshBalances();
});
document.getElementById("opt-transfer-btn").addEventListener("click", async function () {
const d = await apiJson("/api/options/transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ccy: document.getElementById("opt-transfer-ccy").value,
from: document.getElementById("opt-transfer-from").value,
to: document.getElementById("opt-transfer-to").value,
amount: parseFloat(document.getElementById("opt-transfer-amount").value),
}),
});
document.getElementById("opt-transfer-msg").textContent = d.ok ? "划转成功" : (d.msg || "失败");
refreshBalances();
});
updateSizeInputs();
refreshBalances();
loadChain();
refreshPositions();
})();
+73
View File
@@ -0,0 +1,73 @@
(function () {
"use strict";
const root = document.getElementById("options-settings-root");
if (!root) return;
async function apiJson(url, opts) {
const r = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
return r.json();
}
function setMsg(id, text, isErr) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = text || "";
el.classList.toggle("opt-error", !!isErr);
}
document.getElementById("opt-set-swap-btn").addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-set-swap-amount").value);
if (!amount || amount <= 0) {
setMsg("opt-set-swap-msg", "请输入有效数量", true);
return;
}
const d = await apiJson("/api/options/spot/swap", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
direction: document.getElementById("opt-set-swap-dir").value,
amount: amount,
}),
});
setMsg("opt-set-swap-msg", d.ok ? "兑换单已提交" : (d.msg || "失败"), !d.ok);
});
document.getElementById("opt-set-int-btn").addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-set-int-amount").value);
if (!amount || amount <= 0) {
setMsg("opt-set-int-msg", "请输入有效数量", true);
return;
}
const d = await apiJson("/api/options/transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ccy: document.getElementById("opt-set-int-ccy").value,
from: document.getElementById("opt-set-int-from").value,
to: document.getElementById("opt-set-int-to").value,
amount: amount,
}),
});
setMsg("opt-set-int-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
});
document.getElementById("opt-set-cross-btn").addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-set-cross-amount").value);
if (!amount || amount <= 0) {
setMsg("opt-set-cross-msg", "请输入有效数量", true);
return;
}
const d = await apiJson("/api/options/cross-transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ccy: document.getElementById("opt-set-cross-ccy").value,
amount: amount,
account: document.getElementById("opt-set-cross-acct").value,
direction: document.getElementById("opt-set-cross-dir").value,
}),
});
setMsg("opt-set-cross-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
});
})();