定制化 SpreadJS 设计器:从界面到功能的深度解析 | 葡萄城技术团队


定制化 SpreadJS 设计器:从界面到功能的深度解析

SpreadJS Designer (以下简称表格设计器) 是一款与 SpreadJS 电子表格控件配套的可视化设计工具,提供类 Excel 的直观操作界面,涵盖表格布局、数据格式、公式编辑、图表插入等核心功能。它支持用户无代码自定义电子表格模板与交互逻辑,快速搭建符合业务需求的表格应用,且设计成果可直接与 SpreadJS 控件集成,无缝对接开发流程。

一、表格设计器界面认识

核心界面元素:包括顶部 Ribbon 功能区(含 Tab 标签、按钮组 ButtonGroups、操作按钮 Command 等)、右键触发的上下文菜单 ContextMenu (支持单元格、行列、工作表等多对象适配)、显示统计信息与缩放控制的状态栏 StatusBar,以及公式栏 FormulaBar、面板 SidePanels 等辅助元素。

img

这些元素通过 GC.Spread.Sheets.Designer.DefaultConfig / ToolBarModeConfig 配置,下文我们也将围绕 Config 进行介绍。

img

1、Ribbon 功能区

img

由多个选项卡(如 “开始””插入””公式””数据” 等)组成,每个选项卡下包含不同的功能组(如 “开始” 选项卡下的 “剪贴板””字体””对齐方式” 等)。

功能区集中了 Excel 的核心功能,涵盖文本格式设置、数据处理、图表插入、公式运算等各类操作,用户可通过点击不同选项卡和功能按钮快速执行相应任务。

img

观察上图,ribbon 数组中包含 27 项 ,每一项都是一个选项卡内容,包括 id ,text,buttonGroups,visibleWhen 等属性。

img

选中”开始” Tab , 查看 buttonGroups 数组,可以看到数组项有 8 个,对应菜单中的 8 个类别 。例如:剪贴板、字体、对齐方式、数字、单元格类型等。 每一项的 buttonGroups 包含 label,commandGroup,thumbnailClass indicator 等 。

  • label 是每一个 buttonGroup 的 group 名称,如”撤销”。
  • thumbnailClass 关联的是缩略图样式,用于直观展示该组功能的视觉标识。

img

  • indicator 用于提示查看更详细的功能内容。
  • commandGroup 是 command 集合,包含 children ,direction(默认横向布局) 等属性。 比如说字体中 command 是纵向布局,所以 commandGroup 为 [“direction”: “vertical”,”children”: []]。

img

img

以上就是 Ribbon 功能区的初步介绍,相信大家对 Ribbon 数组有了一个基础了解。

2、ContextMenu 上下文菜单

在 SpreadJS 的不同对象(如单元格、图表、形状等)上点击鼠标右键触发。

ContextMenu 提供了与当前选中对象相关的快捷操作选项,例如在单元格上右键会出现 “复制””粘贴””设置单元格格式” 等选项;在行头上右键会出现”插入””删除”行等选项;在图表上右键会出现 “编辑数据””更改图表类型” 等选项;在 sheet 标签处右键会出现”表单保护”,”取消表单保护”,极大地提升了操作的便捷性。

单元格的上下文菜单

img

行头的上下文菜单

img

比如 Sheet 标签处的上下文菜单

img

比如图表处的上下文菜单

img

接下来,我们看下 config 中 contextMenu 数组

img

观察上图,contextMenu 是一个字符串数组,每一个字符串,都是一个 commandName。而每一个 command 通过 visibleContext 作用在不同的对象上。

3、SidePanels 面板

img

目前为止,Designer 提供了 33 个 sidePanel 面板, 其中包括文件面板

img

命令面板

img

每一个 sidePanel 包含以下属性:command,uiTemplate,position ,以及可选的 width,showCloseButton,allowResize 等。

可以通过getCommand获取命令,还可以通过getTemplate获取模板 , 如下图所示:

img

4、FormulaBar 公式栏

img

它包含名称框(左侧显示 “A1” 的部分,用于显示或定义单元格名称)和公式编辑栏(右侧用于输入、编辑单元格中的数据或公式,其中 “fx” 是插入函数的按钮)。通过这个区域,用户可以方便地进行数据输入和复杂公式的编辑操作。

图片下方的配置代码片段补充了公式栏的实现逻辑:它作为 sidePanels 数组中的一项,通过 command: “formulaBarPanel”绑定核心功能命令,uiTemplate: “formulaBarTemplate”关联界面模板,position: “top”指定其在设计器顶部的固定布局,同时通过 height: “34px”等属性控制尺寸,确保与整体界面协调。图片中公式栏的可视化呈现与配置代码的属性设置一一对应,清晰展现了 “配置 – 界面” 的映射关系。

