拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Echarts图表导出图片:getDataURL、toolbox与服务端方案

Echarts图表导出图片:getDataURL、toolbox与服务端方案

“图表画得再漂亮,导不出来就是白干。”这句话是我做数据可视化这几年体会最深的一条。Echarts 图表导出为图片这件事,看着像个小功能,真落到项目里——数据可视化大屏要支持一键存图、周报要把折线图塞进文档、运营要把饼图搬进汇报幻灯片、定时报表要批量生图归档——每一个场景都会让“导出”两个字从可有可无变成必须交付的需求。我手上跑过几个大屏项目,验收阶段被追问最多的三个问题永远是:导出按钮在哪、为什么导出后糊成一片、为什么地图那一块是空白。

这篇就把 Echarts 图表导出为图片的三种方式完整拆一遍:浏览器端用getDataURL直接取图、实例内置的toolbox.saveAsImage保存图片、以及把渲染搬到服务端做截图。三种方式各自解决什么问题、关键参数怎么配、清晰度怎么算、哪些坑必须提前躲开,都会写清楚。不管你是刚接需求的前端新手,还是被批量报表折腾过的老手,看完应该都能直接照着抄。

1. 三种导出方案的整体选型与思路拆解

1.1 先弄明白业务要的到底是什么

很多同学一接到“导出图片”需求就去找代码片段,结果写完了被产品打回来重做。问题不在代码,在于需求本身没有被拆开看。同样是导出,下面这四种诉求对应的技术方案完全不同。

第一种是单图交互导出。用户在页面上看着某个图表,点一下按钮,把当前这张图存到本地。这是最常见的一种,特点是数据已经在浏览器里渲染完了,直接取像素就行。

第二种是图文混排导出。页面上有标题、说明文字、筛选条件、三张图,要合成一整张长图。这时候单张图表取图只是原料,后面还得用 canvas 做二次拼装。

第三种是批量无感导出。后端定时任务跑一遍,把前一天所有指标图生成好,塞进邮件或者归档到对象存储。这种场景里根本没有浏览器窗口给你用。

第四种是分享图导出。移动端分享出去的卡片图,尺寸固定、带品牌底图、要求清晰。这种对像素比和背景色的要求最严。

把这四类想清楚,方案基本就定了。第一、第二类在浏览器端解决最省事,第三类必须走服务端,第四类是前两类的特化版本。

1.2 三种方式的边界与对照

先上一张我平时用来快速判断的对照表,省得每次都要重新推一遍。

对比维度getDataURLtoolbox.saveAsImage服务端渲染导出
执行位置浏览器内存浏览器内存服务器进程
接入成本低,十几行代码最低,一个配置项高,需要搭环境
用户操作自己挂按钮触发图表右上角自带无需用户参与
清晰度控制pixelRatio自由设定pixelRatio可配置视口缩放或矢量转位图
能否批量不擅长不能天然擅长
依赖用户在线必须在线必须在线不依赖
典型场景单图导出、二次合成内部工具、快速交付定时报表、归档、分享图

这张表里我最想强调的是最后一行。很多人一上来就想搞个大而全的统一导出服务,结果开发量翻了三倍,实际上线后发现 80% 的导出请求都是用户在页面上手动点的,用getDataURL二十行就能搞定。

1.3 我的优先选择顺序

按我自己的习惯,遇到导出需求先按这个顺序判断:

  1. 如果用户就在页面上、只要能存一张图,直接用getDataURL,可控性最好。
  2. 如果这是个内部使用的图表工具、没人愿意为导出再写按钮,打开toolbox的saveAsImage就够了。
  3. 只有当出现“定时”“批量”“用户不在页面”这三个关键词之一时,才考虑把渲染搬到服务端。

这个顺序的好处是把复杂度后置。我先用最低成本把需求接住,等真的出现了服务端批量导出的诉求,再把渲染逻辑抽出来独立成服务,前端的代码完全不用动。

还有一个容易被忽略的点:如果项目里有多个页面都要导出,别每处各写一遍getDataURL。抽一个exportChart(chartInstance, options)的工具方法,把背景色、像素比、文件命名规则统一收在里面,后续调整清晰度时只改一个地方。

