Bläddra i källkod

合同管理功能no.1 up

ellisran 1 år sedan
förälder
incheckning
e642bdaf55

+ 1 - 0
app/base/base_controller.js

@@ -47,6 +47,7 @@ class BaseController extends Controller {
         menuList.management.display = ctx.session && ctx.session.sessionProject ? ctx.session.sessionProject.page_show.openManagement : false;
         menuList.file.display = ctx.session && ctx.session.sessionProject ? ctx.session.sessionProject.page_show.openFile : false;
         menuList.construction.display = ctx.session && ctx.session.sessionProject ? ctx.session.sessionProject.page_show.openConstruction : false;
+        menuList.contract.display = ctx.session && ctx.session.sessionProject ? ctx.session.sessionProject.page_show.openContract : false;
         // 菜单列表
         ctx.menuList = menuList;
         ctx.showProject = false;

+ 30 - 0
app/const/contract.js

@@ -0,0 +1,30 @@
+'use strict';
+
+/**
+ * 合同管理
+ *
+ * @author ELlisran
+ * @date 2019/10/20
+ * @version
+ */
+// 类型
+const type = {
+    expenses: 1,
+    income: 2,
+};
+
+const typeMap = {
+    1: 'expenses',
+    2: 'income',
+};
+
+const typeName = {
+    1: '支付',
+    2: '回款',
+};
+
+module.exports = {
+    type,
+    typeMap,
+    typeName,
+};

+ 1 - 0
app/const/page_show.js

@@ -60,6 +60,7 @@ const defaultSetting = {
     openPayment: 1,
     openConstruction: 1,
     openMaterialStageRepeat: 0,
+    openContract: 1,
 };
 
 

+ 528 - 0
app/controller/contract_controller.js

@@ -0,0 +1,528 @@
+'use strict';
+const auditConst = require('../const/audit');
+const contractConst = require('../const/contract');
+const moment = require('moment');
+const sendToWormhole = require('stream-wormhole');
+const fs = require('fs');
+const path = require('path');
+module.exports = app => {
+
+    class ContractController extends app.BaseController {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            ctx.showProject = true;
+            // ctx.showTitle = true;
+        }
+        /**
+         * 项目合同列表页
+         *
+         * @param {Object} ctx - egg全局页面
+         * @return {void}
+         */
+        async index(ctx) {
+            try {
+                if (!ctx.session.sessionProject.page_show.openContract) {
+                    throw '该功能已关闭或无法查看';
+                }
+                const renderData = {
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.contract.index),
+                    auditConst,
+                };
+                // renderData.budgetStd = await ctx.service.budgetStd.getDataByProjectId(ctx.session.sessionProject.id);
+                renderData.projectList = await ctx.service.subProject.getSubProjectByContract(ctx.session.sessionProject.id, ctx.session.sessionUser.accountId, ctx.session.sessionUser.is_admin);
+                for (const t of renderData.projectList) {
+                    if (!t.is_folder) {
+                        const expensessList = await ctx.service.contract.getListByUsers({ spid: t.id, contract_type: contractConst.type.expenses }, ctx.session.sessionUser);
+                        t.expenses_count = expensessList.length;
+                        t.expenses_total_price = expensessList.reduce((total, item) => ctx.helper.add(total, item.total_price), 0);
+                        t.expenses_yf_price = expensessList.reduce((total, item) => ctx.helper.add(total, item.yf_price), 0);
+                        const incomeList = await ctx.service.contract.getListByUsers({ spid: t.id, contract_type: contractConst.type.income }, ctx.session.sessionUser);
+                        t.income_count = incomeList.length;
+                        t.income_total_price = incomeList.reduce((total, item) => ctx.helper.add(total, item.total_price), 0);
+                        t.income_yf_price = incomeList.reduce((total, item) => ctx.helper.add(total, item.yf_price), 0);
+                    }
+                }
+                renderData.tenderList = await ctx.service.tender.getList4Select('stage');
+                const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                    where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                    columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                });
+                renderData.accountList = accountList;
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({where: {pid: ctx.session.sessionProject.id}});
+                renderData.accountGroup = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                });
+                // renderData.permissionConst = ctx.service.subProjPermission.PermissionConst;
+                renderData.categoryData = await this.ctx.service.category.getAllCategory(this.ctx.session.sessionProject.id);
+                renderData.companys = await this.ctx.service.constructionUnit.getAllDataByCondition({where: {pid: ctx.session.sessionProject.id}});
+                // renderData.templates = await this.ctx.service.filingTemplateList.getAllTemplate(ctx.session.sessionProject.id);
+                await this.layout('contract/index.ejs', renderData, 'contract/modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(this.menu.menu.dashboard.url);
+            }
+        }
+
+        /**
+         * 标段合同列表页
+         *
+         * @param {Object} ctx - egg全局页面
+         * @return {void}
+         */
+        async tender(ctx) {
+            try {
+                if (!ctx.session.sessionProject.page_show.openContract) {
+                    throw '该功能已关闭或无法查看';
+                }
+                // 获取用户新建标段权利
+                const accountInfo = await this.ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const userPermission = accountInfo !== undefined && accountInfo.permission !== ''
+                    ? JSON.parse(accountInfo.permission) : null;
+                const tenderList = await ctx.service.tender.getContractList('', userPermission, ctx.session.sessionUser.is_admin);
+                for (const t of tenderList) {
+                    const expensessList = await ctx.service.contract.getListByUsers({ tid: t.id, contract_type: contractConst.type.expenses }, ctx.session.sessionUser);
+                    t.expenses_count = expensessList.length;
+                    t.expenses_total_price = expensessList.reduce((total, item) => ctx.helper.add(total, item.total_price), 0);
+                    t.expenses_yf_price = expensessList.reduce((total, item) => ctx.helper.add(total, item.yf_price), 0);
+                    const incomeList = await ctx.service.contract.getListByUsers({ tid: t.id, contract_type: contractConst.type.income }, ctx.session.sessionUser);
+                    t.income_count = incomeList.length;
+                    t.income_total_price = incomeList.reduce((total, item) => ctx.helper.add(total, item.total_price), 0);
+                    t.income_yf_price = incomeList.reduce((total, item) => ctx.helper.add(total, item.yf_price), 0);
+                }
+                const categoryData = await ctx.service.category.getAllCategory(ctx.session.sessionProject.id);
+                const renderData = {
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.contract.tender),
+                    tenderList,
+                    categoryData,
+                    // selfCategoryLevel: accountInfo ? accountInfo.self_category_level : '',
+                    selfCategoryLevel: '',
+                    pid: ctx.session.sessionProject.id,
+                    uid: ctx.session.sessionUser.accountId,
+                };
+                if (ctx.session.sessionUser.is_admin) {
+                    const projectId = ctx.session.sessionProject.id;
+                    // 获取所有项目参与者
+                    const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                        where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                        columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                    });
+                    const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: projectId } });
+                    const accountGroupList = unitList.map(item => {
+                        const groupList = accountList.filter(item1 => item1.company === item.name);
+                        return { groupName: item.name, groupList };
+                    });
+                    renderData.accountList = accountList;
+                    renderData.accountGroup = accountGroupList;
+                }
+                await this.layout('contract/tender.ejs', renderData, 'contract/modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(this.menu.menu.dashboard.url);
+            }
+        }
+
+        async auditSave(ctx) {
+            try {
+                if (ctx.session.sessionUser.is_admin === 0) throw '没有设置权限';
+                const responseData = {
+                    err: 0, msg: '', data: null,
+                };
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.type) {
+                    throw '提交数据错误';
+                }
+                const info = ctx.contract;
+                const options = ctx.contractOptions;
+                let uids;
+                let result = false;
+                let auditList = [];
+                switch (data.type) {
+                    case 'check':
+                        await ctx.service.contractTree.insertTree(options, info);
+                        // 新建合同信息
+                        break;
+                    case 'add-audit':
+                        // 判断用户是单个还是数组
+                        uids = data.id instanceof Array ? data.id : [data.id];
+                        // 判断该用户的组是否已加入到表中,已加入则提示无需添加
+                        auditList = await ctx.service.contractAudit.getAllDataByCondition({ where: options });
+                        const addAidList = ctx.helper._.difference(uids, ctx.helper._.map(auditList, 'uid'));
+                        if (addAidList.length === 0) {
+                            throw '用户已存在成员管理中,无需重复添加';
+                        }
+                        const accountList = await ctx.service.projectAccount.getAllDataByCondition({ where: { id: addAidList } });
+                        await ctx.service.contractAudit.saveAudits(options, accountList);
+                        responseData.data = await ctx.service.contractAudit.getList(options);
+                        break;
+                    case 'del-audit':
+                        uids = data.id instanceof Array ? data.id : [data.id];
+                        const cloneOptions = ctx.helper._.cloneDeep(options);
+                        cloneOptions.id = uids;
+                        auditList = await ctx.service.contractAudit.getAllDataByCondition({ where: cloneOptions });
+                        if (auditList.length !== uids.length) {
+                            throw '该用户已不存在成员管理中,移除失败';
+                        }
+                        await ctx.service.contractAudit.delAudit(uids);
+                        responseData.data = await ctx.service.contractAudit.getList(options);
+                        break;
+                    case 'save-permission':
+                        result = await ctx.service.contractAudit.updatePermission(data.updateData);
+                        if (!result) {
+                            throw '修改权限失败';
+                        }
+                        break;
+                    case 'list':
+                        responseData.data = await ctx.service.contractAudit.getList(options);
+                        break;
+                    default: throw '参数有误';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        async detail(ctx) {
+            try {
+                const whiteList = ctx.app.config.multipart.whitelist;
+                const contractOptions = ctx.helper._.cloneDeep(ctx.contractOptions);
+                contractOptions.contract_type = ctx.contract_type;
+                const contractTreeAudits = await ctx.service.contractTreeAudit.getAllDataByCondition({ where: contractOptions });
+                const renderData = {
+                    contractTreeAudits,
+                    audit_permission: ctx.contract_audit_permission,
+                    preUrl: ctx.contractOptions.tid ? '/contract/tender' : '/contract',
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.contract.detail),
+                    contract_type: ctx.contract_type,
+                    contractConst,
+                    whiteList,
+                };
+                if (ctx.session.sessionUser.is_admin) {
+                    const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                        where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                        columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                    });
+                    renderData.accountList = accountList;
+                    const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                    renderData.accountGroup = unitList.map(item => {
+                        const groupList = accountList.filter(item1 => item1.company === item.name);
+                        return { groupName: item.name, groupList };
+                    });
+                }
+                await this.layout('contract/detail.ejs', renderData, 'contract/detail_modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(ctx.contractOptions.spid ? '/contract' : '/contract/tender');
+            }
+        }
+
+        async loadDetail(ctx) {
+            try {
+                const responseData = {
+                    err: 0, msg: '', data: {},
+                };
+                const contractOptions = ctx.helper._.cloneDeep(ctx.contractOptions);
+                contractOptions.contract_type = ctx.contract_type;
+                // const data = JSON.parse(ctx.request.body.data);
+                // ctx.contractOptions.contract_type = ctx.contract_type;
+                responseData.data.contractTree = await ctx.service.contractTree.getAllDataByCondition({ where: contractOptions });
+                // 获取权限并展示对应合同
+                responseData.data.contractList = await ctx.service.contract.getListByUsers(contractOptions, ctx.session.sessionUser);
+                // responseData.data.contractList = await ctx.service.contract.getAllDataByCondition({ where: contractOptions });
+                // const userOptions = ctx.helper._.cloneDeep(ctx.contractOptions);
+                // // userOptions.uid = ctx.session.sessionUser.id;
+                // const permissionEdit = ctx.session.sessionUser.is_admin ? true : await ctx.service.contractAudit.getUserPermissionEdit(userOptions);
+                // responseData.data.auditContractTreePermission = permissionEdit ? [] : await ctx.service.contractTreeAudit.getAllDataByCondition({ where: contractOptions });
+                // responseData.data.contractAttList = await ctx.service.contractAtt.getAllDataByCondition({ where: ctx.contractOptions });
+                if (ctx.session.sessionUser.is_admin) {
+                    const contractAudits = await ctx.service.contractAudit.getList(ctx.contractOptions);
+                    const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                    const accountGroup = unitList.map(item => {
+                        const groupList = contractAudits.filter(item1 => item1.company === item.name);
+                        return { groupName: item.name, groupList };
+                    });
+                    responseData.data.contractAudits = contractAudits;
+                    responseData.data.accountGroup = ctx.helper._.filter(accountGroup, function (item) {
+                        return item.groupList.length > 0;
+                    });
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        async updateBills(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.postType || !data.postData) throw '数据错误';
+                const responseData = { err: 0, msg: '', data: {} };
+                const options = ctx.helper._.cloneDeep(ctx.contractOptions);
+                options.contract_type = ctx.contract_type;
+                switch (data.postType) {
+                    case 'add':
+                    case 'delete':
+                    case 'up-move':
+                    case 'down-move':
+                    case 'up-level':
+                    case 'down-level':
+                        responseData.data = await this._billsBase(ctx, data.postType, data.postData, options);
+                        break;
+                    case 'update':
+                        responseData.data = await ctx.service.contractTree.updateCalc(options, data.postData);
+                        break;
+                    case 'update-contract':
+                        responseData.data = await ctx.service.contract.updateCalc(options, data.postData);
+                        break;
+                    case 'paste-block':
+                        responseData.data = await this._pasteBlock(ctx, data.postData, options);
+                        break;
+                    case 'add-contract':
+                        responseData.data = await ctx.service.contract.add(options, data.postData.select, data.postData.contract);
+                        break;
+                    case 'add-tree-audit':
+                        responseData.data = await ctx.service.contractTreeAudit.add(options, data.postData.select, data.postData.auditId);
+                        break;
+                    case 'del-tree-audit':
+                        responseData.data = await ctx.service.contractTreeAudit.dels(options, data.postData.select, data.postData.auditIds);
+                        break;
+                    case 'get-contract':
+                        responseData.data.pays = await ctx.service.contractPay.getPays(options, data.postData);
+                        responseData.data.files = await ctx.service.contractAtt.getAtt(data.postData);
+                        break;
+                    case 'add-contract-pay':
+                        responseData.data = await ctx.service.contractPay.add(options, data.postData.select, data.postData.pay);
+                        break;
+                    case 'save-contract-pay':
+                        responseData.data = await ctx.service.contractPay.save(options, data.postData.select, data.postData.pay);
+                        break;
+                    case 'del-contract-pay':
+                        responseData.data = await ctx.service.contractPay.del(options, data.postData.select, data.postData.pay);
+                        break;
+                    default:
+                        throw '未知操作';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                console.log(err);
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
+        }
+
+        async _billsBase(ctx, type, data, options) {
+            if (isNaN(data.id) || data.id <= 0) throw '数据错误';
+            if (type !== 'add') {
+                if (isNaN(data.count) || data.count <= 0) data.count = 1;
+            }
+            switch (type) {
+                case 'add':
+                    return await ctx.service.contractTree.addNodeBatch(options, data.id, data.count);
+                case 'delete':
+                    return await ctx.service.contractTree.delete(options, data.id, data.count);
+                case 'up-move':
+                    return await ctx.service.contractTree.upMoveNode(options, data.id, data.count);
+                case 'down-move':
+                    return await ctx.service.contractTree.downMoveNode(options, data.id, data.count);
+                case 'up-level':
+                    return await ctx.service.contractTree.upLevelNode(options, data.id, data.count);
+                case 'down-level':
+                    return await ctx.service.contractTree.downLevelNode(options, data.id, data.count);
+                default:
+                    throw '未知操作';
+            }
+        }
+        /**
+         * 复制粘贴整块
+         *
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async _pasteBlock(ctx, data, options) {
+            if ((isNaN(data.id) || data.id <= 0) ||
+                (!data.block || data.block.length <= 0)) throw '参数错误';
+            return await ctx.service.contractTree.pasteBlockData(options, data.id, data.block);
+        }
+
+        /**
+         * 上传附件
+         * @param {*} ctx 上下文
+         */
+        async uploadFile(ctx) {
+            let stream;
+            try {
+                const responseData = { err: 0, msg: '', data: {} };
+                const cid = ctx.params.cid;
+                if (!cid) throw '参数有误';
+                const cpid = parseInt(ctx.params.cpid);
+                const parts = this.ctx.multipart({
+                    autoFields: true,
+                });
+                const files = [];
+                const create_time = Date.parse(new Date()) / 1000;
+                let idx = 0;
+                while ((stream = await parts()) !== undefined) {
+                    if (!stream.filename) {
+                        // 如果没有传入直接返回
+                        return;
+                    }
+                    const fileInfo = path.parse(stream.filename);
+                    const headpath = ctx.contractOptions.spid ? `sp/contract/${ctx.contractOptions.spid}` : `app/public/upload/${ctx.contractOptions.tid}`;
+                    const filepath = `${headpath}/contract/${cid}${cpid ? '/pay/' + cpid : ''}/fujian_${create_time + idx.toString() + fileInfo.ext}`;
+                    // await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, 'app', filepath));
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    files.push({ filepath, name: stream.filename, ext: fileInfo.ext });
+                    ++idx;
+                    stream && (await sendToWormhole(stream));
+                }
+                const in_time = new Date();
+                const payload = files.map(file => {
+                    let idx;
+                    if (Array.isArray(parts.field.name)) {
+                        idx = parts.field.name.findIndex(name => name === file.name);
+                    } else {
+                        idx = 'isString';
+                    }
+                    const newFile = {
+                        spid: ctx.contractOptions.spid || null,
+                        tid: ctx.contractOptions.tid || null,
+                        contract_type: ctx.contract_type,
+                        cid,
+                        uid: ctx.session.sessionUser.accountId,
+                        filename: file.name,
+                        fileext: file.ext,
+                        filesize: ctx.helper.bytesToSize(idx === 'isString' ? parts.field.size : parts.field.size[idx]),
+                        filepath: file.filepath,
+                        upload_time: in_time,
+                    };
+                    if (cpid) {
+                        newFile.cpid = cpid;
+                    }
+                    return newFile;
+                });
+                if (cpid) {
+                    // 执行文件信息写入数据库
+                    await ctx.service.contractPayAtt.saveFileMsgToDb(payload);
+                    // 将最新的当前标段的所有文件信息返回
+                    responseData.data = await ctx.service.contractPayAtt.getAtt(cpid);
+                } else {
+                    // 执行文件信息写入数据库
+                    await ctx.service.contractAtt.saveFileMsgToDb(payload);
+                    // 将最新的当前标段的所有文件信息返回
+                    responseData.data = await ctx.service.contractAtt.getAtt(cid);
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                stream && (await sendToWormhole(stream));
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        /**
+         * 删除附件
+         * @param {Ojbect} ctx 上下文
+         */
+        async deleteFile(ctx) {
+            try {
+                const cpid = ctx.params.cpid;
+                const cid = ctx.params.cid;
+                const responseData = { err: 0, msg: '', data: {} };
+                const data = JSON.parse(ctx.request.body.data);
+                let fileInfo = null;
+                if (cpid) {
+                    fileInfo = await ctx.service.contractPayAtt.getDataById(data.id);
+                } else if (cid) {
+                    fileInfo = await ctx.service.contractAtt.getDataById(data.id);
+                } else {
+                    throw '参数错误';
+                }
+                if (fileInfo || Object.keys(fileInfo).length) {
+                    // 先删除文件
+                    // await fs.unlinkSync(path.resolve(this.app.baseDir, './app', fileInfo.filepath));
+                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
+                    // 再删除数据库
+                    if (cpid) {
+                        await ctx.service.contractPayAtt.delete(data.id);
+                    } else {
+                        await ctx.service.contractAtt.delete(data.id);
+                    }
+                } else {
+                    throw '不存在该文件';
+                }
+                if (cpid) {
+                    responseData.data = await ctx.service.contractPayAtt.getAtt(cpid);
+                } else {
+                    responseData.data = await ctx.service.contractAtt.getAtt(cid);
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        /**
+         * 下载附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async downloadFile(ctx) {
+            const id = ctx.params.fid;
+            if (id) {
+                try {
+                    const cpid = ctx.params.cpid;
+                    const cid = ctx.params.cid;
+                    let fileInfo = null;
+                    if (cpid) {
+                        fileInfo = await ctx.service.contractPayAtt.getDataById(id);
+                    } else if (cid) {
+                        fileInfo = await ctx.service.contractAtt.getDataById(id);
+                    } else {
+                        throw '参数错误';
+                    }
+                    if (fileInfo || Object.keys(fileInfo).length) {
+                        // const fileName = path.join(__dirname, '../', fileInfo.filepath);
+                        // 解决中文无法下载问题
+                        const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                        let disposition = '';
+                        if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                            disposition = 'attachment; filename=' + encodeURIComponent(fileInfo.filename);
+                        } else if (userAgent.indexOf('firefox') >= 0) {
+                            disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(fileInfo.filename) + '"';
+                        } else {
+                            /* safari等其他非主流浏览器只能自求多福了 */
+                            disposition = 'attachment; filename=' + new Buffer(fileInfo.filename).toString('binary');
+                        }
+                        ctx.response.set({
+                            'Content-Type': 'application/octet-stream',
+                            'Content-Disposition': disposition,
+                            'Content-Length': fileInfo.filesize,
+                        });
+                        // ctx.body = await fs.createReadStream(fileName);
+                        ctx.body = await ctx.helper.ossFileGet(fileInfo.filepath);
+                    } else {
+                        throw '不存在该文件';
+                    }
+                } catch (err) {
+                    this.log(err);
+                    this.setMessage(err.toString(), this.messageType.ERROR);
+                }
+            }
+        }
+    }
+
+    return ContractController;
+};

+ 1 - 1
app/controller/datacollect_controller.js

@@ -77,7 +77,7 @@ module.exports = app => {
                 if (ctx.params.index) {
                     ctx.session.sessionProject.dataCollect = parseInt(ctx.params.index);
                 }
-                const is_dz2 = ['P0505', 'P0506', 'P1201', 'P1202', 'GY18Y'].indexOf(ctx.session.sessionProject.code) !== -1
+                const is_dz2 = ['P0505', 'P0506', 'P1201', 'P1202', 'GY18Y', 'GYJJ1'].indexOf(ctx.session.sessionProject.code) !== -1
                     && projectData.data_collect_pages.includes('6') && ctx.session.sessionProject.dataCollect === 6;
                 const renderData = {
                     projectData,

+ 1 - 1
app/controller/setting_controller.js

@@ -1158,7 +1158,7 @@ module.exports = app => {
                 const categoryData = await ctx.service.category.getAllCategory(ctx.session.sessionProject.id);
                 const tenders = await ctx.service.tender.getList('', null, 1);
                 const dcTenders = await ctx.service.datacollectTender.getList(projectId);
-                const is_dz2 = ['P0505', 'P0506', 'P1201', 'P1202', 'GY18Y'].indexOf(ctx.session.sessionProject.code) !== -1 ? 6 : false;
+                const is_dz2 = ['P0505', 'P0506', 'P1201', 'P1202', 'GY18Y', 'GYJJ1'].indexOf(ctx.session.sessionProject.code) !== -1 ? 6 : false;
                 const renderData = {
                     projectData,
                     dataCollectAudits,

+ 10 - 0
app/extend/helper.js

@@ -834,6 +834,16 @@ module.exports = {
         return result;
     },
 
+    _getOptionsSql(options) {
+        const optionSql = [];
+        for (const key in options) {
+            if (options.hasOwnProperty(key)) {
+                optionSql.push(key + ' = ' + this.ctx.app.mysql.escape(options[key]));
+            }
+        }
+        return optionSql.join(' AND ');
+    },
+
     /**
      * 合并 相关数据
      * @param {Array} main - 主数据

+ 93 - 0
app/middleware/contract_check.js

@@ -0,0 +1,93 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const messageType = require('../const/message_type');
+const _ = require('lodash');
+const contractConst = require('../const/contract');
+
+module.exports = options => {
+    /**
+     * 标段校验 中间件
+     * 1. 读取标段数据(包括属性)
+     * 2. 检验用户是否可见标段(不校验具体权限)
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* contractCheck(next) {
+        try {
+            if (!this.session.sessionProject.page_show.openContract) {
+                throw '该功能已关闭或无法查看';
+            }
+            const stid = this.params.stid;
+            const type = this.params.type ? contractConst.type[this.params.type] : contractConst.type.expenses;
+            if (!stid) throw '参数错误';
+            let spid = null;
+            let tid = null;
+            // 判断stid字符串是不是只有数字
+            if (!/^\d+$/.test(stid)) {
+                spid = stid;
+            } else {
+                tid = stid;
+            }
+            if (!spid && !tid) {
+                throw '参数数据错误';
+            }
+            const info = spid ? yield this.service.subProject.getDataById(spid) : yield this.service.tender.getDataById(tid);
+            if (!info) throw '项目或标段不存在';
+            const options = spid ? { spid } : { tid };
+            if (this.request.originalUrl && this.request.originalUrl.indexOf('detail') > -1) yield this.service.contractTree.insertTree(options, info);
+            // 权限控制
+            const cloneOptions = _.cloneDeep(options);
+            cloneOptions.uid = this.session.sessionUser.accountId;
+            const result = yield this.service.contractAudit.getDataByCondition(cloneOptions);
+            // const result = yield this.service.contractAudit.checkPermission(options, this.session.sessionUser.accountId);
+            if (!result && !this.session.sessionUser.is_admin) {
+                throw '当前账号权限不足,请联系管理员添加权限';
+            }
+            this.contract = info;
+            this.contractOptions = options;
+            this.contract_audit_permission = result;
+            this.contract_type = type;
+            yield next;
+        } catch (err) {
+            // 输出错误到日志
+            if (err.stack) {
+                this.logger.error(err);
+            } else {
+                this.session.message = {
+                    type: messageType.ERROR,
+                    icon: 'exclamation-circle',
+                    message: err,
+                };
+                this.getLogger('fail').info(JSON.stringify({
+                    error: err,
+                    project: this.session.sessionProject,
+                    user: this.session.sessionUser,
+                    body: this.session.body,
+                }));
+            }
+            if (this.helper.isAjax(this.request)) {
+                if (err.stack) {
+                    this.body = { err: 4, msg: '标段数据未知错误', data: null };
+                } else {
+                    this.body = { err: 3, msg: err.toString(), data: null };
+                }
+            } else {
+                if (this.helper.isWap(this.request)) {
+                    this.redirect('/wap/list');
+                } else {
+                    this.postError(err, '未知错误');
+                    err === '该功能已关闭或无法查看' ? this.redirect('/dashboard') : this.request.headers.referer ? this.redirect(this.request.headers.referer) : this.redirect('/contract');
+                }
+            }
+        }
+    };
+};

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1947 - 0
app/public/js/contract_detail.js


+ 208 - 0
app/public/js/contract_index.js

@@ -0,0 +1,208 @@
+$(document).ready(function() {
+    autoFlashHeight();
+    const projectTreeObj = (function(setting){
+        const ProjectTree = createNewPathTree('revise', setting.treeSetting);
+        ProjectTree.loadDatas(setting.source);
+        treeCalc.calculateAll(ProjectTree);
+        const TableObj = $(setting.table);
+        let tenderTreeShowLevel;
+
+        const Utils = {
+            getProgressHtml: function(total, yf, title = '') {
+                if (total) {
+                    let yfP = ZhCalc.mul(ZhCalc.div(yf, total, 2), 100, 0);
+                    let other = Math.max(ZhCalc.sub(total, yf), 0);
+                    let otherP = Math.max(100 - yfP, 0);
+                    const html = '<div class="progress">' +
+                        '<div class="progress-bar bg-success" style="width: ' + yfP + '%;" data-placement="bottom" data-toggle="tooltip" data-original-title="'+ title + ':¥' + (yf || 0) + '">' + yfP + '%</div>' +
+                        '<div class="progress-bar bg-gray" style="width: ' + otherP + '%;" data-placement="bottom" data-toggle="tooltip" data-original-title="未完成:¥' + (other || 0) + '">' + otherP + '%</div>' +
+                        '</div>';
+                    return html;
+                } else {
+                    return '';
+                }
+            },
+            getRowTdHtml: function (node, tree) {
+                const html = [];
+                // 名称
+                html.push('<td width="20%" class="in-' + node.tree_level + '">');
+                if (node.is_folder) {
+                    if (node.children.length > 0) {
+                        html.push('<span onselectstart="return false" style="{-moz-user-select:none}" class="fold-switch mr-1" title="收起" id="'+ node.id +'"><i class="fa fa-minus-square-o"></i></span> <i class="fa fa-folder-o"></i> ', node.name);
+                    } else {
+                        html.push('<i class="fa fa-folder-o"></i> ', node.name);
+                    }
+                } else {
+                    html.push(`<span class="text-muted mr-2">${tree.isLastSibling(node) ? '└' : '├'}</span>`);
+                    html.push('<a href="/contract/'+ node.id +'/detail" name="name" id="' + node.id + '">', node.name, '</a>');
+                }
+                html.push('</td>');
+                // 创建时间
+                if (node.is_folder) {
+                    html.push(`<td class="text-center"></td>`);
+                } else {
+                    html.push(`<td class="text-center">${moment(node.create_time).format('YYYY-MM-DD')}</td>`);
+                }
+                html.push(`<td class="text-center">${node.expenses_count || ''}</td>
+                                        <td class="text-right">${node.expenses_total_price || ''}</td>
+                                        <td>
+                                            ${Utils.getProgressHtml(node.expenses_total_price, node.expenses_yf_price, '累计应付')}
+                                        </td>
+                                        <td class="text-center">${node.income_count || ''}</td>
+                                        <td class="text-right">${node.income_total_price || ''}</td>
+                                        <td>
+                                          ${Utils.getProgressHtml(node.income_total_price, node.income_yf_price, '累计应回')} 
+                                        </td>`);
+                // 操作
+                if (is_admin) {
+                    html.push(`<td class="text-center">`);
+                    if (!node.is_folder) {
+                        html.push(`<a href="javascript:void(0);" data-toggle="modal" data-stid="${node.id}" class="btn btn-sm btn-primary get-audits"> 成员管理</a>`);
+                    }
+                    html.push('</td>');
+                }
+                return html.join('');
+            },
+            getNodeTrHtml: function (node, tree) {
+                const html = [];
+                html.push(`<tr tree_id="${node.id}" draggable="true">`);
+                html.push(Utils.getRowTdHtml(node, tree));
+                html.push(`</tr>`);
+                return html.join('');
+            },
+            reloadTable: function () {
+                const html = [];
+                for (const node of ProjectTree.nodes) {
+                    html.push(Utils.getNodeTrHtml(node, ProjectTree));
+                }
+                TableObj.html(html.join(''));
+            },
+            getSelectNode: function() {
+                const selectId = $('tr.table-active').attr('tree_id');
+                return selectId ? ProjectTree.getItems(selectId) : null;
+            },
+            getSelectNodeId: function() {
+                const selectId = $('tr.table-active').attr('tree_id');
+                return selectId || setting.treeSetting.rootId;
+            },
+            refreshTreeTable: function(result) {
+                ProjectTree.loadDatas(result);
+                if (ProjectTree.nodes.length > 0 && $('#no-project').length > 0) window.location.reload();
+                Utils.reloadTable();
+            },
+            refreshRow: function(result) {
+                const refreshData = ProjectTree.loadPostData(result);
+                if (!refreshData.update) return;
+                for (const u of refreshData.update) {
+                    $(`tr[tree_id=${u.id}]`).html(Utils.getRowTdHtml(u, ProjectTree));
+                }
+            },
+            expandByLevel: function(level){
+                ProjectTree.expandByLevel(level);
+                for (const node of ProjectTree.nodes) {
+                    const tr = $(`tr[tree_id=${node.id}]`);
+                    if (node.expanded) {
+                        $('.fold-switch', tr).html(`<i class="fa fa-minus-square-o"></i>`);
+                    } else {
+                        $('.fold-switch', tr).html(`<i class="fa fa-plus-square-o"></i>`);
+                    }
+                    if (node.visible) {
+                        tr.show();
+                    } else {
+                        tr.hide();
+                    }
+                }
+            }
+        };
+        Utils.reloadTable();
+        $('body').on('click', 'tr[tree_id]', function() {
+            if ($(this).hasClass('table-active')) {
+                $(this).removeClass('table-active');
+            } else {
+                $('tr[tree_id].table-active').removeClass('table-active');
+                $(this).addClass('table-active');
+            }
+            // Utils.refreshAddButton();
+        });
+        $('body').on('dragstart', 'tr[tree_id]', function(e) {
+            Utils.dragNode = ProjectTree.getItems(e.target.getAttribute('tree_id'));
+        });
+        $('body').on('drop', 'tr[tree_id]', function(e) {
+            Utils.dropNode = ProjectTree.getItems(e.currentTarget.getAttribute('tree_id'));
+            // Utils.dropTo();
+        });
+        $('body').on('dragover', 'tr[tree_id]', function(e) {
+            const parent = ProjectTree.getItems(e.currentTarget.getAttribute('tree_id'));
+            return !parent || !parent.is_folder || parent.tree_level > 3 || parent.id === Utils.dragNode.id;
+        });
+        $('body').on('click', '.fold-switch', function() {
+            const id = this.getAttribute('id');
+            const node = ProjectTree.getItems(id);
+            ProjectTree.setExpanded(node, !node.expanded);
+            const posterity = ProjectTree.getPosterity(node);
+            if (node.expanded) {
+                $(this).html(`<i class="fa fa-minus-square-o"></i>`);
+            } else {
+                $(this).html(`<i class="fa fa-plus-square-o"></i>`);
+            }
+            for (const p of posterity) {
+                if (p.visible) {
+                    $(`tr[tree_id=${p.id}]`).show();
+                } else {
+                    $(`tr[tree_id=${p.id}]`).hide();
+                }
+            }
+        });
+
+        const getChildrenLevel = function (node) {
+            let iLevel = node.tree_level || 1;
+            if (node.children && node.children.length > 0) {
+                for (const c of node.children) {
+                    iLevel = Math.max(iLevel, getChildrenLevel(c));
+                }
+            }
+            return iLevel;
+        };
+        tenderTreeShowLevel = $.cs_showLevel({
+            selector: '#show-level',
+            levels: [
+                {
+                    type: 'sort', count: 5, visible_count: function () {
+                        return ProjectTree.children.map(getChildrenLevel).reduce((x, y) => { return Math.max(x, y); }, 0) - 1;
+                    }
+                },
+                {
+                    type: 'last', title: '最底层', visible: function () {
+                        const count = ProjectTree.children.map(getChildrenLevel).reduce((x, y) => { return Math.max(x, y); }, 0) - 1;
+                        return count > 0;
+                    }
+                },
+            ],
+            showLevel: function (tag) {
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                    case "5":
+                        Utils.expandByLevel(parseInt(tag));
+                        break;
+                    case "last":
+                        Utils.expandByLevel(20);
+                        break;
+                    default: return;
+                }
+            }
+        });
+        tenderTreeShowLevel.initShowLevel();
+        tenderTreeShowLevel.refreshMenuVisible();
+        return { ProjectTree, TableObj, ...Utils };
+    })({
+        treeSetting: { id: 'id', pid: 'tree_pid', level: 'tree_level', order: 'tree_order', rootId: '-1',
+            keys: ['id', 'project_id'],
+            calcFields: ['expenses_count', 'expenses_total_price', 'expenses_yf_price', 'income_count', 'income_total_price', 'income_yf_price'],
+        },
+        source: projectList,
+        table: '#projectList',
+    });
+});

+ 111 - 0
app/public/js/contract_tender.js

@@ -0,0 +1,111 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2018/10/11
+ * @version
+ */
+const tenderListSpec = (function(){
+    function getProgressHtml(total, yf, title = '') {
+        if (total !== 0) {
+            let yfP = ZhCalc.mul(ZhCalc.div(yf, total, 2), 100, 0);
+            let other = Math.max(ZhCalc.sub(total, yf), 0);
+            let otherP = Math.max(100 - yfP, 0);
+            const html = '<div class="progress">' +
+                '<div class="progress-bar bg-success" style="width: ' + yfP + '%;" data-placement="bottom" data-toggle="tooltip" data-original-title="'+ title + ':¥' + (yf || 0) + '">' + yfP + '%</div>' +
+                '<div class="progress-bar bg-gray" style="width: ' + otherP + '%;" data-placement="bottom" data-toggle="tooltip" data-original-title="未完成:¥' + (other || 0) + '">' + otherP + '%</div>' +
+                '</div>';
+            return html;
+        } else {
+            return '';
+        }
+    }
+    function getTenderNodeHtml(node, arr, pid) {
+        const html = [];
+        html.push('<tr pid="' + pid + '">');
+        // 名称
+        html.push('<td style="min-width: 200px" class="in-' + node.level + '">');
+        if (node.cid) {
+            html.push('<span onselectstart="return false" style="{-moz-user-select:none}" class="fold-switch mr-1" title="收起" cid="'+ node.sort_id +'"><i class="fa fa-minus-square-o"></i></span> <i class="fa fa-folder-o"></i> ', node.name);
+        } else {
+            html.push('<span class="text-muted mr-2">');
+            html.push(arr.indexOf(node) === arr.length - 1 ? '└' : '├');
+            html.push('</span>');
+            //html.push('<a href="/tender/' + node.id + '">', node[c.field], '</a>');
+            html.push('<a href="/contract/'+ node.id +'/detail" name="name" style="min-width: 200px;word-break:break-all;" id="' + node.id + '">', node.name, '</a>');
+        }
+        html.push('</td>');
+
+        // 创建时间
+        html.push('<td style="width: 8%" class="text-center">');
+        html.push(node.create_time ? moment(node.create_time).format('YYYY-MM-DD') : '');
+        html.push('</td>');
+        html.push(`<td style="width: 5%" class="text-center">${node.expenses_count || ''}</td>
+                                        <td style="width: 8%" class="text-right">${node.expenses_total_price || ''}</td>
+                                        <td style="width: 15%">
+                                            ${getProgressHtml(node.expenses_total_price, node.expenses_yf_price, '累计应付')}
+                                        </td>
+                                        <td style="width: 5%" class="text-center">${node.income_count || ''}</td>
+                                        <td style="width: 8%" class="text-right">${node.income_total_price || ''}</td>
+                                        <td style="width: 15%">
+                                            ${getProgressHtml(node.income_total_price, node.income_yf_price, '累计应回')}
+                                        </td>`);
+        // 设置
+        if (is_admin) {
+            html.push('<td style="width: 10%" class="text-center">');
+            if (!node.cid) {
+                html.push(`<a href="javascript:void(0);" data-toggle="modal" data-stid="${node.id}" class="btn btn-sm btn-primary get-audits"> 成员管理</a>`);
+                // html.push('<a href="#empower" data-toggle="modal" data-target="#empower" class="btn btn-sm btn-primary ">成员管理</a>');
+            }
+            html.push('</td>');
+        }
+        html.push('</tr>');
+        return html.join('');
+    }
+    function getTenderTreeHeaderHtml() {
+        const html = [];
+        html.push('<table class="table table-hover table-bordered">');
+        html.push('<thead style="position: fixed;left:56px;top: 34px;" class="text-center">', '<tr>');
+        html.push('<th style="min-width: 200px" rowspan="2">', '标段名称', '</th>');
+        html.push('<th style="width: 8%" rowspan="2">', '创建时间', '</th>');
+        html.push('<th colspan="3">支出合同</th>');
+        html.push('<th colspan="3">收入合同</th>');
+        if (is_admin) {
+            html.push('<th style="width: 10%" rowspan="2">', '操作', '</th>');
+        }
+        html.push('</tr>');
+        html.push(`<tr>
+                        <th style="width: 5%">合同个数</th>
+                        <th style="width: 8%">合同金额</th>
+                        <th style="width: 15%">支出进度</th>
+                        <th style="width: 5%">合同个数</th>
+                        <th style="width: 8%">合同金额</th>
+                        <th style="width: 15%">回款进度</th>
+                    </tr>`, '</thead>');
+        return html.join('');
+    }
+    function calculateParent(node) {
+        if (node.children && node.cid) {
+            node.expenses_count = 0;
+            node.expenses_total_price = 0;
+            node.expenses_yf_price = 0;
+            node.income_count = 0;
+            node.income_total_price = 0;
+            node.income_yf_price = 0;
+            for (const c of node.children) {
+                calculateParent(c);
+                node.expenses_count = ZhCalc.add(node.expenses_count, c.expenses_count);
+                node.expenses_total_price = ZhCalc.add(node.expenses_total_price, c.expenses_total_price);
+                node.expenses_yf_price = ZhCalc.add(node.expenses_yf_price, c.expenses_yf_price);
+                node.income_count = ZhCalc.add(node.income_count, c.income_count);
+                node.income_total_price = ZhCalc.add(node.income_total_price, c.income_total_price);
+                node.income_yf_price = ZhCalc.add(node.income_yf_price, c.income_yf_price);
+            }
+        }
+    }
+    return { getTenderNodeHtml, getTenderTreeHeaderHtml, calculateParent }
+})();
+
+

+ 2 - 2
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -2668,7 +2668,7 @@ const SpreadJsObj = {
                 canvas.restore();
 
                 const col = options.sheet.zh_setting.cols[options.col];
-                const barData = value;
+                const barData = value || [];
                 const left = x + indent;
                 const startTop = y + indent;
                 const validHeight = h - indent - indent - (col.stackedBarCover || barData.length === 1 ? 1 : barData.length / 2 + 1);
@@ -3251,4 +3251,4 @@ const GridSpreadUtils = {
         }
         return [gridData, node, sel.row, count];
     },
-};
+};

+ 27 - 3
app/public/js/tender_showhide.js

@@ -114,11 +114,35 @@ function localHideList(wap = false) {
 $(window).resize(setTopTr);
 // 设置表头固定并动态调整宽度高度
 function setTopTr() {
-    for(let item = 0; item < $(".c-body table>thead>tr>th").length; item ++) {
-        $(".c-body table>thead>tr>th").eq(item).outerWidth($(".c-body table>tbody>tr:first").children('td').eq(item).outerWidth());
+    const colSpanNum = $(".c-body table>thead>tr").eq(1).children("th").length;
+    if (colSpanNum === 0) {
+        for(let item = 0; item < $(".c-body table>thead>tr>th").length; item ++) {
+            $(".c-body table>thead>tr>th").eq(item).outerWidth($(".c-body table>tbody>tr:first").children('td').eq(item).outerWidth());
+        }
+    } else {
+        const colSpanArr = [];
+        let colSpanIndex = 0;
+        for (let item = 0; item < $(".c-body table>thead>tr").eq(0).children("th").length; item ++) {
+            if ($(".c-body table>thead>tr").eq(0).children('th').eq(item).attr('colspan') === undefined) {
+                $(".c-body table>thead>tr").eq(0).children('th').eq(item).outerWidth($(".c-body table>tbody>tr:first").children('td').eq(colSpanIndex).outerWidth());
+                colSpanIndex = colSpanIndex + 1;
+            } else {
+                colSpanIndex = colSpanIndex + parseInt($(".c-body table>thead>tr").eq(0).children('th').eq(item).attr('colspan'));
+                colSpanArr.push({
+                    num: parseInt($(".c-body table>thead>tr").eq(0).children('th').eq(item).attr('colspan')),
+                    end: colSpanIndex,
+                });
+            }
+        }
+        let colStart = 0;
+        for (const col of colSpanArr) {
+            for (let item = col.num; item > 0; item--) {
+                $(".c-body table>thead>tr").eq(1).children('th').eq(colStart).outerWidth($(".c-body table>tbody>tr:first").children('td').eq(col.end - item).outerWidth());
+                colStart = colStart + 1;
+            }
+        }
     }
     $('.c-body table').css('margin-top', $(".c-body table>thead").height() - 4);
-    // $('.c-body table').css('margin-top', -2);
 }
 
 function doTrStatus(node, status, all = '') {

+ 19 - 0
app/router.js

@@ -51,6 +51,8 @@ module.exports = app => {
     const paymentDetailCheck = app.middlewares.paymentDetailCheck();
     // 施工日志中间件
     const constructionCheck = app.middlewares.constructionCheck();
+    // 合同管理中间件
+    const contractCheck = app.middlewares.contractCheck();
     // 登入登出相关
     app.get('/login', 'loginController.index');
     app.get('/login/:code', 'loginController.index');
@@ -928,4 +930,21 @@ module.exports = app => {
     app.post('/construction/:tid/log/:id/file/upload', sessionAuth, constructionCheck, 'constructionController.uploadFile');
     app.post('/construction/:tid/log/:id/file/delete', sessionAuth, constructionCheck, 'constructionController.deleteFile');
     app.get('/construction/:tid/log/:id/file/:fid/download', sessionAuth, constructionCheck, 'constructionController.downloadFile');
+
+    // 合同管理
+    app.get('/contract', sessionAuth, 'contractController.index');
+    app.get('/contract/tender', sessionAuth, 'contractController.tender');
+    app.post('/contract/:stid/audit/save', sessionAuth, contractCheck, 'contractController.auditSave');
+    app.get('/contract/:stid/detail', sessionAuth, contractCheck, 'contractController.detail');
+    app.get('/contract/:stid/detail/:type', sessionAuth, contractCheck, 'contractController.detail');
+    app.post('/contract/:stid/detail/load', sessionAuth, contractCheck, 'contractController.loadDetail');
+    app.post('/contract/:stid/detail/:type/load', sessionAuth, contractCheck, 'contractController.loadDetail');
+    app.post('/contract/:stid/detail/update', sessionAuth, contractCheck, 'contractController.updateBills');
+    app.post('/contract/:stid/detail/:type/update', sessionAuth, contractCheck, 'contractController.updateBills');
+    app.post('/contract/:stid/detail/:type/:cid/file/upload', sessionAuth, contractCheck, 'contractController.uploadFile');
+    app.post('/contract/:stid/detail/:type/:cid/file/delete', sessionAuth, contractCheck, 'contractController.deleteFile');
+    app.get('/contract/:stid/detail/:type/:cid/file/:fid/download', sessionAuth, contractCheck, 'contractController.downloadFile');
+    app.post('/contract/:stid/detail/:type/:cid/pay/:cpid/file/upload', sessionAuth, contractCheck, 'contractController.uploadFile');
+    app.post('/contract/:stid/detail/:type/:cid/pay/:cpid/file/delete', sessionAuth, contractCheck, 'contractController.deleteFile');
+    app.get('/contract/:stid/detail/:type/:cid/pay/:cpid/file/:fid/download', sessionAuth, contractCheck, 'contractController.downloadFile');
 };

+ 177 - 0
app/service/contract.js

@@ -0,0 +1,177 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class Contract extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            const setting = {
+                spid: 'spid',
+                mid: 'tid',
+                type: 'contract_type',
+                kid: 'contract_id',
+                pid: 'contract_pid',
+                order: 'order',
+                level: 'level',
+                isLeaf: 'is_leaf',
+                fullPath: 'full_path',
+                keyPre: 'contract_maxLid:', // 换个名称,防止缓存导致旧数据出问题
+                uuid: true,
+            };
+            super(ctx);
+            this.setting = setting;
+            this.tableName = 'contract';
+            this.dataId = 'id';
+        }
+
+        async add(options, node, data) {
+            if (!options[this.setting.type] || !node || !data) throw '参数有误';
+            let insertData = [];
+            const transaction = await this.db.beginTransaction();
+            try {
+                const maxId = await this.ctx.service.contractTree._getMaxLid(options);
+                insertData = {
+                    id: this.uuid.v4(),
+                    spid: options.spid || null,
+                    tid: options.tid || null,
+                    contract_type: options[this.setting.type],
+                    uid: this.ctx.session.sessionUser.accountId,
+                    contract_id: maxId + 1,
+                    contract_pid: !node.c_code ? node.contract_id : node.contract_pid,
+                    level: !node.c_code ? node.level + 1 : node.level,
+                    is_leaf: 1,
+                    c_code: data.code,
+                    name: data.name,
+                    total_price: data.total_price,
+                    party_b: data.party_b,
+                    remark: data.remark,
+                    create_time: new Date(),
+                };
+                insertData[this.setting.fullPath] = !node.c_code
+                    ? node[this.setting.fullPath] + '-' + insertData[this.setting.kid]
+                    : node[this.setting.fullPath].replace('-' + node[this.setting.kid], '-' + insertData[this.setting.kid]);
+                const order = !node.c_code ? (!node.is_leaf ? await this.getMaxOrder(options, node.contract_id, transaction) : 1) : node.order + 1;
+                insertData[this.setting.order] = order;
+                await this.ctx.service.contractTree._updateChildrenOrder(options, insertData[this.setting.pid], insertData[this.setting.order], 1, transaction);
+                await transaction.insert(this.tableName, insertData);
+                if (!node.c_code && node.is_leaf) {
+                    await transaction.update(this.ctx.service.contractTree.tableName, { id: node.id, is_leaf: 0 });
+                }
+                this.ctx.service.contractTree._cacheMaxLid(options, maxId + 1);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            const updateData = await this.ctx.service.contractTree.getNextsData(options, !node.c_code ? node.contract_id : node[this.setting.pid], node[this.setting.order] + 1);
+            if (!node.c_code) {
+                const parent = await this.ctx.service.contractTree.getDataById(node.id);
+                updateData.push(parent);
+            }
+            return { create: insertData, update: updateData };
+        }
+
+        /**
+         * 提交数据 - 响应计算(增量方式计算)
+         * @param {Number} tenderId
+         * @param {Object} data
+         * @return {Promise<*>}
+         */
+        async updateCalc(options, data) {
+            const helper = this.ctx.helper;
+            if (!data) {
+                throw '提交数据错误';
+            }
+            const datas = data instanceof Array ? data : [data];
+            const ids = [];
+            for (const row of datas) {
+                ids.push(row.id);
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                const updateDatas = [];
+                for (const row of datas) {
+                    const updateNode = await this.getDataById(row.id);
+                    if (!updateNode) {
+                        throw '提交数据错误';
+                    }
+                    updateDatas.push(row);
+                }
+                if (updateDatas.length > 0) await transaction.updateRows(this.tableName, updateDatas);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            return { update: await this.getDataById(ids) };
+        }
+
+        async getMaxOrder(options, pid, transaction) {
+            const sql = 'SELECT MAX(`' + this.setting.order + '`) AS max_order FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ?';
+            const params = [this.tableName, pid];
+            const result = await transaction.query(sql, params);
+            const maxOrder = result[0].max_order || 0;
+            const sql1 = 'SELECT MAX(`' + this.setting.order + '`) AS max_order FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ?';
+            const params1 = [this.ctx.service.contractTree.tableName, pid];
+            const result1 = await transaction.query(sql1, params1);
+            const maxOrder1 = result1[0].max_order || 0;
+            return Math.max(maxOrder, maxOrder1) + 1;
+        }
+
+        async getListByUsers(options, user) {
+            const _ = this._;
+            const list = await this.getAllDataByCondition({ where: options });
+            if (user.is_admin) {
+                return list;
+            }
+            const userPermission = await this.ctx.service.contractAudit.getDataByCondition({ spid: options.spid || null, tid: options.tid || null, uid: user.accountId });
+            if (!userPermission) return [];
+            const cloneOptions = this._.cloneDeep(options);
+            cloneOptions.uid = user.accountId;
+            const userTreePermission = await this.ctx.service.contractTreeAudit.getAllDataByCondition({ where: cloneOptions });
+            if (userTreePermission.length === 0) return list;
+            const newList = this._.filter(list, { uid: user.accountId });
+            const userInfo = await this.ctx.service.projectAccount.getDataById(user.accountId);
+            // const unit = userInfo.company ? await this.ctx.service.constructionUnit.getDataByCondition({ pid: userInfo.project_id, name: userInfo.company }) : null;
+            const uids = await this.ctx.service.projectAccount.getAllDataByCondition({ columns: ['id'], where: { project_id: userInfo.project_id, company: userInfo.company } });
+            for (const ut of userTreePermission) {
+                if (userPermission.permission_show_node) {
+                    const nodes = this._.filter(list, { contract_pid: ut.contract_id });
+                    newList.push(...this._.filter(nodes, function(item) {
+                        return item.uid !== user.accountId;
+                    }));
+                } else if (userPermission.permission_show_unit) {
+                    const nodes = this._.filter(list, function(item) {
+                        return item.contract_pid === ut.contract_id && _.includes(_.map(uids, 'id'), item.uid);
+                    });
+                    newList.push(...this._.filter(nodes, function(item) {
+                        return item.uid !== user.accountId;
+                    }));
+                // } else {
+                //     const nodes = this._.filter(list, { contract_pid: ut.contract_id, uid: user.accountId });
+                //     newList.push(...nodes);
+                }
+            }
+            return newList;
+        }
+
+        // async getCountByUser(stid, type, user) {
+        //
+        // }
+    }
+    return Contract;
+};

+ 61 - 0
app/service/contract_att.js

@@ -0,0 +1,61 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+const contractConst = require('../const/contract');
+
+module.exports = app => {
+
+    class ContractAtt extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'contract_attachment';
+            this.dataId = 'id';
+        }
+
+        async getAtt(cid) {
+            const sql = 'SELECT a.*, b.name as username FROM ?? as a LEFT JOIN ?? as b ON a.`uid` = b.`id` WHERE a.`cid` = ? ORDER BY `upload_time` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid];
+            const result = await this.db.query(sql, sqlParam);
+            return result.map(item => {
+                item.orginpath = this.ctx.app.config.fujianOssPath + item.filepath;
+                if (!this.ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `/contract/${item.spid || item.tid}/detail/${contractConst.typeMap[item.contract_type]}/${item.cid}/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                    item.viewpath = item.filepath;
+                }
+                return item;
+            });
+        }
+
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+    }
+    return ContractAtt;
+};

+ 134 - 0
app/service/contract_audit.js

@@ -0,0 +1,134 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class ContractAudit extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'contract_audit';
+            this.dataId = 'id';
+        }
+
+        async getList(options) {
+            const list = await this.db.select(this.tableName, { where: options, orders: [['id', 'desc']] });
+            for (const l of list) {
+                const accountInfo = await this.ctx.service.projectAccount.getDataById(l.uid);
+                l.name = accountInfo.name;
+                l.role = accountInfo.role;
+                l.company = accountInfo.company;
+                l.mobile = accountInfo.mobile;
+            }
+            return list;
+        }
+
+        async saveAudits(options, accountList, transaction = null) {
+            // 判断是否已存在该用户,存在则不插入
+            const pauditList = await this.getAllDataByCondition({ where: options });
+            const pushData = [];
+            for (const a of this._.uniqBy(accountList, 'id')) {
+                if (this._.findIndex(pauditList, { uid: a.id }) === -1) {
+                    const data = {
+                        spid: options.spid || null,
+                        tid: options.tid || null,
+                        uid: a.id,
+                        create_time: new Date(),
+                    };
+                    pushData.push(data);
+                }
+            }
+            if (pushData.length > 0) {
+                return transaction ? await transaction.insert(this.tableName, pushData) : await this.db.insert(this.tableName, pushData);
+            }
+            return false;
+        }
+
+        async delAudit(id) {
+            return await this.db.delete(this.tableName, { id });
+        }
+
+        async updatePermission(updateData) {
+            if (!updateData.id) {
+                return false;
+            }
+            return await this.db.update(this.tableName, updateData);
+        }
+
+        async checkPermission(options, uid) {
+            if (this.ctx.session.sessionUser.is_admin) {
+                return true;
+            }
+            let flag = false;
+            const cloneOptions = this._.cloneDeep(options);
+            cloneOptions.uid = uid;
+            const info = await this.getDataByCondition(cloneOptions);
+            if (info) {
+                flag = true;
+            }
+            return flag;
+        }
+
+        async getUserPermissionEdit(options, uid) {
+            const cloneOptions = this._.cloneDeep(options);
+            cloneOptions.uid = uid;
+            const info = await this.getDataByCondition(cloneOptions);
+            return info && info.permission_edit;
+        }
+
+        async getUserList(tid, is_report = null) {
+            const reportSql = is_report !== null ? ' AND ca.`is_report` = ' + is_report : '';
+            const sql = 'SELECT ca.*, pa.name as user_name FROM ?? AS ca LEFT JOIN ?? AS pa ON ca.`uid` = pa.`id` ' +
+                'WHERE ca.`tid` = ?' + reportSql + ' ORDER BY ca.`id` DESC';
+            const params = [this.tableName, this.ctx.service.projectAccount.tableName, tid];
+            return await this.db.query(sql, params);
+        }
+
+        async setOtherTender(tidList, userList) {
+            // 根据标段找出创建人去除,已存在的先删除再插入
+            const transaction = await this.db.beginTransaction();
+            try {
+                const tenderList = await this.ctx.service.tender.getAllDataByCondition({
+                    columns: ['id', 'user_id'],
+                    where: { id: tidList.split(',') },
+                });
+                const oldTouristList = await this.getAllDataByCondition({ where: { tid: tidList.split(',') } });
+                const insertData = [];
+                const deleteIdData = [];
+                for (const user of userList) {
+                    for (const t of tenderList) {
+                        const delId = this._.find(oldTouristList, { tid: t.id, uid: user.uid });
+                        if (delId) deleteIdData.push(delId.id);
+                        if (user.uid !== t.user_id) {
+                            insertData.push({
+                                tid: t.id,
+                                uid: user.uid,
+                                is_report: user.is_report,
+                                in_time: new Date(),
+                            });
+                        }
+                    }
+                }
+                if (deleteIdData.length > 0) await transaction.delete(this.tableName, { id: deleteIdData });
+                if (insertData.length > 0) await transaction.insert(this.tableName, insertData);
+                await transaction.commit();
+                return true;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+    }
+    return ContractAudit;
+};

+ 146 - 0
app/service/contract_pay.js

@@ -0,0 +1,146 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+const contractConst = require('../const/contract');
+
+module.exports = app => {
+
+    class ContractPay extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'contract_pay';
+            this.dataId = 'id';
+        }
+
+        async getPays(options, cid) {
+            const sql = 'SELECT * FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND `cid` = ? ORDER BY `create_time` DESC';
+            const sqlParams = [this.tableName, cid];
+            const list = await this.db.query(sql, sqlParams);
+            if (list.length > 0) {
+                const userList = await this.ctx.service.projectAccount.getAllDataByCondition({ where: { id: list.map(item => item.uid) } });
+                for (const l of list) {
+                    const userInfo = userList.find(item => item.id === l.uid);
+                    l.username = userInfo ? userInfo.name : '';
+                    l.files = await this.ctx.service.contractPayAtt.getAtt(l.id);
+                }
+            }
+            return list;
+        }
+
+        async add(options, cid, data) {
+            const node = await this.ctx.service.contract.getDataById(cid);
+            if (!node) {
+                throw '合同不存在';
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                const insertData = {
+                    spid: options.spid || null,
+                    tid: options.tid || null,
+                    contract_type: options.contract_type,
+                    cid,
+                    uid: this.ctx.session.sessionUser.accountId,
+                    pay_time: data.pay_time,
+                    pay_price: data.pay_price,
+                    debit_price: data.debit_price,
+                    yf_price: data.yf_price,
+                    sf_price: data.sf_price,
+                    pay_type: data.pay_type,
+                    remark: data.remark,
+                    create_time: new Date(),
+                };
+                await transaction.insert(this.tableName, insertData);
+                await this.calcContract(transaction, node);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return { pays: await this.getPays(options, cid), node: { update: node } };
+        }
+
+        async save(options, cid, data) {
+            if (!data.id) {
+                throw '参数有误';
+            }
+            const node = await this.ctx.service.contract.getDataById(cid);
+            if (!node) {
+                throw '合同不存在';
+            }
+            const cpInfo = await this.getDataById(data.id);
+            if (!cpInfo) {
+                throw '合同' + contractConst.typeName[cpInfo.contract_type] + '不存在';
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.update(this.tableName, data);
+                await this.calcContract(transaction, node);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return { pays: await this.getPays(options, cid), node: { update: node } };
+        }
+
+        async del(options, cid, cpid) {
+            if (!cpid) {
+                throw '参数有误';
+            }
+            const node = await this.ctx.service.contract.getDataById(cid);
+            if (!node) {
+                throw '合同不存在';
+            }
+            const cpInfo = await this.getDataById(cpid);
+            if (!cpInfo) {
+                throw '合同' + contractConst.typeName[cpInfo.contract_type] + '不存在';
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.delete(this.tableName, { id: cpid });
+                // 删除合同附件
+                const attList = await this.ctx.service.contractPayAtt.getAllDataByCondition({ where: { cpid } });
+                await this.ctx.helper.delFiles(attList);
+                await transaction.delete(this.ctx.service.contractPayAtt.tableName, { cpid });
+                await this.calcContract(transaction, node);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return { pays: await this.getPays(options, cid), node: { update: node } };
+        }
+
+        async calcContract(transaction, node) {
+            const paysList = await transaction.query('SELECT * FROM ?? WHERE `cid` = ?', [this.tableName, node.id]);
+            let pay_price = 0;
+            let debit_price = 0;
+            let yf_price = 0;
+            let sf_price = 0;
+            for (const l of paysList) {
+                pay_price = this.ctx.helper.add(pay_price, l.pay_price);
+                debit_price = this.ctx.helper.add(debit_price, l.debit_price);
+                yf_price = this.ctx.helper.add(yf_price, l.yf_price);
+                sf_price = this.ctx.helper.add(sf_price, l.sf_price);
+            }
+            node.pay_price = pay_price;
+            node.debit_price = debit_price;
+            node.yf_price = yf_price;
+            node.sf_price = sf_price;
+            node.exist_pay = paysList.length === 0 ? 0 : 1;
+            await transaction.update(this.ctx.service.contract.tableName, node);
+        }
+    }
+    return ContractPay;
+};

+ 61 - 0
app/service/contract_pay_att.js

@@ -0,0 +1,61 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+const contractConst = require('../const/contract');
+
+module.exports = app => {
+
+    class ContractPayAtt extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'contract_pay_attachment';
+            this.dataId = 'id';
+        }
+
+        async getAtt(cpid) {
+            const sql = 'SELECT a.*, b.name as username FROM ?? as a LEFT JOIN ?? as b ON a.`uid` = b.`id` WHERE a.`cpid` = ? ORDER BY `upload_time` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cpid];
+            const result = await this.db.query(sql, sqlParam);
+            return result.map(item => {
+                item.orginpath = this.ctx.app.config.fujianOssPath + item.filepath;
+                if (!this.ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `/contract/${item.spid || item.tid}/detail/${contractConst.typeMap[item.contract_type]}/${item.cid}/pay/${item.cpid}/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                    item.viewpath = item.filepath;
+                }
+                return item;
+            });
+        }
+
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+    }
+    return ContractPayAtt;
+};

+ 961 - 0
app/service/contract_tree.js

@@ -0,0 +1,961 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+const contractConst = require('../const/contract');
+const rootId = -1;
+
+module.exports = app => {
+
+    class ContractTree extends app.BaseBillsService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            const setting = {
+                spid: 'spid',
+                mid: 'tid',
+                type: 'contract_type',
+                kid: 'contract_id',
+                pid: 'contract_pid',
+                order: 'order',
+                level: 'level',
+                isLeaf: 'is_leaf',
+                fullPath: 'full_path',
+                keyPre: 'contract_maxLid:', // 换个名称,防止缓存导致旧数据出问题
+                uuid: true,
+            };
+            super(ctx, setting, 'pos');
+            this.setting = setting;
+            this.depart = 0;
+            this.tableName = 'contract_tree';
+        }
+
+        _getStringOptions(options) {
+            const optionStr = [];
+            for (const key in options) {
+                if (options.hasOwnProperty(key)) {
+                    optionStr.push(options[key]);
+                }
+            }
+            return optionStr.join('&&');
+        }
+
+        /**
+         * 获取最大节点id
+         *
+         * @param {Number} mid - master id
+         * @return {Number}
+         * @private
+         */
+        async _getMaxLid(options) {
+            const cacheKey = this.setting.keyPre + this._getStringOptions(options);
+            let maxId = parseInt(await this.cache.get(cacheKey)) || 0;
+            if (!maxId) {
+                const sql = 'SELECT Max(??) As max_id FROM ?? Where ' + this.ctx.helper._getOptionsSql(options);
+                const sqlParam = [this.setting.kid, this.tableName];
+                const queryResult = await this.db.queryOne(sql, sqlParam);
+                if (maxId < queryResult.max_id || 0) {
+                    maxId = queryResult.max_id || 0;
+                }
+                const sql1 = 'SELECT Max(??) As max_id FROM ?? Where ' + this.ctx.helper._getOptionsSql(options);
+                const sqlParam1 = [this.setting.kid, this.ctx.service.contract.tableName];
+                const queryResult1 = await this.db.queryOne(sql1, sqlParam1);
+                if (maxId < queryResult1.max_id || 0) {
+                    maxId = queryResult1.max_id || 0;
+                }
+                this.cache.set(cacheKey, maxId, 'EX', this.ctx.app.config.cacheTime);
+            }
+            return maxId;
+        }
+
+        _cacheMaxLid(options, maxId) {
+            const cacheKey = this.setting.keyPre + this._getStringOptions(options);
+            this.cache.set(cacheKey, maxId, 'EX', this.ctx.app.config.cacheTime);
+        }
+
+        /**
+         * 更新order
+         * @param {Number} mid - master id
+         * @param {Number} pid - 父节点id
+         * @param {Number} order - 开始更新的order
+         * @param {Number} incre - 更新的增量
+         * @returns {Promise<*>}
+         * @private
+         */
+        async _updateChildrenOrder(options, pid, order, incre = 1, transaction = null) {
+            const optionSql = this.ctx.helper._getOptionsSql(options);
+            const sql = 'UPDATE ?? SET `' + this.setting.order + '` = `' + this.setting.order + '` ' + (incre > 0 ? '+' : '-') + Math.abs(incre) + ' WHERE ' + optionSql + ' AND `' + this.setting.order + '` >= ? AND ' + this.setting.pid + ' = ?';
+            const sqlParam = [this.tableName, order, pid];
+            const data = transaction ? await transaction.query(sql, sqlParam) : await this.db.query(sql, sqlParam);
+
+            const sql1 = 'UPDATE ?? SET `' + this.setting.order + '` = `' + this.setting.order + '` ' + (incre > 0 ? '+' : '-') + Math.abs(incre) + ' WHERE ' + optionSql + ' AND `' + this.setting.order + '` >= ? AND ' + this.setting.pid + ' = ?';
+            const sqlParam1 = [this.ctx.service.contract.tableName, order, pid];
+            transaction ? await transaction.query(sql1, sqlParam1) : await this.db.query(sql1, sqlParam1);
+
+            return data;
+        }
+
+        _getOptionsSql(options) {
+            const optionSql = [];
+            for (const key in options) {
+                if (options.hasOwnProperty(key)) {
+                    optionSql.push(key + ' = ' + this.db.escape(options[key]));
+                }
+            }
+            return optionSql.join(' AND ');
+        }
+
+        async insertTree(options, subInfo) {
+            const hadTree = await this.getDataByCondition(options);
+            if (!hadTree) {
+                if (options.tid && !subInfo.spid) {
+                    throw '该标段未绑定项目';
+                }
+                const subProj = options.spid ? subInfo : await this.ctx.service.subProject.getDataById(subInfo.spid);
+                if (subProj.std_id === 0) {
+                    throw '该项目未绑定概预算标准';
+                }
+                const stdInfo = await this.ctx.service.budgetStd.getDataById(subProj.std_id);
+                if (!stdInfo) {
+                    throw '概预算标准不存在';
+                } else if (options.spid && !stdInfo.ht_project_template_id) {
+                    throw '概预算标准未绑定项目合同模板';
+                } else if (options.tid && !stdInfo.ht_tender_template_id) {
+                    throw '概预算标准未绑定标段合同模板';
+                }
+                const ht_template_id = options.spid ? stdInfo.ht_project_template_id.split(',')[0] : stdInfo.ht_tender_template_id.split(',')[0];
+                const ht_template_datas = await this.ctx.service.tenderNodeTemplate.getData(ht_template_id);
+                if (!ht_template_datas.length) throw '模板数据有误';
+                const expensesDatas = [];
+                const incomeDatas = [];
+                for (const t of ht_template_datas) {
+                    const insertData = {
+                        spid: options.spid || null,
+                        tid: options.tid || null,
+                        contract_id: t.template_id,
+                        contract_pid: t.pid,
+                        level: t.level,
+                        order: t.order,
+                        full_path: t.full_path,
+                        is_leaf: t.is_leaf,
+                        code: t.code,
+                        name: t.name,
+                        unit: t.unit,
+                    };
+                    const expensesData = this.ctx.helper._.cloneDeep(insertData);
+                    expensesData.id = this.uuid.v4();
+                    expensesData.contract_type = contractConst.type.expenses;
+                    expensesDatas.push(expensesData);
+                    const incomeData = this.ctx.helper._.cloneDeep(insertData);
+                    incomeData.id = this.uuid.v4();
+                    incomeData.contract_type = contractConst.type.income;
+                    incomeDatas.push(incomeData);
+                }
+                await this.db.insert(this.tableName, [...expensesDatas, ...incomeDatas]);
+            }
+        }
+
+        /**
+         * 提交数据 - 响应计算(增量方式计算)
+         * @param {Number} tenderId
+         * @param {Object} data
+         * @return {Promise<*>}
+         */
+        async updateCalc(options, data) {
+            const helper = this.ctx.helper;
+            if (!data) {
+                throw '提交数据错误';
+            }
+            const datas = data instanceof Array ? data : [data];
+            const ids = [];
+            for (const row of datas) {
+                ids.push(row.id);
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                const updateDatas = [];
+                for (const row of datas) {
+                    const updateNode = await this.getDataById(row.id);
+                    if (!updateNode) {
+                        throw '提交数据错误';
+                    }
+                    const updateData = this._filterUpdateInvalidField(updateNode.id, row);
+                    // 如非子节点,需要更新底下所有已选清单的分部分项等数据
+                    updateDatas.push(updateData);
+                }
+                if (updateDatas.length > 0) await transaction.updateRows(this.tableName, updateDatas);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            return { update: await this.getDataById(ids) };
+        }
+
+        async getDataByKid(options, kid) {
+            const condition = { ...options };
+            condition[this.setting.kid] = kid;
+            return await this.getDataByCondition(condition) || await this.ctx.service.contract.getDataByCondition(condition);
+        }
+
+        async getDataByKidAndCount(options, kid, count) {
+            if (kid <= 0) return [];
+            const select = await this.getDataByKid(options, kid);
+            if (!select) throw '数据错误';
+
+            if (count > 1) {
+                const selects = await this.getNextsData(options, select[this.setting.pid], select[this.setting.order] - 1);
+                if (selects.length < count) throw '数据错误';
+                return selects.slice(0, count);
+            } else {
+                return [select];
+            }
+        }
+
+        /**
+         * 根据 父节点id 和 节点排序order 获取数据
+         *
+         * @param {Number} mid - master id
+         * @param {Number} pid - 父节点id
+         * @param {Number|Array} order - 排序
+         * @return {Object|Array} - 查询结果
+         */
+        async getDataByParentAndOrder(options, pid, order) {
+            const condition = { ...options };
+            condition[this.setting.pid] = pid;
+            condition[this.setting.order] = order;
+            const result = await this.db.select(this.tableName, {
+                where: condition,
+            });
+            const result1 = await this.db.select(this.ctx.service.contract.tableName, {
+                where: condition,
+            });
+            // data和data1合并且按order排序
+            const resultData = result.concat(result1).sort((a, b) => a.order - b.order);
+            return order instanceof Array ? resultData : (resultData.length > 0 ? resultData[0] : null);
+        }
+
+        async addNodeBatch(options, kid, count = 1) {
+            if (!options[this.setting.type]) throw '参数有误';
+            const select = kid ? await this.getDataByKid(options, kid) : null;
+            if (kid && !select) throw '新增节点数据错误';
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 判断select的父节点是否是变更新增的,如果是则修改自己的表就行了,否则修改2个ledger,changeLedger表
+                if (select) await this._updateChildrenOrder(options, select[this.setting.pid], select[this.setting.order] + 1, count, transaction);
+                const newDatas = [];
+                const maxId = await this._getMaxLid(options);
+                for (let i = 1; i < count + 1; i++) {
+                    const newData = [];
+                    if (this.setting.uuid) newData.id = this.uuid.v4();
+                    newData[this.setting.kid] = maxId + i;
+                    newData[this.setting.pid] = select ? select[this.setting.pid] : rootId;
+                    newData[this.setting.spid] = options.spid || null;
+                    newData[this.setting.type] = options[this.setting.type];
+                    newData[this.setting.mid] = options.tid || null;
+                    newData[this.setting.level] = select ? select[this.setting.level] : 1;
+                    newData[this.setting.order] = select ? select[this.setting.order] + i : i;
+                    newData[this.setting.fullPath] = newData[this.setting.level] > 1
+                        ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + newData[this.setting.kid])
+                        : newData[this.setting.kid] + '';
+                    newData[this.setting.isLeaf] = true;
+                    newDatas.push(newData);
+                }
+                const insertResult = await transaction.insert(this.tableName, newDatas);
+                this._cacheMaxLid(options, maxId + count);
+
+                if (insertResult.affectedRows !== count) throw '新增节点数据错误';
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            if (select) {
+                let createData = await this.getChildBetween(options, select[this.setting.pid], select[this.setting.order], select[this.setting.order] + count + 1);
+                let updateData = await this.getNextsData(options, select[this.setting.pid], select[this.setting.order] + count);
+                return { create: createData, update: updateData };
+            } else {
+                const createData = await this.getChildBetween(options, -1, 0, count + 1);
+                return { create: createData };
+            }
+        }
+
+        /**
+         *  tenderId标段中, 删除选中节点及其子节点
+         *
+         * @param {Number} tenderId - 标段id
+         * @param {Number} selectId - 选中节点id
+         * @return {Array} - 被删除的数据
+         */
+        async deleteNode(options, kid) {
+            if (kid <= 0) return [];
+            const select = await this.getDataByKid(options, kid);
+            if (!select) throw '删除节点数据错误';
+            const parent = await this.getDataByKid(options, select[this.setting.pid]);
+            // 获取将要被删除的数据
+            const deleteData = await this.getDataByFullPath(options, select[this.setting.fullPath] + '-%');
+            deleteData.unshift(select);
+            if (deleteData.length === 0) throw '删除节点数据错误';
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 删除
+                if (select.c_code) {
+                    if (select.uid !== this.ctx.session.sessionUser.accountId && !this.ctx.session.sessionUser.is_admin) throw '当前合同无权删除';
+                    const contractPays = await this.ctx.service.contractPay.getDataByCondition({ cid: select.id });
+                    if (contractPays) throw '还存在合同支付项,无法删除';
+                    await transaction.delete(this.ctx.service.contract.tableName, { id: select.id });
+                    const attList = await this.ctx.service.contractAtt.getAllDataByCondition({ where: { cid: select.id } });
+                    await this.ctx.helper.delFiles(attList);
+                    await transaction.delete(this.ctx.service.contractAtt.tableName, { cid: select.id });
+                } else {
+                    await transaction.delete(this.tableName, { id: select.id });
+                    const delOptions = this._.cloneDeep(options);
+                    delOptions.contract_id = this._.map(deleteData, 'contract_id');
+                    await transaction.delete(this.ctx.service.contractTreeAudit.tableName, delOptions);
+                    const contracts = this.ctx.helper._.filter(deleteData, function (item) {
+                        return item.c_code;
+                    });
+                    if (contracts.length > 0) {
+                        const contractUids = this.ctx.helper._.uniq(this.ctx.helper._.map(contracts, 'uid'));
+                        if (contractUids.length > 1 || !(contractUids[0] === this.ctx.session.sessionUser.accountId || this.ctx.session.sessionUser.is_admin)) throw '存在合同你无权删除';
+                        const contractPays = await transaction.select(this.ctx.service.contractPay.tableName, { where: { cid: this.ctx.helper._.map(contracts, 'id') } });
+                        if (contractPays.length > 0) throw '还存在合同支付项,无法删除';
+                        const attList = await this.ctx.service.contractAtt.getAllDataByCondition({ where: { cid: this.ctx.helper._.map(contracts, 'id') } });
+                        await this.ctx.helper.delFiles(attList);
+                        await transaction.delete(this.ctx.service.contractAtt.tableName, { cid: this.ctx.helper._.map(contracts, 'id') });
+                    }
+                    const operate = await this._deletePosterity(options, select, transaction);
+                }
+                // 选中节点--父节点 只有一个子节点时,应升级isLeaf
+                if (parent) {
+                    const condition = { ...options };
+                    condition[this.setting.pid] = select[this.setting.pid];
+                    const count = await this.db.count(this.tableName, condition);
+                    const count1 = await this.db.count(this.ctx.service.contract.tableName, condition);
+                    const sum = count + count1;
+                    if (sum === 1) {
+                        const updateParent = {id: parent.id };
+                        updateParent[this.setting.isLeaf] = true;
+                        await transaction.update(this.tableName, updateParent);
+                    }
+                }
+                // 选中节点--全部后节点 order--
+                await this._updateChildrenOrder(options, select[this.setting.pid], select[this.setting.order] + 1, -1, transaction);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            // 查询结果
+            const updateData = await this.getNextsData(options, select[this.setting.pid], select[this.setting.order] - 1);
+            if (parent) {
+                const updateData1 = await this.getDataByKid(options, select[this.setting.pid]);
+                if (updateData1[this.setting.isLeaf]) {
+                    updateData.push(updateData1);
+                }
+            }
+            return { delete: deleteData, update: updateData };
+        }
+
+        async deleteNodes(options, kid, count) {
+            const _ = this.ctx.helper._;
+            if ((kid <= 0) || (count <= 0)) return [];
+            const selects = await this.getDataByKidAndCount(options, kid, count);
+            const first = selects[0];
+            const parent = await this.getDataByKid(options, first[this.setting.pid]);
+            const condition = { ...options };
+            condition[this.setting.pid] = parent[this.setting.kid];
+            const childCount1 = parent ? await this.count(condition) : -1;
+            const childCount2 = parent ? await this.db.count(this.ctx.service.contract.tableName, condition) : -1;
+            const childCount = childCount1 + childCount2;
+            let deleteData = [];
+            for (const s of selects) {
+                deleteData = deleteData.concat(await this.getDataByFullPath(options, s[this.setting.fullPath] + '-%'));
+                deleteData.push(s);
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 删除
+                for (const s of selects) {
+                    if (s.c_code) {
+                        if (s.uid !== this.ctx.session.sessionUser.accountId && !this.ctx.session.sessionUser.is_admin) throw '存在合同你无权删除';
+                        const contractPays = await this.ctx.service.contractPay.getDataByCondition({ cid: s.id });
+                        if (contractPays) throw '部分合同还存在合同支付项,无法删除';
+                        await transaction.delete(this.ctx.service.contract.tableName, { id: s.id });
+                        const attList = await this.ctx.service.contractAtt.getAllDataByCondition({ where: { cid: s.id } });
+                        await this.ctx.helper.delFiles(attList);
+                        await transaction.delete(this.ctx.service.contractAtt.tableName, { cid: s.id });
+                    } else {
+                        await transaction.delete(this.tableName, { id: s.id });
+                        const contracts = _.filter(deleteData, function (item) {
+                            return item.c_code && _.includes(s.full_path, item.full_path);
+                        });
+                        if (contracts.length > 0) {
+                            const contractUids = _.uniq(_.map(contracts, 'uid'));
+                            if (contractUids.length > 1 || !(contractUids[0] === this.ctx.session.sessionUser.accountId || this.ctx.session.sessionUser.is_admin)) throw '存在合同你无权删除';
+                            const contractPays = await transaction.select(this.ctx.service.contractPay.tableName, { where: { cid: _.map(contracts, 'id') } });
+                            if (contractPays.length > 0) throw '还存在合同支付项,无法删除';
+                            const attList = await this.ctx.service.contractAtt.getAllDataByCondition({ where: { cid: _.map(contracts, 'id') } });
+                            await this.ctx.helper.delFiles(attList);
+                            await transaction.delete(this.ctx.service.contractAtt.tableName, { cid: _.map(contracts, 'id') });
+                        }
+                    }
+                    const operate = await this._deletePosterity(options, s, transaction);
+                }
+                // 选中节点--父节点 只有一个子节点时,应升级isLeaf
+                if (parent && childCount === count) {
+                    const updateParent = {id: parent.id };
+                    updateParent[this.setting.isLeaf] = true;
+                    await transaction.update(this.tableName, updateParent);
+                }
+                // 选中节点--全部后节点 order--
+                await this._updateChildrenOrder(options, first[this.setting.pid], first[this.setting.order] + count, -count, transaction);
+                const delOptions = this._.cloneDeep(options);
+                delOptions.contract_id = this._.map(deleteData, 'contract_id');
+                await transaction.delete(this.ctx.service.contractTreeAudit.tableName, delOptions);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            const updateData = await this.getNextsData(options, first[this.setting.pid], first[this.setting.order] - 1);
+            if (parent && childCount === count) {
+                const updateData1 = await this.getDataByKid(options, parent[this.setting.kid]);
+                updateData.push(updateData1);
+            }
+            return { delete: deleteData, update: updateData };
+        }
+
+        async delete(options, kid, count = null) {
+            if (count && count > 1) {
+                return await this.deleteNodes(options, kid, count);
+            } else {
+                return await this.deleteNode(options, kid);
+            }
+        }
+
+        /**
+         * 上移节点
+         *
+         * @param {Number} mid - master id
+         * @param {Number} kid - 选中节点id
+         * @return {Array} - 发生改变的数据
+         */
+        async upMoveNode(options, kid, count) {
+            if (!count) count = 1;
+            if (!kid || (kid <= 0)) return null;
+            const selects = await this.getDataByKidAndCount(options, kid, count);
+            if (selects.length !== count) throw '上移节点数据错误';
+            const first = selects[0];
+            const pre = await this.getDataByParentAndOrder(options, first[this.setting.pid], first[this.setting.order] - 1);
+            if (!pre) throw '节点不可上移';
+
+            const order = [];
+            const transaction = await this.db.beginTransaction();
+            try {
+                for (const s of selects) {
+                    const sData = { id: s.id };
+                    sData[this.setting.order] = s[this.setting.order] - 1;
+                    !s.c_code ? await transaction.update(this.tableName, sData) : await transaction.update(this.ctx.service.contract.tableName, sData);
+                    order.push(s[this.setting.order] - 1);
+                }
+                const pData = { id: pre.id };
+                pData[this.setting.order] = pre[this.setting.order] + count;
+                !pre.c_code ? await transaction.update(this.tableName, pData) : await transaction.update(this.ctx.service.contract.tableName, pData);
+                order.push(pre[this.setting.order] + count);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            const resultData = await this.getDataByParentAndOrder(options, first[this.setting.pid], order);
+            return { update: resultData };
+        }
+
+        /**
+         * 下移节点
+         *
+         * @param {Number} mid - master id
+         * @param {Number} kid - 选中节点id
+         * @return {Array} - 发生改变的数据
+         */
+        async downMoveNode(options, kid, count) {
+            if (!count) count = 1;
+            if (!kid || (kid <= 0)) return null;
+            const selects = await this.getDataByKidAndCount(options, kid, count);
+            if (selects.length !== count) {
+                throw '下移节点数据错误';
+            }
+            const last = selects[count - 1];
+            const next = await this.getDataByParentAndOrder(options, last[this.setting.pid], last[this.setting.order] + 1);
+            if (!next) {
+                throw '节点不可下移';
+            }
+
+            const order = [];
+            const transaction = await this.db.beginTransaction();
+            try {
+                for (const s of selects) {
+                    const sData = { id: s.id };
+                    sData[this.setting.order] = s[this.setting.order] + 1;
+                    !s.c_code ? await transaction.update(this.tableName, sData) : await transaction.update(this.ctx.service.contract.tableName, sData);
+                    order.push(s[this.setting.order] + 1);
+                }
+                const nData = { id: next.id };
+                nData[this.setting.order] = next[this.setting.order] - count;
+                !next.c_code ? await transaction.update(this.tableName, nData) : await transaction.update(this.ctx.service.contract.tableName, nData);
+                order.push(next[this.setting.order] - count);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            const resultData = await this.getDataByParentAndOrder(options, last[this.setting.pid], order);
+            return { update: resultData };
+        }
+
+        /**
+         * 升级节点
+         *
+         * @param {Number} tenderId - 标段id
+         * @param {Number} selectId - 选中节点id
+         * @return {Array} - 发生改变的数据
+         */
+        async upLevelNode(options, kid, count) {
+            if (!count) count = 1;
+            const selects = await this.getDataByKidAndCount(options, kid, count);
+
+            if (selects.length !== count) throw '升级节点数据错误';
+            const first = selects[0], last = selects[count - 1];
+            const parent = await this.getDataByKid(options, first[this.setting.pid]);
+            if (!parent) throw '升级节点数据错误';
+
+            const newPath = [];
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 选中节点--父节点 选中节点为firstChild时,修改isLeaf
+                if (first[this.setting.order] === 1) {
+                    const updateParentData = { id: parent.id };
+                    updateParentData[this.setting.isLeaf] = true;
+                    await transaction.update(this.tableName, updateParentData);
+                }
+                // 选中节点--父节点--全部后兄弟节点 order+1
+                await this._updateChildrenOrder(options, parent[this.setting.pid], parent[this.setting.order] + 1, count, transaction);
+                for (const [i, s] of selects.entries()) {
+                    // 选中节点 修改pid, order, fullPath, level, isLeaf, 清空计算项
+                    const updateData = { id: s.id };
+                    updateData[this.setting.pid] = parent[this.setting.pid];
+                    updateData[this.setting.order] = parent[this.setting.order] + i + 1;
+                    updateData[this.setting.level] = s[this.setting.level] - 1;
+                    updateData[this.setting.fullPath] = s[this.setting.fullPath].replace(`-${s[this.setting.pid]}-`, '-');
+                    newPath.push(updateData[this.setting.fullPath]);
+                    if (s[this.setting.isLeaf] && s.id === last.id) {
+                        const nexts = await this.getNextsData(options, parent[this.setting.kid], last[this.setting.order]);
+                        if (nexts.length > 0) {
+                            updateData[this.setting.isLeaf] = false;
+                        }
+                    }
+                    await transaction.update(this.tableName, updateData);
+                    // 选中节点--全部子节点(含孙) level-1, fullPath变更
+                    await this._syncUplevelChildren(options, s, transaction);
+                }
+                // 选中节点--全部后兄弟节点 收编为子节点 修改pid, order, fullPath
+                await this._syncUpLevelNexts(options, last, transaction);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            // 查询修改的数据
+            let updateData = await this.getNextsData(options, parent[this.setting.pid], parent[this.setting.order] - 1);
+            for (const path of newPath) {
+                const children = await this.getDataByFullPath(options, path + '-%');
+                updateData = updateData.concat(children);
+            }
+            return { update: updateData };
+        }
+
+        /**
+         * 降级节点
+         *
+         * @param {Number} tenderId - 标段id
+         * @param {Number} selectId - 选中节点id
+         * @return {Array} - 发生改变的数据
+         */
+        async downLevelNode(options, kid, count) {
+            if (!count) count = 1;
+            const selects = await this.getDataByKidAndCount(options, kid, count);
+            if (!selects) throw '降级节点数据错误';
+            const first = selects[0], last = selects[count - 1];
+            const pre = await this.getDataByParentAndOrder(options, first[this.setting.pid], first[this.setting.order] - 1);
+            if (!pre) throw '节点不可降级';
+            const preLastChild = await this.getLastChildData(options, pre[this.setting.kid]);
+
+            const newPath = [];
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 选中节点--全部后节点 order--
+                await this._updateChildrenOrder(options, first[this.setting.pid], last[this.setting.order] + 1, -count, transaction);
+
+                for (const [i, s] of selects.entries()) {
+                    // 选中节点 修改pid, level, order, fullPath
+                    const updateData = { id: s.id };
+                    updateData[this.setting.pid] = pre[this.setting.kid];
+                    updateData[this.setting.order] = preLastChild ? preLastChild[this.setting.order] + i + 1 : i + 1;
+                    updateData[this.setting.level] = s[this.setting.level] + 1;
+                    if (s[this.setting.level] === 1) {
+                        updateData[this.setting.fullPath] = pre[this.setting.kid] + '-' + s[this.setting.kid];
+                    } else {
+                        const index = s[this.setting.fullPath].lastIndexOf(s[this.setting.kid]);
+                        updateData[this.setting.fullPath] = s[this.setting.fullPath].substring(0, index-1) + '-' + pre[this.setting.kid] + '-' + s[this.setting.kid];
+                    }
+                    newPath.push(updateData[this.setting.fullPath]);
+                    await transaction.update(this.tableName, updateData);
+                    // 选中节点--全部子节点(含孙) level++, fullPath
+                    await this._syncDownlevelChildren(options, s, updateData[this.setting.fullPath], transaction);
+                }
+                // 选中节点--前兄弟节点 isLeaf应为false, 清空计算相关字段
+                const updateData2 = { id: pre.id };
+                updateData2[this.setting.isLeaf] = false;
+                await transaction.update(this.tableName, updateData2);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            // 查询修改的数据
+            let updateData = await this.getNextsData(options, pre[this.setting.pid], pre[this.setting.order] - 1);
+            // 选中节点及子节点
+            for (const p of newPath) {
+                updateData = updateData.concat(await this.getDataByFullPath(options, p + '-%'));
+            }
+            updateData = updateData.concat(await this.getDataById(selects.map(x => { return x.id; })));
+            // 选中节点--原前兄弟节点&全部后兄弟节点
+            return { update: updateData };
+        }
+
+        async pasteBlockData(options, kid, pasteData, defaultData) {
+            if (!options[this.setting.type]) throw '参数有误';
+            if (!pasteData || pasteData.length <= 0) throw '复制数据错误';
+            for (const pd of pasteData) {
+                if (!pd || pd.length <= 0) throw '复制数据错误';
+                pd.sort(function (x, y) {
+                    return x.level - y.level
+                });
+                if (pd[0].contract_pid !== pasteData[0][0].contract_pid) throw '复制数据错误:仅可操作同层节点';
+            }
+            const selectData = await this.getDataByKid(options, kid);
+            if (!selectData) throw '粘贴数据错误';
+            const newParentPath = selectData.full_path.replace(selectData.contract_id, '');
+
+            const pasteBillsData = [];
+            let maxId = await this._getMaxLid(options);
+            for (const [i, pd] of pasteData.entries()) {
+                for (const d of pd) {
+                    d.children = pd.filter(function (x) {
+                        return x.contract_pid === d.contract_id;
+                    });
+                }
+                const pbd = [];
+                for (const [j, d] of pd.entries()) {
+                    const newBills = {
+                        id: this.uuid.v4(),
+                        spid: options.spid || null,
+                        tid: options.tid || null,
+                        contract_type: options.contract_type,
+                        contract_id: maxId + j + 1,
+                        contract_pid: j === 0 ? selectData.contract_pid : d.contract_pid,
+                        level: d.level + selectData.level - pd[0].level,
+                        order: j === 0 ? selectData.order + i + 1 : d.order,
+                        is_leaf: d.is_leaf,
+                        code: d.code,
+                        name: d.name,
+                        remark: d.remark,
+                    };
+                    for (const c of d.children) {
+                        c.contract_pid = newBills.contract_id;
+                    }
+                    pbd.push(newBills);
+                }
+                for (const d of pbd) {
+                    const parent = pbd.find(function (x) {
+                        return x.contract_id === d.contract_pid;
+                    });
+                    d.full_path = parent
+                        ? parent.full_path + '-' + d.contract_id
+                        : newParentPath + d.contract_id;
+                    if (defaultData) this.ctx.helper._.assignIn(pbd, defaultData);
+                    pasteBillsData.push(d);
+                }
+                maxId = maxId + pbd.length;
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 选中节点的所有后兄弟节点,order+粘贴节点个数
+                await this._updateChildrenOrder(options, selectData.ledger_pid, selectData.order + 1, pasteData.length, transaction);
+                // 数据库创建新增节点数据
+                if (pasteBillsData.length > 0) {
+                    const newData = await transaction.insert(this.tableName, pasteBillsData);
+                }
+                this._cacheMaxLid(options, maxId);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+
+            // 查询应返回的结果
+            const updateData = await this.getNextsData(options, selectData.contract_pid, selectData.order + pasteData.length);
+            return {
+                ledger: { create: pasteBillsData, update: updateData },
+            };
+        }
+
+        /**
+         * 删除节点
+         * @param {Number} tenderId - 标段id
+         * @param {Object} deleteData - 删除节点数据
+         * @return {Promise<*>}
+         * @private
+         */
+        async _deletePosterity(options, node, transaction = null) {
+            const sql = 'DELETE FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.fullPath + ' LIKE ?';
+            const sqlParam = [this.tableName, node[this.setting.fullPath] + '-%'];
+            const result = transaction ? await transaction.query(sql, sqlParam) : await this.db.query(sql, sqlParam)
+
+            const sql1 = 'DELETE FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.fullPath + ' LIKE ?';
+            const sqlParam1 = [this.ctx.service.contract.tableName, node[this.setting.fullPath] + '-%'];
+            const result1 = transaction ? await transaction.query(sql1, sqlParam1) : await this.db.query(sql1, sqlParam1)
+
+            return result;
+        }
+
+        /**
+         * 根据fullPath获取数据 fullPath Like ‘1.2.3%’(传参fullPath = '1.2.3%')
+         * @param {Number} tenderId - 标段id
+         * @param {String} fullPath - 路径
+         * @return {Promise<void>}
+         */
+        async getDataByFullPath(options, fullPath) {
+            const sql = 'SELECT * FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.fullPath + ' LIKE ?';
+            const sqlParam = [this.tableName, fullPath];
+            const resultData = await this.db.query(sql, sqlParam);
+
+            const sql1 = 'SELECT * FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.fullPath + ' LIKE ?';
+            const sqlParam1 = [this.ctx.service.contract.tableName, fullPath];
+            const resultData1 = await this.db.query(sql1, sqlParam1);
+            return resultData.concat(resultData1).sort((a, b) => a.order - b.order);
+        }
+
+        async getChildBetween(options, pid, order1, order2) {
+            const sql = 'SELECT * FROM ?? WHERE '+ this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ? AND `order` > ? AND `order` < ? ORDER BY `order` ASC';
+            const sqlParam = [this.tableName, pid, order1, order2];
+            const data = await this.db.query(sql, sqlParam);
+
+            const sql1 = 'SELECT * FROM ?? WHERE '+ this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ? AND `order` > ? AND `order` < ? ORDER BY `order` ASC';
+            const sqlParam1 = [this.ctx.service.contract.tableName, pid, order1, order2];
+            const data1 = await this.db.query(sql1, sqlParam1);
+
+            const resultData = data.concat(data1).sort((a, b) => a.order - b.order);
+            return resultData;
+        }
+
+        /**
+         * 根据 父节点ID 和 节点排序order 获取全部后节点数据
+         * @param {Number} mid - master id
+         * @param {Number} pid - 父节点id
+         * @param {Number} order - 排序
+         * @return {Array}
+         */
+        async getNextsData(options, pid, order) {
+            const sql = 'SELECT * FROM ?? WHERE '+ this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ? AND `order` > ? ORDER BY `order` ASC';
+            const sqlParam = [this.tableName, pid, order];
+            const data = await this.db.query(sql, sqlParam);
+
+            const sql1 = 'SELECT * FROM ?? WHERE '+ this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ? AND `order` > ? ORDER BY `order` ASC';
+            const sqlParam1 = [this.ctx.service.contract.tableName, pid, order];
+            const data1 = await this.db.query(sql1, sqlParam1);
+            // data和data1合并且按order排序
+            const resultData = data.concat(data1).sort((a, b) => a.order - b.order);
+            return resultData;
+        }
+
+        /**
+         * 获取最末的子节点
+         * @param {Number} mid - masterId
+         * @param {Number} pid - 父节点id
+         * @return {Object}
+         */
+        async getLastChildData(options, pid, transaction = null) {
+            const sql = 'SELECT * FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ? ORDER BY `order` DESC';
+            const sqlParam = [this.tableName, pid];
+            const resultData = await this.db.queryOne(sql, sqlParam);
+
+            const sql1 = 'SELECT * FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ? ORDER BY `order` DESC';
+            const sqlParam1 = [this.ctx.service.contract.tableName, pid];
+            const resultData1 = await this.db.queryOne(sql1, sqlParam1);
+            // 比较两个结果,返回order大的
+            if (resultData && resultData1) {
+                return resultData.order > resultData1.order ? resultData : resultData1;
+            } else {
+                return resultData || resultData1;
+            }
+        }
+
+        /**
+         * 选中节点的后兄弟节点,全部变为当前节点的子节点
+         * @param {Object} selectData - 选中节点
+         * @return {Object}
+         * @private
+         */
+        async _syncUpLevelNexts(options, select, transaction = null) {
+            // 查询selectData的lastChild
+            const lastChild = await this.getLastChildData(options, select[this.setting.kid]);
+            const nexts = await this.getNextsData(options, select[this.setting.pid], select[this.setting.order]);
+            if (nexts && nexts.length > 0) {
+                // 修改nextsData pid, 排序
+                // this.initSqlBuilder();
+                // this.sqlBuilder.setUpdateData(this.setting.pid, {
+                //     value: select[this.setting.kid],
+                // });
+                // const orderInc = lastChild ? lastChild[this.setting.order] - select[this.setting.order] : - select[this.setting.order];
+                // this.sqlBuilder.setUpdateData(this.setting.order, {
+                //     value: Math.abs(orderInc),
+                //     selfOperate: orderInc > 0 ? '+' : '-',
+                // });
+                // this.sqlBuilder.setAndWhere(this.setting.mid, {
+                //     value: select[this.setting.mid],
+                //     operate: '=',
+                // });
+                // this.sqlBuilder.setAndWhere(this.setting.pid, {
+                //     value: select[this.setting.pid],
+                //     operate: '=',
+                // });
+                // this.sqlBuilder.setAndWhere(this.setting.order, {
+                //     value: select[this.setting.order],
+                //     operate: '>',
+                // });
+                // const [sql1, sqlParam1] = this.sqlBuilder.build(this.tableName, 'update');
+                const orderInc = lastChild ? lastChild[this.setting.order] - select[this.setting.order] : - select[this.setting.order];
+                const sql1 = 'UPDATE ?? SET `' + this.setting.order + '` = ' + (orderInc > 0 ? '+' : '-') + Math.abs(orderInc) + ' WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.pid + ' = ? AND `' + this.setting.order + '` > ?';
+                const sqlParam1 = [this.tableName, select[this.setting.kid], select[this.setting.pid], select[this.setting.order]];
+                transaction ? await transaction.query(sql1, sqlParam1) : await this.db.query(sql1, sqlParam1);
+
+                // 选中节点 isLeaf应为false
+                if (select[this.setting.isLeaf]) {
+                    const updateData = { id: select.id };
+                    updateData[this.setting.isLeaf] = false;
+                    transaction ? await transaction.update(this.tableName, updateData) : await this.db.update(this.tableName, updateData);
+                }
+
+                // 修改nextsData及其子节点的fullPath
+                const oldSubStr = this.db.escape(select[this.setting.pid] + '-');
+                const newSubStr = this.db.escape(select[this.setting.kid] + '-');
+                const sqlArr = [];
+                sqlArr.push('Update ?? SET `' + this.setting.fullPath + '` = Replace(`' + this.setting.fullPath + '`,' + oldSubStr + ',' + newSubStr + ') Where');
+                sqlArr.push('(`' + this.ctx.helper._getOptionsSql(options) + ')');
+                sqlArr.push(' And (');
+                for (const data of nexts) {
+                    sqlArr.push('`' + this.setting.fullPath + '` Like ' + this.db.escape(data[this.setting.fullPath] + '%'));
+                    if (nexts.indexOf(data) < nexts.length - 1) {
+                        sqlArr.push(' Or ');
+                    }
+                }
+                sqlArr.push(')');
+                const sql = sqlArr.join('');
+                const resultData = transaction ? await transaction.query(sql, [this.tableName]) : await this.db.query(sql, [this.tableName]);
+                return resultData;
+            }
+        }
+
+        /**
+         * 升级selectData, 同步修改所有子节点
+         * @param {Object} selectData - 升级操作,选中节点
+         * @return {Object}
+         * @private
+         */
+        async _syncUplevelChildren(options, select, transaction = null) {
+            // this.initSqlBuilder();
+            // this.sqlBuilder.setAndWhere(this.setting.mid, {
+            //     value: select[this.setting.mid],
+            //     operate: '=',
+            // });
+            // this.sqlBuilder.setAndWhere(this.setting.fullPath, {
+            //     value: this.db.escape(select[this.setting.fullPath] + '-%'),
+            //     operate: 'like',
+            // });
+            // this.sqlBuilder.setUpdateData(this.setting.level, {
+            //     value: 1,
+            //     selfOperate: '-',
+            // });
+            // this.sqlBuilder.setUpdateData(this.setting.fullPath, {
+            //     value: [this.setting.fullPath, this.db.escape(`-${select[this.setting.pid]}-`), this.db.escape('-')],
+            //     literal: 'Replace',
+            // });
+            // const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
+            const sql = 'UPDATE ?? SET ' + this.setting.level + ' = ' + this.setting.level + ' -1, ' + this.setting.fullPath + ' = ' +
+                'Replace('+ [this.setting.fullPath, this.db.escape(`-${select[this.setting.pid]}-`), this.db.escape('-')].join(',') +') ' +
+                'WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.fullPath + ' LIKE ?';
+            const sqlParam = [this.tableName, select[this.setting.fullPath] + '-%'];
+            const data = transaction ? await transaction.query(sql, sqlParam) : await this.db.query(sql, sqlParam);
+
+            return data;
+        }
+
+        /**
+         * 降级selectData, 同步修改所有子节点
+         * @param {Object} selectData - 选中节点
+         * @param {Object} preData - 选中节点的前一节点(降级后为父节点)
+         * @return {Promise<*>}
+         * @private
+         */
+        async _syncDownlevelChildren(options, select, newFullPath, transaction = null) {
+            // this.initSqlBuilder();
+            // this.sqlBuilder.setAndWhere(this.setting.mid, {
+            //     value: select[this.setting.mid],
+            //     operate: '=',
+            // });
+            // this.sqlBuilder.setAndWhere(this.setting.fullPath, {
+            //     value: this.db.escape(select[this.setting.fullPath] + '-%'),
+            //     operate: 'like',
+            // });
+            // this.sqlBuilder.setUpdateData(this.setting.level, {
+            //     value: 1,
+            //     selfOperate: '+',
+            // });
+            // this.sqlBuilder.setUpdateData(this.setting.fullPath, {
+            //     value: [this.setting.fullPath, this.db.escape(select[this.setting.fullPath] + '-'), this.db.escape(newFullPath + '-')],
+            //     literal: 'Replace',
+            // });
+            // const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
+            const sql = 'UPDATE ?? SET ' + this.setting.level + ' = ' + this.setting.level + ' + 1, ' + this.setting.fullPath + ' = ' +
+                'Replace(' + [[this.setting.fullPath, this.db.escape(select[this.setting.fullPath] + '-'), this.db.escape(newFullPath + '-')]].join(',') + ') ' +
+                'WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND ' + this.setting.fullPath + ' LIKE ?';
+            const sqlParam = [this.tableName, select[this.setting.fullPath] + '-%'];
+            const data = transaction ? await transaction.query(sql, sqlParam) : await this.db.query(sql, sqlParam);
+
+            return data;
+        }
+    }
+    return ContractTree;
+};

+ 74 - 0
app/service/contract_tree_audit.js

@@ -0,0 +1,74 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class ContractTreeAudit extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'contract_tree_audit';
+            this.dataId = 'id';
+        }
+
+        async add(options, select, uid) {
+            if (!options.contract_type || !select.contract_id || !uid) {
+                throw '参数有误';
+            }
+            const sql = 'SELECT * FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND contract_id = ? AND uid = ?';
+            const sqlParam = [this.tableName, select.contract_id, uid];
+            const resultData = await this.db.queryOne(sql, sqlParam);
+            if (resultData) {
+                throw '该节点下已经存在该用户,请勿重复添加';
+            }
+            const data = {
+                spid: options.spid || null,
+                tid: options.tid || null,
+                contract_type: options.contract_type,
+                contract_id: select.contract_id,
+                uid: uid,
+                create_time: new Date(),
+            };
+            const result = await this.db.insert(this.tableName, data);
+            return {
+                id: result.insertId,
+                ...data,
+            }
+        }
+
+        async dels(options, select, uid) {
+            if (!options.contract_type || !select.contract_id) {
+                throw '参数有误';
+            }
+            const uids = Array.isArray(uid) ? uid : [uid];
+            const sql = 'SELECT * FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND contract_id = ? AND uid in (' + this.ctx.helper.getInArrStrSqlFilter(uids) + ')';
+            const sqlParam = [this.tableName, select.contract_id];
+            const resultData = await this.db.query(sql, sqlParam);
+            if (resultData.length === 0 || resultData.length !== uids.length) {
+                throw '该节点下已不存在该用户';
+            }
+            // for (const u of uids) {
+            //     const sql = 'SELECT * FROM ?? WHERE ' + this.ctx.helper._getOptionsSql(options) + ' AND contract_id = ? AND uid = ?';
+            //     const sqlParam = [this.ctx.service.contract.tableName, select.contract_id, u];
+            //     const resultData = await this.db.queryOne(sql, sqlParam);
+            //     if (resultData) {
+            //         throw '该用户已添加过合同,不可删除';
+            //     }
+            // }
+            const result = await this.db.delete(this.tableName, { id: this._.map(resultData, 'id') });
+            return await this.getAllDataByCondition({ where: options });
+        }
+    }
+    return ContractTreeAudit;
+};

+ 16 - 1
app/service/sub_project.js

@@ -463,7 +463,22 @@ module.exports = app => {
                 throw error;
             }
         }
+
+        // 合同管理获取项目列表
+        async getSubProjectByContract(pid, uid, admin, filterFolder = false) {
+            let result = await this.getAllDataByCondition({ where: { project_id: pid, is_delete: 0 } });
+            if (admin) return this._filterEmptyFolder(result);
+
+            const permission = await this.ctx.service.contractAudit.getAllDataByCondition({ where: { uid } });
+            result = result.filter(x => {
+                if (x.is_folder) return !filterFolder;
+                const pb = permission.find(y => { return x.id === y.spid; });
+                if (!pb) return false;
+                return true;
+            });
+            return this._filterEmptyFolder(result);
+        }
     }
 
     return SubProject;
-};
+};

