♻️ refactor(ui): 重构 UI 渲染系统并优化资源管理 (#2094)

* ♻️ refactor(ui): 重构 UI 渲染系统并优化资源管理

- 【重构】重构 `RendererService` 架构,解耦模板引擎、主题管理与截图引擎
- 【重构】重构 `ui` 模块,采用组件注册机制与数据模型驱动,移除旧版 `builders`
- 【功能】新增 UI 热重载模式,支持在不重启的情况下实时预览 HTML/CSS 修改
- 【功能】增强启动项资源检查,支持基于 `resources.spec` 的版本校验与自动更新
- 【优化】统一内置插件的 UI 渲染逻辑,迁移至新的 `ui.table`、`ui.markdown` 等工厂接口
- 【优化】优化日志脱敏工具,支持自动折叠调试输出中冗长的样式标签
- 【优化】引入 `AssetResolutionService`,完善皮肤、组件、主题间的多级资源回退机制
- 【优化】新增组件生命周期钩子 `prepare`,支持渲染前的异步数据预处理

* 🚨 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
2026-02-06 20:13:40 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent bc8e1659ae
commit 5e30694663
77 changed files with 3035 additions and 3258 deletions
+278 -56
View File
@@ -1,26 +1,61 @@
from collections.abc import Sequence
from pathlib import Path
from typing import Any
from typing import Any, Literal, TypeVar
from zhenxun.services.renderer.protocols import Renderable
from pydantic import BaseModel
from . import builders
from .builders.core.layout import LayoutBuilder
from .models.core.base import RenderableComponent
from .models.core.markdown import MarkdownData
from .models.core.template import TemplateComponent
from zhenxun.services import renderer_service
from zhenxun.services.renderer.types import Renderable, RenderResult
from .models.components import (
Alert,
Avatar,
AvatarGroup,
Badge,
Divider,
KpiCard,
ProgressBar,
Timeline,
TimelineItem,
UserInfoBlock,
)
from .models.core import (
CardData,
DetailsData,
LayoutData,
ListData,
MarkdownData,
NotebookData,
RenderableComponent,
TableData,
TemplateComponent,
TextData,
)
from .registry import component, create
T_Model = TypeVar("T_Model", bound=BaseModel)
def register_component(namespace: str, template_dir: Path):
"""
注册一个第三方组件包(命名空间)。
注册后,可在模板中通过 `@namespace/template.html` 引用。
"""
renderer_service.register_template_namespace(namespace, template_dir)
def template(path: str | Path, data: dict[str, Any]) -> TemplateComponent:
"""
创建一个基于独立模板文件的UI组件。
适用于不希望遵循标准主题结构,而是直接渲染单个HTML文件的场景。
适用于不遵循标准主题结构,直接渲染单个HTML文件的场景。
参数:
path: 指向HTML模板文件的绝对或相对路径。
data: 传递给模板的上下文数据字典。
path: 指向HTML模板文件的绝对或相对路径
data: 传递给模板的上下文数据字典
返回:
TemplateComponent: 一个可被 `render()` 函数处理的组件实例。
TemplateComponent: 可被 `render()` 函数处理的组件实例
"""
if isinstance(path, str):
path = Path(path)
@@ -28,7 +63,7 @@ def template(path: str | Path, data: dict[str, Any]) -> TemplateComponent:
return TemplateComponent(template_path=path, data=data)
def markdown(content: str, style: str | Path | None = "default") -> MarkdownData:
def markdown(content: str = "", style: str | Path | None = "default") -> MarkdownData:
"""
创建一个基于Markdown内容的UI组件。
@@ -38,10 +73,12 @@ def markdown(content: str, style: str | Path | None = "default") -> MarkdownData
自定义CSS文件的路径。
返回:
MarkdownData: 一个可被 `render()` 函数处理的组件实例。
MarkdownData: Markdown 组件实例
"""
builder = builders.MarkdownBuilder().text(content)
component = builder.build()
component = MarkdownData()
if content:
component.text(content)
if isinstance(style, Path):
component.css_path = str(style.absolute())
else:
@@ -49,47 +86,195 @@ def markdown(content: str, style: str | Path | None = "default") -> MarkdownData
return component
def vstack(children: list[RenderableComponent], **layout_options) -> "LayoutBuilder":
def table(title: str, tip: str | None = None) -> TableData:
"""
创建一个表格组件构建器。
支持链式调用,例如: `ui.table("Title").set_headers(["A", "B"]).add_row([1, 2])`
参数:
title: 表格标题
tip: 标题旁的提示文本(可选)
"""
return TableData(title=title, tip=tip, headers=[], rows=[])
def notebook(data: list[Any] | None = None) -> NotebookData:
"""
创建一个 Notebook 文档构建器。
参数:
data: 初始元素列表(可选)
"""
return NotebookData(elements=data or []) # type: ignore
def alert(
title: str,
content: str,
type: Literal["info", "success", "warning", "error"] = "info",
) -> Alert:
"""创建 Alert 组件"""
return Alert(
title=title,
content=content,
type=type,
)
def badge(
text: str,
color_scheme: Literal["primary", "success", "warning", "error", "info"] = "info",
) -> Badge:
"""创建 Badge 组件"""
return Badge(
text=text,
color_scheme=color_scheme,
)
def divider(
margin: str = "2em 0",
color: str = "#f7889c",
style: Literal["solid", "dashed", "dotted"] = "solid",
thickness: str = "1px",
) -> Divider:
"""创建 Divider 组件"""
return Divider(
margin=margin,
color=color,
style=style,
thickness=thickness,
)
def progress_bar(
progress: float,
label: str | None = None,
color_scheme: Literal["primary", "success", "warning", "error", "info"] = "primary",
animated: bool = False,
) -> ProgressBar:
"""
创建 ProgressBar 进度条组件。
支持多种预设颜色方案和动画效果。
"""
return ProgressBar(
progress=progress,
label=label,
color_scheme=color_scheme,
animated=animated,
)
def kpi_card(label: str, value: Any, **kwargs) -> KpiCard:
"""创建 KPI 卡片"""
return KpiCard(label=label, value=value, **kwargs)
def text(
text: str,
align: Literal["left", "right", "center"] = "left",
**kwargs,
) -> TextData:
"""
创建纯文本组件。
参数:
text: 文本内容
align: 对齐方式
**kwargs: 传递给首个文本片段的样式参数 (如 bold=True, color='red')
返回:
TextData: 支持链式调用 .add_span() 的文本模型
"""
model = TextData(align=align)
if text:
model.add_span(text, **kwargs)
return model
def card(content: RenderableComponent) -> CardData:
"""创建 Card 组件"""
return CardData(content=content)
def avatar(
src: str, shape: Literal["circle", "square"] = "circle", size: int = 50
) -> Avatar:
return Avatar(src=src, shape=shape, size=size)
def avatar_group(
avatars: list[Avatar | str] | None = None,
spacing: int = -15,
max_count: int | None = None,
) -> AvatarGroup:
"""
创建头像组。
avatars: 头像对象或URL列表。
"""
avatar_objs = []
if avatars:
for item in avatars:
if isinstance(item, str):
avatar_objs.append(Avatar(src=item)) # type: ignore
else:
avatar_objs.append(item)
return AvatarGroup(avatars=avatar_objs, spacing=spacing, max_count=max_count)
def timeline(items: list[dict[str, Any]] | None = None) -> Timeline:
"""
创建时间轴。
items: 包含 timestamp, title, content, icon, color 的字典列表。
"""
timeline_items = []
if items:
for item in items:
timeline_items.append(TimelineItem(**item))
return Timeline(items=timeline_items)
def user_info_block(
name: str,
avatar_url: str,
subtitle: str | None = None,
tags: list[str] | None = None,
) -> UserInfoBlock:
return UserInfoBlock(
name=name,
avatar_url=avatar_url,
subtitle=subtitle,
tags=tags or [],
)
def vstack(children: Sequence[Any], **layout_options) -> LayoutData:
"""
创建一个垂直布局组件。
便捷函数,用于将多个组件垂直堆叠。
参数:
children: 一个包含 `RenderableComponent` 实例的列表。
**layout_options: 传递给布局模板的额外选项,如 `padding`, `gap`。
返回:
LayoutBuilder: 一个配置好的垂直布局构建器。
"""
builder = LayoutBuilder.column(**layout_options)
layout = LayoutData.column(**layout_options)
for child in children:
builder.add_item(child)
return builder
layout.add_item(child)
return layout
def hstack(children: list[RenderableComponent], **layout_options) -> "LayoutBuilder":
def hstack(children: Sequence[Any], **layout_options) -> LayoutData:
"""
创建一个水平布局组件。
便捷函数,用于将多个组件水平排列。
参数:
children: 一个包含 `RenderableComponent` 实例的列表。
**layout_options: 传递给布局模板的额外选项,如 `padding`, `gap`。
返回:
LayoutBuilder: 一个配置好的水平布局构建器。
"""
builder = LayoutBuilder.row(**layout_options)
layout = LayoutData.row(**layout_options)
for child in children:
builder.add_item(child)
return builder
layout.add_item(child)
return layout
async def render(
component_or_path: Renderable | str | Path,
data: dict | None = None,
*,
template: str | Path | None = None,
use_cache: bool = False,
is_page: bool = False,
**kwargs,
) -> bytes:
"""
@@ -99,32 +284,51 @@ async def render(
用法:
1. 渲染一个已构建的UI组件: `render(my_builder.build())`
2. 直接渲染一个模板文件: `render("path/to/template", data={...})`
3. 动态替换模板: `render(my_data, template="plugins/my_plugin/custom_card")`
参数:
component_or_path: 一个 `Renderable` 实例,或一个指向模板文件的
`str` 或 `Path` 对象。
data: (可选) 当 `component_or_path` 是路径时,必须提供此数据字典。
use_cache: (可选) 是否为此渲染启用文件缓存,默认为 `False`。
**kwargs: 传递给底层截图引擎的额外参数,例如 `viewport`。
`str` 或 `Path` 对象
data: 当 `component_or_path` 是路径时,必须提供此数据字典
template: 强制使用的模板路径,覆盖组件默认设置(可选)
use_cache: 是否启用渲染结果缓存(默认为 False)
is_page: 标记此次渲染是否为完整页面(自带html/body),默认为 False
**kwargs: 传递给截图引擎的参数,如 `viewport={"width": 800, "height": 600}`
返回:
bytes: 渲染后的PNG图片字节数据。
bytes: PNG图片二进制数据
"""
from zhenxun.services import renderer_service
component: Renderable
if isinstance(component_or_path, str | Path):
if data is None:
raise ValueError("使用模板路径渲染时必须提供 'data' 参数。")
component = TemplateComponent(template_path=component_or_path, data=data)
component = TemplateComponent(
template_path=component_or_path, data=data, is_page=is_page
)
else:
component = component_or_path
if template:
if isinstance(template, Path):
template_str = template.as_posix()
else:
template_str = str(template).replace("\\", "/")
if hasattr(component, "template_path"):
setattr(component, "template_path", template_str)
if is_page and hasattr(component, "is_page"):
component.is_page = True
return await renderer_service.render(component, use_cache=use_cache, **kwargs)
async def render_template(
path: str | Path, data: dict, use_cache: bool = False, **kwargs
path: str | Path,
data: dict,
use_cache: bool = False,
*,
is_page: bool = True,
**kwargs,
) -> bytes:
"""
渲染一个独立的Jinja2模板文件。
@@ -135,6 +339,8 @@ async def render_template(
path: 模板文件路径,相对于主题模板目录。
data: 传递给模板的数据字典。
use_cache: (可选) 是否启用渲染缓存,默认为 False。
is_page: (可选) 标记该模板是否为完整页面。默认为 True,因为独立模板通常已
包含文档骨架。
**kwargs: 传递给渲染服务的额外参数。
返回:
@@ -143,7 +349,7 @@ async def render_template(
异常:
RenderingError: 渲染失败时抛出。
"""
return await render(path, data, use_cache=use_cache, **kwargs)
return await render(path, data, use_cache=use_cache, is_page=is_page, **kwargs)
async def render_markdown(
@@ -166,8 +372,9 @@ async def render_markdown(
异常:
RenderingError: 渲染失败时抛出。
"""
builder = builders.MarkdownBuilder().text(md)
component = builder.build()
component = MarkdownData()
component.text(md)
if isinstance(style, Path):
component.css_path = str(style.absolute())
else:
@@ -176,9 +383,6 @@ async def render_markdown(
return await render(component, use_cache=use_cache, **kwargs)
from zhenxun.services.renderer.protocols import RenderResult
async def render_full_result(
component: Renderable, use_cache: bool = False, **kwargs
) -> RenderResult:
@@ -194,17 +398,18 @@ async def render_full_result(
返回:
RenderResult: 一个包含 `image_bytes` 和 `html_content` 的Pydantic模型。
"""
from zhenxun.services import renderer_service
from zhenxun.services.renderer.service import RenderContext
if not renderer_service._initialized:
await renderer_service.initialize()
assert renderer_service._theme_manager is not None, "ThemeManager 未初始化"
assert renderer_service._screenshot_engine is not None, "ScreenshotEngine 未初始化"
assert renderer_service._template_engine is not None, "TemplateEngine 未初始化"
context = RenderContext(
renderer=renderer_service,
theme_manager=renderer_service._theme_manager,
template_engine=renderer_service._template_engine,
screenshot_engine=renderer_service._screenshot_engine,
component=component,
use_cache=use_cache,
@@ -214,13 +419,30 @@ async def render_full_result(
__all__ = [
"builders",
"DetailsData",
"ListData",
"alert",
"avatar",
"avatar_group",
"badge",
"card",
"component",
"create",
"divider",
"hstack",
"kpi_card",
"markdown",
"notebook",
"progress_bar",
"register_component",
"render",
"render_full_result",
"render_markdown",
"render_template",
"table",
"template",
"text",
"timeline",
"user_info_block",
"vstack",
]
-49
View File
@@ -1,49 +0,0 @@
from .charts import EChartsBuilder
from .components import (
AlertBuilder,
AvatarBuilder,
AvatarGroupBuilder,
BadgeBuilder,
DividerBuilder,
KpiCardBuilder,
ProgressBarBuilder,
TimelineBuilder,
UserInfoBlockBuilder,
)
from .core import (
CardBuilder,
DetailsBuilder,
LayoutBuilder,
ListBuilder,
MarkdownBuilder,
NotebookBuilder,
TableBuilder,
TextBuilder,
)
from .presets import (
PluginHelpPageBuilder,
PluginMenuBuilder,
)
__all__ = [
"AlertBuilder",
"AvatarBuilder",
"AvatarGroupBuilder",
"BadgeBuilder",
"CardBuilder",
"DetailsBuilder",
"DividerBuilder",
"EChartsBuilder",
"KpiCardBuilder",
"LayoutBuilder",
"ListBuilder",
"MarkdownBuilder",
"NotebookBuilder",
"PluginHelpPageBuilder",
"PluginMenuBuilder",
"ProgressBarBuilder",
"TableBuilder",
"TextBuilder",
"TimelineBuilder",
"UserInfoBlockBuilder",
]
-117
View File
@@ -1,117 +0,0 @@
from typing import Generic, TypeVar
from typing_extensions import Self
from pydantic import BaseModel
T_DataModel = TypeVar("T_DataModel", bound=BaseModel)
class BaseBuilder(Generic[T_DataModel]):
"""
所有UI构建器的通用基类。
它实现了Builder设计模式,提供了一个流畅的、链式调用的API来创建和配置UI组件的数据模型。
同时,它也提供了通用的样式化方法,如 `with_style`, `with_inline_style` 等。
参数:
T_DataModel: 与此构建器关联的 Pydantic 数据模型类型。
"""
def __init__(self, data_model: T_DataModel, template_name: str):
self._data: T_DataModel = data_model
self._style_name: str | None = None
self._template_name = template_name
self._inline_style: dict | None = None
self._component_css: str | None = None
self._variant: str | None = None
self._extra_classes: list[str] = []
@property
def data(self) -> T_DataModel:
return self._data
def with_style(self, style_name: str) -> Self:
"""
为组件应用一个特定的样式。
参数:
style_name: 在主题的CSS中定义的样式类名。
返回:
Self: 当前构建器实例,以支持链式调用。
"""
self._style_name = style_name
return self
def with_inline_style(self, style: dict[str, str]) -> Self:
"""
为组件的根元素应用动态的内联样式。
参数:
style: 一个CSS样式字典,例如
`{"background-color":"#fff","font-size":"16px"}`。
返回:
Self: 当前构建器实例,以支持链式调用。
"""
self._inline_style = style
return self
def with_variant(self, variant_name: str) -> Self:
"""
为组件应用一个特定的变体/皮肤。
参数:
variant_name: 在组件的 `skins/` 目录下定义的变体名称。
返回:
Self: 当前构建器实例,以支持链式调用。
"""
self._variant = variant_name
return self
def with_component_css(self, css: str) -> Self:
"""
向页面注入一段自定义的CSS样式字符串。
参数:
css: 包含CSS规则的字符串。
返回:
Self: 当前构建器实例,以支持链式调用。
"""
self._component_css = css
return self
def with_classes(self, *class_names: str) -> Self:
"""
为组件的根元素添加一个或多个CSS工具类。
这些类来自主题预定义的工具集。
示例: .with_classes("p-4", "text-center", "font-bold")
"""
self._extra_classes.extend(class_names)
return self
def build(self) -> T_DataModel:
"""
构建并返回配置好的数据模型。
这是构建过程的最后一步,它会将所有配置应用到数据模型上。
返回:
T_DataModel: 最终配置好的、可被渲染服务使用的数据模型实例。
"""
if self._style_name and hasattr(self._data, "style_name"):
setattr(self._data, "style_name", self._style_name)
if self._inline_style and hasattr(self._data, "inline_style"):
setattr(self._data, "inline_style", self._inline_style)
if self._component_css and hasattr(self._data, "component_css"):
setattr(self._data, "component_css", self._component_css)
if self._variant and hasattr(self._data, "variant"):
setattr(self._data, "variant", self._variant)
if self._extra_classes and hasattr(self._data, "extra_classes"):
setattr(self._data, "extra_classes", self._extra_classes)
return self._data
-177
View File
@@ -1,177 +0,0 @@
from typing import Any, Generic, Literal, TypeVar
from typing_extensions import Self
from ..models.charts import (
BaseChartData,
EChartsAxis,
EChartsData,
EChartsGrid,
EChartsSeries,
EChartsTitle,
EChartsTooltip,
)
from .base import BaseBuilder
T_ChartData = TypeVar("T_ChartData", bound=BaseChartData)
class EChartsBuilder(BaseBuilder[EChartsData], Generic[T_ChartData]):
"""
一个统一的、泛型的 ECharts 图表构建器。
提供了设置 ECharts `option` 的核心方法,以及一些常用图表的便利方法。
"""
def __init__(self, template_name: str, title: str):
model = EChartsData(
template_path=template_name,
title=EChartsTitle(text=title),
grid=None,
tooltip=None,
xAxis=None,
yAxis=None,
legend=None,
background_image=None,
)
super().__init__(model, template_name=template_name)
def set_title(
self, text: str, left: Literal["left", "center", "right"] = "center"
) -> Self:
self._data.title_model = EChartsTitle(text=text, left=left)
return self
def set_grid(
self,
left: str | None = None,
right: str | None = None,
top: str | None = None,
bottom: str | None = None,
containLabel: bool = True,
) -> Self:
self._data.grid_model = EChartsGrid(
left=left, right=right, top=top, bottom=bottom, containLabel=containLabel
)
return self
def set_tooltip(self, trigger: Literal["item", "axis", "none"]) -> Self:
self._data.tooltip_model = EChartsTooltip(trigger=trigger)
return self
def set_x_axis(
self,
type: Literal["category", "value", "time", "log"],
data: list[Any] | None = None,
show: bool = True,
) -> Self:
self._data.x_axis_model = EChartsAxis(type=type, data=data, show=show)
return self
def set_y_axis(
self,
type: Literal["category", "value", "time", "log"],
data: list[Any] | None = None,
show: bool = True,
) -> Self:
self._data.y_axis_model = EChartsAxis(type=type, data=data, show=show)
return self
def add_series(
self, type: str, data: list[Any], name: str | None = None, **kwargs: Any
) -> Self:
series = EChartsSeries(type=type, data=data, name=name, **kwargs)
self._data.series_models.append(series)
return self
def set_legend(
self,
data: list[str],
orient: Literal["horizontal", "vertical"] = "horizontal",
left: str = "auto",
) -> Self:
self._data.legend_model = {"data": data, "orient": orient, "left": left}
return self
def set_option(self, key: str, value: Any) -> Self:
"""
[高级] 设置 ECharts `option` 中的一个原始键值对。
这会覆盖由其他流畅API方法设置的同名配置。
"""
self._data.raw_options[key] = value
return self
def set_background_image(self, image_name: str) -> Self:
"""【兼容】为横向柱状图设置背景图片。"""
self._data.background_image = image_name
return self
def bar_chart(
title: str,
items: list[tuple[str, int | float]],
direction: Literal["horizontal", "vertical"] = "horizontal",
) -> EChartsBuilder:
"""便捷工厂函数:创建一个柱状图构建器。"""
builder = EChartsBuilder("components/charts/bar_chart", title)
categories = [item[0] for item in items]
values = [item[1] for item in items]
if direction == "horizontal":
builder.set_x_axis(type="value")
builder.set_y_axis(type="category", data=categories)
builder.add_series(
type="bar",
data=values,
)
else:
builder.set_x_axis(type="category", data=categories)
builder.set_y_axis(type="value")
builder.add_series(type="bar", data=values)
return builder
def pie_chart(title: str, items: list[tuple[str, int | float]]) -> EChartsBuilder:
"""便捷工厂函数:创建一个饼图构建器。"""
builder = EChartsBuilder("components/charts/pie_chart", title)
data = [{"name": name, "value": value} for name, value in items]
legend_data = [item[0] for item in items]
builder.set_legend(data=legend_data)
builder.add_series(
name=title,
type="pie",
data=data,
)
return builder
def line_chart(
title: str, categories: list[str], series: list[dict[str, Any]]
) -> EChartsBuilder:
"""便捷工厂函数:创建一个折线图构建器。"""
builder = EChartsBuilder("components/charts/line_chart", title)
builder.set_x_axis(type="category", data=categories)
builder.set_y_axis(type="value")
for s in series:
builder.add_series(
type="line",
name=s.get("name", ""),
data=s.get("data", []),
smooth=s.get("smooth", False),
)
return builder
def radar_chart(
title: str, indicators: list[tuple[str, int | float]], series: list[dict[str, Any]]
) -> EChartsBuilder:
"""便捷工厂函数:创建一个雷达图构建器。"""
builder = EChartsBuilder("components/charts/radar_chart", title)
legend_data = [s.get("name", "") for s in series]
radar_indicators = [{"name": name, "max": max_val} for name, max_val in indicators]
builder.set_legend(data=legend_data)
builder.set_option("radar", {"indicator": radar_indicators})
builder.add_series(type="radar", data=series)
return builder
@@ -1,25 +0,0 @@
"""
小组件构建器模块
包含各种UI小组件的构建器
"""
from .alert import AlertBuilder
from .avatar import AvatarBuilder, AvatarGroupBuilder
from .badge import BadgeBuilder
from .divider import DividerBuilder
from .kpi_card import KpiCardBuilder
from .progress_bar import ProgressBarBuilder
from .timeline import TimelineBuilder
from .user_info_block import UserInfoBlockBuilder
__all__ = [
"AlertBuilder",
"AvatarBuilder",
"AvatarGroupBuilder",
"BadgeBuilder",
"DividerBuilder",
"KpiCardBuilder",
"ProgressBarBuilder",
"TimelineBuilder",
"UserInfoBlockBuilder",
]
-23
View File
@@ -1,23 +0,0 @@
from typing import Literal
from typing_extensions import Self
from ...models.components.alert import Alert
from ..base import BaseBuilder
class AlertBuilder(BaseBuilder[Alert]):
"""链式构建提示/标注框组件的辅助类"""
def __init__(
self,
title: str,
content: str,
type: Literal["info", "success", "warning", "error"] = "info",
):
data_model = Alert(title=title, content=content, type=type)
super().__init__(data_model, template_name="components/widgets/alert")
def hide_icon(self) -> Self:
"""隐藏提示框的默认图标"""
self._data.show_icon = False
return self
-38
View File
@@ -1,38 +0,0 @@
from typing import Literal
from typing_extensions import Self
from ...models.components.avatar import Avatar, AvatarGroup
from ..base import BaseBuilder
class AvatarBuilder(BaseBuilder[Avatar]):
"""链式构建单个头像的辅助类"""
def __init__(self, src: str):
data_model = Avatar(src=src, shape="circle", size=50)
super().__init__(data_model, template_name="components/widgets/avatar")
def set_shape(self, shape: Literal["circle", "square"]) -> Self:
self._data.shape = shape
return self
def set_size(self, size: int) -> Self:
self._data.size = size
return self
class AvatarGroupBuilder(BaseBuilder[AvatarGroup]):
"""链式构建头像组的辅助类"""
def __init__(self):
data_model = AvatarGroup(avatars=[], spacing=-15, max_count=None)
super().__init__(data_model, template_name="components/widgets/avatar_group")
def add_avatar(self, avatar: Avatar | AvatarBuilder | str) -> Self:
if isinstance(avatar, str):
self._data.avatars.append(Avatar(src=avatar, shape="circle", size=50))
elif isinstance(avatar, AvatarBuilder):
self._data.avatars.append(avatar.build())
else:
self._data.avatars.append(avatar)
return self
-25
View File
@@ -1,25 +0,0 @@
from typing import Literal
from ...models.components.badge import Badge
from ..base import BaseBuilder
class BadgeBuilder(BaseBuilder[Badge]):
"""链式构建徽章组件的辅助类"""
def __init__(
self,
text: str,
color_scheme: Literal[
"primary", "success", "warning", "error", "info"
] = "info",
):
data_model = Badge(text=text, color_scheme=color_scheme)
super().__init__(data_model, template_name="components/widgets/badge")
def set_color_scheme(
self, color_scheme: Literal["primary", "success", "warning", "error", "info"]
) -> "BadgeBuilder":
"""设置徽章的颜色方案。"""
self._data.color_scheme = color_scheme
return self
-20
View File
@@ -1,20 +0,0 @@
from typing import Literal
from ...models.components.divider import Divider
from ..base import BaseBuilder
class DividerBuilder(BaseBuilder[Divider]):
"""链式构建分割线组件的辅助类"""
def __init__(
self,
margin: str = "2em 0",
color: str = "#f7889c",
style: Literal["solid", "dashed", "dotted"] = "solid",
thickness: str = "1px",
):
data_model = Divider(
margin=margin, color=color, style=style, thickness=thickness
)
super().__init__(data_model, template_name="components/widgets/divider")
@@ -1,31 +0,0 @@
from typing import Any, Literal
from typing_extensions import Self
from ...models.components.kpi_card import KpiCard
from ..base import BaseBuilder
class KpiCardBuilder(BaseBuilder[KpiCard]):
"""链式构建统计卡片(KPI Card)的辅助类"""
def __init__(self, label: str, value: Any):
data_model = KpiCard(label=label, value=value)
super().__init__(data_model, template_name="components/widgets/kpi_card")
def with_unit(self, unit: str) -> Self:
"""设置数值的单位"""
self._data.unit = unit
return self
def with_change(
self, change: str, type: Literal["positive", "negative", "neutral"] = "neutral"
) -> Self:
"""设置与上一周期的变化率"""
self._data.change = change
self._data.change_type = type
return self
def with_icon(self, svg_path: str) -> Self:
"""设置卡片图标 (提供SVG path data)"""
self._data.icon_svg = svg_path
return self
@@ -1,42 +0,0 @@
from typing import Literal
from ...models.components.progress_bar import ProgressBar
from ..base import BaseBuilder
class ProgressBarBuilder(BaseBuilder[ProgressBar]):
"""链式构建进度条组件的辅助类"""
def __init__(
self,
progress: float,
label: str | None = None,
color_scheme: Literal[
"primary", "success", "warning", "error", "info"
] = "primary",
animated: bool = False,
):
data_model = ProgressBar(
progress=progress,
label=label,
color_scheme=color_scheme,
animated=animated,
)
super().__init__(data_model, template_name="components/widgets/progress_bar")
def set_label(self, label: str) -> "ProgressBarBuilder":
"""设置进度条上显示的文本。"""
self._data.label = label
return self
def set_color_scheme(
self, color_scheme: Literal["primary", "success", "warning", "error", "info"]
) -> "ProgressBarBuilder":
"""设置进度条的颜色方案。"""
self._data.color_scheme = color_scheme
return self
def set_animated(self, animated: bool = True) -> "ProgressBarBuilder":
"""设置进度条是否显示动画效果。"""
self._data.animated = animated
return self
@@ -1,28 +0,0 @@
from typing_extensions import Self
from ...models.components.timeline import Timeline, TimelineItem
from ..base import BaseBuilder
class TimelineBuilder(BaseBuilder[Timeline]):
"""链式构建时间轴组件的辅助类"""
def __init__(self):
data_model = Timeline(items=[])
super().__init__(data_model, template_name="components/widgets/timeline")
def add_item(
self,
timestamp: str,
title: str,
content: str,
*,
icon: str | None = None,
color: str | None = None,
) -> Self:
"""向时间轴中添加一个事件点"""
item = TimelineItem(
timestamp=timestamp, title=title, content=content, icon=icon, color=color
)
self._data.items.append(item)
return self
@@ -1,33 +0,0 @@
from ...models.components.user_info_block import UserInfoBlock
from ..base import BaseBuilder
class UserInfoBlockBuilder(BaseBuilder[UserInfoBlock]):
"""链式构建用户信息块的辅助类"""
def __init__(
self,
name: str,
avatar_url: str,
subtitle: str | None = None,
tags: list[str] | None = None,
):
data_model = UserInfoBlock(
name=name, avatar_url=avatar_url, subtitle=subtitle, tags=tags or []
)
super().__init__(data_model, template_name="components/widgets/user_info_block")
def set_subtitle(self, subtitle: str) -> "UserInfoBlockBuilder":
"""设置副标题。"""
self._data.subtitle = subtitle
return self
def add_tag(self, tag: str) -> "UserInfoBlockBuilder":
"""添加一个标签。"""
self._data.tags.append(tag)
return self
def add_tags(self, tags: list[str]) -> "UserInfoBlockBuilder":
"""批量添加标签。"""
self._data.tags.extend(tags)
return self
-24
View File
@@ -1,24 +0,0 @@
"""
核心构建器模块
包含基础的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
@@ -1,26 +0,0 @@
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
@@ -1,19 +0,0 @@
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
-121
View File
@@ -1,121 +0,0 @@
from typing import Any
from typing_extensions import Self
from ...models.core.base import RenderableComponent
from ...models.core.layout import LayoutData, LayoutItem
from ..base import BaseBuilder
__all__ = ["LayoutBuilder"]
class LayoutBuilder(BaseBuilder[LayoutData]):
"""
一个用于将多个UI组件组合成单张图片的链式构建器。
它通过在单个渲染流程中动态包含子模板来实现高质量的输出。
"""
def __init__(self):
super().__init__(LayoutData(), template_name="")
self._options: dict[str, Any] = {}
@classmethod
def column(
cls, *, gap: str = "20px", align_items: str = "stretch", **options: Any
) -> Self:
builder = cls()
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, *, gap: str = "10px", align_items: str = "center", **options: Any
) -> Self:
builder = cls()
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
@classmethod
def hstack(
cls, components: list["BaseBuilder | RenderableComponent"], **options: Any
) -> Self:
builder = cls.row(**options)
for component in components:
builder.add_item(component)
return builder
@classmethod
def vstack(
cls, components: list["BaseBuilder | RenderableComponent"], **options: Any
) -> Self:
builder = cls.column(**options)
for component in components:
builder.add_item(component)
return builder
def add_item(
self,
component: "BaseBuilder | RenderableComponent",
metadata: dict[str, Any] | None = None,
) -> Self:
"""
向布局中添加一个组件项。
参数:
component: 一个 `BaseBuilder` 实例 (如 `TableBuilder()`) 或一个已构建的
`RenderableComponent` 数据模型。
metadata: (可选) 与此项目关联的元数据,可在布局模板中访问。
返回:
Self: 当前构建器实例,以支持链式调用。
"""
component_data = (
component.data if isinstance(component, BaseBuilder) else component
)
self._data.children.append(
LayoutItem(component=component_data, metadata=metadata)
)
return self
def add_option(self, key: str, value: Any) -> Self:
"""
为布局模板添加一个自定义选项。
例如,`add_option("padding", "30px")` 会在模板的 `data.options`
字典中添加 `{"padding": "30px"}`。
参数:
key: 选项的键名。
value: 选项的值。
返回:
Self: 当前构建器实例,以支持链式调用。
"""
self._options[key] = value
return self
def build(self) -> LayoutData:
"""
构建并返回 LayoutData 模型实例。
"""
if not self._template_name:
raise ValueError(
"必须通过工厂方法 (如 LayoutBuilder.column()) 初始化布局类型。"
)
self._data.options = self._options
self._data.layout_type = self._template_name.split("/")[-1]
return super().build()
-31
View File
@@ -1,31 +0,0 @@
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
-162
View File
@@ -1,162 +0,0 @@
from contextlib import AbstractContextManager
from pathlib import Path
from typing import Any
from ...models.core.markdown import (
CodeElement,
ComponentElement,
HeadingElement,
ImageElement,
ListElement,
ListItemElement,
MarkdownData,
MarkdownElement,
QuoteElement,
RawHtmlElement,
RenderableComponent,
TableElement,
TextElement,
)
from ..base import BaseBuilder
__all__ = ["MarkdownBuilder"]
class MarkdownBuilder(BaseBuilder[MarkdownData]):
"""链式构建Markdown图片的辅助类,支持上下文管理和组合。"""
def __init__(self):
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
self._css_path: str | None = None
self._context_stack: list[QuoteElement | ListElement | ListItemElement] = []
def _append_element(self, element: MarkdownElement):
"""内部方法,根据上下文将元素添加到正确的位置。"""
if self._context_stack:
self._context_stack[-1].content.append(element)
else:
self._parts.append(element)
return self
def text(self, text: str) -> "MarkdownBuilder":
"""添加Markdown文本"""
self._append_element(TextElement(text=text))
return self
def head(self, text: str, level: int = 1) -> "MarkdownBuilder":
"""添加Markdown标题"""
self._append_element(HeadingElement(text=text, level=level))
return self
def image(self, content: str | Path, alt: str = "image") -> "MarkdownBuilder":
"""添加Markdown图片"""
src = ""
if isinstance(content, Path):
src = content.absolute().as_uri()
elif content.startswith("base64://"):
src = f"data:image/png;base64,{content.split('base64://', 1)[-1]}"
else:
src = content
self._append_element(ImageElement(src=src, alt=alt))
return self
def code(self, code: str, language: str = "") -> "MarkdownBuilder":
"""添加Markdown代码块"""
self._append_element(CodeElement(code=code, language=language))
return self
def table(
self,
headers: list[str],
rows: list[list[str]],
alignments: list[Any] | None = None,
) -> "MarkdownBuilder":
"""添加Markdown表格"""
self._append_element(
TableElement(headers=headers, rows=rows, alignments=alignments)
)
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:
self._context_stack[-1].content.extend(builder._parts)
else:
self._parts.extend(builder._parts)
return self
def quote(self) -> AbstractContextManager["MarkdownBuilder"]:
"""创建一个引用块上下文。"""
return self._context_for(QuoteElement())
def list(self, ordered: bool = False) -> AbstractContextManager["MarkdownBuilder"]:
"""创建一个列表上下文。"""
return self._context_for(ListElement(ordered=ordered))
def list_item(self) -> AbstractContextManager["MarkdownBuilder"]:
"""在列表上下文中创建一个列表项。"""
if not self._context_stack or not isinstance(
self._context_stack[-1], ListElement
):
raise TypeError("list_item() 只能在 list() 上下文中使用。")
return self._context_for(ListItemElement())
class _ContextManager:
def __init__(
self,
builder: "MarkdownBuilder",
element: QuoteElement | ListElement | ListItemElement,
):
self.builder = builder
self.element = element
def __enter__(self):
self.builder._context_stack.append(self.element)
return self.builder
def __exit__(self, exc_type, exc_val, exc_tb):
del exc_type, exc_val, exc_tb
self.builder._context_stack.pop()
def _context_for(
self, element: QuoteElement | ListElement | ListItemElement
) -> AbstractContextManager["MarkdownBuilder"]:
self._append_element(element)
return self._ContextManager(self, element)
def set_width(self, width: int) -> "MarkdownBuilder":
"""设置图片宽度"""
self._width = width
return self
def set_css_path(self, css_path: str) -> "MarkdownBuilder":
"""设置CSS样式路径"""
self._css_path = css_path
return self
def add_divider(self) -> "MarkdownBuilder":
"""添加一条标准的 Markdown 分割线。"""
self._append_element(RawHtmlElement(html="---"))
return self
def build(self) -> MarkdownData:
"""
构建并返回 MarkdownData 模型实例。
"""
self._data.elements = self._parts
self._data.width = self._width
self._data.css_path = self._css_path
return super().build()
-120
View File
@@ -1,120 +0,0 @@
import builtins
from pathlib import Path
from ...models.core.base import RenderableComponent
from ...models.core.notebook import NotebookData, NotebookElement
from ..base import BaseBuilder
__all__ = ["NotebookBuilder"]
class NotebookBuilder(BaseBuilder[NotebookData]):
"""
一个用于链式构建 Notebook 页面的辅助类。
"""
def __init__(self, data: list[NotebookElement] | None = None):
elements = data if data is not None else []
data_model = NotebookData(elements=elements)
super().__init__(data_model, template_name="components/core/notebook")
self._elements = elements
def text(self, text: str) -> "NotebookBuilder":
"""添加Notebook文本"""
self._elements.append(NotebookElement(type="paragraph", text=text))
return self
def head(self, text: str, level: int = 1) -> "NotebookBuilder":
"""添加Notebook标题"""
if not 1 <= level <= 4:
raise ValueError("标题级别必须在1-4之间")
self._elements.append(NotebookElement(type="heading", text=text, level=level))
return self
def image(
self,
content: str,
caption: str | None = None,
) -> "NotebookBuilder":
"""添加Notebook图片"""
src = ""
if isinstance(content, Path):
src = content.absolute().as_uri()
elif content.startswith("base64"):
src = f"data:image/png;base64,{content.split('base64://', 1)[-1]}"
else:
src = content
self._elements.append(NotebookElement(type="image", src=src, caption=caption))
return self
def quote(self, text: str | list[str]) -> "NotebookBuilder":
"""添加Notebook引用文本"""
if isinstance(text, str):
self._elements.append(NotebookElement(type="blockquote", text=text))
elif isinstance(text, list):
for t in text:
self._elements.append(NotebookElement(type="blockquote", text=t))
return self
def code(self, code: str, language: str = "python") -> "NotebookBuilder":
"""添加Notebook代码块"""
self._elements.append(
NotebookElement(type="code", code=code, language=language)
)
return self
def list(self, items: list[str], ordered: bool = False) -> "NotebookBuilder":
"""添加Notebook列表"""
self._elements.append(NotebookElement(type="list", data=items, ordered=ordered))
return self
def add_divider(self, **kwargs) -> "NotebookBuilder":
"""
添加分隔线。
:param kwargs: Divider组件的可选参数, 如 margin, color, style, thickness。
"""
from ...models.components import Divider
self.add_component(Divider(**kwargs))
return self
def add_component(
self, component: "RenderableComponent | BaseBuilder"
) -> "NotebookBuilder":
"""
向 Notebook 中添加一个可渲染的自定义组件。
"""
component_data = (
component.data if isinstance(component, BaseBuilder) else component
)
if not isinstance(component_data, RenderableComponent):
raise TypeError(
f"add_component 只能接受 RenderableComponent 或其 Builder,"
f"但收到了 {type(component)}"
)
self._elements.append(
NotebookElement(type="component", component=component_data)
)
return self
def add_texts(self, texts: builtins.list[str]) -> "NotebookBuilder":
"""批量添加多个文本段落"""
for text in texts:
self.text(text)
return self
def add_quotes(self, quotes: builtins.list[str]) -> "NotebookBuilder":
"""批量添加引用"""
for quote in quotes:
self.quote(quote)
return self
def build(self) -> NotebookData:
"""
构建并返回 NotebookData 模型实例。
"""
self._data.elements = self._elements
return super().build()
-105
View File
@@ -1,105 +0,0 @@
from pathlib import Path
from typing import Any, Literal
from ...models.core.table import (
BaseCell,
ImageCell,
TableCell,
TableData,
TextCell,
)
from ..base import BaseBuilder
__all__ = ["TableBuilder"]
class TableBuilder(BaseBuilder[TableData]):
"""链式构建通用表格的辅助类"""
def __init__(self, title: str, tip: str | None = None):
data_model = TableData(title=title, tip=tip, headers=[], rows=[])
super().__init__(data_model, template_name="components/core/table")
def _normalize_cell(self, cell_data: Any) -> TableCell:
"""内部辅助方法,将各种原生数据类型转换为TableCell模型。"""
if isinstance(cell_data, BaseCell):
return cell_data # type: ignore
if isinstance(cell_data, str | int | float):
return TextCell(content=str(cell_data))
if isinstance(cell_data, Path):
return ImageCell(src=cell_data.resolve().as_uri())
if isinstance(cell_data, tuple) and len(cell_data) == 3:
if (
isinstance(cell_data[0], Path)
and isinstance(cell_data[1], int)
and isinstance(cell_data[2], int)
):
return ImageCell(
src=cell_data[0].resolve().as_uri(),
width=cell_data[1],
height=cell_data[2],
)
return TextCell(content="")
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: 当前构建器实例,以支持链式调用。
"""
normalized_row = [self._normalize_cell(cell) for cell in row]
self._data.rows.append(normalized_row)
return self
def add_rows(self, rows: list[list[TableCell]]) -> "TableBuilder":
"""
向表格中批量添加多行数据, 并自动转换原生类型。
参数:
rows: 一个包含多行数据的列表。
返回:
TableBuilder: 当前构建器实例,以支持链式调用。
"""
for row in rows:
self.add_row(row)
return self
-62
View File
@@ -1,62 +0,0 @@
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
-12
View File
@@ -1,12 +0,0 @@
"""
预设构建器模块
包含预定义的UI组件构建器
"""
from .plugin_help_page import PluginHelpPageBuilder
from .plugin_menu import PluginMenuBuilder
__all__ = [
"PluginHelpPageBuilder",
"PluginMenuBuilder",
]
@@ -1,27 +0,0 @@
from ...models.presets.plugin_help_page import (
HelpCategory,
PluginHelpPageData,
)
from ..base import BaseBuilder
class PluginHelpPageBuilder(BaseBuilder[PluginHelpPageData]):
"""链式构建插件帮助页面的辅助类"""
def __init__(self, bot_nickname: str, page_title: str):
self._data = PluginHelpPageData(
bot_nickname=bot_nickname, page_title=page_title, categories=[]
)
super().__init__(self._data, template_name="pages/core/plugin_help_page")
def add_category(self, category: HelpCategory) -> "PluginHelpPageBuilder":
"""添加一个帮助分类"""
self._data.categories.append(category)
return self
def add_categories(self, categories: list[HelpCategory]) -> "PluginHelpPageBuilder":
"""批量添加帮助分类"""
for category in categories:
self.add_category(category)
return self
@@ -1,36 +0,0 @@
from ...models.presets.plugin_menu import (
PluginMenuCategory,
PluginMenuData,
)
from ..base import BaseBuilder
__all__ = ["PluginMenuBuilder"]
class PluginMenuBuilder(BaseBuilder[PluginMenuData]):
"""链式构建插件菜单的辅助类"""
def __init__(self, bot_name: str, bot_avatar_url: str, is_detail: bool = False):
self._data = PluginMenuData(
bot_name=bot_name,
bot_avatar_url=bot_avatar_url,
is_detail=is_detail,
plugin_count=0,
active_count=0,
categories=[],
)
super().__init__(self._data, template_name="pages/core/plugin_menu")
def add_category(self, category: PluginMenuCategory) -> "PluginMenuBuilder":
self._data.categories.append(category)
self._data.plugin_count += len(category.items)
self._data.active_count += sum(1 for item in category.items if item.status)
return self
def add_categories(
self, categories: list[PluginMenuCategory]
) -> "PluginMenuBuilder":
for category in categories:
self.add_category(category)
return self
+144 -1
View File
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
from typing import Any, Literal
from typing_extensions import Self
import uuid
from pydantic import BaseModel, Field
@@ -94,7 +95,10 @@ class BaseChartData(RenderableComponent, ABC):
class EChartsData(BaseChartData):
"""统一的 ECharts 图表数据模型"""
template_path: str = Field(..., exclude=True, description="图表组件的模板路径")
class Config:
populate_by_name = True
template_path: str = Field(..., exclude=True, description="图表组件的模板路径") # type: ignore
"""图表组件的模板路径"""
title_model: EChartsTitle | None = Field(
None, alias="title", description="标题组件"
@@ -160,3 +164,142 @@ class EChartsData(BaseChartData):
@property
def template_name(self) -> str:
return self.template_path
def set_title(
self, text: str, left: Literal["left", "center", "right"] = "center"
) -> Self:
self.title_model = EChartsTitle(text=text, left=left)
return self
def set_grid(
self,
left: str | None = None,
right: str | None = None,
top: str | None = None,
bottom: str | None = None,
containLabel: bool = True,
) -> Self:
self.grid_model = EChartsGrid(
left=left, right=right, top=top, bottom=bottom, containLabel=containLabel
)
return self
def set_tooltip(self, trigger: Literal["item", "axis", "none"]) -> Self:
self.tooltip_model = EChartsTooltip(trigger=trigger)
return self
def set_x_axis(
self,
type: Literal["category", "value", "time", "log"],
data: list[Any] | None = None,
show: bool = True,
) -> Self:
self.x_axis_model = EChartsAxis(type=type, data=data, show=show)
return self
def set_y_axis(
self,
type: Literal["category", "value", "time", "log"],
data: list[Any] | None = None,
show: bool = True,
) -> Self:
self.y_axis_model = EChartsAxis(type=type, data=data, show=show)
return self
def add_series(
self, type: str, data: list[Any], name: str | None = None, **kwargs: Any
) -> Self:
series = EChartsSeries(type=type, data=data, name=name, **kwargs)
self.series_models.append(series)
return self
def set_legend(
self,
data: list[str],
orient: Literal["horizontal", "vertical"] = "horizontal",
left: str = "auto",
) -> Self:
self.legend_model = {"data": data, "orient": orient, "left": left}
return self
def set_option(self, key: str, value: Any) -> Self:
self.raw_options[key] = value
return self
def set_background_image(self, image_name: str) -> Self:
self.background_image = image_name
return self
@classmethod
def bar_chart(
cls,
title: str,
items: list[tuple[str, int | float]],
direction: Literal["horizontal", "vertical"] = "horizontal",
background_image: str | None = None,
) -> "EChartsData":
"""便捷创建一个柱状图"""
categories = [item[0] for item in items]
values = [item[1] for item in items]
if direction == "horizontal":
x_axis = EChartsAxis(type="value")
y_axis = EChartsAxis(type="category", data=categories)
else:
x_axis = EChartsAxis(type="category", data=categories)
y_axis = EChartsAxis(type="value")
return cls(
template_path="components/charts/bar_chart",
title=EChartsTitle(text=title),
grid=None,
xAxis=x_axis,
yAxis=y_axis,
tooltip=EChartsTooltip(trigger="item"),
series=[EChartsSeries(type="bar", data=values)],
background_image=background_image,
)
@classmethod
def pie_chart(
cls, title: str, items: list[tuple[str, int | float]]
) -> "EChartsData":
"""便捷创建一个饼图"""
data = [{"name": name, "value": value} for name, value in items]
legend_data = [item[0] for item in items]
return cls(
template_path="components/charts/pie_chart",
title=EChartsTitle(text=title),
grid=None,
tooltip=EChartsTooltip(trigger="item"),
xAxis=None,
yAxis=None,
legend={"data": legend_data, "orient": "horizontal", "left": "auto"},
series=[EChartsSeries(type="pie", data=data, name=title)],
background_image=None,
)
@classmethod
def line_chart(
cls, title: str, categories: list[str], series: list[dict[str, Any]]
) -> "EChartsData":
"""便捷创建一个折线图"""
series_models = [
EChartsSeries(
type="line",
name=s.get("name", ""),
data=s.get("data", []),
smooth=s.get("smooth", False),
)
for s in series
]
return cls(
template_path="components/charts/line_chart",
title=EChartsTitle(text=title),
grid=None,
xAxis=EChartsAxis(type="category", data=categories),
yAxis=EChartsAxis(type="value"),
tooltip=EChartsTooltip(trigger="axis"),
series=series_models,
background_image=None,
)
+5 -12
View File
@@ -1,18 +1,11 @@
"""
组件模型模块
包含各种UI组件的数据模型
"""
from .alert import Alert
from .badge import Badge
from .divider import Divider, Rectangle
from .kpi_card import KpiCard
from .progress_bar import ProgressBar
from .timeline import Timeline, TimelineItem
from .user_info_block import UserInfoBlock
from .data import KpiCard, Timeline, TimelineItem
from .display import Avatar, AvatarGroup, Divider, Rectangle, UserInfoBlock
from .feedback import Alert, Badge, ProgressBar
__all__ = [
"Alert",
"Avatar",
"AvatarGroup",
"Badge",
"Divider",
"KpiCard",
-27
View File
@@ -1,27 +0,0 @@
from typing import Literal
from pydantic import Field
from ..core.base import RenderableComponent
__all__ = ["Alert"]
class Alert(RenderableComponent):
"""一个带样式的提示框组件,用于显示重要信息。"""
component_type: Literal["alert"] = "alert"
type: Literal["info", "success", "warning", "error"] = Field(
default="info", description="提示框的类型,决定了颜色和图标"
)
"""提示框的类型,决定了颜色和图标"""
title: str = Field(..., description="提示框的标题")
"""提示框的标题"""
content: str = Field(..., description="提示框的主要内容")
"""提示框的主要内容"""
show_icon: bool = Field(default=True, description="是否显示与类型匹配的图标")
"""是否显示与类型匹配的图标"""
@property
def template_name(self) -> str:
return "components/widgets/alert"
-41
View File
@@ -1,41 +0,0 @@
from typing import Literal
from pydantic import Field
from ..core.base import RenderableComponent
__all__ = ["Avatar", "AvatarGroup"]
class Avatar(RenderableComponent):
"""单个头像组件。"""
component_type: Literal["avatar"] = "avatar"
src: str = Field(..., description="头像的URL或Base64数据URI")
"""头像的URL或Base64数据URI"""
shape: Literal["circle", "square"] = Field("circle", description="头像形状")
"""头像形状"""
size: int = Field(50, description="头像尺寸(像素)")
"""头像尺寸(像素)"""
@property
def template_name(self) -> str:
return "components/widgets/avatar"
class AvatarGroup(RenderableComponent):
"""一组堆叠的头像组件。"""
component_type: Literal["avatar_group"] = "avatar_group"
avatars: list[Avatar] = Field(default_factory=list, description="头像列表")
"""头像列表"""
spacing: int = Field(-15, description="头像间的间距(负数表示重叠)")
"""头像间的间距(负数表示重叠)"""
max_count: int | None = Field(
None, description="最多显示的头像数量,超出部分会显示为'+N'"
)
"""最多显示的头像数量,超出部分会显示为'+N'"""
@property
def template_name(self) -> str:
return "components/widgets/avatar"
-24
View File
@@ -1,24 +0,0 @@
from typing import Literal
from pydantic import Field
from ..core.base import RenderableComponent
__all__ = ["Badge"]
class Badge(RenderableComponent):
"""一个简单的徽章组件,用于显示状态或标签。"""
component_type: Literal["badge"] = "badge"
text: str = Field(..., description="徽章上显示的文本")
"""徽章上显示的文本"""
color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field(
default="info",
description="预设的颜色方案",
)
"""预设的颜色方案"""
@property
def template_name(self) -> str:
return "components/widgets/badge"
@@ -1,16 +1,17 @@
from typing import Any, Literal
from pydantic import Field
from pydantic import BaseModel, Field
from ..core.base import RenderableComponent
__all__ = ["KpiCard"]
__all__ = ["KpiCard", "Timeline", "TimelineItem"]
class KpiCard(RenderableComponent):
"""一个用于展示关键性能指标(KPI)的统计卡片。"""
component_type: Literal["kpi_card"] = "kpi_card"
"""组件类型"""
label: str = Field(..., description="指标的标签或名称")
"""指标的标签或名称"""
value: Any = Field(..., description="指标的主要数值")
@@ -33,3 +34,33 @@ class KpiCard(RenderableComponent):
@property
def template_name(self) -> str:
return "components/widgets/kpi_card"
class TimelineItem(BaseModel):
"""时间轴中的单个事件点。"""
timestamp: str = Field(..., description="显示在时间点旁边的时间或标签")
"""显示在时间点旁边的时间或标签"""
title: str = Field(..., description="事件的标题")
"""事件的标题"""
content: str = Field(..., description="事件的详细描述")
"""事件的详细描述"""
icon: str | None = Field(default=None, description="可选的自定义图标SVG路径")
"""可选的自定义图标SVG路径"""
color: str | None = Field(default=None, description="可选的自定义颜色,覆盖默认")
"""可选的自定义颜色,覆盖默认"""
class Timeline(RenderableComponent):
"""一个垂直时间轴组件,用于按顺序展示事件。"""
component_type: Literal["timeline"] = "timeline"
"""组件类型"""
items: list[TimelineItem] = Field(
default_factory=list, description="时间轴项目列表"
)
"""时间轴项目列表"""
@property
def template_name(self) -> str:
return "components/widgets/timeline"
+102
View File
@@ -0,0 +1,102 @@
from typing import Literal
from pydantic import Field
from ..core.base import RenderableComponent
__all__ = ["Avatar", "AvatarGroup", "Divider", "Rectangle", "UserInfoBlock"]
class Avatar(RenderableComponent):
"""单个头像组件。"""
component_type: Literal["avatar"] = "avatar"
"""组件类型"""
src: str = Field(..., description="头像的URL或Base64数据URI")
"""头像的URL或Base64数据URI"""
shape: Literal["circle", "square"] = Field("circle", description="头像形状")
"""头像形状"""
size: int = Field(50, description="头像尺寸(像素)")
"""头像尺寸(像素)"""
@property
def template_name(self) -> str:
return "components/widgets/avatar"
class AvatarGroup(RenderableComponent):
"""一组堆叠的头像组件。"""
component_type: Literal["avatar_group"] = "avatar_group"
"""组件类型"""
avatars: list[Avatar] = Field(default_factory=list, description="头像列表")
"""头像列表"""
spacing: int = Field(-15, description="头像间的间距(负数表示重叠)")
"""头像间的间距(负数表示重叠)"""
max_count: int | None = Field(
None, description="最多显示的头像数量,超出部分会显示为'+N'"
)
"""最多显示的头像数量,超出部分会显示为'+N'"""
@property
def template_name(self) -> str:
return "components/widgets/avatar"
class Divider(RenderableComponent):
"""一个简单的分割线组件。"""
component_type: Literal["divider"] = "divider"
"""组件类型"""
margin: str = Field("2em 0", description="CSS margin属性,控制分割线上下的间距")
"""CSS margin属性,控制分割线上下的间距"""
color: str = Field("#f7889c", description="分割线颜色")
"""分割线颜色"""
style: Literal["solid", "dashed", "dotted"] = Field("solid", description="线条样式")
"""线条样式"""
thickness: str = Field("1px", description="线条粗细")
"""线条粗细"""
@property
def template_name(self) -> str:
return "components/widgets/divider"
class Rectangle(RenderableComponent):
"""一个矩形背景块组件。"""
component_type: Literal["rectangle"] = "rectangle"
"""组件类型"""
height: str = Field("50px", description="矩形的高度 (CSS value)")
"""矩形的高度 (CSS value)"""
background_color: str = Field("#fdf1f5", description="背景颜色")
"""背景颜色"""
border: str = Field("1px solid #fce4ec", description="CSS border属性")
"""CSS border属性"""
border_radius: str = Field("8px", description="CSS border-radius属性")
"""CSS border-radius属性"""
@property
def template_name(self) -> str:
return "components/widgets/rectangle"
class UserInfoBlock(RenderableComponent):
"""一个带头像、名称和副标题的用户信息块组件。"""
component_type: Literal["user_info_block"] = "user_info_block"
"""组件类型"""
avatar_url: str = Field(..., description="用户头像的URL")
"""用户头像的URL"""
name: str = Field(..., description="用户的名称")
"""用户的名称"""
subtitle: str | None = Field(
default=None, description="显示在名称下方的副标题 (如UID或角色)"
)
"""显示在名称下方的副标题 (如UID或角色)"""
tags: list[str] = Field(default_factory=list, description="附加的标签列表")
"""附加的标签列表"""
@property
def template_name(self) -> str:
return "components/widgets/user_info_block"
-43
View File
@@ -1,43 +0,0 @@
from typing import Literal
from pydantic import Field
from ..core.base import RenderableComponent
__all__ = ["Divider", "Rectangle"]
class Divider(RenderableComponent):
"""一个简单的分割线组件。"""
component_type: Literal["divider"] = "divider"
margin: str = Field("2em 0", description="CSS margin属性,控制分割线上下的间距")
"""CSS margin属性,控制分割线上下的间距"""
color: str = Field("#f7889c", description="分割线颜色")
"""分割线颜色"""
style: Literal["solid", "dashed", "dotted"] = Field("solid", description="线条样式")
"""线条样式"""
thickness: str = Field("1px", description="线条粗细")
"""线条粗细"""
@property
def template_name(self) -> str:
return "components/widgets/divider"
class Rectangle(RenderableComponent):
"""一个矩形背景块组件。"""
component_type: Literal["rectangle"] = "rectangle"
height: str = Field("50px", description="矩形的高度 (CSS value)")
"""矩形的高度 (CSS value)"""
background_color: str = Field("#fdf1f5", description="背景颜色")
"""背景颜色"""
border: str = Field("1px solid #fce4ec", description="CSS border属性")
"""CSS border属性"""
border_radius: str = Field("8px", description="CSS border-radius属性")
"""CSS border-radius属性"""
@property
def template_name(self) -> str:
return "components/widgets/rectangle"
+70
View File
@@ -0,0 +1,70 @@
from typing import Literal
from pydantic import Field
from ...registry import component
from ..core.base import RenderableComponent
__all__ = ["Alert", "Badge", "ProgressBar"]
@component(name="alert", namespace="core")
class Alert(RenderableComponent):
"""一个带样式的提示框组件,用于显示重要信息。"""
component_type: Literal["alert"] = "alert"
"""组件类型"""
type: Literal["info", "success", "warning", "error"] = Field(
default="info", description="提示框的类型,决定了颜色和图标"
)
"""提示框的类型,决定了颜色和图标"""
title: str = Field(..., description="提示框的标题")
"""提示框的标题"""
content: str = Field(..., description="提示框的主要内容")
"""提示框的主要内容"""
show_icon: bool = Field(default=True, description="是否显示与类型匹配的图标")
"""是否显示与类型匹配的图标"""
@property
def template_name(self) -> str:
return "components/widgets/alert"
class Badge(RenderableComponent):
"""一个简单的徽章组件,用于显示状态或标签。"""
component_type: Literal["badge"] = "badge"
"""组件类型"""
text: str = Field(..., description="徽章上显示的文本")
"""徽章上显示的文本"""
color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field(
default="info",
description="预设的颜色方案",
)
"""预设的颜色方案"""
@property
def template_name(self) -> str:
return "components/widgets/badge"
class ProgressBar(RenderableComponent):
"""一个进度条组件。"""
component_type: Literal["progress_bar"] = "progress_bar"
"""组件类型"""
progress: float = Field(..., ge=0, le=100, description="进度百分比 (0-100)")
"""进度百分比 (0-100)"""
label: str | None = Field(default=None, description="显示在进度条上的可选文本")
"""显示在进度条上的可选文本"""
color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field(
default="primary",
description="预设的颜色方案",
)
"""预设的颜色方案"""
animated: bool = Field(default=False, description="是否显示动画效果")
"""是否显示动画效果"""
@property
def template_name(self) -> str:
return "components/widgets/progress_bar"
@@ -1,28 +0,0 @@
from typing import Literal
from pydantic import Field
from ..core.base import RenderableComponent
__all__ = ["ProgressBar"]
class ProgressBar(RenderableComponent):
"""一个进度条组件。"""
component_type: Literal["progress_bar"] = "progress_bar"
progress: float = Field(..., ge=0, le=100, description="进度百分比 (0-100)")
"""进度百分比 (0-100)"""
label: str | None = Field(default=None, description="显示在进度条上的可选文本")
"""显示在进度条上的可选文本"""
color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field(
default="primary",
description="预设的颜色方案",
)
"""预设的颜色方案"""
animated: bool = Field(default=False, description="是否显示动画效果")
"""是否显示动画效果"""
@property
def template_name(self) -> str:
return "components/widgets/progress_bar"
-36
View File
@@ -1,36 +0,0 @@
from typing import Literal
from pydantic import BaseModel, Field
from ..core.base import RenderableComponent
__all__ = ["Timeline", "TimelineItem"]
class TimelineItem(BaseModel):
"""时间轴中的单个事件点。"""
timestamp: str = Field(..., description="显示在时间点旁边的时间或标签")
"""显示在时间点旁边的时间或标签"""
title: str = Field(..., description="事件的标题")
"""事件的标题"""
content: str = Field(..., description="事件的详细描述")
"""事件的详细描述"""
icon: str | None = Field(default=None, description="可选的自定义图标SVG路径")
"""可选的自定义图标SVG路径"""
color: str | None = Field(default=None, description="可选的自定义颜色,覆盖默认")
"""可选的自定义颜色,覆盖默认"""
class Timeline(RenderableComponent):
"""一个垂直时间轴组件,用于按顺序展示事件。"""
component_type: Literal["timeline"] = "timeline"
items: list[TimelineItem] = Field(
default_factory=list, description="时间轴项目列表"
)
"""时间轴项目列表"""
@property
def template_name(self) -> str:
return "components/widgets/timeline"
@@ -1,27 +0,0 @@
from typing import Literal
from pydantic import Field
from ..core.base import RenderableComponent
__all__ = ["UserInfoBlock"]
class UserInfoBlock(RenderableComponent):
"""一个带头像、名称和副标题的用户信息块组件。"""
component_type: Literal["user_info_block"] = "user_info_block"
avatar_url: str = Field(..., description="用户头像的URL")
"""用户头像的URL"""
name: str = Field(..., description="用户的名称")
"""用户的名称"""
subtitle: str | None = Field(
default=None, description="显示在名称下方的副标题 (如UID或角色)"
)
"""显示在名称下方的副标题 (如UID或角色)"""
tags: list[str] = Field(default_factory=list, description="附加的标签列表")
"""附加的标签列表"""
@property
def template_name(self) -> str:
return "components/widgets/user_info_block"
+24 -20
View File
@@ -1,45 +1,48 @@
"""
核心模型模块
包含基础的数据模型类
"""
from .base import RenderableComponent
from .card import CardData
from .details import DetailsData, DetailsItem
from .layout import LayoutData, LayoutItem
from .list import ListData, ListItem
from .markdown import (
from .containers import (
CardData,
LayoutData,
LayoutItem,
ListData,
ListItem,
NotebookData,
NotebookElement,
TemplateComponent,
)
from .content import (
BaseCell,
CodeElement,
ComponentCell,
ComponentElement,
DetailsData,
DetailsItem,
HeadingElement,
ImageCell,
ImageElement,
ListElement,
ListItemElement,
MarkdownData,
MarkdownElement,
ProgressBarCell,
QuoteElement,
RawHtmlElement,
TableElement,
TextElement,
)
from .notebook import NotebookData, NotebookElement
from .table import (
BaseCell,
ComponentCell,
ImageCell,
RichTextCell,
StatusBadgeCell,
TableCell,
TableData,
TableElement,
TextCell,
TextData,
TextElement,
TextSpan,
)
from .template import TemplateComponent
from .text import TextData, TextSpan
__all__ = [
"BaseCell",
"CardData",
"CodeElement",
"ComponentCell",
"ComponentElement",
"DetailsData",
"DetailsItem",
"HeadingElement",
@@ -55,6 +58,7 @@ __all__ = [
"MarkdownElement",
"NotebookData",
"NotebookElement",
"ProgressBarCell",
"QuoteElement",
"RawHtmlElement",
"RenderableComponent",
+118 -23
View File
@@ -1,22 +1,45 @@
from abc import ABC, abstractmethod
from abc import ABC
from collections.abc import Awaitable, Iterable
from typing import Any
from typing_extensions import Self
from pydantic import BaseModel
from pydantic import VERSION as PYDANTIC_VERSION
from pydantic import BaseModel, Field
from zhenxun.services.renderer.protocols import Renderable
from zhenxun.services.renderer.types import Renderable
from zhenxun.utils.pydantic_compat import compat_computed_field, model_dump
__all__ = ["ContainerComponent", "RenderableComponent"]
def _iter_renderables(obj: Any) -> Iterable["Renderable"]:
"""
递归遍历对象,查找所有 Renderable 实例。
支持列表、字典以及嵌套的 Pydantic 模型。
"""
if isinstance(obj, Renderable):
yield obj
elif isinstance(obj, list | tuple):
for item in obj:
yield from _iter_renderables(item)
elif isinstance(obj, dict):
for value in obj.values():
yield from _iter_renderables(value)
elif isinstance(obj, BaseModel):
if PYDANTIC_VERSION.startswith("1"):
fields = obj.__fields__
else:
fields = obj.model_fields # type: ignore
for field_name in fields:
value = getattr(obj, field_name)
yield from _iter_renderables(value)
class RenderableComponent(BaseModel, Renderable):
"""
所有可渲染UI组件的数据模型基类。
它继承自 Pydantic 的 `BaseModel` 用于数据校验和结构化,同时实现了 `Renderable`
协议,确保其能够被 `RendererService` 正确处理。
它还提供了一些所有组件通用的样式属性,如 `inline_style`, `variant` 等。
提供通用的样式属性(如内联样式、CSS类、变体)和链式调用方法。
"""
_is_standalone_template: bool = False
@@ -29,24 +52,103 @@ class RenderableComponent(BaseModel, Renderable):
"""应用于组件根元素的额外CSS类名列表"""
variant: str | None = None
"""组件的变体/皮肤名称"""
style_name: str | None = None
"""组件的样式名称"""
is_page: bool = False
"""标记此组件是否为完整页面(自带html/body), 渲染时将跳过通用包装器"""
template_path: str | None = Field(
default=None, description="动态覆盖的模板路径", exclude=True
)
"""动态覆盖的模板路径,若设置则优先于 template_name 属性"""
@property
def template_name(self) -> str:
"""
返回用于渲染此组件的Jinja2模板的路径。
这是一个抽象属性,所有子类都必须覆盖它。
返回用于渲染此组件的 Jinja2 模板路径。
"""
raise NotImplementedError(
"Subclasses must implement the 'template_name' property."
)
return ""
def with_style(self, style_name: str) -> Self:
"""
设置组件样式名称。
参数:
style_name: 样式名称,通常对应主题中的一组CSS定义
"""
self.style_name = style_name
return self
def with_variant(self, variant: str) -> Self:
"""
设置组件变体(皮肤)。
参数:
variant: 变体名称,用于加载不同的模板或样式集
"""
self.variant = variant
return self
def with_classes(self, *classes: str) -> Self:
"""
添加 CSS 类名。
参数:
*classes: 一个或多个 CSS 类名
"""
if self.extra_classes is None:
self.extra_classes = []
self.extra_classes.extend(classes)
return self
def with_inline_style(self, style: dict[str, str]) -> Self:
"""
设置内联 CSS 样式。
参数:
style: 样式键值对字典 (e.g. {'color': 'red'})
"""
if self.inline_style is None:
self.inline_style = {}
self.inline_style.update(style)
return self
def with_component_css(self, css: str) -> Self:
"""
注入自定义 CSS 代码块。
参数:
css: CSS 代码字符串
"""
self.component_css = css
return self
def update(self, **kwargs) -> Self:
"""批量更新组件属性。"""
for k, v in kwargs.items():
if hasattr(self, k):
setattr(self, k, v)
return self
def build(self) -> Self:
"""
返回组件自身(兼容 Builder 模式调用)。
"""
return self
async def prepare(self) -> None:
"""[可选] 生命周期钩子,默认无操作。"""
"""[生命周期] 渲染前的异步准备步骤。"""
pass
def get_children(self) -> Iterable["RenderableComponent"]:
"""默认实现:非容器组件没有子组件。"""
return []
def get_children(self) -> Iterable["Renderable"]:
"""获取所有子组件的迭代器。"""
if PYDANTIC_VERSION.startswith("1"):
fields = self.__fields__
else:
fields = self.model_fields # type: ignore
for field_name in fields:
value = getattr(self, field_name)
yield from _iter_renderables(value)
def get_required_scripts(self) -> list[str]:
"""[可选] 返回此组件所需的JS脚本路径列表 (相对于assets目录)。"""
@@ -78,13 +180,6 @@ class ContainerComponent(RenderableComponent, ABC):
一个为容器类组件设计的抽象基类,封装了预渲染子组件的通用逻辑。
"""
@abstractmethod
def get_children(self) -> Iterable[RenderableComponent]:
"""
一个抽象方法,子类必须实现它来返回一个可迭代的子组件。
"""
raise NotImplementedError
def get_required_scripts(self) -> list[str]:
"""聚合所有子组件的脚本依赖。"""
scripts = set(super().get_required_scripts())
-27
View File
@@ -1,27 +0,0 @@
from collections.abc import Iterable
from .base import ContainerComponent, RenderableComponent
class CardData(ContainerComponent):
"""通用卡片的数据模型,可以包含头部、内容和尾部"""
header: RenderableComponent | None = None
"""卡片的头部内容组件"""
content: RenderableComponent
"""卡片的主要内容组件"""
footer: RenderableComponent | None = None
"""卡片的尾部内容组件"""
@property
def template_name(self) -> str:
return "components/core/card"
def get_children(self) -> Iterable[RenderableComponent]:
"""让CSS收集器能够遍历卡片的子组件"""
if self.header:
yield self.header
if self.content:
yield self.content
if self.footer:
yield self.footer
+294
View File
@@ -0,0 +1,294 @@
import builtins
from collections.abc import Iterable
from pathlib import Path
from typing import Any, Literal
from typing_extensions import Self
from pydantic import BaseModel, Field
from ...registry import component
from .base import ContainerComponent, Renderable, RenderableComponent
__all__ = [
"CardData",
"LayoutData",
"LayoutItem",
"ListData",
"ListItem",
"NotebookData",
"NotebookElement",
"TemplateComponent",
]
class TemplateComponent(RenderableComponent):
"""基于独立模板文件的UI组件"""
_is_standalone_template: bool = True
template_path: str | Path = Field(..., description="指向HTML模板文件的路径") # type: ignore
"""指向HTML模板文件的路径"""
data: dict[str, Any] = Field(..., description="传递给模板的上下文数据字典")
"""传递给模板的上下文数据字典"""
@property
def template_name(self) -> str:
if isinstance(self.template_path, Path):
return self.template_path.as_posix()
return str(self.template_path)
def get_render_data(self) -> dict[str, Any]:
return self.data
def __getattr__(self, name: str) -> Any:
try:
return self.data[name]
except KeyError:
raise AttributeError(
f"'{type(self).__name__}' 对象没有属性 '{name}'"
) from None
@component(name="card", namespace="core")
class CardData(ContainerComponent):
"""通用卡片的数据模型,可以包含头部、内容和尾部"""
header: RenderableComponent | None = None
content: RenderableComponent
footer: RenderableComponent | None = None
@property
def template_name(self) -> str:
return "components/core/card"
def get_children(self) -> Iterable["Renderable"]:
if self.header:
yield self.header
if self.content:
yield self.content
if self.footer:
yield self.footer
def set_header(self, header: "RenderableComponent") -> Self:
self.header = header
return self
def set_footer(self, footer: "RenderableComponent") -> Self:
self.footer = footer
return self
class LayoutItem(BaseModel):
"""布局中的单个项目"""
component: RenderableComponent = Field(..., description="要渲染的组件的数据模型")
"""要渲染的组件的数据模型"""
metadata: dict[str, Any] | None = Field(None, description="传递给模板的额外元数据")
"""传递给模板的额外元数据"""
class LayoutData(ContainerComponent):
"""布局构建器的数据模型"""
style_name: str | None = None
layout_type: str = "column"
children: list[LayoutItem] = Field(
default_factory=list, description="要布局的项目列表"
)
"""要布局的项目列表"""
options: dict[str, Any] = Field(
default_factory=dict, description="传递给模板的选项"
)
"""传递给模板的选项"""
@property
def template_name(self) -> str:
return f"components/core/layouts/{self.layout_type}"
@classmethod
def column(
cls, *, gap: str = "20px", align_items: str = "stretch", **options: Any
) -> Self:
options.update({"gap": gap, "align_items": align_items})
return cls(layout_type="column", options=options)
@classmethod
def row(
cls, *, gap: str = "10px", align_items: str = "center", **options: Any
) -> Self:
options.update({"gap": gap, "align_items": align_items})
return cls(layout_type="row", options=options)
@classmethod
def grid(cls, columns: int = 2, **options: Any) -> Self:
options.update({"columns": columns})
return cls(layout_type="grid", options=options)
def add_item(
self,
component: "RenderableComponent",
metadata: dict[str, Any] | None = None,
) -> Self:
self.children.append(LayoutItem(component=component, metadata=metadata))
return self
def add_option(self, key: str, value: Any) -> Self:
self.options[key] = value
return self
def get_children(self) -> Iterable["Renderable"]:
for item in self.children:
if item.component:
yield item.component
def get_extra_css(self, context: Any) -> str:
all_css = []
if self.component_css:
all_css.append(self.component_css)
for item in self.children:
if (
item.component
and hasattr(item.component, "component_css")
and item.component.component_css
):
all_css.append(item.component.component_css)
return "\n".join(all_css)
class ListItem(BaseModel):
"""列表中的单个项目"""
component: RenderableComponent = Field(..., description="要渲染的组件的数据模型")
"""要渲染的组件的数据模型"""
class ListData(ContainerComponent):
"""通用列表的数据模型"""
component_type: Literal["list"] = "list"
"""组件类型"""
items: list[ListItem] = Field(default_factory=list, description="列表项目")
"""列表项目"""
ordered: bool = Field(default=False, description="是否为有序列表")
"""是否为有序列表"""
@property
def template_name(self) -> str:
return "components/core/list"
def get_children(self) -> Iterable["Renderable"]:
for item in self.items:
if item.component:
yield item.component
def add_item(self, component: "RenderableComponent") -> Self:
self.items.append(ListItem(component=component))
return self
def set_ordered(self, ordered: bool = True) -> Self:
self.ordered = ordered
return self
class NotebookElement(BaseModel):
"""一个 Notebook 页面中的单个元素"""
type: Literal[
"heading",
"paragraph",
"image",
"blockquote",
"code",
"list",
"divider",
"component",
]
"""元素类型"""
text: str | None = None
"""文本内容"""
level: int | None = None
"""标题级别"""
src: str | None = None
"""图片链接"""
caption: str | None = None
"""图片说明"""
code: str | None = None
"""代码块内容"""
language: str | None = None
"""代码语言"""
data: list[str] | None = None
"""列表数据"""
ordered: bool | None = None
"""是否为有序列表"""
component: RenderableComponent | None = None
"""可渲染组件"""
class NotebookData(ContainerComponent):
"""Notebook转图片的数据模型"""
style_name: str | None = None
elements: list[NotebookElement]
@property
def template_name(self) -> str:
return "components/core/notebook"
def get_children(self) -> Iterable["Renderable"]:
for element in self.elements:
if element.component:
yield element.component
def text(self, text: str) -> Self:
self.elements.append(NotebookElement(type="paragraph", text=text))
return self
def head(self, text: str, level: int = 1) -> Self:
if not 1 <= level <= 4:
raise ValueError("标题级别必须在1-4之间")
self.elements.append(NotebookElement(type="heading", text=text, level=level))
return self
def image(self, content: str | Path, caption: str | None = None) -> Self:
src = ""
if isinstance(content, Path):
src = content.absolute().as_uri()
elif content.startswith("base64"):
src = f"data:image/png;base64,{content.split('base64://', 1)[-1]}"
else:
src = content
self.elements.append(NotebookElement(type="image", src=src, caption=caption))
return self
def quote(self, text: str | list[str]) -> Self:
if isinstance(text, str):
self.elements.append(NotebookElement(type="blockquote", text=text))
elif isinstance(text, list):
for t in text:
self.elements.append(NotebookElement(type="blockquote", text=t))
return self
def code(self, code: str, language: str = "python") -> Self:
self.elements.append(NotebookElement(type="code", code=code, language=language))
return self
def list(self, items: list[str], ordered: bool = False) -> Self:
self.elements.append(NotebookElement(type="list", data=items, ordered=ordered))
return self
def add_divider(self) -> Self:
self.elements.append(NotebookElement(type="divider"))
return self
def add_component(self, component: "RenderableComponent") -> Self:
self.elements.append(NotebookElement(type="component", component=component))
return self
def add_texts(self, texts: builtins.list[str]) -> Self:
for t in texts:
self.text(t)
return self
def add_quotes(self, quotes: builtins.list[str]) -> Self:
for q in quotes:
self.quote(q)
return self
+510
View File
@@ -0,0 +1,510 @@
from abc import ABC, abstractmethod
from contextlib import AbstractContextManager
from pathlib import Path
from typing import Any, Literal
from typing_extensions import Self
import aiofiles
from anyio import Path as AsyncPath
from pydantic import BaseModel, Field, PrivateAttr
from zhenxun.services.log import logger
from zhenxun.ui.models.components.feedback import ProgressBar
from .base import ContainerComponent, RenderableComponent
__all__ = [
"BaseCell",
"CodeElement",
"ComponentCell",
"ComponentElement",
"DetailsData",
"DetailsItem",
"HeadingElement",
"ImageCell",
"ImageElement",
"ListElement",
"ListItemElement",
"MarkdownData",
"MarkdownElement",
"ProgressBarCell",
"QuoteElement",
"RawHtmlElement",
"RichTextCell",
"StatusBadgeCell",
"TableCell",
"TableData",
"TableElement",
"TextCell",
"TextData",
"TextElement",
"TextSpan",
]
class TextSpan(BaseModel):
"""单个富文本片段的数据模型"""
text: str
"""文本内容"""
bold: bool = False
"""是否加粗"""
italic: bool = False
"""是否斜体"""
underline: bool = False
"""是否下划线"""
strikethrough: bool = False
"""是否删除线"""
code: bool = False
"""是否为等宽代码样式"""
color: str | None = None
"""文本颜色 (CSS color)"""
font_size: str | None = None
"""字体大小 (CSS font-size)"""
font_family: str | None = None
"""字体族 (CSS font-family)"""
class TextData(RenderableComponent):
"""轻量级富文本组件的数据模型"""
spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表")
"""文本片段列表"""
align: Literal["left", "right", "center"] = Field(
"left", description="整体文本对齐方式"
)
"""整体文本对齐方式"""
@property
def template_name(self) -> str:
return "components/core/text"
def set_alignment(self, align: Literal["left", "right", "center"]) -> Self:
self.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:
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.spans.append(span)
return self
class DetailsItem(BaseModel):
label: str = Field(..., description="项目的标签/键")
"""项目的标签/键"""
value: Any = Field(..., description="项目的值")
"""项目的值"""
class DetailsData(RenderableComponent):
"""描述列表(键值对)的数据模型"""
title: str | None = Field(None, description="列表的可选标题")
"""列表的可选标题"""
items: list[DetailsItem] = Field(default_factory=list, description="键值对项目列表")
"""键值对项目列表"""
@property
def template_name(self) -> str:
return "components/core/details"
def add_item(self, label: str, value: Any) -> Self:
self.items.append(DetailsItem(label=label, value=str(value)))
return self
class MarkdownElement(BaseModel, ABC):
@abstractmethod
def to_markdown(self) -> str:
pass
class TextElement(MarkdownElement):
type: Literal["text"] = "text"
"""元素类型"""
text: str
"""文本内容"""
def to_markdown(self) -> str:
return self.text
class HeadingElement(MarkdownElement):
type: Literal["heading"] = "heading"
"""元素类型"""
text: str
"""标题文本"""
level: int = Field(..., ge=1, le=6)
"""标题级别 (1-6)"""
def to_markdown(self) -> str:
return f"{'#' * self.level} {self.text}"
class ImageElement(MarkdownElement):
type: Literal["image"] = "image"
src: str
alt: str = "image"
def to_markdown(self) -> str:
return f"![{self.alt}]({self.src})"
class CodeElement(MarkdownElement):
type: Literal["code"] = "code"
code: str
language: str = ""
def to_markdown(self) -> str:
return f"```{self.language}\n{self.code}\n```"
class RawHtmlElement(MarkdownElement):
type: Literal["raw_html"] = "raw_html"
html: str
def to_markdown(self) -> str:
return self.html
class TableElement(MarkdownElement):
type: Literal["table"] = "table"
headers: list[str]
rows: list[list[str]]
alignments: list[Literal["left", "center", "right"]] | None = None
def to_markdown(self) -> str:
header_row = "| " + " | ".join(self.headers) + " |"
if self.alignments:
align_map = {"left": ":---", "center": ":---:", "right": "---:"}
separator_row = (
"| "
+ " | ".join([align_map.get(a, "---") for a in self.alignments])
+ " |"
)
else:
separator_row = "| " + " | ".join(["---"] * len(self.headers)) + " |"
data_rows = "\n".join(
"| " + " | ".join(map(str, row)) + " |" for row in self.rows
)
return f"{header_row}\n{separator_row}\n{data_rows}"
class ContainerElement(MarkdownElement):
content: list[MarkdownElement] = Field(default_factory=list)
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")])
class ListItemElement(ContainerElement):
def to_markdown(self) -> str:
return "\n".join(part.to_markdown() for part in self.content)
class ListElement(ContainerElement):
type: Literal["list"] = "list"
ordered: bool = False
def to_markdown(self) -> str:
lines = []
for i, item in enumerate(self.content):
if isinstance(item, ListItemElement):
prefix = f"{i + 1}." if self.ordered else "*"
item_content = item.to_markdown()
lines.append(f"{prefix} {item_content}")
return "\n".join(lines)
class ComponentElement(MarkdownElement):
type: Literal["component"] = "component"
component: RenderableComponent
def to_markdown(self) -> str:
return ""
class MarkdownData(ContainerComponent):
"""
Markdown组件数据模型。
支持链式调用构建内容,例如:
ui.markdown("").text("hello").code("print(1)")
"""
style_name: str | None = None
elements: list[MarkdownElement] = Field(default_factory=list)
"""Markdown元素列表"""
width: int = 800
"""渲染区域宽度"""
css_path: str | None = None
"""自定义CSS文件路径"""
_context_stack: list[Any] = PrivateAttr(default_factory=list)
@property
def template_name(self) -> str:
return "components/core/markdown"
async def get_extra_css(self, context: Any) -> str:
css_parts = []
if self.component_css:
css_parts.append(self.component_css)
if self.css_path:
css_file = Path(self.css_path)
if await AsyncPath(css_file).is_file():
async with aiofiles.open(css_file, encoding="utf-8") as f:
css_parts.append(await f.read())
else:
logger.warning(f"Markdown自定义CSS文件不存在: {self.css_path}")
else:
style_name = self.style_name or "light"
css_path = await context.theme_manager.resolve_markdown_style_path(
style_name, context
)
if css_path and css_path.exists():
async with aiofiles.open(css_path, encoding="utf-8") as f:
css_parts.append(await f.read())
return "\n".join(css_parts)
def set_width(self, width: int) -> Self:
self.width = width
return self
def set_css_path(self, css_path: str) -> Self:
self.css_path = css_path
return self
def _append_element(self, element: MarkdownElement) -> Self:
if self._context_stack:
self._context_stack[-1].content.append(element)
else:
self.elements.append(element)
return self
def text(self, text: str) -> Self:
return self._append_element(TextElement(text=text))
def head(self, text: str, level: int = 1) -> Self:
return self._append_element(HeadingElement(text=text, level=level))
def image(self, content: str | Path, alt: str = "image") -> Self:
src = ""
if isinstance(content, Path):
src = content.absolute().as_uri()
elif content.startswith("base64://"):
src = f"data:image/png;base64,{content.split('base64://', 1)[-1]}"
else:
src = content
return self._append_element(ImageElement(src=src, alt=alt))
def code(self, code: str, language: str = "") -> Self:
return self._append_element(CodeElement(code=code, language=language))
def table(
self,
headers: list[str],
rows: list[list[str]],
alignments: list[Any] | None = None,
) -> Self:
return self._append_element(
TableElement(headers=headers, rows=rows, alignments=alignments)
)
def add_divider(self) -> Self:
return self._append_element(RawHtmlElement(html="---"))
def add_component(self, component: "RenderableComponent") -> Self:
return self._append_element(ComponentElement(component=component))
class _ContextManager:
def __init__(self, model: "MarkdownData", element: Any):
self.model = model
self.element = element
def __enter__(self):
self.model._context_stack.append(self.element)
return self.model
def __exit__(self, exc_type, exc_val, exc_tb):
self.model._context_stack.pop()
def quote(self) -> AbstractContextManager["MarkdownData"]:
element = QuoteElement()
self._append_element(element)
return self._ContextManager(self, element)
def list(self, ordered: bool = False) -> AbstractContextManager["MarkdownData"]:
element = ListElement(ordered=ordered)
self._append_element(element)
return self._ContextManager(self, element)
def list_item(self) -> AbstractContextManager["MarkdownData"]:
if not self._context_stack or not isinstance(
self._context_stack[-1], ListElement
):
raise TypeError("list_item() 只能在 list() 上下文中使用。")
element = ListItemElement()
self._context_stack[-1].content.append(element)
return self._ContextManager(self, element)
class BaseCell(BaseModel):
type: str
class TextCell(BaseCell):
type: Literal["text"] = "text" # type: ignore
"""单元格类型"""
content: str
"""文本内容"""
bold: bool = False
"""是否加粗"""
color: str | None = None
"""文本颜色"""
class ImageCell(BaseCell):
type: Literal["image"] = "image" # type: ignore
"""单元格类型"""
src: str
"""图片链接"""
width: int = 40
"""显示宽度"""
height: int = 40
"""显示高度"""
shape: Literal["square", "circle"] = "square"
"""图片形状"""
alt: str = "image"
"""替换文本"""
class StatusBadgeCell(BaseCell):
type: Literal["badge"] = "badge" # type: ignore
"""单元格类型"""
text: str
"""徽章文本"""
status_type: Literal["ok", "error", "warning", "info", "success"] = "info"
"""状态类型,决定颜色"""
class ProgressBarCell(BaseCell, ProgressBar):
type: Literal["progress_bar"] = "progress_bar" # type: ignore
class RichTextCell(BaseCell):
type: Literal["rich_text"] = "rich_text" # type: ignore
"""单元格类型"""
spans: list[TextSpan] = Field(default_factory=list)
"""富文本片段列表"""
direction: Literal["column", "row"] = Field("column")
"""排列方向"""
gap: str = "4px"
"""项目间距"""
class ComponentCell(BaseCell):
type: str = "component"
component: RenderableComponent
TableCell = (
TextCell
| ImageCell
| StatusBadgeCell
| ProgressBarCell
| RichTextCell
| ComponentCell
| str
| int
| float
| None
)
class TableData(RenderableComponent):
style_name: str | None = None
title: str
"""表格标题"""
tip: str | None = None
"""标题旁的提示文本"""
headers: list[str] = Field(default_factory=list)
"""表格头字段列表"""
rows: list[list[TableCell]] = Field(default_factory=list)
"""数据行列表"""
column_alignments: list[Literal["left", "center", "right"]] | None = None
"""各列的对齐方式"""
column_widths: list[str | int] | None = None
"""各列的宽度限制"""
@property
def template_name(self) -> str:
return "components/core/table"
def set_headers(self, headers: list[str]) -> Self:
"""设置表格标题行"""
self.headers = headers
return self
def set_column_alignments(
self, alignments: list[Literal["left", "center", "right"]]
) -> Self:
"""设置列对齐方式"""
self.column_alignments = alignments
return self
def set_column_widths(self, widths: list[str | int]) -> Self:
"""设置列宽度"""
self.column_widths = widths
return self
def _normalize_cell(self, cell_data: Any) -> BaseCell:
"""将任意数据标准化为 TableCell 类型"""
if isinstance(cell_data, BaseCell):
return cell_data
if isinstance(cell_data, str | int | float):
return TextCell(content=str(cell_data))
if cell_data is None:
return TextCell(content="")
return TextCell(content=str(cell_data))
def add_row(self, row: list[Any]) -> Self:
"""添加单行数据"""
normalized_row = [self._normalize_cell(cell) for cell in row]
self.rows.append(normalized_row) # type: ignore
return self
def add_rows(self, rows: list[list[Any]]) -> Self:
"""批量添加多行数据"""
for row in rows:
self.add_row(row)
return self
-27
View File
@@ -1,27 +0,0 @@
from typing import Any
from pydantic import BaseModel, Field
from .base import RenderableComponent
class DetailsItem(BaseModel):
"""描述列表中的单个项目"""
label: str = Field(..., description="项目的标签/键")
"""项目的标签/键"""
value: Any = Field(..., description="项目的值")
"""项目的值"""
class DetailsData(RenderableComponent):
"""描述列表(键值对)的数据模型"""
title: str | None = Field(None, description="列表的可选标题")
"""列表的可选标题"""
items: list[DetailsItem] = Field(default_factory=list, description="键值对项目列表")
"""键值对项目列表"""
@property
def template_name(self) -> str:
return "components/core/details"
-58
View File
@@ -1,58 +0,0 @@
from collections.abc import Iterable
from typing import Any
from pydantic import BaseModel, Field
from .base import ContainerComponent, RenderableComponent
__all__ = ["LayoutData", "LayoutItem"]
class LayoutItem(BaseModel):
"""布局中的单个项目,现在持有可渲染组件的数据模型"""
component: RenderableComponent = Field(..., description="要渲染的组件的数据模型")
"""要渲染的组件的数据模型"""
metadata: dict[str, Any] | None = Field(None, description="传递给模板的额外元数据")
"""传递给模板的额外元数据"""
class LayoutData(ContainerComponent):
"""布局构建器的数据模型"""
style_name: str | None = None
"""应用于布局容器的样式名称"""
layout_type: str = "column"
"""布局类型 (如 'column', 'row', 'grid')"""
children: list[LayoutItem] = Field(
default_factory=list, description="要布局的项目列表"
)
"""要布局的项目列表"""
options: dict[str, Any] = Field(
default_factory=dict, description="传递给模板的选项"
)
"""传递给模板的选项"""
@property
def template_name(self) -> str:
return f"components/core/layouts/{self.layout_type}"
def get_extra_css(self, context: Any) -> str:
"""聚合所有子组件的 extra_css。"""
all_css = []
if self.component_css:
all_css.append(self.component_css)
for item in self.children:
if (
item.component
and hasattr(item.component, "component_css")
and item.component.component_css
):
all_css.append(item.component.component_css)
return "\n".join(all_css)
def get_children(self) -> Iterable[RenderableComponent]:
for item in self.children:
yield item.component
-33
View File
@@ -1,33 +0,0 @@
from collections.abc import Iterable
from typing import Literal
from pydantic import BaseModel, Field
from .base import ContainerComponent, RenderableComponent
__all__ = ["ListData", "ListItem"]
class ListItem(BaseModel):
"""列表中的单个项目,其内容可以是任何可渲染组件。"""
component: RenderableComponent = Field(..., description="要渲染的组件的数据模型")
"""要渲染的组件的数据模型"""
class ListData(ContainerComponent):
"""通用列表的数据模型,支持有序和无序列表。"""
component_type: Literal["list"] = "list"
items: list[ListItem] = Field(default_factory=list, description="列表项目")
"""列表项目"""
ordered: bool = Field(default=False, description="是否为有序列表")
"""是否为有序列表"""
@property
def template_name(self) -> str:
return "components/core/list"
def get_children(self) -> Iterable[RenderableComponent]:
for item in self.items:
yield item.component
-215
View File
@@ -1,215 +0,0 @@
from abc import ABC, abstractmethod
from collections.abc import Iterable
from pathlib import Path
from typing import Any, Literal
import aiofiles
from pydantic import BaseModel, Field
from zhenxun.services.log import logger
from .base import ContainerComponent, RenderableComponent
__all__ = [
"CodeElement",
"ComponentElement",
"HeadingElement",
"ImageElement",
"ListElement",
"ListItemElement",
"MarkdownData",
"MarkdownElement",
"QuoteElement",
"RawHtmlElement",
"TableElement",
"TextElement",
]
class MarkdownElement(BaseModel, ABC):
@abstractmethod
def to_markdown(self) -> str:
"""Serializes the element to its Markdown string representation."""
pass
class TextElement(MarkdownElement):
type: Literal["text"] = "text"
text: str
def to_markdown(self) -> str:
return self.text
class HeadingElement(MarkdownElement):
type: Literal["heading"] = "heading"
text: str
"""标题文本"""
level: int = Field(..., ge=1, le=6, description="标题级别 (1-6)")
"""标题级别 (1-6)"""
def to_markdown(self) -> str:
return f"{'#' * self.level} {self.text}"
class ImageElement(MarkdownElement):
type: Literal["image"] = "image"
src: str
"""图片来源 (URL或data URI)"""
alt: str = "image"
"""图片的替代文本"""
def to_markdown(self) -> str:
return f"![{self.alt}]({self.src})"
class CodeElement(MarkdownElement):
type: Literal["code"] = "code"
code: str
"""代码字符串"""
language: str = ""
"""代码语言,用于语法高亮"""
def to_markdown(self) -> str:
return f"```{self.language}\n{self.code}\n```"
class RawHtmlElement(MarkdownElement):
type: Literal["raw_html"] = "raw_html"
html: str
"""原始HTML字符串"""
def to_markdown(self) -> str:
return self.html
class TableElement(MarkdownElement):
type: Literal["table"] = "table"
headers: list[str]
"""表格的表头列表"""
rows: list[list[str]]
"""表格的数据行列表"""
alignments: list[Literal["left", "center", "right"]] | None = None
"""每列的对齐方式"""
def to_markdown(self) -> str:
header_row = "| " + " | ".join(self.headers) + " |"
if self.alignments:
align_map = {"left": ":---", "center": ":---:", "right": "---:"}
separator_row = (
"| "
+ " | ".join([align_map.get(a, "---") for a in self.alignments])
+ " |"
)
else:
separator_row = "| " + " | ".join(["---"] * len(self.headers)) + " |"
data_rows = "\n".join(
"| " + " | ".join(map(str, row)) + " |" for row in self.rows
)
return f"{header_row}\n{separator_row}\n{data_rows}"
class ContainerElement(MarkdownElement):
content: list[MarkdownElement] = Field(
default_factory=list, description="容器内包含的Markdown元素列表"
)
"""容器内包含的Markdown元素列表"""
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")])
class ListItemElement(ContainerElement):
def to_markdown(self) -> str:
return "\n".join(part.to_markdown() for part in self.content)
class ListElement(ContainerElement):
type: Literal["list"] = "list"
ordered: bool = False
"""是否为有序列表 (例如 1., 2.)"""
def to_markdown(self) -> str:
lines = []
for i, item in enumerate(self.content):
if isinstance(item, ListItemElement):
prefix = f"{i + 1}." if self.ordered else "*"
item_content = item.to_markdown()
lines.append(f"{prefix} {item_content}")
return "\n".join(lines)
class ComponentElement(MarkdownElement):
"""一个特殊的元素,用于在Markdown流中持有另一个可渲染组件。"""
type: Literal["component"] = "component"
component: RenderableComponent
"""嵌入在Markdown中的可渲染组件"""
def to_markdown(self) -> str:
return ""
class MarkdownData(ContainerComponent):
"""Markdown转图片的数据模型"""
style_name: str | None = None
"""Markdown内容的样式名称"""
elements: list[MarkdownElement] = Field(
default_factory=list, description="构成Markdown文档的元素列表"
)
"""构成Markdown文档的元素列表"""
width: int = 800
"""最终渲染图片的宽度"""
css_path: str | None = None
"""自定义CSS文件的绝对路径"""
@property
def template_name(self) -> str:
return "components/core/markdown"
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:
css_parts = []
if self.component_css:
css_parts.append(self.component_css)
if self.css_path:
css_file = Path(self.css_path)
if css_file.is_file():
async with aiofiles.open(css_file, encoding="utf-8") as f:
css_parts.append(await f.read())
else:
logger.warning(f"Markdown自定义CSS文件不存在: {self.css_path}")
else:
style_name = self.style_name or "light"
css_path = await context.theme_manager.resolve_markdown_style_path(
style_name, context
)
if css_path and css_path.exists():
async with aiofiles.open(css_path, encoding="utf-8") as f:
css_parts.append(await f.read())
return "\n".join(css_parts)
-59
View File
@@ -1,59 +0,0 @@
from collections.abc import Iterable
from typing import Literal
from pydantic import BaseModel
from .base import ContainerComponent, RenderableComponent
__all__ = ["NotebookData", "NotebookElement"]
class NotebookElement(BaseModel):
"""一个 Notebook 页面中的单个元素"""
type: Literal[
"heading",
"paragraph",
"image",
"blockquote",
"code",
"list",
"divider",
"component",
]
text: str | None = None
"""元素的文本内容 (用于标题、段落、引用)"""
level: int | None = None
"""标题的级别 (1-4)"""
src: str | None = None
"""图片的来源 (URL或data URI)"""
caption: str | None = None
"""图片的说明文字"""
code: str | None = None
"""代码块的内容"""
language: str | None = None
"""代码块的语言"""
data: list[str] | None = None
"""列表项的内容列表"""
ordered: bool | None = None
"""是否为有序列表"""
component: RenderableComponent | None = None
"""嵌入的自定义可渲染组件"""
class NotebookData(ContainerComponent):
"""Notebook转图片的数据模型"""
style_name: str | None = None
"""Notebook的样式名称"""
elements: list[NotebookElement]
"""构成Notebook页面的元素列表"""
@property
def template_name(self) -> str:
return "components/core/notebook"
def get_children(self) -> Iterable[RenderableComponent]:
for element in self.elements:
if element.type == "component" and element.component:
yield element.component
-119
View File
@@ -1,119 +0,0 @@
from typing import Literal
from pydantic import BaseModel, Field
from ...models.components.progress_bar import ProgressBar
from .base import RenderableComponent
from .text import TextSpan
__all__ = [
"BaseCell",
"ComponentCell",
"ImageCell",
"ProgressBarCell",
"RichTextCell",
"StatusBadgeCell",
"TableCell",
"TableData",
"TextCell",
]
class BaseCell(BaseModel):
"""单元格基础模型"""
type: str
class TextCell(BaseCell):
"""文本单元格"""
type: Literal["text"] = "text" # type: ignore
content: str
bold: bool = False
color: str | None = None
class ImageCell(BaseCell):
"""图片单元格"""
type: Literal["image"] = "image" # type: ignore
src: str
width: int = 40
height: int = 40
shape: Literal["square", "circle"] = "square"
alt: str = "image"
class StatusBadgeCell(BaseCell):
"""状态徽章单元格"""
type: Literal["badge"] = "badge" # type: ignore
text: str
status_type: Literal["ok", "error", "warning", "info"] = "info"
class ProgressBarCell(BaseCell, ProgressBar):
"""进度条单元格,继承ProgressBar模型以复用其字段"""
type: Literal["progress_bar"] = "progress_bar" # type: ignore
class RichTextCell(BaseCell):
"""富文本单元格,支持多个带样式的文本片段"""
type: Literal["rich_text"] = "rich_text" # type: ignore
spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表")
"""文本片段列表"""
direction: Literal["column", "row"] = Field("column", description="片段排列方向")
"""片段排列方向"""
gap: str = Field("4px", description="片段之间的间距")
"""片段之间的间距"""
class ComponentCell(BaseCell):
"""一个通用的单元格,可以容纳任何可渲染的组件。"""
type: str = "component"
component: RenderableComponent
TableCell = (
TextCell
| ImageCell
| StatusBadgeCell
| ProgressBarCell
| RichTextCell
| ComponentCell
| str
| int
| float
| None
)
class TableData(RenderableComponent):
"""通用表格的数据模型"""
style_name: str | None = None
"""应用于表格容器的样式名称"""
title: str = Field(..., description="表格主标题")
"""表格主标题"""
tip: str | None = Field(None, description="表格下方的提示信息")
"""表格下方的提示信息"""
headers: list[str] = Field(default_factory=list, description="表头列表")
"""表头列表"""
rows: list[list[TableCell]] = Field(default_factory=list, description="数据行列表")
"""数据行列表"""
column_alignments: list[Literal["left", "center", "right"]] | None = Field(
default=None, description="每列的对齐方式"
)
"""每列的对齐方式"""
column_widths: list[str | int] | None = Field(
default=None, description="每列的宽度 (e.g., ['50px', 'auto', 100])"
)
"""每列的宽度 (e.g., ['50px', 'auto', 100])"""
@property
def template_name(self) -> str:
return "components/core/table"
-39
View File
@@ -1,39 +0,0 @@
from pathlib import Path
from typing import Any
from pydantic import Field
from .base import RenderableComponent
__all__ = ["TemplateComponent"]
class TemplateComponent(RenderableComponent):
"""基于独立模板文件的UI组件"""
_is_standalone_template: bool = True
"""标记此组件为独立模板"""
template_path: str | Path = Field(..., description="指向HTML模板文件的路径")
"""指向HTML模板文件的路径"""
data: dict[str, Any] = Field(..., description="传递给模板的上下文数据字典")
"""传递给模板的上下文数据字典"""
@property
def template_name(self) -> str:
"""返回模板路径"""
if isinstance(self.template_path, Path):
return self.template_path.as_posix()
return str(self.template_path)
def get_render_data(self) -> dict[str, Any]:
"""返回传递给模板的数据"""
return self.data
def __getattr__(self, name: str) -> Any:
"""允许直接访问 `data` 字典中的属性。"""
try:
return self.data[name]
except KeyError:
raise AttributeError(
f"'{type(self).__name__}' 对象没有属性 '{name}'"
) from None
-34
View File
@@ -1,34 +0,0 @@
from typing import Literal
from pydantic import BaseModel, Field
from .base import RenderableComponent
class TextSpan(BaseModel):
"""单个富文本片段的数据模型"""
text: str
bold: bool = False
italic: bool = False
underline: bool = False
strikethrough: bool = False
code: bool = False
color: str | None = None
font_size: str | None = None
font_family: str | None = None
class TextData(RenderableComponent):
"""轻量级富文本组件的数据模型"""
spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表")
"""文本片段列表"""
align: Literal["left", "right", "center"] = Field(
"left", description="整体文本对齐方式"
)
"""整体文本对齐方式"""
@property
def template_name(self) -> str:
return "components/core/text"
+120
View File
@@ -0,0 +1,120 @@
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import ClassVar, TypeVar
from zhenxun.services.log import logger
from zhenxun.services.renderer.types import Renderable
T = TypeVar("T", bound=Renderable)
@dataclass
class ComponentEntry:
"""组件注册条目,存储类与元数据的关联"""
component_class: type[Renderable]
default_template: str | None
class ComponentRegistry:
"""
UI 组件注册中心。
负责管理组件类的索引,并自动处理模板命名空间的注册。
"""
_instance: ClassVar = None
_registry: ClassVar[dict[str, ComponentEntry]] = {}
_class_template_map: ClassVar[dict[type[Renderable], str]] = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def register(
cls,
name: str,
namespace: str = "core",
template: str | None = None,
template_root: Path | None = None,
) -> Callable[[type[T]], type[T]]:
"""
装饰器:注册一个 UI 组件。
参数:
name: 组件名称 (例如 'card')
namespace: 命名空间 (例如 'core' 或插件名)
template: (可选) 该组件默认绑定的模板路径 (例如 'components/card/main.html')
template_root: (可选) 该组件对应的模板根目录。
如果提供,会自动将其注册到 RendererService。
"""
def wrapper(component_cls: type[T]) -> type[T]:
full_key = f"{namespace}:{name}"
if full_key in cls._registry:
logger.warning(f"UI组件 '{full_key}' 已被注册,将被覆盖。")
cls._registry[full_key] = ComponentEntry(
component_class=component_cls, default_template=template
)
if template:
cls._class_template_map[component_cls] = template
if template_root:
if not template_root.exists():
logger.warning(
f"组件 '{full_key}' 提供的模板路径不存在: {template_root}"
)
else:
from zhenxun.services import renderer_service
renderer_service.register_template_namespace(
namespace, template_root
)
logger.debug(
f"已自动注册组件模板空间: @{namespace} -> {template_root}"
)
logger.trace(f"UI组件注册成功: {full_key} -> {component_cls.__name__}")
return component_cls
return wrapper
@classmethod
def get(cls, key: str) -> type[Renderable] | None:
"""根据 'namespace:name' 获取组件类。"""
entry = cls._registry.get(key)
return entry.component_class if entry else None
@classmethod
def get_template_for_class(cls, component_cls: type[Renderable]) -> str | None:
"""
根据组件类查找注册时绑定的默认模板。
支持子类继承查找 (可选,目前先做精确匹配)。
"""
return cls._class_template_map.get(component_cls)
@classmethod
def create(cls, key: str, **kwargs) -> Renderable:
"""
动态工厂方法。
参数:
key: 组件标识符 'namespace:name'
**kwargs: 传递给组件模型的初始化参数
"""
component_cls = cls.get(key)
if not component_cls:
if ":" not in key:
return cls.create(f"core:{key}", **kwargs)
raise ValueError(f"未找到 UI 组件: {key}")
return component_cls(**kwargs) # type: ignore
registry = ComponentRegistry()
component = registry.register
create = registry.create