Przeglądaj źródła

奖罚金,依据材料证明,改为附件形式

MaiXinRong 5 lat temu
rodzic
commit
271424b0ff

+ 125 - 3
app/controller/stage_extra_controller.js

@@ -8,6 +8,10 @@
  * @version
  */
 const auditConst = require('../const/audit').stage;
+const sendToWormhole = require('stream-wormhole');
+const path = require('path');
+const moment = require('moment');
+const fs = require('fs');
 
 module.exports = app => {
 
@@ -88,7 +92,7 @@ module.exports = app => {
             try {
                 const renderData = {
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.stageExtra.bonus)
-                }
+                };
                 await this.layout('stage_extra/bonus.ejs', renderData, 'stage_extra/bonus_modal.ejs');
             } catch (err) {
                 ctx.helper.log(err);
@@ -103,9 +107,13 @@ module.exports = app => {
         async loadBonus (ctx) {
             try {
                 const data = await ctx.service.stageBonus.getStageData(ctx.stage.id);
-                console.log(data);
+                for (const d of data) {
+                    for (const pf of d.proof_file) {
+                        delete pf.filepath;
+                        pf.username = (await ctx.service.projectAccount.getAccountInfoById(pf.uid)).name;
+                    }
+                }
                 const preData = await ctx.service.stageBonus.getPreStageData(ctx.stage.order);
-                console.log(preData);
                 ctx.body = {err: 0, msg: '', data: data.concat(preData)};
             } catch (error) {
                 ctx.helper.log(error);
@@ -182,6 +190,120 @@ module.exports = app => {
                 ctx.body = this.ajaxErrorBody(error, '提交数据失败,请重试');
             }
         }
+
+        async uploadFile(ctx) {
+            let stream;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+                let index = 0;
+                const create_time = Date.parse(new Date()) / 1000;
+                let bonus;
+                while ((stream = await parts()) !== undefined) {
+                    if (!stream.filename) {
+                        throw '未发现上传文件!';
+                    }
+                    if (!bonus) bonus = await ctx.service.stageBonus.getStageDataById(parts.field.bonus_id);
+                    const fileInfo = path.parse(stream.filename);
+                    const dirName = 'app/public/upload/extra/' + moment().format('YYYYMMDD');
+                    const fileName = create_time + '_' + index + fileInfo.ext;
+
+                    // 保存文件
+                    await ctx.helper.saveStreamFile(stream, path.join(this.app.baseDir, dirName, fileName));
+                    await sendToWormhole(stream);
+
+                    // 插入到stage_pay对应的附件列表中
+                    bonus.proof_file.push({
+                        filename: fileInfo.name,
+                        fileext: fileInfo.ext,
+                        filesize: Array.isArray(parts.field.size) ? parts.field.size[index] : parts.field.size,
+                        filepath: path.join(dirName, fileName),
+                        uid: ctx.session.sessionUser.accountId,
+                        in_time: moment(create_time * 1000).format('YYYY-MM-DD'),
+                    });
+                    ++index;
+                }
+                const result = await ctx.service.stageBonus.updateDatas({
+                    update: [
+                        { id: bonus.id, proof_file: bonus.proof_file },
+                    ]
+                });
+                for (const pf of bonus.proof_file) {
+                    delete pf.filepath;
+                    pf.username = (await ctx.service.projectAccount.getAccountInfoById(pf.uid)).name;
+                }
+                ctx.body = {err: 0, msg: '', data: bonus.proof_file};
+            } catch (error) {
+                ctx.helper.log(error);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                ctx.body = this.ajaxErrorBody(error, '上传附件失败,请重试');
+            }
+        }
+
+        async downloadFile(ctx) {
+            const b_id = ctx.query.b_id;
+            const index = ctx.query.index;
+            if (b_id && index) {
+                try {
+                    const bonus = await ctx.service.stageBonus.getStageDataById(b_id);
+
+                    if (!bonus || !bonus.proof_file || !bonus.proof_file[index]) throw '下载的文件不存在';
+
+                    const fileInfo = bonus.proof_file[index];
+                    const fileName = path.join(this.app.baseDir, fileInfo.filepath);
+                    // 解决中文无法下载问题
+                    const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                    let disposition = '';
+                    if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                        disposition = 'attachment; filename=' + encodeURIComponent(fileInfo.filename + fileInfo.fileext);
+                    } else if (userAgent.indexOf('firefox') >= 0) {
+                        disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(fileInfo.filename + fileInfo.fileext) + '"';
+                    } else {
+                        /* safari等其他非主流浏览器只能自求多福了 */
+                        disposition = 'attachment; filename=' + new Buffer(fileInfo.filename + fileInfo.fileext).toString('binary');
+                    }
+                    ctx.response.set({
+                        'Content-Type': 'application/octet-stream',
+                        'Content-Disposition': disposition,
+                        'Content-Length': fileInfo.filesize,
+                    });
+                    ctx.body = await fs.createReadStream(fileName);
+                } catch (err) {
+                    this.log(err);
+                    this.postError(err, '下载文件失败');
+                }
+            }
+        }
+
+        async deleteFile(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+
+                const bonus = await ctx.service.stageBonus.getStageDataById(data.b_id);
+                if (!bonus || !bonus.proof_file || !bonus.proof_file[data.index]) throw '删除的文件不存在'
+
+                const fileInfo = bonus.proof_file[data.index];
+                // 先删除文件
+                await fs.unlinkSync(path.join(this.app.baseDir, fileInfo.filepath));
+                // 再删除数据库
+                bonus.proof_file.splice(data.index, 1);
+                const result = await ctx.service.stageBonus.updateDatas({
+                    update: [
+                        { id: bonus.id, proof_file: bonus.proof_file },
+                    ]
+                });
+                for (const pf of bonus.proof_file) {
+                    delete pf.filepath;
+                    pf.username = (await ctx.service.projectAccount.getAccountInfoById(pf.uid)).name;
+                }
+                ctx.body = {err: 0, msg: '', data: bonus.proof_file};
+            } catch (err) {
+                this.log(err);
+                this.postError(err, '删除文件失败');
+            }
+        }
     }
 
     return StageExtraController;

