diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index f3e5f64..5a35936 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -5297,6 +5297,68 @@ html[data-theme="light"] .opt-source-badge--oo { border-color: #ffb347; color: #ffb347; } +.options-page-wrap .opt-pos-transfer { + margin-top: 10px; + padding: 10px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); +} +.options-page-wrap .opt-pos-transfer-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + cursor: pointer; + list-style: none; + font-weight: 600; + font-size: 0.88rem; + margin: 0 0 8px; +} +.options-page-wrap .opt-pos-transfer-head::-webkit-details-marker { + display: none; +} +.options-page-wrap .opt-pos-transfer[open] .opt-pos-transfer-closed-hint { + display: none; +} +.options-page-wrap .opt-pos-transfer:not([open]) .opt-pos-transfer-open-hint { + display: none; +} +.options-page-wrap .opt-pos-transfer:not([open]) .opt-pos-transfer-body { + display: none; +} +.options-page-wrap .opt-pos-transfer:not([open]) .opt-pos-transfer-head { + margin-bottom: 0; +} +.options-page-wrap .opt-pos-transfer-body .options-settings-subtitle { + margin-bottom: 6px; +} +.options-page-wrap .opt-pos-transfer-form { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; +} +.options-page-wrap .opt-pos-transfer-form select, +.options-page-wrap .opt-pos-transfer-form input[type="number"] { + font-size: 0.74rem; +} +.options-page-wrap .opt-pos-transfer-form input[type="number"] { + width: 96px; + max-width: 30vw; +} +.options-page-wrap .opt-pos-xfer-msg { + margin-top: 6px; + font-size: 0.76rem; + min-height: 1.1em; +} +.options-page-wrap .opt-pos-xfer-msg.opt-error { + color: #ff7b72; +} +.options-page-wrap .opt-pos-xfer-msg.opt-success { + color: #3dd68c; +} .opt-pos-card { margin-bottom: 10px; } diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index a2d12b4..96ad516 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -373,6 +373,164 @@ } } + function setXferMsg(text, isErr) { + const el = document.getElementById("opt-pos-xfer-msg"); + if (!el) return; + el.textContent = text || ""; + el.classList.toggle("opt-error", !!isErr); + el.classList.toggle("opt-success", !!text && !isErr); + } + + function xferRoundAvail(v) { + const n = Number(v); + if (!Number.isFinite(n) || n <= 0) return null; + return Math.round(n * 100) / 100; + } + + function xferPickBalance(bal, account, ccy) { + if (!bal) return null; + const acct = account === "trading" ? "trading" : "funding"; + const c = String(ccy || "").toLowerCase(); + const availKey = acct + "_" + c + "_avail"; + const totalKey = acct + "_" + c; + return xferRoundAvail(bal[availKey] != null ? bal[availKey] : bal[totalKey]); + } + + function xferAccountLabel(acct) { + return acct === "trading" ? "交易账户" : "资金账户"; + } + + function setPosXferBusy(busy) { + ["opt-pos-xfer-btn", "opt-pos-xfer-all-btn"].forEach(function (id) { + const btn = document.getElementById(id); + if (!btn) return; + if (busy) { + if (!btn.dataset.origText) btn.dataset.origText = btn.textContent; + btn.disabled = true; + } else { + btn.disabled = false; + if (btn.dataset.origText) { + btn.textContent = btn.dataset.origText; + delete btn.dataset.origText; + } + } + }); + const input = document.getElementById("opt-pos-xfer-amount"); + if (input) input.disabled = !!busy; + } + + async function submitPosTransfer(amount) { + const ccyEl = document.getElementById("opt-pos-xfer-ccy"); + const fromEl = document.getElementById("opt-pos-xfer-from"); + const toEl = document.getElementById("opt-pos-xfer-to"); + if (!ccyEl || !fromEl || !toEl) return { ok: false }; + setPosXferBusy(true); + setXferMsg("划转中…", false); + try { + const r = await apiJson("/api/options/transfer", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ccy: ccyEl.value, + from: fromEl.value, + to: toEl.value, + amount: amount, + }), + }); + if (r.ok) { + setXferMsg("划转成功", false); + const amtEl = document.getElementById("opt-pos-xfer-amount"); + if (amtEl) amtEl.value = ""; + refreshAllPositions(); + if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot({ force: true }); + await refreshBridgeSellUi(true); + } else { + setXferMsg("划转失败:" + (r.msg || "未知错误"), true); + } + return r; + } catch (e) { + setXferMsg("划转失败:" + (e.message || "网络错误"), true); + return { ok: false }; + } finally { + setPosXferBusy(false); + } + } + + function bindPosTransferControls() { + const xferBtn = document.getElementById("opt-pos-xfer-btn"); + const xferAllBtn = document.getElementById("opt-pos-xfer-all-btn"); + const amtEl = document.getElementById("opt-pos-xfer-amount"); + if (!xferBtn && !xferAllBtn) return; + + function hardenXferAmountInput() { + if (!amtEl) return; + function wipe() { + const v = String(amtEl.value || "").trim(); + if (/^[a-z][a-z0-9._-]{1,31}$/i.test(v)) amtEl.value = ""; + } + wipe(); + amtEl.setAttribute("readonly", "readonly"); + amtEl.addEventListener("focus", function () { + amtEl.removeAttribute("readonly"); + }); + amtEl.addEventListener("blur", function () { + if (!amtEl.value) amtEl.setAttribute("readonly", "readonly"); + }); + setTimeout(wipe, 200); + setTimeout(wipe, 800); + } + hardenXferAmountInput(); + + if (xferBtn) { + xferBtn.addEventListener("click", async function () { + const amount = parseFloat((amtEl || {}).value); + if (!amount || amount <= 0) { + setXferMsg("请输入有效数量", true); + return; + } + await submitPosTransfer(amount); + }); + } + + if (xferAllBtn) { + xferAllBtn.addEventListener( + "click", + function () { + if (amtEl) amtEl.removeAttribute("readonly"); + }, + true + ); + xferAllBtn.addEventListener("click", async function () { + try { + const ccyEl = document.getElementById("opt-pos-xfer-ccy"); + const fromEl = document.getElementById("opt-pos-xfer-from"); + const toEl = document.getElementById("opt-pos-xfer-to"); + const bal = await refreshBridgeSellUi(true); + const ccy = ccyEl ? ccyEl.value : "USDT"; + const from = fromEl ? fromEl.value : "funding"; + const to = toEl ? toEl.value : "trading"; + const amount = xferPickBalance(bal, from, ccy); + if (!amount) { + setXferMsg("划出账户可用余额不足", true); + return; + } + const msg = + "确认全部划转?\n\n" + + "币种:" + ccy + "\n" + + "划出:" + xferAccountLabel(from) + "\n" + + "划入:" + xferAccountLabel(to) + "\n" + + "金额:" + amount.toFixed(2) + " " + ccy + "\n\n" + + "将划转该账户全部可用余额。"; + if (!confirm(msg)) return; + if (amtEl) amtEl.value = String(amount); + await submitPosTransfer(amount); + } catch (e) { + setXferMsg("划转失败:" + (e.message || "余额拉取失败"), true); + } + }); + } + } + async function retrySellBackCoin() { const uly = String(state.underlying || "ETH").toUpperCase(); const d = await refreshBridgeSellUi(true); @@ -2860,6 +3018,7 @@ }); } bindOptionsPosTabs(); + bindPosTransferControls(); hardenOrderAutofill(); (function bindProfitExitOpenControls() { diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 0b90c44..2e9596a 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -18,9 +18,9 @@
-平仓前重新读盘口并校验有效流动性;市价平仓已禁用。
-reduceOnly 限价卖,不吃买二及以下、不走市价。