sss

SpreadJS个性化方法

Quber...大约 51 分钟SpreadJSAPI方法

SpreadJS 常用方法说明

本章,我们主要针对SpreadJS 中一些个性化方法或 API 进行介绍,方便我们日常中查阅!

1、表单

1.1、获取单元格对象

1.1.1、获取某表单中某范围集合中所有的单元格坐标对象集合

具体实现代码如下所示:

/**
 * 获取某表单中某范围集合中所有的单元格坐标对象集合
 * 该方法适用的场景如:手动选择了很多个单元格范围,在这些范围中可能包含独立的单元格,也可能包含合并的单元格,这时候就需要用到如下方法获取到所有单元格对象
 * @param sheet 表单对象
 * @param selectRanges 单元格范围集合,格式如:[{ row: 0, col: 0, rowCount: 2, colCount: 2 }]
 * 返回数组集合,格式如:[{ row: 0, col: 0, rowCount: 1, colCount: 1 }]
 */
const getAllCellObjsByRanges = (
    sheet,
    selectRanges: Array<{
        col: number,
        colCount: number,
        row: number,
        rowCount: number,
    }>
) => {
    // 得到所有的单元格坐标对象集合,格式如:[{ row: 0, col: 0, rowCount: 1, colCount: 1 }]
    let allCellObjs = [];

    selectRanges.forEach((item, index) => {
        //得到当前item中所有合并的单元格对象集合
        const itemSpanCells = sheet.getSpans(item);
        itemSpanCells.forEach((itemSpanCell) => {
            allCellObjs.push({
                row: itemSpanCell.row,
                col: itemSpanCell.col,
                rowCount: itemSpanCell.rowCount,
                colCount: itemSpanCell.colCount,
            });
        });

        //遍历item中涉及到的所有单元格
        for (let i = item.row; i < +item.row + item.rowCount; i++) {
            for (let j = item.col; j < item.col + item.colCount; j++) {
                const curSpanCellObj = sheet.getSpan(i, j);

                //检查当前单元格是否为合并单元格
                if (curSpanCellObj == null) {
                    allCellObjs.push({
                        row: i,
                        col: j,
                        rowCount: 1,
                        colCount: 1,
                    });
                }
            }
        }
    });

    // 对单元格集合对象对象进行排序,先按row后按col进行升序排序(注意:此处非常重要,如果不排序,获取到的单元格顺序有可能是乱的【不完全按照模板顺序返回】)
    allCellObjs.sort(function (a, b) {
        if (a.row == b.row) {
            return a.col - b.col;
        }

        return a.row - b.row;
    });

    return allCellObjs;
};

说明信息

该方法适用的场景:

有时候,我们手动选择了很多个单元格范围,在这些范围中可能包含独立的单元格,也可能包含合并的单元格,这时候就需要用到如下方法获取到所有单元格对象。

其中用到最主要的表单方法如下:

效果如下图所示:

获取
获取

1.1.2、获取某表单中所有的单元格坐标对象集合

具体实现代码如下所示:

/**
 * 获取某表单中所有的单元格坐标对象集合
 * @param sheet 表单对象
 * 返回数组集合,格式如:[{ col: 0, colCount: 2, row: 0, rowCount: 1 }]
 */
const getAllCellObjs = (sheet) => {
    // 得到当前Sheet单元格范围
    const curSheetCellRange = [
        {
            row: 0,
            col: 0,
            rowCount: sheet.getRowCount(),
            colCount: sheet.getColumnCount(),
        },
    ];

    // 得到所有的单元格坐标对象集合,格式如:[{ col: 0, colCount: 2, row: 0, rowCount: 1 }]
    let allCellObjs = getAllCellObjsByRanges(sheet, curSheetCellRange);

    return allCellObjs;
};

说明信息

上述方法中的getAllCellObjsByRanges就是【1.1.1、获取某表单中某范围集合中所有的单元格坐标对象集合】中的方法。

该方法适用的场景:

有时候,我们需要得到某个表单(Sheet)中的所有单元格坐标对象(单元格所在的行索引列索引跨行数量跨列数量),包含合并的单元格和独立的单元格,因此使用上述方法即可实现。

如下图所示的表单就有 12 个单元格:

Sheet表单单元格
Sheet表单单元格

使用上述方法就可以得到这 12 个单元格的坐标对象集合,如下图所示就是得到的结果:

Sheet表单单元格坐标对象获取结果
Sheet表单单元格坐标对象获取结果

1.1.3、获取当前激活表单所有选择的单元格对象集合

具体实现代码如下所示:

/**
 * 获取某表单中选中的所有单元格坐标对象集合
 * @param spread 主Spread对象
 * 返回数组集合,格式如:[{ col: 0, row: 0 }]
 */
const getSheetSelectCellObjs = (spread) => {
    // 当前激活的Sheet
    let sheet = spread.getActiveSheet();

    // 得到当前Sheet中选择的单元格范围集合
    const curSheetCellRange = sheet.getSelections();

    // 得到所有的单元格坐标对象集合,格式如:[{ col: 0, colCount: 2, row: 0, rowCount: 1 }]
    let allCellObjs = getAllCellObjsByRanges(sheet, curSheetCellRange);

    return allCellObjs;
};

说明信息

上述方法中的getAllCellObjsByRanges就是【1.1.1、获取某表单中某范围集合中所有的单元格坐标对象集合】中的方法。

该方法适用的场景:

有时候,我们需要得到某个表单(Sheet)中的所有选择范围的单元格对象,然后对这些单元格的字体样式等进行设置,此时使用此方法就可以得到所有选择的单元格对象了。

1.1.4、获取单元格范围控件的单元格对象

说明信息

该方法适用的场景:

有时候,我们需要根据单元格范围选择控件选择的范围字符串获取该范围对应的单元格对象,如B2:C3Sheet1!B2:C3=Sheet1!B2:C3=Sheet1!B2:C3,Sheet1!B5:D6,Sheet1!B8:C9范围字符串。

因此我们可以使用如下方法来实现。

具体实现代码如下所示:

/**
 * 根据单元格范围字符串获取单元格对象
 * @param formulaStr 单元格范围字符串(如:B2:C3、Sheet1!B2:C3、=Sheet1!B2:C3、=Sheet1!B2:C3,Sheet1!B5:D6,Sheet1!B8:C9)
 * 返回数组集合,格式如:[{ col: 0, row: 0, rowCount: 1, colCount: 1 }]
 */
const getCellObjByRangeStr = (formulaStr: string) => {
    //返回结果
    let ret = [];

    //单元格选择的范围为多个范围
    if (formulaStr.indexOf(',') > -1) {
        //将多范围字符串分离为单个范围的数组
        const formulaStrArr = formulaStr.split(',');
        formulaStrArr.forEach((item, index) => {
            //得到每个范围的字符串
            const rangeStr = item.substring(item.lastIndexOf('!') + 1);

            ret.push(sheet.getRange(rangeStr));
        });

        console.log(`多个范围:`, ret);
    }
    //只有一个范围
    else {
        //直接将范围字符串传给sheet.getRange即可
        ret.push(sheet.getRange(formulaStr));

        console.log(`一个范围:`, ret);
    }

    return ret;
};

最终效果:

最终效果
最终效果

1.2、设置表单显示辅助线

有时候,我们需要在表单中显示额外的自定义辅助线,方便我们规范编辑,通过如下方法即可实现。

/**
 * 设置表单显示辅助线(底部和右侧的辅助线)
 * @param sheet 表单对象
 * @param width 宽度(像素)
 * @param height 高度(像素)
 * @param lineWidth 辅助线宽度(默认为:1像素)
 * @param colorStr 辅助线颜色(默认为:#409eff)
 */
const setSheetAuxiliaryLine = (sheet, width: number, height: number, lineWidth: number = 1, colorStr: string = '#409eff') => {
    //先删除辅助线,再重新绘制
    //原因是:因为在拖动行高或列宽的时候,浮动元素会跟着行高或列宽的改变而移动,因此此处主要是给行高或列宽改变事件调用的时候做的处理
    if (sheet.floatingObjects.get('floatLineBottom')) {
        sheet.floatingObjects.remove('floatLineBottom');
    }
    if (sheet.floatingObjects.get('floatLineRight')) {
        sheet.floatingObjects.remove('floatLineRight');
    }

    // 创建底部和右侧的浮动对象
    let floatBottom = new GC.Spread.Sheets.FloatingObjects.FloatingObject('floatLineBottom', 0, height, width, lineWidth),
        floatRight = new GC.Spread.Sheets.FloatingObjects.FloatingObject('floatLineRight', width, 0, lineWidth, height);

    // 创建底部和右侧的div对象
    let divBottom = document.createElement('div'),
        divRight = document.createElement('div');

    // 设置底部div对象样式
    divBottom.style.width = `${width}px`;
    divBottom.style.height = '1px';
    divBottom.style.borderBottom = `${lineWidth}px dashed ${colorStr}`;

    // 设置右侧div对象样式
    divRight.style.width = '1px';
    divRight.style.height = `${height}px`;
    divRight.style.borderLeft = `${lineWidth}px dashed ${colorStr}`;

    // 设置底部和右侧浮动对象不允许改变大小、不允许拖动和设置浮动对象的内容
    floatBottom.allowResize(false);
    floatBottom.allowMove(false);
    floatBottom.content(divBottom);
    floatRight.allowResize(false);
    floatRight.allowMove(false);
    floatRight.content(divRight);

    // 将浮动元素添加到表单中
    sheet.floatingObjects.add(floatBottom);
    sheet.floatingObjects.add(floatRight);
};

调用方法:

setSheetAuxiliaryLine(spreadObj.getActiveSheet(), 850, 450, 5, '#F56C6C');

注意

上述方式只是在具体某个业务的地方调用示例。

我们还需要在 Spread 初始化的时候,注册行高和列宽发生改变的事件,并在事件中去调用上述方法,不然在行高或列宽改变时,辅助线(浮动元素)会跟随移动。

具体初始化的时候调用如下所示:

for (let i = 0; i < spreadObj.getSheetCount(); i++) {
    const sheet = spreadObj.getSheet(i);

    //重新绘制辅助线,因为在拖动行高或列宽的时候,浮动元素会跟着行高或列宽的改变而移动
    sheet.bind(GC.Spread.Sheets.Events.RowHeightChanged, (e, info) => {
        setSheetAuxiliaryLine(spreadObj.getActiveSheet(), 850, 450, 5, '#F56C6C');
    });
    sheet.bind(GC.Spread.Sheets.Events.ColumnWidthChanged, (e, info) => {
        setSheetAuxiliaryLine(spreadObj.getActiveSheet(), 850, 450, 5, '#F56C6C');
    });
}

