| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207 | /* * @Descripttion: 超高降效相关 * @Author: Zhong * @Date: 2019-12-19 17:45:40 *//* * 整体逻辑为: * 1.一个请求中 *   {删除}所有超高子目 *   {更新}清单、定额下拉列文本 *   {新增}清单、定额、定额人材机、项目人材机 * 2.一个请求{重算被删除的定额清单节点、新增的定额节点 */const OVER_HEIGHT = (() => {    // 选项类型,生成的超高子目所在位置    const Option = {        // 对应清单或分部下(默认)        SEPARATION: 1,        // 指定措施清单011704001        MEASURE: 2,        // 指定具体位置,显示分部分项以及措施项目的树结构显示叶子清单(分项)供勾选        SPECIFIC: 3,    };    // 系数类别,类别不同,生成的定额人材机和算法不同    const RateType = {        LABOUR: 1,        MACHINE: 2,        LABOUR_MACHINE: 3,    };    // 系数类别对应的汇总定额金额字段(feeIndex里的字段)    const FeeField = {        [RateType.LABOUR]: 'labour',        [RateType.MACHINE]: 'machine',        [RateType.LABOUR_MACHINE]: 'material',    };    // 系数类别的定额人材机基础数据,不同系数类别,对应的人材机编码、名称等不同    const BaseRatoinGLJ = {        [RateType.LABOUR]: {            code: '00010052',            original_code: '00010052',            name: '人工降效',            type: gljType.LABOUR,            shorName: '人',        },        [RateType.MACHINE]: {            code: '99918004',            original_code: '99918004',            name: '机械降效',            type: gljType.GENERAL_MACHINE,            shorName: '机',        },        [RateType.LABOUR_MACHINE]: {            code: '00010053',            original_code: '00010053',            name: '人工、机械降效增加费',            type: gljType.GENERAL_MATERIAL,            shorName: '材',        },    }    // 选项二时的前九位清单编号    const fixedCode = '011704001';    const fixedCodeReg = new RegExp(`^${fixedCode}`);    // 取费专业名称    const programName = '超高降效';    //const programName = '公共建筑工程'; // for Test    // 指定清单表格    let specificSpread = null;    // 指定清单树    let specificTree = null;    // 指定清单窗口界面设置    const specificTreeSetting = {        emptyRowHeader: true,        rowHeaderWidth: 15,        treeCol: 0,        emptyRows: 0,        headRows: 1,        headRowHeight: [40],        defaultRowHeight: 21,        cols: [{            width: 140,            readOnly: true,            head: {                titleNames: ["编码"],                spanCols: [1],                spanRows: [1],                vAlign: [1],                hAlign: [1],                font: ["Arial"]            },            data: {                field: "code",                vAlign: 1,                hAlign: 0,                font: "Arial"            }        }, {            width: 45,            readOnly: true,            head: {                titleNames: ["类型"],                spanCols: [1],                spanRows: [1],                vAlign: [1],                hAlign: [1],                font: ["Arial"]            },            data: {                field: "subType",                vAlign: 1,                hAlign: 1,                font: "Arial"            }        },        {            width: 205,            readOnly: true,            head: {                titleNames: ["名称"],                spanCols: [1],                spanRows: [1],                vAlign: [1],                hAlign: [1],                font: ["Arial"]            },            data: {                field: "name",                vAlign: 1,                hAlign: 0,                font: "Arial"            }        },        {            width: 60,            readOnly: true,            head: {                titleNames: ["具体位置"],                spanCols: [1],                spanRows: [1],                vAlign: [1],                hAlign: [1],                font: ["Arial"]            },            data: {                field: "specific",                vAlign: 1,                hAlign: 1,                font: "Arial"            }        },        ]    };    // 源数据    let sourceData;    // 下拉项    let comboData;    // 初始化源数据和下拉项数据    function init(source) {        sourceData = source || [];        comboData = sourceData            ? sourceData                .filter(item => !item.extra || !JSON.parse(item.extra))                .map(item => item.name)            : [];    }    // 获取下拉项    function getComboData() {        return comboData;    }    // 根据名称获取下拉项索引    function getIndex(name) {        return comboData.findIndex(item => item === name);    }    function getOverHeightItem(value) {        return sourceData.find(item => item.name === value);    }    // 获取系数    function getRate(overHeightItem) {        return overHeightItem.labourRate            || overHeightItem.machineRate            || overHeightItem.labourMachineRate            || null;    }    // 下拉项是否需要计算(生成子目)    function isNeedToCalc(overHeightItem) {        if (!overHeightItem) {            return false;        }        const rate = getRate(overHeightItem);        return !!rate;    }    // 是否是超高子目    function isOverHeight(node) {        return node            && node.sourceType === projectObj.project.Ration.getSourceType()            && node.data.type === rationType.overHeight;    }    // 获取超高降效列号    function getOverHeightCol() {        return projectObj.project.projSetting.main_tree_col.cols.findIndex(item => item.data.field === 'overHeight');    }    // 获取指定清单指定列    function getSpecificCol() {        return specificTreeSetting.cols.findIndex(item => item.data.field === 'specific');    }    // 获取触发动作:选项1、选项2、选项3,选项3时需要指定清单ID(specificID)    function getAction() {        return {            option: projectObj.project.projectInfo.property.overHeightOption || Option.SEPARATION,            specificID: projectObj.project.projectInfo.property.overHeightSpecificID || null,        };    }    // 超高降效列的控制,右键计取触发    function switchVisible(visible) {        const curVisible = colSettingObj.getVisible('overHeight');        if (curVisible === visible) {            return;        }        colSettingObj.setVisible('overHeight', visible);        colSettingObj.updateColSetting(true);    }    // 获取系数类型列表    function getRateTypeList(overHeightItem) {        const rst = [];        if (commonUtil.isNumber(overHeightItem.labourRate)) {            rst.push({ type: RateType.LABOUR, rate: overHeightItem.labourRate });        }        if (commonUtil.isNumber(overHeightItem.machineRate)) {            rst.push({ type: RateType.MACHINE, rate: overHeightItem.machineRate });        }        if (commonUtil.isNumber(overHeightItem.labourMachineRate)) {            rst.push({ type: RateType.LABOUR_MACHINE, rate: overHeightItem.labourMachineRate });        }        return rst;    }    // 有效化变化节点的值,若值为无效值(下拉项中不存在),则将变化节点的值设成原值    function validateData(changedData) {        changedData.forEach(item => {            if (!comboData.includes(item.value)) {                item.value = item.node.data.overHeight;            }        });    }    // 简化变化节点:由于子项值继承父项,且变更节点中可能存在父子关系,因此需要去除子项节点    function simplifyData(changedData) {        const rst = [];        const nodes = changedData.map(item => item.node);        changedData.forEach(item => {            let parent = item.parent;            // 父项不存在变化节点中才将此数据放入返回数组中            while (parent) {                if (nodes.includes(parent)) {                    return;                }                parent = parent.parent;            }            rst.push(item);        });        return rst;    }    // 设置单元格文本,单元格文本数据为暂存数据,方便后续获取更新、新增数据,若后续操作失败,则可用节点数据恢复单元格文本内容。    function setTexts(changedData) {        const sheet = projectObj.mainController.sheet;        const func = () => {            const overHeightCol = getOverHeightCol();            changedData.forEach(item => {                // 子项值随父项                const nodes = [item.node, ...item.node.getPosterity()];                nodes.forEach(node => {                    const row = node.serialNo();                    // 单元格没被锁定才填写暂存值                    const locked = sheet.getCell(row, overHeightCol).locked();                    if (!locked) {                        sheet.setText(row, overHeightCol, item.value)                    }                });            });        };        TREE_SHEET_HELPER.massOperationSheet(sheet, func);    }    // 获取其他措施费项目底下固定的节点(011704001...): 选项二时    function getMeasureFixedNode() {        const otherMeasureNode = projectObj.project.mainTree.items.find(node => node.getFlag() === fixedFlag.OTHER_MEASURE_FEE);        const measureChildren = otherMeasureNode.getPosterity();        return measureChildren.find(node => node.data.code && fixedCodeReg.test(node.data.code));    }    // 获取指定清单节点:选项三时    function getSpecificNode(specificID) {        return specificID            ? projectObj.project.mainTree.nodes[`id_${specificID}`]            : null;    }    // 变更超高降效列的操作检验,若选项为2、3时,需检验指定清单是否还存在,不存在则取消操作和提示    function checkAction(action) {        const { option, specificID } = action;        if (option === Option.SEPARATION) {            return true;        } else if (option === Option.MEASURE) {            const isValid = !!getMeasureFixedNode();            if (!isValid) {                $('#overHeightMeasure').modal('show');            }            return isValid;        } else if (option) {            const isValid = !!getSpecificNode(specificID);            if (!isValid) {                $('#overHeightSpecific').modal('show');            }            return isValid;        }    }    // 设置指定清单界面选择框    function setCheckBox(sheet, specificNodes) {        const checkBox = new GC.Spread.Sheets.CellTypes.CheckBox();        const specificCol = getSpecificCol();        // 只有叶子节点和非计算基数节点才是选择框        const func = () => {            specificNodes.forEach((node, index) => {                if (!node.children.length && !node.data.calcBase) {                    sheet.getCell(index, specificCol).cellType(checkBox);                }            });        };        TREE_SHEET_HELPER.massOperationSheet(sheet, func);    }    // 初始化指定清单选择树    function initSpecificTree(data, sheet, setting) {        specificTree = idTree.createNew({ id: 'ID', pid: 'ParentID', nid: 'NextSiblingID', rootId: -1, autoUpdate: true });        const controller = TREE_SHEET_CONTROLLER.createNew(specificTree, sheet, setting, false);        specificTree.loadDatas(data);        controller.showTreeData();        sheet.setRowCount(data.length);        setCheckBox(sheet, specificTree.items);    }    // 表格点击事件,只有checkbox单元格会触发这个方法    function handleSpreadButtonClick(e, args) {        const { sheet, row, col } = args;        // 只能单选,清空其他单元格的值并设置当前值        const func = () => {            const rowCount = sheet.getRowCount();            const oldValue = sheet.getValue(row, col);            for (let row = 0; row < rowCount; row++) {                sheet.setValue(row, col, '');            }            sheet.setValue(row, col, !oldValue);        }        TREE_SHEET_HELPER.massOperationSheet(sheet, func);    }    // 初始化表格    function initWorkbook() {        specificSpread = sheetCommonObj.createSpread($('#specificArea')[0], 1);        specificSpread.options.allowUserDragDrop = false; // 不允许拖填充,影响点击        sheetCommonObj.spreadDefaultStyle(specificSpread);        specificSpread.bind(GC.Spread.Sheets.Events.ButtonClicked, handleSpreadButtonClick);        // 设置表头        const sheet = specificSpread.getSheet(0);        const headers = sheetCommonObj.getHeadersFromTreeSetting(specificTreeSetting);        sheetCommonObj.setHeader(sheet, headers);        return specificSpread;    }    // 从造价书界面筛选获取指定清单界面数据    function getSpecificData() {        // 只显示分部分项、措施项目的清单节点        const billsNodes = projectObj.project.Bills.tree.items;        const flags = [fixedFlag.SUB_ENGINERRING, fixedFlag.MEASURE];        const billsData = flags.reduce((allData, flag) => {            const fixedNode = billsNodes.find(node => node.getFlag() === flag);            const posterity = fixedNode.getPosterity();            const data = [fixedNode, ...posterity].map(node =>                ({                    ID: node.data.ID,                    ParentID: node.data.ParentID,                    NextSiblingID: node.data.NextSiblingID,                    code: node.data.code,                    subType: billText[node.data.type],                    name: node.data.name,                    calcBase: node.data.calcBase,                }));            allData.push(...data);            return allData;        }, []);        return billsData;    }    // 设置指定ID选中    function setSpecific(ID) {        if (!ID) {            return;        }        const sheet = specificSpread.getSheet(0);        const nodes = specificTree.items;        const index = nodes.findIndex(node => node.data.ID === ID);        if (!~index) {            return;        }        const col = getSpecificCol();        sheet.setValue(index, col, true);    }    // 从表格中获取勾选的指定ID    function getSpecificFromSheet() {        const sheet = specificSpread.getSheet(0);        const nodes = specificTree.items;        const rowCount = sheet.getRowCount();        const col = getSpecificCol();        for (let row = 0; row < rowCount; row++) {            const value = sheet.getValue(row, col);            if (value) {                return nodes[row].data.ID;            }        }        return null;    }    // 初始化指定清单选择界面    function initSpecificModal() {        $('#specificArea').show();        const spread = initWorkbook();        const sheet = spread.getSheet(0);        const data = getSpecificData();        initSpecificTree(data, sheet, specificTreeSetting);        const { specificID } = getAction();        setSpecific(specificID);    }    // 隐藏指定清单选择界面    function hideSpecificModal() {        $('#specificArea').hide();        if (specificSpread) {            specificSpread.destroy();            specificSpread = null;        }    }    // 初始化子目生成方式选项设置窗口    function initModal() {        const { option, specificID } = getAction();        $(`input[name=cgx][value=${option}]`).prop('checked', 'checked');        $('#specificArea').hide();        if (option === Option.SPECIFIC) {            initSpecificModal(specificID);        }    }    // 超高降效下拉项或选项是否改变了    function isValueChanged() {        const updateData = getUpdateData();        return !!(updateData.bills.length || updateData.ration.length);    }    // 对比两个选项行为,获取更新选项数据    function getUpdateProjectData(oldAction, newAction) {        if (!oldAction || !newAction) {            return {};        }        const optionDiff = oldAction.option !== newAction.option;        const specificDiff = oldAction.specificID !== newAction.specificID;        const updateData = {            ID: projectObj.project.ID(),            overHeightOption: newAction.option,            overHeightSpecificID: newAction.specificID,        };        return optionDiff || specificDiff            ? updateData            : {};    }    /**     * 获取更新数据:对比项目节点中超高降效的新旧值,新值为暂存的单元格文本,旧值为节点data数据     * @param {Object} newAction - 选项行为     * @return {Object} - {     *                      project: {ID: Number,overHeightOp: Number, overHeightSpecificID: Number||Null},      *                      bills: [{ID: Number, overHeight: String}],      *                      ration: [{ID: Number, overHeight: String}]     *                    }     */    function getUpdateData(newAction) {        const update = {            project: {}, // 可能会更改项目属性的超高降效设置            bills: [],            ration: [],        };        const oldAction = getAction();        const updateProjectData = getUpdateProjectData(oldAction, newAction);        Object.assign(update.project, updateProjectData);        const nodes = projectObj.project.mainTree.items;        const sheet = projectObj.mainController.sheet;        const overHeightCol = getOverHeightCol();        nodes.forEach((node, index) => {            const newValue = sheet.getText(index, overHeightCol);            const oldValue = node.data.overHeight;            // 非严等            if (!commonUtil.similarEqual(newValue, oldValue)) {                const type = node.sourceType === projectObj.project.Bills.getSourceType()                    ? 'bills'                    : 'ration';                update[type].push({                    ID: node.data.ID,                    overHeight: newValue                });            }        });        return update;    }    /**     * 获取删除数据:项目中所有超高子目     * @return {Object} - {ration: [{ID: Number}]}     */    function getDeleteData() {        const del = {            ration: [],        };        const rations = projectObj.project.Ration.datas;        del.ration = rations            .filter(ration => ration.type === rationType.overHeight)            .map(ration => ({ ID: ration.ID }));        return del;    }    /**     * 获取需要生成超高子目的定额节点     * @return {Array} - [{node: Object, overHeight: String}]     */    function getNeedCalcRationItems() {        // 从整个项目中筛选当前下拉项单元格的文本是需要计算的定额节点        const nodes = projectObj.project.mainTree.items;        const sheet = projectObj.mainController.sheet;        const overHeightCol = getOverHeightCol();        const rst = [];        nodes.forEach(node => {            // 非超高子目的定额节点才生成            const notOverHeightRationNode = node.sourceType !== projectObj.project.Ration.getSourceType()                || node.data.type === rationType.overHeight;            if (notOverHeightRationNode) {                return;            }            const overHeight = sheet.getText(node.serialNo(), overHeightCol);            const overHeightItem = getOverHeightItem(overHeight);            if (isNeedToCalc(overHeightItem)) {                rst.push({ node, overHeight });            }        });        return rst;    }    // 根据选项获取超高子目挂载的清单    function getMountedBills(action) {        const { option, specificID } = action;        // 生成清单数据        function initMountedBills() {            // 生成的清单位置为其他措施项目的首项            const measureNode = projectObj.project.mainTree.items.find(node => node.getFlag() === fixedFlag.OTHER_MEASURE_FEE);            const firstNode = measureNode.children[0];            //const parent = measureNode.children[measureNode.children.length - 1];            // 具体完整数据需要在后端跟标准数据对比完善            return {                projectID: projectObj.project.ID(),                billsLibId: +projectObj.project.projectInfo.engineeringInfo.bill_lib[0].id,                ID: uuid.v1(),                ParentID: measureNode.data.ID,                NextSiblingID: firstNode ? firstNode.data.ID : -1,                type: billType.BILL,                code: fixedCode,                name: '超高施工增加',                unit: 'm2',                quantity: '1',            };        }        // 选项一        if (option === Option.SEPARATION) {            return {                isNew: false,                bills: null,            };        } else if (option === Option.MEASURE) {   // 选项二且造价书没有相关清单,需要插入清单            const fixedNode = getMeasureFixedNode();            return {                isNew: !fixedNode,                bills: fixedNode ? fixedNode.data : initMountedBills(),            };        } else {            const specificNode = getSpecificNode(specificID);            return {                isNew: false,                bills: specificNode.data,            }        }    }    // 获取清单节点下最末非超高子目定额    function getLastNormalRationNode(billsNode) {        const nodes = billsNode && billsNode.children            ? billsNode.children.filter(node => node.data.type !== rationType.overHeight)            : [];        return nodes[nodes.length - 1]    }    // 将需要生成超高子目的定额数据按照挂载清单和超高名称进行分组,其下数组为关联的主定额    // @return {Object} {'billsID@overHeight': [node: rationNode]}    function getGroupData(rationItems, mountedBillsID) {        // mapping: billsID-overHeightName        const group = {};        rationItems.forEach(item => {            // 无指定的清单ID,则挂载在各自的清单上            const billsID = mountedBillsID || item.node.data.billsItemID            const key = `${billsID}@${item.overHeight}`;            if (!group[key]) {                group[key] = [];            }            group[key].push(item.node);        });        return group;    }    // 根据系数类型获取汇总的定额金额,这个金额作为计算定额人材机的基数    function getFeeByRateType(rateType, referenceRations) {        const feeField = FeeField[rateType];        // 汇总定额节点相关综合合价        return referenceRations.reduce((sum, rationNode) => {            const feeObj = rationNode.data.feesIndex;            const totalFee = feeObj && feeObj[feeField]                ? feeObj[feeField].totalFee                : 0;            return scMathUtil.roundForObj(sum + totalFee, decimalObj.process);        }, 0);    }    // 通过超高降效计算得来的定额人材机消耗量:消耗量=定额消耗量=算出来的值    function getQuantity(rate, fee) {        return String(scMathUtil.roundForObj(rate * fee, decimalObj.glj.quantity));    }    /**     * 获取定额喝定额人材机数据      * @param {Array} rationItems - 需要生成超高子目的定额数据{node: Object, overHtight: String}     * @param {Object} mountedBills - 挂载到的清单数据     * @return {Object} - {ration: Array, rationGLJ: Array}      */    function getAddRationAndRationGLJData(rationItems, mountedBills) {        // 生成定额数据        function initRation(bills, overHeightItem, serialNo) {            const programID = projectObj.project.calcProgram.compiledTemplateMaps[programName];            // 生成的超高子目消耗量为1            const quantity = '1';            // 含量为 定额消耗量 / 清单消耗量            const tempV = quantity / bills.quantity;            const contain = isFinite(tempV)                ? scMathUtil.roundForObj(tempV, decimalObj.ration.quantity)                : '0';            return {                projectID: projectObj.project.ID(),                billsItemID: bills.ID,                ID: uuid.v1(),                programID,                serialNo,                code: overHeightItem.code,                name: overHeightItem.name,                unit: overHeightItem.unit,                type: rationType.overHeight,                quantity,                contain,            }        }        // 根据分组的keys获取定额serialNo值映射表 billsID@overHeight        function getSerialNoMappig(groupKeys) {            // 清单ID - serialNo映射            const billsIDMap = {};            // 完整的key - serialNo映射            const keyMap = {};            const nodes = projectObj.project.mainTree.nodes;            // 先给根据清单ID设置上第一个serialNo值(基准值),和超高项数据            groupKeys.forEach(key => {                const [billsID, overHeight] = key.split('@');                if (billsIDMap[billsID]) {                    billsIDMap[billsID].items.push(overHeight);                    return;                }                const billsNode = nodes[`id_${billsID}`];                const lastNormalRationNode = billsNode ? getLastNormalRationNode(billsNode) : null;                const serialNo = lastNormalRationNode ? lastNormalRationNode.data.serialNo + 1 : 1;                billsIDMap[billsID] = { serialNo, items: [overHeight] };            });            // 将同一清单下的超高项按照下拉项位置排序            Object.entries(billsIDMap).forEach(([billsID, { serialNo, items }]) => {                items.sort((a, b) => getIndex(a) - getIndex(b));                items.forEach((overHeight, index) => {                    const key = `${billsID}@${overHeight}`;                    keyMap[key] = serialNo + index;                });            });            return keyMap;        }        // 生成超高子目的定额人材机数据,定额人材机的属性只是一部分,还有部分数据需要在后端处理        function initRationGLJ(ration, typeItem, referenceRationNodes) {            const { type, rate } = typeItem;            const sumFee = getFeeByRateType(type, referenceRationNodes);            // 不同类型的基础人材机属性            const quantity = getQuantity(rate, sumFee);            const baseObj = BaseRatoinGLJ[type];            // 补全定额人材机属性,共性属性            const extendObj = {                projectID: projectObj.project.ID(),                ID: uuid.v1(),                billsItemID: ration.billsItemID,                rationID: ration.ID,                repositoryId: -1,                GLJID: -1,                unit: '%',                specs: '',                // 定额人材机没有价格字段,但是生成单价文件需要需要这两个价格字段,默认为“1”                basePrice: '1',                marketPrice: '1',                quantity,                rationItemQuantity: quantity,            };            return { ...baseObj, ...extendObj };        }        const add = {            ration: [],            rationGLJ: []        };        if (!rationItems.length) {            return add;        }        const mountedBillsID = mountedBills            ? mountedBills.ID            : null;        // 分析分组数据,获取定额及定额人材机数据        const group = getGroupData(rationItems, mountedBillsID);        const rationSerialNoMapping = getSerialNoMappig(Object.keys(group));        // 获取定额及定额人材机数据        Object.entries(group).forEach(([key, referenceRationNodes]) => {            const [billsID, overHeight] = key.split('@');            const overHeightItem = getOverHeightItem(overHeight);            const bills = billsID === mountedBillsID                ? mountedBills                : projectObj.project.Bills.datas.find(item => item.ID === billsID);            const serialNo = rationSerialNoMapping[key];            const overHeightRation = initRation(bills, overHeightItem, serialNo);            // 给生成的超高子目定额,设置关联定额列表(关联定额工程量等发生变化,需要重算超高子目)            overHeightRation.referenceRationList = referenceRationNodes.map(node => node.data.ID);            add.ration.push(overHeightRation);            // 根据超高项获取系数列表,系数列表一个元素会根据系数类别生成一条定额人材机(人、机、材料)            const rateTypeList = getRateTypeList(overHeightItem);            const rationGLJs = rateTypeList.map(rateTypeItem => initRationGLJ(overHeightRation, rateTypeItem, referenceRationNodes));            add.rationGLJ.push(...rationGLJs);        });        return add;    }    /**     * 获取插入数据:超高子目(定额)、清单(选项2、3时可能会插入)     * @param {Object} action - {option: Number, specificID: Undefined||Null||Number}     * @return {Array} - {bills: Array, ration: Array, rationGLJ: Array}     */    function getAddData(action) {        const add = {            bills: [],            ration: [],            rationGLJ: [],        };        // 挂载到的清单,新增或已有的        const mountedBills = getMountedBills(action);        if (mountedBills.isNew) {            add.bills.push(mountedBills.bills);        }        // 获取需要生成超高子目的定额数据        const needCalcRationItems = getNeedCalcRationItems();        const subData = getAddRationAndRationGLJData(needCalcRationItems, mountedBills.bills);        add.ration = subData.ration;        add.rationGLJ = subData.rationGLJ;        return add;    }    /**     * 生成传输数据     * @param {Boolean} isCancelCalc - 是否取消计取      * @param {Object} action - 新的选项行为      * @return {Object}     */    function generatePostData(isCancelCalc, action) {        // 默认模板        const postData = {            addData: {                bills: [],                ration: [],                rationGLJ: [],            },            updateData: {                project: {},                bills: [],                ration: [],            },            deleteData: {                ration: [],            },        };        // 取消计取费用,只删除超高子目        if (isCancelCalc) {            postData.deleteData = getDeleteData();            postData.updateData = getUpdateData();            return postData;        }        // 没有新的选项行为,获取当前项目的选项行为        if (!action) {            action = getAction();        }        const addData = getAddData(action);        const updateData = getUpdateData(action);        const deleteData = getDeleteData();        return Object.assign(postData, { addData, updateData, deleteData });    }    /**     * 更改了超高降效列(edited、rangeChanged),触发事件     * @param {Array} changedData - 变化的数据 [{node: Object, value: String}]      * @return {void}      */    function handleValueChanged(changedData) {        validateData(changedData);        changedData = simplifyData(changedData);        setTexts(changedData);        const valuedChanged = isValueChanged();        if (!valuedChanged) {            return;        }        // 选项2、选项3情况下下拉可能会遇到,相关清单已经被删除,需要检测行为        const action = getAction();        const actionValid = checkAction(action);        // actionValid为false的时候,可能后续需要恢复单元格文本值,根据后续用户在弹窗中的选择(后文ready事件中绑定)        if (!actionValid) {            return;        }        handleConfirmed();    }    // 确认事件    async function handleConfirmed(isCancelCalc = false, action = null) {        $.bootstrapLoading.start();        try {            const postData = generatePostData(isCancelCalc, action);            const { addData, updateData, deleteData } = postData;            // 更新、删除、新增数据            // 返回的是新增的清单、定额人材机、项目人材机 rst = {bills: [], rationGLJ: [], projectGLJ: []}            const rst = await ajaxPost('/project/calcOverHeightFee', postData);            // 后续获取重算节点相关:            // 新增的定额节点要在同步数据后才有,删除的定额节点在同步前找不到,因此同步前先获取被删除定额节点的清单ID列表            const rationIDList = deleteData.ration.map(item => item.ID);            let billsIDList = projectObj.project.Ration.datas                .filter(item => rationIDList.includes(item.ID))                .map(item => item.billsItemID);            billsIDList = [...new Set(billsIDList)];            // 同步数据            const newAddData = { ...addData, ...rst };            syncData(deleteData, updateData, newAddData);            // 获取重算节点            const reCalcNodes = getReCalcNodes(billsIDList, newAddData.ration);            if (!reCalcNodes.length) {                $.bootstrapLoading.end();                return;            }            // 获取项目人材机数据,更新缓存            $.bootstrapLoading.end(); // 重算节点相关方法里有loading,防止提前结束了loading            // 重算相关节点            projectObj.project.calcProgram.calcNodesAndSave(reCalcNodes);        } catch (err) {            alert(err);            console.log(err);            recoverCellsText();            $.bootstrapLoading.end();        }    }    /**     * 获取需要重算的节点     * @param {Array} billsIDList - 根据删除的定额ID数据获取的清单ID列表      * @param {Array} rationData - 新增的定额数据     * @return {Array}     */    function getReCalcNodes(billsIDList, rationData) {        // 获取被删除定额的清单节点        const billsNodes = billsIDList.map(ID => projectObj.project.mainTree.nodes[`id_${ID}`]);        // 获取新增的定额数据        const rationNodes = rationData.map(ration => projectObj.project.mainTree.nodes[`id_${ration.ID}`]);        return [...billsNodes, ...rationNodes];    }    /**     * 同步、更新缓存的数据、节点     * @param {Object} delData - 删除的数据,包含定额ID列表      * @param {Object} updateData - 更新的数据      * @param {Object} addData - 新增的完整清单和定额人材机数据,经过后端加工返回      * @return {void}      */    function syncData(delData, updateData, addData) {        // 被删除数据的清单ID@定额serialNo - 价格字段映射        // 在新增节点时,给上原本该行的feesIndex字段,不然会出现新增节点价格为空,重算后价格有值,造成价格字段闪烁的情况        // 新增节点赋上原本该行的feesIndex字段只是为了避免闪烁的情况。        // 新节点fees数组没有赋值,后续计算的时候节点的价格字段会被初始化(calcTools.initFees中),因此这个操作不会对计算结果有影响        function getSerialNoFeesIndexMapping({ ration }) {            const mapping = {};            const rationIDList = ration.map(item => item.ID);            projectObj.project.Ration.datas                .filter(item => rationIDList.includes(item.ID))                .forEach(item => mapping[`${item.billsItemID}@${item.serialNo}`] = item.feesIndex);            return mapping;        }        // 新增定额设置上暂时显示的价格字段        function setTemporaryFeesIndex({ ration }, feesIndexMapping) {            ration.forEach(item => {                const key = `${item.billsItemID}@${item.serialNo}`;                const oldFeesIndex = feesIndexMapping[key];                if (oldFeesIndex) {                    item.feesIndex = oldFeesIndex;                }            });        }        // 删除数据        function del({ ration }) {            const sheet = projectObj.mainController.sheet;            const func = () => {                // 删除定额数据、定额节点及子数据                if (ration.length) {                    const rationIDList = ration.map(item => item.ID);                    projectObj.project.Ration.deleteDataSimply(rationIDList);                    // 由于cacheTree delete方法会将preSelected设置成null                    // 会导致变更焦点行时,清除不了原焦点行的选中色,因此这里重设下preSelected,在这里处理避免影响其他已有代码                    projectObj.project.mainTree.preSelected = projectObj.project.mainTree.selected;                }            };            TREE_SHEET_HELPER.massOperationSheet(sheet, func);        }        // 更新数据        function update({ project, bills, ration }) {            // 更新项目属性            if (project) {                const property = projectObj.project.projectInfo.property;                Object.assign(property, project);                // 更新节点(控制基数只读性,指定清单基数列只读)                const specificNode = getSpecificNode(property.overHeightSpecificID);                if (specificNode) {                    TREE_SHEET_HELPER.refreshTreeNodeData(projectObj.mainController.setting, projectObj.mainController.sheet, [specificNode], false);                }            }            const mainTree = projectObj.project.mainTree;            // 更新节点超高降效            [...bills, ...ration].forEach(({ ID, overHeight }) => {                const node = mainTree.nodes[`id_${ID}`];                if (node) {                    node.data.overHeight = overHeight;                }            });        }        // 插入数据        function add({ bills, ration, rationGLJ, projectGLJ }) {            const sheet = projectObj.mainController.sheet;            const func = () => {                // 插入清单数据和清单节点主树节点                if (bills.length) {                    projectObj.project.Bills.addNewDataSimply(bills);                }                // 插入定额数据和定额节点                if (ration.length) {                    // 按照serialNo排序                    ration.sort((a, b) => a.serialNo - b.serialNo);                    projectObj.project.Ration.addNewDataSimply(ration);                }                // 插入定额人材机数据                if (rationGLJ.length) {                    projectObj.project.ration_glj.addDatasToList(rationGLJ);                }                // 插入项目人材机数据                if (projectGLJ.length) {                    projectObj.project.projectGLJ.loadNewProjectGLJToCaches(projectGLJ);                    // 重算消耗量                    projectObj.project.projectGLJ.calcQuantity();                }            };            TREE_SHEET_HELPER.massOperationSheet(sheet, func);        }        const feesIndexMapping = getSerialNoFeesIndexMapping(delData);        setTemporaryFeesIndex(addData, feesIndexMapping);        del(delData);        update(updateData);        add(addData);    }    // 恢复暂存的单元格文本    function recoverCellsText() {        const sheet = projectObj.mainController.sheet;        const func = () => {            const nodes = projectObj.project.mainTree.items;            const overHeightCol = getOverHeightCol();            nodes.forEach((node, index) => {                const newValue = sheet.getText(index, overHeightCol);                const oldValue = node.data.overHeight;                if (!commonUtil.similarEqual(newValue, oldValue)) {                    sheet.setText(index, overHeightCol, oldValue);                }            });        };        TREE_SHEET_HELPER.massOperationSheet(sheet, func);    }    // 请空所有清单和定额的超高文本    function clearOverHeightText() {        const sheet = projectObj.mainController.sheet;        const func = () => {            const nodes = projectObj.project.mainTree.items;            const overHeightCol = getOverHeightCol();            nodes.forEach((node, index) => {                if (node.data.overHeight) {                    sheet.setText(index, overHeightCol, '');                }            });        };        TREE_SHEET_HELPER.massOperationSheet(sheet, func);    }    // 取消超高降效,删除所有超高子目    function cancelCalc() {        switchVisible(false);        clearOverHeightText();        handleConfirmed(true);    }    // 取消事件,触发了取消操作,需要恢复单元格文本    function handleCancel() {        $.bootstrapLoading.start();        recoverCellsText();        $.bootstrapLoading.end();    }    /**     * 返回清单与超高子目和其定额人材机映射     * @param {Array} rations - 全部超高子目数据      * @param {Array} rationGLJs - 全部超高子目定额人材机数据      * @return {Object} - {billsItemID@超高子目编码@定额人材机编码: {quanqity, rationItemQuantity}}      */    function getRationGLJMap(rations, rationGLJs) {        const mapping = {};        rationGLJs.forEach(rGLJ => {            const ration = rations.find(ration => ration.ID === rGLJ.rationID);            const rationCode = ration ? ration.code : '';            // 由于一个清单下不会存在两个相同编号的超高子目(相同编号会被自动汇总),因此这个key能确定唯一一条定额人材机            const key = `${rGLJ.billsItemID}@${rationCode}@${rGLJ.original_code}`;            mapping[key] = { quantity: rGLJ.quantity, rationItemQuantity: rGLJ.rationItemQuantity };        });        return mapping;    }    // 比较两个清单与超高子目和其定额人材机映射表,看是否相同    function isMappingEqual(oldMapping, newMapping) {        const oldKeys = Object.keys(oldMapping);        const newKeys = Object.keys(newMapping);        if (oldKeys.length !== newKeys.length) {            return false;        }        const isEveryKeySame = oldKeys.every(key => {            const oldData = oldMapping[key];            const newData = newMapping[key];            if (!newData) {                return false;            }            const quantitySame = commonUtil.similarEqual(oldData.quantity, newData.quantity);            const rationQuantitySame = commonUtil.similarEqual(oldData.rationItemQuantity, newData.rationItemQuantity);            return quantitySame && rationQuantitySame;        });        return isEveryKeySame;    }    /**     * 重新计取项目超高子目,超高子目的值与关联定额相关     * 因此各种操作下改变了相关定额,都要重新计算超高子目     * 为了降低复杂度和保证逻辑统一性,重新计取为重新走(删除新增逻辑)     * 需要尽可能地降低操作的触发率     */    async function reCalcOverHeightFee() {        const project = projectObj.project;        // 如果项目没有超高降效数据,项目不可用超高降效,返回        if (!project.isOverHeightProject()) {            return;        }        // 如果没有超高定额,返回(因此删除了选项二三、指定的清单不会触发)        const overHeightRations = project.Ration.datas.filter(ration => ration.type === rationType.overHeight);        if (!overHeightRations.length) {            return;        }        // 获取新旧超高数据映射表,不同才需要计算        const overHeightRationIDs = overHeightRations.map(ration => ration.ID);        const overHeigtRationGLJs = project.ration_glj.datas.filter(rGLJ => overHeightRationIDs.includes(rGLJ.rationID));        const action = getAction();        const { ration, rationGLJ } = getAddData(action);        const oldMapping = getRationGLJMap(overHeightRations, overHeigtRationGLJs);        const newMapping = getRationGLJMap(ration, rationGLJ);        if (isMappingEqual(oldMapping, newMapping)) {            return;        }        // 存在不同,重算        await handleConfirmed();    }    // 事件监听    $(document).ready(() => {        // 设置窗口显示事件        $('#overHeightOpt').on('shown.bs.modal', () => {            initModal();        });        // 设置窗口隐藏事件        $('#overHeightOpt').on('hide.bs.modal', () => {            if (specificSpread) {                specificSpread.destroy();                specificSpread = null;            }            specificTree = null;            // 指定清单时的选项,指定清单被删除,用户下拉改变超高降效列,            // 弹出选中指定清单窗口,用户没有选定直接关闭窗口,此时需要把造价书暂存的值恢复            handleCancel();        });        // 设置窗口单选变更事件        $('input[name=cgx]').change(function () {            const option = +$(this).val();            switch (option) {                case Option.SEPARATION:                case Option.MEASURE:                    hideSpecificModal();                    break;                case Option.SPECIFIC:                    initSpecificModal();                    break;            }        });        // 设置窗口确认事件        $('#overHeightOptConfirmed').click(() => {            const option = +$('input[name=cgx]:checked').val();            switch (option) {                case Option.SEPARATION:                    $('#overHeightOpt').modal('hide');                    handleConfirmed(false, { option, specificID: null });                    break;                case Option.MEASURE:                    const fixedNode = getMeasureFixedNode();                    // 造价书不存在相关清单,提示是否新增清单,由提示窗口进行后续操作                    if (!fixedNode) {                        $('#overHeightMeasure').modal('show');                        return;                    }                    // 存在相关清单                    $('#overHeightOpt').modal('hide');                    handleConfirmed(false, { option, specificID: null });                    break;                case Option.SPECIFIC:                    const specificID = getSpecificFromSheet();                    if (!specificID) {                        alert('请指定清单');                        return;                    }                    $('#overHeightOpt').modal('hide');                    handleConfirmed(false, { option, specificID });                    break;            }        });        // 选项二下,改变超高降效的值,且措施项目下指定清单被删除,弹窗按钮事件        $('#overHeightMeasureY').click(() => { // 确认 - 新增指定清单和相关数据            const action = { option: Option.MEASURE, specificID: null };            $('#overHeightOpt').modal('hide');            $('#overHeightMeasure').modal('hide');            handleConfirmed(false, action);        });        $('#overHeightMeasureN').click(handleCancel); // 取消        // 选项三下,改变超高降效的值,且指定清单被删除,弹窗按钮事件        $('#overHeightSpecificY').click(() => { // 确认 - 弹出设置窗口            $('#overHeightOpt').modal('show');        });        $('#overHeightSpecificN').click(handleCancel); // 取消    });    return {        init,        getComboData,        isOverHeight,        switchVisible,        handleValueChanged,        cancelCalc,        reCalcOverHeightFee,    };})();
 |