浏览代码

资料归集相关

MaiXinRong 2 年之前
父节点
当前提交
46d81acfc6

+ 156 - 2
app/controller/file_controller.js

@@ -8,6 +8,10 @@
  * @version
  */
 const auditConst = require('../const/audit');
+const sendToWormhole = require('stream-wormhole');
+const path = require('path');
+const advanceConst = require('../const/advance');
+
 module.exports = app => {
     class BudgetController extends app.BaseController {
 
@@ -105,9 +109,8 @@ module.exports = app => {
         async loadFile(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
-                const result = await ctx.service.file.getAllDataByCondition({
+                const result = await ctx.service.file.getFiles({
                     where: { filing_id: data.filing_id, is_deleted: 0 },
-                    orders: [['create_time', 'asc']],
                     limit: data.count,
                     offset: (data.page-1)*data.count
                 });
@@ -118,11 +121,162 @@ module.exports = app => {
             }
         }
 
+        async checkCanUpload(ctx) {
+            if (ctx.subProject.permission.file_permission.indexOf(ctx.service.subProjPermission.PermissionConst.file.upload.value) < 0) {
+                throw '您无权上传、导入、删除文件';
+            }
+        }
+
+        async checkFiling(filing) {
+            const child = await this.ctx.service.filing.getDataByCondition({ tree_pid: filing.id, is_deleted: 0 });
+            if (child) throw '该分类下存在子分类,请在子分类下上传、导入文件';
+        }
+
         async uploadFile(ctx){
+            let stream;
+            try {
+                await this.checkCanUpload(ctx);
+
+                const parts = ctx.multipart({ autoFields: true });
+
+                let index = 0;
+                const create_time = Date.parse(new Date()) / 1000;
+                let stream = await parts();
+                const user = await ctx. service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const filing = await ctx.service.filing.getDataById(parts.field.filing_id);
+                if (!filing || filing.is_deleted) throw '分类不存在,请刷新页面后重试';
+                await this.checkFiling(filing);
+
+                const uploadfiles = [];
+                while (stream !== undefined) {
+                    if (!stream.filename) throw '未发现上传文件!';
+
+                    const fileInfo = path.parse(stream.filename);
+                    const filepath = `sp/file/${filing.spid}/${ctx.moment().format('YYYYMMDD')}/${create_time + '_' + index + fileInfo.ext}`;
+
+                    // 保存文件
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    await sendToWormhole(stream);
+
+                    // 插入到stage_pay对应的附件列表中
+                    uploadfiles.push({
+                        filename: fileInfo.name,
+                        fileext: fileInfo.ext,
+                        filesize: Array.isArray(parts.field.size) ? parts.field.size[index] : parts.field.size,
+                        filepath,
+                    });
+                    ++index;
+                    if (Array.isArray(parts.field.size) && index < parts.field.size.length) {
+                        stream = await parts();
+                    } else {
+                        stream = undefined;
+                    }
+                }
+
+                const result = await ctx.service.file.addFiles(filing, uploadfiles, user);
+                ctx.body = {err: 0, msg: '', data: result };
+            } catch (error) {
+                ctx.helper.log(error);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.body = this.ajaxErrorBody(error, '上传附件失败,请重试');
+            }
         }
         async delFile(ctx) {
+            try{
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.del) throw '缺少参数';
+                const result = await ctx.service.file.delFiles(data.del);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch(error) {
+                this.log(error);
+                ctx.ajaxErrorBody(error, '删除附件失败');
+            }
+        }
+
+        async loadValidRelaTender(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (data.type) throw '参数错误';
+
+                const accountInfo = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const userPermission = accountInfo !== undefined && accountInfo.permission !== ''
+                    ? JSON.parse(accountInfo.permission) : null;
+                const tenderList = await ctx.service.tender.getList('', userPermission, ctx.session.sessionUser.is_admin);
+
+                const rela_tender = await ctx.subProject.rela_tender.split(',');
+                const result = tenderList.filter(x => { return rela_tender.indexOf(x.id + '') >= 0});
+                for (const r of result) {
+                    r.advance = await ctx.service.advance.getAllDataByCondition({ columns: ['id', 'order', 'type'], where: { tid: r.id }});
+                    r.advance.forEach(a => {
+                        const type = advanceConst.typeCol.find(x => { return x.type === a.type });
+                        if (type) a.type_str = type.name;
+                    });
+                    r.stage = await ctx.service.stage.getAllDataByCondition({ columns: ['id', 'order'], where: { tid: r.id, status: auditConst.stage.status.checked } });
+                }
+                ctx.body = {err: 0, msg: '', data: result };
+            } catch (error) {
+                ctx.helper.log(error);
+                ctx.body = this.ajaxErrorBody(error, '加载标段信息失败');
+            }
+        }
+        async _loadLedgerAtt(data) {
+            if (!data.tender_id) throw '参数错误';
+            return await this.ctx.service.ledgerAtt.getAllDataByCondition({ where: { tid: data.tender_id }, order: [['id', 'desc']]});
+        }
+        async _loadStageAtt(data) {
+            if (!data.tender_id || !data.stage) throw '参数错误';
+            switch (data.sub_type) {
+                case 'att':
+                    const stage = await this.ctx.service.stage.getDataById(data.stage);
+                    return await this.ctx.service.stageAtt.getAllDataByCondition({ where: { tid: data.tender_id, sid: stage.order }, order: [['id', 'desc']]});
+            }
+        }
+        async _loadAdvanceAtt(data) {
+            if (!data.stage) throw '参数错误';
+            return await this.ctx.service.advanceFile.getAllDataByCondition({ where: { vid: data.stage }, order: [['id', 'desc']]});
+        }
+        async loadRelaFiles(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                console.log(data);
+                if (!data.type) throw '参数错误';
+
+                let files;
+                switch(data.type) {
+                    case 'ledger':
+                        files = await this._loadLedgerAtt(data);
+                        break;
+                    case 'stage':
+                        files = await this._loadStageAtt(data);
+                        break;
+                    case 'advance':
+                        files = await this._loadAdvanceAtt(data);
+                        break;
+                    default: throw '未知文件类型';
+                }
+                ctx.body = {err: 0, msg: '', data: files };
+            } catch (error) {
+                ctx.helper.log(error);
+                ctx.body = this.ajaxErrorBody(error, '加载附件失败,请重试');
+            }
         }
         async relaFile(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.filing_id || !data.files) throw '缺少参数';
+
+                const user = await ctx. service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const filing = await ctx.service.filing.getDataById(data.filing_id);
+                if (!filing || filing.is_deleted) throw '分类不存在,请刷新页面后重试';
+                await this.checkFiling(filing);
+
+                const result = await ctx.service.file.relaFiles(filing, data.files, user);
+                ctx.body = {err: 0, msg: '', data: result };
+            } catch (error) {
+                ctx.helper.log(error);
+                ctx.body = this.ajaxErrorBody(error, '导入附件失败,请重试');
+            }
         }
     }
 

