Browse Source

feat:导出接口重构

vian 5 years atrás
parent
commit
6184020784

+ 6 - 0
modules/main/routes/main_route.js

@@ -33,6 +33,11 @@ module.exports =function (app) {
                 }
                 const markReadProjectIDs = isOpenShareProject ? await pmFacade.markShareItemsRead(projectID, req.session.sessionUser.id) : [];
                 const version = await systemSettingModel.getVersion();
+                let boqType = null;
+                const constructProject = await pmFacade.getConstructionProject(projectID);
+                if (constructProject && constructProject.property && constructProject.property.boqType) {
+                    boqType = constructProject.property.boqType;
+                }
                 res.render('building_saas/main/html/main.html',
                     {
                         userAccount: req.session.userAccount,
@@ -47,6 +52,7 @@ module.exports =function (app) {
                         options:JSON.stringify(options),
                         overWriteUrl:req.session.sessionCompilation.overWriteUrl,
                         markReadProjectIDs: JSON.stringify(markReadProjectIDs),
+                        boqType,
                         title:config[process.env.NODE_ENV].title?config[process.env.NODE_ENV].title:"纵横公路养护云造价",
                         version
                     });

+ 1 - 1
public/common_constants.js

@@ -108,7 +108,7 @@
     };
 
     // 工程量清单类型
