Compare commits

...
Author SHA1 Message Date
b11ac6965f fix(HEL-217): 限制温度走势图高度,按左栏对齐右侧并保留夜间提示
撤掉图表随右栏无限拉伸的布局,恢复一屏内的成熟比例;右侧两卡跟随左栏高度,夜间 tooltip 改用主题变量。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-28 14:05:41 +00:00
3 changed files with 242 additions and 0 deletions
+101
View File
@@ -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));
+136
View File
@@ -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");
+5
View File
@@ -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")