Browse Source

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

# Conflicts:
#	app/controller/payment_controller.js
Tony Kang 2 years ago
parent
commit
054820b01e

+ 1 - 0
app/base/base_controller.js

@@ -74,6 +74,7 @@ class BaseController extends Controller {
                 message: postError,
             };
         }
+        this.ctx.menuList.sub_project.dispaly = this.ctx.session.sessionProject.showSubProj;
         this.ctx.menuList.budget.display = this.ctx.session.sessionProject.showBudget;
 
         try {

+ 5 - 102
app/controller/budget_controller.js

@@ -30,26 +30,15 @@ module.exports = app => {
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.budget.list),
                     auditConst,
                 };
-                renderData.budgetList = await ctx.service.budget.getBudget(ctx.session.sessionUser.is_admin);
+                renderData.budgetList = await ctx.service.subProject.getBudgetProject(ctx.session.sessionProject.id, ctx.session.sessionUser.accountId, ctx.session.sessionUser.is_admin);
                 renderData.budgetStd = await ctx.service.budgetStd.getDataByProjectId(ctx.session.sessionProject.id);
                 for (const bl of renderData.budgetList) {
-                    const std = renderData.budgetStd.find(x => { return x.id === bl.std_id; });
-                    bl.std_name = std ? std.name : '';
-                    bl.gu_tp = await ctx.service.budgetGu.getSumTp(bl.id);
-                    bl.gai_tp = await ctx.service.budgetGai.getSumTp(bl.id);
-                    bl.yu_tp = await ctx.service.budgetYu.getSumTp(bl.id);
+                    if (bl.is_folder) continue;
+                    bl.gu_tp = await ctx.service.budgetGu.getSumTp(bl.budget_id);
+                    bl.gai_tp = await ctx.service.budgetGai.getSumTp(bl.budget_id);
+                    bl.yu_tp = await ctx.service.budgetYu.getSumTp(bl.budget_id);
                 }
                 renderData.tenderList = await ctx.service.tender.getList4Select('stage');
-                const accountList = await ctx.service.projectAccount.getAllDataByCondition({
-                    where: { project_id: ctx.session.sessionProject.id, enable: 1 },
-                    columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
-                });
-                renderData.accountList = accountList;
-                renderData.accountGroup = accountGroup.map((item, idx) => {
-                    const groupList = accountList.filter(item => item.account_group === idx);
-                    return { groupName: item, groupList };
-                });
-                renderData.permissionConst = ctx.service.budgetPermission.PermissionConst;
                 renderData.categoryData = await this.ctx.service.category.getAllCategory(this.ctx.session.sessionProject.id);
                 await this.layout('budget/list.ejs', renderData, 'budget/list_modal.ejs');
             } catch (err) {
@@ -57,92 +46,6 @@ module.exports = app => {
             }
         }
 
