项目预算编制
运行效果
项目预算编制页面分为左右两部分,左边是经济考核指标树,右边是预算编制树。界面如下图所示
案例需求
案例的业务逻辑就是一个工程项目经营预算编制。单据有2个:
- 上游单据:经济考核指标
- 下游单据:项目预算编制
具体要求如下:
- 左边树显示列:指标编码、指标名称、下发指标数值
- 右边树显示列:指标编码、指标名称、预算值、数量、单价,其中预算值 = 数量 * 单价,预算值需要向上汇总
- 左边树点击后,右边树可以新增,新增的时候,要把左边当前点击节点和它的上级全部节点带到右边树
- 右边只能在左边的叶子节点下新增节点
- 右边树的指标不能超过左边树的下发指标数值
知识点
- 使用了树形表格、分割面板等组件
- 通过代码新增树形数据
- 通过代码查询某个节点的所有父节点及子节点
开发过程
数据开发
经济考核指标
添加动态数据集,包括主键、指标编码、指标名称、下发指标数值等列。作为树形数据包括父id、全路径id、全路径编码、全路径名称、叶子节点等列,如下图所示
配置为树形数据,如下图所示。系统通过这些配置,自动维护全路径id、全路径编码、全路径名称、叶子节点等列的值。需求中的指标编码,使用全路径编码列实现,使用“-”作为分隔符。叶子节点及末级节点,整型数据,1表示是叶子节点。
添加初始数据
项目预算
添加动态数据集,包括主键、指标编码、指标名称、预算值、单价、数量、指标叶子、是否新增等列。“指标叶子”和“是否新增”用于控制是否可编辑、可拆解。作为树形数据包括父id、全路径id、全路径编码、全路径名称、叶子节点等列,如下图所示
配置为树形数据,如下图所示。系统通过这些配置,自动维护全路径id、全路径编码、全路径名称、叶子节点等列的值。需求中的指标编码,使用全路径编码列实现,使用“-”作为分隔符。
页面开发
添加页面
添加空白页面
添加数据组件
在页面上添加数据组件,用于增删改查数据。
- 从“数据”中拖拽“经济考核指标”数据组件到页面上,如下图所示,id 设置为 indicatorData,用于绑定到左侧表格上,显示经济指标数据
"经济考核指标"数据组件添加“计算列”:新指标编码;并选中“加载全部树形数据”
设置计算列"新指标编码"的计算规则。系统生成的全路径编码以分隔符开头,如 -BA-02-02,界面上需要显示 BA-02-02,因此设置“新指标编码”为从第二位开始的“全路径编码”
- 从“数据”中拖拽“项目预算”数据组件到页面上,id 设置为 budgetData,用于绑定到右侧表格上,显示项目预算数据
“项目预算”数据组件也添加“计算列”:新指标编码;并选中“加载全部树形数据”。设置方法同"经济考核指标"数据组件。
数据组件默认是自动加载数据的,设置“项目预算”数据组件为不自动加载数据,如下图所示
- 从“数据”中拖拽“自定义数据“添加到页面上,显示名称设置为“页面数据”,id 设置为 pageData,作为一个存储页面状态的数据组件
通过“编辑列”添加几列(ID列是必须的),有的用于数据组件的过滤条件,有的用于控制按钮是否可用。
通过“编辑数据”添加一行数据,用于设置初始值。例如“是否已添加”设置为true,一打开页面,“添加”按钮为禁用状态。
- 再从“数据”中拖拽“项目预算”数据组件到页面上,显示名称改为“项目预算(查询)”,id 设置为 queryData,用于查询项目预算
在需要的时候才查询数据,因此设置为不自动加载数据,如下图所示
“项目预算”作为一个树形数据,默认查询根数据,取消选中“树形数据”,即可作为普通数据使用。设置过滤条件如下图所示,当页面数据的查询预算 id 列表中的值改变后,该数据组件的过滤条件发生变化
添加展现组件
页面由分割面板、卡片、工具栏、按钮、表格等组件组成,页面结构如下图所示
在页面上添加组件的过程如下
- 在页面上添加分割面板组件,设置右侧的分割面板项的初始大小为2,形成1:2的宽度效果。分割面板支持左右拖拽中间的分割线,从而改变两个区域的默认宽度
- 左分割面板项
- 添加卡片组件,形成区域的边距
- 在卡片组件中添加工具栏组件
- 在工具栏组件中添加“添加”按钮,用于实现将当前经济指标添加到项目预算中
- 在卡片组件中添加表格组件
- 表格组件绑定"经济考核指标"数据组件
- 添加新指标编码、指标名称、下发指标数值等列。将“新指标编码”改为“指标编码”
- 在卡片组件中添加工具栏组件
- 添加卡片组件,形成区域的边距
- 右分割面板项
- 添加卡片组件,形成区域的边距
- 在卡片组件中添加工具栏组件
- 在工具栏组件中添加“右侧区域”,在右侧区域中添加“保存”按钮,用于保存项目预算
- 在卡片组件中添加表格组件
- 表格组件绑定“项目预算”数据组件
- 添加新指标编码、指标名称、预算值、单价、数量、主键列。将“新指标编码”改为“指标编码”,将“主键”改为“操作”
- 在操作列中添加“新增”和“删除”两个按钮。新增按钮用于拆解项目预算、删除按钮用于删除项目预算
- 在指标名称、单价、数量中添加输入框组件,如下图所示,绑定表格当前行的列,用于编辑项目预算。在指标名称中添加文本组件,绑定表格当前行的列,用于显示指标名称
- 在卡片组件中添加工具栏组件
- 添加卡片组件,形成区域的边距
- 左分割面板项
实现业务逻辑
将经济指标添加到项目预算中
将当前经济指标添加到项目预算时,需要把当前经济指标及其上级全部节点(不在项目预算中的节点)添加到项目预算中。添加“添加”按钮的“点击”事件,如下图所示
点击事件 JS 代码如下
react 代码
//添加到右侧
onAddBtnClick = (event) => {
//获取经济指标树的当前行
let row = this.comp("indicatorData").getCurrentRow();
this.addBudget(row);
}
//添加到右侧
addBudget = async (row) => {
//从经济指标树当前行的全路径id列中获取各级节点的id
let fullIdArr = row.fullId.split("/");
fullIdArr.splice(0, 1);
//给过滤条件赋值
this.comp("pageData").setValue("ids", fullIdArr.join(","));
//查询项目预算中是否包括这些节点
let queryData = this.comp("queryData");
await queryData.refreshData();
for (let id of fullIdArr) {
let queryRow = queryData.find(["id"], [id]);
let targetRows = this.comp("budgetData").find(["id"], [id]);
//没有此节点则添加
if (queryRow.length == 0 && targetRows.length == 0) {
//从经济指标树中查找
let originRows = this.comp("indicatorData").find(["id"], [id]);
if (originRows.length > 0) {
let originRow = originRows[0];
//添加到项目预算树中
await this.comp("budgetData").newData({
parentRow: originRow.parentId,
defaultValues: [{
id: originRow.id,
parentId: originRow.parentId,
code: originRow.code,
name: originRow.name,
fullCode: originRow.fullCode,
fullName: originRow.fullName,
fullId: originRow.fullId,
isOriginLeaf: originRow.isLeaf, //存储是否是指标中的叶子
isLeaf: originRow.isLeaf,
isNew: 0 //不是新增
}]
});
}
}
}
}
vue 代码
let $page = usePage();
let pageData = useData("pageData");
let indicatorData = useData("indicatorData");
let queryData = useData("queryData");
let budgetData = useData("budgetData");
//添加到右侧
let onAddBtnClick = (event) => {
//获取经济指标树的当前行
let row = indicatorData.getCurrentRow();
addBudget(row);
}
//添加到右侧
let addBudget = async (row) => {
//从经济指标树当前行的全路径id列中获取各级节点的id
let fullIdArr = row.fullId.split("/");
fullIdArr.splice(0, 1);
//给过滤条件赋值
pageData.setValue("ids", fullIdArr.join(","));
//查询项目预算中是否包括这些节点
await queryData.refreshData();
for (let id of fullIdArr) {
let queryRow = queryData.find(["id"], [id]);
let targetRows = budgetData.find(["id"], [id]);
//没有此节点则添加
if (queryRow.length == 0 && targetRows.length == 0) {
//从经济指标树中查找
let originRows = indicatorData.find(["id"], [id]);
if (originRows.length > 0) {
let originRow = originRows[0];
//添加到项目预算树中
await budgetData.newData({
parentRow: originRow.parentId,
defaultValues: [{
id: originRow.id,
parentId: originRow.parentId,
code: originRow.code,
name: originRow.name,
fullCode: originRow.fullCode,
fullName: originRow.fullName,
fullId: originRow.fullId,
isOriginLeaf: originRow.isLeaf, //存储是否是指标中的叶子
isLeaf: originRow.isLeaf,
isNew: 0 //不是新增
}]
});
}
}
}
}
在表格中双击行,也实现添加经济指标到项目预算中。添加表格组件的“行双击事件”,如下图所示
行双击事件 JS 代码如下:
react 代码
//双击行添加到右侧
onIndicatorTableRowDoubleClick = ({ event, record, index }) => {
this.addBudget(record);
}
vue 代码
let onIndicatorTableRowDblclick = ({event,record,index}) => {
addBudget(record);
}
已添加的经济指标,不必再添加。给“添加”按钮设置“禁用”属性,如下图所示
禁用表达式为:页面数据中的“是否已添加”为“是”时,添加按钮禁用,如下图所示
在“项目预算”数据组件的“刷新后事件”中,查询有没有当前的经济指标,如果有则设置“页面数据”中的“是否已添加”为“是”。给项目预算数据组件添加“刷新后事件”,如下图所示
刷新后事件 JS 代码如下
//项目预算刷新后,判断有没有包含当前经济指标,如果没有包含,添加按钮可用
onBudgetDataAfterRefresh = (event) => {
let id = this.comp("indicatorData").current.id;
let rows = event.source.find(["id"], [id]);
this.comp("pageData").setValue("added", rows.length > 0 ? true : false);
}
编制项目预算
项目预算数据编辑、拆解、删除的规则如下:
- 新增的数据 isNew=1 可编辑指标名称
- 叶子节点 isLeaf=1 可编辑单价和数量
- 指标叶子 isOriginLeaf=1 或者 新增的数据 isNew=1 可拆解
- 新增的数据 isNew=1 可删除
用上面的规则设置表格中的输入框、文本和按钮的“动态隐藏”属性,用于控制显示或隐藏。
表格“指标名称”列中的输入框组件的“动态隐藏”属性设置为:是否新增等于1时显示,如下图所示
表格“操作”列中的拆解按钮的"动态隐藏"属性设置为:指标叶子或是否新增等于1时显示,如下图所示
在表格中输入单价和数量后,自动计算预算值,并同步更新父节点的预算值。在“项目预算”数据组件的”数据改变后“事件中实现,如下图所示
数据改变后事件 JS 代码如下
//计算本行的预算值=单价*数量,计算父的预算值
onBudgetDataValueChanged = (event) => {
//数据加载中,不计算
if (event.source.loading.value) return;
//预算值改变,修改父的预算值
if (event.col == "budget") {
let diff = event.newValue - event.oldValue;
let rows = event.source.find(["id"], [event.row.parentId]);
if (rows.length > 0) {
rows[0].budget += diff;
}
} else if(event.col == "price" || event.col == "num"){//单价和数量改变,修改本行的预算值
event.row.budget = event.row.price * event.row.num;
}
}
拆解项目预算
在页面上添加序号组件,用于生成新的指标编码
表格“操作”列中的拆解按钮,添加点击事件,开启扩展参数,如下图所示
参数名称自定义,参数值选择“渲染-行记录”,如下图所示
点击事件 JS 代码如下
react 代码
//拆解项目预算
onNewBtnClick = ({row}) => async (event) => {
//将当前行的数量、单价清零,设置不是叶子节点
this.comp("budgetData").setValueByID("price", 0, row.id);
this.comp("budgetData").setValueByID("num", 0, row.id);
this.comp("budgetData").setValueByID("isLeaf", 0, row.id);
//使用序号组件,获取新的序号
let res = await this.comp("wxSn0").next(row.id, "%02d", 1);
let no = res.split(row.id)[1];
//在当前行下增加新的记录
await this.comp("budgetData").newData({
parentRow: row.id,
defaultValues: [{
code: no,
fullCode: row.fullCode + "-" + no, //全路径编码 = 父的全路径编码 + 新的编码
isLeaf: 1, //是叶子
isNew: 1 //是新增的数据
}]
})
}
vue 代码
//拆解项目预算
let onNewBtnClick = ({row}) => async (event) => {
//将当前行的数量、单价清零,设置不是叶子节点
budgetData.setValueByID("price", 0, row.id);
budgetData.setValueByID("num", 0, row.id);
budgetData.setValueByID("isLeaf", 0, row.id);
//使用序号组件,获取新的序号
let res = await $page.comp("wxSn0").next(row.id, "%02d", 1);
let no = res.split(row.id)[1];
//在当前行下增加新的记录
await budgetData.newData({
parentRow: row.id,
defaultValues: [{
code: no,
fullCode: row.fullCode + "-" + no, //全路径编码 = 父的全路径编码 + 新的编码
isLeaf: 1, //是叶子
isNew: 1 //是新增的数据
}]
})
}
校验是否超过预算
保存按钮的点击事件选择“项目预算”数据组件的保存操作,如下图所示,点保存按钮时,会调用“项目预算”数据组件的保存方法。
数据组件提供“保存前事件”,用于进行数据校验,校验通过则执行保存,校验不通过时,执行代码 event.cancel = true; 将停止保存。“项目预算”数据组件设置“保存前事件”,如下图所示
保存前事件 JS 代码如下
react 代码
import { message } from "antd";
//保存前判断是否超过预算
onBudgetDataBeforeSave = (event) => {
let indicatorData = this.comp("indicatorData");
//遍历经济指标
event.source.each(({ row }) => {
let rows = indicatorData.find(["id"], [row.id]);
if (rows.length > 0) {
if(rows[0].number && row.budget && rows[0].number < row.budget){
message.error(row.name+"已超过预算");
//超过预算,不保存
event.cancel = true;
}
}
})
}
vue 代码
import {message} from 'ant-design-vue';
//保存前判断是否超过预算
onBudgetDataBeforeSave = (event) => {
let indicatorData = this.comp("indicatorData");
//遍历经济指标
event.source.each(({ row }) => {
let rows = indicatorData.find(["id"], [row.id]);
if (rows.length > 0) {
if(rows[0].number && row.budget && rows[0].number < row.budget){
message.error(row.name+"已超过预算");
//超过预算,不保存
event.cancel = true;
}
}
})
}
只显示当前经济指标的项目预算
给“项目预算”数据组件设置过滤条件,只显示当前经济指标的相关数据,如下图所示
给“经济指标”表格添加“行点击事件”,如下图所示。点击表格的行时,根据当前经济指标的数据,设置“项目预算”数据组件的过滤条件,再刷新“项目预算”数据
行点击事件 JS 代码如下
react 代码
//点击经济指标,项目预算显示相关数据
onIndicatorTableRowClick = ({ event, record, index }) => {
if (record.fullId) {
//设置过滤条件
this.comp("pageData").setValue("budgetFullId", record.fullId);
let fullIdArr = record.fullId.split("/");
fullIdArr.splice(0, 1);
this.comp("pageData").setValue("budgetIds", fullIdArr.join(","));
//刷新项目预算
this.comp("budgetData").refreshData();
}
}
vue 代码
let onIndicatorTableRowClick = async ({event,record,index}) => {
if (record.fullId) {
//设置过滤条件
pageData.setValue("budgetFullId", record.fullId);
let fullIdArr = record.fullId.split("/");
fullIdArr.splice(0, 1);
pageData.setValue("budgetIds", fullIdArr.join(","));
//刷新项目预算
await budgetData.refreshData();
}
}