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.
This commit is contained in:
dekun
2026-06-11 11:27:28 +08:00
parent 1042fdeef3
commit 08ae171e48
7 changed files with 237 additions and 57 deletions
+15 -24
View File
@@ -652,41 +652,32 @@ def format_account_remark(ac: dict) -> str:
return "".join(parts) return "".join(parts)
def format_dashboard_account_lines(ac: dict) -> list[dict[str, Any]]: def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
"""数据看板分户卡片:监控持仓逐行展示(含浮盈亏数值供前端着色)。""" """数据看板分户卡片:监控仅数量,持仓逐行(含浮盈亏)。"""
lines: list[dict[str, Any]] = []
mon = ac.get("monitor_lines") or {} mon = ac.get("monitor_lines") or {}
for row in (mon.get("keys") or [])[:3]: position_lines: list[dict[str, Any]] = []
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)})
for p in _filter_open_positions(ac.get("positions") or []): for p in _filter_open_positions(ac.get("positions") or []):
sym = p.get("symbol") or "?" sym = p.get("symbol") or "?"
side = p.get("side") or "?" side = p.get("side") or "?"
upnl = _position_float_pnl(p) upnl = _position_float_pnl(p)
lines.append( position_lines.append(
{ {
"kind": "position", "kind": "position",
"text": f"{sym} {side}", "text": f"{sym} {side}",
"pnl": round(upnl, 4), "pnl": round(upnl, 4),
} }
) )
if not lines: issues = [str(x) for x in (ac.get("issues") or [])[:3]]
issues = ac.get("issues") or [] return {
if issues: "monitor_counts": {
for iss in issues[:3]: "keys": len(mon.get("keys") or []),
lines.append({"kind": "issue", "text": str(iss)}) "orders": len(mon.get("orders") or []),
else: "trends": len(mon.get("trends") or []),
lines.append({"kind": "empty", "text": ""}) "rolls": len(mon.get("rolls") or []),
return lines },
"position_lines": position_lines,
"issues": issues,
}
def collect_closed_trades_snapshot( def collect_closed_trades_snapshot(
+2 -2
View File
@@ -8,7 +8,7 @@ from hub_ai.context import (
build_daily_context, build_daily_context,
collect_closed_trades_snapshot, collect_closed_trades_snapshot,
format_account_remark, format_account_remark,
format_dashboard_account_lines, format_dashboard_account_detail,
) )
from hub_ai.config import trading_day_reset_hour from hub_ai.config import trading_day_reset_hour
from hub_trades_lib import current_trading_day 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"), "float_pnl_u": ac.get("float_pnl_u"),
"open_position_count": ac.get("open_position_count"), "open_position_count": ac.get("open_position_count"),
"remark": format_account_remark(ac), "remark": format_account_remark(ac),
"remark_lines": format_dashboard_account_lines(ac), **format_dashboard_account_detail(ac),
"issues": ac.get("issues") or [], "issues": ac.get("issues") or [],
"daily_loss_pct": loss_pct, "daily_loss_pct": loss_pct,
"loss_alert": loss_alert, "loss_alert": loss_alert,
+41
View File
@@ -3821,6 +3821,8 @@ html[data-theme="light"] button.danger {
/* --- Hub AI 教练(整页一屏,内容区内滚动)--- */ /* --- Hub AI 教练(整页一屏,内容区内滚动)--- */
body.hub-page-ai { body.hub-page-ai {
overflow: hidden; overflow: hidden;
height: 100dvh;
max-height: 100dvh;
} }
body.hub-page-ai .app-shell { body.hub-page-ai .app-shell {
padding-bottom: 12px; padding-bottom: 12px;
@@ -3831,6 +3833,13 @@ body.hub-page-ai .app-shell {
flex-direction: column; flex-direction: column;
box-sizing: border-box; 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 { body.hub-page-ai .app-header {
flex-shrink: 0; flex-shrink: 0;
margin-bottom: 4px; margin-bottom: 4px;
@@ -3876,6 +3885,11 @@ body.hub-page-ai #page-ai {
/* 手机 AI:须在 .ai-layout 双列定义之后,避免被覆盖成半屏 */ /* 手机 AI:须在 .ai-layout 双列定义之后,避免被覆盖成半屏 */
@media (max-width: 720px) { @media (max-width: 720px) {
html:has(body.hub-page-ai) {
height: 100%;
overflow: hidden;
}
body.hub-page-ai .app-shell { body.hub-page-ai .app-shell {
padding-bottom: max(8px, env(safe-area-inset-bottom)); padding-bottom: max(8px, env(safe-area-inset-bottom));
height: var(--hub-vvh, 100dvh); height: var(--hub-vvh, 100dvh);
@@ -3888,8 +3902,24 @@ body.hub-page-ai #page-ai {
} }
body.hub-page-ai { body.hub-page-ai {
position: fixed;
inset: 0;
width: 100%;
overflow: hidden; overflow: hidden;
background: var(--bg); 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, 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 { body.hub-page-ai .ai-chat-messages {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
max-height: none;
padding: 4px 2px 8px; padding: 4px 2px 8px;
overflow-x: hidden;
overflow-y: auto; 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; -webkit-overflow-scrolling: touch;
} }
+16
View File
@@ -3618,6 +3618,22 @@
window.addEventListener("popstate", setActiveNav); 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) => { initAuth().then((ok) => {
if (!ok) return; if (!ok) return;
initShellNav(); initShellNav();
+92
View File
@@ -317,6 +317,75 @@ body.hub-page-dashboard .page#page-dashboard {
color: var(--dash-muted); color: var(--dash-muted);
line-height: 1.4; line-height: 1.4;
word-break: break-word; 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; display: flex;
flex-direction: column; flex-direction: column;
gap: 3px; gap: 3px;
@@ -346,10 +415,33 @@ body.hub-page-dashboard .page#page-dashboard {
font-weight: 600; 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 { .dash-ac-remark-issue {
color: var(--dash-warn); 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 { .dash-table-wrap {
overflow: auto; overflow: auto;
max-height: min(52vh, 480px); max-height: min(52vh, 480px);
+67 -27
View File
@@ -90,33 +90,72 @@
</div>`; </div>`;
} }
function renderRemarkLines(ac) { function renderMonitorCountChips(counts) {
const lines = Array.isArray(ac && ac.remark_lines) ? ac.remark_lines : []; const mc = counts || {};
if (!lines.length) { const chips = [];
const fallback = esc((ac && ac.remark) || "—"); const keys = Number(mc.keys) || 0;
return `<div class="dash-ac-remark"><div class="dash-ac-remark-line">${fallback}</div></div>`; const orders = Number(mc.orders) || 0;
const trends = Number(mc.trends) || 0;
const rolls = Number(mc.rolls) || 0;
if (keys > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-key">关键位 ${keys}</span>`);
if (orders > 0) {
chips.push(`<span class="dash-monitor-chip dash-monitor-order">下单监控 ${orders}</span>`);
} }
return `<div class="dash-ac-remark">${lines if (trends > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-trend">趋势回调 ${trends}</span>`);
.map((ln) => { if (rolls > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-roll">顺势加仓 ${rolls}</span>`);
const kind = ln && ln.kind; return chips;
const text = esc((ln && ln.text) || ""); }
if (kind === "position" && ln.pnl != null && Number.isFinite(Number(ln.pnl))) {
const pnl = Number(ln.pnl); function renderAccountDetail(ac) {
return ( const counts = (ac && ac.monitor_counts) || {};
`<div class="dash-ac-remark-line dash-ac-remark-pos">` + const positions = Array.isArray(ac && ac.position_lines) ? ac.position_lines : [];
`${text} 浮<span class="${pnlClass(pnl)}">${pnlSigned(pnl, 2)}</span>` + const issues = Array.isArray(ac && ac.issues) ? ac.issues : [];
`</div>` const exId = ac && ac.id != null ? String(ac.id) : "";
); const chips = renderMonitorCountChips(counts);
} const expandBtn = exId
const cls = ? `<button type="button" class="dash-ac-expand-btn" data-dash-ex-id="${esc(exId)}" title="放大查看监控详情" aria-label="放大查看监控详情">` +
kind === "monitor" `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M15 3h6v6h-2V6.41l-7.29 7.3-1.42-1.42 7.3-7.29H15V3zM3 9h2v10h10v2H3V9z"/></svg>` +
? "dash-ac-remark-line dash-ac-remark-mon" `</button>`
: kind === "issue" : "";
? "dash-ac-remark-line dash-ac-remark-issue" const monitorRow =
: "dash-ac-remark-line"; chips.length || expandBtn
return `<div class="${cls}">${text}</div>`; ? `<div class="dash-ac-monitor-row">${chips.join("")}${expandBtn}</div>`
}) : "";
.join("")}</div>`; 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 (
`<div class="dash-ac-remark-line dash-ac-remark-pos">` +
`${text} 浮<span class="${pnlClass(pnl)}">${pnlSigned(pnl, 2)}</span>` +
`</div>`
);
}
return `<div class="dash-ac-remark-line dash-ac-remark-pos">${text}</div>`;
})
.join("");
} else if (!chips.length && !issues.length) {
posHtml = `<div class="dash-ac-remark-line dash-ac-remark-empty">无持仓</div>`;
}
const issueHtml = issues
.map((text) => `<div class="dash-ac-remark-line dash-ac-remark-issue">${esc(text)}</div>`)
.join("");
return `<div class="dash-ac-remark">${monitorRow}<div class="dash-ac-positions">${posHtml}</div>${issueHtml}</div>`;
}
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) { function renderAccounts(accounts, threshold) {
@@ -158,10 +197,11 @@
<div class="dash-ac-metric"><span>浮盈亏</span><strong class="${pnlClass(floatPnl)}">${pnlSigned(floatPnl, 2)}</strong></div> <div class="dash-ac-metric"><span>浮盈亏</span><strong class="${pnlClass(floatPnl)}">${pnlSigned(floatPnl, 2)}</strong></div>
</div> </div>
${lossBar} ${lossBar}
${renderRemarkLines(ac)} ${renderAccountDetail(ac)}
</article>`; </article>`;
}) })
.join(""); .join("");
bindDashboardExpand();
} }
function renderTrades(trades, accounts) { function renderTrades(trades, accounts) {
+4 -4
View File
@@ -15,8 +15,8 @@
<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" media="print" onload="this.media='all'" /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript> <noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260611-hub-ai-tabs" /> <link rel="stylesheet" href="/assets/app.css?v=20260612-hub-ai-one-screen" />
<link rel="stylesheet" href="/assets/dashboard.css?v=20260611-dash-remark-lines" /> <link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
</head> </head>
<body> <body>
<div class="app-bg" aria-hidden="true"></div> <div class="app-bg" aria-hidden="true"></div>
@@ -555,8 +555,8 @@
<script src="/assets/chart.js?v=20260609-market-day-split"></script> <script src="/assets/chart.js?v=20260609-market-day-split"></script>
<script src="/assets/archive.js?v=20260608-hub-archive-history"></script> <script src="/assets/archive.js?v=20260608-hub-archive-history"></script>
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script> <script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
<script src="/assets/dashboard.js?v=20260611-dash-remark-lines"></script> <script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
<script src="/assets/ai_review_render.js?v=2"></script> <script src="/assets/ai_review_render.js?v=2"></script>
<script src="/assets/app.js?v=20260611-hub-ai-tabs"></script> <script src="/assets/app.js?v=20260612-hub-monitor-expand"></script>
</body> </body>
</html> </html>