跳转至
受监督的机器翻译

本文内容由机器翻译生成,并遵循人工维护的术语表与风格指南。由于译文未经逐行人工审校,可能偶有错误或表达不当之处。

如有任何出入,请以英文原版为准,英文原版是权威来源。

阅读英文原版

部件

部件系统的完整属性与方法参考,内容由 docstring 生成。如需面向任务式讲解,请参阅自定义视图与部件表单布局

部件是可组合、可渲染的构建块,用于动态构建 UI 元素。下列每个部件类都可以直接从 starlette_admin 导入。

根据上下文不同,部件系统主要承担两种角色:

  • 仪表盘与自定义页面:用作 CustomViewwidget 属性,构建独立界面和指标看板。
  • 表单布局:用作 BaseModelViewform_layout 属性,在创建/编辑表单上排列和分组输入项。

基类

所有部件均继承自一个公共基类,该基类定义了标准的渲染与资源收集接口。

starlette_admin.widgets.BaseWidget dataclass

Bases: ABC

Base class for all dashboard widgets.

Subclasses set template to a path under the theme's widgets/ directory and override get_context to supply template variables. Layout widgets should also override render to recursively render their children before rendering their own template.

Source code in starlette_admin/widgets.py
@dataclass
class BaseWidget(ABC):
    """Base class for all dashboard widgets.

    Subclasses set `template` to a path under the theme's `widgets/` directory
    and override `get_context` to supply template variables. Layout widgets
    should also override `render` to recursively render their children before
    rendering their own template.
    """

    template: ClassVar[str]

    async def get_context(self, request: Request) -> dict[str, Any]:
        return {}

    def additional_css_links(self, request: Request) -> list[str]:
        """CSS URLs to inject into the page <head> when this widget is rendered."""
        return []

    def additional_js_links(self, request: Request) -> list[str]:
        """JS URLs to inject into the page before </body> when this widget is rendered."""
        return []

    async def render(self, request: Request, env: Environment) -> Markup:
        """Render the widget to HTML using `env`."""
        ctx = await self.get_context(request)
        template = env.get_template(self.template)
        return Markup(template.render(ctx, request=request))

CSS URLs to inject into the page when this widget is rendered.

Source code in starlette_admin/widgets.py
def additional_css_links(self, request: Request) -> list[str]:
    """CSS URLs to inject into the page <head> when this widget is rendered."""
    return []

JS URLs to inject into the page before when this widget is rendered.

Source code in starlette_admin/widgets.py
def additional_js_links(self, request: Request) -> list[str]:
    """JS URLs to inject into the page before </body> when this widget is rendered."""
    return []

render(request, env) async

Render the widget to HTML using env.

Source code in starlette_admin/widgets.py
async def render(self, request: Request, env: Environment) -> Markup:
    """Render the widget to HTML using `env`."""
    ctx = await self.get_context(request)
    template = env.get_template(self.template)
    return Markup(template.render(ctx, request=request))

内容部件

内容部件充当 UI 树的叶节点。它们不包含其他部件,而是展示实时数据。每个内容部件都接受一个异步回调,该回调在每个请求中调用一次,确保渲染出的值始终是最新的。

starlette_admin.widgets.StatWidget dataclass

Bases: BaseWidget

Renders a KPI stat card.

Parameters:

Name Type Description Default
title str

Label shown as the card subheader.

required
value_callback Callable[[Request], Awaitable[int | float | str]]

Async callable returning the primary metric value.

required
link str | None

Optional URL; makes the entire card a clickable anchor.

None
description str

Secondary text shown below the value.

''
description_icon str

Icon class for the description badge (e.g. "fa-solid fa-arrow-trend-up").

''
color str

Tabler color token applied to the description area (e.g. "success", "danger").

''
description_icon_position Literal['before', 'after']

"before" or "after" the description text.

'after'
chart_callback Callable[[Request], Awaitable[dict[str, Any]]] | None

Optional async callable returning an ApexCharts series list (e.g. [{"name": "Views", "data": [10, 20, 30]}]). When provided, a sparkline is rendered at the bottom of the card.

None
chart_type str

ApexCharts chart type for the sparkline (default "line").

'line'
chart_height str

Height passed to ApexCharts (default "40px").

'40px'
chart_options dict[str, Any]

Extra ApexCharts options merged over the sparkline defaults.

dict()
countup bool

When True, adds data-countup to the value element so a countup.js animation can target it.