最终效果:

表单辅助线最终效果
表单辅助线最终效果

2、单元格

2.1、单元格光标处插入字符

需求场景

如在设计器中有一个自定义按钮选择符号,该按钮的作用是:点击后会弹选择常用的一些符号的窗体,在窗体中点击某个符号后,可以将该符号插入到编辑的单元格光标处,如果光标选中了某些字符串,则会替换选中的字符串。

具体实现步骤如下。

2.1.1、注册 EditEnding 事件

我们首先应该注册 EditEnding 事件,具体实现如下所示:

//初始化参数
let thisState = reactive({
    // curSelectCell:,
    curGbIndex: {
        startIndex: 0, //光标选中内容的开始索引
        endIndex: 0, //光标选中内容的结束索引
        index: 0, //光标在内容中最后停留的索引
    },
});

/**
 * 注册获取光标在单元格中的索引位置事件
 */
const regGbChgEvent = () => {
    const sheetCount = spreadObj.getSheetCount();

    for (let i = 0; i < sheetCount; i++) {
        let sheet = spreadObj.getSheet(i);

        //解除EditEnding事件绑定
        sheet.unbind(GC.Spread.Sheets.Events.EditEnding);

        //重新设置EditEnding事件绑定
        sheet.bind(GC.Spread.Sheets.Events.EditEnding, function (sender, args) {
            let element = document.querySelectorAll('.gcsj-func-color-text')[1];
            if (element !== undefined) {
                var doc = element.ownerDocument || element.document;
                var win = doc.defaultView || doc.parentWindow;
                var sel;
                // 谷歌、火狐
                if (typeof win.getSelection != 'undefined') {
                    sel = win.getSelection();
                    // 选中的区域
                    if (sel.rangeCount > 0) {
                        var range = win.getSelection().getRangeAt(0);

                        thisState.curGbIndex.startIndex = range.startOffset;
                        thisState.curGbIndex.endIndex = range.endOffset;

                        // 克隆一个选中区域
                        var preCaretRange = range.cloneRange();

                        // 设置选中区域的节点内容为当前节点
                        preCaretRange.selectNodeContents(element);
                        // 重置选中区域的结束位置
                        preCaretRange.setEnd(range.endContainer, range.endOffset);

                        thisState.curGbIndex.index = preCaretRange.toString().length;
                    }
                    // IE
                } else if ((sel = doc.selection) && sel.type != 'Control') {
                    var textRange = sel.createRange();
                    var preCaretTextRange = doc.body.createTextRange();
                    preCaretTextRange.moveToElementText(element);
                    preCaretTextRange.setEndPoint('EndToEnd', textRange);

                    thisState.curGbIndex.index = preCaretTextRange.text.length;
                }
            }
        });
    }
};

2.1.2、具体实现

接下来就是实现将符号插入到光标所在的位置了,具体实现如下所示:

/**
 * 选择符号回调函数
 * @param symbol 选择的符号
 */
const selectSymbol = (symbol: string = '') => {
    //获取当前激活的Sheet
    let sheet = spreadObj.getActiveSheet();

    //得到当前激活Sheet编辑的单元格对象
    const selectCells = sheet.getSelections();

    if (selectCells && selectCells.length > 0) {
        const sheet = spreadObj.getActiveSheet();
        const row = selectCells[0].row,
            col = selectCells[0].col;

        //获得当前编辑的单元格的值
        let cellVal = sheet.getCell(row, col).value();

        //最终得到的值
        let newVal = '';

        //如果单元格的值为null,则说明还没有任何值,就直接将符号赋值给newVal
        if (cellVal == null || cellVal == undefined) {
            newVal = symbol;
        } else {
            //此处做一下字符串的转换,因为cellVal得到的值有可能数据类型为number,因为number没有.length属性,因此做一下转换
            cellVal += '';

            if (
                (thisState.curGbIndex.startIndex == thisState.curGbIndex.endIndex && thisState.curGbIndex.endIndex == thisState.curGbIndex.index) ||
                (thisState.curGbIndex.startIndex == thisState.curGbIndex.endIndex &&
                    thisState.curGbIndex.endIndex == 1 &&
                    thisState.curGbIndex.endIndex < thisState.curGbIndex.index)
            ) {
                if (thisState.curGbIndex.startIndex == 0) {
                    //在最前面追加
                    newVal = symbol + cellVal;
                } else if (
                    thisState.curGbIndex.startIndex == cellVal.length ||
                    (thisState.curGbIndex.startIndex == thisState.curGbIndex.endIndex &&
                        thisState.curGbIndex.endIndex == 1 &&
                        thisState.curGbIndex.endIndex < thisState.curGbIndex.index)
                ) {
                    //在最后面追加
                    newVal = cellVal + symbol;
                } else if (thisState.curGbIndex.startIndex < cellVal.length) {
                    //在中间追加
                    newVal =
                        cellVal.substring(0, thisState.curGbIndex.startIndex) +
                        symbol +
                        cellVal.substring(thisState.curGbIndex.startIndex, cellVal.length);
                }
            } else if (thisState.curGbIndex.startIndex < thisState.curGbIndex.endIndex) {
                //在中间替换选中的字符
                newVal =
                    cellVal.substring(0, thisState.curGbIndex.startIndex) + symbol + cellVal.substring(thisState.curGbIndex.index, cellVal.length);
            }
        }

        //最终设置该单元格的值
        spreadObj.getActiveSheet().setValue(row, col, newVal);
    }
};

最终实现的效果如下图所示:

表单辅助线最终效果
表单辅助线最终效果

2.2、增加或减小表单选择的所有单元格字体大小

需求场景

有时候,我们需要将表单中所有选择的单元格的字体大小进行增加或减少,此时就可以使用如下方法进行实现。

具体实现代码如下所示:

/**
 * 增加或减小当前激活表单选择的所有单元格字体大小
 * @param spread 主Spread对象
 * @param plusMinusNumber 增加或减少字体的号数,默认为:1
 */
const plusMinusFontSize = (spread, plusMinusNumber = 1) => {
    //当前激活的表单对象
    let sheet = spread.getActiveSheet();

    //获得所有选择的单元格对象
    //注意,此处的getSheetSelectCellObjs方法是1.1.2中提到的方法
    const allCells = CommMain.getSheetSelectCellObjs(spread);

    //暂停绘制,此处很重要,如果不启用“暂停”和“恢复”绘制,性能会很慢
    spread.suspendPaint();

    allCells.forEach((item, index) => {
        //获取当前单元格的样式
        const curCellStyle = sheet.getActualStyle(item.row, item.col);

        //提取该单元格字体大小,并转换为数字类型
        const curCellFontSize = parseFloat(curCellStyle.lre.replace(curCellStyle.fpe, ''));

        //增加或减少字体后的数字
        const curCellFontSizePlusMinus = curCellFontSize + plusMinusNumber;

        //获取字体大小的单位
        let curCellFontSizeDw = '';
        if (curCellStyle.fpe != undefined) {
            curCellFontSizeDw = curCellStyle.fpe;
        } else {
            //直接从curCellStyle.lre属性中提取(curCellStyle.lre属性格式为:12.333px)
            const reStr = curCellStyle.lre.match(/[a-zA-Z]+/g);

            if (reStr.length > 0) {
                curCellFontSizeDw = reStr[0];
            }
        }

        //重新设置font属性
        curCellStyle.font = curCellStyle.font.replace(curCellStyle.lre, curCellFontSizePlusMinus + curCellFontSizeDw);

        //重新设置该单元格样式
        sheet.setStyle(item.row, item.col, curCellStyle);
    });

    //恢复绘制
    spread.resumePaint();
};

2.3、锁定表单中指定单元格

需求场景

有时候,我们需要将表单中所有选择的单元格进行锁定,不让用户进行编辑,同时将这些单元格的背景颜色设置为灰色,以便区分。

此时就可以使用如下方式进行设置。

具体实现代码如下所示:

/**
 * 锁定某表单中指定单元格
 * @param spread 主Spread对象
 * @param sheet 表单对象
 * @param cellObjs 锁定的单元格对象集合,格式如:[{row:0,col:0}]
 * @param cellBgColor 锁定单元格的背景颜色,默认值为:#e6e6e6
 */
const lockCells = (spread: any, sheet: any, cellObjs: Array<{ row: number, col: number }>, cellBgColor = '#e6e6e6') => {
    if (!cellObjs || cellObjs.length == 0) {
        return;
    }

    //暂停绘制(使用暂停绘制和恢复绘制的好处是,因为Spread在表单发生改变时,会自动更新绘制,假设很多个表单很多个地方都发生了改变,这样会导致绘制的性能降低,使用了暂停和恢复绘制后,会一次性绘制,不用返回的去绘制,这样就提高了绘制的效率)
    //参考官网:https://demo.grapecity.com.cn/spreadjs/help/docs/BestPractices/UsingsuspendPaintandresumePaint
    spread.suspendPaint();

    //设置默认锁定为false
    //参考文档:https://demo.grapecity.com.cn/spreadjs/help/api/classes/GC.Spread.Sheets.Worksheet#getdefaultstyle
    var defaultStyle = sheet.getDefaultStyle();
    defaultStyle.locked = false;
    sheet.setDefaultStyle(defaultStyle);

    //需要设置表单isProtected为true才生效(注意:设置后就代表该表单所有单元格为锁定状态)
    sheet.options.isProtected = true;

    //此行的作用是,将整个表单的单元格锁定状态设置为false
    sheet.getRange(0, 0, sheet.getRowCount(), sheet.getColumnCount()).locked(false);

    //循环所有单元格对象
    cellObjs.forEach((item, index: number) => {
        //获取单元格对象
        const cellObj = sheet.getCell(item.row, item.col);

        //获取单元格样式
        let cellStyle = sheet.getStyle(item.row, item.col);
        if (!cellStyle) {
            //不存在样式则new一个
            cellStyle = new GC.Spread.Sheets.Style();
        }

        //设置单元格背景颜色
        cellStyle.backColor = cellBgColor;

        //重新设置单元格样式
        cellObj.setStyle(cellStyle);

        //设置该单元格为锁定状态
        cellObj.locked(true);
    });

    //恢复绘制
    spread.resumePaint();
};
最终效果
最终效果

2.4、单元格点击事件

需求场景

有时候,我们可能需要点击不同的单元格做不同的事情。

例如:

  1. 点击单元格 A1,弹出选择设备的页面窗体;
  2. 点击单元格 A2,弹出选择人员的页面窗体
  3. ……

此时,我们就可以使用标签单元格单击事件相结合的方式进行实现。

具体实现如下步骤所示。

2.4.1、设置单元格标签

