Browse Source

Merge branch 'dev' of http://192.168.1.41:3000/maixinrong/Calculation into dev

Tony Kang 3 months ago
parent
commit
81ef4e5200

+ 363 - 138
app/controller/spss_controller.js

@@ -10,6 +10,7 @@
 
 const measureType = require('../const/tender').measureType;
 const status = require('../const/audit').stage.status;
+const moment = require('moment');
 
 module.exports = app => {
 
@@ -26,194 +27,418 @@ 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/spss_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/spss_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/spss_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/spss_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/spss_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/spss_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 }), filter: `第${order}期` };
+        }
+        async _filterMonth(tender, month) {
+            const stages = await this._getValidStages(tender.id);
+            return { type: 'stage', stage: this.ctx.helper._.find(stages, { s_time: month }), filter: month };
+        }
+        async _filterFinal(tender) {
+            const stages = await this._getValidStages(tender.id);
+            return { type: 'stage', stage: stages[0], filter: `第${stages[0].order}期` };
+        }
+        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], filter: `第${checkedStages[0].order}期` };
+        }
+        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);
+                }
+            }
+            return { type: 'stages', stages: validStages, preStage, endStage, filter: `第${iBegin}期 ~ 第${iEnd}期` };
+        }
+        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');
+
+            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;
                 }
-                ctx.body = responseData;
-            } catch (err) {
-                ctx.helper.log(err);
-                ctx.body = this.ajaxErrorBody(err, '查询数据错误');
             }
+            return { type: 'stages', stages: validStages, preStage, endStage, filter: zone };
         }
+        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) {
+            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 },
+            });
+            if (!stage) return [bills, pos];
 
