news 2026/9/12 6:04:11

Refine v5 多文件上传实战:基于 Mantine 的 Multipart/form-data 上传方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine v5 多文件上传实战:基于 Mantine 的 Multipart/form-data 上传方案

Refine v5 多文件上传实战:基于 Mantine 的 Multipart/form-data 上传方案

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

导读

在 Refine v5 中实现文件上传有多种形态,其中multipart/form-data(Multipart Upload)是后端接口最通用的上传协议。本文以仓库中的upload-mantine-multipart示例为主体,讲解如何在 Refine 的useForm表单流程中接入 MantineDropzone,手动构造FormData上传文件、回填表单字段并展示预览,覆盖新建与编辑两个完整场景,最终交付一个可复制的 React 后台管理文件上传方案。

示例概览与运行方式

关联文档 documentation/docs/examples/upload/mantine/multipart.md 指向一个完整的可运行示例upload-mantine-multipart。示例采用的技术栈如下:

  • UI 框架:Mantine(@mantine/core@mantine/dropzone
  • Refine 包@refinedev/core@refinedev/mantine@refinedev/react-table@refinedev/simple-rest
  • 路由@refinedev/react-router+react-router
  • HTTP 客户端axios
  • Markdown 编辑器@uiw/react-md-editor

按示例 README.md 的说明,可以通过 Refine CLI 在本地直接创建该示例工程:

npm create refine-app@latest -- --example upload-mantine-multipart

应用入口 App.tsx 使用simple-rest数据提供器连接https://api.fake-rest.refine.dev,并注册了posts资源的 list / create / edit / show 四个页面路由:

const API_URL = "https://api.fake-rest.refine.dev"; <Refine routerProvider={routerProvider} dataProvider={dataProvider(API_URL)} notificationProvider={useNotificationProvider} resources={[ { name: "posts", list: "/posts", create: "/posts/create", edit: "/posts/edit/:id", show: "/posts/show/:id", }, ]} >

Multipart 上传的核心思路:手动构造 FormData

与 Ant Design 版本(Upload组件自带action直传)不同,Mantine 版本的Dropzone只负责收集文件,不负责网络请求。上传动作需要开发者自行完成,这也让它成为理解multipart/form-data协议本质的最佳示例。

整个流程可以拆解为四步:

  1. 用户把文件拖入Dropzone,触发onDrop回调;
  2. 在回调中把文件对象appendFormData,字段名为file
  3. axios.postFormData发送到上传端点/media/upload,拿到返回的{ url }
  4. 将带url的文件对象写入表单字段(images),提交表单时随记录一并入库。

FormData是浏览器原生 API,axios 在检测到请求体为FormData时,会自动设置Content-Type: multipart/form-data并生成正确的 boundary,无需手动指定。

Create 表单:拖拽上传 + 预览回填

新建页完整实现位于 create.tsx,关键代码如下:

export const PostCreate: React.FC = () => { const [files, setFiles] = useState<FileWithURL[]>([]); const [isUploadLoading, setIsUploadLoading] = useState(false); const { saveButtonProps, getInputProps, setFieldValue, errors } = useForm< IPost, HttpError, FormValues >({ initialValues: { title: "", status: "", category: { id: "" }, content: "", images: [], }, validate: { title: (value) => (value.length < 2 ? "Too short title" : null), status: (value) => (value.length <= 0 ? "Status is required" : null), category: { id: (value) => (value.length <= 0 ? "Category is required" : null), }, content: (value) => (value.length < 10 ? "Too short content" : null), }, }); const apiUrl = useApiUrl(); const handleOnDrop = (files: FileWithPath[]) => { try { setIsUploadLoading(true); files.map(async (file) => { const formData = new FormData(); formData.append("file", file); const res = await axios.post<{ url: string }>( `${apiUrl}/media/upload`, formData, { withCredentials: false, headers: { "Access-Control-Allow-Origin": "*", }, }, ); setFiles( (prev) => [...prev, { url: res.data.url, ...file }] as FileWithURL[], ); }); setIsUploadLoading(false); } catch (error) { setIsUploadLoading(false); } }; useEffect(() => { setFieldValue("images", files); }, [files]); // ... <Dropzone accept={IMAGE_MIME_TYPE} onDrop={handleOnDrop} loading={isUploadLoading}> <Text align="center">Drop images here</Text> </Dropzone>

要点逐条说明:

  • useForm的泛型参数useForm<IPost, HttpError, FormValues>中第三个泛型FormValues是表单的实际值类型,其中images字段被定义为FileWithURL[],即带url的文件对象数组。
  • useApiUrl():来自@refinedev/core,返回当前数据提供器配置的 API 根地址,避免把 URL 硬编码在页面组件里。这里的apiUrlhttps://api.fake-rest.refine.dev
  • FileWithURL类型:示例定义了interface FileWithURL extends FileWithPath { url?: string },在@mantine/dropzoneFileWithPath基础上扩展出上传成功后服务端返回的url字段(见 interfaces/index.d.ts 同目录类型定义)。
  • setFieldValue同步表单Dropzone与 Refine 表单没有直接绑定,示例通过useEffect监听本地files状态,再调用useForm返回的setFieldValue("images", files)把文件列表写入表单值,提交时随记录一起发送。
  • 预览渲染files.map生成<Image src={file.url} />列表,再用SimpleGrid以四列网格展示;编辑页则从values.images读取已有图片,实现已上传图片的回显。
  • IMAGE_MIME_TYPE@mantine/dropzone导出的图片 MIME 类型常量,限定只接受图片文件。

Edit 表单:已有图片回显

编辑页 edit.tsx 与新建页几乎一致,差异主要体现在两处:

const { saveButtonProps, getInputProps, setFieldValue, values, errors, refineCore: { query: queryResult }, } = useForm<IPost, HttpError, FormValues>({ // initialValues 与 validate 同 Create }); // 分类下拉框默认值取自当前记录 const { selectProps } = useSelect<ICategory>({ resource: "categories", defaultValue: queryResult?.data?.data.category.id, pagination: { mode: "server" }, }); // 预览直接读表单值中的已有图片 const previews = values.images?.map((file, index) => { return <Image key={index} src={file.url} />; });
  • 通过refineCore.query拿到当前编辑记录的数据,用于初始化分类等关联字段;
  • 图片预览改为读取values.images(表单当前值),因此编辑页进入时即可回显记录中已保存的图片;
  • 新拖入文件后,handleOnDropsetFieldValue的逻辑与新建页完全复用,新旧文件会合并进images字段。

编辑提交时,Refine 会以PATCH/PUT请求把包含images(含url)的完整表单值发送到/posts/:id

上传端点的接口契约

无论前端如何实现,multipart 上传的成功都依赖后端端点遵守统一契约。参考 multipart-upload.md 中给出的规范:

请求方向:

[POST] https://api.fake-rest.refine.dev/media/upload { "file": "binary" }

该端点必须是Content-Type: multipart/form-data,且表单字段名为file,值为文件二进制数据。

响应方向(示例中axios.post<{ url: string }>的泛型即对应此结构):

{ "url": "https://example.com/uploaded-file.jpeg" }

前端拿到url后与文件对象合并存储。最终随posts记录提交到 API 的image/images字段形态大致如下(以 Ant Design 版本教程文档展示的数据为例,Mantine 示例的images数组语义相同,均以url为核心字段):

{ "title": "Test", "images": [ { "name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg", "url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2", "type": "image/jpeg", "size": 70922 } ] }

与 Ant Design 版本实现方式的对比

同一主题在 Refine 仓库中还有 Ant Design 实现(multipart-upload.md 教程主体基于@refinedev/antd,示例为upload-antd-multipart),两者对比能帮助理解不同 UI 库下的接入差异:

环节Mantine 版本(本文)Ant Design 版本
文件选择组件@mantine/dropzoneDropzoneantdUpload.Dragger
上传动作手动axios.post+FormData组件action属性直传端点
表单接入onDrop回调 +setFieldValuegetValueFromEvent转换事件为UploadFile数组
上传中状态本地isUploadLoading控制DropzoneloadinguseFileUploadState()返回isLoading/onChange,可禁用保存按钮

其中 Ant Design 版本的useFileUploadState是 Refine 为上传场景提供的便捷 Hook,通过saveButtonProps.disabled = isLoading在文件上传过程中禁用"保存"按钮,避免表单在图片尚未传完时被提交。Mantine 版本由于上传逻辑完全自控,等价地使用本地isUploadLoading状态即可达到相同效果。

从源码看数据流:文件如何进入 Refine 表单

综合 create.tsx 与 edit.tsx 的实现,可以梳理出一条完整的数据链路:

Dropzone.onDrop(files) └─> 每个 file append 进 FormData("file", file) └─> axios.post(`${apiUrl}/media/upload`, formData) └─> 响应 { url } 与 file 合并为 FileWithURL └─> setFiles(prev => [...prev, {url, ...file}]) └─> useEffect 触发 setFieldValue("images", files) └─> 表单 values.images 更新 └─> 提交时随记录发送 /posts └─> 编辑页回显 values.images -> <Image src={url} />

这一链路体现了 RefineuseForm的一个核心特性:表单值与 UI 组件之间通过getInputProps/setFieldValue双向桥接,第三方组件(如DropzoneMDEditor)无需依赖 Refine 内部实现,只要把数据写入表单值即可无缝参与saveButtonProps触发的提交流程。上传过程与表单提交解耦,也是保证"先传文件、后存记录"顺序正确性的关键。

延伸阅读

  • 完整理论教程:multipart-upload.md,含 Ant Design 版本的逐步讲解与useFileUploadState用法
  • Mantine 示例工程:examples/upload-mantine-multipart,包含新建、编辑、列表页完整代码
  • 同主题 Base64 上传对比:documentation/docs/examples/upload/mantine/base64.md

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 6:03:36

Radarr 电影海报管理:4 个最常踩的坑,项目里都给了默认解法

Radarr 电影海报管理&#xff1a;4 个最常踩的坑&#xff0c;项目里都给了默认解法 【免费下载链接】Radarr Movie organizer/manager for usenet and torrent users. 项目地址: https://gitcode.com/GitHub_Trending/ra/Radarr Radarr 是一款面向 Usenet 和 BitTorrent…

作者头像 李华
网站建设 2026/9/12 5:59:18

Windows Server与SQL Server备份策略与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 5:59:15

AI即推GEO:构建动态意图-空间-行为耦合的智能推荐系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 5:58:54

EMD+FFT+HHT组合实战:用MATLAB实现非平稳信号时频分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 5:57:42

PHP入门指南:从零基础到Web开发实战

1. 为什么选择PHP作为入门编程语言&#xff1f;PHP作为一门已有27年历史的服务器端脚本语言&#xff0c;至今仍占据着Web开发领域的重要地位。根据W3Techs的最新统计&#xff0c;全球78.9%的网站使用PHP作为服务器端编程语言&#xff0c;其中就包括WordPress、Facebook&#xf…

作者头像 李华