exportStdInterfaceBase.js 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. 'use strict';
  2. /**
  3. *
  4. *
  5. * @author Zhong
  6. * @date 2019/6/20
  7. * @version
  8. */
  9. const XML_EXPORT_BASE = (() => {
  10. // 自检提示的开始记号,区分提示属于哪一部分的类型(eg: 招标、控制价),便于后续做提示的合并去重
  11. const HINT_START = '--start--';
  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 TAX_TYPE = {
  34. '1': '一般计税法',
  35. '2': '简易计税法',
  36. };
  37. // 承包人材料调整类型
  38. const ADJUST_TYPE = {
  39. info: 'priceInfo', // 造价信息差额调整法
  40. coe: 'priceCoe' // 价格指数调整法
  41. };
  42. // 加载数据间隔,减少服务器压力
  43. const TIMEOUT_TIME = 400;
  44. // 导出粒度
  45. const GRANULARITY = {
  46. PROJECT: 1, //导出建设项目
  47. ENGINEERING: 2, //导出单项工程
  48. TENDER: 3 //导出单位工程
  49. };
  50. // 导出的文件类型选项
  51. const EXPORT_KIND = {
  52. Tender: 1, //投标
  53. Bid: 2, //招标
  54. Control: 3 //控制价
  55. };
  56. const EXPORT_KIND_NAME = {
  57. 1: '投标',
  58. 2: '招标',
  59. 3: '控制价'
  60. };
  61. // 配置项
  62. const CONFIG = Object.freeze({
  63. HINT_START,
  64. TYPE,
  65. WHITE_SPACE,
  66. TAX_TYPE,
  67. ADJUST_TYPE,
  68. TIMEOUT_TIME,
  69. GRANULARITY,
  70. EXPORT_KIND,
  71. EXPORT_KIND_NAME
  72. });
  73. // 缓存项 不需要的时候需要清空
  74. let _cache = {
  75. // 失败列表
  76. failList: [],
  77. // 项目数据(不包含详细数据,项目管理数据)
  78. projectData: {},
  79. // 当前导出类型,默认投标
  80. exportKind: EXPORT_KIND.Tender,
  81. // 记录拉取的单位工程项目详细数据,导出的时候,可能会导出多个文件,只有导出第一个文件的时候需要请求数据
  82. tenderDetailMap: {}
  83. };
  84. // 返回缓存项
  85. function getItem(key) {
  86. return _cache[key] || null;
  87. }
  88. // 设置缓存项
  89. function setItem(key, value) {
  90. // 与原数据是同类型的数据才可设置成功
  91. if (_cache[key] &&
  92. Object.prototype.toString.call(_cache[key]) ===
  93. Object.prototype.toString.call(value)) {
  94. _cache[key] = value;
  95. }
  96. }
  97. // 清空缓存项
  98. function clear() {
  99. _cache.failList = [];
  100. _cache.projectData = {};
  101. _cache.exportKind = EXPORT_KIND.Tender;
  102. _cache.tenderDetailMap = {};
  103. }
  104. const CACHE = Object.freeze({
  105. getItem,
  106. setItem,
  107. clear
  108. });
  109. /*
  110. * 定义不设置一个Node方法统一进入的原因:模板化比较直观,不分开定义节点的话,调用传参也很麻烦而且不直观。
  111. * 一个节点对应一个构造方法,方便调整配置、方便其他版本开发、接手的人看起来更直观
  112. * @param {String}name 节点名
  113. * {Array}attrs 节点属性数据
  114. * {String}alias 别名,有些节点名称是英文,但是提示的时候需要显示中文的时候用
  115. * @return {void}
  116. * */
  117. function Element(name, attrs, alias) {
  118. this.name = name;
  119. const hintName = alias || name;
  120. const checkData = check(hintName, attrs);
  121. this.fail = checkData.failHints;
  122. this.attrs = checkData.filterAttrs;
  123. handleXMLEntity(this.attrs);
  124. this.children = [];
  125. _cache.failList.push(...this.fail);
  126. }
  127. /*
  128. * xml字符实体的处理,这些特殊字符不处理会导致xml文件格式出错:""、<>、&
  129. * 要先处理&amp
  130. * */
  131. let _xmlEntity = {
  132. '&': '&amp;',
  133. '\n': '&#xA;',
  134. '"': '&quot;',
  135. '\'': '&apos;',
  136. '<': '&lt;',
  137. '>': '&gt;'
  138. };
  139. // 对每个元素的所有属性值进行特殊字符处理
  140. function handleXMLEntity(attrs) {
  141. for (let attr of attrs) {
  142. if (!attr.value) {
  143. continue;
  144. }
  145. for (let [key, value] of Object.entries(_xmlEntity)) {
  146. attr.value = attr.value.replace(new RegExp(key, 'g'), value);
  147. }
  148. }
  149. }
  150. // 获取处理实体字符后的数据
  151. function getParsedData(arr) {
  152. return arr.map(data => {
  153. for (let [key, value] of Object.entries(_xmlEntity)) {
  154. data = data.replace(new RegExp(key, 'g'), value);
  155. }
  156. return data;
  157. });
  158. }
  159. /*
  160. * 检查
  161. * 创建节点时检查节点的数据
  162. * 1.长度限制minLen,maxLen
  163. * 2.值的限制,固定范围:enumeration
  164. * @param {String}eleName 节点名称
  165. * {Array}datas 需要检查的属性数据
  166. * @return {Object} failHints没通过的属性提示 filterAttrs过滤后的属性数据(失败提示在属性是必须的时候才提示,如果该属性失败了,但是是非必要属性,那么该属性不显示)
  167. * */
  168. function check(eleName, datas) {
  169. let rst = { failHints: [], filterAttrs: [] };
  170. let 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])))/;
  171. for (let data of datas) {
  172. const isHasValue = hasValue(data.value);
  173. // 某属性是非必须的,并且设置了mustHasValue,意味着必须要有值才输出该属性
  174. if (!data.required && data.mustHasValue && !isHasValue) {
  175. continue;
  176. }
  177. // 值统一转换成String,并且处理各类型属性空值时的默认取值
  178. data.value = !isHasValue
  179. ? DEFAULT_VALUE[data.type]
  180. ? DEFAULT_VALUE[data.type]
  181. : ''
  182. : String(data.value);
  183. if (data.whiteSpace && data.whiteSpace === WHITE_SPACE.COLLAPSE) { //处理空格相关
  184. data.value = data.value.replace(/[\r\n\t]/g, ' ');
  185. data.value = data.value.trim();
  186. data.value = data.value.replace(/\s{1,}/g, ' ');
  187. }
  188. let isFail = false;
  189. let tempFail = '';
  190. // 提示的名称需要处理,有的接口xml属性为英文,需要显示其展示名称
  191. const name = data.dName || data.name;
  192. if (data.minLen && data.value.length < data.minLen) {
  193. isFail = true;
  194. tempFail = data.failHint
  195. ? `${data.failHint}字符数不可小于${data.minLen}个。`
  196. : `${eleName}-“${name}”字符数不可小于${data.minLen}个。`;
  197. } else if (data.maxLen && data.value.length > data.maxLen) {
  198. isFail = true;
  199. tempFail = data.failHint
  200. ? `${data.failHint}字符数不可大于${data.maxLen}个。`
  201. : `${eleName}-“${name}”字符数不可大于${data.maxLen}个。`;
  202. } else if (data.enumeration && !data.enumeration.includes(data.value)) {
  203. isFail = true;
  204. let enumerationHint = data.enumerationHint
  205. ? data.enumerationHint.join(';')
  206. : data.enumeration.join(';');
  207. tempFail = data.failHint
  208. ? `${data.failHint}只能从“${enumerationHint}”中选择。`
  209. : `${eleName}-“${name}”只能从“${enumerationHint}”中选择。`;
  210. } else if (data.type && data.type === TYPE.DATE && !dateReg.test(data.value)) {
  211. isFail = true;
  212. tempFail = data.failHint
  213. ? `${data.failHint}日期格式必须是YYYY-MM-DD。`
  214. : `${eleName}-“${name}”日期格式必须是YYYY-MM-DD。`;
  215. } else if (data.type && data.type === TYPE.INT && !Number.isInteger(parseFloat(data.value))) {
  216. isFail = true;
  217. tempFail = data.failHint
  218. ? `${data.failHint}必须为整数。`
  219. : `${eleName}-“${name}”必须为整数。`;
  220. } else if (data.type && data.type === TYPE.DECIMAL && isNaN(parseFloat(data.value))) {
  221. isFail = true;
  222. tempFail = data.failHint
  223. ? `${data.failHint}必须为数值。`
  224. : `${eleName}-“${name}”必须为数值。`;
  225. } else if (data.type && data.type === TYPE.NUM2) {
  226. let v = parseFloat(data.value);
  227. if (isNaN(v)) {
  228. isFail = true;
  229. tempFail = data.failHint
  230. ? `${data.failHint}必须为数值。`
  231. : `${eleName}-“${name}”必须为数值。`;
  232. } else if (!data.value.length || (data.value.split('.').length > 1 && data.value.split('.')[1].length > 2)) {
  233. isFail = true;
  234. }
  235. } else if (data.type && data.type === TYPE.BOOL && !['true', 'false'].includes(String(data.value))) {
  236. isFail = true;
  237. tempFail = data.failHint
  238. ? `${data.failHint}必须为true或false。`
  239. : `${eleName}-“${name}”必须为true或false。`;
  240. }
  241. if (!isFail || data.required) {
  242. rst.filterAttrs.push(data);
  243. }
  244. if (isFail && data.required && tempFail) {
  245. rst.failHints.push(tempFail);
  246. }
  247. }
  248. return rst;
  249. }
  250. // 提取各导出类型的自检数据
  251. function _extractHintParts(failList) {
  252. let rst = [],
  253. curPart;
  254. for (let hint of failList) {
  255. if (hint === HINT_START) {
  256. curPart = [];
  257. rst.push(curPart);
  258. continue;
  259. }
  260. curPart.push(hint);
  261. }
  262. return rst;
  263. }
  264. // 自检提示去重
  265. function deWeightHints(failList) {
  266. let rst = [];
  267. let hintParts = _extractHintParts(failList);
  268. // 建设项目提示文本
  269. let rootHints = [],
  270. // 单位工程提示文本映射
  271. tenderMap = {},
  272. reg = /^<span style="font-weight: bold">单位工程/;
  273. for (let hintPart of hintParts) {
  274. // 单位工程xxx提示
  275. let curTenderHint;
  276. // 提取建设项目提示、各自单位工程提示
  277. for (let hint of hintPart) {
  278. if (reg.test(hint)) {
  279. curTenderHint = hint;
  280. if (!tenderMap[curTenderHint]) {
  281. tenderMap[curTenderHint] = [];
  282. }
  283. continue;
  284. }
  285. if (curTenderHint) {
  286. tenderMap[curTenderHint].push(hint);
  287. } else {
  288. rootHints.push(hint);
  289. }
  290. }
  291. }
  292. // 建设项目提示去重,放入结果中
  293. rootHints = [...new Set(rootHints)];
  294. rst.push(...rootHints);
  295. // 单位工程提示放入结果中
  296. for (let tenderHint in tenderMap) {
  297. rst.push(tenderHint);
  298. // 单位工程提示去重
  299. let tenderHints = [...new Set(tenderMap[tenderHint])];
  300. rst.push(...tenderHints);
  301. }
  302. return rst;
  303. }
  304. // 等待一段时间
  305. function setTimeoutSync(handle, time) {
  306. return new Promise((resolve, reject) => {
  307. setTimeout(() => {
  308. if (handle && typeof handle === 'function') {
  309. handle();
  310. }
  311. resolve();
  312. }, time);
  313. });
  314. }
  315. // v是否定义了,不为undefined和null
  316. function isDef(v) {
  317. return typeof v !== 'undefined' && v !== null;
  318. }
  319. function hasValue(v) {
  320. // v是否有值,不为undefined、null、''
  321. return typeof v !== 'undefined' && v !== null && v !== '';
  322. }
  323. /*
  324. * 将节点属性数据(attr数组)转换成简单key-value数据
  325. * @param {Object}ele 元素节点数据Element实例
  326. * @return {Object}
  327. * */
  328. function getPlainAttrs(ele) {
  329. const obj = {};
  330. ele.attrs.forEach(attr => obj[attr.name] = attr.value);
  331. return obj;
  332. }
  333. /*
  334. * 从fees数组中获取相关费用
  335. * @param {Array}fees 费用数组
  336. * {String}feeFields 费用字段
  337. * @return {Number}
  338. * @example getFee(source.fees, 'common.totalFee')
  339. * */
  340. function getFee(fees, feeFields) {
  341. if (!Array.isArray(fees)) {
  342. return 0;
  343. }
  344. let fields = feeFields.split('.');
  345. let fee = fees.find(data => data.fieldName === fields[0]);
  346. if (!fee) {
  347. return 0;
  348. }
  349. return fee[fields[1]] || 0;
  350. }
  351. // 获取节点的汇总价格
  352. function getAggregateFee(nodes) {
  353. let total = nodes.reduce((acc, node) => {
  354. const price = getFee(node.data.fees, 'common.totalFee');
  355. return acc += price;
  356. }, 0);
  357. return scMathUtil.roundTo(total, -2);
  358. }
  359. // 获取固定类别行的费用
  360. function getFeeByFlag(items, flag, feeFields) {
  361. const node = items.find(node => node.getFlag() === flag);
  362. return node ? getFee(node.data.fees, feeFields) : '0';
  363. }
  364. /*
  365. * 根据key获取对应的基本信息、工程特征数据
  366. * @param {Array}data
  367. * {String}key
  368. * @return {String}
  369. * @example getValueByKey(source.basicInformation, 'projectScale')
  370. * */
  371. function getValueByKey(items, key) {
  372. for (const item of items) {
  373. if (item.key === key) {
  374. return item.value;
  375. }
  376. if (item.items && item.items.length) {
  377. const value = getValueByKey(item.items, key);
  378. if (value) {
  379. return value;
  380. }
  381. }
  382. }
  383. return '';
  384. }
  385. // 获取关联材料
  386. function getRelGLJ(allGLJs, gljId) {
  387. return allGLJs.find(glj => glj.id === gljId);
  388. }
  389. // 随机生成机器信息码:CPU信息;硬盘序列号;mac地址;
  390. // 保存在localStorage中
  391. function generateHardwareId() {
  392. const hardwareCacheId = window.localStorage.getItem('hardwareId');
  393. if (hardwareCacheId) {
  394. return hardwareCacheId;
  395. }
  396. const charList = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
  397. 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
  398. 'V', 'W', 'X', 'Y', 'Z'];
  399. function generateCpuId() {
  400. let id = '';
  401. let count = 16;
  402. while (count--) {
  403. const randomIdx = parseInt(Math.random() * 16);
  404. id += charList[randomIdx];
  405. }
  406. return id;
  407. }
  408. function generateDiskId() {
  409. let id = '';
  410. let count = 8;
  411. while (count--) {
  412. const randomIdx = parseInt(Math.random() * 36);
  413. id += charList[randomIdx];
  414. }
  415. return id;
  416. }
  417. function generateMacId() {
  418. const idList = [];
  419. let outerCount = 6;
  420. while (outerCount--) {
  421. let tempId = '';
  422. let innerCount = 2;
  423. while (innerCount--) {
  424. const randomIdx = parseInt(Math.random() * 16);
  425. tempId += charList[randomIdx];
  426. }
  427. idList.push(tempId);
  428. }
  429. return idList.join('-');
  430. }
  431. const cpuId = generateCpuId();
  432. const diskId = generateDiskId();
  433. const macId = generateMacId();
  434. const hardwareId = [cpuId, diskId, macId].join(';');
  435. window.localStorage.setItem('hardwareId', hardwareId);
  436. return hardwareId;
  437. }
  438. // 数组打平成对象
  439. function arrayToObj(arr) {
  440. let rst = {};
  441. for (let data of arr) {
  442. rst[data.key] = data.value;
  443. }
  444. return rst;
  445. }
  446. /*
  447. * 检测层数
  448. * @param {Number}maxDepth(最大深度)
  449. * {Object}node(需要检测的清单树节点)
  450. * @return {Boolean}
  451. * */
  452. function validDepth(maxDepth, node) {
  453. let nodeDepth = node.depth();
  454. let allNodes = node.getPosterity();
  455. //检测相对深度
  456. for (let n of allNodes) {
  457. let relativeDepth = n.depth() - nodeDepth;
  458. if (relativeDepth > maxDepth) {
  459. return false;
  460. }
  461. }
  462. return true;
  463. }
  464. /*
  465. * 检测唯一性,有些属性在规定的数据范围内唯一
  466. * @param {Object}constraints(约束池)
  467. * {All}data(检测的数据)
  468. * {String}hint(提示已存在的内容)
  469. * {String}subHint(额外提示,有额外提示时,不用data提示)
  470. * @return {void}
  471. * */
  472. function checkUnique(constraints, data, hint, subHint) {
  473. if (constraints.includes(data)) {
  474. let failHint = subHint
  475. ? `${hint}“${subHint}”已存在`
  476. : `${hint}“${data}”已存在`;
  477. _cache.failList.push(failHint);
  478. } else if (data) {
  479. constraints.push(data);
  480. }
  481. }
  482. //根据数据的NextSiblingID进行排序,返回排序后的数组
  483. function sortByNext(datas) {
  484. let target = [],
  485. temp = {};
  486. for (let data of datas) {
  487. temp[data.ID] = { me: data, next: null, prev: null };
  488. }
  489. for (let data of datas) {
  490. let next = temp[data.NextSiblingID] || null;
  491. temp[data.ID].next = next;
  492. if (next) {
  493. next.prev = temp[data.ID];
  494. }
  495. }
  496. let first = null;
  497. for (let data of datas) {
  498. let me = temp[data.ID];
  499. if (!me.prev) {
  500. first = me;
  501. }
  502. }
  503. if (!first) {
  504. return datas;
  505. }
  506. while (first) {
  507. target.push(first.me);
  508. first = first.next;
  509. }
  510. return target;
  511. }
  512. /*
  513. * 根据粒度获取项目(不包含详细数据)数据
  514. * @param {Number}granularity 导出粒度
  515. * {Object}summaryObj 汇总字段
  516. * {Number}tenderID 单位工程ID
  517. * {String}userID 用户ID
  518. * @return {Object} 返回的数据结构:{children: [{children: []}]} 最外层为建设项目,中间为单项工程,最底层为单位工程
  519. * */
  520. async function getProjectByGranularity(granularity, summaryObj, tenderID, userID) {
  521. let projectData = _cache.projectData;
  522. // 没有数据,需要拉取
  523. if (!Object.keys(projectData).length) {
  524. projectData = await ajaxPost('/pm/api/getProjectByGranularity', { user_id: userID, tenderID, granularity, summaryObj });
  525. _cache.projectData = projectData;
  526. }
  527. return projectData;
  528. }
  529. /*
  530. * 通过getData接口获取单位工程详细数据(带缓存功能)
  531. * @param {Number}tenderID 单位工程ID
  532. * {String}userID 用户ID
  533. * @return {Object} 跟projectObj.project的数据结构一致
  534. * */
  535. async function getTenderDetail(tenderID, userID) {
  536. // 获取单位工程详细数据
  537. let tenderDetail = _cache.tenderDetailMap[tenderID];
  538. if (!tenderDetail) {
  539. tenderDetail = PROJECT.createNew(tenderID, userID);
  540. await tenderDetail.loadDataSync();
  541. // 标记序号
  542. const count = Object.keys(_cache.tenderDetailMap).length;
  543. tenderDetail.serialNo = count + 1;
  544. _cache.tenderDetailMap[tenderID] = tenderDetail;
  545. }
  546. return tenderDetail;
  547. }
  548. /*
  549. * 提取要导出的数据
  550. * @param {Function}entryFunc 提取数据的入口方法
  551. * {Number}granularity 导出粒度: 1-建设项目、2-单项工程、3-单位工程
  552. * {Number}exportKind 导出的文件类型:1-投标、2-招标、3-控制价
  553. * {Number}tenderID 单位工程ID
  554. * {String}userID 用户ID
  555. * @return {Array} 数据结构为:[{data: Object, exportKind: Number, fileName: String}]
  556. * */
  557. async function extractExportData(entryFunc, granularity, summaryObj, exportKind, tenderID, userID) {
  558. // 默认导出建设项目
  559. if (!granularity || ![1, 2, 3].includes(granularity)) {
  560. granularity = GRANULARITY.PROJECT;
  561. }
  562. // 默认导出投标文件
  563. if (!exportKind || ![1, 2, 3].includes(exportKind)) {
  564. exportKind = EXPORT_KIND.Tender;
  565. }
  566. // 拉取标段数据:建设项目、单项工程、单位工程数据
  567. let projectData = await getProjectByGranularity(granularity, summaryObj, tenderID, userID);
  568. if (!projectData) {
  569. throw '获取项目数据错误';
  570. }
  571. // 单项工程、单位工程按照树结构数据进行排序
  572. projectData.children = sortByNext(projectData.children);
  573. for (let engData of projectData.children) {
  574. engData.children = sortByNext(engData.children);
  575. }
  576. // 提取相关项目的详细导出数据
  577. return await entryFunc(userID, exportKind, projectData);
  578. }
  579. //获取普通基数: {xxx}
  580. function getNormalBase(str) {
  581. let reg = /{.+?}/g,
  582. matchs = str.match(reg);
  583. return matchs || [];
  584. }
  585. //获取id引用基数: @xxx-xxx-xx
  586. function getIDBase(str) {
  587. let reg = /@.{36}/g,
  588. matchs = str.match(reg);
  589. return matchs || [];
  590. }
  591. // 转换基数表达式
  592. // 1.有子项,则取固定清单对应基数
  593. // 2.无子项,有基数,a.优先转换为行代号(不可自身) b.不能转换为行代号则找对应字典
  594. // 3.基数中有无法转换的,根据导出类型决定
  595. function transformCalcBase(exportKind, tenderDetail, node, { CalcBaseMap, FlagCalcBaseMap }) {
  596. let expr = node.data.calcBase || '';
  597. if (node.children.length) {
  598. let flag = node.getFlag();
  599. return FlagCalcBaseMap[flag] || '';
  600. }
  601. if (expr) {
  602. let illegal = false;
  603. let normalBase = getNormalBase(expr),
  604. idBase = getIDBase(expr);
  605. //普通基数转基数字典
  606. normalBase.forEach(base => {
  607. let replaceStr = CalcBaseMap[base];
  608. //转换成行代号的优先级比较高,进行清单匹配
  609. let flag = FlagCalcBaseMap[base];
  610. if (flag) {
  611. let flagNode = tenderDetail.mainTree.items.find(mNode => mNode.getFlag() === flag);
  612. //匹配到了 普通基数转换成行引用
  613. if (flagNode) {
  614. replaceStr = `F${flagNode.serialNo() + 1}`;
  615. }
  616. }
  617. //存在无法处理的基数
  618. if (!replaceStr) {
  619. illegal = true;
  620. return;
  621. }
  622. expr = expr.replace(new RegExp(base, 'g'), replaceStr);
  623. });
  624. //id引用转行代号引用
  625. idBase.forEach(base => {
  626. let id = base.match(/[^@]+/)[0];
  627. let theNode = tenderDetail.mainTree.getNodeByID(id),
  628. rowCode = theNode ? `F${theNode.serialNo() + 1}` : '';
  629. if (!rowCode) {
  630. illegal = true;
  631. return;
  632. }
  633. expr = expr.replace(new RegExp(base, 'g'), rowCode);
  634. });
  635. //不合法
  636. // 在我们软件中的基数无法找到映射代号的情况下
  637. // 导出招标、控制价时,基数为空
  638. // 导出投标时,基数=综合合价/费率
  639. if (illegal) {
  640. if (exportKind === EXPORT_KIND.Bid || exportKind === EXPORT_KIND.Control) {
  641. return '';
  642. } else {
  643. let totalFee = getFee(node.data.fees, 'common.totalFee'),
  644. feeRate = node.data.feeRate;
  645. return +feeRate ? scMathUtil.roundTo(totalFee / (feeRate / 100), -2) : totalFee
  646. }
  647. }
  648. return expr;
  649. }
  650. }
  651. // 转换基数说明,根据转换后的基数处理
  652. //1.行引用转换为对应行的名称
  653. //2.基数字典转换为中文
  654. function transformCalcBaseState(tenderDetail, expr, CalcStateMap) {
  655. if (!expr) {
  656. return '';
  657. }
  658. expr = String(expr);
  659. //提取基数
  660. let bases = expr.split(/[\+\-\*\/]/g);
  661. //提取操作符
  662. let oprs = expr.match(/[\+\-\*\/]/g);
  663. //转换后的基数
  664. let newBase = [];
  665. let illegal = false;
  666. for (let base of bases) {
  667. //行引用转换为名称.
  668. if (/F\d+/.test(base)) {
  669. let rowCode = base.match(/\d+/)[0],
  670. node = tenderDetail.mainTree.items[rowCode - 1];
  671. if (!node || !node.data.name) {
  672. illegal = true;
  673. break;
  674. }
  675. newBase.push(node && node.data.name ? node.data.name : '');
  676. } else if (CalcStateMap[base]) { //字典转换为中文
  677. newBase.push(CalcStateMap[base]);
  678. } else if (/^\d+(\.\d+)?$/.test(base)) { //金额
  679. newBase.push(base);
  680. } else {
  681. illegal = true;
  682. break;
  683. }
  684. }
  685. if (illegal) {
  686. return '';
  687. }
  688. let newExpr = '';
  689. for (let i = 0; i < newBase.length; i++) {
  690. newExpr += newBase[i];
  691. if (oprs && oprs[i]) {
  692. newExpr += oprs[i];
  693. }
  694. }
  695. return newExpr;
  696. }
  697. // 获取工程编号表格相关数据(导出需要弹出工程编号让用户选择)
  698. function getCodeSheetData(projectData) {
  699. let curCode = '0';
  700. let sheetData = [];
  701. sheetData.push(getObj(projectData));
  702. projectData.children.forEach(eng => {
  703. sheetData.push(getObj(eng));
  704. eng.children.forEach(tender => {
  705. sheetData.push(getObj(tender));
  706. });
  707. });
  708. //建设项目父ID设置为-1
  709. if (sheetData.length) {
  710. sheetData[0].ParentID = -1;
  711. sheetData[0].code = '';
  712. }
  713. return sheetData;
  714. function getObj(data) {
  715. return {
  716. collapsed: false,
  717. ID: data.ID,
  718. ParentID: data.ParentID,
  719. NextSiblingID: data.NextSiblingID,
  720. name: data.name,
  721. code: data.code || String(curCode++)
  722. };
  723. }
  724. }
  725. // 获取节点的某属性
  726. function getAttr(ele, name) {
  727. return (ele.attrs.find(attr => attr.name === name) || {}).value;
  728. }
  729. // 设置节点的某属性
  730. function setAttr(ele, name, value) {
  731. const attr = ele.attrs.find(attr => attr.name === name);
  732. if (attr) {
  733. attr.value = value;
  734. }
  735. }
  736. // 从srcEle节点中获取元素名为eleName的元素
  737. function getElementFromSrc(srcEle, eleName) {
  738. if (!srcEle || !srcEle.children || !srcEle.children.length) {
  739. return [];
  740. }
  741. return srcEle.children.filter(ele => ele.name === eleName);
  742. }
  743. /*
  744. * 设置完工程编号后,更新原始数据的工程编号
  745. * 更新原始数据前需要将编号里的特殊字符进行转换
  746. * @param {Array}exportData 提取出来的需要导出的数据
  747. * {Array}codes 工程编号表中填写的工程编号
  748. * {String}EngineeringName 单项工程元素的名称
  749. * {String}tenderName 单位工程元素的名称
  750. * {String}codeName 编号属性的名称
  751. * @return {void}
  752. * */
  753. function setupCode(exportData, codes, EngineeringName, tenderName, codeName) {
  754. // 转换xml实体字符
  755. let parsedCodes = getParsedData(codes);
  756. // 给导出数据里的单项工程、单位工程填上用户设置的工程编号
  757. exportData.forEach(orgData => {
  758. let curIdx = 0;
  759. let engs = getElementFromSrc(orgData.data, EngineeringName);
  760. engs.forEach(eng => {
  761. eng.attrs.find(attr => attr.name === codeName).value = parsedCodes[curIdx++];
  762. let tenders = getElementFromSrc(eng, tenderName);
  763. tenders.forEach(tender => {
  764. tender.attrs.find(attr => attr.name === codeName).value = parsedCodes[curIdx++];
  765. });
  766. });
  767. });
  768. }
  769. /**
  770. * 项目属性基本信息、工程特征的自检
  771. * 有错误的话,将先直接弹窗提示,不会进行其他的自检
  772. * */
  773. function propertyCheck() {
  774. const type = {
  775. info: '基本信息',
  776. feature: '工程特征'
  777. };
  778. const failList = ['<span style="font-weight: bold">建设项目下:</span>'];
  779. let haveHandledProjectData = false;
  780. const tenderDetailMap = _cache.tenderDetailMap;
  781. // 按照获取顺序serialNo(根据树结构)排序
  782. Object.values(tenderDetailMap)
  783. .sort((a, b) => a.serialNo - b.serialNo)
  784. .forEach(tenderDetail => {
  785. if (!haveHandledProjectData) {
  786. haveHandledProjectData = true;
  787. const basicInformation = tenderDetail.projectInfo.property.basicInformation || [];
  788. const infoFailList = itemsCheck(basicInformation, type.info);
  789. if (!infoFailList.length) {
  790. failList.splice(0, failList.length);
  791. } else {
  792. failList.push(...infoFailList);
  793. }
  794. }
  795. failList.push(`<span style="font-weight: bold">单位工程“${tenderDetail.projectInfo.name}”下:</span>`);
  796. const projectFeature = tenderDetail.projectInfo.property.projectFeature || [];
  797. const featureFailList = itemsCheck(projectFeature, type.feature);
  798. if (!featureFailList.length) {
  799. failList.splice(-1, 1);
  800. } else {
  801. failList.push(...featureFailList);
  802. }
  803. });
  804. return failList;
  805. // 条目检查
  806. function itemsCheck(items, type) {
  807. const requiredData = commonUtil.getRequired([], items);
  808. return requiredData
  809. .filter(item => commonUtil.isEmptyVal(item.value))
  810. .map(item => `${type}-“${item.dispName}”不能为空。`);
  811. }
  812. }
  813. /**
  814. * 弱自检自检,有错误非强制不可导的自检
  815. * failList里存的是导出规定属性时,不符合标准规定的错误,存在这种错误就不允许导出
  816. * 这里的错误是业务上的提示错误,与标准文件规定无关,存在这种错误是允许导出的
  817. * @return {Array} - 提示信息数组
  818. */
  819. function softCheck() {
  820. const tenderDetailMap = _cache.tenderDetailMap;
  821. // 检查清单综合单价是否大于最高限价,大于则提示
  822. function checkMaxPrice(tenderDetail) {
  823. return tenderDetail.mainTree.items
  824. .filter(node => calcTools.unitFeeGTMaxPrice(node, 'common.unitFee'))
  825. .map(node => {
  826. const code = node.data.code || '';
  827. const name = node.data.name || '';
  828. return `第${node.serialNo()}行“${code + name}”,清单综合单价 > 最高限价`;
  829. });
  830. }
  831. const infos = [];
  832. // 按照获取顺序serialNo(根据树结构)排序
  833. Object.values(tenderDetailMap)
  834. .sort((a, b) => a.serialNo - b.serialNo)
  835. .forEach(tenderDetail => {
  836. const maxPriceInfos = checkMaxPrice(tenderDetail);
  837. if (!maxPriceInfos.length) {
  838. return;
  839. }
  840. infos.push(`<span style="font-weight: bold">单位工程“${tenderDetail.projectInfo.name}”下:</span>`);
  841. infos.push(...maxPriceInfos);
  842. });
  843. return infos;
  844. }
  845. const UTIL = Object.freeze({
  846. deWeightHints,
  847. isDef,
  848. hasValue,
  849. setTimeoutSync,
  850. getFee,
  851. getAggregateFee,
  852. getFeeByFlag,
  853. getPlainAttrs,
  854. getValueByKey,
  855. getRelGLJ,
  856. generateHardwareId,
  857. arrayToObj,
  858. validDepth,
  859. checkUnique,
  860. sortByNext,
  861. getTenderDetail,
  862. getProjectByGranularity,
  863. getNormalBase,
  864. getIDBase,
  865. transformCalcBase,
  866. transformCalcBaseState,
  867. getCodeSheetData,
  868. getElementFromSrc,
  869. getAttr,
  870. setAttr,
  871. getParsedData,
  872. setupCode,
  873. propertyCheck,
  874. softCheck,
  875. });
  876. // 开始标签
  877. function _startTag(ele) {
  878. let rst = `<${ele.name}`;
  879. for (let attr of ele.attrs) {
  880. rst += ` ${attr.name}="${attr.value}"`;
  881. }
  882. rst += ele.children.length > 0 ? '>' : '/>';
  883. return rst;
  884. }
  885. // 结束标签
  886. function _endTag(ele) {
  887. return `</${ele.name}>`;
  888. }
  889. // 拼接成xml字符串
  890. function _toXMLStr(eles) {
  891. let rst = '';
  892. for (let ele of eles) {
  893. rst += _startTag(ele);
  894. if (ele.children.length > 0) {
  895. rst += _toXMLStr(ele.children);
  896. rst += _endTag(ele);
  897. }
  898. }
  899. return rst;
  900. }
  901. //格式化xml字符串
  902. function _formatXml(text) {
  903. //去掉多余的空格
  904. text = '\n' + text.replace(/>\s*?</g, ">\n<");
  905. //调整格式
  906. let rgx = /\n(<(([^\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/mg;
  907. let nodeStack = [];
  908. let output = text.replace(rgx, function ($0, all, name, isBegin, isCloseFull1, isCloseFull2, isFull1, isFull2) {
  909. let isClosed = (isCloseFull1 === '/') || (isCloseFull2 === '/') || (isFull1 === '/') || (isFull2 === '/');
  910. let prefix = '';
  911. if (isBegin === '!') {
  912. prefix = getPrefix(nodeStack.length);
  913. } else {
  914. if (isBegin !== '/') {
  915. prefix = getPrefix(nodeStack.length);
  916. if (!isClosed) {
  917. nodeStack.push(name);
  918. }
  919. } else {
  920. nodeStack.pop();
  921. prefix = getPrefix(nodeStack.length);
  922. }
  923. }
  924. let ret = '\n' + prefix + all;
  925. return ret;
  926. });
  927. let outputText = output.substring(1);
  928. return outputText;
  929. function getPrefix(prefixIndex) {
  930. let span = ' ';
  931. let output = [];
  932. for (let i = 0; i < prefixIndex; ++i) {
  933. output.push(span);
  934. }
  935. return output.join('');
  936. }
  937. }
  938. /*
  939. * 根据各自费用定额的文件结构,导出文件
  940. * 每个费用定额可能导出的结果文件都不同
  941. * 比如广东18需要将一个建设项目文件,多个单位工程文件打包成一个zip文件。重庆18就没这种要求
  942. * @param {Array}codes 工程编号数据
  943. * {Array}extractData 提取的数据
  944. * {Function}setCodeFunc 各自费用定额的导出前重设用户输入的工程编号方法
  945. * {Function}saveAsFunc 各自费用定额的导出方法
  946. * @return {void}
  947. * */
  948. async function exportFile(codes, extractData, setCodeFunc, saveAsFunc) {
  949. // 编号重置后将会被导出,需要将编号进行xml字符实体转换
  950. codes = getParsedData(codes);
  951. setCodeFunc(codes, extractData);
  952. // 获取文件数据
  953. let fileData = extractData.map(extractObj => {
  954. // 转换成xml字符串
  955. let xmlStr = _toXMLStr([extractObj.data]);
  956. // 加上xml声明
  957. xmlStr = `<?xml version="1.0" encoding="utf-8"?>${xmlStr}`;
  958. // 格式化
  959. xmlStr = _formatXml(xmlStr);
  960. let blob = new Blob([xmlStr], { type: 'text/plain;charset=utf-8' });
  961. return {
  962. blob: blob,
  963. exportKind: extractObj.exportKind,
  964. fileName: extractObj.fileName
  965. };
  966. });
  967. if (!saveAsFunc) {
  968. return fileData;
  969. }
  970. // 导出
  971. await saveAsFunc(fileData);
  972. }
  973. // 提取xml数据,中间数据(目前为导出指标压缩包用)
  974. async function getExtractData() {
  975. const fileKind = projectObj.project.projectInfo.property.fileKind;
  976. const projectID = projectObj.project.ID();
  977. const extractData = await extractExportData(XMLStandard.entry, GRANULARITY.PROJECT,
  978. XMLStandard.summaryObj, fileKind, projectID, userID);
  979. // projects表数据
  980. const projectData = _.cloneDeep(_cache.projectData);
  981. // 错误提示
  982. const failList = deWeightHints(_.cloneDeep(_cache.failList));
  983. // 清空缓存
  984. clear();
  985. return {
  986. extractData,
  987. projectData,
  988. failList
  989. }
  990. }
  991. /**
  992. * 提取qtf文件数据(目前为导出指标压缩包用)
  993. * @param {Array} codes - 设置工程编号窗口获取到的编号数据
  994. * @param {Array} extractData - 提取的导出中间数据
  995. * @return {Array}
  996. */
  997. async function getExtractFile(codes, extractData) {
  998. return await exportFile(codes, extractData, XMLStandard.resetContentCode);
  999. }
  1000. return {
  1001. CONFIG,
  1002. CACHE,
  1003. UTIL,
  1004. Element,
  1005. extractExportData,
  1006. exportFile,
  1007. getExtractData,
  1008. getExtractFile,
  1009. };
  1010. })();