소스 검색

汇总对比,台账对比&计量对比

MaiXinRong 2 달 전
부모
커밋
352131076a

+ 339 - 138
app/controller/spss_controller.js

@@ -26,194 +26,395 @@ module.exports = app => {
             ctx.showProject = true;
         }
 
-        async _getTzData(tid, includePos = false) {
-            const tender = await this.ctx.service.tender.getTender(tid);
-            if (!tender || tender.project_id !== this.ctx.session.sessionProject.id) {
-                throw '不存在该标段';
+        async info(ctx) {
+            try {
+                const categoryData = await this.ctx.service.category.getAllCategory(this.ctx.subProject);
+                const renderData = {
+                    categoryData,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.spss.info)
+                };
+                await this.layout('spss/info.ejs', renderData, 'spss/gather_select_modal.ejs');
+            } catch (err) {
+                ctx.helper.log(err);
             }
-            const bills = await this.ctx.service.ledger.getData(tid);
-            const pos = tender.measure_type === measureType.tz.value || includePos
-                ? await this.ctx.service.pos.getPosData({tid: tid})
-                : [];
-
-            return { id: tid, name: tender.name, bills: bills, pos: pos };
-        }
-
-        async _checkStage(tid, sorder) {
-            const stage = await this.service.stage.getDataByCondition({ tid: tid, order: sorder });
-            if (!stage) throw '期数据错误';
-            await this.service.stage.doCheckStage(stage);
-            return stage;
         }
 
-        async _getStageData(tid, sorder) {
-            const data = await this._getTzData(tid, true);
-            const stage = await this._checkStage(tid, sorder);
-            const bills = await this.ctx.service.stageBills.getAuditorStageData2(tid, stage.id, stage.curTimes, stage.curOrder);
-            const pos = await this.ctx.service.stagePos.getAuditorStageData2(tid, stage.id, stage.curTimes, stage.curOrder);
-            data.stage = {
-                sid: stage.id, sorder: stage.order, curTimes: stage.curTimes, curOrder: stage.curOrder,
-                bills: bills, pos: pos
-            };
-            return data;
-        }
-
-        /**
-         * 台账 对比 页面
-         *
-         * @param {Object} ctx - egg全局变量
-         * @return {void}
-         */
-        async compareTz(ctx) {
+        async gatherLedger(ctx) {
             try {
+                const categoryData = await this.ctx.service.category.getAllCategory(this.ctx.subProject);
                 const renderData = {
-                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.compare.tz)
+                    categoryData,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.spss.gatherLedger)
                 };
-                await this.layout('spss/compare_tz.ejs', renderData, 'spss/compare_tz_modal.ejs');
+                await this.layout('spss/gather_ledger.ejs', renderData, 'spss/gather_select_modal.ejs');
             } catch (err) {
                 ctx.helper.log(err);
             }
         }
-        /**
-         * 获取 台账 对比 数据(Ajax)
-         * @param ctx
-         * @returns {Promise<void>}
-         */
-        async loadCompareTz(ctx) {
+
+        async gatherStage(ctx) {
             try {
-                const data = JSON.parse(ctx.request.body.data);
-                const responseData = {err: 0, msg: '', data: {}};
-
-                // const tender1 = await ctx.service.tender.getTender(data.tid1);
-                // responseData.data.tender1 = {
-                //     name: tender1.name,
-                //     bills: await ctx.service.ledger.getData(data.tid1),
-                //     pos: tender1.measure_type === measureType.tz.value
-                //         ? await ctx.service.pos.getPosData({tid: data.tid1}) : []
-                // };
-                // const tender2 = await ctx.service.tender.getTender(data.tid2);
-                // responseData.data.tender2 = {
-                //     name: tender2.name,
-                //     bills: await ctx.service.ledger.getData(data.tid2),
-                //     pos: tender2.measure_type === measureType.tz.value
-                //         ? await ctx.service.pos.getPosData({tid: data.tid2}) : []
-                // };
-                responseData.data.tender1 = await this._getTzData(data.tid1);
-                responseData.data.tender2 = await this._getTzData(data.tid2);
-                ctx.body = responseData;
+                const categoryData = await this.ctx.service.category.getAllCategory(this.ctx.subProject);
+                const renderData = {
+                    categoryData,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.spss.gatherStage)
+                };
+                await this.layout('spss/gather_stage.ejs', renderData, 'spss/gather_select_modal.ejs');
             } catch (err) {
                 ctx.helper.log(err);
-                ctx.body = this.ajaxErrorBody(err, '查询数据错误');
             }
         }
 
-
-        /**
-         * 期计量 对比 页面
-         *
-         * @param {Object} ctx - egg全局变量
-         * @return {void}
-         */
-        async compareStage(ctx) {
+        async gatherStageExtra(ctx) {
             try {
+                const categoryData = await this.ctx.service.category.getAllCategory(this.ctx.subProject);
                 const renderData = {
-                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.compare.stage)
+                    categoryData,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.spss.gatherStageExtra)
                 };
-                await this.layout('spss/compare_tz.ejs', renderData, 'spss/compare_stage_modal.ejs');
+                await this.layout('spss/gather_stage_extra.ejs', renderData, 'spss/gather_select_modal.ejs');
             } catch (err) {
                 ctx.helper.log(err);
             }
         }
-        /**
-         * 获取 期计量 对比 数据(Ajax)
-         * @param ctx
-         * @returns {Promise<void>}
-         */
-        async loadCompareStage(ctx) {
+
+        async compareLedger(ctx) {
             try {
-                const data = JSON.parse(ctx.request.body.data);
-                const responseData = {err: 0, msg: '', data: {}};
-                responseData.data.tender1 = await this._getStageData(data.tid1, data.sorder1);
-                responseData.data.tender2 = await this._getStageData(data.tid2, data.sorder2);
-                ctx.body = responseData;
+                const categoryData = await this.ctx.service.category.getAllCategory(this.ctx.subProject);
+                const renderData = {
+                    categoryData,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.spss.compareLedger)
+                };
+                await this.layout('spss/compare_ledger.ejs', renderData, 'spss/compare_select_modal.ejs');
             } catch (err) {
                 ctx.helper.log(err);
-                ctx.body = this.ajaxErrorBody(err, '查询数据错误');
             }
         }
 
-
-        /**
-         * 台账 汇总 页面
-         *
-         * @param {Object} ctx - egg全局变量
-         * @return {void}
-         */
-        async gatherTz(ctx) {
+        async compareStage(ctx) {
             try {
+                const categoryData = await this.ctx.service.category.getAllCategory(this.ctx.subProject);
                 const renderData = {
-                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.gather.tz)
+                    categoryData,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.spss.compareStage),
                 };
-                await this.layout('spss/compare_tz.ejs', renderData, 'spss/gather_tz_modal.ejs');
+                await this.layout('spss/compare_stage.ejs', renderData, 'spss/compare_select_modal.ejs');
             } catch (err) {
                 ctx.helper.log(err);
             }
         }
