瀏覽代碼

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

MaiXinRong 2 年之前
父節點
當前提交
91163046da

+ 169 - 7
app/controller/payment_controller.js

@@ -4,6 +4,10 @@ const JV = require('../reports/rpt_component/jpc_value_define');
 const shenpiConst = require('../const/shenpi');
 const auditConst = require('../const/audit').stage;
 const paymentConst = require('../const/payment');
+const moment = require('moment');
+const path = require('path');
+const sendToWormhole = require('stream-wormhole');
+const fs = require('fs');
 
 module.exports = app => {
 
@@ -354,12 +358,22 @@ module.exports = app => {
                         }
                     }
                 }
+                const auditIdList = ctx.helper._.map(ctx.detail.auditors, 'aid');
+                const rptAuditIdList = ctx.helper._.map(ctx.detail.rptAudits, 'uid');
+                const uidList = ctx.helper._.uniq([...auditIdList, ...rptAuditIdList]);
+
+                // 获取附件列表
+                const attList = await ctx.service.paymentDetailAtt.getPaymentDetailAttachment(ctx.detail.id, 'desc');
                 const renderData = {
                     trInfo: ctx.trInfo,
                     paymentConst,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.payment.detail),
                     auditConst,
                     shenpiConst,
+                    attList,
+                    moment,
+                    whiteList: ctx.app.config.multipart.whitelist,
+                    uidList,
                     preUrl: '/payment/' + ctx.tender.id + '/detail/' + ctx.detail.id,
                     OSS_PATH: ctx.app.config.fujianOssPath,
                 };
@@ -714,12 +728,15 @@ module.exports = app => {
                     where: { project_id: ctx.session.sessionProject.id, enable: 1 },
                     columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
                 });