+ 47 - 0
app/service/tender.js

@@ -608,6 +608,53 @@ module.exports = app => {
 
             return list;
         }
+
+        /**
+         * 获取你所参与的合同标段的列表
+         *
+         * @param {String} listStatus - 取列表状态,如果是管理页要传
+         * @param {String} permission - 根据权限取值
+         * @param {Number} getAll - 是否取所有标段
+         * @return {Array} - 返回标段数据
+         */
+        async getContractList(listStatus = '', permission = null, getAll = 0) {
+            // 获取当前项目信息
+            const session = this.ctx.session;
+            let sql = '';
+            let sqlParam = [];
+            if (getAll === 1) {
+                // 具有查看所有标段权限的用户查阅标段
+                sql = 'SELECT t.`id`, t.`project_id`, t.`name`, t.`status`, t.`category`, t.`user_id`, t.`create_time`,' +
+                    '    pa.`name` As `user_name`, pa.`role` As `user_role`, pa.`company` As `user_company` ' +
+                    '  FROM ?? As t ' +
+                    '  Left Join ?? As pa ' +
+                    '  ON t.`user_id` = pa.`id` ' +
+                    '  WHERE t.`project_id` = ? ORDER BY CONVERT(t.`name` USING GBK) ASC';
+                sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, session.sessionProject.id];
+            } else {
+                // 根据用户权限查阅标段
+                sql = 'SELECT t.`id`, t.`project_id`, t.`name`, t.`status`, t.`category`, t.`user_id`, t.`create_time`,' +
+                    '    pa.`name` As `user_name`, pa.`role` As `user_role`, pa.`company` As `user_company` ' +
+                    // '  FROM ?? As t, ?? As pa ' +
+                    // '  WHERE t.`project_id` = ? AND t.`user_id` = pa.`id` AND (' +
+                    '  FROM ?? As t ' +
+                    '  Left Join ?? As pa ' +
+                    '  ON t.`user_id` = pa.`id` ' +
+                    '  WHERE t.`project_id` = ? AND ' +
+                    // 参与施工 的标段
+                    ' t.id IN ( SELECT ca.`tid` FROM ?? As ca WHERE ca.`uid` = ?)' +
+                    ' ORDER BY CONVERT(t.`name` USING GBK) ASC';
+                sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, session.sessionProject.id,
+                    this.ctx.service.contractAudit.tableName, session.sessionUser.accountId,
+                ];
+            }
+            const list = await this.db.query(sql, sqlParam);
+            for (const l of list) {
+                l.category = l.category && l.category !== '' ? JSON.parse(l.category) : null;
+            }
+
+            return list;
+        }
     }
 
     return Tender;