img

5、StatusBar 状态栏

位于 Designer 窗口的底部。 显示当前工作表的状态信息 (就绪,编辑,输入等状态),如单元格的统计信息(选中区域的求和、平均值、计数等)、缩放比例等,帮助用户实时了解工作表的状态。

img

下方的配置代码揭示了状态栏的实现方式:它同样是 sidePanels 的配置项之一,通过 command: “statusBarPanel”绑定状态栏功能逻辑,uiTemplate: “statusBarPanelTemplate”加载界面模板,position: “bottom”明确其固定在设计器底部的布局位置。图片中状态栏的各项显示元素(状态文本、统计区域、缩放控制),均通过该配置项的属性定义实现,直观体现了配置对界面功能的支撑作用。

img

6、Ribbon 模式

SpreadJS 设计器组件提供了两种不同的 Ribbon 模式,如下所示:

功能区模式:

img

工具栏模式:

img

img

通过下面的代码,可以进行两者切换

var switchConfig = true;
var designer = new GC.Spread.Sheets.Designer.Designer(document.getElementById("designerHost"));
document.getElementById('btn').addEventListener('click', function () {
            designer.setConfig(switchConfig ? GC.Spread.Sheets.Designer.ToolBarModeConfig : GC.Spread.Sheets.Designer.DefaultConfig);
            switchConfig = !switchConfig;
        });

接下来,从代码层面对比差异

img

img

折叠 Ribbon 区域:

img

designer.setData('isRibbonCollapse',true);

7、Command 命令

什么是 Command ?

Command 是 SpreadJS 设计器的功能命令对象,是控制界面元素(菜单、按钮、面板等)功能逻辑的核心载体。每个 Command 包含唯一标识(commandName)、外观配置(text、iconClass 等)、显示规则(visibleContext)、启用条件(enableContext)、执行逻辑(execute)等属性,统一管理功能的 “显示 – 启用 – 执行” 全流程。

1)认识 Command 命令

有两种方式,可以获取到命令。

img

img

API 文档:https://demo.grapecity.com.cn/spreadjs/help/api/designer/classes/GC.Spread.Sheets.Designer.CommandNames

img

img

2)Command 命令属性

核心标识属性

  • commandName:必填属性,命令的唯一标识字符串,用于区分不同功能。
  • text:命令的显示文本(如按钮文字),可选。
  • title:命令的提示文本(如 hover 时显示),可选。

外观样式属性

  • iconClass:图标样式类名,用于显示命令图标,可选。
  • iconHeight/iconWidth:图标尺寸,可选。
  • bigButton:控制按钮是否为大尺寸,支持布尔值或字符串配置,可选。
  • type:命令组件类型(如下拉框、输入框、按钮),可选。

交互行为属性

  • execute:命令触发时执行的函数,定义核心功能逻辑,可选。
  • getState:获取命令当前状态(如是否禁用)的函数,可选。
  • init:命令初始化时执行的函数,可选。
  • visibleContext:控制命令在特定上下文(如选中单元格、图表)显示,可选。
  • visiblePriority:显示优先级,数值越高越优先展示,可选。
  • enableContext :指定命令启用的上下文场景(如下拉项交互、特定组件状态),仅当场景匹配时命令才会激活可用,可选。

其它具体可以查看API链接

二、Ribbon 菜单定制

1、添加选项卡

企业部署 SpreadJS 设计器供内部使用时,可以在界面添加 “联系技术支持” 入口,方便用户遇到操作问题时快速对接售后,因此新增独立选项卡预留功能扩展空间。

通过下面的代码添加”选项卡” 需要设置 id , text , buttonGroups 等属性。

let config = GC.Spread.Sheets.Designer.DefaultConfig
var newTab = {
    id: 'contactUs',
    text: '联系我们',
    buttonGroups: [],
};
// 将新选项卡添加到配置的功能区中
config.ribbon.push(newTab);

//如果designer已经实例化
//designer.setConfig(config) 
//如果designer 还未实例化
var designer = new GC.Spread.Sheets.Designer.Designer(document.getElementById("gc-designer-container"), config);

完成上面的操作后,可以看到一个名为”联系我们”的 ribbon 菜单 已经被添加到了工具栏上。

img

2、设置活动选项卡

某业务系统的表格设计器默认需高频使用 “插入” 功能(如插入图表、数据透视表),为提升操作效率,将 “插入” 选项卡设为初始化时的默认选中状态,减少用户切换步骤。

