base.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926
  1. /**
  2. * @author Zhong
  3. * @date 2019/6/20
  4. * @version
  5. */
  6. const INTERFACE_EXPORT_BASE = (() => {
  7. 'use strict';
  8. const {
  9. hasValue,
  10. isHan
  11. } = window.commonUtil;
  12. // 属性类型
  13. const TYPE = {
  14. DATE: 1, // 日期类型YYYY-MM-DD
  15. DATE_TIME: 2, // 日期类型YYY-MM-DDTHH:mm:ss
  16. INT: 3, // 整数类型
  17. DECIMAL: 4, // 数值类型,不限制小数位数
  18. NUM2: 5, // 数值类型2:最多两位小数
  19. BOOL: 6 // 布尔型
  20. };
  21. // 需要特殊处理的属性类型默认空值(当一个值为undefined、null的时候,默认给赋什么值)
  22. const DEFAULT_VALUE = {
  23. [TYPE.INT]: '0',
  24. [TYPE.DECIMAL]: '0',
  25. [TYPE.NUM2]: '0',
  26. [TYPE.BOOL]: 'false'
  27. };
  28. // 空白字符处理
  29. const WHITE_SPACE = {
  30. COLLAPSE: 1 // 移除所有空白字符(换行、回车、空格以及制表符会被替换为空格,开头和结尾的空格会被移除,而多个连续的空格会被缩减为一个单一的空格)
  31. };
  32. // 承包人材料调整类型
  33. const ADJUST_TYPE = {
  34. info: 'priceInfo', // 造价信息差额调整法
  35. coe: 'priceCoe' // 价格指数调整法
  36. };
  37. // 加载数据间隔,减少服务器压力
  38. const TIMEOUT_TIME = 400;
  39. const {
  40. GRANULARITY,
  41. EXPORT_KIND
  42. } = window.commonConstants;
  43. /* const EXPORT_KIND_NAME = {
  44. 1: '招标',
  45. 2: '投标',
  46. 3: '控制价'
  47. }; */
  48. // 配置项
  49. const CONFIG = Object.freeze({
  50. TYPE,
  51. WHITE_SPACE,
  52. ADJUST_TYPE,
  53. TIMEOUT_TIME,
  54. });
  55. // 缓存项 不需要的时候需要清空
  56. const _cache = {
  57. // 错误提示列表
  58. failList: [],
  59. // 项目数据(不包含详细数据,项目管理数据)
  60. projectData: {},
  61. // 当前导出类型,默认投标
  62. exportKind: EXPORT_KIND.BID_SUBMISSION,
  63. // 记录拉取的单位工程项目详细数据,导出的时候,可能会导出多个文件,只有导出第一个文件的时候需要请求数据
  64. tenderDetailMap: {}
  65. };
  66. // 返回缓存项
  67. function getItem(key) {
  68. return _cache[key] || null;
  69. }
  70. // 设置缓存项
  71. function setItem(key, value) {
  72. // 与原数据是同类型的数据才可设置成功
  73. if (_cache[key] &&
  74. Object.prototype.toString.call(_cache[key]) ===
  75. Object.prototype.toString.call(value)) {
  76. _cache[key] = value;
  77. }
  78. }
  79. // 清空缓存项
  80. function clear() {
  81. _cache.failList = [];
  82. _cache.projectData = {};
  83. _cache.exportKind = EXPORT_KIND.BID_SUBMISSION;
  84. _cache.tenderDetailMap = {};
  85. }
  86. const CACHE = Object.freeze({
  87. getItem,
  88. setItem,
  89. clear
  90. });
  91. /*
  92. * 定义不设置一个Node方法统一进入的原因:模板化比较直观,不分开定义节点的话,调用传参也很麻烦而且不直观。
  93. * 一个节点对应一个构造方法,方便调整配置、方便其他版本开发、接手的人看起来更直观
  94. * @param {String}name 节点名
  95. * {Array}attrs 节点属性数据
  96. * @return {void}
  97. * */
  98. function Element(name, attrs = []) {
  99. this.name = name;
  100. this.attrs = attrs;
  101. this.attrs = check(this.attrs);
  102. handleXMLEntity(this.attrs);
  103. this.children = [];
  104. }
  105. /*
  106. * xml字符实体的处理,这些特殊字符不处理会导致xml文件格式出错:""、<>、&
  107. * 要先处理&amp
  108. * */
  109. const _xmlEntity = {
  110. '&': '&amp;',
  111. '\n': '&#xA;',
  112. '"': '&quot;',
  113. '\'': '&apos;',
  114. '<': '&lt;',
  115. '>': '&gt;'
  116. };
  117. // 对每个元素的所有属性值进行特殊字符处理
  118. function handleXMLEntity(attrs) {
  119. for (const attr of attrs) {
  120. if (!attr.value) {
  121. continue;
  122. }
  123. for (const [key, value] of Object.entries(_xmlEntity)) {
  124. attr.value = attr.value.replace(new RegExp(key, 'g'), value);
  125. }
  126. }
  127. }
  128. // 获取处理实体字符后的数据
  129. function getParsedData(arr) {
  130. return arr.map(data => {
  131. for (const [key, value] of Object.entries(_xmlEntity)) {
  132. data = data.replace(new RegExp(key, 'g'), value);
  133. }
  134. return data;
  135. });
  136. }
  137. // 获取Date类型默认值
  138. function getDateTypeDefaultValue(date) {
  139. const month = String(date.getMonth() + 1);
  140. const formattedMonth = month.length === 1 ? `0${month}` : month;
  141. const day = String(date.getDate());
  142. const formattedDay = day.length === 1 ? `0${day}` : day;
  143. return `${date.getFullYear()}-${formattedMonth}-${formattedDay}`;
  144. }
  145. /*
  146. * 检查
  147. * 创建节点时检查节点的数据(原本是用于自检,现在来处理默认值)
  148. * @param {Array}datas 需要检查的属性数据
  149. * @return {Array}
  150. * */
  151. function check(datas) {
  152. const rst = [];
  153. for (const data of datas) {
  154. // 错误提示
  155. if (data.fail && data.fail.hint) {
  156. _cache.failList.push(data.fail);
  157. }
  158. const isHasValue = hasValue(data.value);
  159. // 设置了mustHasValue,意味着必须要有值才输出该属性
  160. if (data.mustHasValue && !isHasValue) {
  161. continue;
  162. }
  163. rst.push(data);
  164. // 值统一转换成String,并且处理各类型属性空值时的默认取值
  165. data.value = !isHasValue ?
  166. DEFAULT_VALUE[data.type] ?
  167. DEFAULT_VALUE[data.type] :
  168. '' :
  169. String(data.value);
  170. // 如果有限定最少长度,则当长度不够时,自动凑
  171. const autoStr = '1';
  172. if (data.minLen && data.value.length < data.minLen) {
  173. let diff = data.minLen - data.value.length;
  174. while (diff--) {
  175. data.value += autoStr;
  176. }
  177. }
  178. if (data.whiteSpace && data.whiteSpace === WHITE_SPACE.COLLAPSE) { //处理空格相关
  179. data.value = data.value.replace(/[\r\n\t]/g, ' ');
  180. data.value = data.value.trim();
  181. data.value = data.value.replace(/\s{1,}/g, ' ');
  182. }
  183. // 类型对应得值不正确时,赋类型对应默认值
  184. if (!data.type) {
  185. continue;
  186. }
  187. const dateReg = /([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8])))/;
  188. if (data.type === TYPE.DATE && !dateReg.test(data.value)) {
  189. data.value = getDateTypeDefaultValue(new Date());
  190. } else if (data.type === TYPE.INT && !Number.isInteger(parseFloat(data.value))) {
  191. data.value = DEFAULT_VALUE[TYPE.INT];
  192. } else if (data.type === TYPE.DECIMAL && isNaN(parseFloat(data.value))) {
  193. data.value = DEFAULT_VALUE[TYPE.DECIMAL];
  194. } else if (data.type === TYPE.NUM2) {
  195. data.value = DEFAULT_VALUE[TYPE.NUM2];
  196. } else if (data.type === TYPE.BOOL && !['true', 'false'].includes(String(data.value))) {
  197. data.value = DEFAULT_VALUE[TYPE.BOOL];
  198. }
  199. //自动补0
  200. if (data.toFix) {
  201. data.value = scMathUtil.roundToString(data.value,data.toFix)
  202. }
  203. }
  204. return rst;
  205. }
  206. // 等待一段时间
  207. function setTimeoutSync(handle, time) {
  208. return new Promise((resolve, reject) => {
  209. setTimeout(() => {
  210. if (handle && typeof handle === 'function') {
  211. handle();
  212. }
  213. resolve();
  214. }, time);
  215. });
  216. }
  217. /*
  218. * 将节点属性数据(attr数组)转换成简单key-value数据
  219. * @param {Object}ele 元素节点数据Element实例
  220. * @return {Object}
  221. * */
  222. function getPlainAttrs(ele) {
  223. const obj = {};
  224. ele.attrs.forEach(attr => obj[attr.name] = attr.value);
  225. return obj;
  226. }
  227. /*
  228. * 从fees数组中获取相关费用
  229. * @param {Array}fees 费用数组
  230. * {String}feeFields 费用字段
  231. * @return {Number}
  232. * @example getFee(source.fees, 'common.totalFee')
  233. * */
  234. function getFee(fees, feeFields, exportKind = null) {
  235. if (exportKind === EXPORT_KIND.BID_INVITATION || !Array.isArray(fees)) {
  236. return 0;
  237. }
  238. const fields = feeFields.split('.');
  239. const fee = fees.find(data => data.fieldName === fields[0]);
  240. if (!fee) {
  241. return 0;
  242. }
  243. return fee[fields[1]] || 0;
  244. }
  245. //取单价等
  246. function getUnitFee(fee,quantity,decimal){
  247. if(!!+quantity && +quantity !== 0){
  248. return scMathUtil.roundForObj(fee/parseFloat(quantity),decimal)
  249. }
  250. return fee
  251. }
  252. // 获取节点的汇总价格
  253. function getAggregateFee(nodes) {
  254. const total = nodes.reduce((acc, node) => {
  255. const price = getFee(node.data.fees, 'common.totalFee');
  256. return acc += price;
  257. }, 0);
  258. return scMathUtil.roundTo(total, -2);
  259. }
  260. // 获取固定类别行的费用
  261. function getFeeByFlag(items, flag, feeFields) {
  262. const node = items.find(node => node.getFlag() === flag);
  263. return node ? getFee(node.data.fees, feeFields) : '0';
  264. }
  265. // 获取固定类别节点
  266. function getNodeByFlag(tree, flag) {
  267. return tree.items.find(node => node.getFlag() === flag);
  268. }
  269. /*
  270. * 根据key获取对应的基本信息、工程特征数据
  271. * @param {Array}data
  272. * {String}key
  273. * @return {String}
  274. * @example getValueByKey(source.basicInformation, 'projectScale')
  275. * */
  276. function getValueByKey(items, key) {
  277. for (const item of items) {
  278. if (item.key === key) {
  279. return item.value;
  280. }
  281. if (item.items && item.items.length) {
  282. const value = getValueByKey(item.items, key);
  283. if (value) {
  284. return value;
  285. }
  286. }
  287. }
  288. return '';
  289. }
  290. //获取当前日期,格式YYYY-MM-DD
  291. function getNowFormatDay(nowDate) {
  292. var char = "-";
  293. if (nowDate == null) {
  294. nowDate = new Date();
  295. }
  296. var day = nowDate.getDate();
  297. var month = nowDate.getMonth() + 1; //注意月份需要+1
  298. var year = nowDate.getFullYear();
  299. //补全0,并拼接
  300. return year + char + completeDate(month) + char + completeDate(day);
  301. }
  302. //获取当前时间,格式YYYY-MM-DD HH:mm
  303. function getNowFormatTime(T) {
  304. var nowDate = new Date();
  305. var colon = ":";
  306. var h = nowDate.getHours();
  307. var m = nowDate.getMinutes();
  308. var s = nowDate.getSeconds();
  309. if (T) {
  310. return getNowFormatDay(nowDate) + "T" + completeDate(h) + colon + completeDate(m)+colon+completeDate(s);
  311. }
  312. //补全0,并拼接
  313. return getNowFormatDay(nowDate) + " " + completeDate(h) + colon + completeDate(m);
  314. }
  315. //补全0
  316. function completeDate(value) {
  317. return value < 10 ? "0" + value : value;
  318. }
  319. // 获取关联材料
  320. function getRelGLJ(allGLJs, gljId) {
  321. return allGLJs.find(glj => glj.id === gljId);
  322. }
  323. // 随机生成机器信息码:CPU信息;硬盘序列号;mac地址;
  324. // 保存在localStorage中
  325. function generateHardwareId() {
  326. const hardwareCacheId = window.localStorage.getItem('hardwareId');
  327. if (hardwareCacheId) {
  328. return hardwareCacheId;
  329. }
  330. const charList = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
  331. 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
  332. 'V', 'W', 'X', 'Y', 'Z'
  333. ];
  334. function generateCpuId() {
  335. let id = '';
  336. let count = 16;
  337. while (count--) {
  338. const randomIdx = parseInt(Math.random() * 16);
  339. id += charList[randomIdx];
  340. }
  341. return id;
  342. }
  343. function generateDiskId() {
  344. let id = '';
  345. let count = 8;
  346. while (count--) {
  347. const randomIdx = parseInt(Math.random() * 36);
  348. id += charList[randomIdx];
  349. }
  350. return id;
  351. }
  352. function generateMacId() {
  353. const idList = [];
  354. let outerCount = 6;
  355. while (outerCount--) {
  356. let tempId = '';
  357. let innerCount = 2;
  358. while (innerCount--) {
  359. const randomIdx = parseInt(Math.random() * 16);
  360. tempId += charList[randomIdx];
  361. }
  362. idList.push(tempId);
  363. }
  364. return idList.join('-');
  365. }
  366. const cpuId = generateCpuId();
  367. const diskId = generateDiskId();
  368. const macId = generateMacId();
  369. const hardwareId = [cpuId, diskId, macId].join(';');
  370. window.localStorage.setItem('hardwareId', hardwareId);
  371. return hardwareId;
  372. }
  373. // 数组打平成对象
  374. function arrayToObj(arr) {
  375. const rst = {};
  376. for (const data of arr) {
  377. rst[data.key] = data.value;
  378. }
  379. return rst;
  380. }
  381. /*
  382. * 检测层数是否有效
  383. * @param {Number}maxDepth(最大深度)
  384. * {Object}node(需要检测的清单树节点)
  385. * @return {Boolean}
  386. * */
  387. function validDepth(maxDepth, node) {
  388. const nodeDepth = node.depth();
  389. const allNodes = node.getPosterity();
  390. //检测相对深度
  391. for (const n of allNodes) {
  392. const relativeDepth = n.depth() - nodeDepth;
  393. if (relativeDepth > maxDepth) {
  394. return false;
  395. }
  396. }
  397. return true;
  398. }
  399. // 根据数据的NextSiblingID进行排序,返回排序后的数组
  400. function sortByNext(datas) {
  401. const target = [];
  402. const temp = {};
  403. for (const data of datas) {
  404. temp[data.ID] = {
  405. me: data,
  406. next: null,
  407. prev: null
  408. };
  409. }
  410. for (const data of datas) {
  411. const next = temp[data.NextSiblingID] || null;
  412. temp[data.ID].next = next;
  413. if (next) {
  414. next.prev = temp[data.ID];
  415. }
  416. }
  417. let first = null;
  418. for (const data of datas) {
  419. const me = temp[data.ID];
  420. if (!me.prev) {
  421. first = me;
  422. }
  423. }
  424. if (!first) {
  425. return datas;
  426. }
  427. while (first) {
  428. target.push(first.me);
  429. first = first.next;
  430. }
  431. return target;
  432. }
  433. /*
  434. * 根据粒度获取项目(不包含详细数据)数据
  435. * @param {Number}granularity 导出粒度
  436. * {Object}requestForSummaryInfo 项目表级汇总字段(建设项目、单位工程汇总)
  437. * {Number}tenderID 单位工程ID
  438. * {String}userID 用户ID
  439. * @return {Object} 返回的数据结构:{children: [{children: []}]} 最外层为建设项目,中间为单项工程,最底层为单位工程
  440. * */
  441. async function getProjectByGranularity(granularity, requestForSummaryInfo, tenderID, userID) {
  442. let projectData = _cache.projectData;
  443. // 没有数据,需要拉取
  444. if (!Object.keys(projectData).length) {
  445. projectData = await ajaxPost('/pm/api/getProjectByGranularity', {
  446. user_id: userID,
  447. tenderID,
  448. granularity,
  449. requestForSummaryInfo
  450. });
  451. _cache.projectData = projectData;
  452. }
  453. return projectData;
  454. }
  455. /*
  456. * 通过getData接口获取单位工程详细数据(带缓存功能)
  457. * @param {Number}tenderID 单位工程ID
  458. * {String}userID 用户ID
  459. * @return {Object} 跟projectObj.project的数据结构一致
  460. * */
  461. async function getTenderDetail(tenderID, userID) {
  462. // 获取单位工程详细数据
  463. let tenderDetail = _cache.tenderDetailMap[tenderID];
  464. if (!tenderDetail) {
  465. tenderDetail = PROJECT.createNew(tenderID, userID);
  466. await tenderDetail.loadDataSync();
  467. // 标记序号
  468. const count = Object.keys(_cache.tenderDetailMap).length;
  469. tenderDetail.serialNo = count + 1;
  470. _cache.tenderDetailMap[tenderID] = tenderDetail;
  471. }
  472. return tenderDetail;
  473. }
  474. // 获取普通基数: {xxx}
  475. function getNormalBase(str) {
  476. const reg = /{.+?}/g;
  477. const matchs = str.match(reg);
  478. return matchs || [];
  479. }
  480. // 获取id引用基数: @xxx-xxx-xx
  481. function getIDBase(str) {
  482. const reg = /@.{36}/g;
  483. const matchs = str.match(reg);
  484. return matchs || [];
  485. }
  486. // 转换基数表达式
  487. // 1.有子项,则取固定清单对应基数
  488. // 2.无子项,有基数,a.优先转换为行代号(不可自身) b.不能转换为行代号则找对应字典
  489. // 3.基数中有无法转换的,根据导出类型决定
  490. function transformCalcBase(exportKind, tenderDetail, node, {
  491. CalcBaseMap,
  492. FlagCalcBaseMap
  493. }) {
  494. let expr = node.data.calcBase || '';
  495. if (node.children.length) {
  496. const flag = node.getFlag();
  497. return FlagCalcBaseMap[flag] || '';
  498. }
  499. if (expr) {
  500. let illegal = false;
  501. const normalBase = getNormalBase(expr);
  502. const idBase = getIDBase(expr);
  503. // 普通基数转基数字典
  504. normalBase.forEach(base => {
  505. let replaceStr = CalcBaseMap[base];
  506. // 转换成行代号的优先级比较高,进行清单匹配
  507. const flag = FlagCalcBaseMap[base];
  508. if (flag) {
  509. const flagNode = tenderDetail.mainTree.items.find(mNode => mNode.getFlag() === flag);
  510. // 匹配到了 普通基数转换成行引用
  511. if (flagNode) {
  512. replaceStr = `F${flagNode.serialNo() + 1}`;
  513. }
  514. }
  515. // 存在无法处理的基数
  516. if (!replaceStr) {
  517. illegal = true;
  518. return;
  519. }
  520. expr = expr.replace(new RegExp(base, 'g'), replaceStr);
  521. });
  522. // id引用转行代号引用
  523. idBase.forEach(base => {
  524. const id = base.match(/[^@]+/)[0];
  525. const theNode = tenderDetail.mainTree.getNodeByID(id);
  526. const rowCode = theNode ? `F${theNode.serialNo() + 1}` : '';
  527. if (!rowCode) {
  528. illegal = true;
  529. return;
  530. }
  531. expr = expr.replace(new RegExp(base, 'g'), rowCode);
  532. });
  533. // 不合法
  534. // 在我们软件中的基数无法找到映射代号的情况下
  535. // 导出招标、控制价时,基数为空
  536. // 导出投标时,基数=综合合价/费率
  537. if (illegal) {
  538. if (exportKind === EXPORT_KIND.BID_INVITATION || exportKind === EXPORT_KIND.CONTROL) {
  539. return '';
  540. } else {
  541. const totalFee = getFee(node.data.fees, 'common.totalFee');
  542. const feeRate = node.data.feeRate;
  543. return +feeRate ? scMathUtil.roundTo(totalFee / (feeRate / 100), -2) : totalFee
  544. }
  545. }
  546. return expr;
  547. }
  548. }
  549. // 转换基数说明,根据转换后的基数处理
  550. // 1.行引用转换为对应行的名称
  551. // 2.基数字典转换为中文
  552. function transformCalcBaseState(tenderDetail, expr, CalcStateMap) {
  553. if (!expr) {
  554. return '';
  555. }
  556. expr = String(expr);
  557. // 提取基数
  558. const bases = expr.split(/[\+\-\*\/]/g);
  559. // 提取操作符
  560. const oprs = expr.match(/[\+\-\*\/]/g);
  561. // 转换后的基数
  562. const newBase = [];
  563. let illegal = false;
  564. for (const base of bases) {
  565. // 行引用转换为名称.
  566. if (/F\d+/.test(base)) {
  567. const rowCode = base.match(/\d+/)[0];
  568. const node = tenderDetail.mainTree.items[rowCode - 1];
  569. if (!node || !node.data.name) {
  570. illegal = true;
  571. break;
  572. }
  573. newBase.push(node && node.data.name ? node.data.name : '');
  574. } else if (CalcStateMap[base]) { // 字典转换为中文
  575. newBase.push(CalcStateMap[base]);
  576. } else if (/^\d+(\.\d+)?$/.test(base)) { // 金额
  577. newBase.push(base);
  578. } else {
  579. illegal = true;
  580. break;
  581. }
  582. }
  583. if (illegal) {
  584. return '';
  585. }
  586. let newExpr = '';
  587. for (let i = 0; i < newBase.length; i++) {
  588. newExpr += newBase[i];
  589. if (oprs && oprs[i]) {
  590. newExpr += oprs[i];
  591. }
  592. }
  593. return newExpr;
  594. }
  595. // 获取节点的某属性
  596. function getAttr(ele, name) {
  597. return (ele.attrs.find(attr => attr.name === name) || {}).value;
  598. }
  599. // 设置节点的某属性
  600. function setAttr(ele, name, value) {
  601. const attr = ele.attrs.find(attr => attr.name === name);
  602. if (attr) {
  603. attr.value = value;
  604. }
  605. }
  606. // 从srcEle节点中获取元素名为eleName的元素
  607. function getElementFromSrc(srcEle, eleName) {
  608. if (!srcEle || !srcEle.children || !srcEle.children.length) {
  609. return [];
  610. }
  611. return srcEle.children.filter(ele => ele.name === eleName);
  612. }
  613. /*
  614. * 设置完工程编号后,更新原始数据的工程编号
  615. * 更新原始数据前需要将编号里的特殊字符进行转换
  616. * @param {Array}exportData 提取出来的需要导出的数据
  617. * {Array}codes 工程编号表中填写的工程编号
  618. * {String}EngineeringName 单项工程元素的名称
  619. * {String}tenderName 单位工程元素的名称
  620. * {String}codeName 编号属性的名称
  621. * @return {void}
  622. * */
  623. function setupCode(exportData, codes, EngineeringName, tenderName, codeName) {
  624. // 转换xml实体字符
  625. let parsedCodes = getParsedData(codes);
  626. // 给导出数据里的单项工程、单位工程填上用户设置的工程编号
  627. exportData.forEach(orgData => {
  628. let curIdx = 0;
  629. let engs = getElementFromSrc(orgData.data, EngineeringName);
  630. engs.forEach(eng => {
  631. eng.attrs.find(attr => attr.name === codeName).value = parsedCodes[curIdx++];
  632. let tenders = getElementFromSrc(eng, tenderName);
  633. tenders.forEach(tender => {
  634. tender.attrs.find(attr => attr.name === codeName).value = parsedCodes[curIdx++];
  635. });
  636. });
  637. });
  638. }
  639. // 将文本的中文提取出来
  640. function getHan(str) {
  641. if (!str) {
  642. return '';
  643. }
  644. return str
  645. .split('')
  646. .reduce((acc, cur) => {
  647. if (isHan(cur)) {
  648. acc.push(cur);
  649. }
  650. return acc;
  651. }, [])
  652. .join('');
  653. }
  654. // 将错误提示数据进行分类处理
  655. function transformFailList(failList) {
  656. const grouped = _.groupBy(failList, 'type');
  657. const rst = [];
  658. Object
  659. .entries(grouped)
  660. .forEach(([type, items]) => {
  661. if (type) {
  662. rst.push(`<span style="font-weight: bold">${type}:</span>`);
  663. }
  664. items.forEach(({ hint }) => {
  665. rst.push(hint);
  666. });
  667. });
  668. return rst;
  669. }
  670. const UTIL = Object.freeze({
  671. hasValue,
  672. setTimeoutSync,
  673. getFee,
  674. getUnitFee,
  675. getAggregateFee,
  676. getFeeByFlag,
  677. getNodeByFlag,
  678. getPlainAttrs,
  679. getValueByKey,
  680. getRelGLJ,
  681. generateHardwareId,
  682. arrayToObj,
  683. validDepth,
  684. sortByNext,
  685. getTenderDetail,
  686. getProjectByGranularity,
  687. getNormalBase,
  688. getIDBase,
  689. transformCalcBase,
  690. transformCalcBaseState,
  691. getElementFromSrc,
  692. getAttr,
  693. setAttr,
  694. getParsedData,
  695. setupCode,
  696. getHan,
  697. getNowFormatTime,
  698. transformFailList,
  699. });
  700. // 开始标签
  701. function _startTag(ele) {
  702. let rst = `<${ele.name}`;
  703. for (const attr of ele.attrs) {
  704. rst += ` ${attr.name}="${attr.value}"`;
  705. }
  706. rst += ele.children.length > 0 ? '>' : '/>';
  707. return rst;
  708. }
  709. // 结束标签
  710. function _endTag(ele) {
  711. return `</${ele.name}>`;
  712. }
  713. // 拼接成xml字符串
  714. function _toXMLStr(eles) {
  715. let rst = '';
  716. for (const ele of eles) {
  717. rst += _startTag(ele);
  718. if (ele.children.length > 0) {
  719. rst += _toXMLStr(ele.children);
  720. rst += _endTag(ele);
  721. }
  722. }
  723. return rst;
  724. }
  725. // 格式化xml字符串
  726. function _formatXml(text) {
  727. // 去掉多余的空格
  728. text = '\n' + text.replace(/>\s*?</g, ">\n<");
  729. // 调整格式
  730. const reg = /\n(<(([^\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/mg;
  731. const nodeStack = [];
  732. const output = text.replace(reg, function ($0, all, name, isBegin, isCloseFull1, isCloseFull2, isFull1, isFull2) {
  733. const isClosed = (isCloseFull1 === '/') || (isCloseFull2 === '/') || (isFull1 === '/') || (isFull2 === '/');
  734. let prefix = '';
  735. if (isBegin === '!') {
  736. prefix = getPrefix(nodeStack.length);
  737. } else {
  738. if (isBegin !== '/') {
  739. prefix = getPrefix(nodeStack.length);
  740. if (!isClosed) {
  741. nodeStack.push(name);
  742. }
  743. } else {
  744. nodeStack.pop();
  745. prefix = getPrefix(nodeStack.length);
  746. }
  747. }
  748. return '\n' + prefix + all;
  749. });
  750. return output.substring(1);
  751. function getPrefix(prefixIndex) {
  752. const span = ' ';
  753. const output = [];
  754. for (let i = 0; i < prefixIndex; i++) {
  755. output.push(span);
  756. }
  757. return output.join('');
  758. }
  759. }
  760. /**
  761. * 提取要导出的数据
  762. * @param {Function} entryFunc - 提取数据的入口方法
  763. * @param {Object} requestForSummaryInfo - 项目表级汇总字段(建设项目、单位工程汇总)
  764. * @param {Number} exportKind - 导出的文件类型:1-招标、2-投标、3-控制价
  765. * @param {String} areaKey - 地区标识,如:'安徽@马鞍山'
  766. * @param {Number} tenderID - 单位工程ID
  767. * @param {String} userID - 用户ID
  768. * @return {Promise<Array>} - [{data: Object, exportKind: Number, fileName: String}]
  769. */
  770. async function extractExportData(entryFunc, requestForSummaryInfo, exportKind, areaKey, tenderID, userID) {
  771. // 默认导出投标文件
  772. if (!exportKind || ![1, 2, 3].includes(exportKind)) {
  773. exportKind = EXPORT_KIND.BID_SUBMISSION;
  774. }
  775. // 拉取标段数据:建设项目、单位工程数据(projects表数据)
  776. const projectData = await getProjectByGranularity(GRANULARITY.PROJECT, requestForSummaryInfo, tenderID, userID);
  777. if (!projectData) {
  778. throw '获取项目数据错误';
  779. }
  780. // 单位工程按照树结构数据进行排序,这样导出才的单位工程顺序才是对的
  781. projectData.children = sortByNext(projectData.children);
  782. // 先获取需要导出的单位工程的详细数据
  783. const tenderDetailMap = getItem('tenderDetailMap');
  784. for (const tenderItem of projectData.children) {
  785. if (!tenderDetailMap[tenderItem.ID]) {
  786. await setTimeoutSync(() => { }, TIMEOUT_TIME); // 需要请求项目详细数据的时候,间隔一段时间再初始单位工程数据,减少服务器压力
  787. }
  788. // 获取单位工程详细数据
  789. const detail = await getTenderDetail(tenderItem.ID, userID);
  790. tenderDetailPretreatment(detail);
  791. console.log(detail);
  792. }
  793. // 提取相关项目的详细导出数据
  794. return await entryFunc(areaKey, exportKind, projectData, tenderDetailMap);
  795. }
  796. // 对getData返回的数据进行一些通用预处理,方便各接口直接取值、处理
  797. function tenderDetailPretreatment(tenderDetail) {
  798. const bidEvaluationList = tenderDetail.bid_evaluation_list.datas;
  799. const evaluateList = tenderDetail.evaluate_list.datas;
  800. const decimalInfo = tenderDetail.projectInfo.property.decimal;
  801. // 项目人材机汇总排序
  802. tenderDetail.projectGLJ.datas.gljList = gljUtil.sortProjectGLJ(tenderDetail.projectGLJ.datas.gljList);
  803. const projectGLJList = tenderDetail.projectGLJ.datas.gljList;
  804. // 计算人材机总消耗量,否则projectGLJ.datas.gljList里的数据不会有消耗量数据
  805. gljUtil.calcProjectGLJQuantity(tenderDetail.projectGLJ.datas, tenderDetail.ration_glj.datas, tenderDetail.Ration.datas, tenderDetail.Bills.datas, tenderDetail.property.decimal.glj.quantity, _, scMathUtil);
  806. const connectKeyMap = {};
  807. const projectGLJIDMap = {};
  808. projectGLJList.forEach(glj => {
  809. connectKeyMap[gljUtil.getIndex(glj, gljKeyArray)] = glj.id; // 为了方便后续导出处理,给组成物数据设置上对应项目人材机ID
  810. projectGLJIDMap[glj.id] = glj;
  811. // 项目人材机设置价格信息,否则projectGLJ.datas.gljList里的数据不会有相关价格信息
  812. // 价格信息存在新的priceInfo字段,以免对一些方法造成影响
  813. glj.priceInfo = gljUtil.getGLJPrice(glj, tenderDetail.projectGLJ.datas, tenderDetail.property.calcOptions, tenderDetail.labourCoe.datas, decimalInfo, false, _, scMathUtil, {}, tenderDetail.projectGLJ.getTenderPriceCoe(glj, tenderDetail.property));
  814. // 计算合价:人材料总消耗量*预算价
  815. glj.priceInfo.totalPrice = scMathUtil.roundForObj(glj.priceInfo.tenderPrice * glj.tenderQuantity, 2);
  816. });
  817. const ratiosArr = Object.values(tenderDetail.projectGLJ.datas.mixRatioMap);
  818. ratiosArr.forEach(ratios => {
  819. ratios.forEach(ratio => {
  820. const ratioKey = gljUtil.getIndex(ratio, gljKeyArray);
  821. const projectGLJID = connectKeyMap[ratioKey];
  822. if (projectGLJID) {
  823. ratio.projectGLJID = projectGLJID;
  824. }
  825. });
  826. });
  827. // 处理定额人材机,将定额人材机挂到定额的rationGLJList字段中,同时将定额人材机进行排序、计算调价消耗量
  828. tenderDetail.Ration.datas.forEach(ration => {
  829. ration.rationGLJList = tenderDetail.ration_glj.datas.filter(glj => {
  830. const pGLJ = projectGLJIDMap[glj.projectGLJID];
  831. glj.tenderQuantity = gljUtil.getRationGLJTenderQuantity(glj, ration, decimalInfo.glj.quantity, scMathUtil, pGLJ);
  832. return glj.rationID === ration.ID;
  833. });
  834. ration.rationGLJList = gljUtil.sortRationGLJ(ration.rationGLJList);
  835. });
  836. // 获取暂估价材料数据,getData原始数据evaluate_list.datas里的数据缺少一些价格数据,需要调用额外接口
  837. tenderDetail.evaluateMaterialData = configMaterialObj.getEvaluateMaterialDatas(projectGLJList, evaluateList, decimalInfo);
  838. // 获取评标材料数据
  839. tenderDetail.bidMaterialData = configMaterialObj.getBidMaterialDatas(projectGLJList, bidEvaluationList, decimalInfo);
  840. }
  841. /**
  842. * 根据各自费用定额的文件结构,导出文件
  843. * 每个费用定额可能导出的结果文件都不同
  844. * 比如广东18需要将一个建设项目文件,多个单位工程文件打包成一个zip文件。重庆18就没这种要求
  845. * @param {Array} extractData - 提取的数据
  846. * @param {Function} saveAsFunc - 各自费用定额的导出方法,适应不同接口需要不同的最终文件形式
  847. */
  848. async function exportFile(extractData, saveAsFunc) {
  849. // 获取文件数据
  850. const fileData = extractData.map(extractObj => {
  851. // 转换成xml字符串
  852. let xmlStr = _toXMLStr([extractObj.data]);
  853. // 加上xml声明
  854. xmlStr = `<?xml version="1.0" encoding="utf-8"?>${xmlStr}`;
  855. // 格式化
  856. xmlStr = _formatXml(xmlStr);
  857. const blob = new Blob([xmlStr], {
  858. type: 'text/plain;charset=utf-8'
  859. });
  860. return {
  861. blob: blob,
  862. exportKind: extractObj.exportKind,
  863. fileName: extractObj.fileName
  864. };
  865. });
  866. if (!saveAsFunc) {
  867. return fileData;
  868. }
  869. // 导出
  870. await saveAsFunc(fileData);
  871. }
  872. /**
  873. * 默认的通用导出文件方法:一个文件数据对应一个xml文件(变更后缀)
  874. * @param {Array} fileData - 默认的通用导出文件方法:一个文件数据对应一个xml文件(变更后缀)
  875. * @return {Void}
  876. */
  877. async function defaultSaveAs(fileData) {
  878. fileData.forEach(fileItem => saveAs(fileItem.blob, fileItem.fileName));
  879. }
  880. return {
  881. CONFIG,
  882. CACHE,
  883. UTIL,
  884. Element,
  885. extractExportData,
  886. defaultSaveAs,
  887. exportFile,
  888. };
  889. })();