首先我们给 A1 和 A2 单元格分别设置一个标签,标签名称为CusTag1CusTag2,代码如下所示:

//第一个Sheet
const sheet = spread.getSheet(0);

//设置A1单元格的标签名称为:CusTag1
sheet.setTag(0, 0, `CusTag1`);

//设置A1单元格的标签名称为:CusTag2
sheet.setTag(1, 0, `CusTag2`);

2.4.2、注册单元格单击事件

然后我们就可以给表单注册对应的单击事件了。

当我们获取到当前点击的单元格行和列的索引后,就可以得到当前点击的单元格的标签名称了,有了对应的标签名称,我们就可以做对应的事情了。

具体实现如下代码所示:

//注意:此处演示的时只注册了第一个Sheet的单元格点击事件
const sheet = spreadObj.getSheet(0);

sheet.bind(GC.Spread.Sheets.Events.CellClick, function (sender, args) {
    //注意如下3个逻辑判断
    if (args.sheetArea === GC.Spread.Sheets.SheetArea.rowHeader) {
        console.log('你点击了行头部');
    }
    if (args.sheetArea === GC.Spread.Sheets.SheetArea.colHeader) {
        console.log('你点击了列头部');
    }
    if (args.sheetArea === GC.Spread.Sheets.SheetArea.corner) {
        console.log('你点击了行和列头部');
    }

    //获取当前单元格标签名称
    const curCellTagName = sheet.getTag(args.row, args.col);
    if (curCellTagName != undefined) {
        //当前点击的是A1单元格
        if (curCellTagName == 'CusTag1') {
            //TODO:
        }
        //当前点击的是A2单元格
        else if (curCellTagName == 'CusTag2') {
            //TODO:
        }
    }

    console.log(curCellTagName);
});

2.5、单元格内容显示忽略

单元格内容显示忽略相关需求说明

在有些业务场景下,我们可能需要将单元格中特定的一些字符不做显示,比如:A1 单元格引用了公式等于 A2 单元格的值,但是 A2 单元格的值为空,此时 A1 单元格可能会显示#DIV/0!,然而这个#DIV/0!字符串我们并不想显示出来,那么有没有方式可以将该字符串去掉呢?答案是肯定的。

具体方法实现如下代码所示:

/**
 * 忽略表单单元格中指定字符串不显示,如:#DIV/0!、#VALUE!
 */
const ignoreShowStrs = (GC) => {
    //忽略的字符串集合
    const ignoreStrs: Array<string> = ['#DIV/0!', '#VALUE!'];

    //paint对象
    const ignorePaint = GC.Spread.Sheets.CellTypes.Text.prototype.paint;

    //重写paint函数
    GC.Spread.Sheets.CellTypes.Text.prototype.paint = function (ctx, value, x, y, w, h, style, context) {
        //如果在忽略的字符串集合中有匹配的结果,则将其显示为空字符串
        if (ignoreStrs.some((item) => item == value)) {
            ignorePaint.call(this, ctx, '', x, y, w, h, style, context);
        } else {
            ignorePaint.apply(this, arguments);
        }
    };
};

调用(在初始化 SpreadJS 的时候调用即可):

//忽略表单单元格中指定字符串不显示,如:#DIV/0!、#VALUE!
ignoreShowStrs(GC);
调用前
调用前
调用后
调用后

3、自定义公式

在 SpreadJS 中,我们经常会用到自定义公式,接下来针对自定义公式的使用和封装做一个整理说明。

官方示例文档:

3.1、简单使用

  1. 声明自定义函数对象,如下所示:

    const funName = '', //自定义函数名称
        funMinArgs = 1, //最小传入的参数个数
        funMaxArgs = 2; //最大传入的参数个数
    
    //声明自定义函数对象
    const cusFun = function () {
        this.name = 'max';
        this.minArgs = 1;
        this.maxArgs = 3;
    };
    
  2. 设置自定义函数的 prototype 属性,如下所示:

    //设置自定义函数的prototype属性
    cusFun.prototype = new GC.Spread.CalcEngine.Functions.Function(funName, funMinArgs, funMaxArgs);
    
  3. 设置自定义函数的描述信息,如下所示:

    //设置自定义函数的描述信息,以便在输入自定义函数关键字的时候出现提示信息
    cusFun.prototype.description = function () {
        return {
            description: funDesc,
            parameters: funParams,
        };
    };
    
  4. 设置函数的计算依赖于上下文,如下所示:

    //为true时,函数的计算依赖于上下文
    cusFun.prototype.isContextSensitive = function () {
        return true;
    };
    
  5. 设置函数的参数接受引用单元格区域,如下所示:

    //函数的参数接受引用单元格区域
    cusFun.prototype.acceptsReference = function () {
        return true;
    };
    
  6. 最后就是函数的具体逻辑编写,如下所示:

    //自定义函数逻辑
    cusFun.prototype.evaluate = function () {
        //返回所有参数中最大值
        Math.max(...arguments);
    };
    
  7. 最后就是将自定义函数注册到 SpreadJS 的表单中,如下所示:

    //将自定义函数cusFun注册到表单Sheet中
    sheet.addCustomFunction(new cusFun());
    

到此,一个简单的自定义公式函数就完成了。

3.2、函数封装

从上述【简单使用】的介绍我们不难看出,如果需要写很多个自定义公式,岂不是要重复写上述定义自定义公式的代码?

答案是否定的,我们可以将自定义函数的代码封装成公用方法,然后将所有的自定义函数定义为一个配置数组即可,具体实现请见下面内容。

3.2.1 定义 commFormula.ts 文件

首先我们创建commFormula.ts文件,并在该文件中定义CommFormula导出变量,如下所示:

import GC from '@grapecity/spread-sheets';

/**
 * 自定义函数公用方法
 */
export const CommFormula = {
    //
};

3.2.2 定义 initForFun 公用方法

然后我们在CommFormula中定义initForFun方法,具体方法实现如下所示:

import GC from '@grapecity/spread-sheets';

/**
 * 自定义函数公用方法
 */
export const CommFormula = {
    /**
     * 定义公用初始化自定义函数的方法
     * @param spread SpreadJS对象
     * @param funName 函数名称,如:YjMax
     * @param funDesc 自定义函数描述
     * @param funCallback 自定义函数回调函数,但函数返回了3个参数,第一个为Spread对象,第二个为Sheet对象,第三个为JSON对象(包含了arguments【原始参数数组】,cellObj【引用自定义公式的单元格索引对象】,allCellVals【所有参数值数组】)
     * @param funDefaultVal 自定义函数返回的默认值,默认为:0(使用场景:如该自定义函数选择的单元格没有任何值,此时回调中的retData.allCellVals可能为[],因此就可以将默认值传回去,格式如[0.0])
     * @param funMinArgs 最小参数个数
     * @param funMaxArgs 最大参数个数
     * @param funParams 自定义函数参数描述,默认为:[]
     * @param isContext 是否返回上下文对象,默认为:true
     * @param isAcceptArea 函数参数是否为区域单元格,默认为:true
     * @returns
     */
    initForFun: (
        spread,
        funName: string,
        funDesc: string,
        funCallback: (spread, sheet, retData) => any,
        funDefaultVal: number = 0,
        funMinArgs: number = 1,
        funMaxArgs: number = 1000,
        funParams: Array<any> = [],
        isContext: boolean = true,
        isAcceptArea: boolean = true
    ) => {
        //声明自定义函数对象
        const cusFun = function () {
            this.name = funName;
            this.minArgs = funMinArgs;
            this.maxArgs = funMaxArgs;
        };
        //设置自定义函数的prototype属性
        cusFun.prototype = new GC.Spread.CalcEngine.Functions.Function(funName, funMinArgs, funMaxArgs);
        //设置自定义函数的描述信息,以便在输入自定义函数关键字的时候出现提示信息
        cusFun.prototype.description = function () {
            return {
                description: funDesc,
                parameters: funParams,
            };
        };
        //为true时,函数的计算依赖于上下文
        cusFun.prototype.isContextSensitive = function () {
            return isContext;
        };
        //函数的参数接受引用单元格区域
        cusFun.prototype.acceptsReference = function () {
            return isAcceptArea;
        };
        //自定义函数逻辑
        cusFun.prototype.evaluate = function () {
            // 获取该自定义公式被引用单元格的索引

            /**
             * 说明:
             *     如果isContext=true,则arguments的第一个参数为上下文参数,后面的参数才是自定义参数真正传入的参数
             *         此时我们可以通过arguments获得当前sheet或spread对象,如下所示:
             *             const sheet = arguments[0].source.getSheet();
             *             const spread = arguments[0].source.getSheet().getParent();
             *         也可以获取到公式单元格所在的索引,如下所示:
             *             const curForRowIndex= arguments[0].row;
             *             const curForColIndex= arguments[0].column;
             *
             *     如果isContext=false,则arguments所有参数都为自定义函数传进来的
             *
             * 可参考论坛:
             *     https://gcdn.grapecity.com.cn/showtopic-154888-1-1.html
             */

            //得到当前单元格引用公式所在的索引(注意:目前只有在isContext=true的时候才能通过第一个参数获取表单、工作簿以及单元格索引等信息)
            let curCellObj: {
                row: number;
                col: number;
                rowCount: number;
                colCount: number;
            };

            let sheet;

            // //获取所有选择的单元格对象及其值,格式如:[{row:0,col:0,val:1}]
            // let allCells = [];
            //获取所有选择的单元格值,格式如:[1,2,3]
            let allCellVals = [];

            let length = arguments.length;
            for (let i = 1; i < length; i++) {
                const argument = arguments[i];
                if (typeof argument == 'string' || typeof argument == 'number') {
                    allCellVals.push(argument);
                } else {
                    //获取行索引、列索引、行数和列数
                    const row = argument.getRow(),
                        col = argument.getColumn(),
                        rowCount = argument.getRowCount(),
                        colCount = argument.getColumnCount();

                    //参数为单元格区域时,通过argument也可以获取到上下文
                    sheet = argument.getSource().getSheet();
                    // const spread = argument.getSource().getSheet().getParent();
                    // const sheetName = sheet.name();

                    //通过getSpan方法获取合并单元格索引对象,如果为null,则说明该单元格没有合并(也就是行和列的数量都是1),如果不为null,则直接getSpan返回的数据就直接是:{ row: 0, col: 0, rowCount: 1, colCount: 1 }
                    curCellObj = sheet.getSpan(arguments[0].row, arguments[0].column);
                    if (curCellObj == null || curCellObj == undefined) {
                        curCellObj = {
                            row: arguments[0].row,
                            col: arguments[0].column,
                            rowCount: 1,
                            colCount: 1,
                        };
                    }

                    // //声明Range对象
                    // const range = new GC.Spread.Sheets.Range(row, col, rowCount, colCount);
                    // //得到类似【A1:A8】范围的字符串
                    // const string = GC.Spread.Sheets.CalcEngine.rangeToFormula(range, 0, 0, GC.Spread.Sheets.CalcEngine.RangeReferenceRelative.allRelative);
                    // //得到最终的字符串,格式如【Sheet1!B2:B8】,可用于其他特殊场景
                    // const setFormulaString = '' + sheetName + '!' + string;

                    //获取该区域每个单元格的值,返回的格式为:[[22],[33],[10],[5],[80],[1],[2]]
                    const arrVals = sheet.getArray(row, col, rowCount, colCount);
                    arrVals.forEach((item) => {
                        item.forEach((itemSon) => {
                            if (itemSon != null && itemSon != undefined) {
                                allCellVals.push(itemSon);
                            }
                        });
                    });
                }
            }

            //设置默认值
            if (allCellVals.length == 0) {
                if (funDefaultVal != null && allCellVals != undefined) {
                    allCellVals = [funDefaultVal];
                }
            }

            //返回的回调函数参数数据
            let retData = {
                arguments: arguments,
                cellObj: curCellObj,
                allCellVals: allCellVals,
            };

            return funCallback(spread, sheet, retData);
        };

        return cusFun;
    },
};

