项目预算编制

运行效果

项目预算编制页面分为左右两部分,左边是经济考核指标树,右边是预算编制树。界面如下图所示

案例需求

案例的业务逻辑就是一个工程项目经营预算编制。单据有2个:

  • 上游单据:经济考核指标
  • 下游单据:项目预算编制

具体要求如下:

  1. 左边树显示列:指标编码、指标名称、下发指标数值
  2. 右边树显示列:指标编码、指标名称、预算值、数量、单价,其中预算值 = 数量 * 单价,预算值需要向上汇总
  3. 左边树点击后,右边树可以新增,新增的时候,要把左边当前点击节点和它的上级全部节点带到右边树
  4. 右边只能在左边的叶子节点下新增节点
  5. 右边树的指标不能超过左边树的下发指标数值

知识点

  1. 使用了树形表格、分割面板等组件
  2. 通过代码新增树形数据
  3. 通过代码查询某个节点的所有父节点及子节点

开发过程

数据开发

经济考核指标

添加动态数据集,包括主键、指标编码、指标名称、下发指标数值等列。作为树形数据包括父id、全路径id、全路径编码、全路径名称、叶子节点等列,如下图所示

配置为树形数据,如下图所示。系统通过这些配置,自动维护全路径id、全路径编码、全路径名称、叶子节点等列的值。需求中的指标编码,使用全路径编码列实现,使用“-”作为分隔符。叶子节点及末级节点,整型数据,1表示是叶子节点。

添加初始数据

项目预算

添加动态数据集,包括主键、指标编码、指标名称、预算值、单价、数量、指标叶子、是否新增等列。“指标叶子”和“是否新增”用于控制是否可编辑、可拆解。作为树形数据包括父id、全路径id、全路径编码、全路径名称、叶子节点等列,如下图所示

配置为树形数据,如下图所示。系统通过这些配置,自动维护全路径id、全路径编码、全路径名称、叶子节点等列的值。需求中的指标编码,使用全路径编码列实现,使用“-”作为分隔符。

页面开发

添加页面

添加空白页面

添加数据组件

在页面上添加数据组件,用于增删改查数据。

  • 从“数据”中拖拽“经济考核指标”数据组件到页面上,如下图所示,id 设置为 indicatorData,用于绑定到左侧表格上,显示经济指标数据

"经济考核指标"数据组件添加“计算列”:新指标编码;并选中“加载全部树形数据”

设置计算列"新指标编码"的计算规则。系统生成的全路径编码以分隔符开头,如 -BA-02-02,界面上需要显示 BA-02-02,因此设置“新指标编码”为从第二位开始的“全路径编码”

  • 从“数据”中拖拽“项目预算”数据组件到页面上,id 设置为 budgetData,用于绑定到右侧表格上,显示项目预算数据

“项目预算”数据组件也添加“计算列”:新指标编码;并选中“加载全部树形数据”。设置方法同"经济考核指标"数据组件。

数据组件默认是自动加载数据的,设置“项目预算”数据组件为不自动加载数据,如下图所示

  • 从“数据”中拖拽“自定义数据“添加到页面上,显示名称设置为“页面数据”,id 设置为 pageData,作为一个存储页面状态的数据组件

通过“编辑列”添加几列(ID列是必须的),有的用于数据组件的过滤条件,有的用于控制按钮是否可用。

通过“编辑数据”添加一行数据,用于设置初始值。例如“是否已添加”设置为true,一打开页面,“添加”按钮为禁用状态。

  • 再从“数据”中拖拽“项目预算”数据组件到页面上,显示名称改为“项目预算(查询)”,id 设置为 queryData,用于查询项目预算

在需要的时候才查询数据,因此设置为不自动加载数据,如下图所示

“项目预算”作为一个树形数据,默认查询根数据,取消选中“树形数据”,即可作为普通数据使用。设置过滤条件如下图所示,当页面数据的查询预算 id 列表中的值改变后,该数据组件的过滤条件发生变化

1727680855768

添加展现组件

页面由分割面板、卡片、工具栏、按钮、表格等组件组成,页面结构如下图所示

在页面上添加组件的过程如下

  • 在页面上添加分割面板组件,设置右侧的分割面板项的初始大小为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();
    }
}

results matching ""

    No results matching ""