laiguoran 1 rok pred
rodič
commit
1864e97228

+ 8 - 0
app/const/account_permission.js

@@ -53,6 +53,14 @@ const permission = {
             // { title: '修改材料税税率', value: 3, hint: '开启该选项,可在新材差期修改材料税税率', hintIcon: 'fa-question-circle' },
         ],
     },
+    construction: {
+        class: 'fa fa-pencil-square-o',
+        title: '施工日志',
+        type: 'checkbox',
+        children: [
+            { title: '查阅所有施工标段', value: 1 },
+        ],
+    },
     // cooperation: {
     //     class: '',
     //     title: '协作办公',

+ 42 - 0
app/const/construction.js

@@ -0,0 +1,42 @@
+'use strict';
+
+/**
+ * 施工日志
+ *
+ * @author ELlisran
+ * @date 2019/10/20
+ * @version
+ */
+// 类型
+const type = ['新建', '补建'];
+const logJson = {
+    parts: '',
+    weather: '',
+    temperature: '',
+    condition: '',
+    work: '',
+    run: '',
+    other: '',
+};
+const status = {
+    uncheck: 1, // 待提交
+    checking: 2, // 未审签
+    checked: 3, // 已审签
+};
+const statusString = [];
+statusString[status.uncheck] = '待提交';
+statusString[status.checking] = '未审签';
+statusString[status.checked] = '已审签';
+
+const statusClass = [];
+statusClass[status.uncheck] = 'text-primary';
+statusClass[status.checking] = 'text-warning';
+statusClass[status.checked] = 'text-success';
+
+module.exports = {
+    type,
+    logJson,
+    status,
+    statusString,
+    statusClass,
+};

+ 513 - 0
app/controller/construction_controller.js

