diff --git a/app/config/architecture-inventory.json b/app/config/architecture-inventory.json index e75cbda..3e8fe22 100644 --- a/app/config/architecture-inventory.json +++ b/app/config/architecture-inventory.json @@ -451,8 +451,8 @@ }, { "path": "frontend/shared/shell.css", - "bytes": 60932, - "lines": 3677 + "bytes": 62110, + "lines": 3706 }, { "path": "backend/features/heaven/engine.py", @@ -461,8 +461,8 @@ }, { "path": "frontend/index.html", - "bytes": 46449, - "lines": 642 + "bytes": 47039, + "lines": 646 }, { "path": "backend/features/screener/catalog.py", @@ -574,6 +574,11 @@ "bytes": 10539, "lines": 244 }, + { + "path": "frontend/shared/dashboard.js", + "bytes": 9993, + "lines": 220 + }, { "path": "backend/data/providers/tushare_sectors.py", "bytes": 9876, @@ -584,11 +589,6 @@ "bytes": 9348, "lines": 222 }, - { - "path": "frontend/shared/dashboard.js", - "bytes": 9323, - "lines": 210 - }, { "path": "frontend/pages/market/entity-detail.js", "bytes": 9119, diff --git a/app/frontend/index.html b/app/frontend/index.html index e73cc4d..9234513 100644 --- a/app/frontend/index.html +++ b/app/frontend/index.html @@ -107,11 +107,15 @@
+
市场情绪--等待数据
上涨家数--
下跌家数--
-
涨停 / 跌停--
-
两市成交额--
-
数据日期--
+
涨停--
+
跌停--
+
炸板--
+
封板率--
+
两市成交--
+
数据日期--
diff --git a/app/frontend/pages/mentor/foundation.css b/app/frontend/pages/mentor/foundation.css index af78d9d..193a941 100644 --- a/app/frontend/pages/mentor/foundation.css +++ b/app/frontend/pages/mentor/foundation.css @@ -1,5 +1,16 @@ /* Canonical CSS owner: mentor. Historical layers consolidated 2026-08-02. */ +body[data-active-view="mentorView"] .app-page-context { + max-width: 420px; + min-width: 180px; +} + +body[data-active-view="mentorView"] .app-page-context span { + overflow: visible; + white-space: normal; + text-overflow: unset; +} + #mentorView.workspace-view { --qp-bg-header: #F5F6F7; --qp-bg-sidebar: #FFFFFF; @@ -334,7 +345,7 @@ #mentorView .mentor-option { position: relative; - height: 75px; + height: 62px; margin: 0 10px 0 8px; display: flex; align-items: stretch; @@ -1061,11 +1072,11 @@ #mentorView .mentor-send-button { width: 71px; - height: 31px; - min-height: 31px; + height: 30px; + min-height: 30px; padding: 0; border: 0; - border-radius: 10px; + border-radius: 8px; background: var(--qp-accent); color: #FFFFFF; font-size: 13px; diff --git a/app/frontend/pages/pools/foundation.css b/app/frontend/pages/pools/foundation.css index 9903ebb..4067113 100644 --- a/app/frontend/pages/pools/foundation.css +++ b/app/frontend/pages/pools/foundation.css @@ -892,24 +892,6 @@ line-height: 1.6; } -.pool-streak-tag { - display: inline-block; - - padding: 1.5px 7px; - - border: 1px solid transparent; - - border-radius: 5px; - - font-size: 11px; - - line-height: 1.6; - - background: var(--r2-up-soft); - - color: var(--r2-up); -} - .pool-state-tag.one-word { background: rgb(255, 243, 217); @@ -2225,6 +2207,39 @@ table-layout: auto; } +#limitPool #limitTable { + min-width: 0; + + width: 100%; +} + +#limitPool.redesigned-pool-view .pool-table-card.tbl-wrap { + overflow-x: hidden; + + overflow-y: auto; +} + +#limitPool #limitTable.tbl thead th, +#limitPool #limitTable.tbl tbody td, +#limitPool .pool-table-card .data-table thead th, +#limitPool .pool-table-card .data-table tbody td { + padding: 0 8px; +} + +#limitPool #limitTable .reason-column { + min-width: 4.5em; + + overflow: visible; + + white-space: nowrap; +} + +#limitPool #limitTable .pool-reason-cell { + overflow: visible; + + text-overflow: unset; +} + #brokenTable, #downTable, #limitTable, diff --git a/app/frontend/pages/pools/page.js b/app/frontend/pages/pools/page.js index 85d96b1..4201919 100644 --- a/app/frontend/pages/pools/page.js +++ b/app/frontend/pages/pools/page.js @@ -30,7 +30,7 @@ function renderLimitTable() { ${index + 1} ${escapeHtml(row.name)}${escapeHtml(row.code)} - ${streakLabel(row.streak)} + ${streakLabel(row.streak)} ${signed(row.change)} ${formatNumber(row.price, 2)} ${escapeHtml(row.sector || "其他")} diff --git a/app/frontend/pages/sentiment/foundation.css b/app/frontend/pages/sentiment/foundation.css index 6e76e04..74923ef 100644 --- a/app/frontend/pages/sentiment/foundation.css +++ b/app/frontend/pages/sentiment/foundation.css @@ -313,24 +313,10 @@ background: var(--table-header); } -.sentiment-history-table tbody tr:nth-child(2n) td { - background: var(--surface-subtle); -} - .sentiment-history-table tbody tr:hover td { background: var(--table-hover); } -.sentiment-history-table tbody tr.latest-row td { - background: var(--table-selected); - - font-weight: 650; -} - -.sentiment-history-table tbody tr.latest-row td:first-child { - box-shadow: inset 3px 0 0 var(--action); -} - .sentiment-score-cell { font-weight: 750; } @@ -383,9 +369,9 @@ } .sentiment-phase-badge.phase-fermentation { - background: rgb(237, 246, 234); + background: var(--up-soft); - color: rgb(61, 113, 51); + color: var(--up); } .sentiment-phase-badge.phase-climax { @@ -1640,9 +1626,9 @@ } :root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-fermentation { - background: var(--market-down-soft); + background: var(--market-up-soft); - color: var(--market-down); + color: var(--market-up); } :root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-climax { diff --git a/app/frontend/pages/sentiment/page.js b/app/frontend/pages/sentiment/page.js index be74297..135f5bb 100644 --- a/app/frontend/pages/sentiment/page.js +++ b/app/frontend/pages/sentiment/page.js @@ -42,7 +42,7 @@ function renderSentimentHistory() { empty.hidden = rows.length > 0; body.innerHTML = [...rows].reverse().map((row) => { return ` - + ${escapeHtml(displayCompactDate(row.trade_date))} ${number(row.score)} ${escapeHtml(row.phase)} diff --git a/app/frontend/shared/auth.css b/app/frontend/shared/auth.css index f67d153..3a12e66 100644 --- a/app/frontend/shared/auth.css +++ b/app/frontend/shared/auth.css @@ -1178,14 +1178,14 @@ button.account-role-badge:focus-visible { } @media (min-width: 1280px) and (max-width: 1510px) { - .account-role-badge span { - display: none; + .app-header .header-command-group .account-role-badge span { + display: inline; } - .account-role-badge { - min-width: 27px; + .app-header .header-command-group .account-role-badge { + min-width: 0; - justify-content: center; + justify-content: flex-start; } } diff --git a/app/frontend/shared/components/cards.css b/app/frontend/shared/components/cards.css index c3aefa9..8f00854 100644 --- a/app/frontend/shared/components/cards.css +++ b/app/frontend/shared/components/cards.css @@ -1,11 +1,15 @@ /* Canonical CSS owner: cards. Historical layers consolidated 2026-08-02. */ .stock-name { - font-weight: 700; + font-size: var(--fs-table); + + font-weight: 600; } .stock-code { - color: var(--text-muted); + color: var(--text-3); + + font-size: var(--fs-aux); font-variant-numeric: tabular-nums; } @@ -49,25 +53,37 @@ } .streak-pill { - background: var(--coral-soft); - - color: var(--coral); - - font-weight: 700; - display: inline-flex; align-items: center; - min-height: 24px; + justify-content: center; - padding: 2px 8px; + min-width: 30px; - border-radius: 4px; + height: 20px; + + padding: 0 7px; + + border-radius: 999px; + + background: var(--up-soft); + + color: var(--up); + + font-size: var(--fs-caption); + + font-weight: 600; white-space: nowrap; } +.streak-pill.high { + background: var(--up); + + color: #fff; +} + .outcome-tag { display: inline-flex; @@ -253,21 +269,29 @@ } .sname { - font-weight: 700; + font-weight: 600; - font-size: 13px; + font-size: var(--fs-table); } .scode { - font-size: 11px; + font-size: var(--fs-aux); - color: var(--faint); + color: var(--text-3); - margin-left: 6px; + margin-left: 0; font-weight: 400; } +.stock-cell { + display: flex; + + flex-direction: column; + + line-height: 1.3; +} + .muted { color: var(--faint); } diff --git a/app/frontend/shared/components/controls.css b/app/frontend/shared/components/controls.css index 5653425..779d504 100644 --- a/app/frontend/shared/components/controls.css +++ b/app/frontend/shared/components/controls.css @@ -1010,7 +1010,7 @@ textarea { @media (min-width: 768px) { .header-menu-button { - display: none; + display: inline-flex; } } diff --git a/app/frontend/shared/components/tables.css b/app/frontend/shared/components/tables.css index 85b4d65..c565638 100644 --- a/app/frontend/shared/components/tables.css +++ b/app/frontend/shared/components/tables.css @@ -14,13 +14,13 @@ .data-table th.sort-asc::after { content: " ↑"; - color: var(--blue); + color: var(--accent); } .data-table th.sort-desc::after { content: " ↓"; - color: var(--blue); + color: var(--accent); } .data-table tbody tr td { @@ -114,9 +114,9 @@ text-align: left; - height: 42px; + height: 40px; - padding: 8px 11px; + padding: 0 12px; border-right: 0px; @@ -214,7 +214,9 @@ .data-table .stock-name { color: var(--text-primary); - font-weight: 700; + font-size: var(--fs-table); + + font-weight: 600; } .main-grid > .table-frame { @@ -278,9 +280,9 @@ } .main-grid .data-table thead th { - height: 34px; + height: 36px; - padding: 6px 10px; + padding: 0 12px; } .main-grid .data-table tbody td { @@ -339,8 +341,16 @@ margin-left: 3px; } -.tbl thead th.sorted .arr { - color: var(--blue); +.tbl thead th.sorted, +.data-table thead th.sorted, +.data-table thead th.sort-asc, +.data-table thead th.sort-desc { + color: var(--accent); +} + +.tbl thead th.sorted .arr, +.data-table thead th.sorted .arr { + color: var(--accent); } .tbl tbody td { diff --git a/app/frontend/shared/dashboard.js b/app/frontend/shared/dashboard.js index 7ddb943..41e9e57 100644 --- a/app/frontend/shared/dashboard.js +++ b/app/frontend/shared/dashboard.js @@ -89,7 +89,10 @@ function renderDashboard() { const { meta, overview, ladders, sectors } = state.dashboard; animateMetric("tapeUp", overview.up_count, (value) => Math.round(value)); animateMetric("tapeDown", overview.down_count, (value) => Math.round(value)); - setText("tapeLimit", `${overview.limit_up_count} / 跌停 ${overview.limit_down_count}`); + animateMetric("tapeLimit", overview.limit_up_count, (value) => `${Math.round(value)} 家`); + animateMetric("tapeLimitDown", overview.limit_down_count, (value) => `${Math.round(value)} 家`); + animateMetric("detailBroken", overview.broken_count, (value) => `${Math.round(value)} 家`); + animateMetric("detailSealRate", overview.seal_rate, (value) => `${formatNumber(value, 1)}%`); animateMetric("tapeAmount", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`); animateMetric("limitUpMetric", overview.limit_up_count, (value) => `${Math.round(value)} 家`); animateMetric("limitDownMetric", overview.limit_down_count, (value) => `${Math.round(value)} 家`); @@ -98,22 +101,29 @@ function renderDashboard() { animateMetric("amountMetric", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`); setText("dataDateMetric", dashboardDataTimestamp(meta)); animateMetric("sentimentScore", overview.sentiment_score, (value) => Math.round(value)); - setText("sentimentText", sentimentLabel(overview.sentiment_score)); - const sentimentChip = document.querySelector("#sentimentText"); - if (sentimentChip) { - const label = sentimentLabel(overview.sentiment_score); - sentimentChip.classList.toggle("is-hot", label === "情绪高涨"); - sentimentChip.classList.toggle("is-strong", label === "情绪偏强"); - sentimentChip.classList.toggle("is-cold", label === "情绪冰点" || label === "情绪偏弱"); - } + animateMetric("detailSentimentScore", overview.sentiment_score, (value) => Math.round(value)); + const moodLabel = sentimentLabel(overview.sentiment_score); + setText("sentimentText", moodLabel); + setText("detailSentimentText", moodLabel); + document.querySelectorAll("#sentimentText, #detailSentimentText").forEach((sentimentChip) => { + sentimentChip.classList.toggle("is-hot", moodLabel === "情绪高涨"); + sentimentChip.classList.toggle("is-strong", moodLabel === "情绪偏强"); + sentimentChip.classList.toggle("is-cold", moodLabel === "情绪冰点" || moodLabel === "情绪偏弱"); + }); const pageSubtitle = document.querySelector("#currentPageSubtitle"); const activePage = window.XiaobaiPages?.get(state.activeView); - if (pageSubtitle && activePage?.id !== "mentorView") { - const groupLabel = activePage?.group === "market" - ? "市场复盘" - : activePage?.group === "personal" ? "个人" : "智能工具"; + if (pageSubtitle) { const dateText = displayCompactDate(meta.trade_date); - pageSubtitle.textContent = dateText === "--" ? groupLabel : `${groupLabel} · ${dateText}`; + if (activePage?.id === "mentorView") { + pageSubtitle.textContent = dateText === "--" + ? "与不同交易思维模型持续对话 · 数据日期 --" + : `与不同交易思维模型持续对话 · 数据日期 ${dateText}`; + } else { + const groupLabel = activePage?.group === "market" + ? "市场复盘" + : activePage?.group === "personal" ? "个人" : "智能工具"; + pageSubtitle.textContent = dateText === "--" ? groupLabel : `${groupLabel} · ${dateText}`; + } } updateSentimentGauge(overview.sentiment_score); setText("updatedAt", `${dashboardSourceLabel(meta)} · 更新 ${formatTimestamp(meta.updated_at)}`); diff --git a/app/frontend/shared/shell.css b/app/frontend/shared/shell.css index ffae285..8f3fd87 100644 --- a/app/frontend/shared/shell.css +++ b/app/frontend/shared/shell.css @@ -187,12 +187,6 @@ body.sidebar-collapsed .nav-group + .nav-group { .metric { border-top: 1px solid var(--line); } - - .metric-wide { - display: flex; - - grid-column: span 2; - } } @media (max-width: 860px) { @@ -228,10 +222,6 @@ body.sidebar-collapsed .nav-group + .nav-group { grid-template-columns: repeat(3, 1fr); } - .metric-wide { - grid-column: auto; - } - .metric { padding: 10px; } @@ -262,10 +252,6 @@ body.sidebar-collapsed .nav-group + .nav-group { grid-template-columns: repeat(2, 1fr); } - .metric-wide { - grid-column: 1 / -1; - } - .section-title-group { align-items: flex-start; @@ -311,10 +297,6 @@ body.sidebar-collapsed .nav-group + .nav-group { .overview-strip { grid-template-columns: minmax(210px, 1.3fr) repeat(5, minmax(90px, 0.7fr)); } - - .overview-strip .metric-wide { - display: none; - } } @media (max-width: 767px) { @@ -644,10 +626,6 @@ body.sidebar-collapsed .app-main { padding: 0px 8px; } - .overview-strip .metric-wide { - display: none; - } - .overview-strip .metric:nth-of-type(n+4) { display: none; } @@ -662,14 +640,6 @@ body.sidebar-collapsed .app-main { padding: 8px; } - .overview-strip[data-overview-expanded="true"] .metric-wide { - min-height: 54px; - - display: flex; - - border-bottom: 1px solid var(--border); - } - .overview-strip[data-overview-expanded="true"] .metric:nth-of-type(n) { min-height: 54px; @@ -770,10 +740,6 @@ body.sidebar-collapsed .app-main { display: flex; } - .overview-strip .metric-wide { - display: none; - } - .overview-strip .metric:nth-of-type(n+5) { display: none; } @@ -892,7 +858,7 @@ body.sidebar-collapsed .app-main { padding: 0px 10px; - border-radius: 7px; + border-radius: var(--radius-md); font-size: 12.5px; @@ -1452,7 +1418,7 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group { border: 0px; - border-radius: 7px; + border-radius: var(--radius-md); background: transparent; @@ -2719,10 +2685,6 @@ body.sidebar-collapsed .status-bar { padding: 0px; } - .overview-strip .metric-wide { - display: none; - } - .overview-strip .metric:nth-of-type(n+4) { display: none; } @@ -3216,11 +3178,6 @@ body.sidebar-collapsed .status-bar { display: flex; } - body.mobile-shell .overview-strip .metric-wide, - body.mobile-shell .overview-toggle { - display: none; - } - body.mobile-shell .workspace-view, body.mobile-shell .workspace-view.page:not(#heavenView), body.mobile-shell:is([data-active-view]) .workspace-view.active-view { @@ -3367,7 +3324,8 @@ body.sidebar-collapsed .status-bar { color: var(--down); } -.tape-metric .metric-value.warning { +.tape-metric .metric-value.warning, +.tape-detail-item .warning { color: var(--warn); } @@ -3380,7 +3338,8 @@ body.sidebar-collapsed .status-bar { pointer-events: none; } -.tape-sentiment .sentiment-text { +.tape-sentiment .sentiment-text, +.tape-detail-item .sentiment-text { display: inline-flex; align-items: center; height: 18px; @@ -3394,17 +3353,20 @@ body.sidebar-collapsed .status-bar { line-height: 18px; } -.tape-sentiment .sentiment-text.is-hot { +.tape-sentiment .sentiment-text.is-hot, +.tape-detail-item .sentiment-text.is-hot { background: var(--up-soft); color: var(--up); } -.tape-sentiment .sentiment-text.is-strong { +.tape-sentiment .sentiment-text.is-strong, +.tape-detail-item .sentiment-text.is-strong { background: var(--accent-soft); color: var(--accent); } -.tape-sentiment .sentiment-text.is-cold { +.tape-sentiment .sentiment-text.is-cold, +.tape-detail-item .sentiment-text.is-cold { background: var(--down-soft); color: var(--down); } @@ -3479,11 +3441,13 @@ body.sidebar-collapsed .status-bar { } .app-header .header-actions { + position: relative; display: flex; flex: 0 0 auto; align-items: center; gap: 6px; margin-left: 0; + overflow: visible; } .app-header .header-date-group { @@ -3675,3 +3639,68 @@ body.sidebar-collapsed .status-bar { } } +@media (min-width: 768px) { + .app-header .header-menu-button { + display: inline-flex; + } + + .app-header .header-command-group { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 60; + display: none; + flex-direction: column; + align-items: stretch; + gap: 6px; + width: 240px; + padding: 8px; + overflow: visible; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + box-shadow: var(--shadow-float); + } + + .app-header .header-command-group.is-open { + display: flex; + } + + .app-header .header-command-group .command-button, + .app-header .header-command-group .account-button { + width: 100%; + justify-content: flex-start; + } + + .app-header .header-command-group .account-menu-shell { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + } + + .app-header .header-command-group .account-role-badges { + display: flex; + flex-wrap: wrap; + min-height: 0; + } + + .app-header .header-command-group .account-dropdown { + left: 0; + right: auto; + } +} + +body[data-active-view="heavenView"] .app-header .overview-strip, +body[data-active-view="heavenView"] .app-header .overview-strip[data-overview-expanded="true"], +body.mobile-shell[data-active-view="heavenView"] .app-header .overview-strip, +body.mobile-shell[data-active-view="heavenView"] .app-header .overview-strip[data-overview-expanded="true"] { + display: none; + flex: 0 0 0; + width: 0; + min-width: 0; + height: 0; + min-height: 0; + overflow: hidden; +} + diff --git a/app/frontend/shared/shell.js b/app/frontend/shared/shell.js index 682d0b2..f039003 100644 --- a/app/frontend/shared/shell.js +++ b/app/frontend/shared/shell.js @@ -56,11 +56,32 @@ updateSidebarControl(); } + function pageGroupLabel(page) { + return page?.group === "market" + ? "市场复盘" + : page?.group === "personal" ? "个人" : "智能工具"; + } + + function writePageSubtitle(viewId) { + const pageSubtitle = document.querySelector("#currentPageSubtitle"); + if (!pageSubtitle) return; + const page = registry.get(viewId); + const dateText = options.tradeDate?.() || ""; + const hasDate = Boolean(dateText) && dateText !== "--"; + if (viewId === "mentorView") { + pageSubtitle.textContent = hasDate + ? `与不同交易思维模型持续对话 · 数据日期 ${dateText}` + : "与不同交易思维模型持续对话 · 数据日期 --"; + return; + } + const groupLabel = pageGroupLabel(page); + pageSubtitle.textContent = hasDate ? `${groupLabel} · ${dateText}` : groupLabel; + } + function syncNavigation(viewId) { const page = registry.get(viewId); const navigationId = page?.navigation_alias || viewId; const marketView = page?.group === "market"; - const previousActiveView = document.body.dataset.activeView; document.body.dataset.activeView = viewId; const mobileGroup = document.querySelector("#mobilePageGroup"); const mobileTitle = document.querySelector("#mobilePageTitle"); @@ -72,13 +93,7 @@ if (mobileTitle) mobileTitle.textContent = page?.title || "小白复盘"; const pageTitle = document.querySelector("#currentPageTitle"); if (pageTitle) pageTitle.textContent = page?.title || "小白复盘"; - const pageSubtitle = document.querySelector("#currentPageSubtitle"); - if (pageSubtitle && previousActiveView !== viewId) { - const groupLabel = page?.group === "market" - ? "市场复盘" - : page?.group === "personal" ? "个人" : "智能工具"; - pageSubtitle.textContent = groupLabel; - } + writePageSubtitle(viewId); document.querySelectorAll(".module-tab").forEach((button) => { button.classList.toggle("active", button.dataset.view === navigationId); button.classList.toggle( @@ -216,7 +231,7 @@ if (expanded) toggleHeaderCommandMenu(false); }); document.addEventListener("click", (event) => { - if (!event.target.closest(".header-actions")) toggleHeaderCommandMenu(false); + if (!event.target.closest("#headerCommandGroup, #headerMenuButton")) toggleHeaderCommandMenu(false); if (!event.target.closest(".overview-strip")) setOverviewExpanded(false); }); document.addEventListener("keydown", (event) => { diff --git a/app/tests/e2e/app-shell.spec.js b/app/tests/e2e/app-shell.spec.js index bee3550..1950d2d 100644 --- a/app/tests/e2e/app-shell.spec.js +++ b/app/tests/e2e/app-shell.spec.js @@ -510,12 +510,21 @@ async function mockApplication(page, authSession = session(), options = {}) { }); } +async function openHeaderCommandMenu(page) { + const menu = page.locator("#headerCommandGroup"); + if (await menu.isVisible()) return; + await page.locator("#headerMenuButton").click(); + await expect(menu).toBeVisible(); +} + test("admin shell opens every primary workspace and global search", async ({ page }) => { await mockApplication(page); await page.goto("/index.html"); await expect(page.locator("#authGate")).toBeHidden(); + await openHeaderCommandMenu(page); await expect(page.locator("#settingsButton")).toBeVisible(); await expect(page.locator("#syncButton")).toBeVisible(); + await page.keyboard.press("Escape"); await page.locator("#alertButton").click(); await expect(page.locator("#alertsDialog")).toBeVisible(); await page.locator("#closeAlertsDialog").click(); @@ -589,6 +598,7 @@ test("manual refresh stays in place without reopening the full-page loader", asy await page.goto("/index.html"); await expect(page.locator("#loadingOverlay")).toBeHidden(); + await openHeaderCommandMenu(page); await page.locator("#refreshButton").click(); await expect(page.locator("#refreshButton")).toBeDisabled(); @@ -3134,6 +3144,7 @@ test("global dialogs share the stage 18 geometry without changing account or adm expect(assistantGeometry.contentDisplay).toBe("flex"); await page.locator("#closeAssistantDialog").click(); + await openHeaderCommandMenu(page); await page.locator("#accountButton").click(); await page.locator('[data-account-panel="membership"]').click(); await expect(page.locator("#settingsDialog")).toHaveAttribute("aria-labelledby", "accountDialogTitle"); @@ -3144,6 +3155,7 @@ test("global dialogs share the stage 18 geometry without changing account or adm expect(Math.abs(accountBox.y + accountBox.height / 2 - 450)).toBeLessThanOrEqual(2); await page.locator("#closeSettingsDialog").click(); + await openHeaderCommandMenu(page); await page.locator("#settingsButton").click(); await expect(page.locator("#adminDialog")).toHaveAttribute("aria-labelledby", "adminDialogTitle"); await expect(page.locator("#adminSectionSelect")).toBeVisible(); @@ -3163,3 +3175,63 @@ test("global dialogs share the stage 18 geometry without changing account or adm expect(mobileGeometry.width).toBeLessThanOrEqual(375); expect(mobileGeometry.documentOverflow).toBeLessThanOrEqual(1); }); + +test("merged header keeps date, detail fields and 13 pool columns after dual-review rework", async ({ page }) => { + await mockApplication(page, session("admin", true)); + await page.goto("/index.html"); + await page.waitForTimeout(400); + await expect(page.locator("#currentPageSubtitle")).toContainText("市场复盘 ·"); + await expect(page.locator("#headerMenuButton")).toBeVisible(); + await expect(page.locator("#headerCommandGroup")).toBeHidden(); + await expect(page.locator("#themeToggle")).toBeVisible(); + await expect(page.locator("#refreshButton")).toBeHidden(); + + await page.locator('[data-view="limitPool"]').first().click(); + await expect(page.locator("#currentPageSubtitle")).toContainText("市场复盘 ·"); + await expect(page.locator("#currentPageSubtitle")).not.toHaveText("市场复盘"); + + await page.setViewportSize({ width: 1280, height: 800 }); + await expect(page.locator(".tape-optional").first()).toBeHidden(); + await page.locator("#overviewToggle").click(); + const detailText = await page.locator(".tape-detail").innerText(); + for (const label of ["市场情绪", "上涨家数", "下跌家数", "涨停", "跌停", "炸板", "封板率", "两市成交", "数据日期"]) { + expect(detailText).toContain(label); + } + expect(detailText).not.toContain("涨停 / 跌停"); + await expect(page.locator("#tapeLimitDown")).toBeVisible(); + await expect(page.locator("#detailBroken")).toBeVisible(); + await expect(page.locator("#dataDateMetric")).toBeVisible(); + await page.keyboard.press("Escape"); + + await page.setViewportSize({ width: 1600, height: 1000 }); + await page.locator('[data-view="limitPool"]').first().click(); + const tableFit = await page.evaluate(() => { + const card = document.querySelector("#limitPool .pool-table-card"); + const reason = document.querySelector("#limitTable .reason-column"); + const header = document.querySelector(".app-header"); + return { + cardOverflow: card.scrollWidth - card.clientWidth, + reasonVisible: reason.getBoundingClientRect().right <= card.getBoundingClientRect().right + 1, + reasonText: reason.textContent.trim(), + headerOverflow: header.scrollWidth - header.clientWidth, + }; + }); + expect(tableFit.reasonText).toContain("涨停原因"); + expect(tableFit.reasonVisible).toBe(true); + expect(tableFit.cardOverflow).toBeLessThanOrEqual(1); + expect(tableFit.headerOverflow).toBeLessThanOrEqual(1); + + await page.locator('[data-view="heavenView"]').first().click(); + await expect(page.locator(".app-header .overview-strip")).toBeHidden(); + await expect(page.locator("#overviewToggle")).toBeHidden(); + + await page.setViewportSize({ width: 390, height: 844 }); + await page.locator('[data-view="limitPool"]').first().click({ force: true }); + await page.locator("#mobileMarketViewSelect").selectOption("sentimentCycleView"); + await page.locator("#overviewToggle").click(); + const mobileDetail = await page.locator(".tape-detail").innerText(); + for (const label of ["市场情绪", "上涨家数", "下跌家数", "涨停", "跌停", "炸板", "封板率", "两市成交", "数据日期"]) { + expect(mobileDetail).toContain(label); + } + await expect(page.locator("#dataDateMetric")).toBeVisible(); +});