False
Source code in starlette_admin/widgets.py
@dataclass
class StatWidget(BaseWidget):
    """Renders a KPI stat card.

    Args:
        title: Label shown as the card subheader.
        value_callback: Async callable returning the primary metric value.
        link: Optional URL; makes the entire card a clickable anchor.
        description: Secondary text shown below the value.
        description_icon: Icon class for the description badge
            (e.g. ``"fa-solid fa-arrow-trend-up"``).
        color: Tabler color token applied to the description area
            (e.g. ``"success"``, ``"danger"``).
        description_icon_position: ``"before"`` or ``"after"`` the description text.
        chart_callback: Optional async callable returning an ApexCharts ``series``
            list (e.g. ``[{"name": "Views", "data": [10, 20, 30]}]``).
            When provided, a sparkline is rendered at the bottom of the card.
        chart_type: ApexCharts chart type for the sparkline (default ``"line"``).
        chart_height: Height passed to ApexCharts (default ``"40px"``).
        chart_options: Extra ApexCharts options merged over the sparkline defaults.
        countup: When ``True``, adds ``data-countup`` to the value element so a
            countup.js animation can target it.
    """

    template: ClassVar[str] = "widgets/stat_widget.html"

    title: str
    value_callback: Callable[[Request], Awaitable[int | float | str]]
    link: str | None = None
    description: str = ""
    description_icon: str = ""
    color: str = ""
    description_icon_position: Literal["before", "after"] = "after"
    chart_callback: Callable[[Request], Awaitable[dict[str, Any]]] | None = None
    chart_type: str = "line"
    chart_height: str = "40px"
    chart_options: dict[str, Any] = field(default_factory=dict)
    countup: bool = False

    def additional_js_links(self, request: Request) -> list[str]:
        links = []
        if self.countup:
            links.append(static_url(request, "js/vendor/countUp.umd.js", v="2.10.0"))
        if self.chart_callback is not None:
            links.append(
                static_url(request, "js/vendor/apexcharts.min.js", v="v5.15.2")
            )
        links.append(static_url(request, "js/stat_widget.js", v=1))
        return links

    async def get_context(self, request: Request) -> dict[str, Any]:
        value = await self.value_callback(request)
        chart_data = await self.chart_callback(request) if self.chart_callback else None
        chart_id = (
            f"stat-chart-{uuid.uuid4().hex[:8]}" if chart_data is not None else ""
        )
        value_id = f"stat-value-{uuid.uuid4().hex[:8]}" if self.countup else ""
        return {
            "widget": self,
            "title": self.title,
            "value": value,
            "link": self.link,
            "description": self.description,
            "description_icon": self.description_icon,
            "color": self.color,
            "description_icon_position": self.description_icon_position,
            "chart_data": chart_data,
            "chart_id": chart_id,
            "chart_type": self.chart_type,
            "chart_height": self.chart_height,
            "chart_options": self.chart_options,
            "countup": self.countup,
            "value_id": value_id,
        }

starlette_admin.widgets.ChartWidget dataclass

Bases: BaseWidget

Renders an ApexCharts chart inside a Tabler card.

Parameters:

Name Type Description Default
title str

Card heading.

required
chart_type str

ApexCharts chart type, e.g. "line", "area", "bar", "pie", "donut", "radar", "scatter", "heatmap", "radialBar", "treemap".

required
series_callback Callable[[Request], Awaitable[Any]]

Async callable returning the ApexCharts series value. For most types this is a list of {"name": …, "data": […]} dicts; for pie/donut/radialBar it is a flat list of numbers.

required
height int

Chart height in pixels (default 300).

300
options dict[str, Any]

Extra ApexCharts config merged over the chart.type / chart.height defaults. Use this for per-type options such as labels (pie/donut), xaxis.categories (bar/radar), or plotOptions.

dict()
Source code in starlette_admin/widgets.py
@dataclass
class ChartWidget(BaseWidget):
    """Renders an ApexCharts chart inside a Tabler card.

    Args:
        title: Card heading.
        chart_type: ApexCharts chart type, e.g. ``"line"``, ``"area"``,
            ``"bar"``, ``"pie"``, ``"donut"``, ``"radar"``, ``"scatter"``,
            ``"heatmap"``, ``"radialBar"``, ``"treemap"``.
        series_callback: Async callable returning the ApexCharts ``series``
            value.  For most types this is a list of
            ``{"name": …, "data": […]}`` dicts; for pie/donut/radialBar it is
            a flat list of numbers.
        height: Chart height in pixels (default ``300``).
        options: Extra ApexCharts config merged over the ``chart.type`` /
            ``chart.height`` defaults.  Use this for per-type options such as
            ``labels`` (pie/donut), ``xaxis.categories`` (bar/radar), or
            ``plotOptions``.
    """

    template: ClassVar[str] = "widgets/chart_widget.html"

    title: str
    chart_type: str
    series_callback: Callable[[Request], Awaitable[Any]]
    height: int = 300
    options: dict[str, Any] = field(default_factory=dict)

    def additional_js_links(self, request: Request) -> list[str]:
        return [
            static_url(request, "js/vendor/apexcharts.min.js", v="v5.15.2"),
            static_url(request, "js/chart_widget.js", v=1),
        ]

    async def get_context(self, request: Request) -> dict[str, Any]:
        series = await self.series_callback(request)
        chart_id = f"chart-{uuid.uuid4().hex[:8]}"
        return {
            "widget": self,
            "title": self.title,
            "chart_id": chart_id,
            "chart_type": self.chart_type,
            "series": series,
            "height": self.height,
            "options": self.options,
        }

starlette_admin.widgets.TableWidget dataclass

Bases: BaseWidget

Renders a compact summary table from a data callback.

