ARTICLE DETAIL

资讯详情

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

【vue】前端实现批量导出excel并打包成ZIP:TaoToken统一Key通道下的工程化落地

【vue】前端实现批量导出excel并打包成ZIP:TaoToken统一Key通道下的工程化落地 1. 批量导出 Excel 打包 ZIP 的真实业务场景与踩坑点在 Vue 后台管理系统里批量导出 Excel 并打包成 ZIP 是一个高频需求。典型场景是运营同学点一下「导出全部报表」前端需要把几十个甚至上百个数据表分别生成 xlsx再统一压缩成一个 zip 下载。这个链路听起来简单但真正落地时会遇到几个绕不开的问题数据量大了浏览器直接卡死、多个 sheet 的模板结构不一致、xlsx 生成的 Blob 传给 JSZip 时二进制被破坏、下载触发后内存没释放导致页面越来越慢。我试过最原始的写法循环里同步调用XLSX.write然后saveAs结果导出 30 个文件时浏览器直接无响应。后来改成异步分片 递归调度才把主线程让出来。另一个坑是script-loader引入xlsx.core.min时路径写错控制台报XLSX is not defined排查了半天才发现是 Blob.js 的相对路径问题。这篇文章聚焦 Vue 项目中「批量导出 Excel 并打包 ZIP」的完整工程化落地覆盖数据分片、xlsx 生成、JSZip 压缩与浏览器下载触发。我会给出可复制的 composable 封装、依赖版本锁定与本地验证步骤并说明如何通过 TaoToken 统一 Key/API 通道管理导出服务调用。目标是一次跑通多表导出与压缩下载而不是停留在「思路可行」的层面。适合谁看正在做 Vue 中后台、需要批量导出报表的前端同学已经用过xlsx和jszip但被大数据量卡住的人想把导出逻辑从组件里抽成 composable 的工程化实践者。核心检索词就是「Vue 批量导出 Excel 打包 ZIP」下面所有步骤都围绕这个场景展开。先说结论整个链路的关键不在 xlsx 生成而在「分片调度」和「Blob 传递」。xlsx 的XLSX.write返回的是 binary string必须经过s2ab转成 ArrayBuffer 再包成 Blob否则 JSZip 拿到的数据是坏的解压出来的 Excel 打不开。这个细节后面会用代码展开。2. TaoToken 统一 Key 通道在导出服务中的前置准备批量导出往往不只是纯前端计算很多团队会把「数据聚合」或「报表模板渲染」放到服务端前端通过 API 拉取分片数据。这时候就会遇到多服务、多 Key 的管理问题导出服务一个 Key、报表服务一个 Key、AI 辅助生成表头又是一个 Key散落在各个.env文件里换环境就炸。TaoToken 在这里的角色是统一 Key/API 通道。你可以把它理解成一个「钥匙串」所有模型调用和 API 请求走同一个 Base URL用同一个 Key 鉴权切换模型只改 Model ID。对于导出服务来说如果报表里需要 AI 生成摘要列、或者需要调用模型做数据清洗就不用再单独维护一套鉴权逻辑。前置准备分三步。第一步拿到统一 Key。访问 API Keys 管理页创建https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建后复制 Key格式通常是sk-开头。第二步确认 Base URL。所有请求走https://taotoken.net/api注意这个地址不加 UTM 参数直接作为baseURL使用。第三步确定 Model ID。如果你只是用导出服务做数据聚合不涉及模型可以跳过但如果要在导出前调用模型生成表头描述就需要指定模型。可以在模型对话页先验证模型可用性https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite在对话页里选一个模型发一条消息确认返回正常再把这个 Model ID 写进配置。这样做的目的是避免「Key 有了但模型名写错」导致 404。对于长期做编码和 Agent 场景的团队如果导出服务需要频繁调用模型做批量处理可以考虑 Coding Plan它更适合高频、长周期的调用https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite前置准备的核心是「一个 Key 一个 Base URL 一个 Model ID」三件套。后面在 §3 的配置片段里我会把这三件套写进.env和settings.json确保路径和原文一致方便你直接复制。3. 可复制的 Vue composable 封装与依赖配置这一节是全文的技术核心。我会给出完整的package.json依赖锁定、useExportZipcomposable 封装、以及Export2Excel.js的关键函数。所有代码都可以直接复制到项目里跑。先锁定依赖版本。这是经过验证的组合版本不匹配会导致XLSX.write返回类型异常{ dependencies: { file-saver: ^2.0.5, jszip: ^3.7.1, xlsx: ^0.17.0 }, devDependencies: { less: ^3.0.4, less-loader: ^5.0.0, script-loader: ^0.7.2 } }注意xlsx用0.17.0不要升到0.18后者对bookSST的处理有变化会导致多 sheet 导出时样式丢失。jszip用3.7.13.8的generateAsync在部分浏览器上有兼容问题。接下来是Export2Excel.js的核心函数。这个文件负责把二维数组转成 xlsx 的 Blob/* eslint-disable */ require(script-loader!file-saver); require(./Blob.js); require(script-loader!xlsx/dist/xlsx.core.min); function s2ab(s) { var buf new ArrayBuffer(s.length); var view new Uint8Array(buf); for (var i 0; i ! s.length; i) view[i] s.charCodeAt(i) 0xFF; return buf; } function sheet_from_array_of_arrays(data) { var ws {}; var range { s: { c: 10000000, r: 10000000 }, e: { c: 0, r: 0 } }; for (var R 0; R ! data.length; R) { for (var C 0; C ! data[R].length; C) { if (range.s.r R) range.s.r R; if (range.s.c C) range.s.c C; if (range.e.r R) range.e.r R; if (range.e.c C) range.e.c C; var cell { v: data[R][C] }; if (cell.v null) continue; var cell_ref XLSX.utils.encode_cell({ c: C, r: R }); if (typeof cell.v number) cell.t n; else if (typeof cell.v boolean) cell.t b; else if (cell.v instanceof Date) { cell.t n; cell.z XLSX.SSF._table[14]; cell.v datenum(cell.v); } else cell.t s; ws[cell_ref] cell; } } if (range.s.c 10000000) ws[!ref] XLSX.utils.encode_range(range); return ws; } function Workbook() { if (!(this instanceof Workbook)) return new Workbook(); this.SheetNames []; this.Sheets {}; } export function export_blob(th, jsonData) { var data jsonData; data.unshift(th); var ws_name 自定义的sheet; var wb new Workbook(), ws sheet_from_array_of_arrays(data); wb.SheetNames.push(ws_name); wb.Sheets[ws_name] ws; var wbout XLSX.write(wb, { bookType: xlsx, bookSST: false, type: binary }); return new Blob([s2ab(wbout)], { type: application/octet-stream }); }关键点export_blob返回的是 Blob不是直接 saveAs。这样 JSZip 才能拿到二进制数据。如果你直接saveAs就变成单个文件下载没法打包。然后是useExportZipcomposable。它把分片调度、JSZip 压缩、下载触发封装在一起import { ref } from vue; import JSZip from jszip; import { export_blob } from ./Export2Excel; export function useExportZip() { const loading ref({ type: false, text: }); const paths ref([]); const pathName ref([]); function formatJson(filterVal, jsonData) { return jsonData.map(v filterVal.map(j v[j])); } function exportOne(data, index, zipName, deepIndex, muBan, zipNames) { const dataKey Object.keys(data); const key dataKey[index]; if (!key) return; let tempObject {}; let tempArray []; const dataLength data[key].length; let filterVal []; data[key][0].forEach((pEle, pIndex) { for (let i 0; i dataLength; i) { tempObject[String(i)] data[key][i][pIndex]; } tempArray.push(tempObject); tempObject {}; }); data[key][0].forEach((ele, idx) filterVal.push(idx)); paths.value.push(export_blob(muBan, formatJson(filterVal, tempArray))); pathName.value.push(key); index; setTimeout(() { exportOne(data, index, zipName, deepIndex, muBan, zipNames); }, 1000); if (dataKey.length index) { zipOut(paths.value, pathName.value, zipName, deepIndex, zipNames); paths.value []; pathName.value []; } } function zipOut(paths, pathName, zipName, deepIndex, zipNames) { const zip new JSZip(); paths.forEach((item, index) { const file_name pathName[index] .xlsx; zip.file(file_name, item, { binary: true }); }); zip.generateAsync({ type: blob }).then(function (content) { const filename zipName .zip; const eleLink document.createElement(a); eleLink.download filename; eleLink.style.display none; eleLink.href URL.createObjectURL(content); document.body.appendChild(eleLink); eleLink.click(); document.body.removeChild(eleLink); if (deepIndex zipNames.length deepIndex) { loading.value.type false; } }); } function allExport(data, muBan, zipNames) { loading.value { type: true, text: 正在合成压缩包请耐心等待... }; exportOne(data, 0, zipNames[0], 1, muBan, zipNames); } return { loading, allExport }; }这个 composable 的核心是setTimeout分片。每生成一个 Excel 就等 1 秒让浏览器有机会处理其他任务。如果数据量特别大可以把 1000 调到 2000 甚至 3000。踩过的坑是不要用for循环同步生成否则 50 个文件就能让页面卡死。在组件里调用template div button clickhandleExport导出全部 ZIP/button div v-ifloading.type classloading{{ loading.text }}/div /div /template script setup import { useExportZip } from ./composables/useExportZip; const data [ { 黑猫01: [[2021-09-15, 2021-09-16], [7.62, 4.43]] }, { 白猫01: [[2021-09-15, 2021-09-16], [5.88, 3.87]] } ]; const muBan [时间, 贮藏]; const zipNames [zip黑猫, zip白猫]; const { loading, allExport } useExportZip(); function handleExport() { allExport(data, muBan, zipNames); } /script style scoped .loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; background: rgba(0, 0, 0, 0.6); color: white; font-size: 25px; } /style如果你需要多 sheet 导出export_more_blob的逻辑类似只是把多个 sheet 塞进同一个 workbook。关键区别是ws_name数组和wb.SheetNames.push的循环。4. 验证请求与成功结果本地跑通多表导出压缩下载配置写完后必须本地验证。这一节给出完整的验证步骤和预期结果确保你一次跑通。第一步启动项目。确认package.json里的依赖已安装npm install npm run serve如果控制台报XLSX is not defined检查Export2Excel.js里的require(script-loader!xlsx/dist/xlsx.core.min)路径是否正确。xlsx包安装后dist/xlsx.core.min.js应该存在。第二步准备测试数据。用 §3 里的data数组包含两个对象每个对象里有多个 sheet 数据。点击「导出全部 ZIP」按钮。第三步观察 loading 状态。按钮点击后页面应该出现半透明遮罩文字显示「正在合成压缩包请耐心等待...」。每生成一个 Excel控制台不会有报错。如果数据量大你会看到遮罩持续几秒到几十秒。第四步检查下载结果。浏览器应该自动下载一个zip黑猫.zip或你定义的 zipName。解压后里面应该有多个.xlsx文件文件名对应pathName里的 key比如黑猫01.xlsx、黑猫02.xlsx。第五步打开 Excel 验证内容。每个 xlsx 的第一行应该是muBan表头后续行是数据。如果打开时提示「文件格式无效」说明 Blob 传递出了问题回到 §3 检查s2ab和type: application/octet-stream。如果你在导出服务里调用了 TaoToken 的 API 做数据聚合可以用 curl 验证通道是否通curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d { model: 你的ModelID, messages: [{role: user, content: 生成一个报表表头}] }预期返回是标准的 JSON包含choices数组。如果返回 401说明 Key 无效如果返回 404说明 Model ID 写错。验证通过后把同样的 Base URL 和 Key 写进前端.envVITE_TAOTOKEN_BASE_URLhttps://taotoken.net/api VITE_TAOTOKEN_API_KEYsk-你的Key VITE_TAOTOKEN_MODEL_ID你的ModelID这样导出服务在需要调用模型时直接读环境变量即可不用硬编码。成功结果的标志ZIP 下载完成、解压后 Excel 能正常打开、表头和数据行对齐、多 sheet 文件里每个 sheet 名称正确。如果这四点都满足说明整条链路跑通了。5. 本篇常见错误排查401、local proxy failed、reading choices、OAuth这一节对照真实报错给出排查路径。这些错误我在不同项目里都遇到过按顺序排查基本能解决。错误一401 Unauthorized现象调用 TaoToken API 时返回{error: {message: Invalid API key}}。排查检查.env里的VITE_TAOTOKEN_API_KEY是否以sk-开头是否有多余空格。如果 Key 是从 API Keys 页面复制的确认没有复制到换行符。另外检查请求头是不是Authorization: Bearer sk-xxx少了Bearer也会 401。错误二local proxy failed现象前端请求报net::ERR_CONNECTION_REFUSED或local proxy failed。排查这通常是开发环境的代理配置问题。检查vue.config.js里的devServer.proxy确认/api转发到了https://taotoken.net。如果你用的是 Vite检查vite.config.js的server.proxy。注意 Base URL 不要带 UTM 参数直接写https://taotoken.net/api。错误三reading choices现象TypeError: Cannot read properties of undefined (reading choices)。排查这说明返回的 JSON 结构不对。可能是 Model ID 写错导致返回了错误对象也可能是请求体格式不对。先用 curl 单独测一次确认返回里有choices字段。如果 curl 正常但前端报错检查前端是不是把response.data和response搞混了。错误四OAuth 相关报错现象OAuth token expired或invalid_grant。排查如果你用的是 Claude Code 或类似工具OAuth 过期需要重新授权。检查~/.claude/settings.json或项目里的settings.json确认baseURL和apiKey配置正确。对于 Codex 的auth.json确认OPENAI_API_KEY字段填的是 TaoToken 的 Key而不是其他平台的。错误五ZIP 解压后 Excel 打不开现象下载的 zip 能解压但 xlsx 打开提示格式错误。排查这是 Blob 类型问题。检查export_blob返回的 Blob 类型是不是application/octet-stream。如果用了text/plainJSZip 会把二进制当文本处理导致文件损坏。另外确认zip.file(file_name, item, { binary: true })里的binary: true没漏。错误六大数据量页面卡死现象点击导出后页面无响应几秒后浏览器提示「页面无响应」。排查检查分片调度的setTimeout间隔。如果数据量超过 50 个文件把 1000ms 调到 2000ms 或 3000ms。另外确认没有在循环里同步调用XLSX.write每次生成后必须让出主线程。如果以上都排查完还有问题可以去接入文档页对照最新配置https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite文档里有完整的 Base URL、Key 格式和 Model ID 列表比在代码里猜要快得多。6. 语义一致 CTA把导出链路沉淀为可复用的工程能力批量导出 Excel 打包 ZIP 这件事表面上是「生成文件 压缩」实际上考验的是前端对大数据量调度的控制力。分片、异步、Blob 传递、内存释放每一个环节出问题都会让整个链路失败。把这套逻辑封装成 composable 之后你在任何 Vue 项目里都能复用不用每次重新踩坑。如果你在导出服务里需要调用模型做数据清洗、表头生成或摘要列建议把 Key 管理统一到 TaoToken。一个 Key 走所有模型切换只改 Model ID省去多套鉴权的维护成本。验证模型是否可用直接去模型对话页发一条消息https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite长期做编码和 Agent 场景的团队如果导出服务需要高频调用模型Coding Plan 更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite需要管理多个 Key 或查看调用量去 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite配置过程中遇到报错先对照接入文档排查https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite最后给一个实用技巧在zipOut里加一个URL.revokeObjectURL下载触发后释放 Blob 地址避免内存泄漏。这个细节在批量导出场景里很重要尤其是用户连续点击多次导出时不释放会导致内存持续增长。代码里加一行setTimeout(() URL.revokeObjectURL(eleLink.href), 1000);这样整条链路从数据分片到下载释放就完整了。
返回列表