✨ 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
+8
View File
@@ -3,14 +3,22 @@
包含基础的UI构建器类
"""
from .card import CardBuilder
from .details import DetailsBuilder
from .layout import LayoutBuilder
from .list import ListBuilder
from .markdown import MarkdownBuilder
from .notebook import NotebookBuilder
from .table import TableBuilder
from .text import TextBuilder
__all__ = [
"CardBuilder",
"DetailsBuilder",
"LayoutBuilder",
"ListBuilder",
"MarkdownBuilder",
"NotebookBuilder",
"TableBuilder",
"TextBuilder",
]
+26
View File
@@ -0,0 +1,26 @@
from typing_extensions import Self
from ...models.core.base import RenderableComponent
from ...models.core.card import CardData
from ..base import BaseBuilder
class CardBuilder(BaseBuilder[CardData]):
"""链式构建通用卡片容器的辅助类"""
def __init__(self, content: "RenderableComponent | BaseBuilder"):
content_model = content.build() if isinstance(content, BaseBuilder) else content
data_model = CardData(content=content_model)
super().__init__(data_model, template_name="components/core/card")
def set_header(self, header: "RenderableComponent | BaseBuilder") -> Self:
"""设置卡片的头部组件"""
header_model = header.build() if isinstance(header, BaseBuilder) else header
self._data.header = header_model
return self
def set_footer(self, footer: "RenderableComponent | BaseBuilder") -> Self:
"""设置卡片的尾部组件"""
footer_model = footer.build() if isinstance(footer, BaseBuilder) else footer
self._data.footer = footer_model
return self
+19
View File
@@ -0,0 +1,19 @@
from typing import Any
from typing_extensions import Self
from ...models.core.details import DetailsData, DetailsItem
from ..base import BaseBuilder
class DetailsBuilder(BaseBuilder[DetailsData]):
"""链式构建描述列表(键值对)的辅助类"""
def __init__(self, title: str | None = None):
data_model = DetailsData(title=title, items=[])
super().__init__(data_model, template_name="components/core/details")
def add_item(self, label: str, value: Any) -> Self:
"""向列表中添加一个键值对项目"""
value_str = str(value)
self._data.items.append(DetailsItem(label=label, value=value_str))
return self
+34 -22
View File
@@ -19,16 +19,32 @@ class LayoutBuilder(BaseBuilder[LayoutData]):
self._options: dict[str, Any] = {}
@classmethod
def column(cls, **options: Any) -> Self:
def column(
cls, *, gap: str = "20px", align_items: str = "stretch", **options: Any
) -> Self:
builder = cls()
builder._template_name = "layouts/column"
builder._template_name = "components/core/layouts/column"
builder._options["gap"] = gap
builder._options["align_items"] = align_items
builder._options.update(options)
return builder
@classmethod
def row(cls, **options: Any) -> Self:
def row(
cls, *, gap: str = "10px", align_items: str = "center", **options: Any
) -> Self:
builder = cls()
builder._template_name = "layouts/row"
builder._template_name = "components/core/layouts/row"
builder._options["gap"] = gap
builder._options["align_items"] = align_items
builder._options.update(options)
return builder
@classmethod
def grid(cls, columns: int = 2, **options: Any) -> Self:
builder = cls()
builder._template_name = "components/core/layouts/grid"
builder._options["columns"] = columns
builder._options.update(options)
return builder
@@ -56,15 +72,15 @@ class LayoutBuilder(BaseBuilder[LayoutData]):
metadata: dict[str, Any] | None = None,
) -> Self:
"""
向布局中添加一个组件,支持多种组件类型的添加。
向布局中添加一个组件项。
参数:
component: 一个 Builder 实例 (如 TableBuilder) 或一个 RenderableComponent
数据模型。
metadata: (可选) 与此项目关联的元数据,可用于模板。
component: 一个 `BaseBuilder` 实例 (如 `TableBuilder()`) 或一个已构建的
`RenderableComponent` 数据模型。
metadata: (可选) 与此项目关联的元数据,可在布局模板中访问。
返回:
Self: 返回当前布局构建器实例,支持链式调用。
Self: 当前构建器实例,以支持链式调用。
"""
component_data = (
component.data if isinstance(component, BaseBuilder) else component
@@ -76,28 +92,24 @@ class LayoutBuilder(BaseBuilder[LayoutData]):
def add_option(self, key: str, value: Any) -> Self:
"""
为布局添加一个自定义选项,该选项会传递给模板。
为布局模板添加一个自定义选项。
例如,`add_option("padding", "30px")` 会在模板的 `data.options`
字典中添加 `{"padding": "30px"}`。
参数:
key: 选项的键名,用于在模板中引用。
value: 选项的值,可以是任意类型的数据。
key: 选项的键名。
value: 选项的值。
返回:
Self: 返回当前布局构建器实例,支持链式调用。
Self: 当前构建器实例,以支持链式调用。
"""
self._options[key] = value
return self
def build(self) -> LayoutData:
"""
[修改] 构建并返回 LayoutData 模型实例。
此方法现在是同步的,并且不执行渲染。
参数:
无
返回:
LayoutData: 配置好的布局数据模型。
构建并返回 LayoutData 模型实例。
"""
if not self._template_name:
raise ValueError(
@@ -106,4 +118,4 @@ class LayoutBuilder(BaseBuilder[LayoutData]):
self._data.options = self._options
self._data.layout_type = self._template_name.split("/")[-1]
return self._data
return super().build()
+31
View File
@@ -0,0 +1,31 @@
from typing_extensions import Self
from ...models.core.base import RenderableComponent
from ...models.core.list import ListData, ListItem
from ..base import BaseBuilder
class ListBuilder(BaseBuilder[ListData]):
"""链式构建通用列表的辅助类。"""
def __init__(self, ordered: bool = False):
data_model = ListData(ordered=ordered)
super().__init__(data_model, template_name="components/core/list")
def add_item(self, component: "BaseBuilder | RenderableComponent") -> Self:
"""
向列表中添加一个项目。
参数:
component: 一个 Builder 实例或一个 RenderableComponent 数据模型。
"""
component_data = (
component.build() if isinstance(component, BaseBuilder) else component
)
self._data.items.append(ListItem(component=component_data))
return self
def ordered(self, is_ordered: bool = True) -> Self:
"""设置列表是否为有序列表(带数字编号)。"""
self._data.ordered = is_ordered
return self
+14 -3
View File
@@ -4,6 +4,7 @@ from typing import Any
from ...models.core.markdown import (
CodeElement,
ComponentElement,
HeadingElement,
ImageElement,
ListElement,
@@ -12,6 +13,7 @@ from ...models.core.markdown import (
MarkdownElement,
QuoteElement,
RawHtmlElement,
RenderableComponent,
TableElement,
TextElement,
)
@@ -24,7 +26,7 @@ class MarkdownBuilder(BaseBuilder[MarkdownData]):
"""链式构建Markdown图片的辅助类,支持上下文管理和组合。"""
def __init__(self):
data_model = MarkdownData(markdown="", width=800, css_path=None)
data_model = MarkdownData(elements=[], width=800, css_path=None)
super().__init__(data_model, template_name="components/core/markdown")
self._parts: list[MarkdownElement] = []
self._width: int = 800
@@ -78,6 +80,16 @@ class MarkdownBuilder(BaseBuilder[MarkdownData]):
)
return self
def add_component(
self, component: "BaseBuilder | RenderableComponent"
) -> "MarkdownBuilder":
"""添加一个UI组件(如图表、卡片等)。"""
component_data = (
component.build() if isinstance(component, BaseBuilder) else component
)
self._append_element(ComponentElement(component=component_data))
return self
def add_builder(self, builder: "MarkdownBuilder") -> "MarkdownBuilder":
"""将另一个builder的内容组合进来。"""
if self._context_stack:
@@ -144,8 +156,7 @@ class MarkdownBuilder(BaseBuilder[MarkdownData]):
"""
构建并返回 MarkdownData 模型实例。
"""
final_markdown = "\n\n".join(part.to_markdown() for part in self._parts).strip()
self._data.markdown = final_markdown
self._data.elements = self._parts
self._data.width = self._width
self._data.css_path = self._css_path
return super().build()
+50 -3
View File
@@ -1,3 +1,5 @@
from typing import Literal
from ...models.core.table import TableCell, TableData
from ..base import BaseBuilder
@@ -12,16 +14,61 @@ class TableBuilder(BaseBuilder[TableData]):
super().__init__(data_model, template_name="components/core/table")
def set_headers(self, headers: list[str]) -> "TableBuilder":
"""设置表头"""
"""
设置表格的表头。
参数:
headers: 一个包含表头文本的字符串列表。
返回:
TableBuilder: 当前构建器实例,以支持链式调用。
"""
self._data.headers = headers
return self
def set_column_alignments(
self, alignments: list[Literal["left", "center", "right"]]
) -> "TableBuilder":
"""
设置表格每列的文本对齐方式。
参数:
alignments: 一个包含 'left', 'center', 'right' 的对齐方式列表。
返回:
TableBuilder: 当前构建器实例,以支持链式调用。
"""
self._data.column_alignments = alignments
return self
def set_column_widths(self, widths: list[str | int]) -> "TableBuilder":
"""设置每列的宽度"""
self._data.column_widths = widths
return self
def add_row(self, row: list[TableCell]) -> "TableBuilder":
"""添加单行数据"""
"""
向表格中添加一行数据。
参数:
row: 一个包含单元格数据的列表。单元格可以是字符串、数字或
`TextCell`, `ImageCell` 等模型实例。
返回:
TableBuilder: 当前构建器实例,以支持链式调用。
"""
self._data.rows.append(row)
return self
def add_rows(self, rows: list[list[TableCell]]) -> "TableBuilder":
"""批量添加多行数据"""
"""
向表格中批量添加多行数据。
参数:
rows: 一个包含多行数据的列表。
返回:
TableBuilder: 当前构建器实例,以支持链式调用。
"""
self._data.rows.extend(rows)
return self
+62
View File
@@ -0,0 +1,62 @@
from typing import Literal
from typing_extensions import Self
from ...models.core.text import TextData, TextSpan
from ..base import BaseBuilder
class TextBuilder(BaseBuilder[TextData]):
"""链式构建轻量级富文本组件的辅助类"""
def __init__(self, text: str = ""):
data_model = TextData(spans=[], align="left")
super().__init__(data_model, template_name="components/core/text")
if text:
self.add_span(text)
def set_alignment(self, align: Literal["left", "right", "center"]) -> Self:
"""设置整个文本块的对齐方式"""
self._data.align = align
return self
def add_span(
self,
text: str,
*,
bold: bool = False,
italic: bool = False,
underline: bool = False,
strikethrough: bool = False,
code: bool = False,
color: str | None = None,
font_size: str | int | None = None,
font_family: str | None = None,
) -> Self:
"""
添加一个带有样式的文本片段。
参数:
text: 文本内容。
bold: 是否加粗。
italic: 是否斜体。
underline: 是否有下划线。
strikethrough: 是否有删除线。
code: 是否渲染为代码样式。
color: 文本颜色 (e.g., '#ff0000', 'red')。
font_size: 字体大小 (e.g., 16, '1.2em', '12px')。
font_family: 字体族。
"""
font_size_str = f"{font_size}px" if isinstance(font_size, int) else font_size
span = TextSpan(
text=text,
bold=bold,
italic=italic,
underline=underline,
strikethrough=strikethrough,
code=code,
color=color,
font_size=font_size_str,
font_family=font_family,
)
self._data.spans.append(span)
return self