Parameters:

Name Type Description Default
title str

Card heading displayed above the table.

required
columns list[str]

List of column header labels.

required
rows_callback Callable[[Request], Awaitable[list[list[Any]]]]

Async callable returning rows as a list of lists.

required
Source code in starlette_admin/widgets.py
@dataclass
class TableWidget(BaseWidget):
    """Renders a compact summary table from a data callback.

    Args:
        title: Card heading displayed above the table.
        columns: List of column header labels.
        rows_callback: Async callable returning rows as a list of lists.
    """

    template: ClassVar[str] = "widgets/table_widget.html"

    title: str
    columns: list[str]
    rows_callback: Callable[[Request], Awaitable[list[list[Any]]]]

    async def get_context(self, request: Request) -> dict[str, Any]:
        rows = await self.rows_callback(request)
        return {
            "widget": self,
            "title": self.title,
            "columns": self.columns,
            "rows": rows,
        }

starlette_admin.widgets.TextWidget dataclass

Bases: BaseWidget

Renders a block of text, optionally as Markdown.

Parameters:

Name Type Description Default
content str

Static markdown or plain text.

required
markdown bool

Whether to render content as Markdown. If the markdown package is not installed, plain-text rendering is used as a fallback.

False
Source code in starlette_admin/widgets.py
@dataclass
class TextWidget(BaseWidget):
    """Renders a block of text, optionally as Markdown.

    Args:
        content: Static markdown or plain text.
        markdown: Whether to render `content` as Markdown. If the `markdown`
            package is not installed, plain-text rendering is used as a fallback.
    """

    template: ClassVar[str] = "widgets/text_widget.html"

    content: str
    markdown: bool = False
    card: bool = False

    async def get_context(self, request: Request) -> dict[str, Any]:
        rendered: str | Markup
        if self.markdown:
            if _markdown is None:  # pragma: no cover
                raise ImportError(
                    "The 'markdown' package is required when TextWidget.markdown=True. "
                    "Install it with: pip install markdown"
                )
            rendered = Markup(_markdown.markdown(self.content))
        else:
            rendered = self.content
        return {
            "widget": self,
            "content": rendered,
            "markdown": self.markdown,
            "card": self.card,
        }

starlette_admin.widgets.HtmlWidget dataclass

Bases: BaseWidget

Renders an arbitrary block of pre-rendered HTML.

Parameters:

Name Type Description Default
html str

Raw HTML string. It is marked safe and rendered without escaping.

required
Source code in starlette_admin/widgets.py
@dataclass
class HtmlWidget(BaseWidget):
    """Renders an arbitrary block of pre-rendered HTML.

    Args:
        html: Raw HTML string. It is marked safe and rendered without escaping.
    """

    template: ClassVar[str] = "widgets/html_widget.html"

    html: str

    async def get_context(self, request: Request) -> dict[str, Any]:
        return {
            "widget": self,
            "html": Markup(self.html),
        }

starlette_admin.widgets.DividerWidget dataclass

Bases: BaseWidget

A horizontal rule / visual separator between other widgets.

Source code in starlette_admin/widgets.py
@dataclass
class DividerWidget(BaseWidget):
    """A horizontal rule / visual separator between other widgets."""

    template: ClassVar[str] = "widgets/divider_widget.html"

    async def get_context(self, request: Request) -> dict[str, Any]:
        return {"widget": self}

布局部件

布局部件是容器,用于排列其 children(可以是内容部件、表单字段或其他布局部件)。

自动资源管理:布局部件会递归遍历自身的树结构,从子节点收集 additional_css_linksadditional_js_links。这确保深层嵌套的组件无需任何手动接线即可自动加载所需的 CSS/JS 资源。

starlette_admin.widgets.RowWidget dataclass

Bases: BaseWidget

Arranges child widgets horizontally in a plain Bootstrap grid row.

Use CardRowWidget instead when every child is itself a card (KPI stats, charts, tables) and should get Tabler's row-deck row-cards equal-height-card treatment.

Parameters:

Name Type Description Default
children list[BaseWidget | Col]

Widgets (or Col-wrapped widgets) to render side-by-side. Wrap a child in Col to control its responsive column widths; unwrapped children get an auto col class. Also accepts WidgetShorthand (see normalize_widget) in place of a built widget, e.g. a bare field name.

list()
Source code in starlette_admin/widgets.py
@dataclass
class RowWidget(BaseWidget):
    """Arranges child widgets horizontally in a plain Bootstrap grid row.

    Use ``CardRowWidget`` instead when every child is itself a card (KPI
    stats, charts, tables) and should get Tabler's ``row-deck row-cards``
    equal-height-card treatment.

    Args:
        children: Widgets (or ``Col``-wrapped widgets) to render side-by-side.
            Wrap a child in ``Col`` to control its responsive column widths; unwrapped children get an auto ``col`` class.
            Also accepts `WidgetShorthand` (see `normalize_widget`) in place
            of a built widget, e.g. a bare field name.
    """

    template: ClassVar[str] = "widgets/row_widget.html"

    children: list[BaseWidget | Col] = field(default_factory=list)

    def __post_init__(self) -> None:
        self.children = [
            child if isinstance(child, Col) else normalize_widget(child)
            for child in self.children
        ]

    def additional_css_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_css_links")

    def additional_js_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_js_links")

    async def render(self, request: Request, env: Environment) -> Markup:
        items: list[tuple[str, Markup]] = []
        for child in self.children:
            if isinstance(child, Col):
                widget, col_class = child.widget, _col_class(child.breakpoints)
            else:
                widget, col_class = child, "col"
            items.append((col_class, await widget.render(request, env)))
        template = env.get_template(self.template)
        return Markup(template.render({"items": items}, request=request))

