Options: eth-amount dialog, stop note autofill, pending tab in positions.

Selecting 指定币数量 opens an order dialog; pending orders move into a 当前委托 tab after 当前持仓; harden remark autofill that showed dekun.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-20 08:00:39 +08:00
parent debfb116fd
commit 19b46d19fd
5 changed files with 279 additions and 48 deletions
+56 -21
View File
@@ -3928,22 +3928,11 @@ html[data-theme="light"] .options-strike-table--t .opt-strike-row-atm td {
border-radius: 8px;
}
.opt-order-layout {
display: flex;
align-items: stretch;
gap: 14px;
display: block;
}
.opt-order-main {
flex: 1 1 auto;
min-width: 0;
}
.opt-order-pending {
flex: 0 0 280px;
max-width: 320px;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid rgba(158, 192, 255, 0.2);
background: rgba(0, 0, 0, 0.18);
}
.opt-order-pending-head {
display: flex;
align-items: center;
@@ -3973,6 +3962,12 @@ html[data-theme="light"] .options-strike-table--t .opt-strike-row-atm td {
max-height: 220px;
overflow: auto;
}
.opt-pending-list--tab {
max-height: min(52vh, 420px);
}
.opt-pos-pending-pane {
padding: 4px 2px 8px;
}
.opt-pending-empty {
font-size: 0.72rem;
}
@@ -4010,21 +4005,61 @@ html[data-theme="light"] .options-strike-table--t .opt-strike-row-atm td {
font-size: 0.68rem;
padding: 2px 8px;
}
html[data-theme="light"] .opt-order-pending {
background: rgba(0, 0, 0, 0.03);
border-color: rgba(0, 0, 0, 0.08);
.opt-eth-order-backdrop {
position: fixed;
inset: 0;
z-index: 2100;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(0, 0, 0, 0.72);
}
.opt-eth-order-backdrop[hidden] {
display: none !important;
}
.opt-eth-order-dialog {
width: min(92vw, 420px);
background: var(--card-bg, #121726);
color: inherit;
border: 1px solid rgba(127, 127, 127, 0.35);
border-radius: 12px;
padding: 16px 18px 18px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
}
.opt-eth-order-dialog h3 {
margin: 0 0 8px;
font-size: 0.95rem;
}
.opt-eth-order-label {
display: block;
font-size: 0.76rem;
margin-bottom: 6px;
opacity: 0.85;
}
.opt-eth-order-dialog input[type="number"] {
width: 100%;
box-sizing: border-box;
font-size: 0.9rem;
padding: 8px 10px;
border-radius: 8px;
}
.opt-eth-order-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.opt-eth-order-actions .btn-primary,
.opt-eth-order-actions .btn-secondary {
flex: 1 1 140px;
}
html[data-theme="light"] .opt-pending-item {
background: #fff;
border-color: rgba(0, 0, 0, 0.08);
}
@media (max-width: 900px) {
.opt-order-layout {
flex-direction: column;
}
.opt-order-pending {
flex: 1 1 auto;
max-width: none;
.opt-pending-list--tab {
max-height: min(46vh, 360px);
}
}
.opt-order-panel-inner .opt-order-title {
+183 -10
View File
@@ -276,9 +276,144 @@
function updateSizeInputs() {
const mode = currentSizeMode();
const sheetsEl = document.getElementById("opt-sheets-amount");
const ethEl = document.getElementById("opt-eth-amount");
if (sheetsEl) sheetsEl.style.display = mode === "sheets" ? "" : "none";
if (ethEl) ethEl.style.display = mode === "eth_amount" ? "" : "none";
}
function hardenOrderAutofill() {
const note = document.getElementById("opt-signal-note");
if (!note) return;
function wipeNote() {
// 浏览器常把用户名(dekun)填进备注框
if (/^[a-z][a-z0-9._-]{1,31}$/i.test(String(note.value || "").trim())) {
note.value = "";
}
}
wipeNote();
note.addEventListener("focus", function () {
note.removeAttribute("readonly");
});
note.addEventListener("blur", function () {
if (!note.value) note.setAttribute("readonly", "readonly");
});
setTimeout(wipeNote, 200);
setTimeout(wipeNote, 800);
}
function hideEthOrderDialog() {
const bd = document.getElementById("opt-eth-order-backdrop");
if (bd) bd.hidden = true;
}
function openEthOrderDialog() {
if (!state.selectedInst) {
alert("请先选择合约");
const sheetsRadio = document.querySelector('input[name="opt-size-mode"][value="sheets"]');
if (sheetsRadio) {
sheetsRadio.checked = true;
updateSizeInputs();
}
return;
}
const bd = document.getElementById("opt-eth-order-backdrop");
const instEl = document.getElementById("opt-eth-order-inst");
const amountEl = document.getElementById("opt-eth-order-amount");
const preview = document.getElementById("opt-eth-order-preview");
const msg = document.getElementById("opt-eth-order-msg");
if (instEl) instEl.textContent = state.selectedInst;
if (amountEl) {
amountEl.value = document.getElementById("opt-eth-amount").value || "";
amountEl.focus();
}
if (preview) preview.textContent = "输入币数量后显示张数与权利金估算";
if (msg) {
msg.textContent = "";
msg.classList.remove("opt-error");
}
if (bd) bd.hidden = false;
refreshEthOrderPreview();
}
async function refreshEthOrderPreview() {
const preview = document.getElementById("opt-eth-order-preview");
const amountEl = document.getElementById("opt-eth-order-amount");
if (!preview || !amountEl || !state.selectedInst) return;
const eth = parseFloat(amountEl.value);
if (!Number.isFinite(eth) || eth <= 0) {
preview.textContent = "输入币数量后显示张数与权利金估算";
return;
}
preview.textContent = "估算中…";
try {
const url =
"/api/options/quote?inst_id=" +
encodeURIComponent(state.selectedInst) +
"&mode=eth_amount&eth_amount=" +
encodeURIComponent(String(eth));
const d = await apiJson(url);
if (!d || !d.ok) {
preview.textContent = (d && d.msg) || "报价失败";
return;
}
const sz = d.sizing || {};
if (sz.ok === false) {
preview.textContent = sz.msg || "张数无效";
return;
}
const sheets = sz.sheets != null ? sz.sheets : "—";
const prem = sz.total_premium != null ? sz.total_premium : null;
const premTxt = prem != null ? fmt(prem, 2) + " USDC" : "—";
preview.textContent = "约 " + sheets + " 张 · 预估权利金 " + premTxt;
} catch (e) {
preview.textContent = "报价失败";
}
}
async function submitEthOrderDialog() {
const amountEl = document.getElementById("opt-eth-order-amount");
const msg = document.getElementById("opt-eth-order-msg");
const submitBtn = document.getElementById("opt-eth-order-submit");
const eth = amountEl ? parseFloat(amountEl.value) : NaN;
if (!Number.isFinite(eth) || eth <= 0) {
if (msg) {
msg.textContent = "请输入有效币数量";
msg.classList.add("opt-error");
}
return;
}
const hidden = document.getElementById("opt-eth-amount");
if (hidden) hidden.value = String(eth);
const ethRadio = document.querySelector('input[name="opt-size-mode"][value="eth_amount"]');
if (ethRadio) ethRadio.checked = true;
updateSizeInputs();
if (submitBtn) submitBtn.disabled = true;
try {
if (state.selectedInst) {
await selectContract(state.selectedInst, null, true);
}
const ok = await openPosition();
if (ok) {
hideEthOrderDialog();
setOptionsPosTab("pending");
} else if (msg) {
const orderMsg = document.getElementById("opt-order-msg");
msg.textContent = (orderMsg && orderMsg.textContent) || "下单失败";
msg.classList.add("opt-error");
}
} finally {
if (submitBtn) submitBtn.disabled = false;
}
}
function cancelEthOrderDialog() {
hideEthOrderDialog();
const sheetsRadio = document.querySelector('input[name="opt-size-mode"][value="sheets"]');
if (sheetsRadio) {
sheetsRadio.checked = true;
updateSizeInputs();
}
const hidden = document.getElementById("opt-eth-amount");
if (hidden) hidden.value = "";
if (state.selectedInst) selectContract(state.selectedInst, null, true);
}
function quoteUrl(instId) {
@@ -1106,16 +1241,16 @@
async function openPosition() {
if (!state.selectedInst) {
alert("请先选择合约");
return;
return false;
}
const q = state.orderQuote;
if (!q || !q.ok || !q.can_open) {
alert((q && (q.msg || q.open_block_msg)) || "暂无卖一深度,无法按卖一开仓");
return;
return false;
}
if (q.sizing && q.sizing.ok === false) {
alert(q.sizing.msg || "张数无效");
return;
return false;
}
const btn = document.getElementById("opt-open-btn");
btn.disabled = true;
@@ -1136,7 +1271,7 @@
const tgt = parseFloat(tgtRaw);
if (!Number.isFinite(tgt) || tgt <= 0) {
alert("目标位无效");
return;
return false;
}
body.target_index = tgt;
}
@@ -1146,16 +1281,17 @@
body: JSON.stringify(body),
});
const msgEl = document.getElementById("opt-order-msg");
msgEl.textContent = d.ok ? "下单已提交,右侧可查看/撤销未成交委托" : (d.msg || "失败");
msgEl.textContent = d.ok ? "下单已提交,可在「当前委托」查看/撤销" : (d.msg || "失败");
msgEl.classList.toggle("opt-error", !d.ok);
if (d.ok) {
refreshPendingOrders();
startPendingOrdersPoll();
refreshAllPositions();
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
} else {
alert(d.msg || "下单失败");
return true;
}
alert(d.msg || "下单失败");
return false;
} finally {
const latest = state.orderQuote;
btn.disabled = !(latest && latest.ok && latest.can_open && !(latest.sizing && latest.sizing.ok === false));
@@ -1587,6 +1723,10 @@
if (tab === "live" && window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
OptionsExpiryCountdown.ensureTimer();
}
if (tab === "pending") {
refreshPendingOrders();
startPendingOrdersPoll();
}
}
function bindOptionsPosTabs() {
@@ -2047,15 +2187,48 @@
});
}
bindOptionsPosTabs();
hardenOrderAutofill();
document.querySelectorAll('input[name="opt-size-mode"]').forEach(function (r) {
r.addEventListener("change", function () {
const mode = currentSizeMode();
updateSizeInputs();
if (mode === "eth_amount") {
openEthOrderDialog();
return;
}
if (state.selectedInst) selectContract(state.selectedInst, null, true);
});
});
["opt-sheets-amount", "opt-eth-amount", "opt-target-idx"].forEach(function (id) {
const ethBackdrop = document.getElementById("opt-eth-order-backdrop");
const ethCancel = document.getElementById("opt-eth-order-cancel");
const ethSubmit = document.getElementById("opt-eth-order-submit");
const ethAmount = document.getElementById("opt-eth-order-amount");
if (ethCancel) ethCancel.addEventListener("click", cancelEthOrderDialog);
if (ethSubmit) ethSubmit.addEventListener("click", submitEthOrderDialog);
if (ethAmount) {
ethAmount.addEventListener("input", refreshEthOrderPreview);
ethAmount.addEventListener("change", refreshEthOrderPreview);
ethAmount.addEventListener("keydown", function (ev) {
if (ev.key === "Enter") {
ev.preventDefault();
submitEthOrderDialog();
}
});
}
if (ethBackdrop) {
ethBackdrop.addEventListener("click", function (ev) {
if (ev.target === ethBackdrop) cancelEthOrderDialog();
});
}
document.addEventListener("keydown", function (ev) {
if (ev.key !== "Escape") return;
const bd = document.getElementById("opt-eth-order-backdrop");
if (bd && !bd.hidden) cancelEthOrderDialog();
});
["opt-sheets-amount", "opt-target-idx"].forEach(function (id) {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener("change", function () {
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<link rel="stylesheet" href="/static/instance_page.css?v=11">
<link rel="stylesheet" href="/static/instance_theme.css?v=97">
<link rel="stylesheet" href="/static/instance_theme.css?v=98">
<script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14">
<title>{{ pwa_app_name }}</title>
+1 -1
View File
@@ -17,7 +17,7 @@
<link rel="manifest" href="/static/icons/manifest.webmanifest">
<title>{{ pwa_app_name }}</title>
<link rel="stylesheet" href="/static/instance_page.css?v=11">
<link rel="stylesheet" href="/static/instance_theme.css?v=97">
<link rel="stylesheet" href="/static/instance_theme.css?v=98">
</head>
<body
+38 -15
View File
@@ -94,30 +94,41 @@
</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="张数">
<input type="number" id="opt-sheets-amount" min="1" step="1" value="1" placeholder="张数"
autocomplete="off" inputmode="numeric" data-lpignore="true" data-1p-ignore="true" data-form-type="other">
<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="备注(关键位说明)">
<label><input type="radio" name="opt-size-mode" value="eth_amount" id="opt-size-mode-eth"> 指定币数量</label>
{# 实际数量在弹窗录入;隐藏字段仅给报价/下单读 #}
<input type="hidden" id="opt-eth-amount" value="">
<input type="text" id="opt-signal-note" name="opt_signal_note" placeholder="备注(关键位说明)"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-lpignore="true" data-1p-ignore="true" data-form-type="other" readonly>
<button type="button" class="btn-primary" id="opt-open-btn">限价买入 @ 卖一</button>
</div>
<div id="opt-order-msg" class="muted"></div>
</div>
<aside class="opt-order-pending" aria-label="未成交委托">
<div class="opt-order-pending-head">
<h4 class="opt-order-pending-title">委托</h4>
<button type="button" class="btn-secondary" id="opt-pending-refresh">刷新</button>
</div>
<p class="muted opt-pending-ttl-hint" id="opt-pending-ttl-hint">平仓限价超 10 分未成交将自动撤销</p>
<div id="opt-pending-list" class="opt-pending-list">
<div class="muted opt-pending-empty">暂无未成交委托</div>
</div>
</aside>
</div>
</div>
</div>
</div>
{# 指定币数量下单对话框 #}
<div id="opt-eth-order-backdrop" class="opt-eth-order-backdrop" hidden>
<div class="opt-eth-order-dialog" role="dialog" aria-modal="true" aria-labelledby="opt-eth-order-title">
<h3 id="opt-eth-order-title">指定币数量下单</h3>
<p class="muted" id="opt-eth-order-inst" style="margin:0 0 10px;font-size:.78rem;word-break:break-all"></p>
<label class="opt-eth-order-label" for="opt-eth-order-amount">币数量</label>
<input type="number" id="opt-eth-order-amount" min="0.01" step="0.01" placeholder="如 0.5"
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other">
<div id="opt-eth-order-preview" class="muted" style="margin:8px 0 12px;font-size:.74rem;min-height:1.2em">输入币数量后显示张数与权利金估算</div>
<div class="opt-eth-order-actions">
<button type="button" class="btn-primary" id="opt-eth-order-submit">限价买入 @ 卖一</button>
<button type="button" class="btn-secondary" id="opt-eth-order-cancel">取消</button>
</div>
<div id="opt-eth-order-msg" class="muted" style="margin-top:8px;font-size:.72rem"></div>
</div>
</div>
<div class="card options-pos-card-wrap">
<div class="options-pos-head">
<h2>持仓</h2>
@@ -125,6 +136,7 @@
</div>
<div class="options-pos-tabs" role="tablist" aria-label="持仓面板">
<button type="button" class="btn-secondary opt-pos-tab active" data-opt-pos-tab="live" role="tab" aria-selected="true" id="opt-pos-tab-live">当前持仓</button>
<button type="button" class="btn-secondary opt-pos-tab" data-opt-pos-tab="pending" role="tab" aria-selected="false" id="opt-pos-tab-pending">当前委托</button>
<button type="button" class="btn-secondary opt-pos-tab" data-opt-pos-tab="stats" role="tab" aria-selected="false" id="opt-pos-tab-stats">数据统计</button>
<button type="button" class="btn-secondary opt-pos-tab" data-opt-pos-tab="history" role="tab" aria-selected="false" id="opt-pos-tab-history">期权历史</button>
</div>
@@ -152,6 +164,17 @@
</div>
</details>
</div>
<div class="options-pos-pane" data-opt-pos-pane="pending" role="tabpanel" aria-labelledby="opt-pos-tab-pending" hidden>
<div class="opt-pos-pending-pane">
<div class="opt-order-pending-head">
<p class="muted opt-pending-ttl-hint" id="opt-pending-ttl-hint" style="margin:0;flex:1">平仓限价超 10 分未成交将自动撤销</p>
<button type="button" class="btn-secondary" id="opt-pending-refresh">刷新</button>
</div>
<div id="opt-pending-list" class="opt-pending-list opt-pending-list--tab">
<div class="muted opt-pending-empty">暂无未成交委托</div>
</div>
</div>
</div>
<div class="options-pos-pane" data-opt-pos-pane="stats" role="tabpanel" aria-labelledby="opt-pos-tab-stats" hidden>
<div class="options-stats-panel">
<div class="options-stats-pnl-summary" id="opt-stats-pnl-summary">
@@ -274,4 +297,4 @@
</div>
</div>
<script src="/static/options_expiry_countdown.js?v=1"></script>
<script src="/static/options_panel.js?v=40"></script>
<script src="/static/options_panel.js?v=41"></script>