Pārlūkot izejas kodu

feat: 变更附件增加批量下载功能

lanjianrong 4 gadi atpakaļ
vecāks
revīzija
4f983c2dda

+ 45 - 1
app/controller/change_controller.js

@@ -501,7 +501,7 @@ module.exports = app => {
 
                 // 获取是否已存在调用变更令
                 const changeUsedData = await ctx.service.stageChange.getFinalUsedData(ctx.tender.id, change.cid);
-                renderData.stageChangeNum = this.ctx.helper.sum(changeUsedData.map(x => {return Math.abs(x.used_qty)}));
+                renderData.stageChangeNum = this.ctx.helper.sum(changeUsedData.map(x => { return Math.abs(x.used_qty); }));
                 await this.layout('change/info.ejs', renderData, 'change/info_modal.ejs');
             } catch (err) {
                 this.log(err);
@@ -721,6 +721,50 @@ module.exports = app => {
         }
 
         /**
+         * 批量下载 - 压缩成zip文件返回
+         * @param {Oject} ctx - 全局上下文
+         */
+        async downloadZip(ctx) {
+            try {
+                const fileIds = JSON.parse(ctx.request.query.fileIds);
+                // const { fileIds } = JSON.parse(ctx.request.body.data);
+                // console.log('fileIds', fileIds);
+                const { name: changeName } = await ctx.service.changeAtt.getChangeName(ctx.params.cid);
+                const zipFilename = `${ctx.tender.data.name}-工程变更-${changeName}-附件.zip`;
+                const time = Date.now();
+                const zipPath = `app/public/upload/change/fu_jian_zip${time}.zip`;
+                const size = await ctx.service.changeAtt.compressedFile(fileIds, zipPath);
+
+                // 解决中文无法下载问题
+                const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                let disposition = '';
+                if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                    disposition = 'attachment; filename=' + encodeURIComponent(zipFilename);
+                } else if (userAgent.indexOf('firefox') >= 0) {
+                    disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(zipFilename) + '"';
+                } else {
+                    /* safari等其他非主流浏览器只能自求多福了 */
+                    disposition = 'attachment; filename=' + new Buffer(zipFilename).toString('binary');
+                }
+                ctx.response.set({
+                    'Content-Type': 'application/octet-stream',
+                    'Content-Disposition': disposition,
+                    'Content-Length': size,
+                });
+                const readStream = fs.createReadStream(path.join(this.app.baseDir, zipPath));
+
+                ctx.body = readStream;
+                // ctx.body = fs.readFileSync(path.resolve(this.app.baseDir, zipPath));
+                readStream.on('close', () => {
+                    if (fs.existsSync(path.resolve(this.app.baseDir, zipPath))) {
+                        fs.unlinkSync(path.resolve(this.app.baseDir, zipPath));
+                    }
+                });
+            } catch (error) {
+                this.log(error);
+            }
+        }
+        /**
          * 删除附件
          * @param {Object} ctx - egg全局变量
          * @return {void}

+ 235 - 197
app/public/js/change_detail.js

@@ -1,197 +1,235 @@
-'use strict';
-
-/**
- * 变更令详细页js
- *
- * @author EllisRan.
- * @date 2018/11/22
- * @version
- */
-
-$.event.special.valuechange = {
-    teardown: function (namespaces) {
-        $(this).unbind('.valuechange');
-    },
-
-    handler: function (e) {
-        $.event.special.valuechange.triggerChanged($(this));
-    },
-
-    add: function (obj) {
-        $(this).on('keyup.valuechange cut.valuechange paste.valuechange input.valuechange', obj.selector, $.event.special.valuechange.handler)
-    },
-
-    triggerChanged: function (element) {
-        var current = element[0].contentEditable === 'true' ? element.html() : element.val()
-            , previous = typeof element.data('previous') === 'undefined' ? element[0].defaultValue : element.data('previous');
-        if (current !== previous) {
-            element.trigger('valuechange', [element.data('previous')]);
-            element.data('previous', current);
-        }
-    }
-};
-
-$(document).ready(() => {
-    // tab切换
-    $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
-        const tab = $(this).attr('aria-controls');
-        $('.show_title').hide();
-        $('#'+ tab +'_title').show();
-        if (tab === 'bills' && $('#bills').hasClass('first-bill-pane')) {
-            if (table) {
-                table.destroy();
-            }
-            table = $('.table-list').removeAttr('width').DataTable(billsTable);
-            if (!$('.change-detail-checkbox').is(':checked')) {
-                const column = table.column(3);
-                column.visible(!column.visible());
-            }
-            $('#bills').removeClass('first-bill-pane');
-        }
-    });
-
-    // 上传附件
-    $('#upload-file-btn').click(function () {
-        const files = $('#upload-file')[0].files;
-        const formData = new FormData();
-        formData.append('cid', $('#changeId').val());
-        formData.append('tid', $('#tenderId').val());
-        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);
-        }
-        postDataWithFile('/change/upload/file', formData, function (data) {
-            $('#addfujian').modal('hide');
-            let html = '';
-            let index = $('#attList tr').length + 1;
-            for (const fileInfo of data) {
-                html += '<tr> ' +
-                    '<td>' + index + '</td> ' +
-                    `<td><a href="javascript: void(0);" class="file-atn" f-id="${fileInfo.id}">${fileInfo.filename}${fileInfo.fileext}</a></td>`+
-                    '<td>' + fileInfo.filesize + '</td> ' +
-                    '<td>' + fileInfo.in_time + '</td> ' +
-                    '<td> <a class="btn btn-light btn-sm delete-file" data-attid="' + fileInfo.id + '"  title="删除附件"><span class="fa fa-trash text-danger"></span></a> </td> ' +
-                    '</tr>';
-                ++index;
-            }
-            $('#attList').append(html);
-        }, function () {
-
-        });
-        $('#upload-file').val('');
-
-    });
-
-    // 删除附件
-    $('body').on('click', '.delete-file', function () {
-        let attid = $(this).data('attid');
-        console.log(attid);
-        let self = $(this);
-        const data = {id: attid};
-        postData('/change/delete/file', data, function (result) {
-            self.parents('tr').remove();
-            // 重新排序
-            let newsort = 1;
-            $('#attList tr').each(function(){
-                $(this).children('td').eq(0).text(newsort);
-                newsort++;
-            });
-        });
-    });
-    // /change/download/file/
-    $('#attList').on('click', '.file-atn', function() {
-        const id = $(this).attr('f-id')
-        postData(`/change/download/file/${id}`, {}, (data) => {
-            const { filepath } = data
-            $('#file-upload').attr('href', filepath)
-            $('#file-upload')[0].click()
-        })
-    })
-    //
-    const cca = getLocalCache('change-checkbox-account-' + accountId);
-    if (cca !== null && cca !== undefined) {
-        $('#customCheck1').prop('checked', cca !== 'false');
-    }
-    // 变更详情展示和隐藏
-    $('.change-detail-checkbox').on('click', function (e) {
-        if($(e.target).is('label')){
-            return;
-        }
-        let column = table.column(3);
-        // 设置用户项目本地记录展示和隐藏情况
-        setLocalCache('change-checkbox-account-'+ accountId, $(this).is(':checked'));
-        column.visible(!column.visible());
-    })
-
-    // 重新审批获取手机验证码
-    // 获取验证码
-    let isPosting = false;
-    $("#get-code").click(function() {
-        if (isPosting) {
-            return false;
-        }
-        const btn = $(this);
-
-        $.ajax({
-            url: '/profile/code?_csrf=' + csrf,
-            type: 'post',
-            data: { mobile: authMobile, type: 'shenpi' },
-            dataTye: 'json',
-            error: function() {
-                isPosting = false;
-            },
-            beforeSend: function() {
-                isPosting = true;
-            },
-            success: function(response) {
-                isPosting = false;
-                if (response.err === 0) {
-                    codeSuccess(btn);
-                    $("input[name='code']").removeAttr('readonly');
-                    $("#re-shenpi-btn").removeAttr('disabled');
-                } else {
-                    toast(response.msg, 'error');
-                }
-            }
-        });
-    });
-});
-/**
- * 获取成功后的操作
- *
- * @param {Object} btn - 点击的按钮
- * @return {void}
- */
-function codeSuccess(btn) {
-    let counter = 60;
-    btn.addClass('disabled').text('重新获取 ' + counter + 'S');
-    btn.parent().siblings('input').removeAttr('readonly').attr('placeholder', '输入短信中的6位验证码');
-    const bindBtn = $("#bind-btn");
-    bindBtn.removeClass('btn-secondary disabled').addClass('btn-primary');
-
-    const countDown = setInterval(function() {
-        const countString = counter - 1 <= 0 ? '' : ' ' + (counter - 1) + 'S';
-        // 倒数结束后
-        if (countString === '') {
-            clearInterval(countDown);
-            btn.removeClass('disabled');
-        }
-        const text = '重新获取' + countString;
-        btn.text(text);
-        counter -= 1;
-    }, 1000);
-}
+'use strict';
+
+/**
+ * 变更令详细页js
+ *
+ * @author EllisRan.
+ * @date 2018/11/22
+ * @version
+ */
+
+$.event.special.valuechange = {
+    teardown: function (namespaces) {
+        $(this).unbind('.valuechange');
+    },
+
+    handler: function (e) {
+        $.event.special.valuechange.triggerChanged($(this));
+    },
+
+    add: function (obj) {
+        $(this).on('keyup.valuechange cut.valuechange paste.valuechange input.valuechange', obj.selector, $.event.special.valuechange.handler)
+    },
+
+    triggerChanged: function (element) {
+        var current = element[0].contentEditable === 'true' ? element.html() : element.val()
+            , previous = typeof element.data('previous') === 'undefined' ? element[0].defaultValue : element.data('previous');
+        if (current !== previous) {
+            element.trigger('valuechange', [element.data('previous')]);
+            element.data('previous', current);
+        }
+    }
+};
+
+$(document).ready(() => {
+    // tab切换
+    $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+        const tab = $(this).attr('aria-controls');
+        $('.show_title').hide();
+        $('#'+ tab +'_title').show();
+        if (tab === 'bills' && $('#bills').hasClass('first-bill-pane')) {
+            if (table) {
+                table.destroy();
+            }
+            table = $('.table-list').removeAttr('width').DataTable(billsTable);
+            if (!$('.change-detail-checkbox').is(':checked')) {
+                const column = table.column(3);
+                column.visible(!column.visible());
+            }
+            $('#bills').removeClass('first-bill-pane');
+        }
+    });
+
+    // 上传附件
+    $('#upload-file-btn').click(function () {
+        const files = $('#upload-file')[0].files;
+        const formData = new FormData();
+        formData.append('cid', $('#changeId').val());
+        formData.append('tid', $('#tenderId').val());
+        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);
+        }
+        postDataWithFile('/change/upload/file', formData, function (data) {
+            $('#addfujian').modal('hide');
+            let html = '';
+            let index = $('#attList tr').length + 1;
+            for (const fileInfo of data) {
+                html += '<tr> ' +
+                    `<td width="25"><input type="checkbox" class="check-file" file-id=${fileInfo.id}></td>` +
+                    '<td>' + index + '</td> ' +
+                    `<td><a href="javascript: void(0);" class="file-atn" f-id="${fileInfo.id}">${fileInfo.filename}${fileInfo.fileext}</a></td>`+
+                    '<td>' + fileInfo.filesize + '</td> ' +
+                    '<td>' + fileInfo.in_time + '</td> ' +
+                    `<td>
+                        <a href="/change/download/file/${fileInfo.id}" class="btn btn-light btn-sm" title="下载"><span class="fa fa-download text-primary"></span></a>
+                        <a class="btn btn-light btn-sm delete-file" data-attid="${fileInfo.id}"  title="删除附件"><span class="fa fa-trash text-danger"></span></a>
+                    </td>`+
+                    // '<td> <a class="btn btn-light btn-sm delete-file" data-attid="' + fileInfo.id + '"  title="删除附件"><span class="fa fa-trash text-danger"></span></a> </td> ' +
+                    '</tr>';
+                ++index;
+            }
+            $('#attList').append(html);
+        }, function () {
+
+        });
+        $('#upload-file').val('');
+
+    });
+
+    // 删除附件
+    $('body').on('click', '.delete-file', function () {
+        let attid = $(this).data('attid');
+        let self = $(this);
+        const data = {id: attid};
+        postData('/change/delete/file', data, function (result) {
+            self.parents('tr').remove();
+            // 重新排序
+            let newsort = 1;
+            $('#attList tr').each(function(){
+                $(this).children('td').eq(1).text(newsort);
+                newsort++;
+            });
+        });
+    });
+    // /change/download/file/
+    $('#attList').on('click', '.file-atn', function() {
+        const id = $(this).attr('f-id')
+        postData(`/change/download/file/${id}`, {}, (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) {
+            const tid = $('#tenderId').val()
+            const cid = $('#changeId').val()
+            $('#downloadZip').attr('href', `/tender/${tid}/change/${cid}/download/compresse-file?fileIds=${JSON.stringify(fileIds)}`);
+            $('#downloadZip')[0].click();
+        }
+    });
+
+    //
+    const cca = getLocalCache('change-checkbox-account-' + accountId);
+    if (cca !== null && cca !== undefined) {
+        $('#customCheck1').prop('checked', cca !== 'false');
+    }
+    // 变更详情展示和隐藏
+    $('.change-detail-checkbox').on('click', function (e) {
+        if($(e.target).is('label')){
+            return;
+        }
+        let column = table.column(3);
+        // 设置用户项目本地记录展示和隐藏情况
+        setLocalCache('change-checkbox-account-'+ accountId, $(this).is(':checked'));
+        column.visible(!column.visible());
+    })
+
+    // 重新审批获取手机验证码
+    // 获取验证码
+    let isPosting = false;
+    $("#get-code").click(function() {
+        if (isPosting) {
+            return false;
+        }
+        const btn = $(this);
+
+        $.ajax({
+            url: '/profile/code?_csrf=' + csrf,
+            type: 'post',
+            data: { mobile: authMobile, type: 'shenpi' },
+            dataTye: 'json',
+            error: function() {
+                isPosting = false;
+            },
+            beforeSend: function() {
+                isPosting = true;
+            },
+            success: function(response) {
+                isPosting = false;
+                if (response.err === 0) {
+                    codeSuccess(btn);
+                    $("input[name='code']").removeAttr('readonly');
+                    $("#re-shenpi-btn").removeAttr('disabled');
+                } else {
+                    toast(response.msg, 'error');
+                }
+            }
+        });
+    });
+});
+/**
+ * 获取成功后的操作
+ *
+ * @param {Object} btn - 点击的按钮
+ * @return {void}
+ */
+function codeSuccess(btn) {
+    let counter = 60;
+    btn.addClass('disabled').text('重新获取 ' + counter + 'S');
+    btn.parent().siblings('input').removeAttr('readonly').attr('placeholder', '输入短信中的6位验证码');
+    const bindBtn = $("#bind-btn");
+    bindBtn.removeClass('btn-secondary disabled').addClass('btn-primary');
+
+    const countDown = setInterval(function() {
+        const countString = counter - 1 <= 0 ? '' : ' ' + (counter - 1) + 'S';
+        // 倒数结束后
+        if (countString === '') {
+            clearInterval(countDown);
+            btn.removeClass('disabled');
+        }
+        const text = '重新获取' + countString;
+        btn.text(text);
+        counter -= 1;
+    }, 1000);
+}

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