starlette_admin.widgets.CardRowWidget dataclass

Bases: RowWidget

A RowWidget for rows of cards: same layout mechanics, plus Tabler's row-deck row-cards classes so the cards in the row share a consistent, equal height. Used for dashboard rows of StatWidget, ChartWidget, TableWidget, etc.

Source code in starlette_admin/widgets.py
@dataclass
class CardRowWidget(RowWidget):
    """A ``RowWidget`` for rows of cards: same layout mechanics, plus
    Tabler's ``row-deck row-cards`` classes so the cards in the row share a
    consistent, equal height. Used for dashboard rows of ``StatWidget``,
    ``ChartWidget``, ``TableWidget``, etc.
    """

    template: ClassVar[str] = "widgets/card_row_widget.html"

starlette_admin.widgets.ColumnWidget dataclass

Bases: BaseWidget

Stacks child widgets vertically.

Parameters:

Name Type Description Default
children list[BaseWidget]

Widgets to stack. Also accepts WidgetShorthand (see normalize_widget) in place of a built widget, e.g. a bare field name or a tuple of names for a side-by-side row.

list()
Source code in starlette_admin/widgets.py
@dataclass
class ColumnWidget(BaseWidget):
    """Stacks child widgets vertically.

    Args:
        children: Widgets to stack. Also accepts `WidgetShorthand` (see
            `normalize_widget`) in place of a built widget, e.g. a bare
            field name or a tuple of names for a side-by-side row.
    """

    template: ClassVar[str] = "widgets/column_widget.html"

    children: list[BaseWidget] = field(default_factory=list)

    def __post_init__(self) -> None:
        self.children = [normalize_widget(child) for child in self.children]

    def additional_css_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_css_links")

    def additional_js_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_js_links")

    async def get_context(self, request: Request) -> dict[str, Any]:
        return {
            "widget": self,
            "children": self.children,
        }

    async def render(self, request: Request, env: Environment) -> Markup:
        ctx = await self.get_context(request)
        ctx["children_html"] = [
            await child.render(request, env) for child in self.children
        ]
        template = env.get_template(self.template)
        return Markup(template.render(ctx, request=request))

starlette_admin.widgets.GridWidget dataclass

Bases: BaseWidget

Arranges child widgets in a responsive Bootstrap grid.

Uses Bootstrap's row-cols-* system: Tabler/Bootstrap handles all responsive behavior; no custom <style> or media queries are emitted.

Parameters:

Name Type Description Default
children list[BaseWidget]

Widgets to render in the grid. Also accepts WidgetShorthand (see normalize_widget) in place of a built widget.

list()
breakpoints Breakpoints

Items-per-row at each viewport size. default sets the base (smallest) column count; each named breakpoint overrides it upward. Example::

Breakpoints(default=1, md=2, lg=3)
# -> "row-cols-1 row-cols-md-2 row-cols-lg-3"
(lambda: Breakpoints(default=1))()
gutter int

Bootstrap gutter scale applied as g-{n} (values 0-5, default 3, approximately 1 rem).

3
Source code in starlette_admin/widgets.py
@dataclass
class GridWidget(BaseWidget):
    """Arranges child widgets in a responsive Bootstrap grid.

    Uses Bootstrap's ``row-cols-*`` system: Tabler/Bootstrap handles all
    responsive behavior; no custom ``<style>`` or media queries are emitted.

    Args:
        children: Widgets to render in the grid. Also accepts
            `WidgetShorthand` (see `normalize_widget`) in place of a built
            widget.
        breakpoints: Items-per-row at each viewport size. ``default`` sets
            the base (smallest) column count; each named breakpoint overrides
            it upward. Example::

                Breakpoints(default=1, md=2, lg=3)
                # -> "row-cols-1 row-cols-md-2 row-cols-lg-3"

        gutter: Bootstrap gutter scale applied as ``g-{n}`` (values 0-5, default 3, approximately 1 rem).
    """

    template: ClassVar[str] = "widgets/grid_widget.html"

    children: list[BaseWidget] = field(default_factory=list)
    breakpoints: Breakpoints = field(default_factory=lambda: Breakpoints(default=1))
    gutter: int = 3

    def __post_init__(self) -> None:
        self.children = [normalize_widget(child) for child in self.children]

    def additional_css_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_css_links")

    def additional_js_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_js_links")

    async def get_context(self, request: Request) -> dict[str, Any]:
        return {
            "widget": self,
            "children": self.children,
            "row_col_class": _row_col_class(self.breakpoints),
            "gutter": self.gutter,
        }

    async def render(self, request: Request, env: Environment) -> Markup:
        ctx = await self.get_context(request)
        ctx["children_html"] = [
            await child.render(request, env) for child in self.children
        ]
        template = env.get_template(self.template)
        return Markup(template.render(ctx, request=request))

