From 904df342942336205f4c108d487c59533d565d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E4=B8=80=E4=B8=81?= <1769123563@qq.com> Date: Sat, 15 Nov 2025 18:01:55 +0800 Subject: [PATCH] Optimize the Rendering Process --- ReportEngine/renderers/html_renderer.py | 83 +++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/ReportEngine/renderers/html_renderer.py b/ReportEngine/renderers/html_renderer.py index a2f7513..3b247ca 100644 --- a/ReportEngine/renderers/html_renderer.py +++ b/ReportEngine/renderers/html_renderer.py @@ -1028,6 +1028,75 @@ class HTMLRenderer: """ return f'
{cards}
' + def _merge_dicts( + self, base: Dict[str, Any] | None, override: Dict[str, Any] | None + ) -> Dict[str, Any]: + """ + 递归合并两个字典,override覆盖base,均为新副本,避免副作用。 + """ + result = copy.deepcopy(base) if isinstance(base, dict) else {} + if not isinstance(override, dict): + return result + for key, value in override.items(): + if isinstance(value, dict) and isinstance(result.get(key), dict): + result[key] = self._merge_dicts(result[key], value) + else: + result[key] = copy.deepcopy(value) + return result + + def _looks_like_chart_dataset(self, candidate: Any) -> bool: + """启发式判断对象是否包含Chart.js常见的labels/datasets结构""" + if not isinstance(candidate, dict): + return False + labels = candidate.get("labels") + datasets = candidate.get("datasets") + return isinstance(labels, list) or isinstance(datasets, list) + + def _coerce_chart_data_structure(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + 兼容LLM输出的Chart.js完整配置(含type/data/options)。 + 若data中嵌套一个真正的labels/datasets结构,则提取并返回该结构。 + """ + if not isinstance(data, dict): + return {} + if self._looks_like_chart_dataset(data): + return data + for key in ("data", "chartData", "payload"): + nested = data.get(key) + if self._looks_like_chart_dataset(nested): + return copy.deepcopy(nested) + return data + + def _prepare_widget_payload( + self, block: Dict[str, Any] + ) -> tuple[Dict[str, Any], Dict[str, Any]]: + """ + 预处理widget数据,兼容部分block将Chart.js配置写入data字段的情况。 + + 返回: + tuple(props, data): 归一化后的props与chart数据 + """ + props = copy.deepcopy(block.get("props") or {}) + raw_data = block.get("data") + data_copy = copy.deepcopy(raw_data) if isinstance(raw_data, dict) else raw_data + widget_type = block.get("widgetType") or "" + chart_like = isinstance(widget_type, str) and widget_type.startswith("chart.js") + + if chart_like and isinstance(data_copy, dict): + inline_options = data_copy.pop("options", None) + inline_type = data_copy.pop("type", None) + normalized_data = self._coerce_chart_data_structure(data_copy) + if isinstance(inline_options, dict): + props["options"] = self._merge_dicts(props.get("options"), inline_options) + if isinstance(inline_type, str) and inline_type and not props.get("type"): + props["type"] = inline_type + elif isinstance(data_copy, dict): + normalized_data = data_copy + else: + normalized_data = {} + + return props, normalized_data + def _render_widget(self, block: Dict[str, Any]) -> str: """ 渲染Chart.js等交互组件的占位容器,并记录配置JSON。 @@ -1042,11 +1111,12 @@ class HTMLRenderer: canvas_id = f"chart-{self.chart_counter}" config_id = f"chart-config-{self.chart_counter}" + props, normalized_data = self._prepare_widget_payload(block) payload = { "widgetId": block.get("widgetId"), "widgetType": block.get("widgetType"), - "props": block.get("props", {}), - "data": block.get("data", {}), + "props": props, + "data": normalized_data, "dataRef": block.get("dataRef"), } config_json = json.dumps(payload, ensure_ascii=False).replace("{config_json}' ) - title = block.get("props", {}).get("title") + title = props.get("title") title_html = f'
{self._escape_html(title)}
' if title else "" - fallback_html = self._render_widget_fallback(block) + fallback_html = self._render_widget_fallback(normalized_data) return f"""
{title_html} @@ -1067,9 +1137,10 @@ class HTMLRenderer:
""" - def _render_widget_fallback(self, block: Dict[str, Any]) -> str: + def _render_widget_fallback(self, data: Dict[str, Any]) -> str: """渲染图表数据的文本兜底视图,避免Chart.js加载失败时出现空白""" - data = block.get("data") or {} + if not isinstance(data, dict): + return "" labels = data.get("labels") or [] datasets = data.get("datasets") or [] if not labels or not datasets: