Browse Source

1. 概算控制 v0.5
2. 关联台账,中间计量,附件,删除问题

MaiXinRong 3 years ago
parent
commit
51d5f914a3

+ 1 - 2
app/base/base_bills_service.js

@@ -485,7 +485,7 @@ class BaseBillsSerivce extends TreeService {
         const maxId = await this._getMaxLid(this.ctx.tender.id);
 
         const AnalysisExcel = require('../lib/analysis_excel').AnalysisGclExcelTree;
-        const analysisExcel = new AnalysisExcel(this.ctx);
+        const analysisExcel = new AnalysisExcel(this.ctx, this.setting);
         const cacheData = analysisExcel.analysisData(sheet, node, maxId, data);
         if (!cacheData) throw '导入数据错误,请检查Excel文件后重试';
 
@@ -695,7 +695,6 @@ class BaseBillsSerivce extends TreeService {
             pos: pastePosData,
         };
     }
-
 }
 
 module.exports = BaseBillsSerivce;

+ 393 - 0
app/base/base_budget_service.js

@@ -10,6 +10,9 @@
 
 
 const TreeService = require('./base_tree_service');
+const billsUtils = require('../lib/bills_utils');
+const readOnlyFields = ['id', 'bid', 'tree_id', 'tree_pid', 'order', 'level', 'full_path', 'is_leaf'];
+const calcFields = ['unit_price', 'quantity', 'total_price'];
 
 class BaseBudget extends TreeService {
 
@@ -55,12 +58,402 @@ class BaseBudget extends TreeService {
                 is_leaf: tmp.is_leaf,
                 code: tmp.code,
                 name: tmp.name,
+                unit: tmp.unit,
                 node_type: tmp.node_type,
             });
         }
         const operate = await conn.insert(this.tableName, insertData);
         return operate.affectedRows === data.length;
     }