+ 202 - 0
app/view/contract/detail.ejs

@@ -0,0 +1,202 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main  d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <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="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>
+            <% if (ctx.session.sessionUser.is_admin || audit_permission.permission_edit) { %>
+                <div class="d-inline-block">
+                    <a href="javascript: void(0);" name="base-opr" type="add" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="delete" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i class="fa fa-remove" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
+<!--                    <a href="javascript: void(0);" name="cpc" type="copy" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="复制"><i class="fa fa-files-o" aria-hidden="true"></i></a>-->
+<!--                    <a href="javascript: void(0);" name="cpc" type="cut" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="剪切"><i class="fa fa-scissors" aria-hidden="true"></i></a>-->
+<!--                    <a href="javascript: void(0);" name="cpc" type="paste" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="粘贴"><i class="fa fa-clipboard" aria-hidden="true"></i></a>-->
+                </div>
+            <% } %>
+            <div class="ml-auto">
+                <% if (ctx.session.sessionUser.is_admin) { %>
+                <a href="javascript:void(0);" data-stid="<%- ctx.contractOptions.spid || ctx.contractOptions.tid %>" class="btn btn-sm btn-primary get-audits mr-2">成员管理</a>
+                <a href="#empower" data-toggle="modal" data-target="#empower" class="btn btn-sm btn-primary mr-2">节点授权</a>
+                <% } %>
+<!--                <a href="#cons-relat" data-toggle="modal" data-target="#cons-relat" class="btn btn-primary btn-sm pull-right">关联合同</a>-->
+                <% if (ctx.session.sessionUser.is_admin || audit_permission.permission_add) { %>
+                <a href="#cons-add" data-toggle="modal" data-target="#cons-add" class="btn btn-primary btn-sm pull-right mr-2" id="add-cons-btn" style="display: none">新增合同</a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap row">
+        <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="contract-spread">
+                </div>
+                <div class="bcontent-wrap">
+                    <div id="contract-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" data-toggle="tab" href="#htdetail" role="tab">合同详情</a>
+                            </li>
+                            <li class="nav-item">
+                                <a class="nav-link " data-toggle="tab" href="#htpay" role="tab">合同<% if (ctx.contract_type === contractConst.type.expenses) { %>支付<% } else if (ctx.contract_type === contractConst.type.income) { %>回款<% } %></a>
+                            </li>
+                            <li class="nav-item">
+                                <a class="nav-link " data-toggle="tab" href="#htfile" role="tab">合同文件</a>
+                            </li>
+                            <li class="ml-auto">
+                                <!-- 结算合同所有tab可见 ,结算后见解锁合同,2者互斥-->
+                                <a href="#cons-unlock" data-toggle="modal" data-target="#cons-unlock" style="display: none;" class="btn btn-success btn-sm pull-right mr-2">解锁合同</a>
+                                <a href="#cons-close" data-toggle="modal" data-target="#cons-close" style="display: none;" class="btn btn-danger btn-sm pull-right mr-2">结算合同</a>
+                                <!-- 合同文件的按钮 -->
+                                <a href="#cons-upfile" data-toggle="modal" data-target="#cons-upfile" style="display: none;" class="btn btn-primary btn-sm pull-right mr-2">上传文件</a>
+                                <!-- 合同支付的按钮 -->
+<!--                                <a href="#cons-addpay2" data-toggle="modal" data-target="#cons-addpay2" class="btn btn-primary btn-sm pull-right mr-2">添加回款2</a>-->
+                                <a href="javascript:void(0);" id="add_contract_pay_btn" style="display: none;" class="btn btn-primary btn-sm pull-right mr-2">添加<% if (ctx.contract_type === contractConst.type.expenses) { %>支付<% } else if (ctx.contract_type === contractConst.type.income) { %>回款<% } %></a>
+                                <!-- 合同详情的按钮,点击编辑出现确定和取消按钮 -->
+                                <a href="javascript:void(0);" id="cancel_contract_btn" style="display: none" class="btn btn-secondary btn-sm pull-right mr-2">取消</a>
+                                <a href="javascript:void(0);" id="save_contract_btn" style="display: none" class="btn btn-primary btn-sm pull-right mr-2">确定</a>
+                                <a href="javascript:void(0);" id="edit_contract_btn" style="display: none" class="btn btn-primary btn-sm pull-right mr-2">编辑合同</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="tab-content">
+                        <div class="tab-pane active" id="htdetail">
+                            <div class="sp-wrap col-12" style="overflow: auto;">
+                                <table class="table table-sm table-bordered" id="htdetail-table" style="display: none;">
+                                    <tr>
+                                        <th class="text-center align-middle" width="10%">合同编号</th>
+                                        <td width="20%" class="change-input-td" id="htdetail_c_code"></td>
+                                        <th class="text-center align-middle"width="10%">合同名称</th>
+                                        <td width="30%" class="change-input-td" id="htdetail_name"></td>
+                                        <th class="text-center align-middle"width="10%">创建时间</th>
+                                        <td width="20%" id="htdetail_create_time"></td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-center align-middle">合同金额</th>
+                                        <td class="change-input-td" id="htdetail_total_price"></td>
+                                        <th class="text-center align-middle"><% if (ctx.contract_type === contractConst.type.income) { %>累计回款(P)<% } else if (ctx.contract_type === contractConst.type.expenses) { %>累计支付(P)<% } %></th>
+                                        <td id="htdetail_pay_price"></td>
+                                        <th class="text-center align-middle">累计扣款(K)</th>
+                                        <td id="htdetail_debit_price"></td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-center align-middle"><% if (ctx.contract_type === contractConst.type.income) { %>累计应回(S=P-K)<% } else if (ctx.contract_type === contractConst.type.expenses) { %>累计应付(S=P-K)<% } %></th>
+                                        <td id="htdetail_yf_price"></td>
+                                        <th class="text-center align-middle"><% if (ctx.contract_type === contractConst.type.income) { %>累计已回(A)<% } else if (ctx.contract_type === contractConst.type.expenses) { %>累计已付(A)<% } %></th>
+                                        <td id="htdetail_sf_price"></td>
+                                        <th class="text-center align-middle"><% if (ctx.contract_type === contractConst.type.income) { %>待回款(S-A)<% } else if (ctx.contract_type === contractConst.type.expenses) { %>待支付(S-A)<% } %></th>
+                                        <td id="htdetail_df_price"></td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-center align-middle">甲方</th>
+                                        <td class="change-input-td" id="htdetail_party_a"></td>
+                                        <th class="text-center align-middle">签约人</th>
+                                        <td class="change-input-td" id="htdetail_party_a_user"></td>
+                                        <th class="text-center align-middle">签订日期</th>
+                                        <td class="change-input-td" id="htdetail_sign_date"></td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-center align-middle">乙方</th>
+                                        <td class="change-input-td" id="htdetail_party_b"></td>
+                                        <th class="text-center align-middle">签约人</th>
+                                        <td class="change-input-td" id="htdetail_party_b_user"></td>
+                                        <% if (ctx.contract_type === contractConst.type.expenses) { %>
+                                        <th class="text-center align-middle">签约地点</th>
+                                        <td class="change-input-td" id="htdetail_address"></td>
+                                        <% } else if (ctx.contract_type === contractConst.type.income) { %>
+                                        <th class="text-center align-middle">结算书编号</th>
+                                        <td id="htdetail_settle_code"></td>
+                                        <% } %>
+                                    </tr>
+                                    <% if (ctx.contract_type === contractConst.type.expenses) { %>
+                                    <tr class="contract-expenses">
+                                        <th class="text-center align-middle">收款单位</th>
+                                        <td class="change-input-td" id="htdetail_entity"></td>
+                                        <th class="text-center align-middle">收款开户行</th>
+                                        <td class="change-input-td" id="htdetail_bank"></td>
+                                        <th class="text-center align-middle">收款账号</th>
+                                        <td class="change-input-td" id="htdetail_bank_account"></td>
+                                    </tr>
+                                    <% } %>
+                                    <tr>
+                                        <% if (ctx.contract_type === contractConst.type.expenses) { %>
+                                        <th class="text-center align-middle">结算书编号</th>
+                                        <td id="htdetail_settle_code"></td>
+                                        <% } %>
+                                        <th class="text-center align-middle">备注</th>
+                                        <td class="change-input-td" colspan="<% if (ctx.contract_type === contractConst.type.expenses) { %>3<% } else if (ctx.contract_type === contractConst.type.income) { %>5<% } %>" id="htdetail_remark"></td>
+                                    </tr>
+                                </table>
+                            </div>
+                        </div>
+                        <div class="tab-pane" id="htpay">
+                            <div class="sp-wrap" style="overflow: auto">
+                                <table class="table table-sm table-bordered" id="htpay-table" style="display: none;">
+                                    <thead>
+                                    <tr class="text-center">
+                                        <% if (ctx.contract_type === contractConst.type.income) { %>
+                                            <th width="5%">序号</th><th width="5%">回款日期</th><th width="10%">回款金额</th><th width="10%">扣款金额</th><th width="10%">应回金额</th><th width="10%">实回金额</th><th width="5%">回款方式</th><th width="5%">创建人</th><th width="10%">创建时间</th><th width="15%">备注</th><th width="5%">附件</th><th width="10%">操作</th>
+                                        <% } else if (ctx.contract_type === contractConst.type.expenses) { %>
+                                            <th width="5%">序号</th><th width="5%">支付日期</th><th width="10%">付款金额</th><th width="10%">扣款金额</th><th width="10%">应付金额</th><th width="10%">实付金额</th><th width="5%">支付方式</th><th width="5%">创建人</th><th width="10%">创建时间</th><th width="15%">备注</th><th width="5%">附件</th><th width="10%">操作</th>
+                                        <% } %>
+                                    </tr>
+                                    </thead>
+                                    <tbody>
+                                    </tbody>
+                                </table>
+                            </div>
+                        </div>
+                        <div class="tab-pane" id="htfile">
+                            <div class="sp-wrap" style="overflow: auto">
+                                <table class="table table-sm table-bordered" id="htfile-table" style="display: none;">
+                                    <thead>
+                                        <tr class="text-center">
+                                            <th width="5%">序号</th><th>名称</th><th width="5%">上传人</th><th width="15%">上传时间</th><th width="15%">操作</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+                                    </tbody>
+                                </table>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const user_id = <%- ctx.session.sessionUser.accountId %>;
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+    const permission_edit = <%- (ctx.session.sessionUser.is_admin || audit_permission.permission_edit) %>;
+    const permission_add = <%- (ctx.session.sessionUser.is_admin || audit_permission.permission_add) %>;
+    const stid = '<%- ctx.contractOptions.spid || ctx.contractOptions.tid %>';
+    const contract_type = <%- contract_type %>;
+    const whiteList = JSON.parse(unescape('<%- escape(JSON.stringify(whiteList)) %>'));
+    const contractConst = JSON.parse(unescape('<%- escape(JSON.stringify(contractConst)) %>'));
+    let contractTreeAudits = JSON.parse(unescape('<%- escape(JSON.stringify(contractTreeAudits)) %>'));
+    let contractPays = [];
+</script>

+ 264 - 0
app/view/contract/detail_modal.ejs

@@ -0,0 +1,264 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<% if (ctx.session.sessionUser.is_admin || audit_permission.permission_add) { %>
+<!--新增合同-->
+<div class="modal fade" id="cons-add" 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>合同编号<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm" placeholder="请输入合同编号" type="text" name="code">
+                </div>
+                <div class="form-group">
+                    <label>合同名称<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm" placeholder="请输入合同名称" type="text" name="name">
+                </div>
+                <div class="form-group">
+                    <label>合同金额<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm" placeholder="请输入合同金额" type="text" name="total_price">
+                    <div class="invalid-feedback">合同金额不能为0。</div>
+                </div>
+                <div class="form-group">
+                    <label>签订单位(乙方)<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm" placeholder="请输入签订单位" type="text" name="party_b">
+                </div>
+                <div class="form-group">
+                    <label>备注</label>
+                    <textarea class="form-control form-control-sm" name="remark" rows="3"></textarea>
+                </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="add-contract">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if (ctx.session.sessionUser.is_admin) { %>
+<% include modal.ejs %>
+    <!-- 节点授权 -->
+    <div class="modal fade" id="empower" data-backdrop="static" style="z-index: 1049">
+        <div class="modal-dialog modal-xl" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">节点授权</h5>
+                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                        <span aria-hidden="true">×</span>
+                    </button>
+                </div>
+                <div class="modal-body">
+                    <div class="row">
+                        <!-- 左侧默认节点 -->
+                        <div class="col-5">
+                            <div class="mb-2">节点列表</div>
+                            <div class="modal-height-500" id="sq-spread">
+                            </div>
+                        </div>
+                        <!-- 右侧对应节点成员 -->
+                        <div class="col-7">
+                            <div class="d-flex flex-row bg-graye">
+                                <div class="pb-1  dropdown">
+                                    <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                        添加用户
+                                    </button>
+                                    <div class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="width:220px">
+                                        <div class="mb-2 p-2"><input class="form-control form-control-sm" placeholder="姓名/手机 检索" id="gr-search2" autocomplete="off"></div>
+                                        <dl class="list-unstyled book-list">
+                                        </dl>
+                                    </div>
+                                </div>
+                                <div class="ml-2"><a href="javascript:void(0);" id="batch-del-audits" class="text-danger">批量删除</a></div>
+                            </div>
+                            <div class="modal-height-500" style="overflow-y:auto;">
+                                <table class="table table-bordered text-center">
+                                    <thead>
+                                    <tr>
+                                        <th><input class="" type="checkbox" id="select-permission-tree-audit-all" /></th>
+                                        <th>用户名</th>
+                                        <th>授权时间</th>
+                                        <th>权限</th>
+                                        <th>操作</th>
+                                    </tr>
+                                    </thead>
+                                    <tbody id="contract-tree-audits">
+<!--                                    <tr class="text-center">-->
+<!--                                        <td><input type="checkbox" name="ftu-check"></td><td>邓莹洁</td><td>2023-09-21 11:26:41</td><td>编辑节点,添加合同</td>-->
+<!--                                        <td><button class="btn btn-sm btn-outline-danger">移除</button></td>-->
+<!--                                    </tr>-->
+<!--                                    <tr class="text-center">-->
+<!--                                        <td><input type="checkbox" name="ftu-check"></td><td>付一</td><td>2023-09-21 11:26:41</td><td>添加合同,查看本单位合同</td>-->
+<!--                                        <td><button class="btn btn-sm btn-outline-danger">移除</button></td>-->
+<!--                                    </tr>-->
+<!--                                    <tr class="text-center">-->
+<!--                                        <td><input type="checkbox" name="ftu-check"></td><td>付二</td><td>2023-09-21 11:26:41</td><td>添加合同,查看本节点合同</td>-->
+<!--                                        <td><button class="btn btn-sm btn-outline-danger">移除</button></td>-->
+<!--                                    </tr>-->
+<!--                                    <tr class="text-center">-->
+<!--                                        <td><input type="checkbox" name="ftu-check"></td><td>付三</td><td>2023-09-21 11:26:41</td><td>添加合同</td>-->
+<!--                                        <td><button class="btn btn-sm btn-outline-danger">移除</button></td>-->
+<!--                                    </tr>-->
+                                    </tbody>
+                                </table>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-sm btn-outline-secondary" data-dismiss="modal">关闭</button>
+                </div>
+            </div>
+        </div>
+    </div>
+<% } %>
+<!--添加附件-->
+<div class="modal fade" id="cons-upfile">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="myModalLabel">上传附件</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="file-modal">单个文件大小限制:30MB,支持office等文档格式、图片格式、压缩包格式</label>
+                    <!-- <p><a href="javascript: void(0);" class="btn btn-primary" id="file-modal-target">选择文件</a></p> -->
+                    <input type="file" id="file-modal" multiple="multiple">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button id="file-cancel" type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">取消</button>
+                <button id="file-ok" type="button" class="btn btn-primary btn-sm">添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--添加支付,非关联合同显示-->
+<div class="modal fade" id="cons-addpay" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">添加<% if (ctx.contract_type === contractConst.type.expenses) { %>支付<% } else if (ctx.contract_type === contractConst.type.income) { %>回款<% } %></h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group form-group-sm">
+                    <label><% if (ctx.contract_type === contractConst.type.expenses) { %>支付<% } else if (ctx.contract_type === contractConst.type.income) { %>回款<% } %>日期</label>
+                    <input class="datepicker-here form-control form-control-sm" name="pay_time" placeholder="点击选择时间" data-date-format="yyyy-MM-dd" data-language="zh" type="text">
+                </div>
+                <div class="form-group">
+                    <label><% if (ctx.contract_type === contractConst.type.expenses) { %>付<% } else if (ctx.contract_type === contractConst.type.income) { %>回<% } %>款金额<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm" name="pay_price" placeholder="请输入<% if (ctx.contract_type === contractConst.type.expenses) { %>付<% } else if (ctx.contract_type === contractConst.type.income) { %>回<% } %>款金额" type="number">
+                </div>
+                <div class="form-group">
+                    <label>扣款金额</label>
+                    <input class="form-control form-control-sm" name="debit_price" placeholder="请输入扣款金额" type="number">
+                </div>
+                <div class="form-group">
+                        <label>应<% if (ctx.contract_type === contractConst.type.expenses) { %>付<% } else if (ctx.contract_type === contractConst.type.income) { %>回<% } %>金额</label>
+                    <input class="form-control form-control-sm" name="yf_price" placeholder="<% if (ctx.contract_type === contractConst.type.expenses) { %>付<% } else if (ctx.contract_type === contractConst.type.income) { %>回<% } %>款-扣款" type="number" readonly>
+                </div>
+                <div class="form-group">
+                    <label>实<% if (ctx.contract_type === contractConst.type.expenses) { %>付<% } else if (ctx.contract_type === contractConst.type.income) { %>回<% } %>金额</label>
+                    <input class="form-control form-control-sm" name="sf_price" placeholder="请输入实<% if (ctx.contract_type === contractConst.type.expenses) { %>付<% } else if (ctx.contract_type === contractConst.type.income) { %>回<% } %>金额" type="number">
+                </div>
+                <div class="form-group">
+                    <label><% if (ctx.contract_type === contractConst.type.expenses) { %>支付<% } else if (ctx.contract_type === contractConst.type.income) { %>回款<% } %>方式<b class="text-danger">*</b></label>
+                    <select class="form-control form-control-sm" name="pay_type">
+                        <option>网上转账</option>
+                        <option>支付宝</option>
+                        <option>微信</option>
+                        <option>现金</option>
+                        <option>支票</option>
+                        <option>其他</option>
+                    </select>
+                </div>
+                <div class="form-group">
+                    <label>备注</label>
+                    <textarea class="form-control form-control-sm" name="remark" rows="3"></textarea>
+                </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="add-contract-pay">确定</button>
+                <input type="hidden" name="cpid" value="">
+                <button type="button" class="btn btn-sm btn-primary" id="save-contract-pay" style="display: none">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--附件-->
+<div class="modal fade" id="cons-pay-file" data-backdrop="static" style="z-index: 1049">
+    <input type="hidden" name="cpid">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">附件</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group upload-permission">
+                    <label for="formGroupExampleInput">单个文件大小限制:30MB,支持<span data-toggle="tooltip" data-placement="bottom" title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="rar,zip">压缩包格式</span></label>
+                    <br>
+                    <input type="file" class="" multiple>
+                </div>
+                <div class="modal-height-500" style="overflow:auto;">
+                    <table class="table table-sm table-bordered text-center" style="word-break:break-all; table-layout: fixed">
+                        <thead>
+                        <tr><th width="5%">序号</th><th>名称</th><th width="8%">上传人</th><th width="20%">上传时间</th><th width="15%">操作</th></tr>
+                        </thead>
+                        <tbody>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-outline-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <!--<button type="button" class="btn btn-primary btn-sm" id="upload-file-btn">确定</button>-->
+            </div>
+        </div>
+    </div>
+</div>
+<!--结算合同-->
+<div class="modal fade" id="cons-close" 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">
+                <h5>当前合同【<span id="close_contract_code"></span>】:</h5>
+                <h5>存在待支付金额<strong>【<span id="close_df_price"></span>】</strong>,确定关闭?</h5>
+                <h5>关闭后,合同将锁定,无法进行编辑、上传文件等操作。</h5>
+                <div class="form-group mt-3">
+                    <label>结算书编号:<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm" placeholder="输入结算书编号" id="close_settle_code" type="text">
+                </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="close_contract_btn">确认结算</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--解锁合同-->
+<div class="modal fade" id="cons-unlock" 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">
+                <h5>解锁后,合同可进行编辑、上传文件等操作。</h5>
+            </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="unlock_contract_btn">确认解锁</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 56 - 0
app/view/contract/index.ejs

@@ -0,0 +1,56 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex">
+            <div class="d-inline-block">
+                <div class="btn-group group-tab">
+                    <a href="javascript:void(0);" class="btn btn-sm btn-light active">
+                        项目合同
+                    </a>
+                    <a href="/contract/tender" class="btn btn-sm btn-light">
+                        标段合同
+                    </a>
+                </div>
+            </div>
+            <div class="d-inline-block mr-2" id="show-level"></div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+                <% if (!projectList || projectList.length === 0) { %>
+                <div class="jumbotron" id="no-project">
+                    <h3 class="display-6">还没有项目数据</h3>
+                </div>
+                <% } else { %>
+                <table class="table table-bordered">
+                    <tr class="text-center">
+                        <th style="min-width: 200px" rowspan="2">项目名称</th>
+                        <th width="10%" rowspan="2">创建时间</th>
+                        <th colspan="3">支出合同</th>
+                        <th colspan="3">收入合同</th>
+                        <% if (ctx.session.sessionUser.is_admin) { %>
+                        <th width="10%" rowspan="2">操作</th>
+                        <% } %>
+                    </tr>
+                    <tr class="text-center">
+                        <th width="6%">合同个数</th>
+                        <th width="10%">合同金额</th>
+                        <th width="15%">支出进度</th>
+                        <th width="6%">合同个数</th>
+                        <th width="10%">合同金额</th>
+                        <th width="15%">回款进度</th>
+                    </tr>
+                    <tbody id="projectList">
+                    </tbody>
+                </table>
+                <% } %>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const projectList = JSON.parse(unescape('<%- escape(JSON.stringify(projectList)) %>'));
+    console.log(projectList);
+    const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+</script>

+ 263 - 0
app/view/contract/modal.ejs

@@ -0,0 +1,263 @@
+<% if (ctx.session.sessionUser.is_admin) { %>
+<link href="/public/css/bootstrap/bootstrap-table.min.css" rel="stylesheet">
+<link href="/public/css/bootstrap/bootstrap-table-fixed-columns.min.css" rel="stylesheet">
+<style>
+    /*.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer) {*/
+    /*border-bottom: 0;*/
+    /*}*/
+    @-moz-document url-prefix() {
+        table {
+            table-layout: fixed;
+        }
+    }
+    .customize-header tr th .th-inner{
+        padding: 0.3rem!important;
+    }
+</style>
+<!-- 成员管理 -->
+<div class="modal fade" id="authority-list" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">成员管理</h5>
+            </div>
+            <div class="modal-body pt-0">
+                <div class="d-flex flex-row bg-graye">
+                    <input type="hidden" id="stid" />
+                    <div class="p-2 dropdown">
+                        <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton"
+                                data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            添加用户
+                        </button>
+                        <div class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownMenuButton" style="width:220px">
+                            <div class="mb-2 p-2"><input class="form-control form-control-sm" placeholder="姓名/手机 检索"
+                                                         id="gr-search" autocomplete="off"></div>
+                            <dl class="list-unstyled book-list">
+                                <% accountGroup.forEach((group, idx) => { %>
+                                    <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>"
+                                           data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                    <div class="dd-content" data-toggleid="<%- idx %>">
+                                        <% group.groupList.forEach(item => { %>
+                                            <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>">
+                                                <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                            class="ml-auto"><%- item.mobile %></span></p>
+                                                <span class="text-muted"><%- item.role %></span>
+                                            </dd>
+                                        <% });%>
+                                    </div>
+                                <% }) %>
+                            </dl>
+                        </div>
+                    </div>
+<!--                    <div class="p-2"><a href="">同步计量账号</a></div>-->
+                    <div class="ml-auto p-2">
+                        <div class="btn-group">
+                            <a href="javascript:void(0)" data-toggle="dropdown" title="权限说明"><i class="fa fa-question-circle"></i></a>
+                            <div class="dropdown-menu bg-dark">
+                                <div class="dropdown-item text-light bg-dark">1、编辑节点:编辑合同管理内页树结构</div>
+                                <div class="dropdown-item text-light bg-dark">2、添加合同:允许添加合同</div>
+                                <div class="dropdown-item text-light bg-dark">3、授权范围本单位:授权节点下查看本单位人员添加的所有合同</div>
+                                <div class="dropdown-item text-light bg-dark">4、授权范围本节点:授权节点下查看所有人上传的合同</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <table id="contract-audit-table" class="table table-bordered text-center" data-height="300" data-toggle="table">
+                    <thead>
+                    <tr>
+                        <th rowspan="2">用户名</th>
+                        <th rowspan="2">角色/职位</th>
+                        <th rowspan="2">编辑节点</th>
+                        <th rowspan="2">添加合同</th>
+                        <th colspan="2">授权节点合同查看范围</th>
+                        <th rowspan="2">操作</th>
+                    </tr>
+                    <tr>
+                        <th>本单位</th>
+                        <th>本节点</th>
+                    </tr>
+                    </thead>
+                    <tbody id="contract-audit-list">
+                    </tbody>
+                </table>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-outline-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!-- 弹窗删除权限用户 -->
+<div class="modal fade" id="del-contract-audit" 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">
+                <h6>确认删除当前所选用户?</h6>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <input type="hidden" id="del-audit-ids" />
+                <button type="button" class="btn btn-sm btn-danger" id="del-audit-btn">确定删除</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script src="/public/js/bootstrap/bootstrap-table.min.js"></script>
+<script src="/public/js/bootstrap/locales/bootstrap-table-zh-CN.min.js"></script>
+    <script>
+        const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+        const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    </script>
+<script>
+    $(function () {
+        let timer = null
+        let oldSearchVal = null
+        $('#gr-search').bind('input propertychange', function (e) {
+            oldSearchVal = e.target.value
+            timer && clearTimeout(timer)
+            timer = setTimeout(() => {
+                const newVal = $('#gr-search').val()
+                let html = ''
+                if (newVal && newVal === oldSearchVal) {
+                    accountList.filter(item => item && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1))).forEach(item => {
+                        html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                class="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span>
+                    </dd>`
+                    })
+                    $('#authority-list .book-list').empty()
+                    $('#authority-list .book-list').append(html)
+                } else {
+                    if (!$('#authority-list .acc-btn').length) {
+                        accountGroup.forEach((group, idx) => {
+                            if (!group) return
+                            html += `<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i>
+                        </a> ${group.groupName}</dt>
+                        <div class="dd-content" data-toggleid="${idx}">`
+                            group.groupList.forEach(item => {
+                                html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                                    <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                            class="ml-auto">${item.mobile || ''}</span></p>
+                                    <span class="text-muted">${item.role || ''}</span>
+                                </dd>`
+                            });
+                            html += '</div>'
+                        })
+                        $('#authority-list .book-list').empty()
+                        $('#authority-list .book-list').append(html)
+                    }
+                }
+            }, 400);
+        });
+        // 添加到成员中
+        $('.book-list').on('click', 'dt', function () {
+            const idx = $(this).find('.acc-btn').attr('data-groupid')
+            const type = $(this).find('.acc-btn').attr('data-type')
+            if (type === 'hide') {
+                $(this).parent().find(`div[data-toggleid="${idx}"]`).show(() => {
+                    $(this).children().find('i').removeClass('fa-plus-square').addClass('fa-minus-square-o')
+                    $(this).find('.acc-btn').attr('data-type', 'show')
+
+                })
+            } else {
+                $(this).parent().find(`div[data-toggleid="${idx}"]`).hide(() => {
+                    $(this).children().find('i').removeClass('fa-minus-square-o').addClass('fa-plus-square')
+                    $(this).find('.acc-btn').attr('data-type', 'hide')
+                })
+            }
+            return false
+        });
+        // 添加到成员中
+        $('body').on('click', '#authority-list dl dd', function () {
+            const id = parseInt($(this).data('id'));
+            console.log(id);
+            if (!isNaN(id) && id !== 0) {
+                postData('/contract/'+ $('#stid').val() + '/audit/save', {type: 'add-audit', id: id}, function (result) {
+                    setList(result);
+                })
+            }
+        });
+        let first = 1;
+        $('#authority-list').on('shown.bs.modal', function () {
+            if (first) {
+                const option = {
+                    locale: 'zh-CN',
+                    height: 300,
+                }
+                $("#contract-audit-table").bootstrapTable('destroy').bootstrapTable(option);
+                first = 0;
+            }
+            const stid = $('#stid').val();
+            if (stid) {
+                postData('/contract/'+ $('#stid').val() + '/audit/save', { type: 'list' }, function (result) {
+                    setList(result);
+                });
+            }
+        });
+        $('body').on('click', '.get-audits', function () {
+            const stid = $(this).data('stid');
+            $('#stid').val(stid);
+            $('#contract-audit-list').html('');
+            postData('/contract/'+ $('#stid').val() + '/audit/save', { type: 'check' }, function (result) {
+                $('#authority-list').modal('show');
+            });
+        });
+
+        function setList(datas) {
+            let list = '';
+            for (const ca of datas) {
+                list += `<tr>
+                            <td>${ca.name}</td>
+                            <td>${ca.role}</td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_edit" value="${ca.id}" ${ca.permission_edit ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_add" value="${ca.id}" ${ca.permission_add ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_show_unit" value="${ca.id}" ${ca.permission_show_unit ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_show_node" value="${ca.id}" ${ca.permission_show_node ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <a href="#del-contract-audit" data-toggle="modal" data-target="#del-contract-audit" class="btn btn-outline-danger btn-sm ml-1 del-contract-audit-a" data-id="${ca.id}">移除</a>
+                            </td>
+                        </tr>`;
+            }
+            $('#contract-audit-list').html(list);
+            $("#contract-audit-table").bootstrapTable('resetView');
+        }
+
+        $('body').on('click', '.del-contract-audit-a', function () {
+            $('#del-audit-ids').val($(this).attr('data-id'));
+            $('#del-contract-audit').modal('show');
+        });
+
+        $('#del-audit-btn').click(function () {
+            let uids = $('#del-audit-ids').val();
+            postData('/contract/'+ $('#stid').val() + '/audit/save', { type: 'del-audit', id: uids.split(',') }, function (result) {
+                // toastr.success(`成功添加 位用户`);
+                $('#del-contract-audit').modal('hide');
+                setList(result);
+            })
+        });
+
+        // 上报人权限勾选
+        $('body').on('click', '.permission-checkbox', function () {
+            const type = $(this).attr('data-type');
+            const value = $(this).is(':checked') ? 1 : 0;
+            const id = parseInt($(this).val());
+            const updateInfo = { id };
+            updateInfo[type] = value;
+            postData('/contract/'+ $('#stid').val() + '/audit/save', { type: 'save-permission', updateData: updateInfo }, function (result) {
+            })
+        });
+    });
+</script>
+<% } %>