starlette_admin.widgets.PanelWidget dataclass

Bases: BaseWidget

Wraps child widgets inside a titled card/panel.

When used in a form_layout and resolved down to exactly one visible field, that field's label is hidden: the panel title already names it, so the label would be redundant. See BaseModelView.resolve_form_layout.

Parameters:

Name Type Description Default
title str

Panel heading.

required
children list[BaseWidget]

Widgets rendered inside the panel body. Also accepts WidgetShorthand (see normalize_widget) in place of a built widget, e.g. a bare field name or a tuple of names for a side-by-side row.

list()
collapsible bool

Whether the panel can be collapsed.

False
collapsed bool

Initial collapsed state (only meaningful when collapsible).

False
icon str

Optional icon class for the panel header.

''
Source code in starlette_admin/widgets.py
@dataclass
class PanelWidget(BaseWidget):
    """Wraps child widgets inside a titled card/panel.

    When used in a `form_layout` and resolved down to exactly one visible
    field, that field's label is hidden: the panel title already names it,
    so the label would be redundant. See
    [BaseModelView.resolve_form_layout][starlette_admin.views.BaseModelView.resolve_form_layout].

    Args:
        title: Panel heading.
        children: Widgets rendered inside the panel body. Also accepts
            `WidgetShorthand` (see `normalize_widget`) in place of a built
            widget, e.g. a bare field name or a tuple of names for a
            side-by-side row.
        collapsible: Whether the panel can be collapsed.
        collapsed: Initial collapsed state (only meaningful when collapsible).
        icon: Optional icon class for the panel header.
    """

    template: ClassVar[str] = "widgets/panel_widget.html"

    title: str
    children: list[BaseWidget] = field(default_factory=list)
    collapsible: bool = False
    collapsed: bool = False
    icon: str = ""

    def __post_init__(self) -> None:
        self.children = [normalize_widget(child) for child in self.children]

    def additional_css_links(self, request: Request) -> list[str]:
        own = [static_url(request, "css/panel_widget.css", v=1)]
        return own + _collect_child_links(
            self.children, request, "additional_css_links"
        )

    def additional_js_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_js_links")

    async def get_context(self, request: Request) -> dict[str, Any]:
        return {
            "widget": self,
            "widget_id": f"panel-{uuid.uuid4().hex[:8]}",
            "title": self.title,
            "children": self.children,
            "collapsible": self.collapsible,
            "collapsed": self.collapsed,
            # "icon" is reserved for the global icon(name) resolver, so this
            # widget's own icon is named "panel_icon" in the render context.
            "panel_icon": self.icon,
        }

    async def render(self, request: Request, env: Environment) -> Markup:
        ctx = await self.get_context(request)
        ctx["children_html"] = [
            await child.render(request, env) for child in self.children
        ]
        template = env.get_template(self.template)
        return Markup(template.render(ctx, request=request))

starlette_admin.widgets.FieldsetWidget dataclass

Bases: BaseWidget

Wraps child widgets inside a native HTML <fieldset>/<legend>.

Use this instead of PanelWidget when you want the semantics and plain styling of a form fieldset rather than a card: no shadow, no header bar, just a bordered group with its legend as the caption.

Parameters:

Name Type Description Default
legend str

Text rendered in the <legend> element.

required
children list[BaseWidget]

Widgets rendered inside the fieldset. Also accepts WidgetShorthand (see normalize_widget) in place of a built widget, e.g. a bare field name or a tuple of names for a side-by-side row.

list()
disabled bool

Sets the HTML disabled attribute on the <fieldset>, which disables every form control nested inside it.

False
Source code in starlette_admin/widgets.py
@dataclass
class FieldsetWidget(BaseWidget):
    """Wraps child widgets inside a native HTML ``<fieldset>``/``<legend>``.

    Use this instead of ``PanelWidget`` when you want the semantics and
    plain styling of a form fieldset rather than a card: no shadow, no
    header bar, just a bordered group with its ``legend`` as the caption.

    Args:
        legend: Text rendered in the ``<legend>`` element.
        children: Widgets rendered inside the fieldset. Also accepts
            `WidgetShorthand` (see `normalize_widget`) in place of a built
            widget, e.g. a bare field name or a tuple of names for a
            side-by-side row.
        disabled: Sets the HTML ``disabled`` attribute on the ``<fieldset>``,
            which disables every form control nested inside it.
    """

    template: ClassVar[str] = "widgets/fieldset_widget.html"

    legend: str
    children: list[BaseWidget] = field(default_factory=list)
    disabled: bool = False

    def __post_init__(self) -> None:
        self.children = [normalize_widget(child) for child in self.children]

    def additional_css_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_css_links")

    def additional_js_links(self, request: Request) -> list[str]:
        return _collect_child_links(self.children, request, "additional_js_links")

    async def get_context(self, request: Request) -> dict[str, Any]:
        return {
            "widget": self,
            "legend": self.legend,
            "children": self.children,
            "disabled": self.disabled,
        }

    async def render(self, request: Request, env: Environment) -> Markup:
        ctx = await self.get_context(request)
        ctx["children_html"] = [
            await child.render(request, env) for child in self.children
        ]
        template = env.get_template(self.template)
        return Markup(template.render(ctx, request=request))

