From b5a061e7587df9b4d41e7f95abc56aaa9b39e535 Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 27 Jul 2026 07:13:04 +0800 Subject: [PATCH] Add instance phone shell with bottom tabbar and more sheet. --- lib/common/static/instance_embed.js | 5 + lib/common/static/instance_mobile_nav.js | 177 ++++++++++++ lib/common/static/instance_settings_prefs.js | 25 +- lib/common/static/instance_theme.css | 256 ++++++++++++++++++ lib/instance/templates/embed_shell.html | 53 +++- .../templates/instance_header_panel.html | 18 ++ 6 files changed, 523 insertions(+), 11 deletions(-) create mode 100644 lib/common/static/instance_mobile_nav.js diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js index 2c88701..6aa0fea 100644 --- a/lib/common/static/instance_embed.js +++ b/lib/common/static/instance_embed.js @@ -61,6 +61,11 @@ document.querySelectorAll(".embed-top-nav [data-embed-tab]").forEach((a) => { a.classList.toggle("active", a.getAttribute("data-embed-tab") === tab); }); + if (global.InstanceMobileNav && typeof global.InstanceMobileNav.onTabChange === "function") { + global.InstanceMobileNav.onTabChange(tab); + } else if (global.InstanceMobileNav && typeof global.InstanceMobileNav.syncTabActive === "function") { + global.InstanceMobileNav.syncTabActive(tab); + } } function pageNavAllowed(tab) { diff --git a/lib/common/static/instance_mobile_nav.js b/lib/common/static/instance_mobile_nav.js new file mode 100644 index 0000000..2ab2961 --- /dev/null +++ b/lib/common/static/instance_mobile_nav.js @@ -0,0 +1,177 @@ +/** + * 实例手机壳: ≤720px 底栏 +「更多」,与 embed soft-nav 同步. + */ +(function (global) { + const PRIMARY = { trade: 1, key_monitor: 1, records: 1 }; + const MQ = "(max-width: 720px)"; + + function isEmbedShell() { + return document.body && document.body.getAttribute("data-embed-shell") === "1"; + } + + function isMobileLayout() { + return window.matchMedia(MQ).matches; + } + + function syncPhoneClass() { + if (!document.body) return; + document.body.classList.toggle("inst-phone", isMobileLayout()); + } + + function currentTab() { + if (global.InstanceEmbed && typeof global.InstanceEmbed.getTab === "function") { + return global.InstanceEmbed.getTab(); + } + try { + const t = new URLSearchParams(location.search).get("tab"); + if (t) return t; + } catch (_) {} + return (document.body && document.body.getAttribute("data-page")) || "trade"; + } + + function closeMore() { + document.body.classList.remove("inst-mobile-more-open"); + const more = document.getElementById("inst-mobile-more"); + const btn = document.getElementById("inst-m-tab-more"); + if (more) more.setAttribute("aria-hidden", "true"); + if (btn) btn.setAttribute("aria-expanded", "false"); + syncTabActive(currentTab()); + } + + function openMore() { + if (!isMobileLayout()) return; + document.body.classList.add("inst-mobile-more-open"); + const more = document.getElementById("inst-mobile-more"); + const btn = document.getElementById("inst-m-tab-more"); + if (more) more.setAttribute("aria-hidden", "false"); + if (btn) btn.setAttribute("aria-expanded", "true"); + syncTabActive(currentTab()); + } + + function toggleMore() { + if (document.body.classList.contains("inst-mobile-more-open")) closeMore(); + else openMore(); + } + + function syncTabActive(tab) { + const page = tab || currentTab(); + const primary = !!PRIMARY[page]; + const moreOpen = document.body.classList.contains("inst-mobile-more-open"); + document.querySelectorAll("#inst-mobile-tabbar .inst-m-tab").forEach((el) => { + const t = el.getAttribute("data-embed-tab") || ""; + let on = false; + if (t === "more") on = moreOpen || !primary; + else on = !moreOpen && t === page; + el.classList.toggle("active", on); + }); + document.querySelectorAll("#inst-mobile-more .inst-mobile-more-nav [data-embed-tab]").forEach((a) => { + a.classList.toggle("active", a.getAttribute("data-embed-tab") === page); + }); + } + + /** embed 切页时关闭「更多」并同步高亮 */ + function onTabChange(tab) { + document.body.classList.remove("inst-mobile-more-open"); + const more = document.getElementById("inst-mobile-more"); + const btn = document.getElementById("inst-m-tab-more"); + if (more) more.setAttribute("aria-hidden", "true"); + if (btn) btn.setAttribute("aria-expanded", "false"); + syncTabActive(tab); + } + + function goTab(tab) { + if (!tab || tab === "more") return; + closeMore(); + if (global.InstanceEmbed && typeof global.InstanceEmbed.loadTab === "function") { + if (tab === currentTab()) { + syncTabActive(tab); + return; + } + void global.InstanceEmbed.loadTab(tab); + return; + } + const pathMap = { + dashboard: "/dashboard", + key_monitor: "/key_monitor", + trade: "/trade", + strategy: "/strategy", + strategy_records: "/strategy/records", + options: "/options", + options_review: "/options/review", + hedge_plan: "/hedge-plan", + records: "/records", + stats: "/stats", + risk_policy: "/risk_policy", + system_guide: "/system_guide", + env_config: "/env_config", + settings: "/settings", + }; + location.href = pathMap[tab] || "/trade"; + } + + function bindChrome() { + const moreBtn = document.getElementById("inst-m-tab-more"); + const backdrop = document.getElementById("inst-mobile-more-backdrop"); + const closeBtn = document.getElementById("inst-mobile-more-close"); + if (moreBtn) { + moreBtn.addEventListener("click", (ev) => { + ev.preventDefault(); + toggleMore(); + }); + } + if (backdrop) backdrop.addEventListener("click", closeMore); + if (closeBtn) closeBtn.addEventListener("click", closeMore); + document.addEventListener("keydown", (ev) => { + if (ev.key === "Escape" && document.body.classList.contains("inst-mobile-more-open")) { + closeMore(); + } + }); + + document.querySelectorAll("#inst-mobile-tabbar .inst-m-tab[data-embed-tab]").forEach((el) => { + if (el.getAttribute("data-embed-tab") === "more") return; + el.addEventListener("click", (ev) => { + if (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return; + ev.preventDefault(); + goTab(el.getAttribute("data-embed-tab")); + }); + }); + document.querySelectorAll("#inst-mobile-more .inst-mobile-more-nav [data-embed-tab]").forEach((a) => { + a.addEventListener("click", (ev) => { + if (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return; + ev.preventDefault(); + goTab(a.getAttribute("data-embed-tab")); + }); + }); + } + + function boot() { + if (!isEmbedShell()) return; + if (!document.getElementById("inst-mobile-tabbar")) return; + syncPhoneClass(); + bindChrome(); + syncTabActive(currentTab()); + let resizeTimer = null; + window.addEventListener("resize", () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + const was = document.body.classList.contains("inst-phone"); + syncPhoneClass(); + if (!isMobileLayout()) closeMore(); + else if (!was) syncTabActive(currentTab()); + }, 120); + }); + } + + global.InstanceMobileNav = { + syncTabActive, + onTabChange, + closeMore, + isMobileLayout, + }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", boot); + } else { + boot(); + } +})(typeof window !== "undefined" ? window : globalThis); diff --git a/lib/common/static/instance_settings_prefs.js b/lib/common/static/instance_settings_prefs.js index 3ed2acc..c2d0ea6 100644 --- a/lib/common/static/instance_settings_prefs.js +++ b/lib/common/static/instance_settings_prefs.js @@ -31,6 +31,8 @@ function applyDisplayToNav(display) { const map = { dashboard: "show_nav_dashboard", + key_monitor: "show_nav_key_monitor", + trade: "show_nav_trade", strategy: "show_nav_strategy", strategy_records: "show_nav_strategy_records", records: "show_nav_records", @@ -44,14 +46,19 @@ system_guide: "show_nav_system_guide", env_config: "show_nav_env_config", }; - document.querySelectorAll(".embed-top-nav [data-embed-tab], .top-nav a[href^='/']").forEach((a) => { - const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0]; - const key = map[tab]; - if (!key) return; - const show = navPrefShow(display, key); - a.classList.toggle("nav-hidden", !show); - a.style.display = show ? "" : "none"; - }); + document + .querySelectorAll( + ".embed-top-nav [data-embed-tab], .top-nav a[href^='/'], #inst-mobile-tabbar [data-embed-tab], #inst-mobile-more [data-embed-tab]" + ) + .forEach((a) => { + const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0]; + if (tab === "more") return; + const key = map[tab]; + if (!key) return; + const show = navPrefShow(display, key); + a.classList.toggle("nav-hidden", !show); + a.style.display = show ? "" : "none"; + }); global.__INSTANCE_DISPLAY__ = display; } @@ -59,6 +66,8 @@ const d = DISPLAY(); const map = { dashboard: "show_nav_dashboard", + key_monitor: "show_nav_key_monitor", + trade: "show_nav_trade", strategy: "show_nav_strategy", strategy_records: "show_nav_strategy_records", records: "show_nav_records", diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index b69d76b..f510791 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -5526,3 +5526,259 @@ html[data-theme="light"] .options-review-wrap .or-reviewed-table tbody tr:hover color: #ff8b8b; } +/* —— 实例手机壳:底栏四件套(仅 ≤720px + body.inst-phone) —— */ +.inst-mobile-tabbar, +.inst-mobile-more, +.instance-phone-only { + display: none; +} + +@media (max-width: 720px) { + :root { + --inst-m-tabbar-h: 56px; + } + + body.inst-phone .embed-top-nav.top-nav { + display: none !important; + } + + body.inst-phone .header { + margin-bottom: 4px; + } + + body.inst-phone .header h1 { + font-size: 0.85rem !important; + line-height: 1.3; + margin: 0; + } + + body.inst-phone .container { + padding-bottom: calc(var(--inst-m-tabbar-h) + max(10px, env(safe-area-inset-bottom))) !important; + } + + body.inst-phone .instance-phone-only { + display: flex; + } + + body.inst-phone .instance-header-phone-strip { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + } + + html[data-theme="light"] body.inst-phone .instance-header-phone-strip { + border-top-color: rgba(0, 0, 0, 0.08); + } + + body.inst-phone .inst-phone-chip { + display: inline-flex; + align-items: baseline; + gap: 4px; + min-height: 28px; + padding: 4px 8px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.06); + font-size: 12px; + color: var(--inst-text); + } + + html[data-theme="light"] body.inst-phone .inst-phone-chip { + background: rgba(0, 0, 0, 0.03); + border-color: rgba(0, 0, 0, 0.08); + } + + body.inst-phone .inst-phone-chip em { + font-style: normal; + color: var(--inst-muted); + font-size: 11px; + } + + body.inst-phone .inst-phone-chip b { + font-weight: 600; + } + + body.inst-phone .inst-phone-chip--pnl b { + color: var(--inst-nav-idle); + } + + body.inst-phone .inst-mobile-tabbar { + display: flex; + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 80; + height: calc(var(--inst-m-tabbar-h) + env(safe-area-inset-bottom)); + padding: 0 max(8px, env(safe-area-inset-right)) env(safe-area-inset-bottom) + max(8px, env(safe-area-inset-left)); + align-items: stretch; + justify-content: space-around; + gap: 2px; + background: color-mix(in srgb, #12161f 92%, transparent); + border-top: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + box-sizing: border-box; + } + + html[data-theme="light"] body.inst-phone .inst-mobile-tabbar { + background: color-mix(in srgb, #f4f7fb 94%, transparent); + border-top-color: rgba(0, 0, 0, 0.1); + } + + body.inst-phone .inst-mobile-tabbar .inst-m-tab.nav-hidden { + display: none !important; + } + + body.inst-phone .inst-m-tab { + flex: 1 1 0; + min-width: 0; + display: inline-flex; + align-items: center; + justify-content: center; + margin: 6px 2px; + padding: 0 4px; + border: none; + border-radius: 10px; + background: transparent; + color: var(--inst-muted); + font: inherit; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.04em; + text-decoration: none; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + } + + body.inst-phone .inst-m-tab:hover, + body.inst-phone .inst-m-tab:focus-visible { + color: var(--inst-text); + background: rgba(255, 255, 255, 0.04); + outline: none; + } + + body.inst-phone .inst-m-tab.active { + color: var(--inst-nav-idle); + background: rgba(143, 200, 255, 0.12); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--inst-nav-idle) 35%, transparent); + } + + body.inst-phone.inst-mobile-more-open .inst-mobile-more { + display: block; + } + + body.inst-phone .inst-mobile-more { + position: fixed; + inset: 0; + z-index: 90; + } + + body.inst-phone .inst-mobile-more-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.45); + } + + body.inst-phone .inst-mobile-more-sheet { + position: absolute; + left: 0; + right: 0; + bottom: 0; + max-height: min(78vh, 560px); + overflow: auto; + padding: 10px 16px calc(16px + env(safe-area-inset-bottom)); + border-radius: 16px 16px 0 0; + background: #12161f; + border: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: none; + box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.35); + } + + html[data-theme="light"] body.inst-phone .inst-mobile-more-sheet { + background: #f4f7fb; + border-color: rgba(0, 0, 0, 0.1); + } + + body.inst-phone .inst-mobile-more-handle { + width: 36px; + height: 4px; + margin: 2px auto 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.2); + } + + html[data-theme="light"] body.inst-phone .inst-mobile-more-handle { + background: rgba(0, 0, 0, 0.15); + } + + body.inst-phone .inst-mobile-more-title { + margin: 0 0 4px; + font-size: 1rem; + color: var(--inst-text); + } + + body.inst-phone .inst-mobile-more-hint { + margin: 0 0 14px; + font-size: 11px; + color: var(--inst-muted); + } + + body.inst-phone .inst-mobile-more-nav { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + } + + body.inst-phone .inst-mobile-more-nav a { + display: flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 10px 8px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.03); + color: var(--inst-text); + text-decoration: none; + font-size: 13px; + font-weight: 500; + } + + html[data-theme="light"] body.inst-phone .inst-mobile-more-nav a { + border-color: rgba(0, 0, 0, 0.1); + background: rgba(0, 0, 0, 0.03); + } + + body.inst-phone .inst-mobile-more-nav a.nav-hidden { + display: none !important; + } + + body.inst-phone .inst-mobile-more-nav a.active { + border-color: color-mix(in srgb, var(--inst-nav-idle) 45%, transparent); + background: rgba(143, 200, 255, 0.12); + color: var(--inst-nav-idle); + } + + body.inst-phone .inst-mobile-more-close { + width: 100%; + margin-top: 14px; + min-height: 44px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: transparent; + color: var(--inst-text); + font: inherit; + font-size: 14px; + cursor: pointer; + } + + html[data-theme="light"] body.inst-phone .inst-mobile-more-close { + border-color: rgba(0, 0, 0, 0.12); + } +} + diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index e140d89..3061205 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -8,7 +8,7 @@ - + {{ pwa_app_name }} @@ -93,6 +93,52 @@ + + + + @@ -118,8 +164,9 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj - + - + + diff --git a/lib/instance/templates/instance_header_panel.html b/lib/instance/templates/instance_header_panel.html index 842ae61..35b5e52 100644 --- a/lib/instance/templates/instance_header_panel.html +++ b/lib/instance/templates/instance_header_panel.html @@ -37,4 +37,22 @@
{% include 'instance_header_stats.html' %}
+
+ + 交易 + {{ funds_fmt(current_capital) }}U + + + 总资 + {% if total_funds is not none %}{{ funds_fmt(total_funds) }}U{% else %}—{% endif %} + + + 胜率 + {{ rate }}% + + + 浮盈 + + +