Bläddra i källkod

feat: 增加预付款附件功能

lanjianrong 4 år sedan
förälder
incheckning
03f1051959

+ 99 - 0
app/controller/advance_controller.js

@@ -1,6 +1,9 @@
 'use strict';
 const accountGroup = require('../const/account_group').group;
 const auditConst = require('../const/audit').advance;
+const sendToWormhole = require('stream-wormhole');
+const path = require('path');
+const fs = require('fs');
 module.exports = app => {
     class advanceController extends app.BaseController {
 
@@ -61,6 +64,7 @@ module.exports = app => {
                 auditConst,
                 jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.advance.info),
                 preUrl: `/tender/${ctx.tender.id}/advance/${ctx.advance.id}`,
+                whiteList: ctx.app.config.multipart.whitelist,
             };
             // 获取所有项目参与者
             if ((ctx.advance.status === auditConst.status.uncheck || ctx.advance.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.advance.uid) {
@@ -83,6 +87,7 @@ module.exports = app => {
                 }
             }
             data.auditHistory = auditHistory;
+            data.fileList = await ctx.service.advanceFile.getAdvanceFiles({ vid: ctx.advance.id });
             return data;
         }
 
@@ -294,6 +299,100 @@ module.exports = app => {
                 ctx.redirect(ctx.request.header.referer);
             }
         }