2. 浏览器端 getDataURL:最轻量也最可控的取图方式

2.1 getDataURL 到底做了什么

Echarts 实例上的getDataURL方法,本质上是把这个实例当前渲染出来的画布内容,按指定格式编码成一段 data URL 字符串。它拿的是“此刻画布上的像素”,而不是重新渲染一遍。这一点非常关键,直接决定了后面很多坑的成因。

因为拿的是当前状态,所以你在页面上做的任何操作都会体现在导出结果里:图例被点掉了、某个 series 通过legend隐藏了、通过dispatchAction高亮了某个数据点,导出时都会保留下来。这既是优点也是陷阱——用户如果先把图例关了两条再看图,导出时那两条就真的没了。

getDataURL的常用参数有四个,我逐个说清楚。

参数类型作用我的常用值
typestring输出格式,png/jpeg/svgpng
pixelRationumber像素倍率,决定清晰度2 或 3
backgroundColorstring导出图的底色#ffffff
excludeComponentsarray导出时排除的组件['toolbox']

type这一项有个细节必须提醒:想要拿到 SVG 矢量结果,实例初始化时renderer就必须设成'svg'。如果用的是默认的 canvas 渲染器,type: 'svg'拿不到有价值的矢量内容。反过来,SVG 渲染器下拿到的是一段data:image/svg+xml;charset=UTF-8,开头的字符串,有些浏览器不允许直接用它触发下载,需要先转成 Blob 再下载。

2.2 背景色不设,导出图在深色环境里就是灾难

backgroundColor不传的情况我见过太多次了。默认情况下导出的是透明背景,把这张图贴到深色主题的 PPT 或者深色模式的文档里,浅色的坐标轴文字直接看不见。

更麻烦的是项目里常见的“深色大屏”需求。页面背景是深蓝渐变,图表配置里没有单独设背景色,用户点导出,拿到一张透明底的图,贴到白底文档里,白色的坐标文字又消失了。这类工单我至少处理过五次。

正确做法是在导出时手动指定底色:

const url = chart.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#ffffff', // 导出为白底,与文档环境匹配 });

如果大屏本身就是深色的、导出后要发给同事在深色环境里看,那就传深色:

const url = chart.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#0b1a33', });

一个更省心的方案是让图表容器本身有背景色(CSS 设置),同时在setOption里也配一份backgroundColor,这样页面显示和导出结果是一致的,不用在导出时特殊处理。

2.3 pixelRatio 到底该填多少

pixelRatio是导出清晰度的唯一开关,也是最容易配错的地方。它的含义是:导出的图片宽度 = 图表容器宽度 × pixelRatio。

举个具体的数:容器宽 800px,pixelRatio: 2,导出的 PNG 就是 1600px 宽。这个尺寸放进 Word 文档里基本够用,塞进 PPT 单页也能撑住。

如果这张图要用于打印,就需要倒推。A4 纸宽 210mm,按 300dpi 打印质量换算,需要约 2480px 宽。容器是 800px,那么pixelRatio至少要 3.1,取 4 比较稳妥。

代价是文件体积。1500px 宽的折线图 PNG 大概 200KB,pixelRatio从 2 提到 4,宽度翻倍,像素总量变成四倍,文件可能直接涨到 1MB 以上。如果导出后还要走接口上传,建议控制在pixelRatio: 2到3之间,再大就考虑输出 JPEG。

2.4 一份可以直接抄的完整导出代码

下面这段是我在多个项目里迭代过的版本,处理了下载、命名、异常提示三个环节:

