Parcourir la source

结算台账,附件、书签

MaiXinRong il y a 1 an
Parent
commit
9f45751ec9

+ 115 - 0
app/controller/settle_controller.js

@@ -17,6 +17,9 @@ const measureType = tenderConst.measureType;
 const spreadConst = require('../const/spread');
 const spreadSetting = require('../lib/spread_setting');
 
+const path = require('path');
+const sendToWormhole = require('stream-wormhole');
+
 module.exports = app => {
 
     class SettleController extends app.BaseController {
@@ -292,6 +295,9 @@ module.exports = app => {
                 case 'tag':
                     const tag = await ctx.service.ledgerTag.getDatas(ctx.tender.id, -1, ctx.settle.id);
                     return [tag, ''];
+                case 'att':
+                    const att = await ctx.service.ledgerAtt.getViewData(ctx.tender.id, ctx.settle.id);
+                    return [att, ''];
                 default:
                     return [null, ''];
             }
@@ -328,6 +334,115 @@ module.exports = app => {
             }
         }
 
+        async uploadFile(ctx) {
+            let stream;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+                const files = [];
+                let index = 0;
+                const extra_upload = ctx.settle.audit_status === auditConst.settle.status.checked;
+                stream = await parts();
+                while (stream) {
+                    // 判断用户是否选择上传文件
+                    if (!stream.filename) {
+                        throw '请选择上传的文件!';
+                    }
+                    const fileInfo = path.parse(stream.filename);
+                    const create_time = Date.parse(new Date()) / 1000;
+                    const filepath = `${ctx.session.sessionProject.id}/${ctx.tender.id}/settle/${ctx.settle.settle_order}/att_${create_time + index.toString() + fileInfo.ext}`;
+
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+
+                    if (stream) await sendToWormhole(stream);
+
+                    // 保存数据到att表
+                    const fileData = {
+                        tid: ctx.tender.id,
+                        settle_id: ctx.settle.id,
+                        settle_order: ctx.settle.settle_order,
+                        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.getViewDataByFid(result.insertId);
+                    files.length !== 0 ? files.unshift(attData) : files.push(attData);
+                    ++index;
+                    if (Array.isArray(parts.field.size) && index < parts.field.size.length) {
+                        stream = await parts();
+                    } else {
+                        stream = undefined;
+                    }
+                }
+                ctx.body = { err: 0, msg: '', data: files };
+            } catch (err) {
+                ctx.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.ajaxErrorBody(err, '上传文件失败');
+            }
+        }
+
+        async deleteFile(ctx) {
+            try {
+                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.settle.status === auditConst.settle.status.checked) throw '无权限删除';
+
+                if (fileInfo !== undefined && fileInfo !== '') {
+                    // 先删除文件
+                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
+                    // 再删除数据库
+                    await ctx.service.ledgerAtt.deleteById(data.id);
+                } else {
+                    throw '不存在该文件';
+                }
+                ctx.body = { err: 0, msg: '', data: null}
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '删除文件失败');
+            }
+        }
+
+        async saveFile(ctx) {
+            let stream;
+            try {
+                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 filepath = `${ctx.session.sessionProject.id}/${ctx.tender.id}/settle/${ctx.settle.settle_order}/att_${create_time + fileInfo.ext}`;
+
+                    // 保存文件
+                    await ctx.oss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    // 保存数据到att表
+                    fileData = {
+                        filesize: stream.fields.size,
+                        filepath,
+                    };
+                }
+                const org = await ctx.service.ledgerAtt.getDataById(stream.fields.id);
+                const result = await ctx.service.ledgerAtt.updateByID(stream.fields, fileData);
+                if (!result) throw '保存数据失败';
+                // 删除原附件
+                await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + org.filepath);
+                const attData = await ctx.service.ledgerAtt.getViewDataByFid(stream.fields.id);
+                ctx.body = { err: 0, msg: '', data: attData };
+                responseData.data = attData;
+            } catch (err) {
+                ctx.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.ajaxErrorBody(err, '保存数据失败');
+            }
+        }
+
         async loadGatherData(ctx) {
             try {
                 const settle = await this.ctx.service.settle.getLatestCompleteSettle(ctx.tender.id);

+ 3 - 0
app/controller/tender_controller.js

@@ -1137,6 +1137,9 @@ module.exports = app => {
                 } else if (ctx.revise) {
                     const isAuditor = ctx.revise.reviseUsers.indexOf(this.ctx.session.sessionUser.accountId) >= 0;
                     if (!isAuditor && !isValidTourist) throw '您无权进行该操作';
+                } else if (ctx.settle) {
+                    const isAuditor = ctx.settle.userIds.indexOf(this.ctx.session.sessionUser.accountId) >= 0;
+                    if (!isAuditor && !isValidTourist) throw '您无权进行该操作';
                 } else {
                     const isAuditor = ctx.tender.ledgerUsers.indexOf(this.ctx.session.sessionUser.accountId) >= 0;
                     if (!isAuditor && !isValidTourist) throw '您无权进行该操作';

+ 152 - 2
app/public/js/settle_ledger.js

@@ -16,7 +16,7 @@ const ckBillsSpread = window.location.pathname + '-billsSelect';
 $(document).ready(() => {
     autoFlashHeight();
 
-    let searchLedger;
+    let searchLedger, settleAtt;
     const settleTreeSetting = {
         id: 'tree_id',
         pid: 'tree_pid',
@@ -83,6 +83,39 @@ $(document).ready(() => {
         {field: 'gxby', getValue: getGxbyText, url_field: 'gxby_url'},
         {field: 'dagl', getValue: getDaglText, url_field: 'dagl_url'},
     ]);
+    billsSpreadSetting.headColWidth = [50];
+    billsSpreadSetting.rowHeader = [
+        {
+            rowHeaderType: 'tag',
+            setting: {
+                indent: 16,
+                tagSize: 0.8,
+                tagFont: '8px 微软雅黑',
+                getColor: function (index, data) {
+                    if (!data) return;
+                    return billsTag.getBillsTagsColor(data.lid);
+                },
+                getTagHtml: function (index, data) {
+                    if (!data) return;
+                    const getHtml = function (list) {
+                        if (!list || list.length === 0) return '';
+                        const html = [];
+                        for (const l of list) {
+                            html.push('<div class="row mr-1">');
+                            html.push(`<div class="col-auto pr-1 ${l.tagClass}">`, '<i class="fa fa-tag"></i>', '</div>');
+                            html.push('<div class="col p-0">', '<p>', l.comment, '</p>', '</div>');
+                            html.push('</div>');
+                        }
+                        return html.join('');
+                    };
+                    return getHtml(billsTag.getBillsTagsInfo(data.lid));
+                }
+            },
+        },
+    ];
+    billsSpreadSetting.afterLocate = function (node) {
+        settleAtt.getCurAttHtml(node);
+    };
     SpreadJsObj.initSheet(slSheet, billsSpreadSetting);
 
     const spSpread = SpreadJsObj.createNewSpread($('#settle-pos')[0]);
@@ -114,6 +147,7 @@ $(document).ready(() => {
         loadRelaData: function() {
             SpreadJsObj.saveTopAndSelect(slSheet, ckBillsSpread);
             SpreadJsObj.resetTopAndSelect(spSheet);
+            settleBillsObj.loadRelaAtt();
             settlePosObj.loadCurPosData();
         },
         selectionChanged: function(e, info) {
@@ -124,6 +158,10 @@ $(document).ready(() => {
         topRowChanged(e, info) {
             SpreadJsObj.saveTopAndSelect(info.sheet, ckBillsSpread);
         },
+        loadRelaAtt() {
+            const node = SpreadJsObj.getSelectObject(slSheet);
+            settleAtt.getCurAttHtml(node);
+        }
     };
     slSpread.bind(spreadNS.Events.SelectionChanged, settleBillsObj.selectionChanged);
     slSpread.bind(spreadNS.Events.TopRowChanged, settleBillsObj.topRowChanged);
@@ -142,15 +180,80 @@ $(document).ready(() => {
         }
     };
 
-    postData('load', {filter: 'settleBills;settlePos;tag'}, function(result) {
+    const billsTag = $.billsTag({
+        selector: '#bills-tag',
+        relaSpread: slSpread,
+        updateUrl: window.location.pathname + '/tag',
+        key: 'lid',
+        treeId: 'tree_id',
+        afterModify: function (nodes) {
+            SpreadJsObj.repaintNodesRowHeader(slSheet, nodes);
+        },
+        afterLocated:  function () {
+            settlePosObj.loadCurPosData();
+        },
+        afterShow: function () {
+            slSpread.refresh();
+            if (spSpread) spSpread.refresh();
+        },
+    });
+    const addTag = newTag({ledgerSheet: slSheet, billsTag, key: 'lid'});
+    $.contextMenu({
+        selector: '#settle-bills',
+        build: function ($trigger, e) {
+            const target = SpreadJsObj.safeRightClickSelection($trigger, e, slSpread);
+            settleBillsObj.loadRelaData();
+            return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
+        },
+        items: {
+            tag: {
+                name: '书签',
+                callback: function (key, opt, menu, e) {
+                    const node = SpreadJsObj.getSelectObject(slSheet);
+                    addTag.do(node);
+                },
+                disabled: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(slSheet);
+                    return !node;
+                }
+            }
+        }
+    });
+
+    postData('load', {filter: 'settleBills;settlePos;tag;att'}, function(result) {
         settleTree.loadDatas(result.settleBills);
         treeCalc.calculateAll(settleTree);
         settlePos.loadDatas(result.settlePos);
         settlePos.calculateAll();
 
+        for (const t of result.tag) {
+            t.node = settleTree.nodes.find(x => {return x.lid === t.lid});
+        }
+        billsTag.loadDatas(result.tag);
+
         SpreadJsObj.loadSheetData(slSheet, SpreadJsObj.DataType.Tree, settleTree);
         SpreadJsObj.loadTopAndSelect(slSpread.getActiveSheet(), ckBillsSpread);
         settlePosObj.loadCurPosData();
+
+        for (const r of result.att) {
+            r.node = settleTree.datas.find(x => {return x.lid === r.lid});
+        }
+        settleAtt = $.ledger_att({
+            selector: '#fujian',
+            key: 'lid',
+            uploadUrl: 'file/upload',
+            deleteUrl: 'file/delete',
+            checked: settleComplete,
+            zipName: `${tenderName}-计量台账-第${settleOrder}期-附件.zip`,
+            readOnly: false, // todo fileUploadPermission,
+            locate: function (att) {
+                if (!att) return;
+                SpreadJsObj.locateTreeNode(slSheet, att.node.ledger_id, true);
+                settlePosObj.loadCurPosData();
+            }
+        });
+        settleAtt.loadDatas(result.att);
+        settleAtt.getCurAttHtml(SpreadJsObj.getSelectObject(slSheet));
     });
 
     // 展开收起工具栏
@@ -218,4 +321,51 @@ $(document).ready(() => {
             autoFlashHeight();
         }
     });
+    // 加载上下窗口resizer
+    $.divResizer({
+        select: '#main-resize',
+        callback: function () {
+            slSpread.refresh();
+            let bcontent = $(".bcontent-wrap") ? $(".bcontent-wrap").height() : 0;
+            $(".sp-wrap").height(bcontent-30);
+            spSpread.refresh();
+            window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
+        }
+    });
+    // 工具栏resizer
+    $.divResizer({
+        select: '#right-spr',
+        callback: function () {
+            slSpread.refresh();
+            spSpread.refresh();
+            window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
+        }
+    });
+
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            if (!tree) return;
+            setTimeout(() => {
+                showWaitingView();
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                    case "5":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', slSheet);
 });

+ 0 - 1
app/public/js/settle_select.js

@@ -107,7 +107,6 @@ $(document).ready(() => {
     ]);
     SpreadJsObj.initSheet(spSheet, posSpreadSetting);
 
-    // 0: 可结算,1: 合同未完成,2:
     const settleCheck = {
         _analysisPos(pos) {
             pos.undoneDeal = pos.quantity ? !checkZero(ZhCalc.sub(pos.end_contract_qty, pos.quantity)) : false;

+ 3 - 2
app/public/js/shares/cs_tools.js

@@ -756,6 +756,8 @@ const showSelectTab = function(select, spread, afterShow) {
             {tagClass: 'text-warning', color: '#da9500'},
             {tagClass: 'text-info', color: '#17a2b8'},
         ];
+        if (!setting.key) setting.key = 'id';
+        if (!setting.treeId) setting.treeId = 'ledger_id';
         const obj = $(setting.selector);
         const html = [], pageLength = 15;
         let billsTags = [], classIndexes = [], billsIndexes = {}, curShow = [];
@@ -828,7 +830,6 @@ const showSelectTab = function(select, spread, afterShow) {
         };
 
         const getTagDisplayHtml = function (tag) {
-            console.log(tag);
             const tagClass = classIndexes.find(x => {return x.color === tag.color}) || {};
             const tagHtml = [];
             tagHtml.push('<div name="tag-view">');
@@ -840,7 +841,7 @@ const showSelectTab = function(select, spread, afterShow) {
                 tagHtml.push(`<div class="pull-right"><i class="fa fa-users text-warning" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="所有参与台账审批管理的用户都可以看到这条书签"></i> <span>${tag.u_name}</span></div>`);
             }
             tagHtml.push('<div class="pull-right edit-tag-btn">');
-            const lid = tag.node ? tag.node.ledger_id : -1;
+            const lid = tag.node ? tag.node[setting.treeId] : -1;
             tagHtml.push(`<a class="mr-1" name="bills-tag-locate" href="javascript: void(0);" lid="${lid}"><i class="fa fa-crosshairs"></i> 定位</a>`);
             if (tag.uid === userID && !setting.readOnly) tagHtml.push(`<a href="javascript: void(0);" name="bills-tag-edit" tag-id="${tag.id}"><i class="fa fa-edit"></i> 编辑</a>`);
             tagHtml.push('</div>');

+ 1 - 1
app/public/js/shares/new_tag.js

@@ -18,7 +18,7 @@ const newTag = function (setting) {
         const data = {
             add: {
                 color: $('.active[name=addtag-color]').attr('tag-color'),
-                lid: relaNode.id,
+                lid: setting.key ? relaNode[setting.key] : relaNode.id,
                 share: $('#addtag-share')[0].checked,
                 comment: $('#addtag-content').val(),
             }

+ 279 - 0
app/public/js/shares/tools_att.js

@@ -0,0 +1,279 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+
+(function($){
+    $.ledger_att = function (setting) {
+        if (!setting.selector) return;
+        const obj = $(setting.selector);
+        const pageLength = 20;
+        let curNode = null, curPage = 0;
+        obj.html(
+            '<div class="sjs-bar">\n' +
+            '    <ul class="nav nav-tabs">\n' +
+            '        <li class="nav-item"><a class="nav-link active" data-toggle="tab" href="#att-cur" role="tab" att-type="cur" id="att-cur-button">当前节点</a></li>\n' +
+            '        <li class="nav-item"><a class="nav-link" data-toggle="tab" href="#att-all" role="tab" att-type="all">所有附件</a></li>\n' +
+            '        <li class="nav-item ml-auto pt-1">\n' +
+            '            <a href="javascript:void(0);" id="batch-download" class="btn btn-sm btn-primary" type="curr">批量下载</a>\n' +
+            '            <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="att-cur-page">1</span>/<span id="att-total-page">10</span> <a href="javascript:void(0);" class="page-select mr-3" content="next"><i class="fa fa-chevron-right"></i></a></span>\n' +
+            (setting.readOnly ? '' : '            <a href="#upload" data-toggle="modal" data-target="#upload"  class="btn btn-sm btn-outline-primary ml-3">上传</a>\n') +
+            '        </li>\n' +
+            '    </ul>\n' +
+            '</div>\n' +
+            '<div class="sjs-sh tab-content">\n' +
+            '    <div class="tab-pane active" id="att-cur" style="height: 100%">\n' +
+            '        <div style="overflow:auto; overflow-x:hidden; height: 100%">\n' +
+            '            <div class="mt-1 mb-1" id="att-cur-hint"></div>\n' +
+            '            <table class="table table-sm table-bordered table-hover" style="word-break:break-all; table-layout: fixed">\n' +
+            '                <thead><tr><th width="25"><input type="checkbox" class="check-all-file"><th>文件名</th><th width="80">上传</th><th width="80">操作</th></tr></thead>\n' +
+            '                <tbody id="cur-att-list" class="list-table"></tbody>\n' +
+            '            </table>\n' +
+            '        </div>\n' +
+            '    </div>\n' +
+            '    <div class="tab-pane" id="att-all" style="height: 100%">\n' +
+            '        <div style="overflow:auto; overflow-x:hidden; height: 100%">\n' +
+            '            <table class="table table-sm table-bordered table-hover" style="word-break:break-all; table-layout: fixed">\n' +
+            '                <thead><tr><th width="25"><input type="checkbox" class="check-all-file"></th><th>文件名</th><th width="80">上传</th><th width="80">操作</th></tr></thead>\n' +
+            '                <tbody id="all-att-list" class="list-table"></tbody>\n' +
+            '           </table>\n' +
+            '        </div>\n' +
+            '    </div>\n' +
+            '</div>'
+        );
+        autoFlashHeight();
+        $('#att-cur-button')[0].click();
+
+        let allAtts = [], nodeIndexes = {};
+
+        const getAttHtml = function(att, tipNode = false) {
+            const html = [];
+            html.push('<tr>');
+            html.push(`<td width="25"><input type="checkbox" class="check-file" file-id=${att.id}></td>`);
+            let nodeInfo = '';
+            if (tipNode && att.node) nodeInfo = `${att.node.code || att.node.b_code || ''}/${att.node.name || ''}`;
+            const tipHtml = nodeInfo ? `${nodeInfo}\n${att.in_time}` : att.in_time;// nodeInfo ? `${nodeInfo}<br/>${att.in_time}` : att.in_time;
+            const tipType = 'title='; //'data-toggle="tooltip" data-html="true" data-placement="left" data-original-title=';
+            html.push(`<td><div class="d-flex"><a href="javascript:void(0)" ${tipType}"${tipHtml}" class="pl-0 col-11" file-id=${att.id}>${att.filename}${att.fileext}</a></div></td>`);
+            html.push(`<td>${att.username}</td>`);
+            const canDel = setting.readOnly ? false : att.uid === userID && (!setting.checked || att.extra_upload);
+            html.push('<td width="80">',
+                `<a class="ml-1" href="javascript:void(0)" ${tipType}"定位" name="att-locate" file-id="${att.id}"><i class="fa fa-crosshairs"></i></a>`,
+                att.viewpath ? `<a class="ml-1" href="${att.viewpath}" ${tipType}"预览"  target="_blank"><i class="fa fa-eye"></i></a>` : '',
+                `<a class="ml-1" href="javascript:void(0)" ${tipType}"下载" onclick="AliOss.downloadFile('${att.filepath}', '${att.filename}${att.fileext}')"><i class="fa fa-download"></i></a>`,
+                canDel ? `<a class="ml-1 text-danger" href="javascript:void(0)" name="att-delete" file-id="${att.id}"><i class="fa fa-close" ${tipType}"删除"></i></a>` : '',
+                '</td>');
+            html.push('</tr>');
+            return html.join('');
+        };
+        const refreshCurAttHtml = function () {
+            const html = [];
+            const atts = (nodeIndexes[curNode[setting.key]]) || [];
+            for (const att of atts) {
+                html.push(getAttHtml(att));
+            }
+            $('#cur-att-list').html(html.join());
+            $('[data-toggle="tooltip"]').tooltip();
+        };
+        const refreshAllAttHtml = function () {
+            let curPage = parseInt($('#att-cur-page').text());
+            if (allAtts.length === 0 && curPage !== 0) curPage = 0;
+            if (allAtts.length > 0 && curPage === 0) curPage = 1;
+            $('#att-cur-page').text(curPage);
+            const pageNum = Math.ceil(allAtts.length/pageLength);
+            $('#att-total-page').text(pageNum);
+            const currPageAttData = allAtts.slice((curPage-1)*pageLength, curPage*pageLength);
+            const html = [];
+            for(const att of currPageAttData) {
+                html.push(getAttHtml(att, true));
+            }
+            $('#all-att-list').html(html.join());
+            $('[data-toggle="tooltip"]').tooltip();
+        };
+        const getAllAttHtml = function (page = 1) {
+            curPage = allAtts.length ? page : 0;
+            $('#att-cur-page').text(curPage);
+            refreshAllAttHtml();
+        };
+        const getCurAttHtml = function (node) {
+            curNode = node;
+            $('#att-cur-hint').text(`${curNode.code || curNode.b_code || ''}/${curNode.name || ''}`);
+            refreshCurAttHtml();
+        };
+
+        // 选中行
+        $('body').on('click', '#all-att-list tr', function() {
+            $('#all-att-list tr').removeClass('bg-light');
+            $(this).addClass('bg-light');
+        });
+        $('body').on('click', '#cur-att-list tr', function() {
+            $('#cur-att-list tr').removeClass('bg-light');
+            $(this).addClass('bg-light');
+        });
+        // 切换 当前节点/所有附件
+        $('#fujian .nav-link').on('click', function () {
+            const tabPanel = $(this).attr('att-type');
+            if (tabPanel !== 'all') {
+                $('#showPage').hide();
+                $('#batch-download').prop('type', 'curr');
+            } else {
+                $('#showPage').show();
+                $('#batch-download').prop('type', 'all')
+            }
+        });
+        // 切换页数
+        $('.page-select').on('click', function () {
+            const totalPageNum = parseInt($('#att-total-page').text());
+            const lastPageNum = parseInt($('#att-cur-page').text());
+            const status = $(this).attr('content');
+            if (status === 'pre' && lastPageNum > 1) {
+                getAllAttHtml(lastPageNum-1);
+                $('#showAttachment').hide();
+                $('#att-all .check-all-file').prop('checked', false)
+            } else if (status === 'next' && lastPageNum < totalPageNum) {
+                getAllAttHtml(lastPageNum+1);
+                $('#showAttachment').hide();
+                $('#att-all .check-all-file').prop('checked', false)
+            }
+        });
+        // 批量下载
+        $('#batch-download').click(function() {
+            const self = this;
+            const files = [];
+            const type = $(this).prop('type');
+            let node = '';
+            if (type === 'curr') {
+                node = '#cur-att-list .check-file:checked'
+            } else {
+                node = '#all-att-list .check-file:checked'
+            }
+            $(node).each(function() {
+                const fid = $(this).attr('file-id');
+                const att = allAtts.find(function (item) {
+                    return item.id === parseInt(fid);
+                });
+                att && files.push(att);
+            });
+
+            if (files.length === 0) return;
+
+            $(self).attr('disabled', 'true');
+            AliOss.zipFiles(files, setting.zipName, (fails) => {
+                $(self).removeAttr('disabled');
+                if (fails.length === 0) {
+                    toastr.success('下载成功');
+                } else {
+                    toastr.warning(`下载成功(${fails.length}个文件下载失败)`);
+                }
+            }, () => {
+                $(self).removeAttr('disabled');
+                toastr.error('批量下载失败');
+            });
+        });
+        // 上传附件
+        $('#upload-file-btn').click(function () {
+            const files = $('#upload-file')[0].files;
+            const formData = new FormData();
+            formData.append('lid', curNode[setting.key]);
+            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(setting.uploadUrl, formData, function (data) {
+                // 插入到attData中
+                data.forEach(d => {
+                    d.node = curNode;
+                    allAtts.push(d);
+                    _addToNodeIndex(d, true);
+                });
+                // 重新生成List
+                refreshAllAttHtml();
+                refreshCurAttHtml();
+
+                $('#upload').modal('hide');
+            }, function () {
+                toastr.error('附件上传失败');
+            });
+            $('#upload-file').val('');
+        });
+        $('body').on('click', 'a[name=att-locate]', function () {
+            const fid = this.getAttribute('file-id');
+            const att = allAtts.find(item => item.id === parseInt(fid));
+            setting.locate && setting.locate(att);
+        });
+        $('body').on('click', 'a[name=att-delete]', function () {
+            const fid = this.getAttribute('file-id');
+            const data = {id: fid};
+            postData(setting.deleteUrl, data, function (result) {
+                // 删除
+                const att_index = allAtts.findIndex(item => { return item.id === parseInt(fid); });
+                const att = allAtts[att_index];
+                allAtts.splice(att_index, 1);
+                const xi = nodeIndexes[att.node[setting.key]];
+                xi.splice(xi.findIndex(x => { return x.id === parseInt(fid); }), 1);
+                // 重新生成List
+                if (allAtts.length === 1) {
+                    getAllAttHtml();
+                } else {
+                    refreshAllAttHtml();
+                }
+                refreshCurAttHtml();
+            });
+        });
+        // 监听附件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);
+            })
+        });
+
+        const _addToNodeIndex = function(att, isTop = false) {
+            const id = att[setting.key];
+            if (!nodeIndexes[id]) {
+                nodeIndexes[id] = [];
+            }
+            let xi = nodeIndexes[id];
+            isTop ? xi.unshift(att) : xi.push(att);
+        };
+        const loadDatas = function (datas) {
+            for (const d of datas) {
+                allAtts.push(d);
+                _addToNodeIndex(d);
+            }
+            getAllAttHtml();
+        };
+
+        return { loadDatas, getCurAttHtml }
+    };
+})(jQuery);

+ 4 - 1
app/router.js

@@ -433,6 +433,9 @@ module.exports = app => {
     app.get('/tender/:id/settle/:sorder/ledger', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.ledger');
     app.post('/tender/:id/settle/:sorder/load', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.loadSettleData');
     app.post('/tender/:id/settle/:sorder/select/update', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.updateSelect');
+    app.post('/tender/:id/settle/:sorder/file/upload', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.uploadFile');
+    app.post('/tender/:id/settle/:sorder/file/delete', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.deleteFile');
+    app.post('/tender/:id/settle/:sorder/file/save', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.saveFile');
     // 结算汇总
     app.get('/tender/:id/settle/gather', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.gather');
     app.get('/tender/:id/settle/gather/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.loadGatherData');
@@ -488,7 +491,6 @@ module.exports = app => {
     app.post('/tender/:id/signReport/post', sessionAuth, tenderCheck, uncheckTenderCheck, 'reportArchiveController.signPost');
     app.post('/tender/:id/signReport/file', sessionAuth, tenderCheck, uncheckTenderCheck, 'reportArchiveController.signFile');
 
-
     // 变更管理
     app.get('/tender/:id/change', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.index');
     app.get('/tender/:id/change/status/:status', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.status');
@@ -717,6 +719,7 @@ module.exports = app => {
     app.post('/tender/:id/ledger/tag', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.billsTag');
     app.post('/tender/:id/revise/:rid/info/tag', sessionAuth, tenderCheck, uncheckTenderCheck, reviseCheck, 'tenderController.billsTag');
     app.post('/tender/:id/measure/stage/:order/tag', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'tenderController.billsTag');
+    app.post('/tender/:id/settle/:sorder/ledger/tag', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'tenderController.billsTag');
 
     // 总分包
     app.post('/tender/:id/ledger/sumLoad', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.sumLoad');

+ 28 - 0
app/service/ledger_att.js

@@ -144,6 +144,34 @@ module.exports = app => {
                 });
             });
         }
+
+        async getViewDataByFid(id) {
+            const sql = 'SELECT att.id, att.lid, att.uid, att.filepath, att.filename, att.fileext, att.filesize, att.extra_upload, att.remark, att.in_time,' +
+                ' pa.name as `username`' +
+                ' FROM ' + this.tableName + ' att Left Join ' + this.ctx.service.projectAccount.tableName + ' pa On pa.id = att.uid' +
+                ' WHERE att.id = ?';
+            const result = await this.db.query(sql, [id]);
+            for (const r of result) {
+                r.filepath = this.ctx.app.config.fujianOssPath + r.filepath;
+                if (this.ctx.helper.canPreview(r.fileext)) r.viewpath = r.filepath;
+                r.in_time = this.ctx.moment(r.in_time * 1000).format('YYYY-MM-DD');
+            }
+            return result[0];
+        }
+
+        async getViewData(tid, settle_id = -1) {
+            const sql = 'SELECT att.id, att.lid, att.uid, att.filepath, att.filename, att.fileext, att.filesize, att.extra_upload, att.remark, att.in_time,' +
+                ' pa.name as `username`' +
+                ' FROM ' + this.tableName + ' att Left Join ' + this.ctx.service.projectAccount.tableName + ' pa On pa.id = att.uid' +
+                ' WHERE att.tid = ? AND att.settle_id = ? ORDER BY att.id DESC';
+            const result = await this.db.query(sql, [tid, settle_id]);
+            for (const r of result) {
+                r.filepath = this.ctx.app.config.fujianOssPath + r.filepath;
+                if (this.ctx.helper.canPreview(r.fileext)) r.viewpath = r.filepath;
+                r.in_time = this.ctx.moment(r.in_time * 1000).format('YYYY-MM-DD');
+            }
+            return result;
+        }
     }
     return LedgerAtt;
 };

+ 2 - 6
app/service/ledger_tag.js

@@ -33,12 +33,8 @@ module.exports = app => {
         async getDatas(tid, sid = -1, settleId = -1) {
             const sql = 'SELECT la.id, la.uid, la.lid, la.share, la.color, la.comment, pa.name as u_name FROM ' + this.tableName + ' la ' +
                 '  LEFT JOIN ' + this.ctx.service.projectAccount.tableName + ' pa ON la.uid = pa.id' +
-                '  WHERE la.tid = ? and la.sid = ? and (la.uid = ? or la.share) ORDER BY la.create_time DESC';
-            return await this.db.query(sql, [tid, sid, this.ctx.session.sessionUser.accountId]);
-            // const sql = 'SELECT la.id, la.uid, la.lid, la.share, la.color, la.comment, pa.name as u_name FROM ' + this.tableName + ' la ' +
-            //     '  LEFT JOIN ' + this.ctx.service.projectAccount.tableName + ' pa ON la.uid = pa.id' +
-            //     '  WHERE la.tid = ? and la.sid = ? and la.settle_id = ? and (la.uid = ? or la.share) ORDER BY la.create_time DESC';
-            // return await this.db.query(sql, [tid, sid, settleId, this.ctx.session.sessionUser.accountId]);
+                '  WHERE la.tid = ? and la.sid = ? and la.settle_id = ? and (la.uid = ? or la.share) ORDER BY la.create_time DESC';
+            return await this.db.query(sql, [tid, sid, settleId, this.ctx.session.sessionUser.accountId]);
         }
 
         /**

+ 15 - 0
app/view/settle/index.ejs

@@ -72,6 +72,9 @@
                     </div>
                     <div id="bills-tag" class="tab-pane tab-select-show">
                     </div>
+                    <!--附件-->
+                    <div id="fujian" class="tab-pane tab-select-show">
+                    </div>
                 </div>
             </div>
         </div>
@@ -84,13 +87,25 @@
                 <li class="nav-item">
                     <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>
             </ul>
         </div>
     </div>
 </div>
+<div style="display: none">
+    <img src="/public/images/ellipsis_horizontal.png" id="ellipsis-icon" />
+    <img src="/public/images/icon-ok.png" id="icon-ok" />
+    <img src="/public/images/file_clip.png" id="rela-file-icon">
+    <img src="/public/images/file_clip_hover.png" id="rela-file-hover">
+</div>
 <script>
     const readOnly = <%- settle.readOnly %>;
+    const auditConst = JSON.parse(JSON.stringify('<%- auditConst %>'));
+    const tenderName = '<%- ctx.tender.name %>';
+    const settleOrder = <%- ctx.settle.settle_order %>;
+    const settleComplete = <%- (ctx.settle.audit_status === auditConst.status.checked ? 1 : 0 )%>;
     const tenderInfo = JSON.parse(unescape('<%- escape(JSON.stringify(ctx.tender.info)) %>'));
     const thousandth = <%- ctx.tender.info.display.thousandth %>;
     const thirdParty = JSON.parse('<%- JSON.stringify(thirdParty) %>');

+ 2 - 0
app/view/settle/modal.ejs

@@ -0,0 +1,2 @@
+<% include ../shares/new_tag_modal.ejs %>
+<% include ../shares/upload_att.ejs %>

+ 23 - 0
app/view/shares/upload_att.ejs

@@ -0,0 +1,23 @@
+<!--上传附件-->
+<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>
+<script>
+    const whiteList = JSON.parse('<%- JSON.stringify(ctx.app.config.multipart.whitelist) %>');
+</script>

+ 4 - 0
config/web.js

@@ -1368,17 +1368,21 @@ const JsFiles = {
             },
             ledger: {
                 files: [
+                    '/public/js/axios/axios.min.js', '/public/js/file-saver/FileSaver.js', '/public/js/js-xlsx/jszip.min.js',
                     '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
                     '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
                     '/public/js/decimal.min.js',
                 ],
                 mergeFiles: [
+                    '/public/js/shares/ali_oss.js',
                     '/public/js/shares/cs_tools.js',
                     '/public/js/component/menu.js',
                     '/public/js/sub_menu.js',
                     '/public/js/div_resizer.js',
                     '/public/js/spreadjs_rela/spreadjs_zh.js',
                     '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/new_tag.js',
+                    '/public/js/shares/tools_att.js',
                     '/public/js/zh_calc.js',
                     '/public/js/path_tree.js',
                     '/public/js/settle_ledger.js',

+ 8 - 0
sql/update.sql

@@ -23,3 +23,11 @@ CREATE TABLE `zh_change_plan_history` (
   `list_json` json DEFAULT NULL COMMENT '清单json值',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='变更方案内容临时保存表,用于修订撤销与撤回';
+
+ALTER TABLE `zh_ledger_tag`
+ADD COLUMN `settle_id`  int(11) NOT NULL DEFAULT -1 AFTER `sorder`,
+ADD COLUMN `settle_order`  tinyint(4) NOT NULL DEFAULT -1 AFTER `settle_id`;
+
+ALTER TABLE `zh_ledger_attachment`
+ADD COLUMN `settle_id`  int(11) NOT NULL DEFAULT -1 COMMENT '结算id' AFTER `tid`,
+ADD COLUMN `settle_order`  tinyint(4) NOT NULL DEFAULT -1 COMMENT '结算期序号' AFTER `settle_id`;