Browse Source

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

Tony Kang 10 months ago
parent
commit
6cca16e876
43 changed files with 5897 additions and 45 deletions
  1. 1 0
      app/base/base_controller.js
  2. 30 0
      app/const/contract.js
  3. 1 0
      app/const/page_show.js
  4. 544 0
      app/controller/contract_controller.js
  5. 1 1
      app/controller/datacollect_controller.js
  6. 1 1
      app/controller/setting_controller.js
  7. 11 1
      app/extend/helper.js
  8. 93 0
      app/middleware/contract_check.js
  9. 1 1
      app/middleware/stage_check.js
  10. 10 8
      app/public/css/ztree/zTreeStyle.css
  11. 2013 0
      app/public/js/contract_detail.js
  12. 208 0
      app/public/js/contract_index.js
  13. 111 0
      app/public/js/contract_tender.js
  14. 94 10
      app/public/js/measure_compare.js
  15. 1 1
      app/public/js/path_tree.js
  16. 22 1
      app/public/js/shenpi.js
  17. 18 7
      app/public/js/spreadjs_rela/spreadjs_zh.js
  18. 1 1
      app/public/js/stage.js
  19. 2 2
      app/public/js/stage_im.js
  20. 27 3
      app/public/js/tender_showhide.js
  21. 19 0
      app/router.js
  22. 182 0
      app/service/contract.js
  23. 61 0
      app/service/contract_att.js
  24. 134 0
      app/service/contract_audit.js
  25. 146 0
      app/service/contract_pay.js
  26. 61 0
      app/service/contract_pay_att.js
  27. 1008 0
      app/service/contract_tree.js
  28. 74 0
      app/service/contract_tree_audit.js
  29. 1 1
      app/service/stage.js
  30. 12 2
      app/service/stage_stash.js
  31. 28 1
      app/service/sub_project.js
  32. 47 0
      app/service/tender.js
  33. 203 0
      app/view/contract/detail.ejs
  34. 264 0
      app/view/contract/detail_modal.ejs
  35. 55 0
      app/view/contract/index.ejs
  36. 263 0
      app/view/contract/modal.ejs
  37. 15 0
      app/view/contract/sub_menu.ejs
  38. 15 0
      app/view/contract/sub_menu_list.ejs
  39. 16 0
      app/view/contract/sub_mini_menu.ejs
  40. 33 0
      app/view/contract/tender.ejs
  41. 0 2
      app/view/measure/compare.ejs
  42. 8 0
      config/menu.js
  43. 62 2
      config/web.js

+ 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,
 };
 
 

+ 544 - 0
app/controller/contract_controller.js

