feat(ui): 增强表格构建器并完善组件模型文档 (#2048)
Some checks failed
检查bot是否运行正常 / bot check (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Sequential Lint and Type Check / ruff-call (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Force Sync to Aliyun / sync (push) Has been cancelled
Update Version / update-version (push) Has been cancelled
Sequential Lint and Type Check / pyright-call (push) Has been cancelled

*  feat(table): 添加 ComponentCell 以支持表格单元格中嵌入可渲染组件

*  feat(ui): 增强表格构建器并完善组件模型文档

- 增强 `TableBuilder`,新增 `_normalize_cell` 辅助方法,支持自动将原生数据类型(如 `str`, `int`, `Path`)转换为 `TableCell` 模型,简化了表格行的创建。
- 完善 `zhenxun/ui/models` 目录下所有组件模型字段的 `description` 属性和文档字符串,显著提升了代码可读性和开发者体验。
- 优化 `shop/_data_source.py` 中 `gold_rank` 函数的平台路径判断格式,并统一 `my_props` 函数中图标路径的处理逻辑。

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio 2025-09-11 10:31:49 +08:00 committed by GitHub
parent fb0a9813e1
commit c7ef6fdb17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 259 additions and 55 deletions

View File

@ -132,7 +132,7 @@ async def gold_rank(session: Uninfo, group_id: str | None, num: int) -> bytes |
else TextCell(content=""), else TextCell(content=""),
TextCell(content=uid2name.get(user[0]) or user[0]), TextCell(content=uid2name.get(user[0]) or user[0]),
TextCell(content=str(user[1]), bold=True), 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)) if (platform_path := PLATFORM_PATH.get(platform))
else TextCell(content=""), else TextCell(content=""),
] ]
@ -529,18 +529,18 @@ class ShopManage:
if not prop: if not prop:
continue continue
icon = "" icon = None
if prop.icon: if prop.icon:
icon_path = ICON_PATH / 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( table_rows.append(
[ [
ImageCell(src=icon, height=33, width=33), icon,
TextCell(content=i), i,
TextCell(content=prop.goods_name), prop.goods_name,
TextCell(content=user.props[prop_uuid]), user.props[prop_uuid],
TextCell(content=prop.goods_description), prop.goods_description,
] ]
) )

View File

@ -91,7 +91,7 @@ class SignManage:
TextCell(content=uid2name.get(user[0]) or user[0]), TextCell(content=uid2name.get(user[0]) or user[0]),
TextCell(content=str(user[1]), bold=True), TextCell(content=str(user[1]), bold=True),
TextCell(content=str(user[2])), TextCell(content=str(user[2])),
ImageCell(src=platform_path) ImageCell(src=platform_path.resolve().as_uri())
if (platform_path := PLATFORM_PATH.get(platform)) if (platform_path := PLATFORM_PATH.get(platform))
else TextCell(content=""), else TextCell(content=""),
] ]

View File

@ -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 from ..base import BaseBuilder
__all__ = ["TableBuilder"] __all__ = ["TableBuilder"]
@ -13,6 +20,28 @@ class TableBuilder(BaseBuilder[TableData]):
data_model = TableData(title=title, tip=tip, headers=[], rows=[]) data_model = TableData(title=title, tip=tip, headers=[], rows=[])
super().__init__(data_model, template_name="components/core/table") 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": def set_headers(self, headers: list[str]) -> "TableBuilder":
""" """
设置表格的表头 设置表格的表头
@ -57,12 +86,13 @@ class TableBuilder(BaseBuilder[TableData]):
返回: 返回:
TableBuilder: 当前构建器实例以支持链式调用 TableBuilder: 当前构建器实例以支持链式调用
""" """
self._data.rows.append(row) normalized_row = [self._normalize_cell(cell) for cell in row]
self._data.rows.append(normalized_row)
return self return self
def add_rows(self, rows: list[list[TableCell]]) -> "TableBuilder": def add_rows(self, rows: list[list[TableCell]]) -> "TableBuilder":
""" """
向表格中批量添加多行数据 向表格中批量添加多行数据, 并自动转换原生类型
参数: 参数:
rows: 一个包含多行数据的列表 rows: 一个包含多行数据的列表
@ -70,5 +100,6 @@ class TableBuilder(BaseBuilder[TableData]):
返回: 返回:
TableBuilder: 当前构建器实例以支持链式调用 TableBuilder: 当前构建器实例以支持链式调用
""" """
self._data.rows.extend(rows) for row in rows:
self.add_row(row)
return self return self

