overHeight.js 46 KB

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