base.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. /*
  2. * @Descripttion: 导入通用代码
  3. * @Author: vian
  4. * @Date: 2020-09-09 10:45:54
  5. */
  6. const INTERFACE_EXPORT_BASE = (() => {
  7. // xml字符实体
  8. const XMLEntity = {
  9. ' ': 'escape{space}',
  10. ' ': 'escape{simpleSpace}',
  11. '	': 'escape{tab}',
  12. '	': 'escape{simpleTab}',
  13. '
': 'escape{return}',
  14. '
': 'escape{simpleReturn}',
  15. '&#000A;': 'escape{newLine}',
  16. '
': 'escape{simpleNewLine}',
  17. '<': 'escape{less}',
  18. '>': 'escape{greater}',
  19. '&': 'escape{and}',
  20. '"': 'escape{quot}',
  21. ''': 'escape{apos}'
  22. };
  23. // 避免字符实体进行转义。原文本中含有xml字符实体,转换为其他字符。
  24. function escapeXMLEntity(str) {
  25. for (const [key, value] of Object.entries(XMLEntity)) {
  26. str = str.replace(new RegExp(key, 'g'), value);
  27. }
  28. return str;
  29. }
  30. // 将文本还原为字符实体
  31. function restoreXMLEntity(str) {
  32. for (const [key, value] of Object.entries(XMLEntity)) {
  33. str = str.replace(new RegExp(value, 'g'), key);
  34. }
  35. return str;
  36. }
  37. /*
  38. * 根据字段数组获得所要字段的值 eg: 要获取标段下的单项工程: ['标段', '单项工程'];
  39. * 属性需要加前缀:“_”
  40. * 节点的不需要加前缀
  41. * @param {Object}source 源数据
  42. * {Array}fields 字段数组
  43. * @return {String}
  44. * @example getValue(source, ['标段', '_文件类型'])
  45. * */
  46. function getValue(source, fields) {
  47. let cur = source;
  48. for (const field of fields) {
  49. if (!cur[field]) {
  50. return '';
  51. }
  52. cur = cur[field];
  53. }
  54. return cur || '';
  55. }
  56. // 获取布尔型的数据
  57. function getBool(source, fields) {
  58. return getValue(source, fields) === 'true' ? true : false;
  59. }
  60. // 获取数据类型
  61. function _plainType(v) {
  62. return Object.prototype.toString.call(v).slice(8, -1);
  63. }
  64. /*
  65. * 获取某字段的值,强制返回数组,防止一些错误。如果期待返回数组,可以用此方法。
  66. * @param {Object}source 数据源
  67. * {Array}fields 取的字段
  68. * @return {Array}
  69. * @example arrayValue(source, ['标段', '单项工程'])
  70. * */
  71. function arrayValue(source, fields) {
  72. let target = getValue(source, fields);
  73. if (_plainType(target) === 'Object') {
  74. target = [target];
  75. } else if (_plainType(target) !== 'Array') {
  76. target = []
  77. }
  78. return target;
  79. }
  80. // 获取费用
  81. function getFee(fees, fields) {
  82. if (!Array.isArray(fees) || !fees.length) {
  83. return '0';
  84. }
  85. const feeData = fees.find(fee => fee.fieldName === fields[0]);
  86. return feeData && feeData[fields[1]] || '0';
  87. }
  88. // 合并价格
  89. function mergeFees(feesA, feesB) {
  90. if (!feesA) {
  91. return feesB;
  92. }
  93. if (!feesB) {
  94. return [];
  95. }
  96. feesB.forEach(feeB => {
  97. const sameKindFee = feesA.find(feeA => feeA.fieldName === feeB.fieldName);
  98. if (sameKindFee) {
  99. Object.assign(sameKindFee, feeB);
  100. } else {
  101. feesA.push(feeB);
  102. }
  103. });
  104. return feesA;
  105. }
  106. // 将A对象的属性赋值到B对象上
  107. function assignAttr(target, source, attrs) {
  108. if (!source || !target) {
  109. return;
  110. }
  111. const sourceAttrs = attrs || Object.keys(source);
  112. for (const attr of sourceAttrs) {
  113. // 如果值是undefined,则不进行赋值覆盖处理
  114. if (attr === 'children' || source[attr] === undefined) {
  115. continue;
  116. }
  117. target[attr] = attr === 'fees'
  118. ? mergeFees(target[attr], source[attr]) // 如果是价格,不能简单地覆盖,要合并两个对象的价格
  119. : source[attr];
  120. }
  121. }
  122. // 获取固定ID
  123. function getFlag(data) {
  124. return data.flags && data.flags[0] && data.flags[0].flag || 0;
  125. }
  126. // 设置成树结构数据
  127. function setTreeData(data, parent, next) {
  128. const defalutID = -1;
  129. data.ID = uuid.v1();
  130. data.ParentID = parent && parent.ID || defalutID;
  131. data.NextSiblingID = next && next.ID || defalutID;
  132. }
  133. // 递归设置树结构数据,并返回设置好的数据,递归items数组
  134. function mergeDataRecur(parent, items) {
  135. const rst = [];
  136. for (let i = 0; i < items.length; i++) {
  137. const cur = items[i];
  138. const next = items[i + 1];
  139. setTreeData(cur, parent, next);
  140. rst.push(cur);
  141. if (cur.items && cur.items.length) {
  142. rst.push(...mergeDataRecur(cur, cur.items));
  143. }
  144. }
  145. return rst;
  146. }
  147. // 递归获取相关数据,(同层可以出现不同节点)
  148. // fields内字段的顺序即决定了提取数据类型的顺序,如fields = [['gruop'], ['item']],则提取的数据同层中group数据在item数据之前
  149. function extractItemsRecur(src, fields, extractFuc) {
  150. const rst = [];
  151. for (const field of fields) {
  152. const itemsSrc = arrayValue(src, field);
  153. if (itemsSrc.length) {
  154. /* const items = itemsSrc.map(itemSrc => {
  155. const obj = extractFuc(itemSrc, field[0]);
  156. obj.children = extractItemsRecur(itemSrc, fields, extractFuc);
  157. return obj;
  158. }); */
  159. const items = [];
  160. itemsSrc.forEach(itemSrc => {
  161. const obj = extractFuc(itemSrc, field[0]);
  162. if (obj) {
  163. obj.children = extractItemsRecur(itemSrc, fields, extractFuc);
  164. items.push(obj);
  165. }
  166. })
  167. rst.push(...items);
  168. }
  169. }
  170. return rst;
  171. }
  172. const UTIL = Object.freeze({
  173. escapeXMLEntity,
  174. restoreXMLEntity,
  175. getValue,
  176. getBool,
  177. arrayValue,
  178. getFee,
  179. mergeFees,
  180. assignAttr,
  181. setTreeData,
  182. mergeDataRecur,
  183. getFlag,
  184. extractItemsRecur,
  185. });
  186. /**
  187. * 合并基本信息或工程特征
  188. * @param {Array} source - 提取的数据
  189. * @param {Array} target - 模板数据
  190. * @return {Array}
  191. */
  192. function mergeInfo(source, target) {
  193. source.forEach(item => mergeChild(item, target));
  194. return target;
  195. function mergeChild(item, target) {
  196. for (const child of target) {
  197. if (child.key === item.key) {
  198. child.value = item.value;
  199. return true;
  200. }
  201. if (child.items && child.items.length) {
  202. const rst = mergeChild(item, child.items);
  203. if (rst) {
  204. return true;
  205. }
  206. }
  207. }
  208. return false;
  209. }
  210. }
  211. const { fixedFlag, BillType } = window.commonConstants;
  212. /**
  213. * 将提取出来的清单合并进清单模板
  214. * @param {Array} source - 从xml提取出来的清单
  215. * @param {Array} target - 清单模板数据
  216. * @param {Object} parent - 匹配到模板清单的父清单
  217. * @return {void}
  218. */
  219. function mergeBills(source, target, parent) {
  220. source.forEach(bills => {
  221. const simpleName = bills.name ? bills.name.replace(/\s/g, '') : '';
  222. let matched;
  223. if (!parent) {
  224. if (/100章至第700章|100章至700章|100章至第900章|100章至900章/.test(simpleName)) {
  225. matched = target.find(bills => getFlag(bills) === fixedFlag.ONE_SEVEN_BILLS);
  226. } else if (/包含在清单合计中的材料、工程设备、专业工程暂估/.test(simpleName)) {
  227. matched = target.find(bills => getFlag(bills) === fixedFlag.PROVISIONAL_TOTAL);
  228. } else if (/清单合计减去材料、工程设备、专业工程暂估价/.test(simpleName)) {
  229. matched = target.find(bills => getFlag(bills) === fixedFlag.BILLS_TOTAL_WT_PROV);
  230. } else if (/计日工合计/.test(simpleName)) {
  231. matched = target.find(bills => getFlag(bills) === fixedFlag.DAYWORK_LABOR);
  232. } else if (/暂列金额[((]不含/.test(simpleName)) {
  233. matched = target.find(bills => getFlag(bills) === fixedFlag.PROVISIONAL);
  234. } else if (/报价/.test(simpleName)) {
  235. matched = target.find(bills => getFlag(bills) === fixedFlag.TOTAL_COST);
  236. }
  237. } else {
  238. const parentSimpleName = parent.name ? parent.name.replace(/\s/g, '') : '';
  239. if (/100章至第700章|100章至700章|100章至第900章|100章至900章/.test(parentSimpleName) && /100章总则/.test(simpleName)) {
  240. matched = target.find(bills => getFlag(bills) === fixedFlag.ONE_HUNDRED_BILLS);
  241. } else if (/计日工合计/.test(parentSimpleName) && /劳务/.test(simpleName)) {
  242. matched = target.find(bills => getFlag(bills) === fixedFlag.LABOUR_SERVICE);
  243. } else if (/计日工合计/.test(parentSimpleName) && /材料/.test(simpleName)) {
  244. matched = target.find(bills => getFlag(bills) === fixedFlag.MATERIAL);
  245. } else if (/计日工合计/.test(parentSimpleName) && /机械/.test(simpleName)) {
  246. matched = target.find(bills => getFlag(bills) === fixedFlag.CONSTRUCTION_MACHINE);
  247. }
  248. }
  249. if (matched) {
  250. assignAttr(matched, bills);
  251. if (bills.children && bills.children.length) {
  252. mergeBills(bills.children, matched.children, matched);
  253. }
  254. } else {
  255. target.push(bills);
  256. }
  257. });
  258. }
  259. /**
  260. * 处理清单
  261. * @param {Array} tenderBills - 从xml提取出来的清单
  262. * @param {Array} billsTarget - 拷贝一份的模板清单,用于合并提取清单
  263. * @param {Number} tenderID - 单位工程ID
  264. * @return {Array}
  265. */
  266. function handleBills(tenderBills, billsTarget, tenderID) {
  267. const rst = [];
  268. // 将提取的清单数据合并进清单模板数据
  269. mergeBills(tenderBills, billsTarget, null);
  270. // 给清单设置数据
  271. const rowCodeData = []; // 行号数据,用于转换行引用
  272. const toBeTransformBills = []; // 待转换的清单
  273. function setBills(bills, parentID) {
  274. bills.forEach((child, index) => {
  275. rst.push(child);
  276. child.projectID = tenderID;
  277. // 如果本身清单就有ID,那不用处理,可以减少处理清单模板的一些ID引用问题
  278. if (!child.ID) {
  279. child.ID = uuid.v1();
  280. }
  281. child.ParentID = parentID;
  282. child.type = parentID === -1 ? BillType.DXFY : BillType.BILL;
  283. child.NextSiblingID = -1;
  284. const preChild = bills[index - 1];
  285. if (preChild) {
  286. preChild.NextSiblingID = child.ID;
  287. }
  288. if (child.rowCode) {
  289. const regStr = /{[^{}]+}/.test(child.rowCode) ? child.rowCode : `\\b${child.rowCode}\\b`;
  290. rowCodeData.push({ reg: new RegExp(regStr, 'g'), ID: child.ID, rowCode: child.rowCode });
  291. }
  292. if (child.calcBase) {
  293. toBeTransformBills.push(child);
  294. }
  295. if (child.children && child.children.length) {
  296. setBills(child.children, child.ID);
  297. }
  298. });
  299. }
  300. setBills(billsTarget, -1);
  301. // 转换计算基数,将行引用转换为ID引用
  302. toBeTransformBills.forEach(bills => {
  303. rowCodeData.forEach(({ reg, ID, rowCode }) => {
  304. const rowCodeReg = new RegExp(`{${rowCode}}`);
  305. if (rowCodeReg.test(bills.calcBase)) {
  306. bills.calcBase = bills.calcBase.replace(rowCodeReg, `@${ID}`);
  307. } else {
  308. bills.calcBase = bills.calcBase.replace(reg, `@${ID}`);
  309. }
  310. });
  311. });
  312. rst.forEach(bills => delete bills.children);
  313. return rst;
  314. }
  315. function getTemplateBillsTarget(templateBills) {
  316. const templateTarget = _.cloneDeep(templateBills);
  317. const ungroupedData = [];
  318. function getBillsFromChildren(billsData) {
  319. for (const bills of billsData) {
  320. ungroupedData.push(bills);
  321. if (bills.children && bills.children.length) {
  322. getBillsFromChildren(bills.children);
  323. }
  324. }
  325. }
  326. getBillsFromChildren(templateTarget);
  327. BILLS_UTIL.resetTreeData(ungroupedData, uuid.v1, true);
  328. return templateTarget;
  329. }
  330. // 处理单位工程数据
  331. function handleTenderData(tenders, templateData) {
  332. tenders.forEach((tender, index) => {
  333. tender.compilation = compilationData._id;
  334. tender.userID = userID;
  335. tender.ID = templateData.projectBeginID + index + 1;
  336. tender.ParentID = templateData.projectBeginID;
  337. tender.NextSiblingID = index === tenders.length - 1 ? -1 : templateData.projectBeginID + index + 2;
  338. tender.projType = projectType.tender;
  339. const featureTarget = _.cloneDeep(templateData.feature); // 必须拷贝出一份新数据,否则会被下一个单位工程覆盖
  340. const rationValuationData = JSON.parse(rationValuation)[0];
  341. const engineeringList = rationValuationData.engineering_list;
  342. const engineeringLib = engineeringList.find(item => item.lib.visible);
  343. if (!engineeringLib) {
  344. throw '不存在可用工程专业。';
  345. }
  346. const taxData = engineeringLib.lib.tax_group[0];
  347. const featureSource = [
  348. ...(tender.feature || []),
  349. { key: 'valuationType', value: '工程量清单' }, // 导入的时候以下项不一定有数据,但是需要自动生成
  350. { key: 'feeStandard', value: engineeringLib.lib.feeName },
  351. ];
  352. tender.property = {
  353. rootProjectID: tender.ParentID,
  354. region: '全省',
  355. engineering_id: engineeringLib.engineering_id,
  356. engineeringName: engineeringLib.lib.name,
  357. feeStandardName: engineeringLib.lib.feeName,
  358. engineering: engineeringLib.engineering,
  359. isInstall: engineeringLib.lib.isInstall,
  360. projectEngineering: engineeringLib.lib.projectEngineering,
  361. valuation: rationValuationData.id,
  362. valuationName: rationValuationData.name,
  363. valuationType: commonConstants.ValuationType.BOQ, // 必为工程量清单
  364. boqType: commonConstants.BOQType.BID_SUBMISSION, // 导入后必为投标
  365. taxType: taxData.taxType,
  366. projectFeature: mergeInfo(featureSource, featureTarget),
  367. featureLibID: engineeringLib.lib.feature_lib[0] && engineeringLib.lib.feature_lib[0].id || '',
  368. calcProgram: { name: taxData.program_lib.name, id: taxData.program_lib.id },
  369. colLibID: taxData.col_lib.id,
  370. templateLibID: taxData.template_lib.id,
  371. unitPriceFile: { name: tender.name, id: templateData.unitPriceFileBeginID + index }, // 新建单价文件
  372. feeFile: { name: tender.name, id: `newFeeRate@@${taxData.fee_lib.id}` } // 新建费率文件
  373. };
  374. delete tender.feature;
  375. const tenderDataBills = getTemplateBillsTarget(templateData.bills);
  376. tender.bills = handleBills(tender.bills, tenderDataBills, tender.ID,); // 必须要拷贝一份,否则多单位工程情况下,前单位工程的清单数据会被后单位工程的覆盖
  377. // 给暂估材料和评标材料设置项目数据
  378. const setGLJRefFunc = glj => {
  379. glj.ID = uuid.v1();
  380. glj.projectID = tender.ID;
  381. }
  382. if (tender.evaluationList && tender.evaluationList.length) {
  383. tender.evaluationList.forEach(setGLJRefFunc);
  384. }
  385. if (tender.bidEvaluationList && tender.bidEvaluationList.length) {
  386. tender.bidEvaluationList.forEach(setGLJRefFunc);
  387. }
  388. });
  389. }
  390. /**
  391. * 将接口中提取出来数据转换成可入库的有效数据
  392. * 因为无法保证这一套逻辑能不能兼容以后的所有接口,因此提取数据与标准数据模板的合并放在前端进行。
  393. * 当统一逻辑无法满足某一接口时,接口可以根据标准模板数据自行进行相关处理。
  394. * @param {Object} importData - 各接口从xml提取出来的数据
  395. * @return {Promise<Object>}
  396. */
  397. async function handleImportData(importData) {
  398. const valuationID = compilationData.ration_valuation[0].id;
  399. if (!Array.isArray(importData.tenders) && !importData.tenders.length) {
  400. throw '导入的文件中不存在有效的标段数据。';
  401. }
  402. const projectCount = 1 + importData.tenders.length;
  403. const feeName = compilationData.name === '安徽养护(2018)' ? '安徽养护' : '公路工程';
  404. const templateData = await ajaxPost('/pm/api/getImportTemplateData', { user_id: userID, valuationID, feeName, projectCount });
  405. if (!templateData) {
  406. throw '无法获取有效模板数据。';
  407. }
  408. console.log(templateData);
  409. // 处理建设项目数据
  410. // 确定建设项目的名称(不允许重复)
  411. const sameDepthProjs = getProjs(projTreeObj.tree.selected);
  412. const matchedProject = sameDepthProjs.find(node => node.data.name === importData.name);
  413. if (matchedProject) {
  414. importData.name += `(${moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')})`;
  415. }
  416. importData.compilation = compilationData._id;
  417. importData.userID = userID;
  418. importData.ID = templateData.projectBeginID;
  419. const { parentProjectID, preProjectID, nextProjectID } = projTreeObj.getRelProjectID(projTreeObj.tree.selected);
  420. importData.ParentID = parentProjectID;
  421. importData.preID = preProjectID;
  422. importData.NextSiblingID = nextProjectID;
  423. importData.projType = projectType.project;
  424. importData.property = {
  425. valuationType: commonConstants.ValuationType.BOQ, // 必为工程量清单
  426. boqType: commonConstants.BOQType.BID_SUBMISSION, // 导入后必为投标
  427. basicInformation: mergeInfo(importData.info, templateData.basicInfo) // 将提取的基本信息数据与标准基本信息数据进行合并(目前只赋值,没有匹配到的不追加)
  428. };
  429. delete importData.info;
  430. // 处理单位工程数据
  431. handleTenderData(importData.tenders, templateData);
  432. console.log(importData);
  433. }
  434. /*
  435. * 读取文件转换为utf-8编码的字符串
  436. * @param {Blob} file
  437. * @return {Promise}
  438. * */
  439. function readAsTextSync(file) {
  440. return new Promise((resolve, reject) => {
  441. const fr = new FileReader();
  442. fr.readAsText(file); // 默认utf-8,如果出现乱码,得看导入文件是什么编码
  443. fr.onload = function () {
  444. resolve(this.result);
  445. };
  446. fr.onerror = function () {
  447. reject('读取文件失败,请重试。');
  448. }
  449. });
  450. }
  451. /**
  452. *
  453. * @param {Function} entryFunc - 各导入接口提取导入数据方法
  454. * @param {File} file - 导入的文件
  455. * @param {String} areaKey - 地区标识,如:'安徽@马鞍山'
  456. * @param {Boolean} escape - 是否需要避免xml中的实体字符转换
  457. * @return {Promise<Object>}
  458. */
  459. async function extractImportData(entryFunc, file, areaKey, escape = false) {
  460. // 将二进制文件转换成字符串
  461. let xmlStr = await readAsTextSync(file);
  462. if (escape) {
  463. // x2js的str to json的实现方式基于DOMParser,DOMParser会自动将一些实体字符进行转换,比如 “&lt; to <”。如果不想进行自动转换,需要进行处理。
  464. xmlStr = escapeXMLEntity(xmlStr);
  465. }
  466. // 将xml格式良好的字符串转换成对象
  467. const x2js = new X2JS();
  468. let xmlObj = x2js.xml_str2json(xmlStr);
  469. xmlObj = JSON.parse(restoreXMLEntity(JSON.stringify(xmlObj)));
  470. console.log(xmlObj);
  471. if (!xmlObj) {
  472. throw '无有效数据。';
  473. }
  474. const importData = await entryFunc(areaKey, xmlObj);
  475. await handleImportData(importData);
  476. return importData;
  477. }
  478. return {
  479. UTIL,
  480. extractImportData,
  481. }
  482. })();