From 08ae171e48803d4c1f3a40a0b9725a1eb5758617 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 11 Jun 2026 11:27:28 +0800 Subject: [PATCH] feat(hub): mobile AI one-screen and dashboard monitor counts Fix mobile AI to scroll only in the chat area. Dashboard cards show monitor item counts with expand-to-fullscreen and color-coded position floating P&L. --- manual_trading_hub/hub_ai/context.py | 39 ++++------ manual_trading_hub/hub_dashboard.py | 4 +- manual_trading_hub/static/app.css | 41 +++++++++++ manual_trading_hub/static/app.js | 16 +++++ manual_trading_hub/static/dashboard.css | 92 ++++++++++++++++++++++++ manual_trading_hub/static/dashboard.js | 94 ++++++++++++++++++------- manual_trading_hub/static/index.html | 8 +-- 7 files changed, 237 insertions(+), 57 deletions(-) diff --git a/manual_trading_hub/hub_ai/context.py b/manual_trading_hub/hub_ai/context.py index 1ddd343..67f4763 100644 --- a/manual_trading_hub/hub_ai/context.py +++ b/manual_trading_hub/hub_ai/context.py @@ -652,41 +652,32 @@ def format_account_remark(ac: dict) -> str: return ";".join(parts) -def format_dashboard_account_lines(ac: dict) -> list[dict[str, Any]]: - """数据看板分户卡片:监控与持仓逐行展示(含浮盈亏数值供前端着色)。""" - lines: list[dict[str, Any]] = [] +def format_dashboard_account_detail(ac: dict) -> dict[str, Any]: + """数据看板分户卡片:监控仅数量,持仓逐行(含浮盈亏)。""" mon = ac.get("monitor_lines") or {} - for row in (mon.get("keys") or [])[:3]: - if row: - lines.append({"kind": "monitor", "text": str(row)}) - for row in (mon.get("orders") or [])[:3]: - if row: - lines.append({"kind": "monitor", "text": str(row)}) - for row in (mon.get("trends") or [])[:2]: - if row: - lines.append({"kind": "monitor", "text": str(row)}) - for row in (mon.get("rolls") or [])[:2]: - if row: - lines.append({"kind": "monitor", "text": str(row)}) + position_lines: list[dict[str, Any]] = [] for p in _filter_open_positions(ac.get("positions") or []): sym = p.get("symbol") or "?" side = p.get("side") or "?" upnl = _position_float_pnl(p) - lines.append( + position_lines.append( { "kind": "position", "text": f"{sym} {side}", "pnl": round(upnl, 4), } ) - if not lines: - issues = ac.get("issues") or [] - if issues: - for iss in issues[:3]: - lines.append({"kind": "issue", "text": str(iss)}) - else: - lines.append({"kind": "empty", "text": "无"}) - return lines + issues = [str(x) for x in (ac.get("issues") or [])[:3]] + return { + "monitor_counts": { + "keys": len(mon.get("keys") or []), + "orders": len(mon.get("orders") or []), + "trends": len(mon.get("trends") or []), + "rolls": len(mon.get("rolls") or []), + }, + "position_lines": position_lines, + "issues": issues, + } def collect_closed_trades_snapshot( diff --git a/manual_trading_hub/hub_dashboard.py b/manual_trading_hub/hub_dashboard.py index 638dba7..b6fe97f 100644 --- a/manual_trading_hub/hub_dashboard.py +++ b/manual_trading_hub/hub_dashboard.py @@ -8,7 +8,7 @@ from hub_ai.context import ( build_daily_context, collect_closed_trades_snapshot, format_account_remark, - format_dashboard_account_lines, + format_dashboard_account_detail, ) from hub_ai.config import trading_day_reset_hour from hub_trades_lib import current_trading_day @@ -64,7 +64,7 @@ def _enrich_account_row(ac: dict) -> dict: "float_pnl_u": ac.get("float_pnl_u"), "open_position_count": ac.get("open_position_count"), "remark": format_account_remark(ac), - "remark_lines": format_dashboard_account_lines(ac), + **format_dashboard_account_detail(ac), "issues": ac.get("issues") or [], "daily_loss_pct": loss_pct, "loss_alert": loss_alert, diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index e892744..923844a 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -3821,6 +3821,8 @@ html[data-theme="light"] button.danger { /* --- Hub AI 教练(整页一屏,内容区内滚动)--- */ body.hub-page-ai { overflow: hidden; + height: 100dvh; + max-height: 100dvh; } body.hub-page-ai .app-shell { padding-bottom: 12px; @@ -3831,6 +3833,13 @@ body.hub-page-ai .app-shell { flex-direction: column; box-sizing: border-box; } +body.hub-page-ai .app-shell > #page-ai { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} body.hub-page-ai .app-header { flex-shrink: 0; margin-bottom: 4px; @@ -3876,6 +3885,11 @@ body.hub-page-ai #page-ai { /* 手机 AI:须在 .ai-layout 双列定义之后,避免被覆盖成半屏 */ @media (max-width: 720px) { + html:has(body.hub-page-ai) { + height: 100%; + overflow: hidden; + } + body.hub-page-ai .app-shell { padding-bottom: max(8px, env(safe-area-inset-bottom)); height: var(--hub-vvh, 100dvh); @@ -3888,8 +3902,24 @@ body.hub-page-ai #page-ai { } body.hub-page-ai { + position: fixed; + inset: 0; + width: 100%; overflow: hidden; background: var(--bg); + overscroll-behavior: none; + } + + body.hub-page-ai .app-header { + padding: 6px 0; + margin-bottom: 2px; + gap: 8px; + } + + body.hub-page-ai .top-nav a { + min-height: 34px; + padding: 6px 10px; + font-size: 11px; } body.hub-page-ai.hub-ai-keyboard-open .app-header, @@ -4062,8 +4092,19 @@ body.hub-page-ai #page-ai { body.hub-page-ai .ai-chat-messages { flex: 1 1 auto; min-height: 0; + max-height: none; padding: 4px 2px 8px; + overflow-x: hidden; overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + } + + body.hub-page-ai .ai-layout[data-ai-mobile-tab="history"] .ai-chat-history-list { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; -webkit-overflow-scrolling: touch; } diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 0ef0471..6af4cc9 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -3618,6 +3618,22 @@ window.addEventListener("popstate", setActiveNav); } + window.hubOpenMonitorExpand = function hubOpenMonitorExpand(exId) { + const id = String(exId || "").trim(); + if (!id) return; + expandedExchangeId = id; + sessionStorage.setItem("hub_expanded_ex", id); + if (currentPage() !== "monitor") { + history.pushState({}, "", "/monitor"); + setActiveNav(); + } + if (lastMonitorRows.length) { + openExchangeFullscreen(id); + } else { + void fetchMonitorBoardSnapshot({ showLoading: true }); + } + }; + initAuth().then((ok) => { if (!ok) return; initShellNav(); diff --git a/manual_trading_hub/static/dashboard.css b/manual_trading_hub/static/dashboard.css index dd1736a..4ce991e 100644 --- a/manual_trading_hub/static/dashboard.css +++ b/manual_trading_hub/static/dashboard.css @@ -317,6 +317,75 @@ body.hub-page-dashboard .page#page-dashboard { color: var(--dash-muted); line-height: 1.4; word-break: break-word; + display: flex; + flex-direction: column; + gap: 6px; +} + +.dash-ac-monitor-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; +} + +.dash-monitor-chip { + display: inline-flex; + align-items: center; + padding: 3px 8px; + border-radius: 6px; + font-size: 11px; + line-height: 1.3; + border: 1px solid transparent; + font-weight: 600; +} + +.dash-monitor-chip.dash-monitor-key { + color: #b8a0ff; + background: rgba(123, 97, 255, 0.18); + border-color: rgba(123, 97, 255, 0.42); +} + +.dash-monitor-chip.dash-monitor-order { + color: var(--dash-accent); + background: rgba(0, 212, 255, 0.14); + border-color: rgba(0, 212, 255, 0.38); +} + +.dash-monitor-chip.dash-monitor-trend { + color: var(--dash-ok); + background: rgba(0, 255, 157, 0.1); + border-color: rgba(0, 255, 157, 0.38); +} + +.dash-monitor-chip.dash-monitor-roll { + color: #ffb020; + background: rgba(255, 176, 32, 0.14); + border-color: rgba(255, 176, 32, 0.42); +} + +.dash-ac-expand-btn { + margin-left: auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border-radius: 6px; + border: 1px solid var(--dash-card-border); + background: color-mix(in srgb, var(--dash-accent) 8%, var(--dash-card-bg)); + color: var(--dash-accent); + cursor: pointer; + flex-shrink: 0; +} + +.dash-ac-expand-btn:hover { + border-color: var(--dash-accent); + background: color-mix(in srgb, var(--dash-accent) 14%, var(--dash-card-bg)); +} + +.dash-ac-positions { display: flex; flex-direction: column; gap: 3px; @@ -346,10 +415,33 @@ body.hub-page-dashboard .page#page-dashboard { font-weight: 600; } +.dash-ac-remark-pos .pos { + color: var(--dash-ok); +} + +.dash-ac-remark-pos .neg { + color: var(--dash-warn); +} + +.dash-ac-remark-empty { + color: var(--dash-muted); +} + .dash-ac-remark-issue { color: var(--dash-warn); } +html[data-theme="light"] .dash-monitor-chip.dash-monitor-key { + color: #5b4fc7; + background: rgba(91, 79, 199, 0.1); + border-color: rgba(91, 79, 199, 0.28); +} + +html[data-theme="light"] .dash-monitor-chip.dash-monitor-trend { + background: rgba(10, 143, 92, 0.1); + border-color: rgba(10, 143, 92, 0.28); +} + .dash-table-wrap { overflow: auto; max-height: min(52vh, 480px); diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js index f8001e6..de27944 100644 --- a/manual_trading_hub/static/dashboard.js +++ b/manual_trading_hub/static/dashboard.js @@ -90,33 +90,72 @@ `; } - function renderRemarkLines(ac) { - const lines = Array.isArray(ac && ac.remark_lines) ? ac.remark_lines : []; - if (!lines.length) { - const fallback = esc((ac && ac.remark) || "—"); - return `
${fallback}
`; + function renderMonitorCountChips(counts) { + const mc = counts || {}; + const chips = []; + const keys = Number(mc.keys) || 0; + const orders = Number(mc.orders) || 0; + const trends = Number(mc.trends) || 0; + const rolls = Number(mc.rolls) || 0; + if (keys > 0) chips.push(`关键位 ${keys}`); + if (orders > 0) { + chips.push(`下单监控 ${orders}`); } - return `
${lines - .map((ln) => { - const kind = ln && ln.kind; - const text = esc((ln && ln.text) || ""); - if (kind === "position" && ln.pnl != null && Number.isFinite(Number(ln.pnl))) { - const pnl = Number(ln.pnl); - return ( - `
` + - `${text} 浮${pnlSigned(pnl, 2)}` + - `
` - ); - } - const cls = - kind === "monitor" - ? "dash-ac-remark-line dash-ac-remark-mon" - : kind === "issue" - ? "dash-ac-remark-line dash-ac-remark-issue" - : "dash-ac-remark-line"; - return `
${text}
`; - }) - .join("")}
`; + if (trends > 0) chips.push(`趋势回调 ${trends}`); + if (rolls > 0) chips.push(`顺势加仓 ${rolls}`); + return chips; + } + + function renderAccountDetail(ac) { + const counts = (ac && ac.monitor_counts) || {}; + const positions = Array.isArray(ac && ac.position_lines) ? ac.position_lines : []; + const issues = Array.isArray(ac && ac.issues) ? ac.issues : []; + const exId = ac && ac.id != null ? String(ac.id) : ""; + const chips = renderMonitorCountChips(counts); + const expandBtn = exId + ? `` + : ""; + const monitorRow = + chips.length || expandBtn + ? `
${chips.join("")}${expandBtn}
` + : ""; + let posHtml = ""; + if (positions.length) { + posHtml = positions + .map((ln) => { + const text = esc((ln && ln.text) || ""); + if (ln.pnl != null && Number.isFinite(Number(ln.pnl))) { + const pnl = Number(ln.pnl); + return ( + `
` + + `${text} 浮${pnlSigned(pnl, 2)}` + + `
` + ); + } + return `
${text}
`; + }) + .join(""); + } else if (!chips.length && !issues.length) { + posHtml = `
无持仓
`; + } + const issueHtml = issues + .map((text) => `
${esc(text)}
`) + .join(""); + return `
${monitorRow}
${posHtml}
${issueHtml}
`; + } + + function bindDashboardExpand() { + if (!elAccounts) return; + elAccounts.querySelectorAll(".dash-ac-expand-btn").forEach((btn) => { + btn.addEventListener("click", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + const id = btn.getAttribute("data-dash-ex-id"); + if (id && window.hubOpenMonitorExpand) window.hubOpenMonitorExpand(id); + }); + }); } function renderAccounts(accounts, threshold) { @@ -158,10 +197,11 @@
浮盈亏${pnlSigned(floatPnl, 2)}
${lossBar} - ${renderRemarkLines(ac)} + ${renderAccountDetail(ac)} `; }) .join(""); + bindDashboardExpand(); } function renderTrades(trades, accounts) { diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 1e5741a..0cecd2c 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -15,8 +15,8 @@ - - + + @@ -555,8 +555,8 @@ - + - +