Refine v5 中的 Base64 图片上传实战:基于 Mantine useForm 与 Dropzone 的完整实现
2026/9/12 6:04:42 网站建设 项目流程

Refine v5 中的 Base64 图片上传实战:基于 Mantine useForm 与 Dropzone 的完整实现

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

本文以 Refine 官方示例 examples/upload-mantine-base64 为蓝本,讲解如何在 Refine v5 + Mantine 技术栈下,通过@refinedev/mantineuseForm配合@mantine/dropzoneDropzone组件,实现"选择图片 → 转换为 Base64 → 存入表单字段 → 随表单一并提交"的完整上传流程。读完本文,你将掌握 Base64 上传的核心原理(FileReader.readAsDataURL)、创建/编辑页面的落地写法、图片预览的实现方式,以及useForm在 Refine 内部的封装机制,可直接复用到自己的管理后台项目中。

示例概览与技术栈

该示例是一个完整的"文章(posts)"CRUD 管理页面,其中创建(Create)与编辑(Edit)页面都集成了图片上传能力。示例的完整运行说明见 examples/upload-mantine-base64/README.md,可通过如下命令在本地启动:

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

从 package.json 可以看到示例的核心依赖:

依赖包版本职责
@refinedev/core^5.0.12Refine 核心(hooks、数据提供者、路由等)
@refinedev/mantine^3.0.2Mantine 集成层:useFormCreate/Edit页面组件、RefineThemes
@mantine/dropzone^5.4.1拖拽/点击选择文件的 Dropzone 组件
@mantine/form^5.10.4Mantine 原生表单库,被 RefineuseForm包装
@mantine/core^5.10.4Mantine 基础组件库
@refinedev/simple-rest^6.0.1REST 数据提供者
@refinedev/react-router^2.0.4路由提供者

在 App.tsx 中,应用以MantineProvider(使用RefineThemes.Blue主题)包裹<Refine>,配置了simple-rest数据提供者(API 地址为https://api.fake-rest.refine.dev),并注册了posts资源的listcreateeditshow四个页面路由。

Base64 上传的核心思路

与传统的multipart/form-data文件上传不同,Base64 上传将文件内容编码为 Data URL 字符串(形如data:image/png;base64,iVBORw0KGgo...),并作为普通字符串字段存放在表单数据中。当用户点击保存时,这个字符串会随其他字段一起通过数据提供者发送到后端。后端解析字符串后即可还原为图片二进制内容。

这种方案的优点是实现简单、不依赖任何文件上传专用组件或服务端中间件,适合图片体积小、数量少、后端仅需单次请求即可完成 CRUD 提交的场景。需要留意的是,Base64 编码会使数据体积膨胀约 33%,且图片以字符串形式保存在内存与请求体中,因此不适合大图或批量上传(此类场景更适合本仓库中的 multipart 方案,见 multipart.md)。

通用工具函数:convertBase64

示例将文件转 Base64 的逻辑抽离为可复用的工具函数,见 src/utils/convertBase64.ts:

export const convertBase64 = (file: File): Promise<string> => { return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.readAsDataURL(file); fileReader.onload = () => { if (typeof fileReader.result === "string") { resolve(fileReader.result); } }; fileReader.onerror = (error) => { reject(error); }; }); };

该函数基于浏览器内置的FileReaderAPI:

  • fileReader.readAsDataURL(file)以异步方式将文件读取为 Data URL;
  • onload回调中,fileReader.result即为data:...;base64,...字符串,通过resolve返回;
  • onerror回调将读取失败的错误透传给调用方,便于上层做异常处理;
  • 返回类型为Promise<string>,配合async/await使用非常顺手。

创建页面:Dropzone + setFieldValue 组合

创建页面的完整实现位于 src/pages/posts/create.tsx,其核心是useFormDropzone的配合。

1. 定义表单结构与校验规则

useForm接受泛型FormValues,明确表单字段类型,其中imagesstring[](存放 Base64 字符串数组):