-    const BOQType = {
+    const BOQType = { 
         BID_INVITATION: 1, // 招标
         BID_SUBMISSION: 2, // 投标
     };

+ 7 - 1
public/common_util.js

@@ -18,11 +18,16 @@
     function isDef(val) {
         return typeof val !== 'undefined' && val !== null;
     }
-
+    
     function isEmptyVal(val) {
         return val === null || val === undefined || val === '';
     }
 
+    // v是否有值,不为undefined、null、''
+    function hasValue(v) {
+        return typeof v !== 'undefined' && v !== null && v !== '';
+    }
+
     // 是否近似相等(null = undefined = '', 1 = '1'...)
     function similarEqual(a, b) {
         // null == '' 为false,所以不能用非严等
@@ -121,6 +126,7 @@
     return {
         isDef,
         isEmptyVal,
+        hasValue,
         similarEqual,
         getSortedTreeData,
         handleFullscreen,

+ 57 - 0
web/building_saas/main/html/main.html

@@ -1900,6 +1900,63 @@
             </div>
         </div>
     </div>
+    <!--弹出 数据接口导出-->
+    <div class="modal fade" id="interface-export-modal" data-backdrop="static">
+        <div class="modal-dialog" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">电子招投标数据接口
+                    </h5>
+                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                        <span aria-hidden="true">&times;</span>
+                    </button>
+                </div>
+                <div class="modal-body">
+                    <!--招标-->
+                    <% if (boqType == 1) { %>
+                    <div class="form-group">
+                        <label class="mb-0">招标接口文件导出</label>
+                        <small class="form-text text-muted">招标接口文件由以下2个文件组成,其内容必须保持一致,因此建议一次性全部导出。</small>
+                    </div>
+                    <div class="form-group">
+                        <div class="form-check ml-4">
+                            <input class="form-check-input" type="checkbox" value="2" id="ex-bid" checked>
+                            <label class="form-check-label" for="ex-bid">
+                                招标工程量清单
+                            </label>
+                            <small class="form-text text-muted">招标工程量清单数据文件,用于投标人投标报价</small>
+                        </div>
+                        <div class="form-check ml-4">
+                            <input class="form-check-input" type="checkbox" value="3" id="ex-control">
+                            <label class="form-check-label" for="ex-control">
+                                招标控制价
+                            </label>
+                            <small class="form-text text-muted">包含完整组价数据的招标控制价文件</small>
+                        </div>
+                    </div>
+                    <% } else { %>
+                    <!--投标-->
+                    <div class="form-group">
+                        <label class="mb-0">投标接口文件导出</label>
+                    </div>
+                    <div class="form-group">
+                        <div class="form-check ml-4">
+                            <input class="form-check-input" type="checkbox" value="1" id="ex-tender" checked>
+                            <label class="form-check-label" for="ex-tender">
+                                投标文件
+                            </label>
+                            <small class="form-text text-muted">投标工程数据文件</small>
+                        </div>
+                    </div>
+                    <% } %>
+                </div>
+                <div class="modal-footer">
+                    <a id="interface-export-confirm" href="javascript:void(0);" class="btn btn-primary">确定导出</a>
+                    <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                </div>
+            </div>
+        </div>
+    </div>
     <%include ../../../common/components/share/index.html %>
     <img src="/web/dest/css/img/folder_open.png" id="folder_open_pic" style="display: none">
     <img src="/web/dest/css/img/folder_close.png" id="folder_close_pic" style="display: none">

+ 9 - 0
web/building_saas/standard_interface/export/anhui_maanshan.js

@@ -0,0 +1,9 @@
+/*
+ * @Descripttion: 安徽-马鞍山 接口
+ * @Author: vian
+ * @Date: 2020-08-17 15:40:08
+ */
+
+INTERFACE_EXPORT = (() => {
+    'use strict';
+})();

+ 754 - 0
web/building_saas/standard_interface/export/base.js

@@ -0,0 +1,754 @@
+/**
+ * @author Zhong
+ * @date 2019/6/20
+ * @version
+ */
+const XML_EXPORT_BASE = (() => {
+    'use strict';
+
+    const { isDef, hasValue } = window.commonUtil;
+
+    // 属性类型
+    const TYPE = {
+        DATE: 1, // 日期类型YYYY-MM-DD
+        DATE_TIME: 2, // 日期类型YYY-MM-DDTHH:mm:ss
+        INT: 3, // 整数类型
+        DECIMAL: 4, // 数值类型,不限制小数位数
+        NUM2: 5, // 数值类型2:最多两位小数
+        BOOL: 6 // 布尔型
+    };
+    // 需要特殊处理的属性类型默认空值(当一个值为undefined、null的时候,默认给赋什么值)
+    const DEFAULT_VALUE = {
+        [TYPE.INT]: '0',
+        [TYPE.DECIMAL]: '0',
+        [TYPE.NUM2]: '0',
+        [TYPE.BOOL]: 'false'
+    };
+    // 空白字符处理
+    const WHITE_SPACE = {
+        COLLAPSE: 1 // 移除所有空白字符(换行、回车、空格以及制表符会被替换为空格,开头和结尾的空格会被移除,而多个连续的空格会被缩减为一个单一的空格)
+    };
+    // 承包人材料调整类型
+    const ADJUST_TYPE = {
+        info: 'priceInfo', // 造价信息差额调整法
+        coe: 'priceCoe' // 价格指数调整法
+    };
+    // 加载数据间隔,减少服务器压力
+    const TIMEOUT_TIME = 400;
+    // 导出粒度
+    const GRANULARITY = {
+        PROJECT: 1, // 导出建设项目
+        ENGINEERING: 2, // 导出单项工程
+        TENDER: 3 // 导出单位工程
+    };
+    // 导出的文件类型选项
+    const EXPORT_KIND = {
+        BID_INVITATION: 1, // 招标
+        BID_SUBMISSION: 2, // 投标
+        CONTROL: 3 // 控制价
+    };
+    const EXPORT_KIND_NAME = {
+        1: '招标',
+        2: '投标',
+        3: '控制价'
+    };
+    // 配置项
+    const CONFIG = Object.freeze({
+        TYPE,
+        WHITE_SPACE,
+        ADJUST_TYPE,
+        TIMEOUT_TIME,
+        GRANULARITY,
+        EXPORT_KIND,
+        EXPORT_KIND_NAME
+    });
+
+    // 缓存项 不需要的时候需要清空
+    const _cache = {
+        // 项目数据(不包含详细数据,项目管理数据)
+        projectData: {},
+        // 当前导出类型,默认投标
+        exportKind: EXPORT_KIND.BID_SUBMISSION,
+        // 记录拉取的单位工程项目详细数据,导出的时候,可能会导出多个文件,只有导出第一个文件的时候需要请求数据
+        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.projectData = {};
+        _cache.exportKind = EXPORT_KIND.BID_SUBMISSION;
+        _cache.tenderDetailMap = {};
+    }
+    const CACHE = Object.freeze({
+        getItem,
+        setItem,
+        clear
+    });
+
+
+    /*
+     * 定义不设置一个Node方法统一进入的原因:模板化比较直观,不分开定义节点的话,调用传参也很麻烦而且不直观。
+     * 一个节点对应一个构造方法,方便调整配置、方便其他版本开发、接手的人看起来更直观
+     * @param  {String}name 节点名
+     *         {Array}attrs 节点属性数据
+     * @return {void}
+     * */
+    function Element(name, attrs) {
+        this.name = name;
+        this.attrs = attrs;
+        handleXMLEntity(this.attrs);
+        this.children = [];
+    }
+
+    /*
+     * xml字符实体的处理,这些特殊字符不处理会导致xml文件格式出错:""、<>、&
+     * 要先处理&amp
+     * */
+    const _xmlEntity = {
+        '&': '&amp;',
+        '\n': '&#xA;',
+        '"': '&quot;',
+        '\'': '&apos;',
+        '<': '&lt;',
+        '>': '&gt;'
+    };
+    // 对每个元素的所有属性值进行特殊字符处理
+    function handleXMLEntity(attrs) {
+        for (const attr of attrs) {
+            if (!attr.value) {
+                continue;
+            }
+            for (const [key, value] of Object.entries(_xmlEntity)) {
+                attr.value = attr.value.replace(new RegExp(key, 'g'), value);
+            }
+        }
+    }
+    // 获取处理实体字符后的数据
+    function getParsedData(arr) {
+        return arr.map(data => {
+            for (const [key, value] of Object.entries(_xmlEntity)) {
+                data = data.replace(new RegExp(key, 'g'), value);
+            }
+            return data;
+        });
+    }
+    /*
+     * 检查
+     * 创建节点时检查节点的数据(原本是用于自检,现在来处理默认值)
+     * @param {Array}datas 需要检查的属性数据
+     * @return {void}
+     * */
+    function check(datas) {
+        for (const data of datas) {
+            const isHasValue = hasValue(data.value);
+            // 值统一转换成String,并且处理各类型属性空值时的默认取值
+            data.value = !isHasValue
+                ? DEFAULT_VALUE[data.type]
+                    ? DEFAULT_VALUE[data.type]
+                    : ''
+                : 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, ' ');
+            }
+        }
+    }
+    // 等待一段时间
+    function setTimeoutSync(handle, time) {
+        return new Promise((resolve, reject) => {
+            setTimeout(() => {
+                if (handle && typeof handle === 'function') {
+                    handle();
+                }
+                resolve();
+            }, time);
+        });
+    }
+
+    /*
+     * 将节点属性数据(attr数组)转换成简单key-value数据
+     * @param  {Object}ele 元素节点数据Element实例
+     * @return {Object}
+     * */
+    function getPlainAttrs(ele) {
+        const obj = {};
+        ele.attrs.forEach(attr => obj[attr.name] = attr.value);
+        return obj;
+    }
+    /*
+     * 从fees数组中获取相关费用
+     * @param  {Array}fees 费用数组
+     *         {String}feeFields 费用字段
+     * @return {Number}
+     * @example getFee(source.fees, 'common.totalFee')
+     * */
+    function getFee(fees, feeFields) {
+        if (!Array.isArray(fees)) {
+            return 0;
+        }
+        const fields = feeFields.split('.');
+        const fee = fees.find(data => data.fieldName === fields[0]);
+        if (!fee) {
+            return 0;
+        }
+        return fee[fields[1]] || 0;
+    }
+    // 获取节点的汇总价格
+    function getAggregateFee(nodes) {
+        const total = nodes.reduce((acc, node) => {
+            const price = getFee(node.data.fees, 'common.totalFee');
+            return acc += price;
+        }, 0);
+        return scMathUtil.roundTo(total, -2);
+    }
+    // 获取固定类别行的费用
+    function getFeeByFlag(items, flag, feeFields) {
+        const node = items.find(node => node.getFlag() === flag);
+        return node ? getFee(node.data.fees, feeFields) : '0';
+    }
+    /*
+     * 根据key获取对应的基本信息、工程特征数据
+     * @param  {Array}data
+     *         {String}key
+     * @return {String}
+     * @example getValueByKey(source.basicInformation, 'projectScale')
+     * */
+    function getValueByKey(items, key) {
+        for (const item of items) {
+            if (item.key === key) {
+                return item.value;
+            }
+            if (item.items && item.items.length) {
+                const value = getValueByKey(item.items, key);
+                if (value) {
+                    return value;
+                }
+            }
+        }
+        return '';
+    }
+    // 获取关联材料
+    function getRelGLJ(allGLJs, gljId) {
+        return allGLJs.find(glj => glj.id === gljId);
+    }
+    // 随机生成机器信息码:CPU信息;硬盘序列号;mac地址;
+    // 保存在localStorage中
+    function generateHardwareId() {
+        const hardwareCacheId = window.localStorage.getItem('hardwareId');
+        if (hardwareCacheId) {
+            return hardwareCacheId;
+        }
+        const charList = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
+            'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
+            'V', 'W', 'X', 'Y', 'Z'];
+        function generateCpuId() {
+            let id = '';
+            let count = 16;
+            while (count--) {
+                const randomIdx = parseInt(Math.random() * 16);
+                id += charList[randomIdx];
+            }
+            return id;
+        }
+        function generateDiskId() {
+            let id = '';
+            let count = 8;
+            while (count--) {
+                const randomIdx = parseInt(Math.random() * 36);
+                id += charList[randomIdx];
+            }
+            return id;
+        }
+        function generateMacId() {
+            const idList = [];
+            let outerCount = 6;
+            while (outerCount--) {
+                let tempId = '';
+                let innerCount = 2;
+                while (innerCount--) {
+                    const randomIdx = parseInt(Math.random() * 16);
+                    tempId += charList[randomIdx];
+                }
+                idList.push(tempId);
+            }
+            return idList.join('-');
+        }
+        const cpuId = generateCpuId();
+        const diskId = generateDiskId();
+        const macId = generateMacId();
+        const hardwareId = [cpuId, diskId, macId].join(';');
+        window.localStorage.setItem('hardwareId', hardwareId);
+        return hardwareId;
+    }
+    // 数组打平成对象
+    function arrayToObj(arr) {
+        const rst = {};
+        for (const data of arr) {
+            rst[data.key] = data.value;
+        }
+        return rst;
+    }
+    /*
+     * 检测层数是否有效
+     * @param  {Number}maxDepth(最大深度)
+     *         {Object}node(需要检测的清单树节点)
+     * @return {Boolean}
+     * */
+    function validDepth(maxDepth, node) {
+        const nodeDepth = node.depth();
+        const allNodes = node.getPosterity();
+        //检测相对深度
+        for (const n of allNodes) {
+            const relativeDepth = n.depth() - nodeDepth;
+            if (relativeDepth > maxDepth) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // 根据数据的NextSiblingID进行排序,返回排序后的数组
+    function sortByNext(datas) {
+        const target = [];
+        const temp = {};
+        for (const data of datas) {
+            temp[data.ID] = { me: data, next: null, prev: null };
+        }
+        for (const data of datas) {
+            const next = temp[data.NextSiblingID] || null;
+            temp[data.ID].next = next;
+            if (next) {
+                next.prev = temp[data.ID];
+            }
+        }
+        let first = null;
+        for (const data of datas) {
+            const me = temp[data.ID];
+            if (!me.prev) {
+                first = me;
+            }
+        }
+        if (!first) {
+            return datas;
+        }
+        while (first) {
+            target.push(first.me);
+            first = first.next;
+        }
+        return target;
+    }
+
+    /*
+     * 根据粒度获取项目(不包含详细数据)数据
+     * @param  {Number}granularity 导出粒度
+     *         {Object}summaryObj 汇总字段
+     *         {Number}tenderID 单位工程ID
+     *         {String}userID 用户ID
+     * @return {Object} 返回的数据结构:{children: [{children: []}]} 最外层为建设项目,中间为单项工程,最底层为单位工程
+     * */
+    async function getProjectByGranularity(granularity, summaryObj, tenderID, userID) {
+        let projectData = _cache.projectData;
+        // 没有数据,需要拉取
+        if (!Object.keys(projectData).length) {
+            projectData = await ajaxPost('/pm/api/getProjectByGranularity', { user_id: userID, tenderID, granularity, summaryObj });
+            _cache.projectData = projectData;
+        }
+        return projectData;
+    }
+
+    /*
+     * 通过getData接口获取单位工程详细数据(带缓存功能)
+     * @param  {Number}tenderID 单位工程ID
+     *         {String}userID 用户ID
+     * @return {Object} 跟projectObj.project的数据结构一致
+     * */
+    async function getTenderDetail(tenderID, userID) {
+        // 获取单位工程详细数据
+        let tenderDetail = _cache.tenderDetailMap[tenderID];
+        if (!tenderDetail) {
+            tenderDetail = PROJECT.createNew(tenderID, userID);
+            await tenderDetail.loadDataSync();
+            // 标记序号
+            const count = Object.keys(_cache.tenderDetailMap).length;
+            tenderDetail.serialNo = count + 1;
+            _cache.tenderDetailMap[tenderID] = tenderDetail;
+        }
+        return tenderDetail;
+    }
+
+    /*
+     * 提取要导出的数据
+     * @param  {Function}entryFunc 提取数据的入口方法
+     *         {Number}granularity 导出粒度: 1-建设项目、2-单项工程、3-单位工程
+     *         {Number}exportKind 导出的文件类型:1-投标、2-招标、3-控制价
+     *         {Number}tenderID 单位工程ID
+     *         {String}userID 用户ID
+     * @return {Array} 数据结构为:[{data: Object, exportKind: Number, fileName: String}]
+     * */
+    async function extractExportData(entryFunc, granularity, summaryObj, exportKind, tenderID, userID) {
+        // 默认导出建设项目
+        if (!granularity || ![1, 2, 3].includes(granularity)) {
+            granularity = GRANULARITY.PROJECT;
+        }
+        // 默认导出投标文件
+        if (!exportKind || ![1, 2, 3].includes(exportKind)) {
+            exportKind = EXPORT_KIND.BID_SUBMISSION;
+        }
+        // 拉取标段数据:建设项目、单项工程、单位工程数据
+        const projectData = await getProjectByGranularity(granularity, summaryObj, tenderID, userID);
+        if (!projectData) {
+            throw '获取项目数据错误';
+        }
+        // 单项工程、单位工程按照树结构数据进行排序
+        projectData.children = sortByNext(projectData.children);
+        for (const engData of projectData.children) {
+            engData.children = sortByNext(engData.children);
+        }
+        // 提取相关项目的详细导出数据
+        return await entryFunc(userID, exportKind, projectData);
+    }
+
+    // 获取普通基数: {xxx}
+    function getNormalBase(str) {
+        const reg = /{.+?}/g;
+        const matchs = str.match(reg);
+        return matchs || [];
+    }
+    // 获取id引用基数: @xxx-xxx-xx
+    function getIDBase(str) {
+        const reg = /@.{36}/g;
+        const matchs = str.match(reg);
+        return matchs || [];
+    }
+
+    // 转换基数表达式
+    // 1.有子项,则取固定清单对应基数
+    // 2.无子项,有基数,a.优先转换为行代号(不可自身) b.不能转换为行代号则找对应字典
+    // 3.基数中有无法转换的,根据导出类型决定
+    function transformCalcBase(exportKind, tenderDetail, node, { CalcBaseMap, FlagCalcBaseMap }) {
+        let expr = node.data.calcBase || '';
+        if (node.children.length) {
+            const flag = node.getFlag();
+            return FlagCalcBaseMap[flag] || '';
+        }
+        if (expr) {
+            let illegal = false;
+            const normalBase = getNormalBase(expr);
+            const idBase = getIDBase(expr);
+            // 普通基数转基数字典
+            normalBase.forEach(base => {
+                let replaceStr = CalcBaseMap[base];
+                // 转换成行代号的优先级比较高,进行清单匹配
+                const flag = FlagCalcBaseMap[base];
+                if (flag) {
+                    const 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 => {
+                const id = base.match(/[^@]+/)[0];
+                const theNode = tenderDetail.mainTree.getNodeByID(id);
+                const rowCode = theNode ? `F${theNode.serialNo() + 1}` : '';
+                if (!rowCode) {
+                    illegal = true;
+                    return;
+                }
+                expr = expr.replace(new RegExp(base, 'g'), rowCode);
+            });
+            // 不合法
+            // 在我们软件中的基数无法找到映射代号的情况下
+            // 导出招标、控制价时,基数为空
+            // 导出投标时,基数=综合合价/费率
+            if (illegal) {
+                if (exportKind === EXPORT_KIND.BID_INVITATION || exportKind === EXPORT_KIND.CONTROL) {
+                    return '';
+                } else {
+                    const totalFee = getFee(node.data.fees, 'common.totalFee');
+                    const feeRate = node.data.feeRate;
+                    return +feeRate ? scMathUtil.roundTo(totalFee / (feeRate / 100), -2) : totalFee
+                }
+            }
+            return expr;
+        }
+    }
+    // 转换基数说明,根据转换后的基数处理
+    // 1.行引用转换为对应行的名称
+    // 2.基数字典转换为中文
+    function transformCalcBaseState(tenderDetail, expr, CalcStateMap) {
+        if (!expr) {
+            return '';
+        }
+        expr = String(expr);
+        // 提取基数
+        const bases = expr.split(/[\+\-\*\/]/g);
+        // 提取操作符
+        const oprs = expr.match(/[\+\-\*\/]/g);
+        // 转换后的基数
+        const newBase = [];
+        let illegal = false;
+        for (const base of bases) {
+            // 行引用转换为名称.
+            if (/F\d+/.test(base)) {
+                const rowCode = base.match(/\d+/)[0];
+                const 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(projectData) {
+        let curCode = '0';
+        let sheetData = [];
+        sheetData.push(getObj(projectData));
+        projectData.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++)
+            };
+        }
+    }
+    // 获取节点的某属性
+    function getAttr(ele, name) {
+        return (ele.attrs.find(attr => attr.name === name) || {}).value;
+    }
+    // 设置节点的某属性
+    function setAttr(ele, name, value) {
+        const attr = ele.attrs.find(attr => attr.name === name);
+        if (attr) {
+            attr.value = value;
+        }
+    }
+    // 从srcEle节点中获取元素名为eleName的元素
+    function getElementFromSrc(srcEle, eleName) {
+        if (!srcEle || !srcEle.children || !srcEle.children.length) {
+            return [];
+        }
+        return srcEle.children.filter(ele => ele.name === eleName);
+    }
+    /*
+     * 设置完工程编号后,更新原始数据的工程编号
+     * 更新原始数据前需要将编号里的特殊字符进行转换
+     * @param  {Array}exportData 提取出来的需要导出的数据
+     *         {Array}codes 工程编号表中填写的工程编号
+     *         {String}EngineeringName 单项工程元素的名称
+     *         {String}tenderName 单位工程元素的名称
+     *         {String}codeName 编号属性的名称
+     * @return {void}
+     * */
+    function setupCode(exportData, codes, EngineeringName, tenderName, codeName) {
+        // 转换xml实体字符
+        let parsedCodes = getParsedData(codes);
+        // 给导出数据里的单项工程、单位工程填上用户设置的工程编号
+        exportData.forEach(orgData => {
+            let curIdx = 0;
+            let engs = getElementFromSrc(orgData.data, EngineeringName);
+            engs.forEach(eng => {
+                eng.attrs.find(attr => attr.name === codeName).value = parsedCodes[curIdx++];
+                let tenders = getElementFromSrc(eng, tenderName);
+                tenders.forEach(tender => {
+                    tender.attrs.find(attr => attr.name === codeName).value = parsedCodes[curIdx++];
+                });
+            });
+        });
+    }
+
+    const UTIL = Object.freeze({
+        isDef,
+        hasValue,
+        setTimeoutSync,
+        getFee,
+        getAggregateFee,
+        getFeeByFlag,
+        getPlainAttrs,
+        getValueByKey,
+        getRelGLJ,
+        generateHardwareId,
+        arrayToObj,
+        validDepth,
+        sortByNext,
+        getTenderDetail,
+        getProjectByGranularity,
+        getNormalBase,
+        getIDBase,
+        transformCalcBase,
+        transformCalcBaseState,
+        getCodeSheetData,
+        getElementFromSrc,
+        getAttr,
+        setAttr,
+        getParsedData,
+        setupCode,
+    });
+
+    // 开始标签
+    function _startTag(ele) {
+        let rst = `<${ele.name}`;
+        for (const attr of ele.attrs) {
+            rst += ` ${attr.name}="${attr.value}"`;
+        }
+        rst += ele.children.length > 0 ? '>' : '/>';
+        return rst;
+    }
+    // 结束标签
+    function _endTag(ele) {
+        return `</${ele.name}>`;
+    }
+    // 拼接成xml字符串
+    function _toXMLStr(eles) {
+        let rst = '';
+        for (const 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*?</g, ">\n<");
+        // 调整格式
+        const reg = /\n(<(([^\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/mg;
+        const nodeStack = [];
+        const output = text.replace(reg, function ($0, all, name, isBegin, isCloseFull1, isCloseFull2, isFull1, isFull2) {
+            const 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);
+                }
+            }
+            return '\n' + prefix + all;
+        });
+        return output.substring(1);
+
+        function getPrefix(prefixIndex) {
+            const span = '    ';
+            const output = [];
+            for (let i = 0; i < prefixIndex; i++) {
+                output.push(span);
+            }
+            return output.join('');
+        }
+    }
+
+    /*
+     * 根据各自费用定额的文件结构,导出文件
+     * 每个费用定额可能导出的结果文件都不同
+     * 比如广东18需要将一个建设项目文件,多个单位工程文件打包成一个zip文件。重庆18就没这种要求
+     * @param  {Array}extractData 提取的数据
+     *         {Function}saveAsFunc 各自费用定额的导出方法,适应不同接口需要不同的最终文件形式
+     * @return {Array || void}
+     * */
+    async function exportFile(extractData, saveAsFunc = defaultSaveAs) {
+        // 获取文件数据
+        const fileData = extractData.map(extractObj => {
+            // 转换成xml字符串
+            let xmlStr = _toXMLStr([extractObj.data]);
+            // 加上xml声明
+            xmlStr = `<?xml version="1.0" encoding="utf-8"?>${xmlStr}`;
+            // 格式化
+            xmlStr = _formatXml(xmlStr);
+            const blob = new Blob([xmlStr], { type: 'text/plain;charset=utf-8' });
+            return {
+                blob: blob,
+                exportKind: extractObj.exportKind,
+                fileName: extractObj.fileName
+            };
+        });
+        if (!saveAsFunc) {
+            return fileData;
+        }
+        // 导出
+        await saveAsFunc(fileData);
+    }
+
+    /*
+    * 默认的通用导出文件方法:一个文件数据对应一个xml文件(变更后缀)
+    * @param  {Array}fileData 文件数据
+    * @return {void}
+    * */
+    async function defaultSaveAs(fileData) {
+        fileData.forEach(fileItem => saveAs(fileItem.blob, fileItem.fileName));
+    }
+
+    return {
+        CONFIG,
+        CACHE,
+        UTIL,
+        Element,
+        extractExportData,
+        exportFile,
+    };
+})();

+ 75 - 0
web/building_saas/standard_interface/export/view.js

@@ -0,0 +1,75 @@
+/**
+ *
+ *
+ * @author Zhong
+ * @date 2019/6/5
+ * @version
+ */
+//导出接口相关
+const EXPORT_VIEW = (() => {
+    'use strict';
+
+    const _base = XML_EXPORT_BASE;
+    const _cache = _base.CACHE;
+    // 导出数据缓存,为了自检完后,再导出的时候不需要重新运行相关提取程序。(暂时取消了自检,但还是留着这个缓存机制)
+    const _exportCache = [];
+    // 操作状态
+    const STATE = {
+        checking: false, // 自检
+        exporting: false, // 导出
+    };
+    // 回到初始状态,需要清空cache中的数据
+    function resetState() {
+        _exportCache = [];
+        _cache.clear();
+    }
+    //事件监听
+    function exportListener() {
+        // 导出接口
+        $('#export-confirm').click(async function () {
+            let checkedDatas = $('#export input[type="checkbox"]:checked');
+            if (!checkedDatas.length) {
+                return;
+            }
+            if (STATE.exporting) {
+                return;
+            }
+            STATE.exporting = true;
+            let pr = new SCComponent.InitProgressBar();
+            try {
+                if (!_exportCache || !_exportCache.length) {
+                    pr.start('导出数据接口', '正在导出文件,请稍候……');
+                    for (let checkedData of checkedDatas) {
+                        let fileKind = parseInt($(checkedData).val());
+                        let exportData = await _base.extractExportData(XMLStandard.entry, _base.CONFIG.GRANULARITY.PROJECT,
+                            XMLStandard.summaryObj, fileKind, projectObj.project.ID(), userID);
+                        _exportCache.push(...exportData);
+                    }
+                    if (_exportCache && _exportCache.length) {
+                        // 导出文件
+                        await _base.exportFile(_exportCache, XMLStandard.saveAsFile);
+                    }
+                }
+            } catch (err) {
+                console.log(err);
+                alert(err);
+            } finally {
+                pr.end();
+                setTimeout(() => {
+                    STATE.exporting = false;
+                }, 300);
+            }
+        });
+        //导出窗口--------
+        $('#export').on('hide.bs.modal', function () {
+            resetState();
+            STATE.checking = false;
+            STATE.exporting = false;
+            $('#export input[type="checkbox"]:eq(0)').prop('checked', true);
+        });
+        $('#export input[type="checkbox"]').click(function () {
+            resetState();
+        });
+    }
+    return { exportListener }
+})();

+ 80 - 0
web/building_saas/standard_interface/index.js

@@ -0,0 +1,80 @@
+/*
+ * @Descripttion: 招投标数据接口
+ * @Author: vian
+ * @Date: 2020-08-17 15:07:31
+ */
+
+// 用于导出的挂载变量,各地区对外接口需要作覆盖它。
+// eg: 导出接口A时,加载了scriptA,此时scriptA的对外接口INTERFACE_EXPORT = { entry }
+//     导出接口B时,加载了scriptB,此时scriptB的对外接口INTERFACE_EXPORT = { entry }
+let INTERFACE_EXPORT = {};
+// 用于导入的挂载变量,同上
+let INTERFACE_IMPORT = {};
+
+const STD_INTERFACE = (() => {
+    'use strict';
+
+    // 地区配置,key为地区,value为该地区接口的js文件名。注意:相同地区的导入导出接口js文件名称应相同。
+    const config = {
+        '安徽省@马鞍山': 'anhui_maanshan.js',
+    };
+
+    /**
+     * 动态加载script
+     * 由于后续的接口可能会非常多,一次性加载所有的接口文件完全没必要,而且不可控,很容易导致初次加载速度变慢。
+     * 在选定相关地区后,再根据地区对应的script路径,动态加载script
+     * 不需要缓存,缓存可能会影响正常操作的性能。而且导入导出时动态获取接口文件的需要的时间,客户是不可感知的。
+     * @param {String} path - 需要加载的scipt路径
+     * @return {Promise}
+     */
+    function loadScript(path) {
+        return new Promise((resolve, reject) => {
+            const body = document.getElementsByTagName('body')[0];
+            const script = document.createElement('script');
+            script.src = path;
+            script.type = 'text/javascript';
+            body.appendChild(script);
+            script.onload = script.onreadystatechange = function () { // ie、ff触发事件不同,都写上
+                if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
+                    script.onload = script.onreadystatechange = null;
+                    cache[path] = 1;
+                    resolve();
+                }
+            };
+            script.onerror = function () {
+                reject('script加载失败,请稍后重试。');
+            };
+        });
+    }
+
+    const ScriptType = {
+        EXPORT: 'export',
+        IMPORT: 'import',
+    };
+
+    let curArea = '';
+    /**
+     * 根据地区和脚本类型加载脚本
+     * @param {String} area - 地区选项 eg: '安徽省@马鞍山'
+     * @param {Number} scriptType - 脚本类型
+     * @return {Void}
+     */
+    async function loadScriptByArea(area, scriptType) {
+        if (area === curArea) {
+            return;
+        }
+        curArea = area;
+        const path = config[area];
+        if (!path) {
+            throw new Error(`[${area}]不存在有效script配置。`);
+        }
+        const fullPath = `/web/building_saas/standard_interface/${scriptType}/${path}`;
+        await loadScript(fullPath);
+    }
+
+    return {
+        ScriptType,
+        loadScriptByArea
+    }
+
+})();