Showing
1 changed file
with
50 additions
and
0 deletions
| @@ -59,6 +59,8 @@ class HTMLRenderer: | @@ -59,6 +59,8 @@ class HTMLRenderer: | ||
| 59 | self.heading_label_map: Dict[str, Dict[str, Any]] = {} | 59 | self.heading_label_map: Dict[str, Dict[str, Any]] = {} |
| 60 | self.primary_heading_index = 0 | 60 | self.primary_heading_index = 0 |
| 61 | self.secondary_heading_index = 0 | 61 | self.secondary_heading_index = 0 |
| 62 | + self.toc_rendered = False | ||
| 63 | + self.hero_kpi_signature: tuple | None = None | ||
| 62 | 64 | ||
| 63 | # ====== 公共入口 ====== | 65 | # ====== 公共入口 ====== |
| 64 | 66 | ||
| @@ -78,6 +80,7 @@ class HTMLRenderer: | @@ -78,6 +80,7 @@ class HTMLRenderer: | ||
| 78 | self.heading_counter = 0 | 80 | self.heading_counter = 0 |
| 79 | self.metadata = self.document.get("metadata", {}) or {} | 81 | self.metadata = self.document.get("metadata", {}) or {} |
| 80 | raw_chapters = self.document.get("chapters", []) or [] | 82 | raw_chapters = self.document.get("chapters", []) or [] |
| 83 | + self.toc_rendered = False | ||
| 81 | self.chapters = self._prepare_chapters(raw_chapters) | 84 | self.chapters = self._prepare_chapters(raw_chapters) |
| 82 | self.chapter_anchor_map = { | 85 | self.chapter_anchor_map = { |
| 83 | chapter.get("chapterId"): chapter.get("anchor") | 86 | chapter.get("chapterId"): chapter.get("anchor") |
| @@ -90,6 +93,8 @@ class HTMLRenderer: | @@ -90,6 +93,8 @@ class HTMLRenderer: | ||
| 90 | metadata = self.metadata | 93 | metadata = self.metadata |
| 91 | theme_tokens = metadata.get("themeTokens") or self.document.get("themeTokens", {}) | 94 | theme_tokens = metadata.get("themeTokens") or self.document.get("themeTokens", {}) |
| 92 | title = metadata.get("title") or metadata.get("query") or "智能舆情报告" | 95 | title = metadata.get("title") or metadata.get("query") or "智能舆情报告" |
| 96 | + hero_kpis = (metadata.get("hero") or {}).get("kpis") | ||
| 97 | + self.hero_kpi_signature = self._kpi_signature_from_items(hero_kpis) | ||
| 93 | 98 | ||
| 94 | head = self._render_head(title, theme_tokens) | 99 | head = self._render_head(title, theme_tokens) |
| 95 | body = self._render_body() | 100 | body = self._render_body() |
| @@ -320,12 +325,15 @@ class HTMLRenderer: | @@ -320,12 +325,15 @@ class HTMLRenderer: | ||
| 320 | """ | 325 | """ |
| 321 | if not self.toc_entries: | 326 | if not self.toc_entries: |
| 322 | return "" | 327 | return "" |
| 328 | + if self.toc_rendered: | ||
| 329 | + return "" | ||
| 323 | toc_config = self.metadata.get("toc") or {} | 330 | toc_config = self.metadata.get("toc") or {} |
| 324 | toc_title = toc_config.get("title") or "📚 目录" | 331 | toc_title = toc_config.get("title") or "📚 目录" |
| 325 | toc_items = "".join( | 332 | toc_items = "".join( |
| 326 | self._format_toc_entry(entry) | 333 | self._format_toc_entry(entry) |
| 327 | for entry in self.toc_entries | 334 | for entry in self.toc_entries |
| 328 | ) | 335 | ) |
| 336 | + self.toc_rendered = True | ||
| 329 | return f""" | 337 | return f""" |
| 330 | <nav class="toc"> | 338 | <nav class="toc"> |
| 331 | <div class="toc-title">{self._escape_html(toc_title)}</div> | 339 | <div class="toc-title">{self._escape_html(toc_title)}</div> |
| @@ -965,6 +973,8 @@ class HTMLRenderer: | @@ -965,6 +973,8 @@ class HTMLRenderer: | ||
| 965 | 973 | ||
| 966 | def _render_kpi_grid(self, block: Dict[str, Any]) -> str: | 974 | def _render_kpi_grid(self, block: Dict[str, Any]) -> str: |
| 967 | """渲染KPI卡片栅格,包含指标值与涨跌幅""" | 975 | """渲染KPI卡片栅格,包含指标值与涨跌幅""" |
| 976 | + if self._should_skip_overview_kpi(block): | ||
| 977 | + return "" | ||
| 968 | cards = "" | 978 | cards = "" |
| 969 | for item in block.get("items", []): | 979 | for item in block.get("items", []): |
| 970 | delta = item.get("delta") | 980 | delta = item.get("delta") |
| @@ -1051,6 +1061,46 @@ class HTMLRenderer: | @@ -1051,6 +1061,46 @@ class HTMLRenderer: | ||
| 1051 | """ | 1061 | """ |
| 1052 | return table_html | 1062 | return table_html |
| 1053 | 1063 | ||
| 1064 | + # ====== Front-matter guards ====== | ||
| 1065 | + | ||
| 1066 | + def _kpi_signature_from_items(self, items: Any) -> tuple | None: | ||
| 1067 | + """将KPI数组转换为可比较的签名""" | ||
| 1068 | + if not isinstance(items, list): | ||
| 1069 | + return None | ||
| 1070 | + normalized = [] | ||
| 1071 | + for raw in items: | ||
| 1072 | + normalized_item = self._normalize_kpi_item(raw) | ||
| 1073 | + if normalized_item: | ||
| 1074 | + normalized.append(normalized_item) | ||
| 1075 | + return tuple(normalized) if normalized else None | ||
| 1076 | + | ||
| 1077 | + def _normalize_kpi_item(self, item: Any) -> tuple[str, str, str, str, str] | None: | ||
| 1078 | + if not isinstance(item, dict): | ||
| 1079 | + return None | ||
| 1080 | + | ||
| 1081 | + def normalize(value: Any) -> str: | ||
| 1082 | + if value is None: | ||
| 1083 | + return "" | ||
| 1084 | + if isinstance(value, (int, float)): | ||
| 1085 | + return str(value) | ||
| 1086 | + return str(value).strip() | ||
| 1087 | + | ||
| 1088 | + label = normalize(item.get("label")) | ||
| 1089 | + value = normalize(item.get("value")) | ||
| 1090 | + unit = normalize(item.get("unit")) | ||
| 1091 | + delta = normalize(item.get("delta")) | ||
| 1092 | + tone = normalize(item.get("deltaTone") or item.get("tone")) | ||
| 1093 | + return label, value, unit, delta, tone | ||
| 1094 | + | ||
| 1095 | + def _should_skip_overview_kpi(self, block: Dict[str, Any]) -> bool: | ||
| 1096 | + """若KPI内容与封面一致,则判定为重复总览""" | ||
| 1097 | + if not self.hero_kpi_signature: | ||
| 1098 | + return False | ||
| 1099 | + block_signature = self._kpi_signature_from_items(block.get("items")) | ||
| 1100 | + if not block_signature: | ||
| 1101 | + return False | ||
| 1102 | + return block_signature == self.hero_kpi_signature | ||
| 1103 | + | ||
| 1054 | # ====== Inline 渲染 ====== | 1104 | # ====== Inline 渲染 ====== |
| 1055 | 1105 | ||
| 1056 | def _normalize_inline_payload(self, run: Dict[str, Any]) -> tuple[str, List[Dict[str, Any]]]: | 1106 | def _normalize_inline_payload(self, run: Dict[str, Any]) -> tuple[str, List[Dict[str, Any]]]: |
-
Please register or login to post a comment