mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-18 18:50:05 +08:00
✨ feat!(ui): 重构图表组件架构,实现数据与样式分离
🏗️ **架构重构** - 移除charts.py中所有硬编码样式参数(grid、tooltip、legend等) - 将样式配置迁移至主题层style.json文件 - 统一图表模板消费样式文件的能力 📊 **图表组件优化** - bar_chart: 移除grid和坐标轴show参数 - pie_chart: 移除tooltip、legend样式和series视觉参数 - line_chart: 移除tooltip、grid和坐标轴配置 - radar_chart: 移除tooltip硬编码 🎨 **主题系统增强** - 新增pie_chart、line_chart、radar_chart的style.json配置 - 更新bar_chart/style.json,添加grid、xAxis、yAxis样式 - 所有图表模板支持deepMerge样式合并逻辑 🔧 **Breaking Changes** - 图表工厂函数不再接受样式参数 - 主题开发者现可通过style.json完全定制图表外观 - 提升组件可维护性和主题灵活性
This commit is contained in:
@@ -1,19 +1,49 @@
|
||||
from . import widgets
|
||||
from .core.layout import LayoutBuilder
|
||||
from .core.markdown import MarkdownBuilder
|
||||
from .core.notebook import NotebookBuilder
|
||||
from .core.table import TableBuilder
|
||||
from .presets.help_page import PluginHelpPageBuilder
|
||||
from .presets.info_card import InfoCardBuilder
|
||||
from .presets.plugin_menu import PluginMenuBuilder
|
||||
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__ = [
|
||||
"InfoCardBuilder",
|
||||
"AlertBuilder",
|
||||
"AvatarBuilder",
|
||||
"AvatarGroupBuilder",
|
||||
"BadgeBuilder",
|
||||
"CardBuilder",
|
||||
"DetailsBuilder",
|
||||
"DividerBuilder",
|
||||
"EChartsBuilder",
|
||||
"KpiCardBuilder",
|
||||
"LayoutBuilder",
|
||||
"ListBuilder",
|
||||
"MarkdownBuilder",
|
||||
"NotebookBuilder",
|
||||
"PluginHelpPageBuilder",
|
||||
"PluginMenuBuilder",
|
||||
"ProgressBarBuilder",
|
||||
"TableBuilder",
|
||||
"widgets",
|
||||
"TextBuilder",
|
||||
"TimelineBuilder",
|
||||
"UserInfoBlockBuilder",
|
||||
]
|
||||
|
||||
@@ -7,14 +7,24 @@ T_DataModel = TypeVar("T_DataModel", bound=BaseModel)
|
||||
|
||||
|
||||
class BaseBuilder(Generic[T_DataModel]):
|
||||
"""所有UI构建器的基类,提供通用的样式化和构建逻辑。"""
|
||||
"""
|
||||
所有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._extra_css: str | None = None
|
||||
self._component_css: str | None = None
|
||||
self._variant: str | None = None
|
||||
self._extra_classes: list[str] = []
|
||||
|
||||
@property
|
||||
def data(self) -> T_DataModel:
|
||||
@@ -23,6 +33,12 @@ class BaseBuilder(Generic[T_DataModel]):
|
||||
def with_style(self, style_name: str) -> Self:
|
||||
"""
|
||||
为组件应用一个特定的样式。
|
||||
|
||||
参数:
|
||||
style_name: 在主题的CSS中定义的样式类名。
|
||||
|
||||
返回:
|
||||
Self: 当前构建器实例,以支持链式调用。
|
||||
"""
|
||||
self._style_name = style_name
|
||||
return self
|
||||
@@ -32,26 +48,70 @@ class BaseBuilder(Generic[T_DataModel]):
|
||||
为组件的根元素应用动态的内联样式。
|
||||
|
||||
参数:
|
||||
style: 一个CSS样式字典,例如 {"background-color":"#fff","font-size":"16px"}
|
||||
style: 一个CSS样式字典,例如
|
||||
`{"background-color":"#fff","font-size":"16px"}`。
|
||||
|
||||
返回:
|
||||
Self: 当前构建器实例,以支持链式调用。
|
||||
"""
|
||||
self._inline_style = style
|
||||
return self
|
||||
|
||||
def with_extra_css(self, css: str) -> 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._extra_css = css
|
||||
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
|
||||
|
||||
+150
-61
@@ -2,87 +2,176 @@ from typing import Any, Generic, Literal, TypeVar
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..models.charts import (
|
||||
BarChartData,
|
||||
BaseChartData,
|
||||
LineChartData,
|
||||
LineChartSeries,
|
||||
PieChartData,
|
||||
PieChartDataItem,
|
||||
EChartsAxis,
|
||||
EChartsData,
|
||||
EChartsGrid,
|
||||
EChartsSeries,
|
||||
EChartsTitle,
|
||||
EChartsTooltip,
|
||||
)
|
||||
from .base import BaseBuilder
|
||||
|
||||
T_ChartData = TypeVar("T_ChartData", bound=BaseChartData)
|
||||
|
||||
|
||||
class BaseChartBuilder(BaseBuilder[T_ChartData], Generic[T_ChartData]):
|
||||
"""所有图表构建器的基类"""
|
||||
class EChartsBuilder(BaseBuilder[EChartsData], Generic[T_ChartData]):
|
||||
"""
|
||||
一个统一的、泛型的 ECharts 图表构建器。
|
||||
提供了设置 ECharts `option` 的核心方法,以及一些常用图表的便利方法。
|
||||
"""
|
||||
|
||||
def set_title(self, title: str) -> Self:
|
||||
self._data.title = title
|
||||
return self
|
||||
|
||||
|
||||
class BarChartBuilder(BaseChartBuilder[BarChartData]):
|
||||
"""链式构建柱状图的辅助类 (支持横向和竖向)"""
|
||||
|
||||
def __init__(
|
||||
self, title: str, direction: Literal["horizontal", "vertical"] = "horizontal"
|
||||
):
|
||||
data_model = BarChartData(
|
||||
title=title, direction=direction, category_data=[], data=[]
|
||||
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__(data_model, template_name="components/charts/bar_chart")
|
||||
super().__init__(model, template_name=template_name)
|
||||
|
||||
def add_data(self, category: str, value: float) -> Self:
|
||||
"""添加一个数据点"""
|
||||
self._data.category_data.append(category)
|
||||
self._data.data.append(value)
|
||||
return self
|
||||
|
||||
def add_data_items(
|
||||
self, items: list[tuple[str, int | float]] | list[dict[str, Any]]
|
||||
def set_title(
|
||||
self, text: str, left: Literal["left", "center", "right"] = "center"
|
||||
) -> Self:
|
||||
for item in items:
|
||||
if isinstance(item, tuple):
|
||||
self.add_data(item[0], item[1])
|
||||
elif isinstance(item, dict):
|
||||
self.add_data(item.get("category", ""), item.get("value", 0))
|
||||
self._data.title_model = EChartsTitle(text=text, left=left)
|
||||
return self
|
||||
|
||||
def set_background_image(self, background_image: str) -> Self:
|
||||
"""设置背景图片 (仅横向柱状图模板支持)"""
|
||||
self._data.background_image = background_image
|
||||
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
|
||||
|
||||
|
||||
class PieChartBuilder(BaseChartBuilder[PieChartData]):
|
||||
"""链式构建饼图的辅助类"""
|
||||
|
||||
def __init__(self, title: str):
|
||||
data_model = PieChartData(title=title, data=[])
|
||||
super().__init__(data_model, template_name="components/charts/pie_chart")
|
||||
|
||||
def add_slice(self, name: str, value: float) -> Self:
|
||||
"""添加一个饼图扇区"""
|
||||
self._data.data.append(PieChartDataItem(name=name, value=value))
|
||||
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
|
||||
|
||||
class LineChartBuilder(BaseChartBuilder[LineChartData]):
|
||||
"""链式构建折线图的辅助类"""
|
||||
|
||||
def __init__(self, title: str):
|
||||
data_model = LineChartData(title=title, category_data=[], series=[])
|
||||
super().__init__(data_model, template_name="components/charts/line_chart")
|
||||
|
||||
def set_categories(self, categories: list[str]) -> Self:
|
||||
"""设置X轴的分类标签"""
|
||||
self._data.category_data = categories
|
||||
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, name: str, data: list[int | float], smooth: bool = False
|
||||
self, type: str, data: list[Any], name: str | None = None, **kwargs: Any
|
||||
) -> Self:
|
||||
"""添加一条折线"""
|
||||
self._data.series.append(LineChartSeries(name=name, data=data, smooth=smooth))
|
||||
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
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
小组件构建器模块
|
||||
包含各种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",
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
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
|
||||
@@ -0,0 +1,38 @@
|
||||
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
|
||||
@@ -0,0 +1,20 @@
|
||||
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")
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
@@ -0,0 +1,28 @@
|
||||
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
|
||||
@@ -3,14 +3,22 @@
|
||||
包含基础的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",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
@@ -19,16 +19,32 @@ class LayoutBuilder(BaseBuilder[LayoutData]):
|
||||
self._options: dict[str, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def column(cls, **options: Any) -> Self:
|
||||
def column(
|
||||
cls, *, gap: str = "20px", align_items: str = "stretch", **options: Any
|
||||
) -> Self:
|
||||
builder = cls()
|
||||
builder._template_name = "layouts/column"
|
||||
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, **options: Any) -> Self:
|
||||
def row(
|
||||
cls, *, gap: str = "10px", align_items: str = "center", **options: Any
|
||||
) -> Self:
|
||||
builder = cls()
|
||||
builder._template_name = "layouts/row"
|
||||
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
|
||||
|
||||
@@ -56,15 +72,15 @@ class LayoutBuilder(BaseBuilder[LayoutData]):
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
向布局中添加一个组件,支持多种组件类型的添加。
|
||||
向布局中添加一个组件项。
|
||||
|
||||
参数:
|
||||
component: 一个 Builder 实例 (如 TableBuilder) 或一个 RenderableComponent
|
||||
数据模型。
|
||||
metadata: (可选) 与此项目关联的元数据,可用于模板。
|
||||
component: 一个 `BaseBuilder` 实例 (如 `TableBuilder()`) 或一个已构建的
|
||||
`RenderableComponent` 数据模型。
|
||||
metadata: (可选) 与此项目关联的元数据,可在布局模板中访问。
|
||||
|
||||
返回:
|
||||
Self: 返回当前布局构建器实例,支持链式调用。
|
||||
Self: 当前构建器实例,以支持链式调用。
|
||||
"""
|
||||
component_data = (
|
||||
component.data if isinstance(component, BaseBuilder) else component
|
||||
@@ -76,28 +92,24 @@ class LayoutBuilder(BaseBuilder[LayoutData]):
|
||||
|
||||
def add_option(self, key: str, value: Any) -> Self:
|
||||
"""
|
||||
为布局添加一个自定义选项,该选项会传递给模板。
|
||||
为布局模板添加一个自定义选项。
|
||||
|
||||
例如,`add_option("padding", "30px")` 会在模板的 `data.options`
|
||||
字典中添加 `{"padding": "30px"}`。
|
||||
|
||||
参数:
|
||||
key: 选项的键名,用于在模板中引用。
|
||||
value: 选项的值,可以是任意类型的数据。
|
||||
key: 选项的键名。
|
||||
value: 选项的值。
|
||||
|
||||
返回:
|
||||
Self: 返回当前布局构建器实例,支持链式调用。
|
||||
Self: 当前构建器实例,以支持链式调用。
|
||||
"""
|
||||
self._options[key] = value
|
||||
return self
|
||||
|
||||
def build(self) -> LayoutData:
|
||||
"""
|
||||
[修改] 构建并返回 LayoutData 模型实例。
|
||||
此方法现在是同步的,并且不执行渲染。
|
||||
|
||||
参数:
|
||||
无
|
||||
|
||||
返回:
|
||||
LayoutData: 配置好的布局数据模型。
|
||||
构建并返回 LayoutData 模型实例。
|
||||
"""
|
||||
if not self._template_name:
|
||||
raise ValueError(
|
||||
@@ -106,4 +118,4 @@ class LayoutBuilder(BaseBuilder[LayoutData]):
|
||||
|
||||
self._data.options = self._options
|
||||
self._data.layout_type = self._template_name.split("/")[-1]
|
||||
return self._data
|
||||
return super().build()
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
|
||||
from ...models.core.markdown import (
|
||||
CodeElement,
|
||||
ComponentElement,
|
||||
HeadingElement,
|
||||
ImageElement,
|
||||
ListElement,
|
||||
@@ -12,6 +13,7 @@ from ...models.core.markdown import (
|
||||
MarkdownElement,
|
||||
QuoteElement,
|
||||
RawHtmlElement,
|
||||
RenderableComponent,
|
||||
TableElement,
|
||||
TextElement,
|
||||
)
|
||||
@@ -24,7 +26,7 @@ class MarkdownBuilder(BaseBuilder[MarkdownData]):
|
||||
"""链式构建Markdown图片的辅助类,支持上下文管理和组合。"""
|
||||
|
||||
def __init__(self):
|
||||
data_model = MarkdownData(markdown="", width=800, css_path=None)
|
||||
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
|
||||
@@ -78,6 +80,16 @@ class MarkdownBuilder(BaseBuilder[MarkdownData]):
|
||||
)
|
||||
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:
|
||||
@@ -144,8 +156,7 @@ class MarkdownBuilder(BaseBuilder[MarkdownData]):
|
||||
"""
|
||||
构建并返回 MarkdownData 模型实例。
|
||||
"""
|
||||
final_markdown = "\n\n".join(part.to_markdown() for part in self._parts).strip()
|
||||
self._data.markdown = final_markdown
|
||||
self._data.elements = self._parts
|
||||
self._data.width = self._width
|
||||
self._data.css_path = self._css_path
|
||||
return super().build()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Literal
|
||||
|
||||
from ...models.core.table import TableCell, TableData
|
||||
from ..base import BaseBuilder
|
||||
|
||||
@@ -12,16 +14,61 @@ class TableBuilder(BaseBuilder[TableData]):
|
||||
super().__init__(data_model, template_name="components/core/table")
|
||||
|
||||
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: 当前构建器实例,以支持链式调用。
|
||||
"""
|
||||
self._data.rows.append(row)
|
||||
return self
|
||||
|
||||
def add_rows(self, rows: list[list[TableCell]]) -> "TableBuilder":
|
||||
"""批量添加多行数据"""
|
||||
"""
|
||||
向表格中批量添加多行数据。
|
||||
|
||||
参数:
|
||||
rows: 一个包含多行数据的列表。
|
||||
|
||||
返回:
|
||||
TableBuilder: 当前构建器实例,以支持链式调用。
|
||||
"""
|
||||
self._data.rows.extend(rows)
|
||||
return self
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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
|
||||
@@ -3,12 +3,10 @@
|
||||
包含预定义的UI组件构建器
|
||||
"""
|
||||
|
||||
from .help_page import PluginHelpPageBuilder
|
||||
from .info_card import InfoCardBuilder
|
||||
from .plugin_help_page import PluginHelpPageBuilder
|
||||
from .plugin_menu import PluginMenuBuilder
|
||||
|
||||
__all__ = [
|
||||
"InfoCardBuilder",
|
||||
"PluginHelpPageBuilder",
|
||||
"PluginMenuBuilder",
|
||||
]
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from ...models.presets.card import (
|
||||
InfoCardData,
|
||||
InfoCardMetadataItem,
|
||||
InfoCardSection,
|
||||
)
|
||||
from ..base import BaseBuilder
|
||||
|
||||
__all__ = ["InfoCardBuilder"]
|
||||
|
||||
|
||||
class InfoCardBuilder(BaseBuilder[InfoCardData]):
|
||||
def __init__(self, title: str):
|
||||
self._data = InfoCardData(title=title)
|
||||
|
||||
super().__init__(self._data, template_name="components/presets/info_card")
|
||||
|
||||
def add_metadata(self, label: str, value: str | int) -> "InfoCardBuilder":
|
||||
self._data.metadata.append(InfoCardMetadataItem(label=label, value=value))
|
||||
return self
|
||||
|
||||
def add_metadata_items(
|
||||
self, items: list[tuple[str, Any]] | list[dict[str, Any]]
|
||||
) -> "InfoCardBuilder":
|
||||
for item in items:
|
||||
if isinstance(item, tuple):
|
||||
self.add_metadata(item[0], item[1])
|
||||
elif isinstance(item, dict):
|
||||
self.add_metadata(item.get("label", ""), item.get("value", ""))
|
||||
return self
|
||||
|
||||
def add_section(self, title: str, content: str | list[str]) -> "InfoCardBuilder":
|
||||
content_list = [content] if isinstance(content, str) else content
|
||||
self._data.sections.append(InfoCardSection(title=title, content=content_list))
|
||||
return self
|
||||
|
||||
def add_sections(
|
||||
self, sections: list[tuple[str, str | list[str]]] | list[dict[str, Any]]
|
||||
) -> "InfoCardBuilder":
|
||||
for section in sections:
|
||||
if isinstance(section, tuple):
|
||||
self.add_section(section[0], section[1])
|
||||
elif isinstance(section, dict):
|
||||
self.add_section(section.get("title", ""), section.get("content", []))
|
||||
return self
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
from ...models.presets.help_page import (
|
||||
from ...models.presets.plugin_help_page import (
|
||||
HelpCategory,
|
||||
PluginHelpPageData,
|
||||
)
|
||||
@@ -13,7 +13,7 @@ class PluginHelpPageBuilder(BaseBuilder[PluginHelpPageData]):
|
||||
bot_nickname=bot_nickname, page_title=page_title, categories=[]
|
||||
)
|
||||
|
||||
super().__init__(self._data, template_name="pages/core/help_page")
|
||||
super().__init__(self._data, template_name="pages/core/plugin_help_page")
|
||||
|
||||
def add_category(self, category: HelpCategory) -> "PluginHelpPageBuilder":
|
||||
"""添加一个帮助分类"""
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
小组件构建器模块
|
||||
包含各种UI小组件的构建器
|
||||
"""
|
||||
|
||||
from .badge import BadgeBuilder
|
||||
from .progress_bar import ProgressBarBuilder
|
||||
from .user_info_block import UserInfoBlockBuilder
|
||||
|
||||
__all__ = [
|
||||
"BadgeBuilder",
|
||||
"ProgressBarBuilder",
|
||||
"UserInfoBlockBuilder",
|
||||
]
|
||||
Reference in New Issue
Block a user