/* * @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, }; })();