img

默认情况下,在 Designer 组件初始化时,”开始”(HOME)是活动选项卡。但是,如果要设置其他选项目为默认选中效果,可以通过下面的代码设置。

var config = GC.Spread.Sheets.Designer.DefaultConfig;
//将"插入"置为活动选项卡
config.ribbon[1].active = true;
var designer = new GC.Spread.Sheets.Designer.Designer(document.getElementById("gc-designer-container"), config);
let currentActiveRibbonTab = designer.activeRibbonTab();
//将"页面布局"置为活动选项卡
if (currentActiveRibbonTab !== 'pageLayout') {
    designer.activeRibbonTab('pageLayout');
}

3、添加菜单

1) 添加”保存”菜单

为保持与 Designer 原生菜单的视觉风格、操作逻辑高度一致,避免新增功能显得突兀割裂,让用户获得连贯流畅的使用体验,特定制 “保存数据” 专属菜单。该菜单在延续 Designer 简洁直观的交互设计、样式规范基础上,补充原生菜单缺失的业务化数据同步能力,既让功能拓展不破坏整体界面协调性,又能让用户自然接纳新功能,同时满足业务数据直连后端的核心需求。

img

// 访问默认配置
var config = GC.Spread.Sheets.Designer.DefaultConfig;
// 设置新按钮的布局
var saveData = {
    label: '保存数据',
    thumbnailClass: 'ribbon-thumbnail-clipboard',
    commandGroup: {
        children: [
            {
                commands: ['cmdSaveData'],
            },
        ],
    },
};
// 将新按钮添加到"开始"选项卡中
config.ribbon[0].buttonGroups.unshift(saveData);
// 为新按钮创建命令
config.commandMap = {
    cmdSaveData: {
        title: '保存数据到服务器', //鼠标悬浮提示文字
        text: '保存',  //菜单显示名称
        iconClass: 'ribbon-button-namemanager',
        bigButton: 'true',
        commandName: 'cmdSaveData',
        execute: async (context, propertyName, fontItalicChecked) => {
            // 自定义操作
            alert('Save data successfully.');
        },
    },
};
// 初始化设计器实例
var designer = new GC.Spread.Sheets.Designer.Designer("gc-designer-container");
designer.setConfig(config);

上述代码的步骤是:

  1. 先获取 SpreadJS 设计器的默认配置;
  2. 定义 “保存数据” 按钮组的布局 IButtonGroup(定义了 label ,thumbnailClass , commandGroup 以及在 commandGroup 中定义了 commandName);
  3. 把该按钮组加到 “开始” 选项卡最前面;
  4. 配置按钮功能:设置commandMap 。注册一个 Command,包括 title 悬浮提示 “保存数据到服务器”、text 显示 “保存” 文字 + iconClass 对应图标,以及 execute 点击弹出 “保存成功” 提示;
  5. 初始化设计器并应用上述配置。

2) 添加一个下拉框菜单

用户需批量导出表格内容为不同格式(PDF、图片),为简化操作,在 “开始” 选项卡添加 “文件操作” 下拉框,集中放置各类导出功能,避免功能分散。

img

// 访问默认配置
var config = GC.Spread.Sheets.Designer.DefaultConfig;
var ribbonFileConfig =
    {
        "label": "文件操作",
        "thumbnailClass": "ribbon-thumbnail-clipboard",
        "commandGroup": {
            "children": [
                {
                    "command": "exportImage_List",
                    "type": "dropdown",
                    "children":[
                        "exportSheetPDFImage",
                        "exportRangePDFImage",
                        "exportSheetImage",
                        "exportRangeImage"
                    ]
                },
            ]

        }
    }
// 将新按钮添加到"开始"选项卡中
config.ribbon[0].buttonGroups.unshift(ribbonFileConfig);
// 为新按钮创建命令
config.commandMap = {
    "exportImage_List": {
        title: '导出图片',
        text: '导出图片',
        bigButton: true,
        iconClass: 'ribbon-button-namemanager',
        commandName: 'exportImage_List',  //command ,需唯一
    },
    "exportSheetPDFImage": {
        title: 'Sheet导出PDF图片',
        text: "Sheet导出PDF图片",
        iconClass: "ribbon-button-namemanager",
        commandName: "exportSheetPDFImage"
    },
    "exportRangePDFImage": {
        title: '选择区域导出PDF图片',
        text: "选择区域导出PDF图片",
        iconClass: "ribbon-button-namemanager",
        commandName: "exportRangePDFImage"
    },
    "exportSheetImage": {
        title: 'Sheet导出图片',
        text: "Sheet导出图片",
        iconClass: "ribbon-button-namemanager",
        commandName: "exportSheetImage"
    },
    "exportRangeImage": {
        title: '选择区域导出图片',
        text: "选择区域导出图片",
        iconClass: "ribbon-button-namemanager",
        commandName: "exportRangeImage"
    },

};