+ 1 - 1
app/extend/helper.js

@@ -1281,7 +1281,7 @@ module.exports = {
             { text: '文档', ext: ['.json', '.txt', '.xls', '.xlsx', '.doc', '.docx', '.pdf', '.ppt', '.pptx',]},
         ];
         for (const es of ExtStr) {
-            if (es.indexOf(ext) >= 0) return es.text;
+            if (es.ext.indexOf(ext) >= 0) return es.text;
         }
         return '';
     },

+ 514 - 28
app/public/js/file_detail.js

@@ -41,15 +41,15 @@ $(document).ready(function() {
         _getFileHtml(file) {
             const html = [];
             html.push('<tr>');
-            html.push('<tr><td><input type="checkbox"></td>');
-            const editHtml = file.canEdit ? '<a href="" class="mr-1"><i class="fa fa-pencil fa-fw"></i></a>' : '';
+            html.push(`<tr><td><input type="checkbox" name="bd-check" fid="${file.id}"></td>`);
+            const editHtml = file.canEdit ? '<a href="javascript: void(0);" class="mr-1"><i class="fa fa-pencil fa-fw"></i></a>' : '';
             const viewHtml = file.viewpath ? `<a href="${file.viewpath}" class="mr-1"><i class="fa fa-eye fa-fw"></i></a>` : '';
-            const downHtml = '<a href="" class="mr-1"><i class="fa fa-download fa-fw"></i></a>';
-            const delHtml = file.canEdit ? '<a href="" class="mr-1"><i class="fa fa-trash-o fa-fw"></i></a>' : '';
-            html.push(`<td><div class="d-flex justify-content-between align-items-center table-file"><div></div>${file.filename}</div><div class="btn-group-table" style="display: none;">${editHtml}${viewHtml}${downHtml}${delHtml}</div></div></td>`);
+            const downHtml = `<a href="javascript: void(0);" onclick="AliOss.downloadFile('${file.filepath}', '${file.filename + file.fileext}')" class="mr-1"><i class="fa fa-download fa-fw"></i></a>`;
+            const delHtml = file.canEdit ? '<a href="javascript: void(0);" class="mr-1"><i class="fa fa-trash-o fa-fw"></i></a>' : '';
+            html.push(`<td><div class="d-flex justify-content-between align-items-center table-file"><div>${file.filename}${file.fileext}</div><div class="btn-group-table" style="display: none;">${editHtml}${viewHtml}${downHtml}${delHtml}</div></div></td>`);
             html.push(`<td>${file.user_name}</td>`);
             html.push(`<td>${moment(file.create_time).format('YYYY-MM-DD HH:mm:ss')}</td>`);
-            html.push('<td>${file.fileext_str}</td>');
+            html.push(`<td>${file.fileext_str}</td>`);
             html.push('</tr>');
             return html.join('');
         }
@@ -66,18 +66,37 @@ $(document).ready(function() {
             }
             $('#file-list').html(html.join(''));
         }
+        refreshPages() {
+            if (!filingObj.curFiling) return;
+
+            filingObj.curTotalPage = Math.ceil(filingObj.curFiling.source_node.file_count / this.pageCount);
+            $('#curPage').html(filingObj.curPage);
+            $('#curTotalPage').html(filingObj.curTotalPage);
+            if (filingObj.curTotalPage > 1) {
+                $('#showPage').show();
+            } else {
+                $('#showPage').hide();
+            }
+        }
         async loadFiles(node, page) {
             if (node.source_node.children && node.source_node.children.length > 0) return;
+            if (!node.source_node.files) node.source_node.files = [];
 
             if (!node.source_node.file_count) return;
-            if (node.source_node.files.length === node.source_node.file_count) return;
+            if (node.source_node.files && node.source_node.files.length === node.source_node.file_count) return;
 
             const needFiles = Math.min(page*this.pageCount, node.source_node.file_count);
-            if (needFiles < node.source_node.files.length) return;
+            if (node.source_node.files && needFiles <= node.source_node.files.length) return;
 
-            if (!node.source_node.files) node.source_node.files = [];
             const files = await postDataAsync('file/load', { filing_id: node.id, page, count: this.pageCount });
-            node.source_node.files.push(...files);
+            files.forEach(x => {
+                const file = node.source_node.files.find(f => {return x.id === f.id; });
+                if (file) {
+                    Object.assign(file, x);
+                } else {
+                    node.source_node.files.push(x);
+                }
+            });
             node.source_node.files.sort((x, y) => {
                 return x.create_time - y.create_time;
             });
@@ -106,7 +125,112 @@ $(document).ready(function() {
             });
         }
         async renameFiling(node, newName) {
-            return await postDataAsync('filing/save', { id: node.id, name: newName });
+            const result = await postDataAsync('filing/save', { id: node.id, name: newName });
+            node.source_node.name = newName;
+            node.name = node.source_node.name + (node.source_node.total_file_count > 0 ? `(${node.source_node.total_file_count})` : '');
+            return result;
+        }
+        updateFilingFileCount(filing, count) {
+            let differ = count - filing.source_node.file_count;
+            filing.source_node.file_count = count;
+            filing.source_node.total_file_count = count;
+            filing.name = filing.source_node.name + (filing.source_node.total_file_count > 0 ? `(${filing.source_node.total_file_count})` : '');
+            filingObj.filingTree.updateNode(filing);
+            let parent = filing.getParentNode();
+            while (!!parent) {
+                parent.source_node.total_file_count = parent.source_node.total_file_count + differ;
+                parent.name = parent.source_node.name + (parent.source_node.total_file_count > 0 ? `(${parent.source_node.total_file_count})` : '');
+                filingObj.filingTree.updateNode(parent);
+                parent = parent.getParentNode();
+            }
+        }
+        uploadFiles(files, callback) {
+            const formData = new FormData();
+            formData.append('filing_id', filingObj.curFiling.id);
+            for (const file of files) {
+                if (file === undefined) {
+                    toastr.error('未选择上传文件。');
+                    return false;
+                }
+                if (file.size > 30 * 1024 * 1024) {
+                    toastr.error('上传文件大小超过30MB。');
+                    return false;
+                }
+                const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+                if (whiteList.indexOf(fileext) === -1) {
+                    toastr.error('仅支持office文档、图片、压缩包格式,请勿上传' + fileext + '格式文件。');
+                    return false;
+                }
+                formData.append('size', file.size);
+                formData.append('file[]', file);
+            }
+            postDataWithFile('file/upload', formData, function (data) {
+                filingObj.curFiling.source_node.files.unshift(...data.files);
+                filingObj.updateFilingFileCount(filingObj.curFiling, data.filing.file_count);
+                filingObj.refreshFilesTable();
+                filingObj.refreshPages();
+                if (callback) callback();
+            });
+        }
+        delFiles(files, callback) {
+            postData('file/del', { del: files }, async function(data) {
+                for (const id of data.del) {
+                    const fIndex = filingObj.curFiling.source_node.files.findIndex(x => { return x.id === id });
+                    if (fIndex >= 0) filingObj.curFiling.source_node.files.splice(fIndex, 1);
+                }
+                filingObj.updateFilingFileCount(filingObj.curFiling, data.filing.file_count);
+                await filingObj.loadFiles(filingObj.curFiling, filingObj.curPage);
+                filingObj.refreshFilesTable();
+                filingObj.refreshPages();
+                if (callback) callback();
+            });
+        }
+        relaFiles(files, callback) {
+            postData('file/rela', { filing_id: this.curFiling.id, files: files }, async function(data) {
+                filingObj.curFiling.source_node.files.unshift(...data.files);
+                filingObj.updateFilingFileCount(filingObj.curFiling, data.filing.file_count);
+                filingObj.refreshFilesTable();
+                filingObj.refreshPages();
+                if (callback) callback();
+            });
+        }
+        async setCurFiling(node) {
+            filingObj.curFiling = node;
+            filingObj.curPage = 1;
+            filingObj.refreshPages();
+
+            if (filingObj.curFiling.source_node.children && filingObj.curFiling.source_node.children.length > 0) {
+                $('#file-view').hide();
+            } else {
+                $('#file-view').show();
+                await filingObj.loadFiles(node, 1);
+                filingObj.refreshFilesTable();
+            }
+            if (filingObj.curFiling.source_node.filing_type === 5) {
+                $('#rela-file').parent().show();
+            } else {
+                $('#rela-file').parent().hide();
+            }
+        }
+        prePage() {
+            if (this.curPage === 1) return;
+            this.curPage = this.curPage - 1;
+            this.refreshFilesTable();
+        }
+        async nextPage() {
+            if (this.curPage === this.curTotalPage) return;
+            await filingObj.loadFiles(this.curFiling, this.curPage + 1);
+            this.curPage = this.curPage + 1;
+            this.refreshFilesTable();
+        }
+        getCurFilingFullPath(){
+            let cur = filingObj.curFiling;
+            const result = [];
+            while (cur) {
+                result.unshift(cur.source_node.name);
+                cur = cur.getParentNode();
+            }
+            return result.join('/');
         }
     }
     const levelTreeSetting = {
@@ -138,24 +262,18 @@ $(document).ready(function() {
             onClick: async function (e, key, node) {
                 if (filingObj.curFiling && filingObj.curFiling.id === node.id) return;
 
-                filingObj.curFiling = node;
-                filingObj.curPage = 1;
-                if (filingObj.curFiling.source_node.children && filingObj.curFiling.source_node.children.length > 0) {
-                    $('#file-view').hide();
-                } else {
-                    $('#file-view').show();
-                    await filingObj.loadFiles(node, 1);
-                    filingObj.refreshFilesTable();
-                }
-                if (filingObj.curFiling.source_node.filing_type === 5) {
-                    $('#rela-file').parent().show();
-                } else {
-                    $('#rela-file').parent().hide();
-                }
+                filingObj.setCurFiling(node);
+            },
+            beforeEditName: function(key, node) {
+                node.name = node.source_node.name;
             },
             beforeRename: async function(key, node, newName, isCancel) {
-                const result = await filingObj.renameFiling(node, newName);
-                return !result;
+                if (!isCancel) await filingObj.renameFiling(node, newName);
+                return true;
+            },
+            onRename: function(e, key, node, isCancel) {
+                node.name = node.name + (node.source_node.total_file_count > 0 ? `(${node.source_node.total_file_count})` : '');
+                filingObj.filingTree.updateNode(node);
             }
         }
     };
@@ -179,15 +297,383 @@ $(document).ready(function() {
 
         filingObj.addChildFiling(filingObj.curFiling);
     });
-    $('#del-filing').click(() => {
+    $('#del-filing-btn').click(() => {
         if (!filingObj.curFiling) return;
         if (filingObj.curFiling.source_node.is_fixed) {
             toastr.error('固定分类不可删除');
             return;
         }
 
+        $('#del-filing').modal('show');
+    });
+    $('#del-filing-ok').click(() => {
         filingObj.delFiling(filingObj.curFiling);
     });
+    $('#add-file-ok').click(() => {
+        const input = $('#upload-file');
+        filingObj.uploadFiles(input[0].files, function() {
+            $(input).val('');
+            $('#add-file').modal('hide');
+        });
+    });
+    $('body').on('mouseenter', ".table-file", function(){
+        $(this).children(".btn-group-table").css("display","block");
+    });
+    $('body').on('mouseleave', ".table-file", function(){
+        $(this).children(".btn-group-table").css("display","none");
+    });
+    $('.page-select').click(function() {
+        const content = this.getAttribute('content');
+        switch(content) {
+            case 'pre': filingObj.prePage(); break;
+            case 'next': filingObj.nextPage(); break;
+            default: return;
+        }
+    });
+    $('#batch-download').click(function () {
+        const self = this;
+        const files = [];
+        const checkes = $('[name=bd-check]:checked');
+        checkes.each(function() {
+            const fid = this.getAttribute('fid');
+            const file = filingObj.curFiling.source_node.files.find(x => { return x.id === fid; });
+            file && files.push(file);
+        });
+
+        if (files.length === 0) return;
+
+        $(self).attr('disabled', 'true');
+        AliOss.zipFiles(files, filingObj.curFiling.source_node.name + '.zip', (fails) => {
+            $(self).removeAttr('disabled');
+            if (fails.length === 0) {
+                toastr.success('下载成功');
+            } else {
+                toastr.warning(`下载成功(${fails.length}个文件下载失败)`);
+            }
+        }, () => {
+            $(self).removeAttr('disabled');
+            toastr.error('批量下载失败');
+        });
+    });
+    $('#batch-del-file').on('show.bs.modal', function(e) {
+        const checkes = $('[name=bd-check]:checked');
+        if (checkes.length === 0) {
+            e.preventDefault();
+        } else {
+            for (const c of checkes) {
+                const fid = c.getAttribute('fid');
+                const file = filingObj.curFiling.source_node.files.find(x => { return x.id === fid });
+                if (!file) continue;
+
+                if (file.user_id !== userID) {
+                    toastr.error(`文件【${file.filename + file.fileext}】不是您上传的文件,请勿删除`);
+                    e.preventDefault();
+                }
+            }
+        }
+    });
+    $('#batch-del-file-ok').click(function() {
+        const del = [];
+        const checkes = $('[name=bd-check]:checked');
+        checkes.each(function() {
+            del.push(this.getAttribute('fid'));
+        });
+        filingObj.delFiles(del, function() {
+            $('#batch-del-file').modal('hide');
+        });
+    });
+
+    class RelaFileLoader {
+        constructor() {
+            const self = this;
+            // 可导入的标段
+            this.treeSetting = {
+                view: {
+                    selectedMulti: false
+                },
+                data: {
+                    simpleData: {
+                        idKey: 'id',
+                        pIdKey: 'tree_pid',
+                        rootPId: '-1',
+                        enable: true,
+                    }
+                },
+                edit: {
+                    enable: false,
+                },
+                callback: {
+                    onClick: async function (e, key, node) {
+                        if (this.curTender && this.curTender.id === node.id) return;
+
+                        self.setCurTender(node);
+                    },
+                }
+            };
+            $('body').on('click', '[name=rf-check]', function () {
+                self.selectFile(this.getAttribute('rfid'), this.checked);
+            });
+            $('#tf-type').change(function() {
+                self.selectTfType(this.value);
+            });
+            $('#tf-sub-type').change(function() {
+                self.selectTfSubType(this.value);
+            });
+            $('#tf-stage').change(function() {
+                self.selectTfStage(this.value);
+            });
+            $('#rela-file-ok').click(function() {
+                const selectFiles = self.getSelectRelaFile();
+                filingObj.relaFiles(selectFiles, function() {
+                    $('#rela-file').modal('hide');
+                });
+            });
+        }
+        clearFileSelect() {
+            if (!this.tenderTree) return;
+
+            const nodes = this.tenderTree.getNodes();
+            nodes.forEach(node => {
+                const x = node.source_node;
+                x.selectFiles = [];
+                if (x.att) x.att.forEach(la => { la.checked = false });
+                if (x.advance) {
+                    x.advance.forEach(a => {
+                        if (a.att) a.att.forEach(aa => { aa.checked = false });
+                    });
+                }
+                if (x.stage) {
+                    x.stage.forEach(s => {
+                        if (s.att) s.att.forEach(sa => { sa.checked = false });
+                    })
+                }
+            });
+        }
+        refreshSelectHint(){
+            if (this.curTender) {
+                $('#cur-tender-hint').html(`当前标段,已选${this.curTender.source_node.selectFiles.length}文件`);
+            } else {
+                $('#cur-tender-hint').html('');
+            }
+            const nodes = this.tenderTree.getNodes();
+            const selectTenders = nodes.filter(x => { return x.source_node.selectFiles.length > 0; });
+            if (selectTenders.length > 0) {
+                const count = selectTenders.reduce((rst, x) => { return rst + x.source_node.selectFiles.length; }, 0);
+                $('#rela-file-hint').html(`已选择${selectTenders.length}个标段,共${count}个文件`);
+            } else {
+                $('#rela-file-hint').html('未选择标段、文件');
+            }
+        }
+        selectFile(fileId, isSelect) {
+            const file = this.curFiles.find(x => { return x.rf_id == fileId });
+            if (file) {
+                file.checked = isSelect;
+                if (isSelect) {
+                    this.curTender.source_node.selectFiles.push(file);
+                } else {
+                    const index = this.curTender.source_node.selectFiles.findIndex(x => { return x.rf_id === file.rf_id });
+                    this.curTender.source_node.selectFiles.splice(index, 1);
+                }
+                this.refreshSelectHint();
+            }
+        }
+        async showRelaFile(){
+            $('#rela-filing-hint').html(`当前目录:${filingObj.getCurFilingFullPath()}`);
+            if (!this.tenderTree) {
+                const tenders = await postDataAsync('file/rela/tender', {});
+                const sortNodes = tenders.map(x => {
+                    return { id: x.id, tree_pid: -1, name: x.name, source_node: x };
+                });
+                this.tenderTree = this.filingTree = $.fn.zTree.init($('#rela-tender'), this.treeSetting, sortNodes);
+            }
+            this.clearFileSelect();
+            this.refreshSelectHint();
+            const firstNode = this.filingTree.getNodes()[0];
+            if (firstNode) {
+                this.filingTree.selectNode(firstNode);
+                await this.setCurTender(firstNode);
+            }
+        }
+        refreshTenderFileStage() {
+            if (this.rfType.sub_type) {
+                const type = this.tenderFileType.find(x => { return x.value === this.rfType.type});
+                const subType = type.subType ? type.subType.find(x => { return x.value === this.rfType.sub_type; }) : null;
+                if (subType) {
+                    this.rfType.stage = subType.stage[0].value;
+                    const html= [];
+                    for (const stage of subType.stage) {
+                        html.push(`<option value="${stage.value}">${stage.text}</option>`);
+                    }
+                    $('#tf-stage').html(html.join('')).show();
+                } else {
+                    $('#tf-stage').html('').hide();
+                }
+            } else {
+                $('#tf-stage').html('').hide();
+            }
+        }
+        refreshTenderFileSubType() {
+            const type = this.tenderFileType.find(x => { return x.value === this.rfType.type});
+            if (type.subType && type.subType.length > 0) {
+                this.rfType.sub_type = type.subType[0].value;
+                const html= [];
+                for (const tfst of type.subType) {
+                    html.push(`<option value="${tfst.value}">${tfst.text}</option>`);
+                }
+                $('#tf-sub-type').html(html.join('')).show();
+            } else {
+                $('#tf-sub-type').html('').hide();
+            }
+        }
+        refreshTenderFileType() {
+            const html= [];
+            for (const tft of this.tenderFileType) {
+                html.push(`<option value="${tft.value}">${tft.text}</option>`);
+            }
+            $('#tf-type').html(html.join(''));
+        }
+        refreshSelects(tender) {
+            this.tenderFileType = [];
+            this.tenderFileType.push({ value: 'ledger', text: '台账附件' });
+            if (tender.stage && tender.stage.length > 0) {
+                const stages = tender.stage.map(x => { return {value: x.id, text: `第${x.order}期`}; });
+                this.tenderFileType.push({
+                    value: 'stage', text: '计量期',
+                    subType: [
+                        { value: 'att', text: '计量附件', stage: JSON.parse(JSON.stringify(stages)) },
+                    ],
+                });
+            }
+            if (tender.advance && tender.advance.length > 0) {
+                const advanceType = [];
+                tender.advance.forEach(x => {
+                    let at = advanceType.find(y => { return y.value === x.type });
+                    if (!at) {
+                        at = { value: x.type, text: x.type_str, stage: [] };
+                        advanceType.push(at);
+                    }
+                    at.stage.push({ value: x.id, text: `第${x.order}期`});
+                });
+                this.tenderFileType.push({
+                    value: 'advance', text: '预付款', subType: advanceType
+                });
+            }
+            this.rfType = { type: this.tenderFileType[0].value };
+            this.refreshTenderFileType();
+            this.refreshTenderFileSubType();
+            this.refreshTenderFileStage();
+        }
+        async selectTfStage(stage){
+            this.rfType.stage = stage;
+            await this.loadFiles();
+            this.refreshFileTable();
+        }
+        async selectTfSubType(sub_type){
+            this.rfType.sub_type = sub_type;
+            this.refreshTenderFileStage();
+            await this.loadFiles();
+            this.refreshFileTable();
+        }
+        async selectTfType(type){
+            this.rfType.type = type;
+            this.refreshTenderFileSubType();
+            this.refreshTenderFileStage();
+            await this.loadFiles();
+            this.refreshFileTable();
+        }
+        refreshFileTable() {
+            const html = [];
+            const typeStr = [];
+            const selectOption = $('option:selected');
+            selectOption.each((i, x) => {
+               typeStr.push(x.innerText);
+            });
+            for (const f of this.curFiles) {
+                html.push('<tr>');
+                const checked = f.checked ? "checked" : '';
+                html.push(`<td><input type="checkbox" name="rf-check" rfid="${f.rf_id}" ${checked}></td>`);
+                html.push(`<td>${f.filename}${f.fileext}</td>`);
+                html.push(`<td>${typeStr.join(' - ')}</td>`);
+                html.push('</tr>');
+            }
+            $('#rf-files').html(html.join(''));
+        };
+        initFilesId(files){
+            const tender_id = this.curTender.id;
+            const rfType = this.rfType;
+            files.forEach((f, i) => {
+                f.rf_id = `${tender_id}-${rfType.type}-${rfType.sub_type}-${rfType.stage}-${i}`;
+                f.rf_key = {
+                    tender_id, ...rfType, id: f.id,
+                };
+            });
+        }
+        async _loadRelaFiles(rfType) {
+            return await postDataAsync('file/rela/files', { tender_id: this.curTender.id, ...rfType });
+        }
+        async _loadLedgerFile() {
+            if (!this.curTender.source_node.att) this.curTender.source_node.att = await this._loadRelaFiles(this.rfType);
+            this.curFiles = this.curTender.source_node.att;
+        }
+        async _loadStageFile() {
+            const rfType = this.rfType;
+            const stage = this.curTender.source_node.stage.find(x => {
+                return x.id === rfType.stage;
+            });
+            if (!stage) {
+                this.curFiles = [];
+                return;
+            }
+            if (!stage[this.rfType.sub_type]) stage[this.rfType.sub_type] = await this._loadRelaFiles(rfType);
+            this.curFiles = stage[this.rfType.sub_type];
+        }
+        async _loadAdvanceFile() {
+            const rfType = this.rfType;
+            const advance = this.curTender.source_node.advance.find(x => {
+                return x.id === rfType.stage;
+            });
+            if (!advance) {
+                this.curFiles = [];
+                return;
+            }
+            if (!advance.files) advance.files = await this._loadRelaFiles(rfType);
+            this.curFiles = advance.files;
+        }
+        async loadFiles() {
+            switch (this.rfType.type) {
+                case 'ledger': await this._loadLedgerFile(); break;
+                case 'stage': await this._loadStageFile(); break;
+                case 'advance': await this._loadAdvanceFile(); break;
+            }
+            this.initFilesId(this.curFiles);
+        }
+        async setCurTender(node) {
+            this.curTender = node;
+            this.refreshSelects(node.source_node);
+            await this.loadFiles();
+            this.refreshSelectHint();
+            this.refreshFileTable();
+        }
+        getSelectRelaFile() {
+            const data = [];
+            const nodes = this.tenderTree.getNodes();
+            nodes.forEach(node => {
+                if (node.source_node.selectFiles.length === 0) return;
+
+                node.source_node.selectFiles.forEach(x => {
+                    data.push({
+                        filename: x.filename, fileext: x.fileext, filepath: x.filepath, filesize: x.filesize,
+                        rela_info: x.rf_key,
+                    })
+                });
+            });
+            return data;
+        }
+    }
+    const relaFileLoader = new RelaFileLoader();
+    $('#rela-file').on('show.bs.modal', function() {
+        relaFileLoader.showRelaFile(this.getAttribute('content'));
+    });
 
     // 授权相关
     class FilingPermission {

+ 41 - 0
app/public/js/shares/ali_oss.js

@@ -0,0 +1,41 @@
+const AliOss = (function (){
+    const downloadFile = function (url, filename) {
+        axios.get(url, {responseType: 'blob' }).then(res => {
+            saveAs(res.data, filename);
+        });
+    };
+
+    const downloadFileSync = function(url) {
+        return new Promise((resolve, reject) => {
+            axios.get(url, {responseType: 'blob'}).then(res => {
+                resolve(res.data);
+            }).catch(err => {
+                reject(err);
+            });
+        })
+    };
+
+    const zipFiles = function (files, filename = '打包.zip', successCallback, errorCallback) {
+        const zip = new JSZip();
+        const download = [], fails = [];
+        files.forEach(f => {
+            download.push(downloadFileSync(f.filepath).then(data => {
+                zip.file(f.filename + f.fileext, data, {binary: true});
+            }).catch(err => {
+                fails.push(f);
+            }));
+        });
+        Promise.all(download).then(() => {
+            if (fails.length < files.length) {
+                zip.generateAsync({ type: "blob" }).then(content => {
+                    saveAs(content, filename);
+                    successCallback && successCallback(fails);
+                });
+            } else {
+                errorCallback && errorCallback(fails);
+            }
+        })
+    };
+
+    return { downloadFile, downloadFileSync, zipFiles}
+})();

+ 5 - 0
app/router.js

@@ -724,6 +724,11 @@ module.exports = app => {
     app.post('/sp/:id/filing/save', sessionAuth, subProjectCheck, 'fileController.saveFiling');
     app.post('/sp/:id/filing/del', sessionAuth, subProjectCheck, 'fileController.delFiling');
     app.post('/sp/:id/file/load', sessionAuth, subProjectCheck, 'fileController.loadFile');
+    app.post('/sp/:id/file/upload', sessionAuth, subProjectCheck, 'fileController.uploadFile');
+    app.post('/sp/:id/file/del', sessionAuth, subProjectCheck, 'fileController.delFile');
+    app.post('/sp/:id/file/rela', sessionAuth, subProjectCheck, 'fileController.relaFile');
+    app.post('/sp/:id/file/rela/tender', sessionAuth, subProjectCheck, 'fileController.loadValidRelaTender');
+    app.post('/sp/:id/file/rela/files', sessionAuth, subProjectCheck, 'fileController.loadRelaFiles');
 
     // 支付审批
     app.get('/payment', sessionAuth, 'paymentController.index');

+ 84 - 17
app/service/file.js

@@ -25,30 +25,97 @@ module.exports = app => {
             this.tableName = 'file';
         }
 
-        async getFileList(spid, filing_id) {
-            const result = await this.getAllDataByCondition({
-                where: { spid, filing_id },
-                orders: [['create_time', 'asc']],
-                limit: this.app.config.pageSize, offset: (this.ctx.page - 1) * this.app.config.pageSize
-            });
+        analysisFiles(files) {
             const helper = this.ctx.helper;
-            result.forEach(x => {
-                x.viewpath = helper.canPreview(x.fileext) ? ctx.app.config.fujianOssPath + x.filepath : '';
+            const userId = this.ctx.session.sessionUser.accountId;
+            const ossPath = this.ctx.app.config.fujianOssPath;
+            files.forEach(x => {
+                x.filepath = ossPath + x.filepath;
+                x.viewpath = helper.canPreview(x.fileext) ? ossPath + x.filepath : '';
                 x.fileext_str = helper.fileExtStr(x.fileext);
+                x.canEdit = x.user_id === userId;
             });
+        }
+
+        async getFiles(condition) {
+            condition.orders = [['create_time', 'desc']];
+            const result = await this.getAllDataByCondition(condition);
+            this.analysisFiles(result);
+            return result;
+        }
+
+        async addFiles(filing, fileInfo, user) {
+            const conn = await this.db.beginTransaction();
+            const result = {};
+            try {
+                const insertData = fileInfo.map(x => {
+                    return {
+                        id: this.uuid.v4(), spid: filing.spid, filing_id: filing.id, filing_type: filing.filing_type,
+                        user_id: user.id, user_name: user.name, user_company: user.company, user_role: user.role,
+                        filename: x.filename, fileext: x.fileext, filesize: x.filesize, filepath: x.filepath,
+                    };
+                });
+                await conn.insert(this.tableName, insertData);
+                const count = await conn.count(this.tableName, { filing_id: filing.id, is_deleted: 0 });
+                await conn.update(this.ctx.service.filing.tableName, { id: filing.id, file_count: count });
+                await conn.commit();
+                result.files = { id: insertData.map(x => { return x.id; })};
+                result.filing = { id: filing.id, file_count: count };
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            result.files = await this.getFiles({ where: result.files });
             return result;
         }
 
-        async addFile(filing_id, fileInfo, user) {
-            const filing = await this.ctx.service.filing.getDataById(filing_id);
-            if (!filing || filing.is_delete) throw '当前分类不存在';
+        async delFiles(files) {
+            if (files.length === 0) return;
 
-            const insertData = {
-                spid: filing.spid, filing_id: filing.id, filing_type: filing.filing_type,
-                user_id: user.id, user_name: user.name, user_company: user.company, user_role: user.role,
-                filename: fileInfo.filename, fileext: fileInfo.fileext, filesize: fileInfo.fileSize, filepath: fileInfo.filepath,
-            };
-            const result = await this.db.insert(this.tableName, insertData);
+            const fileDatas = await this.getAllDataByCondition({ where: { id: files } });
+            const filing = await this.ctx.service.filing.getDataById(fileDatas[0].filing_id);
+            const result = {};
+
+            const conn = await this.db.beginTransaction();
+            try {
+                const updateData = fileDatas.map(x => { return { id: x.id, is_deleted: 1 }; });
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                const count = await conn.count(this.tableName, { filing_id: filing.id, is_deleted: 0 });
+                await conn.update(this.ctx.service.filing.tableName, { id: filing.id, file_count: count });
+                await conn.commit();
+                result.del = files;
+                result.filing = { id: filing.id, file_count: count };
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return result;
+        }
+
+        async relaFiles(filing, fileInfo, user) {
+            const conn = await this.db.beginTransaction();
+            const result = {};
+            try {
+                const insertData = fileInfo.map(x => {
+                    return {
+                        id: this.uuid.v4(), spid: filing.spid, filing_id: filing.id, filing_type: filing.filing_type,
+                        user_id: user.id, user_name: user.name, user_company: user.company, user_role: user.role,
+                        filename: x.filename, fileext: x.fileext, filesize: x.filesize, filepath: x.filepath,
+                        is_rela: 1, rela_info: x.rela_info,
+                    };
+                });
+                await conn.insert(this.tableName, insertData);
+                const count = await conn.count(this.tableName, { filing_id: filing.id, is_deleted: 0 });
+                await conn.update(this.ctx.service.filing.tableName, { id: filing.id, file_count: count });
+                await conn.commit();
+                result.files = { id: insertData.map(x => { return x.id; })};
+                result.filing = { id: filing.id, file_count: count };
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            result.files = await this.getFiles({ where: result.files });
+            return result;
         }
     }
 

+ 11 - 4
app/view/file/file.ejs

@@ -17,7 +17,7 @@
                         <% if (canFiling) { %>
                         <div class="p-2"><a href="javascript: void(0);" id="add-slibing">添加同级</a></div>
                         <div class="p-2"><a href="javascript: void(0);" id="add-child">添加子级</a></div>
-                        <div class="p-2"><a href="javascript: void(0);" id="del-filing" class="text-danger">删除</a></div>
+                        <div class="p-2"><a href="javascript: void(0);" id="del-filing-btn" class="text-danger">删除</a></div>
                         <% } else { %>
                         <div class="p-2 ml-2">分类目录</div>
                         <% } %>
@@ -30,10 +30,17 @@
                     <div class="d-flex flex-row">
                         <% if (canUpload) { %>
                         <div class="py-2 pr-2"><a href="#add-file" data-toggle="modal" data-target="#add-file">上传文件</a></div>
-                        <div class="p-2"><a href="javascript: void(0)" id="rela-file">引用文件</a></div>
-                        <div class="p-2"><a href="#del-batch-file" data-toggle="modal" data-target="#del-batch-file">批量删除</a></div>
+                        <div class="p-2"><a href="#rela-file" data-toggle="modal" data-target="#rela-file">导入文件</a></div>
+                        <div class="p-2"><a href="#batch-del-file" data-toggle="modal" data-target="#batch-del-file">批量删除</a></div>
                         <% } %>
                         <div class="p-2"><a href="javascript: void(0)" id="batch-download">批量下载</a></div>
+                        <div class="p-2">
+                            <span id="showPage">
+                                <a href="javascript:void(0);" class="page-select ml-3" content="pre"><i class="fa fa-chevron-left"></i></a>
+                                <span id="curPage">1</span>/<span id="curTotalPage">10</span>
+                                <a href="javascript:void(0);" class="page-select mr-3" content="next"><i class="fa fa-chevron-right"></i></a>
+                            </span>
+                        </div>
                     </div>
                     <table class="table table-bordered">
                         <thead>
@@ -48,7 +55,6 @@
                         <tbody id="file-list">
                         </tbody>
                     </table>
-                    <!--翻页-->
                 </div>
             </div>
         </div>
@@ -58,4 +64,5 @@
     const canFiling = <%- canFiling %>;
     const filing = JSON.parse(unescape('<%- escape(JSON.stringify(filing)) %>'));
     const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
+    const whiteList = JSON.parse('<%- JSON.stringify(ctx.app.config.multipart.whitelist) %>');
 </script>

+ 119 - 6
app/view/file/file_modal.ejs

@@ -29,12 +29,14 @@
                     </div>
                     <div class="col-8">
                         <div class="d-flex flex-row bg-graye">
-                            <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdown-up" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                                添加用户
-                            </button>
-                            <div class="dropdown-menu" aria-labelledby="dropdown-up" style="width:220px">
-                                <dl class="list-unstyled book-list" id="puList">
-                                </dl>
+                            <div class="p-2">
+                                <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdown-up" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                    添加用户
+                                </button>
+                                <div class="dropdown-menu" aria-labelledby="dropdown-up" style="width:220px">
+                                    <dl class="list-unstyled book-list" id="puList">
+                                    </dl>
+                                </div>
                             </div>
                             <div class="p-2"><a href="javascript: void(0);" id="batch-del-filing" class="text-danger">批量删除</a></div>
                             <div class="p-2"><a href="javascript: void(0);" id="sync-filing">同步授权至其他类别</a></div>
@@ -58,4 +60,115 @@
         </div>
     </div>
 </div>
+<% } %>
+<% if (canUpload) { %>
+<!--上传附件-->
+<div class="modal fade" id="add-file" 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="add-file-ok">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="batch-del-file" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">批量删除确认</h5>
+            </div>
+            <div class="modal-body">
+                <p>确认删除当前文件类别?</p>
+                <p>删除后,数据无法恢复,请谨慎操作。</p>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-danger" id="batch-del-file-ok">确定删除</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="rela-file" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog modal-lgx" role="document">
+        <div class="modal-content">
+            <div class="modal-header d-flex justify-content-between align-items-center">
+                <h5 class="modal-title">引用文件</h5>
+                <div id="rela-filing-hint">当前目录:项目管理文件/第一合同段/计量台账(自己新增的节点)</div>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-4">
+                        <div class="d-flex justify-content-center bg-graye">
+                            <div class="p-2">标段列表</div>
+                        </div>
+                        <div class="modal-height-400">
+                            <ul id="rela-tender" class="ztree"></ul>
+                        </div>
+                    </div>
+                    <div class="col-8">
+                        <div class="d-flex justify-content-between align-items-center">
+                            <div>
+                                <div class="d-flex flex-row">
+                                    <select class="form-control form-control-sm mr-2" id="tf-type" style="width: 100px">
+                                    </select>
+                                    <select class="form-control form-control-sm mr-2" id="tf-sub-type" style="width: 120px">
+                                    </select>
+                                    <select class="form-control form-control-sm" id="tf-stage" style="width: 80px">
+                                    </select>
+                                </div>
+                            </div>
+                            <div id="cur-tender-hint">当前标段,已选14个文件</div>
+                        </div>
+                        <div class="modal-height-400">
+                            <table class="table table-bordered mt-3">
+                                <thead>
+                                <tr><th>选择</th><th>文件名称</th><th>文件分类</th></tr>
+                                </thead>
+                                <tbody id="rf-files">
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer d-flex justify-content-between align-items-center">
+                <div id="rela-file-hint">已选1个标段,共14个文件</div>
+                <div>
+                    <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                    <button type="button" class="btn btn-sm btn-primary" id="rela-file-ok">确定</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if (canFiling) { %>
+<div class="modal fade" id="del-filing" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">删除确认</h5>
+            </div>
+            <div class="modal-body">
+                <p>如存在子类别,数据文件会一并删除,删除后,数据无法恢复,请谨慎操作。</p>
+                <h6>确认删除当前文件类别?</h6>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-danger" id="del-filing-ok">确定删除</button>
+            </div>
+        </div>
+    </div>
+</div>
 <% } %>

+ 2 - 0
config/web.js

@@ -1056,9 +1056,11 @@ const JsFiles = {
             },
             file: {
                 files: [
+                    '/public/js/axios/axios.min.js', '/public/js/file-saver/FileSaver.js', '/public/js/js-xlsx/jszip.min.js',
                     '/public/js/moment/moment.min.js', '/public/js/ztree/jquery.ztree.core.js', '/public/js/ztree/jquery.ztree.exedit.js',
                 ],
                 mergeFiles: [
+                    '/public/js/shares/ali_oss.js',
                     '/public/js/shares/drag_tree.js',
                     '/public/js/path_tree.js',
                     '/public/js/shares/tenders2tree.js',