mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-08 21:30:01 +08:00
♻️ 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:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bc8e1659ae
commit
5e30694663
+278
-56
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user