// 初始化设计器实例
var designer = new GC.Spread.Sheets.Designer.Designer(document.getElementById('gc-designer-container'),config);

3)自定义有选中效果的 Command

数据保存后需直观提示用户 “已保存” 状态,因此将 “保存数据” 按钮设为复选框类型,点击保存后按钮保持选中状态,直至数据修改后自动取消。

img

var config = GC.Spread.Sheets.Designer.DefaultConfig;
config.commandMap = {
    cmdSaveData: {
        title: '保存数据到服务器', //鼠标悬浮提示文字
        text: '保存',  //菜单显示名称
        iconClass: 'ribbon-button-namemanager',
        bigButton: 'true',
        commandName: 'cmdSaveData',
        type:"checkbox",
        execute: async (context, e, checked) => {
            // 自定义操作
            alert('Save data successfully.');
            context.setData('save_button_check', checked);
        },
        getState: function (context, e, checked) {
            return context.getData('save_button_check');
        }
    },
};
var designer = new GC.Spread.Sheets.Designer.Designer(document.getElementById('gc-designer-container'),config);

4、修改菜单

1) EnableContext

管理员需限制仅特定权限用户(通过 controlByMySelf 标识)可修改表格页边距,因此通过修改 “页边距” 命令的 enableContext,添加权限校验条件。

img

img

let config= GC.Spread.Sheets.Designer.DefaultConfig;
var command = GC.Spread.Sheets.Designer.getCommand('margins');
let enableContext = command.enableContext;
command.enableContext = command.enableContext == null ? "controlByMyself" : command.enableContext + "&& controlByMyself";

config.commandMap = {
    "margins": command
}
var designer = new GC.Spread.Sheets.Designer.Designer("gc-designer-container");
designer.setConfig(config)

2) Execute

原生 “保存 Schema” 功能仅保存基础配置,业务需在保存前校验数据格式合法性,因此重写 execute 方法,在执行原生逻辑前添加数据校验步骤。

重写 Execute

let config= JSON.parse(JSON.stringify(GC.Spread.Sheets.Designer.DefaultConfig));

var command = GC.Spread.Sheets.Designer.getCommand("saveSchema");
var oldExecute = command.execute;
command.execute = function (context, propertyName) {
    //执行业务逻辑
    oldExecute.call(this, context, propertyName)
}

config.commandMap = {
    "saveSchema": command
}
var designer = new GC.Spread.Sheets.Designer.Designer("gc-designer-container");
designer.setConfig(config)

添加撤销逻辑

用户想要合并单元格的实现和 Excel 一致,需要其他单元格的值删除。即需要重写 Execute 。

let mergeCenterCommand = GC.Spread.Sheets.Designer.getCommand(GC.Spread.Sheets.Designer.CommandNames.MergeCenter);
if (mergeCenterCommand) {
    let oldExecute = mergeCenterCommand.execute;
    mergeCenterCommand.execute = function (context, propertyName, args) {

        oldExecute.call(this, context, propertyName, args);

        let spread = context.getWorkbook()
        let sheet = spread.getActiveSheet()
        var selections = sheet.getSelections();
        let {row, col, rowCount, colCount} = selections[0]

        let value = sheet.getValue(row, col)
        sheet.clear(row, col, rowCount, colCount, GC.Spread.Sheets.SheetArea.viewport, GC.Spread.Sheets.StorageType.data);
        sheet.setValue(row, col, value)
    }
}
var config = GC.Spread.Sheets.Designer.DefaultConfig
config.commandMap = {
    "mergeCenter": mergeCenterCommand
}
var designer = new GC.Spread.Sheets.Designer.Designer(document.getElementById("gc-designer-container"), config);

但是重写后,不支持撤销功能恢复原始状态,因此自定义合并单元格命令,通过事务管理(startTransaction/endTransaction)实现撤销 / 重做。

SpreadJS 可以实现一个支持撤销/重做的自定义命令

https://demo.grapecity.com.cn/spreadjs/SpreadJSTutorial/features/worksheet/actions/custom-action/purejs

如果你想实现一个支持撤销/重做的命令,你可以通过如下方法启动一个事务,命令中的任何行为和数据变更将开启自动存储。

GC.Spread.Sheets.Command.startTransaction(context, options);

