前端ui
This commit is contained in:
+17
-2
@@ -167,17 +167,32 @@ def register_hub_routes(app):
|
|||||||
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC"
|
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC"
|
||||||
).fetchall():
|
).fetchall():
|
||||||
trends.append(_row_to_dict(row))
|
trends.append(_row_to_dict(row))
|
||||||
|
rolls = []
|
||||||
|
try:
|
||||||
|
for row in conn.execute(
|
||||||
|
"SELECT * FROM roll_groups WHERE status='active' ORDER BY id DESC"
|
||||||
|
).fetchall():
|
||||||
|
rolls.append(_row_to_dict(row))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
conn.close()
|
conn.close()
|
||||||
enrich = c.get("enrich_monitor")
|
enrich = c.get("enrich_monitor")
|
||||||
if callable(enrich):
|
if callable(enrich):
|
||||||
try:
|
try:
|
||||||
payload = enrich(keys=keys, orders=orders, trends=trends)
|
payload = enrich(keys=keys, orders=orders, trends=trends, rolls=rolls)
|
||||||
if isinstance(payload, dict):
|
if isinstance(payload, dict):
|
||||||
return jsonify({"ok": True, **payload})
|
return jsonify({"ok": True, **payload})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"ok": False, "msg": str(e)}), 500
|
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{"ok": True, "keys": keys, "orders": orders, "trends": trends, "key_prices": []}
|
{
|
||||||
|
"ok": True,
|
||||||
|
"keys": keys,
|
||||||
|
"orders": orders,
|
||||||
|
"trends": trends,
|
||||||
|
"rolls": rolls,
|
||||||
|
"key_prices": [],
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route("/api/hub/add_order", methods=["POST"])
|
@app.route("/api/hub/add_order", methods=["POST"])
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ HUB_BRIDGE_TOKEN = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN")
|
|||||||
_trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower()
|
_trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower()
|
||||||
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
|
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
|
||||||
DIR = Path(__file__).resolve().parent
|
DIR = Path(__file__).resolve().parent
|
||||||
HUB_BUILD = "20260525-expand-fix"
|
HUB_BUILD = "20260525-fullscreen"
|
||||||
|
|
||||||
|
|
||||||
def _is_local(host: str | None) -> bool:
|
def _is_local(host: str | None) -> bool:
|
||||||
|
|||||||
@@ -493,18 +493,62 @@ button:disabled {
|
|||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-monitor.grid-monitor-expanded {
|
.card-expand-zone {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
cursor: pointer;
|
||||||
max-width: 720px;
|
}
|
||||||
|
|
||||||
|
.card-expand-zone:hover .card-title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.hub-fullscreen-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 150;
|
||||||
|
background: rgba(2, 6, 12, 0.92);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-fullscreen-panel {
|
||||||
|
max-width: 820px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.card-expanded {
|
.fs-head {
|
||||||
grid-column: 1 / -1;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-expand-hit {
|
.fs-title {
|
||||||
cursor: pointer;
|
margin: 0;
|
||||||
|
font-family: var(--display);
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-head-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-expand-hint {
|
.card-expand-hint {
|
||||||
|
|||||||
@@ -191,21 +191,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeExchangeFullscreen() {
|
||||||
|
expandedExchangeId = "";
|
||||||
|
sessionStorage.removeItem("hub_expanded_ex");
|
||||||
|
const fs = document.getElementById("exchange-fullscreen");
|
||||||
|
if (fs) {
|
||||||
|
fs.classList.add("hidden");
|
||||||
|
fs.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
|
document.body.classList.remove("hub-fullscreen-open");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExchangeFullscreen(exId) {
|
||||||
|
expandedExchangeId = String(exId);
|
||||||
|
sessionStorage.setItem("hub_expanded_ex", expandedExchangeId);
|
||||||
|
renderMonitorGrid(lastMonitorRows);
|
||||||
|
}
|
||||||
|
|
||||||
function renderMonitorGrid(rows) {
|
function renderMonitorGrid(rows) {
|
||||||
const box = document.getElementById("monitor-grid");
|
const box = document.getElementById("monitor-grid");
|
||||||
|
const fs = document.getElementById("exchange-fullscreen");
|
||||||
|
const fsInner = document.getElementById("exchange-fullscreen-inner");
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) {
|
if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) {
|
||||||
expandedExchangeId = "";
|
closeExchangeFullscreen();
|
||||||
sessionStorage.removeItem("hub_expanded_ex");
|
|
||||||
}
|
}
|
||||||
const visible = expandedExchangeId
|
box.innerHTML =
|
||||||
? rows.filter((r) => String(r.id) === String(expandedExchangeId))
|
rows.map((r) => renderMonitorCard(r)).join("") || '<div class="err">无已启用账户</div>';
|
||||||
: rows;
|
syncMonitorGridColumns(box, rows.length);
|
||||||
box.classList.toggle("grid-monitor-expanded", !!expandedExchangeId);
|
|
||||||
const parts = visible.map((r) => renderMonitorCard(r, !!expandedExchangeId));
|
|
||||||
box.innerHTML = parts.join("") || '<div class="err">无已启用账户</div>';
|
|
||||||
if (!expandedExchangeId) syncMonitorGridColumns(box, rows.length);
|
|
||||||
bindMonitorInteractions(box);
|
bindMonitorInteractions(box);
|
||||||
|
|
||||||
|
if (expandedExchangeId && fs && fsInner) {
|
||||||
|
const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
|
||||||
|
if (row) {
|
||||||
|
fsInner.innerHTML = renderFullscreenExchange(row);
|
||||||
|
fs.classList.remove("hidden");
|
||||||
|
fs.setAttribute("aria-hidden", "false");
|
||||||
|
document.body.classList.add("hub-fullscreen-open");
|
||||||
|
bindMonitorInteractions(fsInner);
|
||||||
|
fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
|
||||||
|
btn.onclick = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
closeExchangeFullscreen();
|
||||||
|
renderMonitorGrid(lastMonitorRows);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
closeExchangeFullscreen();
|
||||||
|
}
|
||||||
|
} else if (fs) {
|
||||||
|
fs.classList.add("hidden");
|
||||||
|
fs.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.classList.remove("hub-fullscreen-open");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindMonitorInteractions(box) {
|
function bindMonitorInteractions(box) {
|
||||||
@@ -249,22 +287,11 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
box.querySelectorAll(".btn-expand-back").forEach((btn) => {
|
box.querySelectorAll(".card-expand-zone").forEach((zone) => {
|
||||||
btn.onclick = (ev) => {
|
zone.onclick = (ev) => {
|
||||||
ev.stopPropagation();
|
if (ev.target.closest("a, button, input, summary, details, .card-actions")) return;
|
||||||
expandedExchangeId = "";
|
const id = zone.closest(".card")?.dataset.exId;
|
||||||
sessionStorage.removeItem("hub_expanded_ex");
|
if (id) openExchangeFullscreen(id);
|
||||||
renderMonitorGrid(lastMonitorRows);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
box.querySelectorAll(".card-expand-hit").forEach((hit) => {
|
|
||||||
hit.onclick = (ev) => {
|
|
||||||
if (ev.target.closest("a, button, input, summary, .pos-orders-collapse")) return;
|
|
||||||
const id = hit.closest(".card")?.dataset.exId;
|
|
||||||
if (!id || expandedExchangeId) return;
|
|
||||||
expandedExchangeId = id;
|
|
||||||
sessionStorage.setItem("hub_expanded_ex", id);
|
|
||||||
renderMonitorGrid(lastMonitorRows);
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
box.querySelectorAll("details.pos-orders-collapse[data-collapse-key]").forEach((el) => {
|
box.querySelectorAll("details.pos-orders-collapse[data-collapse-key]").forEach((el) => {
|
||||||
@@ -461,77 +488,180 @@
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStrategySection(orders, trends) {
|
function renderOrderMonitorSection(orders) {
|
||||||
const parts = [];
|
if (!orders || !orders.length) return "";
|
||||||
(orders || []).forEach((o) => {
|
return orders
|
||||||
parts.push(`<div class="hub-mini-card">
|
.map(
|
||||||
<div class="hub-mini-title">下单监控 #${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)}</div>
|
(o) => `<div class="hub-mini-card">
|
||||||
<div class="hub-mini-line">${esc(o.direction)} · 触发 ${fmt(o.trigger_price, 4)} · SL ${fmt(o.stop_loss, 4)} · TP ${fmt(o.take_profit, 4)}</div>
|
<div class="hub-mini-title">#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} · ${esc(o.direction)}</div>
|
||||||
</div>`);
|
<div class="hub-mini-line">触发 ${fmt(o.trigger_price, 4)} · SL ${fmt(o.stop_loss, 4)} · TP ${fmt(o.take_profit, 4)} · ${esc(o.trade_style || o.monitor_type || "下单监控")}</div>
|
||||||
});
|
</div>`
|
||||||
(trends || []).forEach((t) => {
|
)
|
||||||
parts.push(`<div class="hub-mini-card">
|
.join("");
|
||||||
<div class="hub-mini-title">趋势计划 #${esc(t.id)} · ${esc(t.symbol)}</div>
|
|
||||||
<div class="hub-mini-line">${esc(t.direction)} · SL ${fmt(t.stop_loss, 4)} · TP ${fmt(t.take_profit, 4)}</div>
|
|
||||||
</div>`);
|
|
||||||
});
|
|
||||||
return parts.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCompactBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap) {
|
function renderTrendSection(trends) {
|
||||||
|
if (!trends || !trends.length) return "";
|
||||||
|
return trends
|
||||||
|
.map(
|
||||||
|
(t) => `<div class="hub-mini-card">
|
||||||
|
<div class="hub-mini-title">#${esc(t.id)} · ${esc(t.symbol)} · ${esc(t.direction)}</div>
|
||||||
|
<div class="hub-mini-line">SL ${fmt(t.stop_loss, 4)} · TP ${fmt(t.take_profit, 4)} · 状态 ${esc(t.status || "active")}</div>
|
||||||
|
</div>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRollSection(rolls) {
|
||||||
|
if (!rolls || !rolls.length) return "";
|
||||||
|
return rolls
|
||||||
|
.map(
|
||||||
|
(g) => `<div class="hub-mini-card">
|
||||||
|
<div class="hub-mini-title">组 #${esc(g.id)} · 监控单 #${esc(g.order_monitor_id || "—")}</div>
|
||||||
|
<div class="hub-mini-line">腿数 ${esc(g.leg_count != null ? g.leg_count : "—")} · 止损 ${fmt(g.current_stop_loss, 4)} · ${esc(g.status || "active")}</div>
|
||||||
|
</div>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
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, """);
|
||||||
|
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>
|
||||||
|
<td>${esc(x.symbol)}</td>
|
||||||
|
<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">
|
||||||
|
<div class="pos-action-group">
|
||||||
|
<button type="button" class="btn-place-tpsl btn-sm 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 btn-sm danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
${renderOrdersCollapse(exchangeId, x.symbol, cond, reg)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
|
||||||
let inner = `<div class="stat-row">
|
let inner = `<div class="stat-row">
|
||||||
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
||||||
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 4)}</div></div>
|
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 4)}</div></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
inner += `<div class="section-title">持仓 · ${pos.length}</div>`;
|
inner += `<div class="section-title">交易所持仓</div>`;
|
||||||
if (pos.length) {
|
if (pos.length) {
|
||||||
inner += '<div class="compact-pos-list">';
|
inner += pos.map((p) => renderPositionBlock(row.id, p)).join("");
|
||||||
pos.forEach((p) => {
|
|
||||||
inner += `<div class="compact-pos-line"><span>${esc(p.symbol)} · ${esc(p.side)}</span><span class="${pnlCls(p.unrealized_pnl)}">${fmt(p.unrealized_pnl, 4)}</span></div>`;
|
|
||||||
});
|
|
||||||
inner += "</div>";
|
|
||||||
} else {
|
} else {
|
||||||
inner += '<div class="empty-hint">无持仓</div>';
|
inner += '<div class="empty-hint">无持仓</div>';
|
||||||
}
|
}
|
||||||
const keyN = (row.capabilities || []).includes("key") ? keys.length : 0;
|
if (orders.length) {
|
||||||
const stratN = orders.length + ((row.capabilities || []).includes("trend") ? trends.length : 0);
|
inner += `<div class="section-title">下单监控 · ${orders.length}</div>`;
|
||||||
inner += `<div class="card-expand-hint">点击卡片展开 · 持仓详情 / 关键位 ${keyN} / 策略 ${stratN}</div>`;
|
orders.forEach((o) => {
|
||||||
|
inner += `<div class="list-line">${esc(o.symbol || o.exchange_symbol)} · ${esc(o.direction)} · 触发 ${fmt(o.trigger_price, 4)}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ((row.capabilities || []).includes("key")) {
|
||||||
|
inner += `<div class="section-title">关键位</div>`;
|
||||||
|
if (!flaskOk) {
|
||||||
|
const fe = row.flask_error || hm.msg || hm.error || "策略 Flask 未连通";
|
||||||
|
inner += `<div class="err">${esc(fe)}</div>`;
|
||||||
|
} else if (!keys.length) {
|
||||||
|
inner += '<div class="empty-hint">当前无记录</div>';
|
||||||
|
} else {
|
||||||
|
keys.forEach((k) => {
|
||||||
|
const kp = kmap[k.id] || kmap[String(k.id)] || {};
|
||||||
|
const mt = k.monitor_type || k.type || "";
|
||||||
|
let line = `${esc(k.symbol)} · ${esc(mt)} · ${k.upper} / ${k.lower}`;
|
||||||
|
if (kp.price_display != null || kp.price != null) {
|
||||||
|
line += ` · ${esc(kp.price_display != null ? kp.price_display : kp.price)}`;
|
||||||
|
}
|
||||||
|
line += ` · ${esc(kp.gate_summary || "-")}`;
|
||||||
|
inner += `<div class="list-line">${line}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((row.capabilities || []).includes("trend") && trends.length) {
|
||||||
|
inner += `<div class="section-title">趋势回调 · ${trends.length}</div>`;
|
||||||
|
trends.forEach((t) => {
|
||||||
|
inner += `<div class="list-line">#${t.id} ${esc(t.symbol)} ${t.direction} · SL ${t.stop_loss} · TP ${t.take_profit}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (rolls.length) {
|
||||||
|
inner += `<div class="section-title">顺势加仓 · ${rolls.length}</div>`;
|
||||||
|
rolls.forEach((g) => {
|
||||||
|
inner += `<div class="list-line">组 #${g.id} · 监控 #${g.order_monitor_id || "—"} · ${g.leg_count != null ? g.leg_count : "—"} 腿</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
inner += `<div class="card-expand-hint">点击标题栏放大全屏 · 查看持仓卡片 / 关键位 / 策略详情</div>`;
|
||||||
return inner;
|
return inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderExpandedBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap) {
|
function renderFullscreenExchange(row) {
|
||||||
let inner = `<div class="stat-row">
|
const ag = row.agent || {};
|
||||||
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
||||||
|
const hm = row.hub_monitor || {};
|
||||||
|
const flaskOk = row.flask_ok !== false && hm.ok !== false;
|
||||||
|
const keys = flaskOk ? hm.keys || [] : [];
|
||||||
|
const orders = flaskOk ? hm.orders || [] : [];
|
||||||
|
const trends = flaskOk ? hm.trends || [] : [];
|
||||||
|
const rolls = flaskOk ? hm.rolls || [] : [];
|
||||||
|
const kmap = {};
|
||||||
|
(row.key_prices || []).forEach((k) => {
|
||||||
|
kmap[k.id] = k;
|
||||||
|
});
|
||||||
|
const flaskOpen = row.flask_url_browser || row.flask_url;
|
||||||
|
const strategyUrl = flaskOpen ? esc(flaskOpen.replace(/\/$/, "") + "/strategy") : "";
|
||||||
|
let html = `<div class="fs-head">
|
||||||
|
<div>
|
||||||
|
<h2 class="fs-title">${esc(row.name)}</h2>
|
||||||
|
<div class="fs-sub">${esc(flaskOpen || "")}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fs-head-actions">
|
||||||
|
<button type="button" class="ghost btn-expand-back">返回监控</button>
|
||||||
|
${flaskOpen ? `<a class="btn-link" href="${esc(flaskOpen)}" target="_blank" rel="noopener">打开实例</a>` : ""}
|
||||||
|
${strategyUrl ? `<a class="btn-link" href="${strategyUrl}" target="_blank" rel="noopener">策略交易</a>` : ""}
|
||||||
|
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
if (!row.http_ok || ag.ok === false) {
|
||||||
|
html += `<div class="err">${esc(row.error || ag.error || "子代理不可用")}</div>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
html += `<div class="stat-row">
|
||||||
|
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} U</div></div>
|
||||||
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 4)}</div></div>
|
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 4)}</div></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
inner += '<div class="hub-pos-list">';
|
html += '<div class="section-title">持仓(每币种一卡)</div><div class="hub-pos-list">';
|
||||||
if (pos.length) {
|
if (pos.length) {
|
||||||
pos.forEach((p) => {
|
pos.forEach((p) => {
|
||||||
const mo = findMonitorOrder(orders, p.symbol, p.side);
|
html += renderLivePositionCard(row.id, p, findMonitorOrder(orders, p.symbol, p.side));
|
||||||
inner += renderLivePositionCard(row.id, p, mo);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
inner += '<div class="pos-empty">暂无持仓</div>';
|
html += '<div class="pos-empty">暂无持仓</div>';
|
||||||
}
|
}
|
||||||
inner += "</div>";
|
html += "</div>";
|
||||||
if ((row.capabilities || []).includes("key")) {
|
if ((row.capabilities || []).includes("key")) {
|
||||||
if (!flaskOk) {
|
if (!flaskOk) {
|
||||||
const fe = row.flask_error || hm.msg || hm.error || "策略 Flask 未连通";
|
html += renderHubSectionCard("关键位", `<div class="err">${esc(row.flask_error || hm.error || "Flask 未连通")}</div>`, "");
|
||||||
inner += renderHubSectionCard("关键位", `<div class="err">${esc(fe)}</div>`, "");
|
|
||||||
} else {
|
} else {
|
||||||
inner += renderHubSectionCard("关键位", renderKeySection(keys, kmap), "当前无关键位记录");
|
html += renderHubSectionCard("关键位", renderKeySection(keys, kmap), "当前无关键位记录");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const showStrategy =
|
html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders), "暂无运行中的下单监控");
|
||||||
orders.length || ((row.capabilities || []).includes("trend") && trends.length);
|
if ((row.capabilities || []).includes("trend")) {
|
||||||
if (showStrategy) {
|
html += renderHubSectionCard("趋势回调", renderTrendSection(trends), "暂无运行中的趋势回调计划");
|
||||||
inner += renderHubSectionCard(
|
|
||||||
"策略",
|
|
||||||
renderStrategySection(orders, (row.capabilities || []).includes("trend") ? trends : []),
|
|
||||||
"当前无策略记录"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return inner;
|
html += renderHubSectionCard("顺势加仓", renderRollSection(rolls), "暂无运行中的顺势加仓组");
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) {
|
function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) {
|
||||||
@@ -627,7 +757,13 @@
|
|||||||
if (cancel) cancel.onclick = closeTpslModal;
|
if (cancel) cancel.onclick = closeTpslModal;
|
||||||
if (submit) submit.onclick = () => submitTpslModal();
|
if (submit) submit.onclick = () => submitTpslModal();
|
||||||
document.addEventListener("keydown", (ev) => {
|
document.addEventListener("keydown", (ev) => {
|
||||||
if (ev.key === "Escape") closeTpslModal();
|
if (ev.key === "Escape") {
|
||||||
|
closeTpslModal();
|
||||||
|
if (expandedExchangeId) {
|
||||||
|
closeExchangeFullscreen();
|
||||||
|
renderMonitorGrid(lastMonitorRows);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +808,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMonitorCard(row, expanded) {
|
function renderMonitorCard(row) {
|
||||||
const ag = row.agent || {};
|
const ag = row.agent || {};
|
||||||
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
||||||
const hm = row.hub_monitor || {};
|
const hm = row.hub_monitor || {};
|
||||||
@@ -680,6 +816,7 @@
|
|||||||
const keys = flaskOk ? hm.keys || [] : [];
|
const keys = flaskOk ? hm.keys || [] : [];
|
||||||
const orders = flaskOk ? hm.orders || [] : [];
|
const orders = flaskOk ? hm.orders || [] : [];
|
||||||
const trends = flaskOk ? hm.trends || [] : [];
|
const trends = flaskOk ? hm.trends || [] : [];
|
||||||
|
const rolls = flaskOk ? hm.rolls || [] : [];
|
||||||
const kmap = {};
|
const kmap = {};
|
||||||
(row.key_prices || []).forEach((k) => {
|
(row.key_prices || []).forEach((k) => {
|
||||||
kmap[k.id] = k;
|
kmap[k.id] = k;
|
||||||
@@ -692,10 +829,8 @@
|
|||||||
} else if (!agOk) {
|
} else if (!agOk) {
|
||||||
inner = `<div class="err">${esc(agErr || "子代理返回失败")}</div>`;
|
inner = `<div class="err">${esc(agErr || "子代理返回失败")}</div>`;
|
||||||
inner += `<div class="empty-hint">请检查 PM2 子代理与 <code>${esc(row.agent_url || "")}/status</code></div>`;
|
inner += `<div class="empty-hint">请检查 PM2 子代理与 <code>${esc(row.agent_url || "")}/status</code></div>`;
|
||||||
} else if (expanded) {
|
|
||||||
inner = renderExpandedBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap);
|
|
||||||
} else {
|
} else {
|
||||||
inner = renderCompactBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap);
|
inner = renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap);
|
||||||
}
|
}
|
||||||
const online = row.http_ok && agOk;
|
const online = row.http_ok && agOk;
|
||||||
const cardCls = online ? "card-online" : "card-offline";
|
const cardCls = online ? "card-online" : "card-offline";
|
||||||
@@ -707,12 +842,8 @@
|
|||||||
const openFlask = flaskOpen
|
const openFlask = flaskOpen
|
||||||
? `<a class="btn-link" href="${esc(flaskOpen)}" target="_blank" rel="noopener">实例</a>`
|
? `<a class="btn-link" href="${esc(flaskOpen)}" target="_blank" rel="noopener">实例</a>`
|
||||||
: "";
|
: "";
|
||||||
const backBtn = expanded
|
return `<div class="card ${cardCls}" data-ex-id="${esc(row.id)}">
|
||||||
? `<button type="button" class="ghost btn-expand-back">返回</button>`
|
<div class="card-head card-expand-zone" title="点击放大全屏">
|
||||||
: "";
|
|
||||||
const expandHit = !expanded && online ? " card-expand-hit" : "";
|
|
||||||
return `<div class="card ${cardCls}${expanded ? " card-expanded" : ""}" data-ex-id="${esc(row.id)}">
|
|
||||||
<div class="card-head">
|
|
||||||
<div>
|
<div>
|
||||||
<div class="card-title-row">
|
<div class="card-title-row">
|
||||||
<span class="status-dot ${dotCls}" title="${online ? "在线" : "离线"}"></span>
|
<span class="status-dot ${dotCls}" title="${online ? "在线" : "离线"}"></span>
|
||||||
@@ -721,13 +852,12 @@
|
|||||||
<div class="card-sub">${esc(flaskOpen || "")}</div>
|
<div class="card-sub">${esc(flaskOpen || "")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
${backBtn}
|
|
||||||
${openFlask}
|
${openFlask}
|
||||||
${review}
|
${review}
|
||||||
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
|
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body${expandHit}">${inner}</div>
|
<div class="card-body">${inner}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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 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=20260525-expand-fix" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260525-fullscreen" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -56,6 +56,12 @@
|
|||||||
<div id="monitor-grid" class="grid-monitor"></div>
|
<div id="monitor-grid" class="grid-monitor"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="exchange-fullscreen" class="exchange-fullscreen hidden" aria-hidden="true">
|
||||||
|
<div class="exchange-fullscreen-panel">
|
||||||
|
<div id="exchange-fullscreen-inner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="page-settings" class="page hidden">
|
<div id="page-settings" class="page hidden">
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1><span class="head-tag">CFG</span> 系统设置</h1>
|
<h1><span class="head-tag">CFG</span> 系统设置</h1>
|
||||||
@@ -101,6 +107,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="/assets/app.js?v=20260525-expand-fix"></script>
|
<script src="/assets/app.js?v=20260525-fullscreen"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -176,9 +176,9 @@ curl -s http://127.0.0.1:5100/api/ping
|
|||||||
|
|
||||||
| 功能 | 说明 |
|
| 功能 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **2×2 卡片** | 仅显示「已启用」账户;**点击卡片**可放大,放大后每仓一张「实盘」风格持仓卡 |
|
| **2×2 主界面** | 四所信息**完整展示**:余额、持仓表、委托/平仓、折叠委托单、下单监控、关键位、趋势/加仓摘要 |
|
||||||
| **放大视图** | 持仓详情(与实例「实时持仓」布局一致)、**关键位**与**策略**各为独立卡片;顶栏「返回」回到网格 |
|
| **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡;独立卡片:**关键位**、**下单监控**、**趋势回调**、**顺势加仓** |
|
||||||
| **委托单折叠** | 展开/收起状态保存在浏览器本地,**5 秒自动刷新不会重置**(便于填写委托) |
|
| **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** |
|
||||||
| **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) |
|
| **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) |
|
||||||
| **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel`、`cancel-symbol` |
|
| **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel`、`cancel-symbol` |
|
||||||
| **挂止盈止损** | 持仓行 **「委托」**:弹窗填止损/止盈价 → **先撤该合约全部条件单,再挂新 TP/SL**(币安 / OKX / Gate / Gate趋势 四所统一,逻辑与各实例 `.env` 参数一致) |
|
| **挂止盈止损** | 持仓行 **「委托」**:弹窗填止损/止盈价 → **先撤该合约全部条件单,再挂新 TP/SL**(币安 / OKX / Gate / Gate趋势 四所统一,逻辑与各实例 `.env` 参数一致) |
|
||||||
|
|||||||
Reference in New Issue
Block a user