interface FormValues { title: string; status: string; category: { id: string }; content: string; images: string[]; } const { saveButtonProps, getInputProps, setFieldValue, values, 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), }, });

这里可以看到 RefineuseForm的三个关键返回值:

  • saveButtonProps:直接透传给<Create>页面的保存按钮,自动接管提交逻辑(含 loading 状态与提交处理);
  • getInputProps("fieldName"):将字段值与变更处理绑定到任意输入组件(如TextInputSelectMDEditor),配合 Mantine 的受控表单体系;
  • setFieldValue/values:用于程序化读写字段值——这正是 Base64 上传与表单联动的关键。

validate对象采用 Mantine 表单风格的字段级校验函数,客户端校验不通过时errors会携带对应错误信息,可在界面上展示(示例中对content字段做了红色错误提示渲染)。

2. Dropzone 拖拽上传与 Base64 转换

表单中嵌入@mantine/dropzoneDropzone组件,通过accept={IMAGE_MIME_TYPE}限定仅接受图片类型文件:

<Dropzone accept={IMAGE_MIME_TYPE} onDrop={handleOnDrop} loading={isUploadLoading} > <Text align="center">Drop images here</Text> </Dropzone>

handleOnDrop是上传逻辑的核心(create.tsx 第 57-75 行):

const handleOnDrop = (files: FileWithPath[]) => { try { setIsUploadLoading(true); files.map(async (file) => { const base64 = await convertBase64(file); if (values.images) { setFieldValue("images", [...values.images, base64]); } else { setFieldValue("images", [base64]); } }); setIsUploadLoading(false); } catch (error) { setIsUploadLoading(false); } };

这段代码值得逐行解读:

  1. DropzoneonDrop回调接收FileWithPath[](继承自浏览器File);
  2. 先通过setIsUploadLoading(true)打开 Dropzone 的loading动画,避免用户重复操作;
  3. 对每个文件调用convertBase64(file)得到 Base64 字符串;
  4. 通过setFieldValue("images", [...values.images, base64])将新 Base64追加images数组——注意这里使用展开语法保留了已上传的图片,支持一次拖入多张图片;
  5. 转换或追加过程出错时进入catch,关闭 loading 并保持表单原状。

3. 实时图片预览

由于images字段本身就存的是 Data URL,预览无需任何额外请求:

const previews = values.images?.map((base64, index) => { return <Image key={index} src={base64} />; }); <SimpleGrid cols={4} breakpoints={[{ maxWidth: "sm", cols: 2 }]} mt={previews?.length > 0 ? "xl" : 0} > {previews} </SimpleGrid>

values.images一旦变化,React 即重新渲染,<Image src={base64}>直接以 Data URL 作为图片来源渲染缩略图,并用SimpleGrid做 4 列(小屏 2 列)的网格布局。整个流程完全在客户端完成,所见即所得。

4. 保存提交

页面根节点使用@refinedev/mantine<Create saveButtonProps={saveButtonProps}>包装表单,点击保存时,images数组会作为普通字段随POST /posts请求发送到 API。对后端而言,它只是收到一个包含 Base64 字符串数组的 JSON 对象,无需处理文件流。

编辑页面:数据回填与增量追加

编辑页面 src/pages/posts/edit.tsx 与创建页面几乎一致,差异点在于已有数据的回填

const { saveButtonProps, getInputProps, setFieldValue, values, errors, refineCore: { query: queryResult }, } = useForm<IPost, HttpError, FormValues>({ initialValues: { /* 与创建页一致 */ }, validate: { /* 与创建页一致 */ }, });

refineCore.query暴露了 Refine 内部通过数据提供者拉取当前记录的结果。useForm在编辑模式下会自动将查询到的记录填充进表单(包括images字段),从而保证:

  • 进入编辑页时,已上传图片的 Base64 会作为初始预览显示;
  • useSelect通过defaultValue: queryResult?.data?.data.category.id正确选中当前分类;
  • 用户追加新图片时,handleOnDrop中的展开语法会保留原有图片,实现"旧图 + 新图"共存。