在命令执行结束后,通过如下方法结束当前事务,当前命令的任何变更将被自动存储。

GC.Spread.Sheets.Command.endTransaction(context, options);

如果你想撤销上一次的命令,通过如下方法撤销事务,那么上次命令执行的结果将被撤销。

GC.Spread.Sheets.Command.undoTransaction(context, options);

所以,需要先定义一个命令,然后注册命令。当执行 execute()时,执行刚刚注册的命令。

let config= JSON.parse(JSON.stringify(GC.Spread.Sheets.Designer.DefaultConfig));

var command = GC.Spread.Sheets.Designer.getCommand("mergeCenter");
var oldExecute = command.execute;
command.execute = function (context) {
    let spread = context.getWorkbook();
    let sheet = spread.getActiveSheet();
    spread.commandManager().execute({
        cmd: "mergeCenter2",
        selections: sheet.getSelections(),
        sheetName: sheet.name(),
        oldExecute
    });
}
config.commandMap = {
    "mergeCenter": command
}
spread.commandManager().register("mergeCenter2", {
    canUndo: true,
    execute: function (context, options, isUndo) {
        var Commands = GC.Spread.Sheets.Commands;
        if (isUndo) {
            Commands.undoTransaction(spread, options);
            return true;
        } else {
            let sheet = context.getActiveSheet();
            Commands.startTransaction(context, options);

            spread.suspendPaint();

            var selections = options.selections;
            let {row, col, rowCount, colCount} = selections[0]

            let value = sheet.getValue(row, col)
            sheet.clear(row, col, rowCount, colCount, GC.Spread.Sheets.SheetArea.viewport, GC.Spread.Sheets.StorageType.data);
            sheet.setValue(row, col, value)
            sheet.addSpan(row, col, rowCount, colCount);
            sheet.getCell(row, col).hAlign(GC.Spread.Sheets.HorizontalAlign.center)

            spread.resumePaint();
            Commands.endTransaction(context, options);
            return true;
        }
    },
});
designer.setConfig(config)

5、删除菜单

有时候仅需基础数据录入的场景,隐藏 “数据” 选项卡中的复杂格式设置功能,避免用户误操作。

1) 删除数组元素

let config= JSON.parse(JSON.stringify(GC.Spread.Sheets.Designer.DefaultConfig));
config.ribbon = config.ribbon.filter(item => item.id !== "data");
designer.setConfig(config)

2) visibleWhen

let config = GC.Spread.Sheets.Designer.DefaultConfig
let dataRibbon = config.ribbon.find(item => item.id == "data");
dataRibbon.visibleWhen += "&& controlByMyself";
designer.setConfig(config)

//通过controlByMyself 控制visibleWhen  ,来控制"数据" 选项卡显示与隐藏
designer.setData("controlByMyself",false)
designer.setData("controlByMyself",true)

三、ContextMenu 上下文菜单定制

1、添加菜单

财务报表、审批表格需添加电子签名确认,因此在行头右键菜单中添加 “插入签名” 功能,方便用户快速插入签名信息。

img

var config = GC.Spread.Sheets.Designer.DefaultConfig;
let command = {
    text: "插入签名",
    commandName: "insertSignatureMenu",
    visibleContext: "ClickRowHeader",
    execute: () => {
        console.log("插入签名");
    }
}
config.contextMenu.unshift("insertSignatureMenu");
// 为新的上下文菜单项创建命令
config.commandMap = {"insertSignatureMenu": command}
designer.setConfig(config)

2、修改菜单

1) EnableContext

表格中第 3、4 列是非核心数据列,允许用户随意插入 / 删除,因此通过 enableContext 限制仅非核心列可触发 “插入列 / 删除列” 右键菜单。

img

img

const CONTEXT_MENU_COMMANDS = [
    "gc.spread.contextMenu.insertColumns",
    "gc.spread.contextMenu.deleteColumns",
    "gc.spread.contextMenu.insertRows",
    "gc.spread.contextMenu.deleteRows"
];
const CONTROLLED_COLUMNS = [3, 4]; // 需要控制的列索引
const CONTROL_KEY = "controlByMyself";
const config= JSON.parse(JSON.stringify(GC.Spread.Sheets.Designer.DefaultConfig));

const commandMap = {};
CONTEXT_MENU_COMMANDS.forEach(commandId => {
    const command = GC.Spread.Sheets.Designer.getCommand(commandId);
    // 保留原有启用条件,追加自定义控制逻辑(使用模板字符串更清晰)
    command.enableContext = `${command.enableContext} && ${CONTROL_KEY}`;

    commandMap[commandId] = command;
});

