Browse Source

feat: 台账分解增加附件功能

lanjianrong 4 năm trước cách đây
mục cha
commit
152147ec18

+ 274 - 0
app/controller/ledger_controller.js

@@ -14,6 +14,7 @@ const stdDataAddType = {
     next: 3,
 };
 const audit = require('../const/audit');
+const moment = require('moment');
 const auditConst = audit.ledger;
 const tenderMenu = require('../../config/menu').tenderMenu;
 const measureType = require('../const/tender').measureType;
@@ -28,6 +29,7 @@ const exportExcel = require('../lib/export_excel');
 const billsPosConvert = require('../lib/bills_pos_convert');
 const xlsx = require('js-xlsx');
 const stdConst = require('../const/standard');
+const sendToWormhole = require('stream-wormhole');
 
 module.exports = app => {
 
@@ -145,6 +147,14 @@ module.exports = app => {
                 }
                 const [stdBills, stdChapters] = await this.ctx.service.valuation.getValuationStdList(
                     ctx.tender.data.valuation, ctx.tender.data.measure_type);
+
+                    // 获取附件列表
+                const attData = await ctx.service.ledgerAtt.getDataByTenderId(ctx.tender.id);
+                for (const index in attData) {
+                    attData[index].in_time = moment(attData[index].in_time * 1000).format('YYYY-MM-DD');
+                }
+
+                const whiteList = this.ctx.app.config.multipart.whitelist;
                 const renderData = {
                     tender: tender.data,
                     tenderInfo: tender.info,
@@ -152,6 +162,8 @@ module.exports = app => {
                     auditors,
                     curAuditor,
                     user,
+                    attData,
+                    whiteList,
                     auditHistory,
                     ledgerSpreadSetting: JSON.stringify(ledgerSpread),
                     posSpreadSetting: JSON.stringify(posSpread),
@@ -741,6 +753,268 @@ module.exports = app => {
             const renderData = {};
             await this.layout('ledger/index.ejs', renderData);
         }
+        /**
+         * 上传附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async uploadFile(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.tender.data.ledger_status === auditConst.status.checked;
+                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/${this.ctx.tender.id}/ledger/fujian_${create_time + index.toString() + fileInfo.ext}`;
+                    await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, filepath));
+                    if (stream) {
+                        await sendToWormhole(stream);
+                    }
+                    // 保存数据到att表
+                    const fileData = {
+                        tid: ctx.params.id,
+                        in_time: create_time,
+                        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.ledgerAtt.save(parts.field, fileData, ctx.session.sessionUser.accountId);
+                    if (!result) {
+                        throw '导入数据库保存失败';
+                    }
+                    const attData = await ctx.service.ledgerAtt.getDataByFid(result.insertId);
+                    attData.in_time = moment(create_time * 1000).format('YYYY-MM-DD');
+                    files.length !== 0 ? files.unshift(attData) : files.push(attData);
+                    ++index;
+                }
+                responseData.data = files;
+            } catch (err) {
+                this.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                this.setMessage(err.toString(), this.messageType.ERROR);
+                responseData.err = 1;
+                responseData.msg = err.toString();
+            }
+            ctx.body = responseData;
+        }
+        /**
+         * 下载附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async downloadFile(ctx) {
+            const id = ctx.params.fid;
+            if (id) {
+                try {
+                    const fileInfo = await ctx.service.ledgerAtt.getDataById(id);
+                    if (fileInfo !== undefined && fileInfo !== '') {
+                        const fileName = path.join(this.app.baseDir, fileInfo.filepath);
+                        // 解决中文无法下载问题
+                        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);
+                    } else {
+                        throw '不存在该文件';
+                    }
+                } catch (err) {
+                    this.log(err);
+                    this.setMessage(err.toString(), this.messageType.ERROR);
+                }
+            }
+        }
+
+        /**
+         * 查看附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async checkFile(ctx) {
+            const responseData = { err: 0, msg: '' };
+            const { id = '' } = JSON.parse(ctx.request.body.data);
+            if (id) {
+                try {
+                    const fileInfo = await ctx.service.ledgerAtt.getDataById(id);
+                    if (fileInfo && Object.keys(fileInfo).length) {
+                        let filepath = fileInfo.filepath;
+                        if (!ctx.helper.canPreview(fileInfo.fileext)) {
+                            filepath = `/tender/${ctx.tender.id}/ledger/download/file/${fileInfo.id}`;
+                        } else {
+                            filepath = filepath.replace(/^app|\/app/, '');
+                        }
+                        fileInfo.filepath && (responseData.data = { filepath });
+                    }
+                } catch (error) {
+                    this.log(error);
+                    this.setMessage(error.toString(), this.messageType.ERROR);
+                    responseData.err = 1;
+                    responseData.msg = error.toString();
+                }
+            }
+            ctx.body = responseData;
+        }
+
+        /**
+         * 删除附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async deleteFile(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: '',
+            };
+            try {
+                // this._checkStageCanModifyRe(ctx);
+
+                const data = JSON.parse(ctx.request.body.data);
+                const fileInfo = await ctx.service.ledgerAtt.getDataById(data.id);
+                if (!fileInfo || !Object.keys(fileInfo).length) {
+                    throw '该文件不存在';
+                }
+                if (!fileInfo.extra_upload && ctx.stage.status === auditConst.status.checked) {
+                    throw '无权限删除';
+                }
+                if (fileInfo !== undefined && fileInfo !== '') {
+                    // 先删除文件
+                    await fs.unlinkSync(path.join(this.app.baseDir, fileInfo.filepath));
+                    // 再删除数据库
+                    await ctx.service.ledgerAtt.deleteById(data.id);
+                    responseData.data = '';
+                } else {
+                    throw '不存在该文件';
+                }
+            } catch (err) {
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+
+            ctx.body = responseData;
+        }
+
+        /**
+         * 保存附件(或替换)
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async saveFile(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: [],
+            };
+            let stream;
+            try {
+                // this._checkStageCanModifyRe(ctx);
+                stream = await ctx.getFileStream({ requireFile: false });
+                let fileData = {};
+                if (stream.filename !== undefined) {
+                    const create_time = Date.parse(new Date()) / 1000;
+                    const fileInfo = path.parse(stream.filename);
+                    const dirName = 'app/public/upload/ledger/' + moment().format('YYYYMMDD');
+                    const fileName = 'stage' + create_time + fileInfo.ext;
+
+                    // 判断文件夹是否存在,不存在则直接创建文件夹
+                    if (!fs.existsSync(path.join(this.app.baseDir, dirName))) {
+                        await fs.mkdirSync(path.join(this.app.baseDir, dirName));
+                    }
+                    // 保存文件
+                    await ctx.helper.saveStreamFile(stream, path.join(this.app.baseDir, dirName, fileName));
+                    // 保存数据到att表
+                    fileData = {
+                        filesize: stream.fields.size,
+                        filepath: path.join(dirName, fileName),
+                    };
+                }
+                const result = await ctx.service.ledgerAtt.updateByID(stream.fields, fileData);
+                if (!result) {
+                    throw '导入数据库保存失败';
+                }
+                const attData = await ctx.service.ledgerAtt.getDataByFid(stream.fields.id);
+                attData.in_time = moment(attData.in_time * 1000).format('YYYY-MM-DD');
+                responseData.data = attData;
+            } catch (err) {
+                this.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                this.setMessage(err.toString(), this.messageType.ERROR);
+                responseData.err = 1;
+                responseData.msg = err.toString();
+            }
+            ctx.body = responseData;
+        }
+
+        /**
+         * 批量下载 - 压缩成zip文件返回
+         * @param {Object} ctx - 全局上下文
+         */
+        async downloadZip(ctx) {
+            try {
+                const fileIds = JSON.parse(ctx.request.query.fileIds);
+                const zipFilename = `${ctx.tender.data.name}-计量台账-${ctx.params.order}-附件.zip`;
+                const time = Date.now();
+                const zipPath = `app/public/upload/${ctx.tender.id}/ledger/fu_jian_zip${time}.zip`;
+                const size = await ctx.service.ledgerAtt.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;
+                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 LedgerController;

+ 340 - 1
app/public/js/ledger.js

@@ -85,6 +85,10 @@ $(document).ready(function() {
         node.dgn_price = ZhCalc.round(ZhCalc.div(node.total_price, node.dgn_qty1), 2);
     };
     const ledgerTree = createNewPathTree('ledger', treeSetting);
+
+    //初始化所有附件列表
+    getAllList();
+
     // 初始化 计量单元
     const pos = new PosData({
         id: 'id', ledgerId: 'lid',
@@ -1742,6 +1746,7 @@ $(document).ready(function() {
             if (node) {
                 const posData = pos.ledgerPos[itemsPre + node.id] || [];
                 SpreadJsObj.loadSheetData(posSpread.getActiveSheet(), 'data', posData);
+                getNodeList(node.id);
             } else {
                 SpreadJsObj.loadSheetData(posSpread.getActiveSheet(), 'data', []);
             }
@@ -2354,9 +2359,10 @@ $(document).ready(function() {
         if (!tab.hasClass('active')) {
             const close = $('.active', '#side-menu').length === 0;
             $('a', '#side-menu').removeClass('active');
+            $('.tab-content .tab-select-show').removeClass('active');
             tab.addClass('active');
-            $('.tab-content .tab-pane').removeClass('active');
             tabPanel.addClass('active');
+            // $('.tab-content .tab-pane').removeClass('active');
             showSideTools(tab.hasClass('active'));
             if (tab.attr('content') === '#std-xmj') {
                 if (!stdXmj) {
@@ -2429,6 +2435,9 @@ $(document).ready(function() {
                 errorList.spread.refresh();
             } else if (tab.attr('content') === '#check-list') {
                 checkList.spread.refresh();
+            } else if (tab.attr('content') === '#fujian') {
+              const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+              getNodeList(node.id);
             }
         } else { // 收起工具栏
             tab.removeClass('active');
@@ -3386,7 +3395,337 @@ $(document).ready(function() {
             checkList: checkList,
         })
     });
+
+    // 切换附件里节点和所有附件
+    $('#fujian .nav-link').on('click', function () {
+      const tabPanel = $(this).attr('fujian-content');
+      if (tabPanel !== 'syfujian') {
+          $('#showPage').hide();
+          $('#bach-download').prop('type', 'curr');
+      } else {
+          $('#showPage').show();
+          $('#bach-download').prop('type', 'all')
+      }
+      $('#showAttachment').hide();
+    });
+    // 上传附件
+    $('#upload-file-btn').click(function () {
+        // if (curAuditor && curAuditor.aid !== cur_uid) {
+        //     return toastr.error('当前操作没有权限!');
+        // }
+        const files = $('#upload-file')[0].files;
+        const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+        console.log(node);
+        const formData = new FormData();
+        formData.append('lid', node.id);
+        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('/tender/' + tender.id + '/ledger/upload/file', formData, function (data) {
+            $('#upload').modal('hide');
+            // 插入到attData中
+            attData = data.concat(attData);
+            // 重新生成List
+            getAllList();
+            getNodeList(node.id);
+        }, function () {
+            toastr.error('附件上传失败');
+        });
+        $('#upload-file').val('');
+    });
+    // 获取附件信息
+    $('.list-table').on('click', '.att-file-name', function () {
+      const fid = $(this).attr('file-id');
+      if ($('#showAttachment').attr('file-id') === fid && !$('#showAttachment').is(":hidden")) {
+          return;
+      }
+      const att = attData.find(function (item) {
+          return item.id === parseInt(fid);
+      });
+      $('#edit-att').hide();
+      $('#show-att').show();
+      if (att !== undefined) {
+          // 进来先把编辑功能隐藏
+          $('#btn-att a').eq(3).hide()
+          $('#show-att tr').eq(0).children('td').text(att.filename + att.fileext);
+          const name = att.code !== null && att.code !== '' ? att.code : (att.b_code !== null ? att.b_code : '');
+          $('#show-att tr').eq(1).children('td').text($.trim(name + ' ' + att.lname));
+          // $('#show-att tr').eq(2).find('a').attr('href', '/tender/' + tender.id + '/measure/stage/' + stage.order + '/download/file/' + att.id);
+          // $('#show-att tr').eq(2).find('a').attr('href', att.filepath);
+          $('#show-att tr').eq(2).children('td').eq(0).text(att.username);
+          $('#show-att tr').eq(2).children('td').eq(1).text(att.in_time);
+          $('#show-att tr').eq(3).children('td').text(att.remark);
+          // 附件uid等于当前用户id, 附件上传本人
+          if (parseInt(cur_uid) === att.uid) {
+              $('#btn-att').show();
+              const showDel = tender.ledger_status === auditConst.status.checked ? Boolean(att.extra_upload) : true;
+              if (showDel) $('#btn-att a').eq(3).show();
+              // $('#btn-att a').eq(3).show();
+              $('#btn-att a').eq(2).hide();
+              $('#btn-att a').eq(4).hide();
+              $('#btn-att a').eq(5).hide();
+          } else {
+              $('#btn-att').hide();
+              $('#btn-att a').eq(3).hide();
+              $('#btn-att a').eq(2).hide();
+              $('#btn-att a').eq(4).hide();
+              $('#btn-att a').eq(5).hide();
+          }
+          $('#showAttachment').attr('file-id', fid);
+          $('#showAttachment').show();
+      } else {
+          $('#showAttachment').hide();
+          $('#showAttachment').attr('file-id', '');
+          toastr.error('附件信息获取失败');
+      }
+  });
+  // $('body').on('click', '.alllist-table a', handleFileList);
+  $('body').on('click', '#btn-att a', function () {
+      const content = $(this).attr('content');
+      const fid = $('#showAttachment').attr('file-id');
+      const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+      if (content === 'edit') {
+          $('#btn-att a').eq(3).hide();
+          $('#btn-att a').eq(2).show();
+          $('#btn-att a').eq(4).show();
+          $('#btn-att a').eq(5).show();
+          $('#show-att').hide();
+          $('#edit-att').show();
+          const att = attData.find(function (item) {
+              return item.id === parseInt(fid);
+          });
+          $('#edit-att .form-group').eq(0).find('input').val(att.filename);
+          $('#edit-att .form-group').eq(0).find('span').eq(1).text(att.fileext);
+          const name = att.code !== null && att.code !== '' ? att.code : (att.b_code !== null ? att.b_code : '');
+          $('#edit-att .form-group').eq(1).find('input').val($.trim(name + ' ' + att.lname));
+          $('#edit-att .form-group').eq(2).find('input').val(att.in_time);
+          $('#edit-att .form-group').eq(3).find('input').val(att.remark);
+      } else if (content === 'cancel') {
+          $('#show-att').show();
+          $('#edit-att').hide();
+          $('#btn-att a').eq(3).show();
+          $('#btn-att a').eq(2).hide();
+          $('#btn-att a').eq(4).hide();
+          $('#btn-att a').eq(5).hide();
+      } else if (content === 'save') {
+          const formData = new FormData();
+          formData.append('id', fid);
+          formData.append('filename', $('#edit-att .form-group').eq(0).find('input').val());
+          formData.append('fileext', $('#edit-att .form-group').eq(0).find('span').eq(1).text());
+          formData.append('remark', $('#edit-att .form-group').eq(3).find('input').val());
+          const file = $('#change-att-btn')[0];
+          if (file.files[0] !== undefined) {
+              const filesize = file.files[0].size;
+              formData.append('size', filesize);
+              formData.append('file', file.files[0]);
+          }
+          postDataWithFile('/tender/' + tender.id + '/ledger/save/file', formData, function (data) {
+              // 替换到attData中
+              const att_index = attData.findIndex(function (item) {
+                  return item.id === parseInt(fid);
+              });
+              attData.splice(att_index, 1, data);
+              // 重新生成List
+              getAllList(parseInt($('#currentPage').text()));
+              getNodeList(node.id);
+              $('#show-att').show();
+              $('#edit-att').hide();
+              $('#show-att tr').eq(0).children('td').text(data.filename + data.fileext);
+              const name = data.code !== null && data.code !== '' ? data.code : (data.b_code !== null ? data.b_code : '');
+              $('#show-att tr').eq(1).children('td').text($.trim(name + ' ' + data.lname));
+              $('#show-att tr').eq(3).children('td').eq(0).text(data.username);
+              $('#show-att tr').eq(3).children('td').eq(1).text(data.in_time);
+              $('#show-att tr').eq(4).children('td').text(data.remark);
+              $('#btn-att a').eq(3).show();
+              $('#btn-att a').eq(2).hide();
+              $('#btn-att a').eq(4).hide();
+              $('#btn-att a').eq(5).hide();
+          }, function () {
+              toastr.error('附件上传失败');
+          });
+          $('#change-att-btn').val('');
+      } else if (content === 'del') {
+          const data = {id: fid};
+          postData('/tender/' + tender.id + '/ledger/delete/file', data, function (result) {
+              // 删除到attData中
+              const att_index = attData.findIndex(function (item) {
+                  return item.id === parseInt(fid);
+              });
+              attData.splice(att_index, 1);
+              // 重新生成List
+
+              if ($('#alllist-table tr').length === 1) {
+                  getAllList(parseInt($('#currentPage').text()) - 1);
+
+              } else {
+                  getAllList(parseInt($('#currentPage').text()));
+              }
+              getNodeList(node.id);
+              $('#showAttachment').hide();
+              $('#showAttachment').attr('file-id', '');
+          });
+      } else if (content === 'view') {
+          const data = {id: fid};
+          postData('/tender/' + tender.id + '/ledger/check/file', data, function (result) {
+              const { filepath } = result
+              $('#load-file').attr('href', filepath);
+              $('#load-file')[0].click();
+          });
+      } else if (content === 'location') {
+          const att = attData.find(item => item.id === parseInt(fid));
+          if (Object.keys(att).length) {
+              SpreadJsObj.locateTreeNode(ledgerSpread.getActiveSheet(), att.ledger_id, true);
+              posOperationObj.loadCurPosData();
+          }
+      }
+  });
+
+  // 替换附件
+  $('#change-att-btn').on('change', function () {
+      const file = $('#change-att-btn')[0].files[0];
+      const name = file.name;
+      const filename = name.substring(0, name.lastIndexOf("."));
+      const fileext = name.substr(name.indexOf("."));
+      const filesize = file.size;
+      if (filesize > 10 * 1024 * 1024) {
+          toastr.error('文件大小过大!');
+          $('#change-att-btn').val('');
+          return false;
+      }
+      if (whiteList.indexOf(fileext) === -1) {
+          toastr.error('只能上传指定格式的附件!');
+          $('#change-att-btn').val('');
+          return false;
+      }
+      $('#edit-att .form-group').eq(0).find('input').val(filename);
+      $('#edit-att .form-group').eq(0).find('span').eq(1).text(fileext);
+  });
+
+  // 切换页数
+  $('.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);
+          $('#showAttachment').hide();
+          $('#syfujian .check-all-file').prop('checked', false)
+      } else if (status === 'next' && lastPageNum < totalPageNum) {
+          getAllList(lastPageNum+1);
+          $('#showAttachment').hide();
+          $('#syfujian .check-all-file').prop('checked', false)
+      }
+  });
+
+  // 批量下载
+  $('#bach-download').click(function() {
+      const fileIds = [];
+      const type = $(this).prop('type');
+      let node = ''
+      if (type === 'curr') {
+          node = '#nodelist-table .check-file:checked'
+      } else {
+          node = '#alllist-table .check-file:checked'
+      }
+      $(node).each(function() {
+          const fileId = $(this).attr('file-id');
+          fileId && fileIds.push(fileId);
+      });
+
+      if (fileIds.length) {
+          const url = `/tender/${tender.id}/ledger/download/compresse-file?fileIds=${JSON.stringify(fileIds)}`;
+          $('#zipDown').attr('href', url);
+          $("#zipDown")[0].click();
+      }
+  });
+
+  // 监听附件check是否选中
+  $('.list-table').on('click', '.check-file', function() {
+      const checkedList = $(this).parents('.list-table').children().find('input:checked');
+      const childs = $(this).parents('.list-table').children().length;
+      const checkBox = $(this).parents('.list-table').parent().find('.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');
+      $(this).parents('table').find('.list-table').each(function() {
+          $(this).find('input:checkbox').prop("checked", isCheck);
+      })
+  });
 });
+// 生成当前节点列表
+function getNodeList(node) {
+  let html = '';
+  for(const att of attData) {
+      if (node === att.lid) {
+          // html += '<tr><td><a href="javascript:void(0)" file-id="'+ att.id +'">'+ att.filename + att.fileext +'</a></td><td>'+ att.username +'</td></tr>';
+          html += `<tr>
+          <td width="25"><input type="checkbox" class="check-file" file-id=${att.id}></td>
+          <td>
+          <div class="d-flex">
+              <a href="javascript:void(0)" class="pl-0 col-11 att-file-name" file-id=${att.id}>${att.filename}${att.fileext}</a>
+              <a href="/tender/${tender.id}/ledger/download/file/${att.id}" class="col-1 pl-0 att-file-btn"><i class="fa fa-download"></i></a>
+          </div>
+          </td><td>${att.username}</td></tr>`
+      }
+  }
+  $('#nodelist-table').html(html);
+  $('#nodelist-table').on('click', 'tr', function() {
+      $('#nodelist-table tr').removeClass('bg-light')
+      $(this).addClass('bg-light')
+  })
+}
+
+
+// 生成所有附件列表
+function getAllList(currPageNum = 1) {
+  // 每页最多几个附件
+  const pageCount = 15;
+  // 附件总数
+  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);
+  let html = '';
+  for(const att of currPageAttData) {
+      html += `<tr>
+      <td width="25"><input type="checkbox" class="check-file" file-id=${att.id}></td>
+      <td>
+      <div class="d-flex">
+          <a href="javascript:void(0)" class="pl-0 col-11 att-file-name" file-id=${att.id}>${att.filename}${att.fileext}</a>
+          <a href="/tender/${tender.id}/ledger/download/file/${att.id}" class="col-1 pl-0 att-file-btn"><i class="fa fa-download"></i></a>
+      </div>
+      </td><td>${att.username}</td></tr>`
+  }
+  console.log(attData);
+  $('#alllist-table').html(html);
+  $('#alllist-table').on('click', 'tr', function() {
+      $('#alllist-table tr').removeClass('bg-light')
+      $(this).addClass('bg-light')
+  })
+}
 
 // 检查上报情况
 function checkAuditorFrom () {

+ 8 - 0
app/router.js

@@ -169,6 +169,14 @@ module.exports = app => {
     app.post('/tender/:id/ledger/deal2sgfh', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.deal2sgfh');
     app.post('/tender/:id/ledger/check', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.check');
 
+    // 台账附件
+    app.post('/tender/:id/ledger/upload/file', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.uploadFile');
+    app.get('/tender/:id/ledger/download/file/:fid', sessionAuth, 'ledgerController.downloadFile');
+    app.post('/tender/:id/ledger/delete/file', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.deleteFile');
+    app.post('/tender/:id/ledger/save/file', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.saveFile');
+    app.post('/tender/:id/ledger/check/file', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.checkFile');
+    app.get('/tender/:id/ledger/download/compresse-file', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.downloadZip');
+
     // 台账审批相关
     app.get('/tender/:id/ledger/audit', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerAuditController.index');
     app.post('/tender/:id/ledger/audit/add', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerAuditController.add');

+ 147 - 0
app/service/ledger_att.js

@@ -0,0 +1,147 @@
+'use strict';
+
+/**
+ *
+ *  附件
+ * @author LanJianRong
+ * @date 2021/7/30
+ * @version
+ */
+
+const archiver = require('archiver');
+const path = require('path');
+const fs = require('fs');
+module.exports = app => {
+    class LedgerAtt extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'ledger_attachment';
+        }
+
+        /**
+         * 添加附件
+         * @param {Object} postData - 表单信息
+         * @param {Object} fileData - 文件信息
+         * @param {int} uid - 上传者id
+         * @return {void}
+         */
+        async save(postData, fileData, uid) {
+            const data = {
+                lid: postData.lid,
+                uid,
+                remark: '',
+            };
+            Object.assign(data, fileData);
+            const result = await this.db.insert(this.tableName, data);
+            return result;
+        }
+
+        /**
+         * 更新附件
+         * @param {Object} postData - 表单信息
+         * @param {Object} fileData - 文件信息
+         * @param {int} uid - 上传者id
+         * @return {void}
+         */
+        async updateByID(postData, fileData) {
+            delete postData.size;
+            const data = {};
+            Object.assign(data, fileData);
+            Object.assign(data, postData);
+            const result = await this.db.update(this.tableName, data);
+            return result.affectedRows === 1;
+        }
+
+        /**
+         * 获取所有附件
+         * @param {int} tid - 标段id
+         * @return {void}
+         */
+        async getDataByTenderId(tid) {
+            const sql = 'SELECT att.id, att.lid, att.uid, att.filename, att.fileext, att.filesize, att.extra_upload, att.remark, att.in_time,' +
+                ' pa.name as `username`, leg.name as `lname`, leg.code as `code`, leg.ledger_id as `ledger_id`, leg.b_code as `b_code`' +
+                ' FROM ?? AS att,?? AS pa,?? AS leg' +
+                ' WHERE leg.id = att.lid AND pa.id = att.uid AND att.tid = ? ORDER BY att.id DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, this.ctx.service.ledger.tableName, tid];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        /**
+         * 获取单个附件
+         * @param {int} tid - 标段id
+         * @param {int} sid - 当前期数
+         * @return {void}
+         */
+        async getDataByFid(id) {
+            const { ctx } = this;
+            const sql = 'SELECT att.id, att.lid, att.uid, att.filepath, att.filename, att.extra_upload, att.fileext, att.filesize, att.remark, att.in_time,' +
+                ' pa.name as `username`, leg.name as `lname`, leg.code as `code`, leg.ledger_id as `ledger_id`,leg.b_code as `b_code`' +
+                ' FROM ?? AS att,?? AS pa,?? AS leg' +
+                ' WHERE leg.id = att.lid AND pa.id = att.uid AND att.id = ? ORDER BY att.in_time DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, this.ctx.service.ledger.tableName, id];
+            const result = await this.db.queryOne(sql, sqlParam);
+            if (!ctx.helper.canPreview(result.fileext)) result.filepath = `/tender/${ctx.tender.id}/ledger/download/file/${result.id}`;
+            else result.filepath = result.filepath.replace(/^app|\/app/, '');
+            return result;
+        }
+
+        /**
+         * 将文件压缩成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, reject) => {
+                // 每次开一个新的archiver
+                const ziparchiver = archiver('zip');
+                const outputPath = fs.createWriteStream(path.resolve(this.app.baseDir, zipPath));
+
+                outputPath.on('error', err => {
+                    reject(err);
+                });
+                ziparchiver.pipe(outputPath);
+                files.forEach(item => {
+                    ziparchiver.file(item.filepath, { name: item.filename + item.fileext });
+                });
+
+                // 存档警告
+                ziparchiver.on('warning', function(err) {
+                    if (err.code === 'ENOENT') {
+                        console.warn('stat故障和其他非阻塞错误');
+                    }
+                    reject(err);
+
+                });
+
+                // 存档出错
+                ziparchiver.on('error', function(err) {
+                    console.log(err);
+                    reject(err);
+                });
+
+                ziparchiver.finalize();
+                outputPath.on('close', () => {
+                    resolve(ziparchiver.pointer());
+                });
+            });
+        }
+    }
+    return LedgerAtt;
+};

+ 123 - 0
app/view/ledger/explode.ejs

@@ -156,6 +156,122 @@
                     </div>
                     <div id="check-list" class="tab-pane">
                     </div>
+                    <div id="fujian" class="tab-pane tab-select-show">
+                      <div class="sjs-bar">
+                          <ul class="nav nav-tabs">
+                              <li class="nav-item">
+                                  <a class="nav-link active" data-toggle="tab" href="#dqjiedian" role="tab" fujian-content="dqjiedian">当前节点</a>
+                              </li>
+                              <li class="nav-item">
+                                  <a class="nav-link" data-toggle="tab" href="#syfujian" role="tab" fujian-content="syfujian">所有附件</a>
+                              </li>
+                              <li class="nav-item ml-auto pt-1">
+                                  <a href="javascript:void(0);" id="bach-download" class="btn btn-sm btn-primary" type="curr">批量下载</a>
+                                  <!--所有附件 翻页-->
+                                  <span id="showPage" style="display: none"><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></span>
+                                  <% if (!ctx.tender.isTourist) { %>
+                                  <a href="#upload" data-toggle="modal" data-target="#upload"  class="btn btn-sm btn-outline-primary ml-3">上传</a>
+                                  <% } %>
+                              </li>
+                          </ul>
+                      </div>
+                      <a href="javascript: void(0);" id="zipDown" download style="display: none;"></a>
+                      <div class="tab-content">
+                          <div class="tab-pane active" id="dqjiedian">
+                              <div class="sjs-height-3" style="overflow:auto; overflow-x:hidden;">
+                                  <table class="table table-sm table-bordered table-hover" style="word-break:break-all; table-layout: fixed">
+                                      <tr><th width="25"><input type="checkbox" class="check-all-file"><th>文件名</th><th width="80">上传</th></tr>
+                                      <tbody id="nodelist-table" class="list-table">
+                                      </tbody>
+                                  </table>
+                              </div>
+                          </div>
+                          <div class="tab-pane" id="syfujian">
+                              <div class="sjs-height-3" style="overflow:auto; overflow-x:hidden;">
+                                  <table class="table table-sm table-bordered table-hover" style="word-break:break-all; table-layout: fixed">
+                                      <tr><th width="25"><input type="checkbox" class="check-all-file"></th><th>文件名</th><th width="80">上传</th></tr>
+                                      <tbody id="alllist-table" class="list-table">
+                                      </tbody>
+                                  </table>
+                              </div>
+                          </div>
+                          <div class="sjs-bottom">
+                              <div class="resize-y" id="file-spr" r-Type="height" div1=".sjs-height-3" div2=".sjs-bottom" title="调整大小"><!--调整上下高度条--></div>
+                              <br>
+                              <div class="tab-content" id="showAttachment" style="display: none" file-id="">
+                                  <div class="sjs-bottom-2">
+                                      <a href="javascript:void(0);" target="_blank" style="display: none" id="load-file"></a>
+                                      <div class="d-flex justify-content-end mb-1" id="btn-att">
+                                          <a href="javascript:void(0);" content="location" class="btn btn-sm btn-outline-primary" style="margin-right: 5px">定位</a>
+                                          <a href="javascript:void(0);" content="view" class="btn btn-sm btn-outline-primary" style="margin-right: 5px">查看</a>
+                                          <!--默认 有删除权限-->
+                                          <a href="javascript:void(0);" content="del" class="btn btn-sm text-danger" style="display: none; margin-right: 5px">删除</a>
+                                          <!--默认 有编辑权限-->
+                                          <a href="javascript:void(0);" content="edit" class="btn btn-sm btn-outline-primary" style="display: none; margin-right: 5px">编辑</a>
+                                          <!--编辑模式-->
+                                          <a href="javascript:void(0);" content="save" class="btn btn-sm btn-outline-success mr-1" style="display: none; margin-right: 5px">保存</a>
+                                          <a href="javascript:void(0);" content="cancel" class="btn btn-sm btn-outline-secondary" style="display: none; margin-right: 5px">取消</a>
+                                      </div>
+                                      <!--显示信息-->
+                                      <table class="table table-sm table-bordered" id="show-att" style="word-break:break-all; table-layout: fixed">
+                                          <tbody>
+                                          <tr><th>文件名</th><td colspan="3">asdasd.jpg</td></tr>
+                                          <tr><th>所在节点</th><td colspan="3" id="show-att-node">1 第一部分 建筑安装工程非</td></tr>
+                                          <!-- <tr><td colspan="4"><a href="javascript:void(0);" target="_blank"><span>下载附件</span></a></td></tr> -->
+                                          <tr><th>上传者</th><td>张三</td><th>上传时间</th><td>2018-10-20</td></tr>
+                                          <tr><th>备注</th><td colspan="3"></td></tr>
+                                          </tbody>
+                                      </table>
+                                      <div id="edit-att" style="display: none">
+                                      <!--编辑模式-->
+                                          <div class="form-group">
+                                              <div class="input-group input-group-sm">
+                                                  <div class="input-group-prepend">
+                                                      <span class="input-group-text">文件名</span>
+                                                  </div>
+                                                  <input type="text" class="form-control form-control-sm" value="asdasd">
+                                                  <div class="input-group-append">
+                                                      <span class="input-group-text">.jpg</span>
+                                                  </div>
+                                              </div>
+                                          </div>
+                                          <div class="form-group">
+                                              <div class="input-group input-group-sm">
+                                                  <div class="input-group-prepend">
+                                                      <span class="input-group-text">所在节点</span>
+                                                  </div>
+                                                  <input type="text" class="form-control form-control-sm" value="1 第一部分 建筑安装工程非" readonly="">
+                                              </div>
+                                          </div>
+                                          <div class="form-group">
+                                              <div class="input-group input-group-sm">
+                                                  <div class="input-group-prepend">
+                                                      <span class="input-group-text">上传时间</span>
+                                                  </div>
+                                                  <input type="text" class="form-control form-control-sm" value="2018-10-20" readonly="">
+                                              </div>
+                                          </div>
+                                          <div class="form-group">
+                                              <div class="input-group input-group-sm">
+                                                  <div class="input-group-prepend">
+                                                      <span class="input-group-text">备注</span>
+                                                  </div>
+                                                  <input type="text" class="form-control form-control-sm" value="">
+                                              </div>
+                                          </div>
+                                          <div class="form-group">
+                                              <label>替换文件</label>
+                                              <div class="custom-file">
+                                                  <input type="file" class="custom-file-input" id="change-att-btn">
+                                                  <label class="custom-file-label" data-browse="浏览" for="customFile">选择文件</label>
+                                              </div>
+                                          </div>
+                                      </div>
+                                  </div>
+                              </div>
+                          </div>
+                      </div>
+                  </div>
                 </div>
             </div>
         </div>
@@ -178,6 +294,9 @@
                     <a class="nav-link" content="#bills-tag" href="javascript: void(0);">书签</a>
                 </li>
                 <li class="nav-item">
+                    <a class="nav-link" content="#fujian" href="javascript: void(0);">附件</a>
+                </li>
+                <li class="nav-item">
                     <a class="nav-link" content="#error-list" id="error-list-tab" href="javascript: void(0);" style="display: none;">错误列表</a>
                 </li>
                 <li class="nav-item">
@@ -193,6 +312,9 @@
     const tender = JSON.parse('<%- JSON.stringify(tender) %>');
     const tenderInfo = JSON.parse(unescape('<%- escape(JSON.stringify(tenderInfo)) %>'));
     const thousandth = <%- ctx.tender.info.display.thousandth %>;
+    const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
+    let attData = JSON.parse(unescape('<%- escape(JSON.stringify(attData)) %>'));
+    const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
     const measureType = JSON.parse('<%- JSON.stringify(measureType) %>');
     let ledgerSpreadSetting = '<%- ledgerSpreadSetting %>';
     ledgerSpreadSetting = JSON.parse(ledgerSpreadSetting);
@@ -220,5 +342,6 @@
 <script>
     const accountList = JSON.parse('<%- JSON.stringify(accountList) %>');
     const accountGroup = JSON.parse('<%- JSON.stringify(accountGroup) %>');
+    
 </script>
 <% } %>

+ 22 - 0
app/view/ledger/explode_modal.ejs

@@ -343,6 +343,28 @@
         </div>
     </div>
 </div>
+
+<!--上传附件-->
+<div class="modal fade" id="upload" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">上传附件</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="formGroupExampleInput">大小限制:30MB,支持<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="rar,zip">压缩包格式</span></label>
+                    <input type="file" class="" id="upload-file" multiple>
+                </div>
+            </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>
+
 <% if (ctx.session.sessionUser.accountId === ctx.tender.data.user_id && (ctx.tender.data.ledger_status === auditConst.status.uncheck || ctx.tender.data.ledger_status === auditConst.status.checkNo)) { %>
     <script>
         const shenpi_status = <%- ctx.tender.info.shenpi.ledger %>;

+ 15 - 0
sql/update.sql

@@ -12,3 +12,18 @@ ALTER TABLE `zh_material_list` ADD `expr` VARCHAR(500) NULL DEFAULT '' COMMENT '
 ALTER TABLE `zh_s2b_proj`
 ADD COLUMN `gxby_ratio_valid`  tinyint(1) NULL DEFAULT 0 COMMENT '工序报验,ratio是否生效' AFTER `gxby_status`,
 ADD COLUMN `dagl_ratio_valid`  tinyint(1) NOT NULL DEFAULT 1 COMMENT '工序报验,ratio是否生效' AFTER `dagl_status`;
+
+CREATE TABLE `zh_ledger_attachment` (
+  `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `tid` INT(11) NOT NULL COMMENT '标段id',
+  `lid` VARCHAR(100) CHARACTER SET 'utf8' NOT NULL COMMENT '台账节点id(uuid)',
+  `uid` INT(11) NOT NULL COMMENT '上传者id',
+  `filename` VARCHAR(1000) NOT NULL COMMENT '文件名称',
+  `fileext` VARCHAR(10) NOT NULL COMMENT '文件后缀',
+  `filesize` VARCHAR(255) NOT NULL COMMENT '文件大小',
+  `filepath` VARCHAR(500) NOT NULL COMMENT '文件存储路径',
+  `remark` VARCHAR(100) NOT NULL COMMENT '备注',
+  `status` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '状态:1存在,2回收站',
+  `in_time` VARCHAR(15) NOT NULL COMMENT '创建时间',
+  `extra_upload` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '是否为审核通过后再次上传的文件,0为否',
+  PRIMARY KEY (`id`));