+ 15 - 0
app/view/contract/sub_menu.ejs

@@ -0,0 +1,15 @@
+<div class="panel-sidebar" id="sub-menu">
+    <div class="sidebar-title" data-toggle="tooltip" data-placement="right" data-original-title="<%- ctx.contract.name %>"><%- ctx.contract.name %></div>
+    <div class="scrollbar-auto">
+        <% include ./sub_menu_list.ejs %>
+        <div class="side-show"></div>
+        <div class="side-fold" data-toggle="tooltip" data-placement="top" data-original-title="折叠侧栏" id="to-mini-menu">
+            <i class="fa fa-angle-left"></i>
+        </div>
+    </div>
+    <script>
+        new Vue({
+            el: '.scrollbar-auto',
+        });
+    </script>
+</div>

+ 15 - 0
app/view/contract/sub_menu_list.ejs

@@ -0,0 +1,15 @@
+<nav-menu title="返回" url="<%- preUrl %>" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<div class="nav-box">
+    <ul class="nav-list list-unstyled">
+        <li class="<% if (ctx.url === '/contract/' + ctx.contract.id + '/detail') { %>active<% } %>">
+            <a href="/contract/<%- ctx.contract.id %>/detail"><span class="ml-3">支出合同</span></a>
+        </li>
+    </ul>
+</div>
+<div class="nav-box">
+    <ul class="nav-list list-unstyled">
+        <li class="<% if (ctx.url === '/contract/' + ctx.contract.id + '/detail/income') { %>active<% } %>">
+            <a href="/contract/<%- ctx.contract.id %>/detail/income"><span class="ml-3">收入合同</span></a>
+        </li>
+    </ul>
+</div>

