/* * @Descripttion: 导入通用代码 * @Author: vian * @Date: 2020-09-09 10:45:54 */ const XML_RN_STR = "@-@"; const INTERFACE_EXPORT_BASE = (() => { // xml字符实体 const XMLEntity = { " ": "escape{space}", " ": "escape{simpleSpace}", " ": "escape{tab}", " ": "escape{simpleTab}", " ": "escape{return}", " ": "escape{simpleReturn}", "�A;": "escape{newLine}", " ": "escape{simpleNewLine}", "<": "escape{less}", ">": "escape{greater}", "&": "escape{and}", """: "escape{quot}", "'": "escape{apos}", }; // 避免字符实体进行转义。原文本中含有xml字符实体,转换为其他字符。 function escapeXMLEntity(str) { for (const [key, value] of Object.entries(XMLEntity)) { str = str.replace(new RegExp(key, "g"), value); } return str; } // 将文本还原为字符实体 function restoreXMLEntity(str) { for (const [key, value] of Object.entries(XMLEntity)) { str = str.replace(new RegExp(value, "g"), key); } return str; } /* * 根据字段数组获得所要字段的值 eg: 要获取标段下的单项工程: ['标段', '单项工程']; * 属性需要加前缀:“_” * 节点的不需要加前缀 * @param {Object}source 源数据 * {Array}fields 字段数组 * @return {String} * @example getValue(source, ['标段', '_文件类型']) * */ function getValue(source, fields) { let cur = source; for (const field of fields) { if (!cur[field]) { return ""; } cur = cur[field]; } if (typeof cur === "string") { cur = cur.replace(new RegExp(XML_RN_STR, "g"), "\r\n"); } return cur || ""; } // 获取布尔型的数据 function getBool(source, fields) { return getValue(source, fields) === "true" ? true : false; } // 获取数据类型 function _plainType(v) { return Object.prototype.toString.call(v).slice(8, -1); } /* * 获取某字段的值,强制返回数组,防止一些错误。如果期待返回数组,可以用此方法。 * @param {Object}source 数据源 * {Array}fields 取的字段 * @return {Array} * @example arrayValue(source, ['标段', '单项工程']) * */ function arrayValue(source, fields) { let target = getValue(source, fields); if (_plainType(target) === "Object") { target = [target]; } else if (_plainType(target) !== "Array") { target = []; } return target; } // 获取费用 function getFee(fees, fields) { if (!Array.isArray(fees) || !fees.length) { return "0"; } const feeData = fees.find((fee) => fee.fieldName === fields[0]); return (feeData && feeData[fields[1]]) || "0"; } // 合并价格 function mergeFees(feesA, feesB) { if (!feesA) { return feesB; } if (!feesB) { return []; } feesB.forEach((feeB) => { const sameKindFee = feesA.find((feeA) => feeA.fieldName === feeB.fieldName); if (sameKindFee) { Object.assign(sameKindFee, feeB); } else { feesA.push(feeB); } }); return feesA; } // 将A对象的属性赋值到B对象上 function assignAttr(target, source, attrs) { if (!source || !target) { return; } const sourceAttrs = attrs || Object.keys(source); for (const attr of sourceAttrs) { // 如果值是undefined,则不进行赋值覆盖处理 if (attr === "children" || source[attr] === undefined) { continue; } target[attr] = attr === "fees" ? mergeFees(target[attr], source[attr]) // 如果是价格,不能简单地覆盖,要合并两个对象的价格 : source[attr]; } } // 获取固定ID function getFlag(data) { return (data.flags && data.flags[0] && data.flags[0].flag) || 0; } // 设置成树结构数据 function setTreeData(data, parent, next) { const defalutID = -1; data.ID = uuid.v1(); data.ParentID = (parent && parent.ID) || defalutID; data.NextSiblingID = (next && next.ID) || defalutID; } // 递归设置树结构数据,并返回设置好的数据,递归items数组 function mergeDataRecur(parent, items) { const rst = []; for (let i = 0; i < items.length; i++) { const cur = items[i]; const next = items[i + 1]; setTreeData(cur, parent, next); rst.push(cur); if (cur.items && cur.items.length) { rst.push(...mergeDataRecur(cur, cur.items)); } } return rst; } // 递归获取相关数据,(同层可以出现不同节点) // fields内字段的顺序即决定了提取数据类型的顺序,如fields = [['gruop'], ['item']],则提取的数据同层中group数据在item数据之前 function extractItemsRecur(src, fields, extractFuc) { const rst = []; for (const field of fields) { const itemsSrc = arrayValue(src, field); if (itemsSrc.length) { /* const items = itemsSrc.map(itemSrc => { const obj = extractFuc(itemSrc, field[0]); obj.children = extractItemsRecur(itemSrc, fields, extractFuc); return obj; }); */ const items = []; itemsSrc.forEach((itemSrc) => { const obj = extractFuc(itemSrc, field[0]); if (obj) { if (!obj.children || obj.children.length === 0) { obj.children = extractItemsRecur(itemSrc, fields, extractFuc); } items.push(obj); } }); rst.push(...items); } } return rst; } const UTIL = Object.freeze({ escapeXMLEntity, restoreXMLEntity, getValue, getBool, arrayValue, getFee, mergeFees, assignAttr, setTreeData, mergeDataRecur, getFlag, extractItemsRecur, }); /** * 合并基本信息或工程特征 * @param {Array} source - 提取的数据 * @param {Array} target - 模板数据 * @return {Array} */ function mergeInfo(source, target) { source.forEach((item) => mergeChild(item, target)); return target; function mergeChild(item, target) { for (const child of target) { if (child.key === item.key) { child.value = item.value; return true; } if (child.items && child.items.length) { const rst = mergeChild(item, child.items); if (rst) { return true; } } } return false; } } const { fixedFlag, BillType } = window.commonConstants; // 标题类别 - flag 映射 const titleTypeToFlag = { 1: fixedFlag.ONE_SEVEN_BILLS, 2: fixedFlag.PROVISIONAL_TOTAL, 3: fixedFlag.BILLS_TOTAL_WT_PROV, 4: fixedFlag.DAYWORK_LABOR, 5: fixedFlag.PROVISIONAL, 6: fixedFlag.TOTAL_COST, }; /** * 将提取出来的清单合并进清单模板 * @param {Array} source - 从xml提取出来的清单 * @param {Array} target - 清单模板数据 * @param {Object} parent - 匹配到模板清单的父清单 * @param {Boolean} onlyImportMatchBills - 是否只导入文件中的清单,未匹配的模板清单不生成 * @return {void} */ function mergeBills(source, target, parent, matchedFlags) { source.forEach((bills, index) => { // 为了大项费用排序 if (!parent) { bills.seq = index; } const simpleName = bills.name ? bills.name.replace(/\s/g, "") : ""; const titleType = bills.titleType || "0"; let matched; if (!parent) { if (titleTypeToFlag[titleType] === fixedFlag.ONE_SEVEN_BILLS || /100章.*章|100章.*700章|100章.*900章/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.ONE_SEVEN_BILLS); } else if (titleTypeToFlag[titleType] === fixedFlag.PROVISIONAL_TOTAL || /包含在清单合计中的材料、工程设备、专业工程暂估/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.PROVISIONAL_TOTAL); } else if (titleTypeToFlag[titleType] === fixedFlag.BILLS_TOTAL_WT_PROV || /清单合计减去材料、工程设备、专业工程暂估价/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.BILLS_TOTAL_WT_PROV); } else if (titleTypeToFlag[titleType] === fixedFlag.DAYWORK_LABOR || /计日工合计/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.DAYWORK_LABOR); } else if (titleTypeToFlag[titleType] === fixedFlag.PROVISIONAL || /暂列金额[((]不含/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.PROVISIONAL); } else if (titleTypeToFlag[titleType] === fixedFlag.TOTAL_COST || /报价/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.TOTAL_COST); } } else { const parentSimpleName = parent.name ? parent.name.replace(/\s/g, "") : ""; if (/100章.*章|100章.*700章|100章.*900章/.test(parentSimpleName) && /100章总则/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.ONE_HUNDRED_BILLS); } else if (/计日工合计/.test(parentSimpleName) && /劳务/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.LABOUR_SERVICE); } else if (/计日工合计/.test(parentSimpleName) && /材料/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.MATERIAL); } else if (/计日工合计/.test(parentSimpleName) && /机械/.test(simpleName)) { matched = target.find((bills) => getFlag(bills) === fixedFlag.CONSTRUCTION_MACHINE); } } if (matched) { matchedFlags.push(getFlag(matched)); assignAttr(matched, bills); if (bills.children && bills.children.length) { mergeBills(bills.children, matched.children, matched, matchedFlags); } } else { target.push(bills); } }); } /** * 处理清单 * @param {Array} tenderBills - 从xml提取出来的清单 * @param {Array} billsTarget - 拷贝一份的模板清单,用于合并提取清单 * @param {Number} tenderID - 单位工程ID * @param {Boolean} onlyImportMatchBills - 是否只导入文件中的清单,未匹配的模板清单不生成 * @return {Array} */ function handleBills(tenderBills, billsTarget, tenderID, onlyImportMatchBills) { const rst = []; // 将提取的清单数据合并进清单模板数据 const matchedFlags = []; mergeBills(tenderBills, billsTarget, null, matchedFlags); // 如果只导入文件中有的清单,除100-700/900章清单、报价清单外,其他未匹配到的固定清单模板,均不导入 function removeNotMatchFlagBills(bills) { const rst = []; bills.forEach((item) => { const flag = getFlag(item); if (!flag || [fixedFlag.ONE_SEVEN_BILLS, fixedFlag.TOTAL_COST].includes(flag) || matchedFlags.includes(flag)) { rst.push(item); } if (flag && ![fixedFlag.ONE_SEVEN_BILLS, fixedFlag.TOTAL_COST].includes(flag) && item.children && item.children.length) { item.children = removeNotMatchFlagBills(item.children); } }); return rst; } if (onlyImportMatchBills) { billsTarget = removeNotMatchFlagBills(billsTarget); } // 大项费用按照清单标题排序 billsTarget.sort((a, b) => a.seq - b.seq); // 给清单设置数据 const rowCodeData = []; // 行号数据,用于转换行引用 const toBeTransformBills = []; // 待转换的清单 function setBills(bills, parentID) { bills.forEach((child, index) => { rst.push(child); child.projectID = tenderID; // 如果本身清单就有ID,那不用处理,可以减少处理清单模板的一些ID引用问题 if (!child.ID) { child.ID = uuid.v1(); } if (child.quantity) { child.quantityEXP = child.quantity; } child.ParentID = parentID; child.type = parentID === -1 ? BillType.DXFY : BillType.BILL; child.NextSiblingID = -1; const preChild = bills[index - 1]; if (preChild) { preChild.NextSiblingID = child.ID; } if (child.rowCode) { const regStr = /{[^{}]+}/.test(child.rowCode) ? child.rowCode : `\\b${child.rowCode}\\b`; rowCodeData.push({ reg: new RegExp(regStr, "g"), ID: child.ID, rowCode: child.rowCode }); } if (child.tempCalcBase) { toBeTransformBills.push(child); } if (child.children && child.children.length) { setBills(child.children, child.ID); } }); } setBills(billsTarget, -1); // 转换计算基数 toBeTransformBills.forEach((bills) => { rowCodeData.forEach(({ reg, ID, rowCode }) => { const rowCodeReg = new RegExp(`{${rowCode}}`); // 替换行号 if (rowCodeReg.test(bills.tempCalcBase)) { bills.tempCalcBase = bills.tempCalcBase.replace(rowCodeReg, `@${ID}`); } else { bills.tempCalcBase = bills.tempCalcBase.replace(reg, `@${ID}`); } // 替换基数,防止其他公司的软件导出没有{} bills.tempCalcBase = bills.tempCalcBase.replace(/.?专项暂定合计.?/g, "{专项暂定合计}"); bills.tempCalcBase = bills.tempCalcBase.replace(/.?各章清单合计.?/g, "{各章清单合计}"); }); /* 检查基数有效性,无效则使用模板的基数 */ // 消除ID引用对基数分割的影响 const IDs = bills.tempCalcBase.match(/@[\da-zA-Z-]{36}/g) || []; const bases = [...IDs]; const str = bills.tempCalcBase.replace(/@[\da-zA-Z-]{36}/g, ""); const otherBases = str.split(/[+-/*]/).filter((item) => !!item); bases.push(...otherBases); // 判定基数有效性 const isValid = bases.every((base) => { if (base === "{专项暂定合计}" || base === "{各章清单合计}") { return true; } if (/^(\d+(\.\d+)?)%?$/.test(base)) { // 数值+%(可有可无) return true; } if (/[\da-zA-Z-]{36}/.test(base)) { // ID引用 return true; } return false; }); if (isValid) { bills.calcBase = bills.tempCalcBase; } }); rst.forEach((bills) => delete bills.children); return rst; } function getTemplateBillsTarget(templateBills) { const templateTarget = _.cloneDeep(templateBills); const ungroupedData = []; function getBillsFromChildren(billsData) { for (const bills of billsData) { ungroupedData.push(bills); if (bills.children && bills.children.length) { getBillsFromChildren(bills.children); } } } getBillsFromChildren(templateTarget); BILLS_UTIL.resetTreeData(ungroupedData, uuid.v1, true); return templateTarget; } // 处理单位工程数据 function handleTenderData(tenders, templateData, rationValuationData, engineeringLib, areaKey, onlyImportMatchBills) { tenders.forEach((tender, index) => { tender.compilation = compilationData._id; tender.userID = userID; tender.ID = templateData.projectBeginID + index + 1; tender.ParentID = templateData.projectBeginID; tender.NextSiblingID = index === tenders.length - 1 ? -1 : templateData.projectBeginID + index + 2; tender.projType = projectType.tender; const featureTarget = _.cloneDeep(templateData.feature); // 必须拷贝出一份新数据,否则会被下一个单位工程覆盖 if (!engineeringLib) { throw "不存在可用工程专业。"; } const taxData = engineeringLib.lib.tax_group[0]; const featureSource = [ ...(tender.feature || []), { key: "valuationType", value: "工程量清单" }, // 导入的时候以下项不一定有数据,但是需要自动生成 { key: "feeStandard", value: engineeringLib.lib.feeName }, ]; const needEngineering = featureTarget && featureTarget.find((item) => item.key === "engineering"); if (needEngineering) { featureSource.push({ key: "engineering", value: engineeringLib.lib.name }); } tender.property = { areaKey, rootProjectID: tender.ParentID, region: "全省", engineering_id: engineeringLib.engineering_id, engineeringName: engineeringLib.lib.name, feeStandardName: engineeringLib.lib.feeName, engineering: engineeringLib.engineering, isInstall: engineeringLib.lib.isInstall, projectEngineering: engineeringLib.lib.projectEngineering, valuation: rationValuationData.id, valuationName: rationValuationData.name, valuationType: commonConstants.ValuationType.BOQ, // 必为工程量清单 boqType: commonConstants.BOQType.BID_SUBMISSION, // 导入后必为投标 taxType: taxData.taxType, projectFeature: mergeInfo(featureSource, featureTarget), featureLibID: (engineeringLib.lib.feature_lib[0] && engineeringLib.lib.feature_lib[0].id) || "", calcProgram: { name: taxData.program_lib.name, id: taxData.program_lib.id }, colLibID: taxData.col_lib.id, templateLibID: taxData.template_lib.id, unitPriceFile: { name: tender.name, id: templateData.unitPriceFileBeginID + index }, // 新建单价文件 feeFile: { name: tender.name, id: `newFeeRate@@${taxData.fee_lib.id}` }, // 新建费率文件 }; delete tender.feature; const tenderDataBills = getTemplateBillsTarget(templateData.bills); tender.bills = handleBills(tender.bills, tenderDataBills, tender.ID, onlyImportMatchBills); // 必须要拷贝一份,否则多单位工程情况下,前单位工程的清单数据会被后单位工程的覆盖 // 给暂估材料和评标材料设置项目数据 const setGLJRefFunc = (glj) => { glj.ID = uuid.v1(); glj.projectID = tender.ID; }; if (tender.evaluationList && tender.evaluationList.length) { tender.evaluationList.forEach(setGLJRefFunc); } if (tender.bidEvaluationList && tender.bidEvaluationList.length) { tender.bidEvaluationList.forEach(setGLJRefFunc); } }); } /** * 将接口中提取出来数据转换成可入库的有效数据 * 因为无法保证这一套逻辑能不能兼容以后的所有接口,因此提取数据与标准数据模板的合并放在前端进行。 * 当统一逻辑无法满足某一接口时,接口可以根据标准模板数据自行进行相关处理。 * @param {Object} importData - 各接口从xml提取出来的数据 * @param {String} areaKey - 接口地区 * @param {Boolean} onlyImportMatchBills - 是否只导入文件中的清单,未匹配的模板清单不生成 * @return {Promise} */ async function handleImportData(importData, areaKey, onlyImportMatchBills) { debugger; const valuationID = compilationData.ration_valuation[0].id; if (!Array.isArray(importData.tenders) && !importData.tenders.length) { throw "导入的文件中不存在有效的标段数据。"; } const projectCount = 1 + importData.tenders.length; // const feeName = compilationData.name === '安徽养护(2018)' ? '安徽养护' : '公路工程'; // 一些接口需要根据导入文件,匹配工程专业库 const rationValuationData = rationValuation && JSON.parse(rationValuation)[0]; // 只有工程量清单才能导入接口 if (!rationValuationData) { throw "无法获取工程量清单计价数据"; } const engineeringList = (rationValuationData.engineering_list || []).filter((item) => item.lib.visible); let engineeringLib = engineeringList[0]; if (importData.engineeringName && importData.feeName) { const matchLibs = engineeringList.filter((item) => item.lib && item.lib.name === importData.engineeringName); engineeringLib = matchLibs.find((item) => item.lib.feeName === importData.feeName) || matchLibs[0] || engineeringList[0]; } const engineeringID = engineeringLib.engineering_id || null; const templateData = await ajaxPost("/pm/api/getImportTemplateData", { user_id: userID, valuationID, engineeringID, projectCount }); if (!templateData) { throw "无法获取有效模板数据。"; } console.log(templateData); // 处理建设项目数据 // 确定建设项目的名称(不允许重复) /* const sameDepthProjs = getProjs(projTreeObj.tree.selected); const matchedProject = sameDepthProjs.find(node => node.data.name === importData.name); if (matchedProject) { alert('test'); importData.name += `(${moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')})`; } */ importData.compilation = compilationData._id; importData.userID = userID; importData.ID = templateData.projectBeginID; const { parentProjectID, preProjectID, nextProjectID } = projTreeObj.getRelProjectID(projTreeObj.tree.selected); importData.ParentID = parentProjectID; importData.preID = preProjectID; importData.NextSiblingID = nextProjectID; importData.projType = projectType.project; importData.property = { valuationType: commonConstants.ValuationType.BOQ, // 必为工程量清单 boqType: commonConstants.BOQType.BID_SUBMISSION, // 导入后必为投标 basicInformation: mergeInfo(importData.info, templateData.basicInfo), // 将提取的基本信息数据与标准基本信息数据进行合并(目前只赋值,没有匹配到的不追加) }; delete importData.info; // 处理单位工程数据 handleTenderData(importData.tenders, templateData, rationValuationData, engineeringLib, areaKey, onlyImportMatchBills); console.log(importData); } /* * 读取文件转换为utf-8编码的字符串 * @param {Blob} file * @return {Promise} * */ function readAsTextSync(file) { return new Promise((resolve, reject) => { const fr = new FileReader(); fr.readAsText(file); // 默认utf-8,如果出现乱码,得看导入文件是什么编码 fr.onload = function () { resolve(this.result); }; fr.onerror = function () { reject("读取文件失败,请重试。"); }; }); } /** * * @param {Function} entryFunc - 各导入接口提取导入数据方法 * @param {File} file - 导入的文件 * @param {String} areaKey - 地区标识,如:'安徽@马鞍山' * @param {Boolean} escape - 是否需要避免xml中的实体字符转换 * @param {Boolean} onlyImportMatchBills - 是否只导入文件中的清单,未匹配的模板清单不生成 * @return {Promise} */ async function extractImportData(entryFunc, file, areaKey, escape = false, onlyImportMatchBills = false) { // 将二进制文件转换成字符串 let xmlStr = await readAsTextSync(file); if (escape) { // x2js的str to json的实现方式基于DOMParser,DOMParser会自动将一些实体字符进行转换,比如 “< to <”。如果不想进行自动转换,需要进行处理。 xmlStr = escapeXMLEntity(xmlStr); } let reg1 = new RegExp(">\r\n", "g"); xmlStr = xmlStr.replace(reg1, ">"); let reg2 = new RegExp("\r\n", "g"); xmlStr = xmlStr.replace(reg2, XML_RN_STR); // reg = /(?<=子目名称).+(?=单位)/g; // let matchResult = xmlStr.match(reg); // if (matchResult.length > 0) { // console.log(matchResult[0]); // } // xmlStr = xmlStr.replace(new RegExp('\r\n', 'g'), XML_RN_STR); // 将xml格式良好的字符串转换成对象 const x2js = new X2JS(); let xmlObj = x2js.xml_str2json(xmlStr); xmlObj = JSON.parse(restoreXMLEntity(JSON.stringify(xmlObj))); console.log(xmlObj); if (!xmlObj) { throw "无有效数据。"; } const importData = await entryFunc(areaKey, xmlObj); console.log(areaKey); await handleImportData(importData, areaKey, onlyImportMatchBills); return importData; } return { UTIL, extractImportData, }; })();