@@ -0,0 +1,544 @@
+'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,
+                    isTender: false,
+                };
+                // 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);
+                    }
+                }
+                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);
+                const projectList = await ctx.service.subProject.getSubProjectByTender(ctx.session.sessionProject.id, tenderList);
+                for (const t of tenderList) {
+                    const spInfo = ctx.helper._.find(projectList, { id: t.spid });
+                    spInfo.child_order = spInfo.child_order ? spInfo.child_order : 0;
+                    spInfo.is_folder = 1;
+                    t.tree_pid = spInfo.id;
+                    t.tree_level = spInfo.tree_level + 1;
+                    t.tree_order = spInfo.child_order + 1;
+                    spInfo.child_order++;
+                    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),
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.contract.index),
+                    auditConst,
+                    // tenderList,
+                    projectList: projectList.concat(tenderList),
+                    categoryData,
+                    isTender: true,
+                    // 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');
+                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);
+            }
+        }
+
+        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 'add-child':
+                    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 'add-child':
+                    return await ctx.service.contractTree.addChildNode(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,

+ 11 - 1
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 - 主数据
@@ -1506,7 +1516,7 @@ module.exports = {
 
     async ossFileGet(path) {
         // 判断开头是否带app,否则加上
-        if (!_.includes(path, 'app/')) {
+        if (!_.includes(path, 'app/') && !_.includes(path, 'sp/contract/')) {
             path = 'app/' + path;
         }
         const result = await this.ctx.app.fujianOss.get(this.ctx.app.config.fujianOssFolder + path);

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

+ 1 - 1
app/middleware/stage_check.js

@@ -109,7 +109,7 @@ module.exports = options => {
                         stage.canCheck = true;
                     } else if (stage.curAuditors[0].audit_type === auditType.key.union) {
                         stage.readOnly = stage.relaAuditor.status === status.checked;
-                        stage.canCheck = !stage.readOnly
+                        stage.canCheck = !stage.readOnly;
                     }
                 }
             }

+ 10 - 8
app/public/css/ztree/zTreeStyle.css

@@ -9,13 +9,13 @@ website:	http://code.google.com/p/jquerytree/
 -------------------------------------*/
 
 .ztree * {padding:0; margin:0; font-size:12px; font-family: Verdana, Arial, Helvetica, AppleGothic, sans-serif}
-.ztree {margin:0; padding:5px; color:#333}
+.ztree {margin:0; padding:5px; color:#333;}
 .ztree li{padding:0; margin:0; list-style:none; line-height:14px; text-align:left; white-space:nowrap; outline:0}
 .ztree li ul{ margin:0; padding:0 0 0 18px}
 .ztree li ul.line{ background:url(./img/line_conn.gif) 0 0 repeat-y;}
 
 .ztree li a {padding:1px 3px 0 0; margin:0; cursor:pointer; height:24px; color:#333; background-color: transparent;
-	text-decoration:none; vertical-align:top; display: inline-block}
+	text-decoration:none; vertical-align:top; display: inline-block; height: auto !important;  width: 90%}
 .ztree li a:hover {text-decoration:underline}
 .ztree li a.curSelectedNode {padding-top:0px; background-color:#cce5ff; color:black; height:24px; border:1px #b8daff solid; opacity:1;}
 .ztree li a.curSelectedNode_Edit {padding-top:0px; background-color:#cce5ff; color:black; height:24px; border:1px #b8daff solid; opacity:1;}
@@ -23,9 +23,12 @@ website:	http://code.google.com/p/jquerytree/
 	opacity:0.8; filter:alpha(opacity=80)}
 .ztree li a.tmpTargetNode_prev {}
 .ztree li a.tmpTargetNode_next {}
-.ztree li a input.rename {height:22px; width:160px; padding:0; margin:0;
+.ztree li a input.rename {height:22px; width:300px; padding:0; margin:0;
 	font-size:12px; border:1px #7EC4CC solid; *border:0px;vertical-align:top;}
-.ztree li span {line-height:24px; margin-right:2px;height:24px;display: inline-block;}
+
+.ztree li a span.node_name{ white-space: pre-line;}
+
+.ztree li span {line-height:24px; margin-right:2px;height:24px; display: inline;}
 .ztree li span.button {line-height:0; margin:0; width:16px; height:16px; display: inline-block; vertical-align:middle;
 	border:0 none; cursor: pointer;outline:none;
 	background-color:transparent; background-repeat:no-repeat; background-attachment: scroll;
@@ -72,10 +75,9 @@ website:	http://code.google.com/p/jquerytree/
 
 .ztree li span.button.ico_open{margin-right:2px; margin-top:3px;background-position:-110px -16px; vertical-align:top; *vertical-align:middle}
 .ztree li span.button.ico_close{margin-right:2px; margin-top:3px;background-position:-110px 0; vertical-align:top; *vertical-align:middle}
-.ztree li span.button.ico_docu{margin-right:2px; margin-top:3px;background-position:-110px -32px; vertical-align:top; *vertical-align:middle}
-.ztree li span.button.edit {margin-right:2px; margin-top:3px;background-position:-110px -48px; vertical-align:top; *vertical-align:middle}
-.ztree li span.button.remove {margin-right:2px; margin-top:3px;background-position:-110px -64px; vertical-align:top; *vertical-align:middle}
-
+.ztree li span.button.ico_docu{margin-right:2px; margin-top:3px;background-position:-110px -32px; vertical-align:top; *vertical-align:middle;}
+.ztree li span.button.edit {margin-right:2px; margin-top:3px;background-position:-110px -48px; vertical-align:top; *vertical-align:middle;}
+.ztree li span.button.remove {margin-right:2px; margin-top:3px;background-position:-110px -64px; vertical-align:top; *vertical-align:middle;}
 .ztree li span.button.ico_loading{margin-right:2px; margin-top:3px;background:url(./img/loading.gif) no-repeat scroll 0 0 transparent; vertical-align:top; *vertical-align:middle}
 
 ul.tmpTargetzTree {background-color:#FFE6B0; opacity:0.8; filter:alpha(opacity=80)}

File diff suppressed because it is too large
+ 2013 - 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 }
+})();
+
+

+ 94 - 10
app/public/js/measure_compare.js

@@ -49,6 +49,30 @@ const posSpreadSetting = {
     readOnly: true,
     selectedBackColor: '#fffacd',
 };
+const exportBillsSpreadSetting = {
+    baseCols: [
+        {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 150, formatter: '@', cellType: 'tree'},
+        {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 80, formatter: '@'},
+        {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@'},
+        {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+        {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 60, type: 'Number'},
+        {title: '台账|数量', colSpan: '2|1', rowSpan: '1|1', field: 'quantity', hAlign: 2, width: 60, type: 'Number', },
+        {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 60, type: 'Number', },
+    ],
+    extraCols: [
+        {title: '%s|数量', colSpan: '2|1', rowSpan: '1|1', field: '{%s}_qty{%d}', hAlign: 2, width: 60, type: 'Number', },
+        {title: '|金额', colSpan: '|1', rowSpan: '|1', field: '{%s}_tp{%d}', hAlign: 2, width: 60, type: 'Number', },
+    ],
+    endCols: [],
+    emptyRows: 3,
+    headRows: 2,
+    headRowHeight: [25, 25],
+    headerFont: '12px 微软雅黑',
+    font: '12px 微软雅黑',
+    defaultRowHeight: 21,
+    readOnly: true,
+    selectedBackColor: '#fffacd',
+};
 
 const gclSpreadSetting = {
     baseCols: [
@@ -97,6 +121,39 @@ const leafXmjSpreadSetting = {
     font: '12px 微软雅黑',
     readOnly: true,
 };
+const exportGclSpreadSetting = {
+    baseCols: [
+        {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 80, formatter: '@'},
+        {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@'},
+        {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 100, formatter: '@'},
+        {title: '单位工程', colSpan: '1', rowSpan: '2', field: 'dwgc', hAlign: 0, width: 80, formatter: '@'},
+        {title: '分部工程', colSpan: '1', rowSpan: '2', field: 'fbgc', hAlign: 0, width: 80, formatter: '@'},
+        {title: '分项工程', colSpan: '1', rowSpan: '2', field: 'fxgc', hAlign: 0, width: 80, formatter: '@'},
+        {title: '细目', colSpan: '1', rowSpan: '2', field: 'jldy', hAlign: 0, width: 80, formatter: '@'},
+        {title: '计量单元', colSpan: '1', rowSpan: '2', field: 'bwmx', hAlign: 0, width: 80, formatter: '@'},
+        {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 60, formatter: '@', cellType: 'unit'},
+        {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 60, type: 'Number'},
+        {title: '签约清单|数量', colSpan: '2|1', rowSpan: '1|1', field: 'deal_bills_qty', hAlign: 2, width: 60, type: 'Number'},
+        {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'deal_bills_tp', hAlign: 2, width: 60, type: 'Number'},
+        {title: '台账|数量', colSpan: '2|1', rowSpan: '1|1', field: 'quantity', hAlign: 2, width: 60, type: 'Number'},
+        {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 60, type: 'Number'},
+    ],
+    extraCols: [
+        {title: '%s|数量', colSpan: '2|1', rowSpan: '1|1', field: '{%s}_qty{%d}', hAlign: 2, width: 60, type: 'Number', },
+        {title: '|金额', colSpan: '|1', rowSpan: '|1', field: '{%s}_tp{%d}', hAlign: 2, width: 60, type: 'Number', },
+    ],
+    endCols: [
+        {title: '图册号', colSpan: '1', rowSpan: '2', field: 'drawing_code', hAlign: 0, width: 80, formatter: '@'},
+    ],
+    emptyRows: 0,
+    headRows: 2,
+    headRowHeight: [25, 25],
+    headColWidth: [30],
+    defaultRowHeight: 21,
+    headerFont: '12px 微软雅黑',
+    font: '12px 微软雅黑',
+    readOnly: true,
+};
 
 function initSpreadSettingWithRoles(compareRoles) {
     function setSpreadSettingCols(setting, fieldSufs, Roles) {
@@ -118,6 +175,11 @@ function initSpreadSettingWithRoles(compareRoles) {
         for (const index in fieldSufs) {
             addExtraCols(fieldSufs[index], Roles[index]);
         }
+        if (setting.endCols) {
+            for (const col of setting.endCols) {
+                setting.cols.push(col);
+            }
+        }
     }
     const fieldSufs = [], roles = [], trs = $('tr[stage-id]');
     for (let r of compareRoles) {
@@ -131,8 +193,10 @@ function initSpreadSettingWithRoles(compareRoles) {
     }
     setSpreadSettingCols(billsSpreadSetting, fieldSufs, roles);
     setSpreadSettingCols(posSpreadSetting, fieldSufs, roles);
+    setSpreadSettingCols(exportBillsSpreadSetting, fieldSufs, roles);
     setSpreadSettingCols(gclSpreadSetting, fieldSufs, roles);
     setSpreadSettingCols(leafXmjSpreadSetting, fieldSufs, roles);
+    setSpreadSettingCols(exportGclSpreadSetting, fieldSufs, roles);
 }
 function calculateStageLedgerData(datas) {
     for (const d of datas) {
@@ -379,19 +443,39 @@ $(document).ready(() => {
     })('a[name=showLevel]', billsSheet);
 
     $('#exportExcel').click(function () {
-        const data = [];
-        if (!billsSheet.zh_tree) return;
-        for (const node of billsSheet.zh_tree.nodes) {
-            data.push(node);
-            const posRange = cPos.getLedgerPos(node.id);
-            if (posRange && posRange.length > 0) {
-                for (const pr of posRange) {
-                    data.push(pr);
+        const exportLedger = function () {
+            const data = [];
+            if (!billsSheet.zh_tree) return;
+            for (const node of billsSheet.zh_tree.nodes) {
+                data.push(node);
+                const posRange = cPos.getLedgerPos(node.id);
+                if (posRange && posRange.length > 0) {
+                    for (const pr of posRange) {
+                        data.push(pr);
+                    }
+                }
+            }
+
+            SpreadExcelObj.exportSimpleXlsxSheet(exportBillsSpreadSetting, data, $('.sidebar-title').attr('data-original-title') + "-多期比较.xlsx");
+        };
+        const exportGcl = function () {
+            const data = [];
+            if (!gclSheet.zh_data) return;
+            for (const node of gclSheet.zh_data) {
+                data.push(node);
+                for (const leafXmj of node.leafXmjs) {
+                    data.push(leafXmj);
                 }
             }
-        }
 
-        SpreadExcelObj.exportSimpleXlsxSheet(billsSpreadSetting, data, $('.sidebar-title').attr('data-original-title') + "-多期比较.xlsx");
+            SpreadExcelObj.exportSimpleXlsxSheet(exportGclSpreadSetting, data, $('.sidebar-title').attr('data-original-title') + "-多期比较.xlsx");
+        };
+        const cur = $('.active[name=compareType]').attr('href');
+        if (cur.indexOf('gcl') >= 0) {
+            exportGcl();
+        } else {
+            exportLedger();
+        }
     });
 
     $('[name=compare-data]').click(function () {

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

@@ -556,7 +556,7 @@ const createNewPathTree = function (type, setting) {
         getPosterity(node) {
             const self = this;
             let posterity;
-            if (node[self.setting.fullPath] !== '') {
+            if (node[self.setting.fullPath] !== undefined && node[self.setting.fullPath] !== '') {
                 const reg = new RegExp('^' + node[self.setting.fullPath] + '-');
                 posterity = this.datas.filter(function (x) {
                     return reg.test(x[self.setting.fullPath]);

+ 22 - 1
app/public/js/shenpi.js

@@ -1003,8 +1003,14 @@ $(document).ready(function () {
             SpreadJsObj.selChangedRefreshBackColor(this.sheet);
             this.spread.bind(spreadNS.Events.EditEnded, function(e, info) {
                 const node = SpreadJsObj.getSelectObject(info.sheet);
+                if (!node) return;
+
+                const refreshAuditId = [];
+                if (node.audit_id) refreshAuditId.push(node.audit_id);
                 const col = info.sheet.zh_setting.cols[info.col];
                 node[col.field] = info.editingText;
+                if (node.audit_id) refreshAuditId.push(node.audit_id);
+                self.refreshUnionCount(refreshAuditId);
             });
 
             $('#union').on('shown.bs.modal', function() {
@@ -1016,6 +1022,14 @@ $(document).ready(function () {
                     $('#union').modal('hide');
                 });
             });
+            $('body').on('click', '[name=clear-union]', function() {
+                const aid = parseInt(this.getAttribute('aid'));
+                for (const node of self.tree.nodes) {
+                    if (node.audit_id === aid) node.audit_id = 0;
+                }
+                SpreadJsObj.reloadColData(self.sheet, 2);
+                self.refreshUnionCount(aid);
+            });
         }
         _refreshUnionTree() {
             const ledgerAss = {};
@@ -1035,12 +1049,19 @@ $(document).ready(function () {
             const html = [];
             for (const auditor of auditors) {
                 auditor.lid = auditor.audit_ledger_id ? auditor.audit_ledger_id.split(',') : [];
-                html.push(`<tr><td>${auditor.name}</td><td>${auditor.company}</td><td>${auditor.lid.length}<button class="ml-2 btn btn-sm btn-outline-danger" jaid="${auditor.audit_id}">清空</button></td></tr>`);
+                html.push(`<tr><td>${auditor.name}</td><td>${auditor.company}</td><td><span aid="${auditor.audit_id}">${auditor.lid.length}</span><button class="ml-2 btn btn-sm btn-outline-danger" name="clear-union" aid="${auditor.audit_id}">清空</button></td></tr>`);
                 this.selectUnion.push({ value: auditor.audit_id, text: auditor.name });
             }
             $('#union_table').html(html.join(''));
             this._refreshUnionTree();
         }
+        refreshUnionCount(auditId) {
+            const auditIds = auditId instanceof Array ? auditId : [auditId];
+            for (const aid of auditIds) {
+                const unionNodes = this.tree.nodes.filter(x => { return x.audit_id === aid; });
+                $(`span[aid=${aid}]`).html(unionNodes.length);
+            }
+        }
         loadUnionData(sp_type, audit_order) {
             const data = { sp_type, audit_order };
             if (!this.loaded) data.ledger = 1;

+ 18 - 7
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -62,6 +62,9 @@ const SpreadJsObj = {
             if (col.getValue && Object.prototype.toString.apply(col.getValue) === "[object String]") {
                 col.getValue = getEvent(col.getValue);
             }
+            if (col.foreColor && Object.prototype.toString.apply(col.foreColor) === "[object String]") {
+                col.foreColor = getEvent(col.foreColor);
+            }
         }
     },
 
@@ -464,11 +467,19 @@ const SpreadJsObj = {
     },
     _getForeColor: function (sheet, data, row, col) {
         let foreColor = sheet.getDefaultStyle().foreColor;
-        if (sheet.zh_setting.tree.getForeColor && Object.prototype.toString.apply(sheet.zh_setting.tree.getForeColor) === "[object Function]") {
-            foreColor = sheet.zh_setting.tree.getColor(sheet, data, row, col, foreColor);
-        }
-        if (sheet.zh_setting.getForeColor && Object.prototype.toString.apply(sheet.zh_setting.getForeColor) === "[object Function]") {
-            foreColor = sheet.zh_setting.getForeColor(sheet, data, row, col, foreColor);
+        if (col.foreColor) {
+            if (Object.prototype.toString.apply(col.foreColor) === "[object Function]") {
+                foreColor = col.foreColor(data, foreColor);
+            } else {
+                foreColor = col.foreColor || foreColor;
+            }
+        } else {
+            if (sheet.zh_setting.tree.getForeColor && Object.prototype.toString.apply(sheet.zh_setting.tree.getForeColor) === "[object Function]") {
+                foreColor = sheet.zh_setting.tree.getForeColor(sheet, data, row, col, foreColor);
+            }
+            if (sheet.zh_setting.getForeColor && Object.prototype.toString.apply(sheet.zh_setting.getForeColor) === "[object Function]") {
+                foreColor = sheet.zh_setting.getForeColor(sheet, data, row, col, foreColor);
+            }
         }
         return foreColor;
     },
@@ -2668,7 +2679,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 +3262,4 @@ const GridSpreadUtils = {
         }
         return [gridData, node, sel.row, count];
     },
-};
+};

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

@@ -2415,7 +2415,7 @@ $(document).ready(() => {
             exprCacheKey: ['bqhtje'],
         });
         // 加载中间计量
-        stageIm.init(tenderInfo, stage, imType, tenderInfo.decimal, stage.assist ? stage.assist.ass_ledger_id : '');
+        stageIm.init(tenderInfo, stage, imType, tenderInfo.decimal);
         stageIm.loadData(result.ledgerData, result.posData, result.detailData, result.stageChange, result.import_change, result.detailAtt);
         if (stage.assist) {
             stageIm.loadFilter(stage.assist.ass_ledger_id);

+ 2 - 2
app/public/js/stage_im.js

@@ -93,7 +93,7 @@ const stageIm = (function () {
     }
 
     function loadFilter(filter) {
-        gsTree.loadFilter(filter);
+        if (filter) gsTree.loadFilter(filter);
     }
 
     function loadData4Rela(ledger, pos, stageDetail, stageChange, stageDetailAtt) {
@@ -947,7 +947,7 @@ const stageIm = (function () {
     function recursiveBuildImData (nodes) {
         if (!nodes || nodes.length === 0) { return; }
         for (const node of nodes) {
-            if (node.filter !== undefined && node.filter) continue;
+            if (node.filter) continue;
 
             if (gsTree.isLeafXmj(node) || ((stage.im_type !== imType.bw.value && stage.im_type !== imType.bb.value) && stage.im_gather && node.check)) {
                 if (stage.im_type === imType.tz.value) {

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

+ 182 - 0
app/service/contract.js

@@ -0,0 +1,182 @@
+'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 '参数有误';
+            const insertId = this.uuid.v4();
+            const transaction = await this.db.beginTransaction();
+            try {
+                const maxId = await this.ctx.service.contractTree._getMaxLid(options);
+                const insertData = {
+                    id: insertId,
+                    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 ? parseFloat(data.total_price) : 0,
+                    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 createData = await this.ctx.service.contract.getDataById(insertId);
+            createData.username = this.ctx.session.sessionUser.name;
+            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: createData, 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 });
+            for (const l of list) {
+                l.username = (await this.ctx.service.projectAccount.getAccountInfoById(l.uid)).name;
+            }
+            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;
+};

File diff suppressed because it is too large
+ 1008 - 0
app/service/contract_tree.js


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

+ 1 - 1
app/service/stage.js

@@ -89,7 +89,7 @@ module.exports = app => {
                 name: stage.user.name, role: stage.user.role, company: stage.user.company
             }]);
             stage.finalAuditorIds = stage.userGroups[stage.userGroups.length - 1].map(x => { return x.aid; });
-            stage.relaAuditor = stage.auditors.find(x => { return x.aid === accountId });
+            stage.relaAuditor = this._.findLast(stage.auditors, x => { return x.aid === accountId });
 
             stage.assists = await this.service.stageAuditAss.getData(stage); // 全部协同人
             stage.assists = stage.assists.filter(x => {

+ 12 - 2
app/service/stage_stash.js

@@ -242,9 +242,19 @@ module.exports = app => {
                 { data: settleStatusPos, fields: ['settle_status'], prefix: '', relaId: 'pid' },
             ]);
 
+            const billsIndex = {};
+            for (const b of bills) {
+                billsIndex[b.id] = b;
+            }
+            for (const p of pos) {
+                if (!billsIndex[p.lid]) continue;
+                if (!billsIndex[p.lid].pos) billsIndex[p.lid].pos = [];
+                billsIndex[p.lid].pos.push(p);
+            }
+
             const said = this.ctx.session.sessionUser.accountId;
             for (const d of data) {
-                const b = bills.find(x => { return x.id === d.lid });
+                const b = billsIndex[d.lid]; //bills.find(x => { return x.id === d.lid });
                 if (!b || b.settle_status === settleStatus.finish) continue;
 
                 const nbs = {
@@ -254,7 +264,7 @@ module.exports = app => {
                 };
                 if (d.pos) {
                     for (const bp of d.pos) {
-                        const p = pos.find(x => { return x.id === bp.pid});
+                        const p = b.pos.find(x => { return x.id === bp.pid});
                         if (!p || p.settle_status === settleStatus.finish) continue;
 
                         const nps = { tid: stage.tid, sid: stage.id, said, lid: b.id, pid: p.id, times: 1, order: 0 };

+ 28 - 1
app/service/sub_project.js

@@ -463,7 +463,34 @@ 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);
+        }
+
+        async getSubProjectByTender(pid, tenders, filterFolder = false) {
+            if (tenders.length === 0) return [];
+            const spids = this._.uniq(this._.map(tenders, 'spid'));
+            let result = await this.getAllDataByCondition({ where: { project_id: pid, is_delete: 0 } });
+            result = result.filter(x => {
+                if (x.is_folder) return !filterFolder;
+                if (!x.rela_tender) return false;
+                return this._.includes(spids, x.id);
+            });
+            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`, t.`spid` ' +
+                    '  FROM ?? As t ' +
+                    '  Left Join ?? As pa ' +
+                    '  ON t.`user_id` = pa.`id` ' +
+                    '  WHERE t.`project_id` = ? AND t.`spid` != ? 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`, t.`spid` ' +
+                    // '  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` = ?)' +
+                    ' AND t.`spid` != ? 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;

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

@@ -0,0 +1,203 @@
+<% 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="add-child" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增子节点"><i class="fa fa-sign-in" 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>

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

@@ -0,0 +1,55 @@
+<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="<% if (!isTender) { %>javascript:void(0);<% } else { %>/contract<% } %>" class="btn btn-sm btn-light<% if (!isTender) { %> active<% } %>">
+                        项目合同
+                    </a>
+                    <a href="<% if (isTender) { %>javascript:void(0);<% } else { %>/contract/tender<% } %>" class="btn btn-sm btn-light<% if (isTender) { %> 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">
+                <% 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)) %>'));
+    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>

+ 0 - 2
app/view/measure/compare.ejs

@@ -33,11 +33,9 @@
                 <div class="d-inline-block">
                     <button href="#cate-set" class="btn btn-sm btn-light text-primary" data-toggle="modal" data-target="#select-qi"><i class="fa fa-clone"></i> 选择比较期</button>
                 </div>
-                <% if (ctx.app.config.is_debug) { %>
                 <div class="d-inline-block ml-3">
                     <a id="exportExcel" class="btn btn-primary btn-sm" href="javascript: void(0)">导出清单汇总Excel</a>
                 </div>
-                <% } %>
                 <div class="d-inline-block ml-2">
                     <span>期数据来源:</span>
                     <div class="d-inline-block" style="vertical-align: middle">

+ 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',
+            },
+        },
     },
 };