ARTICLE DETAIL

资讯详情

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

Univer实战:打造只能填指定单元格的在线预算填报表格

Univer实战:打造只能填指定单元格的在线预算填报表格 做线上填报需求做到崩溃的人大概都幻想过同一个画面交给用户的不是一个个零散表单控件而是一张真正的表格想填哪里就填哪里不能填的区域天然锁死。上个月我就接到这样一个活儿给甲方做一个年度预算收集页表格大部分区域只读只有几个指定单元格允许填报人输入其他谁也不能动。研究一圈后我最后选了Univer——一个开源在线表格引擎把Excel级别的表格直接嵌进网页。实测下来这个“表格即表单”的路子确实舒服今天把从选型到实现的完整思路盘一盘。1. 为什么是Univer一个“表格即表单”的骚需求逼我做的选型1.1 需求复盘看起来是表格本质是权限控制甲方原话很简单给每个部门发一张表让各部门把预算填进去表格里大部分内容不能改部门名、指标名、上年数这些都要锁死只有“本年度预算”和“备注”两列能填。听起来像做个问卷但真用问卷做你会被“要支持复制粘贴整列数据”“要能写合计公式”“要能批量填数”这种需求活活逼疯。说白了他们要的是一个“带审批场景的Excel”核心诉求是单元格级别的可编辑权限控制。用传统前端表单你需要自己造一个表格组件处理行列增删、单元格选择、公式计算、粘贴事件工作量直接拉满用现成在线表格库这些基础能力都具备你只需要解决一件事怎么把大部分单元格锁住只开放一小片区域。1.2 Univer是什么Univer是一个开源的Web端Office解决方案定位不只是表格还包括文档和幻灯片有点想做“云端Office基础设施”的意思。它的表格引擎成熟度相当高公式引擎、条件格式、数据验证、筛选、分列这些Excel常用能力都有而且是纯TypeScript写的深度二次开发比老牌开源表格库舒服不少。项目用的是Apache 2.0协议商用不担心授权问题。对“在线表格”这个场景来说Univer完全能撑住官方也一直在推协同编辑和插件化架构。1.3 横向对比我为什么不选Luckysheet、Handsontable、SheetJS这个项目我认真对比过四个方案结论先放这儿如果是“粘贴个数据展示”用SheetJS就够如果是“做可交互网格”Handsontable更顺但你要的是“长得像Excel、能填数、能锁权限、还能上协同”的真表格Univer是当前开源阵营里最贴合的选择。方案定位单元格锁定协同编辑二次开发友好度Univer云端Office基础设施表格/文档/幻灯片有原生的工作表保护机制有官方生态支持高TypeScript 插件化架构Luckysheet老牌开源在线表格弱主要靠UI层拦截编辑需要配套server链路复杂中社区更新趋缓Handsontable数据网格组件有readOnly能力但偏组件逻辑需商业版中框架绑定较深SheetJS电子表格解析/生成引擎没有交互概念无极高但纯数据处理不含UI再说一个细节Univer把表格、公式引擎、渲染引擎拆成了独立包你可以按需加载比如只引表格和公式引擎不引文档模块初始包体可控。对比Luckysheet动辄把整个表格能力焊死的做法Univer的模块化设计在后期优化和裁功能时优势很明显。1.4 为什么这个场景必须用“真表格”而不是表单组件我见过太多人用Ant Design的Table或自研div表格做填报一旦需求变成“用户可以粘贴一整行Excel数据”“金额列要自动汇总”“某个格要根据其他格的值动态变化”组件方案就开始失控。真表格的好处是它天然支持批量操作用户直接选中一片区域从Excel里复制粘贴过来数据格式自动对齐写个SUM公式金额列一变合计立刻更新还有单元格的样式、边框、合并都是白送的能力。你要做的只是控制“哪里能改”这比从零实现一套编辑交互省太多事了。2. 理解Univer的单元格权限模型后面才不踩坑2.1 Excel用户都知道的“先解锁再保护”用Excel做填报单的老手都清楚一个套路右键单元格设置单元格格式在“保护”选项卡里勾掉“锁定”然后再给工作表设置保护密码。这样被“取消锁定”的单元格依然能编辑其余保持“锁定”状态的单元格全部只读。Univer的权限模型基本沿用了这套思想理解这一层后面的实现就顺了。2.2 Univer里“锁定”和“保护”是两码事很多第一次接触Univer的朋友会混淆这两个概念。锁定lock是单元格样式的一部分是个属性保护protect是工作表级别的开关。两者要配合使用工作表保护开启后所有“锁定”属性的单元格都禁止编辑而“解锁”的单元格仍然可编辑。所以正确顺序永远是先设好单元格的锁定状态再打开保护开关顺序反了就会出现“整个表直接锁死”或“保护形同虚设”两种极端。2.3 填表场景下怎么设计“权限矩阵”动手写代码前我强烈建议先画一张权限矩阵表把每个区域的作用、锁定状态、开放状态标清楚。别嫌这个步骤土后面所有配置都是照它填的。我那个预算填报项目最终落了这么一张矩阵区域用途是否锁定说明A1:F1标题行锁定纯展示不可编辑A2:A20部门名称列锁定由管理员维护填报人只读B2:B20预算金额填写区解锁填报人的核心输入区C2:C20填报说明区解锁可选填写D2:F20上月实际、完成率、备注区锁定由公式自动计算或管理员维护末尾行合计行锁定用SUM公式自动汇总把矩阵一画你就能一眼看出哪些样式需要定义、哪些区域需要单独处理。我当时定义了两种核心样式灰色底纹的锁定样式黄色底纹的解锁样式视觉上先给用户做了第一层提示。3. 实操从零搭一个“只能填指定单元格”的Univer表格3.1 准备环境用Vite快速初始化项目我用的是比较常规的前端工程Vite TypeScriptReact或Vue都行Univer本身框架无关。先建工程再装依赖npm create vitelatest univer-fill-demo -- --template vue-ts cd univer-fill-demo npm install然后安装Univer相关包npm install univerjs/core univerjs/design univerjs/engine-formula univerjs/engine-render univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/icons简单解释一下这些包的分工univerjs/core是核心负责注册机制、命令系统、文档模型相当于骨架univerjs/sheets是表格业务逻辑包括单元格操作、公式解析等univerjs/engine-formula是公式引擎要支持SUM这类公式必须引univerjs/ui和univerjs/sheets-ui负责把表格渲染到浏览器并带上工具栏、右键菜单univerjs/design和univerjs/icons提供主题样式和图标。3.2 初始化Univer实例在入口文件里创建Univer实例并注册插件import { LocaleType, Univer, UniverInstanceType } from univerjs/core; import { defaultTheme } from univerjs/design; import { UniverFormulaEnginePlugin } from univerjs/engine-formula; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverUIPlugin } from univerjs/ui; const univer new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: app, }); univer.registerPlugin(UniverSheetsUIPlugin);这个顺序别搞错核心插件要在UI插件之前注册否则渲染层找不到业务逻辑常见的报错就是这样来的。3.3 定义单元格数据锁定区与放开区一把梭初始化完成后创建一个表格单元。这是最核心的一步所有锁定和解锁的配置都体现在这里const workbookConfig { name: 年度预算填报单, sheets: { 1: { name: 预算填报, rowCount: 20, columnCount: 6, cellData: { // 第1行表头全部用 locked 样式 0: { 0: { v: 部门名称, s: locked }, 1: { v: 预算金额元, s: editable }, 2: { v: 说明, s: editable }, 3: { v: 上月实际元, s: locked }, 4: { v: 完成率, s: locked }, 5: { v: 备注, s: locked }, }, // 第2行起A列锁定B、C列解锁 1: { 0: { v: 市场部, s: locked }, 1: { v: , s: editable }, 2: { v: , s: editable }, 3: { v: 120000, s: locked }, 4: { v: , s: locked }, }, 2: { 0: { v: 运营部, s: locked }, 1: { v: , s: editable }, 2: { v: , s: editable }, 3: { v: 95000, s: locked }, 4: { v: , s: locked }, }, }, styles: { locked: { lock: true, backgroundColor: #F2F2F2, fontColor: #808080, }, editable: { lock: false, backgroundColor: #FFF7E0, border: { b: { style: thin, color: #E6A23C }, l: { style: thin, color: #E6A23C }, r: { style: thin, color: #E6A23C }, t: { style: thin, color: #E6A23C }, }, }, }, }, }, }; univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookConfig);关键在于styles里定义了两种样式locked样式的lock属性为trueeditable样式的lock属性为false。然后在cellData里给每个单元格指定styleId这里我没直接写完整样式对象而是用了locked和editable两个ID引用数据量大的时候可以省很多重复定义。3.4 开启工作表保护真正实现“不能改”光设样式还不够现在表格还没开“保护”用户照样能改所有单元格。需要用命令打开工作表保护开关。由于Univer不同版本对新命令的封装略有差异这里的通用思路是通过CommandService执行一个保护Sheet的变更指令import { ICommandService, UniverInstanceType } from univerjs/core; import { SetWorksheetProtectCommand } from univerjs/sheets; const currentUnit univer.getCurrentUnit(UniverInstanceType.UNIVER_SHEET); const commandService currentUnit?.getCommandService(); commandService?.executeCommand(SetWorksheetProtectCommand.id, { unitId: currentUnit.getUnitId(), subUnitId: currentUnit.getActiveSheet().getSheetId(), protect: { selectLockedCells: false, selectUnlockedCells: true, formatCells: false, insertRows: false, deleteRows: false, editObjects: false, }, });这里几个参数值得细说。selectLockedCells: false表示用户连选中被锁单元格都不行这个很关键否则用户虽然改不了但选中灰色区域时状态栏还是会显示单元格信息体验上有种“能摸不能碰”的憋屈感。selectUnlockedCells: true保证可编辑区域正常选中编辑。formatCells: false禁止用户改动单元格格式避免有人把背景色改掉破坏整体视觉。insertRows和deleteRows直接关死防止用户插入行绕过你设计好的区域边界。执行完这段表格就变成灰色区域点不动、选不中黄色区域正常输入公式列自动计算。这一刻的效果真的很解气。3.5 一个小补充把“合计行”的公式加上既然是个预算填报单合计行不能少。在cellData里给最后一行的金额列加一个SUM公式注意样式仍然用locked但公式引擎会无视锁定状态自动重算const lastRow 19; workbookConfig.sheets[1].cellData[String(lastRow)] { 0: { v: 合计, s: locked }, 1: { v: SUM(B2:B19), s: locked }, 2: { v: , s: locked }, 3: { v: SUM(D2:D19), s: locked }, 4: { v: , s: locked }, 5: { v: , s: locked }, };实测下来用户改B列任意一个格合计行的SUM结果会立刻更新公式单元格因为被锁用户也改不了两全其美。4. 从“能填”到“能收”把Univer接进业务链路4.1 监听编辑事件填一个值就拿一个值演示表跑起来只是第一步真实业务里你得知道用户填了什么。最实用的做法是监听命令执行事件每次数据变更命令跑完就去活动工作表里拉最新的数据import { SetRangeValuesCommand } from univerjs/sheets; commandService?.onCommandExecuted((command) { if (command.id SetRangeValuesCommand.id) { syncSheetData(); } }); function syncSheetData() { const sheet currentUnit?.getActiveSheet(); const range sheet?.getRange(0, 0, 20, 6); const data range?.getValues(); console.log(最新表格数据, data); }这个方案比一个个单元格监听更靠谱因为无论用户是手动输入、粘贴整片区域、还是通过公式变更间接修改数据命令都会触发数据永远是同步后的结果。4.2 把表格内容提取成后端要的JSON接后端的时候最忌讳把整个二维数组一股脑丢给接口然后让后端自己去解析哪些行是部门、哪些列是金额。我建议直接在syncSheetData里把数据映射成业务对象function buildPayload(data: any[][]) { const rows: Array{ department: string; amount: number; note: string } []; for (let i 1; i 19; i) { rows.push({ department: data[i][0], amount: data[i][1], note: data[i][2], }); } return rows; }这样后端拿到的就是一个结构清晰的数组每个元素对应一个部门的填报记录省去一堆沟通成本。顺便提一句getValues拿到的单元格类型可能混合字符串和数字提交前务必做一次Number()转换和空值过滤别让脏数据进库。4.3 动态控制根据某个格的值实时收放编辑权限有一次甲方追加需求如果“项目类型”选了“特殊项目”就需要多开放一列“专项说明”。这在Univer里也不难监听项目类型单元格的变化如果是特定值就动态执行一个范围样式命令把目标列的lock改为falseimport { SetRangeStyleCommand } from univerjs/sheets; function setRangeEditable(row: number, col: number, editable: boolean) { commandService.executeCommand(SetRangeStyleCommand.id, { range: { startRow: row, endRow: row, startColumn: col, endColumn: col, }, style: { lock: !editable }, }); }配合前面的事件监听你可以实现类似“表单选项变了对应区域解锁”的联动。注意改样式后要重新执行一遍保护指令否则有些版本的UI不会立刻刷新锁定状态。4.4 把界面裁剪成一个“干净”的填报页给甲方演示的时候我还没拿工具栏和右键菜单“开刀”对方一眼就问这跟Excel一样他们会不会乱点所以上线前要把Univer的工具栏裁到只剩必要能力。UniverSheetsUIPlugin初始化时支持传入工具配置例如关掉导入、公式插入、筛选、排序univer.registerPlugin(UniverSheetsUIPlugin, { toolbar: { export: false, formula: false, mergeCell: false, filter: false, sort: false, }, });右键菜单也可以通过上下文菜单配置隐藏只保留复制、粘贴、撤销这几个。这一步强烈建议做填表用户看到的功能越少误操作率越低你的售后成本也越低。5. 踩坑记录这些问题让我一度想换库5.1 保护顺序不对表格当场“全锁死”我在实验阶段犯过一个低级错误先打开了工作表保护再试图给B列设置解锁样式。结果整个表全部只读因为保护已经生效后续样式变更命令根本执行不进去。正确的顺序必须是先定义样式并设置到单元格再开启保护。顺序颠倒的排查特征很明显——报错信息或者控制台提示操作被保护拦截看到这个先检查保护开关的时机。5.2 用户不知道哪些格能填留言问了我一下午第一版测试的时候页面只给了锁定区域灰底没给出可编辑区域的强提示。结果用户上线后第一件事就是截图问我这些格子怎么有的能点有的不能点。后来我把可编辑区域统一改成浅黄色底加上橙色边框并且在表头加了一个“黄色区域可填写”的说明文字这类问题立刻消失。视觉引导和数据校验同等重要尤其是面对非技术用户。5.3 没有自动保存用户刷新后数据全没这是最容易出生产事故的点。用户填了20个格子中途接了个电话回来按了一下刷新所有数据归零。Univer本身只管理前端内存状态不会自动持久化。我在项目里做了两重保险每5秒自动读取一次单元格数据存进localStorage用户点击“提交”按钮时再把数据发送到后端接口。这样刷新页面后还能从localStorage恢复最少不会让用户白填一场。5.4 公式列也被“锁定样式”挡住不对这里有个容易误解的点。锁定只是禁止用户编辑公式引擎的自动计算不受锁定影响。我在合计行用locked样式没任何问题。倒是要注意别把公式单元格的样式配成editable并开放出来否则用户点进去会看到公式本体容易误改出乱子。所以公式单元格一律锁定放心让引擎替你算。5.5 单元格数量一大滚动开始掉帧我第一版做了一个100行30列的配置表直接喂给Univer结果滚动和输入都有明显卡顿。后来按Univer推荐的性能写法优化只渲染填报人实际要用的区域把rowCount和columnCount控制到合理范围不需要边框的空白区域不预先填样式单元格数据尽量用稀疏格式没值的地方留空不要给每个空单元格都写{v: }。优化之后体感流畅很多这也印证了模块化按需加载的价值。5.6 常见问题速查表现象原因解法整个表全部只读保护开启早于解锁样式先设单元格lock属性再开保护解锁区域边框/底色异常样式ID引用错误或样式名写错检查cellData里的s字段是否匹配styles里的key用户能选中但改不了锁定格selectLockedCells设为true保护参数里改成false刷新后数据清空前端无持久化加localStorage备份或定时提交公式不计算未注册公式引擎插件确认UniverFormulaEnginePlugin已注册修改权限后UI未刷新动态改样式后没重新执行保护指令改完lock后重新跑一次保护命令6. 最后说一点我的真实体会这类“表格即表单”的项目Univer给我最大的启发不是它做得像Excel而是它把Excel的权限模型完整搬到了网页里。以前用Ant Design Table做填报我总得自己发明一套“单元格状态管理”处理复制粘贴、公式依赖、行列锁定的复杂组合换到Univer之后这些问题全部回到Excel的原生逻辑里解决思路清晰了代码反而少了很多。如果这个分享对你也有用我建议动手时第一件事别急着写脚手架先把权限矩阵画清楚然后给锁定区和解锁区各定一种视觉样式最后再碰命令行API。这个顺序能帮你绕开大部分坑。后面我准备继续把Univer的协同编辑模块和自定义公式插件搞透到时再写一篇进阶的欢迎持续关注。
返回列表