/** * 导出 Echarts 实例为图片 * @param {Object} chart echarts 实例 * @param {Object} opts 配置项 */ function exportChartAsImage(chart, opts = {}) { const { fileName = `图表_${formatDate(new Date())}`, type = 'png', pixelRatio = 2, backgroundColor = '#ffffff', } = opts; let url; try { url = chart.getDataURL({ type, pixelRatio, backgroundColor, excludeComponents: ['toolbox'], }); } catch (err) { console.error('[导出失败] getDataURL 抛出异常', err); // 常见原因:canvas 被跨域图片污染 return; } if (!url || url === 'data:,' || url.length < 100) { console.warn('[导出失败] 返回内容为空,检查图表是否已完成渲染'); return; } const a = document.createElement('a'); a.href = url; a.download = `${fileName}.${type === 'jpeg' ? 'jpg' : type}`; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); // 及时回收,避免在低端机上堆积 window.setTimeout(() => document.body.removeChild(a), 0); } function formatDate(d) { const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}`; }

调用就是一行:

exportChartAsImage(myChart, { fileName: '月度销售趋势', pixelRatio: 3 });

注意:getDataURL必须在图表已经完成渲染之后调用。如果放在setOption的紧后面,某些异步数据场景下图表还在动画过程中,导出的可能是半透明的中间帧。稳妥做法是监听finished事件。

myChart.on('finished', () => { // 首次渲染完成,可以安全导出 exportChartAsImage(myChart); });

finished事件在每次动画结束后都会触发,所以记得加个一次性标记,不然会重复执行。

3. toolbox 内置保存图片:零代码可用,但得会改

3.1 打开它只需要一个配置项

Echarts 的toolbox组件自带了一个saveAsImage功能,配置上去之后图表右上角就会出现一个小相机图标,用户点一下就能存图。

option = { toolbox: { show: true, feature: { saveAsImage: { show: true, title: '保存为图片', type: 'png', name: '销售趋势图', backgroundColor: '#ffffff', pixelRatio: 2, }, }, }, // ... 其他配置 };

这是所有导出方案里接入成本最低的,几乎等于白送。内部工具、后台管理系统、给运营同事用的自助分析页面,用它完全够。我做过一个只有七八个图表的内部看板,全程没写一行导出逻辑,就靠这个配置项交付的,用了一个季度没人反馈问题。

3.2 常用的几个配置项该怎么设

saveAsImage支持的配置项不算多,但有几个必须调。

name是导出文件名,默认是chart,导出一堆文件之后全是chart(1).png、chart(2).png,找都找不着。建议用图表主题命名,比如月度活跃用户。

pixelRatio默认值是 1,这是很多人抱怨“导出图很糊”的根源。调到 2 或 3,清晰度立刻上一个台阶。注意这个值和屏幕的devicePixelRatio是两回事,别混淆。

excludeComponents默认就是['toolbox'],也就是导出时会把工具箱本身隐掉。这个默认值是合理的,保留它。如果你额外加了dataZoom、brush之类组件,也可以加进去,让导出图更干净。

backgroundColor默认是'auto',会读取图表配置的backgroundColor。如果图表没设背景,导出的还是透明底。所以显式写一个颜色是最保险的。

type除了png还能选jpeg。JPEG 不支持透明,所以用 JPEG 时backgroundColor必须显式指定,否则透明区域会被填成黑色。这个坑我踩过一次,导出的一批图全部黑底,排查了半天才发现是格式问题。

3.3 三个高频踩坑点

第一个坑是异步渲染。用户手快,页面刚打开图表还在动画中就把相机点了,导出来的是残缺图。解决办法是给 toolbox 加一点限制,或者在数据加载完成后延迟几百毫秒再让 toolbox 显示:

toolbox: { show: false, // 初始隐藏 feature: { saveAsImage: { /* ... */ } }, } // 数据到位后再打开 myChart.setOption({ toolbox: { show: true } });

第二个坑是 SVG 渲染器下的坐标偏移。项目里如果初始化用的是renderer: 'svg',在某些 Echarts 版本下,saveAsImage导出的 SVG 会出现labelLine末端小圆点位置偏移、饼图标签位置对不上的问题。这类问题通常和渲染器在导出时对坐标系的重算有关,如果图表对位置精度要求很高,建议实测后再交付,必要时切回 canvas 渲染器。

第三个坑是 tooltip 不会被导出。tooltip 是挂在图表容器上的 DOM 节点,不在画布里面,saveAsImage捕获不到它。如果业务方期望“导出的图要带着数据提示框”,用这个方案做不到,得自己合成。

3.4 什么时候不该用它

toolbox的按钮在图表右上角,位置固定,样式和交互都不好改。如果你的页面设计稿里导出按钮需要放在页面顶部的工具栏里、需要和其他按钮保持统一风格,那么这个自带的相机图标就没法用了。

另外一个限制是它只能导出当前这一个实例。如果是多图导出,或者需要把标题、筛选条件一起导出,还是得回到getDataURL体系自己拼装。

我的做法是:内部工具放开用,对外产品一律自己写按钮。这样交付质量和设计规范都能兜住。

4. 服务端渲染导出:批量与自动化场景的正解

4.1 什么情况下必须把渲染搬到后端

判断标准很简单,出现下面任意一条,前端方案就不合适了:

  • 需要在没有用户操作的情况下生成图片,比如每天凌晨生成日报图。
  • 需要一次性生成几十上百张图,前端逐张点击不现实。
  • 生成的图片要直接存进对象存储或者作为附件发邮件,不经过用户浏览器。
  • 需要生成带品牌底图、带二维码的分享卡片,前端拼装逻辑太散。

这类需求我在做运营报表系统时遇到过。当时的要求是每天早八点,把前一天的核心指标生成六张图,拼成一张长图,通过邮件发给业务负责人。这个链路里根本没有浏览器,只能走服务端。

4.2 Node 侧用无头浏览器截图的完整流程

最通用的服务端方案是跑一个无头浏览器,打开预先部署好的图表页面,等渲染完成后截图。这个方式的优点是所见即所得——页面长什么样,导出的图就什么样,前端怎么调的颜色、字体、间距,后端完全不用管。

先装依赖:

npm install puppeteer

完整脚本:

const puppeteer = require('puppeteer'); const path = require('path'); async function exportChartPage(pageUrl, selector, outFile, scale = 2) { const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-dev-shm-usage'], }); try { const page = await browser.newPage(); // deviceScaleFactor 等价于前端的 pixelRatio await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: scale }); await page.goto(pageUrl, { waitUntil: 'networkidle0', timeout: 60000 }); // 页面里渲染完成后打个标记,避免截到动画中间帧 await page.waitForFunction(() => window.__CHART_READY__ === true, { timeout: 30000, }); const el = await page.$(selector); if (!el) throw new Error(`找不到元素:${selector}`); await el.screenshot({ path: path.resolve(outFile), type: 'png', omitBackground: false, }); return outFile; } finally { await browser.close(); } } module.exports = { exportChartPage };

对应的前端页面里加上渲染完成标记:

const chart = echarts.init(document.getElementById('main')); chart.setOption(option); chart.on('finished', () => { window.__CHART_READY__ = true; });

这里有两个参数值得展开说。deviceScaleFactor的作用和前端pixelRatio一样,设成 2 就是两倍分辨率截图,配合waitForFunction能拿到非常干净的图。--disable-dev-shm-usage这个参数在容器环境里几乎是必须的,不加的话并发跑几张图就容易因为共享内存不足而崩掉,这个坑我在 Docker 里调了整整一个下午。

waitUntil: 'networkidle0'表示等网络请求全部停下来。如果图表页面会拉取较大的数据,建议改成'domcontentloaded'配合自定义的waitForFunction,否则容易在这步超时。

4.3 无浏览器方案:SSR 加矢量转位图

无头浏览器虽然万能,但启动一个浏览器进程的内存开销不小,跑个几十张图还能接受,如果要高频调用或者部署在资源受限的机器上就有点吃力。Echarts 从 5.3 版本开始提供了服务端渲染能力,可以完全脱离浏览器生成图片。

思路是这样的:用 Echarts 的服务端渲染模式直接把图表渲染成 SVG 字符串,再交给图像处理库转成 PNG。

const echarts = require('echarts'); const { createCanvas } = require('canvas'); const sharp = require('sharp'); // 让 Echarts 知道用什么来创建画布 echarts.setPlatformAPI({ createCanvas: () => createCanvas(1200, 700), }); async function renderToPng(option, outFile, width = 1200, height = 700) { const chart = echarts.init(null, null, { renderer: 'svg', ssr: true, width, height, }); chart.setOption(option); const svgStr = chart.renderToSVGString(); chart.dispose(); // 交给 sharp 转位图,density 控制清晰度 await sharp(Buffer.from(svgStr), { density: 200 }) .png() .toFile(outFile); return outFile; }

这套方案的优点很明显:不依赖浏览器,内存占用低,启动速度快,很适合放在容器里做批量生成。renderer: 'svg'配合ssr: true是官方推荐组合,注意chart.dispose()一定要调,不然跑批量任务时内存会一直涨。

sharp的density参数控制栅格化时的清晰度,默认 72,我一般设 150 到 300 之间。设太高会导致输出文件巨大,实际意义不大。

需要提醒的是,服务端渲染对type: 'custom'的自定义系列支持有限,涉及复杂自定义绘制时,建议还是回退到无头浏览器方案。另外,如果图表依赖浏览器的字体环境,服务端也需要装好对应字体文件,不然会出现文字位置偏移或者方框乱码。

4.4 后端拿到的图片怎么存怎么回传

服务端生成图片后,通常会遇到两个处理分支。

如果只是给接口调用方返回,最省事的是直接返回 base64 字符串,前端拿到之后转 Blob 下载:

// Node 侧 const buffer = await sharp(Buffer.from(svgStr), { density: 200 }).png().toBuffer(); res.json({ code: 0, data: `data:image/png;base64,${buffer.toString('base64')}` });
// 前端侧 function base64ToBlob(base64, mime = 'image/png') { const arr = base64.split(','); const bstr = atob(arr[1]); const u8 = new Uint8Array(bstr.length); for (let i = 0; i < bstr.length; i++) u8[i] = bstr.charCodeAt(i); return new Blob([u8], { type: mime }); }

如果是要归档,直接写进对象存储,返回一个可访问的地址更合适。base64 内联在 JSON 里会让响应体膨胀约三分之一,几十张图的场景下这个开销不能忽略。

顺带说一句,有些后端同学会想在自己熟悉的语言里直接画图。比如 PHP 项目里想生成图表图片,通常不建议重写渲染逻辑——图表的视觉效果在前端已经调好了,后端重画一遍必然对不上。常见做法是把渲染交给一个独立的 Node 渲染服务,PHP 只负责传数据和存文件,职责清晰,维护成本也低。

5. 常见问题与排查技巧实录

5.1 导出结果是空白的

这是被问得最多的问题。按我的经验,原因基本集中在三类。

第一类是图表还没渲染完就调用了导出。表现是图片有背景色但没有内容。处理办法就是前面说的,监听finished事件,或者至少延迟一个动画周期。如果图表设置了animation: false,渲染是同步的,这个问题就不会出现,但生产环境里大部分图表都开着动画。

第二类是容器尺寸为零。图表容器如果用display: none隐藏着,或者父元素没有高度,画布的实际宽高是 0,导出的自然也是空图。常见于“导出前先隐藏其他标签页”的场景。解决办法是先让容器可见、拿到真实尺寸之后再导出,导完再隐藏回去。

第三类是 canvas 被跨域资源污染。这个下面单独说。

5.2 跨域图片导致的导出失败

项目里用到pictorialBar配合自定义图片(比如柱状图上贴一个小图标)、或者地图上的散点用外部图标,图片域名和页面域名不一致时,画布就会被标记为“污染”状态。这时候调getDataURL会直接抛安全异常。

处理方式有两个。一是让图片服务的响应头带上允许跨域的字段,同时在 Echarts 配置里显式声明,让请求带上匿名标记:

// 服务器响应头需要包含 Access-Control-Allow-Origin series: [{ type: 'pictorialBar', symbol: 'image://https://cdn.example.com/icon.png', symbolSize: [40, 40], crossOrigin: 'anonymous', }]

二是把小图标直接转成 base64 内联到代码里。这个方式绕过了跨域问题,代价是包体积变大。图标数量少、体积小的时候我一般选这个,最省事。

还有一种情况是本地开发环境图片能显示、导出也正常,部署到生产就不行。这时候先检查生产的 CDN 有没有配跨域头,别急着改代码。

5.3 导出后文字发虚、线条模糊

发虚几乎总是pixelRatio设得太低。默认的saveAsImage是 1,getDataURL不传也是 1,在很多高分屏上放大看就会糊。

但也有一种情况是调高了还糊,这时候要检查是不是导出的图片被二次缩放。比如导出 1600px 宽的图,又用 CSS 强制显示成 300px 宽,然后再截图,结果自然糊。这类问题出在合成环节而不是导出环节。

还有一种是 SVG 转位图时密度不够。服务端方案里sharp的density默认只有 72,转出来的位图文字边缘会有明显锯齿,调到 150 以上就顺畅了。

5.4 关于 rem 布局下图表尺寸的坑

有同学问过:项目里用pxtorem做了移动端适配,为什么图表导出的尺寸和预期不一致。

原因在于 Echarts 画在 canvas 上,canvas 内部的像素尺寸由初始化时传入的宽高决定,rem 缩放只影响 CSS 显示尺寸,不会改变画布的实际像素。所以导出时算尺寸,要读容器的offsetWidth和offsetHeight,这两个是布局后的真实像素值,而不是 CSS 里写的 rem 数值。

如果图表需要跟着屏幕缩放,正确做法是监听窗口大小变化后调chart.resize(),让 Echarts 重新计算画布尺寸,而不是指望 CSS 缩放自动生效。

5.5 问题速查表

现象大概率原因处理办法
导出图全白渲染未完成或容器尺寸为 0监听finished事件,检查容器可见性
抛安全异常canvas 被跨域图片污染配置跨域响应头加crossOrigin,或改用 base64 图标
文字模糊pixelRatio太低或二次缩放调到 2 至 3,排查合成环节
背景透明未设backgroundColor显式指定底色,JPEG 格式必须设
导出的 JPEG 全黑JPEG 不支持透明必须先设backgroundColor
图例凭空少了几条用户手动隐藏过导出前用dispatchAction恢复图例状态
饼图标签偏移SVG 渲染器兼容问题实测后考虑切回 canvas 渲染器
批量导出内存暴涨实例未释放每次导出后调用dispose()
tooltip 没出现tooltip 是 DOM 不在画布内需要则单独合成,不要指望导出捕获
服务端文字变方框缺少字体文件容器内安装所需字体

这张表我放在项目 Wiki 里,团队里新人遇到导出问题先自查一遍,能解决八成的情况。

5.6 几个提升交付质量的小技巧

导出的文件名带时间戳,能省掉很多“哪张是最新的”这类沟通成本。我一般用指标名_YYYYMMDD_HHmm.png的格式。

导出前把 toolbox 从结果里排除掉,excludeComponents: ['toolbox']就够了,免得导出的图右上角带着一个孤零零的相机图标。

如果是给汇报用的图,导出后可以顺手加个标题栏。做法是创建一个离屏 canvas,先画上标题文字和日期,再把图表图片画在下面。多花二十分钟,交付质感完全不一样。

图表数量多的时候,给导出按钮加个 loading 状态,防止用户连点导致重复下载。这个细节看起来小,但能避免不少“为什么我点了三次下载了三张一样的图”的追问。

6. 我踩过几次坑之后的真实体会

最开始做导出功能的时候,我总想着一步到位,直接上服务端方案,觉得那样最“专业”。结果一个只有三个图表的后台页面,为了导出功能额外搭了一套渲染服务,运维复杂度上去了,收益却没多少。后来我把方案退回到getDataURL,二十行代码解决问题,团队里谁都能维护。

真正需要服务端方案的场景其实很有限,通常是那种“用户不在场”的定时任务。这时候我的判断标准变得很干脆:只要导出动作需要人来触发,就留在前端;只要导出动作不需要人参与,就放到后端。按这个标准走,技术选型几乎不会错。

另一个体会是,导出这件事的难点从来不在代码,而在细节。背景色、像素比、文件名、图例状态、跨域图片、字体环境,每一样单独拎出来都是小事,凑在一起就是交付质量的分水岭。我现在的习惯是,导出功能写完之后,一定要在四种环境里实测一遍:浅色文档、深色文档、打印预览、移动端查看。这四个场景里都没问题,才算真正做完。

最后一个建议给正在接这类需求的朋友:动手之前先问清楚一句话——这张图最终是要放进哪里。答案会直接告诉你该用什么格式、什么像素比、什么背景色。问清楚这一句,能省掉后面两轮返工。

返回列表