config.commandMap = {
    ...config.commandMap, // 保留默认其他命令
    ...commandMap // 覆盖需要自定义的命令
};

const designer = new GC.Spread.Sheets.Designer.Designer("gc-designer-container", config);
const spread = designer.getWorkbook();

const oldOpenMenu = spread.contextMenu.onOpenMenu;
spread.contextMenu.onOpenMenu = function (menuData, itemsDataForShown, hitInfo, spread) {
    const activeSheet = spread.getActiveSheet();
    const selections = activeSheet.getSelections() || [];
    const isControlledColumn = selections.length > 0
        ? CONTROLLED_COLUMNS.includes(selections[0].col)
        : false;

    // 设置控制状态
    designer.setData(CONTROL_KEY, isControlledColumn);

    // 执行原有逻辑(保持this上下文正确)
    oldOpenMenu.call(this, menuData, itemsDataForShown, hitInfo, spread);
};

2) VisibleContext

仅 “Sheet1” 为数据录入表,需快速标记异常数据,因此仅在 Sheet1 中显示 “添加红色背景” 右键菜单,其他工作表隐藏该功能,避免干扰。

通过下面的代码,在 onOpenMenu() 方法中 ,判断当前 sheet 是否是 sheet1 ,如果是的话 designer 将 controlByMySelf 变量置为 true ,此时,如果用户选中的是单元格区域,则 visibleContext 则为 true , 此时可以被用户看到,否则用户将看不到此菜单

img

img

var config = JSON.parse(JSON.stringify(GC.Spread.Sheets.Designer.DefaultConfig));

config.commandMap = {
    addRedBackColorCommand: {
        text: "添加红色背景",
        commandName: "addRedBackColorCommand",
        visibleContext: "ClickViewport && controlByMySelf",
        execute: async (context, _, options) => {
            var spread = context.getWorkbook();
            let sheet = spread.getActiveSheet()
            let cell = sheet.getCell(options.activeRow,options.activeCol)
            cell.backColor('red')
        }
    }
}

config.contextMenu.unshift("addRedBackColorCommand");
var designer = new GC.Spread.Sheets.Designer.Designer("gc-designer-container",config);
let spread = designer.getWorkbook()

var oldOpenMenu = spread.contextMenu.onOpenMenu;
spread.contextMenu.onOpenMenu = function (menuData, itemsDataForShown, hitInfo, spread) {
    let sheet = spread.getActiveSheet()
    if (sheet.name() == 'Sheet1') {
        designer.setData("controlByMySelf", true);
    } else {
        designer.setData("controlByMySelf", false);
    }
    oldOpenMenu.apply(this, arguments);
};

3、删除菜单

在上文中,我们可以通过 visibleContext 属性控制菜单是否显示与隐藏。但是在一些场景下,管理员为防止用户解除表格保护后篡改数据,需永久隐藏 “取消表单保护” 右键菜单,仅保留管理员通过后台操作解除保护的权限。那么我们直接从 contextMenu 数组中删除即可。

img

var config = JSON.parse(JSON.stringify(GC.Spread.Sheets.Designer.DefaultConfig));
let index2 = config.contextMenu.indexOf("unprotectSheet");
config.contextMenu.splice(index2, 1);
designer.setConfig(config)

四、SidePanel 面板的定制

img

1、删除 / 隐藏

报表展示场景中,用户无需查看或修改数据源,仅需浏览报表结果,因此删除左侧 “数据源” 面板,简化界面布局。,可以用下面的代码,删除 tableListPanel

img

var removeSidePanels = ["tableListPanel"];
config.sidePanels = config.sidePanels.filter(function (item) {
    return removeSidePanels.indexOf(item.command) == -1;
});
designer.setConfig(config)

当然,上面的代码,将 formulaBarPanel 直接从 sidePanels 数组中删除,但是如果想在特定条件下删除(隐藏)该如何处理呢? 通过上文,我们知道 command 命令中 visibleContext 可以控制 菜单的显示与隐藏。那么,

我们也按照这个思路,来

img

img

let command = GC.Spread.Sheets.Designer.getCommand("tableListPanel")
command.visibleContext = ` (${command.visibleContext}) && controlByMySelf`
config.commandMap = {
    "tableListPanel":command
}
designer.setConfig(config)
designer.setData("controlByMySelf",true)  //使其显示
// designer.setData("controlByMySelf",false)  //使其隐藏

五、Template 定制

1、添加提示文本

新手用户使用 “插入图表” 功能时需引导,因此在对话框顶部和底部添加提示文本(如 “选择图表类型后点击确定”),提升易用性。

