简介这是一份面向前端开发者的原生 JavaScript 与 Canvas 截图下载示例资源对应「原生jscanvas实现截图实现a标签下载」主题适合想在不引入第三方库的前提下理解网页截图与浏览器下载链路的初中级开发者也适合作为课程演示与个人练手素材。压缩包为 rar 格式仅含 1 个 html 文件体积约 4KB属于可直接打开阅读、按需改造的轻量单页示例。目前已有 257 人学习说明该方向有一定实用需求。示例围绕获取可视区域尺寸与滚动位置、创建离屏 Canvas、将画布内容通过 toDataURL 转为 Base64 图像地址、动态生成带 download 属性的 a 标签并触发 click 完成保存这一完整流程展开同时保留了对跨域图片无法导出、复杂 CSS 效果难以还原等限制的说明便于读者理解原生方案的能力边界与引入 html2canvas 等库的取舍从而快速搭建可运行的截图下载雏形并迁移到自有项目。1. 从一张要下载的报表说起原生 Canvas 截图的适用边界原生 JavaScript 配合 Canvas 做截图再用 a 标签下载听起来像一个小脚本真正落到业务里却常卡在样式丢失、跨域图片空白和移动端点击没反应上。它适合后台报表导出、活动页分享图、可视化看板留档这类场景页面结构可控、图片资源能内联、不想引入大型截图库。常见做法是先把 DOM 序列化成自包含 SVG再画进 Canvas最后用 Blob 和 a 标签触发下载。这条链路不复杂但参数设错就会翻车下面按可复现的顺序拆开写。2. 把 DOM 变成可绘制像素原生 JavaScript 下 foreignObject 序列化的关键步骤2.1 为什么选 SVG foreignObject 而不是逐节点画 Canvas逐节点画 Canvas 的思路是遍历 DOM自己算每个元素的坐标、字体、边框和换行再调fillRect、fillText一点点画。它适合结构固定的图表卡片遇到表格、弹性布局、文字自动换行就会变成体力活。原生 JavaScript 截图更稳妥的路线是借助 SVG 的foreignObject把克隆后的 HTML 塞进 SVG让浏览器按原有布局排版再把整个 SVG 当成图片画进 Canvas。这样布局计算交给渲染引擎脚本只负责样式内联和资源内联。这个方案不是没有边界。foreignObject里的 HTML 必须尽量自包含外链样式表、外链图片、图标字体、伪元素都可能让截图缺块。它也不适合截取包含视频、iframe 或复杂 Canvas 动画的区域这些内容在序列化后往往只剩空白。选型时先判断页面是不是“静态结构 图片 文字”如果是这条链路足够如果页面强依赖实时脚本或第三方嵌入优先考虑后端排版或让业务方提供专门的分享图模板。第一步是把目标节点克隆出来并且把计算后的样式写到克隆节点上。只复制 class 不够因为页面样式可能来自多层选择器、媒体查询或运行时注入。下面这段代码保留影响布局和视觉的属性递归处理子节点。// 只保留影响布局和视觉的属性避免把无关样式全部写进内联 const KEEP_STYLE [ display, position, width, height, margin, padding, color, background-color, border, border-radius, font-family, font-size, font-weight, line-height, text-align, letter-spacing, white-space, box-sizing, flex, flex-direction, justify-content, align-items, grid-template-columns, gap, opacity, overflow ]; function inlineComputedStyle(source, target) { if (source.nodeType Node.ELEMENT_NODE) { const cs getComputedStyle(source); for (const key of KEEP_STYLE) { const value cs.getPropertyValue(key); if (value) target.style.setProperty(key, value); } // fixed 元素在 foreignObject 里容易偏移统一转成 absolute if (cs.position fixed) { target.style.position absolute; } } const srcChildren source.children; const dstChildren target.children; for (let i 0; i srcChildren.length; i) { inlineComputedStyle(srcChildren[i], dstChildren[i]); } }KEEP_STYLE不是越全越好。把transform、animation、filter全量复制容易在 SVG 里出现坐标偏移或渲染差异如果页面依赖 transform 做居中建议在克隆后把外层容器的 transform 清掉改用 margin 或 flex 对齐。position: fixed转absolute是血泪经验固定定位的工具栏在截图里经常跑到错误位置。递归时要求源节点和克隆节点结构一致所以克隆要在替换图片之前完成否则子节点数量会对不上。还有一个细节foreignObject里的 HTML 最好加上 XHTML 命名空间。部分内核在序列化后会把没有命名空间的节点当作文本处理表现是截图一片空白。常见做法是在克隆根节点上设置clone.setAttribute(xmlns, http://www.w3.org/1999/xhtml)再把它塞进foreignObject。这个属性不会影响页面样式但能减少兼容性玄学。2.2 图片、字体与伪元素的内联处理页面里的图片是截图失败的高发区。foreignObject序列化时不会自动把img的二进制内容写进 SVG如果src还是外链画进 Canvas 时要么缺图要么因为跨域把画布标记成不可导出。常见做法是把图片转成 dataURL再替换克隆节点里的src。同源图片可以直接fetch跨域图片要求资源响应允许跨域读取如果拿不到就让服务端直接输出内联数据或者把图片放到同源静态资源路径下。async function imageToDataURL(src) { // 同源或允许跨域的资源才能走这条链路 const response await fetch(src, { mode: cors, credentials: omit }); if (!response.ok) { throw new Error(图片读取失败: ${response.status}); } const blob await response.blob(); return await new Promise((resolve, reject) { const reader new FileReader(); reader.onload () resolve(reader.result); reader.onerror reject; reader.readAsDataURL(blob); }); } async function inlineImages(root) { const images [...root.querySelectorAll(img)]; await Promise.all(images.map(async (img) { if (!img.complete) { await new Promise((resolve) { img.onload img.onerror resolve; }); } const src img.currentSrc || img.src; if (src !src.startsWith(data:)) { img.src await imageToDataURL(src); } })); }mode: cors表示按跨域规则读取credentials: omit避免携带凭据导致响应头不匹配。img.currentSrc用来处理srcset否则可能拿到低分辨率图。inlineImages要作用在克隆节点上不要直接改原页面否则用户会看到图片闪烁。图片加载失败时不要静默跳过至少打一条日志否则导出后才发现缺图排查成本更高。字体比图片更隐蔽。document.fonts.ready只能表示字体加载完成不代表图标字体在 SVG 里可用。如果页面用了自定义字体最好把字体文件也转成 dataURL写进一段font-face样式再塞进克隆节点。图标字体建议直接换成 SVG 图标避免字体子集缺失导致方框。伪元素::before、::after不会被cloneNode复制依赖伪元素画的角标、分割线、装饰图形要么改成真实节点要么在截图前隐藏。下面这段等待逻辑放在克隆之前确保图片和字体都已就绪。async function waitForAssets(root) { // 字体加载完成再克隆避免文字换行位置变化 await document.fonts.ready; const images [...root.querySelectorAll(img)]; await Promise.all(images.map(async (img) { if (img.complete) return; await new Promise((resolve) { img.onload img.onerror resolve; }); })); }waitForAssets只做等待不替换资源替换放在克隆后用inlineImages处理。这样原页面不受影响克隆节点也能拿到已经加载完成的图片尺寸。注意document.fonts.ready在部分内核里返回很快但字体真正参与布局可能晚一帧稳妥做法是在它之后再等一个requestAnimationFrame。如果页面有懒加载图片先触发滚动或手动调用加载逻辑否则截图里只会看到占位图。2.3 生成 SVG 字符串并画进 Canvas样式和图片就绪后就可以构建 SVG。核心是创建一个svg元素设置和截图区域一致的宽高与viewBox再把克隆节点放进foreignObject。序列化时用XMLSerializer生成 Blob 后通过URL.createObjectURL加载成Image最后drawImage到 Canvas。这里的宽高单位是 CSS 逻辑像素不要和 Canvas 的物理像素混在一起。const SVG_NS http://www.w3.org/2000/svg; function buildSvgBlob(root, width, height) { const clone root.cloneNode(true); inlineComputedStyle(root, clone); clone.style.margin 0; clone.style.transform none; clone.setAttribute(xmlns, http://www.w3.org/1999/xhtml); const svg document.createElementNS(SVG_NS, svg); svg.setAttribute(xmlns, SVG_NS); svg.setAttribute(width, String(width)); svg.setAttribute(height, String(height)); svg.setAttribute(viewBox, 0 0 ${width} ${height}); const fo document.createElementNS(SVG_NS, foreignObject); fo.setAttribute(width, 100%); fo.setAttribute(height, 100%); fo.appendChild(clone); svg.appendChild(fo); const xml new XMLSerializer().serializeToString(svg); return new Blob([xml], { type: image/svgxml;charsetutf-8 }); } async function svgBlobToImage(blob) { const url URL.createObjectURL(blob); const img new Image(); img.src url; await img.decode(); URL.revokeObjectURL(url); return img; }width和height用getBoundingClientRect()取整不要用offsetWidth混用小数否则viewBox和 Canvas 尺寸会对不齐。clone.style.transform none是为了去掉父级缩放截图区域如果本身有缩放动画先等动画结束。img.decode()比监听onload更直接能确保图片解码完成再画。URL.revokeObjectURL要在decode之后调用否则部分内核会把图片资源提前释放表现是画布空白。提示foreignObject里的内容越简单越稳外链样式、远程字体、伪元素装饰能去掉就去掉别把整页复杂布局直接往里塞。画进 Canvas 时先创建画布再设置尺寸最后drawImage。Canvas 的宽高一旦重新赋值上下文状态会被重置所以scale、fillStyle这些要放在设置尺寸之后。下一章会展开像素比和导出参数这里先把绘制链路跑通。3. 用 a 标签触发下载Canvas 截图导出时 Blob 与文件名的参数细节3.1 toBlob 与 toDataURL格式、质量和异步Canvas 导出有两种常见方式toDataURL返回 Base64 字符串toBlob异步返回 Blob。小图预览用toDataURL省事但 Base64 会让体积膨胀约三分之一长图导出时字符串拼接和内存占用都很明显。下载场景优先用toBlob再配合URL.createObjectURL生成临时地址。toBlob是异步回调不能像toDataURL那样直接拿返回值封装成 Promise 更好用。function canvasToBlob(canvas, type image/png, quality 0.92) { return new Promise((resolve, reject) { canvas.toBlob((blob) { if (blob) { resolve(blob); } else { reject(new Error(Canvas 导出失败可能是尺寸超限或画布被污染)); } }, type, quality); }); }type常用image/png、image/jpeg、image/webp不同内核支持程度不一样落地时以 PNG 为兜底。quality只对 JPEG 和 WebP 生效PNG 会忽略这个参数JPEG 常用 0.8 到 0.92再低文字边缘会糊。回调里blob为空通常有两个原因Canvas 面积超过设备上限或者画布里绘制过跨域资源导致不可导出。报错信息要带上这两个方向排查时先看控制台有没有画布安全错误再看画布宽高是不是大得离谱。如果业务必须拿字符串比如塞进隐藏输入域或走接口上传toDataURL也能用但要控制尺寸。常见做法是先用toBlob导出再用FileReader转 dataURL这样至少能复用同一份二进制结果。不要对同一张长图反复调用toDataURL每次都会生成新的大字符串内存回收跟不上就会卡死。3.2 a 标签 download 的兼容写法与文件名下载的核心是创建一个隐藏的a元素设置href为 Blob URLdownload为文件名插入文档后触发click()最后移除并释放 URL。download属性在桌面端 Chromium 内核里表现比较直接部分移动端 WebKit 内核支持不完整点击后可能只打开图片而不下载。遇到这种情况常见兜底是检测能力后改为新窗口打开 Blob URL提示用户长按保存或者让服务端返回带附件头的下载地址。function downloadBlob(blob, filename) { const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename.replace(/[\\/:*?|]/g, _); a.rel noopener; a.style.display none; document.body.appendChild(a); a.click(); a.remove(); // 延迟释放避免部分内核还没开始读取就失效 setTimeout(() URL.revokeObjectURL(url), 1000); }文件名过滤掉\ / : * ? |这些字符否则部分系统会截断或直接失败。后缀要和 MIME 对上image/png配.pngimage/jpeg配.jpg不要出现 Blob 是 PNG 但文件名写.jpg的情况。relnoopener是习惯性写法避免新窗口拿到window.opener。延迟释放 URL 不是必须但能减少“点了没反应”的玄学问题如果页面频繁导出延迟时间不要设太长否则临时 URL 会堆积。移动端还有一个细节a.click()必须发生在用户手势的调用链里。如果前面await了太多异步操作手势上下文可能丢失点击会被拦截。常见做法是先同步打开一个空白窗口或显示加载态等 Blob 生成后再把地址塞进去或者直接提示用户“生成后长按保存”。不要指望所有移动端浏览器都按桌面逻辑处理download。3.3 内存与 URL 生命周期截图导出是内存敏感操作。一张 3000×4000 的 Canvas按 4 字节/像素算就是 48MB 左右再叠加 SVG 字符串、Blob、Base64 字符串和临时 URL很容易把移动端内存打满。每导出一张图至少释放三样东西SVG 的 Object URL、下载用的 Object URL、Canvas 的宽高。Canvas 宽高置 0 能尽快让内核回收绘图缓冲区比只把变量置 null 更直接。方式返回值体积特征适用场景toBlobBlob异步接近原始二进制大图下载、上传toDataURLBase64 字符串约膨胀三分之一小图预览、内联展示表格里的体积特征是经验值实际和图片内容有关。纯色背景的 PNG 压缩率很高复杂渐变和文字多的截图会明显变大。导出前先评估目标区域尺寸超过常见阈值就降像素比或分片处理不要等浏览器白屏了再补。URL.revokeObjectURL的调用时机要跟下载节奏配合。桌面端点击后立即释放通常也能下载但部分内核开始读取 Blob 有延迟释放太早会得到空文件。延迟 1000ms 是折中值如果文件很大可以监听a的点击后再等一会儿。更好的做法是把下载封装成“生成 Blob、触发下载、延迟释放”三步调用方只关心文件名和格式。4. 截图质量与像素比Canvas 截图里 devicePixelRatio 与缩放怎么设4.1 逻辑像素、物理像素与 Canvas 尺寸页面布局用的是 CSS 逻辑像素屏幕实际渲染用的是物理像素两者之间隔着devicePixelRatio。如果 Canvas 宽高直接等于getBoundingClientRect()的宽高导出图在高分屏上会显得发虚文字边缘不够锐利。常见做法是把 Canvas 实际宽高乘以像素比再用ctx.scale(dpr, dpr)把绘制坐标拉回逻辑像素。这样代码里仍然按 CSS 尺寸画输出图却拿到了更多像素。function createSnapshotCanvas(width, height, maxDpr 2) { const dpr Math.min(window.devicePixelRatio || 1, maxDpr); const canvas document.createElement(canvas); canvas.width Math.round(width * dpr); canvas.height Math.round(height * dpr); const ctx canvas.getContext(2d); ctx.scale(dpr, dpr); ctx.imageSmoothingEnabled true; ctx.imageSmoothingQuality high; return { canvas, ctx, dpr }; }width和height传逻辑像素maxDpr是上限常用 2 或 3。不要直接放开到设备真实像素比某些移动设备报告 3 甚至更高长图面积会翻几倍。ctx.scale之后drawImage(img, 0, 0, width, height)仍然按逻辑尺寸画不需要手动乘 dpr。imageSmoothingQuality在部分内核里不生效但有比没有好如果导出的是文字为主的长图可以先用 2 倍 dpr 试再根据文件体积降级。像素比不是越高越好。导出图最终是给人看还是给机器识别决定参数取向分享图 2 倍足够打印物料才考虑 3 倍后台留档如果只是存档1.5 倍甚至 1 倍都能接受。把 dpr 做成可配参数别写死在函数里后面调优会省很多事。4.2 长图截取的分段绘制与尺寸上限长报表、长列表截图最容易遇到 Canvas 尺寸上限。不同内核的上限不一样常见做法是把单边控制在 4096 到 8192 像素以内超过就降 dpr 或分段导出。分段绘制的思路是把 SVG 图片按纵向切成若干块每块画进同一个 Canvas每画完一块让出一帧降低主线程长时间阻塞。它不能突破 Canvas 总尺寸上限但能减少瞬时内存压力也能让页面在导出过程中保持响应。async function drawSvgInTiles(ctx, img, width, height, tileHeight 2048) { for (let y 0; y height; y tileHeight) { const h Math.min(tileHeight, height - y); ctx.drawImage(img, 0, y, width, h, 0, y, width, h); // 每块让出一帧避免长任务卡死页面 await new Promise((resolve) requestAnimationFrame(resolve)); } }drawImage九参数版本里前四个是源图裁剪区域后四个是目标区域。tileHeight按设备内存调桌面端可以 2048 到 4096移动端建议 1024 到 2048。分段绘制时目标 Canvas 总高度仍然是完整高度所以总尺寸上限还在如果总高度超过上限优先降 dpr再把最终图缩放回目标尺寸。不要为了追求清晰度硬扛导出失败比稍微糊一点更影响使用。如果业务允许长图可以拆成多张下载比如每 5000 像素一张文件名加序号。这样单张图不超限用户也好打开。拆图时注意重叠区域避免表格行被切断常见做法是按固定高度切但在切点附近找最近的分隔线或行边界找不到就保留固定切点。4.3 导出质量与文件体积的平衡PNG 无损、支持透明适合文字、表格、带透明背景的分享图JPEG 体积小但不支持透明文字边缘容易出现压缩噪点WebP 体积和质量的平衡更好但部分老内核不支持导出。截图里如果有大面积纯色背景PNG 往往比 JPEG 更小如果是照片或复杂渐变JPEG 0.85 左右更合适。导出前先判断目标区域有没有透明背景有就选 PNG没有可以试 JPEG。JPEG 导出前要先铺底色。Canvas 默认是透明黑直接导 JPEG 会把透明区域变成黑色看起来像截图坏了。常见做法是在drawImage之前用白色填充整个画布再画内容。如果设计稿要求其他底色把颜色做成参数传进去。function prepareJpegBackground(ctx, width, height, color #ffffff) { ctx.save(); ctx.fillStyle color; ctx.fillRect(0, 0, width, height); ctx.restore(); }ctx.save()和restore()包住填充避免影响后续绘制状态。width、height用逻辑像素因为上下文已经按 dpr 缩放过。填充要在画 SVG 之前否则会把内容盖住。这个细节看起来小但 JPEG 导出黑底是高频翻车点尤其是分享图带圆角或透明边距时。注意导出格式要和下载文件后缀一致JPEG 不铺底会得到黑底图PNG 强行铺底又会丢掉透明区域先确认业务要不要透明。5. 避坑与排查五个真实翻车现场5.1 截图里图片是空白方块现象页面预览正常导出的 PNG 里头像、商品图、图表全部变成空白控制台没有明显报错。原因通常是克隆节点里的img还是外链地址foreignObject序列化后没有把图片二进制带进去或者图片走了srcset克隆后取到的地址和当前显示的不是同一张。解决分两步先在克隆后执行inlineImages把所有img转成 dataURL再检查currentSrc不要只读src。跨域图片拿不到时让服务端直接输出内联数据或者把资源放到同源静态路径下别在导出时才发现读不了。还有一种情况是图片加载完成但尺寸为 0。页面用 CSS 给图片设了宽高克隆节点只复制了width、height属性没有复制计算样式SVG 里图片就按原始尺寸撑开或塌缩。解决是把width、height、object-fit一起写进内联样式并在替换src后等一次decode()。如果图片是背景图foreignObject对内联背景图的支持更差能改成img就改改不了就在截图前把背景图区域单独绘制。5.2 Canvas 被污染导致 toBlob 抛错现象调用toBlob时回调收到null控制台出现画布安全错误提示无法导出。原因是 Canvas 里绘制过跨域资源且该资源没有允许跨域读取SVG 里引用了外部 URL 也会触发同样问题。解决是保证进入 Canvas 的所有图片都是 dataURL 或同源 Blob URLfetch图片时带上mode: cors并确认响应头允许当前页面读取。只在img上设置crossOriginanonymous不够资源端不配合照样污染。排查时先把 SVG 字符串打印出来搜索http开头的src和href有一个就说明没内联干净。样式里的background-image: url(...)也要查很多人只处理了img忘了背景图。实在拿不到跨域资源就在截图前把该区域替换成占位图或者让业务方提供可导出的图片地址。不要试图用toDataURL绕过画布一旦被污染两种导出方式都会失败。5.3 移动端点击下载没反应现象桌面端点按钮正常下载移动端点完没有任何反应或者只在新窗口打开图片。原因是部分移动端 WebKit 内核对download属性支持不完整Blob URL 在新窗口里也可能被拦截。解决是先做能力检测支持download就走 a 标签下载不支持就打开图片并提示长按保存或者调用系统分享能力把文件交给用户。不要在异步链里隔太久才触发点击用户手势上下文丢失后新窗口会被拦截。常见兜底是生成 Blob 后先显示一个预览层把图片放进去让用户长按保存。这个方案体验不如直接下载但比点击没反应好。另一个方向是让服务端返回带附件头的下载地址前端只负责跳转这条链路适合文件较大或格式固定的场景。无论哪种方案都要在真机测不要只看桌面模拟器。5.4 字体错乱、行高偏移现象截图里的文字换行位置和页面不一致标题被截断图标字体变成方框。原因是foreignObject里的字体没有随 SVG 一起内联或者字体还没加载完就克隆了节点。解决是等待document.fonts.ready再把字体文件转成 dataURL 写进font-face至少要把font-family、font-size、line-height、letter-spacing复制到克隆节点。图标字体尽量换成 SVG字体子集缺字时很难靠样式补救。行高偏移通常和box-sizing、line-height的继承有关。页面里用了line-height: 1.5克隆节点如果只复制了font-size没复制line-height文字占位就会变。解决是把line-height计算后的值一起写进内联样式。表格场景还要注意border-collapse和table-layout缺一个就可能出现列宽错位。5.5 大页面截图内存暴涨或直接白屏现象长报表导出时页面卡死进度条走完后 Canvas 变成空白或者浏览器直接提示页面无响应。原因是 Canvas 面积超过设备上限或者同时保留了 SVG 字符串、Blob、dataURL 和多个临时 URL。解决是限制maxDpr长图分段绘制导出后把 Canvas 宽高置 0并及时revokeObjectURL。不要在一个页面里连续导出多张长图中间至少让用户触发一次或强制等待一帧。排查时先看画布尺寸宽高乘起来超过几千万像素就要警惕。再看是否重复创建 Canvas每次导出都新建一个且不释放内存会一路涨。常见做法是复用同一个离屏 Canvas导出完成后清空宽高如果业务要求并发导出给并发数设上限比如同时只跑一个任务。白屏不一定是代码错很多时候就是内存到顶了。6. 把截图做成可复用能力封装 downloadSnapshot 与验证清单把前面的步骤串成一个函数调用方只需要传根节点、文件名和少量参数。下面这版是精简封装依赖前文的inlineComputedStyle、inlineImages、buildSvgBlob、createSnapshotCanvas、canvasToBlob、downloadBlob实际项目里把这些函数放在同一个模块即可。async function downloadSnapshot(root, filename snapshot.png, options {}) { const { type image/png, quality 0.92, maxDpr 2 } options; const rect root.getBoundingClientRect(); const width Math.ceil(rect.width); const height Math.ceil(rect.height); await document.fonts.ready; const clone root.cloneNode(true); inlineComputedStyle(root, clone); await inlineImages(clone); const svgBlob buildSvgBlob(clone, width, height); const svgUrl URL.createObjectURL(svgBlob); const img new Image(); img.src svgUrl; await img.decode(); const { canvas, ctx } createSnapshotCanvas(width, height, maxDpr); if (type image/jpeg) { ctx.fillStyle #ffffff; ctx.fillRect(0, 0, width, height); } ctx.drawImage(img, 0, 0, width, height); URL.revokeObjectURL(svgUrl); const blob await canvasToBlob(canvas, type, quality); downloadBlob(blob, filename); canvas.width canvas.height 0; }参数类型说明rootHTMLElement要截图的根节点宽高由getBoundingClientRect决定filenamestring下载文件名后缀和type保持一致typestring默认image/png照片类内容可换image/jpegqualitynumber0 到 1仅 JPEG/WebP 生效常用 0.85 到 0.92maxDprnumber像素比上限移动端建议 2长图可降到 1.5调用时先确保根节点在视口内或至少已完成布局隐藏区域要先临时显示再截图。验证清单我一般跑三步先截一个纯文字卡片看字体和换行再截一个带跨域图片的卡片看是否空白或报安全错误最后在移动端真机点一次下载看是否触发保存或需要长按。三步都过再上长图和 JPEG 格式。从那以后我每次接截图需求都先把跨域图片、字体加载、移动端下载这三件事写成检查项跑不通就不往下写。希望帮到你。本文还有配套的精品资源点击获取