overHeight.js 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138
  1. /*
  2. * @Descripttion: 超高降效相关
  3. * @Author: Zhong
  4. * @Date: 2019-12-19 17:45:40
  5. */
  6. /*
  7. * 整体逻辑为:
  8. * 1.一个请求中
  9. * {删除}所有超高子目
  10. * {更新}清单、定额下拉列文本
  11. * {新增}清单、定额
  12. * 2.一个请求{刷新}项目人材机(计算需要)
  13. * 3.一个请求{重算被删除的定额清单节点、新增的定额节点
  14. */
  15. const OVER_HEIGHT = (() => {
  16. // 选项类型,生成的超高子目所在位置
  17. const Option = {
  18. // 对应清单或分部下(默认)
  19. SEPARATION: 1,
  20. // 指定措施清单011704001
  21. MEASURE: 2,
  22. // 指定具体位置,显示分部分项以及措施项目的树结构显示叶子清单(分项)供勾选
  23. SPECIFIC: 3,
  24. };
  25. // 系数类别,类别不同,生成的定额人材机和算法不同
  26. const RateType = {
  27. LABOUR: 1,
  28. MACHINE: 2,
  29. LABOUR_MACHINE: 3,
  30. };
  31. // 系数类别对应的汇总定额金额字段(feeIndex里的字段)
  32. const FeeField = {
  33. [RateType.LABOUR]: 'labour',
  34. [RateType.MACHINE]: 'machine',
  35. [RateType.LABOUR_MACHINE]: 'material',
  36. };
  37. // 系数类别的定额人材机基础数据,不同系数类别,对应的人材机编码、名称等不同
  38. const BaseRatoinGLJ = {
  39. [RateType.LABOUR]: {
  40. code: '00010052',
  41. original_code: '00010052',
  42. name: '人工降效',
  43. type: gljType.LABOUR,
  44. shorName: '人',
  45. },
  46. [RateType.MACHINE]: {
  47. code: '99918004',
  48. original_code: '99918004',
  49. name: '机械降效',
  50. type: gljType.GENERAL_MACHINE,
  51. shorName: '机',
  52. },
  53. [RateType.LABOUR_MACHINE]: {
  54. code: '00010053',
  55. original_code: '00010053',
  56. name: '人工、机械降效增加费',
  57. type: gljType.GENERAL_MATERIAL,
  58. shorName: '材',
  59. },
  60. }
  61. // 选项二时的前九位清单编号
  62. const fixedCode = '011704001';
  63. const fixedCodeReg = new RegExp(`^${fixedCode}`);
  64. // 取费专业名称
  65. // const programName = '超高降效';
  66. const programName = '公共建筑工程';
  67. // 指定清单表格
  68. let specificSpread = null;
  69. // 指定清单树
  70. let specificTree = null;
  71. // 指定清单窗口界面设置
  72. const specificTreeSetting = {
  73. emptyRowHeader: true,
  74. rowHeaderWidth: 15,
  75. treeCol: 0,
  76. emptyRows: 0,
  77. headRows: 1,
  78. headRowHeight: [40],
  79. defaultRowHeight: 21,
  80. cols: [{
  81. width: 140,
  82. readOnly: true,
  83. head: {
  84. titleNames: ["编码"],
  85. spanCols: [1],
  86. spanRows: [1],
  87. vAlign: [1],
  88. hAlign: [1],
  89. font: ["Arial"]
  90. },
  91. data: {
  92. field: "code",
  93. vAlign: 1,
  94. hAlign: 0,
  95. font: "Arial"
  96. }
  97. }, {
  98. width: 45,
  99. readOnly: true,
  100. head: {
  101. titleNames: ["类型"],
  102. spanCols: [1],
  103. spanRows: [1],
  104. vAlign: [1],
  105. hAlign: [1],
  106. font: ["Arial"]
  107. },
  108. data: {
  109. field: "subType",
  110. vAlign: 1,
  111. hAlign: 1,
  112. font: "Arial"
  113. }
  114. },
  115. {
  116. width: 205,
  117. readOnly: true,
  118. head: {
  119. titleNames: ["名称"],
  120. spanCols: [1],
  121. spanRows: [1],
  122. vAlign: [1],
  123. hAlign: [1],
  124. font: ["Arial"]
  125. },
  126. data: {
  127. field: "name",
  128. vAlign: 1,
  129. hAlign: 0,
  130. font: "Arial"
  131. }
  132. },
  133. {
  134. width: 60,
  135. readOnly: true,
  136. head: {
  137. titleNames: ["具体位置"],
  138. spanCols: [1],
  139. spanRows: [1],
  140. vAlign: [1],
  141. hAlign: [1],
  142. font: ["Arial"]
  143. },
  144. data: {
  145. field: "specific",
  146. vAlign: 1,
  147. hAlign: 1,
  148. font: "Arial"
  149. }
  150. },
  151. ]
  152. };
  153. // 源数据
  154. let sourceData;
  155. // 下拉项
  156. let comboData;
  157. // 初始化源数据和下拉项数据
  158. function init(source) {
  159. sourceData = source || [];
  160. comboData = sourceData
  161. ? sourceData
  162. .filter(item => !item.extra)
  163. .map(item => item.name)
  164. : [];
  165. }
  166. // 获取下拉项
  167. function getComboData() {
  168. return comboData;
  169. }
  170. function getOverHeightItem(value) {
  171. return sourceData.find(item => item.name === value);
  172. }
  173. // 获取系数
  174. function getRate(overHeightItem) {
  175. return overHeightItem.labourRate
  176. || overHeightItem.machineRate
  177. || overHeightItem.labourMachineRate
  178. || null;
  179. }
  180. // 下拉项是否需要计算(生成子目)
  181. function isNeedToCalc(overHeightItem) {
  182. if (!overHeightItem) {
  183. return false;
  184. }
  185. const rate = getRate(overHeightItem);
  186. return !!rate;
  187. }
  188. // 是否是超高子目
  189. function isOverHeight(node) {
  190. return node
  191. && node.sourceType === projectObj.project.Ration.getSourceType()
  192. && node.data.type === rationType.overHeight;
  193. }
  194. // 获取超高降效列号
  195. function getOverHeightCol() {
  196. return projectObj.project.projSetting.main_tree_col.cols.findIndex(item => item.data.field === 'overHeight');
  197. }
  198. // 获取指定清单指定列
  199. function getSpecificCol() {
  200. return specificTreeSetting.cols.findIndex(item => item.data.field === 'specific');
  201. }
  202. // 获取触发动作:选项1、选项2、选项3,选项3时需要指定清单ID(specificID)
  203. function getAction() {
  204. return {
  205. option: projectObj.project.projectInfo.property.overHeightOption || Option.SEPARATION,
  206. specificID: projectObj.project.projectInfo.property.overHeightSpecificID || null,
  207. };
  208. }
  209. // 超高降效列的控制,右键计取触发
  210. function switchVisible() {
  211. const curVisible = colSettingObj.getVisible('overHeight');
  212. colSettingObj.setVisible('overHeight', !curVisible);
  213. colSettingObj.updateColSetting(true);
  214. }
  215. // 获取系数类型列表
  216. function getRateTypeList(overHeightItem) {
  217. const rst = [];
  218. if (commonUtil.isNumber(overHeightItem.labourRate)) {
  219. rst.push({ type: RateType.LABOUR, rate: overHeightItem.labourRate });
  220. }
  221. if (commonUtil.isNumber(overHeightItem.machineRate)) {
  222. rst.push({ type: RateType.MACHINE, rate: overHeightItem.machineRate });
  223. }
  224. if (commonUtil.isNumber(overHeightItem.labourMachineRate)) {
  225. rst.push({ type: RateType.LABOUR_MACHINE, rate: overHeightItem.labourMachineRate });
  226. }
  227. return rst;
  228. }
  229. // 有效化变化节点的值,若值为无效值(下拉项中不存在),则将变化节点的值设成原值
  230. function validateData(changedData) {
  231. changedData.forEach(item => {
  232. if (!comboData.includes(item.value)) {
  233. item.value = item.node.data.overHeight;
  234. }
  235. });
  236. }
  237. // 简化变化节点:由于子项值继承父项,且变更节点中可能存在父子关系,因此需要去除子项节点
  238. function simplifyData(changedData) {
  239. const rst = [];
  240. const nodes = changedData.map(item => item.node);
  241. changedData.forEach(item => {
  242. let parent = item.parent;
  243. // 父项不存在变化节点中才将此数据放入返回数组中
  244. while (parent) {
  245. if (nodes.includes(parent)) {
  246. return;
  247. }
  248. parent = parent.parent;
  249. }
  250. rst.push(item);
  251. });
  252. return rst;
  253. }
  254. // 设置单元格文本,单元格文本数据为暂存数据,方便后续获取更新、新增数据,若后续操作失败,则可用节点数据恢复单元格文本内容。
  255. function setTexts(changedData) {
  256. const sheet = projectObj.mainController.sheet;
  257. const func = () => {
  258. const overHeightCol = getOverHeightCol();
  259. changedData.forEach(item => {
  260. // 子项值随父项
  261. const nodes = [item.node, ...item.node.getPosterity()];
  262. nodes.forEach(node => {
  263. const row = node.serialNo();
  264. // 单元格没被锁定才填写暂存值
  265. const locked = sheet.getCell(row, overHeightCol).locked();
  266. if (!locked) {
  267. sheet.setText(row, overHeightCol, item.value)
  268. }
  269. });
  270. });
  271. };
  272. TREE_SHEET_HELPER.massOperationSheet(sheet, func);
  273. }
  274. // 获取措施技术项目底下固定的节点(011704001...): 选项二时
  275. function getTechFixedNode() {
  276. const measureNode = projectObj.project.mainTree.items.find(node => node.getFlag() === fixedFlag.CONSTRUCTION_TECH);
  277. const measureChildren = measureNode.getPosterity();
  278. return measureChildren.find(node => node.data.code && fixedCodeReg.test(node.data.code));
  279. }
  280. // 获取指定清单节点:选项三时
  281. function getSpecificNode(specificID) {
  282. return specificID
  283. ? projectObj.project.mainTree.nodes[`id_${specificID}`]
  284. : null;
  285. }
  286. // 变更超高降效列的操作检验,若选项为2、3时,需检验指定清单是否还存在,不存在则取消操作和提示
  287. function checkAction(action) {
  288. const { option, specificID } = action;
  289. if (option === Option.SEPARATION) {
  290. return true;
  291. } else if (option === Option.MEASURE) {
  292. const isValid = !!getTechFixedNode();
  293. if (!isValid) {
  294. $('#overHeightMeasure').modal('show');
  295. }
  296. return isValid;
  297. } else if (option) {
  298. const isValid = !!getSpecificNode(specificID);
  299. if (!isValid) {
  300. $('#overHeightSpecific').modal('show');
  301. }
  302. return isValid;
  303. }
  304. }
  305. // 设置指定清单界面选择框
  306. function setCheckBox(sheet, specificNodes) {
  307. const checkBox = new GC.Spread.Sheets.CellTypes.CheckBox();
  308. const specificCol = getSpecificCol();
  309. // 只有叶子节点和非计算基数节点才是选择框
  310. const func = () => {
  311. specificNodes.forEach((node, index) => {
  312. if (!node.children.length && !node.data.calcBase) {
  313. sheet.getCell(index, specificCol).cellType(checkBox);
  314. }
  315. });
  316. };
  317. TREE_SHEET_HELPER.massOperationSheet(sheet, func);
  318. }
  319. // 初始化指定清单选择树
  320. function initSpecificTree(data, sheet, setting) {
  321. specificTree = idTree.createNew({ id: 'ID', pid: 'ParentID', nid: 'NextSiblingID', rootId: -1, autoUpdate: true });
  322. const controller = TREE_SHEET_CONTROLLER.createNew(specificTree, sheet, setting, false);
  323. specificTree.loadDatas(data);
  324. controller.showTreeData();
  325. sheet.setRowCount(data.length);
  326. setCheckBox(sheet, specificTree.items);
  327. }
  328. // 表格点击事件,只有checkbox单元格会触发这个方法
  329. function handleSpreadButtonClick(e, args) {
  330. const { sheet, row, col } = args;
  331. // 只能单选,清空其他单元格的值并设置当前值
  332. const func = () => {
  333. const rowCount = sheet.getRowCount();
  334. const oldValue = sheet.getValue(row, col);
  335. for (let row = 0; row < rowCount; row++) {
  336. sheet.setValue(row, col, '');
  337. }
  338. sheet.setValue(row, col, !oldValue);
  339. }
  340. TREE_SHEET_HELPER.massOperationSheet(sheet, func);
  341. }
  342. // 初始化表格
  343. function initWorkbook() {
  344. specificSpread = sheetCommonObj.createSpread($('#specificArea')[0], 1);
  345. specificSpread.options.allowUserDragDrop = false; // 不允许拖填充,影响点击
  346. sheetCommonObj.spreadDefaultStyle(specificSpread);
  347. specificSpread.bind(GC.Spread.Sheets.Events.ButtonClicked, handleSpreadButtonClick);
  348. // 设置表头
  349. const sheet = specificSpread.getSheet(0);
  350. const headers = sheetCommonObj.getHeadersFromTreeSetting(specificTreeSetting);
  351. sheetCommonObj.setHeader(sheet, headers);
  352. return specificSpread;
  353. }
  354. // 从造价书界面筛选获取指定清单界面数据
  355. function getSpecificData() {
  356. // 只显示分部分项、措施项目的清单节点
  357. const billsNodes = projectObj.project.Bills.tree.items;
  358. const flags = [fixedFlag.SUB_ENGINERRING, fixedFlag.MEASURE];
  359. const billsData = flags.reduce((allData, flag) => {
  360. const fixedNode = billsNodes.find(node => node.getFlag() === flag);
  361. const posterity = fixedNode.getPosterity();
  362. const data = [fixedNode, ...posterity].map(node =>
  363. ({
  364. ID: node.data.ID,
  365. ParentID: node.data.ParentID,
  366. NextSiblingID: node.data.NextSiblingID,
  367. code: node.data.code,
  368. subType: billText[node.data.type],
  369. name: node.data.name,
  370. calcBase: node.data.calcBase,
  371. }));
  372. allData.push(...data);
  373. return allData;
  374. }, []);
  375. return billsData;
  376. }
  377. // 设置指定ID选中
  378. function setSpecific(ID) {
  379. if (!ID) {
  380. return;
  381. }
  382. const sheet = specificSpread.getSheet(0);
  383. const nodes = specificTree.items;
  384. const index = nodes.findIndex(node => node.data.ID === ID);
  385. if (!~index) {
  386. return;
  387. }
  388. const col = getSpecificCol();
  389. sheet.setValue(index, col, true);
  390. }
  391. // 从表格中获取勾选的指定ID
  392. function getSpecificFromSheet() {
  393. const sheet = specificSpread.getSheet(0);
  394. const nodes = specificTree.items;
  395. const rowCount = sheet.getRowCount();
  396. const col = getSpecificCol();
  397. for (let row = 0; row < rowCount; row++) {
  398. const value = sheet.getValue(row, col);
  399. if (value) {
  400. return nodes[row].data.ID;
  401. }
  402. }
  403. return null;
  404. }
  405. // 初始化指定清单选择界面
  406. function initSpecificModal() {
  407. $('#specificArea').show();
  408. const spread = initWorkbook();
  409. const sheet = spread.getSheet(0);
  410. const data = getSpecificData();
  411. initSpecificTree(data, sheet, specificTreeSetting);
  412. const { specificID } = getAction();
  413. setSpecific(specificID);
  414. }
  415. // 隐藏指定清单选择界面
  416. function hideSpecificModal() {
  417. $('#specificArea').hide();
  418. if (specificSpread) {
  419. specificSpread.destroy();
  420. specificSpread = null;
  421. }
  422. }
  423. // 初始化子目生成方式选项设置窗口
  424. function initModal() {
  425. const { option, specificID } = getAction();
  426. $(`input[name=cgx][value=${option}]`).prop('checked', 'checked');
  427. $('#specificArea').hide();
  428. if (option === Option.SPECIFIC) {
  429. initSpecificModal(specificID);
  430. }
  431. }
  432. // 超高降效下拉项或选项是否改变了
  433. function isValueChanged() {
  434. const updateData = getUpdateData();
  435. return !!(updateData.bills.length || updateData.ration.length);
  436. }
  437. // 对比两个选项行为,获取更新选项数据
  438. function getUpdateProjectData(oldAction, newAction) {
  439. if (!oldAction || !newAction) {
  440. return {};
  441. }
  442. const optionDiff = oldAction.option !== newAction.option;
  443. const specificDiff = oldAction.specificID !== newAction.specificID;
  444. const updateData = {
  445. ID: projectObj.project.ID(),
  446. overHeightOption: newAction.option,
  447. overHeightSpecificID: newAction.specificID,
  448. };
  449. return optionDiff || specificDiff
  450. ? updateData
  451. : {};
  452. }
  453. /**
  454. * 获取更新数据:对比项目节点中超高降效的新旧值,新值为暂存的单元格文本,旧值为节点data数据
  455. * @param {Object} newAction - 选项行为
  456. * @return {Object} - {
  457. * project: {ID: Number,overHeightOp: Number, overHeightSpecificID: Number||Null},
  458. * bills: [{ID: Number, overHeight: String}],
  459. * ration: [{ID: Number, overHeight: String}]
  460. * }
  461. */
  462. function getUpdateData(newAction) {
  463. const update = {
  464. project: {}, // 可能会更改项目属性的超高降效设置
  465. bills: [],
  466. ration: [],
  467. };
  468. const oldAction = getAction();
  469. const updateProjectData = getUpdateProjectData(oldAction, newAction);
  470. Object.assign(update.project, updateProjectData);
  471. const nodes = projectObj.project.mainTree.items;
  472. const sheet = projectObj.mainController.sheet;
  473. const overHeightCol = getOverHeightCol();
  474. nodes.forEach((node, index) => {
  475. const newValue = sheet.getText(index, overHeightCol);
  476. const oldValue = node.data.overHeight;
  477. // 非严等
  478. if (!commonUtil.similarEqual(newValue, oldValue)) {
  479. const type = node.sourceType === projectObj.project.Bills.getSourceType()
  480. ? 'bills'
  481. : 'ration';
  482. update[type].push({
  483. ID: node.data.ID,
  484. overHeight: newValue
  485. });
  486. }
  487. });
  488. return update;
  489. }
  490. /**
  491. * 获取删除数据:项目中所有超高子目
  492. * @return {Object} - {ration: [{ID: Number}]}
  493. */
  494. function getDeleteData() {
  495. const del = {
  496. ration: [],
  497. };
  498. const rations = projectObj.project.Ration.datas;
  499. del.ration = rations
  500. .filter(ration => ration.type === rationType.overHeight)
  501. .map(ration => ({ ID: ration.ID }));
  502. return del;
  503. }
  504. /**
  505. * 获取需要生成超高子目的定额节点
  506. * @return {Array} - [{node: Object, overHeight: String}]
  507. */
  508. function getNeedCalcRationItems() {
  509. // 从整个项目中筛选当前下拉项单元格的文本是需要计算的定额节点
  510. const nodes = projectObj.project.mainTree.items;
  511. const sheet = projectObj.mainController.sheet;
  512. const overHeightCol = getOverHeightCol();
  513. const rst = [];
  514. nodes.forEach(node => {
  515. // 非超高子目的定额节点才生成
  516. const notOverHeightRationNode = node.sourceType !== projectObj.project.Ration.getSourceType()
  517. || node.data.type === rationType.overHeight;
  518. if (notOverHeightRationNode) {
  519. return;
  520. }
  521. const overHeight = sheet.getText(node.serialNo(), overHeightCol);
  522. const overHeightItem = getOverHeightItem(overHeight);
  523. if (isNeedToCalc(overHeightItem)) {
  524. rst.push({ node, overHeight });
  525. }
  526. });
  527. return rst;
  528. }
  529. // 根据选项获取超高子目挂载的清单
  530. function getMountedBills(action) {
  531. const { option, specificID } = action;
  532. // 生成清单数据
  533. function initMountedBills() {
  534. // 生成的清单位置为施工技术措施项目的最末子项
  535. const measureNode = projectObj.project.mainTree.items.find(node => node.getFlag() === fixedFlag.CONSTRUCTION_TECH);
  536. const parent = measureNode.children[measureNode.children.length - 1];
  537. // 具体完整数据需要在后端跟标准数据对比完善
  538. return {
  539. projectID: projectObj.project.ID(),
  540. billsLibId: +projectObj.project.projectInfo.engineeringInfo.bill_lib[0].id,
  541. ID: uuid.v1(),
  542. ParentID: parent.data.ID,
  543. NextSiblingID: -1,
  544. type: billType.BILL,
  545. code: fixedCode,
  546. name: '超高施工增加',
  547. unit: 'm2',
  548. quantity: '1',
  549. };
  550. }
  551. // 选项一
  552. if (option === Option.SEPARATION) {
  553. return {
  554. isNew: false,
  555. bills: null,
  556. };
  557. } else if (option === Option.MEASURE) { // 选项二且造价书没有相关清单,需要插入清单
  558. const fixedNode = getTechFixedNode();
  559. return {
  560. isNew: !fixedNode,
  561. bills: fixedNode ? fixedNode.data : initMountedBills(),
  562. };
  563. } else {
  564. const specificNode = getSpecificNode(specificID);
  565. return {
  566. isNew: false,
  567. bills: specificNode.data,
  568. }
  569. }
  570. }
  571. // 获取清单节点下最末非超高子目定额
  572. function getLastNormalRationNode(billsNode) {
  573. const nodes = billsNode && billsNode.children
  574. ? billsNode.children.filter(node => node.data.type !== rationType.overHeight)
  575. : [];
  576. return nodes[nodes.length - 1]
  577. }
  578. // 将需要生成超高子目的定额数据按照挂载清单和超高名称进行分组
  579. // @return {Object} {'billsID@overHeight': [node: rationNode]}
  580. function getGroupData(rationItems, mountedBillsID) {
  581. // mapping: billsID-overHeightName
  582. const group = {};
  583. rationItems.forEach(item => {
  584. // 无指定的清单ID,则挂载在各自的清单上
  585. const billsID = mountedBillsID || item.node.data.billsItemID
  586. const key = `${billsID}@${item.overHeight}`;
  587. if (!group[key]) {
  588. group[key] = [];
  589. }
  590. group[key].push(item.node);
  591. });
  592. return group;
  593. }
  594. // 根据系数类型获取汇总的定额金额,这个金额作为计算定额人材机的基数
  595. function getFeeByRateType(rateType, referenceRations) {
  596. const feeField = FeeField[rateType];
  597. // 汇总定额节点相关综合合价
  598. return referenceRations.reduce((sum, rationNode) => {
  599. const feeObj = rationNode.data.feesIndex;
  600. const totalFee = feeObj && feeObj[feeField]
  601. ? feeObj[feeField].totalFee
  602. : 0;
  603. return scMathUtil.roundForObj(sum + totalFee, decimalObj.process);
  604. }, 0);
  605. }
  606. // 通过超高降效计算得来的定额人材机消耗量:消耗量=定额消耗量=算出来的值
  607. function getQuantity(rate, fee) {
  608. return String(scMathUtil.roundForObj(rate * fee, decimalObj.glj.quantity));
  609. }
  610. /**
  611. * 获取定额喝定额人材机数据
  612. * @param {Array} rationItems - 需要生成超高子目的定额数据{node: Object, overHtight: String}
  613. * @param {Object} mountedBills - 挂载到的清单数据
  614. * @return {Object} - {ration: Array, rationGLJ: Array}
  615. */
  616. function getAddRationAndRationGLJData(rationItems, mountedBills) {
  617. // 生成定额数据
  618. function initRation(bills, overHeightItem, serialNo) {
  619. const programID = projectObj.project.calcProgram.compiledTemplateMaps[programName];
  620. // 生成的超高子目消耗量为1
  621. const quantity = '1';
  622. // 含量为 定额消耗量 / 清单消耗量
  623. const tempV = quantity / bills.quantity;
  624. const contain = isFinite(tempV)
  625. ? scMathUtil.roundForObj(tempV, decimalObj.ration.quantity)
  626. : '0';
  627. return {
  628. projectID: projectObj.project.ID(),
  629. billsItemID: bills.ID,
  630. ID: uuid.v1(),
  631. programID,
  632. serialNo,
  633. code: overHeightItem.code,
  634. name: overHeightItem.name,
  635. unit: overHeightItem.unit,
  636. type: rationType.overHeight,
  637. quantity,
  638. contain,
  639. }
  640. }
  641. // 根据分组的keys获取定额serialNo值映射表
  642. function getSerialNoMappig(groupKeys) {
  643. // 清单ID - serialNo映射
  644. const billsIDMap = {};
  645. // 完整的key - serialNo映射
  646. const keyMap = {};
  647. const nodes = projectObj.project.mainTree.nodes;
  648. groupKeys.forEach(key => {
  649. const [billsID] = key.split('@');
  650. if (billsIDMap[billsID]) {
  651. billsIDMap[billsID] = keyMap[key] += 1;
  652. return;
  653. }
  654. const billsNode = nodes[`id_${billsID}`];
  655. const lastNormalRationNode = billsNode ? getLastNormalRationNode(billsNode) : null;
  656. const serialNo = lastNormalRationNode ? lastNormalRationNode.data.serialNo + 1 : 1;
  657. billsIDMap[billsID] = keyMap[key] = serialNo;
  658. });
  659. return keyMap;
  660. }
  661. // 生成超高子目的定额人材机数据,定额人材机的属性只是一部分,还有部分数据需要在后端处理
  662. function initRationGLJ(ration, typeItem, referenceRationNodes) {
  663. const { type, rate } = typeItem;
  664. const sumFee = getFeeByRateType(type, referenceRationNodes);
  665. // 不同类型的基础人材机属性
  666. const quantity = getQuantity(rate, sumFee);
  667. const baseObj = BaseRatoinGLJ[type];
  668. // 补全定额人材机属性,共性属性
  669. const extendObj = {
  670. projectID: projectObj.project.ID(),
  671. ID: uuid.v1(),
  672. billsItemID: ration.billsItemID,
  673. rationID: ration.ID,
  674. repositoryId: -1,
  675. GLJID: -1,
  676. unit: '%',
  677. specs: '',
  678. // 定额人材机没有价格字段,但是生成单价文件需要需要这两个价格字段,默认为“1”
  679. basePrice: '1',
  680. marketPrice: '1',
  681. quantity,
  682. rationItemQuantity: quantity,
  683. };
  684. return { ...baseObj, ...extendObj };
  685. }
  686. const add = {
  687. ration: [],
  688. rationGLJ: []
  689. };
  690. if (!rationItems.length) {
  691. return add;
  692. }
  693. const mountedBillsID = mountedBills
  694. ? mountedBills.ID
  695. : null;
  696. // 分析分组数据,获取定额及定额人材机数据
  697. const group = getGroupData(rationItems, mountedBillsID);
  698. const rationSerialNoMapping = getSerialNoMappig(Object.keys(group));
  699. // 获取定额及定额人材机数据
  700. Object.entries(group).forEach(([key, referenceRationNodes]) => {
  701. const [billsID, overHeight] = key.split('@');
  702. const overHeightItem = getOverHeightItem(overHeight);
  703. const bills = billsID === mountedBillsID
  704. ? mountedBills
  705. : projectObj.project.Bills.datas.find(item => item.ID === billsID);
  706. const serialNo = rationSerialNoMapping[key];
  707. const overHeightRation = initRation(bills, overHeightItem, serialNo);
  708. // 给生成的超高子目定额,设置关联定额列表(关联定额工程量等发生变化,需要重算超高子目)
  709. overHeightRation.referenceRationList = referenceRationNodes.map(node => node.data.ID);
  710. add.ration.push(overHeightRation);
  711. // 根据超高项获取系数列表,系数列表一个元素会根据系数类别生成一条定额人材机(人、机、材料)
  712. const rateTypeList = getRateTypeList(overHeightItem);
  713. const rationGLJs = rateTypeList.map(rateTypeItem => initRationGLJ(overHeightRation, rateTypeItem, referenceRationNodes));
  714. add.rationGLJ.push(...rationGLJs);
  715. });
  716. return add;
  717. }
  718. /**
  719. * 获取插入数据:超高子目(定额)、清单(选项2、3时可能会插入)
  720. * @param {Object} action - {option: Number, specificID: Undefined||Null||Number}
  721. * @return {Array} - {bills: Array, ration: Array, rationGLJ: Array}
  722. */
  723. function getAddData(action) {
  724. const add = {
  725. bills: [],
  726. ration: [],
  727. rationGLJ: [],
  728. };
  729. // 挂载到的清单,新增或已有的
  730. const mountedBills = getMountedBills(action);
  731. if (mountedBills.isNew) {
  732. add.bills.push(mountedBills.bills);
  733. }
  734. // 获取需要生成超高子目的定额数据
  735. const needCalcRationItems = getNeedCalcRationItems();
  736. const subData = getAddRationAndRationGLJData(needCalcRationItems, mountedBills.bills);
  737. add.ration = subData.ration;
  738. add.rationGLJ = subData.rationGLJ;
  739. return add;
  740. }
  741. /**
  742. * 生成传输数据
  743. * @param {Boolean} isCancelCalc - 是否取消计取
  744. * @param {Object} action - 新的选项行为
  745. * @return {Object}
  746. */
  747. function generatePostData(isCancelCalc, action) {
  748. // 默认模板
  749. const postData = {
  750. addData: {
  751. bills: [],
  752. ration: [],
  753. rationGLJ: [],
  754. },
  755. updateData: {
  756. project: {},
  757. bills: [],
  758. ration: [],
  759. },
  760. deleteData: {
  761. ration: [],
  762. },
  763. };
  764. // 取消计取费用,只删除超高子目
  765. if (isCancelCalc) {
  766. postData.deleteData = getDeleteData();
  767. return postData;
  768. }
  769. // 没有新的选项行为,获取当前项目的选项行为
  770. if (!action) {
  771. action = getAction();
  772. }
  773. const addData = getAddData(action);
  774. const updateData = getUpdateData(action);
  775. const deleteData = getDeleteData();
  776. return Object.assign(postData, { addData, updateData, deleteData });
  777. }
  778. /**
  779. * 更改了超高降效列(edited、rangeChanged),触发事件
  780. * @param {Array} changedData - 变化的数据 [{node: Object, value: String}]
  781. * @return {void}
  782. */
  783. function handleValueChanged(changedData) {
  784. validateData(changedData);
  785. changedData = simplifyData(changedData);
  786. setTexts(changedData);
  787. const valuedChanged = isValueChanged();
  788. if (!valuedChanged) {
  789. return;
  790. }
  791. // 选项2、选项3情况下下拉可能会遇到,相关清单已经被删除,需要检测行为
  792. const action = getAction();
  793. const actionValid = checkAction(action);
  794. // actionValid为false的时候,可能后续需要恢复单元格文本值,根据后续用户在弹窗中的选择(后文ready事件中绑定)
  795. if (!actionValid) {
  796. return;
  797. }
  798. handleConfirmed();
  799. }
  800. // 确认事件
  801. async function handleConfirmed(isCancelCalc = false, action = null) {
  802. $.bootstrapLoading.start();
  803. try {
  804. const postData = generatePostData(isCancelCalc, action);
  805. const { addData, updateData, deleteData } = postData;
  806. // 更新、删除、新增数据
  807. // 返回的是新增的清单、定额人材机 rst = {bills: [], rationGLJ: []}
  808. const rst = await ajaxPost('/project/calcOverHeightFee', postData);
  809. // 后续获取重算节点相关:
  810. // 新增的定额节点要在同步数据后才有,删除的定额节点在同步前找不到,因此同步前先获取被删除定额节点的清单ID列表
  811. const rationIDList = deleteData.ration.map(item => item.ID);
  812. let billsIDList = projectObj.project.Ration.datas
  813. .filter(item => rationIDList.includes(item.ID))
  814. .map(item => item.billsItemID);
  815. billsIDList = [...new Set(billsIDList)];
  816. // 同步数据
  817. const newAddData = { ...addData, ...rst };
  818. syncData(deleteData, updateData, newAddData);
  819. // 获取重算节点
  820. const reCalcNodes = getReCalcNodes(billsIDList, newAddData.ration);
  821. if (!reCalcNodes.length) {
  822. $.bootstrapLoading.end();
  823. return;
  824. }
  825. // 获取项目人材机数据,更新缓存
  826. await projectObj.project.projectGLJ.loadDataSync();
  827. $.bootstrapLoading.end(); // 重算节点相关方法里有loading,防止提前结束了loading
  828. // 重算相关节点
  829. projectObj.project.calcProgram.calcNodesAndSave(reCalcNodes);
  830. } catch (err) {
  831. console.log(err);
  832. recoverCellsText();
  833. $.bootstrapLoading.end();
  834. }
  835. }
  836. /**
  837. * 获取需要重算的节点
  838. * @param {Array} billsIDList - 根据删除的定额ID数据获取的清单ID列表
  839. * @param {Array} rationData - 新增的定额数据
  840. * @return {Array}
  841. */
  842. function getReCalcNodes(billsIDList, rationData) {
  843. // 获取被删除定额的清单节点
  844. const billsNodes = billsIDList.map(ID => projectObj.project.mainTree.nodes[`id_${ID}`]);
  845. // 获取新增的定额数据
  846. const rationNodes = rationData.map(ration => projectObj.project.mainTree.nodes[`id_${ration.ID}`]);
  847. return [...billsNodes, ...rationNodes];
  848. }
  849. /**
  850. * 同步、更新缓存的数据、节点
  851. * @param {Object} delData - 删除的数据,包含定额ID列表
  852. * @param {Object} updateData - 更新的数据
  853. * @param {Object} addData - 新增的完整清单和定额人材机数据,经过后端加工返回
  854. * @return {void}
  855. */
  856. function syncData(delData, updateData, addData) {
  857. // 删除数据
  858. function del({ ration }) {
  859. const sheet = projectObj.mainController.sheet;
  860. const func = () => {
  861. // 删除定额数据、定额节点及子数据
  862. if (ration.length) {
  863. const rationIDList = ration.map(item => item.ID);
  864. projectObj.project.Ration.deleteDataSimply(rationIDList);
  865. // 由于cacheTree delete方法会将preSelected设置成null
  866. // 会导致变更焦点行时,清除不了原焦点行的选中色,因此这里重设下preSelected,在这里处理避免影响其他已有代码
  867. projectObj.project.mainTree.preSelected = projectObj.project.mainTree.selected;
  868. }
  869. };
  870. TREE_SHEET_HELPER.massOperationSheet(sheet, func);
  871. }
  872. // 更新数据
  873. function update({ project, bills, ration }) {
  874. // 更新项目属性
  875. if (project) {
  876. const property = projectObj.project.projectInfo.property;
  877. Object.assign(property, project);
  878. }
  879. const mainTree = projectObj.project.mainTree;
  880. // 更新节点超高降效
  881. [...bills, ...ration].forEach(({ ID, overHeight }) => {
  882. const node = mainTree.nodes[`id_${ID}`];
  883. if (node) {
  884. node.data.overHeight = overHeight;
  885. }
  886. });
  887. }
  888. // 插入数据
  889. function add({ bills, ration, rationGLJ }) {
  890. const sheet = projectObj.mainController.sheet;
  891. const func = () => {
  892. // 插入清单数据和清单节点主树节点
  893. if (bills.length) {
  894. projectObj.project.Bills.addNewDataSimply(bills);
  895. }
  896. // 插入定额数据和定额节点
  897. if (ration.length) {
  898. projectObj.project.Ration.addNewDataSimply(ration);
  899. }
  900. // 插入定额人材机数据
  901. if (rationGLJ.length) {
  902. projectObj.project.ration_glj.addDatasToList(rationGLJ);
  903. }
  904. };
  905. TREE_SHEET_HELPER.massOperationSheet(sheet, func);
  906. }
  907. del(delData);
  908. update(updateData);
  909. add(addData);
  910. }
  911. // 恢复暂存的单元格文本
  912. function recoverCellsText() {
  913. const sheet = projectObj.mainController.sheet;
  914. const func = () => {
  915. const nodes = projectObj.project.mainTree.items;
  916. const overHeightCol = getOverHeightCol();
  917. nodes.forEach((node, index) => {
  918. const newValue = sheet.getText(index, overHeightCol);
  919. const oldValue = node.data.overHeight;
  920. if (!commonUtil.similarEqual(newValue, oldValue)) {
  921. sheet.setText(index, overHeightCol, oldValue);
  922. }
  923. });
  924. };
  925. TREE_SHEET_HELPER.massOperationSheet(sheet, func);
  926. }
  927. // 取消超高降效,删除所有超高子目
  928. function cancelCalc() {
  929. handleConfirmed(true);
  930. }
  931. // 取消事件,触发了取消操作,需要恢复单元格文本
  932. function handleCancel() {
  933. $.bootstrapLoading.start();
  934. recoverCellsText();
  935. $.bootstrapLoading.end();
  936. }
  937. /**
  938. * 返回清单与超高子目和其定额人材机映射
  939. * @param {Array} rations - 全部超高子目数据
  940. * @param {Array} rationGLJs - 全部超高子目定额人材机数据
  941. * @return {Object} - {billsItemID@超高子目编码@定额人材机编码: {quanqity, rationItemQuantity}}
  942. */
  943. function getRationGLJMap(rations, rationGLJs) {
  944. const mapping = {};
  945. rationGLJs.forEach(rGLJ => {
  946. const ration = rations.find(ration => ration.ID === rGLJ.rationID);
  947. const rationCode = ration ? ration.code : '';
  948. // 由于一个清单下不会存在两个相同编号的超高子目(相同编号会被自动汇总),因此这个key能确定唯一一条定额人材机
  949. const key = `${rGLJ.billsItemID}@${rationCode}@${rGLJ.original_code}`;
  950. mapping[key] = { quantity: rGLJ.quantity, rationItemQuantity: rGLJ.rationItemQuantity };
  951. });
  952. return mapping;
  953. }
  954. // 比较两个清单与超高子目和其定额人材机映射表,看是否相同
  955. function isMappingEqual(oldMapping, newMapping) {
  956. const oldKeys = Object.keys(oldMapping);
  957. const newKeys = Object.keys(newMapping);
  958. if (oldKeys.length !== newKeys.length) {
  959. return false;
  960. }
  961. const isEveryKeySame = oldKeys.every(key => {
  962. const oldData = oldMapping[key];
  963. const newData = newMapping[key];
  964. if (!newData) {
  965. return false;
  966. }
  967. const quantitySame = commonUtil.similarEqual(oldData.quantity, newData.quantity);
  968. const rationQuantitySame = commonUtil.similarEqual(oldData.rationItemQuantity, newData.rationItemQuantity);
  969. return quantitySame && rationQuantitySame;
  970. });
  971. return isEveryKeySame;
  972. }
  973. /**
  974. * 重新计取项目超高子目,超高子目的值与关联定额相关
  975. * 因此各种操作下改变了相关定额,都要重新计算超高子目
  976. * 为了降低复杂度和保证逻辑统一性,重新计取为重新走(删除新增逻辑)
  977. * 需要尽可能地降低操作的触发率
  978. * @param {type} -
  979. * @return: {type} -
  980. */
  981. function reCalcOverHeightFee() {
  982. const project = projectObj.project;
  983. // 如果项目没有超高降效数据,项目不可用超高降效,返回
  984. if (!project.isOverHeightProject()) {
  985. return;
  986. }
  987. // 如果没有超高定额,返回(因此删除了选项二三、指定的清单不会触发)
  988. const overHeightRations = project.Ration.datas.filter(ration => ration.type === rationType.overHeight);
  989. if (!overHeightRations.length) {
  990. return;
  991. }
  992. // 获取新旧超高数据映射表,不同才需要计算
  993. const overHeightRationIDs = overHeightRations.map(ration => ration.ID);
  994. const overHeigtRationGLJs = project.ration_glj.datas.filter(rGLJ => overHeightRationIDs.includes(rGLJ.rationID));
  995. const action = getAction();
  996. const { ration, rationGLJ } = getAddData(action);
  997. const oldMapping = getRationGLJMap(overHeightRations, overHeigtRationGLJs);
  998. const newMapping = getRationGLJMap(ration, rationGLJ);
  999. if (isMappingEqual(oldMapping, newMapping)) {
  1000. return;
  1001. }
  1002. // 存在不同,重算
  1003. handleConfirmed();
  1004. }
  1005. // 事件监听
  1006. $(document).ready(() => {
  1007. // 设置窗口显示事件
  1008. $('#overHeightOpt').on('shown.bs.modal', () => {
  1009. initModal();
  1010. });
  1011. // 设置窗口隐藏事件
  1012. $('#overHeightOpt').on('hide.bs.modal', () => {
  1013. if (specificSpread) {
  1014. specificSpread.destroy();
  1015. specificSpread = null;
  1016. }
  1017. specificTree = null;
  1018. // 指定清单时的选项,指定清单被删除,用户下拉改变超高降效列,
  1019. // 弹出选中指定清单窗口,用户没有选定直接关闭窗口,此时需要把造价书暂存的值恢复
  1020. handleCancel();
  1021. });
  1022. // 设置窗口单选变更事件
  1023. $('input[name=cgx]').change(function () {
  1024. const option = +$(this).val();
  1025. switch (option) {
  1026. case Option.SEPARATION:
  1027. case Option.MEASURE:
  1028. hideSpecificModal();
  1029. break;
  1030. case Option.SPECIFIC:
  1031. initSpecificModal();
  1032. break;
  1033. }
  1034. });
  1035. // 设置窗口确认事件
  1036. $('#overHeightOptConfirmed').click(() => {
  1037. const option = +$('input[name=cgx]:checked').val();
  1038. switch (option) {
  1039. case Option.SEPARATION:
  1040. $('#overHeightOpt').modal('hide');
  1041. handleConfirmed(false, { option, specificID: null });
  1042. break;
  1043. case Option.MEASURE:
  1044. const fixedNode = getTechFixedNode();
  1045. // 造价书不存在相关清单,提示是否新增清单,由提示窗口进行后续操作
  1046. if (!fixedNode) {
  1047. $('#overHeightMeasure').modal('show');
  1048. return;
  1049. }
  1050. // 存在相关清单
  1051. $('#overHeightOpt').modal('hide');
  1052. handleConfirmed(false, { option, specificID: null });
  1053. break;
  1054. case Option.SPECIFIC:
  1055. const specificID = getSpecificFromSheet();
  1056. if (!specificID) {
  1057. alert('请指定清单');
  1058. return;
  1059. }
  1060. $('#overHeightOpt').modal('hide');
  1061. handleConfirmed(false, { option, specificID });
  1062. break;
  1063. }
  1064. });
  1065. // 选项二下,改变超高降效的值,且措施项目下指定清单被删除,弹窗按钮事件
  1066. $('#overHeightMeasureY').click(() => { // 确认 - 新增指定清单和相关数据
  1067. const action = { option: Option.MEASURE, specificID: null };
  1068. $('#overHeightOpt').modal('hide');
  1069. $('#overHeightMeasure').modal('hide');
  1070. handleConfirmed(false, action);
  1071. });
  1072. $('#overHeightMeasureN').click(handleCancel); // 取消
  1073. // 选项三下,改变超高降效的值,且指定清单被删除,弹窗按钮事件
  1074. $('#overHeightSpecificY').click(() => { // 确认 - 弹出设置窗口
  1075. $('#overHeightOpt').modal('show');
  1076. });
  1077. $('#overHeightSpecificN').click(handleCancel); // 取消
  1078. });
  1079. return {
  1080. init,
  1081. getComboData,
  1082. isOverHeight,
  1083. switchVisible,
  1084. handleValueChanged,
  1085. cancelCalc,
  1086. reCalcOverHeightFee,
  1087. };
  1088. })();