let insertChartDlgTemplate = GC.Spread.Sheets.Designer.getTemplate(GC.Spread.Sheets.Designer.TemplateNames.InsertChartDlgTemplate)
if(insertChartDlgTemplate){
  //上方插入文字
  insertChartDlgTemplate.content.unshift({type: "TextBlock", text: "文本", margin: "5px 0", style: "color: rgb(226, 107, 29);font-size:18px"})
  //下方插入文字
  insertChartDlgTemplate.content.push({type: "TextBlock", text: "文本", margin: "5px 0", style: "color: rgb(226, 107, 29);font-size:18px"})
GC.Spread.Sheets.Designer.registerTemplate(GC.Spread.Sheets.Designer.TemplateNames.InsertChartDlgTemplate, insertChartDlgTemplate)
}

img

2、添加自定义函数

业务需计算阶乘数据(如统计分析场景),而设计器原生无阶乘函数,因此自定义 FACTORIAL 函数,并添加到 “插入函数” 对话框中,方便用户调用。

首先,我们先自定义一个函数

function FactorialFunction() {
    this.typeName = "FactorialFunction";
    this.name = "FACTORIAL";
    this.maxArgs = 1;
    this.minArgs = 1;
    this.description = function () {
        return (
            {
                description: "菲波那切数列",
                parameters: [
                    {
                        name: 'number01',
                        repeatable: false,
                        optional: false
                    }
                ]
            }
        )
    }
}

FactorialFunction.prototype = new GC.Spread.CalcEngine.Functions.Function();
FactorialFunction.prototype.evaluate = function (arg) {
    var result = 1;
    if (arguments.length === 1 && !isNaN(parseInt(arg))) {
        for (var i = 1; i <= arg; i++) {
            result = i * result;
        }
        return result;
    }
    return "#VALUE!";
};

var factorial = new FactorialFunction();
GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction("FACTORIAL", new FactorialFunction());

定义完这个函数后,我们需要找这个弹框对应的 template。对应的是 GC.Spread.Sheets.Designer.TemplateNames.InsertFunctionDialogTemplate。

var template = GC.Spread.Sheets.Designer.getTemplate(GC.Spread.Sheets.Designer.TemplateNames.InsertFunctionDialogTemplate);

我们要添加函数的位置在这个数组中

img

img

通过 bindingPath 找到这个位置,并且添加我们的自定义函数对象

function customFontFamilyInFormatDialogTemplate(templateNode) {
    if (templateNode.bindingPath && templateNode.bindingPath === 'functionDesc.allFunction' && templateNode.items) {
        templateNode.items.unshift({text: "FACTORIAL", value: "FACTORIAL"})
        return;
    }
    let nodes = templateNode.content || templateNode.children;
    if (nodes && nodes instanceof Array) {
        nodes.forEach((subNode) => customFontFamilyInFormatDialogTemplate(subNode));
    }
}

最后注册

GC.Spread.Sheets.Designer.registerTemplate(GC.Spread.Sheets.Designer.TemplateNames.InsertFunctionDialogTemplate, template);

结果:

img

3、添加对话框 Dialog

“插入签名” 功能需用户选择签名插入范围,因此创建自定义对话框,提供单元格范围选择器,明确签名插入位置。

在之前的内容中,我们提到 添加”插入签名”右键菜单,我们继续扩展,再在此基础上,我们添加一个对话框

img

let insertSignature = {
    templateName: 'insertSignature',
    title: '签名对话框',
    content: [
        {
            type: 'FlexContainer',
            children: [
                {
                    type: 'TextBlock',
                    margin: '5px -4px',
                    text: '插入范围',
                },
                {
                    type: 'RangeSelect',
                    title: '选择范围',
                    absoluteReference: true,
                    needSheetName: false,
                    margin: '5px -5px',
                },
            ],
        },
    ],
};
// 使用 registerTemplate 注册对话框模板实例
GC.Spread.Sheets.Designer.registerTemplate('signature', insertSignature);
config.commandMap = {
    insertSignatureMenu: {
        text: '插入签名',
        commandName: 'insertSignatureMenu',
        visibleContext: 'clickRowHeader',
        // 执行 InsertSignature,以下只是一个简单的代码片段示例
        execute: () => {
            // 在此处添加在步骤4中注册的对话框模板名称。
            GC.Spread.Sheets.Designer.showDialog('signature');
        },
    },
};

六、其他

1、主题切换

document.getElementById('themeToggle').addEventListener('click', function () {
    toggleTheme();
});