-        /**
-         * 期计量 汇总 页面
-         *
-         * @param {Object} ctx - egg全局变量
-         * @return {void}
-         */
-        async gatherStage(ctx) {
-            try {
-                const renderData = {
-                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.gather.stage)
+            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 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 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_', helper = this.ctx.helper;
+            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 bills) {
+                billsIndexData[indexPre + bd.id] = bd;
+            }
+            let posIndexData = {};
+            for (const p of pos) {
+                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.filter = stageFilter.filter;
+                        }
+                        [result.bills, result.pos] = await this._loadStageLedgerData(tender, stageFilter);
+                        break;
+                    case 'jgcl':
+                        if (!stageFilter) {
+                            stageFilter = await this._filterStages(tender, stageInfo);
+                            result.filter = stageFilter.filter;
+                        }
+                        result.jgcl = await this._loadJgclData(tender, stageFilter);
+                        break;
+                    case 'yjcl':
+                        if (!stageFilter) {
+                            stageFilter = await this._filterStages(tender, stageInfo);
+                            result.filter = stageFilter.filter;
+                        }
+                        result.jgcl = await this._loadYjclData(tender, stageFilter);
+                        break;
+                    case 'bonus':
+                        if (!stageFilter) {
+                            stageFilter = await this._filterStages(tender, stageInfo);
+                            result.filter = stageFilter.filter;
+                        }
+                        result.jgcl = await this._loadBonusData(tender, stageFilter);
+                        break;
+                    case 'safeProd':
+                        if (!stageFilter) {
+                            stageFilter = await this._filterStages(tender, stageInfo);
+                            result.filter = stageFilter.filter;
+                        }
+                        result.jgcl = await this._loadSafeProdData(tender, stageFilter);
+                        break;
+                    case 'tempLand':
+                        if (!stageFilter) {
+                            stageFilter = await this._filterStages(tender, stageInfo);
+                            result.filter = stageFilter.filter;
+                        }
+                        result.jgcl = await this._loadTempLandData(tender, stageFilter);
+                        break;
+                    case 'other':
+                        if (!stageFilter) {
+                            stageFilter = await this._filterStages(tender, stageInfo);
+                            result.filter = stageFilter.filter;
+                        }
+                        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;
+        }
+
         /**
          * 检测台账 页面
          *

+ 67 - 57
app/public/js/path_tree.js

@@ -322,6 +322,12 @@ const createNewPathTree = function (type, setting) {
             //     });
             // }
         }
+        clearDatas() {
+            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.clearDatas();
             // 加载全部数据
             this.sortByLevel(datas);
             const setting = this.setting;
@@ -1698,12 +1701,6 @@ const createNewPathTree = function (type, setting) {
     }
 
     class FilterTree extends BaseTree {
-        clearDatas() {
-            this.items = {};
-            this.nodes = [];
-            this.datas = [];
-            this.children = [];
-        }
         addData(data, fields) {
             const item = {};
             for (const prop in data) {
@@ -1736,13 +1733,6 @@ const createNewPathTree = function (type, setting) {
     }
 
     class GatherTree extends BaseTree {
-        clearDatas() {
-            this.items = {};
-            this.nodes = [];
-            this.datas = [];
-            this.children = [];
-        }
-
         get newId() {
             if (!this._maxId) {
                 this._maxId = 0;
@@ -1822,28 +1812,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 +1854,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 +1885,7 @@ const createNewPathTree = function (type, setting) {
         }
 
         loadCompareData(data1, data2) {
+            this.clearDatas();
             this.loadCompareTree(data1, this.setting.loadInfo1);
             this.loadCompareTree(data2, this.setting.loadInfo2);
             for (const d of this.datas) {
@@ -1903,7 +1901,6 @@ const createNewPathTree = function (type, setting) {
     }
 
     class TreeGatherTree extends FxTree {
-
         constructor(setting) {
             super(setting);
             this._newId = 1;
@@ -1913,31 +1910,47 @@ const createNewPathTree = function (type, setting) {
             return this._newId++;
         }
 
-        loadGatherNode(node, parent, index, loadFun) {
-            const siblings = parent ? parent.children : this.children;
-            let cur = 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;
-            });
+        findGatherNode(node, parent, source) {
+            if (this.setting.findNode) {
+                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 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;
+                });
+            }
+        }
+
+        loadGatherNode(source, node, parent, index, loadFun) {
+            let cur = this.findGatherNode(node, parent, source);
             if (!cur) {
+                const siblings = parent ? parent.children : this.children;
                 const id = this.newId;
                 cur = {
-                    id: id,
-                    pid: parent ? parent.id : this.setting.rootId,
-                    full_path: parent ? parent.full_path + '-' + id : '' + id,
-                    level: parent ? parent.level + 1 : 1,
-                    order: siblings.length + 1,
-                    children: [],
-                    code: node.code, b_code: node.b_code, name: node.name,
-                    unit: node.unit, unit_price: node.unit_price,
+                    children: [], pos: [],
+                    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;
+                cur[this.setting.fullPath] = parent ? parent[this.setting.fullPath] + '-' + id : '' + id;
+                cur[this.setting.level] = parent ? parent[this.setting.level] + 1 : 1;
+                cur[this.setting.order] = siblings.length + 1;
                 siblings.push(cur);
                 this.datas.push(cur);
             }
-            loadFun(cur, node, index);
+            loadFun(cur, node, index, source);
             for (const c of node.children) {
-                this.loadGatherNode(c, cur, index, loadFun);
+                this.loadGatherNode(source, c, cur, index, loadFun);
+            }
+        }
+
+        loadGatherTree(data, index, loadFun) {
+            for (const c of data.billsTree.children) {
+                this.loadGatherNode(data, c, null, index, loadFun);
             }
         }
 
@@ -1954,14 +1967,6 @@ const createNewPathTree = function (type, setting) {
                 addSortNode(n);
             }
         }
-
-        loadGatherTree(data, index, loadFun) {
-            for (const c of data.billsTree.children) {
-                this.loadGatherNode(c, null, index, loadFun);
-            }
-            // todo load Pos Data;
-        }
-
         calculateSum() {
             if (this.setting.calcSum) {
                 for (const d of this.datas) {
@@ -1969,7 +1974,6 @@ const createNewPathTree = function (type, setting) {
                 }
             }
         }
-
         loadGatherData(datas) {
             this.count = datas.length;
             for (const [i, data] of datas.entries()) {
@@ -2302,13 +2306,19 @@ const treeCalc = {
     },
     calculateNode: function (tree, node) {
         if (node.children && node.children.length > 0) {
-            const gather = node.children.reduce(function (rst, x) {
-                const result = {};
+            const gather = {};
+            node.children.forEach(c => {
                 for (const cf of tree.setting.calcFields) {
-                    result[cf] = ZhCalc.add(rst[cf], x[cf]);
+                    gather[cf] = ZhCalc.add(gather[cf], c[cf]);
                 }
-                return result;
             });
+            // const gather = node.children.reduce(function (rst, x) {
+            //     const result = {};
+            //     for (const cf of tree.setting.calcFields) {
+            //         result[cf] = ZhCalc.add(rst[cf], x[cf]);
+            //     }
+            //     return result;
+            // });
             // 汇总子项
             for (const cf of tree.setting.calcFields) {
                 if (gather[cf]) {

+ 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.dataType === '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 }
+};

+ 4 - 3
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -309,10 +309,11 @@ const SpreadJsObj = {
      * 根据sheet.zh_setting初始化sheet表头
      * @param {GC.Spread.Sheets.Worksheet} sheet
      */