+
+    async addChild(budgetId, selectId, data) {
+        if ((budgetId <= 0) || (selectId <= 0)) return [];
+        const selectData = await this.getDataByKid(budgetId, selectId);
+        if (!selectData) {
+            throw '新增节点数据错误';
+        }
+        const children = await this.getChildrenByParentId(budgetId, selectId);
+
+        const maxId = await this._getMaxLid(budgetId);
+
+        data.id = this.uuid.v4();
+        data.bid = budgetId;
+        data.tree_id = maxId + 1;
+        data.tree_pid = selectData.tree_id;
+        data.level = selectData.level + 1;
+        data.order = children.length + 1;
+        data.full_path = selectData.full_path + '-' + data.tree_id;
+        data.is_leaf = true;
+
+        this.transaction = await this.db.beginTransaction();
+        try {
+            const result = await this.transaction.insert(this.tableName, data);
+            if (children.length === 0) {
+                await this.transaction.update(this.tableName,
+                    { is_leaf: false, quantity: 0, unit_price: 0, total_price: 0 },
+                    { where: { bid: budgetId, tree_id: selectData.tree_id } });
+            }
+            await this.transaction.commit();
+        } catch(err) {
+            this.transaction.rollback();
+            throw err;
+        }
+
+        this._cacheMaxLid(budgetId, maxId + 1);
+
+        // 查询应返回的结果
+        const resultData = {};
+        resultData.create = await this.getDataByKid(budgetId, data.tree_id);
+        if (children.length === 0) resultData.update = await this.getDataByKid(budgetId, selectId);
+        return resultData;
+    }
+
+    _filterStdData(stdData) {
+        const result = {
+            name: stdData.name,
+            unit: stdData.unit,
+            node_type: stdData.node_type,
+        };
+        result.code = stdData.code ? stdData.code : '';
+        result.b_code = stdData.b_code ? stdData.b_code : '';
+        return result;
+    }
+
+    async _addChildNodeData(budgetId, parentData, data) {
+        if (budgetId <= 0) return undefined;
+        if (!data) data = {};
+        const pid = parentData ? parentData.tree_id : this.rootId;
+
+        const maxId = await this._getMaxLid(budgetId);
+
+        data.id = this.uuid.v4();
+        data.bid = budgetId;
+        data.tree_id = maxId + 1;
+        data.tree_pid = pid;
+        if (data.order === undefined) data.order = 1;
+        data.level = parentData ? parentData.level + 1 : 1;
+        data.full_path = parentData ? parentData.full_path + '-' + data.tree_id : '' + data.tree_id;
+        if (data.is_leaf === undefined) data.is_leaf = true;
+        const result = await this.transaction.insert(this.tableName, data);
+
+        this._cacheMaxLid(budgetId, maxId + 1);
+
+        return [result, data];
+    }
+
+    async _addChildAutoOrder(budgetId, parentData, data) {
+        const findPreData = function(list, a) {
+            if (!list || list.length === 0) { return null; }
+            for (let i = 0, iLen = list.length; i < iLen; i++) {
+                if (billsUtils.compareCode(list[i].code, a.code) > 0) {
+                    return i > 0 ? list[i - 1] : null;
+                }
+            }
+            return list[list.length - 1];
+        };
+
+        const pid = parentData ? parentData.tree_id : this.rootId;
+        const children = await this.getChildrenByParentId(budgetId, pid);
+        const preData = findPreData(children, data);
+        if (!preData || children.indexOf(preData) < children.length - 1) {
+            await this._updateChildrenOrder(budgetId, pid, preData ? preData.order + 1 : 1);
+        }
+        data.order = preData ? preData.order + 1 : 1;
+        const [addResult, node] = await this._addChildNodeData(budgetId, parentData, data);
+
+        return [addResult, node];
+    }
+
+    async addStdNodeWithParent(budgetId, stdData, stdLib) {
+        // 查询完整标准清单,并按层次排序
+        const fullLevel = await stdLib.getFullLevelDataByFullPath(stdData.list_id, stdData.full_path);
+        fullLevel.sort(function(x, y) {
+            return x.level - y.level;
+        });
+
+        let isNew = false,
+            node,
+            firstNew,
+            updateParent,
+            addResult;
+        const expandIds = [];
+        this.transaction = await this.db.beginTransaction();
+        try {
+            // 从最顶层节点依次查询是否存在,否则添加
+            for (let i = 0, len = fullLevel.length; i < len; i++) {
+                const stdNode = fullLevel[i];
+
+                if (isNew) {
+                    const newData = this._filterStdData(stdNode);
+                    newData.is_leaf = (i === len - 1);
+                    [addResult, node] = await this._addChildNodeData(budgetId, node, newData);
+                } else {
+                    const parent = node;
+                    node = await this.getDataByCondition({
+                        bid: budgetId,
+                        tree_pid: parent ? parent.tree_id : this.rootId,
+                        code: stdNode.code,
+                        name: stdNode.name,
+                    });
+                    if (!node) {
+                        isNew = true;
+                        const newData = this._filterStdData(stdNode);
+                        newData.is_leaf = (i === len - 1);
+                        [addResult, node] = await this._addChildAutoOrder(budgetId, parent, newData);
+                        if (parent && parent.is_leaf) {
+                            await this.transaction.update(this.tableName, { id: parent.id, is_leaf: false,
+                                unit_price: 0, quantity: 0, total_price: 0});
+                            updateParent = parent;
+                        }
+                        firstNew = node;
+                    } else {
+                        expandIds.push(node.tree_id);
+                    }
+                }
+            }
+            await this.transaction.commit();
+        } catch (err) {
+            await this.transaction.rollback();
+            throw err;
+        }
+
+        // 查询应返回的结果
+        let createData = [],
+            updateData = [];
+        if (firstNew) {
+            createData = await this.getDataByFullPath(budgetId, firstNew.full_path + '%');
+            updateData = await this.getNextsData(budgetId, firstNew.tree_pid, firstNew.order);
+            if (updateParent) {
+                updateData.push(await this.getDataByCondition({ id: updateParent.id }));
+            }
+        }
+        return { create: createData, update: updateData };
+    }
+
+    async addBillsNode(budgetId, selectId, data) {
+        return await this.addNode(budgetId, selectId, data ? data : {});
+    }
+
+    async addStdNode(budgetId, selectId, stdData) {
+        const newData = this._filterStdData(stdData);
+        const result = await this.addBillsNode(budgetId, selectId, newData);
+        return result;
+    }
+
+    async addStdNodeAsChild(budgetId, selectId, stdData) {
+        const newData = this._filterStdData(stdData);
+        const result = await this.addChild(budgetId, selectId, newData);
+        return result;
+    }
+
+    _filterUpdateInvalidField(id, data) {
+        const result = { id };
+        for (const prop in data) {
+            if (readOnlyFields.indexOf(prop) === -1) result[prop] = data[prop];
+        }
+        return result;
+    }
+    _checkCalcField(data) {
+        for (const prop in data) {
+            if (calcFields.indexOf(prop) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    async updateCalc(budgetId, data) {
+        const helper = this.ctx.helper;
+        // 简单验证数据
+        if (budgetId <= 0 || !this.ctx.budget) throw '标段不存在';
+        if (!data) throw '提交数据错误';
+        const datas = data instanceof Array ? data : [data];
+        const ids = [];
+        for (const row of datas) {
+            if (budgetId !== row.bid) throw '提交数据错误';
+            ids.push(row.id);
+        }
+
+        this.transaction = await this.db.beginTransaction();
+        try {
+            for (const row of datas) {
+                const updateNode = await this.getDataById(row.id);
+                if (!updateNode || budgetId !== updateNode.bid || row.tree_id !== updateNode.tree_id) throw '提交数据错误';
+                let updateData;
+
+                // 项目节、工程量清单相关
+                if (row.b_code) {
+                    row.dgn_qty1 = 0;
+                    row.dgn_qty2 = 0;
+                    row.code = '';
+                }
+                if (row.code) row.b_code = '';
+
+                if (this._checkCalcField(row)) {
+                    console.log(row);
+                    let calcData = JSON.parse(JSON.stringify(row));
+                    if (row.quantity !== undefined || row.unit_price !== undefined) {
+                        calcData.quantity = row.quantity === undefined ? updateNode.quantity : helper.round(row.quantity, 2);
+                        calcData.unit_price = row.unit_price === undefined ? updateNode.unit_price : helper.round(row.unit_price, 2);
+                        calcData.total_price = helper.mul(calcData.quantity, calcData.unit_price, 2);
+                    } else if (row.total_price !== undefined ) {
+                        calcData.quantity = 0;
+                        calcData.total_price = helper.round(row.total_price, 2);
+                    }
+                    updateData = this._filterUpdateInvalidField(updateNode.id, calcData);
+                } else {
+                    updateData = this._filterUpdateInvalidField(updateNode.id, row);
+                }
+                await this.transaction.update(this.tableName, updateData);
+            }
+            await this.transaction.commit();
+            this.transaction = null;
+        } catch (err) {
+            await this.transaction.rollback();
+            this.transaction = null;
+            throw err;
+        }
+
+        return { update: await this.getDataById(ids) };
+    }
+    async pasteBlockData (bid, sid, pasteData, defaultData) {
+        if ((bid <= 0) || (sid <= 0)) return [];
+
+        if (!pasteData || pasteData.length <= 0) throw '复制数据错误';
+        for (const pd of pasteData) {
+            if (!pd || pd.length <= 0) throw '复制数据错误';
+            pd.sort(function (x, y) {
+                return x.level - y.level
+            });
+            if (pd[0].tree_pid !== pasteData[0][0].tree_pid) throw '复制数据错误:仅可操作同层节点';
+        }
+        this.newBills = false;
+        const selectData = await this.getDataByKid(bid, sid);
+        if (!selectData) throw '粘贴数据错误';
+        const newParentPath = selectData.full_path.replace(selectData.tree_id, '');
+
+        const pasteBillsData = [];
+        let maxId = await this._getMaxLid(bid);
+        for (const [i, pd] of pasteData.entries()) {
+            for (const d of pd) {
+                d.children = pd.filter(function (x) {
+                    return x.tree_pid === d.tree_id;
+                });
+            }
+            const pbd = [];
+            for (const [j, d] of pd.entries()) {
+                const newBills = {
+                    id: this.uuid.v4(),
+                    bid: bid,
+                    tree_id: maxId + j + 1,
+                    tree_pid: j === 0 ? selectData.tree_pid : d.tree_pid,
+                    level: d.level + selectData.level - pd[0].level,
+                    order: j === 0 ? selectData.order + i + 1 : d.order,
+                    is_leaf: d.is_leaf,
+                    code: d.code,
+                    b_code: d.b_code,
+                    name: d.name,
+                    unit: d.unit,
+                    source: d.source,
+                    remark: d.remark,
+                    drawing_code: d.drawing_code,
+                    memo: d.memo,
+                    node_type: d.node_type,
+                };
+                for (const c of d.children) {
+                    c.tree_pid = newBills.tree_id;
+                }
+                if (d.b_code) {
+                    newBills.unit_price = this.ctx.helper.round(d.unit_price, 2);
+                    newBills.quantity = this.ctx.helper.round(d.quantity, 2);
+                    newBills.total_price = this.ctx.helper.mul(newBills.quantity, newBills.unit_price, 2);
+                } else {
+                    newBills.dgn_qty1 = this.ctx.helper.round(d.dgn_qty1, 2);
+                    newBills.dgn_qty2 = this.ctx.helper.round(d.dgn_qty2, 2);
+                    newBills.total_price = this.ctx.helper.round(d.total_price, 2);
+                }
+                if (defaultData) this.ctx.helper._.assignIn(newBills, defaultData);
+                pbd.push(newBills);
+            }
+            for (const d of pbd) {
+                const parent = pbd.find(function (x) {
+                    return x.tree_id === d.tree_pid;
+                });
+                d.full_path = parent
+                    ? parent.full_path + '-' + d.tree_id
+                    : newParentPath + d.tree_id;
+                if (defaultData) this.ctx.helper._.assignIn(pbd, defaultData);
+                pasteBillsData.push(d);
+            }
+            maxId = maxId + pbd.length;
+        }
+
+        this.transaction = await this.db.beginTransaction();
+        try {
+            // 选中节点的所有后兄弟节点,order+粘贴节点个数
+            await this._updateChildrenOrder(bid, selectData.tree_pid, selectData.order + 1, pasteData.length);
+            // 数据库创建新增节点数据
+            if (pasteBillsData.length > 0) await this.transaction.insert(this.tableName, pasteBillsData);
+            this._cacheMaxLid(bid, maxId);
+            await this.transaction.commit();
+        } catch (err) {
+            await this.transaction.rollback();
+            throw err;
+        }
+
+        // 查询应返回的结果
+        const updateData = await this.getNextsData(selectData.bid, selectData.tree_pid, selectData.order + pasteData.length);
+        return { create: pasteBillsData, update: updateData };
+    }
+
+
+    async importExcel(templateId, excelData, needGcl, filter) {
+        console.log(needGcl);
+        const AnalysisExcel = require('../lib/analysis_excel').AnalysisExcelTree;
+        const analysisExcel = new AnalysisExcel(this.ctx, this.setting);
+        const tempData = await this.ctx.service.tenderNodeTemplate.getData(templateId, true);
+        const cacheTree = analysisExcel.analysisData(excelData, tempData, {
+            filterZeroGcl: filter, filterPos: true, filterGcl: !needGcl, filterCalc: true,
+        });
+        const orgMaxId = await this._getMaxLid(this.ctx.budget.id);
+        const transaction = await this.db.beginTransaction();
+        try {
+            await transaction.delete(this.tableName, { bid: this.ctx.budget.id });
+            const datas = [];
+            for (const node of cacheTree.items) {
+                const data = {
+                    id: node.id,
+                    bid: this.ctx.budget.id,
+                    tree_id: node.tree_id,
+                    tree_pid: node.tree_pid,
+                    level: node.level,
+                    order: node.order,
+                    is_leaf: !node.children || node.children.length === 0,
+                    full_path: node.full_path,
+                    code: node.code || '',
+                    b_code: node.b_code || '',
+                    name: node.name || '',
+                    unit: node.unit || '',
+                    unit_price: !node.children || node.children.length === 0 ? node.unit_price || 0 : 0,
+                    dgn_qty1: node.dgn_qty1 || 0,
+                    dgn_qty2: node.dgn_qty2 || 0,
+                    memo: node.memo,
+                    drawing_code: node.drawing_code,
+                    node_type: node.node_type,
+                    quantity: !node.children || node.children.length === 0 ? node.quantity || 0 : 0,
+                    total_price: !node.children || node.children.length === 0 ? node.total_price || 0 : 0,
+                };
+                datas.push(data);
+            }
+            await transaction.insert(this.tableName, datas);
+            await transaction.commit();
+            if (orgMaxId) this._cacheMaxLid(this.ctx.budget.id, cacheTree.keyNodeId);
+            return datas;
+        } catch (err) {
+            await transaction.rollback();
+            throw err;
+        }
+    }
 }
 
 module.exports = BaseBudget;

+ 1 - 0
app/base/base_tree_service.js

@@ -33,6 +33,7 @@ class TreeService extends Service {
      */
     constructor(ctx, setting) {
         super(ctx);
+        this.rootId = -1;
         this.tableName = setting.tableName;
         this.setting = setting;
         // 以下字段仅可通过树结构操作改变,不可直接通过update方式从接口提交,发现时过滤

+ 159 - 17
app/controller/budget_controller.js

@@ -7,7 +7,13 @@
  * @date 2021/10/27
  * @version
  */
-
+const stdDataAddType = {
+    withParent: 1,
+    child: 2,
+    next: 3,
+};
+const auditConst = require('../const/audit');
+const LzString = require('lz-string');
 module.exports = app => {
     class BudgetController extends app.BaseController {
 
@@ -21,12 +27,14 @@ module.exports = app => {
             try {
                 const renderData = {
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.budget.list),
+                    auditConst,
                 };
                 renderData.budgetList = await ctx.service.budget.getAllDataByCondition({
                     where: { pid: ctx.session.sessionProject.id },
                     orders: [['name', 'asc']],
                 });
                 renderData.budgetStd = await ctx.service.budgetStd.getDataByProjectId(ctx.session.sessionProject.id);
+                renderData.tenderList = await ctx.service.tender.getList4Select('stage');
                 await this.layout('budget/list.ejs', renderData, 'budget/list_modal.ejs');
             } catch (err) {
                 ctx.log(err);
@@ -47,11 +55,33 @@ module.exports = app => {
         }
 
         async del(ctx) {
-
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.id) throw '参数有误';
+                const result = await ctx.service.budget.deleteBudgetNoBackup(data.id);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '删除项目失败');
+            }
         }
 
         async save(ctx) {
-
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.id) throw '参数有误';
+                let result = null;
+                if (data.name !== undefined) {
+                    if (!data.name || data.name.length > 100) throw '项目名称有误';
+                    result = await ctx.service.budget.save({ id: data.id, name: data.name });
+                } else if (data.rela_tender !== undefined) {
+                    result = await ctx.service.budget.save({ id: data.id, rela_tender: data.rela_tender });
+                }
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '保存数据失败');
+            }
         }
 
         async compare(ctx) {
@@ -68,14 +98,15 @@ module.exports = app => {
         _getSpreadSetting(type) {
             const spreadSetting = {
                 cols: [
-                    {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 150, formatter: '@', cellType: 'tree'},
-                    {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 230, formatter: '@'},
+                    {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 180, formatter: '@', cellType: 'tree'},
+                    {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 120, formatter: '@'},
                     {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@'},
                     {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
-                    {title: '清单数量', colSpan: '1', rowSpan: '2', field: 'quantity', hAlign: 2, width: 80, type: 'Number'},
                     {title: '设计数量|数量1', colSpan: '2|1', rowSpan: '1|1', field: 'dgn_qty1', hAlign: 2, width: 80, type: 'Number'},
                     {title: '|数量2', colSpan: '|1', rowSpan: '|1', field: 'dgn_qty2', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '经济指标', colSpan: '1', rowSpan: '2', field: 'dgn_price', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '经济指标', colSpan: '1', rowSpan: '2', field: 'dgn_price', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '清单数量', colSpan: '1', rowSpan: '2', field: 'quantity', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 80, type: 'Number'},
                     {title: '金额', colSpan: '1', rowSpan: '2', field: 'total_price', hAlign: 2, width: 80, type: 'Number'},
                     {title: '图册号', colSpan: '1', rowSpan: '2', field: 'drawing_code', hAlign: 0, width: 100, formatter: '@'},
                     {title: '备注', colSpan: '1', rowSpan: '2', field: 'memo', hAlign: 0, width: 100, formatter: '@'},
@@ -86,32 +117,34 @@ module.exports = app => {
                 defaultRowHeight: 21,
                 headerFont: '12px 微软雅黑',
                 font: '12px 微软雅黑',
-                readOnly: true,
             };
             // todo 根据设置判断预算是否需要清单
             if (type !== 'yu') {
                 spreadSetting.cols = spreadSetting.cols.filter(x => {
-                    return ['b_code', 'quantity'].indexOf(x.field) < 0;
+                    return ['b_code', 'quantity', 'unit_price'].indexOf(x.field) < 0;
                 });
             }
             return spreadSetting;
         }
         _getRelaService(type) {
             switch(type) {
-                case 'gu': return ctx.serivce.budgetGu;
-                case 'gai': return ctx.serivce.budgetGai;
-                case 'yu': return ctx.serivce.budgetYu;
+                case 'gu': return this.ctx.service.budgetGu;
+                case 'gai': return this.ctx.service.budgetGai;
+                case 'yu': return this.ctx.service.budgetYu;
                 default: return null;
             }
         }
-
+        _getNeedGcl() {
+            if (!this.ctx.params.btype) throw '参数错误';
+            return this.ctx.params.btype === 'yu';
+        }
 
         async detail(ctx) {
             try {
                 const renderData = {
                     spreadSetting: this._getSpreadSetting(ctx.params.btype),
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.budget.detail),
-                    needGcl: ctx.params.btype === 'yu',
+                    needGcl: this._getNeedGcl(),
                 };
                 [renderData.stdBills, renderData.stdChapters] = await ctx.service.budgetStd.getStdList(ctx.budget.std_id, ctx.params.btype);
                 await this.layout('budget/detail.ejs', renderData, 'budget/detail_modal.ejs');
@@ -124,8 +157,7 @@ module.exports = app => {
             try {
                 const relaService = this._getRelaService(ctx.params.btype);
                 ctx.body = {
-                    err: 0,
-                    msg: '',
+                    err: 0, msg: '',
                     data: await relaService.getData(ctx.budget.id),
                 }
             } catch (err) {
@@ -134,12 +166,122 @@ module.exports = app => {
             }
         }
 
+        async _billsBase(relaService, type, data) {
+            if (isNaN(data.id) || data.id <= 0) throw '数据错误';
+            if (type !== 'add') {
+                if (isNaN(data.count) || data.count <= 0) data.count = 1;
+            }
+            switch (type) {
+                case 'add':
+                    return await relaService.addNodeBatch(this.ctx.budget.id, data.id, {}, data.count);
+                case 'delete':
+                    return await relaService.delete(this.ctx.budget.id, data.id, data.count);
+                case 'up-move':
+                    return await relaService.upMoveNode(this.ctx.budget.id, data.id, data.count);
+                case 'down-move':
+                    return await relaService.downMoveNode(this.ctx.budget.id, data.id, data.count);
+                case 'up-level':
+                    return await relaService.upLevelNode(this.ctx.budget.id, data.id, data.count);
+                case 'down-level':
+                    return await relaService.downLevelNode(this.ctx.budget.id, data.id, data.count);
+            }
+        }
+        async _addStd(relaService, data) {
+            if ((isNaN(data.id) || data.id <= 0) || !data.stdType || !data.stdNode) throw '参数错误';
+
+            let stdLib, addType;
+            switch (data.stdType) {
+                case 'xmj':
+                    stdLib = this.ctx.service.stdXmj;
+                    addType = stdDataAddType.withParent;
+                    break;
+                case 'gcl':
+                    stdLib = this.ctx.service.stdGcl;
+                    const selectNode = await relaService.getDataByKid(this.ctx.budget.id, data.id);
+                    addType = selectNode.b_code ? stdDataAddType.next : stdDataAddType.child;
+                    break;
+                default:
+                    throw '未知标准库';
+            }
+            const stdData = await stdLib.getDataByDataId(data.stdLibId, data.stdNode);
+            switch (addType) {
+                case stdDataAddType.child:
+                    return await relaService.addStdNodeAsChild(this.ctx.budget.id, data.id, stdData);
+                case stdDataAddType.next:
+                    return await relaService.addStdNode(this.ctx.budget.id, data.id, stdData);
+                case stdDataAddType.withParent:
+                    return await relaService.addStdNodeWithParent(this.ctx.budget.id, stdData, stdLib);
+                default:
+                    throw '未知添加方式';
+            }
+        }
+        async _pasteBlock(relaService, data) {
+            if ((isNaN(data.id) || data.id <= 0) ||
+                (!data.tid && data.tid <= 0) ||
+                (!data.block || data.block.length <= 0)) throw '参数错误';
+            return await relaService.pasteBlockData(this.ctx.budget.id, data.id, data.block);
+        }
         async detailUpdate(ctx) {
+            try {
+                const relaService = this._getRelaService(ctx.params.btype);
+                if (!ctx.budget) throw '项目数据错误';
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.postType || !data.postData) throw '数据错误';
+                const responseData = { err: 0, msg: '', data: {} };
 
+                switch (data.postType) {
+                    case 'add':
+                    case 'delete':
+                    case 'up-move':
+                    case 'down-move':
+                    case 'up-level':
+                    case 'down-level':
+                        responseData.data = await this._billsBase(relaService, data.postType, data.postData);
+                        break;
+                    case 'update':
+                        ctx.helper.checkDgnQtyPrecision(data.postData);
+                        responseData.data = await relaService.updateCalc(ctx.budget.id, data.postData);
+                        break;
+                    case 'add-std':
+                        responseData.data = await this._addStd(relaService, data.postData);
+                        break;
+                    case 'paste-block':
+                        responseData.data = await this._pasteBlock(relaService, data.postData);
+                        break;
+                    default:
+                        throw '未知操作';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
         }
 
         async detailUploadExcel(ctx) {
-
+            try {
+                const relaService = this._getRelaService(ctx.params.btype);
+                const needGcl = this._getNeedGcl();
+                const ueType = ctx.params.ueType;
+                const compressData = ctx.request.body.data;
+                const data = JSON.parse(LzString.decompressFromUTF16(compressData));
+                const responseData = { err: 0, msg: '', data: {} };
+                switch (ueType) {
+                    case 'tz':
+                        const templateId = await this.ctx.service.budgetStd.getTemplateId(this.ctx.budget.std_id, ctx.params.btype);
+                        responseData.data = await relaService.importExcel(templateId, data.sheet, needGcl, data.filter);
+                        break;
+                    case 'gcl2xmj':
+                        responseData.data = await relaService.importGclExcel(data.id, data.sheet);
+                        break;
+                    default:
+                        throw '数据错误';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
         }
     }
 

+ 0 - 1
app/controller/revise_controller.js

@@ -83,7 +83,6 @@ module.exports = app => {
                     }
                 }
                 const [addVisible, addValid] = await this._getAddReviseValid(ctx);
-                console.log(addVisible, addValid);
                 const [stdBills, stdChapters] = await this.ctx.service.valuation.getValuationStdList(
                     ctx.tender.data.valuation, ctx.tender.data.measure_type);
                 const renderData = {

+ 1 - 1
app/extend/helper.js

@@ -750,7 +750,7 @@ module.exports = {
      * @return {*}
      */
     round(value, decimal) {
-        return value ? new Decimal(value).toDecimalPlaces(decimal).toNumber() : null;
+        return value ? new Decimal(value).toDecimalPlaces(decimal).toNumber() : 0;
     },
     /**
      * 汇总

+ 95 - 54
app/lib/analysis_excel.js

@@ -62,8 +62,20 @@ class ImportBaseTree {
      * 构造函数
      * @param {Array} tempData - 清单模板数据
      */
-    constructor (tempData, ctx) {
+    constructor (tempData, ctx, setting) {
         this.ctx = ctx;
+        if (ctx.tender) {
+            this.mid = ctx.tender.id;
+            this.decimal = ctx.tender.info.decimal;
+            this.precision = ctx.tender.info.precision;
+        } else if (ctx.budget) {
+            this.mid = ctx.budget.id;
+            this.decimal = { up: 2, tp: 2};
+            this.precision = {
+                other: { value: 2 },
+            };
+        }
+        this.setting = setting;
         // 常量
         this.splitChar = '-';
         // 索引
@@ -91,10 +103,11 @@ class ImportBaseTree {
      * @private
      */
     _loadTemplateTree(data) {
+        const self = this;
         let loadCodeNodes = true;
         for (const node of data) {
-            node.ledger_id = node.template_id;
-            node.ledger_pid = node.pid;
+            node[this.setting.kid] = node.template_id;
+            node[this.setting.pid] = node.pid;
             node.id = this.ctx.app.uuid.v4();
             delete node.pid;
             if (node.code && loadCodeNodes) {
@@ -104,19 +117,19 @@ class ImportBaseTree {
                 loadCodeNodes = false;
             }
             this.items.push(node);
-            if (node.ledger_pid === -1) {
+            if (node[this.setting.pid] === -1) {
                 this.roots.push(node);
             }
-            if (node.ledger_id >= this.keyNodeId) {
-                this.keyNodeId = node.ledger_id + 1;
+            if (node[this.setting.kid] >= this.keyNodeId) {
+                this.keyNodeId = node[this.setting.kid] + 1;
             }
             this.tempData.push(node);
-            this.nodes[node.ledger_id] = node;
+            this.nodes[node[this.setting.kid]] = node;
         }
         for (const node of this.items) {
-            node.tender_id = this.ctx.tender.id;
+            node[this.setting.mid] = this.mid;
             node.children = this.items.filter(function (i) {
-                return i.ledger_pid === node.ledger_id;
+                return i[self.setting.pid] === node[self.setting.kid];
             });
         }
     }
@@ -175,12 +188,12 @@ class ImportBaseTree {
      */
     addNodeWithParent(node, parent) {
         node.id = this.ctx.app.uuid.v4();
-        node.ledger_id = this.keyNodeId;
+        node[this.setting.kid] = this.keyNodeId;
         this.keyNodeId += 1;
-        node.ledger_pid = parent ? parent.ledger_id : -1;
-        node.level = parent ? parent.level + 1 : 1;
-        node.order = parent ? parent.children.length + 1 : this.roots.length + 1;
-        node.full_path = parent ? parent.full_path + '-' + node.ledger_id : '' + node.ledger_id;
+        node[this.setting.pid] = parent ? parent[this.setting.kid] : -1;
+        node[this.setting.level] = parent ? parent[this.setting.level] + 1 : 1;
+        node[this.setting.order] = parent ? parent.children.length + 1 : this.roots.length + 1;
+        node[this.setting.fullPath] = parent ? parent[this.setting.fullPath] + '-' + node[this.setting.kid] : '' + node[this.setting.kid];
         if (parent) {
             parent.children.push(node);
         } else {
@@ -197,9 +210,9 @@ class ImportBaseTree {
      * @param {Object} node - 当前添加的节点
      */
     defineCacheData(node) {
-        this.nodes[node.ledger_id] = node;
+        this.nodes[node[this.setting.kid]] = node;
         this.finalNode = node;
-        this.finalPrecision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, node.unit);
+        this.finalPrecision = this.ctx.helper.findPrecision(this.precision, node.unit);
         if (node.code) {
             this.finalXmjNode = node;
         }
@@ -222,7 +235,7 @@ class ImportBaseTree {
      */
     addXmjNode(node) {
         node.id = this.ctx.app.uuid.v4();
-        node.tender_id = this.ctx.tender.id;
+        node[this.setting.mid] = this.mid;
         node.children = [];
         if (node.code.split(this.splitChar).length > 1) {
             const temp = this.findTempData(node);
@@ -259,7 +272,7 @@ class ImportBaseTree {
      */
     addGclNode(node) {
         node.id = this.ctx.app.uuid.v4();
-        node.tender_id = this.ctx.tender.id;
+        node[this.setting.mid] = this.mid;
         node.pos = [];
         node.children = [];
         if (this.finalXmjNode) {
@@ -316,7 +329,7 @@ class ImportBaseTree {
             }
         });
         for (const [i, c] of firstPart.children.entries()) {
-            c.order = i + 1;
+            c[this.setting.order] = i + 1;
         }
     }
 
@@ -330,7 +343,7 @@ class ImportBaseTree {
             if (!node.pos || node.pos.length === 0) { continue; }
             node.quantity = this.ctx.helper.sum(_.map(node.pos, 'quantity'));
             if (node.quantity && node.unit_price) {
-                node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.ctx.tender.info.decimal.tp);
+                node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.decimal.tp);
             } else {
                 node.total_price = null;
             }
@@ -371,7 +384,7 @@ class ImportStd18Tree extends ImportBaseTree {
         let parent = this.cacheMainXmjNode;
         while (parent) {
             if (this._checkParent(parent, code)) return parent;
-            parent = this.nodes[parent.ledger_pid];
+            parent = this.nodes[parent[this.setting.pid]];
         }
         return null;
     }
@@ -385,7 +398,7 @@ class ImportStd18Tree extends ImportBaseTree {
         let parent = this.cacheSubXmjNode;
         while (parent && parent.is_sub && parent.code.match(subReg)) {
             if (this._checkParent(parent, code)) return parent;
-            parent = this.nodes[parent.ledger_pid];
+            parent = this.nodes[parent[this.setting.pid]];
         }
         return this.cacheMainXmjNode;
     }
@@ -442,7 +455,6 @@ class ImportStd18Tree extends ImportBaseTree {
         if (!node.code || (!node.code.match(mainReg) && !node.code.match(subReg))) return null;
 
         node.id = this.ctx.app.uuid.v4();
-        node.tender_id = this.ctx.tender.id;
         node.children = [];
         if ((specCode106.code.indexOf(node.code) >= 0)) {
             if (this.cacheSpecMainXmj2 && this.cacheSpecMainXmj2.code.match(specCode106.reg))
@@ -470,9 +482,20 @@ class AnalysisExcelTree {
     /**
      * 构造函数
      */
-    constructor(ctx) {
+    constructor(ctx, setting) {
         this.ctx = ctx;
-        this.decimal = ctx.tender.info.decimal;
+        this.setting = setting;
+        if (ctx.tender) {
+            this.mid = ctx.tender.id;
+            this.decimal = ctx.tender.info.decimal;
+            this.precision = ctx.tender.info.precision;
+        } else if (ctx.budget) {
+            this.mid = ctx.budget.id;
+            this.decimal = { up: 2, tp: 2};
+            this.precision = {
+                other: { value: 2 },
+            };
+        }
         this.colsDef = null;
         this.colHeaderMatch = {
             code: {value: ['项目节编号', '预算项目节'], type: colDefineType.match},
@@ -484,6 +507,7 @@ class AnalysisExcelTree {
             dgn_qty1: {value: ['设计数量1'], type: colDefineType.match},
             dgn_qty2: {value: ['设计数量2'], type: colDefineType.match},
             unit_price: {value: ['单价'], type: colDefineType.match},
+            total_price: {value: ['金额', '合价'], type: colDefineType.match},
             drawing_code: {value: ['图号'], type: colDefineType.match},
             memo: {value: ['备注'], type: colDefineType.match},
         };
@@ -504,11 +528,11 @@ class AnalysisExcelTree {
     _getNewCacheTree(tempData) {
         // 模板符合11编办规则,使用11编办树
         if (this._isMatch18(tempData)) {
-            return new ImportStd18Tree(tempData, this.ctx);
+            return new ImportStd18Tree(tempData, this.ctx, this.setting);
         // 反之使用11编办(未校验模板是否符合,替换注释部分即可实现)
         // } else if (this._isMatch11(tempData)){
         } else {
-            return new ImportBaseTree(tempData, this.ctx);
+            return new ImportBaseTree(tempData, this.ctx, this.setting);
         }
     }
 
@@ -524,18 +548,11 @@ class AnalysisExcelTree {
             node.code = this.ctx.helper.replaceReturn(row[this.colsDef.code]);
             node.name = this.ctx.helper.replaceReturn(row[this.colsDef.name]);
             node.unit = this.ctx.helper.replaceReturn(row[this.colsDef.unit]);
-            const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, node.unit);
-            node.quantity = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.quantity]), precision.value);
             node.dgn_qty1 = aeUtils.toNumber(row[this.colsDef.dgn_qty1]);
             node.dgn_qty2 = aeUtils.toNumber(row[this.colsDef.dgn_qty2]);
-            node.unit_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.unit_price]), this.decimal.up);
+            node.total_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.total_price]), this.decimal.tp);
             node.drawing_code = this.ctx.helper.replaceReturn(row[this.colsDef.drawing_code]);
             node.memo = this.ctx.helper.replaceReturn(row[this.colsDef.memo]);
-            if (node.quantity && node.unit_price) {
-                node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.ctx.tender.info.decimal.tp);
-            } else {
-                node.total_price = null;
-            }
             return this.cacheTree.addXmjNode(node);
         } catch (error) {
             console.log(error);
@@ -559,24 +576,23 @@ class AnalysisExcelTree {
      * @private
      */
     _loadGclNode(row) {
+        if (this.filter.filterGcl) return true;
         const node = {};
         node.b_code = this.ctx.helper.replaceReturn(row[this.colsDef.b_code]);
         node.name = this.ctx.helper.replaceReturn(row[this.colsDef.name]);
         node.unit = this.ctx.helper.replaceReturn(row[this.colsDef.unit]);
-        const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, node.unit);
+        const precision = this.ctx.helper.findPrecision(this.precision, node.unit);
         node.quantity = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.quantity]), precision.value);
         node.sgfh_qty = node.quantity;
         node.unit_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.unit_price]), this.decimal.up);
         node.drawing_code = this.ctx.helper.replaceReturn(row[this.colsDef.drawing_code]);
         node.memo = this.ctx.helper.replaceReturn(row[this.colsDef.memo]);
         if (node.quantity && node.unit_price) {
-            node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.ctx.tender.info.decimal.tp);
+            node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.decimal.tp);
         } else {
             node.total_price = null;
         }
-        if (this.filter && !node.quantity && !node.total_price) {
-            return this.filter;
-        }
+        if (this.filter.filterZeroGcl && !node.quantity && !node.total_price) return true;
         return this.cacheTree.addGclNode(node);
     }
     /**
@@ -586,6 +602,7 @@ class AnalysisExcelTree {
      * @private
      */
     _loadPos(row) {
+        if (this.filter.filterPos) return true;
         const pos = {};
         pos.name = this.ctx.helper.replaceReturn(row[this.colsDef.name]);
         pos.quantity = aeUtils.toNumber(row[this.colsDef.quantity]);
@@ -641,7 +658,7 @@ class AnalysisExcelTree {
      * @returns {ImportBaseTree}
      */
     analysisData(sheet, tempData, filter) {
-        this.filter = filter;
+        this.filter = filter ? filter : {};
         this.colsDef = null;
         this.cacheTree = this._getNewCacheTree(tempData);
         this.errorData = [];
@@ -656,7 +673,7 @@ class AnalysisExcelTree {
             }
         }
         this.cacheTree.resortFirstPartChildren();
-        this.cacheTree.calculateLeafWithPos();
+        if (!this.filter.filterCalc) this.cacheTree.calculateLeafWithPos();
         return this.cacheTree;
     }
 }
@@ -666,8 +683,20 @@ class ImportGclBaseTree {
      * 构造函数
      * @param {Array} tempData - 清单模板数据
      */
-    constructor (ctx, parent, maxId, defaultData) {
+    constructor (ctx, setting, parent, maxId, defaultData) {
         this.ctx = ctx;
+        this.setting = setting;
+        if (ctx.tender) {
+            this.mid = ctx.tender.id;
+            this.decimal = ctx.tender.info.decimal;
+            this.precision = ctx.tender.info.precision;
+        } else if (ctx.budget) {
+            this.mid = ctx.budget.id;
+            this.decimal = { up: 2, tp: 2};
+            this.precision = {
+                other: { value: 2 },
+            };
+        }
         this.parent = parent;
         this.defaultData = defaultData;
         // 常量
@@ -706,13 +735,13 @@ class ImportGclBaseTree {
         if (!parent.children) parent.children = [];
 
         node.id = this.ctx.app.uuid.v4();
-        node.tender_id = this.ctx.tender.id;
-        node.ledger_id = this.keyNodeId;
+        node[this.setting.mid] = this.mid;
+        node[this.setting.kid] = this.keyNodeId;
         this.keyNodeId += 1;
-        node.ledger_pid = parent.ledger_id;
-        node.level = parent.level + 1;
-        node.order = parent.children.length + 1;
-        node.full_path = parent.full_path + '-' + node.ledger_id;
+        node[this.setting.pid] = parent[this.setting.kid];
+        node[this.setting.level] = parent[this.setting.level] + 1;
+        node[this.setting.order] = parent.children.length + 1;
+        node[this.setting.fullPath] = parent[this.setting.fullPath] + '-' + node[this.setting.kid];
         parent.children.push(node);
         node.children = [];
         if (this.defaultData) _.assignIn(node, this.defaultData);
@@ -746,8 +775,20 @@ class AnalysisGclExcelTree {
     /**
      * 构造函数
      */
-    constructor(ctx) {
+    constructor(ctx, setting) {
         this.ctx = ctx;
+        this.setting = setting;
+        if (ctx.tender) {
+            this.mid = ctx.tender.id;
+            this.decimal = ctx.tender.info.decimal;
+            this.precision = ctx.tender.info.precision;
+        } else if (ctx.budget) {
+            this.mid = ctx.budget.id;
+            this.decimal = { up: 2, tp: 2};
+            this.precision = {
+                other: { value: 2 },
+            };
+        }
         this.colsDef = null;
         this.colHeaderMatch = {
             b_code: {value: ['编号', '清单编号', '子目号', '子目编号', '清单号'], type: colDefineType.match},
@@ -776,11 +817,11 @@ class AnalysisGclExcelTree {
         if ((_.isNil(node.b_code) || node.b_code === '') && (_.isNil(node.name) || node.name === '')) return node;
 
         node.unit = this.ctx.helper.replaceReturn(row[this.colsDef.unit]);
-        const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, node.unit);
+        const precision = this.ctx.helper.findPrecision(this.precision, node.unit);
         node.quantity = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.quantity]), precision.value);
-        node.unit_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.unit_price]), this.ctx.tender.info.decimal.up);
+        node.unit_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.unit_price]), this.decimal.up);
         if (node.quantity && node.unit_price) {
-            node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.ctx.tender.info.decimal.tp);
+            node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.decimal.tp);
         } else {
             node.total_price = null;
         }
@@ -796,7 +837,7 @@ class AnalysisGclExcelTree {
     analysisData(sheet, parent, maxId, defaultData) {
         try {
             this.colsDef = null;
-            this.cacheTree = new ImportGclBaseTree(this.ctx, parent, maxId, defaultData);
+            this.cacheTree = new ImportGclBaseTree(this.ctx, this.setting, parent, maxId, defaultData);
             this.errorData = [];
             this.loadEnd = false;
             // 识别表头导入

+ 710 - 1
app/public/js/budget_detail.js

@@ -7,11 +7,19 @@
  * @date
  * @version
  */
-
+const invalidFields = {
+    parent: ['total_price', 'unit_price'],
+    gcl: ['dgn_qty1', 'dgn_qty2'],
+};
 $(document).ready(() => {
+    const copyBlockTag = 'zh.calc.copyBlock';
+    if (needGcl) invalidFields.parent.push('quantity');
+    let stdXmj, stdGcl, searchBudget;
     autoFlashHeight();
     const budgetSpread = SpreadJsObj.createNewSpread($('#budget-spread')[0]);
     const budgetSheet = budgetSpread.getActiveSheet();
+    sjsSettingObj.setFxTreeStyle(spreadSetting, sjsSettingObj.FxTreeStyle.jz);
+    // if (thousandth) sjsSettingObj.setTpThousandthFormat(spreadSetting);
     SpreadJsObj.initSheet(budgetSheet, spreadSetting);
 
     $.subMenu({
@@ -30,4 +38,705 @@ $(document).ready(() => {
             autoFlashHeight();
         }
     });
+    const budgetTree = createNewPathTree('ledger', {
+        id: 'tree_id',
+        pid: 'tree_pid',
+        order: 'order',
+        level: 'level',
+        rootId: -1,
+        keys: ['id', 'tender_id', 'ledger_id'],
+        autoExpand: 3,
+        markExpandKey: 'bills-expand',
+        markExpandSubKey: window.location.pathname.split('/')[2],
+        calcFields: ['total_price'],
+        calcFun: function (node) {
+            node.dgn_price = ZhCalc.div(node.total_price, node.dgn_qty1, 2);
+        },
+    });
+
+    $.divResizer({
+        select: '#right-spr',
+        callback: function () {
+            budgetSpread.refresh();
+            if (stdXmj) stdXmj.spread.refresh();
+            if (stdGcl) stdGcl.spread.refresh();
+            if (searchBudget) searchBudget.spread.refresh();
+        }
+    });
+
+    const budgetTreeOpr = {
+        refreshTree: function (sheet, data) {
+            SpreadJsObj.massOperationSheet(sheet, function () {
+                const tree = sheet.zh_tree;
+                // 处理删除
+                if (data.delete) {
+                    data.delete.sort(function (a, b) {
+                        return b.deleteIndex - a.deleteIndex;
+                    });
+                    for (const d of data.delete) {
+                        sheet.deleteRows(d.deleteIndex, 1);
+                    }
+                }
+                // 处理新增
+                if (data.create) {
+                    const newNodes = data.create;
+                    if (newNodes) {
+                        newNodes.sort(function (a, b) {
+                            return a.index - b.index;
+                        });
+
+                        for (const node of newNodes) {
+                            sheet.addRows(node.index, 1);
+                            SpreadJsObj.reLoadRowData(sheet, tree.nodes.indexOf(node), 1);
+                        }
+                    }
+                }
+                // 处理更新
+                if (data.update) {
+                    const rows = [];
+                    for (const u of data.update) {
+                        rows.push(tree.nodes.indexOf(u));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                }
+                // 处理展开
+                if (data.expand) {
+                    const expanded = [];
+                    for (const e of data.expand) {
+                        if (expanded.indexOf(e) === -1) {
+                            const posterity = tree.getPosterity(e);
+                            for (const p of posterity) {
+                                sheet.setRowVisible(tree.nodes.indexOf(p), p.visible);
+                                expanded.push(p);
+                            }
+                        }
+                    }
+                }
+            });
+        },
+        getDefaultSelectInfo: function (sheet) {
+            const tree = sheet.zh_tree;
+            if (!tree) return;
+            const sel = sheet.getSelections()[0];
+            const node = sheet.zh_tree.nodes[sel.row];
+            if (!node) return;
+            let count = 1;
+            if (sel.rowCount > 1) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = sheet.zh_tree.nodes[sel.row + r];
+                    if (rNode.level > node.level) continue;
+                    if ((rNode.level < node.level) || (rNode.level === node.level && rNode.pid !== node.pid)) {
+                        toastr.warning('请选择同一节点下的节点,进行该操作');
+                        return;
+                    }
+                    count += 1;
+                }
+            }
+            return [tree, node, count];
+        },
+        /**
+         * 刷新顶部按钮是否可用
+         * @param sheet
+         * @param selections
+         */
+        refreshOperationValid: function (sheet, selection) {
+            const setObjEnable = function (obj, enable) {
+                if (enable) {
+                    obj.removeClass('disabled');
+                } else {
+                    obj.addClass('disabled');
+                }
+            };
+            const invalidAll = function () {
+                setObjEnable($('a[name=base-opr][type=add]'), false);
+                setObjEnable($('a[name=base-opr][type=delete]'), false);
+                setObjEnable($('a[name=base-opr][type=up-move]'), false);
+                setObjEnable($('a[name=base-opr][type=down-move]'), false);
+                setObjEnable($('a[name=base-opr][type=up-level]'), false);
+                setObjEnable($('a[name=base-opr][type=down-level]'), false);
+            };
+            const sel = selection ? selection[0] : sheet.getSelections()[0];
+            const row = sel ? sel.row : -1;
+            const tree = sheet.zh_tree;
+            if (!tree) {
+                invalidAll();
+                return;
+            }
+            const first = sheet.zh_tree.nodes[row];
+            if (!first) {
+                invalidAll();
+                return;
+            }
+            let last = first, sameParent = true;
+            if (sel.rowCount > 1 && first) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = tree.nodes[sel.row + r];
+                    if (!rNode) {
+                        sameParent = false;
+                        break;
+                    }
+                    if (rNode.level > first.level) continue;
+                    if ((rNode.level < first.level) || (rNode.level === first.level && rNode.pid !== first.pid)) {
+                        sameParent = false;
+                        break;
+                    }
+                    last = rNode;
+                }
+            }
+            const preNode = tree.getPreSiblingNode(first);
+            const valid = !readOnly;
+
+            setObjEnable($('a[name=base-opr][type=add]'), valid && first && first.level > 1);
+            setObjEnable($('a[name=base-opr][type=delete]'), valid && first && sameParent && first.level > 1);
+            setObjEnable($('a[name=base-opr][type=up-move]'), valid && first && sameParent && first.level > 1 && preNode);
+            setObjEnable($('a[name=base-opr][type=down-move]'), valid && first && sameParent && first.level > 1 && !tree.isLastSibling(last));
+            setObjEnable($('a[name=base-opr][type=up-level]'), valid && first && sameParent && tree.getParent(first) && first.level > 2 && tree.isLastSibling(last));
+            setObjEnable($('a[name=base-opr][type=down-level]'), valid && first && sameParent && first.level > 1 && preNode);
+        },
+        selectionChanged: function (e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    budgetTreeOpr.refreshOperationValid(info.sheet);
+                }
+            }
+        },
+        /**
+         * 新增节点
+         * @param spread
+         */
+        baseOpr: function (sheet, type, addCount = 1) {
+            const self = this;
+            const [tree, node, count] = this.getDefaultSelectInfo(sheet);
+            if (!tree || !node || !count) return;
+
+            if (type === 'delete') {
+                deleteAfterHint(function () {
+                    postData(window.location.pathname + '/update', {
+                        postType: type,
+                        postData: { id: node.tree_id, count: type === 'add' ? addCount : count }
+                    }, function (result) {
+                        const refreshData = tree.loadPostData(result);
+                        self.refreshTree(sheet, refreshData);
+                        const sel = sheet.getSelections()[0];
+                        if (sel) {
+                            sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
+                        }
+                        self.refreshOperationValid(sheet);
+                    });
+                });
+            } else {
+                postData(window.location.pathname + '/update', {
+                    postType: type,
+                    postData: { id: node.tree_id, count: type === 'add' ? addCount : count }
+                }, function (result) {
+                    const refreshData = tree.loadPostData(result);
+                    self.refreshTree(sheet, refreshData);
+                    if (['up-move', 'down-move'].indexOf(type) > -1) {
+                        const sel = sheet.getSelections()[0];
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
+                            SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(node)]);
+                        }
+                    } else if (type === 'add') {
+                        const sel = sheet.getSelections()[0];
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                            SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(refreshData.create[0])]);
+                        }
+                    }
+                    self.refreshOperationValid(sheet);
+                });
+            }
+        },
+        editStarting(e, info) {
+            if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+            const col = info.sheet.zh_setting.cols[info.col];
+            const node = info.sheet.zh_tree.nodes[info.row];
+            if (!node) {
+                info.cancel = true;
+                return;
+            }
+            switch (col.field) {
+                case 'unit_price':
+                    info.cancel = (node.children && node.children.length > 0) || !node.b_code;
+                    break;
+                case 'total_price':
+                case 'quantity':
+                    info.cancel = (node.children && node.children.length > 0);
+                    break;
+                case 'dgn_price':
+                    info.cance = false;
+                    break;
+                case 'dgn_qty1':
+                case 'dgn_qty2':
+                    info.cancel = !_.isEmpty(node.b_code);
+                    break;
+            }
+        },
+        /**
+         * 编辑单元格响应事件
+         * @param {Object} e
+         * @param {Object} info
+         */
+        editEnded: function (e, info) {
+            if (info.sheet.zh_setting) {
+                const col = info.sheet.zh_setting.cols[info.col];
+                const sortData = info.sheet.zh_tree.nodes;
+                const node = sortData[info.row];
+                const data = { id: node.id, bid: node.bid, tree_id: node.tree_id };
+                // 未改变值则不提交
+                const orgValue = node[col.field];
+                const newValue = trimInvalidChar(info.editingText);
+                if (orgValue == info.editingText || ((!orgValue || orgValue === '') && (newValue === ''))) return;
+
+                if (node.b_code && invalidFields.indexOf(col.field) >=0) {
+                    toastr.error('工程量清单请勿输入设计数量');
+                    return;
+                }
+
+                // 获取更新数据
+                if (newValue) {
+                    if (col.type === 'Number') {
+                        const num = _.toNumber(newValue);
+                        if (_.isNaN(num)) {
+                            toastr.error('请输入数字');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                        data[col.field] = num;
+                    } else {
+                        data[col.field] = newValue;
+                    }
+                } else {
+                    data[col.field] = col.type === 'Number' ? 0 : '';
+                }
+                console.log(data);
+                // 更新至服务器
+                postData(window.location.pathname + '/update', {postType: 'update', postData: data}, function (result) {
+                    const refreshNode = budgetTree.loadPostData(result);
+                    budgetTreeOpr.refreshTree(info.sheet, refreshNode);
+                });
+            }
+        },
+        clipboardPasting: function (e, info) {
+            const tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
+            info.cancel = true;
+            if (!setting || !tree) return;
+
+            const pasteData = info.pasteData.html
+                ? SpreadJsObj.analysisPasteHtml(info.pasteData.html)
+                : (info.pasteData.text === ''
+                    ? SpreadJsObj.Clipboard.getAnalysisPasteText()
+                    : SpreadJsObj.analysisPasteText(info.pasteData.text));
+            const hint = {
+                invalidNum: {type: 'warning', msg: '粘贴的数字非法'},
+                parent: {type: 'warning', msg: `含有子项,不可粘贴${needGcl ? '数量、单价、' : ''}金额`},
+                gcl: {type: 'warning', msg: '工程量清单,不可粘贴项目节数量'}
+            };
+            const datas = [], filterNodes = [];
+
+            let filterRow = 0;
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                const curRow = info.cellRange.row + iRow;
+                const node = tree.nodes[curRow];
+                if (!node) continue;
+
+                let bPaste = false;
+                const data = info.sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                    const curCol = info.cellRange.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    const value = trimInvalidChar(pasteData[iRow-filterRow][iCol]);
+                    if (node.children && node.children.length > 0 && invalidFields.parent.indexOf(colSetting.field) >= 0) {
+                        toastMessageUniq(hint.parent);
+                        continue;
+                    }
+                    if (!_.isEmpty(node.b_code) && invalidFields.gcl.indexOf(colSetting.field) >= 0) {
+                        toastMessageUniq(hint.gcl);
+                        continue;
+                    }
+
+                    if (colSetting.type === 'Number') {
+                        if (colSetting.type === 'Number') {
+                            const num = _.toNumber(value);
+                            if (_.isNaN(num)) {
+                                toastMessageUniq(hint.invalidExpr);
+                                continue;
+                            }
+                            data[col.field] = num;
+                        } else {
+                            data[col.field] = value;
+                        }
+                    } else {
+                        data[col.field] = col.type === 'Number' ? 0 : '';
+                    }
+                    bPaste = true;
+                }
+                if (bPaste) {
+                    datas.push(data);
+                } else {
+                    filterNodes.push(node);
+                }
+            }
+            if (datas.length > 0) {
+                postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    if (refreshNode.update) refreshNode.update = refreshNode.update.concat(filterNodes);
+                    budgetTreeOpr.refreshTree(info.sheet, refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
+        },
+        pasteBlock: function (sheet, copyInfo) {
+            const self = this;
+            const [tree, node] = this.getDefaultSelectInfo(sheet);
+
+            postData(window.location.pathname + '/update', {
+                postType: 'paste-block',
+                postData: {
+                    id: tree.getNodeKey(node),
+                    bid: copyInfo.bid,
+                    block: copyInfo.block,
+                }
+            }, function (data) {
+                const result = tree.loadPostData(data);
+                self.refreshTree(sheet, result);
+                const sel = sheet.getSelections()[0];
+                if (sel) {
+                    sheet.setSelection(tree.nodes.indexOf(result.create[0]), sel.col, sel.rowCount, sel.colCount);
+                    SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(result.create[0])]);
+                }
+                self.refreshOperationValid(sheet);
+                removeLocalCache(copyBlockTag);
+            }, null, true);
+        }
+    };
+    budgetSpread.bind(spreadNS.Events.SelectionChanged, budgetTreeOpr.selectionChanged);
+    if (!readOnly) {
+        // 增删上下移升降级
+        $('a[name="base-opr"]').click(function () {
+            budgetTreeOpr.baseOpr(budgetSheet, this.getAttribute('type'));
+        });
+        budgetSpread.bind(spreadNS.Events.EditStarting, budgetTreeOpr.editStarting);
+        budgetSpread.bind(spreadNS.Events.EditEnded, budgetTreeOpr.editEnded);
+        budgetSpread.bind(spreadNS.Events.ClipboardPasting, budgetTreeOpr.clipboardPasting);
+        $.contextMenu({
+            selector: '#budget-spread',
+            build: function ($trigger, e) {
+                const target = SpreadJsObj.safeRightClickSelection($trigger, e, budgetSpread);
+                return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
+            },
+            items: {
+                copyBlock: {
+                    name: '复制整块',
+                    icon: 'fa-files-o',
+                    callback: function (key, opt) {
+                        const copyBlockList = [];
+                        const sheet = budgetSheet;
+                        const sel = sheet.getSelections()[0];
+                        let iRow = sel.row;
+                        const pid = sheet.zh_tree.nodes[iRow].tree_pid;
+                        while (iRow < sel.row + sel.rowCount) {
+                            const node = sheet.zh_tree.nodes[iRow];
+                            if (node.tree_pid !== pid) {
+                                toastr.error('仅可同时选中同层节点');
+                                return;
+                            }
+                            const posterity = sheet.zh_tree.getPosterity(node);
+                            iRow += posterity.length + 1;
+                            posterity.unshift(node);
+                            copyBlockList.push(sheet.zh_tree.getDefaultData(posterity));
+                        }
+                        setLocalCache(copyBlockTag, JSON.stringify({ block: copyBlockList }));
+                    },
+                    visible: function (key, opt) {
+                        const select = SpreadJsObj.getSelectObject(budgetSheet);
+                        return !!select;
+                    },
+                    disabled: function (key, opt) {
+                        const select = SpreadJsObj.getSelectObject(budgetSheet);
+                        return !!select && select.level <= 1;
+                    }
+                },
+                copyBlockXmj: {
+                    name: '复制整块(只复制项目节)',
+                    icon: 'fa-files-o',
+                    callback: function (key, opt) {
+                        const copyBlockList = [];
+                        const sheet = budgetSheet;
+                        const sel = sheet.getSelections()[0];
+                        let iRow = sel.row;
+                        const pid = sheet.zh_tree.nodes[iRow].ledger_pid;
+                        while (iRow < sel.row + sel.rowCount) {
+                            const node = sheet.zh_tree.nodes[iRow];
+                            if (node.ledger_pid !== pid) {
+                                toastr.error('仅可同时选中同层节点');
+                                return;
+                            }
+                            const posterity = sheet.zh_tree.getPosterity(node);
+                            iRow += posterity.length + 1;
+                            const copyPosterity = posterity.filter(x => { return !x.b_code; });
+                            copyPosterity.unshift(node);
+                            const copyData = sheet.zh_tree.getDefaultData(copyPosterity);
+                            for (const p of copyData) {
+                                const children = copyData.filter(y => {return y.ledger_pid === p.ledger_id}) || [];
+                                p.is_leaf = children.length === 0;
+                            }
+                            copyBlockList.push(copyData);
+                        }
+                        setLocalCache(copyBlockTag, JSON.stringify({ block: copyBlockList }));
+                    },
+                    visible: function (key, opt) {
+                        const select = SpreadJsObj.getSelectObject(budgetSheet);
+                        return needGcl && !!select;
+                    },
+                    disabled: function (key, opt) {
+                        const select = SpreadJsObj.getSelectObject(budgetSheet);
+                        return !!select && select.level <= 1;
+                    }
+                },
+                pasteBlock: {
+                    name: '粘贴整块',
+                    icon: 'fa-clipboard',
+                    disabled: function (key, opt) {
+                        const copyInfo = JSON.parse(getLocalCache(copyBlockTag));
+                        return !(copyInfo && copyInfo.block && copyInfo.block.length > 0);
+                    },
+                    callback: function (key, opt) {
+                        const copyInfo = JSON.parse(getLocalCache(copyBlockTag));
+                        if (copyInfo.block.length > 0) {
+                            budgetTreeOpr.pasteBlock(budgetSheet, copyInfo);
+                        } else {
+                            document.execCommand('paste');
+                        }
+                    },
+                    visible: function (key, opt) {
+                        return !readOnly;
+                    }
+                }
+            }
+        });
+    }
+    postData(window.location.pathname + '/load', {}, function (result) {
+        budgetTree.loadDatas(result);
+        treeCalc.calculateAll(budgetTree);
+        SpreadJsObj.loadSheetData(budgetSheet, SpreadJsObj.DataType.Tree, budgetTree);
+        budgetTreeOpr.refreshOperationValid(budgetSheet);
+    });
+
+    const stdLibCellDoubleClick = function (e, info) {
+        const stdSheet = info.sheet;
+        if (!stdSheet.zh_setting || !stdSheet.zh_tree || !budgetSheet.zh_tree) return;
+
+        const stdTree = stdSheet.zh_tree;
+        const stdNode = stdTree.nodes[info.row];
+        if (!stdNode) return;
+
+        const budgetTree = budgetSheet.zh_tree;
+        const sel = budgetSheet.getSelections()[0];
+        const mainNode = budgetTree.nodes[sel.row];
+        if (info.sheet.zh_setting.stdType === 'gcl') {
+            if (mainNode.code && mainNode.code !== '' && !budgetTree.isLeafXmj(mainNode)) {
+                toastr.warning('非最底层项目下,不应添加清单');
+                return;
+            }
+        }
+
+        postData(window.location.pathname + '/update', {
+            postType: 'add-std',
+            postData: {
+                id: budgetTree.getNodeKey(mainNode),
+                tender_id: mainNode.tender_id,
+                stdType: info.sheet.zh_setting.stdType,
+                stdLibId: stdNode.list_id,
+                stdNode: stdTree.getNodeKey(stdNode)
+            }
+        }, function (result) {
+            const refreshNode = budgetTree.loadPostData(result);
+            budgetTreeOpr.refreshTree(budgetSheet, refreshNode);
+            if (refreshNode.create && refreshNode.create.length > 0) {
+                budgetSheet.setSelection(refreshNode.create[refreshNode.create.length - 1].index, sel.col, sel.rowCount, sel.colCount);
+                SpreadJsObj.reloadRowsBackColor(budgetSheet, [sel.row, refreshNode.create[refreshNode.create.length - 1].index]);
+            } else {
+                const node = _.find(budgetTree.nodes, {code: stdNode.code, name: stdNode.name});
+                if (node) {
+                    budgetSheet.setSelection(budgetTree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
+                    SpreadJsObj.reloadRowsBackColor(budgetSheet, [sel.row, budgetTree.nodes.indexOf(node)]);
+                }
+            }
+            budgetTreeOpr.refreshOperationValid(budgetSheet);
+            budgetSpread.focus();
+        });
+    };
+    const stdXmjSetting = {
+        selector: '#std-xmj',
+        stdType: 'xmj',
+        treeSetting: {
+            id: 'chapter_id',
+            pid: 'pid',
+            order: 'order',
+            level: 'level',
+            rootId: -1,
+            keys: ['id', 'list_id', 'chapter_id'],
+        },
+        spreadSetting: {
+            cols: [
+                {title: '项目节编号', field: 'code', hAlign: 0, width: 120, formatter: '@', cellType: 'tree'},
+                {title: '名称', field: 'name', hAlign: 0, width: 150, formatter: '@'},
+                {title: '单位', field: 'unit', hAlign: 1, width: 50, formatter: '@'}
+            ],
+            treeCol: 0,
+            emptyRows: 0,
+            headRows: 1,
+            headRowHeight: [32],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            headColWidth: [30],
+            selectedBackColor: '#fffacd',
+            readOnly: true,
+        },
+        cellDoubleClick: !readOnly ? stdLibCellDoubleClick : null,
+        page: 'ledger',
+    };
+    const stdGclSetting = {
+        selector: '#std-gcl',
+        stdType: 'gcl',
+        treeSetting: {
+            id: 'bill_id',
+            pid: 'pid',
+            order: 'order',
+            level: 'level',
+            rootId: -1,
+            keys: ['id', 'list_id', 'bill_id']
+        },
+        spreadSetting: {
+            cols: [
+                {title: '清单编号', field: 'b_code', hAlign: 0, width: 120, formatter: '@', cellType: 'tree'},
+                {title: '名称', field: 'name', hAlign: 0, width: 150, formatter: '@'},
+                {title: '单位', field: 'unit', hAlign: 1, width: 50, formatter: '@'}
+            ],
+            treeCol: 0,
+            emptyRows: 0,
+            headRows: 1,
+            headRowHeight: [32],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            headColWidth: [30],
+            selectedBackColor: '#fffacd',
+            readOnly: true,
+        },
+        cellDoubleClick: !readOnly ? stdLibCellDoubleClick : null,
+    };
+    // 展开收起标准清单
+    $('a', '.side-menu').bind('click', function (e) {
+        e.preventDefault();
+        const tab = $(this), tabPanel = $(tab.attr('content'));
+        // 展开工具栏、切换标签
+        if (!tab.hasClass('active')) {
+            $('a', '.side-menu').removeClass('active');
+            $('.tab-content .tab-select-show.tab-pane.active').removeClass('active');
+            tab.addClass('active');
+            tabPanel.addClass('active');
+            showSideTools(tab.hasClass('active'));
+            if (tab.attr('content') === '#std-xmj') {
+                if (!stdXmj) stdXmj = new stdLib(stdXmjSetting);
+                stdXmj.spread.refresh();
+            } else if (tab.attr('content') === '#std-gcl') {
+                if (!stdGcl) stdGcl = new stdLib(stdGclSetting);
+                stdGcl.spread.refresh();
+            } else if (tab.attr('content') === '#search') {
+                if (!searchBudget) {
+                    const searchSetting = {
+                        selector: '#search',
+                        searchSpread: budgetSpread,
+                        resultSpreadSetting: {
+                            cols: [
+                                {title: '项目节编号', field: 'code', hAlign: 0, width: 120, formatter: '@'},
+                                {title: '清单编号', field: 'b_code', hAlign: 0, width: 80, formatter: '@'},
+                                {title: '名称', field: 'name', width: 150, hAlign: 0, formatter: '@'},
+                                {title: '单位', field: 'unit', width: 50, hAlign: 1, formatter: '@'},
+                            ],
+                            emptyRows: 0,
+                            headRows: 1,
+                            headRowHeight: [32],
+                            headColWidth: [30],
+                            defaultRowHeight: 21,
+                            headerFont: '12px 微软雅黑',
+                            font: '12px 微软雅黑',
+                            selectedBackColor: '#fffacd',
+                            readOnly: true,
+                        },
+                    };
+                    if (!needGcl) {
+                        searchSetting.resultSpreadSetting.cols = searchSetting.resultSpreadSetting.cols.filter(x => {
+                            return ['b_code', 'quantity', 'unit_price'].indexOf(x.field) < 0;
+                        });
+                        searchSetting.searchRangeStr = '项目节编号/名称';
+                    } else {
+                        searchSetting.searchRangeStr = '项目节编号/清单编号/名称';
+                    }
+                    searchBudget = $.billsSearch(searchSetting);
+                }
+                searchBudget.spread.refresh();
+            }
+        } else { // 收起工具栏
+            tab.removeClass('active');
+            tabPanel.removeClass('active');
+            showSideTools(tab.hasClass('active'));
+        }
+        budgetSpread.refresh();
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            setTimeout(() => {
+                showWaitingView();
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                    case "5":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "leafXmj":
+                        tree.expandToLeafXmj();
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', budgetSheet);
+    // 导入
+    $('#budget-import').click(() => {
+        importExcel.doImport({
+            template: {
+                hint: '导入Excel',
+                url: '/template/导入分项清单EXCEL格式.xlsx',
+            },
+            filter: needGcl,
+            callback: function (sheet, filter) {
+                postDataCompress(window.location.pathname + '/upload-excel/tz', {sheet, filter}, function (result) {
+                    budgetTree.loadDatas(result);
+                    treeCalc.calculateAll(budgetTree);
+                    SpreadJsObj.reLoadSheetData(budgetSheet);
+                    checkShowLast(result.length);
+                }, null);
+            },
+        });
+    })
 });

+ 53 - 3
app/public/js/budget_list.js

@@ -7,14 +7,13 @@
  * @date
  * @version
  */
+let curBudget = {};
 
 const budgetNameChange = function (obj) {
     if (obj.value.length > 100) {
         obj.classList.add('is-invalid');
-        $('.invalid-feedback').show();
     } else {
         obj.classList.remove('is-invalid');
-        $('.invalid-feedback').hide();
     }
 };
 
@@ -25,4 +24,55 @@ const addBudget = function () {
     postData('/budget/add', { name, std_id }, function () {
         window.location.reload();
     });
-};
+};
+
+const showModal = function (obj) {
+    const tr = obj.parentNode.parentNode;
+    curBudget.id = tr.getAttribute('bid');
+    curBudget.name = tr.getAttribute('bname');
+    curBudget.rela_tender = tr.getAttribute('rela-tender');
+    $(obj.getAttribute('data-target')).modal('show');
+};
+
+const saveBudget = function () {
+    const name = $('#modify-budget-name').val();
+    if (!name || name.length > 100) return;
+    postData('/budget/save', { id: curBudget.id, name}, function () {
+        window.location.reload();
+    })
+};
+
+const delBudget = function () {
+    postData('/budget/del', { id: curBudget.id }, function () {
+        window.location.reload();
+    });
+};
+
+const relaTender = function () {
+    // todo 选择标段
+    const rela = [];
+    const select = $('[name=select-rela-check]:checked');
+    for (const s of select) {
+        rela.push(s.getAttribute('tid'));
+    }
+    console.log(rela);
+    postData('/budget/save', { id: curBudget.id, rela_tender: rela.join(',') }, function () {
+        $(`[bid=${curBudget.id}]`)[0].setAttribute('brela', rela.join(','));
+        $('#select-rela').modal('hide');
+    });
+};
+
+$(document).ready(() => {
+    autoFlashHeight();
+    $('#del-budget').on('show.bs.modal', () => {
+        $('#del-budget-name').text(curBudget.name);
+    });
+    $('#select-rela').on('show.bs.modal', () => {
+        $('[name=select-rela-check]').removeAttr('checked');
+        const rela = curBudget.rela_tender ? curBudget.rela_tender.split(',') : [];
+        console.log(rela);
+        for (const r of rela) {
+            $(`[tid=${r}]`).attr("checked", "checked");
+        }
+    });
+});

+ 1 - 1
app/public/js/sr_detail.js

@@ -798,7 +798,7 @@ $(document).ready(() => {
             let html = [];
             if (data.attachment) {
                 for (const att of data.attachment) {
-                    const delHtml = (parseInt(att.uid) === userID && (att.renew || stage.status !== auditConst.status.checked))
+                    const delHtml = (parseInt(att.uid) === userID && att.renew)
                         ? '<a class="delete-att text-danger ml-1" href="javascript:void(0);" data-imid="'+ data.att_uuid +'" data-attid="'+ att.file_id +'" title="删除"><i class="fa fa-remove "></i></a>'
                         : '';
                     const viewHtml = att.viewpath ? `<a class="ml-1" href="${att.viewpath}" target="_blank" title="预览"><i class="fa fa-eye"></i></a>` : '';

+ 1 - 0
app/public/js/std_lib.js

@@ -17,6 +17,7 @@ class stdLib {
         const self = this;
         this.setting = setting;
         this.obj = $(setting.selector + '-spread')[0];
+        $('select', setting.selector).parent().parent().height(32);
         this.stdType = setting.stdType;
         this.treeSetting = setting.treeSetting;
         this.spreadSetting = setting.spreadSetting;

+ 2 - 2
app/router.js

@@ -583,6 +583,6 @@ module.exports = app => {
     app.get('/budget/:id/compare', sessionAuth, budgetCheck, 'budgetController.compare');
     app.get('/budget/:id/:btype', sessionAuth, budgetCheck, 'budgetController.detail');
     app.post('/budget/:id/:btype/load', sessionAuth, budgetCheck, 'budgetController.detailLoad');
-    app.post('/budget/:id/:btype/update', sessionAuth, tenderCheck, uncheckTenderCheck, 'budgetController.detailUpdate');
-    app.post('/budget/:id/:btype/upload-excel', sessionAuth, tenderCheck, uncheckTenderCheck, 'budgetController.detailUploadExcel');
+    app.post('/budget/:id/:btype/update', sessionAuth, budgetCheck, 'budgetController.detailUpdate');
+    app.post('/budget/:id/:btype/upload-excel/:ueType', sessionAuth, budgetCheck, 'budgetController.detailUploadExcel');
 };

+ 5 - 6
app/service/budget.js

@@ -95,9 +95,8 @@ module.exports = app => {
          * @param {Object} postData - 表单post过来的数
          * @return {Boolean} - 返回执行结果
          */
-        async save(id, postData) {
-            const rowData = { id, name: postData.name };
-            const result = await this.db.update(this.tableName, rowData);
+        async save(data) {
+            const result = await this.db.update(this.tableName, data);
             return result.affectedRows > 0;
         }
 
@@ -123,9 +122,9 @@ module.exports = app => {
             const transaction = await this.db.beginTransaction();
             try {
                 await transaction.delete(this.tableName, { id });
-                await transaction.delete(this.ctx.service.guBudget.tableName, { bid: id });
-                await transaction.delete(this.ctx.service.gaiBudget.tableName, { bid: id });
-                await transaction.delete(this.ctx.service.yuBudget.tableName, { bid: id });
+                await transaction.delete(this.ctx.service.budgetGu.tableName, { bid: id });
+                await transaction.delete(this.ctx.service.budgetGai.tableName, { bid: id });
+                await transaction.delete(this.ctx.service.budgetYu.tableName, { bid: id });
                 await transaction.commit();
                 return true;
             } catch (err) {

+ 7 - 0
app/service/budget_std.js

@@ -49,6 +49,13 @@ module.exports = app => {
             const chapterList = chaptersId.length > 0 ? await this.db.query(sql2, sqlParam2) : [];
             return [billsList, chapterList];
         }
+
+        async getTemplateId(id, type) {
+            if (!id || !type) throw '参数错误';
+            const budgetStd = await this.getDataById(id);
+            if (!budgetStd) throw '模板不存在';
+            return budgetStd[type + '_template_id'];
+        }
     }
 
     return BudgetStd;

+ 2 - 3
app/service/ledger.js

@@ -706,9 +706,9 @@ module.exports = app => {
          */
         async importExcel(templateId, excelData, filter) {
             const AnalysisExcel = require('../lib/analysis_excel').AnalysisExcelTree;
-            const analysisExcel = new AnalysisExcel(this.ctx);
+            const analysisExcel = new AnalysisExcel(this.ctx, this.setting);
             const tempData = await this.ctx.service.tenderNodeTemplate.getData(templateId, true);
-            const cacheTree = analysisExcel.analysisData(excelData, tempData, filter);
+            const cacheTree = analysisExcel.analysisData(excelData, tempData, { filterZeroGcl: filter });
             const cacheKey = keyPre + this.ctx.tender.id;
             const orgMaxId = parseInt(await this.cache.get(cacheKey));
             const transaction = await this.db.beginTransaction();
@@ -781,7 +781,6 @@ module.exports = app => {
                 await conn.commit();
                 return result;
             } catch (err) {
-                console.log(err);
                 await conn.rollback();
                 throw (err.stack ? '导入工程量数据出错': err);
             }

+ 23 - 0
app/service/tender.js

@@ -160,6 +160,29 @@ module.exports = app => {
             return list;
         }
 
+        async getList4Select(selectType) {
+            const accountInfo = await this.ctx.service.projectAccount.getDataById(this.ctx.session.sessionUser.accountId);
+            const userPermission = accountInfo !== undefined && accountInfo.permission !== '' ? JSON.parse(accountInfo.permission) : null;
+            const tenderList = await this.ctx.service.tender.getList('', userPermission);
+            for (const t of tenderList) {
+                if (t.ledger_status === auditConst.ledger.status.checked) {
+                    t.lastStage = await this.ctx.service.stage.getLastestStage(t.id, false);
+                }
+            }
+            switch (selectType) {
+                case 'ledger': return tenderList.filter(x => {
+                    return x.ledger_status === auditConst.ledger.status.checked;
+                });
+                case 'revise': tenderList.filter(x => {
+                    return x.ledger_status === auditConst.ledger.status.checked;
+                });
+                case 'stage': return tenderList.filter(x => {
+                    return x.ledger_status === auditConst.ledger.status.checked && !!x.lastStage;
+                });
+                default: return tenderList;
+            }
+        }
+
         async getTender(id) {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('id', {

+ 7 - 9
app/view/budget/detail.ejs

@@ -20,15 +20,12 @@
                     </div>
                 </div>
                 <div class="d-inline-block">
-                    <a href="javascript:void(0)" id="insert" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus" aria-hidden="true"></i></a>
-                    <a href="javascript:void(0)" id="delete" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i class="fa fa-remove" aria-hidden="true"></i></a>
-                    <a href="javascript:void(0)" id="up-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
-                    <a href="javascript:void(0)" id="down-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
-                    <a href="javascript:void(0)" id="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
-                    <a href="javascript:void(0)" id="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
-                    <a href="javascript:void(0)" id="copy" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="复制"><i class="fa fa-files-o" aria-hidden="true"></i></a>
-                    <a href="javascript:void(0)" id="cut" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="剪切"><i class="fa fa-scissors" aria-hidden="true"></i></a>
-                    <a href="javascript:void(0)" id="paste" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="粘贴"><i class="fa fa-clipboard" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="add" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="delete" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i class="fa fa-remove" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
                 </div>
                 <div class="d-inline-block ml-3">
                     <a class="btn btn-sm btn-primary" href="javascript: void(0);" id="budget-import">导入</a>
@@ -116,6 +113,7 @@
     </div>
 </div>
 <script>
+    const readOnly = false;
     const needGcl = <%- needGcl %>;
     const spreadSetting = JSON.parse('<%- JSON.stringify(spreadSetting) %>');
 </script>

+ 2 - 0
app/view/budget/detail_modal.ejs

@@ -0,0 +1,2 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<% include ../shares/import_excel_modal.ejs %>

+ 6 - 14
app/view/budget/list.ejs

@@ -1,6 +1,7 @@
 <div class="panel-content">
     <div class="panel-title fluid">
         <div class="title-main  d-flex justify-content-between">
+            <div>概算投资</div>
             <div class="ml-auto">
                 <a href="#add-budget" name="add" data-toggle="modal" data-target="#add-budget" class="btn btn-sm btn-primary pull-right">新建项目</a>
             </div>
@@ -15,28 +16,19 @@
                 </div>
                 <% } else { %>
                 <table class="table table-hover table-bordered">
-                    <thead>
-                    <tr>
-                        <th class="text-center">项目名称</th>
-                        <th class="text-center">创建人</th>
-                        <th class="text-center">创建时间</th>
-                        <th class="text-center">操作</th>
-                    </tr>
-                    </thead>
-                    <tbody>
+                    <tr class="text-center"><th>项目名称</th><th>创建人</th><th>创建时间</th><th>操作</th></tr>
                     <% for (const bl of budgetList) { %>
-                    <tr>
+                    <tr bid="<%- bl.id %>" bname="<%- bl.name %>" rela-tender="<%- bl.rela_tender %>" >
                         <td><a href="/budget/<%- bl.id %>/compare"><%- bl.name %></a></td>
                         <td><%- bl.username %></td>
                         <td><%- ctx.moment(bl.in_time).format('YYYY-MM-DD HH:mm:ss') %></td>
                         <td>
-                            <a href="#modify-budget" data-toggle="modal" data-target="#modify-budget" class="btn btn-outline-primary btn-sm">编辑</a>
-                            <a href="#select-rela" data-toggle="modal" data-target="#select-rela" class="btn btn-outline-primary btn-sm">关联标段</a>
-                            <a href="#del-budget" data-toggle="modal" data-target="#del-budget" class="btn btn-outline-danger btn-sm ml-1">删除</a>
+                            <a href="javascript: void(0);" data-target="#modify-budget" class="btn btn-outline-primary btn-sm" onclick="showModal(this);">编辑</a>
+                            <a href="javascript: void(0);" data-target="#select-rela" class="btn btn-outline-primary btn-sm" onclick="showModal(this);">关联标段</a>
+                            <a href="javascript: void(0);" data-target="#del-budget" class="btn btn-outline-danger btn-sm ml-1" onclick="showModal(this);">删除</a>
                         </td>
                     </tr>
                     <% } %>
-                    </tbody>
                 </table>
                 <% } %>
             </div>

+ 16 - 34
app/view/budget/list_modal.ejs

@@ -40,16 +40,14 @@
             <div class="modal-body">
                 <div class="form-group">
                     <label>项目名称<b class="text-danger">*</b></label>
-                    <input class="form-control form-control-sm is-invalid"  placeholder="输入项目名称" type="text" value="名称超过100个字">
-                    <div class="invalid-feedback">
-                        名称超过100个字,请缩减名称。
-                    </div>
+                    <input class="form-control form-control-sm"  placeholder="输入项目名称" type="text" id="modify-budget-name" value="" onchange="budgetNameChange(this);">
+                    <div class="invalid-feedback"> 名称超过100个字,请缩减名称。 </div>
                 </div>
 
             </div>
             <div class="modal-footer">
-                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
-                <button type="button" class="btn btn-primary">确定修改</button>
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-primary btn-sm" onclick="saveBudget();">确定修改</button>
             </div>
         </div>
     </div>
@@ -62,12 +60,12 @@
                 <h5 class="modal-title">删除项目</h5>
             </div>
             <div class="modal-body">
-                <h6>确认删除「XXX高速公路」?</h6>
+                <h6>确认删除「<span id="del-budget-name">XXX高速公路</span>」?</h6>
                 <h6>删除后,数据无法恢复,请谨慎操作。</h6>
             </div>
             <div class="modal-footer">
                 <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
-                <button type="button" class="btn btn-sm btn-danger">确定删除</button>
+                <button type="button" class="btn btn-sm btn-danger" onclick="delBudget();">确定删除</button>
             </div>
         </div>
     </div>
@@ -79,36 +77,20 @@
             <div class="modal-header">
                 <h5 class="modal-title">选择决算标段</h5>
             </div>
-            <div class="modal-body">
+            <div class="modal-body" style="display: block; overflow: auto; height: 500px">
                 <h5>可选标段</h5>
                 <table class="table table-sm table-bordered">
-                    <thead>
-                    <tr class="text-center">
-                        <th>选择</th>
-                        <th>标段名称</th>
-                        <th>期数</th>
-                        <th>状态</th>
-                    </tr>
-                    </thead>
-                    <tr>
-                        <td class="text-center"><input type="checkbox"></td><td>第一合同段</td><td>第15期</td><td>审批完成</td>
-                    </tr>
-                    <tr>
-                        <td class="text-center"><input type="checkbox"></td><td>第二合同段</td><td>第16期</td><td>审批完成</td>
-                    </tr>
-                    <tr>
-                        <td class="text-center"><input type="checkbox"></td><td>第三合同段</td><td>第15期</td><td>审批完成</td>
-                    </tr>
-                    <tr>
-                        <td class="text-center"><input type="checkbox"></td><td>第四合同段</td><td>第14期</td><td>审批完成</td>
-                    </tr>
+                    <tr class="text-center"><th>选择</th><th>标段名称</th><th>期数</th><th>状态</th></tr>
+                    <tbody>
+                    <% for (const t of tenderList) { %>
+                    <tr><td class="text-center"><input type="checkbox" name="select-rela-check" tid="<%- t.id %>"></td><td><%- t.name %></td><td>第<%- t.lastStage.order %>期</td><td><%- auditConst.stage.statusString[t.lastStage.status] %></td></tr>
+                    <% } %>
+                    </tbody>
                 </table>
             </div>
-            <div class="modal-footer d-flex justify-content-between">
-                <div>
-                    <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
-                    <button type="button" class="btn btn-sm btn-primary" >确定</button>
-                </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-primary" onclick="relaTender();">确定</button>
             </div>
         </div>
     </div>

+ 2 - 2
app/view/shares/import_excel_modal.ejs

@@ -7,13 +7,12 @@
                 <h5 class="modal-title">导入</h5>
             </div>
             <div class="modal-body">
-                <p id="import-template">请上传符合 <b id="import-type-hint">0号台账</b> 格式的 .xls和.xlsx 文件,<a id="download-template" href="/tender/<%- ctx.tender.id %>/ledger/download/导入分项清单EXCEL格式.xlsx">下载示例</a>。</p>
+                <p id="import-template">请上传符合 <b id="import-type-hint">0号台账</b> 格式的 .xls和.xlsx 文件,<a id="download-template">下载示例</a>。</p>
                 <div class="form-group">
                     <label for="exampleFormControlFile1">选择文件</label><i class="fa fa-spinner fa-pulse fa-lg fa-fw text-primary" id="select-excel-loading" style="display: none;"></i>
                     <input type="file" class="form-control-file" id="import-excel-file" accept="*.xls">
                 </div>
                 <div id="excel-sheets" style="display: none;">
-                    <hr></hr>
                     <h6>选择导入的工作表</h6>
                 </div>
             </div>
@@ -76,6 +75,7 @@
                     $('#select-excel-loading').hide();
                 });
             } catch(err) {
+                console.log(err);
                 $('#select-excel-loading').hide();
                 toastr.error('加载excel异常,请刷新当前页面');
                 $('#excel-sheets').hide();

+ 6 - 0
config/web.js

@@ -899,6 +899,7 @@ const JsFiles = {
                 mergeFiles: [
                     '/public/js/sub_menu.js',
                     '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
                     '/public/js/zh_calc.js',
                     '/public/js/path_tree.js',
                     '/public/js/budget_compare.js',
@@ -907,6 +908,8 @@ const JsFiles = {
             },
             detail: {
                 files: [
+                    '/public/js/js-xlsx/xlsx.full.min.js',
+                    '/public/js/js-xlsx/xlsx.utils.js',
                     '/public/js/moment/moment.min.js',
                     '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
                     '/public/js/decimal.min.js',
@@ -916,6 +919,9 @@ const JsFiles = {
                     '/public/js/sub_menu.js',
                     '/public/js/div_resizer.js',
                     '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/std_lib.js',
                     '/public/js/zh_calc.js',
                     '/public/js/path_tree.js',
                     '/public/js/budget_detail.js',