@@ -0,0 +1,513 @@
+'use strict';
+const moment = require('moment');
+const path = require('path');
+const sendToWormhole = require('stream-wormhole');
+const constructionConst = require('../const/construction');
+
+module.exports = app => {
+
+    class ConstructionController extends app.BaseController {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            ctx.showProject = true;
+            // ctx.showTitle = true;
+        }
+        /**
+         * 支付审批列表页
+         *
+         * @param {Object} ctx - egg全局页面
+         * @return {void}
+         */
+        async index(ctx) {
+            try {
+                // 获取用户新建标段权利
+                const accountInfo = await this.ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const userPermission = accountInfo !== undefined && accountInfo.permission !== ''
+                    ? JSON.parse(accountInfo.permission) : null;
+                const tenderList = await ctx.service.tender.getConstructionList('', userPermission, ctx.session.sessionUser.is_admin);
+                const categoryData = await ctx.service.category.getAllCategory(ctx.session.sessionProject.id);
+                const renderData = {
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.construction.index),
+                    tenderList,
+                    categoryData,
+                    // selfCategoryLevel: accountInfo ? accountInfo.self_category_level : '',
+                    selfCategoryLevel: '',
+                    pid: ctx.session.sessionProject.id,
+                    uid: ctx.session.sessionUser.accountId,
+                };
+                if (ctx.session.sessionUser.is_admin) {
+                    const projectId = ctx.session.sessionProject.id;
+                    // 获取所有项目参与者
+                    const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                        where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                        columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                    });
+                    const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: projectId } });
+                    const accountGroupList = unitList.map(item => {
+                        const groupList = accountList.filter(item1 => item1.company === item.name);
+                        return { groupName: item.name, groupList };
+                    });
+                    renderData.accountList = accountList;
+                    renderData.accountGroup = accountGroupList;
+                }
+                await this.layout('construction/index.ejs', renderData, 'construction/modal.ejs');
+            } catch (err) {
+                console.log(err);
+                this.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(this.menu.menu.dashboard.url);
+            }
+        }
+
+        async auditSave(ctx) {
+            try {
+                if (ctx.session.sessionUser.is_admin === 0) throw '没有设置权限';
+                const tid = parseInt(ctx.params.tid);
+                const responseData = {
+                    err: 0, msg: '', data: null,
+                };
+                const tenderInfo = await ctx.service.tender.getDataById(tid);
+                if (!tenderInfo) throw '标段不存在';
+
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.type) {
+                    throw '提交数据错误';
+                }
+                let uids;
+                let result = false;
+                let auditList = [];
+                switch (data.type) {
+                    case 'add-audit':
+                        // // 判断用户是单个还是数组
+                        uids = data.id instanceof Array ? data.id : [data.id];
+                        // // 判断该用户的组是否已加入到表中,已加入则提示无需添加
+                        auditList = await ctx.service.constructionAudit.getAllDataByCondition({ where: { tid } });
+                        const addAidList = ctx.helper._.difference(uids, ctx.helper._.map(auditList, 'uid'));
+                        if (addAidList.length === 0) {
+                            throw '用户已存在成员管理中,无需重复添加';
+                        }
+                        const accountList = await ctx.service.projectAccount.getAllDataByCondition({ where: { id: addAidList } });
+                        await ctx.service.constructionAudit.saveAudits(tid, accountList);
+                        responseData.data = await ctx.service.constructionAudit.getList(tid);
+                        break;
+                    case 'del-audit':
+                        uids = data.id instanceof Array ? data.id : [data.id];
+                        auditList = await ctx.service.constructionAudit.getAllDataByCondition({ where: { tid, id: uids } });
+                        if (auditList.length !== uids.length) {
+                            throw '该用户已不存在成员管理中,移除失败';
+                        }
+                        await ctx.service.constructionAudit.delAudit(uids);
+                        responseData.data = await ctx.service.constructionAudit.getList(tid);
+                        break;
+                    case 'save-report':
+                        result = await ctx.service.constructionAudit.updateReport(data.updateData);
+                        if (!result) {
+                            throw '修改权限失败';
+                        }
+                        break;
+                    case 'list':
+                        responseData.data = await ctx.service.constructionAudit.getList(tid);
+                        break;
+                    default: throw '参数有误';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        async list(ctx) {
+            try {
+                // 获取上报人列表
+                const reportUserList = await ctx.service.constructionAudit.getUserList(ctx.constructionTender.id, 1);
+                // 判断是否有状态及上报人选中
+                const params = { tid: ctx.constructionTender.id };
+                let curReportUser = null;
+                if (ctx.query.uid) {
+                    curReportUser = ctx.helper._.find(reportUserList, { uid: parseInt(ctx.query.uid) });
+                    if (curReportUser) {
+                        params.report_uid = curReportUser.uid;
+                    }
+                }
+                const statusList = [];
+                for (const s in constructionConst.status) {
+                    const findObject = { tid: ctx.constructionTender.id, status: constructionConst.status[s] };
+                    if (params.report_uid) findObject.report_uid = params.report_uid;
+                    const num = await ctx.service.constructionLog.count(findObject);
+                    statusList.push({
+                        statusName: constructionConst.statusString[constructionConst.status[s]],
+                        status: constructionConst.status[s],
+                        num,
+                    });
+                }
+                let curStatus = null;
+                if (ctx.query.status) {
+                    curStatus = ctx.helper._.find(statusList, { status: parseInt(ctx.query.status) });
+                    if (curStatus) {
+                        params.status = curStatus.status;
+                    }
+                }
+                const page = ctx.page;
+                const total = await ctx.service.constructionLog.getCount(params);
+                ctx.sort = ['id', 'desc'];
+                const logList = await ctx.service.constructionLog.getLogList(params);
+                // 分页相关
+                const pageInfo = {
+                    page,
+                    total: Math.ceil(total / app.config.pageSize),
+                    queryData: JSON.stringify(ctx.urlInfo.query),
+                };
+                const renderData = {
+                    reportUserList,
+                    statusList,
+                    curStatus,
+                    curReportUser,
+                    pageInfo,
+                    logList,
+                    constructionConst,
+                    moment,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.construction.list),
+                };
+                // 获取自己所属的待提交状态,且shenpi_uid不为空的日志列表
+                let reportFlag = false;
+                if (ctx.helper._.findIndex(reportUserList, { uid: ctx.session.sessionUser.accountId }) !== -1) {
+                    reportFlag = true;
+                    renderData.uncheckLogList = await ctx.service.constructionLog.getUncheckedLogList(ctx.constructionTender.id, ctx.session.sessionUser.accountId);
+                    renderData.dateCodeList = await ctx.service.constructionLog.getDateCodeList(ctx.constructionTender.id);
+                }
+                renderData.reportFlag = reportFlag;
+                await this.layout('construction/list.ejs', renderData, 'construction/list_modal.ejs');
+            } catch (err) {
+                console.log(err);
+                this.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(this.request && this.request.headers && this.request.headers.referer ? this.request.headers.referer : '/construction');
+            }
+        }
+
+        async addLog(ctx) {
+            try {
+                const responseData = {
+                    err: 0, msg: '', data: null,
+                };
+                const data = JSON.parse(ctx.request.body.data);
+                const reportUser = await ctx.service.constructionAudit.getDataByCondition({ tid: ctx.constructionTender.id, uid: ctx.session.sessionUser.accountId });
+                if (!reportUser) {
+                    throw '非填报人无法新建日志';
+                }
+                responseData.data = await ctx.service.constructionLog.addLog(ctx.constructionTender.id, ctx.session.sessionUser.accountId, data);
+                ctx.body = responseData;
+            } catch (error) {
+                this.log(error);
+                ctx.body = { err: 1, msg: error.toString(), data: null };
+            }
+        }
+
+        // 批量提交
+        async startMulti(ctx) {
+            try {
+                const responseData = {
+                    err: 0, msg: '', data: null,
+                };
+                const data = JSON.parse(ctx.request.body.data);
+                const reportUser = await ctx.service.constructionAudit.getDataByCondition({ tid: ctx.constructionTender.id, uid: ctx.session.sessionUser.accountId });
+                if (!reportUser) {
+                    throw '非填报人无法提交日志';
+                }
+                responseData.data = await ctx.service.constructionLog.startMulti(data.ids, ctx.session.sessionUser.accountId);
+                ctx.body = responseData;
+            } catch (error) {
+                this.log(error);
+                ctx.body = { err: 1, msg: error.toString(), data: null };
+            }
+        }
+
+        async deleteLog(ctx) {
+            try {
+                const responseData = {
+                    err: 0, msg: '', data: null,
+                };
+                const id = parseInt(ctx.params.id);
+                if (!id) throw '参数有误';
+                const logInfo = await ctx.service.constructionLog.getDataById(id);
+                if (!logInfo) throw '该日志不存在';
+                if (logInfo.report_uid !== ctx.session.sessionUser.accountId) {
+                    throw '非该日志填报人无法删除日志';
+                }
+                responseData.data = await ctx.service.constructionLog.deleteLog(logInfo.id);
+                ctx.body = responseData;
+            } catch (error) {
+                this.log(error);
+                ctx.body = { err: 1, msg: error.toString(), data: null };
+            }
+        }
+
+        async logInfo(ctx) {
+            try {
+                const id = parseInt(ctx.params.id);
+                if (!id) throw '参数有误';
+                const logInfo = await ctx.service.constructionLog.getDataById(id);
+                if (!logInfo) throw '该日志不存在';
+                logInfo.log_json = logInfo.log_json ? JSON.parse(logInfo.log_json) : ctx.helper._.cloneDeep(constructionConst.logJson);
+                const report_user = await ctx.service.projectAccount.getDataById(logInfo.report_uid);
+                logInfo.report_username = report_user ? report_user.name : '';
+                const shenpi_user = logInfo.shenpi_uid ? await ctx.service.projectAccount.getDataById(logInfo.shenpi_uid) : null;
+                logInfo.shenpi_username = shenpi_user ? shenpi_user.name : '';
+                let filePermission = false;
+                if (ctx.session.sessionUser.accountId === logInfo.report_uid || ctx.session.sessionUser.accountId === logInfo.shenpi_uid) {
+                    filePermission = true;
+                }
+                // 获取附件列表
+                const attList = await ctx.service.constructionAtt.getConstructionAttachment(logInfo.id, 'desc');
+                const renderData = {
+                    logInfo,
+                    constructionConst,
+                    attList,
+                    filePermission,
+                    whiteList: ctx.app.config.multipart.whitelist,
+                    OSS_PATH: ctx.app.config.fujianOssPath,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.construction.info),
+                };
+                if (ctx.session.sessionUser.accountId === logInfo.report_uid && logInfo.status === constructionConst.status.uncheck) {
+                    const projectId = ctx.session.sessionProject.id;
+                    // 获取所有项目参与者
+                    const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                        where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                        columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                    });
+                    const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: projectId } });
+                    const accountGroupList = unitList.map(item => {
+                        const groupList = accountList.filter(item1 => item1.company === item.name);
+                        return { groupName: item.name, groupList };
+                    });
+                    renderData.accountList = accountList;
+                    renderData.accountGroup = accountGroupList;
+                }
+                await this.layout('construction/info.ejs', renderData, 'construction/info_modal.ejs');
+            } catch (err) {
+                console.log(err);
+                this.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(ctx.constructionTender ? '/constrution/' + ctx.constructionTender.id + '/list' : '/construction');
+            }
+        }
+
+        async logSave(ctx) {
+            try {
+                const id = parseInt(ctx.params.id);
+                if (!id) throw '参数有误';
+                const logInfo = await ctx.service.constructionLog.getDataById(id);
+                if (!logInfo) throw '该日志不存在';
+                const data = JSON.parse(ctx.request.body.data);
+                // 检查权限等
+                if (logInfo.status === constructionConst.status.checked) {
+                    throw '该日志已审签,无法再操作';
+                }
+                if (logInfo.report_uid !== ctx.session.sessionUser.accountId && !(logInfo.shenpi_uid === ctx.session.sessionUser.accountId && data.type === 'checked')) {
+                    throw '您无权操作';
+                }
+                const responseData = {
+                    err: 0, msg: '', data: {},
+                };
+
+                switch (data.type) {
+                    case 'update_json':
+                        logInfo.log_json = logInfo.log_json ? JSON.parse(logInfo.log_json) : ctx.helper._.cloneDeep(constructionConst.logJson);
+                        responseData.data = await ctx.service.constructionLog.updateLogJson(logInfo.id, logInfo.log_json, data.updateData);
+                        break;
+                    case 'set_shenpi':
+                        if (data.uid === logInfo.report_uid) {
+                            throw '审签人不能同时为填报人';
+                        }
+                        responseData.data = await ctx.service.constructionLog.setShenpiUser(ctx.constructionTender.id, logInfo.id, data.uid);
+                        break;
+                    case 'remove_shenpi':
+                        responseData.data = await ctx.service.constructionLog.removeShenpiUser(logInfo.id);
+                        break;
+                    case 'checked':
+                        if (logInfo.shenpi_uid !== ctx.session.sessionUser.accountId) {
+                            throw '非当前审签人无法审签日志';
+                        }
+                        responseData.data = await ctx.service.constructionLog.checked(logInfo.id);
+                        break;
+                    case 'start':
+                        const reportUser = await ctx.service.constructionAudit.getDataByCondition({ tid: ctx.constructionTender.id, uid: ctx.session.sessionUser.accountId });
+                        if (!reportUser) {
+                            throw '非填报人无法提交日志';
+                        }
+                        if (!logInfo.shenpi_uid) throw '请选择审签人再提交';
+                        responseData.data = await ctx.service.constructionLog.start(logInfo.id);
+                        break;
+                    default: throw '参数有误';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        /**
+         * 上传附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async uploadFile(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: [],
+            };
+            let stream;
+            try {
+                const id = parseInt(ctx.params.id);
+                if (!id) throw '参数有误';
+                const logInfo = await ctx.service.constructionLog.getDataById(id);
+                if (!logInfo) throw '该日志不存在';
+                const parts = ctx.multipart({ autoFields: true });
+                const files = [];
+                let index = 0;
+                const extra_upload = logInfo.status === constructionConst.status.checked;
+                const original_data = {
+                    tid: ctx.constructionTender.id,
+                    log_id: logInfo.id,
+                };
+                while ((stream = await parts()) !== undefined) {
+                    // 判断用户是否选择上传文件
+                    if (!stream.filename) {
+                        throw '请选择上传的文件!';
+                    }
+                    const fileInfo = path.parse(stream.filename);
+                    const create_time = Date.parse(new Date()) / 1000;
+                    const filepath = `app/public/upload/${original_data.tid}/construction/fujian_${create_time + index.toString() + fileInfo.ext}`;
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    await sendToWormhole(stream);
+                    // 保存数据到att表
+                    const fileData = {
+                        upload_time: new Date(),
+                        filename: fileInfo.name,
+                        fileext: fileInfo.ext,
+                        filesize: Array.isArray(parts.field.size) ? parts.field.size[index] : parts.field.size,
+                        filepath,
+                        extra_upload,
+                    };
+                    const result = await ctx.service.constructionAtt.save(original_data, fileData, ctx.session.sessionUser.accountId);
+                    if (!result) {
+                        throw '导入数据库保存失败';
+                    }
+                    fileData.uid = ctx.session.sessionUser.accountId;
+                    fileData.u_name = ctx.session.sessionUser.name;
+                    fileData.id = result.insertId;
+                    fileData.orginpath = ctx.app.config.fujianOssPath + filepath;
+                    delete fileData.filepath;
+                    if (!ctx.helper.canPreview(fileData.fileext)) {
+                        fileData.filepath = `/construction/${original_data.tid}/log/${original_data.log_id}/file/${fileData.id}/download`;
+                    } else {
+                        fileData.filepath = ctx.app.config.fujianOssPath + filepath;
+                        fileData.viewpath = ctx.app.config.fujianOssPath + filepath;
+                    }
+                    files.push(fileData);
+                    ++index;
+                }
+                responseData.data = files;
+            } catch (err) {
+                this.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                this.setMessage(err.toString(), this.messageType.ERROR);
+            }
+            ctx.body = responseData;
+        }
+
+        /**
+         * 下载附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async downloadFile(ctx) {
+            const id = ctx.params.fid;
+            if (id) {
+                try {
+                    const fileInfo = await ctx.service.constructionAtt.getDataById(id);
+                    if (fileInfo !== undefined && fileInfo !== '') {
+                        // 解决中文无法下载问题
+                        const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                        let disposition = '';
+                        if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                            disposition = 'attachment; filename=' + encodeURIComponent(fileInfo.filename + fileInfo.fileext);
+                        } else if (userAgent.indexOf('firefox') >= 0) {
+                            disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(fileInfo.filename + fileInfo.fileext) + '"';
+                        } else {
+                            /* safari等其他非主流浏览器只能自求多福了 */
+                            disposition = 'attachment; filename=' + new Buffer(fileInfo.filename + fileInfo.fileext).toString('binary');
+                        }
+                        ctx.response.set({
+                            'Content-Type': 'application/octet-stream',
+                            'Content-Disposition': disposition,
+                            'Content-Length': fileInfo.filesize,
+                        });
+                        // ctx.body = await fs.createReadStream(fileName);
+                        ctx.body = await ctx.helper.ossFileGet(fileInfo.filepath);
+                    } else {
+                        throw '不存在该文件';
+                    }
+                } catch (err) {
+                    this.log(err);
+                    this.setMessage(err.toString(), this.messageType.ERROR);
+                }
+            }
+        }
+
+        /**
+         * 删除附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async deleteFile(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: '',
+            };
+            try {
+                const id = parseInt(ctx.params.id);
+                if (!id) throw '参数有误';
+                const logInfo = await ctx.service.constructionLog.getDataById(id);
+                if (!logInfo) throw '该日志不存在';
+                const data = JSON.parse(ctx.request.body.data);
+                const fileInfo = await ctx.service.constructionAtt.getDataById(data.id);
+                if (!fileInfo || !Object.keys(fileInfo).length) {
+                    throw '该文件不存在';
+                }
+                if (!fileInfo.extra_upload && logInfo.status === constructionConst.status.checked) {
+                    throw '无权限删除';
+                }
+                if (fileInfo !== undefined && fileInfo !== '') {
+                    // 先删除文件
+                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
+                    // 再删除数据库
+                    await ctx.service.constructionAtt.deleteById(data.id);
+                    responseData.data = '';
+                } else {
+                    throw '不存在该文件';
+                }
+            } catch (err) {
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+
+            ctx.body = responseData;
+        }
+    }
+
+    return ConstructionController;
+};

+ 70 - 0
app/middleware/construction_check.js

@@ -0,0 +1,70 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const messageType = require('../const/message_type');
+const _ = require('lodash');
+
+module.exports = options => {
+    /**
+     * 标段校验 中间件
+     * 1. 读取标段数据(包括属性)
+     * 2. 检验用户是否可见标段(不校验具体权限)
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* constructionCheck(next) {
+        try {
+            const id = parseInt(this.params.tid);
+            if (!id) throw '参数错误';
+            const tender = yield this.service.tender.getTender(id, ['id', 'project_id', 'name']);
+            if (!tender) {
+                throw '标段不存在';
+            }
+            // 权限控制
+            const result = yield this.service.constructionAudit.checkPermission(tender, this.session.sessionUser.accountId);
+            if (!result) {
+                throw '当前账号权限不足,请联系管理员添加权限';
+            }
+            this.constructionTender = tender;
+            yield next;
+        } catch (err) {
+            // 输出错误到日志
+            if (err.stack) {
+                this.logger.error(err);
+            } else {
+                this.session.message = {
+                    type: messageType.ERROR,
+                    icon: 'exclamation-circle',
+                    message: err,
+                };
+                this.getLogger('fail').info(JSON.stringify({
+                    error: err,
+                    project: this.session.sessionProject,
+                    user: this.session.sessionUser,
+                    body: this.session.body,
+                }));
+            }
+            if (this.helper.isAjax(this.request)) {
+                if (err.stack) {
+                    this.body = {err: 4, msg: '标段数据未知错误', data: null};
+                } else {
+                    this.body = {err: 3, msg: err.toString(), data: null};
+                }
+            } else {
+                if (this.helper.isWap(this.request)) {
+                    this.redirect('/wap/list');
+                } else {
+                    err === '您无权查看该内容' ? this.redirect(this.request.headers.referer) : this.redirect('/construction');
+                }
+            }
+        }
+    };
+};

+ 58 - 0
app/public/js/construction_index.js

@@ -0,0 +1,58 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2018/10/11
+ * @version
+ */
+const tenderListSpec = (function(){
+    function getTenderNodeHtml(node, arr, pid) {
+        const html = [];
+        html.push('<tr pid="' + pid + '">');
+        // 名称
+        html.push('<td style="min-width: 300px;" class="in-' + node.level + '">');
+        if (node.cid) {
+            html.push('<span onselectstart="return false" style="{-moz-user-select:none}" class="fold-switch mr-1" title="收起" cid="'+ node.sort_id +'"><i class="fa fa-minus-square-o"></i></span> <i class="fa fa-folder-o"></i> ', node.name);
+        } else {
+            html.push('<span class="text-muted mr-2">');
+            html.push(arr.indexOf(node) === arr.length - 1 ? '└' : '├');
+            html.push('</span>');
+            //html.push('<a href="/tender/' + node.id + '">', node[c.field], '</a>');
+            html.push('<a href="/construction/'+ node.id +'/list" name="name" style="min-width: 300px;word-break:break-all;" id="' + node.id + '">', node.name, '</a>');
+        }
+        html.push('</td>');
+
+        // 创建时间
+        html.push('<td style="width:150px" class="text-center">');
+        html.push(node.create_time ? moment(node.create_time).format('YYYY-MM-DD HH:mm:ss') : '');
+        html.push('</td>');
+        // 设置
+        if (is_admin) {
+            html.push('<td style="width: 100px" class="text-center">');
+            if (!node.cid) {
+                html.push('<a href="#add-cy" data-toggle="modal" data-target="#add-cy" data-tid="' + node.id +
+                    '" class="btn btn-outline-primary btn-sm get-audits">成员管理</a>');
+            }
+            html.push('</td>');
+        }
+        html.push('</tr>');
+        return html.join('');
+    }
+    function getTenderTreeHeaderHtml() {
+        const html = [];
+        html.push('<table class="table table-hover table-bordered">');
+        html.push('<thead style="position: fixed;left:56px;top: 34px;">', '<tr>');
+        html.push('<th class="text-center" style="min-width: 300px;">', '标段名称', '</th>');
+        html.push('<th class="text-center" style="width: 150px;">', '创建时间', '</th>');
+        if (is_admin) {
+            html.push('<th class="text-center" style="width: 100px">', '设置', '</th>');
+        }
+        html.push('</tr>', '</thead>');
+        return html.join('');
+    }
+    return { getTenderNodeHtml, getTenderTreeHeaderHtml }
+})();
+
+

