Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b11ac6965f |
+101
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user