From db4e9ef57be4eccfe8e71ed42d8be50853602421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=BD=E5=B7=A5=E5=91=98?= <4846a27a-ba87-49c9-939d-7275f63efcc7@agents.multica.local> Date: Mon, 24 Aug 2026 17:41:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(HEL-80):=20=E6=89=8B=E6=9C=BA=E7=AB=AF=20P?= =?UTF-8?q?2b=20=E5=85=AD=E4=B8=AA=E8=A1=8C=E6=83=85=E5=A4=8D=E6=9D=82?= =?UTF-8?q?=E9=A1=B5=EF=BC=88=E6=83=85=E7=BB=AA=E5=91=A8=E6=9C=9F/?= =?UTF-8?q?=E5=A4=A9=E6=A2=AF/=E8=BD=AE=E5=8A=A8/=E7=AB=9E=E4=BB=B7/?= =?UTF-8?q?=E9=A2=98=E6=9D=90=E5=BA=93/=E9=BE=99=E8=99=8E=E6=A6=9C?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- app/frontend/m/config/nav.config.js | 83 +++ app/frontend/m/css/features.css | 1074 +++++++++++++++++++++++++++ app/frontend/m/js/pages.js | 935 ++++++++++++++++++++++- 3 files changed, 2084 insertions(+), 8 deletions(-) diff --git a/app/frontend/m/config/nav.config.js b/app/frontend/m/config/nav.config.js index 043887c..ed6d3b9 100644 --- a/app/frontend/m/config/nav.config.js +++ b/app/frontend/m/config/nav.config.js @@ -211,6 +211,89 @@ ] } } + }, + + "market/sentiment/history": { + frozenColumns: [ + { key: "trade_date", label: "日期", type: "text", width: 88 } + ], + primaryColumns: [ + { key: "score", label: "温度", type: "int" }, + { key: "phase", label: "阶段", type: "text" }, + { key: "direction", label: "方向", type: "text" } + ], + scrollColumns: [ + { key: "limit_up_count", label: "涨停", type: "int" }, + { key: "first_board_count", label: "首板", type: "int" }, + { key: "second_board_count", label: "二板", type: "int" }, + { key: "three_plus_count", label: "三板+", type: "int" }, + { key: "max_height", label: "高度", type: "int" }, + { key: "broken_count", label: "炸板", type: "int" }, + { key: "limit_down_count", label: "跌停", type: "int" }, + { key: "seal_rate", label: "封板率%", type: "rate" }, + { key: "previous_limit_count", label: "昨涨停", type: "int" }, + { key: "previous_positive_rate", label: "昨红率%", type: "rate" } + ] + }, + + "market/auction": { + frozenColumns: [ + { key: "stock", label: "股票", type: "stock", width: 96 } + ], + primaryColumns: [ + { key: "attention_score", label: "关注分", type: "score" }, + { key: "change", label: "竞价涨幅%", type: "change" }, + { key: "expectation", label: "预期", type: "expectation" } + ], + scrollColumns: [ + { key: "sector", label: "方向", type: "text" }, + { key: "amount_million", label: "竞价额百万", type: "money" }, + { key: "volume_ratio", label: "量比", type: "rate" } + ] + }, + + "market/rotation/members": { + frozenColumns: [ + { key: "stock", label: "股票", type: "stock", width: 96 } + ], + primaryColumns: [ + { key: "change", label: "涨跌幅%", type: "change" }, + { key: "close", label: "收盘", type: "price" } + ], + scrollColumns: [ + { key: "open", label: "开盘", type: "price" }, + { key: "amount_billion", label: "成交额亿", type: "money" } + ] + }, + + "market/themes/members": { + frozenColumns: [ + { key: "stock", label: "股票", type: "stock", width: 96 } + ], + primaryColumns: [ + { key: "change", label: "涨跌幅%", type: "change" }, + { key: "price", label: "现价", type: "price" } + ], + scrollColumns: [ + { key: "amount_billion", label: "成交额亿", type: "money" } + ] + }, + + "market/dragon/operations": { + frozenColumns: [ + { key: "stock", label: "股票", type: "stock", width: 96 } + ], + primaryColumns: [ + { key: "direction", label: "方向", type: "direction" }, + { key: "change", label: "涨幅%", type: "change" } + ], + scrollColumns: [ + { key: "buy_million", label: "买入百万", type: "money" }, + { key: "sell_million", label: "卖出百万", type: "money" }, + { key: "net_buy_million", label: "净额百万", type: "change" }, + { key: "seat_name", label: "席位", type: "text", wide: true }, + { key: "reason", label: "上榜原因", type: "text", wide: true } + ] } } }; diff --git a/app/frontend/m/css/features.css b/app/frontend/m/css/features.css index 0258783..ccd6488 100644 --- a/app/frontend/m/css/features.css +++ b/app/frontend/m/css/features.css @@ -1033,3 +1033,1077 @@ transform: none; } } + +/* ===== 复杂行情页(P2b):情绪周期 / 天梯 / 轮动 / 竞价 / 题材库 / 龙虎榜 ===== */ + +.m-scroll { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + background: var(--canvas); +} + +.m-table-wrap { + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + touch-action: pan-x pan-y; +} + +.m-section-title { + display: flex; + align-items: center; + justify-content: space-between; + margin: 14px 12px 8px; + font-size: var(--font-size-label); + font-weight: 600; + color: var(--text-secondary); +} + +.m-card, +.m-chart-card, +.m-sent-card, +.m-apex { + margin: 0 12px; + padding: 14px; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); +} + +.m-chart-card { + padding: 8px 6px; + height: var(--mobile-chart-height); +} + +.m-chart-card svg { + width: 100%; + height: 100%; + display: block; +} + +.m-inline-empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + min-height: 60px; + font-size: var(--font-size-label); + color: var(--text-tertiary); +} + +/* 情绪周期 */ +.m-sent-score-row { + display: flex; + align-items: center; + gap: 10px; +} + +.m-sent-score { + font-size: 40px; + font-weight: 700; + line-height: 1; + font-variant-numeric: tabular-nums; +} + +.m-sent-score-side { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} + +.m-phase { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 999px; +} + +.m-phase--ice { background: var(--action-soft); color: var(--action); } +.m-phase--repair { background: var(--market-down-soft); color: var(--market-down); } +.m-phase--fermentation { background: var(--warning-soft); color: var(--warning); } +.m-phase--climax { background: var(--market-up-soft); color: var(--market-up); } +.m-phase--divergence { background: var(--surface-muted); color: var(--text-secondary); } +.m-phase--retreat { background: var(--surface-muted); color: var(--text-tertiary); } + +.m-sent-direction { + font-size: 12px; + font-weight: 600; +} + +.m-sent-dir--up { color: var(--market-up); } +.m-sent-dir--down { color: var(--market-down); } +.m-sent-dir--new { color: var(--action); } +.m-sent-dir--flat { color: var(--text-secondary); } + +.m-sent-label { + margin-top: 6px; + font-size: var(--font-size-card-title); + color: var(--text-secondary); +} + +.m-sent-metrics { + display: flex; + gap: 8px; + margin-top: 12px; + flex-wrap: wrap; +} + +.m-sent-metric { + flex: 1; + min-width: 72px; + display: flex; + flex-direction: column; + gap: 2px; + padding: 8px; + border-radius: 6px; + background: var(--surface-muted); +} + +.m-sent-metric span { + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-sent-metric b { + font-size: var(--font-size-card-title); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.m-sent-up b { color: var(--market-up); } +.m-sent-warn b { color: var(--warning); } +.m-sent-down b { color: var(--market-down); } + +.m-sent-advice { + margin-top: 10px; + font-size: var(--font-size-label); + color: var(--text-secondary); + line-height: 1.5; +} + +.m-sentiment-line { + fill: none; + stroke: var(--action); + stroke-width: 2; +} + +.m-sentiment-area { + fill: var(--action-soft); + stroke: none; +} + +.m-components { + margin: 0 12px; + padding: 4px 14px 14px; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); +} + +.m-component { + padding: 12px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.m-component:last-child { + border-bottom: 0; +} + +.m-component-head { + display: flex; + align-items: center; + justify-content: space-between; +} + +.m-component-head span { + font-size: var(--font-size-label); + font-weight: 500; +} + +.m-component-head b { + font-size: var(--font-size-card-title); + font-variant-numeric: tabular-nums; +} + +.m-component-track { + height: 6px; + margin: 8px 0 6px; + border-radius: 3px; + background: var(--surface-muted); + overflow: hidden; +} + +.m-component-track i { + display: block; + height: 100%; + border-radius: 3px; + background: var(--action); +} + +.m-component small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-range-tabs { + display: inline-flex; + gap: 4px; +} + +.m-range-tab { + height: 26px; + padding: 0 10px; + border: 1px solid var(--border-strong); + border-radius: 999px; + background: var(--surface); + color: var(--text-secondary); + font-size: var(--font-size-aux); + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.m-range-tab.active { + background: var(--action-soft); + border-color: var(--action); + color: var(--action); + font-weight: 600; +} + +/* 市场天梯 */ +.m-apex-head { + display: flex; + align-items: baseline; + gap: 8px; +} + +.m-apex-head span { + font-size: var(--font-size-label); + color: var(--text-secondary); +} + +.m-apex-head strong { + font-size: 24px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.m-apex-head em { + font-size: var(--font-size-aux); + color: var(--text-tertiary); + font-style: normal; +} + +.m-apex p { + margin: 8px 0 4px; + font-size: var(--font-size-label); + line-height: 1.5; +} + +.m-apex p b { + color: var(--text-primary); +} + +.m-apex small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-ladder-tier { + margin: 8px 12px 0; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); + overflow: hidden; +} + +.m-ladder-tier-head { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-bottom: 1px solid var(--border-subtle); +} + +.m-ladder-tier-level { + font-size: var(--font-size-card-title); + font-weight: 600; +} + +.m-ladder-tier-count { + font-size: var(--font-size-label); + color: var(--text-secondary); +} + +.m-ladder-rate { + margin-left: auto; + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-ladder-stocks { + padding: 4px 0; +} + +.m-ladder-stock { + display: flex; + flex-direction: column; + gap: 2px; + width: 100%; + padding: 10px 14px; + border: 0; + background: transparent; + text-align: left; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: background-color var(--motion-press-release) var(--ease-press); +} + +.m-ladder-stock:active { + background: var(--surface-hover); +} + +.m-ladder-stock-main { + display: flex; + align-items: center; + gap: 6px; +} + +.m-ladder-stock-main strong { + font-size: var(--font-size-table); + font-weight: 500; +} + +.m-ladder-stock-main small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); + font-variant-numeric: tabular-nums; +} + +.m-ladder-tag { + font-style: normal; + font-size: 10px; + padding: 1px 4px; + border-radius: 3px; + background: var(--action-soft); + color: var(--action); +} + +.m-ladder-tag.is-broken { + background: var(--warning-soft); + color: var(--warning); +} + +.m-ladder-stock-sub { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.m-ladder-stock-sub b { + font-size: var(--font-size-label); + font-weight: 400; + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.m-ladder-stock-sub small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); + flex: 0 0 auto; +} + +.m-ladder-gap { + padding: 12px 14px; + font-size: var(--font-size-label); + color: var(--text-tertiary); +} + +.m-ladder-more { + width: 100%; + height: 40px; + border: 0; + border-top: 1px solid var(--border-subtle); + background: transparent; + color: var(--action); + font-size: var(--font-size-label); + cursor: pointer; +} + +.m-rate-list { + margin: 0 12px; + padding: 12px 14px; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); +} + +.m-rate-row { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 0; +} + +.m-rate-row span { + width: 76px; + flex: 0 0 auto; + font-size: var(--font-size-label); + color: var(--text-secondary); +} + +.m-rate-row i { + flex: 1; + height: 8px; + border-radius: 4px; + background: var(--surface-muted); + overflow: hidden; +} + +.m-rate-row i b { + display: block; + height: 100%; + background: var(--action); +} + +.m-rate-row strong { + width: 52px; + flex: 0 0 auto; + text-align: right; + font-size: var(--font-size-label); + font-variant-numeric: tabular-nums; +} + +/* 主题轮动 */ +.m-rotation-day { + margin: 0 12px 10px; + padding: 10px 12px; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); +} + +.m-rotation-day.is-latest { + border: 1px solid var(--action-soft); +} + +.m-rotation-day header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.m-rotation-day header time { + font-size: var(--font-size-card-title); + font-weight: 600; +} + +.m-rotation-day header span { + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-rotation-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.m-rotation-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid var(--border-strong); + background: var(--surface); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: transform var(--motion-press-release) var(--ease-press), + background-color var(--motion-press-release) var(--ease-press); +} + +.m-rotation-chip:active { + transform: scale(0.96); + background: var(--surface-hover); +} + +.m-rotation-chip strong { + font-size: var(--font-size-label); + font-weight: 500; +} + +.m-rotation-chip small { + font-size: var(--font-size-aux); + color: var(--text-secondary); +} + +.m-rotation-chip.heat-strong { + border-color: var(--market-up); +} + +.m-rotation-chip.heat-warm { + border-color: var(--warning); +} + +.m-rotation-rank { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + background: var(--surface-muted); + color: var(--text-secondary); +} + +.m-rotation-rank.rank-1 { background: var(--market-up-soft); color: var(--market-up); } +.m-rotation-rank.rank-2 { background: var(--warning-soft); color: var(--warning); } +.m-rotation-rank.rank-3 { background: var(--market-down-soft); color: var(--market-down); } +.m-rotation-rank.rank-4 { background: var(--action-soft); color: var(--action); } + +/* 竞价 */ +.m-phase-notice { + margin: 10px 12px; + padding: 10px 12px; + border-radius: 8px; + background: var(--action-soft); + display: flex; + flex-direction: column; + gap: 2px; +} + +.m-phase-notice strong { + font-size: var(--font-size-label); + color: var(--action); +} + +.m-phase-notice span { + font-size: var(--font-size-aux); + color: var(--text-secondary); +} + +.m-phase-notice--selection { + background: var(--warning-soft); +} + +.m-phase-notice--selection strong { + color: var(--warning); +} + +.m-sum-strip { + display: flex; + margin: 10px 12px; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); + overflow: hidden; +} + +.m-sum-metric { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; + padding: 10px 2px; + text-align: center; +} + +.m-sum-metric + .m-sum-metric { + border-left: 1px solid var(--border); +} + +.m-sum-metric span { + font-size: var(--font-size-aux); + color: var(--text-secondary); + white-space: nowrap; +} + +.m-sum-metric strong { + font-size: var(--font-size-card-title); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.m-sum-metric strong small { + font-size: 10px; + font-weight: 400; + color: var(--text-tertiary); +} + +.m-sum-up strong { color: var(--market-up); } +.m-sum-down strong { color: var(--market-down); } +.m-sum-warn strong { color: var(--warning); } + +.m-mini-bars { + display: flex; + align-items: flex-end; + gap: 4px; + height: 100%; + padding: 0 6px; +} + +.m-mini-bar { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + height: 100%; + gap: 3px; +} + +.m-mini-bar span { + width: 100%; + max-width: 18px; + min-height: 2px; + border-radius: 3px 3px 0 0; + background: var(--action-soft); +} + +.m-mini-bar.current span { + background: var(--action); +} + +.m-mini-bar small { + font-size: 9px; + color: var(--text-tertiary); + white-space: nowrap; +} + +.m-carry-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.m-carry-row:last-child { + border-bottom: 0; +} + +.m-carry-name { + flex: 1; + min-width: 0; + font-size: var(--font-size-label); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.m-carry-leader { + font-size: var(--font-size-aux); + color: var(--text-tertiary); + flex: 0 0 auto; +} + +.m-carry-status { + font-size: 10px; + padding: 1px 6px; + border-radius: 4px; + flex: 0 0 auto; +} + +.m-carry-status.strong { background: var(--market-up-soft); color: var(--market-up); } +.m-carry-status.steady { background: var(--action-soft); color: var(--action); } +.m-carry-status.mixed { background: var(--warning-soft); color: var(--warning); } +.m-carry-status.weak { background: var(--surface-muted); color: var(--text-secondary); } + +.m-carry-median { + flex: 0 0 auto; + font-size: var(--font-size-label); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.m-carry-median.up { color: var(--market-up); } +.m-carry-median.down { color: var(--market-down); } + +/* 题材库 */ +.m-theme-list { + margin: 0 12px; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); + overflow: hidden; +} + +.m-theme-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 12px 14px; + border: 0; + border-bottom: 1px solid var(--border-subtle); + background: transparent; + text-align: left; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: background-color var(--motion-press-release) var(--ease-press); +} + +.m-theme-item:last-child { + border-bottom: 0; +} + +.m-theme-item:active { + background: var(--surface-hover); +} + +.m-theme-rank { + flex: 0 0 auto; + width: 22px; + text-align: center; + font-size: var(--font-size-aux); + color: var(--text-tertiary); + font-variant-numeric: tabular-nums; +} + +.m-theme-item:nth-child(-n+3) .m-theme-rank { + color: var(--warning); + font-weight: 600; +} + +.m-theme-copy { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 1px; +} + +.m-theme-copy strong { + font-size: var(--font-size-table); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.m-theme-copy small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-theme-item > b { + flex: 0 0 auto; + font-size: var(--font-size-label); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.m-theme-item > b.up { color: var(--market-up); } +.m-theme-item > b.down { color: var(--market-down); } + +.m-theme-detail-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; +} + +.m-theme-detail-head strong { + font-size: var(--font-size-page-title); + font-weight: 600; +} + +.m-theme-detail-head b { + font-size: var(--font-size-card-title); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.m-theme-detail-head b.up { color: var(--market-up); } +.m-theme-detail-head b.down { color: var(--market-down); } + +.m-theme-detail-code { + margin-top: 2px; + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-sheet-meta { + margin-bottom: 8px; + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +/* 龙虎榜 */ +.m-dragon-list { + margin: 0 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.m-dragon-card { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + border: 0; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); + text-align: left; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: transform var(--motion-press-release) var(--ease-press); +} + +.m-dragon-card:active { + transform: scale(0.98); +} + +.m-dragon-card-rank { + flex: 0 0 auto; + font-size: var(--font-size-aux); + color: var(--text-tertiary); + font-variant-numeric: tabular-nums; +} + +.m-dragon-card-mono { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 50%; + background: var(--action-soft); + color: var(--action); + font-size: 13px; + font-weight: 600; +} + +.m-dragon-card-copy { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 1px; +} + +.m-dragon-card-copy strong { + font-size: var(--font-size-table); + font-weight: 500; +} + +.m-dragon-card-copy small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.m-dragon-card-stats { + flex: 0 0 auto; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 1px; +} + +.m-dragon-card-stats small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-dragon-card-stats b { + font-size: var(--font-size-label); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.m-dragon-card-stats b.up { color: var(--market-up); } +.m-dragon-card-stats b.down { color: var(--market-down); } + +.m-dragon-unclassified { + margin: 8px 12px 0; + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-dragon-trader-totals { + display: flex; + gap: 8px; + margin-bottom: 10px; +} + +.m-total { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + padding: 8px; + border-radius: 6px; + background: var(--surface-muted); +} + +.m-total span { + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-total b { + font-size: var(--font-size-label); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.m-total b.up { color: var(--market-up); } +.m-total b.down { color: var(--market-down); } + +.m-profile-list { + margin: 0 12px; + border-radius: 8px; + background: var(--surface); + box-shadow: var(--elevation-card); + overflow: hidden; +} + +.m-profile-row { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 12px 14px; + border: 0; + border-bottom: 1px solid var(--border-subtle); + background: transparent; + text-align: left; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.m-profile-row:last-child { + border-bottom: 0; +} + +.m-profile-row:active { + background: var(--surface-hover); +} + +.m-profile-mono { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 8px; + background: var(--action-soft); + color: var(--action); + font-size: 13px; + font-weight: 600; +} + +.m-profile-copy { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 1px; +} + +.m-profile-copy strong { + font-size: var(--font-size-table); + font-weight: 500; +} + +.m-profile-copy small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.m-profile-seat { + flex: 0 0 auto; + font-size: var(--font-size-aux); + color: var(--text-secondary); +} + +.m-profile-detail-head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.m-profile-detail-head .m-profile-mono { + width: 44px; + height: 44px; + font-size: 16px; +} + +.m-profile-detail-head strong { + font-size: var(--font-size-page-title); +} + +.m-profile-detail-head small { + font-size: var(--font-size-aux); + color: var(--text-tertiary); +} + +.m-profile-desc { + font-size: var(--font-size-label); + color: var(--text-secondary); + line-height: 1.6; +} + +.m-profile-orgs { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.m-profile-orgs span { + font-size: var(--font-size-label); + padding: 6px 10px; + border-radius: 6px; + background: var(--surface-muted); + color: var(--text-primary); +} + +.m-profile-notice { + margin-top: 12px; + font-size: var(--font-size-aux); + color: var(--warning); +} + +.m-tag--exp-above { background: var(--market-up-soft); color: var(--market-up); } +.m-tag--exp-matched { background: var(--surface-muted); color: var(--text-secondary); } +.m-tag--exp-below { background: var(--market-down-soft); color: var(--market-down); } +.m-tag--exp-one { background: var(--warning-soft); color: var(--warning); } + +.m-tag--dir-up { background: var(--market-up-soft); color: var(--market-up); } +.m-tag--dir-down { background: var(--market-down-soft); color: var(--market-down); } +.m-tag--dir-flat { background: var(--surface-muted); color: var(--text-secondary); } + +@media (prefers-reduced-motion: reduce) { + .m-rotation-chip:active, + .m-dragon-card:active, + .m-range-tab:active { + transform: none; + } +} diff --git a/app/frontend/m/js/pages.js b/app/frontend/m/js/pages.js index d018ba0..b6a8fda 100644 --- a/app/frontend/m/js/pages.js +++ b/app/frontend/m/js/pages.js @@ -110,6 +110,21 @@ calCursor: null, sort: { key: "", dir: null }, detail: null, + sentimentHistory: null, + sentimentRange: 20, + rotation: null, + rotationSelectedSector: "", + rotationSelectedDate: "", + rotationMembers: null, + auction: null, + auctionDataset: "focus", + themes: null, + themesDetail: null, + themesSelectedCode: "", + dragon: null, + dragonProfiles: null, + dragonViewMode: "daily", + dragonSelectedTrader: "", }; let sheetToken = 0; @@ -137,7 +152,7 @@ } function isNumericType(type) { - return ["rank", "change", "price", "rate", "money", "gap", "int", "streak", "advance", "height", "move"].indexOf(type) >= 0; + return ["rank", "change", "price", "rate", "money", "gap", "int", "streak", "advance", "height", "move", "score"].indexOf(type) >= 0; } function sortableColumn(col) { @@ -203,7 +218,7 @@ function colAlign(col) { if (col.type === "rank") return "m-align-center"; - if (["change", "price", "rate", "money", "gap", "int", "streak", "advance", "height", "move"].indexOf(col.type) >= 0) return "m-align-right"; + if (["change", "price", "rate", "money", "gap", "int", "streak", "advance", "height", "move", "score"].indexOf(col.type) >= 0) return "m-align-right"; return "m-align-left"; } @@ -289,6 +304,23 @@ return '' + escapeHtml(text) + ""; } + function scoreCell(value) { + if (value == null || value === "") return ''; + return '' + formatNumber(number(value), 1) + ""; + } + + function expectationCell(value) { + if (value == null || value === "") return ""; + const map = { "超预期": "above", "符合预期": "matched", "低于预期": "below", "竞价一字": "one" }; + return '' + escapeHtml(value) + ""; + } + + function directionCell(value) { + if (value == null || value === "") return ""; + const map = { "买入": "up", "卖出": "down", "持平": "flat" }; + return '' + escapeHtml(value) + ""; + } + function cellHtml(col, row, index) { const value = row[col.key]; switch (col.type) { @@ -307,6 +339,9 @@ case "move": return moveCell(value); case "concepts": return conceptsCell(value); case "dual": return dualCell(value); + case "score": return scoreCell(value); + case "expectation": return expectationCell(value); + case "direction": return directionCell(value); case "text": default: return textCell(value); } @@ -314,7 +349,7 @@ /* ---------------------------------------------------------------- table */ - function buildTable(cols, rows) { + function buildTable(cols, rows, opts) { const frozen = cols.frozenColumns || []; const primary = cols.primaryColumns || []; const scroll = cols.scrollColumns || []; @@ -354,7 +389,8 @@ const cells = ordered.map(function (col, i) { return cellOpen(col, i, false) + cellHtml(col, row, rIndex) + ""; }).join(""); - const code = /^\d{6}$/.test(String(row.code || "")) ? ' data-code="' + escapeHtml(row.code) + '"' : ""; + const linkable = !(opts && opts.noLink); + const code = linkable && /^\d{6}$/.test(String(row.code || "")) ? ' data-code="' + escapeHtml(row.code) + '"' : ""; return "" + cells + ""; }).join(""); @@ -545,6 +581,12 @@ } function renderPage(key) { + if (isComplexPage(key)) { + COMPLEX_PAGES[key](key); + const loader = COMPLEX_LOADERS[key]; + if (loader) loader(); + return; + } const cfg = pageConfig(key); if (!cfg) return; state.key = key; @@ -666,7 +708,7 @@ } function renderError(message) { - const scroll = document.getElementById("m-table-scroll"); + const scroll = document.getElementById("m-table-scroll") || document.getElementById("m-scroll"); if (scroll) scroll.innerHTML = errorHtml(message); const page = document.querySelector(".m-page"); if (page) { @@ -675,6 +717,822 @@ } } + /* ---------------------------------------------------------------- complex pages (P2b) */ + + function nextSeq() { + return ++state.seq; + } + + function columnsForKey(subKey) { + const cfg = global.MobileNav && global.MobileNav.tableColumns ? global.MobileNav.tableColumns[subKey] : null; + return cfg || null; + } + + function tableScrollHtml(subKey, rows, opts) { + const cols = columnsForKey(subKey); + if (!cols) return emptyHtml(); + if (!rows || !rows.length) return emptyHtml(); + return '
' + buildTable(cols, rows, opts || {}) + "
"; + } + + function complexFrame(key, bodyHtml) { + return '
' + bodyHtml + "
"; + } + + function complexScroll(bodyHtml) { + return '
' + bodyHtml + "
"; + } + + function resetComplexState(key) { + state.key = key; + state.requestedDate = todayString(); + state.dashboard = null; + state.popularity = null; + state.calCursor = null; + state.sort = { key: "", dir: null }; + state.detail = null; + state.sentimentHistory = null; + state.rotation = null; + state.rotationSelectedSector = ""; + state.rotationSelectedDate = ""; + state.rotationMembers = null; + state.auction = null; + state.auctionDataset = "focus"; + state.themes = null; + state.themesDetail = null; + state.themesSelectedCode = ""; + state.dragon = null; + state.dragonProfiles = null; + state.dragonViewMode = "daily"; + state.dragonSelectedTrader = ""; + } + + function reloadCurrent() { + const loader = COMPLEX_LOADERS[state.key]; + if (loader) { loader(); return; } + load(); + } + + function clampScore(value) { + return Math.max(0, Math.min(100, number(value))); + } + + function phaseTone(phase) { + return { + "冰点": "ice", "修复": "repair", "发酵": "fermentation", + "高潮": "climax", "分化": "divergence", "退潮": "retreat", + }[phase] || "divergence"; + } + + function phaseBadgeHtml(phase) { + if (!phase) return ""; + return '' + escapeHtml(phase) + ""; + } + + function directionTone(direction) { + return { "升温": "up", "降温": "down", "新进": "new", "持平": "flat" }[direction] || "flat"; + } + + function formatMoneyMillion(value) { + const parsed = number(value); + const sign = parsed > 0 ? "+" : ""; + if (Math.abs(parsed) >= 100) return sign + formatNumber(parsed / 100, 2) + " 亿"; + return sign + formatNumber(parsed * 100, 0) + " 万"; + } + + function sentimentTrendChart(rows) { + const W = 360, H = 240, padL = 8, padR = 30, padT = 12, padB = 22; + const pw = W - padL - padR; + const ph = H - padT - padB; + if (!rows.length) return emptyChart("暂无情绪历史数据"); + function x(i) { return padL + (rows.length <= 1 ? pw / 2 : i / (rows.length - 1) * pw); } + function y(v) { return padT + (100 - clampScore(v)) / 100 * ph; } + const line = rows.map(function (r, i) { + return (i ? "L" : "M") + x(i).toFixed(1) + " " + y(number(r.score)).toFixed(1); + }).join(" "); + const area = line + " L" + x(rows.length - 1).toFixed(1) + " " + (padT + ph).toFixed(1) + + " L" + padL + " " + (padT + ph).toFixed(1) + " Z"; + const right = W - 4; + const labelStep = Math.max(1, Math.ceil(rows.length / 6)); + const xLabels = rows.map(function (r, i) { + if (i % labelStep !== 0 && i !== rows.length - 1) return ""; + const anchor = i === 0 ? "start" : i === rows.length - 1 ? "end" : "middle"; + return svgAxisText(x(i), H - 6, anchor, "", dateMMDD(displayCompactDate(r.trade_date))); + }).join(""); + return '"; + } + + /* ----- 情绪周期 ----- */ + + function loadSentiment() { + const seq = nextSeq(); + const date = state.requestedDate; + const historyUrl = "/api/sentiment/history?trade_date=" + encodeURIComponent(date) + + "&limit=" + state.sentimentRange; + const req = Promise.all([ + global.MobileAPI.request("/api/dashboard?trade_date=" + encodeURIComponent(date)), + global.MobileAPI.request(historyUrl), + ]); + req.then(function (results) { + if (seq !== state.seq) return; + state.dashboard = results[0]; + state.sentimentHistory = results[1]; + const meta = (results[0] && results[0].meta) || {}; + state.requestedDate = displayCompactDate(meta.requested_date || meta.trade_date || date); + updateHeader(findLabel(state.key) || state.key, state.requestedDate); + renderSentiment(); + }).catch(function (error) { + if (seq !== state.seq) return; + renderError(error && error.message ? error.message : "情绪周期加载失败"); + }); + } + + function sentimentComponentList(components) { + const items = Object.keys(components || {}).map(function (key) { return components[key]; }); + if (!items.length) return ""; + return '
' + items.map(function (item) { + const score = clampScore(item.score); + return '
' + + '
' + escapeHtml(item.label) + "" + + "" + formatNumber(number(item.score), 1) + "
" + + '
' + + '' + escapeHtml(item.summary || "") + " · 权重 " + number(item.weight) + "%" + + "
"; + }).join("") + "
"; + } + + function renderSentiment() { + const scroll = document.getElementById("m-scroll"); + if (!scroll) return; + const dash = state.dashboard || {}; + const overview = dash.overview || {}; + const history = state.sentimentHistory || {}; + const rawRows = history.rows || []; + const latest = rawRows.length ? rawRows[rawRows.length - 1] : null; + const tableRows = rawRows.slice().reverse(); + + let card = ""; + if (latest) { + const score = number(latest.score); + const dayChange = number(latest.day_change); + const components = latest.components || overview.sentiment_components || {}; + card = + '
' + + '
' + score + "" + + '
' + phaseBadgeHtml(latest.phase) + + '' + escapeHtml(latest.direction) + "
" + + '
' + escapeHtml(latest.label || "") + "
" + + '
' + + sentMetric("较前日", (dayChange > 0 ? "+" : "") + formatNumber(dayChange, 1), changeClass(dayChange)) + + sentMetric("封板率", formatNumber(number(latest.seal_rate), 1) + "%", "") + + sentMetric("涨停", number(latest.limit_up_count), "up") + + sentMetric("炸板", number(latest.broken_count), "warn") + + "
" + + '

' + escapeHtml(sentimentAdvice(latest.phase)) + "

" + + "
" + + '
情绪趋势(近 ' + rawRows.length + " 日)
" + + '
' + sentimentTrendChart(rawRows) + "
" + + '
五维分项
' + + sentimentComponentList(components); + } + + const rangeTabs = [20, 40, 60].map(function (n) { + const active = state.sentimentRange === n; + return '"; + }).join(""); + + const histRows = tableRows.map(function (r) { + return Object.assign({}, r, { trade_date: displayCompactDate(r.trade_date) }); + }); + + scroll.innerHTML = card + + '
历史明细 ' + rangeTabs + "
" + + tableScrollHtml("market/sentiment/history", histRows, {}); + } + + function sentMetric(label, valueHtml, tone) { + return '
' + escapeHtml(label) + "" + valueHtml + "
"; + } + + function sentimentAdvice(phase) { + return { + "冰点": "情绪处于极弱区,先观察风险释放。", + "修复": "风险开始收敛,关注率先转强的核心。", + "发酵": "主线与梯队正在形成,优先跟随核心。", + "高潮": "情绪处高位,聚焦核心并主动降低后排暴露。", + "分化": "强弱开始分层,关注承接与回流。", + "退潮": "情绪指标继续走弱,控制仓位。", + }[phase] || "市场结构尚未形成清晰阶段,保持观察。"; + } + + /* ----- 市场天梯 ----- */ + + const expandedLadder = {}; + + function loadLadder() { + const seq = nextSeq(); + const date = state.requestedDate; + global.MobileAPI.request("/api/dashboard?trade_date=" + encodeURIComponent(date)).then(function (payload) { + if (seq !== state.seq) return; + state.dashboard = payload; + const meta = payload.meta || {}; + state.requestedDate = displayCompactDate(meta.requested_date || meta.trade_date || date); + updateHeader(findLabel(state.key) || state.key, state.requestedDate); + renderLadder(); + }).catch(function (error) { + if (seq !== state.seq) return; + renderError(error && error.message ? error.message : "市场天梯加载失败"); + }); + } + + function ladderTierHtml(level, group, groupMap, maxLevel) { + const label = group ? group.label : (level === 1 ? "首板" : level + "板"); + const count = group ? number(group.count) : 0; + const gap = count === 0; + const color = { 1: "var(--action)", 2: "var(--market-down)", 3: "var(--warning)", 4: "var(--market-up)" }[level] || "var(--text-tertiary)"; + const rate = level > 1 && count ? Math.round(count / Math.max(number((groupMap[level - 1] || {}).count), 1) * 100 * 10) / 10 : 0; + const rateHtml = level > 1 && count ? '较' + (level - 1) + "板 " + rate + "%" : ""; + + let stocksHtml = ""; + if (gap) { + stocksHtml = '
' + (level >= maxLevel ? "断层 · " + escapeHtml(label) + "及以上空缺" : "该层暂时空缺") + "
"; + } else { + const stocks = (group.stocks || []).slice(); + const cap = level === 1 || level === 2 ? 8 : 0; + const expanded = expandedLadder[level]; + const visible = (expanded || !cap) ? stocks : stocks.slice(0, cap); + stocksHtml = visible.map(function (s) { + const onePrice = String(s.first_time || "").startsWith("09:25") && number(s.open_times) === 0; + const broken = number(s.open_times) >= 6; + const amount = number(s.seal_amount_million) ? "封单 " + formatNumber(s.seal_amount_million, 0) + " 万" : "成交 " + formatNumber(s.amount_billion, 1) + " 亿"; + return '"; + }).join(""); + const remaining = !expanded && cap && stocks.length > cap ? stocks.length - cap : 0; + if (remaining) { + stocksHtml += '"; + } + } + + return '
' + + '
' + escapeHtml(label) + "" + + '' + count + " 只" + rateHtml + "
" + + '
' + stocksHtml + "
"; + } + + function renderLadder() { + const scroll = document.getElementById("m-scroll"); + if (!scroll) return; + const dash = state.dashboard || {}; + const ladders = dash.ladders || []; + if (!ladders.length) { scroll.innerHTML = emptyHtml(); return; } + const maxLevel = ladders.reduce(function (m, g) { return Math.max(m, number(g.level)); }, 0); + const topVisible = Math.max(5, maxLevel); + const groupMap = {}; + ladders.forEach(function (g) { groupMap[number(g.level)] = g; }); + const yesterday = dash.yesterday_limits || []; + const prevMax = yesterday.reduce(function (m, r) { return Math.max(m, number(r.prior_streak)); }, 0); + const spaceStocks = (groupMap[maxLevel] || {}).stocks || []; + const spaceChange = prevMax && maxLevel < prevMax ? "较昨日 " + prevMax + " 板 ↓ 压缩" + : prevMax && maxLevel > prevMax ? "较昨日 " + prevMax + " 板 ↑ 抬升" : "高度与昨日接近"; + const spaceNote = maxLevel >= 5 ? "高位梯队仍有辨识度,重点看承接。" + : maxLevel >= 3 ? "空间位于中段,梯队延续性比绝对高度更重要。" : "高度受压缩,先看首板向二板的结构修复。"; + + const apexHtml = '
' + + '
空间板' + (maxLevel ? maxLevel + " 板" : "--") + "" + escapeHtml(spaceChange) + "
" + + '

' + (spaceStocks.length ? spaceStocks.map(function (s) { return "" + escapeHtml(s.name) + "(" + escapeHtml(s.sector || "其他") + ")"; }).join(" · ") : "暂无空间板") + "

" + + "" + escapeHtml(spaceNote) + "
"; + + const tiers = []; + for (let level = topVisible; level >= 1; level -= 1) { + tiers.push(ladderTierHtml(level, groupMap[level], groupMap, maxLevel)); + } + + const perf = dash.limit_performance || []; + const perfRows = perf.map(function (p) { + const value = Math.max(0, Math.min(100, number(p.advance_rate))); + return '
' + escapeHtml(p.label || "昨日" + number(p.level) + "板") + "" + + '' + formatNumber(value, 1) + "%
"; + }).join(""); + const perfHtml = perfRows + ? '
晋级率参考(昨日梯队 → 今日)
' + perfRows + "
" + : ""; + + scroll.innerHTML = apexHtml + tiers.join("") + perfHtml; + } + + /* ----- 主题轮动 ----- */ + + function loadRotation() { + const seq = nextSeq(); + const date = state.requestedDate; + global.MobileAPI.request("/api/rotation/history?trade_date=" + encodeURIComponent(date)).then(function (payload) { + if (seq !== state.seq) return; + state.rotation = payload; + state.rotationMembers = null; + state.rotationSelectedSector = ""; + state.rotationSelectedDate = ""; + const meta = payload || {}; + state.requestedDate = displayCompactDate(meta.trade_date || date); + updateHeader(findLabel(state.key) || state.key, state.requestedDate); + renderRotation(); + }).catch(function (error) { + if (seq !== state.seq) return; + renderError(error && error.message ? error.message : "主题轮动加载失败"); + }); + } + + function renderRotation() { + const scroll = document.getElementById("m-scroll"); + if (!scroll) return; + const rows = (state.rotation && state.rotation.rows) || []; + if (!rows.length) { scroll.innerHTML = emptyHtml(); return; } + const latestDate = rows[0].trade_date; + scroll.innerHTML = rows.map(function (day) { + const sectors = day.sectors || []; + const chips = sectors.map(function (sector) { + const strength = clampScore(sector.strength); + const heat = strength >= 90 ? "strong" : strength >= 70 ? "warm" : "mild"; + return '"; + }).join(""); + return '
' + + '
" + + "" + sectors.length + " 个热点
" + + '
' + chips + "
"; + }).join(""); + } + + function loadRotationMembers(sector, date) { + const seq = nextSeq(); + const url = "/api/rotation/members?trade_date=" + encodeURIComponent(date) + "§or=" + encodeURIComponent(sector); + global.MobileAPI.request(url).then(function (payload) { + if (seq !== state.seq) return; + state.rotationMembers = payload; + renderRotationMembersSheet(sector, payload); + }).catch(function (error) { + if (seq !== state.seq) return; + state.rotationMembers = { error: error.message || "成分股加载失败" }; + renderRotationMembersSheet(sector, state.rotationMembers); + }); + } + + function openRotationMembersSheet(sector, date) { + openSheet( + '

' + escapeHtml(sector) + "成分股

" + + '
" + + '
' + skeletonHtml(8) + "
", + { detail: false } + ); + loadRotationMembers(sector, date); + } + + function renderRotationMembersSheet(sector, payload) { + const body = document.getElementById("m-rotation-members-body"); + if (!body) return; + if (payload.error) { body.innerHTML = emptyHtml(); return; } + const meta = payload.meta || {}; + const rows = payload.rows || []; + const metaLine = '
' + escapeHtml(displayCompactDate(meta.trade_date) || "--") + + " · " + number(meta.quoted_count) + " / " + number(meta.member_count) + " 只
"; + body.innerHTML = metaLine + tableScrollHtml("market/rotation/members", rows, { noLink: true }); + } + + /* ----- 竞价 ----- */ + + const AUCTION_DATASETS = [ + { key: "focus", label: "重点异动" }, + { key: "all", label: "全部候选" }, + { key: "onePrice", label: "竞价一字" }, + { key: "watchlist", label: "我的自选" }, + ]; + + function loadAuction() { + const seq = nextSeq(); + const date = state.requestedDate; + global.MobileAPI.request("/api/auction?trade_date=" + encodeURIComponent(date)).then(function (payload) { + if (seq !== state.seq) return; + state.auction = payload; + const meta = payload.meta || {}; + state.requestedDate = displayCompactDate(meta.trade_date || date); + updateHeader(findLabel(state.key) || state.key, state.requestedDate); + renderAuction(); + }).catch(function (error) { + if (seq !== state.seq) return; + renderError(error && error.message ? error.message : "竞价数据加载失败"); + }); + } + + function auctionRows() { + const data = state.auction || {}; + const datasets = { + focus: data.focus_rows || [], + all: data.rows || [], + onePrice: data.one_price_rows || [], + watchlist: data.watchlist_rows || [], + }; + const rows = (datasets[state.auctionDataset] || []).slice(); + if (state.auctionDataset === "onePrice") { + return rows.map(function (r) { return Object.assign({}, r, { expectation: "竞价一字" }); }); + } + return rows; + } + + function auctionAmountTrend(history) { + if (!history || !history.length) return '
历史竞价量能尚未形成
'; + const max = Math.max.apply(null, history.map(function (h) { return number(h.amount_billion); })) || 1; + return '
' + history.map(function (h, i) { + const hgt = Math.max(8, number(h.amount_billion) / max * 100); + const current = i === history.length - 1 ? " current" : ""; + return '
' + + '' + escapeHtml(String(h.trade_date || "").slice(5)) + "
"; + }).join("") + "
"; + } + + function auctionThemeCarry(carry) { + if (!carry || !carry.length) return ""; + const tone = { "强承接": "strong", "有承接": "steady", "分歧": "mixed", "承接弱": "weak" }; + return '
' + carry.slice(0, 6).map(function (item) { + return '
' + + '' + escapeHtml(item.name) + "" + + '' + escapeHtml(item.leader || "--") + " · 昨 " + number(item.prior_limit_count) + " 只" + + '' + escapeHtml(item.status) + "" + + '' + + (item.median_change == null ? "暂无候选" : (number(item.median_change) > 0 ? "+" : "") + formatNumber(number(item.median_change), 2) + "%") + "
"; + }).join("") + "
"; + } + + function auctionPhaseNotice(meta) { + const phase = meta.phase || "archive"; + const copy = { + pending: ["竞价尚未开始", "9:15 进入观察期,9:25 读取最终竞价结果。"], + observing: ["竞价观察期", "此阶段先观察盘前变化,系统将在 9:25 自动读取最终结果。"], + selection: ["竞价筛选窗口", "最终竞价结果已经定格,请在 9:30 前完成筛选。"], + finalized: ["今日竞价已定格", "9:30 后停止更新,仅保留用于复盘与回测。"], + archive: ["历史竞价归档", "当前展示所选交易日的最终竞价结果。"], + }[phase] || ["竞价状态", "当前竞价状态待确认。"]; + return '
' + escapeHtml(copy[0]) + "" + escapeHtml(copy[1]) + "
"; + } + + function renderAuction() { + const scroll = document.getElementById("m-scroll"); + if (!scroll) return; + const payload = state.auction; + if (!payload) return; + const summary = payload.summary || {}; + const meta = payload.meta || {}; + const themes = payload.themes || {}; + + const metrics = [ + ["竞价覆盖", formatNumber(number(summary.stock_count), 0) + " 只", ""], + ["重点异动", formatNumber(number(summary.focus_count), 0) + " 只", "up"], + ["竞价一字", formatNumber(number(summary.one_price_count), 0) + " 只", ""], + ["竞价成交额", formatNumber(number(summary.amount_billion), 2) + " 亿", ""], + ].map(function (m) { + return '
' + escapeHtml(m[0]) + "" + m[1] + "
"; + }).join(""); + + const tabs = AUCTION_DATASETS.map(function (d) { + const active = state.auctionDataset === d.key; + return '"; + }).join(""); + + const carry = auctionThemeCarry(themes.carry); + scroll.innerHTML = + auctionPhaseNotice(meta) + + '
' + metrics + "
" + + '
竞价量能
' + auctionAmountTrend(payload.amount_history || []) + "
" + + (carry ? '
昨日强势题材承接
' + carry + "
" : "") + + '
' + tabs + "
" + + '
'; + renderAuctionTable(); + } + + function renderAuctionTable() { + const table = document.getElementById("m-auction-table"); + if (!table) return; + const rows = auctionRows(); + if (!rows.length) { table.innerHTML = emptyHtml(); return; } + table.innerHTML = buildTable(columnsForKey("market/auction"), rows); + } + + /* ----- 题材库 ----- */ + + function loadThemes() { + const seq = nextSeq(); + const date = state.requestedDate; + global.MobileAPI.request("/api/themes?trade_date=" + encodeURIComponent(date)).then(function (payload) { + if (seq !== state.seq) return; + state.themes = payload; + const meta = payload.meta || {}; + state.requestedDate = displayCompactDate(meta.trade_date || date); + updateHeader(findLabel(state.key) || state.key, state.requestedDate); + renderThemes(); + }).catch(function (error) { + if (seq !== state.seq) return; + renderError(error && error.message ? error.message : "题材库加载失败"); + }); + } + + function renderThemes() { + const scroll = document.getElementById("m-scroll"); + if (!scroll) return; + const payload = state.themes; + if (!payload) return; + const summary = payload.summary || {}; + const items = payload.items || []; + const metrics = [ + ["收录题材", number(summary.theme_count), "个", ""], + ["当日上涨", number(summary.up_count), "个", "up"], + ["当日下跌", number(summary.down_count), "个", "down"], + ["人气题材", number(summary.hot_count), "个", "warn"], + ].map(function (m) { + return '
' + escapeHtml(m[0]) + "" + m[1] + '' + escapeHtml(m[2]) + "
"; + }).join(""); + + const list = items.length ? '
' + items.map(function (item, index) { + return '"; + }).join("") + "
" : emptyHtml(); + + scroll.innerHTML = '
' + metrics + "
" + list; + } + + function loadThemeDetail(code) { + const seq = nextSeq(); + const url = "/api/themes/detail?code=" + encodeURIComponent(code) + "&trade_date=" + encodeURIComponent(state.requestedDate); + global.MobileAPI.request(url).then(function (payload) { + if (seq !== state.seq) return; + state.themesDetail = payload; + renderThemeDetailSheet(payload); + }).catch(function (error) { + if (seq !== state.seq) return; + const body = document.getElementById("m-theme-detail-body"); + if (body) body.innerHTML = errorHtml(error.message || "题材详情加载失败"); + }); + } + + function openThemeDetailSheet(code) { + openSheet( + '

题材详情

' + + '
" + + '
' + skeletonHtml(6) + "
", + { detail: true } + ); + loadThemeDetail(code); + } + + function renderThemeDetailSheet(payload) { + const body = document.getElementById("m-theme-detail-body"); + if (!body) return; + const theme = payload.theme || {}; + const summary = payload.summary || {}; + const meta = payload.meta || {}; + const metrics = [ + ["成分股", number(summary.member_count) + " 只", ""], + ["有行情", number(summary.quoted_count) + " 只", ""], + ["上涨", number(summary.up_count) + " 只", "up"], + ["下跌", number(summary.down_count) + " 只", "down"], + ["换手率", formatNumber(number(theme.turnover_rate), 2) + "%", ""], + ].map(function (m) { + return '
' + escapeHtml(m[0]) + "" + m[1] + "
"; + }).join(""); + + body.innerHTML = + '
' + escapeHtml(theme.name || "--") + "" + + '' + (number(theme.change) > 0 ? "+" : "") + formatNumber(number(theme.change), 2) + "%
" + + '
' + escapeHtml(theme.code || "--") + " · " + escapeHtml(displayCompactDate(meta.trade_date) || "--") + "
" + + '
' + metrics + "
" + + '
成分股
' + + tableScrollHtml("market/themes/members", payload.members || [], { noLink: true }); + } + + /* ----- 龙虎榜 ----- */ + + function loadDragon() { + const seq = nextSeq(); + const date = state.requestedDate; + global.MobileAPI.request("/api/dragon-tiger?trade_date=" + encodeURIComponent(date)).then(function (payload) { + if (seq !== state.seq) return; + state.dragon = payload; + const meta = payload.meta || {}; + state.requestedDate = displayCompactDate(meta.trade_date || date); + updateHeader(findLabel(state.key) || state.key, state.requestedDate); + renderDragon(); + }).catch(function (error) { + if (seq !== state.seq) return; + renderError(error && error.message ? error.message : "龙虎榜加载失败"); + }); + } + + function loadDragonProfiles() { + const seq = nextSeq(); + global.MobileAPI.request("/api/dragon-tiger/profiles").then(function (payload) { + if (seq !== state.seq) return; + state.dragonProfiles = payload; + renderDragonProfiles(); + }).catch(function (error) { + if (seq !== state.seq) return; + const scroll = document.getElementById("m-scroll"); + if (scroll) scroll.innerHTML = errorHtml(error.message || "游资档案加载失败"); + }); + } + + function dragonTraders() { + const payload = state.dragon || {}; + return (payload.traders || []).filter(function (item) { + return item.identity_type === "trader" && item.recognized !== false; + }); + } + + function renderDragon() { + const scroll = document.getElementById("m-scroll"); + if (!scroll) return; + const payload = state.dragon; + if (!payload) return; + const summary = payload.summary || {}; + const status = payload.meta && payload.meta.status; + const traders = dragonTraders(); + const unclassified = payload.unclassified_seats || []; + + const unavailable = ["error", "unavailable"].indexOf(status) >= 0; + const empty = status === "empty"; + if (unavailable || (empty && !traders.length && !unclassified.length)) { + scroll.innerHTML = '
' + icon("inbox", 26) + "" + + "

" + escapeHtml(unavailable ? "龙虎榜数据暂不可用" : payload.meta.trade_date + " 暂无龙虎榜明细") + "

" + + "龙虎榜明细通常在盘后陆续披露,可稍后刷新或查看前一交易日。
"; + return; + } + + const modeTabs = '
' + + '' + + '' + + "
"; + + const metrics = [ + ["上榜游资", number(summary.trader_count) + " 位", ""], + ["操作明细", number(summary.operation_count) + " 条", ""], + ["席位净买入", formatMoneyMillion(summary.seat_net_buy_million), changeClass(summary.seat_net_buy_million)], + ["活跃股票", number(summary.active_stock_count) + " 只", ""], + ].map(function (m) { + return '
' + escapeHtml(m[0]) + "" + m[1] + "
"; + }).join(""); + + const cards = traders.map(function (trader, index) { + const desc = trader.description || number(trader.stock_count) + " 只股票," + number(trader.operation_count) + " 笔操作"; + return '"; + }).join(""); + + scroll.innerHTML = modeTabs + + '
' + metrics + "
" + + '
上榜游资
' + + '
' + (cards || emptyHtml()) + "
" + + (unclassified.length ? '
另有 ' + number(unclassified.length) + " 个待归类席位
" : ""); + } + + function renderDragonProfiles() { + const scroll = document.getElementById("m-scroll"); + if (!scroll) return; + const payload = state.dragonProfiles; + if (!payload) return; + const profiles = payload.profiles || []; + const summary = payload.summary || {}; + const modeTabs = '
' + + '' + + '' + + "
"; + + const metrics = [ + ["收录游资", number(summary.profile_count), ""], + ["已有简介", number(summary.described_count), ""], + ["关联席位", number(summary.organization_count), ""], + ].map(function (m) { + return '
' + escapeHtml(m[0]) + "" + m[1] + "
"; + }).join(""); + + const list = profiles.length ? '
' + profiles.map(function (profile, index) { + return '"; + }).join("") + "
" : emptyHtml(); + + scroll.innerHTML = modeTabs + '
' + metrics + "
" + + '
游资名录(收录 ' + number(summary.profile_count) + " 位)
" + list; + } + + function openDragonTraderSheet(id) { + const payload = state.dragon || {}; + const traders = dragonTraders(); + const trader = traders.find(function (t) { return t.id === id; }); + if (!trader) return; + const ops = trader.operations || []; + openSheet( + '
' + + '' + escapeHtml(trader.name) + "" + + '' + number(trader.stock_count) + " 股 · " + number(trader.operation_count) + " 笔
" + + '
" + + '
' + + '
' + + totRow("买入", trader.buy_million, "up") + + totRow("卖出", trader.sell_million, "down") + + totRow("净额", trader.net_buy_million, changeClass(trader.net_buy_million)) + + "
" + + tableScrollHtml("market/dragon/operations", ops, { noLink: true }) + + "
", + { detail: true } + ); + } + + function totRow(label, value, cls) { + return '
' + escapeHtml(label) + "" + formatMoneyMillion(value) + "
"; + } + + function openProfileSheet(id) { + const profiles = (state.dragonProfiles && state.dragonProfiles.profiles) || []; + const profile = profiles.find(function (p) { return p.id === id; }); + if (!profile) return; + const orgs = profile.organizations || []; + openSheet( + '

游资档案

' + + '
" + + '
' + + '
' + escapeHtml(profile.name.slice(0, 2)) + "" + + "
" + escapeHtml(profile.name) + "" + (orgs.length ? "关联 " + orgs.length + " 个公开席位" : "暂无关联席位") + "
" + + '
人物简介
' + + '

' + escapeHtml(profile.description || "名录暂未收录该游资的公开简介。") + "

" + + (orgs.length ? '
关联营业部
' + + orgs.map(function (o) { return "" + escapeHtml(o) + ""; }).join("") + "
" : "") + + (payloadNotice(state.dragonProfiles) || "") + + "
", + { detail: false } + ); + } + + function payloadNotice(payload) { + const meta = payload && payload.meta; + return meta && meta.notice ? '

' + escapeHtml(meta.notice) + "

" : ""; + } + + /* ----- complex page registry ----- */ + + function setupComplexPage(key) { + resetComplexState(key); + document.getElementById("m-view").classList.add("m-view-feature"); + updateHeader(findLabel(key) || key, state.requestedDate); + document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(8))); + } + + const COMPLEX_PAGES = { + "market/sentiment": setupComplexPage, + "market/ladder": setupComplexPage, + "market/rotation": setupComplexPage, + "market/auction": setupComplexPage, + "market/themes": setupComplexPage, + "market/dragon": setupComplexPage, + }; + + const COMPLEX_LOADERS = { + "market/sentiment": loadSentiment, + "market/ladder": loadLadder, + "market/rotation": loadRotation, + "market/auction": loadAuction, + "market/themes": loadThemes, + "market/dragon": loadDragon, + }; + + function isComplexPage(key) { + return Boolean(COMPLEX_PAGES[key]); + } + /* ---------------------------------------------------------------- sheets */ function ensureSheetRoot() { @@ -744,7 +1602,7 @@ if (!dateStr) return; state.requestedDate = dateStr; closeSheet(); - load(); + reloadCurrent(); } function openDateSheet() { @@ -1153,11 +2011,72 @@ if (dateBtn) { openDateSheet(); return; } const retry = event.target.closest("[data-action=retry]"); - if (retry) { load(); return; } + if (retry) { reloadCurrent(); return; } const sortHead = event.target.closest("[data-sort-key]"); if (sortHead) { toggleSort(sortHead.dataset.sortKey); return; } + const rangeTab = event.target.closest("[data-sentiment-range]"); + if (rangeTab) { + state.sentimentRange = number(rangeTab.dataset.sentimentRange) || 20; + document.querySelectorAll("[data-sentiment-range]").forEach(function (tab) { + const active = number(tab.dataset.sentimentRange) === state.sentimentRange; + tab.classList.toggle("active", active); + tab.setAttribute("aria-pressed", String(active)); + }); + loadSentiment(); + return; + } + + const auctionDataset = event.target.closest("[data-action=auction-dataset]"); + if (auctionDataset) { + state.auctionDataset = auctionDataset.dataset.dataset || "focus"; + document.querySelectorAll("[data-action=auction-dataset]").forEach(function (tab) { + const active = tab.dataset.dataset === state.auctionDataset; + tab.classList.toggle("active", active); + tab.setAttribute("aria-selected", String(active)); + }); + renderAuctionTable(); + return; + } + + const dragonMode = event.target.closest("[data-action=dragon-mode]"); + if (dragonMode) { + const mode = dragonMode.dataset.mode; + state.dragonViewMode = mode === "profiles" ? "profiles" : "daily"; + if (mode === "profiles") loadDragonProfiles(); + else if (state.dragon) renderDragon(); + else loadDragon(); + return; + } + + const ladderStock = event.target.closest("[data-ladder-stock]"); + if (ladderStock) { openDetailSheet(ladderStock.dataset.ladderStock); return; } + + const ladderExpand = event.target.closest("[data-ladder-expand]"); + if (ladderExpand) { + const level = number(ladderExpand.dataset.ladderExpand); + if (expandedLadder[level]) delete expandedLadder[level]; + else expandedLadder[level] = true; + renderLadder(); + return; + } + + const rotationSector = event.target.closest("[data-rotation-sector]"); + if (rotationSector) { + openRotationMembersSheet(rotationSector.dataset.rotationSector, rotationSector.dataset.rotationDate); + return; + } + + const themeCode = event.target.closest("[data-theme-code]"); + if (themeCode) { openThemeDetailSheet(themeCode.dataset.themeCode); return; } + + const dragonTrader = event.target.closest("[data-dragon-trader]"); + if (dragonTrader) { openDragonTraderSheet(dragonTrader.dataset.dragonTrader); return; } + + const profileId = event.target.closest("[data-profile-id]"); + if (profileId) { openProfileSheet(profileId.dataset.profileId); return; } + const watchBtn = event.target.closest("[data-action=watch]"); if (watchBtn) { toggleWatch(); return; } @@ -1216,6 +2135,6 @@ global.MobilePages = { render: renderPage, - has: function (key) { return Boolean(pageConfig(key)); }, + has: function (key) { return Boolean(pageConfig(key) || isComplexPage(key)); }, }; })(window);