starlette_admin.widgets.TabsWidget dataclass

Bases: BaseWidget

Renders child widgets as Bootstrap tabs.

Parameters:

Name Type Description Default
tabs list[tuple[str, BaseWidget]]

List of (tab_label, widget) tuples. A tab's widget accepts WidgetShorthand (see normalize_widget) in place of a built widget: a bare field name, a tuple of items for a side-by-side row, or a list of entries stacked vertically, normalized to a ColumnWidget. The list form lets a tab hold more than one row without an explicit ColumnWidget wrapper, e.g. ("Tab", ["aa", ("b", "c")]).

list()
save_state bool

Remember the last active tab in the browser's sessionStorage, so it stays selected across page reloads within the same tab/session. The storage key is derived from the request path and each tab's label (a UUID5, so it's still uniquely identifying), not stored on the instance: a TabsWidget may be a persistent view attribute or, like in a dashboard CustomView.widget callable, rebuilt from scratch on every request, so nothing about the key can depend on Python object identity surviving between requests.

True
Source code in starlette_admin/widgets.py
@dataclass
class TabsWidget(BaseWidget):
    """Renders child widgets as Bootstrap tabs.

    Args:
        tabs: List of (tab_label, widget) tuples. A tab's `widget` accepts
            `WidgetShorthand` (see `normalize_widget`) in place of a built
            widget: a bare field name, a tuple of items for a side-by-side
            row, or a *list* of entries stacked vertically, normalized to a
            `ColumnWidget`. The list form lets a tab hold more than one row
            without an explicit `ColumnWidget` wrapper, e.g.
            `("Tab", ["aa", ("b", "c")])`.
        save_state: Remember the last active tab in the browser's
            `sessionStorage`, so it stays selected across page reloads
            within the same tab/session. The storage key is derived from the
            request path and each tab's label (a UUID5, so it's still
            uniquely identifying), not stored on the instance: a `TabsWidget`
            may be a persistent view attribute or, like in a dashboard
            `CustomView.widget` callable, rebuilt from scratch on every
            request, so nothing about the key can depend on Python object
            identity surviving between requests.
    """

    template: ClassVar[str] = "widgets/tabs_widget.html"

    tabs: list[tuple[str, BaseWidget]] = field(default_factory=list)
    save_state: bool = True

    def __post_init__(self) -> None:
        self.tabs = [(label, normalize_widget(widget)) for label, widget in self.tabs]

    def additional_css_links(self, request: Request) -> list[str]:
        return _collect_child_links(
            [w for _, w in self.tabs], request, "additional_css_links"
        )

    def additional_js_links(self, request: Request) -> list[str]:
        links = _collect_child_links(
            [w for _, w in self.tabs], request, "additional_js_links"
        )
        if self.save_state:
            links.append(static_url(request, "js/tabs_widget.js", v=1))
        return links

    def _storage_key(self, request: Request) -> str:
        seed = str(request.url.path) + "|" + "|".join(label for label, _ in self.tabs)
        return f"starlette-admin-tabs-{uuid.uuid5(uuid.NAMESPACE_URL, seed).hex}"

    async def get_context(self, request: Request) -> dict[str, Any]:
        tab_ids = [f"tab-{uuid.uuid4().hex[:8]}" for _ in self.tabs]
        return {
            "widget": self,
            "tabs": self.tabs,
            "tab_ids": tab_ids,
            "save_state": self.save_state,
            "storage_key": self._storage_key(request) if self.save_state else None,
        }

    async def render(self, request: Request, env: Environment) -> Markup:
        ctx = await self.get_context(request)
        ctx["tabs_html"] = [
            (label, await widget.render(request, env)) for label, widget in self.tabs
        ]
        template = env.get_template(self.template)
        return Markup(template.render(ctx, request=request))

响应式尺寸

专用于管理不同屏幕尺寸下的响应式网格行为、列宽和断点的工具类。

starlette_admin.widgets.Breakpoints dataclass

Bootstrap breakpoint column sizes for Col and GridWidget.

Each field maps to a Bootstrap responsive infix. None (the default) means the breakpoint is omitted from the generated class string.

For Col, the values are Bootstrap column spans (1-12), or the literal "auto" for an equal-width flexible column ("col-md" rather than "col-md-6"): Breakpoints(default=12, md=6) -> "col-12 col-md-6" Breakpoints(default=12, md="auto") -> "col-12 col-md"

For GridWidget, the values are items-per-row counts ("auto" does not apply there): Breakpoints(default=1, md=2, lg=3) -> "row-cols-1 row-cols-md-2 row-cols-lg-3"

Source code in starlette_admin/widgets.py
@dataclass
class Breakpoints:
    """Bootstrap breakpoint column sizes for ``Col`` and ``GridWidget``.

    Each field maps to a Bootstrap responsive infix. ``None`` (the default)
    means the breakpoint is omitted from the generated class string.

    For ``Col``, the values are Bootstrap column spans (1-12), or the literal
    ``"auto"`` for an equal-width flexible column (``"col-md"`` rather than
    ``"col-md-6"``):
      ``Breakpoints(default=12, md=6)`` -> ``"col-12 col-md-6"``
      ``Breakpoints(default=12, md="auto")`` -> ``"col-12 col-md"``

    For ``GridWidget``, the values are items-per-row counts (``"auto"`` does
    not apply there):
      ``Breakpoints(default=1, md=2, lg=3)`` -> ``"row-cols-1 row-cols-md-2 row-cols-lg-3"``
    """

    default: int | Literal["auto"] | None = None
    sm: int | Literal["auto"] | None = None
    md: int | Literal["auto"] | None = None
    lg: int | Literal["auto"] | None = None
    xl: int | Literal["auto"] | None = None
    xxl: int | Literal["auto"] | None = None

starlette_admin.widgets.Col dataclass

Responsive column wrapper for children of RowWidget.

Wrap a child widget with Col to control its Bootstrap column span at each viewport size. Omitting breakpoints (or leaving all fields None) yields an auto col class.

widget accepts the same shorthand as any other widget slot (see normalize_widget), so Col("email", Breakpoints(md=6)) is equivalent to Col(FieldRef("email"), Breakpoints(md=6)).

Example::

Col(my_widget, breakpoints=Breakpoints(default=12, md=6))
# -> class="col-12 col-md-6"
Source code in starlette_admin/widgets.py
@dataclass
class Col:
    """Responsive column wrapper for children of ``RowWidget``.

    Wrap a child widget with ``Col`` to control its Bootstrap column span at
    each viewport size. Omitting ``breakpoints`` (or leaving all fields
    ``None``) yields an auto ``col`` class.

    ``widget`` accepts the same shorthand as any other widget slot (see
    `normalize_widget`), so ``Col("email", Breakpoints(md=6))`` is
    equivalent to ``Col(FieldRef("email"), Breakpoints(md=6))``.

    Example::

        Col(my_widget, breakpoints=Breakpoints(default=12, md=6))
        # -> class="col-12 col-md-6"
    """

    widget: BaseWidget
    breakpoints: Breakpoints = field(default_factory=Breakpoints)

    def __post_init__(self) -> None:
        self.widget = normalize_widget(self.widget)

表单布局引用

专用于模型表单场景的特殊部件,用于引用特定的数据库字段。

starlette_admin.widgets.FieldRef dataclass

Bases: BaseWidget

Leaf widget referencing a declared field by name, for use inside BaseModelView.form_layout.

FieldRef("email") placed anywhere in a form_layout tree — bare, or nested inside RowWidget, PanelWidget, TabsWidget, etc. — means "render the declared email field here". It is not self-sufficient: the _field/_value/_error attributes are filled in by BaseModelView.resolve_form_layout from the current request's obj/errors/field permissions before rendering, so a bare FieldRef constructed and rendered outside that pipeline has nothing to show.

Parameters:

Name Type Description Default
name str

Name of the declared field to render.

required
show_label bool

Whether to render the field's <label>. Set to False when the surrounding layout already conveys the field's purpose, so the label would be redundant.

True
prepend str | None

Bootstrap/Tabler input-group addon rendered before the input, e.g. "@" or '<i class="fa fa-phone"></i>'. Plain text or raw HTML, rendered unescaped. Only honored by fields whose form template renders a native <input>. Other field types silently ignore it.

None
append str | None

Same as prepend, rendered after the input.

None
flat bool

Render the input-group with Tabler's input-group-flat style. Only applies when prepend or append is set.

False
Source code in starlette_admin/widgets.py
@dataclass
class FieldRef(BaseWidget):
    """Leaf widget referencing a declared field by name, for use inside
    ``BaseModelView.form_layout``.

    ``FieldRef("email")`` placed anywhere in a `form_layout` tree — bare,
    or nested inside ``RowWidget``, ``PanelWidget``, ``TabsWidget``, etc. —
    means "render the declared ``email`` field here". It is not
    self-sufficient: the ``_field``/``_value``/``_error`` attributes are
    filled in by ``BaseModelView.resolve_form_layout`` from the current
    request's `obj`/`errors`/field permissions before rendering, so a bare
    ``FieldRef`` constructed and rendered outside that pipeline has nothing
    to show.

    Args:
        name: Name of the declared field to render.
        show_label: Whether to render the field's `<label>`. Set to `False`
            when the surrounding layout already conveys the field's purpose,
            so the label would be redundant.
        prepend: Bootstrap/Tabler input-group addon rendered before the
            input, e.g. ``"@"`` or ``'<i class="fa fa-phone"></i>'``. Plain
            text or raw HTML, rendered unescaped. Only honored by fields
            whose form template renders a native `<input>`. Other field types
            silently ignore it.
        append: Same as `prepend`, rendered after the input.
        flat: Render the input-group with Tabler's `input-group-flat` style.
            Only applies when `prepend` or `append` is set.
    """

    template: ClassVar[str] = "widgets/form_field_widget.html"

    name: str
    show_label: bool = True
    prepend: str | None = None
    append: str | None = None
    flat: bool = False
    _field: BaseField | None = field(default=None, repr=False)
    _value: Any = field(default=None, repr=False)
    _error: str | None = field(default=None, repr=False)

    async def get_context(self, request: Request) -> dict[str, Any]:
        return {
            "field": self._field,
            "value": self._value,
            "error": self._error,
            "show_label": self.show_label,
            "input_group_prepend": (
                Markup(self.prepend) if self.prepend is not None else None
            ),
            "input_group_append": (
                Markup(self.append) if self.append is not None else None
            ),
            "input_group_flat": self.flat,
        }

简写与规范化

为了让布局代码整洁且易于阅读,容器部件接受普通 Python 类型来代替显式的部件类实例化。在初始化期间(__post_init__),容器会自动将这些简写值解析为对应的部件对象。

支持的简写形式:

  • str:解析为字段引用(FieldRef)。
  • tuple:解析为并排行(RowWidget)。
  • list:解析为垂直堆叠(ColumnWidget)。

starlette_admin.widgets.WidgetShorthand = 'BaseWidget | str | tuple[Any, ...] | list[Any]' module-attribute

Anything a container widget's children (or Col.widget, or a TabsWidget tab's widget) will accept in place of an already-built BaseWidget. See normalize_widget for what each shorthand expands to.