+ 225 - 0
app/public/js/construction_info.js

@@ -0,0 +1,225 @@
+$(function () {
+    autoFlashHeight();
+
+    //编辑input及textarea
+    $('.edit-input').on('change', function () {
+        const newVal = _.trim($(this).val());
+        const id = $(this).data('id');
+        if (newVal.length > 1000) {
+            toastr.error('内容字符长度不能超过1000');
+            return false;
+        }
+        updateJsonData(newVal, id);
+    });
+
+    $('.edit-textarea').on('change', function () {
+        const newVal = $(this).val();
+        const id = $(this).data('id');
+        if (newVal.length > 10000) {
+            toastr.error('内容字符长度不能超过10000');
+            return false;
+        }
+        updateJsonData(newVal, id);
+    });
+
+    function updateJsonData(newVal, id) {
+        postData('/construction/' + tender_id + '/log/' + log_id + '/save', { type: 'update_json', updateData: { key: id, value: newVal } }, function (result) {
+        });
+    }
+
+    // 审签人选择
+    let timer = null
+    let oldSearchVal = null
+    $('#gr-search').bind('input propertychange', function(e) {
+        oldSearchVal = e.target.value
+        timer && clearTimeout(timer)
+        timer = setTimeout(() => {
+            const newVal = $('#gr-search').val()
+            let html = ''
+            if (newVal && newVal === oldSearchVal) {
+                accountList.filter(item => item && cur_uid !== item.id && (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 => {
+                            if (item.id !== cur_uid) {
+                                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
+    });
+
+    // 添加到审批流程中
+    $('dl').on('click', 'dd', function () {
+        const id = parseInt($(this).data('id'));
+        if (id) {
+            postData('/construction/' + tender_id + '/log/' + log_id + '/save', { type: 'set_shenpi', uid: id }, function (result) {
+                $('#show-shenpi-btn').hide();
+                $('#show-shenpi span').text(result.shenpi_username);
+                $('#show-shenpi').show();
+            });
+        }
+    });
+
+    $('.remove-shenpi-btn').on('click', function () {
+        postData('/construction/' + tender_id + '/log/' + log_id + '/save', { type: 'remove_shenpi' }, function (result) {
+            $('#show-shenpi-btn').show();
+            $('#show-shenpi span').text('');
+            $('#show-shenpi').hide();
+        });
+    });
+
+    $('#start-btn').click(function () {
+        // 判断是否已选择审签人,有则提交
+        const username = $('#show-shenpi span').text();
+        if (username == '') {
+            toastr.error('请选择审签人再提交');
+            return false;
+        }
+        postData('/construction/' + tender_id + '/log/' + log_id + '/save', { type: 'start' }, function (result) {
+            window.location.reload();
+        });
+    });
+
+    $('#edit-start').click(function () {
+       $('#edit-end').show();
+       $('#edit-start').hide();
+       $('.edit-input').removeAttr('readonly');
+       $('.edit-textarea').removeAttr('readonly');
+    });
+
+    $('#edit-end').click(function () {
+        $('#edit-start').show();
+        $('#edit-end').hide();
+        $('.edit-input').attr('readonly', true);
+        $('.edit-textarea').attr('readonly', true);
+    });
+
+    $('#check-btn').click(function () {
+        postData('/construction/' + tender_id + '/log/' + log_id + '/save', { type: 'checked' }, function (result) {
+            window.location.reload();
+        });
+    });
+
+    // 上传附件
+    $('#upload-file-btn').click(function () {
+        const files = $('#upload-file')[0].files;
+        const formData = new FormData();
+        for (const file of files) {
+            if (file === undefined) {
+                toastr.error('未选择上传文件!');
+                return false;
+            }
+            const filesize = file.size;
+            if (filesize > 30 * 1024 * 1024) {
+                toastr.error('文件大小过大!');
+                return false;
+            }
+            const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+            if (whiteList.indexOf(fileext) === -1) {
+                toastr.error('只能上传指定格式的附件!');
+                return false;
+            }
+            formData.append('size', filesize);
+            formData.append('file[]', file);
+        }
+        if (!filePermission) {
+            toastr.error('无权限上传!');
+            return false;
+        }
+        postDataWithFile('/construction/' + tender_id + '/log/' + log_id + '/file/upload', formData, function (data) {
+            attData = data.concat(attData);
+            // 重新生成List
+            getAllList();
+            $('#upload').modal('hide');
+        }, function () {
+        });
+        $('#upload-file').val('');
+
+    });
+
+    // 删除附件
+    $('body').on('click', '.delete-file', function () {
+        let attid = $(this).data('attid');
+        const data = {id: attid};
+        postData('/construction/' + tender_id + '/log/' + log_id + '/file/delete', data, function (result) {
+            // 删除到attData中
+            const att_index = attData.findIndex(function (item) {
+                return item.id === parseInt(attid);
+            });
+            attData.splice(att_index, 1);
+            getAllList();
+        });
+    });
+
+    // $('#attList').on('click', '.file-atn', function() {
+    //     const id = $(this).attr('f-id');
+    //     postData(`/construction/${tender_id}/log/${log_id}/file/${id}/download`, {}, (data) => {
+    //         const { filepath } = data;
+    //         $('#file-upload').attr('href', filepath);
+    //         $('#file-upload')[0].click();
+    //     })
+    // });
+    // 生成附件列表
+    function getAllList() {
+        let html = '';
+        for(const [index,att] of attData.entries()) {
+            html += `<tr>
+        <td width="5%">${index+1}</td>
+        <td class="text-left"><a href="${att.filepath}" target="_blank">${att.filename}${att.fileext}</a></td>
+        <td width="15%">${moment(att.upload_time).format("YYYY-MM-DD HH:mm:ss")}</td>
+        <td width="10%">
+            <a href="/construction/${tender_id}/log/${log_id}/file/${att.id}/download" class="mr-2" title="下载"><span class="fa fa-download text-primary"></span></a>`
+            html += (att.uid === accountId && (logStatus === constructionStatusConst.checked ? Boolean(att.extra_upload) : true)) ?
+                `<a href="javascript:void(0)" class="mr-2 delete-file" data-attid="${att.id}" title="删除附件"><span class="fa fa-trash text-danger"></span></a>` : '';
+            html += `</td>`;
+        }
+        $('#attList').html(html);
+        $('#attList').on('click', 'tr', function() {
+            $('#attList tr').removeClass('bg-light');
+            $(this).addClass('bg-light');
+        });
+    }
+
+});

+ 140 - 0
app/public/js/construction_list.js

@@ -0,0 +1,140 @@
+$(function () {
+    autoFlashHeight();
+
+    $('.to-log-link').on('click', function () {
+        const status = parseInt($(this).data('status'));
+        const uid = parseInt($('#curReportUid').data('uid'));
+        const queryArr = [];
+        if (status) {
+            queryArr.push('status=' + status);
+        }
+        if (uid) {
+            queryArr.push('uid=' + uid);
+        }
+        const queryStr = queryArr.length > 0 ? '?' + queryArr.join('&') : '';
+        const link = '/construction/' + tender_id + '/list' + queryStr;
+        window.location.href = link;
+    });
+
+    $('.to-log-link2').on('click', function () {
+        const status = parseInt($('#curReportStatus').data('status'));
+        const uid = parseInt($(this).data('uid'));
+        const queryArr = [];
+        if (status) {
+            queryArr.push('status=' + status);
+        }
+        if (uid) {
+            queryArr.push('uid=' + uid);
+        }
+        const queryStr = queryArr.length > 0 ? '?' + queryArr.join('&') : '';
+        const link = '/construction/' + tender_id + '/list' + queryStr;
+        window.location.href = link;
+    });
+    if (reportFlag) {
+        const codeDate = $('#code-date').datepicker({
+            onSelect: function (formattedDate, date, inst) {
+                const showDate = date ? moment(date[date.length - 1]).format('YYYYMMDD') : '';
+                const showCode = showDate ? showDate + getCodeByDate(showDate) : '';
+                if (!showCode) {
+                    $('#existMsg').hide();
+                }
+                $('#show-code').val(showCode);
+            }
+        }).data('datepicker');
+        codeDate.selectDate(new Date());
+        $('#show-code').val(moment().format('YYYYMMDD') + getCodeByDate(moment().format('YYYYMMDD')));
+
+        $('#add-log input[name="log-type"]').on('click', function () {
+            const type = parseInt($(this).val());
+            if (type) {
+                $('#code-date').val('');
+                codeDate.clear();
+                $('#show-code').val('');
+                $('#existMsg').hide();
+            } else {
+                codeDate.clear();
+                $('#code-date').val(moment().format('YYYY-MM-DD'));
+                codeDate.selectDate(new Date());
+                $('#show-code').val(moment().format('YYYYMMDD') + getCodeByDate(moment().format('YYYYMMDD')));
+            }
+        });
+
+        $('#add-log-btn').click(function () {
+            if (_.trim($('#code-date').val()) === '') {
+                toastr.error('请选择日期/周期');
+                return false;
+            }
+
+            const newData = {
+                type: parseInt($('#add-log input[name="log-type"]:checked').val()),
+                code: $('#show-code').val(),
+                period: _.trim($('#code-date').val()),
+            };
+            postData('/construction/' + tender_id + '/list/add', newData, function (result) {
+                console.log(result);
+                window.location.href = '/construction/' + tender_id + '/log/' + result;
+            })
+        });
+
+        // 批量提交
+        $('#confirm-uncheck-btn').click(function () {
+            // 获取选中值
+            const checkedIds = [];
+            $('#uncheck-list input[type="checkbox"]:checked').each(function () {
+                checkedIds.push(parseInt($(this).data('id')));
+            });
+            if (checkedIds.length === 0) {
+                toastr.error('请勾选日志再批量提交');
+                return false;
+            }
+            postData('/construction/' + tender_id + '/list/startmulti', { ids: checkedIds }, function (result) {
+                window.location.reload();
+            });
+        });
+
+        $('.delete-log-a').on('click', function () {
+            $('#delete-log-id').val($(this).data('id'));
+        });
+
+        // 删除
+        $('#delete-log-btn').click(function () {
+            const log_id = $('#delete-log-id').val();
+            if (!log_id) {
+                toastr.error('删除的日志id为空');
+                return false;
+            }
+            postData('/construction/' + tender_id + '/log/' + log_id + '/delete', {}, function (result) {
+                window.location.reload();
+            })
+        });
+
+        function getCodeByDate(date) {
+            let num = '001';
+            if (date) {
+                const dateInfo = _.find(dateCodeList, { date_code: date });
+                if (dateInfo) {
+                    num = convertToThreeDigits(dateInfo.num + 1);
+                    $('#existMsg').show();
+                } else {
+                    $('#existMsg').hide();
+                }
+            } else {
+                $('#existMsg').hide();
+            }
+            return num;
+        }
+
+        function convertToThreeDigits(number) {
+            // 将数字转换为字符串
+            let numStr = String(number);
+
+            // 检查数字是否不足3位
+            if (numStr.length < 3) {
+                // 在前面添加足够的零使其成为3位数
+                numStr = numStr.padStart(3, '0');
+            }
+
+            return numStr;
+        }
+    }
+});

+ 15 - 0
app/router.js

@@ -44,6 +44,8 @@ module.exports = app => {
     // 支付审批中间件
     const paymentTenderCheck = app.middlewares.paymentTenderCheck();
     const paymentDetailCheck = app.middlewares.paymentDetailCheck();
+    // 施工日志中间件
+    const constructionCheck = app.middlewares.constructionCheck();
     // 登入登出相关
     app.get('/login', 'loginController.index');
     app.get('/login/:code', 'loginController.index');
@@ -780,4 +782,17 @@ module.exports = app => {
     app.get('/wx/work/:corpid/project', wxWorkAuth, 'wechatController.workProject');
     app.get('/wx/work/:corpid/test', wxWorkAuth, 'wechatController.workTest');
     app.get('/wx/tips', 'wechatController.tips');
+
+    // 施工日志
+    app.get('/construction', sessionAuth, 'constructionController.index');
+    app.post('/construction/:tid/audit/save', sessionAuth, 'constructionController.auditSave');
+    app.get('/construction/:tid/list', sessionAuth, constructionCheck, 'constructionController.list');
+    app.post('/construction/:tid/list/add', sessionAuth, constructionCheck, 'constructionController.addLog');
+    app.post('/construction/:tid/list/startmulti', sessionAuth, constructionCheck, 'constructionController.startMulti');
+    app.get('/construction/:tid/log/:id', sessionAuth, constructionCheck, 'constructionController.logInfo');
+    app.post('/construction/:tid/log/:id/delete', sessionAuth, constructionCheck, 'constructionController.deleteLog');
+    app.post('/construction/:tid/log/:id/save', sessionAuth, constructionCheck, 'constructionController.logSave');
+    app.post('/construction/:tid/log/:id/file/upload', sessionAuth, constructionCheck, 'constructionController.uploadFile');
+    app.post('/construction/:tid/log/:id/file/delete', sessionAuth, constructionCheck, 'constructionController.deleteFile');
+    app.get('/construction/:tid/log/:id/file/:fid/download', sessionAuth, constructionCheck, 'constructionController.downloadFile');
 };

+ 68 - 0
app/service/construction_att.js

@@ -0,0 +1,68 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class ConstructionAtt extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'construction_attachment';
+        }
+
+        /**
+         * 添加附件
+         * @param {Object} postData - 表单信息
+         * @param {Object} fileData - 文件信息
+         * @param {int} uid - 上传者id
+         * @return {void}
+         */
+        async save(postData, fileData, uid) {
+            const data = {
+                tid: postData.tid,
+                log_id: postData.log_id,
+                uid,
+            };
+            Object.assign(data, fileData);
+            const result = await this.db.insert(this.tableName, data);
+            return result;
+        }
+
+        /**
+         * 获取 详情 所有附件
+         * @param {uuid} td_id - 详情id
+         * @return {Promise<void>}
+         */
+        async getConstructionAttachment(log_id, sort = 'asc') {
+            const sql = 'SELECT ca.*, pa.name As u_name, pa.role As u_role ' +
+                '  FROM ?? As ca ' +
+                '  Left Join ?? As pa ' +
+                '  On ca.uid = pa.id ' +
+                '  Where ca.log_id = ? order by id ' + sort;
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, log_id];
+            const result = await this.db.query(sql, sqlParam);
+            return result.map(item => {
+                item.orginpath = this.ctx.app.config.fujianOssPath + item.filepath;
+                if (!this.ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `/construction/${item.tid}/log/${item.log_id}/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                    item.viewpath = item.filepath;
+                }
+                return item;
+            });
+        }
+    }
+    return ConstructionAtt;
+};

+ 92 - 0
app/service/construction_audit.js

@@ -0,0 +1,92 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class ConstructionAudit extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'construction_audit';
+            this.dataId = 'id';
+        }
+
+        async getList(tid) {
+            const list = await this.db.select(this.tableName, { where: { tid }, orders: [['id', 'desc']] });
+            for (const l of list) {
+                const accountInfo = await this.ctx.service.projectAccount.getDataById(l.uid);
+                l.name = accountInfo.name;
+                l.role = accountInfo.role;
+                l.company = accountInfo.company;
+            }
+            return list;
+        }
+
+        async saveAudits(tid, accountList, transaction = null) {
+            // 判断是否已存在该用户,存在则不插入
+            const pauditList = await this.getAllDataByCondition({ where: { tid } });
+            const pushData = [];
+            for (const a of this._.uniqBy(accountList, 'id')) {
+                if (this._.findIndex(pauditList, { uid: a.id }) === -1) {
+                    const data = {
+                        tid,
+                        uid: a.id,
+                        in_time: new Date(),
+                    };
+                    pushData.push(data);
+                }
+            }
+            if (pushData.length > 0) {
+                return transaction ? await transaction.insert(this.tableName, pushData) : await this.db.insert(this.tableName, pushData);
+            }
+            return false;
+        }
+
+        async delAudit(id) {
+            return await this.db.delete(this.tableName, { id });
+        }
+
+        async updateReport(updateData) {
+            if (!updateData.id || updateData.is_report === undefined) {
+                return false;
+            }
+            return await this.db.update(this.tableName, updateData);
+        }
+
+        async checkPermission(tender, uid) {
+            let flag = false;
+            const accountInfo = await this.ctx.service.projectAccount.getDataById(uid);
+            const permission = accountInfo !== undefined && accountInfo.permission !== ''
+                ? JSON.parse(accountInfo.permission) : null;
+            if (accountInfo.project_id === tender.project_id && (accountInfo.is_admin || (permission !== null && permission.construction !== undefined && permission.construction.indexOf('1') !== -1))) {
+                flag = true;
+            } else {
+                const info = await this.getDataByCondition({ tid: tender.id, uid });
+                if (info) {
+                    flag = true;
+                }
+            }
+            return flag;
+        }
+
+        async getUserList(tid, is_report = null) {
+            const reportSql = is_report !== null ? ' AND ca.`is_report` = ' + is_report : '';
+            const sql = 'SELECT ca.*, pa.name as user_name FROM ?? AS ca LEFT JOIN ?? AS pa ON ca.`uid` = pa.`id` ' +
+                'WHERE ca.`tid` = ?' + reportSql + ' ORDER BY ca.`id` DESC';
+            const params = [this.tableName, this.ctx.service.projectAccount.tableName, tid];
+            return await this.db.query(sql, params);
+        }
+    }
+    return ConstructionAudit;
+};

+ 163 - 0
app/service/construction_log.js

@@ -0,0 +1,163 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+const constructionConst = require('../const/construction');
+
+module.exports = app => {
+
+    class ConstructionLog extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'construction_log';
+        }
+
+        async getCount(params) {
+            return await this.count(params);
+        }
+
+        async getLogList(params) {
+            // 获取所有标段的成员管理人员
+            // const userList = await this.ctx.service.constructionAudit.getUserList(params.tid);
+            const list = await this.getList({ where: params });
+            if (list.length > 0) {
+                const userIdList = this._.uniq(this._.concat(this._.map(list, 'report_uid'), this._.map(list, 'shenpi_uid')));
+                const userList = await this.ctx.service.projectAccount.getAllDataByCondition({ where: { id: userIdList } });
+                for (const l of list) {
+                    l.report_username = this._.find(userList, { id: l.report_uid }) ? this._.find(userList, { id: l.report_uid }).name : '';
+                    l.shenpi_username = l.shenpi_uid && this._.find(userList, { id: l.shenpi_uid }) ? this._.find(userList, { id: l.shenpi_uid }).name : '';
+                }
+            }
+            return list;
+        }
+
+        async getUncheckedLogList(tid, uid) {
+            const uncheckList = await this.getAllDataByCondition({ where: { tid, report_uid: uid, status: constructionConst.status.uncheck }, orders: [['id', 'desc']] });
+            const list = this._.filter(uncheckList, item => item.shenpi_uid !== null);
+            if (list.length > 0) {
+                const userIdList = this._.uniq(this._.concat(this._.map(list, 'report_uid'), this._.map(list, 'shenpi_uid')));
+                const userList = await this.ctx.service.projectAccount.getAllDataByCondition({ where: { id: userIdList } });
+                for (const l of list) {
+                    l.report_username = this._.find(userList, { id: l.report_uid }) ? this._.find(userList, { id: l.report_uid }).name : '';
+                    l.shenpi_username = l.shenpi_uid && this._.find(userList, { id: l.shenpi_uid }) ? this._.find(userList, { id: l.shenpi_uid }).name : '';
+                }
+            }
+            return list;
+        }
+
+        async getDateCodeList(tid) {
+            const sql = 'SELECT `date_code`, count(*) AS num FROM ?? WHERE `tid` = ? GROUP BY `date_code` ORDER BY `date_code`';
+            const params = [this.tableName, tid];
+            return await this.db.query(sql, params);
+        }
+
+        async addLog(tid, uid, data) {
+            if (!data) throw '参数有误';
+            if (!data.code || !data.period || data.type === undefined) throw '参数有误';
+            const newData = {
+                tid,
+                report_uid: uid,
+                create_time: new Date(),
+                type: data.type ? data.type : 0,
+                period: data.period,
+                code: data.code,
+                date_code: data.code.substring(0, 8),
+            };
+            const result = await this.db.insert(this.tableName, newData);
+            return result.insertId;
+        }
+
+        async updateLogJson(id, json, data) {
+            json[data.key] = data.value;
+            const updateData = {
+                id,
+                log_json: JSON.stringify(json),
+            };
+            return await this.db.update(this.tableName, updateData);
+        }
+
+        async setShenpiUser(tid, id, shenpi_uid) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const userInfo = await this.ctx.service.projectAccount.getDataById(shenpi_uid);
+                if (!userInfo) throw '该用户不存在';
+                await transaction.update(this.tableName, { id, shenpi_uid });
+                // 判断有无,无则插入到constructionAudit成员管理表里
+                const audit = await this.ctx.service.constructionAudit.getDataByCondition({ tid, uid: shenpi_uid });
+                if (!audit) {
+                    const addAudit = {
+                        tid,
+                        uid: shenpi_uid,
+                        in_time: new Date(),
+                    };
+                    await transaction.insert(this.ctx.service.constructionAudit.tableName, addAudit);
+                }
+                await transaction.commit();
+                return { shenpi_uid, shenpi_username: userInfo.name };
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async removeShenpiUser(id) {
+            return await this.db.update(this.tableName, { id, shenpi_uid: null });
+        }
+
+        async start(id) {
+            return await this.db.update(this.tableName, { id, status: constructionConst.status.checking, start_time: new Date() });
+        }
+
+        async startMulti(ids, uid) {
+            const list = await this.getAllDataByCondition({ where: { id: ids } });
+            if (list.length > 0) {
+                const updateList = [];
+                for (const l of list) {
+                    if (l.report_uid !== uid) {
+                        throw '非填报人无法提交日志';
+                    }
+                    if (l.status === constructionConst.status.uncheck) {
+                        updateList.push({
+                            id: l.id,
+                            status: constructionConst.status.checking,
+                            start_time: new Date(),
+                        });
+                    }
+                }
+                if (updateList.length > 0) await this.db.updateRows(this.tableName, updateList);
+            }
+            return true;
+        }
+
+        async checked(id) {
+            return await this.db.update(this.tableName, { id, status: constructionConst.status.checked, checked_time: new Date() });
+        }
+
+        async deleteLog(id) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 先删附件,再删数据记录
+                const attList = await this.ctx.service.constructionAtt.getAllDataByCondition({ where: { log_id: id } });
+                await this.ctx.helper.delFiles(attList);
+                await transaction.delete(this.ctx.service.constructionAtt.tableName, { log_id: id });
+                await transaction.delete(this.tableName, { id });
+                await transaction.commit();
+                return true;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+    }
+    return ConstructionLog;
+};

+ 50 - 3
app/service/tender.js

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

+ 23 - 0
app/view/construction/index.ejs

@@ -0,0 +1,23 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex justify-content-between">
+            <div class="d-inline-block" id="show-level"></div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const tenders = JSON.parse(unescape('<%- escape(JSON.stringify(tenderList)) %>'));
+    const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
+    const selfCategoryLevel = JSON.parse(unescape('<%- escape(JSON.stringify(selfCategoryLevel)) %>'));
+    const uid = '<%- uid %>';
+    const pid = '<%- pid %>';
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+
+    const uphlname = 'user_' + uid + '_pro_' + pid + '_category_hide_construction_list';
+</script>

+ 189 - 0
app/view/construction/info.ejs

@@ -0,0 +1,189 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex justify-content-between">
+            <div class="d-inline-block">
+                <a href="/construction/<%- ctx.constructionTender.id %>/list" class="mr-2"><i class="fa fa-chevron-left mr-2"></i>
+                    <span>返回</span></a><%- (ctx.constructionTender.name.length > 20 ? ctx.constructionTender.name.substring(0,20) + '...' : ctx.constructionTender.name) %> / <%- logInfo.code %></div>
+            <div>
+                <span class="pull-right <%- constructionConst.statusClass[logInfo.status] %>"><%- constructionConst.statusString[logInfo.status] %></span>
+                <% if (ctx.session.sessionUser.accountId === logInfo.shenpi_uid && logInfo.status === constructionConst.status.checking) { %>
+                <span class=" pull-right mr-2">|</span>
+                <a href="javascript:void(0);" id="check-btn" class="btn btn-sm btn-success pull-right mr-2">确认审签</a>
+                <% } %>
+                <% if (ctx.session.sessionUser.accountId === logInfo.report_uid && logInfo.status === constructionConst.status.uncheck) { %>
+                <span class=" pull-right mr-2">|</span>
+                <a href="javascript:void(0);" id="start-btn" class="btn btn-sm btn-primary pull-right mr-2">提交审签</a>
+                <% } %>
+                <!-- 提交审签后,可再次编辑,提供取消按钮 -->
+                <% if (logInfo.report_uid === ctx.session.sessionUser.accountId && logInfo.status === constructionConst.status.checking) { %>
+                <span class=" pull-right mr-2">|</span>
+                <a href="javascript:void(0);" id="edit-end" class="btn btn-sm btn-secondary pull-right mr-2" style="display: none">取消</a>
+                <a href="javascript:void(0);" id="edit-start" data-target="#" class="btn btn-sm btn-outline-primary pull-right mr-2">编辑</a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <div class="col-9 px-3 mx-auto">
+                    <div class="card">
+                        <div class="card-header">
+                            基本内容
+                        </div>
+                        <div class="card-body">
+                            <div class="row">
+                                <div class="col-6 form-group row mb-2 mr-2">
+                                    <label class="col-form-label col-2  text-right px-0">日志编号:</label>
+                                    <input type="text" class="form-control form-control-sm col"  value="<%- logInfo.code %>" readonly>
+                                </div>
+                                <div class="col-6 form-group row mb-2">
+                                    <label class="col-form-label col-2  text-right px-0">日志类型:</label>
+                                    <input type="text" class="form-control form-control-sm col"  value="<%- constructionConst.type[logInfo.type] %>" readonly>
+                                </div>
+                            </div>
+                            <div class="row">
+                                <div class="col-6 form-group row mb-2 mr-2">
+                                    <label class="col-form-label col-2  text-right px-0">施工日期:</label>
+                                    <input type="text" class="form-control form-control-sm col" value="<%- logInfo.period %>" readonly>
+                                </div>
+                                <div class="col-6 form-group row mb-2">
+                                    <label class="col-form-label col-2  text-right px-0">施工部位:</label>
+                                    <input type="text" class="form-control form-control-sm col edit-input" value="<%- logInfo.log_json.parts %>" data-id="parts"
+                                           <% if (!(logInfo.status === constructionConst.status.uncheck && logInfo.report_uid === ctx.session.sessionUser.accountId)) { %>readonly<% } %>>
+                                </div>
+                            </div>
+                            <div class="row">
+                                <div class="col-6 form-group row mb-2 mr-2">
+                                    <label class="col-form-label col-2  text-right px-0">填报人:</label>
+                                    <input type="text" class="form-control form-control-sm col" value="<%- logInfo.report_username %>" readonly>
+                                </div>
+                                <div class="col-6 form-group row mb-2">
+                                    <label class="col-form-label col-2  text-right px-0">审签人:</label>
+                                    <% if (logInfo.status === constructionConst.status.uncheck && logInfo.report_uid === ctx.session.sessionUser.accountId) { %>
+                                        <div class="dropdown text-right align-middle pt-1" id="show-shenpi-btn" <% if (logInfo.shenpi_uid) { %>style="display: none" <% } %>>
+                                            <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                                选择审批人
+                                            </button>
+                                            <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton" style="width:220px">
+                                                <div class="mb-2 p-2"><input class="form-control form-control-sm" placeholder="姓名/手机 检索"
+                                                                             id="gr-search" autocomplete="off"></div>
+                                                <dl class="list-unstyled book-list">
+                                                    <% accountGroup.forEach((group, idx) => { %>
+                                                        <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>"
+                                                               data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                                        <div class="dd-content" data-toggleid="<%- idx %>">
+                                                            <% group.groupList.forEach(item => { %>
+                                                                <% if (item.id !== logInfo.report_uid) {%>
+                                                                <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="col-2" id="show-shenpi" <% if (!logInfo.shenpi_uid) { %>style="display: none" <% } %>>
+                                            <label class="col-form-label"><span><%- logInfo.shenpi_username %></span><i class="fa fa-remove text-danger px-1 remove-shenpi-btn" style="cursor: pointer"></i></label>
+                                        </div>
+                                    <% } else { %>
+                                    <input type="text" class="form-control form-control-sm col" value="<%- logInfo.shenpi_username %>" readonly>
+                                    <% } %>
+                                </div>
+                            </div>
+                            <div class="row">
+                                <div class="col-6 form-group row mb-2 mr-2">
+                                    <label class="col-form-label col-2  text-right px-0">天气:</label>
+                                    <textarea class="form-control form-control-sm col edit-textarea" data-id="weather" rows="3" <% if (!(logInfo.status === constructionConst.status.uncheck && logInfo.report_uid === ctx.session.sessionUser.accountId)) { %>readonly<% } %>><%- logInfo.log_json.weather %></textarea>
+                                </div>
+                                <div class="col-6 form-group row mb-2">
+                                    <label class="col-form-label col-2  text-right px-0">温度:</label>
+                                    <textarea class="form-control form-control-sm col edit-textarea" data-id="temperature" rows="3" <% if (!(logInfo.status === constructionConst.status.uncheck && logInfo.report_uid === ctx.session.sessionUser.accountId)) { %>readonly<% } %>><%- logInfo.log_json.temperature %></textarea>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="card">
+                        <div class="card-header">
+                            施工情况记录(施工内容、机械作业、班组工作、施工存在问题)
+                        </div>
+                        <div class="card-body p-0">
+                            <textarea class="form-control form-control-sm edit-textarea" data-id="condition" rows="5" <% if (!(logInfo.status === constructionConst.status.uncheck && logInfo.report_uid === ctx.session.sessionUser.accountId)) { %>readonly<% } %>><%- logInfo.log_json.condition %></textarea>
+                        </div>
+                    </div>
+                    <div  class="card">
+                        <div class="card-header">
+                            技术、质量、安全工作记录(技术质量安全活动、检查评定验收、技术质量安全问题)
+                        </div>
+                        <div class="card-body p-0">
+                            <textarea class="form-control form-control-sm edit-textarea" data-id="work" rows="5" <% if (!(logInfo.status === constructionConst.status.uncheck && logInfo.report_uid === ctx.session.sessionUser.accountId)) { %>readonly<% } %>><%- logInfo.log_json.work %></textarea>
+                        </div>
+                    </div>
+                    <div  class="card">
+                        <div class="card-header">
+                            人员、材料(构配件)、机械到场及运行情况
+                        </div>
+                        <div class="card-body p-0">
+                            <textarea class="form-control form-control-sm edit-textarea" data-id="run" rows="5" <% if (!(logInfo.status === constructionConst.status.uncheck && logInfo.report_uid === ctx.session.sessionUser.accountId)) { %>readonly<% } %>><%- logInfo.log_json.run %></textarea>
+                        </div>
+                    </div>
+                    <div  class="card">
+                        <div class="card-header">
+                            其他事宜
+                        </div>
+                        <div class="card-body p-0">
+                            <textarea class="form-control form-control-sm edit-textarea" data-id="other" rows="5" <% if (!(logInfo.status === constructionConst.status.uncheck && logInfo.report_uid === ctx.session.sessionUser.accountId)) { %>readonly<% } %>><%- logInfo.log_json.other %></textarea>
+                        </div>
+                    </div>
+                    <div  class="card">
+                        <div class="card-header d-flex">
+                            <span>附件</span>
+                            <% if (filePermission) { %>
+                            <div class="ml-auto">
+                                <a href="#upload" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="添加清单"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a>
+                            </div>
+                            <% } %>
+                        </div>
+                        <div class="card-body p-0">
+                            <table class="table table-bordered m-0" id="attList">
+                                <% if (attList.length !== 0) { %>
+                                <% for (const [index, att] of attList.entries()) { %>
+                                <tr class="text-center">
+                                    <td width="5%"><%- index+1 %></td>
+                                    <td class="text-left"><a href="<%- att.filepath %>" target="_blank"><%- att.filename %><%- att.fileext %></a></td>
+                                    <td width="15%"><%- moment(att.upload_time).format("YYYY-MM-DD HH:mm:ss") %></td>
+                                    <td width="10%"><a href="/construction/<%- ctx.constructionTender.id %>/log/<%- logInfo.id %>/file/<%- att.id %>/download" class="mr-2" title="下载"><i class="fa fa-download"></i></a>
+                                    <% if (att.uid === ctx.session.sessionUser.accountId && (logInfo.status === constructionConst.status.checked ? Boolean(att.extra_upload) : true )) { %>
+                                        <a href="javascript:void(0)" class="mr-2 delete-file" data-attid="<%- att.id %>" title="删除附件"><span class="fa fa-trash text-danger"></span></a>
+                                    <% } %>
+                                    </td>
+                                </tr>
+                                <% } %>
+                                <% } %>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const tender_id = <%- ctx.constructionTender.id %>;
+    const log_id = <%- logInfo.id %>;
+    const cur_uid = <%- logInfo.report_uid %>;
+    const accountId = parseInt('<%- ctx.session.sessionUser.accountId %>');
+    const logStatus = parseInt('<%- logInfo.status %>');
+    const filePermission = <%- filePermission %>;
+    const constructionStatusConst = JSON.parse(unescape('<%- escape(JSON.stringify(constructionConst.status)) %>'));
+    <% if (ctx.session.sessionUser.accountId === logInfo.report_uid && logInfo.status === constructionConst.status.uncheck) { %>
+    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    <% } %>
+    const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
+    let attData = JSON.parse(unescape('<%- escape(JSON.stringify(attList)) %>'));
+</script>

+ 23 - 0
app/view/construction/info_modal.ejs

@@ -0,0 +1,23 @@
+<% if (filePermission) { %>
+<!--上传附件-->
+<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true" id="upload">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="myModalLabel">上传附件</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <p>单个文件大小限制:30MB,支持office等文档格式、图片格式、压缩包格式</p>
+                <p><input value="选择文件" type="file" id="upload-file" multiple /></p>
+            </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="upload-file-btn">添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>

+ 101 - 0
app/view/construction/list.ejs

@@ -0,0 +1,101 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main d-flex justify-content-between">
+            <div class="col-4 pl-0">
+                <div class="d-inline-block"><a href="/construction" class="mr-2"><i class="fa fa-chevron-left mr-2"></i><span>返回</span></a><%- (ctx.constructionTender.name.length > 20 ? ctx.constructionTender.name.substring(0,20) + '...' : ctx.constructionTender.name) %></div>
+            </div>
+            <div class="col-3">
+                <div class="d-inline-block mr-2">
+                    <span>日志状态:</span>
+                    <div class="btn-group pl-0">
+                        <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle pl-0" data-toggle="dropdown"
+                                id="curReportStatus" data-status="<%- curStatus ? curStatus.status : 0 %>"><% if (curStatus) { %><%- curStatus.statusName %> (<%- curStatus.num %>)<% } else { %>全部<% } %></button>
+                        <div class="dropdown-menu" aria-labelledby="zhankai">
+                            <% if (curStatus) { %>
+                            <a class="dropdown-item to-log-link" href="javascript:void(0);" data-status="0">全部</a>
+                            <% } %>
+                            <% for (const s of statusList) { %>
+                            <% if (!curStatus || (curStatus && curStatus.status !== s.status)) { %>
+                            <a class="dropdown-item to-log-link" href="javascript:void(0);" data-status="<%- s.status %>"><%- s.statusName %> (<%- s.num %>)</a>
+                            <% } %>
+                            <% } %>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-inline-block mr-2">
+                    <span>填报人:</span>
+                    <div class="btn-group">
+                        <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle pl-0" data-toggle="dropdown"
+                                id="curReportUid" data-uid="<%- curReportUser ? curReportUser.uid : 0 %>"><% if (curReportUser) { %><%- curReportUser.user_name %><% } else { %>全部<% } %></button>
+                        <div class="dropdown-menu" aria-labelledby="zhankai">
+                            <% if (curReportUser) { %>
+                            <a class="dropdown-item to-log-link2" href="javascript:void(0);" data-uid="0">全部</a>
+                            <% } %>
+                            <% for (const ru of reportUserList) { %>
+                            <% if (!curReportUser || (curReportUser && curReportUser.uid !== ru.uid)) { %>
+                            <a class="dropdown-item to-log-link2" href="javascript:void(0);" data-uid="<%- ru.uid %>"><%- ru.user_name %></a>
+                            <% } %>
+                            <% } %>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="ml-auto">
+                <% if (reportFlag) { %>
+                <a href="#add-log" data-toggle="modal" data-target="#add-log" class="btn btn-sm btn-primary pull-right">新建日志</a>
+                <a href="#batch-commit" data-toggle="modal" data-target="#batch-commit" class="btn btn-sm btn-primary pull-right mr-2">批量提交</a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <table class="table table-bordered">
+                    <thead class="text-center">
+                    <tr>
+                        <th width="10%">日志编号</th>
+                        <th width="20%">创建时间</th>
+                        <th width="20%">时间/周期</th>
+                        <th width="10%">日志类型</th>
+                        <th width="10%">填报人</th>
+                        <th width="10%">审签人</th>
+                        <th width="10%">日志状态</th>
+                        <th width="10%">操作</th>
+                    </tr>
+                    </thead>
+                    <tbody class="text-center">
+                    <% for (const log of logList) { %>
+                    <tr>
+                        <td><a href="/construction/<%- ctx.constructionTender.id %>/log/<%- log.id %>"><%- log.code %></a></td>
+                        <td><%- moment(log.create_time).format('YYYY-MM-DD') %></td>
+                        <td><%- log.period %></td>
+                        <td><%- constructionConst.type[log.type] %></td>
+                        <td><%- log.report_username %></td>
+                        <td><%- log.shenpi_username %></td>
+                        <td><span class="<%- constructionConst.statusClass[log.status] %>"><%- constructionConst.statusString[log.status] %></span></td>
+                        <td>
+                            <% if (ctx.session.sessionUser.accountId === log.shenpi_uid && log.status === constructionConst.status.checking) { %>
+                                <a href="/construction/<%- ctx.constructionTender.id %>/log/<%- log.id %>" class="btn btn-success btn-sm">审签</a>
+                            <% } %>
+                            <% if (ctx.session.sessionUser.accountId === log.report_uid && log.status === constructionConst.status.uncheck) { %>
+                                    <a href="/construction/<%- ctx.constructionTender.id %>/log/<%- log.id %>" class="btn btn-primary btn-sm">提交</a>
+                            <a href="#del-bg" data-toggle="modal" data-target="#del-bg" data-id="<%- log.id %>" class="btn btn-outline-danger btn-sm delete-log-a">删除</a>
+                            <% } %>
+                        </td>
+                    </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+                <% include ../layout/page.ejs %>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const tender_id = <%- ctx.constructionTender.id %>;
+    const reportFlag = <%- reportFlag %>;
+    <% if (reportFlag) { %>
+    const dateCodeList = JSON.parse(unescape('<%- escape(JSON.stringify(dateCodeList)) %>'));
+    <% } %>
+</script>

+ 102 - 0
app/view/construction/list_modal.ejs

@@ -0,0 +1,102 @@
+<% if (reportFlag) { %>
+<!--新建日志-->
+<div class="modal fade" id="add-log" 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>
+                        <div class="form-check form-check-inline">
+                            <input class="form-check-input" type="radio" id="type_0" value="0" checked="" name="log-type">
+                            <label class="form-check-label" for="type_0">新建</label>
+                        </div>
+                        <div class="form-check form-check-inline">
+                            <input class="form-check-input" type="radio" id="type_1" value="1" name="log-type">
+                            <label class="form-check-label" for="type_1">补建</label>
+                        </div>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label>日期 / 周期<b class="text-danger">*</b></label>
+                    <input class="datepicker-here form-control form-control-sm mb-1" id="code-date" autocomplete="off" readonly
+                           name="period" placeholder="点击选择时间/周期" value="<%- moment().format('YYYY-MM-DD') %>" data-range="true" data-multiple-dates-separator=" ~ " data-language="zh" type="text">
+                </div>
+                <div class="form-group">
+                    <label>日志编号</label>
+                    <input class="form-control form-control-sm" id="show-code" placeholder="" type="text" value="" readonly>
+                    <div class="text-warning m-1" id="existMsg" style="display: none">
+                        今日已存在日志文件,请注意核查哦。
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button class="btn btn-primary btn-sm" id="add-log-btn">确认新建</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--批量提交-->
+<div class="modal fade" id="batch-commit" 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="modal-height-250" style="overflow-y: auto;">
+                    <table class="table table-bordered">
+                        <thead>
+                        <tr class="text-center">
+                            <th width="10%">选择</th>
+                            <th>日志编号</th>
+                            <th width="15%">填报人</th>
+                            <th width="15%">审签人</th>
+                            <th class="text-center" width="15%">日志状态</th>
+                        </tr>
+                        </thead>
+                        <tbody id="uncheck-list">
+                        <% for (const ull of uncheckLogList) { %>
+                        <tr class="text-center">
+                            <td><input data-id="<%- ull.id %>" type="checkbox"></td>
+                            <td><%- ull.code %></td>
+                            <td><%- ull.report_username %></td>
+                            <td><%- ull.shenpi_username %></td>
+                            <td>待提交</td>
+                        </tr>
+                        <% } %>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button class="btn btn-primary btn-sm" id="confirm-uncheck-btn">确认提交</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
+<!--删除日志-->
+<div class="modal fade" id="del-bg" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">确认删除日志</h5>
+            </div>
+            <div class="modal-body">
+                <h6>删除后,数据无法恢复,请谨慎操作。</h6>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" id="delete-log-id" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" id="delete-log-btn" class="btn btn-sm btn-danger">确定删除</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 233 - 0
app/view/construction/modal.ejs

@@ -0,0 +1,233 @@
+<% if (ctx.session.sessionUser.is_admin) { %>
+<link href="/public/css/bootstrap/bootstrap-table.min.css" rel="stylesheet">
+<link href="/public/css/bootstrap/bootstrap-table-fixed-columns.min.css" rel="stylesheet">
+<style>
+    /*.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer) {*/
+    /*border-bottom: 0;*/
+    /*}*/
+    @-moz-document url-prefix() {
+        table {
+            table-layout: fixed;
+        }
+    }
+    .customize-header tr th .th-inner{
+        padding: 0.3rem!important;
+    }
+</style>
+<div class="modal fade" id="add-cy" 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">
+                <input type="hidden" id="tender_id" />
+                <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 dropdown-menu-left" aria-labelledby="dropdownMenuButton" style="width:220px">
+                        <div class="mb-2 p-2"><input class="form-control form-control-sm" placeholder="姓名/手机 检索"
+                                                     id="gr-search" autocomplete="off"></div>
+                        <dl class="list-unstyled book-list">
+                            <% accountGroup.forEach((group, idx) => { %>
+                                <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>"
+                                       data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                <div class="dd-content" data-toggleid="<%- idx %>">
+                                    <% group.groupList.forEach(item => { %>
+                                        <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>">
+                                            <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                        class="ml-auto"><%- item.mobile %></span></p>
+                                            <span class="text-muted"><%- item.role %></span>
+                                        </dd>
+                                    <% });%>
+                                </div>
+                            <% }) %>
+                        </dl>
+                    </div>
+                </div>
+                <div class="mt-1" style="height:300px">
+                    <table id="construction-audit-table" class="table table-bordered" data-height="300" data-toggle="table">
+                        <thead class="text-center customize-header">
+                        <tr>
+                            <th width="100">成员名称</th>
+                            <th width="150">角色/职位</th>
+                            <th>填报人</th>
+                            <th>操作</th>
+                        </tr>
+                        </thead>
+                        <tbody id="construction-audit-list" class="text-center">
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!-- 弹窗删除权限用户 -->
+<div class="modal fade" id="del-construction-audit" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">删除用户</h5>
+            </div>
+            <div class="modal-body">
+                <h6>确认删除当前所选用户?</h6>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <input type="hidden" id="del-audit-ids" />
+                <button type="button" class="btn btn-sm btn-danger" id="del-audit-btn">确定删除</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script src="/public/js/bootstrap/bootstrap-table.min.js"></script>
+<script src="/public/js/bootstrap/locales/bootstrap-table-zh-CN.min.js"></script>
+<script>
+    $(function () {
+        let timer = null
+        let oldSearchVal = null
+        const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+        const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+        $('#gr-search').bind('input propertychange', function (e) {
+            oldSearchVal = e.target.value
+            timer && clearTimeout(timer)
+            timer = setTimeout(() => {
+                const newVal = $('#gr-search').val()
+                let html = ''
+                if (newVal && newVal === oldSearchVal) {
+                    accountList.filter(item => item && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1))).forEach(item => {
+                        html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                class="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span>
+                    </dd>`
+                    })
+                    $('.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
+        });
+        // 添加到成员中
+        $('dl').on('click', 'dd', function () {
+            const id = parseInt($(this).data('id'));
+            console.log(id);
+            if (!isNaN(id) && id !== 0) {
+                postData('/construction/'+ $('#tender_id').val() + '/audit/save', {type: 'add-audit', id: id}, function (result) {
+                    setList(result);
+                })
+            }
+        });
+        let first = 1;
+        $('#add-cy').on('shown.bs.modal', function () {
+            if (first) {
+                const option = {
+                    locale: 'zh-CN',
+                    height: 300,
+                }
+                $("#construction-audit-table").bootstrapTable('destroy').bootstrapTable(option);
+                first = 0;
+            }
+            const tid = $('#tender_id').val();
+            if (tid) {
+                postData('/construction/'+ $('#tender_id').val() + '/audit/save', { type: 'list' }, function (result) {
+                    setList(result);
+                });
+            }
+        });
+        $('body').on('click', '.get-audits', function () {
+            const tid = $(this).data('tid');
+            $('#tender_id').val(tid);
+            $('#construction-audit-list').html('');
+        });
+
+        function setList(datas) {
+            let list = '';
+            for (const ca of datas) {
+                list += `<tr>
+                            <td>${ca.name}</td>
+                            <td>${ca.role}</td>
+                            <td>
+                                <input type="checkbox" class="report-checkbox" data-type="is_report" value="${ca.id}" ${ca.is_report ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <a href="#del-construction-audit" data-toggle="modal" data-target="#del-construction-audit" class="btn btn-outline-danger btn-sm ml-1 del-construction-audit-a" data-id="${ca.id}">移除</a>
+                            </td>
+                        </tr>`;
+            }
+            $('#construction-audit-list').html(list);
+            $("#construction-audit-table").bootstrapTable('resetView');
+        }
+
+        $('body').on('click', '.del-construction-audit-a', function () {
+            $('#del-audit-ids').val($(this).attr('data-id'));
+            $('#del-construction-audit').modal('show');
+        });
+
+        $('#del-audit-btn').click(function () {
+            let uids = $('#del-audit-ids').val();
+            postData('/construction/'+ $('#tender_id').val() + '/audit/save', { type: 'del-audit', id: uids.split(',') }, function (result) {
+                // toastr.success(`成功添加 位用户`);
+                $('#del-construction-audit').modal('hide');
+                setList(result);
+            })
+        });
+
+        // 上报人权限勾选
+        $('body').on('click', '.report-checkbox', function () {
+            const type = $(this).attr('data-type');
+            const value = $(this).is(':checked') ? 1 : 0;
+            const id = parseInt($(this).val());
+            const updateInfo = {
+                id,
+                is_report: $(this).is(':checked'),
+            }
+            postData('/construction/'+ $('#tender_id').val() + '/audit/save', { type: 'save-report', updateData: updateInfo }, function (result) {
+            })
+        });
+    });
+</script>
+<% } %>

+ 8 - 0
config/menu.js

@@ -65,6 +65,14 @@ const menu = {
         caption: '支付审批',
         children: null,
     },
+    construction: {
+        name: '施工日志',
+        icon: 'fa-pencil-square-o',
+        display: true,
+        url: '/construction',
+        caption: '施工日志',
+        children: null,
+    },
     management: {
         name: '项目管理系统',
         icon: 'fa-cubes',

+ 38 - 0
config/web.js

@@ -1226,6 +1226,44 @@ const JsFiles = {
                 mergeFile: 'payment_compare',
             },
         },
+        construction: {
+            index: {
+                files: [
+                    '/public/js/ztree/jquery.ztree.core.js', '/public/js/ztree/jquery.ztree.exedit.js', '/public/js/decimal.min.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/zh_calc.js',
+                    '/public/js/PinYinOrder.bundle.js',
+                    '/public/js/shares/tender_list_order.js',
+                    '/public/js/shares/show_level.js',
+                    '/public/js/tender_showhide.js',
+                    '/public/js/construction_index.js',
+                    '/public/js/tender_list_base.js',
+                ],
+                mergeFile: 'construction_index',
+            },
+            list: {
+                files: [
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/construction_list.js',
+                ],
+                mergeFile: 'construction_list',
+            },
+            info: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/construction_info.js',
+                ],
+                mergeFile: 'construction_info',
+            },
+        },
     },
 };