+ 16 - 0
app/view/contract/sub_mini_menu.ejs

@@ -0,0 +1,16 @@
+<!--折起的菜单-->
+<div class="min-side" id="sub-mini-menu" style="display: none;">
+    <div class="side-switch" data-toggle="tooltip" data-placement="left" data-original-title="点击这里打开收起的菜单栏">
+        <i class="fa fa-bars mt-2"></i>
+        <i class="fa fa-indent mt-2 text-primary" style="display: none;cursor: pointer;" id="to-menu"></i>
+    </div>
+    <div class="side-menu" id="mini-menu-list" style="display: none">
+        <% include ./sub_menu_list.ejs %>
+        <div class="side-fold"><a href="javascript: void(0);" data-toggle="tooltip" data-placement="top" data-original-title="展开侧栏" id="to-menu"><i class="fa fa-upload fa-rotate-90"></i></a></div>
+    </div>
+</div>
+<script>
+    new Vue({
+        el: '.side-menu',
+    });
+</script>

+ 33 - 0
app/view/contract/tender.ejs

@@ -0,0 +1,33 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex">
+            <div class="d-inline-block">
+                <div class="btn-group group-tab">
+                    <a href="/contract" class="btn btn-sm btn-light">
+                        项目合同
+                    </a>
+                    <a href="javascript:void(0);" class="btn btn-sm btn-light active">
+                        标段合同
+                    </a>
+                </div>
+            </div>
+            <div class="d-inline-block mr-2" id="show-level"></div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const tenders = JSON.parse(unescape('<%- escape(JSON.stringify(tenderList)) %>'));
+    const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
+    const selfCategoryLevel = JSON.parse(unescape('<%- escape(JSON.stringify(selfCategoryLevel)) %>'));
+    const uid = '<%- uid %>';
+    const pid = '<%- pid %>';
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+
+    const uphlname = 'user_' + uid + '_pro_' + pid + '_category_hide_contract_list';
+</script>