3.2.3 定义 initFormulas 公用方法

然后我们在CommFormula中定义initFormulas方法,具体方法实现如下所示:

import GC from '@grapecity/spread-sheets';

/**
 * 自定义函数公用方法
 */
export const CommFormula = {
    /**
     * 初始化自定义函数(需要在SpreadJS工作簿创建成功或加载模板成功后调用)
     * @param spread SpreadJS对象
     */
    initFormulas: (spread) => {
        // const sheetCount = spread.getSheetCount();

        // for (let i = 0; i < sheetCount; i++) {
        // 	let sheet = spread.getSheet(i);

        // 	CommFormula.allCusFormulas.forEach((item) => {
        // 		const cusFun = CommFormula.initForFun(
        // 			spread,
        // 			sheet,
        // 			item.funName,
        // 			item.funDesc,
        // 			item.funCallback,
        // 			item.funDefaultVal,
        // 			item.funMinArgs,
        // 			item.funMaxArgs,
        // 			item.funParams,
        // 			item.isContext,
        // 			item.isAcceptArea
        // 		);

        // 		sheet.addCustomFunction(new cusFun());
        // 	});
        // }

        CommFormula.allCusFormulas.forEach((item) => {
            const cusFun = CommFormula.initForFun(
                spread,
                item.funName,
                item.funDesc,
                item.funCallback,
                item.funDefaultVal,
                item.funMinArgs,
                item.funMaxArgs,
                item.funParams,
                item.isContext,
                item.isAcceptArea
            );

            //全局注册自定义公式
            GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction(item.funName, new cusFun());
        });
    },
};

3.2.4 定义 allCusFormulas 数组实现

然后我们在CommFormula中定义allCusFormulas数组,该数组就是我们要实现的自定义公式数组配置,也就是我们只需要操作该数组就可以实现自定义公式的增加或减少。

具体方法实现如下所示:

import GC from "@grapecity/spread-sheets";

/**
 * 自定义函数公用方法
 */
