Browse Source

feat: 材料调差附件下载改版

lanjianrong 4 years ago
parent
commit
420a7d4187

+ 44 - 0
app/controller/material_controller.js

@@ -990,6 +990,50 @@ module.exports = app => {
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
         }
+
+        /**
+         * 批量下载 - 压缩成zip文件返回
+         * @param {Object} 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 zipFilename = `${ctx.tender.data.name}-材料调差-${ctx.params.order}-附件.zip`;
+                const time = Date.now();
+                const zipPath = `app/public/upload/${ctx.tender.id}/tc/fu_jian_zip${time}.zip`;
+                const size = await ctx.service.stageAtt.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);
+            }
+        }
     }
 
     return MaterialController;

+ 60 - 12
app/public/js/material_file.js

@@ -46,9 +46,9 @@ $(document).ready(function () {
         }
     })
     // 选择/未选所有期列表
-    $('#file-checkbox').click(function() {
-        getAllList()
-    })
+    // $('#file-checkbox').click(function() {
+    //     getAllList()
+    // })
 
     // 删除附件
     $('body').on('click', '.delete-file', function () {
@@ -100,6 +100,20 @@ $(document).ready(function () {
 
             curPageNo !== $(this).text() && getAllList(parseInt($(this).text()))
         }
+    });
+
+    $('.dropdown-item').click(function() {
+        const type = $('#dropdownMenuButton').attr('btn-type')
+        if (type === 'curr') {
+            $(this).text('当前期')
+            $('#dropdownMenuButton').text('所有期')
+            $('#dropdownMenuButton').attr('btn-type', 'all')
+        } else {
+            $(this).text('所有期')
+            $('#dropdownMenuButton').text('当前期')
+            $('#dropdownMenuButton').attr('btn-type', 'curr')
+        }
+        getAllList()
     })
     // 生成所有附件列表
     function getAllList(currPageNum = 1) {
@@ -109,14 +123,14 @@ $(document).ready(function () {
         // 总页数
         const pageNum = Math.ceil(total/pageCount);
         // 当前页附件内容
-        const currPageAttData = fileData && $('#file-checkbox').is(':checked') ? fileData.slice((currPageNum-1)*pageCount, currPageNum*pageCount) : filterFileData.map((v, index) => {
+        const currPageAttData = fileData && $('#dropdownMenuButton').attr('type') === 'all' ? fileData.slice((currPageNum-1)*pageCount, currPageNum*pageCount) : filterFileData.map((v, index) => {
             return {...v, index }
         }).slice((currPageNum-1)*pageCount, currPageNum*pageCount);
 
         renderHtml(currPageAttData)
         // 渲染分页器
         renderPagination(currPageNum, pageNum)
-    }
+    };
 
 
     function renderPagination(pageNo, pageSize) {
@@ -185,12 +199,14 @@ $(document).ready(function () {
         }
 
         $('.page-next').before(html)
-    }
+    };
 
     function renderHtml(list) {
         let html = '';
+        $('#check-all-file').prop("checked", false)
         list.forEach(fileInfo => {
             html += `<tr style="height: 31px;">
+            <td width="25"><input type="checkbox" class="check-file" file-id=${fileInfo.id}></td>
             <td>${fileInfo.index + 1}</td>
             <td><a href="/${fileInfo.filepath}" target="_blank">${fileInfo.file_name}</a></td>
             <td>${fileInfo.file_size}</td>
@@ -198,18 +214,50 @@ $(document).ready(function () {
             <td>${fileInfo.upload_time}</td>`
             if (fileInfo.canDel ) {
                 html += `<td>
-                <a class="btn btn-light btn-sm delete-file" data-attid="${fileInfo.id}" title="删除附件">
+                <a href="/${fileInfo.filepath}" class="btn btn-light btn-sm" title="下载"><span class="fa fa-download text-primary"></span></a>
+                <a href="javascript:void(0);" class="btn btn-light btn-sm delete-file" data-attid="${fileInfo.id}" title="删除附件">
                 <span class="fa fa-trash text-danger"></span>
                 </a>
                 </td></tr>`
             } else {
-                html += `<td></td></tr>`
+                html += `<td><a href="/${fileInfo.filepath}" class="btn btn-light btn-sm" title="下载"><span class="fa fa-download text-primary"></span></a></td></tr>`
             }
         })
         $('#file-list').empty();
         $('#file-list').append(html);
-    }
+    };
+
+    $('#file-list').on('click', '.check-file', function() {
+        const checkedList = $('#file-list').find('input:checked')
+        const childs = $('#file-list').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')
+        $('#file-list').children().each(function() {
+            $(this).find('input:checkbox').prop("checked", isCheck)
+        })
+    });
 
+    $('#bach-download').click(function() {
+        const fileIds = []
+        $( '#file-list .check-file:checked').each(function() {
+            const fileId = $(this).attr('file-id')
+            fileId && fileIds.push(fileId)
+        })
+        console.log('fileIds', fileIds)
+
+        if (fileIds.length) {
+            // postData( `/tender/${tid}/measure/material/${order}/file/download/compresse-file`, { fileIds })
+            $('#downloadZip').attr('href', `/tender/${tid}/measure/material/${order}/file/download/compresse-file?fileIds=${JSON.stringify(fileIds)}`);
+            $('#downloadZip')[0].click();
+        }
+    });
     function handleFileList(fileList) {
         fileData = fileList.map((file, index) => {
             let showDel = false
@@ -224,7 +272,7 @@ $(document).ready(function () {
             }
             return showDel ? {...file, canDel: true, index} : {...file, index}
         })
-    }
+    };
 
     function calcCount() {
         // 附件总数
@@ -233,7 +281,7 @@ $(document).ready(function () {
             total = fileData && fileData.filter(file => file.mid === parseInt(mid) && file.tid === parseInt(tid)).length
         }
         return total
-    }
+    };
 
     $.subMenu({
         menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
@@ -273,4 +321,4 @@ function validateFiles(files) {
         }
         return true
     })
-}
+};

+ 1 - 0
app/router.js

@@ -346,6 +346,7 @@ module.exports = app => {
     app.get('/tender/:id/measure/material/:order/file/:fid/download', sessionAuth, tenderCheck, uncheckTenderCheck, 'materialController.downloadFile');
     app.post('/tender/:id/measure/material/:order/file/find', sessionAuth, tenderCheck, uncheckTenderCheck, materialCheck, 'materialController.getCurMatericalFiles');
     app.post('/tender/measure/material/file/delete', sessionAuth, 'materialController.deleteFile');
+    app.get('/tender/:id/measure/material/:order/file/download/compresse-file', sessionAuth, tenderCheck, uncheckTenderCheck, 'materialController.downloadZip');
 
     // 个人账号相关
     app.get('/profile/info', sessionAuth, 'profileController.info');

+ 124 - 75
app/service/material_file.js

@@ -1,75 +1,124 @@
-'use strict';
-const auditConst = require('../const/audit');
-/**
- * 附件表 数据模型
- * @author LanJianRong
- * @date 2020/6/30
- * @version
- */
-
-module.exports = app => {
-    class MaterialFile extends app.BaseService {
-        /**
-         * 构造函数
-         *
-         * @param {Object} ctx - egg全局变量
-         * @return {void}
-         */
-        constructor(ctx) {
-            super(ctx);
-            this.tableName = 'material_file';
-        }
-
-        /**
-         * 获取当前标段(期)所有上传的附件
-         * @param {Number} tid 标段id
-         * @param {Number?} mid 期id
-         * @return {Promise<void>} 数据库查询实例
-         */
-        async getAllMaterialFiles(tid, mid) {
-            const { ctx } = this;
-            const where = { tid };
-            if (mid) where.mid = mid;
-            const result = await this.db.select(this.tableName, {
-                where,
-                orders: [['upload_time', 'desc']],
-            });
-            return result.map(item => {
-                if (!ctx.helper.canPreview(item.fileext)) {
-                    item.filepath = `tender/${ctx.tender.id}/measure/material/${item.s_order}/file/${item.id}/download`;
-                }
-                return item;
-            });
-        }
-
-
-        /**
-         * 存储上传的文件信息至数据库
-         * @param {Array} payload 载荷
-         * @return {Promise<void>} 数据库插入执行实例
-         */
-        async saveFileMsgToDb(payload) {
-            return await this.db.insert(this.tableName, payload);
-        }
-
-        /**
-         * 获取单个文件信息
-         * @param {Number} id 文件id
-         * @return {Promise<void>} 数据库查询实例
-         */
-        async getMaterialFileById(id) {
-            return await this.getDataByCondition({ id });
-        }
-
-        /**
-         * 删除附件
-         * @param {Number} id - 附件id
-         * @return {void}
-         */
-        async delete(id) {
-            return await this.deleteById(id);
-        }
-    }
-    return MaterialFile;
-};
-
+'use strict';
+const archiver = require('archiver');
+const path = require('path');
+const fs = require('fs');
+/**
+ * 附件表 数据模型
+ * @author LanJianRong
+ * @date 2020/6/30
+ * @version
+ */
+
+module.exports = app => {
+    class MaterialFile extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'material_file';
+        }
+
+        /**
+         * 获取当前标段(期)所有上传的附件
+         * @param {Number} tid 标段id
+         * @param {Number?} mid 期id
+         * @return {Promise<void>} 数据库查询实例
+         */
+        async getAllMaterialFiles(tid, mid) {
+            const { ctx } = this;
+            const where = { tid };
+            if (mid) where.mid = mid;
+            const result = await this.db.select(this.tableName, {
+                where,
+                orders: [['upload_time', 'desc']],
+            });
+            return result.map(item => {
+                if (!ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `tender/${ctx.tender.id}/measure/material/${item.s_order}/file/${item.id}/download`;
+                }
+                return item;
+            });
+        }
+
+
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 获取单个文件信息
+         * @param {Number} id 文件id
+         * @return {Promise<void>} 数据库查询实例
+         */
+        async getMaterialFileById(id) {
+            return await this.getDataByCondition({ id });
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+
+        /**
+         * 将文件压缩成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);
+            // const paths = files.map(item => {
+            //     return { name: item.filename + item.fileext, path: item.filepath }
+            // })
+            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, 'app', item.filepath), { name: item.file_name });
+                });
+
+                // 存档警告
+                ziparchiver.on('warning', function(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());
+                });
+            });
+        }
+    }
+    return MaterialFile;
+};
+

+ 14 - 2
app/view/material/file.ejs

@@ -5,14 +5,24 @@
       <% include ./material_sub_mini_menu.ejs %>
       <div>
         <div class="d-inline-block">
-          <a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="添加清单"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a>
+          <div class="dropdown">
+            <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" btn-type="curr" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">当前期</button>
+            <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+              <a class="dropdown-item" href="#">所有期</a>
+            </div>
+          </div>
         </div>
         <div class="d-inline-block">
+          <a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="添加清单"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a>
+          <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>
+        <!-- <div class="d-inline-block">
           <span class="d-flex align-items-center" style="margin-left: 5px;">
             <input type="checkbox" id="file-checkbox">
             <span class="text-primary" style="margin-left: 5px;">所有期</span>
           </span>
-        </div>
+        </div> -->
       </div>
       <!--<div class="d-flex justify-content-start align-items-center">-->
           <!--<a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="添加清单"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a>-->
@@ -29,6 +39,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>
@@ -106,6 +117,7 @@
   const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
   const tid = '<%- ctx.tender.id %>';
   const mid = '<%- ctx.material.id %>';
+  const order = '<%- ctx.material.order %>';
   const fileList = JSON.parse('<%- JSON.stringify(fileList) %>');
   const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
 </script>