-        async add(ctx) {
-            try {
-                const data = JSON.parse(ctx.request.body.data);
-                if (!data.name || data.name.length > 100) throw '项目名称有误';
-                if (!data.std_id) throw '概预算标准有误';
-                const result = await ctx.service.budget.add(data);
-                ctx.body = { err: 0, msg: '', data: result };
-            } catch (err) {
-                ctx.log(err);
-                ctx.ajaxErrorBody(err, '新建项目失败');
-            }
-        }
-
-        async del(ctx) {
-            try {
-                const data = JSON.parse(ctx.request.body.data);
-                if (!data.id) throw '参数有误';
-                const result = await ctx.service.budget.deleteBudgetNoBackup(data.id);
-                ctx.body = { err: 0, msg: '', data: result };
-            } catch(err) {
-                ctx.log(err);
-                ctx.ajaxErrorBody(err, '删除项目失败');
-            }
-        }
-
-        async save(ctx) {
-            try {
-                const data = JSON.parse(ctx.request.body.data);
-                if (!data.id) throw '参数有误';
-                let result = null;
-                if (data.name !== undefined) {
-                    if (!data.name || data.name.length > 100) throw '项目名称有误';
-                    result = await ctx.service.budget.save({ id: data.id, name: data.name });
-                } else if (data.rela_tender !== undefined) {
-                    result = await ctx.service.budget.save({ id: data.id, rela_tender: data.rela_tender });
-                }
-                ctx.body = { err: 0, msg: '', data: result };
-            } catch(err) {
-                ctx.log(err);
-                ctx.ajaxErrorBody(err, '保存数据失败');
-            }
-        }
-
-        async rela(ctx) {
-            try {
-                const id = parseInt(ctx.query.id);
-                const budgetList = await ctx.service.budget.getBudget(true);
-                const otherBudget = budgetList.filter(x => { return x.id !== id || !x.rela_tender });
-                const _ = ctx.helper._;
-                const otherRela = _.map(_.map(otherBudget, 'rela_tender').join(',').split(','), _.toInteger);
-                const tenderList = await ctx.service.tender.getList4Select('stage');
-                ctx.body = {
-                    err: 0,
-                    msg: '',
-                    data: tenderList.filter(x => { return otherRela.indexOf(x.id) === -1})
-                        .map(y => { return {id: y.id, name: y.name, lastStageOrder: y.lastStage.order, lastStageStatus: auditConst.stage.statusString[y.lastStage.status], category: y.category}}),
-                };
-            } catch (err) {
-                ctx.log(err);
-                ctx.postError(err, '获取数据失败');
-            }
-        }
-
-        async member(ctx) {
-            try {
-                const data = JSON.parse(ctx.request.body.data);
-                const member = await ctx.service.budgetPermission.getBudgetPermission(data.id);
-                ctx.body = { err: 0, msg: '', data: member };
-            } catch (err) {
-                ctx.log(err);
-                ctx.ajaxErrorBody(err, '查询项目成员失败');
-            }
-        }
-
-        async memberSave(ctx) {
-            try {
-                const data = JSON.parse(ctx.request.body.data);
-                if (!data.id) throw '参数有误';
-                await ctx.service.budgetPermission.saveBudgetPermission(data.id, data.member);
-                ctx.body = { err: 0, msg: '', data: '' };
-            } catch (err) {
-                ctx.log(err);
-                ctx.ajaxErrorBody(err, '保存数据失败');
-            }
-        }
-
         async compare(ctx) {
             try {
                 const renderData = {

+ 37 - 0
app/controller/file_controller.js

@@ -0,0 +1,37 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2021/10/27
+ * @version
+ */
+const auditConst = require('../const/audit');
+module.exports = app => {
+    class BudgetController extends app.BaseController {
+
+        /**
+         * 概算投资
+         *
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async index(ctx) {
+            try {
+                const renderData = {
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.file.index),
+                    auditConst,
+                };
+                renderData.projectList = await ctx.service.subProject.getFileProject(ctx.session.sessionProject.id, ctx.session.sessionUser.accountId, ctx.session.sessionUser.is_admin);
+                renderData.tenderList = await ctx.service.tender.getList4Select('stage');
+                renderData.categoryData = await this.ctx.service.category.getAllCategory(this.ctx.session.sessionProject.id);
+                await this.layout('file/index.ejs', renderData, 'file/modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+            }
+        }
+    }
+
+    return BudgetController;
+};

+ 8 - 2
app/controller/payment_controller.js

@@ -207,9 +207,11 @@ module.exports = app => {
                 const id = parseInt(ctx.params.id);
                 if (!id) throw '参数错误';
                 const info = await ctx.service.paymentDetail.getDataById(id);
-
+                const rptTpl = await ctx.service.rptTpl.getDataById(3030);
+                const pageRst = ctx.service.jpcReport.getAllPreviewPagesCommon(rptTpl, 'A4');
                 const renderData = {
                     info,
+                    pageRst,
                 };
                 await this.layout('payment/detail.ejs', renderData);
             } catch (err) {
@@ -236,12 +238,16 @@ module.exports = app => {
                         // 根据模板ID获取报表JSON
                         //* 
                         const pageRst = ctx.service.jpcReport.getAllPreviewPagesCommon(rptTpl, 'A4');
+<<<<<<< HEAD
                         /* /
                         //--------------------
                         params.rpt_tpl_id = rpt.ID;
                         const actRptTpl = JSON.parse(rptTpl.rpt_content);
                         const pageRst = await ctx.service.jpcReport.getAllPagesCommon(ctx, actRptTpl, params, JV.PAGING_OPTION_NORMAL, JV.OUTPUT_TYPE_NORMAL, this.app.baseDir, null);
                         // */
+=======
+                        rptTplList.push(pageRst.items[0]);
+>>>>>>> 35a81d9688de9c26a0cda92a957e370bc8a7f4d6
                     }
                 }
                 const renderData = {
@@ -250,7 +256,7 @@ module.exports = app => {
                     rptProjectList,
                     rptTplList,
                 };
-                await this.layout('payment/process.ejs', renderData);
+                await this.layout('payment/process.ejs', renderData, 'payment/process_modal.ejs');
             } catch (err) {
                 console.log(err);
                 this.log(err);

+ 160 - 0
app/controller/sub_proj_controller.js

@@ -0,0 +1,160 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+const auditConst = require('../const/audit');
+const accountGroup = require('../const/account_group').group;
+module.exports = app => {
+    class SubProjController extends app.BaseController {
+
+        /**
+         * 概算投资
+         *
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async index(ctx) {
+            try {
+                const renderData = {
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.subProject.list),
+                    auditConst,
+                };
+                renderData.budgetStd = await ctx.service.budgetStd.getDataByProjectId(ctx.session.sessionProject.id);
+                renderData.projectList = await ctx.service.subProject.getSubProject(ctx.session.sessionProject.id, ctx.session.sessionUser.accountId, ctx.session.sessionUser.is_admin);
+                renderData.tenderList = await ctx.service.tender.getList4Select('stage');
+                const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                    where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                    columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                });
+                renderData.accountList = accountList;
+                renderData.accountGroup = accountGroup.map((item, idx) => {
+                    const groupList = accountList.filter(item => item.account_group === idx);
+                    return { groupName: item, groupList };
+                });
+                renderData.permissionConst = ctx.service.subProjPermission.PermissionConst;
+                renderData.categoryData = await this.ctx.service.category.getAllCategory(this.ctx.session.sessionProject.id);
+                await this.layout('sub_proj/index.ejs', renderData, 'sub_proj/modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+            }
+        }
+
+        async addFolder(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.name || data.name.length > 100) throw '文件夹名称有误';
+                const result = await ctx.service.subProject.addFolder(data);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '新建文件夹失败');
+            }
+        }
+
+        async addProj(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.name || data.name.length > 100) throw '项目名称有误';
+                const result = await ctx.service.subProject.addSubProject(data);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '新建项目失败');
+            }
+        }
+
+        async dragTo(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.drag_id || !data.drop_id) throw '提交数据错误';
+                const result = await ctx.service.subProject.dragTo(data);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '调整所属文件夹失败');
+            }
+        }
+
+        async del(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.id) throw '参数有误';
+                const result = await ctx.service.subProject.del(data.id);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '删除项目失败');
+            }
+        }
+
+        async save(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.id) throw '参数有误';
+                let result = null;
+                if (data.name !== undefined) {
+                    if (!data.name || data.name.length > 100) throw '项目名称有误';
+                    result = await ctx.service.subProject.save({ id: data.id, name: data.name });
+                } else if (data.rela_tender !== undefined) {
+                    result = await ctx.service.subProject.setRelaTender({ id: data.id, rela_tender: data.rela_tender });
+                } else if (data.std_id !== undefined) {
+                    result = await ctx.service.subProject.setBudgetStd({ id: data.id, std_id: data.std_id });
+                }
+                ctx.body = { err: 0, msg: '', data: { update: [result] } };
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '保存数据失败');
+            }
+        }
+
+        async rela(ctx) {
+            try {
+                const id = ctx.query.id;
+                const projectList = await ctx.service.subProject.getSubProject(this.ctx.session.sessionProject.id, this.ctx.session.sessionUser.accountId, true);
+                const otherProj = projectList.filter(x => { return x.id !== id || !x.rela_tender || x.is_folder });
+                const _ = ctx.helper._;
+                const otherRela = _.map(_.map(otherProj, 'rela_tender').join(',').split(','), _.toInteger);
+                const tenderList = await ctx.service.tender.getList4Select('stage');
+                ctx.body = {
+                    err: 0,
+                    msg: '',
+                    data: tenderList.filter(x => { return otherRela.indexOf(x.id) === -1})
+                        .map(y => { return {id: y.id, name: y.name, lastStageOrder: y.lastStage.order, lastStageStatus: auditConst.stage.statusString[y.lastStage.status], category: y.category}}),
+                };
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '获取数据失败');
+            }
+        }
+
+        async member(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const member = await ctx.service.subProjPermission.getPermission(data.id);
+                ctx.body = { err: 0, msg: '', data: member };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '查询项目成员失败');
+            }
+        }
+
+        async memberSave(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.id) throw '参数有误';
+                await ctx.service.subProjPermission.savePermission(data.id, data.member);
+                ctx.body = { err: 0, msg: '', data: '' };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '保存数据失败');
+            }
+        }
+    }
+
+    return SubProjController;
+};

+ 2 - 2
app/middleware/budget_check.js

@@ -27,9 +27,9 @@ module.exports = options => {
             if (this.session.sessionUser.is_admin) {
                 this.budget.readOnly = false;
             } else {
-                const bp = yield this.service.budgetPermission.getBudgetUserPermission(id);
+                const bp = yield this.service.subProjPermission.getBudgetUserPermission(id);
                 if (!bp) throw '您无权查看该项目';
-                this.budget.readOnly = bp.permission.indexOf(this.service.budgetPermission.PermissionConst.edit.value) < 0;
+                this.budget.readOnly = bp.permission.indexOf(this.service.subProjPermission.PermissionConst.budget.edit.value) < 0;
             }
             yield next;
         } catch (err) {

+ 3 - 6
app/middleware/session_auth.js

@@ -59,10 +59,12 @@ module.exports = options => {
             // 判断是否有权限查看支付审批
             let showPayment = 0;
             if (sessionUser.is_admin) {
+                this.session.sessionProject.showSubProj = true;
                 this.session.sessionProject.showBudget = true;
                 showPayment = 1;
             } else {
-                this.session.sessionProject.showBudget = yield this.service.budgetPermission.showBudget(sessionUser.accountId);
+                this.session.sessionProject.showSubProj = false;
+                this.session.sessionProject.showBudget = yield this.service.subProjPermission.showBudget(sessionUser.accountId);
                 // const grounpInfo = yield this.service.paymentPermissionAudit.getGroupInfo(projectData.id, accountInfo.account_group);
                 // if (grounpInfo) {
                 //     showPayment = 1;
@@ -74,11 +76,6 @@ module.exports = options => {
                 // }
             }
             this.session.sessionProject.showPayment = showPayment;
-            // if (sessionUser.is_admin) {
-            //     this.session.sessionProject.showBudget = true;
-            // } else {
-            //     this.session.sessionProject.showBudget = yield this.service.budgetPermission.showBudget(sessionUser.accountId);
-            // }
 
             // 同步消息
             yield this.service.notify.syncNotifyData();

+ 83 - 166
app/public/js/budget_list.js

@@ -9,179 +9,94 @@
  */
 let curBudget = {};
 
-const budgetNameChange = function (obj) {
-    if (obj.value.length > 100) {
-        obj.classList.add('is-invalid');
-    } else {
-        obj.classList.remove('is-invalid');
-    }
-};
-
-const addBudget = function () {
-    const name = $('#add-budget-name').val();
-    if (!name || name.length > 100) return;
-    const std_id = parseInt($('[name=std_id]:checked').val());
-    postData('/budget/add', { name, std_id }, function () {
-        window.location.reload();
-    });
-};
-
 const showModal = function (obj) {
     const tr = obj.parentNode.parentNode;
-    curBudget.id = tr.getAttribute('bid');
+    curBudget.id = tr.getAttribute('tree_id');
+    curBudget.bid = tr.getAttribute('bid');
     curBudget.name = tr.getAttribute('bname');
     curBudget.rela_tender = tr.getAttribute('rela-tender');
     $(obj.getAttribute('data-target')).modal('show');
 };
 
-const saveBudget = function () {
-    const name = $('#modify-budget-name').val();
-    if (!name || name.length > 100) return;
-    postData('/budget/save', { id: curBudget.id, name}, function () {
-        window.location.reload();
-    })
-};
-
-const delBudget = function () {
-    postData('/budget/del', { id: curBudget.id }, function () {
-        window.location.reload();
-    });
-};
-
-
 $(document).ready(() => {
     autoFlashHeight();
-    $('#modify-budget').on('show.bs.modal', () => {
-        $('#modify-budget-name').val(curBudget.name);
-    });
-    $('#del-budget').on('show.bs.modal', () => {
-        $('#del-budget-name').text(curBudget.name);
-    });
-
-    let timer = null;
-    let oldSearchVal = null;
-    $('#member-search').bind('input propertychange', function(e) {
-        oldSearchVal = e.target.value;
-        timer && clearTimeout(timer);
-        timer = setTimeout(() => {
-            const newVal = $('#member-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>`
-                    });
-                $('.book-list').empty();
-                $('.book-list').append(html);
-            } else {
-                if (!$('.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>';
-                    });
-                    $('.book-list').empty();
-                    $('.book-list').append(html);
+    const budgetTreeObj = (function(setting){
+        const budgetTree = createDragTree(setting.treeSetting);
+        budgetTree.loadDatas(setting.source);
+        const TableObj = $(setting.table);
+
+        const Utils = {
+            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="收起" 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('<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="/budget/${node.budget_id}/compare" name="name" id="${node.id}">`, node.name, '</a>');
                 }
-            }
-        }, 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
-    });
-    let orgMembers, newMembers;
-    const generateMemberHtml = function () {
-        const html = [];
-        for (const mem of newMembers) {
-            html.push('<tr>');
-            html.push(`<td>${mem.name}</td>`);
-            html.push(`<td>${mem.role}</td>`);
-            // const view = mem.permission.indexOf(permissionConst.view.value) >= 0;
-            // html.push(`<td><div class="custom-control custom-checkbox mb-2">
-            //             <input type="checkbox" id="view${mem.uid}" ptype="view" uid="${mem.uid}" class="custom-control-input" ${(view ? 'checked' : '')}>
-            //             <label class="custom-control-label" for="view${mem.uid}"></label></div></td>`);
-            const edit = mem.permission.indexOf(permissionConst.edit.value) >= 0;
-            html.push(`<td><div class="custom-control custom-checkbox mb-2">
-                        <input type="checkbox" id="edit${mem.uid}" ptype="edit" uid="${mem.uid}" class="custom-control-input" ${(edit ? 'checked' : '')}>
-                        <label class="custom-control-label" for="edit${mem.uid}"></label></div></td>`);
-            html.push(`<td><a href="javascript: void(0);" class="btn btn-outline-danger btn-sm ml-1" name="del-member" uid="${mem.uid}">移除</a></td>`);
-            html.push('</tr>');
-        }
-        $('#member-list').html(html.join(''));
-    };
-    $('[data-target="#member"]').click(function(){
-        const tr = this.parentNode.parentNode;
-        curBudget.id = tr.getAttribute('bid');
-        curBudget.name = tr.getAttribute('bname');
-        curBudget.rela_tender = tr.getAttribute('rela-tender');
-        postData(window.location.pathname + '/member', curBudget, function (result) {
-            orgMembers = result;
-            newMembers = result;
-            generateMemberHtml();
-            $('#member').modal('show');
-        });
-    });
-    $('dl').on('click', 'dd', function () {
-        const auditorId = parseInt($(this).data('id'));
-        const user = accountList.find(x => { return x.id === auditorId; });
-        const check = $(`tr[uid=${auditorId}]`, '#member-list');
-        if (check.length > 0) {
-            toastr.error('请勿重复添加成员');
-            return;
-        }
-        newMembers.push({
-            uid: user.id,
-            name: user.name,
-            role: user.role,
-            permission: [1],
-        });
-        generateMemberHtml();
-    });
-    $('#member').on('click', 'a[name="del-member"]', function () {
-        const id = parseInt(this.getAttribute('uid'));
-        newMembers.splice(newMembers.findIndex(x => { return x.uid === id}), 1);
-        generateMemberHtml();
-    });
-    $('#member-ok').click(() => {
-        postData(window.location.pathname + '/member-save', {id: curBudget.id, member: newMembers}, function () {
-            $('#member').modal('hide');
-        })
-    });
-    $('#member-list').on('click', 'input', function () {
-        const id = parseInt(this.getAttribute('uid'));
-        const mem = newMembers.find(x => { return x.uid === id});
-        if (this.checked) {
-            mem.permission.push(parseInt(permissionConst[this.getAttribute('ptype')].value));
-        } else {
-            mem.permission.splice(mem.permission.indexOf(permissionConst[this.getAttribute('ptype')].value), 1);
-        }
+                html.push('</td>');
+                // 概预算标准
+                if (node.is_folder) {
+                    html.push(`<td class="text-center"></td>`);
+                } else {
+                    html.push(`<td class="text-center">${node.std_name}</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-right">${node.gu_tp || ''}</td>`);
+                html.push(`<td class="text-right">${node.gai_tp || ''}</td>`);
+                html.push(`<td class="text-right">${node.yu_tp || ''}</td>`);
+                // 操作
+                if (node.is_folder) {
+                    html.push(`<td></td>`);
+                } else {
+                    html.push(`<td>`);
+                    html.push('<button class="btn btn-outline-primary btn-sm" data-target="#select-rela" name="del" onclick="showModal(this);">关联标段</button>');
+                    html.push('</td>');
+                }
+                return html.join('');
+            },
+            getNodeTrHtml: function (node, tree) {
+                const html = [];
+                html.push(`<tr tree_id="${node.id}" bid="${node.budget_id}" bname="${node.name}" rela-tender="${node.rela_tender}">`);
+                html.push(Utils.getRowTdHtml(node, tree));
+                html.push(`</tr>`);
+                return html.join('');
+            },
+            reloadTable: function () {
+                const html = [];
+                for (const node of budgetTree.nodes) {
+                    html.push(Utils.getNodeTrHtml(node, budgetTree));
+                }
+                TableObj.html(html.join(''));
+            },
+            getSelectNode: function() {
+                const selectId = $('tr.table-active').attr('tree_id');
+                return selectId ? budgetTree.getItems(selectId) : null;
+            },
+            getSelectNodeId: function() {
+                const selectId = $('tr.table-active').attr('tree_id');
+                return selectId || setting.treeSetting.rootId;
+            },
+        };
+
+        Utils.reloadTable();
+        return { budgetTree, TableObj, ...Utils };
+    })({
+        treeSetting: { id: 'id', pid: 'tree_pid', level: 'tree_level', order: 'tree_order', rootId: '-1' },
+        source: budgetList,
+        table: '#budgetList',
     });
 
     class srObject {
@@ -231,8 +146,8 @@ $(document).ready(() => {
             });
             $('#select-rela-ok').click(() => {
                 const rela = self.getSelects();
-                postData('/budget/save', { id: curBudget.id, rela_tender: rela.join(',') }, function () {
-                    $(`[bid=${curBudget.id}]`)[0].setAttribute('rela-tender', rela.join(','));
+                postData('/subproj/save', { id: curBudget.id, rela_tender: rela.join(',') }, function () {
+                    $(`[bid=${curBudget.bid}]`)[0].setAttribute('rela-tender', rela.join(','));
                     $('#select-rela').modal('hide');
                 });
             });
@@ -254,13 +169,15 @@ $(document).ready(() => {
         init() {
             $('#sr-select-all')[0].checked = false;
             const self = this;
-            postData(`/budget/rela?id=${curBudget.id}`, {}, tenders => {
+            postData(`/subproj/rela?id=${curBudget.id}`, {}, tenders => {
                 const rela = curBudget.rela_tender ? curBudget.rela_tender.split(',') : [];
+                console.log(rela);
                 self.selectTree = Tender2Tree.convert(category, tenders, null, null, function (node, source) {
                     node.lastStageOrder = `第${source.lastStageOrder}期`;
                     node.lastStageStatus = source.lastStageStatus;
                 });
                 for (const node of self.selectTree.nodes) {
+                    console.log(node);
                     node.selected = rela.indexOf(node.tid + '') >= 0;
                 }
                 SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.selectTree);

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

@@ -33,7 +33,7 @@ function Draw(canvas, degree, config = {}) {
     canvas.height = height;
   }
 
-  context.lineWidth = 6;
+  context.lineWidth = 8;
   context.strokeStyle = 'black';
   context.lineCap = 'round';
   context.lineJoin = 'round';

+ 181 - 0
app/public/js/file.js

@@ -0,0 +1,181 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+let curProject = {};
+
+const showModal = function (obj) {
+    const tr = obj.parentNode.parentNode;
+    curProject.id = tr.getAttribute('tree_id');
+    curProject.name = tr.getAttribute('pname');
+    curProject.rela_tender = tr.getAttribute('rela-tender');
+    $(obj.getAttribute('data-target')).modal('show');
+};
+
+$(document).ready(() => {
+    autoFlashHeight();
+    const projectTreeObj = (function(setting){
+        const projectTree = createDragTree(setting.treeSetting);
+        projectTree.loadDatas(setting.source);
+        const TableObj = $(setting.table);
+
+        const Utils = {
+            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="收起" 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('<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="/file/${node.id}/compare" name="name" id="${node.id}">`, node.name, '</a>');
+                }
+                html.push('</td>');
+                // 管理单位
+                html.push(`<td class="text-center">${node.management}</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>`);
+                }
+                // 操作
+                if (node.is_folder) {
+                    html.push(`<td></td>`);
+                } else {
+                    html.push(`<td>`);
+                    html.push('<button class="btn btn-outline-primary btn-sm" data-target="#select-rela" name="del" onclick="showModal(this);">关联标段</button>');
+                    html.push('</td>');
+                }
+                return html.join('');
+            },
+            getNodeTrHtml: function (node, tree) {
+                const html = [];
+                html.push(`<tr tree_id="${node.id}" pname="${node.name}" rela-tender="${node.rela_tender}">`);
+                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;
+            },
+        };
+
+        Utils.reloadTable();
+        return { projectTree, TableObj, ...Utils };
+    })({
+        treeSetting: { id: 'id', pid: 'tree_pid', level: 'tree_level', order: 'tree_order', rootId: '-1' },
+        source: projectList,
+        table: '#projectList',
+    });
+
+    class srObject {
+        constructor() {
+            const self = this;
+            this.selectTree = null;
+            const srSpreadSetting = {
+                cols: [
+                    {title: '选择', field: 'selected', hAlign: 1, width: 40, formatter: '@', cellType: 'checkbox'},
+                    {title: '名称', field: 'name', hAlign: 0, width: 180, formatter: '@', cellType: 'tree'},
+                    {title: '期数', field: 'lastStageOrder', hAlign: 1, width: 60, formatter: '@'},
+                    {title: '审批状态', field: 'lastStageStatus', hAlign: 1, width: 60, formatter: '@'},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                headColWidth: [30],
+                selectedBackColor: '#fffacd',
+                readOnly: true,
+            };
+            this.spread = SpreadJsObj.createNewSpread($('#sr-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            SpreadJsObj.initSheet(this.sheet, srSpreadSetting);
+
+            this.spread.bind(spreadNS.Events.ButtonClicked, function (e, info) {
+                if (!info.sheet.zh_setting) return;
+
+                const col = info.sheet.zh_setting.cols[info.col];
+                if (col.field !== 'selected') return;
+
+                const node = SpreadJsObj.getSelectObject(info.sheet);
+                self.selectNode(node, !node[col.field]);
+                SpreadJsObj.reloadColData(info.sheet, 0);
+            });
+
+
+
+            $('#sr-select-all').click(function () {
+                if (!self.selectTree) return;
+                for (const n of self.selectTree.nodes) {
+                    n.selected = this.checked;
+                }
+                SpreadJsObj.reloadColData(self.sheet, 0);
+            });
+            $('#select-rela-ok').click(() => {
+                const rela = self.getSelects();
+                postData('/subproj/save', { id: curProject.id, rela_tender: rela.join(',') }, function () {
+                    $(`[tree_id=${curProject.id}]`)[0].setAttribute('rela-tender', rela.join(','));
+                    $('#select-rela').modal('hide');
+                });
+            });
+        }
+        selectNode(node, select) {
+            const posterity = this.selectTree.getPosterity(node);
+            posterity.unshift(node);
+            for (const p of posterity) {
+                p.selected = select;
+            }
+        }
+        getSelects() {
+            const select = [];
+            for (const n of this.selectTree.nodes) {
+                if ((!n.children || n.children.length === 0) && n.selected) select.push(n.tid);
+            }
+            return select;
+        }
+        init() {
+            $('#sr-select-all')[0].checked = false;
+            const self = this;
+            postData(`/subproj/rela?id=${curProject.id}`, {}, tenders => {
+                const rela = curProject.rela_tender ? curProject.rela_tender.split(',') : [];
+                self.selectTree = Tender2Tree.convert(category, tenders, null, null, function (node, source) {
+                    node.lastStageOrder = `第${source.lastStageOrder}期`;
+                    node.lastStageStatus = source.lastStageStatus;
+                });
+                for (const node of self.selectTree.nodes) {
+                    node.selected = rela.indexOf(node.tid + '') >= 0;
+                }
+                SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.selectTree);
+            });
+        }
+    }
+    let srSelect;
+    $('#select-rela').on('shown.bs.modal', () => {
+        if (!srSelect) srSelect = new srObject();
+        srSelect.init();
+    });
+});

+ 444 - 0
app/public/js/shares/drag_tree.js

@@ -0,0 +1,444 @@
+const createDragTree = function (setting) {
+    class DragTree {
+        /**
+         * 构造函数
+         */
+        constructor(setting) {
+            // 无索引
+            this.datas = [];
+            // 以key为索引indexedDB
+            this.items = {};
+            // 以排序为索引
+            this.nodes = [];
+            // 根节点
+            this.children = [];
+            // 树设置
+            this.setting = setting;
+        }
+        /**
+         * 树结构根据显示排序
+         */
+        sortTreeNode(isResort) {
+            const self = this;
+            const addSortNodes = function (nodes) {
+                if (!nodes) { return }
+                for (let i = 0; i < nodes.length; i++) {
+                    self.nodes.push(nodes[i]);
+                    nodes[i].index = self.nodes.length - 1;
+                    if (!isResort) {
+                        nodes[i].children = self.getChildren(nodes[i]);
+                    } else {
+                        nodes[i].children.sort((a, b) => { return a[self.setting.order] - b[self.setting.order]; })
+                    }
+                    addSortNodes(nodes[i].children);
+                }
+            };
+            this.nodes = [];
+            if (!isResort) {
+                this.children = this.getChildren();
+            } else {
+                this.children.sort((a, b) => { return a[self.setting.order] - b[self.setting.order]; });
+            }
+            addSortNodes(this.children);
+        }
+        /**
+         * 加载数据(初始化), 并给数据添加部分树结构必须数据
+         * @param datas
+         */
+        loadDatas(datas) {
+            const self = this;
+            // 清空旧数据
+            this.items = {};
+            this.nodes = [];
+            this.datas = [];
+            this.children = [];
+            // 加载全部数据
+            datas.sort(function (a, b) {
+                return a[self.setting.level] - b[self.setting.level];
+            });
+            for (const data of datas) {
+                const keyName = itemsPre + data[this.setting.id];
+                if (this.items[keyName]) continue;
+
+                const item = JSON.parse(JSON.stringify(data));
+                item.children = [];
+                item.expanded = true;
+                item.visible = true;
+                if (item[setting.pid] === setting.rootId) {
+                    this.children.push(item);
+                } else {
+                    const parent = this.getParent(item);
+                    if (!parent) continue;
+                    parent.children.push(item);
+                }
+                this.items[keyName] = item;
+                this.datas.push(item);
+            }
+            this.children.sort((a, b) => { return a[self.setting.order] - b[self.setting.order]; });
+            this.sortTreeNode(true);
+        }
+
+        getItemsByIndex(index) {
+            return this.nodes[index];
+        }
+        /**
+         * 根据id获取树结构节点数据
+         * @param {Number} id
+         * @returns {Object}
+         */
+        getItems(id) {
+            return this.items[itemsPre + id];
+        };
+        getNodeIndex(node) {
+            return this.nodes.indexOf(node);
+        }
+        /**
+         * 查找node的parent
+         * @param {Object} node
+         * @returns {Object}
+         */
+        getParent(node) {
+            return this.getItems(node[this.setting.pid]);
+        };
+        getTopParent(node) {
+            const parents = this.getAllParents(node);
+            parents.sort((a, b) => { return a.level - b.level; });
+            return parents[0];
+        };
+        getAllParents(node) {
+            const parents = [];
+            if (!node) return parents;
+
+            let vP = this.getParent(node);
+            while (vP) {
+                parents.push(vP);
+                vP = this.getParent(vP);
+            }
+            return parents;
+        }
+
+        /**
+         * 查找node的前兄弟节点
+         * @param node
+         * @returns {*}
+         */
+        getPreSiblingNode(node) {
+            if (!node) return null;
+            const parent = this.getParent(node);
+            const siblings = parent ? parent.children : this.children;
+            const index = siblings.indexOf(node);
+            return (index > 0) ? siblings[index - 1] : null;
+        }
+        /**
+         * 查找node的后兄弟节点
+         * @param node
+         * @returns {*}
+         */
+        getNextSiblingNode(node) {
+            const parent = this.getParent(node);
+            const siblings = parent ? parent.children : this.children;
+            const index = siblings.indexOf(node);
+            if (index >= 0 && index < siblings.length - 1) {
+                return siblings[index + 1];
+            } else {
+                return null;
+            }
+        }
+
+        /**
+         * 查询node的已下载子节点
+         * @param {Object} node
+         * @returns {Array}
+         */
+        getChildren(node) {
+            const setting = this.setting;
+            const pid = node ? node[setting.id] : setting.rootId;
+            const children = this.datas.filter(function (x) {
+                return x[setting.pid] === pid;
+            });
+            children.sort((a, b) => { return a[setting.order] - b[setting.order]; });
+            return children;
+        };
+        /**
+         * 递归方式 查询node的已下载的全部后代 (兼容full_path不存在的情况)
+         * @param node
+         * @returns {*}
+         * @private
+         */
+        _recursiveGetPosterity(node) {
+            let posterity = node.children;
+            for (const c of node.children) {
+                posterity = posterity.concat(this._recursiveGetPosterity(c));
+            }
+            return posterity;
+        };
+        /**
+         * 查询node的已下载的全部后代
+         * @param {Object} node
+         * @returns {Array}
+         */
+        getPosterity(node) {
+            const self = this;
+            const posterity = this._recursiveGetPosterity(node);
+            posterity.sort(function (x, y) {
+                return self.getNodeIndex(x) - self.getNodeIndex(y);
+            });
+            return posterity;
+        };
+
+        /**
+         * 查询node是否是父节点的最后一个子节点
+         * @param {Object} node
+         * @returns {boolean}
+         */
+        isLastSibling(node) {
+            const siblings = this.getChildren(this.getParent(node));
+            return (siblings && siblings.length > 0) ? node[this.setting.order] === siblings[siblings.length - 1][this.setting.order] : false;
+        };
+        /**
+         * 查询node是否是父节点的最后一个可见子节点
+         * @param {Object} node
+         * @returns {boolean}
+         */
+        isLastViewSibling(node) {
+            const siblings = (this.getChildren(this.getParent(node))).filter(x => { return !x.filter });
+            return (siblings && siblings.length > 0) ? node.order === siblings[siblings.length - 1].order : false;
+        };
+
+        /**
+         * 得到树结构构成id
+         * @param node
+         * @returns {*}
+         */
+        getNodeKey(node) {
+            return node[this.setting.id];
+        };
+        /**
+         * 刷新子节点是否可见
+         * @param {Object} node
+         * @private
+         */
+        _refreshChildrenVisible(node) {
+            if (!node.children) {
+                node.children = this.getChildren(node);
+            }
+            if (node.children && node.children.length > 0) {
+                for (const child of node.children) {
+                    child.visible = node.expanded && node.visible && !child.filter;
+                    this._refreshChildrenVisible(child);
+                }
+            }
+        };
+        /**
+         * 设置节点是否展开, 并控制子节点可见
+         * @param {Object} node
+         * @param {Boolean} expanded
+         */
+        setExpanded(node, expanded) {
+            node.expanded = expanded;
+            this._refreshChildrenVisible(node);
+        };
+        /**
+         * 递归 设置节点展开状态
+         * @param {Array} nodes - 需要设置状态的节点
+         * @param {Object} parent - nodes的父节点
+         * @param {Function} checkFun - 判断节点展开状态的方法
+         * @private
+         */
+        _recursiveExpand(nodes, parent, checkFun) {
+            for (const node of nodes) {
+                const expanded = checkFun(node);
+                if (node.expanded !== expanded) {
+                    node.expanded = expanded;
+                }
+                node.visible = parent ? (parent.expanded && parent.visible && !node.filter) : !node.filter;
+                this._recursiveExpand(node.children, node, checkFun);
+            }
+        }
+        /**
+         * 自定义展开规则
+         * @param checkFun
+         */
+        expandByCustom(checkFun) {
+            this._recursiveExpand(this.children, null, checkFun);
+            this._saveMarkExpandFold();
+        }
+        /**
+         * 展开到第几层
+         * @param {Number} level - 展开层数
+         */
+        expandByLevel(level) {
+            this.expandByCustom(function (n) {
+                return n.level < level;
+            });
+        }
+
+        /**
+         * 自动展开节点node
+         * @param node
+         * @returns {*}
+         */
+        autoExpandNode(node) {
+            const parents = this.getAllParents(node);
+            const reload = [];
+            for (const p of parents) {
+                if (!p.expanded) {
+                    reload.push(p);
+                    this.setExpanded(p, true);
+                }
+            }
+            return reload;
+        }
+
+        /**
+         * 加载数据(动态),只加载不同部分
+         * @param {Array} datas
+         * @return {Array} 加载到树的数据
+         * @privateA
+         */
+        _updateData(datas) {
+            datas = datas instanceof Array ? datas : [datas];
+            let loadedData = [];
+            for (const data of datas) {
+                let node = this.getItems(data[this.setting.id]);
+                if (node) {
+                    for (const prop in data) {
+                        if (data[prop] !== undefined && data[prop] !== node[prop]) {
+                            if (prop === this.setting.pid) {
+                                loadedData.push(this.getItems(node[this.setting.pid]));
+                                loadedData.push(this.getItems(data[this.setting.pid]));
+                            }
+                            if (prop === this.setting.order) {
+                                loadedData = loadedData.concat(this.getPosterity(node));
+                            }
+                            node[prop] = data[prop];
+                        }
+                    }
+                    loadedData.push(node);
+                }
+            }
+            loadedData = _.uniq(loadedData);
+            for (const node of loadedData) {
+                if (node) {
+                    node.children = this.getChildren(node);
+                    node.expanded = node.children.length === 0 ? true : node.children[0].visible;
+                } else {
+                    this.children = this.getChildren(null);
+                }
+            }
+            this.sortTreeNode(true);
+            return loadedData;
+        };
+        /**
+         * 加载数据(动态),只加载不同部分
+         * @param {Array} datas
+         * @return {Array} 加载到树的数据
+         * @privateA
+         */
+        _loadData(datas) {
+            datas = datas instanceof Array ? datas : [datas];
+            const loadedData = [], resortData = [];
+            for (const data of datas) {
+                let node = this.getItems(data[this.setting.id]);
+                if (node) {
+                    const parent = this.getItems(node[this.setting.pid]);
+                    for (const prop in data) {
+                        if (data[prop] !== undefined && data[prop] !== node[prop]) {
+                            node[prop] = data[prop];
+                            if (parent && resortData.indexOf(parent) === -1) {
+                                resortData.push(parent);
+                            }
+                        }
+                    }
+                    loadedData.push(node);
+                } else {
+                    const keyName = itemsPre + data[this.setting.id];
+                    const node = JSON.parse(JSON.stringify(data));
+                    this.items[keyName] = node;
+                    this.datas.push(node);
+                    node.expanded = true;
+                    node.visible = true;
+                    loadedData.push(node);
+                    if (resortData.indexOf(node) === -1) {
+                        resortData.push(node);
+                    }
+                    const parent = this.getItems(node[this.setting.pid]);
+                    if (parent && resortData.indexOf(parent) === -1) {
+                        resortData.push(parent);
+                    } else {
+                        resortData.push(this.setting.rootId);
+                    }
+                }
+            }
+            for (const node of resortData) {
+                if (node && node !== this.setting.rootId) {
+                    node.children = this.getChildren(node);
+                } else {
+                    this.children = this.getChildren(null);
+                }
+            }
+            this.sortTreeNode(true);
+            for (const node of loadedData) {
+                if (!node.expanded) {
+                    this.setExpanded(node, true);
+                }
+            }
+            return loadedData;
+        };
+        /**
+         * 清理数据(动态)
+         * @param datas
+         * @private
+         */
+        _freeData(datas) {
+            datas = datas instanceof Array ? datas : [datas];
+            const freeDatas = [];
+            const removeArrayData = function (array, data) {
+                const index = array.indexOf(data);
+                array.splice(index, 1);
+            };
+            for (const data of datas) {
+                const node = this.getItems(data[this.setting.id]);
+                if (node) {
+                    freeDatas.push(node);
+                    node.deleteIndex = this.nodes.indexOf(node);
+                    delete this.items[itemsPre + node[this.setting.id]];
+                    if (node[this.setting.pid] !== this.setting.rootId) {
+                        const parent = this.getItems(node[this.setting.pid]);
+                        if (parent) {
+                            removeArrayData(parent.children, node);
+                        }
+                    } else {
+                        removeArrayData(this.children, node);
+                    }
+                    removeArrayData(this.datas, node);
+                }
+            }
+            for (const node of freeDatas) {
+                removeArrayData(this.nodes, node);
+            }
+            return freeDatas;
+        };
+
+        /**
+         * 因为提交其他数据,引起的树结构数据更新,调用该方法
+         *
+         * @param data - 更新的数据 {update, create, delete}
+         * @returns {{}}
+         */
+        loadPostData(data) {
+            const result = {};
+            if (data.delete) {
+                result.delete = this._freeData(data.delete);
+            }
+            if (data.create) {
+                result.create = this._loadData(data.create);
+            }
+            if (data.update) {
+                result.update = this._updateData(data.update);
+            }
+            return result;
+        }
+    }
+    return new DragTree(setting);
+};

+ 435 - 0
app/public/js/sub_project.js

@@ -0,0 +1,435 @@
+
+const NameChange = function (obj) {
+    if (obj.value.length > 100) {
+        obj.classList.add('is-invalid');
+    } else {
+        obj.classList.remove('is-invalid');
+    }
+};
+
+$(document).ready(function() {
+    const projectTreeObj = (function(setting){
+        const ProjectTree = createDragTree(setting.treeSetting);
+        ProjectTree.loadDatas(setting.source);
+        const TableObj = $(setting.table);
+
+        const Utils = {
+            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="收起" 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('<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="javascript: void(0)" name="name" id="' + node.id + '">', node.name, '</a>');
+                }
+                html.push('</td>');
+                // 概预算标准
+                if (node.is_folder) {
+                    html.push(`<td class="text-center"></td>`);
+                } else {
+                    if (node.std_name) {
+                        html.push(`<td class="text-center">${node.std_name}</td>`);
+                    } else {
+                        html.push(`<td class="text-center"><button class="btn btn-outline-primary btn-sm ml-1" name="set-std">选择</button></td>`);
+                    }
+                }
+                // 创建时间
+                if (node.is_folder) {
+                    html.push(`<td class="text-center"></td>`);
+                    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.management || ''}</td>`);
+                }
+                // 操作
+                html.push(`<td>`);
+                html.push('<button class="btn btn-outline-primary btn-sm ml-1" name="edit">编辑</button>');
+                html.push('<button class="btn btn-outline-danger btn-sm ml-1" name="del">删除</button>');
+                if (!node.is_folder) html.push('<button class="btn btn-outline-primary btn-sm ml-1" name="member">成员管理</button>');
+                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;
+            },
+            refreshAddButton: function() {
+                const select = this.getSelectNode();
+                $('[href="#add-folder"]').attr('disabled', select && select.tree_level >= 4);
+            },
+            refreshTreeTable: function(result) {
+                ProjectTree.loadDatas(result);
+                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));
+                }
+            },
+            dropTo: function() {
+                console.log(Utils.dragNode.name, Utils.dropNode.name);
+                if (!Utils.dropNode.is_folder) {
+                    toastr.warning('请将文件夹或者项目拖动到目标文件夹下');
+                    return;
+                }
+                const posterity = ProjectTree.getPosterity(Utils.dragNode);
+                const maxLevelFolder = posterity.reduce((prev, cur) => { return cur.is_folder ? Math.max(cur.tree_level, prev) : prev; }, Utils.dragNode.tree_level);
+                const resultMaxLevelFolder = Utils.dropNode.tree_level + maxLevelFolder - Utils.dragNode.tree_level + 1;
+                if (resultMaxLevelFolder > 4) {
+                    toastr.warning('文件夹不可超过4层');
+                    return;
+                }
+                postData('/subproj/dragTo', { drag_id: Utils.dragNode.id, drop_id: Utils.dropNode.id }, function (result){
+                    Utils.refreshTreeTable(result);
+                });
+                delete Utils.dragNode;
+                delete Utils.dropNode;
+            },
+        };
+
+        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', 'button[name=del]', function(e) {
+            const treeId = $(this).parent().parent().attr('tree_id');
+            const node = ProjectTree.getItems(treeId);
+            if (node.is_folder) {
+                $('.modal-title', '#del').html('确认删除文件夹');
+                $('#del-hint').attr('tree_id', treeId).html(`确定删除「<strong style="word-break: break-word;">${node.name}</strong>」及其下所有内容?`)
+            } else {
+                $('.modal-title', '#del').html('确认删除标段');
+                $('#del-hint').attr('tree_id', treeId).html(`确定删除「<strong style="word-break: break-word;">${node.name}</strong>」?`)
+            }
+            $('#del').modal('show');
+        });
+        $('body').on('click', 'button[name=edit]', function(e) {
+            const treeId = $(this).parent().parent().attr('tree_id');
+            const node = ProjectTree.getItems(treeId);
+            $('#edit-project-name').attr('tree_id', treeId).val(node.name);
+            $('#edit-project').modal('show');
+        });
+        $('body').on('click', 'button[name=set-std]', function(e) {
+            const treeId = $(this).parent().parent().attr('tree_id');
+            const node = ProjectTree.getItems(treeId);
+            if (node.is_folder || node.std_id) return;
+            $('[name=std_id]').attr('tree_id', treeId);
+            $('#set-std').modal('show');
+        });
+        return { ProjectTree, TableObj, ...Utils };
+    })({
+        treeSetting: { id: 'id', pid: 'tree_pid', level: 'tree_level', order: 'tree_order', rootId: '-1' },
+        source: projectList,
+        table: '#projectList',
+    });
+
+    $('#add-folder').on('show.bs.modal', function(e) {
+        const select = projectTreeObj.getSelectNode();
+        if (select) {
+            if (select.is_folder) e.stopPropagation();
+            $('#folder-hint').html(`在文件夹 [${select.name}] 下新增`).show();
+        } else {
+            $('#folder-hint').hide();
+        }
+        $('#folder-name').val('');
+    });
+    $('#add-folder-ok').click(function() {
+        const tree_pid = projectTreeObj.getSelectNodeId();
+        const name = $('#folder-name').val();
+        if (!name || name.length > 100) return;
+        postData('/subproj/addFolder', { name, is_folder: 1, tree_pid }, function(result) {
+            projectTreeObj.refreshTreeTable(result);
+            $('#add-folder').modal('hide');
+        });
+    });
+
+    $('#add-project').on('show.bs.modal', function(e) {
+        const select = projectTreeObj.getSelectNode();
+        if (select) {
+            if (select.is_folder) e.stopPropagation();
+            $('#project-hint').html(`在文件夹 [${select.name}] 下新增`).show();
+        } else {
+            $('#project-hint').hide();
+        }
+        $('#project-name').val('');
+    });
+    $('#add-project-ok').click(function() {
+        const tree_pid = projectTreeObj.getSelectNodeId();
+        const name = $('#project-name').val();
+        if (!name || name.length > 100) return;
+        postData('/subproj/addProj', { name, is_folder: 1, tree_pid }, function(result) {
+            projectTreeObj.refreshTreeTable(result);
+            $('#add-project').modal('hide');
+        });
+    });
+    $('#del-ok').click(function() {
+        const id = $('#del-hint').attr('tree_id');
+        postData('/subproj/del', { id }, function(result) {
+            projectTreeObj.refreshTreeTable(result);
+            $('#del').modal('hide');
+            $('#del-hint').attr('tree_id', '');
+        });
+    });
+    $('#edit-project-ok').click(function() {
+        const nameObj = $('#edit-project-name');
+        const name = name.val();
+        if (!name || name.length > 100) return;
+        postData('/subproj/save', { id: nameObj.attr('tree_id'), name }, function(result) {
+            projectTreeObj.refreshRow(result);
+            $('#edit-project').modal('hide');
+            $('#edit-project-name').attr('tree_id', '');
+        });
+    });
+    $('#set-std-ok').click(function() {
+        const select = $('[name=std_id]:checked');
+        const id = select.attr('tree_id');
+        const std_id = parseInt(select.val());
+        postData('/subproj/save', { id, std_id }, function(result) {
+            projectTreeObj.refreshRow(result);
+            $('#set-std').modal('hide');
+            $('[name=std_id]').attr('tree_id', '');
+        });
+    });
+
+    let timer = null;
+    let oldSearchVal = null;
+    $('#member-search').bind('input propertychange', function(e) {
+        oldSearchVal = e.target.value;
+        timer && clearTimeout(timer);
+        timer = setTimeout(() => {
+            const newVal = $('#member-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>`
+                    });
+                $('.book-list').empty();
+                $('.book-list').append(html);
+            } else {
+                if (!$('.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>';
+                    });
+                    $('.book-list').empty();
+                    $('.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
+    });
+    let orgMembers, newMembers;
+    const generateMemberHtml = function () {
+        const html = [];
+        for (const mem of newMembers) {
+            html.push('<tr>');
+            html.push(`<td>${mem.name}</td>`);
+            html.push(`<td>${mem.role}</td>`);
+            // 动态投资
+            const viewBudget = mem.budget_permission.indexOf(permissionConst.budget.view.value) >= 0;
+            html.push(`<td><div class="custom-control custom-checkbox mb-2">
+                        <input type="checkbox" ptype="budget" sptype="view" uid="${mem.uid}" id="budgetview${mem.uid}" class="custom-control-input" ${(viewBudget ? 'checked' : '')}>
+                        <label class="custom-control-label" for="budgetview${mem.uid}"></label></div></td>`);
+            const editBudget = mem.budget_permission.indexOf(permissionConst.budget.edit.value) >= 0;
+            html.push(`<td><div class="custom-control custom-checkbox mb-2">
+                        <input type="checkbox" ptype="budgetEdit" sptype="edit" id="budgetedit${mem.uid}" uid="${mem.uid}" class="custom-control-input" ${(editBudget ? 'checked' : '')}>
+                        <label class="custom-control-label" for="budgetedit${mem.uid}"></label></div></td>`);
+            // 电子文档
+
+            const fileView = mem.file_permission.indexOf(permissionConst.file.view.value) >= 0;
+            html.push(`<td><div class="custom-control custom-checkbox mb-2">
+                        <input type="checkbox" ptype="file" sptype="view" uid="${mem.uid}" id="fileview${mem.uid}" class="custom-control-input" ${(fileView ? 'checked' : '')}>
+                        <label class="custom-control-label" for="fileview${mem.uid}"></label></div></td>`);
+            const fileUpload = mem.file_permission.indexOf(permissionConst.file.upload.value) >= 0;
+            html.push(`<td><div class="custom-control custom-checkbox mb-2">
+                        <input type="checkbox" ptype="file" sptype="upload" id="fileupload${mem.uid}" uid="${mem.uid}" class="custom-control-input" ${(fileUpload ? 'checked' : '')}>
+                        <label class="custom-control-label" for="fileupload${mem.uid}"></label></div></td>`);
+            const fileEdit = mem.file_permission.indexOf(permissionConst.file.edit.value) >= 0;
+            html.push(`<td><div class="custom-control custom-checkbox mb-2">
+                        <input type="checkbox" ptype="file" sptype="eidt" uid="${mem.uid}" id="fileedit${mem.uid}" class="custom-control-input" ${(fileEdit ? 'checked' : '')}>
+                        <label class="custom-control-label" for="fileedit${mem.uid}"></label></div></td>`);
+            // 关联标段
+            const rela = mem.manage_permission.indexOf(permissionConst.manage.rela.value) >= 0;
+            html.push(`<td><div class="custom-control custom-checkbox mb-2">
+                        <input type="checkbox" ptype="manage" sptype="rela" id="rela${mem.uid}" uid="${mem.uid}" class="custom-control-input" ${(rela ? 'checked' : '')}>
+                        <label class="custom-control-label" for="rela${mem.uid}"></label></div></td>`);
+            html.push(`<td><a href="javascript: void(0);" class="btn btn-outline-danger btn-sm ml-1" name="del-member" uid="${mem.uid}">移除</a></td>`);
+            html.push('</tr>');
+        }
+        $('#member-list').html(html.join(''));
+    };
+    $('[data-target="#member"]').click(function(){
+        const tr = this.parentNode.parentNode;
+        curBudget.id = tr.getAttribute('tree_id');
+        curBudget.bid = tr.getAttribute('bid');
+        curBudget.name = tr.getAttribute('bname');
+        curBudget.rela_tender = tr.getAttribute('rela-tender');
+
+    });
+
+    $('body').on('click', 'button[name=member]', function(e) {
+        const treeId = $(this).parent().parent().attr('tree_id');
+        const node = projectTreeObj.ProjectTree.getItems(treeId);
+        if (node.is_folder) return;
+        $('#member-ok').attr('tree_id', treeId);
+        postData('/subproj/member', { id: treeId }, function (result) {
+            orgMembers = result;
+            newMembers = result;
+            generateMemberHtml();
+            $('#member').modal('show');
+        });
+    });
+    $('dl').on('click', 'dd', function () {
+        const auditorId = parseInt($(this).data('id'));
+        const user = accountList.find(x => { return x.id === auditorId; });
+        const check = $(`tr[uid=${auditorId}]`, '#member-list');
+        if (check.length > 0) {
+            toastr.error('请勿重复添加成员');
+            return;
+        }
+        newMembers.push({
+            uid: user.id,
+            name: user.name,
+            role: user.role,
+            budget_permission: [],
+            file_permission: [],
+            manage_permission: [],
+        });
+        generateMemberHtml();
+    });
+    $('#member').on('click', 'a[name="del-member"]', function () {
+        const id = parseInt(this.getAttribute('uid'));
+        newMembers.splice(newMembers.findIndex(x => { return x.uid === id}), 1);
+        generateMemberHtml();
+    });
+    $('#member-list').on('click', 'input', function () {
+        const id = parseInt(this.getAttribute('uid'));
+        const mem = newMembers.find(x => { return x.uid === id});
+        const pType = this.getAttribute('ptype'), spType = this.getAttribute('sptype');
+        if (this.checked) {
+            if (pType === 'budget' && spType === 'view') {
+                mem.budget_permission.push(parseInt(permissionConst.budget.view.value));
+            } else if (pType === 'budget' && spType === 'edit') {
+                mem.budget_permission.push(parseInt(permissionConst.budget.view.value));
+                if (mem.budget_permission.indexOf(permissionConst.budget.view.value) < 0) {
+                    mem.budget_permission.push(parseInt(permissionConst.budget.edit.value));
+                    $(`#budgetview${id}`)[0].checked = true;
+                }
+            } else if (pType === 'file' && spType === 'view') {
+                mem.file_permission.push(parseInt(permissionConst.file.view.value));
+            } else if (pType === 'file' && spType === 'upload') {
+                mem.file_permission.push(parseInt(permissionConst.file.upload.value));
+                if (mem.file_permission.indexOf(permissionConst.file.view.value) < 0) {
+                    mem.file_permission.push(parseInt(permissionConst.file.view.value));
+                    $(`#fileview${id}`)[0].checked = true;
+                }
+            } else if (pType === 'file' && spType === 'edit') {
+                mem.file_permission.push(parseInt(permissionConst.file.view.value));
+                if (mem.file_permission.indexOf(permissionConst.file.view.value) < 0) {
+                    mem.file_permission.push(parseInt(permissionConst.file.view.value));
+                    $(`#fileview${id}`)[0].checked = true;
+                }
+            } else if (pType === 'manage' && spType === 'rela') {
+                mem.manage_permission.push(parseInt(permissionConst.manage.rela.value));
+            }
+        } else {
+            if (pType === 'budget' && spType === 'view') {
+                mem.budget_permission = [];
+                $(`#budgetedit${id}`)[0].checked = false;
+            } else if (pType === 'budget' && spType === 'edit') {
+                mem.budget_permission.splice(mem.budget_permission.indexOf(permissionConst.budget.edit.value), 1);
+            } else if (pType === 'file' && spType === 'view') {
+                mem.file_permission = [];
+                $(`#fileupload${id}`)[0].checked = false;
+                $(`#fileedit${id}`)[0].checked = false;
+            } else if (pType === 'file' && spType === 'upload') {
+                mem.file_permission.splice(mem.file_permission.indexOf(permissionConst.file.upload.value), 1);
+            } else if (pType === 'file' && spType === 'edit') {
+                mem.file_permission.splice(mem.file_permission.indexOf(permissionConst.file.edit.value), 1);
+            } else if (pType === 'manage' && spType === 'rela') {
+                mem.manage_permission = [];
+            }
+        }
+    });
+    $('#member-ok').click(function(){
+        const id = this.getAttribute('tree_id');
+        postData('/subproj/memberSave', {id, member: newMembers}, function () {
+            $('#member').modal('hide');
+        })
+    });
+});

+ 2 - 1
app/public/js/tender_list_manage.js

@@ -31,7 +31,8 @@ const tenderListSpec = (function(){
         html.push('<td style="width: 20%" tid="' + node.id + '">');
         if (!node.cid) {
             html.push('<a href="javascript: void(0)" name="edit" class="btn btn-outline-primary btn-sm">编辑</a>');
-            if (node.lastStage === null || node.lastStage === undefined) {
+            const hasStage = node.progress ? node.stage_count > 0 : !!node.lastStage;
+            if (!hasStage) {
                 html.push('<a href="javascript: void(0)" name="del" class="btn btn-outline-danger btn-sm ml-1">删除</a>');
             } else {
                 html.push('<button class="btn btn-outline-secondary btn-sm ml-1" data-toggle="tooltip" data-placement="top" title="请先删除所有期">删除</button>');

+ 11 - 6
app/router.js

@@ -693,14 +693,17 @@ module.exports = app => {
     app.get('/datacollect', sessionAuth, 'datacollectController.index');
     app.post('/datacollect/load', sessionAuth, 'datacollectController.loadData');
 
+    app.get('/subproj', sessionAuth, 'subProjController.index');
+    app.post('/subproj/addFolder', sessionAuth, projectManagerCheck, 'subProjController.addFolder');
+    app.post('/subproj/addProj', sessionAuth, projectManagerCheck, 'subProjController.addProj');
+    app.post('/subproj/dragTo', sessionAuth, projectManagerCheck, 'subProjController.dragTo');
+    app.post('/subproj/del', sessionAuth, projectManagerCheck, 'subProjController.del');
+    app.post('/subproj/save', sessionAuth, projectManagerCheck, 'subProjController.save');
+    app.post('/subproj/rela', sessionAuth, 'subProjController.rela');
+    app.post('/subproj/member', sessionAuth, projectManagerCheck, 'subProjController.member');
+    app.post('/subproj/memberSave', sessionAuth, projectManagerCheck, 'subProjController.memberSave');
     // 概算投资
     app.get('/budget', sessionAuth, 'budgetController.list');
-    app.post('/budget/add', sessionAuth, projectManagerCheck, 'budgetController.add');
-    app.post('/budget/del', sessionAuth, projectManagerCheck, 'budgetController.del');
-    app.post('/budget/save', sessionAuth, 'budgetController.save');
-    app.post('/budget/rela', sessionAuth, 'budgetController.rela');
-    app.post('/budget/member', sessionAuth, projectManagerCheck, 'budgetController.member');
-    app.post('/budget/member-save', sessionAuth, projectManagerCheck, 'budgetController.memberSave');
     app.get('/budget/:id/compare', sessionAuth, budgetCheck, 'budgetController.compare');
     app.post('/budget/:id/compare/load', sessionAuth, budgetCheck, 'budgetController.compareLoad');
     app.post('/budget/:id/compare/final', sessionAuth, budgetCheck, 'budgetController.compareFinal');
@@ -709,6 +712,8 @@ module.exports = app => {
     app.post('/budget/:id/:btype/update', sessionAuth, budgetCheck, 'budgetController.detailUpdate');
     app.post('/budget/:id/:btype/upload-excel/:ueType', sessionAuth, budgetCheck, 'budgetController.detailUploadExcel');
     app.post('/budget/:id/decimal', sessionAuth, budgetCheck, 'budgetController.decimal');
+    // 电子档案
+    app.get('/file', sessionAuth, 'fileController.index');
 
     // 支付审批
     app.get('/payment', sessionAuth, 'paymentController.index');

+ 14 - 30
app/service/budget.js

@@ -62,12 +62,12 @@ module.exports = app => {
             });
             if (admin) return result;
 
-            const permissionConst = this.ctx.service.budgetPermission.PermissionConst;
-            const permissionBudget = await this.ctx.service.budgetPermission.getUserPermission();
+            const permissionConst = this.ctx.service.subProjPermission.PermissionConst.budget;
+            const permissionBudget = await this.ctx.service.subProjPermission.getUserPermission();
             result = result.filter(x => {
                 const pb = permissionBudget.find(y => { return x.id === y.bid});
                 if (pb) {
-                    x.canEdit = pb.permission.indexOf(permissionConst.edit.value) >= 0;
+                    x.canEdit = pb.budget_permission.indexOf(permissionConst.edit.value) >= 0;
                 }
                 return !!pb;
             });
@@ -87,35 +87,19 @@ module.exports = app => {
          * @param {Object} data - 提交的数据
          * @return {Boolean} - 返回新增结果
          */
-        async add(data) {
-            const budgetStd = await this.ctx.service.budgetStd.getDataById(data.std_id);
-            if (!budgetStd) throw '选择的概算标准不存在,请刷新页面重试';
+        async add(transaction, data, budgetStd) {
+            if (!transaction) throw '数据错误';
 
-            const conn = await this.db.beginTransaction();
-            try {
-                // 获取当前用户信息
-                const sessionUser = this.ctx.session.sessionUser;
-                // 获取当前项目信息
-                const sessionProject = this.ctx.session.sessionProject;
-
-                const insertData = {
-                    pid: sessionProject.id, user_id: sessionUser.accountId, in_time: new Date(),
-                    name: data.name, std_id: data.std_id,
-                };
-                const operate = await conn.insert(this.tableName, insertData);
-
-                if (operate.insertId === 0) throw '新增标段数据失败';
+            data.in_time = new Date();
+            data.std_id = budgetStd.id;
+            const operate = await transaction.insert(this.tableName, data);
+            if (operate.insertId === 0) throw '初始化动态投资数据失败';
 
-                // 获取合同支付模板 并添加到标段
-                await this.ctx.service.budgetGu.initByTemplate(conn, operate.insertId, budgetStd.gu_template_id);
-                await this.ctx.service.budgetGai.initByTemplate(conn, operate.insertId, budgetStd.gai_template_id);
-                await this.ctx.service.budgetYu.initByTemplate(conn, operate.insertId, budgetStd.yu_template_id);
-                await conn.commit();
-                return await this.getDataById(operate.insertId);
-            } catch (error) {
-                await conn.rollback();
-                throw error;
-            }
+            // 获取合同支付模板 并添加到标段
+            await this.ctx.service.budgetGu.initByTemplate(transaction, operate.insertId, budgetStd.gu_template_id);
+            await this.ctx.service.budgetGai.initByTemplate(transaction, operate.insertId, budgetStd.gai_template_id);
+            await this.ctx.service.budgetYu.initByTemplate(transaction, operate.insertId, budgetStd.yu_template_id);
+            return operate.insertId;
         }
 
         /**

+ 0 - 100
app/service/budget_permission.js

@@ -1,100 +0,0 @@
-'use strict';
-
-/**
- *
- *
- * @author Mai
- * @date
- * @version
- */
-
-module.exports = app => {
-    class BudgetPermission extends app.BaseService {
-
-        /**
-         * 构造函数
-         *
-         * @param {Object} ctx - egg全局变量
-         * @param {String} tableName - 表名
-         * @return {void}
-         */
-        constructor(ctx) {
-            super(ctx);
-            this.tableName = 'budget_permission';
-            this.PermissionConst = {
-                view: { title: '查看', value: 1 },
-                edit: { title: '编辑', value: 2 },
-            };
-        }
-
-        async showBudget(uid) {
-            const count = await this.count({ pid: this.ctx.session.sessionProject.id, uid });
-            return count > 0;
-        }
-
-        async getBudgetPermission(bid) {
-            const _ = this.ctx.helper._;
-            const result = await this.db.query(`SELECT bp.*, p.name, p.role 
-                FROM ${this.tableName} bp LEFT JOIN ${this.ctx.service.projectAccount.tableName} p
-                On bp.uid = p.id WHERE bid = ?`, [bid]);
-            result.forEach(x => {
-                x.permission = x.permission ? _.map(x.permission.split(','), _.toInteger) : []
-            });
-            return result;
-        }
-
-        async getUserPermission() {
-            const _ = this.ctx.helper._;
-            const result = await this.getAllDataByCondition({
-                where: { uid: this.ctx.session.sessionUser.accountId, pid: this.ctx.session.sessionProject.id }
-            });
-            result.forEach(x => {
-                x.permission = x.permission ? _.map(x.permission.split(','), _.toInteger) : []
-            });
-            return result;
-        }
-
-        async getBudgetUserPermission(bid) {
-            const _ = this.ctx.helper._;
-            const result = await this.getDataByCondition({uid: this.ctx.session.sessionUser.accountId, bid});
-            if (result) result.permission = result.permission ? _.map(result.permission.split(','), _.toInteger) : [];
-            return result;
-        }
-
-        async saveBudgetPermission(bid, member) {
-            const orgMember = await this.getAllDataByCondition({ where: { bid } });
-            const dm = [], um = [], im = [], cur = new Date();
-            for (const om of orgMember) {
-                const nm = member.find(x => { return om.uid === x.uid });
-                if (!nm) {
-                    dm.push(om.id);
-                } else {
-                    um.push({
-                        id: om.id,
-                        permission: nm.permission.join(','),
-                        modify_time: cur,
-                    });
-                    member.splice(member.indexOf(nm), 1);
-                }
-            }
-            for (const m of member) {
-                im.push({
-                    pid: this.ctx.session.sessionProject.id, bid, uid: m.uid,
-                    permission: m.permission.join(','), in_time: cur, modify_time: cur,
-                })
-            }
-            const conn = await this.db.beginTransaction();
-            try {
-                if (dm.length > 0) await conn.delete(this.tableName, { id: dm });
-                if (um.length > 0) await conn.updateRows(this.tableName, um);
-                if (im.length > 0) await conn.insert(this.tableName, im);
-                await conn.commit();
-            } catch (err) {
-                await conn.rollback();
-                throw err;
-            }
-        }
-    }
-
-    return BudgetPermission;
-};

+ 131 - 0
app/service/sub_proj_permission.js

@@ -0,0 +1,131 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+module.exports = app => {
+    class subProjPermission extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @param {String} tableName - 表名
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'sub_project_permission';
+            this.PermissionConst = {
+                budget: {
+                    view: { title: '查看', value: 1 },
+                    edit: { title: '编辑', value: 2 },
+                },
+                file: {
+                    view: { title: '查看', value: 1 },
+                    upload: { title: '上传文件', value: 2 },
+                    edit: { title: '文件类别编辑', value: 3 },
+                },
+                manage: {
+                    rela: { title: '关联标段', value: 1 },
+                }
+            };
+        }
+
+        async showSubTab(uid, type) {
+            const sql = `SELECT count(*) FROM ${this.tableName} WHERE ${type}_permission <> '' AND uid = ?`;
+            const result = await this.db.queryOne(sql, [uid]);
+            return result.count;
+        }
+        async showBudget(uid) {
+            return await this.showSubTab(uid, 'budget');
+        }
+        async showFile(uid) {
+            return await this.showSubTab(uid, 'file');
+        }
+
+        parsePermission(data) {
+            const _ = this.ctx.helper._;
+            const datas = data instanceof Array ? data : [data];
+            datas.forEach(x => {
+                x.budget_permission = x.budget_permission ? _.map(x.budget_permission.split(','), _.toInteger) : [];
+                x.file_permission = x.file_permission ? _.map(x.file_permission.split(','), _.toInteger) : [];
+                x.manage_permission = x.manage_permission ? _.map(x.manage_permission.split(','), _.toInteger) : [];
+            });
+        }
+
+        async getPermission(subProjectId) {
+            const result = await this.db.query(`SELECT spp.*, p.name, p.role 
+                FROM ${this.tableName} spp LEFT JOIN ${this.ctx.service.projectAccount.tableName} p
+                On spp.uid = p.id WHERE spp.spid = ?`, [subProjectId]);
+            this.parsePermission(result);
+            return result;
+        }
+
+        async getBudgetPermission(subProjectId) {
+            const result = await this.db.query(`SELECT spp.*, p.name, p.role 
+                FROM ${this.tableName} spp LEFT JOIN ${this.ctx.service.projectAccount.tableName} p
+                On spp.uid = p.id WHERE spp.spid = ? and budget_permission <> ''`, [subProjectId]);
+            this.parsePermission(result);
+            return result;
+        }
+
+        async getUserPermission(pid, uid) {
+            const result = await this.getAllDataByCondition({
+                where: { uid: this.ctx.session.sessionUser.accountId, pid: this.ctx.session.sessionProject.id }
+            });
+            this.parsePermission(result);
+            return result;
+        }
+
+        async getBudgetUserPermission(bid) {
+            const _ = this.ctx.helper._;
+            const result = await this.getDataByCondition({uid: this.ctx.session.sessionUser.accountId, bid});
+            if (result) this.parsePermission(result);
+            return result;
+        }
+
+        async savePermission(subProjectId, member) {
+            const orgMember = await this.getAllDataByCondition({ where: { spid: subProjectId } });
+            const dm = [], um = [], im = [];
+            for (const om of orgMember) {
+                const nm = member.find(x => { return om.uid === x.uid; });
+                if (!nm) {
+                    dm.push(om.id);
+                } else {
+                    um.push({
+                        id: om.id, budget_permission: nm.budget_permission.join(','),
+                        file_permission: nm.file_permission.join(','),
+                        manage_permission: nm.manage_permission.join(',')
+                    });
+                    member.splice(member.indexOf(nm), 1);
+                }
+            }
+            for (const m of member) {
+                im.push({
+                    spid: subProjectId, pid: this.ctx.session.sessionProject.id, uid: m.uid,
+                    budget_permission: m.budget_permission.join(','),
+                    file_permission: m.file_permission.join(','),
+                    manage_permission: m.manage_permission.join(',')
+                })
+            }
+            const conn = await this.db.beginTransaction();
+            try {
+                if (dm.length > 0) await conn.delete(this.tableName, { id: dm });
+                if (um.length > 0) await conn.updateRows(this.tableName, um);
+                if (im.length > 0) await conn.insert(this.tableName, im);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+    }
+
+    return subProjPermission;
+};

+ 258 - 0
app/service/sub_project.js

@@ -0,0 +1,258 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+const rootId = '-1';
+
+module.exports = app => {
+    class SubProject extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @param {String} tableName - 表名
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'sub_project';
+        }
+
+        async getSubProject(pid, uid, admin) {
+            let result = await this.getAllDataByCondition({ where: { project_id: pid, is_delete: 0 } });
+            if (admin) return result;
+
+            const permission = await this.ctx.service.subProjPermission.getUserPermission(pid, uid);
+            result = result.filter(x => {
+                if (x.is_folder) return true;
+                const pb = permission.find(y => { return x.id === y.spid});
+                if (!pb) return false;
+                x.user_permission = pb;
+                return x.user_permission.budget_permission.length > 0 || x.user_permission.file_permission.length > 0 || x.manage_permission.length > 0;
+            });
+            return result;
+        }
+
+        _filterEmptyFolder(data) {
+            data.sort((a, b) => { return b.tree_level - a.tree_level});
+            const result = [];
+            for (const d of data) {
+                if (!d.is_folder) result.push(d);
+                if (result.find(x => { return x.tree_pid === d.id; })) result.push(d);
+            }
+            return result;
+        }
+
+        async getBudgetProject(pid, uid, admin) {
+            const sql = `SELECT sp.*, b.std_id From ${this.tableName} sp LEFT JOIN ${this.ctx.service.budget.tableName} b ON sp.budget_id = b.id  
+                WHERE sp.project_id = ? AND sp.is_delete = 0`;
+            let result = await this.db.query(sql, [pid]);
+
+            const permission = await this.ctx.service.subProjPermission.getUserPermission(pid, uid);
+            result = result.filter(x => {
+                if (!x.is_folder && !x.budget_id) return false;
+                if (x.is_folder || admin) return true;
+                const pb = permission.find(y => { return x.id === y.spid});
+                if (!pb) return false;
+                x.permission = pb.budget_permission;
+                x.manage_permission = pb.manage_permission;
+                return x.budget_permission.length > 0;
+            });
+            console.log(result.length);
+            return this._filterEmptyFolder(result);
+        }
+
+        async getFileProject(pid, uid, admin) {
+            let result = await this.getAllDataByCondition({ where: { project_id: pid, is_delete: 0 } });
+
+            const permission = await this.ctx.service.subProjPermission.getUserPermission(pid, uid);
+            result = result.filter(x => {
+                if (x.is_folder || admin) return true;
+                const pb = permission.find(y => { return x.id === y.spid});
+                if (!pb) return false;
+                x.permission = pb.file_permission;
+                x.manage_permission = pb.manage_permission;
+                return x.file_permission.length > 0;
+            });
+            return this._filterEmptyFolder(result);
+        }
+
+        async getLastChild(tree_pid) {
+            const result = await this.getAllDataByCondition({ where: { tree_pid }, orders: [['tree_order', 'desc']], limit: 1, offset: 0 });
+            return result[0];
+        }
+
+        async addFolder(data) {
+            const parent = await this.getDataById(data.tree_pid);
+            if (parent && !parent.is_folder) throw '添加数据结构错误';
+            const lastChild = await this.getLastChild(parent ? parent.id : rootId);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                // 获取当前用户信息
+                const sessionUser = this.ctx.session.sessionUser;
+                // 获取当前项目信息
+                const sessionProject = this.ctx.session.sessionProject;
+
+                const insertData = {
+                    id: this.uuid.v4(), project_id: sessionProject.id, user_id: sessionUser.accountId,
+                    tree_pid: data.tree_pid,
+                    tree_level: parent ? parent.tree_level + 1 : 1,
+                    tree_order: lastChild ? lastChild.tree_order + 1 : 1,
+                    name: data.name, is_folder: 1,
+                };
+                const operate = await conn.insert(this.tableName, insertData);
+
+                if (operate.affectedRows === 0) throw '新增文件夹失败';
+
+                await conn.commit();
+                return await this.getSubProject(sessionProject.id, sessionUser.accountId, sessionUser.is_admin);
+            } catch (error) {
+                await conn.rollback();
+                throw error;
+            }
+        }
+
+        async addSubProject(data) {
+            const parent = await this.getDataById(data.tree_pid);
+            if (parent && !parent.is_folder) throw '添加数据结构错误';
+            const lastChild = await this.getLastChild(parent ? parent.id : rootId);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                // 获取当前用户信息
+                const sessionUser = this.ctx.session.sessionUser;
+                // 获取当前项目信息
+                const sessionProject = this.ctx.session.sessionProject;
+
+                const insertData = {
+                    id: this.uuid.v4(), project_id: sessionProject.id, user_id: sessionUser.accountId,
+                    tree_pid: data.tree_pid,
+                    tree_level: parent ? parent.tree_level + 1 : 1,
+                    tree_order: lastChild ? lastChild.tree_order + 1 : 1,
+                    name: data.name, is_folder: 0,
+                };
+                const operate = await conn.insert(this.tableName, insertData);
+                // todo 根据节点新增时的其他操作
+
+                if (operate.affectedRows === 0) throw '新增文件夹失败';
+
+                await conn.commit();
+                return await this.getSubProject(sessionProject.id, sessionUser.accountId, sessionUser.is_admin);
+            } catch (error) {
+                await conn.rollback();
+                throw error;
+            }
+        }
+
+        async getPosterityData(id){
+            const result = [];
+            let cur = await this.getAllDataByCondition({ where: { tree_pid: id } });
+            let iLevel = 1;
+            while (cur.length > 0 && iLevel < 6) {
+                result.push(...cur);
+                cur = await this.getAllDataByCondition({ where: { tree_pid: cur.map(x => { return x.id })} });
+                iLevel += 1;
+            }
+            return result;
+        }
+
+        async dragTo(data) {
+            const dragNode = await this.getDataById(data.drag_id);
+            const dropNode = await this.getDataById(data.drop_id);
+            if (!dragNode || !dropNode || !dropNode.is_folder) throw '拖拽数据结构错误';
+            const lastChild = await this.getLastChild(dropNode.id);
+            const posterity = await this.getPosterityData(dragNode.id);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                const updateData = {
+                    id: dragNode.id, tree_pid: dropNode.id, tree_level: dropNode.tree_level + 1,
+                    tree_order: lastChild ? lastChild.tree_order + 1 : 1,
+                };
+                await conn.update(this.tableName, updateData);
+                if (dragNode.tree_level !== dropNode.tree_level + 1 && posterity.length > 0) {
+                    const posterityUpdateData = posterity.map(x => {
+                        return { id: x.id, tree_level: dropNode.tree_level + 1 - dragNode.tree_level + x.tree_level }
+                    });
+                    await conn.updateRows(this.tableName, posterityUpdateData);
+                }
+                // 升级原来的后项的order
+                await conn.query(`UPDATE ${this.tableName} SET tree_order = tree_order-1 WHERE tree_pid = ? AND tree_order > ?`, [dragNode.tree_pid, dragNode.tree_order]);
+                await conn.commit();
+            } catch (error) {
+                await conn.rollback();
+                throw error;
+            }
+
+            return await this.getSubProject(this.ctx.session.sessionProject.id, this.ctx.session.sessionUser.accountId, this.ctx.session.sessionUser.is_admin);
+        }
+
+        async del(id) {
+            const node = await this.getDataById(id);
+            if (!node) throw '删除的数据不存在';
+
+            const posterity = await this.getPosterityData(node.id);
+            const updateData = [
+                { id: node.id, is_delete: 1 },
+            ];
+            posterity.forEach(x => {
+                updateData.push({ id: x.id, is_delete: 1});
+            });
+            await this.db.updateRows(this.tableName, updateData);
+
+            return await this.getSubProject(this.ctx.session.sessionProject.id, this.ctx.session.sessionUser.accountId, this.ctx.session.sessionUser.is_admin);
+        }
+
+        async save(data) {
+            const result = await this.db.update(this.tableName, data);
+            if (result.affectedRows > 0) {
+                return data;
+            } else {
+                throw '更新数据失败';
+            }
+        }
+
+        async setBudgetStd(data) {
+            const subProject = await this.getDataById(data.id);
+            const budgetStd = await this.ctx.service.budgetStd.getDataById(data.std_id);
+            if (!budgetStd) throw '选择的概算标准不存在,请刷新页面重试';
+
+            const conn = await this.db.beginTransaction();
+            try {
+                const budget_id = await this.ctx.service.budget.add(conn, {
+                    pid: subProject.project_id, user_id: subProject.user_id, rela_tender: subProject.rela_tender
+                }, budgetStd);
+                const updateData = { id: data.id, std_id: budgetStd.id, std_name: budgetStd.name, budget_id };
+                await conn.update(this.tableName, updateData);
+                await conn.commit();
+                return updateData;
+            } catch (error) {
+                await conn.rollback();
+                throw error;
+            }
+        }
+
+        async setRelaTender(data) {
+            const subProject = await this.getDataById(data.id);
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.tableName, data);
+                await conn.update(this.ctx.service.budget.tableName, { id: subProject.budget_id, rela_tender: data.rela_tender });
+                await conn.commit();
+                return data;
+            } catch (error) {
+                await conn.rollback();
+                throw error;
+            }
+        }
+    }
+
+    return SubProject;
+};

+ 4 - 26
app/view/budget/list.ejs

@@ -1,12 +1,7 @@
 <div class="panel-content">
     <div class="panel-title fluid">
         <div class="title-main  d-flex justify-content-between">
-            <div>动态决算</div>
-            <% if (ctx.session.sessionUser.is_admin) { %>
-            <div class="ml-auto">
-                <a href="#add-budget" name="add" data-toggle="modal" data-target="#add-budget" class="btn btn-sm btn-primary pull-right">新建项目</a>
-            </div>
-            <% } %>
+            <div>动态投资</div>
         </div>
     </div>
     <div class="content-wrap">
@@ -19,26 +14,8 @@
                 <% } else { %>
                 <table class="table table-hover table-bordered">
                     <tr class="text-center"><th style="min-width: 200px">项目名称</th><th>概预算标准</th><th>创建时间</th><th>投资估算</th><th>初步概算</th><th>施工图预算</th><th>操作</th></tr>
-                    <% for (const bl of budgetList) { %>
-                    <tr bid="<%- bl.id %>" bname="<%- bl.name %>" rela-tender="<%- bl.rela_tender %>" >
-                        <td><a href="/budget/<%- bl.id %>/compare"><%- bl.name %></a></td>
-                        <td><%- bl.std_name %></td>
-                        <td><%- ctx.moment(bl.in_time).format('YYYY-MM-DD') %></td>
-                        <td class="text-right"><%- (bl.gu_tp ? bl.gu_tp : '')%></td>
-                        <td class="text-right"><%- (bl.gai_tp ? bl.gai_tp : '')%></td>
-                        <td class="text-right"><%- (bl.yu_tp ? bl.yu_tp : '')%></td>
-                        <td>
-                            <% if (ctx.session.sessionUser.is_admin || bl.canEdit) { %>
-                            <a href="javascript: void(0);" data-target="#modify-budget" class="btn btn-outline-primary btn-sm" onclick="showModal(this);">编辑</a>
-                            <a href="javascript: void(0);" data-target="#select-rela" class="btn btn-outline-primary btn-sm" onclick="showModal(this);">关联标段</a>
-                            <% } %>
-                            <% if (ctx.session.sessionUser.is_admin) { %>
-                            <a href="javascript: void(0);" data-target="#member" class="btn btn-outline-primary btn-sm ml-1">成员管理</a>
-                            <a href="javascript: void(0);" data-target="#del-budget" class="btn btn-outline-danger btn-sm ml-1" onclick="showModal(this);">删除</a>
-                            <% } %>
-                        </td>
-                    </tr>
-                    <% } %>
+                    <tbody id="budgetList">
+                    </tbody>
                 </table>
                 <% } %>
             </div>
@@ -46,5 +23,6 @@
     </div>
 </div>
 <script>
+    const budgetList = JSON.parse(unescape('<%- escape(JSON.stringify(budgetList)) %>'));
     const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
 </script>

+ 2 - 108
app/view/budget/list_modal.ejs

@@ -1,35 +1,3 @@
-<!--弹出添加标段-->
-<div class="modal fade" id="add-budget" 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" id="add-budget-name" onchange="budgetNameChange(this);">
-                    <div class="invalid-feedback">名称超过100个字,请缩减名称。</div>
-                </div>
-                <div class="form-group">
-                    <label>概预算标准<b class="text-danger">*</b></label>
-                    <div>
-                        <% for (const [i, bs] of budgetStd.entries()) { %>
-                        <div class="form-check form-check-inline mt-2">
-                            <input class="form-check-input" name="std_id" type="radio" id="std<%- bs.id %>" value="<%- bs.id %>" <% if (i === 0) { %>checked<% } %>>
-                            <label class="form-check-label" for="std<%- bs.id %>"><%- bs.name %></label>
-                        </div>
-                        <% } %>
-                    </div>
-                </div>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
-                <button type="button" class="btn btn-primary btn-sm" id="add-budget-ok" onclick="addBudget();">确定添加</button>
-            </div>
-        </div>
-    </div>
-</div>
 <!--编辑标段-->
 <div class="modal fade" id="modify-budget" data-backdrop="static">
     <div class="modal-dialog" role="document">
@@ -40,7 +8,7 @@
             <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" id="modify-budget-name" value="" onchange="budgetNameChange(this);">
+                    <input class="form-control form-control-sm"  placeholder="输入项目名称" type="text" id="modify-budget-name" value="" >
                     <div class="invalid-feedback"> 名称超过100个字,请缩减名称。 </div>
                 </div>
 
@@ -52,24 +20,6 @@
         </div>
     </div>
 </div>
-<!--删除标段-->
-<div class="modal fade" id="del-budget" 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>确认删除「<span id="del-budget-name">XXX高速公路</span>」?</h6>
-                <h6>删除后,数据无法恢复,请谨慎操作。</h6>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
-                <button type="button" class="btn btn-sm btn-danger" onclick="delBudget();">确定删除</button>
-            </div>
-        </div>
-    </div>
-</div>
 <!--弹出关联标段-->
 <div class="modal fade" id="select-rela" data-backdrop="static">
     <div class="modal-dialog" role="document">
@@ -91,60 +41,4 @@
             </div>
         </div>
     </div>
-</div>
-<!--成员管理-->
-<div class="modal fade" id="member" 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="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="member-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="mt-1">
-                    <table class="table table-bordered">
-                        <thead>
-                        <th>成员名称</th>
-                        <th>角色/职位</th>
-                        <th>编辑</th>
-                        <th>移除</th>
-                        </thead>
-                        <tbody id="member-list">
-
-                        </tbody>
-                    </table>
-                </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="member-ok">确认修改</button>
-            </div>
-        </div>
-    </div>
-</div>
-<script>
-    const accountList = JSON.parse('<%- JSON.stringify(accountList) %>');
-    const accountGroup = JSON.parse('<%- JSON.stringify(accountGroup) %>');
-    const permissionConst = JSON.parse('<%- JSON.stringify(permissionConst) %>');
-</script>
+</div>

+ 28 - 0
app/view/file/index.ejs

@@ -0,0 +1,28 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex justify-content-between">
+            <div>资料归集</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">
+                    <h3 class="display-6">还没有项目数据</h3>
+                </div>
+                <% } else { %>
+                <table class="table table-hover table-bordered">
+                    <tr class="text-center"><th style="min-width: 200px">项目名称</th><th>管理单位</th><th>创建时间</th><th>操作</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)) %>'));
+</script>

+ 22 - 0
app/view/file/modal.ejs

@@ -0,0 +1,22 @@
+<!--弹出关联标段-->
+<div class="modal fade" id="select-rela" 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 id="sr-spread" style="height: 300px"></div>
+            </div>
+            <div class="modal-footer">
+                <div class="form-check form-check-inline">
+                    <input class="form-check-input" type="checkbox" id="sr-select-all">
+                    <label class="form-check-label" for="sr-select-all">全选</label>
+                </div>
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-primary" id="select-rela-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>

File diff suppressed because it is too large
+ 2 - 1406
app/view/payment/detail.ejs


+ 1 - 1
app/view/payment/process.ejs

@@ -85,7 +85,7 @@
                         <div class="tab-pane fade" id="profile" role="tabpanel" aria-labelledby="profile-tab">
                             <div class="row">
                                 <div class="col-4">
-                                    <div class="my-3"><a href="#">添加表单</a></div>
+                                    <div class="my-3"><a href="#add-rpt" data-toggle="modal" data-target="#add-rpt">添加表单</a></div>
                                     <table class="table table-bordered">
                                         <thead>
                                         <tr>

+ 38 - 0
app/view/payment/process_modal.ejs

@@ -0,0 +1,38 @@
+<div class="modal fade" id="add-rpt" 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">
+                <table id="rpt-table" class="table table-bordered">
+                    <thead>
+                    <tr>
+                        <th class="text-center"><input type="checkbox">选择</th>
+                        <th class="text-center">表单名</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    <tr>
+                        <td class="text-center"><input type="checkbox"></td>
+                        <td>指挥部工程结算款</td>
+                    </tr>
+                    <tr>
+                        <td class="text-center"><input type="checkbox"></td>
+                        <td>指挥部工程结算付款</td>
+                    </tr>
+                    <tr>
+                        <td class="text-center"><input type="checkbox"></td>
+                        <td>指挥部监理、设计等预付款</td>
+                    </tr>
+                    </tbody>
+                </table>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" id="add_tender_folder_id" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-primary" id="new_tender_btn">确定添加</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 1 - 1
app/view/sign/info.ejs

@@ -3,7 +3,7 @@
 
 <head>
     <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
     <meta http-equiv="x-ua-compatible" content="ie=edge">
     <title>签字-计量支付</title>
     <style>

+ 34 - 0
app/view/sub_proj/index.ejs

@@ -0,0 +1,34 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex justify-content-between">
+            <div>项目列表</div>
+            <% if (ctx.session.sessionUser.is_admin) { %>
+            <div class="ml-auto">
+                <a href="#add-folder" name="add" data-toggle="modal" data-target="#add-folder" class="btn btn-sm btn-primary">新建文件夹</a>
+                <a href="#add-project" name="add" data-toggle="modal" data-target="#add-project" class="btn btn-sm btn-primary pull-right ml-2">新建项目</a>
+            </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">
+                    <h3 class="display-6">还没有项目数据</h3>
+                </div>
+                <% } else { %>
+                <table class="table table-bordered">
+                    <tr class="text-center"><th style="min-width: 200px">项目名称</th><th>创建时间</th><th>概预算标准</th><th>管理单位</th><th>操作</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)) %>'));
+</script>

+ 192 - 0
app/view/sub_proj/modal.ejs

@@ -0,0 +1,192 @@
+<div class="modal" id="add-project" 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">
+                <form>
+                    <div class="form-group">
+                        <label for="text" class="col-form-label-sm" id="project-hint"></label>
+                    </div>
+                    <div class="form-group row">
+                        <label for="text" class="col-sm-3 col-form-label-sm">项目名称</label>
+                        <div class="col-sm-9">
+                            <input type="text" class="form-control form-control-sm" id="project-name" placeholder="请输入项目名称" onchange="NameChange(this);">
+                        </div>
+                    </div>
+                    <div class="form-group row">
+                        <label for="text" class="col-sm-3 col-form-label-sm">管理单位</label>
+                        <div class="col-sm-9">
+                            <input type="text" class="form-control form-control-sm" id="management" placeholder="请输入管理名称">
+                        </div>
+                    </div>
+                </form>
+            </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-project-ok">确定添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal" id="add-folder" 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">
+                <form>
+                    <div class="form-group">
+                        <label for="text" class="col-form-label-sm" id="folder-hint"></label>
+                    </div>
+                    <div class="form-group row">
+                        <label for="text" class="col-sm-3 col-form-label-sm">文件夹名称</label>
+                        <div class="col-sm-9">
+                            <input type="text" class="form-control form-control-sm" id="folder-name" placeholder="请输入文件夹名称" onchange="NameChange(this);">
+                        </div>
+                    </div>
+                </form>
+            </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-folder-ok">确定添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="del" 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>
+                <h5 id="del-hint">确定删除「<strong style="word-break: break-word;" id="del-tender-name"></strong>」?</h5>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-danger btn-sm" id="del-ok">确定删除</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--弹出选择概预算标准-->
+<div class="modal fade" id="set-std" 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>
+                    <div>
+                        <% for (const [i, bs] of budgetStd.entries()) { %>
+                        <div class="form-check form-check-inline mt-2">
+                            <input class="form-check-input" name="std_id" type="radio" id="std<%- bs.id %>" value="<%- bs.id %>" <% if (i === 0) { %>checked<% } %>>
+                            <label class="form-check-label" for="std<%- bs.id %>"><%- bs.name %></label>
+                        </div>
+                        <% } %>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary btn-sm" id="set-std-ok">确定添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal" id="edit-project" 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">
+                <form>
+                    <div class="form-group row">
+                        <label for="text" class="col-sm-3 col-form-label-sm">项目名称</label>
+                        <div class="col-sm-9">
+                            <input type="text" class="form-control form-control-sm" id="edit-project-name" placeholder="请输入项目名称" onchange="NameChange(this);">
+                        </div>
+                    </div>
+                </form>
+            </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="edit-project-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--成员管理-->
+<div class="modal fade" id="member" 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">
+                <div class="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="member-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="mt-1">
+                    <table class="table table-bordered">
+                        <thead class="text-center">
+                        <tr>
+                            <th rowspan="2">成员名称</th>
+                            <th rowspan="2">角色/职位</th>
+                            <th colspan="2">动态投资</th>
+                            <th colspan="3">电子档案</th>
+                            <th rowspan="2">关联标段</th>
+                            <th rowspan="2">移除</th>
+                        </tr>
+                        <tr>
+                            <th>查看</th>
+                            <th>编辑</th>
+                            <th>查看</th>
+                            <th>导入</th>
+                            <th>编辑</th>
+                        </tr>
+                        </thead>
+                        <tbody id="member-list">
+
+                        </tbody>
+                    </table>
+                </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="member-ok">确认修改</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const accountList = JSON.parse('<%- JSON.stringify(accountList) %>');
+    const accountGroup = JSON.parse('<%- JSON.stringify(accountGroup) %>');
+    const permissionConst = JSON.parse('<%- JSON.stringify(permissionConst) %>');
+</script>

+ 11 - 3
config/menu.js

@@ -33,13 +33,21 @@ const menu = {
         children: null,
         caption: '项目',
     },
+    sub_project: {
+        name: '项目管理',
+        icon: 'fa-tags',
+        display: true,
+        url: '/subproj',
+        children: null,
+        caption: '项目管理',
+    },
     file: {
-        name: '电子档案',
-        icon: 'fa-file',
+        name: '资料归集',
+        icon: 'fa-credit-card',
         display: true,
         url: '/file',
         children: null,
-        caption: '电子档案',
+        caption: '资料归集',
     },
     budget: {
         name: '动态投资',

+ 33 - 0
config/web.js

@@ -1023,6 +1023,38 @@ const JsFiles = {
                 mergeFile: 'index',
             },
         },
+        subProject:{
+            list: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/shares/drag_tree.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/sub_project.js',
+                ],
+                mergeFile: 'sub_project',
+            },
+        },
+        file: {
+            index: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/shares/drag_tree.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/file.js',
+                ],
+                mergeFile: 'file',
+            },
+        },
         budget: {
             list: {
                 files: [
@@ -1030,6 +1062,7 @@ const JsFiles = {
                     '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
                 ],
                 mergeFiles: [
+                    '/public/js/shares/drag_tree.js',
                     '/public/js/path_tree.js',
                     '/public/js/shares/tenders2tree.js',
                     '/public/js/spreadjs_rela/spreadjs_zh.js',

+ 75 - 0
db_script/sub_project.js

@@ -0,0 +1,75 @@
+const BaseUtil = require('./baseUtils');
+const querySql = BaseUtil.querySql;
+const uuid = require('node-uuid');
+
+const getInsertSql = function (tableName, data) {
+    const column = [], query = [], value = [];
+    for (const prop in data) {
+        column.push(prop);
+        query.push('?');
+        value.push(data[prop]);
+    }
+    return [`INSERT INTO ${tableName} (${column.join(',')}) VALUES (${query.join(',')})`, value]
+};
+
+const syncBudget = async function(budget, order) {
+    try {
+        const subProject = {
+            id: uuid.v4(), project_id: budget.pid, tree_pid: '-1', tree_level: 1, tree_order: order,
+            name: budget.name, user_id: budget.user_id, rela_tender: budget.rela_tender,
+            is_folder: 0, budget_id: budget.id,
+        };
+        const [subProjSql, subProjSqlParam] = getInsertSql('zh_sub_project', subProject);
+        await querySql(subProjSql, subProjSqlParam);
+        const permission = await querySql('SELECT * FROM zh_budget_permission WHERE bid = ?', [budget.id]);
+        for (const p of permission) {
+            const np = {
+                id: uuid.v4(), spid: subProject.id, pid: budget.pid, uid: p.uid,
+                budget_permission: p.permission, manage_permission: '1'
+            };
+            const [pSql, pSqlParam] = getInsertSql('zh_sub_project_permission', np);
+            await querySql(pSql, pSqlParam);
+        }
+    } catch (err) {
+        console.log(err);
+    }
+};
+
+const doComplete = async function() {
+    try {
+        const project = await querySql('Select * From zh_project');
+        for (const p of project) {
+            console.log(`Update Project ${p.code}(${p.id}):`);
+            const budget = await querySql('SELECT * FROM zh_budget where pid = ?', [p.id]);
+            for (const [i, b] of budget.entries()) {
+                console.log(`Sync Budget ${b.name}(${b.id})`);
+                await syncBudget(b, i + 1);
+            }
+        }
+    } catch (err) {
+        console.log(err);
+    }
+    BaseUtil.closePool();
+};
+const doCompleteTest = async function(code) {
+    try {
+        const project = await querySql('Select * From zh_project where code = ?', [code]);
+        for (const p of project) {
+            console.log(`Update Project ${p.code}(${p.id}):`);
+            const budget = await querySql('SELECT * FROM zh_budget where pid = ?', [p.id]);
+            for (const [i, b] of budget.entries()) {
+                console.log(`Sync Budget ${b.name}(${b.id})`);
+                await syncBudget(b, i + 1);
+            }
+        }
+    } catch (err) {
+        console.log(err);
+    }
+    BaseUtil.closePool();
+};
+const projectCode = process.argv[3];
+if (projectCode) {
+    doCompleteTest(projectCode);
+} else {
+    doComplete()
+}

+ 8 - 1
sql/update.sql

@@ -3,4 +3,11 @@ set defined_content='{"ctrls":[{"ID":"Default","Wrap":"T","Shrink":"F","Horizon"
 where userId='Administrator';
 
 ALTER TABLE `zh_project_account`
-ADD COLUMN `self_category_level`  varchar(255) NOT NULL DEFAULT '' AFTER `invalid_time`;
+ADD COLUMN `self_category_level`  varchar(255) NOT NULL DEFAULT '' AFTER `invalid_time`;
+
+
+
+-- 很重要!!!!
+-- 请在该sql语句上方添加下一版本升级所需sql
+UPDATE `zh_sub_project` p LEFT JOIN `zh_budget` b ON p.budget_id = b.id SET p.std_id = IFNULL(b.std_id, 0);
+UPDATE `zh_sub_project` p LEFT JOIN `zh_budget_std` bs ON p.std_id = bs.id SET p.std_name = IFNULL(bs.name, '');