exportStdInterfaceBase.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. 'use strict';
  2. /**
  3. *
  4. *
  5. * @author Zhong
  6. * @date 2019/6/20
  7. * @version
  8. */
  9. const XML_EXPORT_BASE = (() => {
  10. // 属性类型
  11. const TYPE = {
  12. DATE: 1, //日期类型YYYY-MM-DD
  13. DATE_TIME: 2, //日期类型YYY-MM-DDTHH:mm:ss
  14. INT: 3, //整数类型
  15. DECIMAL: 4, //数值类型,不限制小数位数
  16. NUM2: 5, //数值类型2:最多两位小数
  17. BOOL: 6 //布尔型
  18. };
  19. // 空白字符处理
  20. const WHITE_SPACE = {
  21. COLLAPSE: 1 //移除所有空白字符(换行、回车、空格以及制表符会被替换为空格,开头和结尾的空格会被移除,而多个连续的空格会被缩减为一个单一的空格)
  22. };
  23. // 计税方法
  24. const TAX_TYPE = {
  25. '1': '一般计税法',
  26. '2': '简易计税法',
  27. };
  28. // 承包人材料调整类型
  29. const ADJUST_TYPE = {
  30. info: 'priceInfo', // 造价信息差额调整法
  31. coe: 'priceCoe' // 价格指数调整法
  32. };
  33. // 加载数据间隔,减少服务器压力
  34. const TIMEOUT_TIME = 500;
  35. // 导出粒度
  36. const GRANULARITY = {
  37. PROJECT: 1, //导出建设项目
  38. ENGINEERING: 2, //导出单项工程
  39. TENDER: 3 //导出单位工程
  40. };
  41. // 导出的文件类型选项
  42. const EXPORT_KIND = {
  43. Tender: 1, //投标
  44. Bid: 2, //招标
  45. Control: 3 //控制价
  46. };
  47. // 配置项
  48. const CONFIG = Object.freeze({
  49. TYPE,
  50. WHITE_SPACE,
  51. TAX_TYPE,
  52. ADJUST_TYPE,
  53. TIMEOUT_TIME,
  54. GRANULARITY,
  55. EXPORT_KIND
  56. });
  57. /*
  58. * 定义不设置一个Node方法统一进入的原因:模板化比较直观,不分开定义节点的话,调用传参也很麻烦而且不直观。
  59. * 一个节点对应一个构造方法,方便调整配置、方便其他版本开发、接手的人看起来更直观
  60. * @param {String}name 节点名
  61. * {Array}attrs 节点属性数据
  62. * {Array}failList 失败列表
  63. * @return {void}
  64. * */
  65. function Element(name, attrs, failList) {
  66. this.name = name;
  67. let checkData = check(name, attrs);
  68. this.fail = checkData.failHints;
  69. this.attrs = checkData.filterAttrs;
  70. this.children = [];
  71. failList.push(...this.fail);
  72. }
  73. /*
  74. * 检查
  75. * 创建节点时检查节点的数据
  76. * 1.长度限制minLen,maxLen
  77. * 2.值的限制,固定范围:enumeration
  78. * @param {String}eleName 节点名称
  79. * {Array}datas 需要检查的属性数据
  80. * @return {Object} failHints没通过的属性提示 filterAttrs过滤后的属性数据(失败提示在属性是必须的时候才提示,如果该属性失败了,但是是非必要属性,那么该属性不显示)
  81. * */
  82. function check(eleName, datas) {
  83. let rst = {failHints: [], filterAttrs: []};
  84. 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])))/;
  85. for (let data of datas) {
  86. data.value = typeof data.value === 'undefined' || data.value === null ? '' : String(data.value);
  87. if (data.whiteSpace && data.whiteSpace === WHITE_SPACE.COLLAPSE) { //处理空格相关
  88. data.value = data.value.replace(/[\r,\n,\t]/g, ' ');
  89. data.value = data.value.trim();
  90. data.value = data.value.replace(/\s{1,}/g, ' ');
  91. }
  92. if (!data.value && !data.minLen && !data.enumeration) { //值为空,且没有限制最小字符数,且没有限制值,则不需判断
  93. rst.filterAttrs.push(data);
  94. continue;
  95. }
  96. let isFail = false,
  97. tempFail = '';
  98. if (data.minLen && data.value.length < data.minLen){
  99. isFail = true;
  100. tempFail = data.failHint
  101. ? `${data.failHint}字符数不可小于${data.minLen}个。`
  102. :`${eleName}-“${data.name}”字符数不可小于${data.minLen}个。`;
  103. } else if (data.maxLen && data.value.length > data.maxLen) {
  104. isFail = true;
  105. tempFail = data.failHint
  106. ? `${data.failHint}字符数不可大于${data.maxLen}个。`
  107. : `${eleName}-“${data.name}”字符数不可大于${data.maxLen}个。`;
  108. } else if (data.enumeration && !data.enumeration.includes(data.value)) {
  109. isFail = true;
  110. let enumerationHint = data.enumerationHint
  111. ? data.enumerationHint.join(';')
  112. : data.enumeration.join(';');
  113. tempFail = data.failHint
  114. ? `${data.failHint}只能从“${enumerationHint}”中选择。`
  115. : `${eleName}-“${data.name}”只能从“${enumerationHint}”中选择。`;
  116. } else if (data.type && data.type === TYPE.DATE && !dateReg.test(data.value)) {
  117. isFail = true;
  118. tempFail = data.failHint
  119. ? `${data.failHint}日期格式必须是YYYY-MM-DD。`
  120. : `${eleName}-“${data.name}”日期格式必须是YYYY-MM-DD。`;
  121. } else if (data.type && data.type === TYPE.INT && !Number.isInteger(parseFloat(data.value))) {
  122. isFail = true;
  123. tempFail = data.failHint
  124. ? `${data.failHint}必须为整数。`
  125. : `${eleName}-“${data.name}”必须为整数。`;
  126. } else if (data.type && data.type === TYPE.DECIMAL && isNaN(parseFloat(data.value))) {
  127. isFail = true;
  128. tempFail = data.failHint
  129. ? `${data.failHint}必须为数值。`
  130. : `${eleName}-“${data.name}”必须为数值。`;
  131. } else if (data.type && data.type === TYPE.NUM2) {
  132. let v = parseFloat(data.value);
  133. if (isNaN(v)) {
  134. isFail = true;
  135. tempFail = data.failHint
  136. ? `${data.failHint}必须为数值。`
  137. : `${eleName}-“${data.name}”必须为数值。`;
  138. } else if (!data.value.length || (data.value.split('.').length > 1 && data.value.split('.')[1].length > 2)){
  139. isFail = true;
  140. }
  141. } else if (data.type && data.type === TYPE.BOOL && !['true', 'false'].includes(String(data.value))) {
  142. isFail = true;
  143. tempFail = data.failHint
  144. ? `${data.failHint}必须为true或false。`
  145. : `${eleName}-“${data.name}”必须为true或false。`;
  146. }
  147. if (!isFail || data.required) {
  148. rst.filterAttrs.push(data);
  149. }
  150. if (isFail && data.required && tempFail) {
  151. rst.failHints.push(tempFail);
  152. }
  153. }
  154. return rst;
  155. }
  156. // 等待一段时间
  157. function setTimeoutSync(handle, time) {
  158. return new Promise((resolve, reject) => {
  159. setTimeout(() => {
  160. if (handle && typeof handle === 'function') {
  161. handle();
  162. }
  163. resolve();
  164. }, time);
  165. });
  166. }
  167. function isDef(v) {
  168. return typeof v !== 'undefined' && v !== null;
  169. }
  170. /*
  171. * 从fees数组中获取相关费用
  172. * @param {Array}fees 费用数组
  173. * {String}feeFields 费用字段
  174. * @return {Number}
  175. * @example getFee(source.fees, 'common.totalFee')
  176. * */
  177. function getFee(fees, feeFields) {
  178. if (!Array.isArray(fees)) {
  179. return 0;
  180. }
  181. let fields = feeFields.split('.');
  182. let fee = fees.find(data => data.fieldName === fields[0]);
  183. if (!fee) {
  184. return 0;
  185. }
  186. return fee[fields[1]] || 0;
  187. }
  188. /*
  189. * 根据key获取对应的基本信息、工程特征数据
  190. * @param {Array}data
  191. * {String}key
  192. * @return {String}
  193. * @example getValueByKey(source.basicInformation, 'projectScale')
  194. * */
  195. function getValueByKey(data, key) {
  196. for (let d of data) {
  197. if (d.key === key) {
  198. return d.value;
  199. }
  200. if (d.items && d.items.length > 0) {
  201. let findData = d.items.find(x => x.key === key);
  202. if (findData) {
  203. return findData.value;
  204. }
  205. }
  206. }
  207. return '';
  208. }
  209. // 数组打平成对象
  210. function arrayToObj(arr) {
  211. let rst = {};
  212. for (let data of arr) {
  213. rst[data.key] = data.value;
  214. }
  215. return rst;
  216. }
  217. //检测层数
  218. //@param {Number}maxDepth(最大深度) {Object}node(需要检测的清单树节点)
  219. //@return {Boolean}
  220. function validDepth(maxDepth, node) {
  221. let nodeDepth = node.depth();
  222. let allNodes = node.getPosterity();
  223. //检测相对深度
  224. for (let n of allNodes) {
  225. let relativeDepth = n.depth() - nodeDepth;
  226. if (relativeDepth > maxDepth) {
  227. return false;
  228. }
  229. }
  230. return true;
  231. }
  232. //检测唯一性
  233. //@param {Object}constraints(约束池) {All}data(检测的数据) {String}hint(提示已存在的内容) {String}subHint(额外提示,有额外提示时,不用data提示)
  234. //@return {void}
  235. function checkUnique(constraints, failList, data, hint, subHint) {
  236. if (constraints.includes(data)) {
  237. let failHint = subHint
  238. ? `${hint}“${subHint}”已存在`
  239. : `${hint}“${data}”已存在`;
  240. failList.push(failHint);
  241. } else if (data) {
  242. constraints.push(data);
  243. }
  244. }
  245. //根据数据的NextSiblingID进行排序,返回排序后的数组
  246. function sortByNext(datas) {
  247. let target = [],
  248. temp = {};
  249. for (let data of datas) {
  250. temp[data.ID] = {me: data, next: null, prev: null};
  251. }
  252. for (let data of datas) {
  253. let next = temp[data.NextSiblingID] || null;
  254. temp[data.ID].next = next;
  255. if (next) {
  256. next.prev = temp[data.ID];
  257. }
  258. }
  259. let first = null;
  260. for (let data of datas) {
  261. let me = temp[data.ID];
  262. if (!me.prev) {
  263. first = me;
  264. }
  265. }
  266. if (!first) {
  267. return datas;
  268. }
  269. while (first) {
  270. target.push(first.me);
  271. first = first.next;
  272. }
  273. return target;
  274. }
  275. //转换基数表达式
  276. //1.有子项,则取固定清单对应基数
  277. //2.无子项,有基数,a.优先转换为行代号(不可自身) b.不能转换为行代号则找对应字典
  278. //3.基数中有无法转换的,设为金额
  279. function transformCalcBase(tenderDetail, node, {CalcBaseMap, FlagCalcBaseMap}) {
  280. let expr = node.data.calcBase || '';
  281. if (node.children.length) {
  282. let flag = node.getFlag();
  283. return FlagCalcBaseMap[flag] || '';
  284. }
  285. if (expr) {
  286. let illegal = false;
  287. let normalBase = _getNormalBase(expr),
  288. idBase = _getIDBase(expr);
  289. //普通基数转基数字典
  290. normalBase.forEach(base => {
  291. let replaceStr = CalcBaseMap[base];
  292. //转换成行代号的优先级比较高,进行清单匹配
  293. let flag = FlagCalcBaseMap[base];
  294. if (flag) {
  295. let flagNode = tenderDetail.mainTree.items.find(mNode => mNode.getFlag() === flag);
  296. //匹配到了 普通基数转换成行引用
  297. if (flagNode) {
  298. replaceStr = `F${flagNode.serialNo() + 1}`;
  299. }
  300. }
  301. //存在无法处理的基数
  302. if (!replaceStr) {
  303. illegal = true;
  304. return;
  305. }
  306. expr = expr.replace(new RegExp(base, 'g'), replaceStr);
  307. });
  308. //id引用转行代号引用
  309. idBase.forEach(base => {
  310. let id = base.match(/[^@]+/)[0];
  311. let theNode = tenderDetail.mainTree.getNodeByID(id),
  312. rowCode = theNode ? `F${theNode.serialNo() + 1}` : '';
  313. if (!rowCode) {
  314. illegal = true;
  315. return;
  316. }
  317. expr = expr.replace(new RegExp(base, 'g'), rowCode);
  318. });
  319. //不合法 返回金额
  320. if (illegal) {
  321. return getFee(node.data.fees, 'common.totalFee');
  322. }
  323. return expr;
  324. }
  325. //获取普通基数: {xxx}
  326. function _getNormalBase(str) {
  327. let reg = /{.+?}/g,
  328. matchs = str.match(reg);
  329. return matchs || [];
  330. }
  331. //获取id引用基数: @xxx-xxx-xx
  332. function _getIDBase(str) {
  333. let reg = /@.{36}/g,
  334. matchs = str.match(reg);
  335. return matchs || [];
  336. }
  337. }
  338. //转换基数说明,根据转换后的基数处理
  339. //1.行引用转换为对应行的名称
  340. //2.基数字典转换为中文
  341. function transformCalcBaseState(tenderDetail, expr, CalcStateMap) {
  342. if (!expr) {
  343. return '';
  344. }
  345. expr = String(expr);
  346. //提取基数
  347. let bases = expr.split(/[\+\-\*\/]/g);
  348. //提取操作符
  349. let oprs = expr.match(/[\+\-\*\/]/g);
  350. //转换后的基数
  351. let newBase = [];
  352. let illegal = false;
  353. for (let base of bases) {
  354. //行引用转换为名称
  355. if (/F\d+/.test(base)) {
  356. let rowCode = base.match(/\d+/)[0],
  357. node = tenderDetail.mainTree.items[rowCode - 1];
  358. if (!node || !node.data.name) {
  359. illegal = true;
  360. break;
  361. }
  362. newBase.push(node && node.data.name ? node.data.name : '');
  363. } else if (CalcStateMap[base]){ //字典转换为中文
  364. newBase.push(CalcStateMap[base]);
  365. } else if (/^\d+(\.\d+)?$/.test(base)) { //金额
  366. newBase.push(base);
  367. } else {
  368. illegal = true;
  369. break;
  370. }
  371. }
  372. if (illegal) {
  373. return '';
  374. }
  375. let newExpr = '';
  376. for (let i = 0; i < newBase.length; i++) {
  377. newExpr += newBase[i];
  378. if (oprs && oprs[i]) {
  379. newExpr += oprs[i];
  380. }
  381. }
  382. return newExpr;
  383. }
  384. // 获取工程编号表格相关数据(导出需要弹出工程编号让用户选择)
  385. function getCodeSheetData(PMData) {
  386. let curCode = '0';
  387. let sheetData = [];
  388. sheetData.push(getObj(PMData));
  389. PMData.children.forEach(eng => {
  390. sheetData.push(getObj(eng));
  391. eng.children.forEach(tender => {
  392. sheetData.push(getObj(tender));
  393. });
  394. });
  395. //建设项目父ID设置为-1
  396. if (sheetData.length) {
  397. sheetData[0].ParentID = -1;
  398. sheetData[0].code = '';
  399. }
  400. return sheetData;
  401. function getObj(data) {
  402. return {
  403. collapsed: false,
  404. ID: data.ID,
  405. ParentID: data.ParentID,
  406. NextSiblingID: data.NextSiblingID,
  407. name: data.name,
  408. code: data.code || String(curCode++)
  409. };
  410. }
  411. }
  412. // 从srcNode节点中获取为target实例的节点
  413. function _getNodeFromSrc(srcNode, target) {
  414. if (!srcNode || !srcNode.children || !srcNode.children.length) {
  415. return [];
  416. }
  417. return srcNode.children.filter(node => node instanceof target);
  418. }
  419. // 设置完工程编号后,更新原始数据的工程编号
  420. function setupCode(originalDatas, codeDatas, {Tender, Engineering}) {
  421. originalDatas.forEach(orgData => {
  422. let curIdx = 0;
  423. let engs = _getNodeFromSrc(orgData.data, Engineering);
  424. engs.forEach(eng => {
  425. eng.attrs.find(attr => attr.name === '编号').value = codeDatas[curIdx++];
  426. let tenders = _getNodeFromSrc(eng, Tender);
  427. tenders.forEach(tender => {
  428. tender.attrs.find(attr => attr.name === '编号').value = codeDatas[curIdx++];
  429. });
  430. });
  431. });
  432. }
  433. const UTIL = Object.freeze({
  434. isDef,
  435. setTimeoutSync,
  436. getFee,
  437. getValueByKey,
  438. arrayToObj,
  439. validDepth,
  440. checkUnique,
  441. sortByNext,
  442. transformCalcBase,
  443. transformCalcBaseState,
  444. getCodeSheetData,
  445. setupCode
  446. });
  447. // 开始标签
  448. function _startTag(ele) {
  449. let rst = `<${ele.name}`;
  450. for (let attr of ele.attrs) {
  451. rst += ` ${attr.name}="${attr.value}"`;
  452. }
  453. rst += ele.children.length > 0 ? '>' : '/>';
  454. return rst;
  455. }
  456. // 结束标签
  457. function _endTag(ele) {
  458. return `</${ele.name}>`;
  459. }
  460. // 拼接成xml字符串
  461. function _toXMLStr(eles) {
  462. let rst = '';
  463. for (let ele of eles) {
  464. rst += _startTag(ele);
  465. if (ele.children.length > 0) {
  466. rst += _toXMLStr(ele.children);
  467. rst += _endTag(ele);
  468. }
  469. }
  470. return rst;
  471. }
  472. //格式化xml字符串
  473. function _formatXml(text) {
  474. //去掉多余的空格
  475. text = '\n' + text.replace(/>\s*?</g, ">\n<");
  476. //调整格式
  477. let rgx = /\n(<(([^\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/mg;
  478. let nodeStack = [];
  479. let output = text.replace(rgx, function($0, all, name, isBegin, isCloseFull1, isCloseFull2, isFull1, isFull2){
  480. let isClosed = (isCloseFull1 === '/') || (isCloseFull2 === '/' ) || (isFull1 === '/') || (isFull2 === '/');
  481. let prefix = '';
  482. if (isBegin === '!') {
  483. prefix = getPrefix(nodeStack.length);
  484. } else {
  485. if (isBegin !== '/') {
  486. prefix = getPrefix(nodeStack.length);
  487. if (!isClosed) {
  488. nodeStack.push(name);
  489. }
  490. } else {
  491. nodeStack.pop();
  492. prefix = getPrefix(nodeStack.length);
  493. }
  494. }
  495. let ret = '\n' + prefix + all;
  496. return ret;
  497. });
  498. let outputText = output.substring(1);
  499. return outputText;
  500. function getPrefix(prefixIndex) {
  501. let span = ' ';
  502. let output = [];
  503. for (let i = 0 ; i < prefixIndex; ++i) {
  504. output.push(span);
  505. }
  506. return output.join('');
  507. }
  508. }
  509. // 将数据转换为xml文件并导出
  510. async function exportFile(originalDatas) {
  511. let fileDatas = [];
  512. //源数据转换成blob
  513. for (let orgData of originalDatas) {
  514. //转换成xml字符串
  515. let xmlStr = _toXMLStr([orgData.data]);
  516. //加上xml声明
  517. xmlStr = `<?xml version="1.0" encoding="utf-8"?>${xmlStr}`;
  518. //格式化
  519. xmlStr = _formatXml(xmlStr);
  520. let blob = new Blob([xmlStr], {type: 'text/plain;charset=utf-8'});
  521. fileDatas.push({blob: blob, fileName: orgData.fileName});
  522. }
  523. //导出文件
  524. if (fileDatas.length === 1) {
  525. saveAs(fileDatas[0].blob, fileDatas[0].fileName);
  526. } else if (fileDatas.length > 1) { //导出压缩包
  527. let zip = new JSZip();
  528. for (let file of fileDatas) {
  529. zip.file(file.fileName, file.blob, {binary: true});
  530. }
  531. let zipFile = await zip.generateAsync({type: 'blob'});
  532. saveAs(zipFile, '重庆标准交换数据.zip');
  533. }
  534. }
  535. return {
  536. CONFIG,
  537. UTIL,
  538. Element,
  539. exportFile
  540. };
  541. })();