Refine options position accordion: default collapsed, 2dp PnL, card outside scroll.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3213,6 +3213,29 @@ html[data-theme="light"] .options-estimate-row {
|
|||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: #b8c0dc;
|
color: #b8c0dc;
|
||||||
}
|
}
|
||||||
|
.options-pos-live-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.opt-pos-expanded-host {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.opt-pos-expanded-host .opt-pos-card {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.options-pos-live-pane--bars-only {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 72px;
|
||||||
|
max-height: 140px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.options-pos-subcard .options-pos-live-layout {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
.options-pos-live-pane {
|
.options-pos-live-pane {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
@@ -3284,8 +3307,7 @@ html[data-theme="light"] .options-estimate-row {
|
|||||||
background: #171d2a;
|
background: #171d2a;
|
||||||
}
|
}
|
||||||
.opt-pos-accordion-item.is-expanded .opt-pos-bar {
|
.opt-pos-accordion-item.is-expanded .opt-pos-bar {
|
||||||
border-radius: 8px 8px 0 0;
|
border-color: #4a6fd8;
|
||||||
border-bottom-color: transparent;
|
|
||||||
background: #171d2a;
|
background: #171d2a;
|
||||||
}
|
}
|
||||||
.opt-pos-bar-main {
|
.opt-pos-bar-main {
|
||||||
@@ -3337,19 +3359,6 @@ html[data-theme="light"] .options-estimate-row {
|
|||||||
.opt-pos-accordion-item.is-expanded .opt-pos-bar-chevron {
|
.opt-pos-accordion-item.is-expanded .opt-pos-bar-chevron {
|
||||||
transform: rotate(90deg);
|
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 {
|
.options-page-wrap .opt-pos-card .pos-card-symbol strong {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -457,7 +457,7 @@
|
|||||||
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
|
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
|
||||||
'<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 + '">' + fmt(p.upl, 2) + "</span></div>" +
|
||||||
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
'<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>" +
|
(p.upl_ratio_pct != null ? fmt(p.upl_ratio_pct, 2) + "%" : "—") + "</span></div>" +
|
||||||
"</div>"
|
"</div>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -470,7 +470,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPositionAccordionItem(p, expanded) {
|
function renderPositionBar(p, expanded) {
|
||||||
const upl = p.upl;
|
const upl = p.upl;
|
||||||
const uplCls = upl > 0 ? "pos-pnl-profit" : upl < 0 ? "pos-pnl-loss" : "";
|
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 sideCls = (p.opt_type || "").toUpperCase() === "P" ? "pos-side-short" : "pos-side-long";
|
||||||
@@ -492,17 +492,51 @@
|
|||||||
: "") +
|
: "") +
|
||||||
'<span class="opt-pos-bar-pnl ' + uplCls + '">' + fmt(p.upl, 2) + " USDC</span>" +
|
'<span class="opt-pos-bar-pnl ' + uplCls + '">' + fmt(p.upl, 2) + " USDC</span>" +
|
||||||
'<span class="opt-pos-bar-roi ' + uplCls + '">' +
|
'<span class="opt-pos-bar-roi ' + uplCls + '">' +
|
||||||
(p.upl_ratio_pct != null ? p.upl_ratio_pct + "%" : "—") + "</span>" +
|
(p.upl_ratio_pct != null ? fmt(p.upl_ratio_pct, 2) + "%" : "—") + "</span>" +
|
||||||
"</span>" +
|
"</span>" +
|
||||||
"</button>" +
|
"</button></div>"
|
||||||
'<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) {
|
function renderExpandedPositionCard(p) {
|
||||||
|
return renderPositionCard(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAccordionState(list) {
|
||||||
|
const wrap = document.getElementById("opt-pos-cards");
|
||||||
|
const expandedHost = document.getElementById("opt-pos-expanded-host");
|
||||||
|
const livePane = document.getElementById("opt-pos-live");
|
||||||
|
if (!wrap) return;
|
||||||
|
const expanded = (list || []).find(function (p) { return p.inst_id === state.expandedPosInst; }) || null;
|
||||||
|
wrap.querySelectorAll(".opt-pos-accordion-item").forEach(function (el) {
|
||||||
|
const open = el.getAttribute("data-inst") === state.expandedPosInst;
|
||||||
|
el.classList.toggle("is-expanded", open);
|
||||||
|
const btn = el.querySelector(".opt-pos-bar");
|
||||||
|
if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
|
});
|
||||||
|
if (expandedHost) {
|
||||||
|
if (expanded) {
|
||||||
|
expandedHost.hidden = false;
|
||||||
|
expandedHost.setAttribute("aria-hidden", "false");
|
||||||
|
expandedHost.innerHTML = renderExpandedPositionCard(expanded);
|
||||||
|
expandedHost.querySelectorAll(".opt-close-btn").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
closePosition(btn.getAttribute("data-inst"), btn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
expandedHost.hidden = true;
|
||||||
|
expandedHost.setAttribute("aria-hidden", "true");
|
||||||
|
expandedHost.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (livePane) {
|
||||||
|
livePane.classList.toggle("options-pos-live-pane--bars-only", !!(list && list.length >= 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindPositionActions(container, list) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.querySelectorAll(".opt-close-btn").forEach(function (btn) {
|
container.querySelectorAll(".opt-close-btn").forEach(function (btn) {
|
||||||
btn.addEventListener("click", function (e) {
|
btn.addEventListener("click", function (e) {
|
||||||
@@ -516,15 +550,10 @@
|
|||||||
if (!item) return;
|
if (!item) return;
|
||||||
const inst = item.getAttribute("data-inst");
|
const inst = item.getAttribute("data-inst");
|
||||||
state.expandedPosInst = state.expandedPosInst === inst ? null : inst;
|
state.expandedPosInst = state.expandedPosInst === inst ? null : inst;
|
||||||
const wrap = document.getElementById("opt-pos-cards");
|
applyAccordionState(list);
|
||||||
wrap.querySelectorAll(".opt-pos-accordion-item").forEach(function (el) {
|
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||||
const open = el.getAttribute("data-inst") === state.expandedPosInst;
|
OptionsExpiryCountdown.ensureTimer();
|
||||||
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");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -564,10 +593,19 @@
|
|||||||
const d = await apiJson("/api/options/positions");
|
const d = await apiJson("/api/options/positions");
|
||||||
const wrap = document.getElementById("opt-pos-cards");
|
const wrap = document.getElementById("opt-pos-cards");
|
||||||
const empty = document.getElementById("opt-pos-empty");
|
const empty = document.getElementById("opt-pos-empty");
|
||||||
|
const expandedHost = document.getElementById("opt-pos-expanded-host");
|
||||||
|
const livePane = document.getElementById("opt-pos-live");
|
||||||
const list = (d.ok && d.positions) || [];
|
const list = (d.ok && d.positions) || [];
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
|
if (expandedHost) {
|
||||||
|
expandedHost.hidden = true;
|
||||||
|
expandedHost.setAttribute("aria-hidden", "true");
|
||||||
|
expandedHost.innerHTML = "";
|
||||||
|
}
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
empty.style.display = "";
|
empty.style.display = "";
|
||||||
|
state.expandedPosInst = null;
|
||||||
|
if (livePane) livePane.classList.remove("options-pos-live-pane--bars-only");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
empty.style.display = "none";
|
empty.style.display = "none";
|
||||||
@@ -575,23 +613,26 @@
|
|||||||
wrap.classList.toggle("opt-pos-cards--accordion", multi);
|
wrap.classList.toggle("opt-pos-cards--accordion", multi);
|
||||||
if (multi) {
|
if (multi) {
|
||||||
const ids = list.map(function (p) { return p.inst_id; });
|
const ids = list.map(function (p) { return p.inst_id; });
|
||||||
if (!state.expandedPosInst || ids.indexOf(state.expandedPosInst) < 0) {
|
if (state.expandedPosInst && ids.indexOf(state.expandedPosInst) < 0) {
|
||||||
state.expandedPosInst = list[0].inst_id || null;
|
state.expandedPosInst = null;
|
||||||
}
|
}
|
||||||
list.forEach(function (p) {
|
list.forEach(function (p) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.innerHTML = renderPositionAccordionItem(p, p.inst_id === state.expandedPosInst);
|
div.innerHTML = renderPositionBar(p, p.inst_id === state.expandedPosInst);
|
||||||
wrap.appendChild(div.firstChild);
|
wrap.appendChild(div.firstChild);
|
||||||
});
|
});
|
||||||
|
applyAccordionState(list);
|
||||||
|
bindPositionActions(wrap, list);
|
||||||
} else {
|
} else {
|
||||||
state.expandedPosInst = null;
|
state.expandedPosInst = null;
|
||||||
|
if (livePane) livePane.classList.remove("options-pos-live-pane--bars-only");
|
||||||
list.forEach(function (p) {
|
list.forEach(function (p) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.innerHTML = renderPositionCard(p);
|
div.innerHTML = renderPositionCard(p);
|
||||||
wrap.appendChild(div.firstChild);
|
wrap.appendChild(div.firstChild);
|
||||||
});
|
});
|
||||||
|
bindPositionActions(wrap, list);
|
||||||
}
|
}
|
||||||
bindPositionActions(wrap);
|
|
||||||
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||||
OptionsExpiryCountdown.ensureTimer();
|
OptionsExpiryCountdown.ensureTimer();
|
||||||
}
|
}
|
||||||
@@ -617,11 +658,11 @@
|
|||||||
if (closedEl) closedEl.textContent = String(d.total_closed || 0);
|
if (closedEl) closedEl.textContent = String(d.total_closed || 0);
|
||||||
if (profitEl) {
|
if (profitEl) {
|
||||||
profitEl.textContent = d.total_profit != null && d.total_profit > 0
|
profitEl.textContent = d.total_profit != null && d.total_profit > 0
|
||||||
? fmt(d.total_profit, 4) + " USDC" : (d.total_closed ? "0 USDC" : "—");
|
? fmt(d.total_profit, 2) + " USDC" : (d.total_closed ? "0 USDC" : "—");
|
||||||
}
|
}
|
||||||
if (lossEl) {
|
if (lossEl) {
|
||||||
lossEl.textContent = d.total_loss != null && d.total_loss > 0
|
lossEl.textContent = d.total_loss != null && d.total_loss > 0
|
||||||
? fmt(d.total_loss, 4) + " USDC" : (d.total_closed ? "0 USDC" : "—");
|
? fmt(d.total_loss, 2) + " USDC" : (d.total_closed ? "0 USDC" : "—");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,7 +701,7 @@
|
|||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
const prem = h.status === "closed" ? h.premium_received : h.premium_paid;
|
const prem = h.status === "closed" ? h.premium_received : h.premium_paid;
|
||||||
const pnl = h.realized_pnl;
|
const pnl = h.realized_pnl;
|
||||||
const pnlTxt = pnl != null ? fmt(pnl, 4) : "—";
|
const pnlTxt = pnl != null ? fmt(pnl, 2) : "—";
|
||||||
const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : "";
|
const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : "";
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
"<td><code>" + (h.inst_id || "") + "</code></td>" +
|
"<td><code>" + (h.inst_id || "") + "</code></td>" +
|
||||||
|
|||||||
@@ -81,10 +81,13 @@
|
|||||||
<div class="options-pos-stack">
|
<div class="options-pos-stack">
|
||||||
<div class="card-nested options-pos-subcard">
|
<div class="card-nested options-pos-subcard">
|
||||||
<h3>当前持仓</h3>
|
<h3>当前持仓</h3>
|
||||||
|
<div class="options-pos-live-layout">
|
||||||
<div id="opt-pos-live" class="panel-scroll pos-list options-pos-live-pane">
|
<div id="opt-pos-live" class="panel-scroll pos-list options-pos-live-pane">
|
||||||
<div class="pos-empty" id="opt-pos-empty">暂无持仓</div>
|
<div class="pos-empty" id="opt-pos-empty">暂无持仓</div>
|
||||||
<div id="opt-pos-cards"></div>
|
<div id="opt-pos-cards"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="opt-pos-expanded-host" class="opt-pos-expanded-host" hidden aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-nested options-pos-subcard options-pos-stats-card">
|
<div class="card-nested options-pos-subcard options-pos-stats-card">
|
||||||
<h3>数据统计</h3>
|
<h3>数据统计</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user