export const CommFormula = {
	/**
	 * 所以自定义函数集合
	 */
	allCusFormulas: [
		{
			funName: 'YjMax',
			funDesc: '获取所有选择的单元格中的最大值',
			funDefaultVal: null,
			funCallback: (spread, sheet, retData) => {
				return Math.max(...retData.allCellVals);
			},
		},
		{
			funName: 'YjMin',
			funDesc: '获取所有选择的单元格中的最小值',
			funCallback: (spread, sheet, retData) => {
				return Math.min(...retData.allCellVals);
			},
		},
		{
			funName: 'YjMid',
			funDesc: '获取所有选择的单元格中的中间值',
			funDefaultVal: null,
			funParams: [
				{
					name: 'cellContent1',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContent2',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContent3',
					repeatable: false,
					optional: false,
				},
			]
	] as any
};

3.2.5 完整代码

commFormula.ts文件的完整代码如下所示:

详情
import GC from '@grapecity/spread-sheets';

/**
 * 自定义函数公用方法
 */
export const CommFormula = {
	/**
	 * 初始化自定义函数(需要在SpreadJS工作簿创建成功或加载模板成功后调用)
	 * @param spread SpreadJS对象
	 */
	initFormulas: (spread) => {
		// const sheetCount = spread.getSheetCount();

		// for (let i = 0; i < sheetCount; i++) {
		// 	let sheet = spread.getSheet(i);

		// 	CommFormula.allCusFormulas.forEach((item) => {
		// 		const cusFun = CommFormula.initForFun(
		// 			spread,
		// 			sheet,
		// 			item.funName,
		// 			item.funDesc,
		// 			item.funCallback,
		// 			item.funDefaultVal,
		// 			item.funMinArgs,
		// 			item.funMaxArgs,
		// 			item.funParams,
		// 			item.isContext,
		// 			item.isAcceptArea
		// 		);

		// 		sheet.addCustomFunction(new cusFun());
		// 	});
		// }

		CommFormula.allCusFormulas.forEach((item) => {
			const cusFun = CommFormula.initForFun(
				spread,
				item.funName,
				item.funDesc,
				item.funCallback,
				item.funDefaultVal,
				item.funMinArgs,
				item.funMaxArgs,
				item.funParams,
				item.isContext,
				item.isAcceptArea
			);

			//全局注册自定义公式
			GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction(item.funName, new cusFun());
		});
	},

	/**
	 * 定义公用初始化自定义函数的方法
	 * @param spread SpreadJS对象
	 * @param funName 函数名称,如:YjMax
	 * @param funDesc 自定义函数描述
	 * @param funCallback 自定义函数回调函数,但函数返回了3个参数,第一个为Spread对象,第二个为Sheet对象,第三个为JSON对象(包含了arguments【原始参数数组】,cellObj【引用自定义公式的单元格索引对象】,allCellVals【所有参数值数组】)
	 * @param funDefaultVal 自定义函数返回的默认值,默认为:0(使用场景:如该自定义函数选择的单元格没有任何值,此时回调中的retData.allCellVals可能为[],因此就可以将默认值传回去,格式如[0.0])
	 * @param funMinArgs 最小参数个数
	 * @param funMaxArgs 最大参数个数
	 * @param funParams 自定义函数参数描述,默认为:[]
	 * @param isContext 是否返回上下文对象,默认为:true
	 * @param isAcceptArea 函数参数是否为区域单元格,默认为:true
	 * @returns
	 */
	initForFun: (
		spread,
		funName: string,
		funDesc: string,
		funCallback: (spread, sheet, retData) => any,
		funDefaultVal: number = 0,
		funMinArgs: number = 1,
		funMaxArgs: number = 1000,
		funParams: Array<any> = [],
		isContext: boolean = true,
		isAcceptArea: boolean = true
	) => {
		//声明自定义函数对象
		const cusFun = function () {
			this.name = funName;
			this.minArgs = funMinArgs;
			this.maxArgs = funMaxArgs;
		};
		//设置自定义函数的prototype属性
		cusFun.prototype = new GC.Spread.CalcEngine.Functions.Function(funName, funMinArgs, funMaxArgs);
		//设置自定义函数的描述信息,以便在输入自定义函数关键字的时候出现提示信息
		cusFun.prototype.description = function () {
			return {
				description: funDesc,
				parameters: funParams,
			};
		};
		//为true时,函数的计算依赖于上下文
		cusFun.prototype.isContextSensitive = function () {
			return isContext;
		};
		//函数的参数接受引用单元格区域
		cusFun.prototype.acceptsReference = function () {
			return isAcceptArea;
		};
		//自定义函数逻辑
		cusFun.prototype.evaluate = function () {
			// 获取该自定义公式被引用单元格的索引

			/**
			 * 说明:
			 *     如果isContext=true,则arguments的第一个参数为上下文参数,后面的参数才是自定义参数真正传入的参数
			 *         此时我们可以通过arguments获得当前sheet或spread对象,如下所示:
			 *             const sheet = arguments[0].source.getSheet();
			 *             const spread = arguments[0].source.getSheet().getParent();
			 *         也可以获取到公式单元格所在的索引,如下所示:
			 *             const curForRowIndex= arguments[0].row;
			 *             const curForColIndex= arguments[0].column;
			 *
			 *     如果isContext=false,则arguments所有参数都为自定义函数传进来的
			 *
			 * 可参考论坛:
			 *     https://gcdn.grapecity.com.cn/showtopic-154888-1-1.html
			 */

			//得到当前单元格引用公式所在的索引(注意:目前只有在isContext=true的时候才能通过第一个参数获取表单、工作簿以及单元格索引等信息)
			let curCellObj: { row: number; col: number; rowCount: number; colCount: number };

			let sheet;

			// //获取所有选择的单元格对象及其值,格式如:[{row:0,col:0,val:1}]
			// let allCells = [];
			//获取所有选择的单元格值,格式如:[1,2,3]
			let allCellVals = [];

			let length = arguments.length;
			for (let i = 1; i < length; i++) {
				const argument = arguments[i];
				if (typeof argument == 'string' || typeof argument == 'number') {
					allCellVals.push(argument);
				} else {
					//获取行索引、列索引、行数和列数
					const row = argument.getRow(),
						col = argument.getColumn(),
						rowCount = argument.getRowCount(),
						colCount = argument.getColumnCount();

					//参数为单元格区域时,通过argument也可以获取到上下文
					sheet = argument.getSource().getSheet();
					// const spread = argument.getSource().getSheet().getParent();
					// const sheetName = sheet.name();

					//通过getSpan方法获取合并单元格索引对象,如果为null,则说明该单元格没有合并(也就是行和列的数量都是1),如果不为null,则直接getSpan返回的数据就直接是:{ row: 0, col: 0, rowCount: 1, colCount: 1 }
					curCellObj = sheet.getSpan(arguments[0].row, arguments[0].column);
					if (curCellObj == null || curCellObj == undefined) {
						curCellObj = { row: arguments[0].row, col: arguments[0].column, rowCount: 1, colCount: 1 };
					}

					// //声明Range对象
					// const range = new GC.Spread.Sheets.Range(row, col, rowCount, colCount);
					// //得到类似【A1:A8】范围的字符串
					// const string = GC.Spread.Sheets.CalcEngine.rangeToFormula(range, 0, 0, GC.Spread.Sheets.CalcEngine.RangeReferenceRelative.allRelative);
					// //得到最终的字符串,格式如【Sheet1!B2:B8】,可用于其他特殊场景
					// const setFormulaString = '' + sheetName + '!' + string;

					//获取该区域每个单元格的值,返回的格式为:[[22],[33],[10],[5],[80],[1],[2]]
					const arrVals = sheet.getArray(row, col, rowCount, colCount);
					arrVals.forEach((item) => {
						item.forEach((itemSon) => {
							if (itemSon != null && itemSon != undefined) {
								allCellVals.push(itemSon);
							}
						});
					});
				}
			}

			//设置默认值
			if (allCellVals.length == 0) {
				if (funDefaultVal != null && allCellVals != undefined) {
					allCellVals = [funDefaultVal];
				}
			}

			//返回的回调函数参数数据
			let retData = {
				arguments: arguments,
				cellObj: curCellObj,
				allCellVals: allCellVals,
			};

			return funCallback(spread, sheet, retData);
		};

		return cusFun;
	},

	/**
	 * 供自定义函数内部使用的公用方法
	 */
	commFun: {
		/**
		 * 验证传入的内容是否为数字
		 * @param content 传入的内容
		 * @returns 返回true或false
		 */
		isNumber: (content) => {
			return /^(-?\d+)(\.\d+)?$/.test(content);
		},
		/**
		 * 根据试件尺寸计算面积
		 * @param content 传入的内容,格式如:φ100×200或150×150×150
		 * @returns 返回计算的面积
		 */
		getNum: (content) => {
			let ret, contentArr;
			const reg = /[×φ*]/g;

			try {
				contentArr = content.split(reg);

				let x;
				if (content.substring(0, 1) == 'φ') {
					x = contentArr[1] - 0;
					ret = (Math.PI * x * x) / 4;
				} else {
					ret = contentArr[0] - 0;
					x = contentArr[1] - 0;
					ret = ret * x;
				}
			} catch (e) {
				return 0;
			}

			return ret;
		},
	},

	/**
	 * 所以自定义函数集合
	 */
	allCusFormulas: [
		{
			funName: 'YjMax',
			funDesc: '获取所有选择的单元格中的最大值',
			funDefaultVal: null,
			funCallback: (spread, sheet, retData) => {
				return Math.max(...retData.allCellVals);
			},
		},
		{
			funName: 'YjMin',
			funDesc: '获取所有选择的单元格中的最小值',
			funCallback: (spread, sheet, retData) => {
				return Math.min(...retData.allCellVals);
			},
		},
		{
			funName: 'YjMid',
			funDesc: '获取所有选择的单元格中的中间值',
			funDefaultVal: null,
			funParams: [
				{
					name: 'cellContent1',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContent2',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContent3',
					repeatable: false,
					optional: false,
				},
			],
			funCallback: (spread, sheet, retData) => {
				//如果传递的参数小于3个,则返回0
				if (retData.allCellVals.length < 3) {
					return 0;
				}

				const maxVal = Math.max(...retData.allCellVals),
					minVal = Math.min(...retData.allCellVals);

				return retData.allCellVals[0] * 1 + retData.allCellVals[1] * 1 + retData.allCellVals[2] * 1 - maxVal * 1 - minVal * 1;
			},
		},
		{
			funName: 'YjGetNum',
			funDesc: '根据试件尺寸计算面积',
			funDefaultVal: null,
			funParams: [
				{
					name: 'cellContent',
					repeatable: false,
					optional: false,
				},
			],
			funCallback: (spread, sheet, retData) => {
				//如果传递的参数小于1个,则返回0
				if (retData.allCellVals.length < 1) {
					return 0;
				}

				return CommFormula.commFun.getNum(retData.allCellVals[0]);
			},
		},
		{
			funName: 'YjGetS',
			funDesc: '计算标准差',
			funDefaultVal: null,
			funCallback: (spread, sheet, retData) => {
				//得到所有参数中为数字的集合,即:数字参数才参与计算
				retData.allCellVals = retData.allCellVals.filter((item) => CommFormula.commFun.isNumber(item));

				//如果传递的参数小于1个,则返回''
				if (retData.allCellVals.length < 1) {
					return '';
				}

				//总和,参数个数
				let total = 0,
					len = retData.allCellVals.length;

				//平均值、求平方和
				let avgVal = 0,
					squVal = 0;

				for (let i = 0; i < len; i++) {
					total += retData.allCellVals[i] * 1;
				}
				avgVal = total / len;

				for (let i = 0; i < len; i++) {
					squVal += (retData.allCellVals[i] - avgVal) * (retData.allCellVals[i] - avgVal);
				}

				//标准差
				const ret = Math.sqrt(squVal / (len - 1));

				return !ret ? '' : ret;
			},
		},
		{
			funName: 'YjGetCv',
			funDesc: '计算变异系数Cv',
			funDefaultVal: null,
			funCallback: (spread, sheet, retData) => {
				//得到所有参数中为数字的集合,即:数字参数才参与计算
				retData.allCellVals = retData.allCellVals.filter((item) => CommFormula.commFun.isNumber(item));

				//如果传递的参数小于1个,则返回''
				if (retData.allCellVals.length < 1) {
					return '';
				}

				//总和,参数个数
				let total = 0,
					len = retData.allCellVals.length;

				//平均值、求平方和
				let avgVal = 0,
					squVal = 0;

				for (let i = 0; i < len; i++) {
					total = total + retData.allCellVals[i] * 1;
				}
				avgVal = total / len;

				for (let i = 0; i < len; i++) {
					squVal += (retData.allCellVals[i] - avgVal) * (retData.allCellVals[i] - avgVal);
				}

				//标准差
				const staVal = Math.sqrt(squVal / (len - 1));

				//变异系数
				const ret = (staVal * 100) / avgVal;

				return !ret ? '' : ret;
			},
		},
		{
			funName: 'YjInterpolationMethod',
			funDesc: '直线内插法(已知y求x)',
			funDefaultVal: null,
			funParams: [
				{
					name: 'cellContentX1',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContentX2',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContentY1',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContentY2',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContentY',
					repeatable: false,
					optional: false,
				},
			],
			funCallback: (spread, sheet, retData) => {
				//如果传递的参数小于5个,则返回/
				if (retData.allCellVals.length < 5) {
					return '/';
				}

				const x1 = retData.allCellVals[0],
					x2 = retData.allCellVals[1],
					y1 = retData.allCellVals[2],
					y2 = retData.allCellVals[3],
					y = retData.allCellVals[4];

				const ret = ((y - y1) * (x2 - x1)) / (y2 - y1) + x1 * 1;

				return isNaN(ret) ? '/' : ret;
			},
		},
		{
			funName: 'YjInterpolationMethod_Y',
			funDesc: '直线内插法(已知x求y)',
			funDefaultVal: null,
			funParams: [
				{
					name: 'cellContentX1',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContentX2',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContentY1',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContentY2',
					repeatable: false,
					optional: false,
				},
				{
					name: 'cellContentX',
					repeatable: false,
					optional: false,
				},
			],
			funCallback: (spread, sheet, retData) => {
				//如果传递的参数小于5个,则返回/
				if (retData.allCellVals.length < 5) {
					return '/';
				}

				const x1 = retData.allCellVals[0],
					x2 = retData.allCellVals[1],
					y1 = retData.allCellVals[2],
					y2 = retData.allCellVals[3],
					x = retData.allCellVals[4];

				const ret = ((x - x1) * (y1 - y2)) / (x1 - x2) + y1 * 1;

				return isNaN(ret) ? '/' : ret;
			},
		},
	] as any,
};

3.2.6 演示效果

如下所示为输入关键字YJ后出现的自定义函数名称列表:

演示效果
演示效果

如下所示效果为得到选择单元格中的最大值:

演示效果
演示效果

3.2.7 allCusFormulas 配置说明

该数组中的各个配置,和initForFun公用方法中的参数保持一致的,具体说明如下所示:

{
    funName: 'YjMid', //必填,自定义公式名称
    funDesc: '测试的描述信息', //选填,自定义公式的描述内容
    /**
     * 选填,返回默认值,默认为:0
     *     如果当前公式没有选择任何单元格进行传参,并且该参数设置为了null,则funCallback中的retData.allCellVals值为:[]
     *     如果当前公式没有选择任何单元格进行传参,并且没有设置该参数,则funCallback中的retData.allCellVals值为:[0]
     */
    funDefaultVal: null,
    funMinArgs: 1, //选填,自定义公式传入的最小参数个数,默认为:1
    funMaxArgs: 3, //选填,自定义公式传入的最大参数个数,默认为:1000
    isContext: false, //选填,函数的计算是否依赖于上下文,默认为:true
    isAcceptArea: false, //选填,函数参数是否为区域单元格,默认为:true
    funParams: [
        //选填,自定义公式的参数说明,默认为:[]
        {
            name: 'cellContent1', //参数1的名称
            repeatable: false, //是否可重复
            optional: false, //是否可为空
        },
        {
            name: 'cellContent2', //参数2的名称
            repeatable: false,
            optional: false,
        },
        {
            name: 'cellContent3', //参数3的名称
            repeatable: false,
            optional: false,
        },
    ],
    /**
     * 具体公式逻辑
     * @param spread 返回的Spread对象
     * @param sheet 返回的当前表单对象
     * @param retData //返回的数据对象,格式为:
            {
                arguments: ,//原始arguments参数对象
                allCellVals: [],//选中所有单元格的值数组
            }
        * @returns
        */
    funCallback: (spread, sheet, retData) => {
        //下面代码是具体实现逻辑

        //如果传递的参数小于3个,则返回0
        if (retData.allCellVals.length < 3) {
            return 0;
        }

        const maxVal = Math.max(...retData.allCellVals),
            minVal = Math.min(...retData.allCellVals);

        return retData.allCellVals[0] * 1 + retData.allCellVals[1] * 1 + retData.allCellVals[2] * 1 - maxVal * 1 - minVal * 1;
    },
}

3.2.8 常用配置

大多数情况,我们会用到如下常用的配置:

import GC from '@grapecity/spread-sheets';

/**
 * 自定义函数公用方法
 */
export const CommFormula = {
    /**
     * 所以自定义函数集合
     */
    allCusFormulas: [
        {
            funName: 'YjMax',
            funDesc: '获取所有选择的单元格中的最大值',
            funDefaultVal: null,
            funCallback: (spread, sheet, retData) => {
                return Math.max(...retData.allCellVals);
            },
        },
        {
            funName: 'YjMid',
            funDesc: '获取所有选择的单元格中的中间值',
            funDefaultVal: null,
            funParams: [
                {
                    name: 'cellContent1',
                    repeatable: false,
                    optional: false,
                },
                {
                    name: 'cellContent2',
                    repeatable: false,
                    optional: false,
                },
            ],
            funCallback: (spread, sheet, retData) => {
                //如果传递的参数小于2个,则返回0
                if (retData.allCellVals.length < 2) {
                    return 0;
                }

                const maxVal = Math.max(...retData.allCellVals),
                    minVal = Math.min(...retData.allCellVals);

                return retData.allCellVals[0] * 1 + retData.allCellVals[1] * 1 + retData.allCellVals[2] * 1 - maxVal * 1 - minVal * 1;
            },
        },
    ] as any,
};

3.3、初始化调用

如果我们想在所有的 Sheet 中都使用自定义的所有公式,那么就需要在SpreadJS 初始化完成后或加载模板成功后调用一次initFormulas方法即可。

//导入自定义公式公用方法
import { CommFormula } from './common/commFormula';

//初始化调用
CommFormula.initFormulas(spread);

3.4、全局注册公式

在上述过程中,我们是将所有的自定义公式注册到每个 Sheet 中的sheet.addCustomFunction(new cusFun());,这样稍显麻烦,是否可以全局注册所有的自定义公式呢?答案是肯定可以的。

全局注册自定义公式

我们可通过GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction进行全局注册,因此,我们可以将上述的initFormulas方法进行改造,具体如下所示:

import GC from '@grapecity/spread-sheets';

/**
 * 自定义函数公用方法
 */
export const CommFormula = {
    /**
     * 初始化自定义函数(需要在SpreadJS工作簿创建成功或加载模板成功后调用)
     * @param spread SpreadJS对象
     */
    initFormulas: (spread) => {
        CommFormula.allCusFormulas.forEach((item) => {
            const cusFun = CommFormula.initForFun(
                spread,
                item.funName,
                item.funDesc,
                item.funCallback,
                item.funDefaultVal,
                item.funMinArgs,
                item.funMaxArgs,
                item.funParams,
                item.isContext,
                item.isAcceptArea
            );

            //全局注册自定义公式
            GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction(item.funName, new cusFun());
        });
    },
};

4、自定义选择

适用场景说明

有时候,我们需要在 SpreadJS 工作簿外部区域自定义一个单元格范围选择的控件,然后得到选择后的结果。

结果可以是选择后的字符串,也可以是选择后的所有单元格对象集合

4.1、定义外部选择容器

我们可以在 SpreadJS 工作簿以外的任意区域定义一个容器(该容器就是我们触发选择单元格的控件),假设我们定义为一个div,具体代码如下所示:

<div id="demoId" spellcheck="false" class="border:1px solid #f00"></div>

4.2、初始化选择容器

此时我们就应该初始化该容器了,具体代码如下所示:

const inputObj = new GC.Spread.Sheets.FormulaTextBox.FormulaTextBox(document.getElementById('demoId'), { rangeSelectMode: true });
inputObj.workbook(spread);

到此,容器就初始化完成了,具体效果如下图所示:

初始化控件效果
初始化控件效果
选择单元格范围效果
选择单元格范围效果

4.3、获取选择的结果

此时我们可以通过初始化控件inputObj获取字符串结果,如下所示:

const inputObjVal = inputObj.text(); //结果为:Sheet1!C2:C3,Sheet1!D4:D5,Sheet1!E6:E7,Sheet1!E3,Sheet1!F3,Sheet1!H3,Sheet1!G5,Sheet1!G7

说明

上述代码中,我们是通过text方法获取的值(官方文档:https://demo.grapecity.com.cn/spreadjs/help/api/classes/GC.Spread.Sheets.FormulaTextBox.FormulaTextBox#textopen in new window)。

同理,我们也可以通过text方法设置值,如下所示:

inputObj.text('Sheet1!C2:C3,Sheet1!D4:D5,Sheet1!E6:E7,Sheet1!E3,Sheet1!F3,Sheet1!H3,Sheet1!G5,Sheet1!G7');

4.4、转换为单元格集合对象

通过上述步骤我们可以得到选择单元格范围的字符串结果,但很多时候我们需要的结果是单元格对象集合,有没有办法实现呢?答案是肯定的。

具体实现如下所示:

//字符串结果值
const inputObjVal = inputObj.text();

//注意:此处演示的是以激活表单为例,具体的可能会涉及到多个表单的选择结果,因此需要另写逻辑

//当前激活表单
const sheet = spread.getActiveSheet();

//注意:results可能会包含多个表单的结果,此处以激活表单为例

//通过formulaToRanges将字符串结果转换为选择范围集合
const results = GC.Spread.Sheets.CalcEngine.formulaToRanges(sheet, inputObjVal);

if (results.length > 0) {
    //得到最终的单元格对象集合
    const allCells = getAllCellObjsByRanges(sheet, results[0].ranges);
}

说明

上述代码中我们用到了formulaToRanges方法将字符串结果转换为单元格选择的范围集合,具体可参见官方文档:https://demo.grapecity.com.cn/spreadjs/help/api/modules/GC.Spread.Sheets.CalcEngine#formulatorangesopen in new window

上述方法中的getAllCellObjsByRanges就是【1.1.1、获取某表单中某范围集合中所有的单元格坐标对象集合】中的方法。

4.5、销毁控件对象

说明

假设我们有这样的使用场景:

可以动态增加很多个选择控件,然后也可以删除某些选择控件,在我们删除某个选择控件之前,一定要先将该控件销毁掉,否则可能会造成内存溢出的情况。

具体销毁控件代码如下所示:

//定义控件数组集合
let cellObjs = [{ inputObj: null },{},……];

//销毁掉要删除的选择控件对象
cellObjs.value[0].inputObj.destroy();

//从数组中移除
cellObjs.value.splice(0, 1);



 
 




5、打印

适用场景说明

根据业务系统的打印需求,常常需要将打印的左边距设置为25mm,其他边距为15mm

根据这一需求,在使用浏览器打印预览前,就需要先将浏览器的打印边距设置为,然后再通过 SpreadJS 去设置各个边距的距离。

具体实现步骤如下。

5.1、设置浏览器打印边距

首先我们需要将浏览器的各个边距设置为0,或者将边距直接选择为

如下图所示:

设置浏览器打印边距
设置浏览器打印边距

5.2、设置 SpreadJS 的打印配置

接下来我们就可以通过 SpreadJS 去设置打印的相关配置,具体实现如下代码所示:

/**
 * 打印所有表单
 */
const print = () => {
    //得到Sheet个数
    const sheetCount = spreadObj.getSheetCount();

    for (let i = 0; i < sheetCount; i++) {
        //获取每个表单的打印信息
        let printInfo = spreadObj.getSheet(i).printInfo();

        //隐藏行和列的头部信息
        printInfo.showRowHeader(GC.Spread.Sheets.Print.PrintVisibilityType.hide);
        printInfo.showColumnHeader(GC.Spread.Sheets.Print.PrintVisibilityType.hide);

        //设置SpreadJS自身的边距为合适的距离
        printInfo.margin({
            /**
             * 设置默认边距
             * 单位是:以百分之一英寸为单位
             *      也就是,如果我们需要设置左边距为25mm,需要将25mm先转换为英寸,然后再用这个英寸*100就是要设置的边距
             *      即:(25mm转换为英寸的结果)*100
             *
             * 参考文档:
             *     https://demo.grapecity.com.cn/spreadjs/help/api/classes/GC.Spread.Sheets.Print.PrintInfo#margin
             *     https://www.67tool.com/converter/length
             */
            top: 0.5905511999999999 * 100,
            bottom: 0.5905511999999999 * 100,
            left: 0.9842520000000001 * 100,
            right: 0.5905511999999999 * 100,

            // top: 0,
            // bottom: 0,
            // left: 0,
            // right: 0,
            header: 0,
            footer: 0,
        });

        //设置打印的纸张
        printInfo.paperSize(new GC.Spread.Sheets.Print.PaperSize(GC.Spread.Sheets.Print.PaperKind.a4));
    }

    //调用打印方法
    spreadObj.print();
};








 
 

 
 
 
 
 
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 
 
 

说明信息:

  • showRowHeader 方法:代表是否显示行标题;
  • showColumnHeader 方法:代表是否显示列标题;
  • margin 方法:代表设置打印的各个边距;
    • top 属性:顶部的边距,此处我们设置的值为0.5905511999999999 * 100,约等于 15mm,之所以要这样设置,是因为 top 属性的单位为百分之一英寸,因此就需要先将 15mm 换算为英寸,然后再乘以 100,即:(15mm 转换为英寸的结果)*100;
    • right 属性:同 top 属性;
    • bottom 属性:同 top 属性;
    • left 属性:同 top 属性,只不过 left 的边距是 25mm,因此计算公式为:(25mm 转换为英寸的结果)*100。
  • paperSize 方法:设置打印的纸张类型,通常我们设置为 A4 类型,即该方法的参数为:new GC.Spread.Sheets.Print.PaperSize(GC.Spread.Sheets.Print.PaperKind.a4)
  • print 方法:代表打印开始,调用该方法后,浏览器就会呈现打印预览窗口。

5.3、注意事项

如果想要内容正好撑满整个打印预览界面,那么就需要在编辑模板的时候严格按照规定的内容宽度和高度进行编辑,否则可能会呈现多页的问题。

如下图所示,编辑的内容不超过蓝色虚线的范围就能在一页中撑满整个打印页面:

内容范围
内容范围

5.4、打印效果

最终打印的效果如下图所示:

打印效果
打印效果

6、导出 Excel

导出相关需求说明

根据业务系统的导出需求,通常需要将文件导出为 Excel。

但是在导出为 Excel 文件的过程中,可能需要我们实现各类的功能,如下需求所示:

  • 涉及到引用自定义公式的单元格,其值应该在导出后保留下来;
  • ……

需求分析:

由于在 SpreadJS 编辑的过程中,我们可能用到了各种自定义公式,这样按照常规导出为 Excel 的话,涉及到引用自定义公式的单元格的值是没办法直接导出呈现的,于是就需要我们自己去实现。

6.1、保留自定义公式值

实现思路说明:

  1. 首先将原始的工作簿导出为 JSON 对象;
  2. 然后将导出的 JSON 对象导入临时工作簿(后面的所有操作都是对这个临时工作簿进处理,这样做的目的是避免改变原来工作簿的数据);
  3. 得到临时工作簿的表单(Sheet 数量);
  4. 循环处理每个表单;
  5. 在每个表单中分别循环所有的行和列,目的是得到所有的单元格;
  6. 得到单元格的行和列索引后,就可以根据方法getFormula得到该单元格是否有公式;
  7. 如果得到的公式不为null,则说明该单元格引用了某公式;
  8. 此时我们可以先获取到该单元格的值,目的是后续会给该单元格赋值;
  9. 然后我们可以使用方法formula将该单元格的公式移除;
  10. 接着就将之前我们获取到的单元格值为该单元格重新赋值;
  11. 到此,实现的主要步骤就基本完成了,后续的代码主要就是导出的代码了。

注意事项

在移除单元格引用的公式时,可能我们并不想把所有的公式都移除掉,如 Excel 内置的公式就保留,只移除自定义的公式。

针对这个需求其实也好办,我们只需要提供一个自定义公式名称的数组集合,然后在移除的时候通过some函数indexOf函数去验证即可。

最后导出完成后,记得销毁临时工作簿。

具体实现参见下面的方法exportToExcelPlus

6.2、具体方法实现

具体方法实现如下代码所示:

/**
 * 导出为Excel文件(升级版)
 * 如支持导出自定义公式的值等功能
 * @param spread 原始Spread对象
 * @param GC Spread的GC对象
 * @param fileName 导出的文件名称,不传则为第一个表单的名称
 */
const exportToExcelPlus = (spread: any, GC: any, fileName: string = '') => {
    //导出模板为JSON对象,此处设置了参数includeBindingSource,该参数代表包含数据绑定的值
    const json = spread.toJSON({ includeBindingSource: true });

    //需要移除的自定义公式名称集合(该变量的作用是,在导出Excel前,将工作簿中所有的自定义公式移除掉,其他内置的公式保留)
    const removeCustomFormulas = ['YJMAX', 'YJMIN', 'YJMID', 'YJGETNUM', 'YJGETS', 'YJGETCV', 'YJINTERPOLATIONMETHOD', 'YJINTERPOLATIONMETHOD_Y'];

    //临时工作簿
    const tempSpread = new GC.Spread.Sheets.Workbook();
    tempSpread.fromJSON(json);

    //暂停绘制
    tempSpread.suspendPaint();

    //获取Sheet数量
    const sheetCount = tempSpread.getSheetCount();

    for (let iSheet = 0; iSheet < sheetCount; iSheet++) {
        //当前表单
        const sheet = tempSpread.getSheet(iSheet);

        //暂停公式的计算,提高性能
        sheet.suspendCalcService();

        for (var i = 0; i < sheet.getRowCount(); i++) {
            for (var j = 0; j < sheet.getColumnCount(); j++) {
                //获取单元格的公式
                const cellFormula = sheet.getFormula(i, j);

                //如果cellFormula不为null或undefined,则说明该单元格引用了公式
                if (cellFormula != null && cellFormula != undefined && removeCustomFormulas.some((item) => cellFormula.indexOf(item) > -1)) {
                    //获取单元格的值
                    const cellVal = sheet.getValue(i, j);

                    //移除单元格引用的公式
                    sheet.getCell(i, j).formula(undefined);

                    //设置单元格的值
                    sheet.setValue(i, j, cellVal);
                }
            }
        }

        //恢复计算
        sheet.resumeCalcService(false);
    }

    //恢复绘制
    tempSpread.resumePaint();

    if (fileName == '') {
        fileName = tempSpread.getSheet(0).name();
    }

    let options = {
        fileType: GC.Spread.Sheets.FileType.excel,
        includeBindingSource: true,
        includeStyles: true,
        includeFormulas: true,
        saveAsView: false,
        rowHeadersAsFrozenColumns: false,
        columnHeadersAsFrozenRows: false,
        includeAutoMergedCells: false,
        includeCalcModelCache: false,
        includeUnusedNames: true,
        includeEmptyRegionCells: true,
    };

    tempSpread.export(
        (blob) => {
            saveAs(blob, `${fileName}.xlsx`);
        },
        () => {},
        options
    );

    //销毁临时工作簿
    tempSpread.destroy();
};

说明

上述方法实现中,最后导出使用到了saveAs保存组件,请自行安装该组件yarn add file-saver


7、循环生成指定区域

循环生成指定区域相关需求说明

在有些业务场景下,我们可能有这样一个需求:鼠标选中表单中某个区域,然后在这个区域下方循环生成 N 个选中的区域内容。

7.1、实现原理

首先,我们应该得到指定区域的单元格 JSON 对象,格式为:{ row: 0, col: 0, rowCount: 3, colCount: 10 }

想要得到指定区域的单元格 JSON 对象,可以通过如下方式得到:

  • 如果选择指定区域方式为鼠标选择的,可以通过方法sheet.getSelections()[0]获得;

  • 如果选择指定区域方式为自定义选择单元格范围控件,可以通过如下方式获得(下面代码中的getCellObjByRangeStr就是1.1.4、获取单元格范围控件的单元格对象中提到的方法):

    //得到指定单元格范围的字符串,格式为:=Sheet1!B4:K4
    const formulaStr = inputObj.text();
    
    //得到指定区域的JSON单元格对象
    const cyclicRangeJsonObj = getCellObjByRangeStr(sheet, formulaStr)[0];
    

7.2、具体方法实现

具体方法实现如下代码所示:

/**
 * 循环生成指定区域内容
 * @param spread 主Spread对象
 * @param GC Spread的GC对象
 * @param sheet Sheet表单对象
 * @param cyclicCount 循环生成的个数
 * @param cyclicRangeJsonObj 指定生成的区域JSON对象(格式为:{row:0,col:0,rowCount:3,colCount:10})
 */
const createCyclicArea = (spread: any, GC: any, sheet: any, cyclicCount: number, cyclicRangeJsonObj: any) => {
    //暂停绘制
    spread.suspendPaint();

    //循环生成点数数据行
    for (let i = 1; i <= cyclicCount; i++) {
        //需要插入新行的行索引位置
        const addNewRowIndex = cyclicRangeJsonObj.row + i * cyclicRangeJsonObj.rowCount;

        //插入新行
        sheet.addRows(addNewRowIndex, cyclicRangeJsonObj.rowCount);

        //要复制和粘贴的范围
        const fromRanges = [
                new GC.Spread.Sheets.Range(cyclicRangeJsonObj.row, cyclicRangeJsonObj.col, cyclicRangeJsonObj.rowCount, cyclicRangeJsonObj.colCount),
            ],
            pastedRanges = [
                new GC.Spread.Sheets.Range(addNewRowIndex, cyclicRangeJsonObj.col, cyclicRangeJsonObj.rowCount, cyclicRangeJsonObj.colCount),
            ];

        //执行复制粘贴命令
        spread.commandManager().execute({
            cmd: 'clipboardPaste',
            sheetName: sheet.name(), //此参数的作用是:粘贴到某个名称的Sheet中
            fromSheet: sheet,
            fromRanges: fromRanges,
            pastedRanges: pastedRanges,
            isCutting: false,
            clipboardText: '',
            pasteOption: GC.Spread.Sheets.ClipboardPasteOptions.all,
        });
    }

    //取消选中状态
    sheet.clearSelection();

    //恢复绘制
    spread.resumePaint();
};

调用:

//得到指定单元格范围的字符串,格式为:=Sheet1!B4:K4
const formulaStr = inputObj.text();

//得到指定区域的JSON单元格对象
const cyclicRangeJsonObj = getCellObjByRangeStr(sheet, formulaStr)[0];

//循环生成指定区域内容
createCyclicArea(spreadObj, GC, sheet, 3, cyclicRangeJsonObj);
最终效果
最终效果

8、隐藏特定的数据行

隐藏特定的数据行相关需求说明

在有些业务场景下,我们可能有这样一个需求:

鼠标选中某一列中的单元格区域,然后判断选中的每个单元格是否都没有值,如果都没有值,然后根据如下情况来隐藏该单元格所在的行:

  1. 如果某行选中的单元格只有一个,并且该单元格所在行的第一个单元格没有跨行,则直接判断该单元格是否没有值,没有的话则隐藏该行,如下图所示:
    示例
    上图所示中,序号 7 所在的行就会隐藏。

  2. 如果某行选中的单元格有 1 个及以上,并且该单元格所在行的第一个单元格没有跨行,则需要判断选中这行的所有单元格是否都没有值,都没有的话则隐藏该行,如果其中一个单元格有值的话就不做隐藏处理,如下图所示:
    示例
    上图所示中,序号 20 所在的行就会隐藏。

  3. 如果选中的某个单元格所在行的第一个单元格跨行了,则需要判断第一个跨行单元格总共包含了多少个选中的单元格(假设为 10 个),则需要判断这 10 个单元格是否都没有值,都没有的话则隐藏该行,如果其中一个单元格有值的话就不做隐藏处理,如下图所示:
    示例
    上图所示中,序号 27~31 所在的行就会隐藏。

具体方法实现如下代码所示:

/**
 * 隐藏特定行(检查选中的单元格的值是否为空,是则隐藏单元格所在的行,只要有值,就不隐藏)
 * @param sheet 表单对象
 * @param selectRanges 单元格范围集合,格式如:[{ row: 0, col: 0, rowCount: 2, colCount: 2 }]
 * @returns
 */
const hideGivenRows = (sheet: any, selectRanges: Array<{ col: number; colCount: number; row: number; rowCount: number }>) => {
    //暂停绘制(表单级)
    sheet.suspendPaint();

    //得到所有验证单元格对象集合
    const checkRetCellObjs = getAllCellObjsByRanges(sheet, selectRanges);

    /**
     * 检查单元格的值是有为空
     * @param row 单元格行索引
     * @param col 单元格列索引
     */
    const checkCellIsEmpty = (row: number, col: number) => {
        let retIsEmpty: boolean = false;

        const curCheckCellVal = sheet.getValue(row, col);
        if (curCheckCellVal == null || /^ *$/.test(curCheckCellVal)) {
            retIsEmpty = true;
        }

        return retIsEmpty;
    };

    //记录检查单元格对应的第一个单元格跨行数量、第一个单元格行索引、对应跨行所有检查的单元格对象集合
    let firstCellSpanCount = 1,
        firstCellSpanRow = 0,
        firstCellSpanCheckObj: Array<{
            row: number;
            col: number;
            rowCount: number;
            colCount: number;
            isEmpty: boolean;
        }> = [];

    //记录当前Sheet所有隐藏的行索引集合(方便后期还原显示)
    let curSheetAllHideRowsIndex: Array<number> = [];

    //获取所有需要重新排序的行索引(个性需求:如果第一列为“序号列”,则序号列的单元格值按照1、2、3、4、5……进行重新排序)
    let allCellRowIndexNew: Array<number> = [];

    //循环所有选择的单元格
    checkRetCellObjs.forEach((item: { row: number; col: number; rowCount: number; colCount: number }) => {
        //获取当前行对应的第一个单元格合并对象,用于判断第一个单元格是否跨行了
        const firstCellSpanObj = sheet.getSpan(item.row, 0);

        //对应的第一个单元格没有跨行或跨行为1的时候
        if (firstCellSpanObj == null || firstCellSpanObj.rowCount == 1) {
            //当前行所有检查的单元格对象集合
            const curRowAllCheckCellObjs = checkRetCellObjs.filter((itemChk: any) => itemChk.row == item.row);

            //该行只有一个检查单元格的时候,直接判断该单元格是否有值
            if (curRowAllCheckCellObjs.length == 1) {
                //记录隐藏的行索引
                if (!curSheetAllHideRowsIndex.some((itemHideRows) => itemHideRows == item.row) && checkCellIsEmpty(item.row, item.col)) {
                    curSheetAllHideRowsIndex.push(item.row);
                } else {
                    //记录显示的行索引
                    if (!allCellRowIndexNew.some((itemNew) => itemNew == item.row)) {
                        allCellRowIndexNew.push(item.row);
                    }
                }
            }
            //该行有多个检查单元格的时候,需要验证该行所有检查单元格是否都为空
            else if (curRowAllCheckCellObjs.length > 1) {
                //是否所有单元格都为空
                let isAllCellIsEmpty = true;

                curRowAllCheckCellObjs.forEach((item: any) => {
                    if (!checkCellIsEmpty(item.row, item.col)) {
                        isAllCellIsEmpty = false;
                    }
                });

                //记录隐藏的行索引
                if (!curSheetAllHideRowsIndex.some((itemHideRows) => itemHideRows == item.row) && isAllCellIsEmpty) {
                    curSheetAllHideRowsIndex.push(item.row);
                } else {
                    //记录显示的行索引
                    if (!isAllCellIsEmpty && !allCellRowIndexNew.some((itemNew) => itemNew == item.row)) {
                        allCellRowIndexNew.push(item.row);
                    }
                }
            }
        }
        //对应的第一个单元格有跨行
        else if (firstCellSpanObj != null && firstCellSpanObj.rowCount > 1) {
            //记录变量firstCellSpanCount的值,代表跨行的数量
            if (firstCellSpanCount == 1) {
                firstCellSpanCount = firstCellSpanObj.rowCount;
                firstCellSpanRow = item.row;

                //记录显示的行索引
                if (!allCellRowIndexNew.some((itemNew) => itemNew == item.row)) {
                    allCellRowIndexNew.push(item.row);
                }
            }

            //当前行<=第一个单元格跨行的最后一行索引
            if (item.row <= firstCellSpanRow + firstCellSpanCount - 1) {
                //如果firstCellSpanCheckObj中没有包含当前行的任何检查单元格
                if (!firstCellSpanCheckObj.some((itemChk) => itemChk.row == item.row)) {
                    //当前行所有检查的单元格集合
                    const curRowAllChkCells = checkRetCellObjs.filter((itemRetChk: any) => itemRetChk.row == item.row);
                    curRowAllChkCells.forEach((itemRowAllCells: any) => {
                        firstCellSpanCheckObj.push({
                            row: itemRowAllCells.row,
                            col: itemRowAllCells.col,
                            rowCount: itemRowAllCells.rowCount,
                            colCount: itemRowAllCells.colCount,
                            isEmpty: checkCellIsEmpty(itemRowAllCells.row, itemRowAllCells.col),
                        });
                    });
                }

                //循环到跨行的最后一行,并且检查的单元格为该行最后一个单元格的时候
                const curRowAllCheckCells = checkRetCellObjs.filter((itemAllCells: any) => itemAllCells.row == item.row);
                if (item.row == firstCellSpanRow + firstCellSpanCount - 1 && curRowAllCheckCells[curRowAllCheckCells.length - 1].col == item.col) {
                    //如果firstCellSpanCheckObj中所有isEmpty为true(代表为空)的数量等于firstCellSpanCheckObj的数量,则说明这一组跨行的所有检查的单元格的值都为空,此时就需要隐藏这一组的所有行
                    if (firstCellSpanCheckObj.filter((itemSpan: any) => itemSpan.isEmpty == true).length == firstCellSpanCheckObj.length) {
                        firstCellSpanCheckObj.forEach((itemHide: any) => {
                            //记录隐藏的行索引
                            if (!curSheetAllHideRowsIndex.some((itemHideRows) => itemHideRows == itemHide.row)) {
                                curSheetAllHideRowsIndex.push(itemHide.row);

                                //移除allCellRowIndexNew中的行索引
                                if (allCellRowIndexNew.indexOf(itemHide.row) > -1) {
                                    allCellRowIndexNew.splice(allCellRowIndexNew.indexOf(itemHide.row), 1);
                                }
                            }
                        });
                    }

                    //还原变量初始值,方便记录下一组跨行的单元格检查
                    firstCellSpanCount = 1;
                    firstCellSpanRow = 0;
                    firstCellSpanCheckObj = [];
                }
            }
        }
    });

    //统一隐藏行
    curSheetAllHideRowsIndex.forEach((item: number) => {
        sheet.setRowVisible(item, false);
    });

    /**
     * 扩展需求:
     *     如果第一列为“序号列”,则序号列的单元格值按照1、2、3、4、5……进行重新排序
     */
    if (curSheetAllHideRowsIndex.length > 0) {
        allCellRowIndexNew.forEach((item, index) => {
            sheet.setValue(item, 0, index + 1);
        });
    }

    //恢复绘制(表单级)
    sheet.resumePaint();

    //返回所有隐藏的行索引集合
    return curSheetAllHideRowsIndex;
};

提示信息

上述代码中getAllCellObjsByRanges就是1.1.1、获取某表单中某范围集合中所有的单元格坐标对象集合中提到的方法。

最终效果
最终效果

9、监听事件

9.1、监听 Sheet 各种状态

监听 Sheet 各种状态相关需求说明

在有些业务场景下,我们可能需要监听 Sheet 的激活事件、Sheet 的添加事件、Sheet 的删除事件以及 Sheet 的拖动事件来做一些特定的业务处理。

此时我们就可以通过使用ActiveSheetChangedSheetChangedSheetMoved这三个监听事件来实现。

所有监听事件的文档参见官方地址:https://demo.grapecity.com.cn/spreadjs/help/api/classes/GC.Spread.Sheets.Eventsopen in new window

  • ActiveSheetChanged:

    spread.bind(GC.Spread.Sheets.Events.ActiveSheetChanged, function (e, args) {
        /**
         * args参数包含的属性:newSheet和oldSheet
         */
        console.log('激活了Sheet,返回的参数为:', args, spread.getActiveSheetIndex());
    });
    
  • SheetChanged:

    spreadObj.bind(GC.Spread.Sheets.Events.SheetChanged, function (e, args) {
        /**
         * args参数包含的属性:
         *     args.propertyName为insertSheet时:propertyName、sheetIndex、sheetName和sheetPosition
         *     args.propertyName为deleteSheet时:propertyName、sheetIndex、sheetName和sheetPosition
         *     args.propertyName为isSelected时:newValue、oldValue、propertyName、sheetIndex、sheetName和sheetPosition
         */
    
        if (args.propertyName == 'insertSheet') {
            console.log('添加了Sheet,返回的参数为:', args);
        } else if (args.propertyName == 'deleteSheet') {
            console.log('删除了Sheet,返回的参数为:', args);
        }
        // else if (args.propertyName == 'isSelected' && args.newValue && !args.oldValue) {
        //     console.log('切换了Sheet(切换后),返回的参数为:', args);
        // } else if (args.propertyName == 'isSelected' && !args.newValue && args.oldValue) {
        //     console.log('切换了Sheet(切换前),返回的参数为:', args);
        // }
    });
    
  • SheetMoved:

    spread.bind(GC.Spread.Sheets.Events.SheetMoved, function (e, args) {
        /**
         * args参数包含的属性:newIndex、oldIndex、sheet和sheetName
         */
        console.log('拖动了Sheet,返回的参数为:', args);
    });
    

通过上述三个监听事件我们就可以实现对 Sheet 的各个状态(激活状态、添加状态、删除状态和拖动状态)进行监听了。

如下代码所示就是记录这四个状态下的演示效果:

//模拟每个Sheet记录一个值
let data = ['Sheet1', 'Sheet2', 'Sheet3'];

//当前激活的Sheet索引
let activeIndex = 0;

spread.bind(GC.Spread.Sheets.Events.ActiveSheetChanged, function (e, args) {
    /**
     * args参数包含的属性:newSheet和oldSheet
     */
    console.log('激活了Sheet,返回的参数为:', args, spread.getActiveSheetIndex());

    activeIndex.value = spread.getActiveSheetIndex();
});
spreadObj.bind(GC.Spread.Sheets.Events.SheetChanged, function (e, args) {
    /**
     * args参数包含的属性:
     *     args.propertyName为insertSheet时:propertyName、sheetIndex、sheetName和sheetPosition
     *     args.propertyName为deleteSheet时:propertyName、sheetIndex、sheetName和sheetPosition
     *     args.propertyName为isSelected时:newValue、oldValue、propertyName、sheetIndex、sheetName和sheetPosition
     */

    if (args.propertyName == 'insertSheet') {
        console.log('添加了Sheet,返回的参数为:', args);

        data.value.push(`Sheet${args.sheetIndex + 1}`);
    } else if (args.propertyName == 'deleteSheet') {
        console.log('删除了Sheet,返回的参数为:', args);

        data.value.splice(args.sheetIndex, 1);
    }
    // else if (args.propertyName == 'isSelected' && args.newValue && !args.oldValue) {
    //     console.log('切换了Sheet(切换后),返回的参数为:', args);
    // } else if (args.propertyName == 'isSelected' && !args.newValue && args.oldValue) {
    //     console.log('切换了Sheet(切换前),返回的参数为:', args);
    // }

    // console.log(args.propertyName, args.sheetIndex, args.sheetName, args.newValue, args.oldValue, args.sheetPosition);
    // console.log(args);
});
spread.bind(GC.Spread.Sheets.Events.SheetMoved, function (e, args) {
    /**
     * args参数包含的属性:newIndex、oldIndex、sheet和sheetName
     */
    console.log('拖动了Sheet,返回的参数为:', args);

    activeIndex.value = args.newIndex;

    moveItemInArray(data.value, args.oldIndex, args.newIndex);
});

/**
 * 移动数组中某个元素到另一个索引为止
 * @param array 原数组
 * @param fromIndex 需要移动元素的原始索引位置
 * @param toIndex 移动到新的索引位置
 */
const moveItemInArray = (array: Array<any>, fromIndex: number, toIndex: number) => {
    const element = array.splice(fromIndex, 1)[0];
    array.splice(toIndex, 0, element);
};
演示效果
演示效果