+
+        _checkAdvanceFileCanModify(ctx) {
+            // 检查登录用户,是否可操作
+            const accountId = ctx.session.sessionUser.accountId;
+            if (!ctx.advance.curAuditor) {
+                if (ctx.advance.status === auditConst.status.uncheck || ctx.advance.status === auditConst.status.checkNo && accountId === ctx.advance.uid) {
+                    return;
+                }
+                throw '该预付款期当前您无权操作';
+            } else {
+                if (ctx.advance.curAuditor.audit_id === accountId) return;
+                throw '该预付款期当前您无权操作';
+            }
+        }
+
+        /**
+         * 上传附件
+         * @param {*} ctx 上下文
+         */
+        async upload(ctx) {
+            let stream;
+            try {
+                this._checkAdvanceFileCanModify(ctx);
+                const parts = this.ctx.multipart({
+                    autoFields: true,
+                });
+                const files = [];
+                const create_time = Date.parse(new Date()) / 1000;
+                let idx = 0;
+                while ((stream = await parts()) !== undefined) {
+                    if (!stream.filename) {
+                        // 如果没有传入直接返回
+                        return;
+                    }
+                    const fileInfo = path.parse(stream.filename);
+                    const filepath = `public/upload/${this.ctx.tender.id.toString()}/yfk/fujian_${create_time + idx.toString() + fileInfo.ext}`;
+                    await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, 'app', filepath));
+                    files.push({ filepath, name: stream.filename });
+                    ++idx;
+                    stream && (await sendToWormhole(stream));
+                }
+                const in_time = new Date();
+                const payload = files.map(file => {
+                    let idx;
+                    if (Array.isArray(parts.field.name)) {
+                        idx = parts.field.name.findIndex(name => name === file.name);
+                    } else {
+                        idx = 'isString';
+                    }
+                    const newFile = {
+                        uid: ctx.session.sessionUser.accountId,
+                        vid: ctx.advance.id,
+                        create_time: in_time,
+                        filepath: file.filepath,
+                        filesize: ctx.helper.bytesToSize(idx === 'isString' ? parts.field.size : parts.field.size[idx]),
+                        filename: file.name,
+                    };
+                    return newFile;
+                });
+                // 执行文件信息写入数据库
+                await ctx.service.advanceFile.saveFileMsgToDb(payload);
+                // 将最新的当前标段的所有文件信息返回
+                const data = await ctx.service.advanceFile.getAdvanceFiles({ vid: ctx.advance.id });
+                ctx.body = { err: 0, msg: '', data };
+            } catch (err) {
+                stream && (await sendToWormhole(stream));
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        /**
+         * 删除附件
+         * @param {Ojbect} ctx 上下文
+         */
+        async deleteFile(ctx) {
+            try {
+                const { id } = JSON.parse(ctx.request.body.data);
+                const fileInfo = await ctx.service.advanceFile.getDataById(id);
+                if (fileInfo) {
+                    // 先删除文件
+                    await fs.unlinkSync(path.resolve(this.app.baseDir, './app', fileInfo.filepath));
+                    // 再删除数据库
+                    await ctx.service.advanceFile.delete(id);
+                } else {
+                    throw '不存在该文件';
+                }
+                const data = await ctx.service.advanceFile.getAdvanceFiles({ vid: ctx.advance.id });
+                ctx.body = { err: 0, msg: '请求成功', data };
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
     }
     return advanceController;
 };

+ 139 - 26
app/public/js/advance_audit.js

@@ -9,6 +9,9 @@
  */
 
 $(document).ready(function () {
+    let oldVal = null
+    let timer = null
+    handleFileList(fileList)
     // 控制上报弹窗的文案
     function checkModal(isHide) {
         if (isHide) {
@@ -21,11 +24,6 @@ $(document).ready(function () {
             $('#tm-submit').show()
         }
     }
-    // 获取审核相关url
-    function getUrlPre () {
-        const path = window.location.pathname.split('/');
-        return _.take(path, 4).join('/') + '/' + advance.id;
-    }
     // 审批人分组选择
     $('#account_group').change(function () {
         let account_html = '<option value="0">选择审批人</option>'
@@ -43,7 +41,7 @@ $(document).ready(function () {
         let id = $(this).val()
         id = parseInt(id)
         if (id !== 0) {
-            postData(getUrlPre() + '/audit/add', { auditorId: id }, (data) => {
+            postData(preUrl + '/audit/add', { auditorId: id }, (data) => {
                 // <p class="m-0 ml-2"><small class="text-muted">中交第一公路工程局有限公司国道311线满别公路施工一分部</small></p>
                 const html = []
                 html.push('<li class="list-group-item" auditorId="'+ data.audit_id +'"><a href="javascript: void(0)" class="text-danger pull-right">移除</a>')
@@ -76,7 +74,7 @@ $(document).ready(function () {
         const data = {
             auditorId: parseInt(li.attr('auditorId')),
         };
-        postData(getUrlPre() + '/audit/delete', data, (result) => {
+        postData(preUrl + '/audit/delete', data, (result) => {
             li.remove();
             for (const rst of result) {
                 const aLi = $('li[auditorId=' + rst.audit_id + ']');
@@ -109,9 +107,9 @@ $(document).ready(function () {
         }
         const prev_amount = prevAdvance && prevAdvance.prev_total_amount || 0
         const prev_total_amount = ZhCalc.add(cur_amount, prev_amount)
-        const remark = $('#ad-remark').val() || null
+        const remark = $('#ad-remark').val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ') || null
         const data = {pay_ratio, cur_amount, prev_amount, prev_total_amount, remark, status: auditConst.status.checking}
-        postData(getUrlPre() + '/audit/start', data, (data) => {
+        postData(preUrl + '/audit/start', data, (data) => {
             window.location.reload()
         }, () => {
             window.location.reload()
@@ -163,36 +161,151 @@ $(document).ready(function () {
         const data = {
             pay_ratio,
             cur_amount,
-            prev_amount: p_amount,
-            prev_total_amount: ZhCalc.add(cur_amount, p_amount),
         }
-        update(data)
+        oldVal = {
+            cur_amount: parseInt($(`.pay-input[data-type=${1}]`).val()),
+            remark: filterText($('#ad-remark').val())
+        }
+        clearTimeout(timer)
+        timer = setTimeout(() => {
+            if (checkInput()) {
+                update(data)
+                clearTimeout(timer)
+            }
+        }, 2000);
     })
 
+    function checkInput() {
+        const newVal = {
+            cur_amount: parseInt($(`.pay-input[data-type=${1}]`).val()),
+            remark: filterText($('#ad-remark').val())
+        }
+        return newVal.pay_ratio === oldVal.pay_ratio && newVal.remark === oldVal.remark
+    }
+
     $('#ad-remark').on('input propertychange', function(e) {
-        const data = { remark: e.target.value || ''}
-        update(data)
+        const remark = filterText(e.target.value);
+        const data = { remark }
+        oldVal = {
+            cur_amount: parseInt($(`.pay-input[data-type=${1}]`).val()),
+            remark
+        }
+        clearTimeout(timer)
+        timer = setTimeout(() => {
+            if (checkInput()) {
+                update(data)
+                clearTimeout(timer)
+            }
+        }, 2000);
     })
 
+    function filterText(text) {
+        if (!text) return null
+        return text.replace(/(\r\n)|(\n)/g, '<br/>').replace(/\s/g, ' ')
+    }
     function update(data) {
-        postData(getUrlPre() + '/update', data)
+        postData(preUrl + '/update', data)
     }
 
-    function reverse(num){
-        return 1^num
+    $('#file-modal-target').click(function () {
+        $('#file-modal').trigger('click')
+    })
+    $('#file-ok').click(function () {
+        const files = Array.from($('#file-modal')[0].files)
+        const valiData = files.map(v => {
+            const ext = v.name.substring(v.name.lastIndexOf('.') + 1)
+            return {
+                size: v.size,
+                ext
+            }
+        })
+        if (validateFiles(valiData)) {
+            if (files.length) {
+                const formData = new FormData()
+                files.forEach(file => {
+                    formData.append('name', file.name)
+                    formData.append('size', file.size)
+                    formData.append('file', file)
+                })
+                postDataWithFile(preUrl + '/file/upload', formData, function (result) {
+                    handleFileList(result)
+                    $('#file-cancel').click()
+                });
+            }
+        }
+    })
+    function handleFileList(files) {
+        $('#file-content').empty()
+        const { uncheck, checkNo } = auditConst
+        const newFiles = files.map(file => {
+            let showDel = false;
+            if (file.uid === cur_uid) {
+                if (!curAuditor) {
+                    advance.status === uncheck && cur_uid === advance.uid && (showDel = true)
+                    advance.status === checkNo && cur_uid === advance.uid && (showDel = true)
+                } else {
+                    curAuditor.audit_id === cur_uid && (showDel = true)
+                }
+            }
+            return {...file, showDel}
+        })
+        let html = `<tr><td colspan="3"><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></td></tr>`
+        newFiles.forEach((file, idx) => {
+            if (file.showDel) {
+                html += `<tr><td width="70">${idx + 1}</td><td><a href="/${file.filepath}" target="_blank">${file.filename}</a></td><td width="90"><a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}">移除</a></td></tr>`
+            } else {
+                html += `<tr><td width="70">${idx + 1}</td><td><a href="/${file.filepath}" target="_blank">${file.filename}</a></td><td width="90"></td></tr>`
+            }
+        })
+        $('#file-content').append(html)
     }
 
-    function formatMoney(s, dot = ',') {
-        if (!s) return '0.00';
-        s = parseFloat((s + '').replace(/[^\d\.-]/g, '')).toFixed(2) + '';
-        let l = s.split('.')[0].split('').reverse(),
-            r = s.split('.')[1];
-        let t = '';
-        for (let i = 0; i < l.length; i++) {
-            t += l[i] + ((i + 1) % 3 == 0 && (i + 1) != l.length ? dot : '');
+    $('#file-content').on('click', 'a', function () {
+        if ($(this).hasClass('file-del')) {
+            const id = $(this).data('id')
+            postData(preUrl + '/file/del', {id}, (result) => {
+                handleFileList(result)
+            })
         }
-        return t.split('').reverse().join('') + '.' + r;
+    })
+
+})
+
+
+/**
+ * 校验文件大小、格式
+ * @param {Array} files 文件数组
+ */
+function validateFiles(files) {
+    if (files.length > 10) {
+        toastr.error('至多同时上传10个文件');
+        return false
     }
+    return files.every(file => {
+        if (file.size > 1024 * 1024 * 30) {
+            toastr.error('文件大小限制为30MB');
+            return false
+        }
+        if (whiteList.indexOf('.' + file.ext) === -1) {
+            toastr.error('请上传正确的格式文件');
+            return false
+        }
+        return true
     })
+}
 
+function reverse(num){
+    return 1^num
+}
 
+function formatMoney(s, dot = ',') {
+    if (!s) return '0.00';
+    s = parseFloat((s + '').replace(/[^\d\.-]/g, '')).toFixed(2) + '';
+    let l = s.split('.')[0].split('').reverse(),
+        r = s.split('.')[1];
+    let t = '';
+    for (let i = 0; i < l.length; i++) {
+        t += l[i] + ((i + 1) % 3 == 0 && (i + 1) != l.length ? dot : '');
+    }
+    return t.split('').reverse().join('') + '.' + r;
+}

+ 2 - 0
app/router.js

@@ -118,6 +118,8 @@ module.exports = app => {
     app.post('/tender/:id/advance/:order/audit/start', sessionAuth, tenderCheck, advanceCheck, 'advanceController.start');
     app.post('/tender/:id/advance/:order/audit/check', sessionAuth, tenderCheck, advanceCheck, 'advanceController.checkAudit');
     app.post('/tender/:id/advance/:order/update', sessionAuth, tenderCheck, advanceCheck, 'advanceController.update');
+    app.post('/tender/:id/advance/:order/file/upload', sessionAuth, tenderCheck, advanceCheck, 'advanceController.upload');
+    app.post('/tender/:id/advance/:order/file/del', sessionAuth, tenderCheck, advanceCheck, 'advanceController.deleteFile');
 
     // 标段协作办公
     app.get('/tender/:id/cooperation', sessionAuth, tenderCheck, 'tenderController.tenderCooperation');

+ 9 - 4
app/service/advance.js

@@ -1,7 +1,6 @@
 'use strict';
 
 const auditConst = require('../const/audit').advance;
-const bc = require('../lib/base_calc.js');
 
 module.exports = app => {
     class Advance extends app.BaseService {
@@ -131,11 +130,17 @@ module.exports = app => {
 
         /**
          * 更新预付款记录
-         * @param {Object} condition 载荷
+         * @param {Object} payload 载荷
          * @param {Number} id 预付款id
          */
-        async updateAdvance(condition, id) {
-            return await this.update(condition, {
+        async updateAdvance(payload, id) {
+            const { ctx } = this;
+            const prevRecord = await this.getPreviousRecord(ctx.tender.id, ctx.advance.type) || { prev_total_amount: 0 };
+            const { cur_amount } = payload;
+            console.log(ctx.helper.add(cur_amount, prevRecord.prev_total_amount));
+            payload.prev_total_amount = ctx.helper.add(cur_amount, prevRecord.prev_total_amount);
+            console.log('payload', payload);
+            return await this.update(payload, {
                 id,
             });
 

+ 48 - 0
app/service/advance_file.js

@@ -0,0 +1,48 @@
+'use strict';
+const auditConst = require('../const/audit');
+/**
+ * 附件表 数据模型
+ * @author LanJianRong
+ * @date 2020/8/17
+ * @version
+ */
+
+module.exports = app => {
+    class AdvanceFile extends app.BaseService {
+        /**
+         * 构造函数
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'advance_file';
+        }
+
+        /**
+         * 获取文件
+         * @param {*} payload 载荷|查询条件
+         */
+        async getAdvanceFiles(payload) {
+            return this.db.select(this.tableName, payload);
+        }
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+    }
+    return AdvanceFile;
+};

+ 8 - 5
app/view/advance/detail.ejs

@@ -76,7 +76,7 @@
                                 <td colspan="3">
                                     <textarea id="ad-remark" class="form-control form-control-sm"
                                         <%- isEdited && ctx.session.sessionUser.accountId === ctx.advance.uid ? '' : 'disabled' %>
-                                        ><%- advance.remark %></textarea>
+                                        ></textarea>
                                 </td>
                             </tr>
                         </tbody>
@@ -87,10 +87,7 @@
                                 <th colspan="3" class="text-center">附件</th>
                             </tr>
                         </thead>
-                        <tbody>
-                            <tr><td colspan="3"><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></td></tr>
-                            <tr><td width="70">1</td><td><a href="">11212.jpg</a></td><td width="90"><a href="" class="text-danger">移除</a></td></tr>
-                            <tr><td width="70">2</td><td><a href="">444555.jpg</a></td><td width="90"><a href="" class="text-danger">移除</a></td></tr>
+                        <tbody id="file-content">
                         </tbody>
                     </table>
                     <% if(isEdited && ctx.session.sessionUser.accountId === ctx.advance.uid) { %>
@@ -344,12 +341,16 @@
     </div>
 </div>
 <script>
+    const cur_uid = parseInt('<%- ctx.session.sessionUser.accountId %>');
     const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
     const advance = JSON.parse('<%- JSON.stringify(advance) %>');
     const prevAdvance = JSON.parse('<%- JSON.stringify(prevAdvance) %>');
     const isEdited = JSON.parse('<%- isEdited %>');
     const advancePayTotal = parseInt('<%- advancePayTotal %>');
     const preUrl = '<%- preUrl %>';
+    const fileList = JSON.parse('<%- JSON.stringify(fileList) %>') || [];
+    const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
+    const curAuditor = JSON.parse('<%- JSON.stringify(ctx.advance.curAuditor) %>');
     $('#fold-btn').click(function() {
         const type = $(this).data('target')
         if (type === 'hide') {
@@ -362,6 +363,8 @@
             $(this).text('收起历史记录流程')
         }
     });
+    // 处理换行
+    $('#ad-remark').html(advance.remark.replace(/<br\/>/g, '\n').replace(/' '/, '\s'));
 </script>
 <% if(isEdited && ctx.session.sessionUser.accountId === ctx.advance.uid) { %>
 <script>

+ 0 - 1
app/view/advance/index.ejs

@@ -94,5 +94,4 @@
 <script>
     const type = parseInt('<%- type %>');
     const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
-    const advanceList = JSON.parse('<%- JSON.stringify(advanceList) %>');
 </script>

+ 22 - 0
app/view/advance/modal_audit.ejs

@@ -17,6 +17,28 @@
         </div>
     </div>
 </div>
+<!--添加附件-->
+<div class="modal fade" 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><a href="javascript: void(0);" class="btn btn-primary" id="file-modal-target">选择文件</a></p>
+            <input type="file" id="file-modal" multiple="multiple" style="display: none;">
+        </div>
+        <div class="modal-footer">
+            <button id="file-cancel" type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
+            <button id="file-ok"type="button" class="btn btn-primary">添加</button>
+        </div>
+        </div>
+    </div>
+    </div>
 <% if (ctx.advance.status === auditConst.status.checking) { %>
     <% if (ctx.advance.curAuditor && ctx.advance.curAuditor.audit_id === ctx.session.sessionUser.accountId) { %>
         <!--审批通过-->