@@ -250,7 +250,7 @@ $(document).ready(function () {
             const fileId = $(this).attr('file-id')
             fileId && fileIds.push(fileId)
         })
-        console.log('fileIds', fileIds)
+        // console.log('fileIds', fileIds)
 
         if (fileIds.length) {
             // postData( `/tender/${tid}/measure/material/${order}/file/download/compresse-file`, { fileIds })

+ 1 - 0
app/router.js

@@ -305,6 +305,7 @@ module.exports = app => {
     app.get('/change/download/file/:id', sessionAuth, 'changeController.downloadFile');
     app.post('/change/download/file/:id', sessionAuth, 'changeController.checkFile');
     app.post('/change/delete/file', sessionAuth, 'changeController.deleteFile');
+    app.get('/tender/:id/change/:cid/download/compresse-file', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.downloadZip');
     app.post('/tender/:id/change/delete', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.delete');
     app.post('/tender/:id/change/bills', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.bills');
 

+ 134 - 76
app/service/change_att.js

@@ -1,76 +1,134 @@
-'use strict';
-
-/**
- *
- *  附件
- * @author Ellisran
- * @date 2019/1/11
- * @version
- */
-
-module.exports = app => {
-    class ChangeAtt extends app.BaseService {
-        /**
-         * 构造函数
-         *
-         * @param {Object} ctx - egg全局变量
-         * @return {void}
-         */
-        constructor(ctx) {
-            super(ctx);
-            this.tableName = 'change_attachment';
-        }
-
-        /**
-         * 添加附件
-         * @param {Object} postData - 表单信息
-         * @param {Object} fileData - 文件信息
-         * @param {int} uid - 上传者id
-         * @return {void}
-         */
-        async save(postData, fileData, uid) {
-            const data = {
-                tid: postData.tid,
-                cid: postData.cid,
-                uid,
-            };
-            Object.assign(data, fileData);
-            const result = await this.db.insert(this.tableName, data);
-            return result;
-        }
-
-        /**
-         * 获取 变更令 所有附件
-         * @param {uuid} cid - 变更令id
-         * @return {Promise<void>}
-         */
-        async getChangeAttachment(cid) {
-            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.cid = ?';
-            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid];
-            return await this.db.query(sql, sqlParam);
-        }
-
-        /**
-         * 获取所有附件
-         * @param {String} cid 变更令id
-         */
-        async getAllChangeFiles(cid) {
-            const { ctx } = this;
-            const result = await this.db.select(this.tableName, { where: { cid } });
-            return result.map(item => {
-                if (!ctx.helper.canPreview(item.fileext)) {
-                    item.filepath = `/change/download/file/${item.id}`;
-                } else {
-                    item.filepath = item.filepath.replace(/^app|\/app/, '');
-                }
-                return item;
-            });
-        }
-    }
-
-    return ChangeAtt;
-};
+'use strict';
+
+/**
+ *
+ *  附件
+ * @author Ellisran
+ * @date 2019/1/11
+ * @version
+ */
+
+const archiver = require('archiver');
+const path = require('path');
+const fs = require('fs');
+module.exports = app => {
+    class ChangeAtt extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'change_attachment';
+        }
+
+        /**
+         * 添加附件
+         * @param {Object} postData - 表单信息
+         * @param {Object} fileData - 文件信息
+         * @param {int} uid - 上传者id
+         * @return {void}
+         */
+        async save(postData, fileData, uid) {
+            const data = {
+                tid: postData.tid,
+                cid: postData.cid,
+                uid,
+            };
+            Object.assign(data, fileData);
+            const result = await this.db.insert(this.tableName, data);
+            return result;
+        }
+
+        /**
+         * 获取 变更令 所有附件
+         * @param {uuid} cid - 变更令id
+         * @return {Promise<void>}
+         */
+        async getChangeAttachment(cid) {
+            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.cid = ?';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        /**
+         * 获取所有附件
+         * @param {String} cid 变更令id
+         */
+        async getAllChangeFiles(cid) {
+            const { ctx } = this;
+            const result = await this.db.select(this.tableName, { where: { cid } });
+            return result.map(item => {
+                if (!ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `/change/download/file/${item.id}`;
+                } else {
+                    item.filepath = item.filepath.replace(/^app|\/app/, '');
+                }
+                return item;
+            });
+        }
+
+        /**
+         * 将文件压缩成zip,并返回zip文件的路径
+         * @param {array} fileIds - 文件数组id
+         * @param {string} zipPath - 压缩文件存储路径
+         * @return {string} 压缩后的zip文件路径
+         */
+        async compressedFile(fileIds, zipPath) {
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('id', {
+                value: fileIds,
+                operate: 'in',
+            });
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const files = await this.db.query(sql, sqlParam);
+            return new Promise(resolve => {
+                // 每次开一个新的archiver
+                const ziparchiver = archiver('zip');
+                const outputPath = fs.createWriteStream(path.resolve(this.app.baseDir, zipPath));
+                ziparchiver.pipe(outputPath);
+                files.forEach(item => {
+                    ziparchiver.file(path.resolve(this.app.baseDir, item.filepath), { name: item.filename + item.fileext });
+                });
+
+                // 存档警告
+                ziparchiver.on('warning', function(err) {
+                    console.log(err);
+                    if (err.code === 'ENOENT') {
+                        console.warn('stat故障和其他非阻塞错误');
+                    } else {
+                        throw err;
+                    }
+                });
+
+                // 存档出错
+                ziparchiver.on('error', function(err) {
+                    console.log(err);
+                    throw err;
+                });
+                ziparchiver.finalize();
+                outputPath.on('close', () => {
+                    resolve(ziparchiver.pointer());
+                });
+            });
+        }
+
+        /**
+         * 返回所查询的变更令的名称
+         * @param {string} cid - 变更令id
+         */
+        async getChangeName(cid) {
+            const sql = 'SELECT name FROM ?? WHERE cid = ?';
+            const sqlParam = [this.ctx.service.change.tableName, cid];
+            return await this.db.queryOne(sql, sqlParam);
+        }
+    }
+
+    return ChangeAtt;
+};

+ 136 - 137
app/service/login_logging.js

@@ -1,137 +1,136 @@
-'use strict';
-
-/**
- * 登录日志-数据模型
- *
- * @author lanjianrong
- * @date 2020/8/31
- * @version
- */
-const UAParser = require('ua-parser-js');
-
-module.exports = app => {
-    class LoginLogging extends app.BaseService {
-        constructor(ctx) {
-            super(ctx);
-            this.tableName = 'login_logging';
-        }
-
-        /**
-         * 创建记录
-         * @param {Object} payload - 载荷
-         */
-        async createLog(payload) {
-            const transaction = await this.db.beginTransaction();
-            try {
-                transaction.insert(this.tableName, payload);
-                await transaction.commit();
-            } catch (error) {
-                await transaction.rollback();
-                throw error;
-            }
-        }
-
-        /**
-         * 创建登录日志
-         * @return {Boolean} 日志是否创建成功
-         * @param {Number} type - 登录类型
-         * @param {Number} status - 是否显示记录
-         */
-        async addLoginLog(type, status) {
-            const { ctx } = this;
-            const ip = ctx.request.ip ? ctx.request.ip : '';
-            const ipInfo = await this.getIpInfoFromApi(ip);
-            const parser = new UAParser(ctx.header['user-agent']);
-            const osInfo = parser.getOS();
-            const cpuInfo = parser.getCPU();
-            const browserInfo = parser.getBrowser();
-            const payload = {
-                os: `${osInfo.name} ${osInfo.version} ${cpuInfo.architecture}`,
-                browser: `${browserInfo.name} ${browserInfo.version}`,
-                ip,
-                address: ipInfo,
-                uid: ctx.session.sessionUser.accountId,
-                pid: ctx.session.sessionProject.id,
-                type,
-                show: status,
-            };
-            return await this.createLog(payload);
-        }
-
-        /**
-         * 根据ip请求获取详细地址
-         * @param {String} a_ip - ip地址
-         * @return {String} 详细地址
-         */
-        async getIpInfoFromApi(a_ip = '') {
-            console.log('a_ip', a_ip);
-            if (!a_ip) return '';
-            if (a_ip === '127.0.0.1' || a_ip === '::1' || a_ip.indexOf('192.168') !== -1) return '服务器本机访问';
-            const { ip = '', region = '', city = '', isp = '' } = await this.sendRequest(a_ip);
-            let address = '';
-            region && (address += region + '省');
-            city && (address += city + '市 ');
-            isp && (address += isp + ' ');
-            ip && (address += `(${ip})`);
-            return address;
-        }
-
-        /**
-         * 发送请求获取详细地址
-         * @param {String} ip - ip地址
-         * @param {Number} max - 最大重试次数
-         * @return {Object} the result of request
-         * @private
-         */
-        async sendRequest(ip, max = 3) {
-            return new Promise(resolve => {
-                const start = () => {
-                    if (max <= 0) {
-                        resolve(); // 已达到最大重试次数,返回空的执行承若
-                    }
-                    max--;
-                    this.ctx.curl(`https://api01.aliyun.venuscn.com/ip?ip=${ip}`, {
-                        dateType: 'json',
-                        encoding: 'utf8',
-                        timeout: 2000,
-                        headers: {
-                            Authorization: 'APPCODE 85c64bffe70445c4af9df7ae31c7bfcc',
-                        },
-                    }).then(({ status, data }) => {
-                        if (status === 200) {
-                            const result = JSON.parse(data.toString()).data;
-                            if (!result.ip) {
-                                start();
-                            } else {
-                                max++;
-                                resolve(result);
-                            }
-                        } else {
-                            max--;
-                            start();
-                        }
-                    }).catch(() => {
-                        start();
-                    });
-                };
-                start();
-            });
-        }
-
-        /**
-         * 获取登录日志
-         * @param {Number} pid - 项目id
-         * @param {Number} uid - 用户id
-         * @return {Promise<Array>} 日志数组
-         */
-        async getLoginLogs(pid, uid) {
-            return this.db.select(this.tableName, {
-                where: { pid, uid, show: 0 },
-                orders: [['create_time', 'desc']],
-                columns: ['browser', 'create_time', 'ip', 'os', 'address'],
-                limit: 10, offset: 0,
-            });
-        }
-    }
-    return LoginLogging;
-};
+'use strict';
+
+/**
+ * 登录日志-数据模型
+ *
+ * @author lanjianrong
+ * @date 2020/8/31
+ * @version
+ */
+const UAParser = require('ua-parser-js');
+
+module.exports = app => {
+    class LoginLogging extends app.BaseService {
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'login_logging';
+        }
+
+        /**
+         * 创建记录
+         * @param {Object} payload - 载荷
+         */
+        async createLog(payload) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                transaction.insert(this.tableName, payload);
+                await transaction.commit();
+            } catch (error) {
+                await transaction.rollback();
+                throw error;
+            }
+        }
+
+        /**
+         * 创建登录日志
+         * @return {Boolean} 日志是否创建成功
+         * @param {Number} type - 登录类型
+         * @param {Number} status - 是否显示记录
+         */
+        async addLoginLog(type, status) {
+            const { ctx } = this;
+            const ip = ctx.request.ip ? ctx.request.ip : '';
+            const ipInfo = await this.getIpInfoFromApi(ip);
+            const parser = new UAParser(ctx.header['user-agent']);
+            const osInfo = parser.getOS();
+            const cpuInfo = parser.getCPU();
+            const browserInfo = parser.getBrowser();
+            const payload = {
+                os: `${osInfo.name} ${osInfo.version} ${cpuInfo.architecture}`,
+                browser: `${browserInfo.name} ${browserInfo.version}`,
+                ip,
+                address: ipInfo,
+                uid: ctx.session.sessionUser.accountId,
+                pid: ctx.session.sessionProject.id,
+                type,
+                show: status,
+            };
+            return await this.createLog(payload);
+        }
+
+        /**
+         * 根据ip请求获取详细地址
+         * @param {String} a_ip - ip地址
+         * @return {String} 详细地址
+         */
+        async getIpInfoFromApi(a_ip = '') {
+            if (!a_ip) return '';
+            if (a_ip === '127.0.0.1' || a_ip === '::1' || a_ip.indexOf('192.168') !== -1) return '服务器本机访问';
+            const { ip = '', region = '', city = '', isp = '' } = await this.sendRequest(a_ip);
+            let address = '';
+            region && (address += region + '省');
+            city && (address += city + '市 ');
+            isp && (address += isp + ' ');
+            ip && (address += `(${ip})`);
+            return address;
+        }
+
+        /**
+         * 发送请求获取详细地址
+         * @param {String} ip - ip地址
+         * @param {Number} max - 最大重试次数
+         * @return {Object} the result of request
+         * @private
+         */
+        async sendRequest(ip, max = 3) {
+            return new Promise(resolve => {
+                const start = () => {
+                    if (max <= 0) {
+                        resolve(); // 已达到最大重试次数,返回空的执行承若
+                    }
+                    max--;
+                    this.ctx.curl(`https://api01.aliyun.venuscn.com/ip?ip=${ip}`, {
+                        dateType: 'json',
+                        encoding: 'utf8',
+                        timeout: 2000,
+                        headers: {
+                            Authorization: 'APPCODE 85c64bffe70445c4af9df7ae31c7bfcc',
+                        },
+                    }).then(({ status, data }) => {
+                        if (status === 200) {
+                            const result = JSON.parse(data.toString()).data;
+                            if (!result.ip) {
+                                start();
+                            } else {
+                                max++;
+                                resolve(result);
+                            }
+                        } else {
+                            max--;
+                            start();
+                        }
+                    }).catch(() => {
+                        start();
+                    });
+                };
+                start();
+            });
+        }
+
+        /**
+         * 获取登录日志
+         * @param {Number} pid - 项目id
+         * @param {Number} uid - 用户id
+         * @return {Promise<Array>} 日志数组
+         */
+        async getLoginLogs(pid, uid) {
+            return this.db.select(this.tableName, {
+                where: { pid, uid, show: 0 },
+                orders: [['create_time', 'desc']],
+                columns: ['browser', 'create_time', 'ip', 'os', 'address'],
+                limit: 10, offset: 0,
+            });
+        }
+    }
+    return LoginLogging;
+};

