告别硬编码让你的前端表格自动计算的实例代码
<p><span style="color: #ff0000"><strong>序言</strong></span></p><p>当我的团队进行税务系统模块开发的时候,我发现他们需要花费80%的时间去解决计算问题,尤其体现在表格(Grid)中的计算,这些时间花在:</p>
<ul>
<li>写前台js代码(因为用户在表格中的输入会影响其他单元格,所以需要即时将运算后的新值呈现给用户看)</li>
<li>写后台代码(因为用户对表格数据的更改会影响其他表格,所以要在用户点击保存时更新受影响表格的数据)</li>
<li>实施修改计算方法,导致开发者需要修改代码</li>
</ul>
<p>于是我调研了税务其他模块的功能,发现税务系统大量使用表格控件,而其中或多或少都会涉及到计算问题。而处理计算的方法,都是采用硬编码。</p>
<p>计算,这个习以为常的编码动作,其实很容易让人联想到Excel中的公式,更何况需求文档本身就是以Excel的形式提供的。当我们在使用Excel的时候,可以在单元格中设置公式,通过改变源头单元格的值,Excel将自动计算单元格公式,将结果值赋予目标单元格。那么,我们是否可以参考这种模式,开发者不再需要写复杂难懂的计算逻辑,只需要根据实施提供的公式,将它们转成某种格式的语句,再调用某种计算引擎产出结果,将结果呈现给用户看或者持久化到数据库?答案是肯定的,而这一切的核心就是自动计算引擎——AutoCalculate。</p>
<p><span style="color: #ff0000"><strong>作用</strong></span></p>
<p>AutoCalculate是表格复杂运算的解决方案,可以让你省掉成百上千行的计算逻辑代码,从此写代码就像写Excel公式一般简单。</p>
<p><span style="color: #ff0000"><strong>适用范围</strong></span></p>
<p>前台:</p>
<p>适用于ElementUI表格、EasyUI Grid控件、ParamQuery Grid等所有js表格控件中带有公式的复杂运算</p>
<p>后台:</p>
<p>适用,需要V8引擎</p>
<p><span style="color: #ff0000"><strong>前台用法</strong></span></p>
<p>AutoCalculate由两部分组成,分别是公式和计算引擎,公式是就是根据特定语法编写的字符串,如:#3 = * 10,计算引擎即是AutoCalculate.js,负责解析公式。以下开始介绍如何书写公式。</p>
<p>单元格</p>
<p>假设有这样的场景,单元格①=单元格②+单元格③,对应的公式是:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= + </pre>
</div>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202239.jpg" /></p>
<p>先来看看<code></code>代表什么,首先,中括号<code>[ ]</code>代表一个单元格,Month1即“1月”对应的列名,紧接着是一个逗号<code>,</code>,后面的1代表RowNo = 1,以此类推,</p>
<p><code></code>代表列为“1月”且RowNo = 2的单元格</p>
<p><code></code>代表列为“1月”且RowNo = 3的单元格</p>
<p>所以我们可以用<code></code>来代表一个单元格,y即列名,也称作纵坐标, x即RowNo的值,也称作横坐标</p>
<p><em>如果表格没有RowNo列怎么办?如想寻找答案,请继续往下阅读</em></p>
<p><strong>让公式生效</strong></p>
<div class="jb51code">
<pre class="brush:js;">
//首先引入AutoCalculate.js
import AutoCalculate from '../components/AutoCalculate';
...
//定义一个AutoCalculate实例,formulas为公式数组
let autoCal = new AutoCalculate(formulas);
/* 调用cal方法
* gridDatas(必填):表格数据
* refField(必填):参考字段,即单元格中x是哪个字段的值
*/
autoCal.cal(gridDatas, refField);</pre>
</div>
<p><strong>区域公式</strong></p>
<p>实际上,除了1月,2月,3月……10月也存在类似的公式,即:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= +
= +
= +
……
……
……
= + </pre>
</div>
<p>也就是说我们需要写10条这样的公式,对于简单的场景来说,这不成问题,但是对于某些包含大量公式的表格,这种写法存在一些弊端,比如容易写错,还有,公式长的时候也需要花费较多时间才能写完。所以,便有了区域公式。</p>
<p>观察上面的公式可以发现,其实每条公式都可以用一条公式来代替,例如以下公式:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
[@,1] = [@,2] + [@,3]</pre>
</div>
<p>这里没有明确的列名,只是用了一个占位符@,但它足以代表以上10条公式。这个时候,我们只需要在适当的位置补上列名就可以了,所以,最终的公式就是:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
{Month1, Month2, Month3, Month4, Month5, Month6, Month7, Month8, Month9, Month10}[@,1] = [@,2] + [@,3]</pre>
</div>
<p>你需要将列名用<code>,</code>隔开,并放置在大括号<code>{ }</code>内,如此,1条公式便相当于10条公式。</p>
<p>占位符不仅仅可以用于纵坐标,还可用于横坐标,如以下公式:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
//公式1:
= + + + + + + + + +
//公式2:
= + + + + + + + + +
//公式3:
= + + + + + + + + +
//公式4:
= + + + + + + + + +
//公式5:
= + + + + + + + + +
//公式6:
= + + + + + + + + +
//公式7:
= + + + + + + + + +
//公式8:
= + + + + + + + + +
//公式9:
= + + + + + + + + + </pre>
</div>
<p>使用区域公式,可以写成:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
{2, 3, 4, 5, 6, 7, 9, 12, 13} = + + + + + + + + + </pre>
</div>
<p>由此可见,区域公式为公式的书写带来了极大的便利。</p>
<p>支持js语法</p>
<p>在实际场景中,我们经常会碰到一些复杂的公式,如下图,单元格公式使用了Excel自带的Max函数,对于这样的公式,我们可以这样写:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= ( - - > 0 ? - - : 0) + </pre>
</div>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202340.png" /></p>
<p>如你所见,公式支持js语法,你可以在公式等号右边放入一个js变量,甚至js函数,只要是js解析引擎认识的语法,都被支持。</p>
<p>这里有个需要注意的地方,就是不可以将数组元素放入公式中,因为js的数组元素通常带有“[ ]”符号,这与公式当中的单元格表示符”[ ]”产生冲突,所以数组元素被禁止使用,请留意这一点。</p>
<p><strong>公式</strong></p>
<p>接下来,带大家看一看另外一种场景,如图,存在这样的关系:</p>
<p>单元格① = 单元格② - 单元格③</p>
<p>你可能很快就写出了以下公式:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= -
= - </pre>
</div>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202341.png" /></p>
<p>这样写本身没有错,但是我得提醒你,这里的行是不固定的,也就是说表格有多少行完全取决于当时的数据库情况,有可能今天只有3行数据,明天会有5行,后天会有50行。我们不可能随着行数增多而增加公式,所以对于这种行数不确定的表格,我们有一种新的写法,我将它称为公式,因为跟普通公式相比,它没有横坐标:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= - </pre>
</div>
<p>只需要一行公式,AutoCalculate便会将公式应用于指定列名下的所有行。</p>
<p><strong>合计列与小数位数</strong></p>
<p>有时候,我们需要求某一列的和,虽然求某一列的和可能不是我们的最终目的,但却是我们完成计算的必要步骤,如存在以下关系:</p>
<p>单元格③ = 单元格① / 单元格②</p>
<p>单元格②是<code>GroupApprovedTotal</code>列的合计值,我们用<code><列名></code>来表示,即:<code><GroupApprovedTotal></code>。加上这里的行不固定,需要用到公式,所以公式应该写成:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= / <GroupApprovedTotal></pre>
</div>
<p><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202342.png" /></p>
<p>我们知道,在除法中,除数是不可以为0的,所以正确的写法应该是:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= <GroupApprovedTotal> === 0 ? 0 : / <GroupApprovedTotal></pre>
</div>
<p>当你将这条公式放你的代码,并启动程序后,聪明的你应该很快发现,你得到的值不够精确,如上面单元格③显示的数值是66.91%,如果你的单元格①和单元格②跟上图的数值相同,你的单元格③很可能是67%,这是为什么呢?</p>
<p>默认的,AutoCalculate会将计算结果保留2位小数,67%,即0.67,如果想得到66.91%,即0.6691,那就是需要保留4位小数,这时,你需要告诉AutoCalculate,你需要保留4位小数,所以,完整的写法应该是:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
#4 = <GroupApprovedTotal> === 0 ? 0 : / <GroupApprovedTotal></pre>
</div>
<p>在公式的等号左边,被赋值单元格的右边,加“#”号,紧跟着写上小数位数,注意,“#”和小数位数之间不能有空格,前后可以有空格。</p>
<p>没有RowNo的表格</p>
<p>终于到了回答这个问题的时候,我想问问大家,我们是如何在一个平面找到一个点的?答案就是需要这个点的横坐标和纵坐标,同样的,在一个表中,如何找到一个单元格?首先我们可以确定纵坐标,因为所有的列名都是已知的,关键就在于横坐标的确定。采用RowNo来定位,大家一定会觉得似曾相识,因为它跟Excel左侧的序号很像,但不代表只有数字才能作为横坐标。只要值具有唯一性,即不重复,就可以作为横坐标。</p>
<p>举个例子,假设以下的表格是固定两行,没有RowNo,但是可以看出公司编号(BuCode)具有唯一性,那么BuCode就可以作为参考字段,BuCode的值就是横坐标,那么公式就可以写成:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= -
= - </pre>
</div>
<p>如果有RowNo,用RowNo做参考字段时这样写:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= -
= - </pre>
</div>
<p><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202343.png" /></p>
<p>跨数据源计算</p>
<p>何为跨数据源计算?用过Excel公式的朋友应该能看懂下面这个单元格的公式代表的意思。很明显这个单元格的值是其他Sheet的数据经过运算后的值,跨数据源计算就是专门处理这样的场景。</p>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202344.png" /></p>
<p>我们很少甚至不会在前台做跨数据源计算,这里是想告诉大家如何书写公式及调用AutoCalculate的方法,以便在“后台用法”这一章节真正使用到它。</p>
<p>首先,为了取得其他数据源单元格的数据,我们需要拓展一下单元格,之前,我们的单元格是这样的:,暂且称为二元单元格吧,还有这样的单元格:,成为一元单元格,现在,你会看到这样的单元格:[外部数据源,y,x],即三元单元格,三元单元格的出现令到AutoCalculate定位单元格的能力从二维拓展到三维,即不管你有多少表,AutoCalculate都能找到你要的数据。</p>
<p>这是一条使用了三元单元格的公式:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= </pre>
</div>
<p>其中OutputTax是某个数据源的名称,你可以任意取名,越简洁越好,否则复杂的公式会被写得很长,难以阅读。</p>
<p>下面这条公式会从两个数据源OutputTax和TaxRate取值:</p>
<div class="jb51code">
<pre class="brush:xhtml;">
= * (1 + / 100)</pre>
</div>
<p>我相信通过阅读前面章节的内容,你已经能够看懂下面公式的意思,其中前三行公式使用了外部数据源,并结合了区域公式的写法。</p>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202345.png" /></p>
<p>是时候调用我们的计算方法了,为了演示效果,我添加了一个按钮,并将方法写在按钮事件中</p>
<p style="text-align: center"><img alt="" src="https://img.jbzj.com/file_images/article/202009/2020092714202346.png" /></p>
<p>看看我们做了什么:</p>
<p>① 取得某个外部数据源outputTaxDatas</p>
<p>② 取得当前表格的数据源payableTaxDatas</p>
<p>③ 从数据库获取另一个外部数据源taxRateDatas</p>
<p>④ 这里是重点,先来看看AutoCalculate 的构造函数,这里有两个参数:</p>
<p style="text-align: center"><img alt="" src="https://img.jbzj.com/file_images/article/202009/2020092714202347.png" /></p>
<p>formulas:公式,一个数组</p>
<p>options:可选参数,一个object对象</p>
<p>options有个属性externalDatas,表示外部数据源,是一个数组,因为数据可能有多个,每个数组元素都是一个对象,有3个属性:</p>
<p>name:外部数据源名称,这里取什么名称,对应公式中的外部数据源名称</p>
<p>refField:参考字段</p>
<p>datas:数据源</p>
<p>实例化AutoCalculate后,这里调用了一个新的方法calculate,它有2个参数:</p>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202348.png" /></p>
<p>gridDatas:需要重新计算的表格数据,是一个数组</p>
<p>refField:参考字段</p>
<p>AutoCalculate之所有支持所有的js表格控件以及能被后台调用,就是借助于这个方法,因为不论是哪种js表格控件,都能够提取出表格数据(纯数据),数据通常是数组形式,只要将这个数组传进来就可以了。</p>
<p>⑤ 调用calculate后,payableTaxDatas的值已经是运算过的最新值,现在将它绑定到当前的表格即可。</p>
<p>运行程序后的界面:</p>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202449.png" /></p>
<p>点击获取数据后:</p>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202450.png" /></p>
<p><span style="color: #ff0000"><strong>后台用法</strong></span></p>
<p>后台调用AutoCalculate,我们需要用到V8引擎,还有一点很重要,后台调用AutoCalculate也需要用到公式,我们之前的做法是将所有公式放在Extjs的Controller文件中,如下图:</p>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202451.png" /></p>
<p>为了方便后台调用,我们将公式提取出来作为一个单独的文件</p>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202452.png" /></p>
<p>项目中对AutoCalculate后台调用进行了封装,使用非常简单。</p>
<p style="text-align: center"><img alt="" loading="lazy" src="https://img.jbzj.com/file_images/article/202009/2020092714202453.png" /></p>
<p>调用方法如图:</p>
<p style="text-align: center"><img alt="" src="https://img.jbzj.com/file_images/article/202009/2020092714202454.png" /></p>
<p>还是分步解析:</p>
<p>① 保存当前表格的数据</p>
<p>② 获取公式所在js文件的目录</p>
<p>③ 获取两个外部数据源</p>
<p>④ 调用封装后的后台方法,使用了第②步和第③步获取的数据,其中FormulaExpression是公式表达式,即通过这个表达是来找到你提供的js文件中的公式</p>
<p>⑤ 上一步返回的newDatas已经是经过运算的最新数据,现在将这些数据保存到数据库</p>
<p><span style="color: #ff0000"><strong>注意事项</strong></span></p>
<p>书写公式时有两点需要注意:</p>
<p>单元格中不允许出现空格</p>
<div class="jb51code">
<pre class="brush:js;">
/正确写法:
= * 10
//错误写法:
= [ Month11, 1] * 10</pre>
</div>
<p>小数位数标记与小数位数之前不能有空格</p>
<div class="jb51code">
<pre class="brush:js;">
//正确写法:
#3 = * 10
//错误写法:
# 3 = * 10</pre>
</div>
頁:
[1]