-                for (const s of trDetailList) {
-                    // s.curAuditor = null;
-                    // 根据期状态返回展示用户
-                    s.curAuditor = await ctx.service.paymentDetailAudit.getAuditorByStatus(s.id, s.status, s.times);
-                    const userInfo = ctx.helper._.find(accountList, { id: s.uid });
-                    s.user_name = userInfo ? userInfo.name : '';
+                if (trDetailList.length > 0) {
+                    for (const s of trDetailList) {
+                        // s.curAuditor = null;
+                        // 根据期状态返回展示用户
+                        s.curAuditor = await ctx.service.paymentDetailAudit.getAuditorByStatus(s.id, s.status, s.times);
+                        const userInfo = ctx.helper._.find(accountList, { id: s.uid });
+                        s.user_name = userInfo ? userInfo.name : '';
+                    }
+                    trDetailList[0].emptySign = await ctx.service.paymentRptAudit.haveEmptySign(trDetailList[0].id);
                 }
                 const renderData = {
                     tender: ctx.tender,
@@ -748,7 +765,7 @@ module.exports = app => {
                             const report_items_json = JSON.parse(trInfo.report_items_json);
                             const items = ['cells', 'signature_audit_cells', 'signature_cells', 'signature_date_cells', 'interact_cells'];
                             for (const item of items) {
-                                if (!difference && report_items_json[item] &&
+                                if (report_items_json[item] &&
                                     !ctx.helper._.isEmpty(ctx.helper._.differenceWith(JSON.parse(JSON.stringify(renderData.rptMsg[item])), report_items_json[item], ctx.helper._.isEqual))) {
                                     // 因为interact_cells里存在undefind值,必须先用JSON.parse和JSON.stringify转义才能对比
                                     difference = true;
@@ -918,6 +935,151 @@ module.exports = app => {
                 ctx.redirect(ctx.request.header.referer);
             }
         }
+
+        /**
+         * 上传附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async uploadDetailFile(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: [],
+            };
+            let stream;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+                const files = [];
+                let index = 0;
+                const extra_upload = ctx.detail.status === auditConst.status.checked;
+                const original_data = {
+                    tender_id: ctx.tender.id,
+                    tr_id: ctx.trInfo.id,
+                    td_id: ctx.detail.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/payment/${original_data.tender_id}/detail/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.paymentDetailAtt.save(original_data, fileData, ctx.session.sessionUser.accountId);
+                    if (!result) {
+                        throw '导入数据库保存失败';
+                    }
+                    fileData.uid = ctx.session.sessionUser.accountId;
+                    fileData.id = result.insertId;
+                    fileData.orginpath = ctx.app.config.fujianOssPath + filepath;
+                    delete fileData.filepath;
+                    if (!ctx.helper.canPreview(fileData.fileext)) {
+                        fileData.filepath = `/payment/${original_data.tender_id}/detail/${original_data.td_id}/file/${fileData.id}/download`;
+                    } else {
+                        fileData.filepath = 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 downloadDetailFile(ctx) {
+            const id = ctx.params.fid;
+            if (id) {
+                try {
+                    const fileInfo = await ctx.service.paymentDetailAtt.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 deleteDetailFile(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: '',
+            };
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const fileInfo = await ctx.service.paymentDetailAtt.getDataById(data.id);
+                if (!fileInfo || !Object.keys(fileInfo).length) {
+                    throw '该文件不存在';
+                }
+                if (!fileInfo.extra_upload && ctx.detail.status === auditConst.status.checked) {
+                    throw '无权限删除';
+                }
+                if (fileInfo !== undefined && fileInfo !== '') {
+                    // 先删除文件
+                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
+                    // 再删除数据库
+                    await ctx.service.paymentDetailAtt.deleteById(data.id);
+                    responseData.data = '';
+                } else {
+                    throw '不存在该文件';
+                }
+            } catch (err) {
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+
+            ctx.body = responseData;
+        }
     }
 
     return PaymentController;

+ 2 - 9
app/middleware/payment_detail_check.js

@@ -50,15 +50,7 @@ module.exports = options => {
                 } else {
                     detail.curOrder = detail.curAuditor.aid === accountId ? detail.curAuditor.order : detail.curAuditor.order - 1;
                 }
-            } else if (this.tender.isTourist) {
-                detail.curTimes = detail.times;
-                if (detail.status === status.uncheck || detail.status === status.checkNo) {
-                    detail.curOrder = 0;
-                } else if (detail.status === status.checked) {
-                    detail.curOrder = _.max(_.map(detail.auditors, 'order'));
-                } else {
-                    detail.curOrder = detail.curAuditor.order;
-                }
+                detail.filePermission = true;
             } else if (auditorIds.indexOf(accountId) !== -1 || rptAuditIds.indexOf(accountId) !== -1 || this.payment.auditPermission.view_all) { // 审批人及签署人及查看所有权人
                 if (detail.status === status.uncheck) {
                     throw '您无权查看该数据';
@@ -75,6 +67,7 @@ module.exports = options => {
                 } else {
                     detail.curOrder = accountId === detail.curAuditor.aid ? detail.curAuditor.order : detail.curAuditor.order - 1;
                 }
+                detail.filePermission = auditorIds.indexOf(accountId) !== -1 || rptAuditIds.indexOf(accountId) !== -1;
             } else { // 其他不可见
                 throw '您无权查看该数据';
             }

+ 193 - 2
app/public/js/payment_detail.js

@@ -25,6 +25,8 @@ $(function () {
     autoFlashHeight();
     auditRptPrintHelper.showPage();
     iniPage();
+    //初始化所有附件列表
+    getAllList();
 
     $('#rpt-form input').on('change', function () {
        const newVal = $(this).val();
@@ -72,16 +74,21 @@ $(function () {
         $('#sign_path').prop('checked', rptAudit.signature_msg.sign_path !== null);
         $('#company_stamp').prop('checked', rptAudit.signature_msg.company_stamp !== null);
         $('#stamp_path').prop('checked', rptAudit.signature_msg.stamp_path !== null);
-        $('#signature_date').val(rptAudit.signature_msg.date ? rptAudit.signature_msg.date : '');
+        $('#signature_date').val(rptAudit.signature_msg.date ? rptAudit.signature_msg.date : new Date());
         signatureDate = !signatureDate ? $('#signature_date').datepicker().data('datepicker') : signatureDate;
         if (signatureDate && rptAudit.signature_msg.date) {
             signatureDate.selectDate(new Date(rptAudit.signature_msg.date));
         } else if (signatureDate) {
-            signatureDate.clear();
+            signatureDate.selectDate(new Date());
+            // signatureDate.clear();
         }
         $('#signature_content').val(rptAudit.signature_msg.content ? rptAudit.signature_msg.content : '');
     });
 
+    $('#sp-done').on('show.bs.modal', function () {
+        $('#sp-done').find('textarea').val(rptAudit.signature_msg.content ? rptAudit.signature_msg.content : '同意');
+    });
+
     $('#commit_sign').click(function () {
         rptAudit.signature_msg.sign_path = $('#sign_path').is(':checked') ? $('#sign_path').val() : null;
         rptAudit.signature_msg.company_stamp = $('#company_stamp').is(':checked') ? $('#company_stamp').val() : null;
@@ -132,4 +139,188 @@ $(function () {
             clearTimeout(timer);
         }, 500);
     }
+
+    //tab change
+    $('a[data-toggle="tab"]').on('shown.bs.tab', function () {
+        const tab = $(this).data('tab');
+        if (tab === 'fujian') {
+            $('#fujian_btn').show();
+        } else {
+            $('#fujian_btn').hide();
+        }
+    });
+
+    // 切换页数
+    $('.page-select').on('click', function () {
+        const totalPageNum = parseInt($('#totalPage').text());
+        const lastPageNum = parseInt($('#currentPage').text());
+        const status = $(this).attr('content');
+        if (status === 'pre' && lastPageNum > 1) {
+            getAllList(lastPageNum-1);
+            $('#annex .check-all-file').prop('checked', false)
+        } else if (status === 'next' && lastPageNum < totalPageNum) {
+            getAllList(lastPageNum+1);
+            $('#annex .check-all-file').prop('checked', false)
+        }
+    });
+    // 上传附件
+    $('#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 (uidList.indexOf(accountId) === -1) {
+            return toastr.error('暂无权限上传!');
+        }
+        postDataWithFile(window.location.pathname + '/file/upload', formData, function (data) {
+            attData = data.concat(attData);
+            // 重新生成List
+            getAllList();
+            $('#addfujian').modal('hide');
+        }, function () {
+        });
+        $('#upload-file').val('');
+
+    });
+
+    // 删除附件
+    $('body').on('click', '.delete-file', function () {
+        let attid = $(this).data('attid');
+        let self = $(this);
+        const data = {id: attid};
+        postData(window.location.pathname + '/file/delete', data, function (result) {
+            // 删除到attData中
+            const att_index = attData.findIndex(function (item) {
+                return item.id === parseInt(attid);
+            });
+            attData.splice(att_index, 1);
+            // 重新生成List
+            if ($('#attList tr').length === 1) {
+                getAllList(parseInt($('#currentPage').text()) - 1);
+
+            } else {
+                getAllList(parseInt($('#currentPage').text()));
+            }
+        });
+    });
+
+    $('#attList').on('click', '.file-atn', function() {
+        const id = $(this).attr('f-id');
+        postData(`/payment/${tenderId}/detail/${detailId}/file/${id}/download`, {}, (data) => {
+            const { filepath } = data;
+            $('#file-upload').attr('href', filepath);
+            $('#file-upload')[0].click();
+        })
+    });
+
+    $('#attList').on('click', '.check-file', function() {
+        const checkedList = $('#attList').find('input:checked');
+        const childs = $('#attList').children().length;
+        const checkBox = $('#check-all-file');
+        if (checkedList.length === childs) {
+            checkBox.prop("checked", true);
+        } else {
+            checkBox.prop("checked", false);
+        }
+    });
+    $('#check-all-file').click(function() {
+        const isCheck = $(this).is(':checked');
+        $('#attList').children().each(function() {
+            $(this).find('input:checkbox').prop("checked", isCheck);
+        })
+    });
+
+    $('#bach-download').click(function() {
+        const fileIds = [];
+        $( '#attList .check-file:checked').each(function() {
+            const fileId = $(this).attr('file-id');
+            fileId && fileIds.push(fileId);
+        });
+
+        if (fileIds.length) {
+            if (fileIds.length > 20) {
+                return toastr.warning(`最大允许20个文件(当前${fileIds.length}个)`);
+            }
+            toastr.success('正在进行压缩文件...', '', { timeOut: 0, extendedTimeOut: 0});
+            $(this).attr('disabled', "true");
+            const btn = $(this);
+
+            const fileArr = [];
+            for (const id of fileIds) {
+                const fileInfo = _.find(currPageFileData, { id: parseInt(id) });
+                fileArr.push({
+                    url: fileInfo.orginpath, //文件的oss存储路径 (必填)
+                    name: fileInfo.filename, // 文件名 (可选, 不需要填扩展名)
+                    foldPath: '' // (可选, 文件在压缩包中的存储路径)
+                });
+            }
+            const packageName = `${trName}-${detailName}-附件.zip`;
+            try {
+                zipOss.downloadFromAliOss(fileArr, packageName, btn);
+            } catch (e) {
+                btn.removeAttr('disabled');
+                toastr.clear();
+                toastr.error('批量下载失败');
+            }
+        }
+    });
 });
+
+// 生成附件列表
+function getAllList(currPageNum = 1) {
+    // 每页最多几个附件
+    const pageCount = 20;
+    // 附件总数
+    const total = attData.length;
+    // 总页数
+    const pageNum = Math.ceil(total/pageCount);
+    $('#totalPage').text(pageNum);
+    $('#currentPage').text(total === 0 ? 0 : currPageNum);
+    // 当前页附件内容
+    const currPageAttData = attData.slice((currPageNum-1)*pageCount, currPageNum*pageCount);
+    currPageFileData = currPageAttData;
+    let html = '';
+    // '/tender/' + tender.id + '/measure/stage/' + stage.order + '/download/file/' + att.id
+    for(const [index,att] of currPageAttData.entries()) {
+        html += `<tr>
+        <td width="25"><input type="checkbox" class="check-file" file-id=${att.id}></td>
+        <td>${((currPageNum-1)*pageCount)+index+1}</td>
+        <td><a href="${att.filepath}" target="_blank" class="pl-0 col-11 att-file-name" file-id=${att.id}>${att.filename}${att.fileext}</a></td>
+        <td>${moment(att.upload_time).format("YYYY-MM-DD HH:mm:ss")}<br>${bytesToSize(att.filesize)}</td>
+        <td>
+            <a href="/payment/${tenderId}/detail/${detailId}/file/${att.id}/download" class="mr-2" title="下载"><span class="fa fa-download text-primary"></span></a>`
+        html += (att.uid === accountId && (detailStatus === auditConst.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>` : '';
+        html += `</td>`;
+    }
+    $('#attList').html(html);
+    $('#attList').on('click', 'tr', function() {
+        $('#attList tr').removeClass('bg-light');
+        $(this).addClass('bg-light');
+    });
+}
+
+function bytesToSize(bytes) {
+    if (parseInt(bytes) === 0) return '0 B';
+    const k = 1024;
+    const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    // return (bytes / Math.pow(k, i)) + ' ' + sizes[i];
+    return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
+}

+ 5 - 1
app/public/js/payment_list.js

@@ -136,7 +136,11 @@ $(function () {
             toastr.error('未配置好表单角色无法新建表单');
             $('#set-bdjs').modal('show');
         } else {
-            $('#add-catalogue').modal('show');
+            if (emptySign) {
+                $('#add-tips').modal('show');
+            } else {
+                $('#add-catalogue').modal('show');
+            }
         }
     });
 

+ 5 - 0
app/router.js

@@ -754,6 +754,11 @@ module.exports = app => {
     app.post('/payment/:id/list/:trid/auditors', sessionAuth, paymentTenderCheck, 'paymentController.detailAuditors');
     app.post('/payment/:id/detail/:did/audit/start', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.startAudit');
     app.post('/payment/:id/detail/:did/audit/check', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.checkAudit');
+    // 附件
+    app.post('/payment/:id/detail/:did/file/upload', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.uploadDetailFile');
+    app.post('/payment/:id/detail/:did/file/delete', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.deleteDetailFile');
+    app.get('/payment/:id/detail/:did/file/:fid/download', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.downloadDetailFile');
+
 
     // 企业微信回调
     app.get('/wx/work/callback/command', 'wechatController.command');

+ 69 - 0
app/service/payment_detail_att.js

@@ -0,0 +1,69 @@
+'use strict';
+
+/**
+ *
+ *  附件
+ * @author Ellisran
+ * @date 2019/1/11
+ * @version
+ */
+
+module.exports = app => {
+    class PaymentDetailAtt extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'payment_detail_attachment';
+        }
+
+        /**
+         * 添加附件
+         * @param {Object} postData - 表单信息
+         * @param {Object} fileData - 文件信息
+         * @param {int} uid - 上传者id
+         * @return {void}
+         */
+        async save(postData, fileData, uid) {
+            const data = {
+                tender_id: postData.tender_id,
+                tr_id: postData.tr_id,
+                td_id: postData.td_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 getPaymentDetailAttachment(td_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.td_id = ? order by id ' + sort;
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, td_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 = `/payment/${item.tender_id}/detail/${item.td_id}/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                }
+                return item;
+            });
+        }
+    }
+
+    return PaymentDetailAtt;
+};

+ 18 - 10
app/service/payment_detail_audit.js

@@ -51,16 +51,18 @@ module.exports = app => {
             const auditors = await this.getAuditGroupByList(preDetail.id, preDetail.times);
             const newAuditors = [];
             for (const a of auditors) {
-                const na = {
-                    tender_id: preDetail.tender_id,
-                    tr_id: preDetail.tr_id,
-                    td_id: newDetail.id,
-                    aid: a.aid,
-                    times: newDetail.times,
-                    order: newAuditors.length + 1,
-                    status: auditConst.status.uncheck,
-                };
-                newAuditors.push(na);
+                if (a.aid !== newDetail.uid) {
+                    const na = {
+                        tender_id: preDetail.tender_id,
+                        tr_id: preDetail.tr_id,
+                        td_id: newDetail.id,
+                        aid: a.aid,
+                        times: newDetail.times,
+                        order: newAuditors.length + 1,
+                        status: auditConst.status.uncheck,
+                    };
+                    newAuditors.push(na);
+                }
             }
             const result = await transaction.insert(this.tableName, newAuditors);
             return (result.effectRows = auditors.length);
@@ -423,6 +425,7 @@ module.exports = app => {
 
         async _checkNo(pid, detailId, checkData, times) {
             const time = new Date();
+            const detailInfo = await ctx.service.paymentDetail.getDataById(detailId);
             // 整理当前流程审核人状态更新
             const audit = await this.getDataByCondition({ td_id: detailId, times, status: auditConst.status.checking });
             if (!audit) {
@@ -438,6 +441,11 @@ module.exports = app => {
                 a.status = auditConst.status.uncheck;
                 order++;
             }
+            // 可能更换了上报人且存在于审批流程中,需要删除
+            const userIndex = this._.findIndex(auditors, { aid: detailInfo.uid });
+            if (userIndex !== -1) {
+                auditors.splice(userIndex, 1);
+            }
             const transaction = await this.db.beginTransaction();
             try {
                 await transaction.update(this.tableName, { id: audit.id, status: checkData.checkType, opinion: checkData.opinion, end_time: time });

+ 5 - 0
app/service/payment_rpt_audit.js

@@ -82,6 +82,11 @@ module.exports = app => {
             }
             return list;
         }
+
+        async haveEmptySign(td_id) {
+            const count = await this.count({ td_id, signature_msg: null });
+            return count > 0;
+        }
     }
 
     return PaymentRptAudit;

+ 92 - 18
app/view/payment/detail.ejs

@@ -31,23 +31,86 @@
     <div class="content-wrap">
         <div class="c-body">
             <div class="sjs-height-0">
-                <div class="row m-0 mt-3">
-                    <div id="rpt-form" class="col-5 mr-3">
-                        <% for (const c of content) { %>
-                        <div>
-                            <h5><%- c.title %>内容</h5>
-                            <% for (const item of c.items) { %>
-                            <div class="form-group">
-                                <label><%- item.label %></label>
-                                <% if (item.type === 'textarea') { %>
-                                    <textarea <% if (ctx.detail.readOnly) { %>readonly<% } %> class="form-control form-control-sm" data-index="<%- item.index %>"><%- item.value %></textarea>
-                                <% } else { %>
-                                    <input <% if (ctx.detail.readOnly) { %>readonly<% } %> type="<%- item.type %>" value="<%- item.type === 'number' && ctx.helper._.indexOf(item.value, '元') !== -1 ? parseFloat(item.value) : item.value %>"  data-index="<%- item.index %>" class="form-control form-control-sm" placeholder="请输入">
-                                <% } %>
+                <div class="row m-0 my-2">
+                    <div class="col-5">
+                        <div class="sjs-bar-1">
+                            <ul class="nav nav-tabs" id="myTab" role="tablist">
+                                <li class="nav-item">
+                                    <a class="nav-link active" id="content-tab" data-tab="form" data-toggle="tab" href="#content" role="tab" aria-controls="content" aria-selected="true">表单内容</a>
+                                </li>
+                                <li class="nav-item">
+                                    <a class="nav-link" id="annex-tab" data-tab="fujian" data-toggle="tab" href="#annex" role="tab" aria-controls="annex" aria-selected="false">附件</a>
+                                </li>
+                                <li class="nav-item ml-auto pt-1" id="fujian_btn" style="display:none;">
+                                    <!--所有附件 翻页-->
+                                    <button  data-toggle="modal" class="btn btn-sm btn-primary" id="bach-download"><i class="fa fa-download "></i> 批量下载</button>
+                                    <!-- <a href="javascript: void(0);" data-toggle="modal" class="btn btn-sm btn-primary" id="bach-download"><i class="fa fa-download "></i> 批量下载</a> -->
+                                    <a href="javascript:void(0);" class="page-select ml-3" content="pre"><i class="fa fa-chevron-left"></i></a> <span id="currentPage">1</span>/<span id="totalPage">10</span> <a href="javascript:void(0);" class="page-select mr-3" content="next"><i class="fa fa-chevron-right"></i></a>
+                                    <% if (ctx.detail.filePermission) { %>
+                                        <a href="#addfujian" 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>
+                                    <% } %>
+                                    <a href="javascript: void(0);" id="zipDown" download style="display: none;"></a>
+                                </li>
+                            </ul>
+                        </div>
+                        <div class="tab-content mt-2" id="myTabContent">
+                            <div class="tab-pane fade show active" id="content" role="tabpanel" aria-labelledby="content-tab">
+                                <div id="rpt-form">
+                                    <% for (const c of content) { %>
+                                    <div>
+                                        <h5><%- c.title %>内容</h5>
+                                        <% for (const item of c.items) { %>
+                                        <div class="form-group">
+                                            <label><%- item.label %></label>
+                                            <% if (item.type === 'textarea') { %>
+                                                <textarea <% if (ctx.detail.readOnly) { %>readonly<% } %> class="form-control form-control-sm" data-index="<%- item.index %>"><%- item.value %></textarea>
+                                            <% } else { %>
+                                                <input <% if (ctx.detail.readOnly) { %>readonly<% } %> type="<%- item.type %>" value="<%- item.type === 'number' && ctx.helper._.indexOf(item.value, '元') !== -1 ? parseFloat(item.value) : item.value %>"  data-index="<%- item.index %>" class="form-control form-control-sm" placeholder="请输入">
+                                            <% } %>
+                                        </div>
+                                        <% } %>
+                                    </div>
+                                    <% } %>
+                                </div>
+                            </div>
+                            <!-- 附件 -->
+                            <div class="tab-pane fade" id="annex"  role="tabpanel" aria-labelledby="annex-tab">
+                                <div class="sjs-sh-1" style="overflow-y: auto;">
+                                    <div class="p-2">
+                                        <table class="table table-bordered" style="word-break:break-all; table-layout: fixed">
+                                            <thead>
+                                            <tr>
+                                                <td width="25" style="background-color: #e9ecef;"><input type="checkbox" id="check-all-file" ></td>
+                                                <th width="40">序号</th>
+                                                <th>名称</th>
+                                                <th width="130">上传时间/大小</th>
+                                                <th width="60">操作</th>
+                                            </tr>
+                                            </thead>
+                                            <tbody id="attList">
+                                            <% if (attList !== undefined && attList !== '') { %>
+                                                <% for (const [index,att] of attList.entries()) { %>
+                                                    <tr>
+                                                        <td width="25"><input type="checkbox" class="check-file" file-id=<%- att.id %>></td>
+                                                        <td><%- index+1 %></td>
+                                                        <td><a href="javascript: void(0);" class="file-atn" f-id="<%- att.id %>"><%- att.filename %><%- att.fileext %></a></td>
+                                                        <td><%- moment(att.upload_time).format("YYYY-MM-DD HH:mm:ss") %><br><%- ctx.helper.bytesToSize(att.filesize) %></td>
+                                                        <td>
+                                                            <a href="/payment/<%- ctx.tender.id %>/detail/<%- ctx.detail.id %>/file/<%- att.id %>/download" class="mr-2" title="下载"><span class="fa fa-download text-primary"></span></a>
+                                                            <% if (att.uid === ctx.session.sessionUser.accountId && (ctx.detail.status === auditConst.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>
+                                                <% } %>
+                                            <% } %>
+                                            </tbody>
+                                        </table>
+                                        <a href="" id="file-upload" target="_blank" style="display: none;"></a>
+                                    </div>
+                                </div>
                             </div>
-                            <% } %>
                         </div>
-                        <% } %>
                     </div>
                     <div class="col-6 ml-5">
                         <div class="d-flex flex-row">
@@ -66,8 +129,13 @@
 </div>
 
 <script type="text/javascript">
+    const accountId = parseInt('<%- ctx.session.sessionUser.accountId %>');
     const tenderId = parseInt('<%- ctx.tender.id %>');
     const detailId = parseInt('<%- ctx.detail.id %>');
+    const detailStatus = parseInt('<%- ctx.detail.status %>');
+    const trName =  JSON.parse(unescape('<%- escape(JSON.stringify(trInfo.rpt_name)) %>'));
+    const detailName =  JSON.parse(unescape('<%- escape(JSON.stringify(ctx.detail.code)) %>'));
+    const auditConst = JSON.parse(unescape('<%- escape(JSON.stringify(auditConst)) %>'));
     const rptAudit = JSON.parse(unescape('<%- escape(JSON.stringify(rptAudit)) %>'));
     <% if (rptAudit &&
             ((ctx.detail.status !== auditConst.status.checkNo && ctx.detail.status !== auditConst.status.checked) ||
@@ -78,6 +146,10 @@
     <% } %>
     let tesRpttData = JSON.parse(unescape('<%- escape(JSON.stringify(report_json)) %>'));
     console.log(tesRpttData);
+    const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
+    let attData = JSON.parse(unescape('<%- escape(JSON.stringify(attList)) %>'));
+    const uidList = JSON.parse(unescape('<%- escape(JSON.stringify(uidList)) %>'));
+    let currPageFileData = [];
     const SCREEN_DPI = [];
     const PAGE_SHOW = {closeWaterMark: 1};
     const current_stage_status = -1;
@@ -236,7 +308,7 @@
                         imgSrc = OSS_PATH + dtlPath;
                     }
                     let couldCreate = true;
-                    if (srcCell.picFeatures && srcCell.picFeatures.length === paths.length && srcCell.picFeatures[dtlPathIdx] === 'not found!') {
+                    if (srcCell.picFeatures && srcCell.picFeatures.length === paths.length && (srcCell.picFeatures[dtlPathIdx] === 'not found!')) {
                         couldCreate = false;
                     }
                     if (couldCreate) {
@@ -249,6 +321,7 @@
                             }
                             let tmpArea = srcCell.area;
                             srcCell.area = area;
+                            srcCell.isOrgShow = (dtlPathIdx !== 0);
                             let signArea = getProperSignatureArea(srcCell, control, 0, 0, JV);
                             srcCell.area = tmpArea;
                             area[JV.PROP_LEFT] = signArea[0];
@@ -259,11 +332,12 @@
                         const newStampCell = {
                             signature_name: 'dummy_pic',
                             control: srcCell.control,
-                            style: srcCell.style,
+                            // style: srcCell.style,
+                            style: (srcCell.style && srcCell.style !== '') ? srcCell.style : 'sign_pic' ,
                             path: imgSrc,
                             isStamp: true,
                             area,
-                            // isOrgShow: (dtlPathIdx === 0),
+                            isOrgShow: (dtlPathIdx === 0),
                         };
                         rstCells.push(newStampCell);
                     }

+ 21 - 0
app/view/payment/detail_modal.ejs

@@ -137,4 +137,25 @@
         </div>
     </div>
 </div>
+<!--添加附件-->
+<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true" id="addfujian">
+    <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>
 <% include ./audit_modal.ejs %>

+ 1 - 0
app/view/payment/list.ejs

@@ -93,5 +93,6 @@
     let rpt_audit = JSON.parse(unescape('<%- escape(JSON.stringify(trInfo.rpt_audit)) %>'));
     let old_rpt_audit = _.cloneDeep(rpt_audit);
     const is_first = parseInt('<%- trInfo.is_first %>');
+    const emptySign = <%- trDetailList && trDetailList[0] ? trDetailList[0].emptySign : false %>
     console.log(rpt_audit);
 </script>

+ 16 - 0
app/view/payment/list_modal.ejs

@@ -64,6 +64,22 @@
 </div>
 
 <!-- 弹窗新建目录 -->
+<div class="modal fade" id="add-tips" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">新建审批</h5>
+            </div>
+            <div class="modal-body">
+                <h5>第<%= trDetailList && trDetailList[0] ? trDetailList[0].order : 1 %>期存在人员报表未签署,新增期后将无法进行签署,请确认是否新增?</h5>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <a class="btn btn-sm btn-primary" data-dismiss="modal" href="#add-catalogue" data-toggle="modal" data-target="#add-catalogue">确定</a>
+            </div>
+        </div>
+    </div>
+</div>
 <div class="modal fade" id="add-catalogue" data-backdrop="static">
     <div class="modal-dialog" role="document">
         <div class="modal-content">

+ 3 - 1
config/web.js

@@ -1159,8 +1159,10 @@ const JsFiles = {
                 mergeFile: 'payment_list',
             },
             detail: {
-                files: ['/public/js/datepicker/datepicker.min.js', '/public/js/datepicker/datepicker.zh.js'],
+                files: ['/public/js/datepicker/datepicker.min.js', '/public/js/datepicker/datepicker.zh.js', '/public/js/moment/moment.min.js',
+                    '/public/js/axios/axios.min.js', '/public/js/js-xlsx/jszip.min.js', '/public/js/file-saver/FileSaver.min.js'],
                 mergeFiles: [
+                    '/public/js/zip_oss.js',
                     '/public/js/payment_detail.js',
                     '/public/js/payment_detail_audit.js',
                 ],