Add confirm dialogs and busy states for swap/transfer all actions.
Use available (free) balances for full-amount ops, support sub-account scope, and show clear success/failure feedback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,10 @@
|
||||
const root = document.getElementById("options-settings-root");
|
||||
if (!root) return;
|
||||
|
||||
const SWAP_BTNS = ["opt-set-swap-btn", "opt-set-swap-all-btn"];
|
||||
const INT_BTNS = ["opt-set-int-btn", "opt-set-int-all-btn"];
|
||||
const CROSS_BTNS = ["opt-set-cross-btn", "opt-set-cross-all-btn"];
|
||||
|
||||
async function apiJson(url, opts) {
|
||||
const r = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
|
||||
return r.json();
|
||||
@@ -22,16 +26,66 @@
|
||||
if (!el) return;
|
||||
el.textContent = text || "";
|
||||
el.classList.toggle("opt-error", !!isErr);
|
||||
el.classList.toggle("opt-success", !!text && !isErr);
|
||||
}
|
||||
|
||||
function floorAmount(v) {
|
||||
function fmtAmt(amount, ccy) {
|
||||
return `${Number(amount).toFixed(2)} ${ccy}`;
|
||||
}
|
||||
|
||||
function accountLabel(acct) {
|
||||
return acct === "trading" ? "交易账户" : "资金账户";
|
||||
}
|
||||
|
||||
function swapDirLabel(dir) {
|
||||
return dir === "usdc_to_usdt" ? "USDC → USDT" : "USDT → USDC";
|
||||
}
|
||||
|
||||
function confirmOk(message) {
|
||||
return window.confirm(message);
|
||||
}
|
||||
|
||||
function setButtonsBusy(btnIds, busy, busyText) {
|
||||
btnIds.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;
|
||||
if (busyText) btn.textContent = busyText;
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
if (btn.dataset.origText) {
|
||||
btn.textContent = btn.dataset.origText;
|
||||
delete btn.dataset.origText;
|
||||
}
|
||||
}
|
||||
});
|
||||
const amountIds = {
|
||||
"opt-set-swap-btn": "opt-set-swap-amount",
|
||||
"opt-set-swap-all-btn": "opt-set-swap-amount",
|
||||
"opt-set-int-btn": "opt-set-int-amount",
|
||||
"opt-set-int-all-btn": "opt-set-int-amount",
|
||||
"opt-set-cross-btn": "opt-set-cross-amount",
|
||||
"opt-set-cross-all-btn": "opt-set-cross-amount",
|
||||
};
|
||||
btnIds.forEach(function (id) {
|
||||
const input = document.getElementById(amountIds[id]);
|
||||
if (input) input.disabled = busy;
|
||||
});
|
||||
}
|
||||
|
||||
function roundAvail(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
return Math.floor(n * 100) / 100;
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
|
||||
async function loadBalances(force) {
|
||||
const q = force ? "?force=1" : "";
|
||||
async function loadBalances(force, scope) {
|
||||
const parts = [];
|
||||
if (force) parts.push("force=1");
|
||||
if (scope && scope !== "main") parts.push("scope=" + encodeURIComponent(scope));
|
||||
const q = parts.length ? "?" + parts.join("&") : "";
|
||||
const d = await apiJson("/api/options/balances" + q);
|
||||
if (!d.ok) throw new Error(d.msg || "余额拉取失败");
|
||||
return d;
|
||||
@@ -39,26 +93,42 @@
|
||||
|
||||
function pickBalance(bal, account, ccy) {
|
||||
const acct = account === "trading" ? "trading" : "funding";
|
||||
const key = acct + "_" + String(ccy || "").toLowerCase();
|
||||
return floorAmount(bal[key]);
|
||||
const c = String(ccy || "").toLowerCase();
|
||||
const availKey = acct + "_" + c + "_avail";
|
||||
const totalKey = acct + "_" + c;
|
||||
return roundAvail(bal[availKey] != null ? bal[availKey] : bal[totalKey]);
|
||||
}
|
||||
|
||||
async function resolveMaxAmount(account, ccy) {
|
||||
const bal = await loadBalances(true);
|
||||
async function resolveMaxAmount(account, ccy, scope) {
|
||||
const bal = await loadBalances(true, scope || "main");
|
||||
return pickBalance(bal, account, ccy);
|
||||
}
|
||||
|
||||
async function submitSwap(amount) {
|
||||
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);
|
||||
if (d.ok && typeof refreshAccountSnapshot === "function") refreshFundsAfterMutation();
|
||||
setButtonsBusy(SWAP_BTNS, true, "兑换中…");
|
||||
setMsg("opt-set-swap-msg", "兑换中,市价成交可能有延时…", false);
|
||||
try {
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (d.ok) {
|
||||
setMsg("opt-set-swap-msg", "兑换成功", false);
|
||||
refreshFundsAfterMutation();
|
||||
} else {
|
||||
setMsg("opt-set-swap-msg", "兑换失败:" + (d.msg || "未知错误"), true);
|
||||
}
|
||||
return d;
|
||||
} catch (e) {
|
||||
setMsg("opt-set-swap-msg", "兑换失败:" + (e.message || "网络错误"), true);
|
||||
return { ok: false };
|
||||
} finally {
|
||||
setButtonsBusy(SWAP_BTNS, false);
|
||||
}
|
||||
}
|
||||
|
||||
const swapBtn = document.getElementById("opt-set-swap-btn");
|
||||
@@ -79,32 +149,52 @@
|
||||
try {
|
||||
const dir = document.getElementById("opt-set-swap-dir").value;
|
||||
const ccy = dir === "usdc_to_usdt" ? "USDC" : "USDT";
|
||||
const amount = await resolveMaxAmount("funding", ccy);
|
||||
const amount = await resolveMaxAmount("funding", ccy, "main");
|
||||
if (!amount) {
|
||||
setMsg("opt-set-swap-msg", "资金账户可用余额不足", true);
|
||||
return;
|
||||
}
|
||||
const msg =
|
||||
"确认全部兑换?\n\n" +
|
||||
"方向:" + swapDirLabel(dir) + "\n" +
|
||||
"金额:" + fmtAmt(amount, ccy) + "\n\n" +
|
||||
"将兑换资金账户全部可用余额。市价成交可能有延时,请确认。";
|
||||
if (!confirmOk(msg)) return;
|
||||
document.getElementById("opt-set-swap-amount").value = String(amount);
|
||||
await submitSwap(amount);
|
||||
} catch (e) {
|
||||
setMsg("opt-set-swap-msg", e.message || "余额拉取失败", true);
|
||||
setMsg("opt-set-swap-msg", "兑换失败:" + (e.message || "余额拉取失败"), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function submitInternalTransfer(amount) {
|
||||
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);
|
||||
if (d.ok && typeof refreshAccountSnapshot === "function") refreshFundsAfterMutation();
|
||||
setButtonsBusy(INT_BTNS, true, "划转中…");
|
||||
setMsg("opt-set-int-msg", "划转中…", false);
|
||||
try {
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (d.ok) {
|
||||
setMsg("opt-set-int-msg", "划转成功", false);
|
||||
refreshFundsAfterMutation();
|
||||
} else {
|
||||
setMsg("opt-set-int-msg", "划转失败:" + (d.msg || "未知错误"), true);
|
||||
}
|
||||
return d;
|
||||
} catch (e) {
|
||||
setMsg("opt-set-int-msg", "划转失败:" + (e.message || "网络错误"), true);
|
||||
return { ok: false };
|
||||
} finally {
|
||||
setButtonsBusy(INT_BTNS, false);
|
||||
}
|
||||
}
|
||||
|
||||
const intBtn = document.getElementById("opt-set-int-btn");
|
||||
@@ -125,33 +215,56 @@
|
||||
try {
|
||||
const ccy = document.getElementById("opt-set-int-ccy").value;
|
||||
const from = document.getElementById("opt-set-int-from").value;
|
||||
const amount = await resolveMaxAmount(from, ccy);
|
||||
const to = document.getElementById("opt-set-int-to").value;
|
||||
const amount = await resolveMaxAmount(from, ccy, "main");
|
||||
if (!amount) {
|
||||
setMsg("opt-set-int-msg", "划出账户可用余额不足", true);
|
||||
return;
|
||||
}
|
||||
const msg =
|
||||
"确认全部划转?\n\n" +
|
||||
"币种:" + ccy + "\n" +
|
||||
"划出:" + accountLabel(from) + "\n" +
|
||||
"划入:" + accountLabel(to) + "\n" +
|
||||
"金额:" + fmtAmt(amount, ccy) + "\n\n" +
|
||||
"将划转该账户全部可用余额。";
|
||||
if (!confirmOk(msg)) return;
|
||||
document.getElementById("opt-set-int-amount").value = String(amount);
|
||||
await submitInternalTransfer(amount);
|
||||
} catch (e) {
|
||||
setMsg("opt-set-int-msg", e.message || "余额拉取失败", true);
|
||||
setMsg("opt-set-int-msg", "划转失败:" + (e.message || "余额拉取失败"), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function submitCrossTransfer(amount) {
|
||||
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,
|
||||
from_account: document.getElementById("opt-set-cross-from").value,
|
||||
to_account: document.getElementById("opt-set-cross-to").value,
|
||||
direction: document.getElementById("opt-set-cross-dir").value,
|
||||
}),
|
||||
});
|
||||
setMsg("opt-set-cross-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
|
||||
if (d.ok && typeof refreshAccountSnapshot === "function") refreshFundsAfterMutation();
|
||||
setButtonsBusy(CROSS_BTNS, true, "划转中…");
|
||||
setMsg("opt-set-cross-msg", "划转中…", false);
|
||||
try {
|
||||
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,
|
||||
from_account: document.getElementById("opt-set-cross-from").value,
|
||||
to_account: document.getElementById("opt-set-cross-to").value,
|
||||
direction: document.getElementById("opt-set-cross-dir").value,
|
||||
}),
|
||||
});
|
||||
if (d.ok) {
|
||||
setMsg("opt-set-cross-msg", "划转成功", false);
|
||||
refreshFundsAfterMutation();
|
||||
} else {
|
||||
setMsg("opt-set-cross-msg", "划转失败:" + (d.msg || "未知错误"), true);
|
||||
}
|
||||
return d;
|
||||
} catch (e) {
|
||||
setMsg("opt-set-cross-msg", "划转失败:" + (e.message || "网络错误"), true);
|
||||
return { ok: false };
|
||||
} finally {
|
||||
setButtonsBusy(CROSS_BTNS, false);
|
||||
}
|
||||
}
|
||||
|
||||
const crossBtn = document.getElementById("opt-set-cross-btn");
|
||||
@@ -172,15 +285,28 @@
|
||||
try {
|
||||
const ccy = document.getElementById("opt-set-cross-ccy").value;
|
||||
const from = document.getElementById("opt-set-cross-from").value;
|
||||
const amount = await resolveMaxAmount(from, ccy);
|
||||
const to = document.getElementById("opt-set-cross-to").value;
|
||||
const direction = document.getElementById("opt-set-cross-dir").value;
|
||||
const scope = direction === "sub_to_main" ? "sub" : "main";
|
||||
const sideLabel = direction === "sub_to_main" ? "子账户" : "主账户";
|
||||
const amount = await resolveMaxAmount(from, ccy, scope);
|
||||
if (!amount) {
|
||||
setMsg("opt-set-cross-msg", "划出账户可用余额不足", true);
|
||||
setMsg("opt-set-cross-msg", sideLabel + "划出账户可用余额不足", true);
|
||||
return;
|
||||
}
|
||||
const msg =
|
||||
"确认全部划转?\n\n" +
|
||||
"方向:" + (direction === "main_to_sub" ? "主 → 子" : "子 → 主") + "\n" +
|
||||
"币种:" + ccy + "\n" +
|
||||
"划出:" + sideLabel + " · " + accountLabel(from) + "\n" +
|
||||
"划入:" + (direction === "main_to_sub" ? "子账户" : "主账户") + " · " + accountLabel(to) + "\n" +
|
||||
"金额:" + fmtAmt(amount, ccy) + "\n\n" +
|
||||
"将划转该账户全部可用余额。";
|
||||
if (!confirmOk(msg)) return;
|
||||
document.getElementById("opt-set-cross-amount").value = String(amount);
|
||||
await submitCrossTransfer(amount);
|
||||
} catch (e) {
|
||||
setMsg("opt-set-cross-msg", e.message || "余额拉取失败", true);
|
||||
setMsg("opt-set-cross-msg", "划转失败:" + (e.message || "余额拉取失败"), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user