+ 8 - 0
config/menu.js

@@ -41,6 +41,14 @@ const menu = {
         children: null,
         caption: '项目管理',
     },
+    contract: {
+        name: '合同管理',
+        icon: 'fa-cny',
+        display: true,
+        url: '/contract',
+        children: null,
+        caption: '合同管理',
+    },
     file: {
         name: '资料归集',
         icon: 'fa-file-zip-o',

+ 62 - 2
config/web.js

@@ -1561,8 +1561,68 @@ const JsFiles = {
 
                 ],
                 mergeFile: 'settle_gather',
-            }
-        }
+            },
+        },
+        contract: {
+            index: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/zh_calc.js',
+                    // '/public/js/shares/drag_tree.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/shares/show_level.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/contract_index.js',
+                ],
+                mergeFile: 'contract_index',
+            },
+            tender: {
+                files: [
+                    '/public/js/ztree/jquery.ztree.core.js', '/public/js/ztree/jquery.ztree.exedit.js', '/public/js/decimal.min.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/zh_calc.js',
+                    '/public/js/PinYinOrder.bundle.js',
+                    '/public/js/shares/tender_list_order.js',
+                    '/public/js/shares/show_level.js',
+                    '/public/js/tender_showhide.js',
+                    '/public/js/contract_tender.js',
+                    '/public/js/tender_list_base.js',
+                ],
+                mergeFile: 'contract_tender',
+            },
+            detail: {
+                files: [
+                    '/public/js/js-xlsx/xlsx.full.min.js',
+                    '/public/js/js-xlsx/xlsx.utils.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/moment/moment.min.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/cs_tools.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/contract_detail.js',
+                ],
+                mergeFile: 'contract_detail',
+            },
+        },
     },
 };