深入原理:Refine 的 Mantine useForm 是如何工作的

要真正理解示例,需要知道@refinedev/mantineuseForm并不只是简单封装。查看源码 packages/mantine/src/hooks/form/useForm/index.ts,可以看到它的实现层次:

useForm (@refinedev/mantine) ├── useMantineForm (@mantine/form) → 表单状态、校验、getInputProps/setFieldValue └── useFormCore (@refinedev/core) → 数据获取与提交(getOne/create/update)

几个关键实现细节:

1. 双重参数通道。顶层useForm接收refineCoreProps与其余参数(源码第 93-113 行):前者透传给useFormCore(负责数据请求、提交),后者透传给useMantineForm(负责表单状态),因此一套 hooks 同时管理了"表单 UI 状态"与"服务端数据状态"。

2. 表单状态委托给 Mantine。源码第 127-141 行 将除refineCoreProps外的配置(initialValuesvalidate等)原样交给@mantine/formuseForm,并从其结果中解构出setValuesonSubmitisDirtyresetDirtysetFieldErrorvalues——示例中使用的setFieldValuevalues正是来自这一层。

3. 服务端错误自动映射到字段。源码第 152-164 行 在onMutationError中读取error.errors,逐字段调用setFieldError将后端返回的字段级错误映射到对应表单字段上(可通过disableServerSideValidation关闭)。这意味着 Base64 图片字段若被后端校验拒绝,同样能以表单错误的形式反馈给用户。

4. 返回值的组装。源码第 253-259 行 最终返回{ ...useMantineFormResult, onSubmit, refineCore: useFormCoreResult }——这就是为什么示例中既能直接拿到getInputPropssetFieldValueerrors,又能通过refineCore.query访问数据查询结果。

类型定义与资源结构

示例的类型定义见 src/interfaces/index.d.ts:

export interface ICategory { id: number; title: string; } export interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; category: { id: number }; }

注意IPost中并未声明images字段——表单层通过FormValues类型承载images: string[],体现了"数据模型与表单模型分离"的写法:数据库记录关心业务字段,表单可以携带额外的提交数据(图片、确认密码、临时状态等)。列表页 list.tsx 与详情页 show.tsx 仍按IPost渲染标准字段,不受影响。

实战注意事项

  • 体积膨胀与内存占用:Base64 会使数据体积增加约 33%,且整个字符串常驻表单状态中,多张大图可能导致页面卡顿与请求体过大,建议限制文件数量与大小;
  • IMAGE_MIME_TYPE约束:Dropzone 的accept属性只做客户端过滤,服务端仍需校验 MIME 与解码合法性;
  • key稳定性:预览使用index作为key,若需支持删除中间图片,建议改为稳定唯一标识;
  • 后端解码:后端需能解析 Data URL(截取base64,之后的部分再atob/Buffer.from解码),或直接以字符串形式存入数据库,具体取决于业务设计;
  • 增量追加语义handleOnDrop中的[...values.images, base64]依赖闭包中的最新values,多文件拖拽时逐次追加,逻辑上安全;若追求更严格的并发一致性,可改用setValues基于函数式更新。

总结

Base64 上传是 Refine 管理后台中最轻量的文件上传方案之一:前端仅需FileReader.readAsDataURL完成编码、setFieldValue写入表单、<Image>完成预览,后端无需任何上传中间件即可随 CRUD 请求一并处理。结合 examples/upload-mantine-base64 示例与 useForm 源码 可以看出,Refine 的useForm通过"Mantine 表单层 + Refine 数据层"的双层架构,让开发者可以用最小的胶水代码把任意组件(包括文件选择组件)接入完整的 CRUD 数据流。对于大文件与批量上传场景,则建议参考同目录下的 multipart 方案 与仓库中的 upload-mantine-multipart 示例,按业务需求权衡选择。

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询