+ 8 - 2
app/view/change/info.ejs

@@ -153,12 +153,15 @@
             </div>
             <div id="files_title" class="show_title" style="line-height:34px;display: none">
                 <% if (auditStatus === 1 || auditStatus === 2 || auditStatus === 6) { %>
-                <a href="#addfujian" data-toggle="modal" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="添加清单">
+                <!-- <a href="#addfujian" data-toggle="modal" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="添加清单">
                     <i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件
-                </a>
+                </a> -->
+                <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>
                 <% } else { %>
                 <h2 class="change-title">附件</h2>
                 <% } %>
+                <a href="javascript: void(0);" data-toggle="modal" class="btn btn-sm btn-light text-primary" id="bach-download"><i class="fa fa-download "></i> 批量下载</a>
+                <a href="" id="downloadZip" style="display: none;" download></a>
             </div>
             <input id="tenderId" value="<%= tender.id %>" type="hidden">
             <input id="changeId" value="<%= change.cid %>" type="hidden">
@@ -705,6 +708,7 @@
             <table class="table table-bordered">
                 <thead>
                 <tr>
+                    <td width="25" style="background-color: #e9ecef;"><input type="checkbox" id="check-all-file" ></td>
                     <th width="50">序号</th>
                     <th>名称</th>
                     <th width="90">大小</th>
@@ -716,11 +720,13 @@
                 <% 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><%- ctx.helper.bytesToSize(att.filesize) %></td>
                     <td><%- moment(att.in_time * 1000).format('YYYY-MM-DD') %></td>
                     <td>
+                        <a href="/change/download/file/<%- att.id %>" class="btn btn-light btn-sm" title="下载"><span class="fa fa-download text-primary"></span></a>
                         <% if (att.uid === uid && (auditStatus === 1 || auditStatus === 2 || auditStatus === 6)) { %>
                         <a class="btn btn-light btn-sm delete-file" data-attid="<%- att.id %>" title="删除附件"><span class="fa fa-trash text-danger"></span></a>
                         <% } %>