✨ feat!(ui): 重构图表组件架构,实现数据与样式分离 (#2035)

* ✨ feat!(ui): 重构图表组件架构,实现数据与样式分离

🏗️ **架构重构**
- 移除charts.py中所有硬编码样式参数(grid、tooltip、legend等)
- 将样式配置迁移至主题层style.json文件
- 统一图表模板消费样式文件的能力

📊 **图表组件优化**
- bar_chart: 移除grid和坐标轴show参数
- pie_chart: 移除tooltip、legend样式和series视觉参数
- line_chart: 移除tooltip、grid和坐标轴配置
- radar_chart: 移除tooltip硬编码

🎨 **主题系统增强**
- 新增pie_chart、line_chart、radar_chart的style.json配置
- 更新bar_chart/style.json,添加grid、xAxis、yAxis样式
- 所有图表模板支持deepMerge样式合并逻辑

🔧 **Breaking Changes**
- 图表工厂函数不再接受样式参数
- 主题开发者现可通过style.json完全定制图表外观
- 提升组件可维护性和主题灵活性

* 📦️ build(pyinstaller): 引入 resources.spec 并更新 .gitignore 规则

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio
2025-08-28 09:20:15 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent d9e65057cf
commit 7472cabd48
61 changed files with 2272 additions and 648 deletions
+47 -12
View File
@@ -1,16 +1,18 @@
from abc import ABC, abstractmethod
from collections.abc import Iterable
from pathlib import Path
from typing import Literal
from typing import Any, Literal
import aiofiles
from pydantic import BaseModel, Field
from zhenxun.services.log import logger
from .base import RenderableComponent
from .base import ContainerComponent, RenderableComponent
__all__ = [
"CodeElement",
"ComponentElement",
"HeadingElement",
"ImageElement",
"ListElement",
@@ -32,6 +34,7 @@ class MarkdownElement(BaseModel, ABC):
class TextElement(MarkdownElement):
type: Literal["text"] = "text"
text: str
def to_markdown(self) -> str:
@@ -39,6 +42,7 @@ class TextElement(MarkdownElement):
class HeadingElement(MarkdownElement):
type: Literal["heading"] = "heading"
text: str
level: int = Field(..., ge=1, le=6)
@@ -47,6 +51,7 @@ class HeadingElement(MarkdownElement):
class ImageElement(MarkdownElement):
type: Literal["image"] = "image"
src: str
alt: str = "image"
@@ -55,6 +60,7 @@ class ImageElement(MarkdownElement):
class CodeElement(MarkdownElement):
type: Literal["code"] = "code"
code: str
language: str = ""
@@ -63,6 +69,7 @@ class CodeElement(MarkdownElement):
class RawHtmlElement(MarkdownElement):
type: Literal["raw_html"] = "raw_html"
html: str
def to_markdown(self) -> str:
@@ -70,6 +77,7 @@ class RawHtmlElement(MarkdownElement):
class TableElement(MarkdownElement):
type: Literal["table"] = "table"
headers: list[str]
rows: list[list[str]]
alignments: list[Literal["left", "center", "right"]] | None = None
@@ -98,6 +106,8 @@ class ContainerElement(MarkdownElement):
class QuoteElement(ContainerElement):
type: Literal["quote"] = "quote"
def to_markdown(self) -> str:
inner_md = "\n".join(part.to_markdown() for part in self.content)
return "\n".join([f"> {line}" for line in inner_md.split("\n")])
@@ -109,6 +119,7 @@ class ListItemElement(ContainerElement):
class ListElement(ContainerElement):
type: Literal["list"] = "list"
ordered: bool = False
def to_markdown(self) -> str:
@@ -121,11 +132,21 @@ class ListElement(ContainerElement):
return "\n".join(lines)
class MarkdownData(RenderableComponent):
class ComponentElement(MarkdownElement):
"""一个特殊的元素,用于在Markdown流中持有另一个可渲染组件。"""
type: Literal["component"] = "component"
component: RenderableComponent
def to_markdown(self) -> str:
return ""
class MarkdownData(ContainerComponent):
"""Markdown转图片的数据模型"""
style_name: str | None = None
markdown: str
elements: list[MarkdownElement] = Field(default_factory=list)
width: int = 800
css_path: str | None = None
@@ -133,7 +154,23 @@ class MarkdownData(RenderableComponent):
def template_name(self) -> str:
return "components/core/markdown"
async def get_extra_css(self, theme_manager) -> str:
def get_children(self) -> Iterable[RenderableComponent]:
"""让CSS/JS依赖收集器能够递归地找到所有嵌入的组件。"""
def find_components_recursive(
elements: list[MarkdownElement],
) -> Iterable[RenderableComponent]:
for element in elements:
if isinstance(element, ComponentElement):
yield element.component
if hasattr(element.component, "get_children"):
yield from element.component.get_children()
elif isinstance(element, ContainerElement):
yield from find_components_recursive(element.content)
yield from find_components_recursive(self.elements)
async def get_extra_css(self, context: Any) -> str:
if self.css_path:
css_file = Path(self.css_path)
if css_file.is_file():
@@ -142,14 +179,12 @@ class MarkdownData(RenderableComponent):
else:
logger.warning(f"Markdown自定义CSS文件不存在: {self.css_path}")
else:
style_name = self.style_name or "github-light"
css_path = (
theme_manager.current_theme.default_assets_dir
/ "css"
/ "markdown"
/ f"{style_name}.css"
style_name = self.style_name or "light"
# 使用上下文对象来解析路径
css_path = await context.theme_manager.resolve_markdown_style_path(
style_name, context
)
if css_path.exists():
if css_path and css_path.exists():
async with aiofiles.open(css_path, encoding="utf-8") as f:
return await f.read()
return ""