View File

@ -12,6 +12,7 @@ from .components import (
from .core import ( from .core import (
BaseCell, BaseCell,
CodeElement, CodeElement,
ComponentCell,
HeadingElement, HeadingElement,
ImageCell, ImageCell,
ImageElement, ImageElement,
@ -49,6 +50,7 @@ __all__ = [
"BaseCell", "BaseCell",
"BaseChartData", "BaseChartData",
"CodeElement", "CodeElement",
"ComponentCell",
"Divider", "Divider",
"EChartsData", "EChartsData",
"HeadingElement", "HeadingElement",

View File

@ -11,44 +11,68 @@ from .core.base import RenderableComponent
class EChartsTitle(BaseModel): class EChartsTitle(BaseModel):
text: str text: str
"""图表主标题"""
left: Literal["left", "center", "right"] = "center" left: Literal["left", "center", "right"] = "center"
"""标题水平对齐方式"""
class EChartsAxis(BaseModel): class EChartsAxis(BaseModel):
type: Literal["category", "value", "time", "log"] type: Literal["category", "value", "time", "log"]
"""坐标轴类型"""
data: list[Any] | None = None data: list[Any] | None = None
"""类目数据"""
show: bool = True show: bool = True
"""是否显示坐标轴"""
class EChartsSeries(BaseModel): class EChartsSeries(BaseModel):
type: str type: str
"""系列类型 (e.g., 'bar', 'line', 'pie')"""
data: list[Any] data: list[Any]
"""系列数据"""
name: str | None = None name: str | None = None
"""系列名称,用于 tooltip 的显示"""
label: dict[str, Any] | None = None label: dict[str, Any] | None = None
"""图形上的文本标签"""
itemStyle: dict[str, Any] | None = None itemStyle: dict[str, Any] | None = None
"""图形样式"""
barMaxWidth: int | None = None barMaxWidth: int | None = None
"""柱条的最大宽度"""
smooth: bool | None = None smooth: bool | None = None
"""是否平滑显示折线"""
class EChartsTooltip(BaseModel): class EChartsTooltip(BaseModel):
trigger: Literal["item", "axis", "none"] = "item" trigger: Literal["item", "axis", "none"] = Field("item", description="触发类型")
"""触发类型"""
class EChartsGrid(BaseModel): class EChartsGrid(BaseModel):
left: str | None = None left: str | None = None
"""grid 组件离容器左侧的距离"""
right: str | None = None right: str | None = None
"""grid 组件离容器右侧的距离"""
top: str | None = None top: str | None = None
"""grid 组件离容器上侧的距离"""
bottom: str | None = None bottom: str | None = None
"""grid 组件离容器下侧的距离"""
containLabel: bool = True containLabel: bool = True
"""grid 区域是否包含坐标轴的刻度标签"""
class BaseChartData(RenderableComponent, ABC): class BaseChartData(RenderableComponent, ABC):
"""所有图表数据模型的基类""" """所有图表数据模型的基类"""
style_name: str | None = None 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_options: dict[str, Any] | None = None
"""原始ECharts选项用于高级自定义"""
@abstractmethod @abstractmethod
def build_option(self) -> dict[str, Any]: def build_option(self) -> dict[str, Any]:
@ -70,21 +94,37 @@ class BaseChartData(RenderableComponent, ABC):
class EChartsData(BaseChartData): class EChartsData(BaseChartData):
"""统一的 ECharts 图表数据模型""" """统一的 ECharts 图表数据模型"""
template_path: str = Field(..., exclude=True) template_path: str = Field(..., exclude=True, description="图表组件的模板路径")
title_model: EChartsTitle | None = Field(None, alias="title") """图表组件的模板路径"""
grid_model: EChartsGrid | None = Field(None, alias="grid") title_model: EChartsTitle | None = Field(
tooltip_model: EChartsTooltip | None = Field(None, alias="tooltip") None, alias="title", description="标题组件"
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") grid_model: EChartsGrid | None = Field(None, alias="grid", description="网格组件")
legend_model: dict[str, Any] | None = Field(default_factory=dict, alias="legend") """网格组件"""
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( raw_options: dict[str, Any] = Field(
default_factory=dict, description="用于 set_option 的原始覆盖选项" default_factory=dict, description="用于 set_option 的原始覆盖选项"
) )
"""用于 set_option 的原始覆盖选项"""
background_image: str | None = Field( background_image: str | None = Field(None, description="用于横向柱状图的背景图片")
None, description="【兼容】用于横向柱状图的背景图片" """用于横向柱状图的背景图片"""
)
def build_option(self) -> dict[str, Any]: def build_option(self) -> dict[str, Any]:
"""将 Pydantic 模型序列化为 ECharts 的 option 字典。""" """将 Pydantic 模型序列化为 ECharts 的 option 字典。"""

View File

@ -14,9 +14,13 @@ class Alert(RenderableComponent):
type: Literal["info", "success", "warning", "error"] = Field( type: Literal["info", "success", "warning", "error"] = Field(
default="info", description="提示框的类型,决定了颜色和图标" default="info", description="提示框的类型,决定了颜色和图标"
) )
"""提示框的类型,决定了颜色和图标"""
title: str = Field(..., description="提示框的标题") title: str = Field(..., description="提示框的标题")
"""提示框的标题"""
content: str = Field(..., description="提示框的主要内容") content: str = Field(..., description="提示框的主要内容")
"""提示框的主要内容"""
show_icon: bool = Field(default=True, description="是否显示与类型匹配的图标") show_icon: bool = Field(default=True, description="是否显示与类型匹配的图标")
"""是否显示与类型匹配的图标"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,8 +12,11 @@ class Avatar(RenderableComponent):
component_type: Literal["avatar"] = "avatar" component_type: Literal["avatar"] = "avatar"
src: str = Field(..., description="头像的URL或Base64数据URI") src: str = Field(..., description="头像的URL或Base64数据URI")
"""头像的URL或Base64数据URI"""
shape: Literal["circle", "square"] = Field("circle", description="头像形状") shape: Literal["circle", "square"] = Field("circle", description="头像形状")
"""头像形状"""
size: int = Field(50, description="头像尺寸(像素)") size: int = Field(50, description="头像尺寸(像素)")
"""头像尺寸(像素)"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:
@ -25,10 +28,13 @@ class AvatarGroup(RenderableComponent):
component_type: Literal["avatar_group"] = "avatar_group" component_type: Literal["avatar_group"] = "avatar_group"
avatars: list[Avatar] = Field(default_factory=list, description="头像列表") avatars: list[Avatar] = Field(default_factory=list, description="头像列表")
"""头像列表"""
spacing: int = Field(-15, description="头像间的间距(负数表示重叠)") spacing: int = Field(-15, description="头像间的间距(负数表示重叠)")
"""头像间的间距(负数表示重叠)"""
max_count: int | None = Field( max_count: int | None = Field(
None, description="最多显示的头像数量,超出部分会显示为'+N'" None, description="最多显示的头像数量,超出部分会显示为'+N'"
) )
"""最多显示的头像数量,超出部分会显示为'+N'"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,10 +12,12 @@ class Badge(RenderableComponent):
component_type: Literal["badge"] = "badge" component_type: Literal["badge"] = "badge"
text: str = Field(..., description="徽章上显示的文本") text: str = Field(..., description="徽章上显示的文本")
"""徽章上显示的文本"""
color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field( color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field(
default="info", default="info",
description="预设的颜色方案", description="预设的颜色方案",
) )
"""预设的颜色方案"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,9 +12,13 @@ class Divider(RenderableComponent):
component_type: Literal["divider"] = "divider" component_type: Literal["divider"] = "divider"
margin: str = Field("2em 0", description="CSS margin属性控制分割线上下的间距") margin: str = Field("2em 0", description="CSS margin属性控制分割线上下的间距")
"""CSS margin属性控制分割线上下的间距"""
color: str = Field("#f7889c", description="分割线颜色") color: str = Field("#f7889c", description="分割线颜色")
"""分割线颜色"""
style: Literal["solid", "dashed", "dotted"] = Field("solid", description="线条样式") style: Literal["solid", "dashed", "dotted"] = Field("solid", description="线条样式")
"""线条样式"""
thickness: str = Field("1px", description="线条粗细") thickness: str = Field("1px", description="线条粗细")
"""线条粗细"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:
@ -26,9 +30,13 @@ class Rectangle(RenderableComponent):
component_type: Literal["rectangle"] = "rectangle" component_type: Literal["rectangle"] = "rectangle"
height: str = Field("50px", description="矩形的高度 (CSS value)") height: str = Field("50px", description="矩形的高度 (CSS value)")
"""矩形的高度 (CSS value)"""
background_color: str = Field("#fdf1f5", description="背景颜色") background_color: str = Field("#fdf1f5", description="背景颜色")
"""背景颜色"""
border: str = Field("1px solid #fce4ec", description="CSS border属性") border: str = Field("1px solid #fce4ec", description="CSS border属性")
"""CSS border属性"""
border_radius: str = Field("8px", description="CSS border-radius属性") border_radius: str = Field("8px", description="CSS border-radius属性")
"""CSS border-radius属性"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,17 +12,23 @@ class KpiCard(RenderableComponent):
component_type: Literal["kpi_card"] = "kpi_card" component_type: Literal["kpi_card"] = "kpi_card"
label: str = Field(..., description="指标的标签或名称") label: str = Field(..., description="指标的标签或名称")
"""指标的标签或名称"""
value: Any = Field(..., description="指标的主要数值") value: Any = Field(..., description="指标的主要数值")
"""指标的主要数值"""
unit: str | None = Field(default=None, description="数值的单位,可选") unit: str | None = Field(default=None, description="数值的单位,可选")
"""数值的单位,可选"""
change: str | None = Field( change: str | None = Field(
default=None, description="与上一周期的变化,例如 '+15%''-100'" default=None, description="与上一周期的变化,例如 '+15%''-100'"
) )
"""与上一周期的变化,例如 '+15%''-100'"""
change_type: Literal["positive", "negative", "neutral"] = Field( change_type: Literal["positive", "negative", "neutral"] = Field(
default="neutral", description="变化的类型,用于决定颜色" default="neutral", description="变化的类型,用于决定颜色"
) )
"""变化的类型,用于决定颜色"""
icon_svg: str | None = Field( icon_svg: str | None = Field(
default=None, description="卡片中显示的可选图标 (SVG path data)" default=None, description="卡片中显示的可选图标 (SVG path data)"
) )
"""卡片中显示的可选图标 (SVG path data)"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,12 +12,16 @@ class ProgressBar(RenderableComponent):
component_type: Literal["progress_bar"] = "progress_bar" component_type: Literal["progress_bar"] = "progress_bar"
progress: float = Field(..., ge=0, le=100, description="进度百分比 (0-100)") progress: float = Field(..., ge=0, le=100, description="进度百分比 (0-100)")
"""进度百分比 (0-100)"""
label: str | None = Field(default=None, description="显示在进度条上的可选文本") label: str | None = Field(default=None, description="显示在进度条上的可选文本")
"""显示在进度条上的可选文本"""
color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field( color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field(
default="primary", default="primary",
description="预设的颜色方案", description="预设的颜色方案",
) )
"""预设的颜色方案"""
animated: bool = Field(default=False, description="是否显示动画效果") animated: bool = Field(default=False, description="是否显示动画效果")
"""是否显示动画效果"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -11,10 +11,15 @@ class TimelineItem(BaseModel):
"""时间轴中的单个事件点。""" """时间轴中的单个事件点。"""
timestamp: str = Field(..., description="显示在时间点旁边的时间或标签") timestamp: str = Field(..., description="显示在时间点旁边的时间或标签")
"""显示在时间点旁边的时间或标签"""
title: str = Field(..., description="事件的标题") title: str = Field(..., description="事件的标题")
"""事件的标题"""
content: str = Field(..., description="事件的详细描述") content: str = Field(..., description="事件的详细描述")
"""事件的详细描述"""
icon: str | None = Field(default=None, description="可选的自定义图标SVG路径") icon: str | None = Field(default=None, description="可选的自定义图标SVG路径")
"""可选的自定义图标SVG路径"""
color: str | None = Field(default=None, description="可选的自定义颜色,覆盖默认") color: str | None = Field(default=None, description="可选的自定义颜色,覆盖默认")
"""可选的自定义颜色,覆盖默认"""
class Timeline(RenderableComponent): class Timeline(RenderableComponent):
@ -24,6 +29,7 @@ class Timeline(RenderableComponent):
items: list[TimelineItem] = Field( items: list[TimelineItem] = Field(
default_factory=list, description="时间轴项目列表" default_factory=list, description="时间轴项目列表"
) )
"""时间轴项目列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,11 +12,15 @@ class UserInfoBlock(RenderableComponent):
component_type: Literal["user_info_block"] = "user_info_block" component_type: Literal["user_info_block"] = "user_info_block"
avatar_url: str = Field(..., description="用户头像的URL") avatar_url: str = Field(..., description="用户头像的URL")
"""用户头像的URL"""
name: str = Field(..., description="用户的名称") name: str = Field(..., description="用户的名称")
"""用户的名称"""
subtitle: str | None = Field( subtitle: str | None = Field(
default=None, description="显示在名称下方的副标题 (如UID或角色)" default=None, description="显示在名称下方的副标题 (如UID或角色)"
) )
"""显示在名称下方的副标题 (如UID或角色)"""
tags: list[str] = Field(default_factory=list, description="附加的标签列表") tags: list[str] = Field(default_factory=list, description="附加的标签列表")
"""附加的标签列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -24,6 +24,7 @@ from .markdown import (
from .notebook import NotebookData, NotebookElement from .notebook import NotebookData, NotebookElement
from .table import ( from .table import (
BaseCell, BaseCell,
ComponentCell,
ImageCell, ImageCell,
RichTextCell, RichTextCell,
StatusBadgeCell, StatusBadgeCell,
@ -38,6 +39,7 @@ __all__ = [
"BaseCell", "BaseCell",
"CardData", "CardData",
"CodeElement", "CodeElement",
"ComponentCell",
"DetailsData", "DetailsData",
"DetailsItem", "DetailsItem",
"HeadingElement", "HeadingElement",

View File

@ -20,10 +20,15 @@ class RenderableComponent(BaseModel, Renderable):
""" """
_is_standalone_template: bool = False _is_standalone_template: bool = False
"""标记此组件是否为独立模板"""
inline_style: dict[str, str] | None = None inline_style: dict[str, str] | None = None
"""应用于组件根元素的内联CSS样式"""
component_css: str | None = None component_css: str | None = None
"""注入到页面的额外CSS字符串"""
extra_classes: list[str] | None = None extra_classes: list[str] | None = None
"""应用于组件根元素的额外CSS类名列表"""
variant: str | None = None variant: str | None = None
"""组件的变体/皮肤名称"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -7,8 +7,11 @@ class CardData(ContainerComponent):
"""通用卡片的数据模型,可以包含头部、内容和尾部""" """通用卡片的数据模型,可以包含头部、内容和尾部"""
header: RenderableComponent | None = None header: RenderableComponent | None = None
"""卡片的头部内容组件"""
content: RenderableComponent content: RenderableComponent
"""卡片的主要内容组件"""
footer: RenderableComponent | None = None footer: RenderableComponent | None = None
"""卡片的尾部内容组件"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -9,14 +9,18 @@ class DetailsItem(BaseModel):
"""描述列表中的单个项目""" """描述列表中的单个项目"""
label: str = Field(..., description="项目的标签/键") label: str = Field(..., description="项目的标签/键")
"""项目的标签/键"""
value: Any = Field(..., description="项目的值") value: Any = Field(..., description="项目的值")
"""项目的值"""
class DetailsData(RenderableComponent): class DetailsData(RenderableComponent):
"""描述列表(键值对)的数据模型""" """描述列表(键值对)的数据模型"""
title: str | None = Field(None, description="列表的可选标题") title: str | None = Field(None, description="列表的可选标题")
"""列表的可选标题"""
items: list[DetailsItem] = Field(default_factory=list, description="键值对项目列表") items: list[DetailsItem] = Field(default_factory=list, description="键值对项目列表")
"""键值对项目列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,20 +12,26 @@ class LayoutItem(BaseModel):
"""布局中的单个项目,现在持有可渲染组件的数据模型""" """布局中的单个项目,现在持有可渲染组件的数据模型"""
component: RenderableComponent = Field(..., description="要渲染的组件的数据模型") component: RenderableComponent = Field(..., description="要渲染的组件的数据模型")
"""要渲染的组件的数据模型"""
metadata: dict[str, Any] | None = Field(None, description="传递给模板的额外元数据") metadata: dict[str, Any] | None = Field(None, description="传递给模板的额外元数据")
"""传递给模板的额外元数据"""
class LayoutData(ContainerComponent): class LayoutData(ContainerComponent):
"""布局构建器的数据模型""" """布局构建器的数据模型"""
style_name: str | None = None style_name: str | None = None
"""应用于布局容器的样式名称"""
layout_type: str = "column" layout_type: str = "column"
"""布局类型 (如 'column', 'row', 'grid')"""
children: list[LayoutItem] = Field( children: list[LayoutItem] = Field(
default_factory=list, description="要布局的项目列表" default_factory=list, description="要布局的项目列表"
) )
"""要布局的项目列表"""
options: dict[str, Any] = Field( options: dict[str, Any] = Field(
default_factory=dict, description="传递给模板的选项" default_factory=dict, description="传递给模板的选项"
) )
"""传递给模板的选项"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,6 +12,7 @@ class ListItem(BaseModel):
"""列表中的单个项目,其内容可以是任何可渲染组件。""" """列表中的单个项目,其内容可以是任何可渲染组件。"""
component: RenderableComponent = Field(..., description="要渲染的组件的数据模型") component: RenderableComponent = Field(..., description="要渲染的组件的数据模型")
"""要渲染的组件的数据模型"""
class ListData(ContainerComponent): class ListData(ContainerComponent):
@ -19,7 +20,9 @@ class ListData(ContainerComponent):
component_type: Literal["list"] = "list" component_type: Literal["list"] = "list"
items: list[ListItem] = Field(default_factory=list, description="列表项目") items: list[ListItem] = Field(default_factory=list, description="列表项目")
"""列表项目"""
ordered: bool = Field(default=False, description="是否为有序列表") ordered: bool = Field(default=False, description="是否为有序列表")
"""是否为有序列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -44,7 +44,9 @@ class TextElement(MarkdownElement):
class HeadingElement(MarkdownElement): class HeadingElement(MarkdownElement):
type: Literal["heading"] = "heading" type: Literal["heading"] = "heading"
text: str 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: def to_markdown(self) -> str:
return f"{'#' * self.level} {self.text}" return f"{'#' * self.level} {self.text}"
@ -53,7 +55,9 @@ class HeadingElement(MarkdownElement):
class ImageElement(MarkdownElement): class ImageElement(MarkdownElement):
type: Literal["image"] = "image" type: Literal["image"] = "image"
src: str src: str
"""图片来源 (URL或data URI)"""
alt: str = "image" alt: str = "image"
"""图片的替代文本"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return f"![{self.alt}]({self.src})" return f"![{self.alt}]({self.src})"
@ -62,7 +66,9 @@ class ImageElement(MarkdownElement):
class CodeElement(MarkdownElement): class CodeElement(MarkdownElement):
type: Literal["code"] = "code" type: Literal["code"] = "code"
code: str code: str
"""代码字符串"""
language: str = "" language: str = ""
"""代码语言,用于语法高亮"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return f"```{self.language}\n{self.code}\n```" return f"```{self.language}\n{self.code}\n```"
@ -71,6 +77,7 @@ class CodeElement(MarkdownElement):
class RawHtmlElement(MarkdownElement): class RawHtmlElement(MarkdownElement):
type: Literal["raw_html"] = "raw_html" type: Literal["raw_html"] = "raw_html"
html: str html: str
"""原始HTML字符串"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return self.html return self.html
@ -79,8 +86,11 @@ class RawHtmlElement(MarkdownElement):
class TableElement(MarkdownElement): class TableElement(MarkdownElement):
type: Literal["table"] = "table" type: Literal["table"] = "table"
headers: list[str] headers: list[str]
"""表格的表头列表"""
rows: list[list[str]] rows: list[list[str]]
"""表格的数据行列表"""
alignments: list[Literal["left", "center", "right"]] | None = None alignments: list[Literal["left", "center", "right"]] | None = None
"""每列的对齐方式"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
header_row = "| " + " | ".join(self.headers) + " |" header_row = "| " + " | ".join(self.headers) + " |"
@ -102,7 +112,10 @@ class TableElement(MarkdownElement):
class ContainerElement(MarkdownElement): class ContainerElement(MarkdownElement):
content: list[MarkdownElement] = Field(default_factory=list) content: list[MarkdownElement] = Field(
default_factory=list, description="容器内包含的Markdown元素列表"
)
"""容器内包含的Markdown元素列表"""
class QuoteElement(ContainerElement): class QuoteElement(ContainerElement):
@ -121,6 +134,7 @@ class ListItemElement(ContainerElement):
class ListElement(ContainerElement): class ListElement(ContainerElement):
type: Literal["list"] = "list" type: Literal["list"] = "list"
ordered: bool = False ordered: bool = False
"""是否为有序列表 (例如 1., 2.)"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
lines = [] lines = []
@ -137,6 +151,7 @@ class ComponentElement(MarkdownElement):
type: Literal["component"] = "component" type: Literal["component"] = "component"
component: RenderableComponent component: RenderableComponent
"""嵌入在Markdown中的可渲染组件"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return "" return ""
@ -146,9 +161,15 @@ class MarkdownData(ContainerComponent):
"""Markdown转图片的数据模型""" """Markdown转图片的数据模型"""
style_name: str | None = None 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 width: int = 800
"""最终渲染图片的宽度"""
css_path: str | None = None css_path: str | None = None
"""自定义CSS文件的绝对路径"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:
@ -180,7 +201,6 @@ class MarkdownData(ContainerComponent):
logger.warning(f"Markdown自定义CSS文件不存在: {self.css_path}") logger.warning(f"Markdown自定义CSS文件不存在: {self.css_path}")
else: else:
style_name = self.style_name or "light" style_name = self.style_name or "light"
# 使用上下文对象来解析路径
css_path = await context.theme_manager.resolve_markdown_style_path( css_path = await context.theme_manager.resolve_markdown_style_path(
style_name, context style_name, context
) )

View File

@ -22,21 +22,32 @@ class NotebookElement(BaseModel):
"component", "component",
] ]
text: str | None = None text: str | None = None
"""元素的文本内容 (用于标题、段落、引用)"""
level: int | None = None level: int | None = None
"""标题的级别 (1-4)"""
src: str | None = None src: str | None = None
"""图片的来源 (URL或data URI)"""
caption: str | None = None caption: str | None = None
"""图片的说明文字"""
code: str | None = None code: str | None = None
"""代码块的内容"""
language: str | None = None language: str | None = None
"""代码块的语言"""
data: list[str] | None = None data: list[str] | None = None
"""列表项的内容列表"""
ordered: bool | None = None ordered: bool | None = None
"""是否为有序列表"""
component: RenderableComponent | None = None component: RenderableComponent | None = None
"""嵌入的自定义可渲染组件"""
class NotebookData(ContainerComponent): class NotebookData(ContainerComponent):
"""Notebook转图片的数据模型""" """Notebook转图片的数据模型"""
style_name: str | None = None style_name: str | None = None
"""Notebook的样式名称"""
elements: list[NotebookElement] elements: list[NotebookElement]
"""构成Notebook页面的元素列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -1,8 +1,6 @@
from pathlib import Path
from typing import Literal from typing import Literal
from nonebot.compat import field_validator from pydantic import BaseModel, Field
from pydantic import BaseModel
from ...models.components.progress_bar import ProgressBar from ...models.components.progress_bar import ProgressBar
from .base import RenderableComponent from .base import RenderableComponent
@ -10,6 +8,7 @@ from .text import TextSpan
__all__ = [ __all__ = [
"BaseCell", "BaseCell",
"ComponentCell",
"ImageCell", "ImageCell",
"ProgressBarCell", "ProgressBarCell",
"RichTextCell", "RichTextCell",
@ -30,7 +29,7 @@ class TextCell(BaseCell):
"""文本单元格""" """文本单元格"""
type: Literal["text"] = "text" # type: ignore type: Literal["text"] = "text" # type: ignore
content: str | float content: str
bold: bool = False bold: bool = False
color: str | None = None color: str | None = None
@ -39,18 +38,12 @@ class ImageCell(BaseCell):
"""图片单元格""" """图片单元格"""
type: Literal["image"] = "image" # type: ignore type: Literal["image"] = "image" # type: ignore
src: str | Path src: str
width: int = 40 width: int = 40
height: int = 40 height: int = 40
shape: Literal["square", "circle"] = "square" shape: Literal["square", "circle"] = "square"
alt: str = "image" 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): class StatusBadgeCell(BaseCell):
"""状态徽章单元格""" """状态徽章单元格"""
@ -70,20 +63,28 @@ class RichTextCell(BaseCell):
"""富文本单元格,支持多个带样式的文本片段""" """富文本单元格,支持多个带样式的文本片段"""
type: Literal["rich_text"] = "rich_text" # type: ignore 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 = ( TableCell = (
TextCell TextCell
| ImageCell | ImageCell
| StatusBadgeCell | StatusBadgeCell
| ProgressBarCell | ProgressBarCell
| RichTextCell | RichTextCell
| ComponentCell
| str | str
| int | int
| float | float
@ -95,17 +96,22 @@ class TableData(RenderableComponent):
"""通用表格的数据模型""" """通用表格的数据模型"""
style_name: str | None = None 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])""" """每列的宽度 (e.g., ['50px', 'auto', 100])"""
@property @property

View File

@ -1,6 +1,8 @@
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from pydantic import Field
from .base import RenderableComponent from .base import RenderableComponent
__all__ = ["TemplateComponent"] __all__ = ["TemplateComponent"]
@ -10,8 +12,11 @@ class TemplateComponent(RenderableComponent):
"""基于独立模板文件的UI组件""" """基于独立模板文件的UI组件"""
_is_standalone_template: bool = True _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 @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -23,9 +23,11 @@ class TextData(RenderableComponent):
"""轻量级富文本组件的数据模型""" """轻量级富文本组件的数据模型"""
spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表") spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表")
"""文本片段列表"""
align: Literal["left", "right", "center"] = Field( align: Literal["left", "right", "center"] = Field(
"left", description="整体文本对齐方式" "left", description="整体文本对齐方式"
) )
"""整体文本对齐方式"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -13,25 +13,35 @@ class HelpItem(BaseModel):
"""帮助菜单中的单个功能项""" """帮助菜单中的单个功能项"""
name: str name: str
"""功能名称"""
description: str description: str
"""功能描述"""
usage: str usage: str
"""功能用法说明"""
class HelpCategory(BaseModel): class HelpCategory(BaseModel):
"""帮助菜单中的一个功能类别""" """帮助菜单中的一个功能类别"""
title: str title: str
"""分类标题"""
icon_svg_path: str icon_svg_path: str
"""分类图标的SVG路径数据"""
items: list[HelpItem] items: list[HelpItem]
"""该分类下的功能项列表"""
class PluginHelpPageData(RenderableComponent): class PluginHelpPageData(RenderableComponent):
"""通用插件帮助页面的数据模型""" """通用插件帮助页面的数据模型"""
style_name: str | None = None style_name: str | None = None
"""页面样式名称"""
bot_nickname: str bot_nickname: str
"""机器人昵称"""
page_title: str page_title: str
"""页面主标题"""
categories: list[HelpCategory] categories: list[HelpCategory]
"""帮助分类列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -13,29 +13,43 @@ class PluginMenuItem(BaseModel):
"""插件菜单中的单个插件项""" """插件菜单中的单个插件项"""
id: str id: str
"""插件的唯一ID"""
name: str name: str
"""插件名称"""
status: bool status: bool
"""插件在当前群组的开关状态"""
has_superuser_help: bool has_superuser_help: bool
commands: list[str] = Field(default_factory=list) """插件是否有超级用户专属帮助"""
commands: list[str] = Field(default_factory=list, description="插件的主要命令列表")
"""插件的主要命令列表"""
class PluginMenuCategory(BaseModel): class PluginMenuCategory(BaseModel):
"""插件菜单中的一个分类""" """插件菜单中的一个分类"""
name: str name: str
items: list[PluginMenuItem] """插件分类名称"""
items: list[PluginMenuItem] = Field(..., description="该分类下的插件项列表")
"""该分类下的插件项列表"""
class PluginMenuData(RenderableComponent): class PluginMenuData(RenderableComponent):
"""通用插件帮助菜单的数据模型""" """通用插件帮助菜单的数据模型"""
style_name: str | None = None style_name: str | None = None
"""页面样式名称"""
bot_name: str bot_name: str
"""机器人名称"""
bot_avatar_url: str bot_avatar_url: str
"""机器人头像URL"""
is_detail: bool is_detail: bool
"""是否为详细菜单模式"""
plugin_count: int plugin_count: int
"""总插件数量"""
active_count: int active_count: int
"""已启用插件数量"""
categories: list[PluginMenuCategory] categories: list[PluginMenuCategory]
"""插件分类列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str: