组件工坊:Reflex 组件库 100 例
·
课程概览
布局
·
Box
·
Flex
·
Stack
·
Grid
·
Container
·
Section
·
Center
·
Spacer
·
Separator
·
Card
·
Inset
·
Aspect Ratio
·
Fragment
排版
·
Text
·
Heading
·
Link
·
Code
·
Markdown
·
Blockquote
·
Quote
·
Strong
·
Em
·
Kbd
表单
·
Button
·
Input
·
Text Area
·
Checkbox
·
Switch
·
Radio Group
·
Select
·
Slider
·
Form
·
Upload
数据展示
·
Badge
·
Avatar
·
Icon
·
Callout
·
List
·
Data List
·
Code Block
·
Progress
·
Spinner
·
Scroll Area
·
Moment
展开与切换
·
Tabs
·
Accordion
·
Segmented Control
浮层
·
Tooltip
·
Popover
·
Hover Card
·
Dialog
·
Alert Dialog
·
Drawer
·
Dropdown Menu
·
Context Menu
·
Toast
动态渲染
·
Cond
·
Match
·
Foreach
·
Auto Scroll
媒体
·
Image
·
Audio
·
Video
表格与数据网格
·
Table
·
Data Table
·
Data Editor
图表
·
Area Chart
·
Bar Chart
·
Line Chart
·
Composed Chart
·
Pie Chart
·
Radar Chart
·
Radial Bar Chart
·
Scatter Chart
·
Funnel Chart
·
Treemap
·
Sankey Chart
·
Error Bar
图表部件
·
Axis
·
Cartesian Grid
·
Chart Tooltip
·
Legend
·
Label
·
Reference
·
Brush
其他图表库
·
Plotly
·
Pyplot
HTML 元素
·
HTML
·
HTML Text
·
HTML Layout
·
HTML Forms
·
HTML Media
·
HTML Tables
·
SVG
其他工具
·
Theme
·
Skeleton
·
Clipboard
·
Html Embed
·
Script
·
Memo
rx.table.rootrx.table.headerrx.table.rowrx.table.column_header_cellrx.table.bodyrx.table.cellrx.table.row_header_cell官方文档
工坊订单
在团队管理页面做一个成员列表表格。每一行包含头像、姓名链接、邮箱和角色徽章。
rx.table 是由表头、行和单元格亲手拼装出来的表格。单元格里可以放头像、链接、按钮等任何组件,适合需要自由决定外观的列表。如果需要一开始就带排序、搜索和分页的表格,Data Table 可能更快。
table.root:整个表格。table.header:表头部分(<thead>)。里面放 table.row 和 table.column_header_cell(列标题单元格)。table.body:表体(<tbody>)。里面放 table.row。table.row:一行。table.row_header_cell:代表该行的第一个单元格(加粗显示的行标题)。table.cell:普通单元格。rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Full name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Group"),
),
),
rx.table.body(
rx.table.row(
rx.table.row_header_cell("Danilo Sousa"),
rx.table.cell("danilo@example.com"),
rx.table.cell("Developer"),
),
rx.table.row(
rx.table.row_header_cell("Zahra Ambessa"),
rx.table.cell("zahra@example.com"),
rx.table.cell("Admin"),
),
rx.table.row(
rx.table.row_header_cell("Jasper Eriks"),
rx.table.cell("jasper@example.com"),
rx.table.cell("Developer"),
),
),
width="100%",
)表格最好用 width 适配外层容器。如果列太多溢出,可以像 rx.box(rx.table.root(...), overflow_x="auto", width="100%") 这样包一层,让表格自己横向滚动,而不是整个页面滚动。
variant 和 size 设置在 table.root 上,单元格内容的水平对齐 justify 设置在各个单元格上。数字列通常右对齐(end)。
试着改属性
variant
size
justify
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Item"),
rx.table.column_header_cell("Qty", justify="end"),
rx.table.column_header_cell("Price", justify="end"),
),
),
rx.table.body(
rx.table.row(
rx.table.row_header_cell("Coffee beans"),
rx.table.cell("2", justify="end"),
rx.table.cell("$24.00", justify="end"),
),
rx.table.row(
rx.table.row_header_cell("Paper filters"),
rx.table.cell("1", justify="end"),
rx.table.cell("$5.50", justify="end"),
),
),
variant="ghost",
size="2",
width="100%",
)| 部件 | 属性 | 取值 | 用途 |
|---|---|---|---|
root | variant | "surface" "ghost" | 带边框和背景的表格、只有分隔线的表格 |
root | size | "1" "2" "3" | 单元格内边距与字号 |
root | align · summary | "left" "center" "right" · 字符串 | 表格对齐、表格摘要(HTML 属性) |
row | align | "start" "center" "end" "baseline" | 行内各单元格的垂直对齐 |
column_header_cell · cell · row_header_cell | justify | "start" "center" "end" | 单元格内容的水平对齐 |
| 三种单元格 | col_span · row_span | 整数 | 合并多列、多行的单元格 |
| 三种单元格 | min_width · max_width | CSS 长度 | 列宽的最小值与最大值 |
cell · row_header_cell | p px py pt pr pb pl | "0" ~ "9" | 单元格内边距(主题间距等级) |
column_header_cell · row_header_cell | scope | "col" "row" 等 | 表头单元格所指的范围(HTML 属性) |
header 和 body 没有专属属性。单元格的 align 是 HTML 的旧式 align 属性,Radix 的对齐要用 justify。
col_span=2 让一个单元格占两列,row_span=2 让它占两行。行的 align 决定该行各单元格的垂直位置。
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Day"),
rx.table.column_header_cell("Morning"),
rx.table.column_header_cell("Afternoon"),
),
),
rx.table.body(
rx.table.row(
rx.table.row_header_cell("Mon"),
rx.table.cell("Workshop", col_span=2, justify="center"),
),
rx.table.row(
rx.table.row_header_cell("Tue"),
rx.table.cell("Review", row_span=2),
rx.table.cell("Demo"),
align="center",
),
rx.table.row(
rx.table.row_header_cell("Wed"),
rx.table.cell("Q&A"),
),
),
variant="surface",
width="100%",
)如果表格内容需要变化,就把数据放进 State,用 rx.foreach 渲染每一行。把渲染一行的代码单独写成函数,表格代码会更简短。行数据也可以用 dict 或 dataclass 代替列表(用 dataclass 时,可以像 person.email 这样按名称取值)。
class TableForEachState(rx.State):
people: list[list[str]] = [
["Danilo Sousa", "danilo@example.com", "Developer"],
["Zahra Ambessa", "zahra@example.com", "Admin"],
["Jasper Eriks", "jasper@example.com", "Developer"],
]
def table_person_row(person: list):
return rx.table.row(
rx.table.cell(person[0]),
rx.table.cell(person[1]),
rx.table.cell(person[2]),
)
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Full name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Group"),
),
),
rx.table.body(rx.foreach(TableForEachState.people, table_person_row)),
width="100%",
)如果数据在内存中,就用 Python 的 sorted() 和列表推导式来排序和搜索。原始数据放在 _people 这样以下划线开头的后端专用变量里,不发送到浏览器;页面上显示的列表则写成计算变量 current_people。因为设置了 cache=True,只有排序方式或搜索词变化时才会重新计算。搜索框的 on_change 加上了 .debounce(300),不是每敲一个字就发送事件,而是在输入停下来时才发送。
class TableSearchState(rx.State):
_people: list[dict[str, str]] = [
{"full_name": "Danilo Sousa", "email": "danilo@example.com", "group": "Developer"},
{"full_name": "Zahra Ambessa", "email": "zahra@example.com", "group": "Admin"},
{"full_name": "Jasper Eriks", "email": "zjasper@example.com", "group": "B-Developer"},
{"full_name": "Alice Brown", "email": "alice@example.com", "group": "Admin"},
]
sort_value: str = ""
search_value: str = ""
@rx.event
def set_sort_value(self, value: str):
self.sort_value = value
@rx.event
def set_search_value(self, value: str):
self.search_value = value
@rx.var(cache=True)
def current_people(self) -> list[dict[str, str]]:
people = self._people
if self.sort_value != "":
people = sorted(people, key=lambda p: p[self.sort_value].lower())
if self.search_value != "":
term = self.search_value.lower()
people = [p for p in people if any(term in v.lower() for v in p.values())]
return people
def table_search_row(person: dict):
return rx.table.row(
rx.table.cell(person["full_name"]),
rx.table.cell(person["email"]),
rx.table.cell(person["group"]),
)
rx.vstack(
rx.flex(
rx.select(
["full_name", "email", "group"],
placeholder="Sort By: full_name",
on_change=TableSearchState.set_sort_value,
),
rx.input(
placeholder="Search here...",
on_change=TableSearchState.set_search_value.debounce(300),
),
spacing="3",
wrap="wrap",
),
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Full name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Group"),
),
),
rx.table.body(rx.foreach(TableSearchState.current_people, table_search_row)),
width="100%",
),
width="100%",
)如果数据在数据库里,就用 SQL 查询实现同样的界面。把官方文档的示例精简后如下(本页不会执行它)。ilike 是不区分大小写的部分匹配,搜索词前后的 % 表示「任意字符」。
from sqlmodel import asc, or_, select
class Customer(rx.Model, table=True):
name: str
email: str
phone: str
address: str
class DatabaseTableState(rx.State):
users: list[Customer] = []
sort_value: str = ""
search_value: str = ""
@rx.event
def load_entries(self):
with rx.session() as session:
query = select(Customer)
if self.search_value:
pattern = f"%{self.search_value.lower()}%"
query = query.where(or_(Customer.name.ilike(pattern), Customer.email.ilike(pattern)))
if self.sort_value:
query = query.order_by(asc(getattr(Customer, self.sort_value)))
self.users = session.exec(query).all()数据越多,越应该在数据库端过滤;数据量小或来自 API 时,在内存中过滤更简单。
如果一次只想显示几行,就在 State 中放 offset(跳过的行数)和 limit(显示的行数),只截取这么多放进 users。初始数据由 table.root 的 on_mount 填充。如果是数据库,就不用截取,改用 query.offset(self.offset).limit(self.limit)。
Page 1 / 1
class TablePageState(rx.State):
_customers: list[dict[str, str]] = [
{"name": "John Doe", "email": "john@example.com", "phone": "555-1234"},
{"name": "Jane Smith", "email": "jane@example.com", "phone": "555-5678"},
{"name": "Bob Johnson", "email": "bob@example.com", "phone": "555-9012"},
{"name": "Alice Brown", "email": "alice@example.com", "phone": "555-3456"},
{"name": "Charlie Wilson", "email": "charlie@example.com", "phone": "555-7890"},
{"name": "Emily Davis", "email": "emily@example.com", "phone": "555-2345"},
{"name": "Frank Miller", "email": "frank@example.com", "phone": "555-6789"},
]
users: list[dict[str, str]] = []
total_items: int = 0
offset: int = 0
limit: int = 3
@rx.var
def page_number(self) -> int:
return self.offset // self.limit + 1
@rx.var
def total_pages(self) -> int:
return max(1, -(-self.total_items // self.limit))
@rx.event
def load_entries(self):
self.total_items = len(self._customers)
self.users = self._customers[self.offset : self.offset + self.limit]
@rx.event
def prev_page(self):
self.offset = max(self.offset - self.limit, 0)
self.load_entries()
@rx.event
def next_page(self):
if self.offset + self.limit < self.total_items:
self.offset += self.limit
self.load_entries()
def table_page_row(user: dict):
return rx.table.row(
rx.table.cell(user["name"]),
rx.table.cell(user["email"]),
rx.table.cell(user["phone"]),
)
rx.vstack(
rx.hstack(
rx.button("Prev", on_click=TablePageState.prev_page, variant="soft"),
rx.text("Page ", TablePageState.page_number, " / ", TablePageState.total_pages),
rx.button("Next", on_click=TablePageState.next_page, variant="soft"),
align="center",
),
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Phone"),
),
),
rx.table.body(rx.foreach(TablePageState.users, table_page_row)),
on_mount=TablePageState.load_entries,
width="100%",
),
width="100%",
)如果一个页面上有多个分页表格,每个表格都要有各自的 offset 和 limit。搜索词或排序变化时,要把 offset 重置为 0,以免停留在结果变少后已不存在的页上。想多次复用同一个表格,可以用 rx.ComponentState 让每个表格拥有独立的 State。
用 rx.download 可以让用户把表格数据下载成文件。JSON 直接在页面一侧传入 State 值,非字符串的值会转成 JSON 文本保存。CSV 则在事件处理器里用 csv 模块生成文本,然后返回 rx.download。两种情况下文件都由用户的浏览器接收,服务器上什么都不会保存。
import csv
import io
import json
class TableDownloadState(rx.State):
users: list[dict[str, str]] = [
{"name": "John Doe", "email": "john@example.com", "phone": "555-1234"},
{"name": "Jane Smith", "email": "jane@example.com", "phone": "555-5678"},
{"name": "Bob Johnson", "email": "bob@example.com", "phone": "555-9012"},
]
@rx.event
def download_csv_data(self):
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=["name", "email", "phone"])
writer.writeheader()
writer.writerows(self.users)
return rx.download(data=output.getvalue(), filename="data.csv")
def table_download_row(user: dict):
return rx.table.row(
rx.table.cell(user["name"]),
rx.table.cell(user["email"]),
rx.table.cell(user["phone"]),
)
rx.vstack(
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Phone"),
),
),
rx.table.body(rx.foreach(TableDownloadState.users, table_download_row)),
width="100%",
),
rx.hstack(
rx.button("Download as JSON", on_click=rx.download(data=TableDownloadState.users, filename="data.json")),
rx.button("Download as CSV", on_click=TableDownloadState.download_csv_data),
spacing="3",
wrap="wrap",
),
width="100%",
spacing="4",
)这是一个没有表头、只有表体的成员列表。第一个单元格放头像,行标题单元格放姓名链接,最后一个单元格放角色徽章,每一行都用 align="center" 让头像和文字垂直对齐。为了在窄屏上也不让页面溢出,表格包在 overflow_x="auto" 的容器里。
Invite and manage your team members
rx.flex(
rx.heading("Your Team", as_="h2", size="5"),
rx.text("Invite and manage your team members"),
rx.flex(
rx.input(placeholder="Email Address"),
rx.button("Invite"),
spacing="2",
),
rx.box(
rx.table.root(
rx.table.body(
rx.table.row(
rx.table.cell(rx.avatar(fallback="DS")),
rx.table.row_header_cell(rx.link("Danilo Sousa")),
rx.table.cell("danilo@example.com"),
rx.table.cell(rx.badge("Developer")),
align="center",
),
rx.table.row(
rx.table.cell(rx.avatar(fallback="ZA")),
rx.table.row_header_cell(rx.link("Zahra Ambessa")),
rx.table.cell("zahra@example.com"),
rx.table.cell(rx.badge("Admin", color_scheme="orange")),
align="center",
),
rx.table.row(
rx.table.cell(rx.avatar(fallback="JE")),
rx.table.row_header_cell(rx.link("Jasper Eriksson")),
rx.table.cell("jasper@example.com"),
rx.table.cell(rx.badge("Developer")),
align="center",
),
),
width="100%",
),
overflow_x="auto",
width="100%",
),
width="100%",
direction="column",
spacing="2",
)工坊笔记
root > header(row > column_header_cell) + body(row > row_header_cell · cell)。
root 上的 variant(surface/ghost)· size,单元格上的 justify · col_span · row_span,行上的 align。
会变化的数据用 State + rx.foreach。原始数据放在 _后端变量里,要显示的列表写成计算变量。
排序、搜索、分页需要自己实现。需要功能齐全的表格时,用 rx.data_table。
宽表格用 overflow_x="auto" 的容器包起来,只让表格滚动。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。