function setTheme(theme) {
    currentTheme = theme;
    var designerTag = document.querySelector('link[theme-label="designer"]');
    var runtimeTag = document.querySelector('link[theme-label="runtime"]');
    var designerTheme, runtimeTheme;
    if (theme === 'dark') {
        document.documentElement.setAttribute('data-theme', 'dark');
    } else {
        document.documentElement.removeAttribute('data-theme');
    }
    addThemeLink(getNewTheme(designerTag, 'gc.spread.sheets.designer.', currentTheme + ".min.css"), 'designer', designerTag);
    addThemeLink(getNewTheme(runtimeTag, 'gc.spread.sheets.', currentTheme === 'dark' ? "excel2016black.css" : "excel2013white.css"), 'runtime', runtimeTag);
}

function getNewTheme(target, themeBase, themeVariant) {
    var header = document.getElementsByTagName('head')[0];
    var href = target.href,
        pos = href.indexOf(themeBase),
        item = href.substr(0, pos) + themeBase + themeVariant;
    return item;
}

function toggleTheme() {
    const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
    setTheme(newTheme);
}

function addThemeLink(href, themeLabel, target) {
    var header = document.getElementsByTagName('head')[0];
    var link = document.createElement('link');
    link.type = "text/css";
    link.rel = "stylesheet";
    link.href = href;
    link.setAttribute('theme-label', themeLabel);
    link.onload = function () {
        header.removeChild(target);
        var designer = GC.Spread.Sheets.Designer.findControl("gc-designer-container");
        var spread = designer.getWorkbook();
        spread.refresh();
        designer.refresh();
    };

    header.appendChild(link);
}

img

2、添加中文字体

var res = GC.Spread.Sheets.Designer.getResources();
let reorganizeFontFamilies = [];
reorganizeFontFamilies.push({ name: "微软雅黑", text: "微软雅黑" });
reorganizeFontFamilies.push({ name: "黑体", text: "黑体" });
reorganizeFontFamilies.push({ name: "新宋体", text: "新宋体" });
reorganizeFontFamilies.push({ name: "仿宋", text: "仿宋" });
reorganizeFontFamilies.push({ name: "隶书", text: "隶书" });
reorganizeFontFamilies.push({ name: "楷体", text: "楷体" });
Object.keys(res.ribbon.fontFamilies).forEach(function (key) {
    reorganizeFontFamilies.push(res.ribbon.fontFamilies[key]);
});
res.ribbon.fontFamilies = {};
reorganizeFontFamilies.forEach(function (item, index) {
    res.ribbon.fontFamilies['ff' + (1 + index)] = item;
});
GC.Spread.Sheets.Designer.setResources(res);
var designer = new GC.Spread.Sheets.Designer.Designer("dss");

img

3、执行命令

通过下面的代码,打开文件菜单

var command = GC.Spread.Sheets.Designer.getCommand("fileMenuButton");
command.execute(designer);

通过下面的代码,关闭文件菜单

 var command =  GC.Spread.Sheets.Designer.getCommand('fileMenuPanel')
 command.execute(designer, "activeCategory_hide", true)

以上就是 SpreadJS 设计器定制的核心实战内容,从界面认知到 Ribbon 功能区菜单、上下文菜单、SidePanels 等核心模块的定制,再到主题切换、中文字体添加等拓展配置,基本覆盖了设计器个性化改造的关键场景。这些实战方案基于 SpreadJS 的原生 API 与配置逻辑,兼顾了功能实现与落地实用性,能帮助你快速适配表格权限管控、界面风格统一等实际业务需求。

若需进一步深化定制,可结合 API 文档细化命令逻辑,或根据具体场景调整配置参数。希望这份文档能为你的开发工作提供有效支撑,助力高效完成 SpreadJS 设计器的个性化改造与落地。

学习资料:

学习指南:https://demo.grapecity.com.cn/spreadjs/SpreadJSTutorial/

产品文档:https://demo.grapecity.com.cn/spreadjs/help/docs/overview

产品首页:https://www.grapecity.com.cn/developer/spreadjs

在线Excel:https://demo.grapecity.com.cn/SpreadJS/WebDesigner/index.html

                                                                                </div>



Source link

未经允许不得转载:紫竹林-程序员中文网 » 定制化 SpreadJS 设计器:从界面到功能的深度解析 | 葡萄城技术团队

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
关于我们 免责申明 意见反馈 隐私政策
程序员中文网:公益在线网站,帮助学习者快速成长!
关注微信 技术交流
推荐文章
每天精选资源文章推送
推荐文章
随时随地碎片化学习
推荐文章
发现有趣的