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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user