尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Javascript导出Excel实战:自动合并单元格、自动列宽与进度条,配 TaoToken 统一 Key 通道

Javascript导出Excel实战:自动合并单元格、自动列宽与进度条,配 TaoToken 统一 Key 通道 1. 为什么纯前端导出 Excel 总在合并单元格上翻车Javascript 导出 Excel 这件事看起来只是把表格数据塞进文件真做起来坑集中在三处合并单元格错位、列宽全靠手调、导出大表时页面像卡死一样没有任何反馈。尤其是合并单元格只要表格里出现rowspan或colspan用「按行按列顺序写值」的朴素思路就会整体错位导出的文件打开一看标题跑到第二列、合计行串行用户第一反应就是「这功能不能用」。这篇聚焦的场景很明确前端纯 JS 导出 Excel用 SheetJS 处理合并单元格与自动列宽用进度条反馈导出状态并且把整条链路接到 TaoToken 统一 Key 通道上做验证。适合谁适合正在写后台管理系统、报表页、数据看板的前端同学尤其是被rowspan折磨过、又不想引入重型后端导出服务的人。目标是一次跑通带进度条的 Excel 导出代码可以直接复制进项目改字段名就能用。我会先讲清楚合并单元格和列宽的计算逻辑再给出可复制的导出函数骨架最后演示怎么在settings.json里配置 TaoToken 的 API 通道验证导出链路是通的。整个过程不需要后端参与浏览器里就能完成。2. TaoToken 前置统一 Key 通道解决什么问题在讲导出代码之前先说清楚为什么要把导出链路和 TaoToken 放在一起。很多团队的导出功能不是孤立的它往往还要配合一个「智能填表」「数据摘要」「字段翻译」之类的 AI 能力比如导出前让模型把备注列润色一下或者根据表头自动生成一列分析结论。这时候如果每个功能各自维护一套 Key、各自写一套请求封装配置会散落在十几个文件里换环境时非常痛苦。TaoToken 在这里的角色是统一 Key 通道把模型调用的入口收敛到一个 API 地址Key 集中管理前端只需要认一个配置项。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置时别把推广参数拼进去。你需要提前准备的东西不多一个可用的 Key以及项目里能读写配置文件的环境。Key 在控制台生成地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 生成后复制保存后面写进settings.json。如果你只是想先验证模型通道是否通可以直接用模型对话页测试 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。注意Key 属于敏感信息不要硬编码进前端打包产物。本文的settings.json方案适合本地开发和内网工具生产环境建议走服务端代理转发。3. 可复制配置SheetJS 导出函数骨架3.1 安装依赖与基础数据结构先装 SheetJS社区版就够用npm install xlsx核心思路是把 DOM 表格解析成一个二维数组同时记录每个单元格的合并信息最后交给 SheetJS 的ws[!merges]和ws[!cols]处理。先定义导出函数的骨架import * as XLSX from xlsx; /** * 导出表格为 Excel * param {HTMLTableElement} tableEl 目标表格 * param {Object} options 配置项 */ export async function exportTableToExcel(tableEl, options {}) { const { fileName export.xlsx, sheetName Sheet1, onProgress () {}, autoWidth true, minWidth 8, maxWidth 50, } options; const rows Array.from(tableEl.rows); const total rows.length; const data []; const merges []; const colWidths []; // 逐行解析进度条按行推进 for (let r 0; r total; r) { const row rows[r]; const rowData []; let c 0; for (const cell of row.cells) { rowData[c] cell.innerText.trim(); const rowSpan cell.rowSpan || 1; const colSpan cell.colSpan || 1; if (rowSpan 1 || colSpan 1) { merges.push({ s: { r, c }, e: { r: r rowSpan - 1, c: c colSpan - 1 }, }); } // 列宽按内容长度估算 const len Math.max( String(rowData[c]).length * 2, minWidth ); colWidths[c] Math.min( Math.max(colWidths[c] || 0, len), maxWidth ); c colSpan; } data.push(rowData); onProgress(Math.round(((r 1) / total) * 100)); // 让出主线程避免进度条卡住 if (r % 50 0) await new Promise((res) setTimeout(res, 0)); } const ws XLSX.utils.aoa_to_sheet(data); ws[!merges] merges; if (autoWidth) { ws[!cols] colWidths.map((w) ({ wch: w })); } const wb XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, sheetName); XLSX.writeFile(wb, fileName); return { rows: total, merges: merges.length }; }这段代码的关键点有三个。第一c colSpan而不是c这样合并列后面的单元格索引才不会错位。第二merges数组用的是 SheetJS 的{s, e}结构s是起始行列e是结束行列行列都从 0 开始。第三每 50 行await一次setTimeout把主线程让出去进度条才有机会刷新否则 UI 会一直冻结到导出结束。3.2 合并单元格的规则配置实际业务里合并规则往往不是「照搬 DOM」而是有业务含义的。比如同一订单的多行明细订单号列要纵向合并表头分组要横向合并。我建议把合并规则抽成配置而不是写死在解析循环里const mergeRules [ // 第 0 行整行合并大标题 { type: row, row: 0, from: 0, to: 5 }, // 第 1 行前两列横向合并分组表头 { type: col, row: 1, from: 0, to: 1 }, // 按字段值纵向合并orderNo 相同的连续行合并 { type: vertical, field: orderNo, col: 0 }, ]; function buildMerges(data, rules) { const merges []; for (const rule of rules) { if (rule.type row) { merges.push({ s: { r: rule.row, c: rule.from }, e: { r: rule.row, c: rule.to }, }); } else if (rule.type col) { merges.push({ s: { r: rule.row, c: rule.from }, e: { r: rule.row, c: rule.to }, }); } else if (rule.type vertical) { let start 0; for (let i 1; i data.length; i) { const prev data[i - 1]?.[rule.col]; const curr data[i]?.[rule.col]; if (curr ! prev) { if (i - start 1) { merges.push({ s: { r: start, c: rule.col }, e: { r: i - 1, c: rule.col }, }); } start i; } } } } return merges; }纵向合并的逻辑是「找连续相同值」注意i data.length这个边界最后一段合并要在循环结束时补上。这个规则配置的好处是业务变了只改mergeRules数组解析逻辑不用动。3.3 自动列宽的计算细节SheetJS 的!cols用wch表示字符宽度中文按 2 个字符估算比较接近实际。上面骨架里已经做了length * 2的估算但有两个细节要补一是表头通常比数据短但视觉上希望表头不被压缩所以列宽要取「表头长度」和「数据最大长度」的较大值二是要设上下限太窄看不清太宽打印会溢出。function calcColWidths(data, headerRowIndex 0, min 8, max 50) { const widths []; for (let r 0; r data.length; r) { const row data[r]; for (let c 0; c row.length; c) { const text String(row[c] ?? ); // 中文按 2 字符宽度估算 const len text.replace(/[^\x00-\xff]/g, aa).length; const weight r headerRowIndex ? 1.2 : 1; widths[c] Math.max(widths[c] || 0, len * weight); } } return widths.map((w) ({ wch: Math.min(Math.max(Math.ceil(w), min), max), })); }把calcColWidths的结果赋给ws[!cols]打开 Excel 时列宽就是自适应的不用手动拖。4. 验证请求进度条与导出链路跑通4.1 进度条实现进度条不要用假动画要真实反映解析进度。上面骨架里的onProgress回调按行数百分比触发UI 层这样接div idprogressWrap styledisplay:none div idprogressBar stylewidth:0%;height:8px;background:#2f6fed/div span idprogressText0%/span /divconst wrap document.getElementById(progressWrap); const bar document.getElementById(progressBar); const text document.getElementById(progressText); async function handleExport() { wrap.style.display block; const table document.getElementById(dataTable); const result await exportTableToExcel(table, { fileName: 订单明细.xlsx, onProgress: (p) { bar.style.width p %; text.textContent p %; }, }); wrap.style.display none; console.log(导出完成${result.rows} 行${result.merges} 处合并); }实测下来几千行的表格进度条会平滑推进不会出现「点一下卡三秒然后突然完成」的情况关键就是那个setTimeout让出主线程。4.2 配置 TaoToken 统一 Key 通道导出功能本身不依赖网络但如果你要在导出前调用模型做数据加工就需要配置通道。在项目根目录建settings.json{ taotoken: { apiBase: https://taotoken.net/api, apiKey: sk-你的Key, model: claude-sonnet-4-5, timeout: 30000 }, export: { autoWidth: true, minWidth: 8, maxWidth: 50, progressStep: 50 } }读取配置并封装一个最小请求函数import settings from ./settings.json; async function callModel(prompt) { const { apiBase, apiKey, model, timeout } settings.taotoken; const controller new AbortController(); const timer setTimeout(() controller.abort(), timeout); try { const res await fetch(${apiBase}/v1/messages, { method: POST, headers: { Content-Type: application/json, x-api-key: apiKey, anthropic-version: 2023-06-01, }, body: JSON.stringify({ model, max_tokens: 1024, messages: [{ role: user, content: prompt }], }), signal: controller.signal, }); if (!res.ok) throw new Error(HTTP ${res.status}); const json await res.json(); return json.content?.[0]?.text ?? ; } finally { clearTimeout(timer); } }4.3 验证导出链路可用写一个最小的验证脚本确认「模型通道通 导出函数可用」async function verifyPipeline() { // 1. 验证模型通道 const reply await callModel(只回复两个字正常); console.log(模型通道返回, reply); // 2. 验证导出函数 const table document.getElementById(dataTable); const result await exportTableToExcel(table, { fileName: verify.xlsx, onProgress: (p) console.log(进度, p %), }); console.log(导出结果, result); }成功时控制台会先打印模型返回的「正常」再逐行打印进度最后输出导出的行数和合并数浏览器自动下载verify.xlsx。打开文件检查三件事大标题是否跨列居中、订单号列是否纵向合并、列宽是否自适应。三项都对链路就算跑通了。如果你需要长期在编码场景里用这套通道比如让模型帮你生成导出字段映射可以了解 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Key 管理入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。5. 本篇常见错排查5.1 合并单元格错位最常见的现象是「合并后数据整体右移一列」。原因几乎都是解析时用了c而不是c colSpan。合并列占了几列后续单元格的列索引就要跳过几列否则 SheetJS 会把值写到错误的列上。排查方法在解析循环里打印每行的c变化对照 DOM 的colSpan看是否一致。另一个坑是merges的e.c算错。结束列应该是c colSpan - 1不是c colSpan。差一位就会多合并一列把旁边的数据吞掉。5.2 进度条不动或卡死进度条不动通常是两个原因。一是解析循环里没有await让出主线程浏览器把整个循环当成一个任务执行完才渲染进度条自然看不到中间状态。解决办法就是每 N 行await new Promise(res setTimeout(res, 0))。二是onProgress回调里直接改了 DOM 但没触发重排确认bar.style.width赋值后浏览器有渲染机会配合上面的让出主线程即可。5.3 列宽异常列宽要么全一样宽要么某列特别宽。全一样宽通常是!cols没赋值或者calcColWidths返回了空数组。特别宽一般是没设maxWidth某个备注字段几百个字把列撑爆了。记得Math.min(w, max)兜底。中文宽度估算用replace(/[^\x00-\xff]/g, aa)比直接length * 2更准因为标点符号宽度不一样。5.4 大文件导出内存暴涨几万行的表格一次性aoa_to_sheet会占用大量内存。如果确实要导大表建议分批构建或者改用XLSX.write的流式写法。日常后台报表几千行以内本文的方案足够不用过度优化。5.5 请求返回 401 或超时如果模型通道报 401先检查settings.json里的apiKey是否复制完整有没有多余空格。超时的话看timeout设置默认 30 秒对长文本可能不够适当调大。注意apiBase不要带末尾斜杠拼接路径时容易出双斜杠。6. 把导出和通道配置固化下来这套方案落地后我建议做两件收尾的事。第一把mergeRules和calcColWidths抽成独立模块导出函数只负责编排业务表格换一个就换一份规则配置代码复用率会高很多。第二settings.json里的 Key 不要提交到仓库用.gitignore排除团队协作时提供一份settings.example.json模板各自填自己的 Key。导出链路验证通过后如果你还想让模型参与数据加工比如导出前自动生成一列「异常说明」直接在exportTableToExcel调用前插一段callModel即可通道已经配好不用再折腾鉴权。整条链路的价值就在于导出是纯前端的模型调用是统一入口的两者拼起来不打架。
返回列表