报销单里要贴发票扫描件、巡检记录后面附现场照片、合同评审表挂上 PDF 原文——这类需求在前端业务系统里并不少见。Excel 用户对此非常熟悉:插入超链接,点击就能打开附件。但把这套体验搬到 Web 应用时,很多团队会发现纯 HTML 表格做起来相当别扭:单元格只是文本,点击行为要自己从头接管,刷新之后附件和行的对应关系也容易断。
附件上传很重要
对使用者来说,附件是业务凭证的一部分。如果每次都要在表格系统和网盘之间来回切换,一次录入就变成"填表、上传、复制链接、说明对应关系"四个步骤,出错率随之上升。对开发者来说,难点是三个问题叠在一起:怎么让一个普通单元格"看起来像附件";怎么在点击时触发下载而不是跳转网页;以及整张表保存、导出时,附件信息如何跟着走。SpreadJS 作为运行在浏览器中的 JavaScript 电子表格控件,提供了把这三件事串起来的 API 组合。
SpreadJS 的实现思路
核心思路可以概括为一句话:超链接负责外观和交互,Tag 负责携带数据。先给单元格写入文件名作为显示值,再用 setHyperlink 设置带下划线和链接色的样式,并把 command 指向一个事先注册的自定义命令;同时用 setTag 在同一单元格挂上一个对象,记录附件类型标识和文件信息。Tag 对用户完全不可见,可以存放任意结构的数据,正好用来承载"这个格子背后是哪个文件"。
// 先通过 spread.commandManager().register("downloadAttachFile", {...}) 注册命令
sheet.setValue(row, col, file.name);
sheet.setTag(row, col, {
type: 'attachfile', // 自定义的附件类型标识
fileInfo: file // 实际项目中应存放文件在服务器上的访问地址
});
sheet.setHyperlink(row, col, {
url: file.name,
linkColor: '#0066cc',
visitedLinkColor: '#3399ff',
drawUnderline: true,
command: 'downloadAttachFile' // 点击超链接时触发的命令名
}, GC.Spread.Sheets.SheetArea.viewport);
下载命令在 execute 回调里通过 getTag 读回 fileInfo,再交给 file-saver 的 saveAs 落地成文件。清除附件则注册另一个命令,调用 sheet.clear 并传入 StorageType.data 与 StorageType.tag 的组合,把显示值和隐藏数据一起清掉,避免出现"文件名还在、文件已丢失"的悬空状态。
整表层面的动作同样齐备:spread.toJSON() 与 fromJSON() 负责保存和还原表格状态;引入 I/O 插件后,spread.export 可以导出 xlsx 文件。如果要"表格加附件一起交付",常见做法是遍历各工作表单元格,凡 Tag 中带附件标识的就收集其文件内容,连同导出的主表一起交给 JSZip 打包成一个压缩包下载。
可扩展到的场景
- 审批流单据:报销、请假、采购申请表中直接附证明材料。
- 设备巡检与质检记录:每条记录旁挂现场照片或检测报告。
- 合同与法务管理:条款表格后跟扫描件,点开即读原文。
- 教务与人事档案:花名册里维护证件、证书的电子副本。
- 数据交付:向下游提交"主表 + 佐证材料"的完整压缩包。
也要说清边界:纯前端示例为了省事,会把浏览器的 File 对象直接塞进 Tag;真实项目必须先把文件上传到文件服务器,再把访问地址写入 fileInfo,否则刷新或换台机器后文件引用就会失效。另外,函数形式的 command 无法通过 toJSON 序列化,跨保存周期使用时应统一采用字符串命令。
关键词
setHyperlink、setTag、getTag、commandManager().register、commandManager().execute、sheet.clear、StorageType.tag、SheetArea.viewport、toJSON/fromJSON、spread.export、saveAs、JSZip、SpreadJS 附件单元格。
小结
把上传、下载、清除、打包导出收进单元格的点击行为里,用户就不必离开表格去操作另一套文件系统。超链接管展示、Tag 管数据、命令管行为,这条路径的实现成本不高,却能让审批、巡检这类以单据为核心的业务真正闭环。