2424 ("medium" , "中线" , "2-12周" ),
2525 ("short" , "短线" , "1-10个交易日" ),
2626)
27+ INDEX_HISTORY_LIMIT = 12
28+ RSS_ITEM_LIMIT = 20
2729
2830SITE_ICON_FILENAME = "favicon.svg"
2931SITE_ICON_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
@@ -562,6 +564,7 @@ def render_report_html(report: dict[str, Any]) -> str:
562564 </a>
563565 <div class="topbar-actions">
564566 <a href="index.html">返回首页</a>
567+ <a href="archive.html">历史归档</a>
565568 <a href="feed.xml">RSS 订阅</a>
566569 </div>
567570 </nav>
@@ -661,7 +664,8 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
661664 </section>
662665 """
663666 items = []
664- for report in sorted_reports :
667+ recent_reports = sorted_reports [1 : INDEX_HISTORY_LIMIT + 1 ]
668+ for report in recent_reports :
665669 filename = report_filename (report )
666670 top_themes = format_theme_ids (report ["summary" ].get ("top_theme_ids" , []))
667671 top_symbols = [str (symbol ) for symbol in report ["summary" ].get ("top_recommended_symbols" , [])]
@@ -675,7 +679,7 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
675679 </article>
676680 """
677681 )
678- archive = "" .join (items ) or '<p class="empty-archive">暂无报告 。</p>'
682+ archive = "" .join (items ) or '<p class="empty-archive">暂无更多历史报告 。</p>'
679683 return f"""<!doctype html>
680684<html lang="zh-CN">
681685<head>
@@ -734,7 +738,8 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
734738 h1 {{ margin: 0; max-width: 780px; font-family: var(--font-display); font-size: clamp(2.15rem, 5vw, 4.05rem); font-weight: 700; line-height: 1.06; letter-spacing: -.045em; }}
735739 .hero p {{ margin: 16px 0 0; max-width: 680px; color: var(--muted); font-size: 1rem; line-height: 1.7; }}
736740 .rss-card {{ justify-self: end; min-width: 170px; padding: 14px 16px; border: 1px solid var(--line); border-radius: 18px; background: rgba(255,255,255,.66); box-shadow: 0 18px 45px rgba(31,45,61,.08); }}
737- .rss-card a {{ color: var(--ink); text-decoration: none; font-weight: 800; }}
741+ .rss-card a {{ display: block; color: var(--ink); text-decoration: none; font-weight: 800; }}
742+ .rss-card a + a {{ margin-top: 8px; }}
738743 .rss-card span {{ display: block; color: var(--muted); margin-top: 5px; font-size: .88rem; }}
739744 .latest-panel {{ position: relative; overflow: hidden; display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(360px, .95fr); gap: 22px; border: 1px solid rgba(30,77,216,.16); border-radius: 30px; padding: 26px; background: linear-gradient(135deg, rgba(255,255,255,.9), rgba(255,250,240,.76)); box-shadow: 0 26px 70px rgba(31,45,61,.13); }}
740745 .latest-panel::after {{ content: ""; position: absolute; right: -80px; top: -110px; width: 260px; height: 260px; border-radius: 999px; background: rgba(0,166,200,.16); filter: blur(2px); }}
@@ -760,6 +765,7 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
760765 .section-title {{ display: flex; align-items: end; justify-content: space-between; gap: 16px; margin: 34px 0 14px; }}
761766 .section-title h2 {{ font-size: 1.28rem; }}
762767 .section-title p {{ margin: 0; color: var(--muted); }}
768+ .archive-action {{ color: var(--ink); font-weight: 850; text-decoration: none; white-space: nowrap; }}
763769 .archive-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }}
764770 .archive-card {{ border: 1px solid var(--line); border-radius: 24px; padding: 18px; background: var(--panel); box-shadow: 0 12px 34px rgba(31,45,61,.08); }}
765771 .archive-title {{ color: var(--ink); font-size: 1rem; font-weight: 850; text-decoration: none; }}
@@ -791,15 +797,19 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
791797 <p>把主题动量、市场确认和政策/新闻证据整理成普通投资者能读懂的研究结论。页面只展示推荐、周期、背景、理由和风险。</p>
792798 </div>
793799 <aside class="rss-card">
800+ <a href="archive.html">历史归档</a>
794801 <a href="feed.xml">RSS 订阅</a>
795802 <span>周度更新 · 静态页面</span>
796803 </aside>
797804 </section>
798805 { latest_block }
799806 <section class="archive">
800807 <div class="section-title">
801- <h2>历史报告</h2>
802- <p>按发布日期倒序</p>
808+ <div>
809+ <h2>近期历史报告</h2>
810+ <p>首页最多显示最近 { INDEX_HISTORY_LIMIT } 期,完整记录保留在归档页</p>
811+ </div>
812+ <a class="archive-action" href="archive.html">查看全部</a>
803813 </div>
804814 <div class="archive-grid">{ archive } </div>
805815 </section>
@@ -809,12 +819,161 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
809819"""
810820
811821
822+ def render_archive_card (report : dict [str , Any ]) -> str :
823+ filename = report_filename (report )
824+ top_themes = format_theme_ids (report ["summary" ].get ("top_theme_ids" , []))
825+ top_symbols = [str (symbol ) for symbol in report ["summary" ].get ("top_recommended_symbols" , [])]
826+ return f"""
827+ <article class="archive-card">
828+ <a class="archive-title" href="{ html .escape (filename )} ">{ html .escape (report ['as_of' ])} { html .escape (cadence_label (report ))} 复盘</a>
829+ <p>主要信号:{ html .escape (top_themes or '无' )} </p>
830+ <div class="archive-symbols">{ render_symbol_tags (top_symbols )} </div>
831+ { render_horizon_snapshot (report , linked = True )}
832+ </article>
833+ """
834+
835+
836+ def render_archive_html (reports : list [dict [str , Any ]]) -> str :
837+ sorted_reports = sorted (reports , key = lambda item : item ["as_of" ], reverse = True )
838+ groups : dict [str , list [dict [str , Any ]]] = {}
839+ for report in sorted_reports :
840+ key = str (report .get ("as_of" , "" ))[:7 ] or "unknown"
841+ groups .setdefault (key , []).append (report )
842+
843+ sections = []
844+ for key , group_reports in groups .items ():
845+ year , _ , month = key .partition ("-" )
846+ title = f"{ year } 年 { month } 月" if month else key
847+ cards = "" .join (render_archive_card (report ) for report in group_reports )
848+ sections .append (
849+ f"""
850+ <section class="month-group">
851+ <h2>{ html .escape (title )} </h2>
852+ <div class="archive-grid">{ cards } </div>
853+ </section>
854+ """
855+ )
856+ archive = "" .join (sections ) or '<p class="empty-archive">暂无报告。</p>'
857+ return f"""<!doctype html>
858+ <html lang="zh-CN">
859+ <head>
860+ <meta charset="utf-8">
861+ <meta name="viewport" content="width=device-width, initial-scale=1">
862+ <title>历史归档 - 智慧投顾研究系统</title>
863+ <link rel="icon" type="image/svg+xml" href="{ SITE_ICON_FILENAME } ">
864+ <link rel="alternate" type="application/rss+xml" title="智慧投顾研究 RSS" href="feed.xml">
865+ <style>
866+ :root {{
867+ color-scheme: light;
868+ --ink: #172033;
869+ --muted: #667085;
870+ --line: #d9e2ef;
871+ --paper: #fffaf0;
872+ --panel: rgba(255, 255, 255, .78);
873+ --blue: #1e4dd8;
874+ --cyan: #00a6c8;
875+ --gold: #d99b2b;
876+ --green: #0f8b62;
877+ --font-sans: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", ui-sans-serif, system-ui, sans-serif;
878+ --font-display: "Songti SC", "Noto Serif CJK SC", "Source Han Serif SC", "STSong", "SimSun", ui-serif, serif;
879+ font-family: var(--font-sans);
880+ }}
881+ * {{ box-sizing: border-box; }}
882+ body {{
883+ margin: 0;
884+ color: var(--ink);
885+ background:
886+ radial-gradient(circle at 12% 8%, rgba(0,166,200,.18), transparent 28rem),
887+ radial-gradient(circle at 88% 0%, rgba(217,155,43,.18), transparent 24rem),
888+ linear-gradient(135deg, #f8fafc 0%, #eef4fb 52%, #fff7e6 100%);
889+ min-height: 100vh;
890+ }}
891+ main {{ max-width: 1180px; margin: 0 auto; padding: 34px 20px 64px; }}
892+ .topbar {{ display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-bottom: 28px; }}
893+ .brand-link {{ display: inline-flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none; font-size: .98rem; font-weight: 800; }}
894+ .brand-link .site-mark {{ width: 38px; height: 38px; margin: 0; }}
895+ .topbar-actions {{ display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end; }}
896+ .topbar-actions a {{ min-height: 38px; display: inline-flex; align-items: center; padding: 0 14px; border: 1px solid var(--line); border-radius: 999px; background: rgba(255,255,255,.72); color: var(--ink); text-decoration: none; font-size: .92rem; font-weight: 750; }}
897+ .site-mark {{ display: inline-flex; width: 64px; height: 64px; filter: drop-shadow(0 18px 28px rgba(30,77,216,.22)); }}
898+ .site-mark svg {{ width: 100%; height: 100%; }}
899+ .site-mark rect {{ fill: #172033; }}
900+ .site-mark .mark-line {{ fill: none; stroke: var(--cyan); stroke-width: 5.5; stroke-linecap: round; stroke-linejoin: round; }}
901+ .site-mark circle {{ fill: var(--paper); }}
902+ .site-mark .mark-ring {{ fill: none; stroke: var(--paper); stroke-width: 3.2; stroke-linecap: round; opacity: .88; }}
903+ .hero {{ padding: 22px 0 30px; }}
904+ .eyebrow {{ margin: 0 0 10px; color: var(--blue); font-size: .72rem; font-weight: 850; letter-spacing: .18em; text-transform: uppercase; }}
905+ h1 {{ margin: 0; font-family: var(--font-display); font-size: clamp(2.1rem, 4.4vw, 3.7rem); font-weight: 700; line-height: 1.08; letter-spacing: -.045em; }}
906+ .hero p {{ margin: 14px 0 0; max-width: 720px; color: var(--muted); line-height: 1.7; }}
907+ .month-group {{ margin-top: 30px; }}
908+ .month-group h2 {{ margin: 0 0 14px; font-size: 1.28rem; font-weight: 850; }}
909+ .archive-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }}
910+ .archive-card {{ border: 1px solid var(--line); border-radius: 24px; padding: 18px; background: var(--panel); box-shadow: 0 12px 34px rgba(31,45,61,.08); }}
911+ .archive-title {{ color: var(--ink); font-size: 1rem; font-weight: 850; text-decoration: none; }}
912+ .archive-card p {{ margin: 10px 0; color: var(--muted); line-height: 1.55; }}
913+ .archive-symbols {{ margin: 0 0 14px; }}
914+ .symbol-strip {{ display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }}
915+ .symbol-tag {{ display: inline-flex; align-items: center; min-height: 30px; padding: 6px 10px; border-radius: 999px; border: 1px solid rgba(30,77,216,.22); background: #fff; color: var(--ink); font-size: .94rem; font-weight: 800; box-shadow: 0 6px 16px rgba(31,45,61,.06); }}
916+ .symbol-tag.empty {{ color: var(--muted); font-weight: 700; }}
917+ .snapshot-grid {{ display: grid; grid-template-columns: 1fr; gap: 8px; }}
918+ .snapshot-column {{ padding: 12px; border: 1px solid var(--line); border-radius: 16px; background: rgba(255,255,255,.72); }}
919+ .snapshot-label {{ margin: 0; font-size: 1rem; font-weight: 850; }}
920+ .snapshot-window {{ margin: 4px 0 8px; color: var(--muted); font-size: .88rem; }}
921+ .snapshot-long {{ border-top: 4px solid var(--green); }}
922+ .snapshot-medium {{ border-top: 4px solid var(--blue); }}
923+ .snapshot-short {{ border-top: 4px solid var(--gold); }}
924+ .horizon-link {{ color: inherit; text-decoration: none; }}
925+ .empty-archive {{ color: var(--muted); }}
926+ @media (max-width: 720px) {{
927+ .topbar {{ align-items: flex-start; flex-direction: column; }}
928+ .topbar-actions {{ justify-content: flex-start; }}
929+ }}
930+ </style>
931+ </head>
932+ <body>
933+ <main>
934+ <nav class="topbar" aria-label="站点导航">
935+ <a class="brand-link" href="index.html">
936+ { render_site_mark ()}
937+ <span>QuantStrategyLab</span>
938+ </a>
939+ <div class="topbar-actions">
940+ <a href="index.html">返回首页</a>
941+ <a href="feed.xml">RSS 订阅</a>
942+ </div>
943+ </nav>
944+ <section class="hero">
945+ <p class="eyebrow">Archive</p>
946+ <h1>历史归档</h1>
947+ <p>报告文件长期保留,便于复盘系统结论、观察风格漂移和检查不同阶段的主题变化。首页只展示最新和近期记录,这里按月份列出全部报告。</p>
948+ </section>
949+ { archive }
950+ </main>
951+ </body>
952+ </html>
953+ """
954+
955+
956+ def render_reports_index_json (reports : list [dict [str , Any ]]) -> str :
957+ items = []
958+ for report in sorted (reports , key = lambda item : item ["as_of" ], reverse = True ):
959+ as_of = str (report .get ("as_of" , "" ))
960+ items .append (
961+ {
962+ "as_of" : as_of ,
963+ "cadence" : str (report .get ("cadence" , "" )),
964+ "html" : report_filename (report ),
965+ "json" : f"advisory_report_{ as_of } .json" ,
966+ }
967+ )
968+ return json .dumps ({"schema_version" : 1 , "reports" : items }, ensure_ascii = False , indent = 2 ) + "\n "
969+
970+
812971def render_feed_xml (reports : list [dict [str , Any ]], * , site_url : str , feed_title : str ) -> str :
813972 channel = ET .Element ("channel" )
814973 ET .SubElement (channel , "title" ).text = feed_title
815974 ET .SubElement (channel , "link" ).text = site_url
816975 ET .SubElement (channel , "description" ).text = "QuantStrategyLab 智慧投顾研究系统,包含推荐理由、周期和风险提示。"
817- for report in sorted (reports , key = lambda item : item ["as_of" ], reverse = True ):
976+ for report in sorted (reports , key = lambda item : item ["as_of" ], reverse = True )[: RSS_ITEM_LIMIT ] :
818977 filename = report_filename (report )
819978 link = f"{ site_url .rstrip ('/' )} /{ quote (filename )} "
820979 item = ET .SubElement (channel , "item" )
@@ -848,6 +1007,12 @@ def publish_reports(report_paths: list[str | Path], output_dir: str | Path, *, s
8481007 index_path = output / "index.html"
8491008 index_path .write_text (render_index_html (reports ), encoding = "utf-8" )
8501009 written .append (index_path )
1010+ archive_path = output / "archive.html"
1011+ archive_path .write_text (render_archive_html (reports ), encoding = "utf-8" )
1012+ written .append (archive_path )
1013+ reports_index_path = output / "reports_index.json"
1014+ reports_index_path .write_text (render_reports_index_json (reports ), encoding = "utf-8" )
1015+ written .append (reports_index_path )
8511016 feed_path = output / "feed.xml"
8521017 feed_path .write_text (render_feed_xml (reports , site_url = site_url , feed_title = feed_title ), encoding = "utf-8" )
8531018 written .append (feed_path )
0 commit comments