Skip to content

Commit ba62ebe

Browse files
committed
Add site icon branding
1 parent 7c093d2 commit ba62ebe

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

src/quant_advisor_research/publisher.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,27 @@
2525
("short", "短线", "1-10个交易日"),
2626
)
2727

28+
SITE_ICON_FILENAME = "favicon.svg"
29+
SITE_ICON_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
30+
<defs>
31+
<linearGradient id="qsl-bg" x1="8" y1="6" x2="58" y2="62" gradientUnits="userSpaceOnUse">
32+
<stop stop-color="#1e4dd8"/>
33+
<stop offset="0.56" stop-color="#00a6c8"/>
34+
<stop offset="1" stop-color="#d99b2b"/>
35+
</linearGradient>
36+
<filter id="qsl-shadow" x="-20%" y="-20%" width="140%" height="140%">
37+
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#172033" flood-opacity=".28"/>
38+
</filter>
39+
</defs>
40+
<rect x="5" y="5" width="54" height="54" rx="16" fill="#172033"/>
41+
<path d="M18 43V25.5L29.2 36.7 44.8 20" fill="none" stroke="url(#qsl-bg)" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round" filter="url(#qsl-shadow)"/>
42+
<circle cx="18" cy="43" r="4" fill="#fffaf0"/>
43+
<circle cx="30" cy="36" r="4" fill="#fffaf0"/>
44+
<circle cx="46" cy="19" r="4.5" fill="#fffaf0"/>
45+
<path d="M45 45c-4 3.7-9.7 5-15.1 3.3-8.8-2.8-13.7-12.2-10.9-21 2.1-6.7 8.1-11.1 14.7-11.6" fill="none" stroke="#fffaf0" stroke-width="3.2" stroke-linecap="round" opacity=".88"/>
46+
</svg>
47+
"""
48+
2849

2950
def slug(value: str) -> str:
3051
text = re.sub(r"[^a-zA-Z0-9]+", "-", value.strip().lower()).strip("-")
@@ -74,6 +95,21 @@ def format_candidate_theme_ids(candidate: dict[str, Any]) -> str:
7495
return ", ".join(labels) or "无"
7596

7697

98+
def render_site_mark() -> str:
99+
return """
100+
<span class="site-mark" aria-hidden="true">
101+
<svg viewBox="0 0 64 64" focusable="false">
102+
<rect x="5" y="5" width="54" height="54" rx="16"></rect>
103+
<path class="mark-line" d="M18 43V25.5L29.2 36.7 44.8 20"></path>
104+
<circle cx="18" cy="43" r="4"></circle>
105+
<circle cx="30" cy="36" r="4"></circle>
106+
<circle cx="46" cy="19" r="4.5"></circle>
107+
<path class="mark-ring" d="M45 45c-4 3.7-9.7 5-15.1 3.3-8.8-2.8-13.7-12.2-10.9-21 2.1-6.7 8.1-11.1 14.7-11.6"></path>
108+
</svg>
109+
</span>
110+
"""
111+
112+
77113
def horizon_pick_score(pick: dict[str, Any], horizon: str) -> float:
78114
score = pick.get("horizon_scores", {}).get(horizon, {}).get("score")
79115
return float(score) if isinstance(score, (int, float)) else as_sortable_float(pick.get("combined_score"))
@@ -232,12 +268,19 @@ def render_report_html(report: dict[str, Any]) -> str:
232268
<meta charset="utf-8">
233269
<meta name="viewport" content="width=device-width, initial-scale=1">
234270
<title>{html.escape(title)}</title>
271+
<link rel="icon" type="image/svg+xml" href="{SITE_ICON_FILENAME}">
235272
<link rel="alternate" type="application/rss+xml" title="量化模型推荐 RSS" href="feed.xml">
236273
<style>
237274
:root {{ color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }}
238275
body {{ margin: 0; background: #f6f7f9; color: #1b1f24; }}
239276
main {{ max-width: 1080px; margin: 0 auto; padding: 32px 20px 56px; }}
240277
.hero {{ text-align: center; padding: 8px 0 22px; margin-bottom: 10px; }}
278+
.site-mark {{ display: inline-flex; width: 58px; height: 58px; margin: 0 auto 14px; filter: drop-shadow(0 12px 22px rgb(9 105 218 / 18%)); }}
279+
.site-mark svg {{ width: 100%; height: 100%; }}
280+
.site-mark rect {{ fill: #172033; }}
281+
.site-mark .mark-line {{ fill: none; stroke: #00a6c8; stroke-width: 5.5; stroke-linecap: round; stroke-linejoin: round; }}
282+
.site-mark circle {{ fill: #fffaf0; }}
283+
.site-mark .mark-ring {{ fill: none; stroke: #fffaf0; stroke-width: 3.2; stroke-linecap: round; opacity: .88; }}
241284
h1 {{ margin: 0; font-size: clamp(2rem, 5vw, 3.25rem); line-height: 1.15; letter-spacing: -0.04em; }}
242285
.warning {{ background: #fff1f2; border: 1px solid #fecdd3; padding: 14px 16px; margin-bottom: 20px; }}
243286
.final-decisions {{ margin-bottom: 20px; }}
@@ -287,6 +330,7 @@ def render_report_html(report: dict[str, Any]) -> str:
287330
<body>
288331
<main>
289332
<section class="hero">
333+
{render_site_mark()}
290334
<h1>{html.escape(title)}</h1>
291335
</section>
292336
{final_decisions_html}
@@ -372,6 +416,7 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
372416
<meta charset="utf-8">
373417
<meta name="viewport" content="width=device-width, initial-scale=1">
374418
<title>量化模型推荐</title>
419+
<link rel="icon" type="image/svg+xml" href="{SITE_ICON_FILENAME}">
375420
<link rel="alternate" type="application/rss+xml" title="量化模型推荐 RSS" href="feed.xml">
376421
<style>
377422
:root {{
@@ -410,6 +455,13 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
410455
}}
411456
main {{ max-width: 1180px; margin: 0 auto; padding: 34px 20px 64px; position: relative; }}
412457
.hero {{ display: grid; grid-template-columns: 1fr auto; gap: 24px; align-items: end; padding: 22px 0 28px; }}
458+
.brand-lockup {{ display: flex; align-items: center; gap: 16px; margin-bottom: 16px; }}
459+
.site-mark {{ flex: 0 0 auto; display: inline-flex; width: 64px; height: 64px; filter: drop-shadow(0 18px 28px rgba(30,77,216,.22)); }}
460+
.site-mark svg {{ width: 100%; height: 100%; }}
461+
.site-mark rect {{ fill: #172033; }}
462+
.site-mark .mark-line {{ fill: none; stroke: var(--cyan); stroke-width: 5.5; stroke-linecap: round; stroke-linejoin: round; }}
463+
.site-mark circle {{ fill: var(--paper); }}
464+
.site-mark .mark-ring {{ fill: none; stroke: var(--paper); stroke-width: 3.2; stroke-linecap: round; opacity: .88; }}
413465
.eyebrow {{ margin: 0 0 10px; color: var(--blue); font-weight: 800; letter-spacing: .16em; text-transform: uppercase; font-size: .78rem; }}
414466
h1 {{ margin: 0; max-width: 780px; font-family: "Iowan Old Style", Georgia, ui-serif, serif; font-size: clamp(2.55rem, 7vw, 5.7rem); line-height: .92; letter-spacing: -.07em; }}
415467
.hero p {{ margin: 18px 0 0; max-width: 680px; color: var(--muted); font-size: 1.06rem; line-height: 1.7; }}
@@ -461,8 +513,13 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
461513
<main>
462514
<section class="hero">
463515
<div>
464-
<p class="eyebrow">QuantStrategyLab</p>
465-
<h1>量化模型推荐</h1>
516+
<div class="brand-lockup">
517+
{render_site_mark()}
518+
<div>
519+
<p class="eyebrow">QuantStrategyLab</p>
520+
<h1>量化模型推荐</h1>
521+
</div>
522+
</div>
466523
<p>把主题动量、市场确认和政策/新闻证据合成为非个性化研究结论。页面只展示推荐、周期、背景、理由和风险。</p>
467524
</div>
468525
<aside class="rss-card">
@@ -517,6 +574,9 @@ def publish_reports(report_paths: list[str | Path], output_dir: str | Path, *, s
517574
path = output / report_filename(report)
518575
path.write_text(render_report_html(report), encoding="utf-8")
519576
written.append(path)
577+
icon_path = output / SITE_ICON_FILENAME
578+
icon_path.write_text(SITE_ICON_SVG, encoding="utf-8")
579+
written.append(icon_path)
520580
index_path = output / "index.html"
521581
index_path.write_text(render_index_html(reports), encoding="utf-8")
522582
written.append(index_path)

tests/test_publisher.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def test_render_report_html_is_direct_public_recommendation_page() -> None:
2525

2626
assert "量化模型推荐周度复盘 - 2026-05-30" in html
2727
assert "text-align: center" in html
28+
assert 'rel="icon" type="image/svg+xml" href="favicon.svg"' in html
29+
assert "site-mark" in html
2830
assert "2-12周" in html
2931
assert "来源模式:fixture" not in html
3032
assert "股票背景" in html
@@ -67,11 +69,17 @@ def test_publish_reports_writes_site_files(tmp_path: Path) -> None:
6769
filenames = {path.name for path in written}
6870
assert "index.html" in filenames
6971
assert "feed.xml" in filenames
72+
assert "favicon.svg" in filenames
7073
assert "2026-05-30-weekly-model-recommendations.html" in filenames
7174
index_html = (tmp_path / "site" / "index.html").read_text(encoding="utf-8")
75+
favicon = (tmp_path / "site" / "favicon.svg").read_text(encoding="utf-8")
7276
assert "来源:" not in index_html
7377
assert "主要信号" in index_html
7478
assert "最终推荐" not in index_html
79+
assert 'rel="icon" type="image/svg+xml" href="favicon.svg"' in index_html
80+
assert "site-mark" in index_html
81+
assert "<svg" in favicon
82+
assert "#172033" in favicon
7583
assert "Latest briefing" in index_html
7684
assert "latest-panel" in index_html
7785
assert "snapshot-grid" in index_html

0 commit comments

Comments
 (0)