+ 68 - 25
app/public/js/se_bonus.js

@@ -14,31 +14,6 @@ const isPre = function (data) {
 $(document).ready(() => {
     autoFlashHeight();
 
-    const fileObj = {
-        generateFilesHtml(data) {
-            const id = data.id, files = data.proof_file;
-            let html = [];
-            if (files !== null && files !== undefined) {
-                for (const [i, f] of files.entries()) {
-                    html.push('<tr>');
-                    html.push('<td style="width: 200px">', f.filename + f.fileext, '</td>');
-                    html.push('<td>', f.username, '</td>');
-                    html.push('<td>', f.in_time, '</td>');
-                    html.push('<td>');
-                    // 下载
-                    html.push('<a href="' + window.location.pathname + '/file/download?id=' + id + '&index=' + i + ' title="下载><i class="fa fa-download "></i></a>');
-                    // 删除
-                    if (!readOnly && uploadPermission && !isPre(data)) {
-                        html.push('<a class="delete-att text-danger" href="javascript:void(0);" data-id ="' + id + '"file-index="' + i + '" title="删除"><i class="fa fa-remove "></i></a>');
-                    }
-                    html.push('</td>');
-                    html.push('</tr>');
-                }
-            }
-            $('#file-list').html(html.join(''));
-        },
-    };
-
     let datepicker;
     const spreadSetting = {
         cols: [
@@ -151,6 +126,72 @@ $(document).ready(() => {
         }
     });
 
+    const fileObj = {
+        generateFilesHtml(data) {
+            const id = data.id, files = data.proof_file;
+            let html = [];
+            if (files !== null && files !== undefined) {
+                for (const [i, f] of files.entries()) {
+                    html.push('<tr>');
+                    html.push('<td style="width: 200px">', f.filename + f.fileext, '</td>');
+                    html.push('<td>', f.username, '</td>');
+                    html.push('<td>', f.in_time, '</td>');
+                    html.push('<td>');
+                    // 下载
+                    html.push('<a href="download/file?b_id=' + id + '&index=' + i + '" title="下载"><i class="fa fa-download "></i></a>');
+                    // 删除
+                    if (!readOnly && !isPre(data)) {
+                        html.push('<a class="delete-att text-danger ml-1" href="javascript:void(0);" data-id ="' + id + '"file-index="' + i + '" title="删除"><i class="fa fa-remove "></i></a>');
+                    }
+                    html.push('</td>');
+                    html.push('</tr>');
+                }
+            }
+            $('#file-list').html(html.join(''));
+        },
+        uploadFile() {
+            const files = this.files;
+            const select = SpreadJsObj.getSelectObject(bonusSheet);
+            const formData = new FormData();
+            formData.append('bonus_id', select.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('upload/file', formData, function (data) {
+                select.proof_file = data;
+                fileObj.generateFilesHtml(select);
+                SpreadJsObj.reLoadNodesData(bonusSheet, [select]);
+            });
+            $(this).val('');
+        },
+        deleteFile() {
+            const data = {
+                b_id: $(this).attr('data-id'),
+                index: parseInt($(this).attr('file-index')),
+            };
+            postData('delete/file', data, function (data) {
+                const select = SpreadJsObj.getSelectObject(bonusSheet);
+                select.proof_file = data;
+                fileObj.generateFilesHtml(select);
+                SpreadJsObj.reLoadNodesData(bonusSheet, [select]);
+            });
+        }
+    };
+
     class Bonus {
         constructor () {
             this.data = [];
@@ -502,4 +543,6 @@ $(document).ready(() => {
     $('#exportExcel').click(function () {
         SpreadExcelObj.exportSimpleXlsxSheet(spreadSetting, bonusObj.data, $('h2')[0].innerHTML + "-奖罚金.xlsx");
     });
+    $('#upload-file').change(fileObj.uploadFile);
+    $('body').on('click', '.delete-att', fileObj.deleteFile);
 });

+ 3 - 0
app/router.js

@@ -220,6 +220,9 @@ module.exports = app => {
     app.get('/tender/:id/measure/stage/:order/extra/other', sessionAuth, tenderCheck, stageCheck, 'stageExtraController.other');
     app.post('/tender/:id/measure/stage/:order/extra/other/load', sessionAuth, tenderCheck, stageCheck, 'stageExtraController.loadOther');
     app.post('/tender/:id/measure/stage/:order/extra/other/update', sessionAuth, tenderCheck, stageCheck, 'stageExtraController.updateOther');
+    app.post('/tender/:id/measure/stage/:order/extra/upload/file', sessionAuth, tenderCheck, stageCheck, 'stageExtraController.uploadFile');
+    app.get('/tender/:id/measure/stage/:order/extra/download/file', sessionAuth, tenderCheck, stageCheck, 'stageExtraController.downloadFile');
+    app.post('/tender/:id/measure/stage/:order/extra/delete/file', sessionAuth, tenderCheck, stageCheck, 'stageExtraController.deleteFile');
 
     // 报表
     app.get('/tender/:id/report', sessionAuth, tenderCheck, 'reportController.index');

+ 22 - 0
app/service/stage_bonus.js

@@ -22,6 +22,18 @@ module.exports = app => {
             this.tableName = 'stage_bonus';
         }
 
+        _parseData(data) {
+            if (!data) return;
+            const datas = data instanceof Array ? data : [data];
+            for (const d of datas) {
+                if (d.proof_file) {
+                    d.proof_file = JSON.parse(d.proof_file);
+                } else {
+                    d.proof_file = [];
+                }
+            }
+        }
+
         async getStageData(sid) {
             const data = await this.getAllDataByCondition({where: { sid: sid }});
             if (this.ctx.stage && this.ctx.stage.readOnly && this.ctx.stage.status !== auditConst.status.checked) {
@@ -33,6 +45,7 @@ module.exports = app => {
                     d.tp = h ? h.tp : null;
                 }
             }
+            this._parseData(data);
             return data;
         }
 
@@ -40,6 +53,7 @@ module.exports = app => {
             const sql = 'SELECT * From ' + this.tableName + ' WHERE sorder < ? And tid = ?';
             const sqlParam = [sorder, this.ctx.tender.id];
             const data = await this.db.query(sql, sqlParam);
+            this._parseData(data);
             return data;
         }
 
@@ -47,9 +61,16 @@ module.exports = app => {
             const sql = 'SELECT * From ' + this.tableName + ' WHERE sorder <= ? And tid = ? ORDER BY `sorder`, `order`';
             const sqlParam = [sorder, this.ctx.tender.id];
             const data = await this.db.query(sql, sqlParam);
+            this._parseData(data);
             return data;
         }
 
+        async getStageDataById(bonusId) {
+            const data = await this.getAllDataByCondition({ where: { sid: this.ctx.stage.id, id: bonusId } });
+            this._parseData(data);
+            return data[0];
+        }
+
         async _addDatas(data) {
             const datas = data instanceof Array ? data : [data];
             const insertData = [];
@@ -105,6 +126,7 @@ module.exports = app => {
                 if (d.tp !== undefined) nd.tp = this.ctx.helper.round(d.tp, this.ctx.tender.info.decimal.tp);
                 if (d.code !== undefined) nd.code = d.code;
                 if (d.proof !== undefined) nd.proof = d.proof;
+                if (d.proof_file !== undefined) nd.proof_file = JSON.stringify(d.proof_file);
                 if (d.real_time !== undefined) nd.real_time = new Date(d.real_time);
                 if (d.memo !== undefined) nd.memo = d.memo;
                 if (d.order !== undefined) nd.order = d.order;

+ 1 - 0
app/view/stage_extra/bonus.ejs

@@ -34,4 +34,5 @@
     const stageId = <%- ctx.stage.id %>;
     const stageUserId = <%- ctx.stage.user_id %>;
     const readOnly = <%- ctx.stage.readOnly %>;
+    const whiteList = JSON.parse('<%- JSON.stringify(ctx.app.config.multipart.whitelist) %>');
 </script>

+ 3 - 1
app/view/stage_extra/bonus_modal.ejs

@@ -6,10 +6,12 @@
                 <h5 class="modal-title">附件</h5>
             </div>
             <div class="modal-body">
+                <% if (!ctx.stage.readOnly) { %>
                 <div class="form-group">
                     <label for="formGroupExampleInput">大小限制:30MB,支持<span data-toggle="tooltip" data-placement="bottom" title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="rar,zip">压缩包格式</span></label>
-                    <input type="file" class="" id="upload-file" multiple>
+                    <input type="file" class="" id="upload-file" multiple onclick="file">
                 </div>
+                <% } %>
                 <div class="modal-height-500" style="overflow:auto;">
                     <table class="table table-sm table-bordered" style="word-break:break-all; table-layout: fixed">
                         <thead>

+ 1 - 1
config/config.default.js

@@ -57,7 +57,7 @@ module.exports = appInfo => {
     config.static = {
         maxAge: 0,
         buffer: false,
-    }
+    };
 
     // 分页相关
     config.pageSize = 15;

+ 3 - 0
sql/update.sql

@@ -36,3 +36,6 @@ ADD COLUMN `cid`  int(11) NOT NULL DEFAULT -1 COMMENT '所属报表组' AFTER `r
 
 ALTER TABLE `zh_rpt_tpl`
 ADD COLUMN `rpt_d_type`  tinyint(1) NOT NULL DEFAULT 0 COMMENT '报表,数据类型' AFTER `rpt_c_type`;
+
+ALTER TABLE `zh_stage_bonus`
+ADD COLUMN `proof_file`  text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '材料依据证明,文件' AFTER `proof`;