From 8b0607d83f83fb2c3c0ae8465e7034d8d9409a9d Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 24 May 2026 08:41:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AFui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hub_bridge.py | 19 +- manual_trading_hub/hub.py | 2 +- manual_trading_hub/static/app.css | 58 +++++- manual_trading_hub/static/app.js | 296 +++++++++++++++++++-------- manual_trading_hub/static/index.html | 10 +- manual_trading_hub/使用说明.md | 6 +- 6 files changed, 293 insertions(+), 98 deletions(-) diff --git a/hub_bridge.py b/hub_bridge.py index 8ca0476..9bdaf07 100644 --- a/hub_bridge.py +++ b/hub_bridge.py @@ -167,17 +167,32 @@ def register_hub_routes(app): "SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC" ).fetchall(): 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() enrich = c.get("enrich_monitor") if callable(enrich): try: - payload = enrich(keys=keys, orders=orders, trends=trends) + payload = enrich(keys=keys, orders=orders, trends=trends, rolls=rolls) if isinstance(payload, dict): return jsonify({"ok": True, **payload}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 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"]) diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 449034a..43af05d 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -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() HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off") DIR = Path(__file__).resolve().parent -HUB_BUILD = "20260525-expand-fix" +HUB_BUILD = "20260525-fullscreen" def _is_local(host: str | None) -> bool: diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 44eda7d..966c016 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -493,18 +493,62 @@ button:disabled { grid-template-columns: repeat(3, minmax(0, 1fr)); } -.grid-monitor.grid-monitor-expanded { - grid-template-columns: minmax(0, 1fr); - max-width: 720px; +.card-expand-zone { + cursor: pointer; +} + +.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; } -.card.card-expanded { - grid-column: 1 / -1; +.fs-head { + 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 { - cursor: pointer; +.fs-title { + 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 { diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index c111df9..4904acd 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -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) { const box = document.getElementById("monitor-grid"); + const fs = document.getElementById("exchange-fullscreen"); + const fsInner = document.getElementById("exchange-fullscreen-inner"); if (!box) return; if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) { - expandedExchangeId = ""; - sessionStorage.removeItem("hub_expanded_ex"); + closeExchangeFullscreen(); } - const visible = expandedExchangeId - ? rows.filter((r) => String(r.id) === String(expandedExchangeId)) - : rows; - box.classList.toggle("grid-monitor-expanded", !!expandedExchangeId); - const parts = visible.map((r) => renderMonitorCard(r, !!expandedExchangeId)); - box.innerHTML = parts.join("") || '
无已启用账户
'; - if (!expandedExchangeId) syncMonitorGridColumns(box, rows.length); + box.innerHTML = + rows.map((r) => renderMonitorCard(r)).join("") || '
无已启用账户
'; + syncMonitorGridColumns(box, rows.length); 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) { @@ -249,22 +287,11 @@ ); }; }); - box.querySelectorAll(".btn-expand-back").forEach((btn) => { - btn.onclick = (ev) => { - ev.stopPropagation(); - expandedExchangeId = ""; - sessionStorage.removeItem("hub_expanded_ex"); - 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(".card-expand-zone").forEach((zone) => { + zone.onclick = (ev) => { + if (ev.target.closest("a, button, input, summary, details, .card-actions")) return; + const id = zone.closest(".card")?.dataset.exId; + if (id) openExchangeFullscreen(id); }; }); box.querySelectorAll("details.pos-orders-collapse[data-collapse-key]").forEach((el) => { @@ -461,77 +488,180 @@ .join(""); } - function renderStrategySection(orders, trends) { - const parts = []; - (orders || []).forEach((o) => { - parts.push(`
-
下单监控 #${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)}
-
${esc(o.direction)} · 触发 ${fmt(o.trigger_price, 4)} · SL ${fmt(o.stop_loss, 4)} · TP ${fmt(o.take_profit, 4)}
-
`); - }); - (trends || []).forEach((t) => { - parts.push(`
-
趋势计划 #${esc(t.id)} · ${esc(t.symbol)}
-
${esc(t.direction)} · SL ${fmt(t.stop_loss, 4)} · TP ${fmt(t.take_profit, 4)}
-
`); - }); - return parts.join(""); + function renderOrderMonitorSection(orders) { + if (!orders || !orders.length) return ""; + return orders + .map( + (o) => `
+
#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} · ${esc(o.direction)}
+
触发 ${fmt(o.trigger_price, 4)} · SL ${fmt(o.stop_loss, 4)} · TP ${fmt(o.take_profit, 4)} · ${esc(o.trade_style || o.monitor_type || "下单监控")}
+
` + ) + .join(""); } - function renderCompactBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap) { + function renderTrendSection(trends) { + if (!trends || !trends.length) return ""; + return trends + .map( + (t) => `
+
#${esc(t.id)} · ${esc(t.symbol)} · ${esc(t.direction)}
+
SL ${fmt(t.stop_loss, 4)} · TP ${fmt(t.take_profit, 4)} · 状态 ${esc(t.status || "active")}
+
` + ) + .join(""); + } + + function renderRollSection(rolls) { + if (!rolls || !rolls.length) return ""; + return rolls + .map( + (g) => `
+
组 #${esc(g.id)} · 监控单 #${esc(g.order_monitor_id || "—")}
+
腿数 ${esc(g.leg_count != null ? g.leg_count : "—")} · 止损 ${fmt(g.current_stop_loss, 4)} · ${esc(g.status || "active")}
+
` + ) + .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 `
+ + + + + + + + +
合约方向张数浮盈操作
${esc(x.symbol)}${esc(x.side)}${fmt(x.contracts, 4)}${fmt(x.unrealized_pnl, 4)} +
+ + +
+
+ ${renderOrdersCollapse(exchangeId, x.symbol, cond, reg)} +
`; + } + + function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) { let inner = `
余额
${fmt(ag.balance_usdt, 2)} U
浮盈合计
${fmt(ag.total_unrealized_pnl, 4)}
`; - inner += `
持仓 · ${pos.length}
`; + inner += `
交易所持仓
`; if (pos.length) { - inner += '
'; - pos.forEach((p) => { - inner += `
${esc(p.symbol)} · ${esc(p.side)}${fmt(p.unrealized_pnl, 4)}
`; - }); - inner += "
"; + inner += pos.map((p) => renderPositionBlock(row.id, p)).join(""); } else { inner += '
无持仓
'; } - const keyN = (row.capabilities || []).includes("key") ? keys.length : 0; - const stratN = orders.length + ((row.capabilities || []).includes("trend") ? trends.length : 0); - inner += `
点击卡片展开 · 持仓详情 / 关键位 ${keyN} / 策略 ${stratN}
`; + if (orders.length) { + inner += `
下单监控 · ${orders.length}
`; + orders.forEach((o) => { + inner += `
${esc(o.symbol || o.exchange_symbol)} · ${esc(o.direction)} · 触发 ${fmt(o.trigger_price, 4)}
`; + }); + } + if ((row.capabilities || []).includes("key")) { + inner += `
关键位
`; + if (!flaskOk) { + const fe = row.flask_error || hm.msg || hm.error || "策略 Flask 未连通"; + inner += `
${esc(fe)}
`; + } else if (!keys.length) { + inner += '
当前无记录
'; + } 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 += `
${line}
`; + }); + } + } + if ((row.capabilities || []).includes("trend") && trends.length) { + inner += `
趋势回调 · ${trends.length}
`; + trends.forEach((t) => { + inner += `
#${t.id} ${esc(t.symbol)} ${t.direction} · SL ${t.stop_loss} · TP ${t.take_profit}
`; + }); + } + if (rolls.length) { + inner += `
顺势加仓 · ${rolls.length}
`; + rolls.forEach((g) => { + inner += `
组 #${g.id} · 监控 #${g.order_monitor_id || "—"} · ${g.leg_count != null ? g.leg_count : "—"} 腿
`; + }); + } + inner += `
点击标题栏放大全屏 · 查看持仓卡片 / 关键位 / 策略详情
`; return inner; } - function renderExpandedBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap) { - let inner = `
-
余额
${fmt(ag.balance_usdt, 2)} U
+ function renderFullscreenExchange(row) { + const ag = row.agent || {}; + 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 = `
+
+

${esc(row.name)}

+
${esc(flaskOpen || "")}
+
+
+ + ${flaskOpen ? `打开实例` : ""} + ${strategyUrl ? `策略交易` : ""} + +
+
`; + if (!row.http_ok || ag.ok === false) { + html += `
${esc(row.error || ag.error || "子代理不可用")}
`; + return html; + } + html += `
+
余额
${fmt(ag.balance_usdt, 2)} U
浮盈合计
${fmt(ag.total_unrealized_pnl, 4)}
`; - inner += '
'; + html += '
持仓(每币种一卡)
'; if (pos.length) { pos.forEach((p) => { - const mo = findMonitorOrder(orders, p.symbol, p.side); - inner += renderLivePositionCard(row.id, p, mo); + html += renderLivePositionCard(row.id, p, findMonitorOrder(orders, p.symbol, p.side)); }); } else { - inner += '
暂无持仓
'; + html += '
暂无持仓
'; } - inner += "
"; + html += "
"; if ((row.capabilities || []).includes("key")) { if (!flaskOk) { - const fe = row.flask_error || hm.msg || hm.error || "策略 Flask 未连通"; - inner += renderHubSectionCard("关键位", `
${esc(fe)}
`, ""); + html += renderHubSectionCard("关键位", `
${esc(row.flask_error || hm.error || "Flask 未连通")}
`, ""); } else { - inner += renderHubSectionCard("关键位", renderKeySection(keys, kmap), "当前无关键位记录"); + html += renderHubSectionCard("关键位", renderKeySection(keys, kmap), "当前无关键位记录"); } } - const showStrategy = - orders.length || ((row.capabilities || []).includes("trend") && trends.length); - if (showStrategy) { - inner += renderHubSectionCard( - "策略", - renderStrategySection(orders, (row.capabilities || []).includes("trend") ? trends : []), - "当前无策略记录" - ); + html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders), "暂无运行中的下单监控"); + if ((row.capabilities || []).includes("trend")) { + html += renderHubSectionCard("趋势回调", renderTrendSection(trends), "暂无运行中的趋势回调计划"); } - return inner; + html += renderHubSectionCard("顺势加仓", renderRollSection(rolls), "暂无运行中的顺势加仓组"); + return html; } function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) { @@ -627,7 +757,13 @@ if (cancel) cancel.onclick = closeTpslModal; if (submit) submit.onclick = () => submitTpslModal(); 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 pos = Array.isArray(ag.positions) ? ag.positions : []; const hm = row.hub_monitor || {}; @@ -680,6 +816,7 @@ 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; @@ -692,10 +829,8 @@ } else if (!agOk) { inner = `
${esc(agErr || "子代理返回失败")}
`; inner += `
请检查 PM2 子代理与 ${esc(row.agent_url || "")}/status
`; - } else if (expanded) { - inner = renderExpandedBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap); } 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 cardCls = online ? "card-online" : "card-offline"; @@ -707,12 +842,8 @@ const openFlask = flaskOpen ? `实例` : ""; - const backBtn = expanded - ? `` - : ""; - const expandHit = !expanded && online ? " card-expand-hit" : ""; - return `
-
+ return `
+
@@ -721,13 +852,12 @@
${esc(flaskOpen || "")}
- ${backBtn} ${openFlask} ${review}
-
${inner}
+
${inner}
`; } diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 21444f4..6e00352 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -7,7 +7,7 @@ - + @@ -56,6 +56,12 @@
+ +