news 2026/10/16 7:03:50

Vue项目PDF导出实战:html2canvas+jsPDF避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue项目PDF导出实战:html2canvas+jsPDF避坑指南

前阵子在后台管理项目里接了个导出需求:把订单详情页导成 PDF,客户看到什么就得导出什么。当时第一个想到的就是 html2canvas + jsPDF 这个经典组合,先把 DOM 画成 canvas,再把 canvas 转成图片塞进 PDF。这套流程跑 demo 很快,但真正落到业务里,分页截断、样式错位、背景色丢失、图片跨域、导出模糊这些延伸问题接踵而来。这篇就把我实际踩过的坑和最终的解决方案整理出来,给同样在做 Vue 导出 PDF 的朋友一个参考。

1. 为什么选 html2canvas + jsPDF,而不是其他方案

先说结论:这项需求“所见即所得”的要求比“数据完整”更高。用户不关心 PDF 里有没有结构化的数据流,他们只希望打印出来的效果和页面上看到的一模一样。

市面方案大致有这么几类,我简单比对过:

方案优点缺点
浏览器 window.print()原生支持、CSS 分页可控用户要手动选择打印机,交互割裂;样式兼容性差异大
后端生成 PDF数据结构化、文件体积小需要前后端联调,样式还原成本高,开发周期长
html2canvas + jsPDF纯前端、所见即所得、上手快生成的是位图,放大可能模糊;分页和样式有各种坑
快照类方案(服务端渲染网页再截图)效果最还原依赖服务端浏览器环境,成本高,不通用

我这个项目选 html2canvas + jsPDF 的原因很简单:团队没有后端配合时间,页面本身是典型的”富样式报表“,表格、色块、签名、图片混在一起,用数据结构去描述这些元素会非常痛苦。位图方案虽然牺牲了文件可编辑性,但换来的是最小的沟通成本和最快的上线速度。

而且这个组合在社区里积累的问题和解决方案已经比较多了,遇到问题基本能搜到思路,不至于卡死。

2. 从 HTML 到 PDF,这中间到底发生了什么

很多同学一上来就抄代码,抄完发现导出的图片歪歪扭扭,然后各种加参数试错。我建议先花十分钟搞清楚这两个库各自干了什么,后面排查问题会快很多。

2.1 html2canvas 不是“截图”,是“重新绘制”

html2canvas 并不是从浏览器获取像素级的截图。它的工作方式是:解析目标 DOM,克隆一份到离屏的 iframe 或容器里,遍历所有节点的计算样式,然后用 canvas API 把每个元素一笔一笔画出来。

这就能解释很多“怪现象”了。为什么某些box-shadow画不出来?因为 canvas 的重绘过程不经过浏览器的原生渲染引擎,它只是用自己的逻辑尽可能还原样式。为什么图片没加载完成时画出来是空白?因为 canvas 绘制图片时,如果图片还没 ready,drawImage 什么都不会画。为什么某些 CSS 渐变背景会丢?因为旧版 html2canvas 对linear-gradient的处理一直不稳定,新版本即便支持了部分场景,遇到复杂叠加依然会翻车。

这里有一个最关键的概念,就是scale参数。html2canvas 生成的 canvas 物理尺寸 = 元素尺寸 × scale。默认值是 1,也就是 1 个 CSS 像素对应 canvas 的 1 个物理像素。浏览器在 96 DPI 下,1px 换算成物理长度是 25.4 / 96 ≈ 0.2646mm。如果直接把默认 scale 的 canvas 塞进 PDF,打印到 A4 纸上的效果会偏小,而且文字边缘有明显锯齿。调大 scale 可以让 canvas 拥有更多物理像素,PDF 里显示就更清晰,但代价是内存占用呈指数级增长,后面单独讲。

