ARTICLE DETAIL

资讯详情

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

基于Univer构建在线填报表单:锁定单元格与数据校验实战

基于Univer构建在线填报表单:锁定单元格与数据校验实战 做数据收集这件事我一开始用的都是传统表单工具后来项目里必须让用户在一张“长得像 Excel”的表格里直接填数而且还要把表头、公式、说明区域全部锁死用户只能动我指定的那几个单元格。折腾一圈之后我从 Luckysheet 迁移到了 univer算是在这个项目里把整套权限控制、数据校验、结果导出的链路都摸了一遍。如果你也在做在线表格、需要“用户定义表格后只允许填写部分单元格”这种需求这篇应该能帮你少走不少弯路。我会从为什么选 univer 讲起然后给一个可以跑通的最小项目再到单元格锁定、数据校验、结果收集这三大块最后补一些上线前容易踩的坑。整个内容以“运动会报名表”这个真实场景贯穿代码可以直接抄。1. 为什么我看好 univer从“填表收集数据”这个刚需场景说起如果你只在内部系统里做“填表”传统表单确实够用。但一旦遇到下面这类需求传统表单就很别扭字段不是固定的几个输入框而是几十行、十几列的结构化表格用户需要在一个界面里同时看到说明、历史数据、汇总公式然后自己在指定区域补录后端拿到的数据最好直接按“行列坐标”落库而不是解析一堆 JSON 表单字段管理员希望表格是自己定义的定义好之后用户只能改其中一部分单元格其他区域动都不能动。这种场景下最自然的产品形态就是在线表格。用户打开一个网页看到一个已经定义好的表格该填的格子高亮显示不该填的格子点了也没反应填完直接提交数据自动回到后端。univer 正好就是干这个的。1.1 univer 是什么它和 Excel、Luckysheet 的区别univer 是一套开源的表格/文档/幻灯片基础设施库底层用 TypeScript 写核心是“数据与 UI 分离”的架构。它有这几个特点非常吸引我真正的前端组件化可以像引入一个 npm 包一样嵌进 React / Vue / 原生项目支持自定义插件后期加公式、数据校验、协同编辑都有对应的扩展点默认的视觉交互和 Excel 非常接近单元格样式、合并、边框、冻结行列都有免除了我早期用 Excel 宏程序收集数据的困局——以前每个用户电脑上装的东西不一样宏运行环境千奇百怪换成网页表格之后所有逻辑都在服务端控制用户只需要一个浏览器。和 Luckysheet 相比univer 的代码更新更快、社区更活跃而且它不只是一个表格后续还能扩展文档和幻灯片。对于一个要长期维护的项目来说选一个还在快速迭代的框架比选一个“已经很稳但基本停更”的框架更安心。1.2 热门需求背后管理员定义表格用户只填指定单元格我看到这段时间 univer 相关的讨论集中在“支持用户定义表格然后让用户去填写一些单元格其他单元格用户无法修改”这一点上。这其实是两个能力叠加管理员侧能通过页面交互设计表格、合并单元格、写公式、给表头加样式用户侧表格打开后处于保护状态只有明确标注过的单元格可编辑。在 Excel 里对应的就是“工作表保护 单元格锁定”。管理员先设计好整张表然后默认所有单元格都是 locked 状态再把允许用户编辑的区域取消锁定最后开启工作表保护。univer 也沿用了这套思路所以如果你之前理解过 Excel 的保护机制在 univer 里做同样的事会非常顺。2. 三步跑通 univer从零搭建在线表格的最小可用项目在动权限控制之前先要把 univer 本体跑起来。我用的版本是当前 npm 上的最新稳定版这里我给一个最简示例前端构建工具用的 Vite框架用的原生 TypeScript不嵌套任何前端框架这样最容易讲清楚原理。2.1 环境准备与安装依赖创建一个空项目mkdir univer-demo cd univer-demo npm init -y npm install univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/design univerjs/sheets-data-validation注意这里的数据校验包univerjs/sheets-data-validation如果你只做表格展示不需要但做用户填报表单一定用得上。后续版本号建议锁定同一个大版本因为 univer 目前还在比较活跃的迭代期小版本之间 API 偶尔会动混装不同版本容易出现类型不匹配。然后配一个 Vite 环境npm install -D vite typescript npx tsc --inittsconfig.json里记得把moduleResolution设为bundlertarget设为ES2020以上避免遇到 univer 源码里的新语法报错。2.2 渲染一个最小表格在一个index.html里放容器!DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleUniver 报名表格/title style html, body, #app { height: 100%; margin: 0; } /style /head body div idapp/div script typemodule src/src/main.ts/script /body /htmlsrc/main.ts里做初始化import { Univer } from univerjs/core; import { defaultTheme } from univerjs/design; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverUIPlugin } from univerjs/ui; import { UniverSheetsDataValidationPlugin } from univerjs/sheets-data-validation; const univer new Univer({ theme: defaultTheme, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin, { container: app, }); univer.registerPlugin(UniverUIPlugin, { container: app, }); univer.registerPlugin(UniverSheetsDataValidationPlugin);这里有同学会困惑UniverSheetsUIPlugin和UniverUIPlugin都要传container到底谁在渲染我的理解是UniverUIPlugin负责最外层的工作台 UI 框架UniverSheetsUIPlugin负责表格编辑区的交互与渲染。两个都注册、且都指向同一个容器最终表格才会完整显示出来。2.3 创建一个工作簿并用代码填充表头初始化完之后创建带一个 sheet 的工作簿const workbook univer.createUniverSheet({ id: workbook-1, sheets: [ { id: sheet-1, name: 报名表, rowCount: 100, columnCount: 10, cellData: { 1: { 0: { v: 姓名, s: { bold: 1, fill: { rgb: #E8F0FE } } }, 1: { v: 部门, s: { bold: 1, fill: { rgb: #E8F0FE } } }, 2: { v: 手机号码, s: { bold: 1, fill: { rgb: #E8F0FE } } }, 3: { v: 参赛项目, s: { bold: 1, fill: { rgb: #E8F0FE } } }, 4: { v: 备注, s: { bold: 1, fill: { rgb: #E8F0FE } } }, }, }, }, ], });这里的cellData是二维坐标映射外层 key 是行号内层 key 是列号都从 0 开始。s是样式对象bold代表加粗fill是背景填充。这样创建出来的表格第一眼就已经有可读性了。如果你需要“管理员在界面上自己定义表头”后期可以把这份cellData改成由后端配置驱动表格结构发生变化时前端重新createUniverSheet或者用 API 动态更新即可。我项目里的做法是管理员在一个管理页用同样的 univer 实例编辑模板保存后把整个快照存到后端用户端加载时直接渲染这份快照这样就实现了“管理员定义表格”。3. 单元格锁定的真正实现让用户只能改允许填写的区域这是整个项目里最核心的一块。用户侧打开表格之后默认情况下表格的所有单元格都是可以编辑的这肯定不行。我们需要做两件事第一把整张表保护起来第二在保护状态下单独放行我们指定的可编辑区域。3.1 理解 univer 的保护与锁定关系在 Excel 里保护工作表只是开启一个开关真正决定单元格能否被修改的是单元格自己的 locked 样式。默认所有单元格 locked 属性是真开启工作表保护后全都不能改如果你想让某个区域可编辑就先选中它取消锁定再启用保护。univer 虽然 API 形式不完全一样但设计思维是一致的保护作用于工作表维度锁定作用于单元格样式维度。你在创建表格时可以通过style里设置locked: 1表示锁定locked: 0表示不锁定然后在用户侧渲染页面里开启工作表保护。3.2 我的做法明确“锁定默认值”和“放行区域”因为 univer 的默认样式通常是不锁定或跟随主题为了安全我建议你在创建表格时就显式给单元格样式加上 locked 标志不要依赖默认值。否则一旦某个版本改了默认行为你辛辛苦苦做的权限控制可能直接失效。具体到运动会报名表我的定义是第 1 行放标题合并单元格锁定第 2 行放填写说明比如“请只填写白色区域灰色区域由管理员维护”锁定第 3 行是表头锁定第 4 行到第 50 行是用户填写区取消锁定最后一列或最后一行放统计公式锁定。创建表格时我直接用循环给数据区域加样式const editableRegionStartRow 3; // 第4行 const editableRegionEndRow 49; // 第50行 const editableColumns [0, 1, 2, 3, 4]; const cellData: Recordnumber, Recordnumber, unknown {}; function setStyle(row: number, col: number, cell: unknown) { if (!cellData[row]) cellData[row] {}; cellData[row][col] cell; } // 第1行标题 setStyle(0, 0, { v: 年度运动会报名表, s: { bold: 1, fontColor: #1F4E79, fontSize: 16, locked: 1 }, }); // 第2行说明 setStyle(1, 0, { v: 请填写白色可编辑区域其他区域不可修改。, s: { fontColor: #888888, locked: 1 }, }); // 第3行表头 [姓名, 部门, 手机号码, 参赛项目, 备注].forEach((name, col) { setStyle(2, col, { v: name, s: { bold: 1, fill: { rgb: #E8F0FE }, locked: 1 }, }); }); // 可填写区域显式锁定为 0 for (let row editableRegionStartRow; row editableRegionEndRow; row) { for (const col of editableColumns) { const style { locked: 0, fill: { rgb: #FFFFFF }, }; setStyle(row, col, { s: style }); } }这里我专门把所有可编辑区域都设成了白色的填充色锁定的表头和数据保留灰色底色用户在视觉上就能一眼看出哪里能填。3.3 在用户侧开启工作表保护表格快照创建完之后需要在渲染时对工作表启用保护。univer 提供了命令式 API不同版本名称可能略微不同但思路一致拿到当前工作表对象调用保护相关命令。我记得在univerjs/sheets中有SetWorksheetProtectionCommand还可以设置一个密码字符串。用起来大概是import { Worksheet } from univerjs/core; import { ICommandService, LocaleService } from univerjs/core; const sheet workbook.getSheetBySheetId(sheet-1); // 命令方式 const commandService univer.__getInjector().get(ICommandService); commandService.executeCommand(sheet.command.set-worksheet-protection, { sheetId: sheet.getSheetId(), protection: { lock: true, password: , // 不需要用户解除密码空即可 }, });如果你们的 univer 版本已经把这套封装成了更友好的 API比如worksheet.protect()直接调用即可。重点不是 API 长什么样而是你要意识到保护必须发生在所有单元格样式设置完毕之后。如果你先把保护开了再调整样式可能会被权限拦截掉。补充一个容易忽略的点工作表保护默认会一并禁止用户插入行列、删除行列、调整行高列宽、合并单元格等操作。这对于填表场景其实是好事不会有人乱拉列宽把布局搞坏。但如果你希望用户能调整行高以便看清楚内容需要在保护配置里单独开放resizeRow、resizeColumn权限。这一点在 Excel 保护设置里也有对应选项univer 做得比较接近找一下protection对象下的permissions字段即可。3.4 被锁定单元格的交互反馈从产品体验角度光“不能编辑”还不够最好让用户知道为什么不能编辑。我自己测试时发现锁定单元格点击后通常没有任何反应第一次用的人会以为是系统卡了。我后来处理的方式是在页面里通过监听单元格选中事件来判断当前选中的区域是否可编辑如果用户选中的是锁定单元格就弹一个轻提示“该区域已锁定仅管理员可修改”。虽然不能让 univer 原生弹出气泡但完全可以在上层监听onCellClick或onSelectionChange来做。这个交互细节属于“做了用户没感觉不做用户会困惑”的类型。你产品里如果是给内部员工填表建议无论如何都加上能减少很多客服咨询。4. 表格权限之外还需要配套的设计数据校验与表单提交锁定单元格只是第一步。真正的填表场景光靠锁定远远不够用户可能漏填、填错格式、填了不存在的项目、甚至绕过前端直接提交伪造数据。这一节我讲两块在 univer 里做校验以及如何把用户填写的内容完整、可靠地收集上来。4.1 数据校验手机号正则、必填标记、下拉枚举univer 的数据校验插件提供了类似 Excel 数据验证的能力。我可以给指定区域设置规则比如姓名列非空手机号码列匹配 11 位数字参赛项目列只能是“100米短跑 / 跳远 / 铅球 / 4x100接力”四选一备注列允许为空但最多 50 字。数据校验的规则写法在不同版本里略有差异。我用的版本是通过 data validation 插件注册规则大致形式是const dataValidationPlugin univer.getPlugin(UniverSheetsDataValidationPlugin); dataValidationPlugin.addRule({ uid: rule-name, ranges: [sheet-1!A4:A50], validator: { type: required, message: 姓名不能为空, }, }); dataValidationPlugin.addRule({ uid: rule-phone, ranges: [sheet-1!C4:C50], validator: { type: regex, pattern: ^1[0-9]{10}$, message: 请输入11位手机号码, }, });下拉枚举我使用的是列表校验把四个项目写进 optionsdataValidationPlugin.addRule({ uid: rule-sport, ranges: [sheet-1!D4:D50], validator: { type: list, options: [100米短跑, 跳远, 铅球, 4x100接力], message: 请从列表中选择参赛项目, }, });设置完之后用户填错时 univer 会在单元格上给出提示这是原生能力比自己写正则监听要稳得多。需要注意一点**数据校验只解决了“用户在 UI 上填写时的体验”不能保证后端收到的数据一定合法。**因为用户完全可以拿到页面源代码直接构造 HTTP 请求提交假数据。所以后端接口里必须把手机号正则、项目枚举再校验一遍前端校验只是减少无效提交和提升体验不是安全边界。4.2 收集填写结果监听单元格变更还是定时拉快照用户填完表格之后数据怎么回到后端有三种做法我在实际项目里都试过分别适用于不同情况。第一种实时监听单元格变更。在 univer 里可以通过事件监听拿到用户修改了哪个单元格、新值是什么然后立刻发送到后端。这种方式适合做自动保存用户不需要点任何按钮关闭页面也不怕丢数据。const workbook univer.getCurrentWorkbook(); const sheet workbook.getActiveSheet(); sheet.onCellChange((event) { const { row, column, value } event; saveToBackend(sheet.getSheetId(), row, column, value); });第二种用户手动点击提交按钮前端遍历整个可编辑区域把所有单元格的值打包成一个二维数组发给后端。这种方式最可靠因为你能确保用户是“填完、检查过、再提交”的。运动会报名这种低频、小数据量的场景我建议用这种方式。一次性拿到的是完整的填写结果后端可以直接落库或覆盖更新。第三种定时自动保存快照。每隔几十秒把整个 sheet 的数据序列化成 JSON 传给后端。好处是简单坏处是数据量大时网络开销和写入压力都高适合协同编辑类产品不适合普通填表。我自己在报名表场景用的是第二种配合第一种实时监听做“暂存”功能每改一格先存到浏览器 localStorage用户如果误关页面下次打开还能从本地恢复。等到确认无误后再点提交一次性写入后端。4.3 提交时的数据打包与后端接口设计提交按钮一般放在表格外部也就是页面里 univer 容器下方。点击后我这样拿数据function collectEditableData(workbook: Univer, sheetId: string) { const sheet workbook.getCurrentWorkbook().getSheetBySheetId(sheetId); const rows []; for (let row editableRegionStartRow; row editableRegionEndRow; row) { const rowData: Recordstring, string {}; for (const col of editableColumns) { const cell sheet.getCell(row, col); rowData[col] cell ? String(cell.v ?? ) : ; } rows.push(rowData); } return rows; }拿到rows之后可以把一个“行对象数组” POST 给后端。后端只需要做三件事重新校验必填、正则、枚举给每行补一个唯一 ID防止重复提交落库并返回一个提交编号。这样用户如果再点一次提交也不会因为网络重试产生重复数据。5. 上线前必须解决的几个问题版本、性能与边界情况前面三条路跑通之后项目已经能用了。但如果你想把它放到正式环境有四个问题必须提前想清楚。5.1 版本迭代快锁定版本很重要univer 目前迭代很快可能你几个月后回来升级依赖发现某个插件 API 变了导致整个初始化代码要重写。我的建议是所有univerjs/*包统一锁到同一个版本号不要出现核心包是 1.x、UI 包是 2.x 这种混装在 package.json 里不要用^或~直接锁死精确版本或者用 lockfile 统一管理升级时先看官方 changelog尤其注意破坏性变更说明。如果你不是特别需要新功能完全可以把依赖锁在一个稳定版本上跑很久。表格这种组件稳定性比前沿性重要得多。5.2 大数据量下的渲染性能与节流我有一次拖了 5000 行数据进去页面能跑但明显感觉选中单元格和滚动有卡顿。univer 本身做了 canvas 渲染性能在同类库里算好的但还是要避免做一些“看起来很合理但实际很费”的操作。比如上面说的“遍历所有可编辑区域打包数据”如果可编辑区域是几百行那没问题如果是几千行就要分页或者只在用户点击提交时打包一次不要在每次单元格变更时全量扫描。另外如果表格里大量使用了整列样式或整行样式渲染层会维护大量样式对象内存占用会蹭蹭往上涨。能用“局部单元格样式”就别用“整行整列样式”尤其在宽表场景下。5.3 我踩过的几个坑第一个坑是样式覆盖。我在创建表格时先给可编辑区域设置了白色背景后来又在某个列上设置了黄色高亮结果因为创建顺序问题高亮被之前的白色背景盖住了。排查了半天才发现是单元格样式在cellData里的合并规则并不像 CSS 那样有层级必须保证你最终想要的样式在创建时完整覆盖而不是依赖后续“覆盖”操作。第二个坑是保护命令的调用时机。我最初在创建 workbook 后立刻执行保护命令但当时 sheet 还没有完全挂载到组件树上命令执行后实际没有生效。后来改成在页面渲染完成后再调用并且用requestAnimationFrame或setTimeout做了一次兜底才稳定复现出“锁定”效果。如果你遇到“设置了保护但用户还是能改”优先检查这个时机问题。第三个坑和数据校验有关校验规则里的ranges如果引用了不存在的 sheet 名插件不会报错而是静默失效。用户填任何值都不会触发提示排查起来非常隐蔽。我后来专门写了一个自检函数初始化完把所有规则的范围进行一遍断言确保每个 sheetId 真实存在。5.4 后续扩展方向跑通这个报名表之后你可以把同一套方案复制到几乎所有“数据收集 表格呈现”的项目里后台管理端的“批量录入”页面客户自助填写产品配置单财务报销单的多行明细录入考试报名和成绩确认表。如果你以后要支持两人同时编辑univer 也有协同编辑的接入方案不过那需要你自建 websocket 服务和 CRDT/OT 同步逻辑复杂度会上去一个量级。在普通填表场景下其实并不需要多人同时编辑同一个格子做好“一人填完后提交”就够了。从我个人的实际体会来说univer 这套组合最舒服的一点是它把 Excel 的保护、校验、样式这些成熟概念都搬到了 Web 端后端不需要做太多额外设计前端也只需要理解“单元格样式 工作表保护 数据校验”这三板斧。如果你正在为“用户只能在指定单元格里填数据”这件事发愁完全可以照着上面的链路搭一个最小原型先把锁定和校验跑通再逐步加提交逻辑。
返回列表