From c5262a0a54641e8f483ca252f56a8737d08ec143 Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 29 Jun 2026 16:42:38 +0800 Subject: [PATCH] Add responsive mobile layout, records cards, and tablet settings fold fix. Mobile gets compact trade/records UI with detail modals; static assets are cache-busted and settings cards fold correctly on tablet grid layout. Co-authored-by: Cursor --- app.py | 52 +++- scripts/deploy_responsive.py | 34 +++ static/css/mobile.css | 553 +++++++++++++++++++++++++++++++++++ static/css/records.css | 188 ++++++++++++ static/css/responsive.css | 279 +++++++++++++++++- static/css/trade.css | 2 +- static/js/nav.js | 12 +- static/js/orientation.js | 102 +++++++ static/js/records.js | 96 ++++++ templates/base.html | 47 ++- templates/records.html | 112 ++++++- templates/settings.html | 15 +- templates/strategy.html | 6 +- templates/trade.html | 2 +- 14 files changed, 1465 insertions(+), 35 deletions(-) create mode 100644 scripts/deploy_responsive.py create mode 100644 static/css/mobile.css create mode 100644 static/css/records.css create mode 100644 static/js/orientation.js create mode 100644 static/js/records.js diff --git a/app.py b/app.py index 3265693..37d53aa 100644 --- a/app.py +++ b/app.py @@ -218,11 +218,45 @@ def require_nav(key: str): return decorator +def _static_asset_v() -> str: + base = os.path.dirname(os.path.abspath(__file__)) + rels = ( + "static/js/trade.js", + "static/js/orientation.js", + "static/css/records.css", + "static/js/records.js", + "static/js/settings.js", + "static/css/mobile.css", + "static/css/responsive.css", + "static/css/trade.css", + "static/css/base.css", + ) + mtimes = [] + for rel in rels: + path = os.path.join(base, rel.replace("/", os.sep)) + if os.path.isfile(path): + mtimes.append(os.path.getmtime(path)) + return str(int(max(mtimes))) if mtimes else "0" + + +def _ua_is_phone(ua: str) -> bool: + ua_l = (ua or "").lower() + if "ipad" in ua_l: + return False + if "android" in ua_l and "mobile" not in ua_l: + return False + if any(x in ua_l for x in ("iphone", "ipod", "windows phone", "iemobile")): + return True + if "android" in ua_l and "mobile" in ua_l: + return True + if "mobile" in ua_l or "harmonyos" in ua_l or "openharmony" in ua_l: + return True + return False + + @app.context_processor def inject_globals(): - trade_js = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static", "js", "trade.js") - asset_v = str(int(os.path.getmtime(trade_js))) if os.path.isfile(trade_js) else "0" - return {"nav_items": get_nav_items(get_setting), "asset_v": asset_v} + return {"nav_items": get_nav_items(get_setting), "asset_v": _static_asset_v()} def _trading_mode() -> str: @@ -755,8 +789,18 @@ def index(): @app.route("/manifest.webmanifest") def web_manifest(): - response = app.send_static_file("manifest.json") + import json + + manifest_path = os.path.join(app.static_folder, "manifest.json") + with open(manifest_path, encoding="utf-8") as fh: + data = json.load(fh) + if _ua_is_phone(request.headers.get("User-Agent", "")): + data["orientation"] = "portrait-primary" + else: + data["orientation"] = "any" + response = app.make_response(json.dumps(data, ensure_ascii=False)) response.mimetype = "application/manifest+json" + response.headers["Cache-Control"] = "no-cache" return response diff --git a/scripts/deploy_responsive.py b/scripts/deploy_responsive.py new file mode 100644 index 0000000..c2ba5a2 --- /dev/null +++ b/scripts/deploy_responsive.py @@ -0,0 +1,34 @@ +"""Deploy responsive / orientation layout fixes.""" +import paramiko +import sys +from pathlib import Path + +sys.stdout.reconfigure(encoding="utf-8", errors="replace") +root = Path(__file__).resolve().parents[1] +files = [ + "app.py", + "static/css/trade.css", + "static/css/mobile.css", + "static/css/responsive.css", + "static/css/records.css", + "static/js/orientation.js", + "static/js/nav.js", + "static/js/records.js", + "static/js/settings.js", + "templates/base.html", + "templates/trade.html", + "templates/strategy.html", + "templates/records.html", + "templates/settings.html", +] +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("192.168.8.21", username="root", password="woaini88", timeout=15) +sftp = c.open_sftp() +for rel in files: + sftp.put(str(root / rel), f"/opt/qihuo/{rel.replace(chr(92), '/')}") + print("uploaded", rel) +sftp.close() +_, o, _ = c.exec_command("cd /opt/qihuo && pm2 restart qihuo") +print(o.read().decode("utf-8", errors="replace")) +c.close() diff --git a/static/css/mobile.css b/static/css/mobile.css new file mode 100644 index 0000000..a6c3a0c --- /dev/null +++ b/static/css/mobile.css @@ -0,0 +1,553 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. + * 手机端竖屏 UI + */ + +html:is([data-mobile="1"], .layout-phone) { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + overflow-x: hidden; + width: 100%; + max-width: 100vw; +} + +html:is([data-mobile="1"], .layout-phone) body { + font-size: 15px; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + max-width: 100vw; + -webkit-overflow-scrolling: touch; +} + +html:is([data-mobile="1"], .layout-phone) .page-wrap { + max-width: 100%; + width: 100%; + overflow-x: hidden; +} + +html:is([data-mobile="1"], .layout-phone) .tech-bg .tech-scanline { + display: none; +} + +/* ── 顶栏:菜单 | 标题 | 主题 ── */ +html:is([data-mobile="1"], .layout-phone) .site-header { + display: grid; + grid-template-columns: 40px minmax(0, 1fr) auto; + grid-template-areas: "toggle title tools"; + align-items: center; + column-gap: .45rem; + padding: calc(var(--safe-top) + .4rem) .6rem .5rem; + text-align: left; + border-bottom: 1px solid var(--border-header); + position: sticky; + top: 0; + z-index: 80; + background: var(--card-bg); + backdrop-filter: blur(10px); + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +html:is([data-mobile="1"], .layout-phone) .header-bar { + display: contents; +} + +html:is([data-mobile="1"], .layout-phone) .nav-toggle { + display: inline-flex !important; + grid-area: toggle; + width: 40px; + height: 40px; + flex-shrink: 0; +} + +html:is([data-mobile="1"], .layout-phone) .header-tools { + grid-area: tools; + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: .2rem; + flex-shrink: 0; +} + +html:is([data-mobile="1"], .layout-phone) .header-tools .pwa-install-btn { + display: none; +} + +html:is([data-mobile="1"], .layout-phone) .theme-switch { + padding: 2px; + border-radius: 999px; +} + +html:is([data-mobile="1"], .layout-phone) .theme-switch-btn { + padding: .28rem .42rem; + font-size: .66rem; + min-height: 28px; + line-height: 1.1; +} + +html:is([data-mobile="1"], .layout-phone) .user-bar { + display: none; +} + +html:is([data-mobile="1"], .layout-phone) .site-title { + grid-area: title; + font-size: .9rem; + font-weight: 700; + margin: 0; + text-align: left; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +html:is([data-mobile="1"], .layout-phone) .site-title-mobile { + display: inline; +} + +html:is([data-mobile="1"], .layout-phone) .site-title-desktop { + display: none; +} + +.site-title-mobile { + display: none; +} + +html:is([data-mobile="1"], .layout-phone) .site-title-sub { + display: none !important; +} + +html:is([data-mobile="1"], .layout-phone) .pwa-ios-hint { + display: none !important; +} + +html:is([data-mobile="1"], .layout-phone) .site-nav { + position: fixed !important; + top: 0; + left: 0; + width: min(88vw, 300px); + height: 100dvh; + margin: 0; + padding: calc(var(--safe-top) + 3.25rem) .85rem 1.25rem; + flex-direction: column !important; + flex-wrap: nowrap !important; + justify-content: flex-start !important; + align-items: stretch !important; + gap: .3rem; + background: var(--card-bg); + border-right: 1px solid var(--card-border); + box-shadow: var(--shadow-card-hover); + z-index: 100; + transform: translateX(-105%); + transition: transform .25s ease; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +html:is([data-mobile="1"], .layout-phone) .site-nav.open { + transform: translateX(0); +} + +html:is([data-mobile="1"], .layout-phone) .site-nav a { + width: 100%; + text-align: left; + padding: .7rem .85rem; + font-size: .88rem; + min-height: 44px; + display: flex; + align-items: center; + border-radius: 10px; + white-space: nowrap; +} + +html:is([data-mobile="1"], .layout-phone) .main { + padding: .65rem .6rem calc(1rem + var(--safe-bottom)); + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +html:is([data-mobile="1"], .layout-phone) .card { + padding: .85rem; + border-radius: 12px; + margin-bottom: .75rem; + overflow: visible; + max-width: 100%; + box-sizing: border-box; +} + +html:is([data-mobile="1"], .layout-phone) .card h2 { + font-size: .95rem; + margin-bottom: .55rem; +} + +html:is([data-mobile="1"], .layout-phone) .split-grid, +html:is([data-mobile="1"], .layout-phone) .trade-split, +html:is([data-mobile="1"], .layout-phone) .strategy-page .split-grid { + display: flex !important; + flex-direction: column !important; + gap: .75rem !important; + grid-template-columns: 1fr !important; + width: 100%; + max-width: 100%; +} + +html:is([data-mobile="1"], .layout-phone) .split-grid .card, +html:is([data-mobile="1"], .layout-phone) .trade-split .card, +html:is([data-mobile="1"], .layout-phone) .strategy-page .split-grid .card { + min-height: auto !important; + height: auto !important; + width: 100%; + max-width: 100%; +} + +/* ── 下单监控页 ── */ +html:is([data-mobile="1"], .layout-phone) .trade-page { + width: 100%; + max-width: 100%; + overflow-x: hidden; +} + +html:is([data-mobile="1"], .layout-phone) .trade-top-bar { + flex-direction: column; + gap: .55rem; + padding: .65rem; + border-radius: 12px; + background: var(--card-inner); + border: 1px solid var(--card-border); + margin-bottom: .75rem; + max-width: 100%; + box-sizing: border-box; +} + +html:is([data-mobile="1"], .layout-phone) .trade-top-bar-main { + flex-direction: column; + align-items: flex-start; + gap: .35rem; + width: 100%; + min-width: 0; +} + +html:is([data-mobile="1"], .layout-phone) .trade-top-bar-main .badge { + font-size: .68rem; +} + +html:is([data-mobile="1"], .layout-phone) .trade-session-clock { + display: block; + font-size: .7rem; + line-height: 1.45; + word-break: break-word; +} + +html:is([data-mobile="1"], .layout-phone) .trade-top-bar-actions { + width: 100%; +} + +html:is([data-mobile="1"], .layout-phone) .trade-top-bar-actions .btn-ctp-sm { + width: 100%; + min-height: 44px; +} + +html:is([data-mobile="1"], .layout-phone) .trade-top-hint { + font-size: .65rem; + line-height: 1.4; + white-space: normal; +} + +html:is([data-mobile="1"], .layout-phone) .trade-form-rows { + width: 100%; + max-width: 100%; + min-width: 0; +} + +html:is([data-mobile="1"], .layout-phone) .trade-form-line { + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; +} + +/* 品种独占一行;方向 + 手数并排 */ +html:is([data-mobile="1"], .layout-phone) .trade-form-line.line-3 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + gap: .45rem !important; + align-items: end; +} + +html:is([data-mobile="1"], .layout-phone) .trade-form-line.line-3 .trade-field:first-child { + grid-column: 1 / -1 !important; +} + +html:is([data-mobile="1"], .layout-phone) .trade-form-line.line-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + gap: .45rem !important; +} + +html:is([data-mobile="1"], .layout-phone) .trade-field, +html:is([data-mobile="1"], .layout-phone) .symbol-wrap { + min-width: 0; + max-width: 100%; +} + +html:is([data-mobile="1"], .layout-phone) .trade-field label, +html:is([data-mobile="1"], .layout-phone) .text-label { + font-size: .68rem; + margin-bottom: .18rem; +} + +html:is([data-mobile="1"], .layout-phone) .trade-field input, +html:is([data-mobile="1"], .layout-phone) .trade-field select { + padding: .42rem .5rem; + max-width: 100%; +} + +html:is([data-mobile="1"], .layout-phone) .symbol-input { + font-size: 14px; +} + +html:is([data-mobile="1"], .layout-phone) .price-type-tabs { + margin-bottom: .22rem; + gap: .25rem; +} + +html:is([data-mobile="1"], .layout-phone) .price-tab { + padding: .2rem .3rem; + font-size: .66rem; + flex: 1; +} + +html:is([data-mobile="1"], .layout-phone) .form-compact .line-trend-head { + grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.7fr) minmax(0, 0.75fr) !important; + gap: .4rem .45rem !important; +} + +html:is([data-mobile="1"], .layout-phone) .form-compact .line-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + gap: .4rem .45rem !important; +} + +html:is([data-mobile="1"], .layout-phone) .form-compact .line-3 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + gap: .4rem .45rem !important; +} + +html:is([data-mobile="1"], .layout-phone) #roll-form .form-line.line-2 { + grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr) !important; + gap: .4rem .45rem !important; +} + +html:is([data-mobile="1"], .layout-phone) .form-compact .form-line { + min-width: 0; +} + +html:is([data-mobile="1"], .layout-phone) .form-compact input, +html:is([data-mobile="1"], .layout-phone) .form-compact select { + padding: .42rem .5rem; +} + +html:is([data-mobile="1"], .layout-phone) .trade-action-row { + flex-direction: column; + align-items: stretch; +} + +html:is([data-mobile="1"], .layout-phone) .trade-action-row .btn-open { + width: 100%; + min-height: 44px; +} + +html:is([data-mobile="1"], .layout-phone) .pos-metrics { + grid-template-columns: repeat(2, 1fr) !important; + gap: .45rem; +} + +html:is([data-mobile="1"], .layout-phone) .pos-card { + padding: .75rem; +} + +html:is([data-mobile="1"], .layout-phone) .pos-card-head { + flex-direction: column; + align-items: stretch; + gap: .45rem; +} + +html:is([data-mobile="1"], .layout-phone) .pos-card-actions { + width: 100%; + justify-content: flex-end; +} + +html:is([data-mobile="1"], .layout-phone) .rec-sort-bar { + flex-direction: column; + align-items: stretch; + gap: .45rem; +} + +html:is([data-mobile="1"], .layout-phone) .rec-sort-bar select { + width: 100%; + min-width: 0; +} + +html:is([data-mobile="1"], .layout-phone) #recommend .trade-table-wrap, +html:is([data-mobile="1"], .layout-phone) .trade-table-wrap { + overflow-x: auto !important; + overflow-y: auto; + max-height: 55vh; + -webkit-overflow-scrolling: touch; + width: 100%; + max-width: 100%; +} + +html:is([data-mobile="1"], .layout-phone) .trade-table { + font-size: .72rem; +} + +html:is([data-mobile="1"], .layout-phone) .strategy-page .split-grid .card { + min-height: auto !important; +} + +html:is([data-mobile="1"], .layout-phone) .strategy-preview-table { + font-size: .66rem; + min-width: 0; + width: 100%; +} + +html:is([data-mobile="1"], .layout-phone) .table-responsive { + margin-bottom: .5rem; + max-width: 100%; + overflow-x: auto; +} + +html:is([data-mobile="1"], .layout-phone) .module-rules summary { + font-size: .78rem; +} + +html:is([data-mobile="1"], .layout-phone) input, +html:is([data-mobile="1"], .layout-phone) select, +html:is([data-mobile="1"], .layout-phone) textarea, +html:is([data-mobile="1"], .layout-phone) button { + font-size: 16px; +} + +html:is([data-mobile="1"], .layout-phone) .btn-primary { + min-height: 44px; +} + +html:is([data-mobile="1"], .layout-phone) .site-nav::after { + content: attr(data-user-label); + display: block; + margin-top: auto; + padding: .85rem .5rem 0; + font-size: .72rem; + color: var(--text-muted); + border-top: 1px solid var(--table-border); +} + +/* 桌面端:标题仍居中,工具栏绝对定位 */ +html:not([data-mobile="1"]):not(.layout-phone) .header-bar { + display: block; + position: relative; + min-height: 2rem; +} + +html:not([data-mobile="1"]):not(.layout-phone) .nav-toggle { + display: none; +} + +html:not([data-mobile="1"]):not(.layout-phone) .site-title { + display: block; + text-align: center; + margin: 0 0 1.65rem; + font-size: 1.75rem; + width: 100%; +} + +html:not([data-mobile="1"]):not(.layout-phone) .site-title-mobile { + display: none; +} + +html:not([data-mobile="1"]):not(.layout-phone) .site-title-desktop { + display: inline; +} + +html:not([data-mobile="1"]):not(.layout-phone) .header-tools { + position: absolute; + top: 0; + left: 0; + z-index: 20; +} + +html:not([data-mobile="1"]):not(.layout-phone) .user-bar { + display: block; + position: absolute; + top: 0; + right: 0; +} + +/* 触控小屏兜底(JS 未执行时) */ +@media (pointer: coarse) and (max-width: 600px) { + body { + overflow-x: hidden; + overflow-y: auto; + max-width: 100vw; + } + + .site-header { + display: grid !important; + grid-template-columns: 40px minmax(0, 1fr) auto !important; + grid-template-areas: "toggle title tools" !important; + align-items: center; + column-gap: .45rem; + padding: calc(var(--safe-top) + .4rem) .6rem .5rem; + text-align: left; + max-width: 100%; + } + + .header-bar { + display: contents !important; + } + + .nav-toggle { + display: inline-flex !important; + grid-area: toggle; + } + + .header-tools { + grid-area: tools; + justify-content: flex-end; + } + + .user-bar { + display: none !important; + } + + .site-title { + grid-area: title; + font-size: .9rem; + margin: 0; + text-align: left; + min-width: 0; + } + + .site-title-mobile { + display: inline !important; + } + + .site-title-desktop { + display: none !important; + } + + .trade-form-line.line-3 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + gap: .45rem !important; + } + + .trade-form-line.line-3 .trade-field:first-child { + grid-column: 1 / -1 !important; + } + + .card { + overflow: visible; + max-width: 100%; + } +} diff --git a/static/css/records.css b/static/css/records.css new file mode 100644 index 0000000..24a841c --- /dev/null +++ b/static/css/records.css @@ -0,0 +1,188 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. + * 交易记录与复盘 — 手机简洁列表 + 详情弹窗 + */ + +.records-page .records-mobile-list { + display: none; +} + +.records-page .records-desktop-only { + display: block; +} + +.records-mobile-item { + display: block; + width: 100%; + text-align: left; + border: 1px solid var(--card-border); + border-radius: 12px; + background: var(--card-inner); + padding: .75rem .85rem; + margin-bottom: .55rem; + cursor: pointer; + color: inherit; + font: inherit; + transition: border-color .2s, box-shadow .2s; +} + +.records-mobile-item:hover, +.records-mobile-item:focus-visible { + border-color: var(--accent); + box-shadow: 0 0 0 1px rgba(56, 189, 248, .2); + outline: none; +} + +.records-mobile-item-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: .5rem; + margin-bottom: .35rem; +} + +.records-mobile-symbol { + font-size: .92rem; + font-weight: 600; + color: var(--text-title); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.records-mobile-item-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .35rem .5rem; + font-size: .72rem; + color: var(--text-muted); + margin-bottom: .3rem; +} + +.records-mobile-item-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: .5rem; +} + +.records-mobile-pnl { + font-size: .88rem; + font-weight: 600; +} + +.records-mobile-pnl.is-profit { color: var(--profit); } +.records-mobile-pnl.is-loss { color: var(--loss); } +.records-mobile-pnl.is-flat { color: var(--text-muted); } + +.records-mobile-chevron { + font-size: .72rem; + color: var(--accent); + white-space: nowrap; +} + +.records-mobile-empty { + padding: 1.25rem .5rem; + text-align: center; + color: var(--text-muted); + font-size: .85rem; +} + +.records-page .trade-switch-label.records-desktop-only { + display: flex; +} + +#trade-detail-modal .records-detail-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .55rem .75rem; + margin-bottom: 1rem; +} + +#trade-detail-modal .records-detail-item label { + display: block; + font-size: .68rem; + color: var(--text-muted); + margin-bottom: .12rem; +} + +#trade-detail-modal .records-detail-item div { + font-size: .84rem; + color: var(--text-primary); + line-height: 1.35; + word-break: break-word; +} + +#trade-detail-modal .records-detail-item.wide { + grid-column: 1 / -1; +} + +#trade-detail-modal .records-detail-actions { + display: flex; + flex-wrap: wrap; + gap: .45rem; + padding-top: .75rem; + border-top: 1px solid var(--table-border); +} + +#trade-detail-modal .records-detail-actions a, +#trade-detail-modal .records-detail-actions button { + font-size: .78rem; + padding: .45rem .7rem; + border-radius: 8px; + text-decoration: none; + border: none; + cursor: pointer; +} + +.records-review-mobile .records-mobile-item.is-emotion { + border-color: rgba(239, 68, 68, .35); +} + +html:is([data-mobile="1"], .layout-phone) .records-page .records-mobile-list, +html:is([data-layout="phone"], .layout-phone) .records-page .records-mobile-list { + display: block; +} + +html:is([data-mobile="1"], .layout-phone) .records-page .records-desktop-only, +html:is([data-layout="phone"], .layout-phone) .records-page .records-desktop-only { + display: none !important; +} + +html:is([data-mobile="1"], .layout-phone) .records-page .records-trade-card .card-body, +html:is([data-layout="phone"], .layout-phone) .records-page .records-trade-card .card-body { + padding: 0; +} + +html:is([data-mobile="1"], .layout-phone) .records-page .records-equity-card #equity-curve-chart, +html:is([data-layout="phone"], .layout-phone) .records-page .records-equity-card #equity-curve-chart { + min-height: 180px; +} + +html:is([data-mobile="1"], .layout-phone) .records-page .preset-tabs, +html:is([data-layout="phone"], .layout-phone) .records-page .preset-tabs { + flex-wrap: wrap; + gap: .35rem; +} + +html:is([data-mobile="1"], .layout-phone) .records-page .preset-tabs a, +html:is([data-layout="phone"], .layout-phone) .records-page .preset-tabs a { + flex: 1; + text-align: center; + min-width: 0; + padding: .45rem .35rem; + font-size: .75rem; +} + +html:is([data-mobile="1"], .layout-phone) #review-modal .review-detail-headers, +html:is([data-mobile="1"], .layout-phone) #review-modal .review-detail-values, +html:is([data-layout="phone"], .layout-phone) #review-modal .review-detail-headers, +html:is([data-layout="phone"], .layout-phone) #review-modal .review-detail-values { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +@media (pointer: coarse) and (max-width: 600px) { + .records-page .records-mobile-list { display: block; } + .records-page .records-desktop-only { display: none !important; } +} diff --git a/static/css/responsive.css b/static/css/responsive.css index 504b693..e046a7d 100644 --- a/static/css/responsive.css +++ b/static/css/responsive.css @@ -214,42 +214,42 @@ body { } @media (max-width: 767px) { - .nav-toggle { + html:not([data-mobile="1"]):not(.layout-phone) .nav-toggle { display: inline-flex; } - .header-bar { + html:not([data-mobile="1"]):not(.layout-phone) .header-bar { grid-template-columns: auto 1fr; grid-template-areas: "toggle tools" "user user"; } - .nav-toggle { grid-area: toggle; } - .header-tools { grid-area: tools; justify-content: flex-end; } - .user-bar { + html:not([data-mobile="1"]):not(.layout-phone) .nav-toggle { grid-area: toggle; } + html:not([data-mobile="1"]):not(.layout-phone) .header-tools { grid-area: tools; justify-content: flex-end; } + html:not([data-mobile="1"]):not(.layout-phone) .user-bar { grid-area: user; text-align: center; width: 100%; } - .site-header { + html:not([data-mobile="1"]):not(.layout-phone) .site-header { padding: .85rem .75rem .75rem; text-align: left; } - .site-title { + html:not([data-mobile="1"]):not(.layout-phone) .site-title { font-size: 1.15rem; margin-bottom: .65rem; text-align: center; } - .site-title-sub { + html:not([data-mobile="1"]):not(.layout-phone) .site-title-sub { font-size: .58rem; letter-spacing: .1em; } - .site-nav { + html:not([data-mobile="1"]):not(.layout-phone) .site-nav { position: fixed; top: 0; left: 0; @@ -547,3 +547,264 @@ body.login-page { padding: 1.75rem 1.25rem 1.5rem; } } + +/* ── 设备布局:手机竖屏 / 平板横屏 ── */ + +.orientation-lock { + position: fixed; + inset: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + background: var(--modal-mask); + backdrop-filter: blur(6px); +} + +.orientation-lock[hidden] { + display: none !important; +} + +.orientation-lock-box { + max-width: 18rem; + padding: 1.75rem 1.25rem; + border-radius: 14px; + border: 1px solid var(--card-border); + background: var(--card-bg); + text-align: center; + box-shadow: var(--shadow-card-hover); +} + +.orientation-lock-icon { + font-size: 2.5rem; + line-height: 1; + margin-bottom: .85rem; + color: var(--accent); + animation: orientation-spin 2.4s ease-in-out infinite; +} + +.orientation-lock-box p { + margin: 0; + font-size: .95rem; + line-height: 1.55; + color: var(--text-title); +} + +@keyframes orientation-spin { + 0%, 100% { transform: rotate(0deg); } + 50% { transform: rotate(90deg); } +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .nav-toggle { + display: inline-flex; +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .header-bar { + grid-template-columns: auto 1fr; + grid-template-areas: + "toggle tools" + "user user"; +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .nav-toggle { grid-area: toggle; } +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .header-tools { grid-area: tools; justify-content: flex-end; } +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .user-bar { + grid-area: user; + text-align: center; + width: 100%; +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .site-header { + padding: .75rem .75rem .65rem; + text-align: left; +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .site-title { + font-size: 1.05rem; + margin-bottom: .5rem; + text-align: center; +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .site-title-sub { + display: none; +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .site-nav { + position: fixed; + top: 0; + left: 0; + width: min(86vw, 320px); + height: 100dvh; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + gap: .35rem; + padding: calc(var(--safe-top) + 3.5rem) 1rem 1.5rem; + background: var(--card-bg); + border-right: 1px solid var(--card-border); + box-shadow: var(--shadow-card-hover); + z-index: 100; + transform: translateX(-105%); + transition: transform .28s ease; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .site-nav.open { + transform: translateX(0); +} + +html[data-layout="phone"]:not([data-mobile="1"]):not(.layout-phone) .site-nav a { + width: 100%; + text-align: left; + padding: .75rem 1rem; + font-size: .9rem; + min-height: var(--touch-min); + display: flex; + align-items: center; + border-radius: 10px; +} + +html[data-layout="phone"] .main { + padding: .75rem .65rem 1.1rem; +} + +html[data-layout="phone"] .split-grid { + grid-template-columns: 1fr; + gap: .85rem; +} + +html[data-layout="phone"] .split-grid .card, +html[data-layout="phone"] .trade-split .card { + min-height: auto; +} + +html[data-layout="phone"] .trade-top-bar-main { + flex-direction: column; + align-items: flex-start; + gap: .35rem; +} + +html[data-layout="phone"] .trade-session-clock { + display: block; + font-size: .72rem; + line-height: 1.45; +} + +html[data-layout="phone"] .session-clock-detail { + display: block; + margin-top: .15rem; +} + +html[data-layout="phone"] .trade-top-hint { + font-size: .68rem; + line-height: 1.4; +} + +html[data-layout="phone"] .pos-metrics { + grid-template-columns: repeat(2, 1fr); +} + +html[data-layout="phone"] .pos-card-head { + flex-direction: column; + align-items: stretch; +} + +html[data-layout="phone"] .pos-card-actions { + width: 100%; + justify-content: flex-end; +} + +html[data-layout="phone"] .rec-sort-bar { + flex-direction: column; + align-items: stretch; +} + +html[data-layout="phone"] .rec-sort-bar select { + width: 100%; + min-width: 0; +} + +html[data-layout="phone"] #recommend .trade-table-wrap { + overflow-x: auto; + overflow-y: auto; + max-height: 52vh; +} + +html[data-layout="phone"] .strategy-preview-table { + font-size: .68rem; +} + +html[data-layout="tablet"][data-orientation="landscape"] .site-header { + padding: .85rem 1rem .75rem; +} + +html[data-layout="tablet"][data-orientation="landscape"] .site-title { + font-size: 1.35rem; + margin-bottom: .85rem; +} + +html[data-layout="tablet"][data-orientation="landscape"] .site-nav { + flex-wrap: nowrap; + overflow-x: auto; + justify-content: flex-start; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} + +html[data-layout="tablet"][data-orientation="landscape"] .site-nav::-webkit-scrollbar { + display: none; +} + +html[data-layout="tablet"][data-orientation="landscape"] .site-nav a { + flex-shrink: 0; + padding: .48rem .75rem; + font-size: .8rem; +} + +html[data-layout="tablet"][data-orientation="landscape"] .main { + padding: 1rem .85rem; +} + +html[data-layout="tablet"][data-orientation="landscape"] .split-grid, +html[data-layout="tablet"][data-orientation="landscape"] .trade-split { + grid-template-columns: 1fr 1fr; + gap: 1rem; + align-items: stretch; +} + +html[data-layout="tablet"][data-orientation="landscape"] .split-grid .card, +html[data-layout="tablet"][data-orientation="landscape"] .trade-split .card { + min-height: 380px; +} + +html[data-layout="tablet"][data-orientation="landscape"] .trade-form-line.line-3 { + grid-template-columns: 1fr 1fr; +} + +html[data-layout="tablet"][data-orientation="landscape"] .trade-form-line.line-3 .trade-field:first-child { + grid-column: 1 / -1; +} + +html[data-layout="tablet"][data-orientation="landscape"] .pos-metrics { + grid-template-columns: repeat(4, 1fr); +} + +html[data-layout="tablet"][data-orientation="landscape"] .trade-top-bar { + flex-direction: row; + align-items: flex-start; +} + +html[data-layout="tablet"][data-orientation="landscape"] .trade-top-bar-actions { + width: auto; + flex-shrink: 0; +} + +html[data-layout="tablet"][data-orientation="landscape"] .strategy-page .split-grid { + grid-template-columns: 1fr 1fr; +} + +html[data-layout="tablet"][data-orientation="landscape"] .strategy-page .split-grid .card { + min-height: 420px; +} diff --git a/static/css/trade.css b/static/css/trade.css index 983a0ce..e66504f 100644 --- a/static/css/trade.css +++ b/static/css/trade.css @@ -134,7 +134,7 @@ .trade-top-bar-actions{width:100%} .btn-ctp-sm{width:100%;min-height:44px} .trade-split .card{min-height:auto} - .trade-form-line.line-3{grid-template-columns:1fr} + html:not([data-mobile="1"]) .trade-form-line.line-3{grid-template-columns:1fr} .trade-card-full{margin-bottom:1rem} .trade-table-wrap{max-height:320px} } diff --git a/static/js/nav.js b/static/js/nav.js index d643240..ccc205e 100644 --- a/static/js/nav.js +++ b/static/js/nav.js @@ -29,7 +29,9 @@ } function isMobileNav() { - return window.matchMedia('(max-width: 767px)').matches; + if (window.qihuoLayout && window.qihuoLayout.isPhone()) return true; + return document.documentElement.dataset.mobile === '1' + || window.matchMedia('(max-width: 767px)').matches; } toggle.addEventListener('click', function () { @@ -70,6 +72,14 @@ if (!isMobileNav()) closeNav(); }); + if (window.qihuoLayout) { + window.addEventListener('orientationchange', function () { + setTimeout(function () { + if (!isMobileNav()) closeNav(); + }, 150); + }); + } + document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeNav(); }); diff --git a/static/js/orientation.js b/static/js/orientation.js new file mode 100644 index 0000000..21cc4bb --- /dev/null +++ b/static/js/orientation.js @@ -0,0 +1,102 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. + * 专有软件 — 未经授权禁止复制、传播、转售。 + * 详见 LICENSE.zh-CN.txt + */ +(function () { + function isCoarsePointer() { + return window.matchMedia('(hover: none) and (pointer: coarse)').matches; + } + + function isTabletUa() { + var ua = navigator.userAgent || ''; + if (/iPad/i.test(ua)) return true; + if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) return true; + if (/Android/i.test(ua) && !/Mobile/i.test(ua)) return true; + if (/Tablet|PlayBook|Silk/i.test(ua)) return true; + return false; + } + + function isPhoneUa() { + var ua = navigator.userAgent || ''; + if (isTabletUa()) return false; + if (/iPhone|iPod/i.test(ua)) return true; + if (/Android/i.test(ua) && /Mobile/i.test(ua)) return true; + if (/HarmonyOS|OpenHarmony/i.test(ua) && !/Tablet/i.test(ua)) return true; + if (/Mobile|Windows Phone|IEMobile|BlackBerry/i.test(ua)) return true; + return false; + } + + function screenShortSide() { + return Math.min(window.screen.width || 0, window.screen.height || 0); + } + + function detectLayout() { + var shortSide = screenShortSide(); + var coarse = isCoarsePointer(); + + if (isPhoneUa()) return 'phone'; + if (isTabletUa()) return 'tablet'; + + if (shortSide > 0 && shortSide < 600) return 'phone'; + if (shortSide >= 600 && shortSide <= 1100 && coarse) return 'tablet'; + if (window.innerWidth <= 767) return 'phone'; + if (window.innerWidth <= 1100 && coarse) return 'tablet'; + return 'desktop'; + } + + function updateLayoutState() { + var root = document.documentElement; + var layout = detectLayout(); + var isPhone = layout === 'phone'; + var landscape = window.innerWidth > window.innerHeight; + + root.dataset.layout = layout; + root.dataset.mobile = isPhone ? '1' : '0'; + root.dataset.orientation = isPhone ? 'portrait' : (landscape ? 'landscape' : 'portrait'); + root.classList.toggle('layout-phone', isPhone); + root.classList.toggle('layout-tablet', layout === 'tablet'); + + var nav = document.getElementById('site-nav'); + var userBar = document.querySelector('.user-bar'); + if (nav && userBar && isPhone) { + nav.setAttribute('data-user-label', (userBar.textContent || '').replace(/\s+/g, ' ').trim()); + } + + var overlay = document.getElementById('orientation-lock'); + var msg = document.getElementById('orientation-lock-msg'); + if (!overlay) return; + + if (isPhone) { + if (landscape) { + overlay.hidden = false; + if (msg) msg.textContent = '请竖屏使用'; + } else { + overlay.hidden = true; + } + return; + } + + if (layout === 'tablet' && root.dataset.orientation === 'portrait') { + overlay.hidden = false; + if (msg) msg.textContent = '平板请旋转至横屏使用'; + return; + } + + overlay.hidden = true; + } + + window.qihuoLayout = { + isPhone: function () { return document.documentElement.dataset.mobile === '1'; }, + isTablet: function () { return document.documentElement.dataset.layout === 'tablet'; }, + refresh: updateLayoutState, + }; + + updateLayoutState(); + window.addEventListener('resize', updateLayoutState); + window.addEventListener('orientationchange', function () { + setTimeout(updateLayoutState, 150); + }); + document.addEventListener('visibilitychange', function () { + if (!document.hidden) updateLayoutState(); + }); +})(); diff --git a/static/js/records.js b/static/js/records.js new file mode 100644 index 0000000..43def63 --- /dev/null +++ b/static/js/records.js @@ -0,0 +1,96 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. + * 交易记录 — 手机简洁列表与详情弹窗 + */ +(function () { + function esc(v) { + if (v === null || v === undefined || v === '') return '-'; + return String(v); + } + + function fmtTime(v) { + if (!v) return '-'; + return String(v).replace('T', ' ').slice(0, 16); + } + + function pnlClass(v) { + var n = parseFloat(v); + if (isNaN(n) || n === 0) return 'is-flat'; + return n > 0 ? 'is-profit' : 'is-loss'; + } + + function showTradeModal(data) { + var mask = document.getElementById('trade-detail-modal'); + var body = document.getElementById('trade-detail-modal-body'); + if (!mask || !body) return; + + var fields = [ + { label: '品种', value: esc(data.symbol), wide: false }, + { label: '合约', value: esc(data.symbol_code), wide: false }, + { label: '类型', value: esc(data.monitor_type) + ' · ' + esc(data.source), wide: false }, + { label: '方向', value: esc(data.direction), wide: false }, + { label: '成交价', value: esc(data.entry_price), wide: false }, + { label: '手数', value: esc(data.lots), wide: false }, + { label: '止损', value: esc(data.stop_loss), wide: false }, + { label: '止盈', value: esc(data.take_profit), wide: false }, + { label: '保证金', value: data.margin != null ? esc(data.margin) : '-', wide: false }, + { label: '保证金占比', value: data.margin_pct != null ? esc(data.margin_pct) + '%' : '-', wide: false }, + { label: '持仓分钟', value: esc(data.holding_minutes), wide: false }, + { label: '开仓时间', value: fmtTime(data.open_time), wide: false }, + { label: '平仓时间', value: fmtTime(data.close_time), wide: false }, + { label: '盈亏(元)', value: esc(data.pnl), wide: false }, + { label: '手续费', value: esc(data.fee), wide: false }, + { label: '净盈亏', value: esc(data.pnl_net), wide: false }, + { label: '最新资金', value: esc(data.equity_after), wide: false }, + { label: '结果', value: esc(data.result) + (data.verified ? ' · 已核对' : ''), wide: true } + ]; + + var html = '
'; + fields.forEach(function (f) { + html += '
'; + html += '
' + f.value + '
'; + }); + html += '
'; + + html += '
'; + if (data.fill_review_url) { + html += '填入复盘'; + } + if (data.del_url) { + html += '删除'; + } + html += '
'; + + body.innerHTML = html; + mask.classList.add('show'); + } + + function bindTradeModal() { + var mask = document.getElementById('trade-detail-modal'); + if (!mask) return; + var closeBtn = mask.querySelector('.modal-close'); + if (closeBtn) { + closeBtn.addEventListener('click', function () { + mask.classList.remove('show'); + }); + } + mask.addEventListener('click', function (e) { + if (e.target === mask) mask.classList.remove('show'); + }); + document.querySelectorAll('.records-trade-item').forEach(function (btn) { + btn.addEventListener('click', function () { + try { + showTradeModal(JSON.parse(btn.getAttribute('data-trade'))); + } catch (err) { /* ignore */ } + }); + }); + } + + function bootRecordsPage() { + if (!document.querySelector('.records-page')) return; + bindTradeModal(); + } + + if (window.qihuoPageBoot) window.qihuoPageBoot(bootRecordsPage, '.records-page'); + else if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(bootRecordsPage); + else document.addEventListener('DOMContentLoaded', bootRecordsPage); +})(); diff --git a/templates/base.html b/templates/base.html index 51c60e1..dbe8b75 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,11 +24,32 @@ } } catch (e) { /* ignore */ } - - - - + + + + + {% block extra_css %}{% endblock %} +