From b11ac6965facf808aaf7e5bcf80f637b8cf92e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=80=BB=E5=B7=A5?= Date: Fri, 28 Aug 2026 14:05:41 +0000 Subject: [PATCH] =?UTF-8?q?fix(HEL-217):=20=E9=99=90=E5=88=B6=E6=B8=A9?= =?UTF-8?q?=E5=BA=A6=E8=B5=B0=E5=8A=BF=E5=9B=BE=E9=AB=98=E5=BA=A6=EF=BC=8C?= =?UTF-8?q?=E6=8C=89=E5=B7=A6=E6=A0=8F=E5=AF=B9=E9=BD=90=E5=8F=B3=E4=BE=A7?= =?UTF-8?q?=E5=B9=B6=E4=BF=9D=E7=95=99=E5=A4=9C=E9=97=B4=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 撤掉图表随右栏无限拉伸的布局,恢复一屏内的成熟比例;右侧两卡跟随左栏高度,夜间 tooltip 改用主题变量。 Co-authored-by: Cursor Co-authored-by: multica-agent --- frontend/pages/sentiment/foundation.css | 101 ++++++++++++++++++ tests/e2e/app-shell.spec.js | 136 ++++++++++++++++++++++++ tests/test_frontend_contract.py | 5 + 3 files changed, 242 insertions(+) diff --git a/frontend/pages/sentiment/foundation.css b/frontend/pages/sentiment/foundation.css index 6accf89..263351b 100644 --- a/frontend/pages/sentiment/foundation.css +++ b/frontend/pages/sentiment/foundation.css @@ -1021,9 +1021,21 @@ } .sentiment-chart-tooltip b { + color: inherit; + font-weight: 700; } +:root[data-theme="dark"] #sentimentCycleView .sentiment-chart-tooltip { + border: 1px solid var(--border-strong); + + background: var(--surface-muted); + + color: var(--text-primary); + + box-shadow: var(--elevation-float); +} + .sentiment-phase-block { display: flex; @@ -1673,6 +1685,95 @@ } } +@media (min-width: 1024px) { + .redesigned-emotion-grid, + .emo-grid { + align-items: stretch; + } + + .redesigned-sentiment-view .sentiment-analysis-main { + height: max-content; + } + + .redesigned-sentiment-view .sentiment-analysis-rail { + height: 0; + + min-height: 100%; + + overflow: auto; + } + + .redesigned-sentiment-view .sentiment-trend-panel { + display: flex; + + flex-direction: column; + } + + .redesigned-sentiment-view .sentiment-chart-shell { + flex: 0 0 auto; + + height: min(350px, 40vh); + } + + .redesigned-sentiment-view .sentiment-chart-shell canvas { + height: 100%; + } + + .redesigned-sentiment-view .sentiment-cycle-summary { + flex: 0 0 auto; + } + + .redesigned-sentiment-view .sentiment-components-panel { + display: flex; + + flex: 1 1 auto; + + flex-direction: column; + + min-height: 0; + } + + .redesigned-sentiment-view .sentiment-component-list { + flex: 1 1 auto; + + min-height: 0; + + overflow: auto; + + padding: 4px 16px 8px; + } + + .redesigned-sentiment-view .sentiment-component-item { + padding: 4px 0px; + } + + .redesigned-sentiment-view .sentiment-component-item small { + display: none; + } + + .redesigned-sentiment-view .sentiment-phase-block { + gap: 12px; + + padding: 10px 12px; + } + + .redesigned-sentiment-view .sentiment-current-phase-badge { + padding: 8px 12px; + } + + .redesigned-sentiment-view .sentiment-phase-advice { + margin-top: 4px; + + padding: 4px 8px; + + line-height: 1.4; + } + + .redesigned-sentiment-view .sentiment-feedback-strip > span { + padding: 4px 10px; + } +} + @media (min-width: 1024px) { .sentiment-cycle-summary { grid-template-columns: minmax(270px, 1.25fr) repeat(3, minmax(160px, 1fr)); diff --git a/tests/e2e/app-shell.spec.js b/tests/e2e/app-shell.spec.js index 9ceaa8b..9bd7f60 100644 --- a/tests/e2e/app-shell.spec.js +++ b/tests/e2e/app-shell.spec.js @@ -1,4 +1,6 @@ const { test, expect } = require("@playwright/test"); +const fs = require("fs"); +const path = require("path"); const dashboard = { meta: { @@ -725,6 +727,7 @@ test("collapsed overview and sentiment layout keep a single current reading", as const trend = document.querySelector(".sentiment-trend-panel").getBoundingClientRect(); const summary = document.querySelector(".sentiment-cycle-summary").getBoundingClientRect(); const chart = document.querySelector(".sentiment-chart-shell").getBoundingClientRect(); + const rail = document.querySelector(".sentiment-analysis-rail").getBoundingClientRect(); const detail = document.querySelector(".sentiment-detail-toolbar").getBoundingClientRect(); const label = document.querySelector(".sentiment-block .metric-label"); const status = document.querySelector(".sentiment-block .sentiment-text"); @@ -733,8 +736,10 @@ test("collapsed overview and sentiment layout keep a single current reading", as return { columnsAligned: Math.abs(trend.top - summary.top) < 1, railAligned: Math.abs(summary.x - components.x) < 1 && Math.abs(summary.width - components.width) < 1 && components.top > summary.bottom, + bottomsAligned: Math.abs(trend.bottom - rail.bottom) < 2, detailAfterAnalysis: detail.top > Math.max(trend.bottom, components.bottom), chartHeight: chart.height, + viewportHeight: window.innerHeight, sameType: labelStyle.fontSize === statusStyle.fontSize && labelStyle.fontWeight === statusStyle.fontWeight && labelStyle.lineHeight === statusStyle.lineHeight, @@ -744,8 +749,11 @@ test("collapsed overview and sentiment layout keep a single current reading", as }); expect(alignment.columnsAligned).toBe(true); expect(alignment.railAligned).toBe(true); + expect(alignment.bottomsAligned).toBe(true); expect(alignment.detailAfterAnalysis).toBe(true); expect(alignment.chartHeight).toBeGreaterThanOrEqual(340); + expect(alignment.chartHeight).toBeLessThanOrEqual(360); + expect(alignment.chartHeight / alignment.viewportHeight).toBeLessThan(0.45); expect(alignment.sameType).toBe(true); expect(alignment.sameBaseline).toBe(true); expect(alignment.noStatusOffset).toBe(true); @@ -780,6 +788,134 @@ test("collapsed overview and sentiment layout keep a single current reading", as } }); +function buildSentimentHistoryRows(count = 20) { + const phases = ["冰点", "修复", "发酵", "高潮", "分化", "退潮"]; + return Array.from({ length: count }, (_, index) => { + const score = 28 + ((index * 7) % 55); + return { + trade_date: `2026-07-${String(index + 1).padStart(2, "0")}`, + score, + label: "情绪观察", + phase: phases[index % phases.length], + direction: index % 2 ? "升温" : "降温", + day_change: index % 2 ? 2.4 : -1.6, + seal_rate: 70 + (index % 10), + limit_up_count: 30 + index, + first_board_count: 20, + second_board_count: 6, + three_plus_count: 3, + max_height: 4, + broken_count: 17, + limit_down_count: 8, + previous_limit_count: 40, + previous_positive_count: 12, + previous_positive_rate: 30, + average_previous_change: -1.2, + normalization: "固定锚点", + history_days: count, + components: { + breadth: { label: "市场宽度", score: 55.8, weight: 20, summary: "红盘家数偏少" }, + limit: { label: "涨停生态", score: 79.0, weight: 25, summary: "封板率仍需确认" }, + profit: { label: "赚钱效应", score: 67.0, weight: 30, summary: "昨日反馈偏弱" }, + ladder: { label: "连板结构", score: 81.5, weight: 15, summary: "高度仍在压缩" }, + amount: { label: "成交活跃度", score: 46.1, weight: 10, summary: "量能低于均值" }, + }, + }; + }); +} + +function contrastRatio(rgbA, rgbB) { + const channel = (value) => { + const scaled = value / 255; + return scaled <= 0.03928 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4; + }; + const luminance = (rgb) => { + const [red, green, blue] = rgb.match(/\d+/g).map(Number); + return 0.2126 * channel(red) + 0.7152 * channel(green) + 0.0722 * channel(blue); + }; + const first = luminance(rgbA); + const second = luminance(rgbB); + const [high, low] = first > second ? [first, second] : [second, first]; + return (high + 0.05) / (low + 0.05); +} + +test("sentiment trend chart keeps one-screen proportion across 4K-equivalent zooms", async ({ page }) => { + const screenshotDir = path.join("runtime", "hel217-screenshots"); + fs.mkdirSync(screenshotDir, { recursive: true }); + await mockApplication(page, session("user", true)); + await page.goto("/index.html"); + const rows = buildSentimentHistoryRows(20); + await page.evaluate((historyRows) => { + state.sentimentHistory = { available_days: 20, rows: historyRows }; + renderSentimentHistory(); + }, rows); + await page.locator('[data-view="sentimentCycleView"]').first().click(); + + const viewports = [ + { name: "4k150-100", width: 2560, height: 1440 }, + { name: "4k150-110", width: 2328, height: 1309 }, + { name: "4k150-125", width: 2048, height: 1152 }, + ]; + + for (const viewport of viewports) { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await page.waitForTimeout(200); + for (const theme of ["light", "dark"]) { + const current = await page.locator("html").getAttribute("data-theme"); + if (current !== theme) { + await page.locator("#themeToggle").click(); + await expect(page.locator("html")).toHaveAttribute("data-theme", theme); + } + const metrics = await page.evaluate(() => { + const trend = document.querySelector(".sentiment-trend-panel").getBoundingClientRect(); + const rail = document.querySelector(".sentiment-analysis-rail").getBoundingClientRect(); + const chart = document.querySelector(".sentiment-chart-shell").getBoundingClientRect(); + const detail = document.querySelector(".sentiment-detail-toolbar").getBoundingClientRect(); + const history = document.querySelector(".sentiment-history-frame").getBoundingClientRect(); + return { + chartHeight: chart.height, + viewportHeight: window.innerHeight, + bottomsAligned: Math.abs(trend.bottom - rail.bottom) < 2, + detailAfterAnalysis: detail.top > Math.max(trend.bottom, rail.bottom) - 1, + historyVisibleBelow: history.top > detail.bottom - 1, + overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1, + }; + }); + expect(metrics.chartHeight, viewport.name).toBeGreaterThanOrEqual(280); + expect(metrics.chartHeight, viewport.name).toBeLessThanOrEqual(360); + expect(metrics.chartHeight / metrics.viewportHeight, viewport.name).toBeLessThan(0.42); + expect(metrics.bottomsAligned, viewport.name).toBe(true); + expect(metrics.detailAfterAnalysis, viewport.name).toBe(true); + expect(metrics.historyVisibleBelow, viewport.name).toBe(true); + expect(metrics.overflowX, viewport.name).toBe(false); + + if (theme === "dark") { + const tooltip = page.locator("#sentimentChartTooltip"); + await page.locator("#sentimentTrendChart").hover({ position: { x: 360, y: 110 } }); + await expect(tooltip).toBeVisible(); + const tooltipColors = await tooltip.evaluate((element) => { + const style = getComputedStyle(element); + const bold = element.querySelector("b"); + return { + color: style.color, + background: style.backgroundColor, + boldColor: bold ? getComputedStyle(bold).color : style.color, + text: element.textContent, + }; + }); + expect(tooltipColors.text).toMatch(/温度/); + expect(contrastRatio(tooltipColors.color, tooltipColors.background)).toBeGreaterThanOrEqual(4.5); + expect(contrastRatio(tooltipColors.boldColor, tooltipColors.background)).toBeGreaterThanOrEqual(4.5); + } + + await page.screenshot({ + path: path.join(screenshotDir, `${viewport.name}-${theme}.png`), + fullPage: true, + }); + } + } +}); + test("limit-up pool separates stock identity and restores the reason column", async ({ page }) => { await mockApplication(page, session("user", true)); await page.goto("/index.html"); diff --git a/tests/test_frontend_contract.py b/tests/test_frontend_contract.py index ab8f0fd..ebd22bb 100644 --- a/tests/test_frontend_contract.py +++ b/tests/test_frontend_contract.py @@ -343,6 +343,11 @@ class FrontendContractTests(unittest.TestCase): self.assertIn("--sentiment-history-max-height: 510px;", self.tokens) self.assertIn("max-height: var(--sentiment-history-max-height);", self.sentiment_styles) self.assertIn("overflow: auto;", self.sentiment_styles) + self.assertIn("height: min(350px, 40vh);", self.sentiment_styles) + self.assertIn(':root[data-theme="dark"] #sentimentCycleView .sentiment-chart-tooltip {', self.sentiment_styles) + self.assertIn("background: var(--surface-muted);", self.sentiment_styles) + self.assertIn("color: var(--text-primary);", self.sentiment_styles) + self.assertNotIn("flex: 1 1 auto;\n\n height: auto;", self.sentiment_styles) def test_mentor_final_visual_fix_contract(self): shell_styles = (STATIC_DIR / "shared" / "shell.css").read_text(encoding="utf-8")