组件工坊: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
工坊订单
请在退款申请页做一个附加收据照片的区域。最多 3 张图片,拖进来就立即附上,并显示文件名和大小。
rx.upload 是一个可以拖放文件或点击选择文件的区域(拖放区)。选中的文件先留在浏览器里,把 rx.upload_files() 交给事件处理器时才会发送到服务器。服务器上的处理器收到文件列表,再读取或保存。
本课程的示例运行在公共服务器上,所以不保存文件,只显示文件名和大小。保存文件的代码另外只以代码形式展示。
Drop files here or click to select
rx.upload(
rx.text("Drop files here or click to select"),
id="upload_basic",
border="2px dashed var(--gray-7)",
padding="2em",
)这些属性决定拖放区的行为。选几个文件后,试着关掉 multiple 或打开 no_click。选中的文件名会以徽章形式显示在下方。
试着改属性
Drop files here or click to select
label
multiple
disabled
no_click
no_drag
no_keyboard
rx.vstack(rx.upload(rx.text("Drop files here or click to select"), id="upload_lab", multiple=True, disabled=False, no_click=False, no_drag=False, no_keyboard=False, border="2px dashed var(--gray-7)", padding="2em"), rx.foreach(rx.selected_files("upload_lab"), lambda f: rx.badge(f, variant="soft")))| 属性 | 取值 | 默认值 | 用途 |
|---|---|---|---|
id | 字符串(固定) | "default" | 这个拖放区的名字。下面的特殊 Var·事件使用同一个 id |
accept | {MIME: [扩展名...]} | 所有文件 | 接受的文件类型 |
multiple | True / False | True | 允许多选 |
max_files | 整数 | 不限 | 最多文件数 |
max_size, min_size | 字节 | 不限 | 单个文件的最大·最小尺寸 |
disabled | True / False | False | 停用拖放区 |
no_click, no_drag, no_keyboard | True / False | False | 禁止点击 / 拖放 / 键盘打开 |
drag_active_style | 样式 dict | 无 | 文件拖入时的样式(0.10) |
on_drop | 事件 | 保存所选文件 | 放下或选中文件后立即执行 |
on_drop_rejected | 事件 | 错误提示(toast) | 有不符合条件的文件进来时 |
选中的文件会进入浏览器端的列表。用特殊 Var rx.selected_files(id) 把文件名列表显示在页面上,用特殊事件 rx.clear_selected_files(id) 清空。此时还没有任何东西发到服务器。
id 是连接拖放区与特殊 Var·事件的钥匙,必须全部相同。请用字符串或模块常量,不能用 State 变量。拖放区里的 "Select File" 按钮只是个样子,因为在拖放区里点任何地方都会打开文件选择窗口。
Drag and drop files here or click to select files
rx.vstack(
rx.upload(
rx.vstack(
rx.button("Select File", variant="outline"),
rx.text("Drag and drop files here or click to select files", size="2"),
align="center",
),
id="upload_selected",
border="1px dotted var(--indigo-8)",
padding="2em",
),
rx.hstack(rx.foreach(rx.selected_files("upload_selected"), lambda f: rx.badge(f)), wrap="wrap"),
rx.button("Clear", on_click=rx.clear_selected_files("upload_selected"), variant="soft", color_scheme="gray"),
)把 rx.upload_files(upload_id=id) 交给处理器,选中的文件就会发到服务器。处理器是一个普通的 async 事件,接收一个 files: list[rx.UploadFile]。每个文件都可以用 file.name(原文件名)、file.size(字节)、await file.read()(内容)。
上传处理器不要加 @rx.event(background=True),也不要用 async with self。那些只用于下面的分块上传。另外,上传按钮要放在拖放区外面。放在里面的话,每次点击都会打开文件窗口,同时开始上传。
Drop up to 5 files (2 MB each)
def upload_size_label(size: int) -> str:
return f"{size / 1024:.1f} KB" if size < 1024 * 1024 else f"{size / 1024 / 1024:.2f} MB"
class UploadInfoState(rx.State):
files: list[dict[str, str]] = []
@rx.event
async def handle_upload(self, files: list[rx.UploadFile]):
# Read only the name and size. Nothing is saved to disk.
self.files = [{"name": str(f.name), "size": upload_size_label(f.size or 0)} for f in files]
rx.vstack(
rx.upload(
rx.text("Drop up to 5 files (2 MB each)"),
id="upload_info",
max_files=5,
max_size=2 * 1024 * 1024,
border="2px dashed var(--gray-7)",
padding="2em",
),
rx.hstack(rx.foreach(rx.selected_files("upload_info"), lambda f: rx.badge(f, variant="soft")), wrap="wrap"),
rx.hstack(
rx.button("Upload", on_click=UploadInfoState.handle_upload(rx.upload_files(upload_id="upload_info"))),
rx.button("Clear", on_click=rx.clear_selected_files("upload_info"), variant="soft", color_scheme="gray"),
),
rx.foreach(
UploadInfoState.files,
lambda f: rx.hstack(rx.icon("file", size=14), rx.text(f["name"], size="2"), rx.code(f["size"]), align="center"),
),
)实际应用中,把文件保存到 rx.get_upload_dir() 返回的文件夹(Path,默认 ./uploaded_files,可用环境变量 REFLEX_UPLOADED_FILES_DIR 修改),页面上则用 rx.get_upload_url(文件名) 生成地址,交给 rx.image 等组件。下面是官方文档的基础示例,本页不会执行它。
class State(rx.State):
uploaded_files: list[str] = []
@rx.event
async def handle_upload(self, files: list[rx.UploadFile]):
for file in files:
data = await file.read()
path = rx.get_upload_dir() / file.name
with path.open("wb") as f:
f.write(data)
self.uploaded_files.append(file.name)
def upload_component():
return rx.vstack(
rx.upload(id="upload"),
rx.button("Upload", on_click=State.handle_upload(rx.upload_files("upload"))),
rx.foreach(
State.uploaded_files,
lambda f: rx.image(src=rx.get_upload_url(f), alt="Uploaded file preview"),
),
)rx.get_upload_dir():服务器(后端)上的保存路径,在事件处理器中使用。rx.get_upload_url(name):页面(前端)上使用的地址。它不是 Python 字符串而是前端 Var,所以只在组件代码里用。地址形如 /_upload/文件名,但不要手写,要通过这个函数生成。/_upload 对所有人公开。保存时最好加上随机前缀,避免重名。把处理器接到 on_drop 上,不用按钮,选中就立即上传。用 accept 过滤文件类型,用 max_files 和 max_size 过滤数量和大小。不符合条件的文件会进入 on_drop_rejected;不提供这个事件时,0.10 会弹出错误提示。在下面放一个不是图片或 PDF 的文件,或者超过 2MB 的文件试试。
Images or PDF only, up to 3 files
class UploadDropState(rx.State):
names: list[str] = []
rejected: int = 0
@rx.event
async def handle_drop(self, files: list[rx.UploadFile]):
self.names = [f"{f.name} ({f.size or 0} bytes)" for f in files]
@rx.event
def handle_rejected(self, rejections: list):
self.rejected = len(rejections)
rx.vstack(
rx.upload(
rx.text("Images or PDF only, up to 3 files", size="2"),
id="upload_drop",
multiple=True,
accept={
"application/pdf": [".pdf"],
"image/png": [".png"],
"image/jpeg": [".jpg", ".jpeg"],
"image/webp": [".webp"],
},
max_files=3,
max_size=2 * 1024 * 1024,
no_keyboard=True,
on_drop=UploadDropState.handle_drop(rx.upload_files(upload_id="upload_drop")),
on_drop_rejected=UploadDropState.handle_rejected,
border="2px dashed var(--gray-7)",
padding="2em",
),
rx.foreach(UploadDropState.names, lambda n: rx.text(n, size="2")),
rx.cond(UploadDropState.rejected > 0, rx.text("Rejected: ", UploadDropState.rejected, size="2", color_scheme="red")),
)rx.upload 是带默认边框和内边距的拖放区。想从头自己做外观,就用 rx.upload.root。属性和事件都一样。用 drag_active_style 可以改变文件拖入期间的背景。
Click to upload
or drag and drop
SVG, PNG, JPG or GIF (MAX. 5MB)
rx.vstack(
rx.upload.root(
rx.box(
rx.icon("cloud-upload", size=40, color=rx.color("indigo", 9)),
rx.hstack(rx.text("Click to upload", weight="bold", color=rx.color("indigo", 11)), rx.text("or drag and drop"), size="2"),
rx.text("SVG, PNG, JPG or GIF (MAX. 5MB)", size="1", color=rx.color("gray", 10)),
display="flex",
flex_direction="column",
align_items="center",
gap="6px",
padding="1.5rem",
text_align="center",
),
id="upload_root_demo",
max_width="24rem",
border=f"2px dashed {rx.color('indigo', 7)}",
border_radius="12px",
cursor="pointer",
drag_active_style={"background_color": rx.color("indigo", 3)},
),
rx.hstack(rx.foreach(rx.selected_files("upload_root_demo"), lambda f: rx.badge(f)), wrap="wrap"),
)rx.upload_files(以及 rx.upload_files_chunk)接受 on_upload_progress。上传期间,{"loaded": 已发送字节, "total": 总字节, "progress": 0~1} 这样的 dict 会传给这个处理器。rx.cancel_upload(id) 会中止正在进行的上传。本页只接受 2MB 以内的文件,所以进度很快就到 100。
Drag and drop files here or click to select files
Total bytes received: 0
class UploadProgressState(rx.State):
uploading: bool = False
progress: int = 0
total_bytes: int = 0
@rx.event
async def handle_upload(self, files: list[rx.UploadFile]):
self.total_bytes = sum(f.size or 0 for f in files)
@rx.event
def handle_upload_progress(self, progress: dict):
self.uploading = True
self.progress = round(progress["progress"] * 100)
if self.progress >= 100:
self.uploading = False
@rx.event
def cancel_upload(self):
self.uploading = False
return rx.cancel_upload("upload_progress")
rx.vstack(
rx.upload(
rx.text("Drag and drop files here or click to select files", size="2"),
id="upload_progress",
max_size=2 * 1024 * 1024,
border="1px dotted var(--indigo-8)",
padding="2em",
),
rx.vstack(rx.foreach(rx.selected_files("upload_progress"), lambda f: rx.text(f, size="2"))),
rx.progress(value=UploadProgressState.progress, max=100),
rx.cond(
~UploadProgressState.uploading,
rx.button(
"Upload",
on_click=UploadProgressState.handle_upload(
rx.upload_files(
upload_id="upload_progress",
on_upload_progress=UploadProgressState.handle_upload_progress,
),
),
),
rx.button("Cancel", on_click=UploadProgressState.cancel_upload, color_scheme="red"),
),
rx.text("Total bytes received: ", UploadProgressState.total_bytes, size="2"),
width="100%",
)await file.read() 会把整个文件读进内存。大文件用 rx.upload_files_chunk(upload_id=...) 一块一块接收,直接写入磁盘。分块处理器必须是 @rx.event(background=True),接收 chunk_iter: rx.UploadChunkIterator 并且必须读到最后(中途返回会导致上传失败)。每个分块都有 chunk.filename、chunk.offset、chunk.content_type、chunk.data。这段代码会写磁盘,所以这里不执行。
class ChunkUploadState(rx.State):
uploaded_files: list[str] = []
@rx.event(background=True)
async def handle_large_upload(self, chunk_iter: rx.UploadChunkIterator):
file_handles = {}
try:
async for chunk in chunk_iter: # must consume every chunk
fh = file_handles.get(chunk.filename)
if fh is None:
path = rx.get_upload_dir() / "stream" / chunk.filename
path.parent.mkdir(parents=True, exist_ok=True)
fh = file_handles[chunk.filename] = path.open("wb")
fh.seek(chunk.offset)
fh.write(chunk.data)
finally:
for fh in file_handles.values():
fh.close()
async with self:
self.uploaded_files = sorted(file_handles)
# rx.button("Upload Large Files",
# on_click=ChunkUploadState.handle_large_upload(rx.upload_files_chunk(upload_id="large_upload")))表单的 on_submit 不包含文件。如果表单里有拖放区,文件要通过 type="button" 的按钮或 on_drop 调用 rx.upload_files() 单独发送,其余输入再用 on_submit 接收。
拖放区先过滤图片类型、数量和大小,on_drop 让文件一放下服务器就收到文件名和大小。列表只保留最近三个。不做保存。
Receipt photos (max 3, 2 MB each)
Drop or click to attach
class UploadReceiptState(rx.State):
files: list[dict[str, str]] = []
@rx.event
async def attach(self, files: list[rx.UploadFile]):
added = [{"name": str(f.name), "size": f"{(f.size or 0) / 1024:.0f} KB"} for f in files]
self.files = (self.files + added)[-3:]
@rx.event
def clear(self):
self.files = []
rx.card(
rx.vstack(
rx.text("Receipt photos (max 3, 2 MB each)", weight="bold", size="2"),
rx.upload(
rx.hstack(rx.icon("image-plus", size=18), rx.text("Drop or click to attach", size="2"), align="center"),
id="upload_receipt",
accept={"image/png": [".png"], "image/jpeg": [".jpg", ".jpeg"]},
max_files=3,
max_size=2 * 1024 * 1024,
on_drop=UploadReceiptState.attach(rx.upload_files(upload_id="upload_receipt")),
border="1px dashed var(--gray-8)",
border_radius="8px",
padding="1em",
width="100%",
drag_active_style={"background_color": "var(--accent-3)"},
),
rx.foreach(
UploadReceiptState.files,
lambda f: rx.hstack(rx.icon("image", size=14), rx.text(f["name"], size="2"), rx.spacer(), rx.code(f["size"]), width="100%", align="center"),
),
rx.button("Remove all", on_click=UploadReceiptState.clear, size="1", variant="soft", color_scheme="gray"),
width="100%",
),
width="100%",
max_width="380px",
)工坊笔记
用 rx.upload(id="固定 id") 创建拖放区。选中的文件用 rx.selected_files(id),清空用 rx.clear_selected_files(id)。
发送靠处理器(rx.upload_files(upload_id=id))。处理器是 async,参数为 files: list[rx.UploadFile]。
用 on_drop 时选中即上传。用 accept · max_files · max_size 过滤,被拒的进入 on_drop_rejected。
保存用 rx.get_upload_dir(),显示用 rx.get_upload_url()。上传文件夹是公开的。
上传按钮放在拖放区外面。大文件用 rx.upload_files_chunk + background 处理器。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。