From c7ef6fdb17b4172ef52894d52d567a3ee73e5976 Mon Sep 17 00:00:00 2001 From: Rumio <32546670+webjoin111@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:31:49 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ui):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E6=9E=84=E5=BB=BA=E5=99=A8=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E7=BB=84=E4=BB=B6=E6=A8=A1=E5=9E=8B=E6=96=87=E6=A1=A3?= =?UTF-8?q?=20=20(#2048)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat(table): 添加 ComponentCell 以支持表格单元格中嵌入可渲染组件 * ✨ feat(ui): 增强表格构建器并完善组件模型文档 - 增强 `TableBuilder`,新增 `_normalize_cell` 辅助方法,支持自动将原生数据类型(如 `str`, `int`, `Path`)转换为 `TableCell` 模型,简化了表格行的创建。 - 完善 `zhenxun/ui/models` 目录下所有组件模型字段的 `description` 属性和文档字符串,显著提升了代码可读性和开发者体验。 - 优化 `shop/_data_source.py` 中 `gold_rank` 函数的平台路径判断格式,并统一 `my_props` 函数中图标路径的处理逻辑。 * :rotating_light: 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> --- zhenxun/builtin_plugins/shop/_data_source.py | 16 ++--- .../builtin_plugins/sign_in/_data_source.py | 2 +- zhenxun/ui/builders/core/table.py | 41 ++++++++++-- zhenxun/ui/models/__init__.py | 2 + zhenxun/ui/models/charts.py | 66 +++++++++++++++---- zhenxun/ui/models/components/alert.py | 4 ++ zhenxun/ui/models/components/avatar.py | 6 ++ zhenxun/ui/models/components/badge.py | 2 + zhenxun/ui/models/components/divider.py | 8 +++ zhenxun/ui/models/components/kpi_card.py | 6 ++ zhenxun/ui/models/components/progress_bar.py | 4 ++ zhenxun/ui/models/components/timeline.py | 6 ++ .../ui/models/components/user_info_block.py | 4 ++ zhenxun/ui/models/core/__init__.py | 2 + zhenxun/ui/models/core/base.py | 5 ++ zhenxun/ui/models/core/card.py | 3 + zhenxun/ui/models/core/details.py | 4 ++ zhenxun/ui/models/core/layout.py | 6 ++ zhenxun/ui/models/core/list.py | 3 + zhenxun/ui/models/core/markdown.py | 28 ++++++-- zhenxun/ui/models/core/notebook.py | 11 ++++ zhenxun/ui/models/core/table.py | 46 +++++++------ zhenxun/ui/models/core/template.py | 9 ++- zhenxun/ui/models/core/text.py | 2 + zhenxun/ui/models/presets/plugin_help_page.py | 10 +++ zhenxun/ui/models/presets/plugin_menu.py | 18 ++++- 26 files changed, 259 insertions(+), 55 deletions(-) diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py index ab957332..15f5935a 100644 --- a/zhenxun/builtin_plugins/shop/_data_source.py +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -132,7 +132,7 @@ async def gold_rank(session: Uninfo, group_id: str | None, num: int) -> bytes | else TextCell(content=""), TextCell(content=uid2name.get(user[0]) or user[0]), TextCell(content=str(user[1]), bold=True), - ImageCell(src=platform_path) + ImageCell(src=platform_path.resolve().as_uri()) if (platform_path := PLATFORM_PATH.get(platform)) else TextCell(content=""), ] @@ -529,18 +529,18 @@ class ShopManage: if not prop: continue - icon = "" + icon = None if prop.icon: icon_path = ICON_PATH / prop.icon - icon = icon_path if icon_path.exists() else "" + icon = icon_path if icon_path.exists() else None table_rows.append( [ - ImageCell(src=icon, height=33, width=33), - TextCell(content=i), - TextCell(content=prop.goods_name), - TextCell(content=user.props[prop_uuid]), - TextCell(content=prop.goods_description), + icon, + i, + prop.goods_name, + user.props[prop_uuid], + prop.goods_description, ] ) diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index d979c3d7..ec64083b 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -91,7 +91,7 @@ class SignManage: TextCell(content=uid2name.get(user[0]) or user[0]), TextCell(content=str(user[1]), bold=True), TextCell(content=str(user[2])), - ImageCell(src=platform_path) + ImageCell(src=platform_path.resolve().as_uri()) if (platform_path := PLATFORM_PATH.get(platform)) else TextCell(content=""), ] diff --git a/zhenxun/ui/builders/core/table.py b/zhenxun/ui/builders/core/table.py index 5b996203..f250ac0a 100644 --- a/zhenxun/ui/builders/core/table.py +++ b/zhenxun/ui/builders/core/table.py @@ -1,6 +1,13 @@ -from typing import Literal +from pathlib import Path +from typing import Any, Literal -from ...models.core.table import TableCell, TableData +from ...models.core.table import ( + BaseCell, + ImageCell, + TableCell, + TableData, + TextCell, +) from ..base import BaseBuilder __all__ = ["TableBuilder"] @@ -13,6 +20,28 @@ class TableBuilder(BaseBuilder[TableData]): 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": """ 设置表格的表头。 @@ -57,12 +86,13 @@ class TableBuilder(BaseBuilder[TableData]): 返回: TableBuilder: 当前构建器实例,以支持链式调用。 """ - self._data.rows.append(row) + 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: 一个包含多行数据的列表。 @@ -70,5 +100,6 @@ class TableBuilder(BaseBuilder[TableData]): 返回: TableBuilder: 当前构建器实例,以支持链式调用。 """ - self._data.rows.extend(rows) + for row in rows: + self.add_row(row) return self diff --git a/zhenxun/ui/models/__init__.py b/zhenxun/ui/models/__init__.py index 34fb418c..7f65faa6 100644 --- a/zhenxun/ui/models/__init__.py +++ b/zhenxun/ui/models/__init__.py @@ -12,6 +12,7 @@ from .components import ( from .core import ( BaseCell, CodeElement, + ComponentCell, HeadingElement, ImageCell, ImageElement, @@ -49,6 +50,7 @@ __all__ = [ "BaseCell", "BaseChartData", "CodeElement", + "ComponentCell", "Divider", "EChartsData", "HeadingElement", diff --git a/zhenxun/ui/models/charts.py b/zhenxun/ui/models/charts.py index e393bd7f..a30d21f0 100644 --- a/zhenxun/ui/models/charts.py +++ b/zhenxun/ui/models/charts.py @@ -11,44 +11,68 @@ from .core.base import RenderableComponent class EChartsTitle(BaseModel): text: str + """图表主标题""" left: Literal["left", "center", "right"] = "center" + """标题水平对齐方式""" class EChartsAxis(BaseModel): type: Literal["category", "value", "time", "log"] + """坐标轴类型""" data: list[Any] | None = None + """类目数据""" show: bool = True + """是否显示坐标轴""" class EChartsSeries(BaseModel): type: str + """系列类型 (e.g., 'bar', 'line', 'pie')""" data: list[Any] + """系列数据""" name: str | None = None + """系列名称,用于 tooltip 的显示""" label: dict[str, Any] | None = None + """图形上的文本标签""" itemStyle: dict[str, Any] | None = None + """图形样式""" barMaxWidth: int | None = None + """柱条的最大宽度""" smooth: bool | None = None + """是否平滑显示折线""" class EChartsTooltip(BaseModel): - trigger: Literal["item", "axis", "none"] = "item" + trigger: Literal["item", "axis", "none"] = Field("item", description="触发类型") + """触发类型""" class EChartsGrid(BaseModel): left: str | None = None + """grid 组件离容器左侧的距离""" right: str | None = None + """grid 组件离容器右侧的距离""" top: str | None = None + """grid 组件离容器上侧的距离""" bottom: str | None = None + """grid 组件离容器下侧的距离""" containLabel: bool = True + """grid 区域是否包含坐标轴的刻度标签""" class BaseChartData(RenderableComponent, ABC): """所有图表数据模型的基类""" style_name: str | None = None - chart_id: str = Field(default_factory=lambda: f"chart-{uuid.uuid4().hex}") + """组件的样式名称""" + chart_id: str = Field( + default_factory=lambda: f"chart-{uuid.uuid4().hex}", + description="图表的唯一ID,用于前端渲染", + ) + """图表的唯一ID,用于前端渲染""" echarts_options: dict[str, Any] | None = None + """原始ECharts选项,用于高级自定义""" @abstractmethod def build_option(self) -> dict[str, Any]: @@ -70,21 +94,37 @@ class BaseChartData(RenderableComponent, ABC): class EChartsData(BaseChartData): """统一的 ECharts 图表数据模型""" - template_path: str = Field(..., exclude=True) - title_model: EChartsTitle | None = Field(None, alias="title") - grid_model: EChartsGrid | None = Field(None, alias="grid") - tooltip_model: EChartsTooltip | None = Field(None, alias="tooltip") - x_axis_model: EChartsAxis | None = Field(None, alias="xAxis") - y_axis_model: EChartsAxis | None = Field(None, alias="yAxis") - series_models: list[EChartsSeries] = Field(default_factory=list, alias="series") - legend_model: dict[str, Any] | None = Field(default_factory=dict, alias="legend") + template_path: str = Field(..., exclude=True, description="图表组件的模板路径") + """图表组件的模板路径""" + title_model: EChartsTitle | None = Field( + None, alias="title", description="标题组件" + ) + """标题组件""" + grid_model: EChartsGrid | None = Field(None, alias="grid", description="网格组件") + """网格组件""" + tooltip_model: EChartsTooltip | None = Field( + None, alias="tooltip", description="提示框组件" + ) + """提示框组件""" + x_axis_model: EChartsAxis | None = Field(None, alias="xAxis", description="X轴配置") + """X轴配置""" + y_axis_model: EChartsAxis | None = Field(None, alias="yAxis", description="Y轴配置") + """Y轴配置""" + series_models: list[EChartsSeries] = Field( + default_factory=list, alias="series", description="系列列表" + ) + """系列列表""" + legend_model: dict[str, Any] | None = Field( + default_factory=dict, alias="legend", description="图例组件" + ) + """图例组件""" raw_options: dict[str, Any] = Field( default_factory=dict, description="用于 set_option 的原始覆盖选项" ) + """用于 set_option 的原始覆盖选项""" - background_image: str | None = Field( - None, description="【兼容】用于横向柱状图的背景图片" - ) + background_image: str | None = Field(None, description="用于横向柱状图的背景图片") + """用于横向柱状图的背景图片""" def build_option(self) -> dict[str, Any]: """将 Pydantic 模型序列化为 ECharts 的 option 字典。""" diff --git a/zhenxun/ui/models/components/alert.py b/zhenxun/ui/models/components/alert.py index d205d6a7..efd5a682 100644 --- a/zhenxun/ui/models/components/alert.py +++ b/zhenxun/ui/models/components/alert.py @@ -14,9 +14,13 @@ class Alert(RenderableComponent): 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: diff --git a/zhenxun/ui/models/components/avatar.py b/zhenxun/ui/models/components/avatar.py index 287cf1a7..5676ec24 100644 --- a/zhenxun/ui/models/components/avatar.py +++ b/zhenxun/ui/models/components/avatar.py @@ -12,8 +12,11 @@ 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: @@ -25,10 +28,13 @@ 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: diff --git a/zhenxun/ui/models/components/badge.py b/zhenxun/ui/models/components/badge.py index 08be7830..06128360 100644 --- a/zhenxun/ui/models/components/badge.py +++ b/zhenxun/ui/models/components/badge.py @@ -12,10 +12,12 @@ 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: diff --git a/zhenxun/ui/models/components/divider.py b/zhenxun/ui/models/components/divider.py index b7073938..acb5d542 100644 --- a/zhenxun/ui/models/components/divider.py +++ b/zhenxun/ui/models/components/divider.py @@ -12,9 +12,13 @@ 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: @@ -26,9 +30,13 @@ 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: diff --git a/zhenxun/ui/models/components/kpi_card.py b/zhenxun/ui/models/components/kpi_card.py index a3bf8704..836c1943 100644 --- a/zhenxun/ui/models/components/kpi_card.py +++ b/zhenxun/ui/models/components/kpi_card.py @@ -12,17 +12,23 @@ class KpiCard(RenderableComponent): component_type: Literal["kpi_card"] = "kpi_card" label: str = Field(..., description="指标的标签或名称") + """指标的标签或名称""" value: Any = Field(..., description="指标的主要数值") + """指标的主要数值""" unit: str | None = Field(default=None, description="数值的单位,可选") + """数值的单位,可选""" change: str | None = Field( default=None, description="与上一周期的变化,例如 '+15%' 或 '-100'" ) + """与上一周期的变化,例如 '+15%' 或 '-100'""" change_type: Literal["positive", "negative", "neutral"] = Field( default="neutral", description="变化的类型,用于决定颜色" ) + """变化的类型,用于决定颜色""" icon_svg: str | None = Field( default=None, description="卡片中显示的可选图标 (SVG path data)" ) + """卡片中显示的可选图标 (SVG path data)""" @property def template_name(self) -> str: diff --git a/zhenxun/ui/models/components/progress_bar.py b/zhenxun/ui/models/components/progress_bar.py index 83c5b759..1bde9a0f 100644 --- a/zhenxun/ui/models/components/progress_bar.py +++ b/zhenxun/ui/models/components/progress_bar.py @@ -12,12 +12,16 @@ 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: diff --git a/zhenxun/ui/models/components/timeline.py b/zhenxun/ui/models/components/timeline.py index 822313e0..d48b8b83 100644 --- a/zhenxun/ui/models/components/timeline.py +++ b/zhenxun/ui/models/components/timeline.py @@ -11,10 +11,15 @@ 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): @@ -24,6 +29,7 @@ class Timeline(RenderableComponent): items: list[TimelineItem] = Field( default_factory=list, description="时间轴项目列表" ) + """时间轴项目列表""" @property def template_name(self) -> str: diff --git a/zhenxun/ui/models/components/user_info_block.py b/zhenxun/ui/models/components/user_info_block.py index 3a40ef24..20762c8f 100644 --- a/zhenxun/ui/models/components/user_info_block.py +++ b/zhenxun/ui/models/components/user_info_block.py @@ -12,11 +12,15 @@ 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: diff --git a/zhenxun/ui/models/core/__init__.py b/zhenxun/ui/models/core/__init__.py index e19ed058..94ae517e 100644 --- a/zhenxun/ui/models/core/__init__.py +++ b/zhenxun/ui/models/core/__init__.py @@ -24,6 +24,7 @@ from .markdown import ( from .notebook import NotebookData, NotebookElement from .table import ( BaseCell, + ComponentCell, ImageCell, RichTextCell, StatusBadgeCell, @@ -38,6 +39,7 @@ __all__ = [ "BaseCell", "CardData", "CodeElement", + "ComponentCell", "DetailsData", "DetailsItem", "HeadingElement", diff --git a/zhenxun/ui/models/core/base.py b/zhenxun/ui/models/core/base.py index 0858ee16..09d14862 100644 --- a/zhenxun/ui/models/core/base.py +++ b/zhenxun/ui/models/core/base.py @@ -20,10 +20,15 @@ class RenderableComponent(BaseModel, Renderable): """ _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 + """组件的变体/皮肤名称""" @property def template_name(self) -> str: diff --git a/zhenxun/ui/models/core/card.py b/zhenxun/ui/models/core/card.py index 462ee6f6..3ceb4a14 100644 --- a/zhenxun/ui/models/core/card.py +++ b/zhenxun/ui/models/core/card.py @@ -7,8 +7,11 @@ class CardData(ContainerComponent): """通用卡片的数据模型,可以包含头部、内容和尾部""" header: RenderableComponent | None = None + """卡片的头部内容组件""" content: RenderableComponent + """卡片的主要内容组件""" footer: RenderableComponent | None = None + """卡片的尾部内容组件""" @property def template_name(self) -> str: diff --git a/zhenxun/ui/models/core/details.py b/zhenxun/ui/models/core/details.py index de324234..abd83eed 100644 --- a/zhenxun/ui/models/core/details.py +++ b/zhenxun/ui/models/core/details.py @@ -9,14 +9,18 @@ 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: diff --git a/zhenxun/ui/models/core/layout.py b/zhenxun/ui/models/core/layout.py index 0fb078a0..1c850b17 100644 --- a/zhenxun/ui/models/core/layout.py +++ b/zhenxun/ui/models/core/layout.py @@ -12,20 +12,26 @@ 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: diff --git a/zhenxun/ui/models/core/list.py b/zhenxun/ui/models/core/list.py index 97bd7b2c..880cf9bc 100644 --- a/zhenxun/ui/models/core/list.py +++ b/zhenxun/ui/models/core/list.py @@ -12,6 +12,7 @@ class ListItem(BaseModel): """列表中的单个项目,其内容可以是任何可渲染组件。""" component: RenderableComponent = Field(..., description="要渲染的组件的数据模型") + """要渲染的组件的数据模型""" class ListData(ContainerComponent): @@ -19,7 +20,9 @@ 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: diff --git a/zhenxun/ui/models/core/markdown.py b/zhenxun/ui/models/core/markdown.py index 614ccf52..4eabd06f 100644 --- a/zhenxun/ui/models/core/markdown.py +++ b/zhenxun/ui/models/core/markdown.py @@ -44,7 +44,9 @@ class TextElement(MarkdownElement): class HeadingElement(MarkdownElement): type: Literal["heading"] = "heading" text: str - level: int = Field(..., ge=1, le=6) + """标题文本""" + level: int = Field(..., ge=1, le=6, description="标题级别 (1-6)") + """标题级别 (1-6)""" def to_markdown(self) -> str: return f"{'#' * self.level} {self.text}" @@ -53,7 +55,9 @@ class HeadingElement(MarkdownElement): 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})" @@ -62,7 +66,9 @@ class ImageElement(MarkdownElement): class CodeElement(MarkdownElement): type: Literal["code"] = "code" code: str + """代码字符串""" language: str = "" + """代码语言,用于语法高亮""" def to_markdown(self) -> str: return f"```{self.language}\n{self.code}\n```" @@ -71,6 +77,7 @@ class CodeElement(MarkdownElement): class RawHtmlElement(MarkdownElement): type: Literal["raw_html"] = "raw_html" html: str + """原始HTML字符串""" def to_markdown(self) -> str: return self.html @@ -79,8 +86,11 @@ class RawHtmlElement(MarkdownElement): class TableElement(MarkdownElement): type: Literal["table"] = "table" headers: list[str] + """表格的表头列表""" rows: list[list[str]] + """表格的数据行列表""" alignments: list[Literal["left", "center", "right"]] | None = None + """每列的对齐方式""" def to_markdown(self) -> str: header_row = "| " + " | ".join(self.headers) + " |" @@ -102,7 +112,10 @@ class TableElement(MarkdownElement): class ContainerElement(MarkdownElement): - content: list[MarkdownElement] = Field(default_factory=list) + content: list[MarkdownElement] = Field( + default_factory=list, description="容器内包含的Markdown元素列表" + ) + """容器内包含的Markdown元素列表""" class QuoteElement(ContainerElement): @@ -121,6 +134,7 @@ class ListItemElement(ContainerElement): class ListElement(ContainerElement): type: Literal["list"] = "list" ordered: bool = False + """是否为有序列表 (例如 1., 2.)""" def to_markdown(self) -> str: lines = [] @@ -137,6 +151,7 @@ class ComponentElement(MarkdownElement): type: Literal["component"] = "component" component: RenderableComponent + """嵌入在Markdown中的可渲染组件""" def to_markdown(self) -> str: return "" @@ -146,9 +161,15 @@ class MarkdownData(ContainerComponent): """Markdown转图片的数据模型""" style_name: str | None = None - elements: list[MarkdownElement] = Field(default_factory=list) + """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: @@ -180,7 +201,6 @@ class MarkdownData(ContainerComponent): 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 ) diff --git a/zhenxun/ui/models/core/notebook.py b/zhenxun/ui/models/core/notebook.py index 2038cc17..2c62ccae 100644 --- a/zhenxun/ui/models/core/notebook.py +++ b/zhenxun/ui/models/core/notebook.py @@ -22,21 +22,32 @@ class NotebookElement(BaseModel): "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: diff --git a/zhenxun/ui/models/core/table.py b/zhenxun/ui/models/core/table.py index daef088b..c124ab25 100644 --- a/zhenxun/ui/models/core/table.py +++ b/zhenxun/ui/models/core/table.py @@ -1,8 +1,6 @@ -from pathlib import Path from typing import Literal -from nonebot.compat import field_validator -from pydantic import BaseModel +from pydantic import BaseModel, Field from ...models.components.progress_bar import ProgressBar from .base import RenderableComponent @@ -10,6 +8,7 @@ from .text import TextSpan __all__ = [ "BaseCell", + "ComponentCell", "ImageCell", "ProgressBarCell", "RichTextCell", @@ -30,7 +29,7 @@ class TextCell(BaseCell): """文本单元格""" type: Literal["text"] = "text" # type: ignore - content: str | float + content: str bold: bool = False color: str | None = None @@ -39,18 +38,12 @@ class ImageCell(BaseCell): """图片单元格""" type: Literal["image"] = "image" # type: ignore - src: str | Path + src: str width: int = 40 height: int = 40 shape: Literal["square", "circle"] = "square" alt: str = "image" - @field_validator("src", mode="before") - def validate_src(cls, v: str) -> str: - if isinstance(v, Path): - v = v.resolve().as_uri() - return v - class StatusBadgeCell(BaseCell): """状态徽章单元格""" @@ -70,20 +63,28 @@ class RichTextCell(BaseCell): """富文本单元格,支持多个带样式的文本片段""" type: Literal["rich_text"] = "rich_text" # type: ignore - spans: list[TextSpan] = [] + spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表") """文本片段列表""" - direction: Literal["column", "row"] = "column" + direction: Literal["column", "row"] = Field("column", description="片段排列方向") """片段排列方向""" - gap: str = "4px" + gap: str = Field("4px", description="片段之间的间距") """片段之间的间距""" +class ComponentCell(BaseCell): + """一个通用的单元格,可以容纳任何可渲染的组件。""" + + type: str = "component" + component: RenderableComponent + + TableCell = ( TextCell | ImageCell | StatusBadgeCell | ProgressBarCell | RichTextCell + | ComponentCell | str | int | float @@ -95,17 +96,22 @@ class TableData(RenderableComponent): """通用表格的数据模型""" style_name: str | None = None - title: str + """应用于表格容器的样式名称""" + title: str = Field(..., description="表格主标题") """表格主标题""" - tip: str | None = None + tip: str | None = Field(None, description="表格下方的提示信息") """表格下方的提示信息""" - headers: list[str] = [] # noqa: RUF012 + headers: list[str] = Field(default_factory=list, description="表头列表") """表头列表""" - rows: list[list[TableCell]] = [] # noqa: RUF012 + rows: list[list[TableCell]] = Field(default_factory=list, description="数据行列表") """数据行列表""" - column_alignments: list[Literal["left", "center", "right"]] | None = None + column_alignments: list[Literal["left", "center", "right"]] | None = Field( + default=None, description="每列的对齐方式" + ) """每列的对齐方式""" - column_widths: list[str | int] | None = None + column_widths: list[str | int] | None = Field( + default=None, description="每列的宽度 (e.g., ['50px', 'auto', 100])" + ) """每列的宽度 (e.g., ['50px', 'auto', 100])""" @property diff --git a/zhenxun/ui/models/core/template.py b/zhenxun/ui/models/core/template.py index 62723a4e..82169d00 100644 --- a/zhenxun/ui/models/core/template.py +++ b/zhenxun/ui/models/core/template.py @@ -1,6 +1,8 @@ from pathlib import Path from typing import Any +from pydantic import Field + from .base import RenderableComponent __all__ = ["TemplateComponent"] @@ -10,8 +12,11 @@ class TemplateComponent(RenderableComponent): """基于独立模板文件的UI组件""" _is_standalone_template: bool = True - template_path: str | Path - data: dict[str, Any] + """标记此组件为独立模板""" + template_path: str | Path = Field(..., description="指向HTML模板文件的路径") + """指向HTML模板文件的路径""" + data: dict[str, Any] = Field(..., description="传递给模板的上下文数据字典") + """传递给模板的上下文数据字典""" @property def template_name(self) -> str: diff --git a/zhenxun/ui/models/core/text.py b/zhenxun/ui/models/core/text.py index 2647d035..5e849b67 100644 --- a/zhenxun/ui/models/core/text.py +++ b/zhenxun/ui/models/core/text.py @@ -23,9 +23,11 @@ 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: diff --git a/zhenxun/ui/models/presets/plugin_help_page.py b/zhenxun/ui/models/presets/plugin_help_page.py index dfc013ab..c7227274 100644 --- a/zhenxun/ui/models/presets/plugin_help_page.py +++ b/zhenxun/ui/models/presets/plugin_help_page.py @@ -13,25 +13,35 @@ class HelpItem(BaseModel): """帮助菜单中的单个功能项""" name: str + """功能名称""" description: str + """功能描述""" usage: str + """功能用法说明""" class HelpCategory(BaseModel): """帮助菜单中的一个功能类别""" title: str + """分类标题""" icon_svg_path: str + """分类图标的SVG路径数据""" items: list[HelpItem] + """该分类下的功能项列表""" class PluginHelpPageData(RenderableComponent): """通用插件帮助页面的数据模型""" style_name: str | None = None + """页面样式名称""" bot_nickname: str + """机器人昵称""" page_title: str + """页面主标题""" categories: list[HelpCategory] + """帮助分类列表""" @property def template_name(self) -> str: diff --git a/zhenxun/ui/models/presets/plugin_menu.py b/zhenxun/ui/models/presets/plugin_menu.py index 0b42105e..8fe9df56 100644 --- a/zhenxun/ui/models/presets/plugin_menu.py +++ b/zhenxun/ui/models/presets/plugin_menu.py @@ -13,29 +13,43 @@ class PluginMenuItem(BaseModel): """插件菜单中的单个插件项""" id: str + """插件的唯一ID""" name: str + """插件名称""" status: bool + """插件在当前群组的开关状态""" has_superuser_help: bool - commands: list[str] = Field(default_factory=list) + """插件是否有超级用户专属帮助""" + commands: list[str] = Field(default_factory=list, description="插件的主要命令列表") + """插件的主要命令列表""" class PluginMenuCategory(BaseModel): """插件菜单中的一个分类""" name: str - items: list[PluginMenuItem] + """插件分类名称""" + items: list[PluginMenuItem] = Field(..., description="该分类下的插件项列表") + """该分类下的插件项列表""" class PluginMenuData(RenderableComponent): """通用插件帮助菜单的数据模型""" style_name: str | None = None + """页面样式名称""" bot_name: str + """机器人名称""" bot_avatar_url: str + """机器人头像URL""" is_detail: bool + """是否为详细菜单模式""" plugin_count: int + """总插件数量""" active_count: int + """已启用插件数量""" categories: list[PluginMenuCategory] + """插件分类列表""" @property def template_name(self) -> str: