中控增加条件单委托
This commit is contained in:
@@ -594,6 +594,130 @@ button:disabled {
|
||||
padding: 6px 4px 8px;
|
||||
}
|
||||
|
||||
.td-actions-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.orders-collapse {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.orders-collapse-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
padding: 4px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.orders-collapse-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.orders-collapse-summary::before {
|
||||
content: "▸";
|
||||
color: var(--muted);
|
||||
margin-right: 6px;
|
||||
font-size: 10px;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.orders-collapse[open] > .orders-collapse-summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.orders-collapse-body {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
padding: 20px 22px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.modal-panel h3 {
|
||||
margin: 0 0 8px;
|
||||
font-family: var(--display);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.modal-meta {
|
||||
margin: 0 0 14px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.modal-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-field label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.modal-field input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 6px;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.modal-hint {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin: 0 0 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
let settingsCache = null;
|
||||
let monitorTimer = null;
|
||||
let authState = { required: false, logged_in: true };
|
||||
let tpslPending = null;
|
||||
|
||||
async function apiFetch(url, opts) {
|
||||
const r = await fetch(url, opts);
|
||||
@@ -146,8 +147,22 @@
|
||||
);
|
||||
});
|
||||
box.querySelectorAll(".btn-cancel-cond-all").forEach((btn) => {
|
||||
btn.onclick = () =>
|
||||
btn.onclick = (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
cancelSymbolOrders(btn.dataset.exId, btn.dataset.symbol, "conditional");
|
||||
};
|
||||
});
|
||||
box.querySelectorAll(".btn-place-tpsl").forEach((btn) => {
|
||||
btn.onclick = () =>
|
||||
openTpslModal(
|
||||
btn.dataset.exId,
|
||||
btn.dataset.symbol,
|
||||
btn.dataset.side,
|
||||
btn.dataset.contracts,
|
||||
btn.dataset.sl || "",
|
||||
btn.dataset.tp || ""
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
box.innerHTML = `<div class="err">${esc(e)}</div>`;
|
||||
@@ -180,15 +195,34 @@
|
||||
return `<table class="data-table data-table-sub"><thead><tr><th>类型</th><th>数量</th><th>触发/价格</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
}
|
||||
|
||||
function guessTpslFromCondOrders(side, cond) {
|
||||
const triggers = (cond || [])
|
||||
.map((o) => o.trigger_price)
|
||||
.filter((v) => v != null && !Number.isNaN(Number(v)))
|
||||
.map(Number);
|
||||
if (!triggers.length) return { sl: "", tp: "" };
|
||||
triggers.sort((a, b) => a - b);
|
||||
const s = (side || "long").toLowerCase();
|
||||
if (s === "short") {
|
||||
return { sl: triggers[triggers.length - 1], tp: triggers[0] };
|
||||
}
|
||||
return { sl: triggers[0], tp: triggers[triggers.length - 1] };
|
||||
}
|
||||
|
||||
function renderPositionBlock(exchangeId, x) {
|
||||
const symAttr = esc(x.symbol || "").replace(/"/g, """);
|
||||
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
|
||||
const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(/"/g, """);
|
||||
const cond = Array.isArray(x.conditional_orders) ? x.conditional_orders : [];
|
||||
const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
|
||||
const guess = guessTpslFromCondOrders(x.side, cond);
|
||||
const slAttr = esc(String(guess.sl)).replace(/"/g, """);
|
||||
const tpAttr = esc(String(guess.tp)).replace(/"/g, """);
|
||||
const condAllBtn =
|
||||
cond.length > 0
|
||||
? `<button type="button" class="btn-cancel-cond-all ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}">撤销全部条件单</button>`
|
||||
? `<button type="button" class="btn-cancel-cond-all ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}">撤销全部</button>`
|
||||
: "";
|
||||
const condBody = renderOrderRows(exchangeId, x.symbol, cond, "conditional");
|
||||
return `<div class="pos-block">
|
||||
<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th><th>操作</th></tr></thead><tbody>
|
||||
<tr>
|
||||
@@ -196,15 +230,20 @@
|
||||
<td>${esc(x.side)}</td>
|
||||
<td>${fmt(x.contracts, 4)}</td>
|
||||
<td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 4)}</td>
|
||||
<td class="td-actions"><button type="button" class="btn-close-pos danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button></td>
|
||||
<td class="td-actions td-actions-row">
|
||||
<button type="button" class="btn-place-tpsl ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}" data-contracts="${contractsAttr}" data-sl="${slAttr}" data-tp="${tpAttr}">委托</button>
|
||||
<button type="button" class="btn-close-pos danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<div class="pos-orders">
|
||||
<div class="pos-orders-head">
|
||||
<span class="pos-orders-title">条件单 · ${cond.length}</span>
|
||||
${condAllBtn}
|
||||
</div>
|
||||
${renderOrderRows(exchangeId, x.symbol, cond, "conditional")}
|
||||
<details class="orders-collapse">
|
||||
<summary class="orders-collapse-summary">
|
||||
<span class="pos-orders-title">条件单 · ${cond.length}</span>
|
||||
${condAllBtn}
|
||||
</summary>
|
||||
<div class="orders-collapse-body">${condBody}</div>
|
||||
</details>
|
||||
<div class="pos-orders-head" style="margin-top:10px">
|
||||
<span class="pos-orders-title">普通委托 · ${reg.length}</span>
|
||||
</div>
|
||||
@@ -213,6 +252,103 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) {
|
||||
tpslPending = {
|
||||
exchangeId,
|
||||
symbol,
|
||||
side: (side || "long").toLowerCase(),
|
||||
contracts: parseFloat(contracts),
|
||||
};
|
||||
const modal = document.getElementById("tpsl-modal");
|
||||
const meta = document.getElementById("tpsl-modal-meta");
|
||||
const slIn = document.getElementById("tpsl-sl");
|
||||
const tpIn = document.getElementById("tpsl-tp");
|
||||
if (!modal || !meta || !slIn || !tpIn) return;
|
||||
meta.textContent = `${symbol} · ${side} · ${contracts} 张`;
|
||||
slIn.value = slHint !== "" && slHint != null ? String(slHint) : "";
|
||||
tpIn.value = tpHint !== "" && tpHint != null ? String(tpHint) : "";
|
||||
modal.classList.remove("hidden");
|
||||
modal.setAttribute("aria-hidden", "false");
|
||||
slIn.focus();
|
||||
}
|
||||
|
||||
function closeTpslModal() {
|
||||
tpslPending = null;
|
||||
const modal = document.getElementById("tpsl-modal");
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
modal.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
|
||||
async function submitTpslModal() {
|
||||
if (!tpslPending) return;
|
||||
const slIn = document.getElementById("tpsl-sl");
|
||||
const tpIn = document.getElementById("tpsl-tp");
|
||||
const sl = parseFloat(slIn && slIn.value);
|
||||
const tp = parseFloat(tpIn && tpIn.value);
|
||||
if (!sl || sl <= 0 || !tp || tp <= 0) {
|
||||
showToast("请填写有效的止损价与止盈价", true);
|
||||
return;
|
||||
}
|
||||
const { exchangeId, symbol, side, contracts } = tpslPending;
|
||||
if (
|
||||
!confirm(
|
||||
`确认 ${symbol} ${side}\n先撤销全部条件单,再挂止损 ${sl}、止盈 ${tp}?`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const btn = document.getElementById("tpsl-submit");
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const r = await apiFetch(
|
||||
"/api/orders/" + encodeURIComponent(exchangeId) + "/place-tpsl",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
symbol,
|
||||
side,
|
||||
stop_loss: sl,
|
||||
take_profit: tp,
|
||||
contracts: contracts > 0 ? contracts : null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const j = await r.json();
|
||||
const pl = j.payload || {};
|
||||
const ok = j.ok && pl.ok !== false;
|
||||
const n = pl.placed && pl.placed.cancelled_conditional;
|
||||
showToast(
|
||||
ok
|
||||
? `已挂单(已撤 ${n != null ? n : "?"} 笔旧条件单)`
|
||||
: pl.error || JSON.stringify(j),
|
||||
!ok
|
||||
);
|
||||
if (ok) {
|
||||
closeTpslModal();
|
||||
loadMonitorBoard();
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(String(e), true);
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function initTpslModal() {
|
||||
const backdrop = document.getElementById("tpsl-modal-backdrop");
|
||||
const cancel = document.getElementById("tpsl-cancel");
|
||||
const submit = document.getElementById("tpsl-submit");
|
||||
if (backdrop) backdrop.onclick = closeTpslModal;
|
||||
if (cancel) cancel.onclick = closeTpslModal;
|
||||
if (submit) submit.onclick = () => submitTpslModal();
|
||||
document.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Escape") closeTpslModal();
|
||||
});
|
||||
}
|
||||
|
||||
async function cancelOneOrder(exchangeId, symbol, orderId, channel) {
|
||||
if (!confirm(`撤销委托 ${symbol} #${orderId}?`)) return;
|
||||
try {
|
||||
@@ -557,6 +693,8 @@
|
||||
showToast("已添加一行,请填写 URL 后点「保存设置」");
|
||||
};
|
||||
|
||||
initTpslModal();
|
||||
|
||||
initAuth().then((ok) => {
|
||||
if (!ok) return;
|
||||
loadSettings().catch(() => {});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260524-open-orders" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260525-tpsl-ui" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-bg" aria-hidden="true"></div>
|
||||
@@ -79,7 +79,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tpsl-modal" class="modal hidden" aria-hidden="true">
|
||||
<div class="modal-backdrop" id="tpsl-modal-backdrop"></div>
|
||||
<div class="modal-panel" role="dialog" aria-labelledby="tpsl-modal-title">
|
||||
<h3 id="tpsl-modal-title">挂止盈 / 止损</h3>
|
||||
<p id="tpsl-modal-meta" class="modal-meta"></p>
|
||||
<div class="modal-field">
|
||||
<label for="tpsl-sl">止损价</label>
|
||||
<input id="tpsl-sl" type="number" step="any" autocomplete="off" />
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label for="tpsl-tp">止盈价</label>
|
||||
<input id="tpsl-tp" type="number" step="any" autocomplete="off" />
|
||||
</div>
|
||||
<p class="modal-hint">先撤销该合约全部条件单,再挂新止盈与止损(四所统一)。</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="tpsl-cancel" class="ghost">取消</button>
|
||||
<button type="button" id="tpsl-submit" class="primary">确认挂单</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast"></div>
|
||||
<script src="/assets/app.js?v=20260524-open-orders"></script>
|
||||
<script src="/assets/app.js?v=20260525-tpsl-ui"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user