主要信号:{html.escape(top_themes or '无')}
-From 58b6bf7f49d355b0d2780bc93414765265b2081b Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:20:28 +0900 Subject: [PATCH] frontend: match research homepage to approved UI audit page 9 --- src/quant_advisor_research/publisher.py | 171 +++++++++++++++++------- tests/test_publisher.py | 24 ++-- tests/test_publisher_homepage_design.py | 113 ++++++++++++++++ 3 files changed, 250 insertions(+), 58 deletions(-) create mode 100644 tests/test_publisher_homepage_design.py diff --git a/src/quant_advisor_research/publisher.py b/src/quant_advisor_research/publisher.py index 60d0574..df6fe12 100644 --- a/src/quant_advisor_research/publisher.py +++ b/src/quant_advisor_research/publisher.py @@ -29,7 +29,7 @@ ("medium", "中线", "2-12周"), ("short", "短线", "1-10个交易日"), ) -INDEX_HISTORY_LIMIT = 12 +INDEX_HISTORY_LIMIT = 5 RSS_ITEM_LIMIT = 20 SITE_ICON_FILENAME = "favicon.svg" @@ -605,20 +605,30 @@ def format_candidate_theme_ids(candidate: dict[str, Any]) -> str: """ INDEX_PAGE_CSS = """ + :root { --paper: #f7f6f2; --sheet: #fff; --ink: #202124; --ink-soft: #595f66; --muted: #737980; --rule: #dfddd5; --accent: #24587f; --container: 1250px; } + @media (prefers-color-scheme: dark) { + :root { --paper: #11151b; --sheet: #171c23; --ink: #e5e7ea; --ink-soft: #c5cad2; --muted: #98a0ad; --rule: #2b323c; --accent: #8cbcdf; } + } + .site-header { background: var(--sheet); border-bottom: 1px solid var(--rule); } + .site-header-inner { border: 0; } + .brand-link { font-family: var(--font-sans); } + .site-nav a { color: var(--accent); font-size: 1rem; text-decoration: underline; } main { max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter) 96px; } - .hero { padding: clamp(40px, 7vw, 80px) 0 clamp(28px, 4vw, 44px); } + .hero { padding: clamp(40px, 5vw, 52px) 0 32px; border-bottom: 2px solid var(--ink); } .hero-copy { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 16px clamp(28px, 5vw, 72px); align-items: end; } h1 { margin: 0; font-family: var(--font-display); font-size: clamp(2.375rem, 5.6vw, 4rem); font-weight: 700; line-height: 1.08; letter-spacing: .005em; } - .hero p { margin: 0; max-width: 30em; padding-left: 18px; border-left: 2px solid var(--accent); color: var(--ink-soft); font-size: 1rem; line-height: 1.85; } + .hero p { margin: 0; max-width: 32em; color: var(--ink-soft); font-size: 1rem; line-height: 1.85; } + .risk-notice { display: flex; gap: 14px; align-items: center; margin: 40px 0; padding: 12px 18px; border: 1px solid #e9c77c; border-radius: 8px; background: #fff8e6; color: #705a24; font-size: 1rem; } + .risk-notice svg { width: 20px; height: 20px; flex: 0 0 auto; } + .index-content { display: grid; grid-template-columns: minmax(0, 1.94fr) minmax(0, 1fr); gap: 26px; align-items: start; } - /* Latest issue: lead story set against the paper, not a boxed card. */ + /* Homepage composition follows the approved report overview design. */ .latest-panel { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); - gap: clamp(28px, 5vw, 72px); - padding: 28px 0 36px; - border-top: 4px solid var(--rule-strong); - border-bottom: 1px solid var(--rule); + min-width: 0; + padding: 32px; + background: var(--sheet); + border: 1px solid var(--rule); + border-radius: 11px; } h2 { margin: 0; font-family: var(--font-display); font-size: clamp(1.5rem, 2.8vw, 2.125rem); font-weight: 700; line-height: 1.28; } .lead { margin: 14px 0 0; max-width: 32em; color: var(--ink-soft); line-height: 1.85; } @@ -640,6 +650,24 @@ def format_candidate_theme_ids(candidate: dict[str, Any]) -> str: } .primary-action:hover { color: var(--accent); } .latest-panel > .snapshot-grid { align-self: start; } + .latest-meta { display: flex; align-items: center; gap: 12px; margin: 0 0 22px; color: var(--muted); font-family: var(--font-figures); font-size: .875rem; } + .latest-meta strong { background: var(--ink); color: var(--sheet); font-family: var(--font-sans); padding: 1px 9px; border-radius: 3px; } + .latest-panel .snapshot-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 13px; margin: 22px 0; border: 0; } + .latest-panel .snapshot-column { display: block; padding: 15px; background: var(--paper); border: 1px solid var(--rule); border-radius: 8px; } + .latest-panel .snapshot-label { display: inline; font-family: var(--font-sans); font-size: .9375rem; } + .latest-panel .snapshot-window { display: inline; font-size: .875rem; white-space: normal; } + .latest-panel .symbol-strip { margin-top: 4px; color: var(--ink-soft); font-size: .9375rem; } + .snapshot-reason { margin: 4px 0 0; color: var(--muted); font-size: .8125rem; line-height: 1.6; } + .empty-conclusion { margin: 20px 0; color: var(--ink-soft); font-size: .9375rem; } + .latest-panel .primary-action { padding: 10px 20px; background: var(--accent); color: var(--sheet); border: 0; border-radius: 6px; font-family: var(--font-sans); font-size: .9375rem; } + .latest-panel .primary-action:hover { opacity: .9; } + .recent-reports h2 { font-family: var(--font-sans); font-size: 1.0625rem; padding-bottom: 10px; } + .recent-list { border-top: 1px solid var(--rule); } + .recent-report { display: flex; justify-content: space-between; gap: 14px; padding: 8px 0; border-bottom: 1px solid var(--rule); color: var(--ink-soft); text-decoration: none; font-size: .875rem; } + .recent-report time, .recent-report small { display: block; } + .recent-report small, .recent-count { color: var(--muted); font-size: .8125rem; } + .recent-count { align-self: center; } + .recent-reports .archive-action { margin-top: 12px; font-size: .9375rem; font-weight: 400; } .archive { margin-top: clamp(48px, 7vw, 80px); } .section-title { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 0; padding-bottom: 14px; } @@ -647,9 +675,11 @@ def format_candidate_theme_ids(candidate: dict[str, Any]) -> str: .section-title p { margin: 4px 0 0; color: var(--muted); font-size: .875rem; } .archive-action { display: inline-flex; align-items: center; min-height: 44px; font-weight: 600; white-space: nowrap; text-decoration: underline; } @media (max-width: 960px) { - .hero-copy, .latest-panel { grid-template-columns: minmax(0, 1fr); } + .hero-copy, .index-content { grid-template-columns: minmax(0, 1fr); } } @media (max-width: 640px) { + .latest-panel { padding: 20px; } + .latest-panel .snapshot-grid { grid-template-columns: minmax(0, 1fr); } .section-title { flex-direction: column; align-items: flex-start; gap: 4px; } } """ @@ -1024,44 +1054,94 @@ def render_horizon_snapshot(report: dict[str, Any], *, linked: bool = False) -> return f'
{label}
{window.replace('-', '–').replace('年', ' 年').replace('周', ' 周').replace('个', ' 个')}
+{reason}
' if reason else ''} +本期三个周期均未产生合格推荐。{explanation}
' + + def render_index_html(reports: list[dict[str, Any]], *, now: dt.datetime | None = None) -> str: sorted_reports = sorted(reports, key=lambda item: item["as_of"], reverse=True) reference_now = now or dt.datetime.now(dt.UTC) latest = next((item for item in sorted_reports if not is_report_expired(item, now=reference_now)), None) if latest is None and sorted_reports: latest = sorted_reports[0] - latest_block = "" + latest_block = '已过期报告
' if is_report_expired(latest, now=reference_now) else ""} -{"该报告已过期,不再作为当前公开推荐。" if is_report_expired(latest, now=reference_now) else "结合主题动量、市场确认和事件证据,生成普通投资者更容易阅读的研究结论。"}
-该报告已过期,不再作为当前公开推荐。
' if expired else ''} + {render_index_horizons(latest)} + {render_index_empty_conclusion(latest)} + 阅读完整报告主要信号:{html.escape(top_themes or '无')}
-暂无更多历史报告。
' @@ -1079,12 +1159,13 @@ def render_index_html(reports: list[dict[str, Any]], *, now: dt.datetime | None把主题动量、市场确认和政策/新闻证据整理成普通投资者能读懂的研究结论。投资有风险,不构成投资建议。
+把主题动量、市场确认和政策 / 新闻证据整理成普通投资者能读懂的研究结论。
首页最多显示最近 {INDEX_HISTORY_LIMIT} 期,完整记录保留在归档页
-投资有风险。本站内容为模型研究输出,不构成投资建议。
+