-        /**
-         * 获取 台账 汇总 数据(Ajax)
-         * @param ctx
-         * @returns {Promise<void>}
-         */
-        async loadGatherTz(ctx) {
-            try {
-                const data = JSON.parse(ctx.request.body.data);
-                const responseData = {err: 0, msg: '', data: []};
 
-                for (const t of data.tenders) {
-                    responseData.data.push(await this._getTzData(t.id));
+        async _loadInfoData(tender) {
+
+        }
+        async _loadLedgerData(tender) {
+            const bills = await this.ctx.service.ledger.getAllDataByCondition({
+                columns: ['id', 'ledger_id', 'ledger_pid', 'level', 'full_path', 'is_leaf', 'order', 'code', 'b_code', 'name', 'unit', 'unit_price', 'quantity', 'total_price'],
+                where: { tender_id: tender.id },
+            });
+            const pos = await this.ctx.service.pos.getAllDataByCondition({
+                columns: ['id', 'lid', 'name', 'quantity'],
+                where: { tid: tender.id },
+            });
+            return [bills, pos];
+        }
+        async _loadDealBillsData(tender) {
+
+        }
+        async _getValidStages(tenderId, sort = 'desc') {
+            const accountId = this.ctx.session.sessionUser.accountId;
+            const stages = await this.ctx.service.stage.getAllDataByCondition({ where: { tid: tenderId }, orders: [['order', sort]] });
+            return stages.filter(s => { return s.status !== status.uncheck || s.user_id === accountId; });
+        }
+        async _filterOrder(tender, order) {
+            const stages = await this._getValidStages(tender.id);
+            return { type: 'stage', stage: this.ctx.helper._.find(stages, { order }) };
+        }
+        async _filterMonth(tender, month) {
+            const stages = await this._getValidStages(tender.id);
+            return { type: 'stage', stage: this.ctx.helper._.find(stages, { s_time: month }) };
+        }
+        async _filterFinal(tender) {
+            const stages = await this._getValidStages(tender.id);
+            return { type: 'stage', stage: stages[0] };
+        }
+        async _filterCheckedFinal(tender) {
+            const stages = await this._getValidStages(tender.id);
+            const checkedStages = stages.filter(x => { return x.status === status.checked; });
+            return { type: 'stage', stage: checkedStages[0] };
+        }
+        async _filterOrderZone(tender, zone) {
+            let [iBegin, iEnd] = zone.split(':');
+            iBegin = this.ctx.helper._.toInteger(iBegin) || 0;
+            iEnd = this.ctx.helper._.toInteger(iEnd) || 0;
+            const stages = await this._getValidStages(tender.id, 'asc'), validStages = [];
+            let preStage, endStage;
+            for (const stage of stages) {
+                if (stage.order < iBegin) {
+                    if (!preStage || preStage.order < stage.order) preStage = stage;
+                } else if (stage.order > iEnd) {
+                    if (!endStage || endStage.order > stage.order) endStage = stage;
+                } else {
+                    validStages.push(stage);
                 }
-                ctx.body = responseData;
-            } catch (err) {
-                ctx.helper.log(err);
-                ctx.body = this.ajaxErrorBody(err, '查询数据错误');
             }
+            return { type: 'stages', stages: validStages, preStage, endStage };
         }
+        async _filterTimeZone(tender, zone) {
+            const times = zone.split(' - ');
+            if (times.length !== 2) throw '选择的汇总周期无效';
+            const beginTime = moment(times[0], 'YYYY-MM');
+            const endTime = moment(times[1], 'YYYY-MM');
 
-        /**
-         * 期计量 汇总 页面
-         *
-         * @param {Object} ctx - egg全局变量
-         * @return {void}
-         */
-        async gatherStage(ctx) {
-            try {
-                const renderData = {
-                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.gather.stage)
+            const stages = await this._getValidStages(tender.id, 'asc'), validStages = [];
+            let preStage, endStage;
+            for (const stage of stages) {
+                const sTime = moment(stage.s_time, 'YYYY-MM');
+                if (sTime.isBetween(beginTime, endTime, null, '[]')) {
+                    validStages.push(stage);
+                } else if (sTime.isBefore(beginTime)) {
+                    if (!preStage || moment(preStage.s_time, 'YYYY-MM').isBefore(sTime)) preStage = stage;
+                } else if (sTime.isAfter(endTime)) {
+                    if (!endStage || moment(endStage.s_time, 'YYYY-MM').isAfter(sTime)) endStage = stage;
+                }
+            }
+            return { type: 'stages', stages: validStages, preStage, endStage };
+        }
+        async _filterStages(tender, stageInfo) {
+            switch (stageInfo.type) {
+                case 'stage':
+                    return await this._filterOrder(tender, stageInfo.stage);
+                case 'month':
+                    return await this._filterMonth(tender, stageInfo.month);
+                case 'final':
+                    return await this._filterFinal(tender);
+                case 'checked-final':
+                    return await this._filterCheckedFinal(tender);
+                case 'zone':
+                    return await this._filterTimeZone(tender, stageInfo.zone);
+                case 'stage-zone':
+                    return await this._filterOrderZone(tender, stageInfo.stage_zone);
+                default:
+                    return;
+            }
+        }
+        async _loadStageData(tender, stage) {
+            await this.ctx.service.stage.doCheckStage(stage);
+            const bills = await this.ctx.service.ledger.getAllDataByCondition({
+                columns: ['id', 'ledger_id', 'ledger_pid', 'level', 'full_path', 'is_leaf', 'order', 'code', 'b_code', 'name', 'unit', 'unit_price', 'quantity', 'total_price'],
+                where: { tender_id: tender.id },
+            });
+            const curStage = stage.readOnly
+                ? await this.ctx.service.stageBills.getAuditorStageData2(tender.id, stage.id, stage.curTimes, stage.curOrder)
+                : await this.ctx.service.stageBills.getLastestStageData2(tender.id, stage.id);
+            const preStage = stage.preCheckedStage ? await this.ctx.service.stageBillsFinal.getFinalData(tender, stage.preCheckedStage.order) : [];
+            const bpcStage = await this.ctx.service.stageBillsPc.getAllDataByCondition({ where: { sid: stage.id } });
+            this.ctx.helper.assignRelaData(bills, [
+                { data: curStage, fields: ['contract_qty', 'contract_tp', 'qc_qty', 'qc_tp'], prefix: '', relaId: 'lid' },
+                { data: preStage, fields: ['contract_qty', 'contract_tp', 'qc_qty', 'qc_tp'], prefix: 'pre_', relaId: 'lid' },
+                { data: bpcStage, fields: ['contract_pc_tp', 'qc_pc_tp', 'pc_tp'], prefix: '', relaId: 'lid' },
+            ]);
+
+            const pos = await this.ctx.service.pos.getAllDataByCondition({
+                columns: ['id', 'lid', 'name', 'quantity'],
+                where: { tid: tender.id },
+            });
+            const curStagePos = stage.readOnly
+                ? await this.ctx.service.stagePos.getAuditorStageData2(tender.id, stage.id, stage.curTimes, stage.curOrder)
+                : await this.ctx.service.stagePos.getLastestStageData2(tender.id, stage.id);
+            const preStagePos = stage.preCheckedStage ? await this.ctx.service.stagePosFinal.getFinalData(tender, stage.preCheckedStage.order) : [];
+            this.ctx.helper.assignRelaData(pos, [
+                { data: curStagePos, fields: ['contract_qty', 'qc_qty'], prefix: '', relaId: 'pid' },
+                { data: preStagePos, fields: ['contract_qty', 'qc_qty'], prefix: 'pre_', relaId: 'pid' },
+            ]);
+            return [bills, pos];
+        }
+        async _loadStagesData(tender, stages, preStage, endStage) {
+            const indexPre = 'id_';
+            const sumAssignRelaData = function (index, rela) {
+                const loadFields = function (datas, fields, prefix, relaId) {
+                    for (const d of datas) {
+                        const key = indexPre + d[relaId];
+                        const m = index[key];
+                        if (m) {
+                            for (const f of fields) {
+                                if (d[f] !== undefined) {
+                                    m[prefix + f] = helper.add(m[prefix + f], d[f]);
+                                }
+                            }
+                        }
+                    }
                 };
-                await this.layout('spss/compare_tz.ejs', renderData, 'spss/gather_stage_modal.ejs');
-            } catch (err) {
-                ctx.helper.log(err);
+                for (const r of rela) {
+                    loadFields(r.data, r.fields, r.prefix, r.relaId);
+                }
+            };
+            const bills = await this.ctx.service.ledger.getAllDataByCondition({
+                columns: ['id', 'ledger_id', 'ledger_pid', 'level', 'full_path', 'is_leaf', 'order', 'code', 'b_code', 'name', 'unit', 'unit_price', 'quantity', 'total_price'],
+                where: { tender_id: tender.id },
+            });
+            const pos = await this.ctx.service.pos.getAllDataByCondition({
+                columns: ['id', 'lid', 'name', 'quantity'],
+                where: { tid: tender.id },
+            });
+
+            let billsIndexData = {};
+            for (const bd of billsData) {
+                billsIndexData[indexPre + bd.id] = bd;
+            }
+            let posIndexData = {};
+            for (const p of posData) {
+                posIndexData[indexPre + p.id] = p;
+            }
+
+            if (preStage) {
+                const endStage = await this.ctx.service.stageBillsFinal.getFinalData(tender, preStage.order);
+                sumAssignRelaData(billsIndexData, [
+                    { data: endStage, fields: ['contract_qty', 'contract_tp', 'qc_qty', 'qc_tp'], prefix: 'pre_', relaId: 'lid'},
+                ]);
+                const endStagePos = await this.ctx.service.stagePosFinal.getFinalData(tender, preStage.order);
+                sumAssignRelaData(posIndexData, [
+                    {data: endStagePos, fields: ['contract_qty', 'qc_qty'], prefix: 'pre_', relaId: 'lid'},
+                ]);
+            }
+
+            for (const stage of stages) {
+                await this.ctx.service.stage.doCheckStage(stage);
+                const curStage = stage.readOnly
+                    ? await this.ctx.service.stageBills.getAuditorStageData2(tender.id, stage.id, stage.curTimes, stage.curOrder)
+                    : await this.ctx.service.stageBills.getLastestStageData2(tender.id, stage.id);
+                const bpcStage = await this.ctx.service.stageBillsPc.getAllDataByCondition({ where: { sid: stage.id } });
+                sumAssignRelaData(billsIndexData, [
+                    {data: curStage, fields: ['contract_qty', 'contract_tp', 'qc_qty', 'qc_tp'], prefix: '', relaId: 'lid'},
+                    { data: bpcStage, fields: ['contract_pc_tp', 'qc_pc_tp', 'pc_tp'], prefix: '', relaId: 'lid' },
+                ]);
+
+                const curStagePos = stage.readOnly
+                    ? await this.ctx.service.stagePos.getAuditorStageData2(tender.id, stage.id, stage.curTimes, stage.curOrder)
+                    : await this.ctx.service.stagePos.getLastestStageData2(tender.id, stage.id);
+                sumAssignRelaData(posIndexData, [
+                    {data: curStagePos, fields: ['contract_qty', 'qc_qty'], prefix: '', relaId: 'pid'},
+                ]);
             }
+            return [bills, pos];
         }
-        /**
-         * 获取 期计量 汇总 数据(Ajax)
-         * @param ctx
-         * @returns {Promise<void>}
-         */
-        async loadGatherStage(ctx) {
+        async _loadStageLedgerData(tender, filter) {
+            return filter.type === 'stage'
+                ? await this._loadStageData(tender, filter.stage)
+                : await this._loadStagesData(tender, filter.stages, filter.preStage, filter.endStage);
+        }
+        async _loadJgclData(tender, stages) {
+
+        };
+        async _loadYjclData(tender, stages) {
+
+        };
+        async _loadBonusData(tender, stages) {
+
+        };
+        async _loadSafeProdData(tender, stages) {
+
+        };
+        async _loadTempLandData(tender, stages) {
+
+        };
+        async _loadOtherData(tender, stages) {
+
+        };
+        async _loadTenderData(tid, filter, stageInfo) {
+            this.ctx.tender = null;
+            const tender = await this.ctx.service.tender.checkTender(tid);
+            const result = { id: tender.id, name: tender.name };
+            let stageFilter = null;
+            for (const f of filter) {
+                switch (f) {
+                    case 'info':
+                        result.info = await this._loadInfoData(tender);
+                        break;
+                    case 'ledger':
+                        if (filter.indexOf('stage') < 0) [result.bills, result.pos] = await this._loadLedgerData(tender);
+                        break;
+                    case 'stage':
+                        if (!stageFilter) stageFilter = await this._filterStages(tender, stageInfo);
+                        [result.bills, result.pos, result.stages] = await this._loadStageLedgerData(tender, stageFilter);
+                        break;
+                    case 'jgcl':
+                        if (!stageFilter) stageFilter = await this._filterStages(tender, stageInfo);
+                        result.jgcl = await this._loadJgclData(tender, stageFilter);
+                        break;
+                    case 'yjcl':
+                        if (!stageFilter) stageFilter = await this._filterStages(tender, stageInfo);
+                        result.jgcl = await this._loadYjclData(tender, stageFilter);
+                        break;
+                    case 'bonus':
+                        if (!stageFilter) stageFilter = await this._filterStages(tender, stageInfo);
+                        result.jgcl = await this._loadBonusData(tender, stageFilter);
+                        break;
+                    case 'safeProd':
+                        if (!stageFilter) stageFilter = await this._filterStages(tender, stageInfo);
+                        result.jgcl = await this._loadSafeProdData(tender, stageFilter);
+                        break;
+                    case 'tempLand':
+                        if (!stageFilter) stageFilter = await this._filterStages(tender, stageInfo);
+                        result.jgcl = await this._loadTempLandData(tender, stageFilter);
+                        break;
+                    case 'other':
+                        if (!stageFilter) stageFilter = await this._filterStages(tender, stageInfo);
+                        result.jgcl = await this._loadOtherData(tender, stageFilter);
+                        break;
+                    default: throw '查询的数据类型未定义';
+                }
+            }
+            return result;
+        }
+
+        async load(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
-                const responseData = {err: 0, msg: '', data: []};
-
-                for (const t of data.tenders) {
-                    responseData.data.push(await this._getStageData(t.id, t.sorder));
+                const filter = data.filter.split(',');
+                const result = [];
+                for (const t of data.tender) {
+                    result.push(await this._loadTenderData(t.tid, filter, t.stageInfo));
                 }
-                ctx.body = responseData;
+                ctx.body = { err: 0, msg: '', data: result };
             } catch (err) {
-                ctx.helper.log(err);
-                ctx.body = this.ajaxErrorBody(err, '查询数据错误');
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '加载数据错误');
             }
         }
+
+        async _getTzData(tid, includePos = false) {
+            const tender = await this.ctx.service.tender.getTender(tid);
+            if (!tender || tender.project_id !== this.ctx.session.sessionProject.id) {
+                throw '不存在该标段';
+            }
+            const bills = await this.ctx.service.ledger.getData(tid);
+            const pos = tender.measure_type === measureType.tz.value || includePos
+                ? await this.ctx.service.pos.getPosData({tid: tid})
+                : [];
+
+            return { id: tid, name: tender.name, bills: bills, pos: pos };
+        }
+
+        async _checkStage(tid, sorder) {
+            const stage = await this.service.stage.getDataByCondition({ tid: tid, order: sorder });
+            if (!stage) throw '期数据错误';
+            await this.service.stage.doCheckStage(stage);
+            return stage;
+        }
+
+        async _getStageData(tid, sorder) {
+            const data = await this._getTzData(tid, true);
+            const stage = await this._checkStage(tid, sorder);
+            const bills = await this.ctx.service.stageBills.getAuditorStageData2(tid, stage.id, stage.curTimes, stage.curOrder);
+            const pos = await this.ctx.service.stagePos.getAuditorStageData2(tid, stage.id, stage.curTimes, stage.curOrder);
+            data.stage = {
+                sid: stage.id, sorder: stage.order, curTimes: stage.curTimes, curOrder: stage.curOrder,
+                bills: bills, pos: pos
+            };
+            return data;
+        }
+
         /**
          * 检测台账 页面
          *

+ 24 - 13
app/public/js/path_tree.js

@@ -322,6 +322,12 @@ const createNewPathTree = function (type, setting) {
             //     });
             // }
         }
+        clearData() {
+            this.items = {};
+            this.nodes = [];
+            this.datas = [];
+            this.children = [];
+        }
         /**
          * 树结构根据显示排序
          */
@@ -373,10 +379,7 @@ const createNewPathTree = function (type, setting) {
         loadDatas(datas) {
             const self = this;
             // 清空旧数据
-            this.items = {};
-            this.nodes = [];
-            this.datas = [];
-            this.children = [];
+            this.clearData();
             // 加载全部数据
             this.sortByLevel(datas);
             const setting = this.setting;
@@ -1822,28 +1825,29 @@ const createNewPathTree = function (type, setting) {
             return this._newId++;
         }
 
-        findCompareNode(node, parent) {
+        findCompareNode(node, parent, source) {
             if (this.setting.findNode) {
-                return this.setting.findNode(this, node, parent);
+                return this.setting.findNode(this, node, parent, source);
             } else {
                 const siblings = parent ? parent.children : this.children;
+                const checkData = { code: node.code || '', b_code: node.b_code || '', name: node.name || '', unit: node.unit || '', unit_price: node.unit_price || 0 };
                 return siblings.find(function (x) {
-                    return node.b_code
-                        ? x.b_code === node.b_code && x.name === node.name && x.unit === node.unit && x.unit_price === node.unit_price
-                        : x.code === node.code && x.name === node.name;
+                    return checkData.b_code
+                        ? x.b_code === checkData.b_code && x.name === checkData.name && x.unit === checkData.unit && x.unit_price === checkData.unit_price
+                        : x.code === checkData.code && x.name === checkData.name;
                 });
             }
         }
 
         loadCompareNode(source, node, parent, loadFun) {
-            let cur = this.findCompareNode(node, parent);
+            let cur = this.findCompareNode(node, parent, source);
             if (!cur) {
                 const siblings = parent ? parent.children : this.children;
                 const id = this.newId;
                 cur = {
                     children: [], pos: [],
-                    code: node.code, b_code: node.b_code, name: node.name,
-                    unit: node.unit, unit_price: node.unit_price,
+                    code: node.code || '', b_code: node.b_code || '', name: node.name || '',
+                    unit: node.unit || '', unit_price: node.unit_price || 0,
                 };
                 cur[this.setting.id] = id;
                 cur[this.setting.pid] = parent ? parent[this.setting.id] : this.setting.rootId;
@@ -1863,11 +1867,17 @@ const createNewPathTree = function (type, setting) {
             const self = this;
             const addSortNode = function (node) {
                 self.nodes.push(node);
+                if (self.setting.sortFun) {
+                    self.setting.sortFun(node.children);
+                }
                 for (const c of node.children) {
                     addSortNode(c);
                 }
-            }
+            };
             this.nodes = [];
+            if (this.setting.sortFun) {
+                this.setting.sortFun(this.children);
+            }
             for (const n of this.children) {
                 addSortNode(n);
             }
@@ -1888,6 +1898,7 @@ const createNewPathTree = function (type, setting) {
         }
 
         loadCompareData(data1, data2) {
+            this.clearData();
             this.loadCompareTree(data1, this.setting.loadInfo1);
             this.loadCompareTree(data2, this.setting.loadInfo2);
             for (const d of this.datas) {

+ 275 - 0
app/public/js/shares/tender_select_multi.js

@@ -0,0 +1,275 @@
+const TenderSelectMulti = function (setting) {
+    $('#tsm-title').html(setting.title);
+    if (setting.type === 'gather' && setting.type === 'stage') {
+        $('#tsm-stage-info').show();
+    }
+    $('[name=tsm-source]').click(function() {
+        $('[name=gather-type]').hide();
+        const type = this.value;
+        const gatherBy = $(`#gather-by-${type}`);
+        if (gatherBy.length > 0) {
+            $('#tsm-stage-info-detail').show();
+            $(`#gather-by-${type}`).show();
+        } else {
+            $('#tsm-stage-info-detail').hide();
+        }
+    });
+    $('.datepicker-here').datepicker({
+        autoClose: true,
+    });
+    const tsObj = {
+        setting,
+        selectSpread: null,
+        selectSheet: null,
+        resultSpread: null,
+        resultSheet: null,
+        tenderSourceTree: null,
+        orgHistroy: {},
+        trHistory: {},
+        trArray: [],
+        _rebuildStageSelect: function () {
+            if (tsObj.setting.type === 'compare') {
+                const getItems = function (data) {
+                    if (!data) return [];
+                    const items = [];
+                    for (let i = 1; i <= data.stageCount; i++) {
+                        items.push({value: i, text: `第${i}期`});
+                    }
+                    return items;
+                };
+                for (let i = 0; i < tsObj.resultSheet.getRowCount(); i++) {
+                    const cellType2 = new spreadNS.CellTypes.ComboBox().itemHeight(10).editorValueType(spreadNS.CellTypes.EditorValueType.value).items(getItems(tsObj.trArray[i]));
+                    tsObj.resultSheet.getCell(i, 1).cellType(cellType2);
+                }
+            }
+        },
+        _initSelected: function () {
+            for (const node of this.tenderSourceTree.nodes) {
+                node.selected = this.trArray.findIndex(x => { return node.tid === x.tid; }) >= 0;
+            }
+        },
+        _addTender: function (tender) {
+            const tr = tsObj.trArray.find(x => { return x.tid === tender.tid; });
+            const t = { tid: tender.tid, name: tender.name, stageCount: tender.stageCount };
+            if (!tr) tsObj.trArray.push(t);
+            return t;
+        },
+        _removeTender: function (tender) {
+            const gri = tsObj.trArray.findIndex(function (x, i, arr) {
+                return x.tid === tender.tid;
+            });
+            if (gri >= 0) tsObj.trArray.splice(gri, 1);
+        },
+        reloadResultData: function () {
+            SpreadJsObj.reLoadSheetData(tsObj.resultSheet);
+            this._rebuildStageSelect();
+        },
+        tsButtonClicked: function (e, info) {
+            if (!info.sheet.zh_setting) return;
+
+            const col = info.sheet.zh_setting.cols[info.col];
+            if (col.field !== 'selected') return;
+
+            const node = SpreadJsObj.getSelectObject(info.sheet);
+            if (setting.type === 'compare') {
+                if (node.children && node.children.length > 0) {
+                    toastr.warning('对比标段请直接勾选需要对比的标段');
+                    return;
+                }
+                if (!node.selected && tsObj.trArray.length >= 2) {
+                    toastr.warning('仅可选择两个标段进行对比');
+                    return;
+                }
+            }
+            node.selected = !node.selected;
+            if (node.children && node.children.length > 0) {
+                const posterity = tsObj.tenderSourceTree.getPosterity(node);
+                for (const p of posterity) {
+                    p.selected = node.selected;
+                    if (!p.children || p.children.length === 0){
+                        if (p.selected) {
+                            tsObj._addTender(p);
+                        } else {
+                            tsObj._removeTender(p);
+                        }
+                    }
+                }
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, posterity.length + 1);
+            } else {
+                if (node.selected) {
+                    tsObj._addTender(node);
+                } else {
+                    tsObj._removeTender(node);
+                }
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+            }
+            tsObj.reloadResultData();
+        },
+        trEditEnded: function (e, info) {
+            const data = SpreadJsObj.getSelectObject(info.sheet);
+            if (!data) return;
+            const col = info.sheet.zh_setting.cols[info.col];
+            data[col.field] = info.sheet.getValue(info.row, info.col);
+        },
+        loadTenders: function () {
+            postData(`/sp/${spid}/list/load2`, {type: this.setting.dataType + '-checked' }, data => {
+                tsObj.tenderSourceTree = Tender2Tree.convert(data.category, data.tenders, data.ledgerAuditConst, data.stageAuditConst);
+                SpreadJsObj.loadSheetData(tsObj.selectSheet, SpreadJsObj.DataType.Tree, tsObj.tenderSourceTree);
+                SpreadJsObj.loadSheetData(tsObj.resultSheet, SpreadJsObj.DataType.Data, tsObj.trArray);
+            });
+        },
+        getSelectData: function() {
+            const selectData =  tsObj.trArray;
+            if (selectData.length === 0) {
+                toastr.warning('请选择标段');
+                return;
+            }
+            if (this.setting.type === 'compare') {
+                if (selectData.length !== 2) {
+                    toastr.warning('请选择两个标段进行对比');
+                    return;
+                }
+                if (this.setting.dataType === 'stage') {
+                    for (const s of selectData) {
+                        if (!s.stage) {
+                            toastr.warning('请选择标段进行对比的期');
+                            return;
+                        }
+                        s.stageInfo = { type: 'stage', stage: s.stage };
+                    }
+                }
+            }
+            if (this.setting.type === 'gather' && this.setting.dataType === 'stage') {
+                // todo 检查汇总选项
+                const stage = { type: $('[name=tsm-source]:checked').val() };
+                if (stage.type === 'stage') {
+                    stage.stage = _.toInteger($('#gather-stage').val()) || 0;
+                    if (!stage.stage) {
+                        toastr.warning('请选择 汇总期');
+                        return;
+                    }
+                    const validStage = _.min(_.map(tsObj.trArray, 'stageCount'));
+                    if (stage.stage > validStage) {
+                        toastr.warning('选择的期无效,请重新选择');
+                        return;
+                    }
+                } else if (stage.type === 'month') {
+                    stage.month = $('#gather-month').val();
+                    if (stage.month === '') {
+                        toastr.warning('请选择 汇总年月');
+                        return;
+                    }
+                } else if (stage.type === 'zone') {
+                    stage.zone = $('#gather-zone').val();
+                    if (stage.zone === '') {
+                        toastr.warning('请选择 汇总周期');
+                        return;
+                    } else if(stage.zone.indexOf(' - ') < 0) {
+                        toastr.warning('请选择 完整汇总周期');
+                        return;
+                    }
+                } else if (stage.type === 'stage-zone') {
+                    const stageBegin = _.toInteger($('#gather-stage-begin').val()) || 0;
+                    const stageEnd = _.toInteger($('#gather-stage-end').val()) || 0;
+                    const validStage = _.max(_.map(tsObj.trArray, 'stageCount'));
+                    if (!stageBegin || !stageEnd) {
+                        toastr.warning('请选择 汇总开始期与结束期');
+                        return;
+                    }
+                    if (stageEnd <= stageBegin) {
+                        toastr.warning('结束期应大于开始期');
+                        return;
+                    }
+                    if (stageEnd > validStage) {
+                        toastr.warning('选择的期无效,请重新选择');
+                        return;
+                    }
+                    stage.stage_zone = stageBegin + ':' + stageEnd;
+                } else if (stage.type === 'custom-zone') {
+                    stage.custom_zone = $('#gather-custom-zone').val();
+                    if (stage.custom_zone === '') {
+                        toastr.warning('请选择 汇总周期');
+                        return;
+                    } else if(stage.custom_zone.indexOf(' - ') < 0) {
+                        toastr.warning('请选择 完整汇总周期');
+                        return;
+                    }
+                }
+                selectData.forEach(s => { s.stageInfo = stage; });
+            }
+            return selectData;
+        },
+        initTenderSelect: function () {
+            if (this.selectSpread) return;
+
+            this.selectSpread = SpreadJsObj.createNewSpread($('#tsm-select-spread')[0]);
+            this.selectSheet = this.selectSpread.getActiveSheet();
+            SpreadJsObj.initSheet(this.selectSheet, {
+                cols: [
+                    {title: '选择', field: 'selected', hAlign: 1, width: 40, formatter: '@', cellType: 'checkbox'},
+                    {title: '名称', field: 'name', hAlign: 0, width: 300, formatter: '@', cellType: 'tree'},
+                    {title: '期数', field: 'phase', hAlign: 1, width: 80, formatter: '@'},
+                    {title: '状态', field: 'status', hAlign: 1, width: 80, formatter: '@'}
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                headColWidth: [30],
+                selectedBackColor: '#fffacd',
+                readOnly: true,
+            });
+
+            this.resultSpread = SpreadJsObj.createNewSpread($('#tsm-result-spread')[0]);
+            this.resultSheet = this.resultSpread.getActiveSheet();
+            const resultSpreadSetting = {
+                cols: [
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 300, formatter: '@', readOnly: true, cellType: 'ellipsisAutoTip'}
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                headColWidth: [30],
+                getColor: function (sheet, data, row, col, defaultColor) {
+                    if (data) {
+                        return data.invalid ? '#ddd' : defaultColor;
+                    } else {
+                        return defaultColor;
+                    }
+                }
+            };
+            if (this.setting.type === 'compare' && this.setting.dataType === 'stage') {
+                resultSpreadSetting.cols.push({ title: '可选期', colSpan: '1', rowSpan: '1', field: 'stage', hAlign: 0, width: 60 });
+            }
+            SpreadJsObj.initSheet(this.resultSheet, resultSpreadSetting);
+            this.selectSpread.bind(spreadNS.Events.ButtonClicked, tsObj.tsButtonClicked);
+            if (this.setting.type === 'compare' && this.setting.dataType === 'stage') {
+                this.resultSpread.bind(spreadNS.Events.EditEnded, tsObj.trEditEnded);
+            }
+
+            $('#tender-select-multi-ok').click(() => {
+                const selectData = tsObj.getSelectData();
+                if (!selectData) return;
+                this.setting.afterSelect(selectData);
+                $('#tender-select-multi').modal('hide');
+            });
+
+            this.loadTenders();
+        },
+    };
+
+    $('#tender-select-multi').on('shown.bs.modal', () => {
+        tsObj.initTenderSelect();
+    });
+
+    const showSelect = function () {
+        $('#tender-select-multi').modal('show');
+    };
+
+    return { showSelect }
+};

+ 274 - 0
app/public/js/spss_compare_ledger.js

@@ -0,0 +1,274 @@
+const billsCompareField = ['quantity', 'total_price'];
+const posCompareField = ['quantity'];
+
+$(document).ready(() => {
+    autoFlashHeight();
+    const billsSpreadSetting = {
+        cols: [
+            {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 185, formatter: '@', cellType: 'tree'},
+            {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 100, formatter: '@'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+            {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 60, formatter: '@'},
+            {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 60, type: 'Number'},
+            {title: '标段1|数量', colSpan: '2|1', rowSpan: '1|1', field: 'quantity_1', hAlign: 2, width: 80, type: 'Number', formatTitle: '%s|数量'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price_1', hAlign: 2, width: 80, type: 'Number'},
+            {title: '标段2|数量', colSpan: '2|1', rowSpan: '1|1', field: 'quantity_2', hAlign: 2, width: 80, type: 'Number', formatTitle: '%s|数量'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price_2', hAlign: 2, width: 80, type: 'Number'},
+            {title: '差值|数量', colSpan: '2|1', rowSpan: '1|1', field: 'differ_quantity', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'differ_total_price', hAlign: 2, width: 80, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [32, 32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+    const posSpreadSetting = {
+        cols: [
+            {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+            {title: '标段1', colSpan: '1', rowSpan: '1', field: 'quantity_1', hAlign: 2, width: 100, type: 'Number', formatTitle: '%s'},
+            {title: '标段2', colSpan: '1', rowSpan: '1', field: 'quantity_2', hAlign: 2, width: 100, type: 'Number', formatTitle: '%s'},
+            {title: '差值', colSpan: '1', rowSpan: '1', field: 'differ_quantity', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+
+    const billsSpread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
+    const billsSheet = billsSpread.getActiveSheet();
+    sjsSettingObj.setFxTreeStyle(billsSpreadSetting, sjsSettingObj.FxTreeStyle.jz);
+    SpreadJsObj.initSheet(billsSheet, billsSpreadSetting);
+
+    const posSpread = SpreadJsObj.createNewSpread($('#pos-spread')[0]);
+    const posSheet = posSpread.getActiveSheet();
+    SpreadJsObj.initSheet(posSheet, posSpreadSetting);
+
+    const treeSetting = {
+        id: 'ledger_id',
+        pid: 'ledger_pid',
+        order: 'order',
+        level: 'level',
+        full_path: 'full_path',
+        rootId: -1,
+        keys: ['id', 'tender_id', 'ledger_id'],
+        calcFields: ['total_price_1', 'total_price_2', 'differ_tp'],
+        // todo 判断同一清单时,是否应区分 清单是否含计量单元?
+        // findNode: function(tree, node, parent, source) {
+        //     const siblings = parent ? parent.children : tree.children;
+        //     const checkData = { code: node.code || '', b_code: node.b_code || '', name: node.name || '', unit: node.unit || '', unit_price: node.unit_price || 0 };
+        //     return siblings.find(function (x) {
+        //         if (!checkData.b_code) return x.code === checkData.code && x.name === checkData.name;
+        //         const posRange = source.pos.getLedgerPos(node.id);
+        //         const hasPos = posRange && posRange.length > 0;
+        //         const xHasPos = x.pos && x.pos.length > 0;
+        //         return hasPos === xHasPos && x.b_code === checkData.b_code && x.name === checkData.name && x.unit === checkData.unit && x.unit_price === checkData.unit_price;
+        //     });
+        // },
+        loadInfo1: function (node, sourceNode, source) {
+            for (const f of billsCompareField) {
+                node[f + '_1'] = sourceNode[f];
+            }
+            const posRange = source.pos.getLedgerPos(sourceNode.id);
+            if (posRange && posRange.length > 0) {
+                if (!node.pos) node.pos = [];
+                for (const p of posRange) {
+                    let nP = _.find(node.pos, {name: p.name || ''});
+                    if (!nP) {
+                        nP = {name: p.name || ''};
+                        node.pos.push(nP);
+                    }
+                    for (const f of posCompareField) {
+                        nP[f + '_1'] = p[f];
+                    }
+                }
+            }
+        },
+        loadInfo2: function (node, sourceNode, source) {
+            for (const f of billsCompareField) {
+                node[f + '_2'] = sourceNode[f];
+            }
+            const posRange = source.pos.getLedgerPos(sourceNode.id);
+            if (posRange && posRange.length > 0) {
+                if (!node.pos) node.pos = [];
+                for (const p of posRange) {
+                    let nP = _.find(node.pos, {name: p.name || ''});
+                    if (!nP) {
+                        nP = {name: p.name || ''};
+                        node.pos.push(nP);
+                    }
+                    for (const f of posCompareField) {
+                        nP[f + '_2'] = p[f];
+                    }
+                }
+            }
+        },
+        afterLoad: function (tree) {
+            for (const data of tree.datas) {
+                for (const f of billsCompareField) {
+                    data['differ_' + f] = ZhCalc.sub(data[f + '_1'], data[f + '_2']);
+                }
+                for (const p of data.pos) {
+                    for (const f of posCompareField) {
+                        p['differ_' + f] = ZhCalc.sub(p[f + '_1'], p[f + '_2']);
+                    }
+                }
+            }
+        },
+    };
+    const billsTree = createNewPathTree('compare', treeSetting);
+
+    const billsTreeSpreadObj = {
+        selectionChanged: function (e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    posSpreadObj.loadCurPosData();
+                }
+            }
+        },
+    };
+    billsSpread.bind(spreadNS.Events.SelectionChanged, billsTreeSpreadObj.selectionChanged);
+    const posSpreadObj = {
+        loadCurPosData: function () {
+            const node = SpreadJsObj.getSelectObject(billsSheet);
+            if (node) {
+                SpreadJsObj.loadSheetData(posSheet, 'data', node.pos || []);
+            } else {
+                SpreadJsObj.loadSheetData(posSheet, 'data', []);
+            }
+        },
+    };
+
+    const tenderSelect = TenderSelectMulti({
+        title: '对比标段',
+        type: 'compare',
+        dataType: 'ledger',
+        afterSelect: function(select) {
+            const data = { filter: 'ledger', tender: select };
+            postData(`/sp/${spid}/spss/load`, data, function(result) {
+                const tenderTreeSetting = {
+                    id: 'ledger_id',
+                    pid: 'ledger_pid',
+                    order: 'order',
+                    level: 'level',
+                    rootId: -1,
+                    keys: ['id', 'tender_id', 'ledger_id'],
+                    calcFields: ['total_price'],
+                };
+                const tenders = [];
+                for (const [i, t] of result.entries()) {
+                    const fieldName = 'quantity_' + (i + 1);
+                    const billsQty = billsSpreadSetting.cols.find(x => { return x.field === fieldName; });
+                    billsQty.title = billsQty.formatTitle.replace('%s', t.name);
+                    const posQty = posSpreadSetting.cols.find(x => { return x.field === fieldName; });
+                    posQty.title = posQty.formatTitle.replace('%s', t.name);
+
+                    const tender = {
+                        billsTree: createNewPathTree('ledger', tenderTreeSetting),
+                        pos: new PosData({ id: 'id', ledgerId: 'lid', }),
+                    };
+                    tender.billsTree.loadDatas(t.bills);
+                    tender.pos.loadDatas(t.pos);
+                    tenders.push(tender);
+                }
+                SpreadJsObj.reLoadSheetHeader(billsSheet);
+                SpreadJsObj.reLoadSheetHeader(posSheet);
+
+                billsTree.loadCompareData(tenders[0], tenders[1]);
+                treeCalc.calculateAll(billsTree);
+                SpreadJsObj.loadSheetData(billsSheet, SpreadJsObj.DataType.Tree, billsTree);
+            });
+        },
+    });
+    $('#gather-select').click(tenderSelect.showSelect);
+
+    $('#export-excel').click(function() {
+        const excelData = [];
+        for (const node of billsTree.nodes) {
+            const data = {};
+            for (const c of billsSpreadSetting.cols) {
+                data[c.field] = node[c.field];
+            }
+            excelData.push(data);
+            for (const p of node.pos) {
+                const pData = {};
+                for (const c of posSpreadSetting.cols) {
+                    pData[c.field] = p[c.field];
+                }
+                excelData.push(pData);
+            }
+        }
+        SpreadExcelObj.exportSimpleXlsxSheet(billsSpreadSetting, excelData, "台账对比.xlsx");
+    });
+    $.divResizer({
+        select: '#pos-resize',
+        callback: function () {
+            billsSpread.refresh();
+            let bcontent = $(".bcontent-wrap") ? $(".bcontent-wrap").height() : 0;
+            $(".sp-wrap").height(bcontent-30);
+            posSpread.refresh();
+        }
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            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;
+                case "differ":
+                    tree.expandByCustom(function (x) {
+                        const posterity = tree.getPosterity(x);
+                        if (posterity.length === 0) return x.differ_qty || x.differ_tp;
+                        for (const p of posterity) {
+                            if (x.differ_qty || x.differ_tp) return true;
+                        }
+                        return false;
+                    });
+                    SpreadJsObj.refreshTreeRowVisible(sheet);
+            }
+        });
+    })('a[name=showLevel]', billsSheet);
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+                $('.c-body table thead').css('left', '56px');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+                $('.c-body table thead').css('left', '176px');
+            }
+            autoFlashHeight();
+            billsSpread.refresh();
+            posSpread.refresh();
+        }
+    });
+});

+ 347 - 0
app/public/js/spss_compare_stage.js

@@ -0,0 +1,347 @@
+const billsCompareField = [
+    'quantity', 'total_price',
+    'contract_qty', 'contract_tp', 'qc_qty', 'qc_tp', 'gather_qty', 'gather_tp',
+    'end_contract_qty', 'end_contract_tp', 'end_qc_qty', 'end_qc_tp', 'end_gather_qty', 'end_gather_tp',
+];
+const posCompareField = [
+    'quantity',
+    'contract_qty', 'qc_qty', 'gather_qty',
+    'end_contract_qty', 'end_qc_qty', 'end_gather_qty',
+];
+
+$(document).ready(() => {
+    autoFlashHeight();
+    const billsSpreadSetting = {
+        cols: [
+            {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 185, formatter: '@', cellType: 'tree'},
+            {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 100, formatter: '@'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+            {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 60, formatter: '@'},
+            {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 60, type: 'Number'},
+            {title: '标段1|数量', colSpan: '2|1', rowSpan: '1|1', field: 'compare_qty_1', hAlign: 2, width: 80, type: 'Number', formatTitle: '%s|数量'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'compare_tp_1', hAlign: 2, width: 80, type: 'Number'},
+            {title: '标段2|数量', colSpan: '2|1', rowSpan: '1|1', field: 'compare_qty_2', hAlign: 2, width: 80, type: 'Number', formatTitle: '%s|数量'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'compare_tp_2', hAlign: 2, width: 80, type: 'Number'},
+            {title: '差值|数量', colSpan: '2|1', rowSpan: '1|1', field: 'differ_qty', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'differ_tp', hAlign: 2, width: 80, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [32, 32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+    const posSpreadSetting = {
+        cols: [
+            {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+            {title: '标段1', colSpan: '1', rowSpan: '1', field: 'compare_qty_1', hAlign: 2, width: 100, type: 'Number', formatTitle: '%s'},
+            {title: '标段2', colSpan: '1', rowSpan: '1', field: 'compare_qty_2', hAlign: 2, width: 100, type: 'Number', formatTitle: '%s'},
+            {title: '差值', colSpan: '1', rowSpan: '1', field: 'differ_qty', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+
+    const billsSpread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
+    const billsSheet = billsSpread.getActiveSheet();
+    sjsSettingObj.setFxTreeStyle(billsSpreadSetting, sjsSettingObj.FxTreeStyle.jz);
+    SpreadJsObj.initSheet(billsSheet, billsSpreadSetting);
+
+    const posSpread = SpreadJsObj.createNewSpread($('#pos-spread')[0]);
+    const posSheet = posSpread.getActiveSheet();
+    SpreadJsObj.initSheet(posSheet, posSpreadSetting);
+
+    const treeSetting = {
+        id: 'ledger_id',
+        pid: 'ledger_pid',
+        order: 'order',
+        level: 'level',
+        full_path: 'full_path',
+        rootId: -1,
+        keys: ['id', 'tender_id', 'ledger_id'],
+        calcFields: [
+            'total_price_1', 'total_price_2', 'differ_tp',
+            'contract_tp_1', 'qc_tp_1', 'gather_tp_1', 'end_contract_tp_1', 'end_qc_tp_1', 'end_gather_tp_1',
+            'contract_tp_2', 'qc_tp_2', 'gather_tp_2', 'end_contract_tp_2', 'end_qc_tp_2', 'end_gather_tp_2',
+            'differ_contract_tp', 'differ_qc_tp', 'differ_gather_tp', 'differ_end_contract_tp', 'differ_end_qc_tp', 'differ_end_gather_tp',
+        ],
+        // todo 判断同一清单时,是否应区分 清单是否含计量单元?
+        // findNode: function(tree, node, parent, source) {
+        //     const siblings = parent ? parent.children : tree.children;
+        //     const checkData = { code: node.code || '', b_code: node.b_code || '', name: node.name || '', unit: node.unit || '', unit_price: node.unit_price || 0 };
+        //     return siblings.find(function (x) {
+        //         if (!checkData.b_code) return x.code === checkData.code && x.name === checkData.name;
+        //         const posRange = source.pos.getLedgerPos(node.id);
+        //         const hasPos = posRange && posRange.length > 0;
+        //         const xHasPos = x.pos && x.pos.length > 0;
+        //         return hasPos === xHasPos && x.b_code === checkData.b_code && x.name === checkData.name && x.unit === checkData.unit && x.unit_price === checkData.unit_price;
+        //     });
+        // },
+        loadInfo1: function (node, sourceNode, source) {
+            for (const f of billsCompareField) {
+                node[f + '_1'] = sourceNode[f];
+            }
+            const posRange = source.pos.getLedgerPos(sourceNode.id);
+            if (posRange && posRange.length > 0) {
+                if (!node.pos) node.pos = [];
+                for (const p of posRange) {
+                    let nP = _.find(node.pos, {name: p.name || ''});
+                    if (!nP) {
+                        nP = {name: p.name || ''};
+                        node.pos.push(nP);
+                    }
+                    for (const f of posCompareField) {
+                        nP[f + '_1'] = p[f];
+                    }
+                }
+            }
+        },
+        loadInfo2: function (node, sourceNode, source) {
+            for (const f of billsCompareField) {
+                node[f + '_2'] = sourceNode[f];
+            }
+            const posRange = source.pos.getLedgerPos(sourceNode.id);
+            if (posRange && posRange.length > 0) {
+                if (!node.pos) node.pos = [];
+                for (const p of posRange) {
+                    let nP = _.find(node.pos, {name: p.name || ''});
+                    if (!nP) {
+                        nP = {name: p.name || ''};
+                        node.pos.push(nP);
+                    }
+                    for (const f of posCompareField) {
+                        nP[f + '_2'] = p[f];
+                    }
+                }
+            }
+        },
+        afterLoad: function (tree) {
+            for (const data of tree.datas) {
+                for (const f of billsCompareField) {
+                    data['differ_' + f] = ZhCalc.sub(data[f + '_1'], data[f + '_2']);
+                }
+                for (const p of data.pos) {
+                    for (const f of posCompareField) {
+                        p['differ_' + f] = ZhCalc.sub(p[f + '_1'], p[f + '_2']);
+                    }
+                }
+            }
+        },
+    };
+    const billsTree = createNewPathTree('compare', treeSetting);
+
+    const billsTreeSpreadObj = {
+        compareData: '',
+        selectionChanged: function (e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    posSpreadObj.loadCurPosData();
+                }
+            }
+        },
+        loadShowData: function() {
+            const field = $('[name=compare-data]:checked').val();
+            for (const node of billsTree.nodes) {
+                node.compare_qty_1 = node[field + '_qty_1'];
+                node.compare_tp_1 = node[field + '_tp_1'];
+                node.compare_qty_2 = node[field + '_qty_2'];
+                node.compare_tp_2 = node[field + '_tp_2'];
+                node.differ_qty = node['differ_' + field + '_qty'];
+                node.differ_tp = node['differ_' + field + '_tp'];
+                for (const p of node.pos) {
+                    p.compare_qty_1 = p[field + '_qty_1'];
+                    p.compare_qty_2 = p[field + '_qty_2'];
+                    p.differ_qty = p['differ_' + field + '_qty'];
+                }
+            }
+            this.compareData = field;
+        },
+        refreshShowData: function() {
+            const field = $('[name=compare-data]:checked').val();
+            if (field === this.compareData) return;
+
+            this.loadShowData();
+            SpreadJsObj.reloadColData(billsSheet, 5, 6);
+            SpreadJsObj.reloadColData(posSheet, 1, 3);
+        }
+    };
+    billsSpread.bind(spreadNS.Events.SelectionChanged, billsTreeSpreadObj.selectionChanged);
+    const posSpreadObj = {
+        loadCurPosData: function () {
+            const node = SpreadJsObj.getSelectObject(billsSheet);
+            if (node) {
+                console.log(node.pos);
+                SpreadJsObj.loadSheetData(posSheet, 'data', node.pos || []);
+            } else {
+                SpreadJsObj.loadSheetData(posSheet, 'data', []);
+            }
+        },
+    };
+
+    const tenderSelect = TenderSelectMulti({
+        title: '对比标段',
+        type: 'compare',
+        dataType: 'stage',
+        afterSelect: function(select) {
+            const data = { filter: 'stage', tender: select };
+            postData(`/sp/${spid}/spss/load`, data, function(result) {
+                const tenderTreeSetting = {
+                    id: 'ledger_id',
+                    pid: 'ledger_pid',
+                    order: 'order',
+                    level: 'level',
+                    rootId: -1,
+                    keys: ['id', 'tender_id', 'ledger_id'],
+                    calcFields: ['total_price', 'contract_tp', 'qc_tp', 'gather_tp', 'end_contract_tp', 'end_qc_tp', 'end_gather_tp'],
+                    calcFun: function(node) {
+                        if (!node.children || node.children.length === 0) {
+                            node.pre_gather_qty = ZhCalc.add(node.pre_contract_qty, node.pre_qc_qty);
+                            node.gather_qty = ZhCalc.add(node.contract_qty, node.qc_qty);
+                            node.end_contract_qty = ZhCalc.add(node.pre_contract_qty, node.contract_qty);
+                            node.end_gather_qty = ZhCalc.add(node.pre_gather_qty, node.gather_qty);
+                            node.end_qc_qty = ZhCalc.add(node.pre_qc_qty, node.qc_qty);
+                            node.end_qc_minus_qty = ZhCalc.add(node.pre_qc_minus_qty, node.qc_minus_qty);
+                        }
+                        node.pre_gather_tp = ZhCalc.add(node.pre_contract_tp, node.pre_qc_tp);
+                        node.gather_tp = ZhCalc.sum([node.contract_tp, node.qc_tp, node.pc_tp]);
+                        node.end_contract_tp = ZhCalc.sum([node.pre_contract_tp, node.contract_tp, node.contract_pc_tp]);
+                        node.end_qc_tp = ZhCalc.sum([node.pre_qc_tp, node.qc_tp, node.qc_pc_tp]);
+                        node.end_gather_tp = ZhCalc.add(node.pre_gather_tp, node.gather_tp);
+                    }
+                };
+                const tenderPosSetting = {
+                    id: 'id', ledgerId: 'lid',
+                    calcFun: function(pos) {
+                        pos.pre_gather_qty = ZhCalc.add(pos.pre_contract_qty, pos.pre_qc_qty);
+                        pos.gather_qty = ZhCalc.add(pos.contract_qty, pos.qc_qty);
+                        pos.end_contract_qty = ZhCalc.add(pos.pre_contract_qty, pos.contract_qty);
+                        pos.end_qc_qty = ZhCalc.add(pos.pre_qc_qty, pos.qc_qty);
+                        pos.end_qc_minus_qty = ZhCalc.add(pos.pre_qc_minus_qty, pos.qc_minus_qty);
+                        pos.end_gather_qty = ZhCalc.add(pos.pre_gather_qty, pos.gather_qty);
+                    }
+                };
+                const tenders = [];
+                for (const [i, t] of result.entries()) {
+                    const fieldName = 'compare_qty_' + (i + 1);
+                    const billsQty = billsSpreadSetting.cols.find(x => { return x.field === fieldName; });
+                    billsQty.title = billsQty.formatTitle.replace('%s', t.name);
+                    const posQty = posSpreadSetting.cols.find(x => { return x.field === fieldName; });
+                    posQty.title = posQty.formatTitle.replace('%s', t.name);
+
+                    const tender = {
+                        billsTree: createNewPathTree('ledger', tenderTreeSetting),
+                        pos: new PosData(tenderPosSetting),
+                    };
+                    tender.billsTree.loadDatas(t.bills);
+                    treeCalc.calculateAll(tender.billsTree);
+                    tender.pos.loadDatas(t.pos);
+                    tender.pos.calculateAll();
+                    tenders.push(tender);
+                }
+                SpreadJsObj.reLoadSheetHeader(billsSheet);
+                SpreadJsObj.reLoadSheetHeader(posSheet);
+
+                billsTree.loadCompareData(tenders[0], tenders[1]);
+                treeCalc.calculateAll(billsTree);
+                billsTreeSpreadObj.loadShowData();
+                SpreadJsObj.loadSheetData(billsSheet, SpreadJsObj.DataType.Tree, billsTree);
+            });
+        },
+    });
+    $('#gather-select').click(tenderSelect.showSelect);
+
+    $('[name=compare-data]').click(function() {
+        billsTreeSpreadObj.refreshShowData();
+    });
+
+    $('#export-excel').click(function() {
+        const excelData = [];
+        for (const node of billsTree.nodes) {
+            const data = {};
+            for (const c of billsSpreadSetting.cols) {
+                data[c.field] = node[c.field];
+            }
+            excelData.push(data);
+            for (const p of node.pos) {
+                const pData = {};
+                for (const c of posSpreadSetting.cols) {
+                    pData[c.field] = p[c.field];
+                }
+                excelData.push(pData);
+            }
+        }
+        SpreadExcelObj.exportSimpleXlsxSheet(billsSpreadSetting, excelData, "计量对比.xlsx");
+    });
+    $.divResizer({
+        select: '#pos-resize',
+        callback: function () {
+            billsSpread.refresh();
+            let bcontent = $(".bcontent-wrap") ? $(".bcontent-wrap").height() : 0;
+            $(".sp-wrap").height(bcontent-30);
+            posSpread.refresh();
+        }
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            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;
+                case "differ":
+                    tree.expandByCustom(function (x) {
+                        const posterity = tree.getPosterity(x);
+                        if (posterity.length === 0) return x.differ_qty || x.differ_tp;
+                        for (const p of posterity) {
+                            if (x.differ_qty || x.differ_tp) return true;
+                        }
+                        return false;
+                    });
+                    SpreadJsObj.refreshTreeRowVisible(sheet);
+            }
+        });
+    })('a[name=showLevel]', billsSheet);
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+                $('.c-body table thead').css('left', '56px');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+                $('.c-body table thead').css('left', '176px');
+            }
+            autoFlashHeight();
+            billsSpread.refresh();
+            posSpread.refresh();
+        }
+    });
+});

+ 9 - 8
app/router.js

@@ -276,6 +276,15 @@ module.exports = app => {
     app.post('/sp/:id/list/batchUpdate', sessionAuth, subProjectCheck, 'tenderController.batchUpdate');
     app.get('/sp/:id/list/refreshCache', sessionAuth, subProjectCheck, 'tenderController.refreshCache');
 
+    // 统计分析
+    app.get('/sp/:id/spss/info', sessionAuth, subProjectCheck, 'spssController.info');
+    app.get('/sp/:id/spss/gather/ledger', sessionAuth, subProjectCheck, 'spssController.gatherLedger');
+    app.get('/sp/:id/spss/gather/stage', sessionAuth, subProjectCheck, 'spssController.gatherStage');
+    app.get('/sp/:id/spss/gather/stage/extra', sessionAuth, subProjectCheck, 'spssController.gatherStageExtra');
+    app.get('/sp/:id/spss/compare/ledger', sessionAuth, subProjectCheck, 'spssController.compareLedger');
+    app.get('/sp/:id/spss/compare/stage', sessionAuth, subProjectCheck, 'spssController.compareStage');
+    app.post('/sp/:id/spss/load', sessionAuth, subProjectCheck, 'spssController.load');
+
     // **标段合同管理 todo 接入项目内部
     // app.get('/sp/:id/contract', sessionAuth, subProjectCheck, 'contractController.index');
     app.get('/sp/:id/contract/tender', sessionAuth, subProjectCheck, 'contractController.tender');
@@ -966,14 +975,6 @@ module.exports = app => {
     app.post('/tender/:id/measure/material/gcl/load', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'materialController.loadGclData');
 
     // 标段对比
-    app.get('/compare/tz', sessionAuth, 'spssController.compareTz');
-    app.post('/compare/tz/load', sessionAuth, 'spssController.loadCompareTz');
-    app.get('/compare/stage', sessionAuth, 'spssController.compareStage');
-    app.post('/compare/stage/load', sessionAuth, 'spssController.loadCompareStage');
-    app.get('/gather/tz', sessionAuth, 'spssController.gatherTz');
-    app.post('/gather/tz/load', sessionAuth, 'spssController.loadGatherTz');
-    app.get('/gather/stage', sessionAuth, 'spssController.gatherStage');
-    app.post('/gather/stage/load', sessionAuth, 'spssController.loadGatherStage');
     app.get('/tools/check-tz', sessionAuth, 'spssController.checkTz');
     app.post('/tools/load', sessionAuth, 'spssController.loadBaseData');
 

+ 2 - 1
app/service/tender.js

@@ -522,8 +522,9 @@ module.exports = app => {
         }
 
         async checkTender(tid) {
-            if (this.ctx.tender) return;
+            if (this.ctx.tender) return this.ctx.tender;
             this.ctx.tender = await this.getCheckTender(tid);
+            return this.ctx.tender;
         }
 
         async setTenderType(tender, type) {

+ 55 - 0
app/view/spss/compare_ledger.ejs

@@ -0,0 +1,55 @@
+<% include ../tender/list_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ../tender/list_sub_mini_menu.ejs %>
+            <div class="title-main  d-flex justify-content-between">
+                <div>
+                    <div class="d-inline-block mr-2">
+                        <!--展开/收起-->
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="zhankai" aria-expanded="false">显示层级</button>
+                            <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                                <a class="dropdown-item" name="showLevel" tag="1" href="javascript:void(0);">第一层</a>
+                                <a class="dropdown-item" name="showLevel" tag="2" href="javascript:void(0);">第二层</a>
+                                <a class="dropdown-item" name="showLevel" tag="3" href="javascript:void(0);">第三层</a>
+                                <a class="dropdown-item" name="showLevel" tag="4" href="javascript:void(0);">第四层</a>
+                                <a class="dropdown-item" name="showLevel" tag="5" href="javascript:void(0);">第五层</a>
+                                <a class="dropdown-item" name="showLevel" tag="last" href="javascript:void(0);">最底层</a>
+                                <a class="dropdown-item" name="showLevel" tag="leafXmj" href="javascript:void(0);">只显示项目节</a>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="d-inline-block">
+                        <button class="btn btn-primary btn-sm mr-2" id="export-excel">导出Excel</button>
+                        <button class="btn btn-sm btn-primary" id="gather-select">对比标段</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-header p-0 col-12"></div>
+        <!--核心内容(两栏)-->
+        <div class="row w-100 sub-content">
+            <!--左栏-->
+            <div class="c-body" id="left-view" style="width: 100%">
+                <!--0号台账模式-->
+                <div class="sjs-height-1" style="overflow: hidden" id="bills-spread">
+                </div>
+                <div class="bcontent-wrap">
+                    <div id="pos-resize" class="resize-y" id="top-spr" r-Type="height" div1=".sjs-height-1" div2=".bcontent-wrap" title="调整大小"><!--调整上下高度条--></div>
+                    <div class="bc-bar mb-1">
+                        <ul class="nav nav-tabs">
+                            <li class="nav-item">
+                                <a class="nav-link active" href="javascript:void(0)">计量单元</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="sp-wrap" id="pos-spread">
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 114 - 0
app/view/spss/compare_select_modal.ejs

@@ -0,0 +1,114 @@
+<div class="modal fade" id="tender-select-multi" data-backdrop="static">
+    <div class="modal-dialog modal-xl" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="tsm-title">对比标段</h5>
+            </div>
+            <div class="modal-body">
+                <div class="" id="tsm-stage-info" style="display: none">
+                    <div class="form-row align-items-center mb-2">
+                        <div class="col-auto"><span class="form-check-label">计量类型:</span></div>
+                        <div class="col-auto">
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="tsm-source-stage" value="stage" name="tsm-source">
+                                <label class="form-check-label" for="tsm-source-stage">指定期</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="tsm-source-final" value="final" name="tsm-source">
+                                <label class="form-check-label" for="tsm-source-final">最新期</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="tsm-source-check-final" value="checked-final" checked="" name="tsm-source">
+                                <label class="form-check-label" for="tsm-source-check-final">最新审批完成期</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="tsm-source-stage-zone" value="stage-zone" name="tsm-source">
+                                <label class="form-check-label" for="tsm-source-stage-zone">期区间</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="tsm-source-month" value="month" name="tsm-source">
+                                <label class="form-check-label" for="tsm-source-month">指定月</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="tsm-source-zone" value="zone" name="tsm-source">
+                                <label class="form-check-label" for="tsm-source-zone">月区间</label>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="form-row align-items-center mb-2" id="tsm-stage-info-detail">
+                        <div class="col-auto"><span class="form-check-label">计量设置:</span>
+                        </div>
+                        <div class="d-flex">
+                            <div id="gather-by-month" name="gather-type" style="display: none">
+                                <div class="input-group input-group-sm">
+                                    <input id="gather-month" class="datepicker-here form-control mt-0" auto-close="true" autocomplete="off" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text" autocomplete="off">
+                                </div>
+                            </div>
+                            <div id="gather-by-zone" name="gather-type" style="display: none">
+                                <div class="input-group input-group-sm">
+                                    <input id="gather-zone" class="datepicker-here form-control mt-0" placeholder="点击选择周期" data-range="true" data-multiple-dates-separator=" - "  data-min-view="months" data-view="months" data-date-format="yyyy-MM" data-language="zh" type="text" autocomplete="off">
+                                </div>
+                            </div>
+                            <div id="gather-by-custom-zone" name="gather-type" style="display: none">
+                                <div class="input-group input-group-sm">
+                                    <input id="gather-custom-zone" class="datepicker-here form-control mt-0" placeholder="点击选择周期" data-range="true" data-multiple-dates-separator=" - "  data-min-view="days" data-view="days" data-date-format="yyyy-MM-dd" data-language="zh" type="text" autocomplete="off">
+                                </div>
+                            </div>
+                            <div id="gather-by-stage" name="gather-type" style="display: none">
+                                <div class="input-group input-group-sm">
+                                    <select class="form-control" style="width: 80px"  id="gather-stage">
+                                        <option>第1期</option>
+                                    </select>
+                                </div>
+                            </div>
+                            <div id="gather-by-stage-zone" name="gather-type" style="display: none">
+                                <div class="input-group input-group-sm">
+                                    <select class="form-control" style="width: 80px"  id="gather-stage-begin">
+                                        <option>第1期</option>
+                                    </select>
+                                    <div class="input-group-prepend">
+                                        <span class="input-group-text">至</span>
+                                    </div>
+                                    <select class="form-control" style="width: 80px"  id="gather-stage-end">
+                                        <option>第5期</option>
+                                    </select>
+                                </div>
+                            </div>
+                            <div id="gather-by-final" name="gather-type" style="display: none">
+                                <div class="input-group input-group-sm">
+                                    <select class="form-control" style="width: 80px"  disabled>
+                                    </select>
+                                </div>
+                            </div>
+                            <div id="gather-by-checked-final" name="gather-type">
+                                <div class="input-group input-group-sm">
+                                    <select class="form-control" style="width: 80px" disabled>
+                                    </select>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-flex">
+                    <div class="col-6 pl-0">
+                        <label>可选标段</label>
+                        <div class="modal-height-500" id="tsm-select-spread">
+                        </div>
+                    </div>
+                    <div class="col-6">
+                        <label>已选标段</label>
+                        <div class="modal-height-500" id="tsm-result-spread">
+                        </div>
+                    </div>
+                </div>
+            </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" id="tender-select-multi-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const category = JSON.parse('<%- JSON.stringify(categoryData) %>');
+</script>

+ 84 - 0
app/view/spss/compare_stage.ejs

@@ -0,0 +1,84 @@
+<% include ../tender/list_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ../tender/list_sub_mini_menu.ejs %>
+            <div class="title-main  d-flex justify-content-between">
+                <div>
+                    <div class="d-inline-block mr-2">
+                        <!--展开/收起-->
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="zhankai" aria-expanded="false">显示层级</button>
+                            <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                                <a class="dropdown-item" name="showLevel" tag="1" href="javascript:void(0);">第一层</a>
+                                <a class="dropdown-item" name="showLevel" tag="2" href="javascript:void(0);">第二层</a>
+                                <a class="dropdown-item" name="showLevel" tag="3" href="javascript:void(0);">第三层</a>
+                                <a class="dropdown-item" name="showLevel" tag="4" href="javascript:void(0);">第四层</a>
+                                <a class="dropdown-item" name="showLevel" tag="5" href="javascript:void(0);">第五层</a>
+                                <a class="dropdown-item" name="showLevel" tag="last" href="javascript:void(0);">最底层</a>
+                                <a class="dropdown-item" name="showLevel" tag="leafXmj" href="javascript:void(0);">只显示项目节</a>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="d-inline-block">
+                        <button class="btn btn-primary btn-sm mr-2" id="export-excel">导出Excel</button>
+                        <button class="btn btn-sm btn-primary" id="gather-select">对比标段</button>
+                    </div>
+                    <div class="d-inline-block ml-2">
+                        <span>对比数据:</span>
+                        <div class="d-inline-block" style="vertical-align: middle">
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input pt-1" type="radio" id="radio_contract" value="contract" name="compare-data">
+                                <label class="form-check-label" for="radio_contract">合同计量</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="radio_qc" value="qc" name="compare-data">
+                                <label class="form-check-label" for="radio_qc">变更计量</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="radio_gather" value="gather" checked="" name="compare-data">
+                                <label class="form-check-label" for="radio_gather">完成计量</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input pt-1" type="radio" id="radio_end_contract" value="end_contract" name="compare-data">
+                                <label class="form-check-label" for="radio_end_contract">截止合同计量</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="radio_end_qc" value="end_qc" name="compare-data">
+                                <label class="form-check-label" for="radio_end_qc">截止变更计量</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" id="radio_end_gather" value="end_gather" name="compare-data">
+                                <label class="form-check-label" for="radio_end_gather">截止完成计量</label>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-header p-0 col-12"></div>
+        <!--核心内容(两栏)-->
+        <div class="row w-100 sub-content">
+            <!--左栏-->
+            <div class="c-body" id="left-view" style="width: 100%">
+                <!--0号台账模式-->
+                <div class="sjs-height-1" style="overflow: hidden" id="bills-spread">
+                </div>
+                <div class="bcontent-wrap">
+                    <div id="pos-resize" class="resize-y" id="top-spr" r-Type="height" div1=".sjs-height-1" div2=".bcontent-wrap" title="调整大小"><!--调整上下高度条--></div>
+                    <div class="bc-bar mb-1">
+                        <ul class="nav nav-tabs">
+                            <li class="nav-item">
+                                <a class="nav-link active" href="javascript:void(0)">计量单元</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="sp-wrap" id="pos-spread">
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 40 - 0
app/view/spss/gather_ledger.ejs

@@ -0,0 +1,40 @@
+<% include ../tender/list_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ../tender/list_sub_mini_menu.ejs %>
+            <div class="title-main  d-flex justify-content-between">
+                <div>
+                    <div class="d-inline-block mr-2">
+                        <!--展开/收起-->
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="zhankai" aria-expanded="false">显示层级</button>
+                            <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                                <a class="dropdown-item" name="showLevel" tag="1" href="javascript:void(0);">第一层</a>
+                                <a class="dropdown-item" name="showLevel" tag="2" href="javascript:void(0);">第二层</a>
+                                <a class="dropdown-item" name="showLevel" tag="3" href="javascript:void(0);">第三层</a>
+                                <a class="dropdown-item" name="showLevel" tag="4" href="javascript:void(0);">第四层</a>
+                                <a class="dropdown-item" name="showLevel" tag="5" href="javascript:void(0);">第五层</a>
+                                <a class="dropdown-item" name="showLevel" tag="last" href="javascript:void(0);">最底层</a>
+                                <a class="dropdown-item" name="showLevel" tag="leafXmj" href="javascript:void(0);">只显示项目节</a>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="d-inline-block">
+                        <button class="btn btn-primary btn-sm mr-2" id="export-excel">导出Excel</button>
+                        <button class="btn btn-sm btn-primary" id="gather-select">汇总标段</button>
+                    </div>
+                </div>
+                <div class="">
+
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>

+ 0 - 0
app/view/spss/gather_select_modal.ejs


+ 46 - 0
app/view/spss/gather_stage.ejs

@@ -0,0 +1,46 @@
+<% include ../tender/list_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ../tender/list_sub_mini_menu.ejs %>
+            <div class="title-main  d-flex justify-content-between">
+                <div>
+                    <div class="d-inline-block">
+                        <div class="btn-group btn-group-toggle group-tab" data-toggle="buttons">
+                            <a href="/sp/<%- ctx.subProject.id %>/spss/gather/stage" class="btn btn-sm btn-light active">计量台账 </a>
+                            <a href="/sp/<%- ctx.subProject.id %>/spss/gather/se" class="btn btn-sm btn-light">其他台账 </a>
+                        </div>
+                    </div>
+                    <div class="d-inline-block mr-2">
+                        <!--展开/收起-->
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="zhankai" aria-expanded="false">显示层级</button>
+                            <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                                <a class="dropdown-item" name="showLevel" tag="1" href="javascript:void(0);">第一层</a>
+                                <a class="dropdown-item" name="showLevel" tag="2" href="javascript:void(0);">第二层</a>
+                                <a class="dropdown-item" name="showLevel" tag="3" href="javascript:void(0);">第三层</a>
+                                <a class="dropdown-item" name="showLevel" tag="4" href="javascript:void(0);">第四层</a>
+                                <a class="dropdown-item" name="showLevel" tag="5" href="javascript:void(0);">第五层</a>
+                                <a class="dropdown-item" name="showLevel" tag="last" href="javascript:void(0);">最底层</a>
+                                <a class="dropdown-item" name="showLevel" tag="leafXmj" href="javascript:void(0);">只显示项目节</a>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="d-inline-block">
+                        <button class="btn btn-primary btn-sm mr-2" id="export-excel">导出Excel</button>
+                        <button class="btn btn-sm btn-primary" id="gather-select">汇总标段</button>
+                    </div>
+                </div>
+                <div class="">
+
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>

+ 43 - 0
app/view/spss/gather_stage_extra.ejs

@@ -0,0 +1,43 @@
+<% include ../tender/list_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ../tender/list_sub_mini_menu.ejs %>
+            <div class="title-main  d-flex justify-content-between">
+                <div>
+                    <div class="d-inline-block">
+                        <div class="btn-group btn-group-toggle group-tab" data-toggle="buttons">
+                            <a href="/sp/<%- ctx.subProject.id %>/spss/gather/stage" class="btn btn-sm btn-light">计量台账 </a>
+                            <a href="/sp/<%- ctx.subProject.id %>/spss/gather/se" class="btn btn-sm btn-light active">其他台账 </a>
+                        </div>
+                    </div>
+                    <div class="d-inline-block mr-2">
+                        <!--展开/收起-->
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="zhankai" aria-expanded="false">显示层级</button>
+                            <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                                <a class="dropdown-item" name="showLevel" tag="1" href="javascript:void(0);">第一层</a>
+                                <a class="dropdown-item" name="showLevel" tag="2" href="javascript:void(0);">第二层</a>
+                                <a class="dropdown-item" name="showLevel" tag="3" href="javascript:void(0);">第三层</a>
+                                <a class="dropdown-item" name="showLevel" tag="4" href="javascript:void(0);">第四层</a>
+                                <a class="dropdown-item" name="showLevel" tag="5" href="javascript:void(0);">第五层</a>
+                                <a class="dropdown-item" name="showLevel" tag="last" href="javascript:void(0);">最底层</a>
+                                <a class="dropdown-item" name="showLevel" tag="leafXmj" href="javascript:void(0);">只显示项目节</a>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="d-inline-block">
+                        <button class="btn btn-primary btn-sm mr-2">导出Excel</button>
+                        <button class="btn btn-sm btn-primary" id="gather-select">汇总标段</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>

+ 33 - 0
app/view/spss/info.ejs

@@ -0,0 +1,33 @@
+<% include ../tender/list_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ../tender/list_sub_mini_menu.ejs %>
+            <div class="title-main  d-flex justify-content-between">
+                <div>
+                    <div class="d-inline-block mr-2">
+                        <!--展开/收起-->
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="zhankai" aria-expanded="false">显示层级</button>
+                            <div class="dropdown-menu" aria-labelledby="zhankai" style="will-change: transform;">
+                                <a class="dropdown-item" href="#">第一层</a>
+                                <a class="dropdown-item" href="#">第二层</a>
+                                <a class="dropdown-item" href="#">最底层</a>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="d-inline-block">
+                        <button class="btn btn-primary btn-sm mr-2" id="export-excel">导出Excel</button>
+                        <button class="btn btn-sm btn-primary" id="gather-select">汇总标段</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>

+ 13 - 0
app/view/tender/list_sub_menu_list.ejs

@@ -23,3 +23,16 @@
     </ul>
 </div>
 <% } %>
+<div class="nav-box">
+    <ul class="nav-list list-unstyled">
+        <li class=""><a href="#spssCollapse" class="" data-toggle="collapse" aria-expanded="true"><i class="fa fa-angle-down"></i><span class="ml-2">汇总对比</span></a></li>
+        <div class="collapse show" id="spssCollapse" style="">
+            <li class="<% if (ctx.url === '/sp/' + ctx.subProject.id + '/spss/info') { %>active<% } %>"><a href="/sp/<%- ctx.subProject.id %>/spss/info"><span class="ml-3">金额汇总</span></a></li>
+            <li class="<% if (ctx.url === '/sp/' + ctx.subProject.id + '/spss/gather/ledger') { %>active<% } %>"><a href="/sp/<%- ctx.subProject.id %>/spss/gather/ledger"><span class="ml-3">台账汇总</span></a></li>
+            <li class="<% if (ctx.url === '/sp/' + ctx.subProject.id + '/spss/gather/stage') { %>active<% } %>"><a href="/sp/<%- ctx.subProject.id %>/spss/gather/stage"><span class="ml-3">计量汇总</span></a></li>
+            <li class="<% if (ctx.url === '/sp/' + ctx.subProject.id + '/spss/compare/ledger') { %>active<% } %>"><a href="/sp/<%- ctx.subProject.id %>/spss/compare/ledger"><span class="ml-3">台账对比</span></a></li>
+            <li class="<% if (ctx.url === '/sp/' + ctx.subProject.id + '/spss/compare/stage') { %>active<% } %>"><a href="/sp/<%- ctx.subProject.id %>/spss/compare/stage"><span class="ml-3">计量对比</span></a></li>
+        </div>
+    </ul>
+</div>
+

+ 1 - 1
config/menu.js

@@ -97,7 +97,7 @@ const menu = {
         children: null,
         caption: '标段管理',
         controller: 'list',
-        controllers: ['list', 'tender', 'contract', 'construction'],
+        controllers: ['list', 'tender', 'contract', 'construction', 'spss'],
         includedUrl: { contract: ['/contract/tender'] },
     },
     contract: {

+ 142 - 0
config/web.js

@@ -1884,6 +1884,148 @@ const JsFiles = {
                 mergeFile: 'financial_summary',
             },
         },
+        spss: {
+            info: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFiles: [
+                    '/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/export_excel.js',
+                    '/public/js/shares/tender_select_multi.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/spss_info.js',
+                ],
+                mergeFile: 'spss_info',
+            },
+            gatherLedger: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                ],
+                mergeFiles: [
+                    '/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/export_excel.js',
+                    '/public/js/shares/tender_select_multi.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/spss_gather_ledger.js',
+                ],
+                mergeFile: 'spss_gather_ledger',
+            },
+            gatherStage: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFiles: [
+                    '/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/export_excel.js',
+                    '/public/js/shares/tender_select_multi.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/spss_gather_stage.js',
+                ],
+                mergeFile: 'spss_gather_stage',
+            },
+            gatherStageExtra: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFiles: [
+                    '/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/export_excel.js',
+                    '/public/js/shares/tender_select_multi.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/spss_gather_stage_extra.js',
+                ],
+                mergeFile: 'spss_gather_stage_extra',
+            },
+            compareLedger: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                ],
+                mergeFiles: [
+                    '/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/export_excel.js',
+                    '/public/js/shares/tender_select_multi.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/spss_compare_ledger.js',
+                ],
+                mergeFile: 'spss_compare_ledger',
+            },
+            compareStage: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFiles: [
+                    '/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/export_excel.js',
+                    '/public/js/shares/tender_select_multi.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/spss_compare_stage.js',
+                ],
+                mergeFile: 'spss_compare_stage',
+            },
+        }
     },
 };