'use strict'; /** * * * @author Zhong * @date 2019/6/20 * @version */ const XML_EXPORT_BASE = (() => { // 属性类型 const TYPE = { DATE: 1, //日期类型YYYY-MM-DD DATE_TIME: 2, //日期类型YYY-MM-DDTHH:mm:ss INT: 3, //整数类型 DECIMAL: 4, //数值类型,不限制小数位数 NUM2: 5, //数值类型2:最多两位小数 BOOL: 6 //布尔型 }; // 空白字符处理 const WHITE_SPACE = { COLLAPSE: 1 //移除所有空白字符(换行、回车、空格以及制表符会被替换为空格,开头和结尾的空格会被移除,而多个连续的空格会被缩减为一个单一的空格) }; // 计税方法 const TAX_TYPE = { '1': '一般计税法', '2': '简易计税法', }; // 承包人材料调整类型 const ADJUST_TYPE = { info: 'priceInfo', // 造价信息差额调整法 coe: 'priceCoe' // 价格指数调整法 }; // 加载数据间隔,减少服务器压力 const TIMEOUT_TIME = 500; // 导出粒度 const GRANULARITY = { PROJECT: 1, //导出建设项目 ENGINEERING: 2, //导出单项工程 TENDER: 3 //导出单位工程 }; // 导出的文件类型选项 const EXPORT_KIND = { Tender: 1, //投标 Bid: 2, //招标 Control: 3 //控制价 }; // 配置项 const CONFIG = Object.freeze({ TYPE, WHITE_SPACE, TAX_TYPE, ADJUST_TYPE, TIMEOUT_TIME, GRANULARITY, EXPORT_KIND }); // 缓存项 不需要的时候需要清空 let _cache = { // 失败列表 failList: [], // 项目数据(不包含详细数据,项目管理数据) projectData: {}, // 当前导出类型,默认投标 exportKind: EXPORT_KIND.Tender, // 记录拉取的单位工程项目详细数据,导出的时候,可能会导出多个文件,只有导出第一个文件的时候需要请求数据 tenderDetailMap: {} }; // 返回缓存项 function getItem(key) { return _cache[key] || null; } // 设置缓存项 function setItem(key, value) { // 与原数据是同类型的数据才可设置成功 if (_cache[key] && Object.prototype.toString.call(_cache[key]) === Object.prototype.toString.call(value)) { _cache[key] = value; } } // 清空缓存项 function clear() { _cache.failList = []; _cache.projectData = {}; _cache.exportKind = EXPORT_KIND.Tender; _cache.tenderDetailMap = {}; } const CACHE = Object.freeze({ getItem, setItem, clear }); /* * 定义不设置一个Node方法统一进入的原因:模板化比较直观,不分开定义节点的话,调用传参也很麻烦而且不直观。 * 一个节点对应一个构造方法,方便调整配置、方便其他版本开发、接手的人看起来更直观 * @param {String}name 节点名 * {Array}attrs 节点属性数据 * {Array}failList 失败列表 * @return {void} * */ function Element(name, attrs) { handleXMLEntity(attrs); this.name = name; let checkData = check(name, attrs); this.fail = checkData.failHints; this.attrs = checkData.filterAttrs; this.children = []; _cache.failList.push(...this.fail); } /* * xml字符实体的处理,这些特殊字符不处理会导致xml文件格式出错:""、<>、& * 要先处理& * */ let _xmlEntity = { '&': '&', '\n': ' ', '"': '"', '\'': ''', '<': '<', '>': '>' }; // 对每个元素的所有属性值进行特殊字符处理 function handleXMLEntity(attrs) { for (let attr of attrs) { // 值统一转换成String attr.value = !isDef(attr.value) ? '' : String(attr.value); if (!attr.value) { continue; } for (let [key, value] of Object.entries(_xmlEntity)) { attr.value = attr.value.replace(new RegExp(key, 'g'), value); } } } // 获取处理实体字符后的数据 function getParsedData(arr) { return arr.map(data => { for (let [key, value] of Object.entries(_xmlEntity)) { data = data.replace(new RegExp(key, 'g'), value); } return data; }); } /* * 检查 * 创建节点时检查节点的数据 * 1.长度限制minLen,maxLen * 2.值的限制,固定范围:enumeration * @param {String}eleName 节点名称 * {Array}datas 需要检查的属性数据 * @return {Object} failHints没通过的属性提示 filterAttrs过滤后的属性数据(失败提示在属性是必须的时候才提示,如果该属性失败了,但是是非必要属性,那么该属性不显示) * */ function check(eleName, datas) { let rst = {failHints: [], filterAttrs: []}; 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])))/; for (let data of datas) { //data.value = typeof data.value === 'undefined' || data.value === null ? '' : String(data.value); if (data.whiteSpace && data.whiteSpace === WHITE_SPACE.COLLAPSE) { //处理空格相关 data.value = data.value.replace(/[\r,\n,\t]/g, ' '); data.value = data.value.trim(); data.value = data.value.replace(/\s{1,}/g, ' '); } /*if (!data.value && !data.minLen && !data.enumeration) { //值为空,且没有限制最小字符数,且没有限制值,则不需判断 rst.filterAttrs.push(data); continue; }*/ let isFail = false, tempFail = ''; if (data.minLen && data.value.length < data.minLen){ isFail = true; tempFail = data.failHint ? `${data.failHint}字符数不可小于${data.minLen}个。` :`${eleName}-“${data.name}”字符数不可小于${data.minLen}个。`; } else if (data.maxLen && data.value.length > data.maxLen) { isFail = true; tempFail = data.failHint ? `${data.failHint}字符数不可大于${data.maxLen}个。` : `${eleName}-“${data.name}”字符数不可大于${data.maxLen}个。`; } else if (data.enumeration && !data.enumeration.includes(data.value)) { isFail = true; let enumerationHint = data.enumerationHint ? data.enumerationHint.join(';') : data.enumeration.join(';'); tempFail = data.failHint ? `${data.failHint}只能从“${enumerationHint}”中选择。` : `${eleName}-“${data.name}”只能从“${enumerationHint}”中选择。`; } else if (data.type && data.type === TYPE.DATE && !dateReg.test(data.value)) { isFail = true; tempFail = data.failHint ? `${data.failHint}日期格式必须是YYYY-MM-DD。` : `${eleName}-“${data.name}”日期格式必须是YYYY-MM-DD。`; } else if (data.type && data.type === TYPE.INT && !Number.isInteger(parseFloat(data.value))) { isFail = true; tempFail = data.failHint ? `${data.failHint}必须为整数。` : `${eleName}-“${data.name}”必须为整数。`; } else if (data.type && data.type === TYPE.DECIMAL && isNaN(parseFloat(data.value))) { isFail = true; tempFail = data.failHint ? `${data.failHint}必须为数值。` : `${eleName}-“${data.name}”必须为数值。`; } else if (data.type && data.type === TYPE.NUM2) { let v = parseFloat(data.value); if (isNaN(v)) { isFail = true; tempFail = data.failHint ? `${data.failHint}必须为数值。` : `${eleName}-“${data.name}”必须为数值。`; } else if (!data.value.length || (data.value.split('.').length > 1 && data.value.split('.')[1].length > 2)){ isFail = true; } } else if (data.type && data.type === TYPE.BOOL && !['true', 'false'].includes(String(data.value))) { isFail = true; tempFail = data.failHint ? `${data.failHint}必须为true或false。` : `${eleName}-“${data.name}”必须为true或false。`; } if (!isFail || data.required) { rst.filterAttrs.push(data); } if (isFail && data.required && tempFail) { rst.failHints.push(tempFail); } } return rst; } // 等待一段时间 function setTimeoutSync(handle, time) { return new Promise((resolve, reject) => { setTimeout(() => { if (handle && typeof handle === 'function') { handle(); } resolve(); }, time); }); } function isDef(v) { return typeof v !== 'undefined' && v !== null; } /* * 从fees数组中获取相关费用 * @param {Array}fees 费用数组 * {String}feeFields 费用字段 * @return {Number} * @example getFee(source.fees, 'common.totalFee') * */ function getFee(fees, feeFields) { if (!Array.isArray(fees)) { return 0; } let fields = feeFields.split('.'); let fee = fees.find(data => data.fieldName === fields[0]); if (!fee) { return 0; } return fee[fields[1]] || 0; } /* * 根据key获取对应的基本信息、工程特征数据 * @param {Array}data * {String}key * @return {String} * @example getValueByKey(source.basicInformation, 'projectScale') * */ function getValueByKey(data, key) { for (let d of data) { if (d.key === key) { return d.value; } if (d.items && d.items.length > 0) { let findData = d.items.find(x => x.key === key); if (findData) { return findData.value; } } } return ''; } // 数组打平成对象 function arrayToObj(arr) { let rst = {}; for (let data of arr) { rst[data.key] = data.value; } return rst; } //检测层数 //@param {Number}maxDepth(最大深度) {Object}node(需要检测的清单树节点) //@return {Boolean} function validDepth(maxDepth, node) { let nodeDepth = node.depth(); let allNodes = node.getPosterity(); //检测相对深度 for (let n of allNodes) { let relativeDepth = n.depth() - nodeDepth; if (relativeDepth > maxDepth) { return false; } } return true; } //检测唯一性 //@param {Object}constraints(约束池) {All}data(检测的数据) {String}hint(提示已存在的内容) {String}subHint(额外提示,有额外提示时,不用data提示) //@return {void} function checkUnique(constraints, data, hint, subHint) { if (constraints.includes(data)) { let failHint = subHint ? `${hint}“${subHint}”已存在` : `${hint}“${data}”已存在`; _cache.failList.push(failHint); } else if (data) { constraints.push(data); } } //根据数据的NextSiblingID进行排序,返回排序后的数组 function sortByNext(datas) { let target = [], temp = {}; for (let data of datas) { temp[data.ID] = {me: data, next: null, prev: null}; } for (let data of datas) { let next = temp[data.NextSiblingID] || null; temp[data.ID].next = next; if (next) { next.prev = temp[data.ID]; } } let first = null; for (let data of datas) { let me = temp[data.ID]; if (!me.prev) { first = me; } } if (!first) { return datas; } while (first) { target.push(first.me); first = first.next; } return target; } //转换基数表达式 //1.有子项,则取固定清单对应基数 //2.无子项,有基数,a.优先转换为行代号(不可自身) b.不能转换为行代号则找对应字典 //3.基数中有无法转换的,设为金额 function transformCalcBase(tenderDetail, node, {CalcBaseMap, FlagCalcBaseMap}) { let expr = node.data.calcBase || ''; if (node.children.length) { let flag = node.getFlag(); return FlagCalcBaseMap[flag] || ''; } if (expr) { let illegal = false; let normalBase = _getNormalBase(expr), idBase = _getIDBase(expr); //普通基数转基数字典 normalBase.forEach(base => { let replaceStr = CalcBaseMap[base]; //转换成行代号的优先级比较高,进行清单匹配 let flag = FlagCalcBaseMap[base]; if (flag) { let flagNode = tenderDetail.mainTree.items.find(mNode => mNode.getFlag() === flag); //匹配到了 普通基数转换成行引用 if (flagNode) { replaceStr = `F${flagNode.serialNo() + 1}`; } } //存在无法处理的基数 if (!replaceStr) { illegal = true; return; } expr = expr.replace(new RegExp(base, 'g'), replaceStr); }); //id引用转行代号引用 idBase.forEach(base => { let id = base.match(/[^@]+/)[0]; let theNode = tenderDetail.mainTree.getNodeByID(id), rowCode = theNode ? `F${theNode.serialNo() + 1}` : ''; if (!rowCode) { illegal = true; return; } expr = expr.replace(new RegExp(base, 'g'), rowCode); }); //不合法 返回金额 if (illegal) { return getFee(node.data.fees, 'common.totalFee'); } return expr; } //获取普通基数: {xxx} function _getNormalBase(str) { let reg = /{.+?}/g, matchs = str.match(reg); return matchs || []; } //获取id引用基数: @xxx-xxx-xx function _getIDBase(str) { let reg = /@.{36}/g, matchs = str.match(reg); return matchs || []; } } //转换基数说明,根据转换后的基数处理 //1.行引用转换为对应行的名称 //2.基数字典转换为中文 function transformCalcBaseState(tenderDetail, expr, CalcStateMap) { if (!expr) { return ''; } expr = String(expr); //提取基数 let bases = expr.split(/[\+\-\*\/]/g); //提取操作符 let oprs = expr.match(/[\+\-\*\/]/g); //转换后的基数 let newBase = []; let illegal = false; for (let base of bases) { //行引用转换为名称 if (/F\d+/.test(base)) { let rowCode = base.match(/\d+/)[0], node = tenderDetail.mainTree.items[rowCode - 1]; if (!node || !node.data.name) { illegal = true; break; } newBase.push(node && node.data.name ? node.data.name : ''); } else if (CalcStateMap[base]){ //字典转换为中文 newBase.push(CalcStateMap[base]); } else if (/^\d+(\.\d+)?$/.test(base)) { //金额 newBase.push(base); } else { illegal = true; break; } } if (illegal) { return ''; } let newExpr = ''; for (let i = 0; i < newBase.length; i++) { newExpr += newBase[i]; if (oprs && oprs[i]) { newExpr += oprs[i]; } } return newExpr; } // 获取工程编号表格相关数据(导出需要弹出工程编号让用户选择) function getCodeSheetData(PMData) { let curCode = '0'; let sheetData = []; sheetData.push(getObj(PMData)); PMData.children.forEach(eng => { sheetData.push(getObj(eng)); eng.children.forEach(tender => { sheetData.push(getObj(tender)); }); }); //建设项目父ID设置为-1 if (sheetData.length) { sheetData[0].ParentID = -1; sheetData[0].code = ''; } return sheetData; function getObj(data) { return { collapsed: false, ID: data.ID, ParentID: data.ParentID, NextSiblingID: data.NextSiblingID, name: data.name, code: data.code || String(curCode++) }; } } // 从srcNode节点中获取为target实例的节点 function _getNodeFromSrc(srcNode, target) { if (!srcNode || !srcNode.children || !srcNode.children.length) { return []; } return srcNode.children.filter(node => node instanceof target); } // 设置完工程编号后,更新原始数据的工程编号 function setupCode(originalDatas, codeDatas, {Tender, Engineering}) { // 转换xml实体字符 let parsedCodeDatas = getParsedData(codeDatas); originalDatas.forEach(orgData => { let curIdx = 0; let engs = _getNodeFromSrc(orgData.data, Engineering); engs.forEach(eng => { eng.attrs.find(attr => attr.name === '编号').value = parsedCodeDatas[curIdx++]; let tenders = _getNodeFromSrc(eng, Tender); tenders.forEach(tender => { tender.attrs.find(attr => attr.name === '编号').value = parsedCodeDatas[curIdx++]; }); }); }); } const UTIL = Object.freeze({ isDef, setTimeoutSync, getFee, getValueByKey, arrayToObj, validDepth, checkUnique, sortByNext, transformCalcBase, transformCalcBaseState, getCodeSheetData, setupCode }); // 开始标签 function _startTag(ele) { let rst = `<${ele.name}`; for (let attr of ele.attrs) { rst += ` ${attr.name}="${attr.value}"`; } rst += ele.children.length > 0 ? '>' : '/>'; return rst; } // 结束标签 function _endTag(ele) { return ``; } // 拼接成xml字符串 function _toXMLStr(eles) { let rst = ''; for (let ele of eles) { rst += _startTag(ele); if (ele.children.length > 0) { rst += _toXMLStr(ele.children); rst += _endTag(ele); } } return rst; } //格式化xml字符串 function _formatXml(text) { //去掉多余的空格 text = '\n' + text.replace(/>\s*?\n<"); //调整格式 let rgx = /\n(<(([^\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/mg; let nodeStack = []; let output = text.replace(rgx, function($0, all, name, isBegin, isCloseFull1, isCloseFull2, isFull1, isFull2){ let isClosed = (isCloseFull1 === '/') || (isCloseFull2 === '/' ) || (isFull1 === '/') || (isFull2 === '/'); let prefix = ''; if (isBegin === '!') { prefix = getPrefix(nodeStack.length); } else { if (isBegin !== '/') { prefix = getPrefix(nodeStack.length); if (!isClosed) { nodeStack.push(name); } } else { nodeStack.pop(); prefix = getPrefix(nodeStack.length); } } let ret = '\n' + prefix + all; return ret; }); let outputText = output.substring(1); return outputText; function getPrefix(prefixIndex) { let span = ' '; let output = []; for (let i = 0 ; i < prefixIndex; ++i) { output.push(span); } return output.join(''); } } // 将数据转换为xml文件并导出 async function exportFile(originalDatas) { let fileDatas = []; //源数据转换成blob for (let orgData of originalDatas) { //转换成xml字符串 let xmlStr = _toXMLStr([orgData.data]); //加上xml声明 xmlStr = `${xmlStr}`; //格式化 xmlStr = _formatXml(xmlStr); let blob = new Blob([xmlStr], {type: 'text/plain;charset=utf-8'}); fileDatas.push({blob: blob, fileName: orgData.fileName}); } //导出文件 if (fileDatas.length === 1) { saveAs(fileDatas[0].blob, fileDatas[0].fileName); } else if (fileDatas.length > 1) { //导出压缩包 let zip = new JSZip(); for (let file of fileDatas) { zip.file(file.fileName, file.blob, {binary: true}); } let zipFile = await zip.generateAsync({type: 'blob'}); saveAs(zipFile, '重庆标准交换数据.zip'); } } return { CONFIG, CACHE, UTIL, Element, exportFile }; })();