前言
在现代商业环境中,预测销售数据和实际成本是每个公司CEO和领导都极为重视的关键指标。然而,由于市场的不断变化,准确地预测和管理这些数据变得愈发具有挑战性。为了应对这一挑战,建立一个高效的系统来管理和审查销售数据的重要性不言而喻。今天小编就将为大家介绍一下如何使用葡萄城公司的纯前端表格控件SpreadJS实现一个预算编制系统。
环境准备
Node.js
VSCode代码编辑器
完整代码Github地址(可在阅读本文时配合参考使用)
使用代码实现的在线Demo地址(可在阅读本文时配合参考使用)
实现步骤
1)自定义菜单栏
上图中红色方框划出来的菜单栏叫做在线表格编辑器(Designer),Designer的菜单提供了各种定制化的能力,如新增菜单,修改菜单执行的逻辑,修改图标,修改文字以及删除菜单等功能。
观察上图中,首先新建了一个“预算操作(定制按钮)”tab ,此tab内容包括了三部分,分别是“预算类型”、“预算编制”、“数据”。对应的代码如下:
let config = JSON.parse(JSON.stringify(GC.Spread.Sheets.Designer.DefaultConfig));
config.ribbon.push(
{
id: "fill-custom",
text: "预算操作(定制按钮)",
buttonGroups: [
{
label:"预算类型",commandGroup:{}
},
{
label: "预算编制", commandGroup:{}
},
{
label: "数据", commandGroup:{}
}]
})
designer.setConfig(config)
通过上述代码,我们来看看实现结果:
Ok ,发现添加了一个“预算操作(定制按钮)”tab,点击此tab,已经有了基础框架
接下来,继续,我们设置当前tab为激活状态,加上active属性,这样子页面初始化后看到的当前tab就是“预算操作(定制按钮)”
{
id: "fill-custom",
text: "预算操作(定制按钮)",
active: true,
buttonGroups: []
}
接下来,我们设置预算模型command, 我们再次看上面的第一张图,发现预算类型只有一个节点,且该节点是一个下拉框。对应的代码实现方式如下:
{
label:"预算类型",
commandGroup: {
children: ["selectBudgetType"]
}
},
接下来定义“selectBudgetType”,代码如下所示:( 关于定义下拉框子菜单的实现方法详细解释,可以参考此篇文章)
const budgetType = {
cost: 'cost' , //成本预算
sales: 'sales' //销售预算
}
let selectBudgetType = {
text: "选择预算类型",
comboWidth: 120,
type:"comboBox",
commandName: "selectBudgetType",
dropdownList:[
{
text:"成本预算",
value: budgetType.cost
},{
text:"销售预算",
value:budgetType.sales
},
],
execute:(context,propertyName) => {
console.log('选择',propertyName)
},
}
config.commandMap = {selectBudgetType}
designer.setConfig(config)
上述代码为子菜单“selectBudgetType”定义了text,type ,以及dropdownList以及点击事件。exexute方法中propertyName对应的是dropdownList中的value值。
结果如下:
上述代码已经熟悉了如何定义菜单以及子菜单,接下来的两个子菜单(预算编制和数据)就不重复详细介绍,直接上代码:
config.ribbon.push(
{
id: "fill-custom",
text: "预算操作(定制按钮)",
active: true,
buttonGroups: [
{
label:"预算类型",
commandGroup: {
children: ["selectBudgetType"]
}
},
{
label: "预算编制",
thumbnailClass: "ribbon-thumbnail-editing",
commandGroup: {
children: [ "distributeTask"]
}
},
{
label: "数据",
commandGroup: {
children: ["clearLocalData"]
}
}]
})
config.commandMap = {
selectBudgetType:{
text: "选择预算类型",
comboWidth: 120,
type:"comboBox",
commandName: "selectBudgetType",
dropdownList:[
{
text:"成本预算",
value: budgetType.cost
},{
text:"销售预算",
value:budgetType.sales
},
],
execute:(context,propertyName) => {
console.log('选择',propertyName)
}
},
distributeTask: {
title: "下发预算任务",
text: "预算编制",
iconClass: "distribute-icon",
bigButton: true,
commandName: "distributeTask",
execute: function (context) {
}
},
clearLocalData: {
title: "清除本地缓存",
text: "清除本地缓存",
iconClass: "clear-local-icon",
bigButton: true,
commandName: "clearLocalData",
execute: function () {
localStorage.clear()
}
},
}
designer.setConfig(config)
icon相关代码,注意iconClass要添加相应的背景图片。
.clear-local-icon {
background: url("../assets/clear.png");
background-size: 35px 35px;
}
.distribute-icon {
background: url("../assets/distribute.png");
background-size: 35px 35px;
}
上述三个子菜单中的execute方法需要自定义,如选择选择预算类型后,模板需要进行切换。
2)设置模板
当“选择预算类型”选择“成本预算”时,加载cost.json文件
当“选择预算类型”选择“销售预算”时,加载sales.json文件
let selectBudgetType = {
text: "选择预算类型",
comboWidth: 120,
type:"comboBox",
commandName: "selectBudgetType",
dropdownList:[
{
text:"成本预算",
value: budgetType.cost
},{
text:"销售预算",
value:budgetType.sales
},
],
execute:(context,propertyName) => {
if(propertyName){
selectedBudget.value = propertyName
loadTemplate(context,propertyName,taskId)
}
},
getState:(context)=>{
return selectedBudget.value
},
}
const loadTemplate = async (designer,fileName,taskId) => {
let templateStr = await BusinessType.getTemplate(fileName)
let template = JSON.parse(templateStr)
let spread = designer.getWorkbook()
spread.fromJSON(template)
}
上述代码介绍了【选择预算类型】下拉框选中的事件,选中后,导入对应的json文件,通过fromJSON进行导入。
对于需要设置的模板,可以通过Designer中菜单快速设计,其菜单基本与Excel一致,对于熟悉Excel的用户来说,真的很友好。
3)设置数据源
下面小编以“销售预算”模板为例,介绍如何设置数据源:
点击“数据”tab,接下来点击“工作表绑定”,此时出现右侧字段列表Panel。发现字段列表中存在“id”和“name ",这是因为在模板(sales.json)中已经设置好字段。
此时进行数据绑定setDataSource():
const bindInitialData = (spread,type,taskId) => {
// 绑定初始数据
let data = defaultBudgetData[type]
let source = new GC.Spread.Sheets.Bindings.CellBindingSource(data)
spread.suspendPaint()
let sheetCount = spread.getSheetCount()
for(let i=0; i<sheetCount;i++){
let sheet = spread.getSheet(i)
sheet.setDataSource(source)
}
spread.resumePaint()
taskId.value = data.id
}
const defaultBudgetData = {
[budgetType.cost]: {
id:`成本NV-${getNowTime()}`,//项目编号
name:'', //项目名称
city: '', //项目所在地
customer: '', //客户名称
price: 0 //本次报价
},
[budgetType.sales]:{
id: `销售NV-${getNowTime()}`,
name:''
}
}
4)任务下发
(1)在任务下发前 ,需要确认预测因子,预测因子基于往年数据,确认接下来的销售计划。
(2)填写预算名称 。
(3)点击“预算编制”菜单。
distributeTask: {
title: "下发预算任务",
text: "预算编制",
iconClass: "distribute-icon",
bigButton: true,
commandName: "distributeTask",
execute: function (context) {
confirmDistribute(context,selectedBudget,distributeVisible)
}
},
const confirmDistribute = (context,selectBudgetType,distributeVisible) => {
/**预算任务下发时必填信息校验 */
let sheet = context.getWorkbook().getSheet(0)
let source = sheet.getDataSource().getSource()
for(let key in source){
if(!source[key]){
ElMessage.error("红色区域必填项信息缺失")
return
}
}
// 确认是否下发编制任务
ElMessageBox.confirm("确认下发预算编制任务吗?","下发确认",{
confirmButtonText:'确认',
cancelButtonText:"取消",
type:'warning'
}).then(() => {
// 确认下发,存储当前预算模板,下发部门信息
saveBudgetRecord(context, selectBudgetType)
distributeBudgetTask(context,distributeVisible)
}).catch(() => {
ElMessage({
type:'error',
message:'取消发布'
})
})
}
在上述代码confirmDistribute()中,通过getDataSource()获取数据源,来判断红色区域的必填项是否填写。当确认下发任务后,执行saveBudgetRecord 、distributeBudgetTask方法。
5)填写任务
当确定下发任务后,对不同部门生成不同的编制链接。此弹窗可以参考代码中的OnlineDesigner.vue文件。
部门经理获取链接,打开链接,显示内容是自己部门区域预算明细填写和实际填写,此时,部门经理可以在左侧蓝色区域填写,而其他单元格不能编辑,这个是怎么做到的呢?具体可以参考这篇文章中第二点对少部分单元格可以编辑。
var defaultStyle = new GC.Spread.Sheets.Style();
defaultStyle.locked = false;
sheet.setDefaultStyle(defaultStyle, GC.Spread.Sheets.SheetArea.viewport);
// 设置第1行不可编辑
var style = new GC.Spread.Sheets.Style();
style.locked = true;
style.backColor = "red";
sheet.setStyle(0, -1, style);
// 设置表单保护
sheet.options.isProtected = true;
介绍完单元格的权限后,我们再来看下上图中还有哪些值得说一说的功能。
(1)添加签名
当经理设置完预算后,可以在区域总监单元格右键,看到多出来两个菜单“添加签名”和“添加手写签名”。
所以接下来介绍如何在右键菜单中新增菜单并定义其事件,代码如下:
let signMenu = {
text:"添加签名",
name:"signName",
command:"signMenuCommand",
workArea: "viewport"
}
spread.contextMenu.menuData.push(signMenu)
上述代码在spread.contextMenu.menuData中push了一条对象,结果就是可以在右键菜单中看见“添加签名菜单” ,观察到上述对象定义了command属性,接下来定义“signMenuCommand”:
let signMenuCommand = {
canUndo: true,
execute: function(context,options,isUndo){
if(isUndo){
GC.Spread.Sheets.Commands.undoTransaction(context,options)
return true
}else{
GC.Spread.Sheets.Commands.startTransaction(context,options)
let {activeRow,activeCol,sheetName} = options
let sheet = context.getSheetFromName(sheetName)
sheet.getCell(activeRow,activeCol).value(user).backColor('#F7A711').font('bold normal 15px normal')
GC.Spread.Sheets.Commands.endTransaction(context,options)
return true
}
}
}
commandManager.register("signMenuCommand",signMenuCommand,null, false, false, false, false)
上述代码是SpreadJS中注册命令的方法,并提供了撤销机制。我们主要看else里面的内容:首先从上下文context中获取sheet对象,接着获取单元格并设置内容、背景色、字体等。上述两段代码就实现了在SpreadJS中在右键菜单中添加菜单,并完整相应的点击逻辑。
(2)添加手写签名
接下来,我们看看如何设置“添加手写签名”:
// 注册签名的右键菜单
let commandManager = spread.commandManager()
let signMenu = {
text:"添加手写签名",
name:"handWriteName",
command:"handWriteCommand",
workArea: "viewport"
}
spread.contextMenu.menuData.push(signMenu)
let handWriteCommand = {
canUndo: false,
execute: function(context,options,isUndo){
showWriteDialog.value = true
}
}
commandManager.register("handWriteCommand",handWriteCommand,null, false, false, false, false)
添加菜单和菜单命令的方式与前文一致,不同的就是execute的执行逻辑。
最后,签名设置后,就可以点击“提交预算”按钮。
对了,如果数据不符合预期,可能会有红色预警,比如
这个是SpreadJS的数据验证功能,我们可以通过UI方式设置。如下图所示:
6)编制完成
当所有部门经理填写完预算后,就可以点击“编制完成”
此时点击“预算审核”,预算类型设置为“销售预算”,可以看到有一条待审核的标签,点进去看看。
看到了我们熟悉的页面
此时点击“华东”sheet看看
这个时候就看到了华东部门经理填写的销售预测数据,这个时候点击右上角的“导入年度实际销售数据”看看。
嗯,表格内容基本上填写完整了,这时候审核员(副总经理)如果对销售数据表示满意,可以签上自己的大名,就可以点击“审核完毕了”
当四个sheet都“审核完毕”,此时返回首页,发现标签变了。
这时候可以进行打印了。
最后
简单的全面预算编制系统就算介绍完了。大家可以在Demo地址实际体验下。总结下本文介绍的SpreadJS的几个知识点:
1、自定义Designer菜单
2、导入模板
3、设置数据源
4、获取数据源
5、自定义右键菜单
6、单元格权限
如果您想了解更多的信息,欢迎点击这篇参考资料查看。
扩展链接:
【干货放送】财务报表勾稽分析要点,一文读尽!
为什么你的财务报表不出色?推荐你了解这四个设计要点和!
纯前端类 Excel 表格控件在报表勾稽分析领域的应用场景解析
评论区