const canvas = await html2canvas(element, { scale: 2, // 每个 CSS 像素用 2x2 物理像素去画 useCORS: true, backgroundColor: '#ffffff', })

2.2 jsPDF 把“位图”摆进“矢量页面”

jsPDF 创建的 PDF 页面本质上是矢量坐标系,单位默认是毫米。A4 纸是 210mm × 297mm。把 canvas 图片放进 PDF,核心是计算图片在页面上的物理尺寸和位置。

公式很简单:图片的物理宽度 = canvas 像素宽度 × 25.4 / 96。

const pdf = new jsPDF('p', 'mm', 'a4') const pdfWidth = pdf.internal.pageSize.getWidth() const margin = 10 const px2mm = 25.4 / 96 const imgWidth = pdfWidth - margin * 2 const imgHeight = canvas.height * px2mm * (imgWidth / (canvas.width * px2mm)) pdf.addImage(canvasData, 'PNG', margin, margin, imgWidth, imgHeight)

这里有个容易绕晕的点:imgHeight不需要单独用canvas.height * px2mm算,因为图片宽高必须等比缩放。先确定宽度占满可用区域,高度按比例自然就出来了。我见过有人直接传canvas.height * px2mm,结果图片超出页面底部一大截,就是这个比例没保持好。

2.3 jsPDF 不会自动分页,一切截断都来自这里

这是整套方案里最容易踩、也最影响观感的问题。jsPDF 的addImage只是把图片放在当前页面的某个坐标上,图片超过页面边界就直接切掉,而且它也不会自动帮你新增页面。

所以如果内容高度超过一页,你需要自己决定:是在第二页重新打印同一张图但调整偏移,还是把 canvas 按像素高度切成几段,分别放到每一页。两者的效果差别很大,后面画一整节讲。

3. 基础实现:一个能跑的导出函数

先把地基打好,后面再谈优化。我直接给出 Vue 3 + TypeScript 环境下的最小可用版本。

3.1 等待图片和字体加载完毕

导出动作发起时,页面上的图片可能还在请求中。如果不管直接调 html2canvas,canvas 里对应的位置就是空白。字体也一样,@font-face没加载完,渲染出的文字用的是 fallback 字体,导出结果和页面显示对不上。

async function ensureAssetsReady(element: HTMLElement): Promise<void> { // 等待页面字体加载完成 if (document.fonts && document.fonts.ready) { await document.fonts.ready } // 找出所有未加载完成的 img 元素,逐个等 load const images = Array.from(element.querySelectorAll('img')) const pending = images.filter((img) => !img.complete) await Promise.all( pending.map( (img) => new Promise<void>((resolve) => { img.onload = () => resolve() img.onerror = () => resolve() }) ) ) }

注意onerror也要 resolve,否则某张图挂了,导出流程会一直卡在 pending 状态,用户那边就是白屏转圈。我的习惯是:图片挂了至少让流程继续,出来的 PDF 缺一块图,总比永远导不出来强。

3.2 导出主流程

import html2canvas from 'html2canvas' import { jsPDF } from 'jspdf' const px2mm = (px: number) => px * (25.4 / 96) export async function exportDomToPdf( element: HTMLElement, fileName: string ): Promise<void> { await ensureAssetsReady(element) const canvas = await html2canvas(element, { scale: 2, useCORS: true, allowTaint: false, backgroundColor: '#ffffff', logging: false, }) const imgData = canvas.toDataURL('image/png') const pdf = new jsPDF('p', 'mm', 'a4') const pdfWidth = pdf.internal.pageSize.getWidth() const pdfHeight = pdf.internal.pageSize.getHeight() const margin = 10 const contentWidth = pdfWidth - margin * 2 const contentHeight = pdfHeight - margin * 2 // 图片等比缩放后的物理尺寸 const imgWidth = contentWidth const imgHeight = canvas.height * px2mm(canvas.width) / px2mm(canvas.width) * contentWidth // 简化写法:保持宽高比 const scaledImageHeight = (canvas.height * contentWidth) / canvas.width pdf.addImage(imgData, 'PNG', margin, margin, imgWidth, scaledImageHeight) pdf.save(`${fileName}.pdf`) }

上面的scaledImageHeight就是简化的等比计算:canvas 高度 ÷ canvas 宽度 × 目标宽度。这段代码只处理单页内容,超过一页会直接被切。下面重点处理分页。

3.3 分页的基本思路

最常见的做法是:第一页正常 addImage,然后计算剩余高度,如果还有剩余,就pdf.addPage(),再次 addImage 同一张图,但 y 坐标用负的偏移量往上顶,让第二页显示图片的下半部分。

let heightLeft = scaledImageHeight let position = margin pdf.addImage(imgData, 'PNG', margin, position, imgWidth, scaledImageHeight) heightLeft -= contentHeight while (heightLeft > 0) { position -= contentHeight pdf.addPage() pdf.addImage(imgData, 'PNG', margin, position, imgWidth, scaledImageHeight) heightLeft -= contentHeight }

这种写法的核心原理:PDF 的坐标系是固定的,图片越界部分会被裁掉。第二页我把 y 坐标设为margin - contentHeight,图片就相当于向上移动了整整一页的高度,页面可见的就是图片的第二段内容。这样能把完整内容分页打出来,代码也简单。

但这个方案有观感问题:如果正好有一行文字跨越两页边界,它会被从中间切断。这是位图分页的天然缺陷,很难彻底消除,只能缓解。后面我给出更可控的切片方案。

4. 延伸问题一:分页截断与内容缺失

上一节的位移分页方案虽然能用,但直接把一整张图往上顶,会有一个隐蔽的问题:如果你的 canvas 高度恰好不是整数页,最后一段内容的排布会不太均匀。更麻烦的是,有些表格的边框线正好落在页边界上,被切断后看起来像表格没有闭合。

在实际项目中,我更推荐直接对 canvas 做像素切片。意思是:把高度超过一页的 canvas,按每页可容纳的像素高度切成若干份,每一份独立转成图片,再放到对应的 PDF 页上。这样每一页的内容是固定的,不会出现“内容重复”或“偏移量算不准”的隐患。

function addCanvasToPdf( pdf: jsPDF, canvas: HTMLCanvasElement, margin: number, pageWidth: number, pageHeight: number ): void { const px2mm = 25.4 / 96 const contentWidth = pageWidth - margin * 2 const contentHeight = pageHeight - margin * 2 // 一页能容纳多少 canvas 像素 const pagePixelHeight = Math.floor(contentHeight / px2mm) const totalPixelHeight = canvas.height let offsetY = 0 let pageIndex = 1 while (offsetY < totalPixelHeight) { // 当前页实际需要切多高 const sliceHeight = Math.min(pagePixelHeight, totalPixelHeight - offsetY) // 创建一个临时 canvas,只截取原图的一部分 const sliceCanvas = document.createElement('canvas') sliceCanvas.width = canvas.width sliceCanvas.height = sliceHeight const ctx = sliceCanvas.getContext('2d') if (!ctx) continue ctx.drawImage( canvas, 0, offsetY, canvas.width, sliceHeight, 0, 0, canvas.width, sliceHeight ) const data = sliceCanvas.toDataURL('image/jpeg', 0.95) if (pageIndex > 1) { pdf.addPage() } pdf.addImage( data, 'JPEG', margin, margin, contentWidth, sliceHeight * px2mm ) offsetY += sliceHeight pageIndex++ } }

切片方案里有个细节值得注意:sliceHeight * px2mm不一定等于contentHeight,因为最后一页可能剩下不满一页的高度。剩余部分直接按实际高度放在页面顶部即可,这样最后一页不会出现大片空白,也不用额外调整位置。

切片方案为什么比位移方案稳?因为位移方案里每一页看到的区域是计算出来的“图片的相对位置”,一旦最后一个页面的剩余高度小于图片宽度,整张图片依然会被完整塞进去,容易在底部留白;而切片方案每次都精确控制切下来的高度,最后一页的高度就是剩余量,可控性强得多。

但切片方案同样绕不开“跨页文字被切断”的问题。缓解办法有两个。第一,导出的容器样式尽量紧凑,表格行高调小一些,避免大量文本行正好骑在边界上。第二,可以给切片高度加一个轻微的 overlap,比如每页多切 10px 像素,放到下一页时从 offsetY 减去这 10px,让内容的连续性看起来更好,代价是边界处可能有极轻微的内容重复。我实际项目中用的就是 overlap = 8px,肉眼几乎感知不到重复,但断行的情况明显改善了。

5. 延伸问题二:样式错乱的各种坑

样式问题往往是最让人头疼的,因为不报错,就是结果不对。这里列几个我实际踩过的。

5.1 背景色和渐变消失

html2canvas 对背景色的兼容性比想象中差。最常见的是你明明设置了body的背景色,导出后 PDF 却是白色的。原因在于 canvas 默认背景是透明黑,html2canvas 虽然提供了backgroundColor参数,但也只影响最终的画布底色。如果容器本身没有显式背景色,它就不会向上取 body 的背景色。

解决方法是导出前给目标容器显式加上白色背景:

const cloneElement = element.cloneNode(true) as HTMLElement cloneElement.style.backgroundColor = '#ffffff' cloneElement.style.position = 'fixed' cloneElement.style.left = '-9999px' cloneElement.style.top = '0' document.body.appendChild(cloneElement) // html2canvas 用 cloneElement,结束后 remove

更省事的是在 html2canvas 配置里直接设backgroundColor: '#ffffff'。但要注意,这只保证画布底色是白色,各单元格的半透明背景色叠加后会产生色差。如果表格里有大量淡色背景的小格子,叠加结果和屏幕显示不一定一致,这种情况建议导出专用 DOM 模板,不要直接导业务页面。

5.2 字体和单位导致的对不齐

html2canvas 读取的是元素的计算样式,这对我们是好事,意味着大多数样式都能还原。但它对 css 单位兼容有历史遗留问题,尤其是rem。如果你页面根节点的 font-size 不是默认 16px,或者有动态调整,有些版本算出来的尺寸会不准确。所以导出专用模板里,字体大小我全部用 px 写死。

另外字体加载的问题,除了前面说的document.fonts.ready,还要注意部分浏览器对font-display: swap的处理。如果字体的 block 阶段很短,页面显示时 fallback 字体一闪而过,html2canvas 抓取 DOM 时拿到的是最终渲染状态,没问题;但如果字体本身就加载失败,canvas 里会显示 fallback,和页面不一致,这个只能从字体资源的稳定性上解决。

5.3 伪元素和阴影完全不渲染

项目里用::before、::after做的图标、角标、价格标签非常常见。html2canvas 对标准的 content 型伪元素支持还可以,但如果伪元素里放了背景图、渐变或者依赖content: ''+ 背景色绘制图形,就有概率丢失。我的经验是:导出项目里不要依赖伪元素做关键信息展示,价格、状态、金额这些从真实 DOM 节点读取渲染,伪元素只做装饰性视觉,丢失了也不影响内容完整性。

box-shadow在部分版本里会渲染失败或者只渲染一层模糊边缘。如果页面里用大面积的阴影做层次感,导出前建议直接给模板加个box-shadow: none的样式重置,同时用边框或者底色来区分层次,这样 PDF 里不会出现难看的黑色块。

6. 延伸问题三:图片、字体与跨域

图片问题是导出 PDF 里占比最高的报错类型。尤其是系统里有鉴权图片、OSS 图片、CDN 图片的场景。

6.1 图片不显示,最常见的原因是 canvas 被污染

浏览器有一个安全机制:如果 canvas 被绘制了跨域图片,且没有经过 CORS 授权,那canvas.toDataURL()会直接抛SecurityError。这其实是 canvas 在保护用户隐私,防止脚本读取其他域名的像素数据。

解决办法有几层。第一层,给图片加crossOrigin="anonymous"属性,告诉浏览器“我要以匿名方式加载这张跨域图片”。第二层,服务器要返回Access-Control-Allow-Origin响应头,允许该图片被跨域使用。第三层,html2canvas 的配置里打开useCORS: true。

// 导出前遍历图片,动态补 crossorigin element.querySelectorAll('img').forEach((img) => { img.setAttribute('crossorigin', 'anonymous') })

但有个现实问题:很多 CDN 图片服务不会返回 CORS 响应头。useCORS打开之后依然可能报错。我在某个老项目里遇到的是:单域名的图片没问题,走 CDN 的图片全部白屏。最后折中方案是把图片先转成 Blob,再用本地 URL 创建对象替换 img 的 src。

async function fetchAndReplaceImages(element: HTMLElement): Promise<void> { const images = Array.from(element.querySelectorAll('img')) for (const img of images) { const src = img.getAttribute('src') if (!src || src.startsWith('data:')) continue try { const resp = await fetch(src) const blob = await resp.blob() const objectUrl = URL.createObjectURL(blob) img.src = objectUrl } catch { // 单条失败不阻塞整体导出 } } }

用 Blob 方案前先确认图片服务允许 fetch。允许的话基本能解决跨域问题;如果不允许,就得通过同域后端代理中转,这不属于前端的范畴,排在最后。

6.2 图片加载完成的检测,别只依赖 img.complete

img.complete只能表示浏览器对这张图片的加载流程结束了,不代表绘制一定成功。如果图片有 CORS 限制,complete 是 true,但 drawImage 时依然画不上去。所以在 ensureAssetsReady 之外,我还会在 html2canvas 渲染前强制走一遍替换逻辑,把有问题的图片统一换成已确认可用的数据源。

另外canvg输出的 SVG 图片、或含foreignObject的 SVG,在 html2canvas 里支持也不完整。如果你页面里有这类高级 SVG 场景,别浪费时间调参,直接在导出模板里换成普通 img 或 canvas。

6.3 字体加载,除了等待还有嵌入问题

页面里用了自定义字体时,document.fonts.ready只能保证字体已经加载,但 html2canvas 绘制文字时能不能拿到字体信息,取决于浏览器的 canvas 字体机制。大部分现代浏览器都能正确绘制,但也有坑:如果字体文件本身是跨域的,和图片一样的 CORS 问题也会发生在字体上。

解决办法是给@font-face注册时使用相对同源的字体文件,或者用 base64 内嵌字体。不推荐在业务里为了一个导出功能内嵌字体,文件体积代价太大。实际项目中,我遇到的绝大多数情况是“字体已经加载但因为 fallback 导致字体不一样”,这种直接接受即可,PDF 里用系统字体输出,对阅读影响不大。

7. 延伸问题四:清晰度、内存与性能

7.1 scale 不是越大越好

很多人觉得 PDF 模糊,第一反应就是把 scale 调到 3、4,结果页面直接卡死。这是因为 canvas 的内存占用是像素量的四倍:每个像素需要存 RGBA 四个通道。一张 A4 宽度的内容,元素宽度 794px 左右,scale 2 时 canvas 宽度约 1588px,如果内容高度有 3000px,canvas 像素总量接近 476 万,占用内存接近 19MB。scale 4 就是 76MB。这还只是一张 canvas,切片时还会创建更多临时 canvas。

我的经验是 scale 2 到 3 之间基本够用。导出出来的 PDF 在屏幕上放大看没有明显锯齿,打印出来也够清晰。超过 3 不仅渲染慢,移动端还容易把浏览器内存打爆。

7.2 大页面导出的分段渲染策略

如果页面内容特别长(比如几十页的报表),一次性渲染整张长 canvas 会让主线程长时间无响应,用户会以为浏览器死了。我的做法是分段渲染:先估算总高度,按每页高度分成若干段,每一段单独调用 html2canvas 渲染,再把各段拼进 PDF。这样每一段渲染耗时可控,还可以在每段之间await new Promise(r => setTimeout(r, 0))让出主线程,页面不会完全冻结。

这个方案的问题是段落边界处如果有跨页的行,一样会有截断风险。所以分段点和切片点最好重合。如果分段策略实现成本太高,退一步用 loading 遮罩 + 单次渲染,至少让用户知道程序在干活。

7.3 移动端的固定定位和滚动问题

移动端浏览器对position: fixed元素的处理一直很微妙。如果导出目标里有 fixed 头部,html2canvas 渲染时这个元素可能会出现在每一段的相同位置,而不是跟随文档流,最终 PDF 里会出现内容重叠。

我的习惯是导出之前,给元素加一个独立的类,把所有 fixed 元素改成 absolute,并把父容器滚动位置强制重置到顶部。这不是万能解,但能规避大部分移动端陷阱。

另一个移动端常见问题是:导出内容在页面内滚动容器里。html2canvas 默认只绘制元素的可见区域,如果容器有滚动,导出结果会缺内容。解决办法是导出前临时把容器的滚动高度设置为总高度,或者把scrollTop置 0 后再渲染,具体看布局结构。最稳定的还是用一个离屏的克隆节点,把样式全部强制成静态计算后的值,再挂进 body 渲染。

8. 常见问题速查表

把这段时间积累的问题整理成一个速查表,遇到问题先对着表排查一轮。

症状可能原因处理办法
PDF 内容模糊scale 太低调大 scale 至 2 或 3
导出结果全白/图片区域空白图片未加载完成ensureAssetsReady 等待后再渲染
toDataURL 抛 SecurityError跨域图片污染 canvas图片加 crossorigin、服务器开 CORS、或转 Blob
背景色不对/变白html2canvas 默认透明底色容器显式设置背景色,配置传 backgroundColor
内容从中间切断jsPDF 不自动分页使用 canvas 切片方案或 overlap 方案
渐变色块消失html2canvas 对渐变支持不全导出模板改用纯色
导出页面卡死scale 太大或页面过长降 scale、分段渲染
字体与页面显示不一致字体未加载或跨域等待 fonts.ready,确保字体源同域
表格边框断裂表格正好跨页边界调整间距或 overlap,必要时优化行高
滚动容器内容缺失只渲染了可见区域用离屏克隆节点导出完整高度

9. 我最后想说的几个习惯

这几个习惯不是我一开始就有的,是踩了无数坑之后沉淀下来的,项目稳定的几乎都靠它们。

第一,给导出功能单独维护一个干净的 DOM 模板,而不是直接引业务页面。业务页面里各种弹窗、按钮、hover 状态、交互动效,导出时全都变成了负担。克隆模板再手动清理干扰元素,是成本最低的稳定方案。

第二,导出永远不要硬编码几个固定参数。scale、margin、页面尺寸、JPEG 压缩比,这些都应该是函数的参数,给业务方留出调整空间。否则每次改版,你都要去翻代码找魔法数字。

第三,导出过程一定给用户反馈。哪怕只是一个简单的 loading 遮罩,也比用户看着页面卡死然后刷新强。渲染百万级像素需要时间,这不是浏览器 bug,是物理限制。

第四,多测试几种入口。同一套导出代码,在不同浏览器、不同缩放比例下的结果可能都不一样。我最常翻车的是 iOS 微信内置浏览器和桌面 Safari。有空的话,准备一张测试清单,上线前每个分支过一遍。

这套方案说不上完美,但在前端资源有限、又要快速交付的情况下,它确实是最能平衡成本和效果的选择。如果你正在折腾 html2canvas + jsPDF 的分页和样式问题,希望对你有启发。

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

C# Web查询Access数据库从入门到实战:OleDb连接与性能优化

简介&#xff1a;这套项目示例面向需要掌握C#与ASP.NET Web开发并操作Access数据库的初学者&#xff0c;演示了从建立OleDb连接、执行SQL查询&#xff0c;到在GridView中展示结果的完整流程。资源包共62个文件&#xff0c;以C#源码&#xff08;.cs&#xff09;、解决方案与项目…

作者头像 李华
网站建设 2026/10/16 7:03:07

openclaw 详解:开源智能体编排架构的设计与实践

最近不管是在技术社区还是朋友圈&#xff0c;都在刷一个词&#xff1a;openclaw。如果你也和我一样&#xff0c;第一反应是“这又是什么新模型”&#xff0c;那这篇文章就是写给你的。openclaw 并不是某个单一的大模型&#xff0c;而是一套开源的、面向大模型应用的智能体编排架…

作者头像 李华
网站建设 2026/10/16 7:02:59

64位Matlab调用REFPROP热物性计算接口配置与避坑指南

简介&#xff1a;这份资源面向需要在MATLAB环境中调用REFPROP进行流体热力学与传输性质计算的工程师、科研人员及高校学生&#xff0c;尤其适合制冷、化工、热交换器设计等场景的64位Windows用户。压缩包共117个文件&#xff0c;约1.64MB&#xff0c;以105个fld流体定义文件、5…

作者头像 李华
网站建设 2026/10/16 7:02:54

ArkTS分类数据获取与页面渲染:从数据建模到状态联动实践

DAY9训练营的题目发到手里的时候&#xff0c;我刚接触ArkTS不到两周。分类数据获取与页面渲染实践&#xff0c;听起来不算难——无非是拿到数据、丢到页面上。可真在OpenHarmony的ArkUI框架里跑起来&#xff0c;“分类数据”这四个字背后的东西比我预想的多得多&#xff1a;数据…

作者头像 李华
网站建设 2026/10/16 7:02:54

Flutter与OpenHarmony家具购买提醒功能实战

1. 从零开始拆解这个项目&#xff1a;为什么是“记录 提醒”这个组合拿到“Flutter for OpenHarmony 家具购买记录App实战&#xff1a;添加提醒实现”这个标题&#xff0c;我先给你说说我第一眼看到它的感觉&#xff1a;这个项目选得聪明。它没有贪大求全去做一个电商闭环&…

作者头像 李华