-    _initSheetHeader: function (sheet) {
+    _initSheetHeader: function (sheet, force) {
         const setting = sheet.zh_setting;
         if (!setting || !setting.cols) { return; }
 
+        if (force) sheet.setColumnCount(0);
         sheet.setColumnCount(setting.cols.length);
         sheet.setRowCount(setting.headRows, spreadNS.SheetArea.colHeader);
         for (let iRow = 0; iRow < setting.headRowHeight.length; iRow ++) {
@@ -417,11 +418,11 @@ const SpreadJsObj = {
         this.endMassOperation(sheet);
         if (!setting.invalidCopyFilterHiddenRow) this.copyFilterHiddenRow(sheet);
     },
-    reLoadSheetHeader: function (sheet) {
+    reLoadSheetHeader: function (sheet, force = false) {
         if (sheet.zh_setting) {
             this.beginMassOperation(sheet);
             this._loadCacheSetting(sheet, sheet.zh_setting);
-            this._initSheetHeader(sheet);
+            this._initSheetHeader(sheet, force);
             this.endMassOperation(sheet);
         }
     },

+ 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();
+        }
+    });
+});

+ 299 - 0
app/public/js/spss_gather_ledger.js

@@ -0,0 +1,299 @@
+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: '合计|数量', colSpan: '2|1', rowSpan: '1|1', field: 'sum_quantity', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'sum_total_price', hAlign: 2, width: 80, type: 'Number'},
+        ],
+        baseCols: [
+            {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'},
+        ],
+        extraCols: [
+            {title: '标段1|数量', colSpan: '2|1', rowSpan: '1|1', field: 'quantity_1', hAlign: 2, width: 80, type: 'Number', formatTitle: '%s|数量', formatField: 'quantity_%d'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price_1', hAlign: 2, width: 80, type: 'Number', formatField: 'total_price_%d'},
+        ],
+        endCols: [
+            {title: '合计|数量', colSpan: '2|1', rowSpan: '1|1', field: 'sum_quantity', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'sum_total_price', hAlign: 2, width: 80, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [32, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+    const posSpreadSetting = {
+        cols: [
+            {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+            {title: '差值', colSpan: '1', rowSpan: '1', field: 'su,_quantity', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        baseCols: [
+            {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+        ],
+        extraCols: [
+            {title: '标段1', colSpan: '1', rowSpan: '1', field: 'quantity_1', hAlign: 2, width: 100, type: 'Number', formatTitle: '%s', formatField: 'quantity_%d'},
+        ],
+        endCols: [
+            {title: '合计', colSpan: '1', rowSpan: '1', field: 'sum_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: ['sum_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;
+        //     });
+        // },
+        loadInfo: function (node, sourceNode, index, source) {
+            for (const f of billsCompareField) {
+                node[f + '_' + index] = 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 + '_' + index] = p[f];
+                    }
+                }
+            }
+        },
+        calcSum: function(data, count) {
+            for (const f of billsCompareField) {
+                data['sum_' + f] = 0;
+                for (let i = 1; i <= count; i++) {
+                    data['sum_' + f] = ZhCalc.add(data['sum_' + f], data[f + '_' + i]);
+                }
+            }
+            for (const p of data.pos) {
+                for (const f of posCompareField) {
+                    p['sum_' + f] = 0;
+                    for (let i = 1; i <= count; i++) {
+                        p['sum_' + f] = ZhCalc.add(p['sum_' + f], p[f + '_' + i]);
+                    }
+                }
+            }
+        },
+    };
+    const billsTree = createNewPathTree('tree-gather', treeSetting);
+
+    const billsTreeSpreadObj = {
+        selectionChanged: function (e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    posSpreadObj.loadCurPosData();
+                }
+            }
+        },
+        rebuildSpreadSetting(tenders) {
+            billsSpreadSetting.cols = [...billsSpreadSetting.baseCols];
+            posSpreadSetting.cols = [...posSpreadSetting.baseCols];
+            for (const [i, tender] of tenders.entries()) {
+                for (const col of billsSpreadSetting.extraCols) {
+                    const newCol = JSON.parse(JSON.stringify(col));
+                    if (newCol.formatTitle) newCol.title = newCol.formatTitle.replace('%s', tender.name);
+                    if (newCol.formatField) newCol.field = newCol.formatField.replace('%d', i + 1);
+                    billsSpreadSetting.cols.push(newCol);
+                }
+                for (const col of posSpreadSetting.extraCols) {
+                    const newCol = JSON.parse(JSON.stringify(col));
+                    if (newCol.formatTitle) newCol.title = newCol.formatTitle.replace('%s', tender.name);
+                    if (newCol.formatField) newCol.field = newCol.formatField.replace('%d', i + 1);
+                    posSpreadSetting.cols.push(newCol)
+                }
+            }
+            billsSpreadSetting.cols.push(...billsSpreadSetting.endCols);
+            posSpreadSetting.cols.push(...posSpreadSetting.endCols);
+        }
+    };
+    billsSpread.bind(spreadNS.Events.SelectionChanged, billsTreeSpreadObj.selectionChanged);
+    const posSpreadObj = {
+        loadCurPosData: function () {
+            const node = SpreadJsObj.getSelectObject(billsSheet);
+            console.log(node);
+            if (node) {
+                SpreadJsObj.loadSheetData(posSheet, 'data', node.pos || []);
+            } else {
+                SpreadJsObj.loadSheetData(posSheet, 'data', []);
+            }
+        },
+    };
+
+    const tenderSelect = TenderSelectMulti({
+        title: '汇总标段',
+        type: 'gather',
+        dataType: 'ledger',
+        afterSelect: function(select) {
+            const data = { filter: 'ledger', tender: select };
+            postData(`/sp/${spid}/spss/load`, data, function(result) {
+                billsTreeSpreadObj.rebuildSpreadSetting(result);
+                SpreadJsObj.reLoadSheetHeader(billsSheet);
+                SpreadJsObj.reLoadSheetHeader(posSheet);
+
+                treeSetting.calcFields = ['sum_total_price'];
+                for (let i = 1; i <= result.length; i++) {
+                    treeSetting.calcFields.push('total_price_' + i);
+                }
+
+                const tenderTreeSetting = {
+                    id: 'ledger_id', pid: 'ledger_pid', order: 'order', level: 'level',
+                    rootId: -1,
+                    keys: ['id', 'tender_id', 'ledger_id'],
+                    calcFields: ['total_price'],
+                };
+                const posSetting = { id: 'id', ledgerId: 'lid' };
+                const tenders = [];
+                for (const t of result) {
+                    const tender = {
+                        billsTree: createNewPathTree('ledger', tenderTreeSetting),
+                        pos: new PosData(posSetting),
+                    };
+                    tender.billsTree.loadDatas(t.bills);
+                    tender.pos.loadDatas(t.pos);
+                    tenders.push(tender);
+                }
+
+                billsTree.clearDatas();
+                billsTree.loadGatherData(tenders);
+                treeCalc.calculateAll(billsTree);
+                SpreadJsObj.loadSheetData(billsSheet, SpreadJsObj.DataType.Tree, billsTree, true);
+                posSpreadObj.loadCurPosData();
+            });
+        },
+    });
+    $('#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();
+        }
+    });
+});

+ 374 - 0
app/public/js/spss_gather_stage.js

@@ -0,0 +1,374 @@
+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: '3', field: 'code', hAlign: 0, width: 185, formatter: '@', cellType: 'tree'},
+            {title: '清单编号', colSpan: '1', rowSpan: '3', field: 'b_code', hAlign: 0, width: 100, formatter: '@'},
+            {title: '名称', colSpan: '1', rowSpan: '3', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+            {title: '单位', colSpan: '1', rowSpan: '3', field: 'unit', hAlign: 1, width: 60, formatter: '@'},
+            {title: '单价', colSpan: '1', rowSpan: '3', field: 'unit_price', hAlign: 2, width: 60, type: 'Number'},
+            {title: '合计||数量', colSpan: '2||1', rowSpan: '2||1', field: 'sum_qty', hAlign: 2, width: 80, type: 'Number'},
+            {title: '||金额', colSpan: '|1', rowSpan: '|1', field: 'sum_tp', hAlign: 2, width: 80, type: 'Number'},
+        ],
+        baseCols: [
+            {title: '项目节编号', colSpan: '1', rowSpan: '3', field: 'code', hAlign: 0, width: 185, formatter: '@', cellType: 'tree'},
+            {title: '清单编号', colSpan: '1', rowSpan: '3', field: 'b_code', hAlign: 0, width: 100, formatter: '@'},
+            {title: '名称', colSpan: '1', rowSpan: '3', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+            {title: '单位', colSpan: '1', rowSpan: '3', field: 'unit', hAlign: 1, width: 60, formatter: '@'},
+            {title: '单价', colSpan: '1', rowSpan: '3', field: 'unit_price', hAlign: 2, width: 60, type: 'Number'},
+        ],
+        extraCols: [
+            {title: '标段1|第1期|数量', colSpan: '2|2|1', rowSpan: '1|1|1', field: 'quantity_1', hAlign: 2, width: 80, type: 'Number', formatTitle: '%s|%f|数量', formatField: 'gather_qty_%d'},
+            {title: '||金额', colSpan: '|1', rowSpan: '|1', field: 'total_price_1', hAlign: 2, width: 80, type: 'Number', formatField: 'gather_tp_%d'},
+        ],
+        endCols: [
+            {title: '合计||数量', colSpan: '2||1', rowSpan: '2||1', field: 'sum_qty', hAlign: 2, width: 80, type: 'Number'},
+            {title: '||金额', colSpan: '|1', rowSpan: '|1', field: 'sum_tp', hAlign: 2, width: 80, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 3,
+        headRowHeight: [32, 25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+    const posSpreadSetting = {
+        cols: [
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+            {title: '差值', colSpan: '1', rowSpan: '2', field: 'sum_qty', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        baseCols: [
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 235, formatter: '@'},
+        ],
+        extraCols: [
+            {title: '标段1|第1期', colSpan: '1|1', rowSpan: '1|1', field: 'quantity_1', hAlign: 2, width: 100, type: 'Number', formatTitle: '%s|%f', formatField: 'gather_qty_%d'},
+        ],
+        endCols: [
+            {title: '合计', colSpan: '1', rowSpan: '2', field: 'sum_qty', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [32, 25],
+        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: ['sum_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;
+        //     });
+        // },
+        loadInfo: function (node, sourceNode, index, source) {
+            for (const f of billsCompareField) {
+                node[f + '_' + index] = 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 + '_' + index] = p[f];
+                    }
+                }
+            }
+        },
+        calcSum: function(data, count) {
+            for (const f of billsCompareField) {
+                data['sum_' + f] = 0;
+                for (let i = 1; i <= count; i++) {
+                    data['sum_' + f] = ZhCalc.add(data['sum_' + f], data[f + '_' + i]);
+                }
+            }
+            for (const p of data.pos) {
+                for (const f of posCompareField) {
+                    p['sum_' + f] = 0;
+                    for (let i = 1; i <= count; i++) {
+                        p['sum_' + f] = ZhCalc.add(p['sum_' + f], p[f + '_' + i]);
+                    }
+                }
+            }
+        },
+    };
+    const billsTree = createNewPathTree('tree-gather', treeSetting);
+
+    const billsTreeSpreadObj = {
+        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) {
+                for (let i = 1; i < billsTree.count; i++) {
+                    node['gather_qty_' + i] = node[field + '_qty_' + i];
+                    node['gather_tp_' + i] = node[field + '_tp_' + i];
+                }
+                node.sum_qty = node['sum_' + field + '_qty'];
+                node.sum_tp = node['sum_' + field + '_tp'];
+                for (const p of node.pos) {
+                    for (let i = 1; i < billsTree.count; i++) {
+                        p['gather_qty_' + i] = p[field + '_qty_' + i];
+                    }
+                    p.sum_qty = p['sum_' + field + '_qty'];
+                }
+            }
+            this.compareData = field;
+        },
+        rebuildSpreadSetting(tenders) {
+            if(tenders.length > 0) {
+                if (tenders[0].filter.indexOf('~') > 0) {
+                    $('[datatype=end]').hide();
+                } else {
+                    $('[datatype=end]').show();
+                }
+            }
+            billsSpreadSetting.cols = [...billsSpreadSetting.baseCols];
+            posSpreadSetting.cols = [...posSpreadSetting.baseCols];
+            for (const [i, tender] of tenders.entries()) {
+                for (const col of billsSpreadSetting.extraCols) {
+                    const newCol = JSON.parse(JSON.stringify(col));
+                    if (newCol.formatTitle) newCol.title = newCol.formatTitle.replace('%s', tender.name).replace('%f', tender.filter);
+                    if (newCol.formatField) newCol.field = newCol.formatField.replace('%d', i + 1);
+                    billsSpreadSetting.cols.push(newCol);
+                }
+                for (const col of posSpreadSetting.extraCols) {
+                    const newCol = JSON.parse(JSON.stringify(col));
+                    if (newCol.formatTitle) newCol.title = newCol.formatTitle.replace('%s', tender.name).replace('%f', tender.filter);
+                    if (newCol.formatField) newCol.field = newCol.formatField.replace('%d', i + 1);
+                    posSpreadSetting.cols.push(newCol)
+                }
+            }
+            billsSpreadSetting.cols.push(...billsSpreadSetting.endCols);
+            posSpreadSetting.cols.push(...posSpreadSetting.endCols);
+        },
+        refreshShowData: function() {
+            const field = $('[name=compare-data]:checked').val();
+            if (field === this.compareData) return;
+
+            this.loadShowData();
+            SpreadJsObj.reloadColData(billsSheet, 5, billsSpreadSetting.cols.length - 5);
+            SpreadJsObj.reloadColData(posSheet, 1, posSpreadSetting.cols.length - 1);
+        }
+    };
+    billsSpread.bind(spreadNS.Events.SelectionChanged, billsTreeSpreadObj.selectionChanged);
+    const posSpreadObj = {
+        loadCurPosData: function () {
+            const node = SpreadJsObj.getSelectObject(billsSheet);
+            if (node) {
+                console.log(node);
+                SpreadJsObj.loadSheetData(posSheet, 'data', node.pos || []);
+            } else {
+                SpreadJsObj.loadSheetData(posSheet, 'data', []);
+            }
+        },
+    };
+
+    const tenderSelect = TenderSelectMulti({
+        title: '汇总标段',
+        type: 'gather',
+        dataType: 'stage',
+        afterSelect: function(select) {
+            const data = { filter: 'stage', tender: select };
+            postData(`/sp/${spid}/spss/load`, data, function(result) {
+                billsTreeSpreadObj.rebuildSpreadSetting(result);
+                SpreadJsObj.reLoadSheetHeader(billsSheet, true);
+                SpreadJsObj.reLoadSheetHeader(posSheet, true);
+
+                treeSetting.calcFields = ['sum_total_price'];
+                for (let i = 1; i <= result.length; i++) {
+                    treeSetting.calcFields.push('total_price_' + i, 'contract_tp_' + i, 'qc_tp_' + i, 'gather_tp_' + i, 'end_contract_tp_' + i, 'end_qc_tp_' + i, 'end_gather_tp_' + i);
+                }
+
+                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 posSetting = {
+                    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 t of result) {
+                    const tender = {
+                        billsTree: createNewPathTree('ledger', tenderTreeSetting),
+                        pos: new PosData(posSetting),
+                    };
+                    tender.billsTree.loadDatas(t.bills);
+                    treeCalc.calculateAll(tender.billsTree);
+                    tender.pos.loadDatas(t.pos);
+                    tender.pos.calculateAll();
+                    tenders.push(tender);
+                }
+
+                billsTree.clearDatas();
+                billsTree.loadGatherData(tenders);
+                treeCalc.calculateAll(billsTree);
+                billsTreeSpreadObj.loadShowData();
+                SpreadJsObj.loadSheetData(billsSheet, SpreadJsObj.DataType.Tree, billsTree);
+                SpreadJsObj.locateRow(billsSheet, 0);
+                posSpreadObj.loadCurPosData();
+                SpreadJsObj.locateRow(posSheet, 0);
+            });
+        },
+    });
+    $('#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();
+        }
+    });
+});