starlette_admin.widgets.normalize_widget(node)

Expand a shorthand WidgetShorthand into a real widget.

A bare str becomes a FieldRef referencing the declared field of that name. A tuple becomes a RowWidget with each item in its own Col, full width below the md breakpoint and equal width at md and above (matching how a single item renders full width on its own); if every item resolves to a StatWidget, a CardRowWidget is used instead so the cards get the row-deck equal-height treatment. A list becomes a ColumnWidget stacking each item vertically. Anything else is assumed to already be a BaseWidget and is returned unchanged.

Each item handed to the resulting RowWidget/ColumnWidget is itself shorthand-typed, so nesting (a tuple inside a list, a list inside a tuple, and so on) is expanded in turn by that container's own __post_init__ when it is constructed below: no explicit recursion is needed here.

Source code in starlette_admin/widgets.py
def normalize_widget(node: WidgetShorthand) -> BaseWidget:
    """Expand a shorthand `WidgetShorthand` into a real widget.

    A bare `str` becomes a `FieldRef` referencing the declared field of
    that name. A `tuple` becomes a `RowWidget` with each item in its own
    `Col`, full width below the `md` breakpoint and equal width at `md` and
    above (matching how a single item renders full width on its own); if
    every item resolves to a `StatWidget`, a `CardRowWidget` is used instead
    so the cards get the row-deck equal-height treatment. A `list` becomes a
    `ColumnWidget` stacking each item vertically. Anything else is assumed
    to already be a `BaseWidget` and is returned unchanged.

    Each item handed to the resulting `RowWidget`/`ColumnWidget` is itself
    shorthand-typed, so nesting (a tuple inside a list, a list inside a
    tuple, and so on) is expanded in turn by that container's own
    `__post_init__` when it is constructed below: no explicit recursion is
    needed here.
    """
    if isinstance(node, str):
        return FieldRef(node)
    if isinstance(node, tuple):
        # `Col`/`ColumnWidget` accept `WidgetShorthand` at construction and
        # normalize it in `__post_init__`, but their fields are declared as
        # the already-normalized `BaseWidget` for readers after that point.
        cols: list[BaseWidget | Col] = [
            Col(cast(BaseWidget, item), Breakpoints(default=12, md="auto"))
            for item in node
        ]
        row_cls = (
            CardRowWidget
            if cols
            and all(
                isinstance(col, Col) and isinstance(col.widget, StatWidget)
                for col in cols
            )
            else RowWidget
        )
        return row_cls(children=cols)
    if isinstance(node, list):
        return ColumnWidget(children=cast("list[BaseWidget]", node))
    return node

辅助函数

用于在 Jinja2 模板或自定义上下文中渲染部件的工具函数。

starlette_admin.widgets.render_widget(widget, request, env) async

Render widget to HTML using env.

This is a convenience helper for custom views that want to render widgets outside of the default CustomView.widget flow.

Source code in starlette_admin/widgets.py
async def render_widget(
    widget: BaseWidget, request: Request, env: Environment
) -> Markup:
    """Render `widget` to HTML using `env`.

    This is a convenience helper for custom views that want to render widgets
    outside of the default ``CustomView.widget`` flow.
    """
    return await widget.render(request, env)