mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-29 08:39:59 +08:00
* ♻️ 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>
198 lines
6.3 KiB
Python
198 lines
6.3 KiB
Python
from abc import ABC
|
|
from collections.abc import Awaitable, Iterable
|
|
from typing import Any
|
|
from typing_extensions import Self
|
|
|
|
from pydantic import VERSION as PYDANTIC_VERSION
|
|
from pydantic import BaseModel, Field
|
|
|
|
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组件的数据模型基类。
|
|
提供通用的样式属性(如内联样式、CSS类、变体)和链式调用方法。
|
|
"""
|
|
|
|
_is_standalone_template: bool = False
|
|
"""标记此组件是否为独立模板"""
|
|
inline_style: dict[str, str] | None = None
|
|
"""应用于组件根元素的内联CSS样式"""
|
|
component_css: str | None = None
|
|
"""注入到页面的额外CSS字符串"""
|
|
extra_classes: list[str] | None = None
|
|
"""应用于组件根元素的额外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 模板路径。
|
|
"""
|
|
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["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目录)。"""
|
|
return []
|
|
|
|
def get_required_styles(self) -> list[str]:
|
|
"""[可选] 返回此组件所需的CSS样式表路径列表 (相对于assets目录)。"""
|
|
return []
|
|
|
|
def get_render_data(self) -> dict[str, Any | Awaitable[Any]]:
|
|
"""默认实现,返回模型自身的数据字典。"""
|
|
return model_dump(
|
|
self, exclude={"inline_style", "component_css", "inline_style_str"}
|
|
)
|
|
|
|
@compat_computed_field
|
|
def inline_style_str(self) -> str:
|
|
"""一个辅助属性,将内联样式字典转换为CSS字符串"""
|
|
if not self.inline_style:
|
|
return ""
|
|
return "; ".join(f"{k}: {v}" for k, v in self.inline_style.items())
|
|
|
|
def get_extra_css(self, context: Any) -> str | Awaitable[str]:
|
|
return self.component_css or ""
|
|
|
|
|
|
class ContainerComponent(RenderableComponent, ABC):
|
|
"""
|
|
一个为容器类组件设计的抽象基类,封装了预渲染子组件的通用逻辑。
|
|
"""
|
|
|
|
def get_required_scripts(self) -> list[str]:
|
|
"""聚合所有子组件的脚本依赖。"""
|
|
scripts = set(super().get_required_scripts())
|
|
for child in self.get_children():
|
|
if child:
|
|
scripts.update(child.get_required_scripts())
|
|
return list(scripts)
|
|
|
|
def get_required_styles(self) -> list[str]:
|
|
"""聚合所有子组件的样式依赖。"""
|
|
styles = set(super().get_required_styles())
|
|
for child in self.get_children():
|
|
if child:
|
|
styles.update(child.get_required_styles())
|
|
return list(styles)
|