+ 0 - 0
app/public/js/spss_gather_stage_extra.js


+ 0 - 0
app/public/js/spss_gather_stage_info.js


+ 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');
 

+ 1 - 1
app/service/change_audit.js

@@ -1336,7 +1336,7 @@ module.exports = app => {
                     //     }
                     // });
                 } else if (data.operate === 'change') {
-                    const nowAudit = await this.getDataByCondition({ cid, times, uid: now_audit.uid, usite: now_audit.usite });
+                    const nowAudit = await this.getDataByCondition({ cid, times, uid: now_audit.uid, usite: now_audit.usite, usort: now_audit.usort });
                     if (now_audit.status !== auditConst.status.uncheck || !nowAudit) {
                         throw '当前人无法操作替换';
                     }

+ 1 - 4
app/service/material_list.js

@@ -632,10 +632,7 @@ module.exports = app => {
                 // if (delList.length > 0) await transaction.delete(this.tableName, { id: delList });
                 // 新增工料清单关联
                 if (list.length > 0) {
-                    const result = await transaction.insert(this.tableName, list);
-                    if (result.affectedRows === 0) {
-                        throw '新增工料数据失败';
-                    }
+                    await this.insertBigDatas(transaction, list);
                 }
                 if (listGcl.length > 0) {
                     const result2 = await transaction.insert(this.ctx.service.materialListGcl.tableName, listGcl);

+ 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>

+ 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>

+ 0 - 38
app/view/spss/compare_stage_modal.ejs

@@ -1,38 +0,0 @@
-<!--弹出对比标段-->
-<div class="modal fade" id="data-select" data-backdrop="static">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title">选择对比标段</h5>
-            </div>
-            <div class="modal-body">
-                <div class="form-group">
-                    <label>对比标段1<b class="text-danger">*</b></label>
-                    <div class="row ml-1 mr-1">
-                        <div class="col-6">
-                            <input class="form-control form-control-sm"  placeholder="输入标段1的ID" type="text" name="id1">
-                        </div>
-                        <div class="col-6">
-                            <input class="form-control form-control-sm"  placeholder="输入标段1的对比期序号" type="text" name="sorder1">
-                        </div>
-                    </div>
-                </div>
-                <div class="form-group">
-                    <label>对比标段2<b class="text-danger">*</b></label>
-                    <div class="row ml-1 mr-1">
-                        <div class="col-6">
-                            <input class="form-control form-control-sm"  placeholder="输入标段2的ID" type="text" name="id2">
-                        </div>
-                        <div class="col-6">
-                            <input class="form-control form-control-sm"  placeholder="输入标段2的对比期序号" type="text" name="sorder2">
-                        </div>
-                    </div>
-                </div>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
-                <button type="button" class="btn btn-primary btn-sm" id="select-ok">确定</button>
-            </div>
-        </div>
-    </div>
-</div>

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

@@ -1,55 +0,0 @@
-<% include ./sub_menu.ejs %>
-<div class="panel-content">
-    <div class="panel-title">
-        <div class="title-main d-flex">
-            <% include ./sub_mini_menu.ejs %>
-            <div>
-                <div class="d-inline-block">
-                    <div class="dropdown">
-                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                            <i class="fa fa-list-ol"></i> 显示层级
-                        </button>
-                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                            <a class="dropdown-item" name="showLevel" tag="1" href="javascirpt: void(0);">第一层</a>
-                            <a class="dropdown-item" name="showLevel" tag="2" href="javascirpt: void(0);">第二层</a>
-                            <a class="dropdown-item" name="showLevel" tag="3" href="javascirpt: void(0);">第三层</a>
-                            <a class="dropdown-item" name="showLevel" tag="4" href="javascirpt: void(0);">第四层</a>
-                            <a class="dropdown-item" name="showLevel" tag="5" href="javascirpt: void(0);">第五层</a>
-                            <a class="dropdown-item" name="showLevel" tag="last" href="javascirpt: void(0);">最底层</a>
-                            <a class="dropdown-item" name="showLevel" tag="leafXmj" href="javascirpt: void(0);">只显示项目节</a>
-                        </div>
-                    </div>
-                </div>
-                <div class="d-inline-block">
-                    <button href="#data-select" class="btn btn-sm btn-light text-primary" data-toggle="modal" data-target="#data-select"><i class="fa fa-clone"></i>
-                        选择<%- ctx.url.indexOf('compare') > 0 ? '对比' : '汇总' %>标段
-                    </button>
-                </div>
-            </div>
-            <div class="ml-auto"></div>
-        </div>
-    </div>
-    <div class="content-wrap">
-        <div class="c-header p-0"></div>
-        <div class="c-body">
-            <div class="sjs-height-1" id="bills-spread">
-            </div>
-            <div class="bcontent-wrap" id="main-bottom">
-                <div id="main-resize" class="resize-y"  r-Type="height" div1="#bills-spread" div2="#main-bottom" store-id="compare-main" store-version="1.0.0" min="100"></div>
-                <div class="bc-bar mb-1">
-                    <ul class="nav nav-tabs">
-                        <li class="nav-item">
-                            <a class="nav-link active" data-toggle="tab" href="#xmujie" role="tab">计量单元</a>
-                        </li>
-                    </ul>
-                </div>
-                <div class="tab-content">
-                    <div class="tab-pane active" id="xmujie">
-                        <div class="sp-wrap" id="pos-spread">
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>

+ 0 - 24
app/view/spss/compare_tz_modal.ejs

@@ -1,24 +0,0 @@
-<!--弹出对比标段-->
-<div class="modal fade" id="data-select" data-backdrop="static">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title">选择对比标段</h5>
-            </div>
-            <div class="modal-body">
-                <div class="form-group">
-                    <label>对比标段1<b class="text-danger">*</b></label>
-                    <input class="form-control form-control-sm"  placeholder="输入标段1的ID" type="text" name="id1">
-                </div>               
-                <div class="form-group">
-                    <label>对比标段2<b class="text-danger">*</b></label>
-                    <input class="form-control form-control-sm"  placeholder="输入标段2的ID" type="text" name="id2">
-                </div>               
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
-                <button type="button" class="btn btn-primary btn-sm" id="select-ok">确定</button>
-            </div>
-        </div>
-    </div>
-</div>

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

@@ -0,0 +1,58 @@
+<% 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="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>

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

@@ -0,0 +1,93 @@
+<% 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">
+                            <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 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" datatype="end">
+                                <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" datatype="end">
+                                <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" datatype="end">
+                                <label class="form-check-label" for="radio_end_gather">截止完成计量</label>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="">
+
+                </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>

+ 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>

+ 0 - 35
app/view/spss/gather_stage_modal.ejs

@@ -1,35 +0,0 @@
-<!--弹出汇总标段-->
-<div class="modal fade" id="data-select" data-backdrop="static">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title">选择汇总标段</h5>
-            </div>
-            <div class="modal-body">
-                <div class="form-group">
-                    <div class="row">
-                        <div class="col-10 d-inline-flex">
-                            <input class="form-control form-control-sm"  placeholder="输入需要汇总的标段ID" type="text" name="tid">
-                            <input class="form-control form-control-sm ml-1"  placeholder="输入需要汇总的标段的期序号" type="text" name="sorder">
-                            <a href="javascript: void(0);" id="add-t" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus text-primary" aria-hidden="true"></i></a>
-                        </div>
-                    </div>
-                </div>
-                <div class="form-group">
-                    <label>汇总标段列表<b class="text-danger">*</b></label>
-                    <table class="table table-bordered table-sm mt-1">
-                        <thead>
-                        <th style="width: 40%">标段id</th><th style="width: 40%">第几期</th><th></th>
-                        </thead>
-                        <tbody>
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
-                <button type="button" class="btn btn-primary btn-sm" id="select-ok">确定</button>
-            </div>
-        </div>
-    </div>
-</div>

+ 0 - 34
app/view/spss/gather_tz_modal.ejs

@@ -1,34 +0,0 @@
-<!--弹出汇总标段-->
-<div class="modal fade" id="data-select" data-backdrop="static">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title">选择汇总标段</h5>
-            </div>
-            <div class="modal-body">
-                <div class="form-group">
-                    <div class="row">
-                        <div class="col-6 d-inline-flex">
-                            <input class="form-control form-control-sm"  placeholder="输入需要汇总的标段ID" type="text" name="tid">
-                            <a href="javascript: void(0);" id="add-t" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus text-primary" aria-hidden="true"></i></a>
-                        </div>
-                    </div>
-                </div>
-                <div class="form-group">
-                    <label>汇总标段列表<b class="text-danger">*</b></label>
-                    <table class="table table-bordered table-sm mt-1">
-                        <thead>
-                        <th style="width: 80%">标段id</th><th></th>
-                        </thead>
-                        <tbody>
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
-                <button type="button" class="btn btn-primary btn-sm" id="select-ok">确定</button>
-            </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>

+ 114 - 0
app/view/spss/spss_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>

+ 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: {

+ 146 - 0
config/web.js

@@ -1884,6 +1884,152 @@ 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',
+                    '/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_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',
+                    '/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_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',
+            },
+        }
     },
 };