Improve OKX options sizing, expiry sync, and multi-position accordion UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3254,6 +3254,102 @@ html[data-theme="light"] .options-estimate-row {
|
||||
font-size: 0.76rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.opt-pos-cards--accordion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.opt-pos-accordion-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.opt-pos-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #141923;
|
||||
border: 1px solid #2a3348;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.opt-pos-bar:hover {
|
||||
border-color: #3d4d6e;
|
||||
background: #171d2a;
|
||||
}
|
||||
.opt-pos-accordion-item.is-expanded .opt-pos-bar {
|
||||
border-radius: 8px 8px 0 0;
|
||||
border-bottom-color: transparent;
|
||||
background: #171d2a;
|
||||
}
|
||||
.opt-pos-bar-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.opt-pos-bar-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.opt-pos-bar-title {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 11rem;
|
||||
}
|
||||
.opt-pos-bar-meta {
|
||||
font-size: 0.66rem;
|
||||
color: #8b95b0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.opt-pos-bar-cd {
|
||||
font-size: 0.66rem;
|
||||
color: #8b95b0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.opt-pos-bar-pnl,
|
||||
.opt-pos-bar-roi {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.opt-pos-bar-chevron {
|
||||
display: inline-block;
|
||||
font-size: 0.58rem;
|
||||
color: #8b95b0;
|
||||
transition: transform 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.opt-pos-accordion-item.is-expanded .opt-pos-bar-chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.opt-pos-accordion-body {
|
||||
border: 1px solid #2a3348;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
background: #141923;
|
||||
}
|
||||
.opt-pos-card--nested {
|
||||
margin-bottom: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
.options-page-wrap .opt-pos-card .pos-card-symbol strong {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
@@ -3441,6 +3537,29 @@ html[data-theme="light"] .options-settings-subtitle {
|
||||
color: #142232 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .opt-pos-bar {
|
||||
background: #f6f9fc;
|
||||
border-color: #c8d4e0;
|
||||
color: #142232;
|
||||
}
|
||||
html[data-theme="light"] .opt-pos-bar:hover,
|
||||
html[data-theme="light"] .opt-pos-accordion-item.is-expanded .opt-pos-bar {
|
||||
background: #eef3f8;
|
||||
border-color: #9eb0c4;
|
||||
}
|
||||
html[data-theme="light"] .opt-pos-bar-title {
|
||||
color: #142232;
|
||||
}
|
||||
html[data-theme="light"] .opt-pos-bar-meta,
|
||||
html[data-theme="light"] .opt-pos-bar-cd,
|
||||
html[data-theme="light"] .opt-pos-bar-chevron {
|
||||
color: #5a6d82;
|
||||
}
|
||||
html[data-theme="light"] .opt-pos-accordion-body {
|
||||
background: #f6f9fc;
|
||||
border-color: #c8d4e0;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .options-page-wrap .options-hint,
|
||||
html[data-theme="light"] .options-page-wrap #opt-index-line,
|
||||
html[data-theme="light"] .options-order-grid .k,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
chain: panelCache.chain || null,
|
||||
selectedInst: null,
|
||||
orderQuote: null,
|
||||
expandedPosInst: null,
|
||||
};
|
||||
|
||||
function fmt(v, d) {
|
||||
@@ -427,14 +428,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderPositionCard(p) {
|
||||
function renderPositionCardInner(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";
|
||||
const expMs = p.exp_time_ms != null ? p.exp_time_ms : p.exp_time;
|
||||
const expAttr = expMs != null && expMs !== "" ? String(expMs) : "";
|
||||
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>" +
|
||||
@@ -458,10 +458,77 @@
|
||||
'<div class="pos-cell"><span class="pos-label">浮盈亏</span><span class="pos-value ' + uplCls + '">' + fmt(p.upl, 2) + "</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>"
|
||||
"</div>"
|
||||
);
|
||||
}
|
||||
|
||||
function renderPositionCard(p) {
|
||||
return (
|
||||
'<div class="pos-card opt-pos-card" data-inst="' + (p.inst_id || "") + '">' +
|
||||
renderPositionCardInner(p) +
|
||||
"</div>"
|
||||
);
|
||||
}
|
||||
|
||||
function renderPositionAccordionItem(p, expanded) {
|
||||
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";
|
||||
const expMs = p.exp_time_ms != null ? p.exp_time_ms : p.exp_time;
|
||||
const expAttr = expMs != null && expMs !== "" ? String(expMs) : "";
|
||||
const inst = p.inst_id || "";
|
||||
return (
|
||||
'<div class="opt-pos-accordion-item' + (expanded ? " is-expanded" : "") + '" data-inst="' + inst + '">' +
|
||||
'<button type="button" class="opt-pos-bar" aria-expanded="' + (expanded ? "true" : "false") + '">' +
|
||||
'<span class="opt-pos-bar-main">' +
|
||||
'<span class="opt-pos-bar-chevron" aria-hidden="true">▶</span>' +
|
||||
'<strong class="opt-pos-bar-title">' + inst + "</strong>" +
|
||||
'<span class="pos-side-badge ' + sideCls + '">' + optTypeLabel(p.opt_type) + "</span>" +
|
||||
'<span class="opt-pos-bar-meta">行权 ' + fmt(p.strike, 0) + " · " + fmt(p.pos, 0) + "张</span>" +
|
||||
"</span>" +
|
||||
'<span class="opt-pos-bar-side">' +
|
||||
(expAttr
|
||||
? '<span class="opt-pos-bar-cd">到期 <span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
|
||||
: "") +
|
||||
'<span class="opt-pos-bar-pnl ' + uplCls + '">' + fmt(p.upl, 2) + " USDC</span>" +
|
||||
'<span class="opt-pos-bar-roi ' + uplCls + '">' +
|
||||
(p.upl_ratio_pct != null ? p.upl_ratio_pct + "%" : "—") + "</span>" +
|
||||
"</span>" +
|
||||
"</button>" +
|
||||
'<div class="opt-pos-accordion-body"' + (expanded ? "" : ' hidden') + ">" +
|
||||
'<div class="pos-card opt-pos-card opt-pos-card--nested" data-inst="' + inst + '">' +
|
||||
renderPositionCardInner(p) +
|
||||
"</div></div></div>"
|
||||
);
|
||||
}
|
||||
|
||||
function bindPositionActions(container) {
|
||||
if (!container) return;
|
||||
container.querySelectorAll(".opt-close-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
closePosition(btn.getAttribute("data-inst"), btn);
|
||||
});
|
||||
});
|
||||
container.querySelectorAll(".opt-pos-bar").forEach(function (bar) {
|
||||
bar.addEventListener("click", function () {
|
||||
const item = bar.closest(".opt-pos-accordion-item");
|
||||
if (!item) return;
|
||||
const inst = item.getAttribute("data-inst");
|
||||
state.expandedPosInst = state.expandedPosInst === inst ? null : inst;
|
||||
const wrap = document.getElementById("opt-pos-cards");
|
||||
wrap.querySelectorAll(".opt-pos-accordion-item").forEach(function (el) {
|
||||
const open = el.getAttribute("data-inst") === state.expandedPosInst;
|
||||
el.classList.toggle("is-expanded", open);
|
||||
const body = el.querySelector(".opt-pos-accordion-body");
|
||||
const btn = el.querySelector(".opt-pos-bar");
|
||||
if (body) body.hidden = !open;
|
||||
if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function closePosition(inst, btn) {
|
||||
const q = await apiJson("/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=sheets&sheets=1");
|
||||
if (!q.ok) {
|
||||
@@ -504,16 +571,27 @@
|
||||
return;
|
||||
}
|
||||
empty.style.display = "none";
|
||||
list.forEach(function (p) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = renderPositionCard(p);
|
||||
wrap.appendChild(div.firstChild);
|
||||
});
|
||||
wrap.querySelectorAll(".opt-close-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
closePosition(btn.getAttribute("data-inst"), btn);
|
||||
const multi = list.length >= 2;
|
||||
wrap.classList.toggle("opt-pos-cards--accordion", multi);
|
||||
if (multi) {
|
||||
const ids = list.map(function (p) { return p.inst_id; });
|
||||
if (!state.expandedPosInst || ids.indexOf(state.expandedPosInst) < 0) {
|
||||
state.expandedPosInst = list[0].inst_id || null;
|
||||
}
|
||||
list.forEach(function (p) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = renderPositionAccordionItem(p, p.inst_id === state.expandedPosInst);
|
||||
wrap.appendChild(div.firstChild);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
state.expandedPosInst = null;
|
||||
list.forEach(function (p) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = renderPositionCard(p);
|
||||
wrap.appendChild(div.firstChild);
|
||||
});
|
||||
}
|
||||
bindPositionActions(wrap);
|
||||
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||
OptionsExpiryCountdown.ensureTimer();
|
||||
}
|
||||
@@ -560,6 +638,15 @@
|
||||
refreshAllPositions();
|
||||
}
|
||||
|
||||
function optHistoryStatus(h) {
|
||||
if (h.status !== "closed") return "持仓中";
|
||||
if ((h.signal_note || "").indexOf("到期结算") >= 0) return "到期";
|
||||
if (h.premium_received === 0 && h.realized_pnl != null && h.realized_pnl < 0 && !h.close_ord_id) {
|
||||
return "到期";
|
||||
}
|
||||
return "已平";
|
||||
}
|
||||
|
||||
async function refreshHistory() {
|
||||
const d = await apiJson("/api/options/history");
|
||||
const tbody = document.getElementById("opt-history-tbody");
|
||||
@@ -579,7 +666,7 @@
|
||||
"<td><code>" + (h.inst_id || "") + "</code></td>" +
|
||||
"<td>" + fmt(h.sheets, 0) + "</td>" +
|
||||
"<td>" + fmt(prem, 4) + "</td>" +
|
||||
"<td>" + (h.status === "closed" ? "已平" : "持仓中") + "</td>" +
|
||||
"<td>" + optHistoryStatus(h) + "</td>" +
|
||||
'<td class="' + pnlCls + '">' + pnlTxt + "</td>" +
|
||||
"<td>" + (h.closed_at || h.created_at || "—") + "</td>" +
|
||||
'<td><button type="button" class="btn-secondary btn-sm opt-history-del" data-id="' + h.id + '" data-status="' + (h.status || "") + '">删除</button></td>';
|
||||
|
||||
Reference in New Issue
Block a user