antd Image 预览自定义工具栏实战:用 actionsRender 实现图片下载、翻转、旋转与缩放控制
导读
antd 的 <Image> 组件在点击缩略图后会弹出全屏预览层(Preview),内置翻页、缩放、旋转、翻转与关闭等操作按钮。实际业务里我们往往不满足于默认工具栏——例如需要"下载当前预览的原图""下载翻转/旋转后的成图",或者按场景重新排布图标。本指南以仓库中 toolbarRender.md 这个官方 demo 为核心主体,逐步拆解其背后的 actionsRender 定制方案:读完你将掌握 actionsRender 的完整签名与回调 info 结构、内置行为 API(actions)的调用方式、受控下载图片的完整链路,以及旧版 toolbarRender 与新 API 之间的兼容关系,并能基于仓库源码定位每一条结论的真实出处。
一、demo 在讲什么:自定义工具栏 + 图片下载
该 demo 的定位描述很精炼:
可以自定义工具栏并添加下载原图或翻转旋转后图片的按钮。 You can customize the toolbar and add a button for downloading the original image or downloading the flipped and rotated image.
也就是说,它属于 antd Image 组件"图片预览定制"系列,在 Image API 文档 中对应标题 Custom toolbar render(自定义工具栏渲染),中文版索引页 index.zh-CN.md 则将其命名为"自定义工具栏"。示例的完整可运行实现位于同目录的 toolbarRender.tsx,两者组合构成了本指南全部技术信息的来源。
一句话概括 demo 的能力边界:
- 使用
<Image.PreviewGroup>组合多张图片,实现缩略图网格 + 统一预览; - 通过
preview={{ actionsRender: ... }}完全替换预览层底部的默认工具栏; - 工具栏内自绘上一张/下一张、下载、水平/垂直翻转、左右旋转、放大/缩小、重置等按钮;
- 预览切换时同步记录当前图片索引,供"下载当前图片"功能使用。
二、整体结构:PreviewGroup + preview.actionsRender
先看 demo 的核心 JSX 结构(省略样式细节后的骨架):
import { Image, Space } from 'antd';
import { DownloadOutlined, LeftOutlined, RightOutlined, SwapOutlined } from '@ant-design/icons';
// ...
const App: React.FC = () => {
const [current, setCurrent] = React.useState(0);
return (
<Image.PreviewGroup
preview={{
actionsRender: (_, { transform, actions }) => (
<Space size={12}>
{/* 自定义按钮集合 */}
</Space>
),
onChange: (index) => {
setCurrent(index);
},
}}
>
{imageList.map((item, index) => (
<Image alt={`image-${index}`} key={item} src={item} width={200} />
))}
</Image.PreviewGroup>
);
};
这里有两层组件值得说明:
<Image.PreviewGroup>:预览组容器。仓库中对应 PreviewGroup.tsx,它内部仍然把配置交给@rc-component/image的RcImage.PreviewGroup渲染,同时负责 antd 侧的样式前缀、classNames/styles语义结构与预览配置合并。items支持传入string[]或对象数组,也可以像 demo 一样用多个<Image>子节点声明(源码中Image.PreviewGroup = PreviewGroup的挂载见 index.tsx 第 319 行)。preview配置对象:同一份配置被 usePreviewConfig.ts 解析归一化后再传给底层组件,actionsRender就是其中最关键的自定义渲染入口。
需要留意,actionsRender 在 Image 单组件与 Image.PreviewGroup 两种场景下都可使用。若单图使用则写在 Image 的 preview 属性里,类型对应 PreviewType;若组图预览则写在 PreviewGroup 的 preview 属性里,类型对应 PreviewGroupType(两种类型下 actionsRender 均定义,见 index.en-US.md API 表格)。区别在于组图模式下 info 会额外携带 current 与 total,可用于感知当前预览第几张。
三、actionsRender 签名与 info 对象全解析
3.1 函数签名
官方类型定义(见 index.en-US.md 的 Interface 章节):
type ActionsRender = (
originalNode: React.ReactElement,
info: ToolbarRenderInfoType,
) => React.ReactNode;
originalNode:默认工具栏的原始 React 元素。返回它则沿用默认;demo 完全不使用它,直接返回自己拼装的<Space>,实现"整体替换"。info:ToolbarRenderInfoType,把当前预览状态与全部内置操作暴露给我们,是自定义工具栏真正的"控制中心"。
3.2 info:ToolbarRenderInfoType 字段说明
接口文档给出的完整结构为:
{
icons: {
flipYIcon: React.ReactNode; // 垂直翻转图标
flipXIcon: React.ReactNode; // 水平翻转图标
rotateLeftIcon: React.ReactNode; // 左旋图标
rotateRightIcon: React.ReactNode;// 右旋图标
zoomOutIcon: React.ReactNode; // 缩小图标
zoomInIcon: React.ReactNode; // 放大图标
};
actions: {
onActive?: (index: number) => void; // 切换至指定索引的图片(接口文档标注 5.21.0 起支持)
onFlipY: () => void; // 垂直翻转
onFlipX: () => void; // 水平翻转
onRotateLeft: () => void; // 向左旋转 90°
onRotateRight: () => void; // 向右旋转 90°
onZoomOut: () => void; // 缩小
onZoomIn: () => void; // 放大
onReset: () => void; // 重置变换(接口文档标注 5.17.3 起支持)
onClose: () => void; // 关闭预览
};
transform: TransformType; // 当前变换状态
current: number; // 当前图片索引(0 起)
total: number; // 图片总数
image: ImgInfo; // 当前图片信息
}
3.3 transform:当前变换状态
TransformType 结构(接口文档定义)为:
{
x: number; // 水平位移
y: number; // 垂直位移
rotate: number; // 旋转角度
scale: number; // 缩放倍数
flipX: boolean; // 是否水平翻转
flipY: boolean; // 是否垂直翻转
}
它是只读状态,供 UI 做条件渲染判断(比如 demo 里用 scale 判断放大/缩小按钮是否禁用)。真正改变它的动作由 actions 里的回调触发。另外,缩放边界与步进在 API 中有对应可配置项:
minScale(默认 1)与maxScale(默认 50)控制缩放上下限;scaleStep(默认 0.5)表示每次缩放的倍率步进为1 + scaleStep。
这些默认值可从 index.en-US.md 的 PreviewType/PreviewGroupType 表格查得,正好解释了 3.4 中 demo 的禁用逻辑为何使用 scale === 1 与 scale === 50 两个边界判断。
3.4 image:图片元信息
{
url: string; // 预览图地址
alt: string; // 替代文本
width: string | number; // 宽
height: string | number; // 高
}
ImgInfo 的作用在 imageRender/actionsRender 中非常实用(例如在渲染函数中展示当前图元信息,参见同目录 demo preview-imgInfo.tsx)。
四、逐按钮拆解 demo:翻页、翻转、旋转、缩放与重置
demo 把整套内置操作重绘为一行胶囊状的图标按钮组,每个图标背后都调用了 info.actions 对应方法:
| demo 中的按钮 | 图标 | 调用的 actions 方法 | 备注 |
|---|---|---|---|
| 上一张 | LeftOutlined |
onActive?.(-1) |
首图时 disabled={current === 0} |
| 下一张 | RightOutlined |
onActive?.(1) |
末图时 disabled={current === imageList.length - 1} |
| 下载 | DownloadOutlined |
自定义 onDownload |
4.2 单独讲解 |
| 垂直翻转 | SwapOutlined rotate={90} |
onFlipY |
|
| 水平翻转 | SwapOutlined |
onFlipX |
|
| 向左旋转 | RotateLeftOutlined |
onRotateLeft |
|
| 向右旋转 | RotateRightOutlined |
onRotateRight |
|
| 缩小 | ZoomOutOutlined |
onZoomOut |
scale === 1 时禁用 |
| 放大 | ZoomInOutlined |
onZoomIn |
scale === 50 时禁用 |
| 重置 | UndoOutlined |
onReset |
一键还原缩放/旋转/翻转/位移 |
注意 demo 中 actionsRender 的实参解构写法:
actionsRender: (
_,
{
transform: { scale },
actions: { onActive, onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset },
},
) => ...
它同时用到了 info.transform(读取只读状态)与 info.actions(触发用户交互),这正是 actionsRender 与"纯静态渲染"的根本差异——渲染函数被接入到了预览层的状态机中。
值得对比的是:antd 预览层默认工具栏本身也维护着同样一套图标。在 PreviewGroup.tsx 中可见默认图标映射 icons 常量:
export const icons = {
rotateLeft: <RotateLeftOutlined />,
rotateRight: <RotateRightOutlined />,
zoomIn: <ZoomInOutlined />,
zoomOut: <ZoomOutOutlined />,
close: <CloseOutlined />,
left: <LeftOutlined />,
right: <RightOutlined />,
flipX: <SwapOutlined />,
flipY: <SwapOutlined rotate={90} />,
};
自定义 actionsRender 时若只想"微调",可以把 originalNode 与 info.icons 组合使用;若想完全重排(demo 的方案),则从 info.actions 取行为、自己选图标即可。RTL 场景下 PreviewGroup 还会根据 direction 自动交换左右箭头图标(源码第 90–97 行的 memoizedIcons),这是内部实现细节,自定义时需自行考虑 RTL 语义。
五、核心场景:把"当前图片"下载到本地
5.1 追踪当前索引
由于下载目标随预览翻页而变化,demo 用两块协作完成:
- 外层维护
const [current, setCurrent] = React.useState(0); - 在
preview配置里注册onChange: (index) => setCurrent(index);
PreviewGroupType.onChange 签名是 (current: number, prevCurrent: number) => void(见 index.en-US.md),即每次切换图片都会把新索引回传。demo 同时把两张测试图存进数组:
const imageList = [
'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg',
];
(以上为 demo 自带的远程演示素材地址,运行示例时可直接复用;部署到生产请替换为自己的图片域名。)
5.2 下载函数完整链路
demo 中的 onDownload 实现了一套浏览器端标准的"远端资源转本地下载"流程:
const onDownload = () => {
const url = imageList[current];
const suffix = url.slice(url.lastIndexOf('.')); // 提取扩展名
const filename = Date.now() + suffix; // 生成文件名
fetch(url) // ① 拉取二进制
.then((response) => response.blob()) // ② 转 Blob
.then((blob) => {
const blobUrl = URL.createObjectURL(new Blob([blob])); // ③ 生成对象 URL
const link = document.createElement('a');
link.href = blobUrl;
link.download = filename; // ④ 指定下载文件名
document.body.appendChild(link);
link.click(); // ⑤ 触发下载
URL.revokeObjectURL(blobUrl); // ⑥ 释放对象 URL
link.remove(); // ⑦ 清理临时节点
});
};
整个链路可归纳为:fetch 拉取图片 → response.blob() 转为二进制对象 → URL.createObjectURL 生成本地临时地址 → 动态创建带 download 属性的 <a> 并模拟点击 → 下载完成后 revokeObjectURL 回收资源并移除节点。
几点工程化提示:
- 文件名时间戳化:
Date.now() + suffix保证多次下载互不覆盖;demo 使用当前原图 URL 的扩展名作为后缀,SVG 场景亦可直接使用。 - CORS 前提:
fetch跨域读取图片字节要求目标资源允许跨域(响应头携带 CORS 许可),同源资源则无此限制。若直接a.href = url方式受跨域限制时可参考此 Blob 中转方案。 - "下载翻转/旋转后图片"的实现边界:demo 源码中
onDownload下载的是当前索引对应的原始图片,而预览层内的翻转/旋转仅作用于渲染状态。若要真正导出"变换后的成图",需要把info.transform中的rotate/flipX/flipY/scale应用到<canvas>(绘制后toBlob)或 SVG 后再走相同的下载链路——仓库示例本体并未内置该导出实现(源码注释中仅在说明此思路),因此描述为"可自定义添加下载原图或翻转旋转后图片的按钮"这一目标方向,落地时需自行实现变换合成逻辑。
5.3 受控开关下载按钮
自定义按钮属于完全受控的 UI,因此 demo 中下载按钮不带禁用态;而翻页/缩放按钮利用可用信息做了禁用判断,形成完整的可用性闭环:首图禁用"上一张"、末图禁用"下一张"、scale 触底禁用缩小、scale 触顶禁用放大。这套写法可直接移植到任意需要状态感知的工具栏实现中。
六、从 toolbarRender 到 actionsRender:废弃属性与兼容逻辑
早期版本的 API 名是 toolbarRender,如今已被 actionsRender 取代。仓库代码给出了完整的"废弃 + 兼容"证据链:
6.1 类型层面标记废弃
在 index.tsx 的 DeprecatedPreviewConfig 中:
/** @deprecated Use `actionsRender` instead */
toolbarRender?: OriginPreviewConfig['actionsRender'];
并在 PreviewConfig 注释中同样标明 @deprecated。API 文档中 PreviewType 与 PreviewGroupType 两处也以删除线形式列出 toolbarRender 并注明 "please use 'actionsRender' instead"(见 index.en-US.md 第 94、131 行)。
6.2 运行时做"新老兼容"映射
usePreviewConfig.ts 是所有 preview 配置的归一化入口,它从原始配置中把两个属性同时解构出来:
const { ..., actionsRender, ..., toolbarRender, ... } = rawPreviewConfig;
// 返回时:
actionsRender: actionsRender ?? toolbarRender,
即:只要传了新的 actionsRender 就优先使用,没传则回退到旧的 toolbarRender,二者在运行时是同一渲染函数的两个名字。开发环境下它还通过 devUseWarning 触发迁移提示:
['toolbarRender', 'actionsRender'].forEach(([deprecatedName, newName]) => {
warning.deprecated(!(deprecatedName in rawPreviewConfig), deprecatedName, newName);
});
6.3 测试侧的证据
对应的行为在测试中同样被固化:tests/deprecated.test.tsx 中有专门用例 it('toolbarRender', ...),断言渲染 <Image preview={{ toolbarRender: () => <div /> }} /> 时控制台输出警告:
Warning: [antd: Image] 'toolbarRender' is deprecated. Please use 'actionsRender' instead.
因此新项目请一律使用 actionsRender;旧代码可以继续工作,但应把 toolbarRender 平滑迁移为 actionsRender,二者参数签名(originalNode、info)保持一致。值得一提的是,迁移说明中还有一条配套提示:旧 toolbarRender 的 info 不包含 current/total(单图上下文),组图场景统一使用 actionsRender 即可获得完整信息。
七、工具栏外观定制:样式封装的正确姿势
demo 还演示了如何用 antd 官方样式方案(antd-style)给工具栏做外观定制:
import { createStyles } from 'antd-style';
const useStyles = createStyles((props) => {
const { css, iconPrefixCls, cssVar } = props;
return {
wrapper: css`
padding: 0 ${cssVar.paddingLG};
color: ${cssVar.colorWhite};
font-size: ${cssVar.fontSizeXL};
background-color: rgba(0, 0, 0, 0.1);
border-radius: 100px;
.${iconPrefixCls} {
padding: ${cssVar.paddingSM};
cursor: pointer;
&:hover { opacity: 0.3; }
&[disabled] {
opacity: 0.3;
cursor: not-allowed;
}
}
`,
};
});
要点拆解:
cssVar暴露了 antd 的设计令牌(如paddingLG、paddingSM、colorWhite、fontSizeXL),使自定义样式与全局主题保持一致的间距与色彩体系;- 通过
iconPrefixCls精准命中 antd 图标类名,为图标统一设置内边距与悬停效果; [disabled]属性选择器同时兜底了"图标自带的 disabled 态"与 demo 里通过条件渲染赋值的禁用按钮,保证禁用时指针样式为not-allowed且透明度降低;- 外层使用
Space size={12}+ 胶囊底色(border-radius: 100px),得到悬浮在预览底部的工具栏视觉。
需要说明:demo 对图标的禁用主要依赖图标组件自身支持 disabled 属性(demo 中 onZoomIn 图标点击放大的判断同时配合 onZoomOut 的边界条件实现),样式层仅负责把禁用态渲染得更清晰。若要完全控制预览容器层级,还可以结合 classNames/styles 语义(popup.actions 等)进一步深入定制,相关语义结构可查 index.en-US.md 的 Semantic DOM 章节。
八、可验证性与延伸阅读
本 demo 有自动化测试守护:快照测试会渲染 toolbarRender.tsx 并比对输出,相关快照记录在 snapshots/demo.test.ts.snap(用例名 renders components/image/demo/toolbarRender.tsx correctly)与 snapshots/demo-extend.test.ts.snap(renders ... extend context correctly)。这意味着官方会持续保证该示例可在当前版本正常渲染,示例代码本身即是"版本内事实"的最直接依据。
想继续深入,可以按需查阅以下仓库文件:
- 组件 API 全表(含
PreviewType/PreviewGroupType/ToolbarRenderInfoType/TransformType):index.en-US.md、index.zh-CN.md; - 预览配置归一化与废弃告警逻辑:usePreviewConfig.ts;
- PreviewGroup 与默认图标映射:PreviewGroup.tsx;
- 组件主实现与类型定义:index.tsx;
- 同系列定制示例:自定义预览内容 imageRender.tsx、渲染函数中获取图片信息 preview-imgInfo.tsx。
小结
围绕 toolbarRender.md 描述的能力,本文把 demo 拆成了四条可复用主线:用 actionsRender 接管工具栏渲染、用 info.actions 触发预览内置行为、用 info.transform 做状态感知(缩放边界禁用等)、用 fetch+Blob+<a download> 完成当前图片下载。同时澄清了 toolbarRender → actionsRender 的迁移路径与兼容实现(usePreviewConfig.ts 中 actionsRender ?? toolbarRender 的合并逻辑及开发告警),避免读者在使用中踩到"新旧 API 混用"的坑。若你的下一个需求是"相册预览 + 导出当前变换成图",本文的 info 结构解析与下载链路即可作为起点,结合 Canvas 合成即可扩展出完整能力。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00