Просмотр исходного кода

Merge branch 'dev' of http://192.168.1.41:3000/maixinrong/Calculation into dev

Tony Kang 1 неделя назад
Родитель
Сommit
59c40cd630

+ 26 - 30
app/controller/quality_controller.js

@@ -19,6 +19,7 @@ const sendToWormhole = require('stream-wormhole');
 const fs = require('fs');
 const path = require('path');
 const PermissionCheck = require('../const/account_permission').PermissionCheck;
+const InspectionAttachment = require('../lib/inspection_attachment');
 
 module.exports = app => {
     class QualityController extends app.BaseController {
@@ -609,7 +610,7 @@ module.exports = app => {
                 filter.count[filter.status.checkStop] = await ctx.service.qualityInspection.getCountByStatus(ctx.tender.id, filter.status.checkStop);// await ctx.service.change.pendingDatas(tender.id, ctx.session.sessionUser.accountId);
                 const inspectionList = await ctx.service.qualityInspection.getListByStatus(ctx.tender.id, status, 1, sorts, orders);
                 const total = await ctx.service.qualityInspection.getCountByStatus(ctx.tender.id, status);
-                const allAttList = inspectionList.length > 0 ? await ctx.service.qualityInspectionAtt.getAllAtt(ctx.tender.id, ctx.helper._.map(inspectionList, 'id')) : [];
+                const allAttList = inspectionList.length > 0 ? await new InspectionAttachment(ctx, 'quality').getFiles(ctx.helper._.map(inspectionList, 'id')) : [];
                 for (const c of inspectionList) {
                     c.attList = ctx.helper._.filter(allAttList, { qiid: c.id });
                     c.curAuditors = await ctx.service.qualityInspectionAudit.getAuditorsByStatus(c.id, c.status, c.times);
@@ -735,7 +736,7 @@ module.exports = app => {
                 const tender = await ctx.service.tender.getDataById(ctx.tender.id);
                 await this._getInspectionAuditViewData(ctx);
                 // 获取附件列表
-                const fileList = await ctx.service.qualityInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const fileList = await new InspectionAttachment(ctx, 'quality').getFiles(ctx.inspection.id);
                 // 获取用户人验证手机号
                 const renderData = {
                     moment,
@@ -898,26 +899,23 @@ module.exports = app => {
          */
         async uploadInspectionFile(ctx) {
             let stream;
+            const files = [];
+            let saved = false;
             try {
-                if (ctx.inspection.flow_version === 1 && !ctx.inspection.canUpload) throw '当前不可上传附件';
+                const attachment = new InspectionAttachment(ctx, 'quality');
+                if (!attachment.canUpload(ctx.inspection)) throw '当前不可上传附件';
                 const parts = this.ctx.multipart({
                     autoFields: true,
                 });
-                const files = [];
-                const create_time = Date.parse(new Date()) / 1000;
-                let idx = 0;
-                const extra_upload = ctx.inspection.status === auditConst.inspection.status.checked;
                 while ((stream = await parts()) !== undefined) {
                     if (!stream.filename) {
-                        // 如果没有传入直接返回
-                        return;
+                        throw '请选择附件';
                     }
                     const fileInfo = path.parse(stream.filename);
-                    const filepath = `app/public/upload/${this.ctx.tender.id.toString()}/quality_inspection/fujian_${create_time + idx.toString() + fileInfo.ext}`;
+                    const filepath = `app/public/upload/${this.ctx.tender.id.toString()}/quality_inspection/fujian_${ctx.app.uuid.v4()}${fileInfo.ext}`;
                     // await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, 'app', filepath));
                     await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
                     files.push({ filepath, name: stream.filename, ext: fileInfo.ext });
-                    ++idx;
                     stream && (await sendToWormhole(stream));
                 }
                 const in_time = new Date();
@@ -937,17 +935,27 @@ module.exports = app => {
                         filesize: ctx.helper.bytesToSize(idx === 'isString' ? parts.field.size : parts.field.size[idx]),
                         filepath: file.filepath,
                         upload_time: in_time,
-                        extra_upload,
                     };
                     return newFile;
                 });
                 // 执行文件信息写入数据库
-                await ctx.service.qualityInspectionAtt.saveFileMsgToDb(payload);
+                await attachment.saveFiles(payload, JSON.parse(parts.field.flowContext || 'null'));
+                saved = true;
                 // 将最新的当前标段的所有文件信息返回
-                const data = await ctx.service.qualityInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const data = await attachment.getFiles(ctx.inspection.id);
                 ctx.body = { err: 0, msg: '', data };
             } catch (err) {
                 stream && (await sendToWormhole(stream));
+                // 流程已变更或保存失败时,仅清理本次未入库的上传文件。
+                if (!saved) {
+                    for (const file of files) {
+                        try {
+                            await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + file.filepath);
+                        } catch (cleanupError) {
+                            this.log(cleanupError);
+                        }
+                    }
+                }
                 this.log(err);
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
@@ -959,22 +967,10 @@ module.exports = app => {
          */
         async deleteInspectionFile(ctx) {
             try {
-                const { id } = JSON.parse(ctx.request.body.data);
-                const fileInfo = await ctx.service.qualityInspectionAtt.getDataById(id);
-                if (ctx.inspection.flow_version === 1) {
-                    if (!fileInfo || fileInfo.qiid !== ctx.inspection.id || !ctx.inspection.canUpload || fileInfo.uid !== ctx.session.sessionUser.accountId) throw '您无权删除该附件';
-                    if (ctx.inspection.status === qualityInspectionConst.status.checked && !fileInfo.extra_upload && !PermissionCheck.delFile(ctx.session.sessionUser.permission)) throw '该附件不可删除';
-                }
-                if (fileInfo && Object.keys(fileInfo).length) {
-                    // 先删除文件
-                    // await fs.unlinkSync(path.resolve(this.app.baseDir, './app', fileInfo.filepath));
-                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
-                    // 再删除数据库
-                    await ctx.service.qualityInspectionAtt.delete(id);
-                } else {
-                    throw '不存在该文件';
-                }
-                const data = await ctx.service.qualityInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const { id, flowContext } = JSON.parse(ctx.request.body.data);
+                const attachment = new InspectionAttachment(ctx, 'quality');
+                await attachment.deleteFile(id, flowContext);
+                const data = await attachment.getFiles(ctx.inspection.id);
                 ctx.body = { err: 0, msg: '请求成功', data };
             } catch (err) {
                 this.log(err);

+ 26 - 30
app/controller/safe_controller.js

@@ -19,6 +19,7 @@ const sendToWormhole = require('stream-wormhole');
 const fs = require('fs');
 const path = require('path');
 const PermissionCheck = require('../const/account_permission').PermissionCheck;
+const InspectionAttachment = require('../lib/inspection_attachment');
 
 module.exports = app => {
     class SafeController extends app.BaseController {
@@ -669,7 +670,7 @@ module.exports = app => {
                 filter.count[filter.status.checkStop] = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, filter.status.checkStop);// await ctx.service.change.pendingDatas(tender.id, ctx.session.sessionUser.accountId);
                 const inspectionList = await ctx.service.safeInspection.getListByStatus(ctx.tender.id, status, 1, sorts, orders);
                 const total = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, status);
-                const allAttList = inspectionList.length > 0 ? await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.helper._.map(inspectionList, 'id')) : [];
+                const allAttList = inspectionList.length > 0 ? await new InspectionAttachment(ctx, 'safe').getFiles(ctx.helper._.map(inspectionList, 'id')) : [];
                 for (const c of inspectionList) {
                     c.attList = ctx.helper._.filter(allAttList, { qiid: c.id });
                     c.curAuditors = await ctx.service.safeInspectionAudit.getAuditorsByStatus(c.id, c.status, c.times);
@@ -804,7 +805,7 @@ module.exports = app => {
                 const tender = await ctx.service.tender.getDataById(ctx.tender.id);
                 await this._getInspectionAuditViewData(ctx);
                 // 获取附件列表
-                const fileList = await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const fileList = await new InspectionAttachment(ctx, 'safe').getFiles(ctx.inspection.id);
                 // 获取用户人验证手机号
                 const renderData = {
                     moment,
@@ -972,26 +973,23 @@ module.exports = app => {
          */
         async uploadInspectionFile(ctx) {
             let stream;
+            const files = [];
+            let saved = false;
             try {
-                if (ctx.inspection.flow_version === 1 && !ctx.inspection.canUpload) throw '当前不可上传附件';
+                const attachment = new InspectionAttachment(ctx, 'safe');
+                if (!attachment.canUpload(ctx.inspection)) throw '当前不可上传附件';
                 const parts = this.ctx.multipart({
                     autoFields: true,
                 });
-                const files = [];
-                const create_time = Date.parse(new Date()) / 1000;
-                let idx = 0;
-                const extra_upload = ctx.inspection.status === auditConst.inspection.status.checked;
                 while ((stream = await parts()) !== undefined) {
                     if (!stream.filename) {
-                        // 如果没有传入直接返回
-                        return;
+                        throw '请选择附件';
                     }
                     const fileInfo = path.parse(stream.filename);
-                    const filepath = `app/public/upload/${this.ctx.tender.id.toString()}/safe_inspection/fujian_${create_time + idx.toString() + fileInfo.ext}`;
+                    const filepath = `app/public/upload/${this.ctx.tender.id.toString()}/safe_inspection/fujian_${ctx.app.uuid.v4()}${fileInfo.ext}`;
                     // await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, 'app', filepath));
                     await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
                     files.push({ filepath, name: stream.filename, ext: fileInfo.ext });
-                    ++idx;
                     stream && (await sendToWormhole(stream));
                 }
                 const in_time = new Date();
@@ -1011,17 +1009,27 @@ module.exports = app => {
                         filesize: ctx.helper.bytesToSize(idx === 'isString' ? parts.field.size : parts.field.size[idx]),
                         filepath: file.filepath,
                         upload_time: in_time,
-                        extra_upload,
                     };
                     return newFile;
                 });
                 // 执行文件信息写入数据库
-                await ctx.service.safeInspectionAtt.saveFileMsgToDb(payload);
+                await attachment.saveFiles(payload, JSON.parse(parts.field.flowContext || 'null'));
+                saved = true;
                 // 将最新的当前标段的所有文件信息返回
-                const data = await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const data = await attachment.getFiles(ctx.inspection.id);
                 ctx.body = { err: 0, msg: '', data };
             } catch (err) {
                 stream && (await sendToWormhole(stream));
+                // 流程已变更或保存失败时,仅清理本次未入库的上传文件。
+                if (!saved) {
+                    for (const file of files) {
+                        try {
+                            await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + file.filepath);
+                        } catch (cleanupError) {
+                            this.log(cleanupError);
+                        }
+                    }
+                }
                 this.log(err);
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
@@ -1033,22 +1041,10 @@ module.exports = app => {
          */
         async deleteInspectionFile(ctx) {
             try {
-                const { id } = JSON.parse(ctx.request.body.data);
-                const fileInfo = await ctx.service.safeInspectionAtt.getDataById(id);
-                if (ctx.inspection.flow_version === 1) {
-                    if (!fileInfo || fileInfo.qiid !== ctx.inspection.id || !ctx.inspection.canUpload || fileInfo.uid !== ctx.session.sessionUser.accountId) throw '您无权删除该附件';
-                    if (ctx.inspection.status === safeInspectionConst.status.checked && !fileInfo.extra_upload && !PermissionCheck.delFile(ctx.session.sessionUser.permission)) throw '该附件不可删除';
-                }
-                if (fileInfo && Object.keys(fileInfo).length) {
-                    // 先删除文件
-                    // await fs.unlinkSync(path.resolve(this.app.baseDir, './app', fileInfo.filepath));
-                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
-                    // 再删除数据库
-                    await ctx.service.safeInspectionAtt.delete(id);
-                } else {
-                    throw '不存在该文件';
-                }
-                const data = await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const { id, flowContext } = JSON.parse(ctx.request.body.data);
+                const attachment = new InspectionAttachment(ctx, 'safe');
+                await attachment.deleteFile(id, flowContext);
+                const data = await attachment.getFiles(ctx.inspection.id);
                 ctx.body = { err: 0, msg: '请求成功', data };
             } catch (err) {
                 this.log(err);

+ 2 - 1
app/controller/wap_controller.js

@@ -26,6 +26,7 @@ const uuid = require('node-uuid');
 const nlsToken = require('../lib/nls_token');
 const streamToArray = require('stream-to-array');
 const PermissionCheck = require('../const/account_permission').PermissionCheck;
+const InspectionAttachment = require('../lib/inspection_attachment');
 
 module.exports = app => {
 
@@ -1315,7 +1316,7 @@ module.exports = app => {
                 const inspection = ctx.inspection;
                 await ctx.service[type + 'Inspection'].loadAuditViewData(ctx.inspection);
                 // 获取附件列表
-                const fileList = await ctx.service[type + 'InspectionAtt'].getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const fileList = await new InspectionAttachment(ctx, type).getFiles(ctx.inspection.id);
                 const renderData = {
                     moment,
                     tender,

+ 112 - 0
app/lib/inspection_attachment.js

@@ -0,0 +1,112 @@
+'use strict';
+
+const PermissionCheck = require('../const/account_permission').PermissionCheck;
+
+// 安全、质量巡检共用附件阶段及操作权限规则。
+class InspectionAttachment {
+    constructor(ctx, type) {
+        if (!['safe', 'quality'].includes(type)) throw '无效的巡检类型';
+        this.ctx = ctx;
+        this.inspectionService = ctx.service[type + 'Inspection'];
+        this.auditService = ctx.service[type + 'InspectionAudit'];
+        this.attachmentService = ctx.service[type + 'InspectionAtt'];
+    }
+
+    getFileStage(file) {
+        // 尚未回填的历史数据与数据库迁移后的规则保持一致。
+        return file.flow_stage === null || file.flow_stage === undefined || Number(file.flow_stage) === 0 ? 1 : Number(file.flow_stage);
+    }
+
+    getUploadStage(inspection) {
+        if (inspection.flow_version === 1) return [1, 2, 3].includes(inspection.flow_stage) ? inspection.flow_stage : 0;
+        // 旧流程只有上报、整改,完成后的补传归入整改阶段。
+        if ([3, 12].includes(inspection.status)) return 2;
+        return [1, 2, 5, 6].includes(inspection.status) ? 1 : 0;
+    }
+
+    canUpload(inspection, records) {
+        if (!inspection || inspection.status === 4 || !this.getUploadStage(inspection)) return false;
+        if (inspection.flow_version !== 1) return Boolean(inspection.filePermission);
+        // 页面和上传入口沿用中间件已计算的权限;事务内传入最新审批记录后重新校验。
+        if (!records && typeof inspection.canUpload === 'boolean') return inspection.canUpload;
+        const uid = this.ctx.session.sessionUser.accountId;
+        if ([1, 3, 5].includes(inspection.status)) return inspection.uid === uid;
+        const current = (records || inspection.auditors || []).filter(record => !record.is_old && record.times === inspection.times
+            && record.flow_stage === inspection.flow_stage && record.stage_round === inspection.stage_round);
+        if (inspection.flow_stage === 2) {
+            current.sort((a, b) => a.audit_order - b.audit_order || a.order - b.order || a.id - b.id);
+            const active = current.find(record => [1, 12].includes(record.status));
+            return inspection.status === 12 && Boolean(active && active.aid === uid && active.status === 12);
+        }
+        return [2, 6].includes(inspection.status) && current.some(record => record.aid === uid && record.status === 2);
+    }
+
+    canDeleteFile(file, inspection, records) {
+        if (!file || !inspection || file.tid !== inspection.tid || file.qiid !== inspection.id) return false;
+        if (file.uid !== this.ctx.session.sessionUser.accountId || !this.canUpload(inspection, records)) return false;
+        if (this.getFileStage(file) !== this.getUploadStage(inspection)) return false;
+        return inspection.status !== 3 || Boolean(file.extra_upload) || PermissionCheck.delFile(this.ctx.session.sessionUser.permission);
+    }
+
+    async getFiles(qiid) {
+        if (this.ctx.inspection) this.ctx.inspection.canUploadAttachments = this.canUpload(this.ctx.inspection);
+        const files = await this.attachmentService.getAllAtt(this.ctx.tender.id, qiid);
+        return files.map(file => Object.assign(file, {
+            flow_stage: this.getFileStage(file),
+            stageName: ['', '上报', '整改', '批复'][this.getFileStage(file)] || '未知阶段',
+            canDelete: this.canDeleteFile(file, this.ctx.inspection),
+        }));
+    }
+
+    async saveFiles(files, flowContext) {
+        const transaction = await this.attachmentService.beginTransaction();
+        try {
+            const { current } = await this.getCurrentContext(transaction, flowContext);
+            if (!files.length) throw '请选择附件';
+            const payload = files.map(file => Object.assign({}, file, {
+                tid: current.tid, qiid: current.id, uid: this.ctx.session.sessionUser.accountId,
+                flow_stage: this.getUploadStage(current), extra_upload: current.status === 3,
+            }));
+            const result = await this.attachmentService.saveFileMsgToDb(payload, transaction);
+            await transaction.commit();
+            return result;
+        } catch (err) {
+            await transaction.rollback();
+            throw err;
+        }
+    }
+
+    async deleteFile(id, flowContext) {
+        id = Number(id);
+        if (!Number.isSafeInteger(id) || id <= 0) throw '无效的附件';
+        const transaction = await this.attachmentService.beginTransaction();
+        try {
+            const { current, records } = await this.getCurrentContext(transaction, flowContext);
+            const file = await this.attachmentService.getMaterialFileById(id, transaction);
+            if (!this.canDeleteFile(file, current, records)) throw '只能删除本人在当前阶段上传的附件';
+            await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + file.filepath);
+            await this.attachmentService.delete(id, transaction);
+            await transaction.commit();
+        } catch (err) {
+            await transaction.rollback();
+            throw err;
+        }
+    }
+
+    async getCurrentContext(transaction, flowContext) {
+        const { ctx } = this;
+        const rows = await this.inspectionService.getAllDataForUpdate({ id: ctx.inspection.id, tid: ctx.tender.id }, transaction);
+        const current = rows[0];
+        if (!current) throw '巡检不存在';
+        const fields = ['flow_version', 'flow_stage', 'stage_round', 'times', 'status'];
+        if (!flowContext || fields.some(key => Number(flowContext[key]) !== Number(current[key]) || Number(ctx.inspection[key]) !== Number(current[key]))) {
+            throw '流程已更新,请刷新后操作附件';
+        }
+        current.filePermission = ctx.inspection.filePermission;
+        const records = await this.auditService.getAllDataByCondition({ where: { qiid: current.id, times: current.times, is_old: 0 } }, transaction);
+        if (!this.canUpload(current, records)) throw '当前不可操作附件';
+        return { current, records };
+    }
+}
+
+module.exports = InspectionAttachment;

+ 36 - 0
app/lib/project_screen.js

@@ -0,0 +1,36 @@
+'use strict';
+
+module.exports = {
+    getProgressPaths(rows) {
+        const nodes = new Map(rows.map(row => [String(row.tree_id), row]));
+        const paths = new Map();
+        for (const row of rows) {
+            const path = [];
+            const visited = new Set();
+            let node = row;
+            while (node && !visited.has(String(node.tree_id))) {
+                path.unshift(node);
+                visited.add(String(node.tree_id));
+                node = nodes.get(String(node.tree_pid));
+            }
+            paths.set(row.id, path);
+        }
+        return paths;
+    },
+    sortProgressRows(rows, paths) {
+        return rows.slice().sort((a, b) => {
+            const pathA = paths.get(a.id);
+            const pathB = paths.get(b.id);
+            for (let i = 0; i < Math.min(pathA.length, pathB.length); i++) {
+                if (pathA[i].id === pathB[i].id) continue;
+                return pathA[i].tree_order - pathB[i].tree_order || String(pathA[i].id).localeCompare(String(pathB[i].id));
+            }
+            return pathA.length - pathB.length;
+        });
+    },
+    formatProgressPath(path, abbreviated = false) {
+        const names = path.map(node => node.name);
+        if (abbreviated && names.length > 2) return [names[0], '...', names[names.length - 1]].join(' / ');
+        return names.join(' / ');
+    },
+};

+ 59 - 20
app/public/css/main.css

@@ -2820,10 +2820,6 @@ animation:shake 1s .2s ease both;}
     .gy-header .gy-brand,.gy-header .gy-brand>div{min-width:0;max-width:100%}
     .gy-header .gy-brand strong{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
 }
-.gy-finished-page{display:grid;grid-template-rows:minmax(230px,1fr) minmax(550px,2.4fr);gap:12px;flex:1;min-height:804px}
-.gy-finished-layout{display:grid;grid-template-columns:minmax(0,4fr) minmax(0,6fr);gap:12px;min-height:0}
-.gy-finished-detail{display:grid;grid-template-rows:minmax(230px,1fr) minmax(270px,1.2fr);gap:12px;min-width:0;min-height:0}
-@media(max-width:1000px){.gy-finished-page{flex:none;grid-template-rows:260px auto}.gy-finished-layout{grid-template-columns:1fr}.gy-finished-detail{grid-template-rows:auto 300px}}
 .gy-header-actions{display:flex;align-items:center;gap:12px}
 .gy-header-actions .project-screen-switcher>button{color:#c9d4df;border-color:#58616a;padding:9px 14px;font-size:13px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
 .gy-header-actions .dropdown-menu{background:#343a40;border-color:#58616a;max-height:60vh;overflow:auto;min-width:180px;max-width:320px}
@@ -2836,8 +2832,6 @@ animation:shake 1s .2s ease both;}
 .gy-change-table td{text-align:right!important}
 .gy-change-table td:first-child{text-align:left!important;white-space:normal}
 .gy-change-negative{color:#ffc16b}
-#gy-building-page .gy-planned-detail{grid-template-rows:minmax(210px,1fr) minmax(270px,1.2fr) minmax(260px,1fr)}
-@media(max-width:1000px){#gy-building-page .gy-planned-detail{grid-template-rows:auto 300px 300px}}
 .gy-stage-dialog .modal-body{scrollbar-width:thin;scrollbar-color:#626e7a #343a40}
 .gy-stage-dialog .modal-body::-webkit-scrollbar{width:8px;height:8px}
 .gy-stage-dialog .modal-body::-webkit-scrollbar-track,.gy-stage-dialog .modal-body::-webkit-scrollbar-corner{background:#343a40}
@@ -2851,13 +2845,14 @@ animation:shake 1s .2s ease both;}
 .gy-stage-dialog #gy-dialog-project{font-size:16px;font-weight:400;line-height:1.6;color:#c4d4e5;overflow-wrap:anywhere}
 .gy-stage-dialog .gy-table thead th{text-align:center!important}
 .gy-stage-dialog .gy-table tbody td{text-align:center!important}
-.gy-stage-dialog .gy-table thead th:first-child,.gy-stage-dialog .gy-table tbody td:first-child{text-align:left!important}
+.gy-stage-dialog .gy-table tbody td:nth-child(2){text-align:left!important}
 .gy-stage-dialog .gy-stage-group .gy-table{table-layout:fixed}
-.gy-stage-dialog .gy-stage-group th:first-child{width:46%}
-.gy-stage-dialog .gy-stage-group th:nth-child(2){width:14%}
-.gy-stage-dialog .gy-stage-group th:nth-child(3){width:18%}
-.gy-stage-dialog .gy-stage-group th:last-child{width:22%}
-.gy-stage-dialog .gy-stage-group td:first-child,.gy-stage-dialog .gy-stage-group td:last-child{white-space:normal;overflow-wrap:anywhere}
+.gy-stage-dialog .gy-stage-group th:first-child{width:10%}
+.gy-stage-dialog .gy-stage-group th:nth-child(2){width:38%}
+.gy-stage-dialog .gy-stage-group th:nth-child(3){width:14%}
+.gy-stage-dialog .gy-stage-group th:nth-child(4){width:18%}
+.gy-stage-dialog .gy-stage-group th:last-child{width:20%}
+.gy-stage-dialog .gy-stage-group td:nth-child(2),.gy-stage-dialog .gy-stage-group td:last-child{white-space:normal;overflow-wrap:anywhere}
 .gy-stage-dialog .close:focus:not(:focus-visible){outline:none;box-shadow:none}
 .gy-stage-dialog .gy-stage-group{--gy-stage-accent:#67b7ff;border:1px solid #52616e;border-radius:6px;overflow:auto}
 .gy-stage-dialog .gy-stage-group + .gy-stage-group{margin-top:24px}
@@ -2872,7 +2867,8 @@ animation:shake 1s .2s ease both;}
 .gy-stage-dialog .gy-stage-state.gy-status-2{background:rgba(170,182,195,.12)}
 .gy-stage-dialog .gy-stage-group .gy-table tbody tr:last-child td{border-bottom:0}
 .gy-planned-page{display:grid;grid-template-columns:minmax(0,4fr) minmax(0,6fr);gap:12px;flex:1;min-height:804px}
-.gy-planned-detail{display:grid;grid-template-rows:minmax(210px,1fr) minmax(270px,1.2fr) minmax(max-content,1fr);gap:12px;min-width:0;min-height:0}
+.gy-planned-detail{display:grid;grid-template-rows:minmax(210px,1fr) minmax(270px,1.2fr) minmax(280px,1fr);gap:12px;min-width:0;min-height:0}
+.gy-planned-detail>.gy-card>.gy-title{height:30px;flex-shrink:0}
 .gy-detail-top{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,3fr);gap:12px;min-height:0}
 .gy-detail-text{font-size:14px;line-height:2;background:#303b47;padding:16px;overflow:auto;overflow-wrap:anywhere}
 .gy-detail-text strong{display:block;color:#84c4ff;margin-bottom:12px}
@@ -2891,7 +2887,7 @@ animation:shake 1s .2s ease both;}
 .gy-detail-button{background:transparent;border:1px solid #56718b;border-radius:4px;color:#8ac8ff;padding:5px 10px;font-size:12px!important}
 .gy-stage-table{table-layout:fixed}.gy-stage-table td,.gy-stage-table th{text-align:center!important}.gy-stage-table button{border:0;background:none;color:inherit;padding:4px 12px;text-decoration:underline;text-underline-offset:4px}.gy-status-0{color:#67d6a2}.gy-status-1{color:#ffc16b}.gy-status-2{color:#aab6c3}
 .gy-stage-dialog .modal-dialog{width:90vw;max-width:900px}.gy-stage-dialog .modal-content{background:#343a40;color:#edf3fa;border:1px solid #627589;border-radius:6px}.gy-stage-dialog .modal-header{border-bottom-color:#48515a}.gy-stage-dialog .modal-body{max-height:70vh;overflow:auto}.gy-stage-dialog .close{color:#edf3fa;text-shadow:none}.gy-stage-dialog p{color:#aab6c3}.gy-stage-dialog table{min-width:600px}.gy-empty{padding:24px;text-align:center;color:#aab6c3}
-@media(max-width:1000px){.gy-planned-page{grid-template-columns:1fr;flex:none}.gy-project-list{height:350px}.gy-planned-detail{grid-template-rows:auto 300px auto}.gy-detail-top{min-height:250px}}
+@media(max-width:1000px){.gy-planned-page{grid-template-columns:1fr;flex:none}.gy-project-list{height:350px}.gy-planned-detail{grid-template-rows:auto 300px 300px}.gy-detail-top{grid-auto-rows:270px;min-height:250px}}
 @media(max-width:650px){.gy-detail-top{grid-template-columns:1fr}.gy-detail-text{max-height:240px}}
 .gy-grid{flex:1 0 auto}
 .gy-screen button:focus:not(:focus-visible){outline:none;box-shadow:none}
@@ -3750,23 +3746,23 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
 
 .quality-inspection-attachments .modal-dialog,
 .safe-inspection-attachments .modal-dialog {
-    width: 900px;
+    width: max-content;
     max-width: calc(100% - 32px);
     margin-right: auto;
     margin-left: auto;
 }
 .quality-inspection-attachments table,
 .safe-inspection-attachments table {
-    width: 100%;
-    max-width: 100%;
-    table-layout: fixed;
+    width: auto;
+    min-width: 100%;
+    max-width: none;
+    table-layout: auto;
 }
 .quality-inspection-attachments th,
 .quality-inspection-attachments td,
 .safe-inspection-attachments th,
 .safe-inspection-attachments td {
-    white-space: normal;
-    overflow-wrap: break-word;
+    white-space: nowrap;
 }
 
 .quality-inspection-menu .nav-list a,
@@ -3821,3 +3817,46 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
     top: 0;
     height: 100%;
 }
+
+.inspection-detail-attachments {
+    max-width: 100%;
+    overflow-x: auto;
+}
+.inspection-detail-attachments-table {
+    width: 100%;
+    min-width: 600px;
+    margin-bottom: 0;
+    table-layout: fixed;
+}
+.inspection-detail-attachments-table .attachment-col-number {
+    width: 5%;
+}
+.inspection-detail-attachments-table .attachment-col-name {
+    width: auto;
+}
+.inspection-detail-attachments-table .attachment-col-stage,
+.inspection-detail-attachments-table .attachment-col-user,
+.inspection-detail-attachments-table .attachment-col-actions {
+    width: 10%;
+}
+.inspection-detail-attachments-table .attachment-col-time {
+    width: 15%;
+}
+.inspection-detail-attachments-table th,
+.inspection-detail-attachments-table td {
+    overflow-wrap: break-word;
+}
+.inspection-detail-attachments-table thead th:nth-child(n+3),
+.inspection-detail-attachments-table .inspection-attachment-row td:nth-child(n+3) {
+    text-align: center;
+}
+.inspection-detail-attachments-table .inspection-attachment-name {
+    min-width: 300px;
+    text-align: left;
+}
+.inspection-detail-attachments-table .inspection-attachment-name a {
+    display: block;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}

Разница между файлами не показана из-за своего большого размера
+ 3 - 3
app/public/js/project_screen_guangyuan.js


Разница между файлами не показана из-за своего большого размера
+ 2 - 2
app/public/js/project_screen_guangyuan_building.js


Разница между файлами не показана из-за своего большого размера
+ 5 - 5
app/public/js/project_screen_guangyuan_finished.js


Разница между файлами не показана из-за своего большого размера
+ 3 - 3
app/public/js/project_screen_guangyuan_planned.js


+ 4 - 3
app/public/js/project_screen_setting.js

@@ -91,11 +91,11 @@ $(function() {
         const screen = selectedScreen();
         if (!screen) return;
         const remaining = screens.filter(x => x.id !== screen.id);
-        let hint = '删除后该大屏配置会被移除,已有业务数据不会受到影响。';
+        let hint = '移除后,已有业务数据不会受到影响。';
         if (parseInt(screen.is_default) === 1 && remaining.length) {
             hint += ` “${remaining[0].name}”将自动成为默认大屏。`;
         } else if (parseInt(screen.is_default) === 1) {
-            hint += ' 这是最后一个大屏,删除后左侧项目大屏入口将隐藏。';
+            hint += ' 这是最后一个大屏,移除后左侧项目大屏入口将隐藏。';
         }
         $('#project-screen-delete-name').text(screen.name);
         $('#project-screen-delete-hint').text(hint);
@@ -176,7 +176,7 @@ $(function() {
             selectedId = screens.length ? String(screens[0].id) : '';
             renderScreens();
             $('#project-screen-delete-modal').modal('hide');
-            toastr.success('大屏删除成功');
+            toastr.success('大屏移除成功');
         });
     });
 
@@ -200,6 +200,7 @@ $(function() {
             const adminIcon = account.is_admin ? ' <span class="ml-1" title="管理员"><i class="fa fa-user-circle-o"></i></span>' : '';
             permissionOriginal[account.id] = authorized;
             html.push(`<tr data-id="${account.id}"><td>${escapeHtml(account.account)}${adminIcon}</td><td>${escapeHtml(account.name)}</td><td>${escapeHtml(account.company)}</td><td>${escapeHtml(account.role)}</td>`);
+            html.push(`<td class="text-nowrap">${escapeHtml(account.mobile)}</td>`);
             html.push(`<td><input type="checkbox" class="project-screen-permission-check" ${checked} ${disabled}></td></tr>`);
         });
         $('#project-screen-permission-body').html(html.join(''));

+ 1 - 1
app/public/js/quality_inspection.js

@@ -321,7 +321,7 @@ $(document).ready(() => {
         $('#file-content').empty();
         let html = '';
         files.forEach((file, idx) => {
-            html += `<tr><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td class="text-center">${file.username}</td><td class="text-center">${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td class="text-center"><a href="/sp/${spid}/quality/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download"><i class="fa fa-download"></i></a></td></tr>`
+            html += `<tr><td class="text-center">${file.stageName || '上报'}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td class="text-center">${file.username}</td><td class="text-center">${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td class="text-center"><a href="/sp/${spid}/quality/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download"><i class="fa fa-download"></i></a></td></tr>`
         })
         $('#file-content').append(html);
     }

+ 19 - 21
app/public/js/quality_inspection_information.js

@@ -616,6 +616,7 @@ $(document).ready(function () {
         if (validateFiles(valiData)) {
             if (files.length) {
                 const formData = new FormData();
+                formData.append('flowContext', JSON.stringify(getQualityInspectionAttachmentContext()));
                 files.forEach(file => {
                     formData.append('name', file.name);
                     formData.append('size', file.size);
@@ -631,30 +632,22 @@ $(document).ready(function () {
     })
     function handleFileList(files = []) {
         $('#file-content').empty();
-        const newFiles = files.map(file => {
-            let showDel = false;
-            if (file.uid === cur_uid) {
-                if (inspection.status === auditConst.status.checked) {
-                    showDel = Boolean(file.extra_upload ) || deleteFilePermission
-                } else {
-                    showDel = true
-                }
-            }
-            if (inspection.flow_version === 1 && !inspection.canUpload) showDel = false;
-            return {...file, showDel}
-        })
+        const canUploadAttachments = typeof inspection.canUploadAttachments === 'boolean'
+            ? inspection.canUploadAttachments
+            : Boolean(inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission);
+        const newFiles = files.map(file => ({...file, showDel: file.canDelete === true}));
         const is_wap = window.location.pathname.indexOf('/wap/') !== -1;
         if (is_wap) {
             let html = `<div style="display: flex;">
             <b>附件</b>`;
-                if (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission) {
+                if (canUploadAttachments) {
                     html += `<span style="margin-left: auto;"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></span>`;
                 }
             html += `</div><div>`;
             newFiles.forEach((file, idx) => {
                 html += `<a href="${file.filepath}" target="_blank">
                     <div class="card my-1" style="cursor: pointer">
-                        <div class="card-body"><i class="fa fa-file"></i> ${file.filename}<small class="pull-right">${file.showDel ? `<span class="badge">
+                        <div class="card-body"><span class="badge badge-light mr-1">${file.stageName || '上报'}</span><i class="fa fa-file"></i> ${file.filename}<small class="pull-right">${file.showDel ? `<span class="badge">
                                     <span class="dropdown">
                                 <a href="javascript:void(0)" class="btn-sm text-danger px-1" title="移除" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-remove"></i></a>
                                 <div class="dropdown-menu">
@@ -674,13 +667,11 @@ $(document).ready(function () {
             html += '</div>';
             $('#file-content').append(html);
         } else {
-            let html = (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission) ? `<tr><td colspan="5"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></td></tr>` : '';
+            let html = canUploadAttachments ? `<tr><td colspan="6"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></td></tr>` : '';
             newFiles.forEach((file, idx) => {
-                if (file.showDel) {
-                    html += `<tr><td>${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a><a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a></td></tr>`
-                } else {
-                    html += `<tr><td width="70">${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a></td></tr>`
-                }
+                const filename = _.escape(file.filename || '');
+                const deleteLink = file.showDel ? `<a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a>` : '';
+                html += `<tr class="inspection-attachment-row"><td>${idx + 1}</td><td class="inspection-attachment-name"><a href="${file.filepath}" target="_blank" title="${filename}">${filename}</a></td><td>${file.stageName || '上报'}</td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a>${deleteLink}</td></tr>`;
             });
             $('#file-content').append(html);
         }
@@ -689,7 +680,7 @@ $(document).ready(function () {
     $('#file-content').on('click', 'a', function () {
         if ($(this).hasClass('file-del')) {
             const id = $(this).data('id');
-            postData(`${preUrl}/file/delete`, {id}, (result) => {
+            postData(`${preUrl}/file/delete`, {id, flowContext: getQualityInspectionAttachmentContext()}, (result) => {
                 handleFileList(result);
             })
         }
@@ -1101,3 +1092,10 @@ function bindQualityInspectionRectificationAutosave(readData) {
         });
     }
 }
+
+function getQualityInspectionAttachmentContext() {
+    return {
+        flow_version: inspection.flow_version, flow_stage: inspection.flow_stage,
+        stage_round: inspection.stage_round, times: inspection.times, status: inspection.status
+    };
+}

+ 1 - 1
app/public/js/safe_inspection.js

@@ -321,7 +321,7 @@ $(document).ready(() => {
         $('#file-content').empty();
         let html = '';
         files.forEach((file, idx) => {
-            html += `<tr><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td class="text-center">${file.username}</td><td class="text-center">${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td class="text-center"><a href="/sp/${spid}/safe/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download"><i class="fa fa-download"></i></a></td></tr>`
+            html += `<tr><td class="text-center">${file.stageName || '上报'}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td class="text-center">${file.username}</td><td class="text-center">${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td class="text-center"><a href="/sp/${spid}/safe/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download"><i class="fa fa-download"></i></a></td></tr>`
         })
         $('#file-content').append(html);
     }

+ 19 - 21
app/public/js/safe_inspection_information.js

@@ -616,6 +616,7 @@ $(document).ready(function () {
         if (validateFiles(valiData)) {
             if (files.length) {
                 const formData = new FormData();
+                formData.append('flowContext', JSON.stringify(getSafeInspectionAttachmentContext()));
                 files.forEach(file => {
                     formData.append('name', file.name);
                     formData.append('size', file.size);
@@ -631,30 +632,22 @@ $(document).ready(function () {
     })
     function handleFileList(files = []) {
         $('#file-content').empty();
-        const newFiles = files.map(file => {
-            let showDel = false;
-            if (file.uid === cur_uid) {
-                if (inspection.status === auditConst.status.checked) {
-                    showDel = Boolean(file.extra_upload ) || deleteFilePermission
-                } else {
-                    showDel = true
-                }
-            }
-            if (inspection.flow_version === 1 && !inspection.canUpload) showDel = false;
-            return {...file, showDel}
-        })
+        const canUploadAttachments = typeof inspection.canUploadAttachments === 'boolean'
+            ? inspection.canUploadAttachments
+            : Boolean(inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission);
+        const newFiles = files.map(file => ({...file, showDel: file.canDelete === true}));
         const is_wap = window.location.pathname.indexOf('/wap/') !== -1;
         if (is_wap) {
             let html = `<div style="display: flex;">
             <b>附件</b>`;
-                if (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission) {
+                if (canUploadAttachments) {
                     html += `<span style="margin-left: auto;"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></span>`;
                 }
             html += `</div><div>`;
             newFiles.forEach((file, idx) => {
                 html += `<a href="${file.filepath}" target="_blank">
                     <div class="card my-1" style="cursor: pointer">
-                        <div class="card-body"><i class="fa fa-file"></i> ${file.filename}<small class="pull-right">${file.showDel ? `<span class="badge">
+                        <div class="card-body"><span class="badge badge-light mr-1">${file.stageName || '上报'}</span><i class="fa fa-file"></i> ${file.filename}<small class="pull-right">${file.showDel ? `<span class="badge">
                                     <span class="dropdown">
                                 <a href="javascript:void(0)" class="btn-sm text-danger px-1" title="移除" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-remove"></i></a>
                                 <div class="dropdown-menu">
@@ -674,13 +667,11 @@ $(document).ready(function () {
             html += '</div>';
             $('#file-content').append(html);
         } else {
-            let html = (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission) ? `<tr><td colspan="5"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></td></tr>` : '';
+            let html = canUploadAttachments ? `<tr><td colspan="6"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></td></tr>` : '';
             newFiles.forEach((file, idx) => {
-                if (file.showDel) {
-                    html += `<tr><td>${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a><a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a></td></tr>`
-                } else {
-                    html += `<tr><td width="70">${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a></td></tr>`
-                }
+                const filename = _.escape(file.filename || '');
+                const deleteLink = file.showDel ? `<a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a>` : '';
+                html += `<tr class="inspection-attachment-row"><td>${idx + 1}</td><td class="inspection-attachment-name"><a href="${file.filepath}" target="_blank" title="${filename}">${filename}</a></td><td>${file.stageName || '上报'}</td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a>${deleteLink}</td></tr>`;
             });
             $('#file-content').append(html);
         }
@@ -689,7 +680,7 @@ $(document).ready(function () {
     $('#file-content').on('click', 'a', function () {
         if ($(this).hasClass('file-del')) {
             const id = $(this).data('id');
-            postData(`${preUrl}/file/delete`, {id}, (result) => {
+            postData(`${preUrl}/file/delete`, {id, flowContext: getSafeInspectionAttachmentContext()}, (result) => {
                 handleFileList(result);
             })
         }
@@ -1101,3 +1092,10 @@ function bindSafeInspectionRectificationAutosave(readData) {
         });
     }
 }
+
+function getSafeInspectionAttachmentContext() {
+    return {
+        flow_version: inspection.flow_version, flow_stage: inspection.flow_stage,
+        stage_round: inspection.stage_round, times: inspection.times, status: inspection.status
+    };
+}

+ 24 - 19
app/service/project_screen.js

@@ -1,6 +1,7 @@
 'use strict';
 
 const projectScreenConst = require('../const/project_screen');
+const projectScreenLib = require('../lib/project_screen');
 const Ledger = require('../lib/ledger');
 
 module.exports = app => {
@@ -180,7 +181,7 @@ module.exports = app => {
             }
             await ctx.service.tenderCache.loadTenderCaches(allTenders, user.accountId);
             const allChanges = !options.summary && status === '在建' && allTenders.length ? await ctx.service.change.getAllCheckedChanges(allTenders.map(t => t.id)) : [];
-            const allProgress = !options.summary && selectedNodes.length && status !== '已建' ? await ctx.service.subProjProgress.getAllDataByCondition({ where: { spid: selectedNodes.map(n => n.id) }, orders: [['update_time', 'desc'], ['id', 'asc']] }) : [];
+            const allProgress = !options.summary && selectedNodes.length ? await ctx.service.subProjProgress.getAllDataByCondition({ where: { spid: selectedNodes.map(n => n.id) }, orders: [['update_time', 'desc'], ['id', 'asc']] }) : [];
             for (const node of selectedNodes) {
                 const info = infos.find(i => i.id === node.id);
                 if (!info) continue;
@@ -225,13 +226,15 @@ module.exports = app => {
                 }
                 const parent = nodes.find(n => n.id === node.tree_pid);
                 const progressRows = allProgress.filter(row => row.spid === node.id);
+                const progressPaths = projectScreenLib.getProgressPaths(progressRows);
                 const latest = progressRows[0];
                 const keys = ['edit', 'submit', 'reply'];
                 const latestEvent = latest && keys.map((key, index) => ({ date: latest[key + '_date'], department: latest[key + '_department'], step: ['编制', '报审', '批复'][index] }))
                     .filter(event => event.date).sort((a, b) => String(b.date).localeCompare(String(a.date)))[0];
-                const stages = progressRows.filter(row => row.tree_level > 1 && row.tree_is_leaf === 1).map(row => ({
-                    id: row.id, name: row.name,
-                    path: String(row.tree_full_path).split('-').map(id => progressRows.find(p => String(p.tree_id) === id)).filter(Boolean).map(p => p.name).join(' / ') || row.name,
+                const stages = projectScreenLib.sortProgressRows(progressRows, progressPaths).filter(row => row.tree_level > 1 && row.tree_is_leaf === 1).map(row => ({
+                    id: row.id, name: row.name, code: row.code || '',
+                    path: projectScreenLib.formatProgressPath(progressPaths.get(row.id)),
+                    shortPath: projectScreenLib.formatProgressPath(progressPaths.get(row.id), true),
                     states: keys.map(key => row[key + '_progress'] === '已完成' ? 0 : row[key + '_progress'] === '进行中' ? 1 : 2),
                     dates: keys.map(key => row[key + '_date'] || ''), departments: keys.map(key => row[key + '_department'] || '')
                 }));
@@ -261,21 +264,23 @@ module.exports = app => {
             }) : [];
             const dateText = value => value instanceof Date ? [value.getFullYear(), String(value.getMonth() + 1).padStart(2, '0'), String(value.getDate()).padStart(2, '0')].join('-') : String(value || '').slice(0, 10);
             const trees = new Map(projects.map(p => [p.id, rows.filter(row => row.spid === p.id)]));
-            const leafColumns = tree => tree.filter(row => Number(row.tree_level) > 1 && Number(row.tree_is_leaf) === 1)
-                .sort((a, b) => a.tree_order - b.tree_order).map(row => {
-                    const path = String(row.tree_full_path).split('-').map(id => tree.find(n => String(n.tree_id) === id)).filter(Boolean);
-                    if (!path.length || path[path.length - 1].id !== row.id) path.push(row);
-                    return { key: JSON.stringify([path[0].name, row.name]), root: path[0].name,
-                        rootOrder: Number(path[0].tree_order) || 0,
-                        orderPath: path.slice(1).map(n => Number(n.tree_order) || 0),
-                        name: row.name, date: dateText(row.reply_date) };
-                }).sort((a, b) => {
-                    if (a.root !== b.root) return a.rootOrder - b.rootOrder || a.root.localeCompare(b.root, 'zh-CN');
-                    for (let i = 0; i < Math.min(a.orderPath.length, b.orderPath.length); i++) {
-                        if (a.orderPath[i] !== b.orderPath[i]) return a.orderPath[i] - b.orderPath[i];
-                    }
-                    return a.orderPath.length - b.orderPath.length;
-                });
+            const leafColumns = tree => {
+                const paths = projectScreenLib.getProgressPaths(tree);
+                return tree.filter(row => Number(row.tree_level) > 1 && Number(row.tree_is_leaf) === 1)
+                    .sort((a, b) => a.tree_order - b.tree_order).map(row => {
+                        const path = paths.get(row.id);
+                        return { key: JSON.stringify([path[0].name, row.name]), root: path[0].name,
+                            rootOrder: Number(path[0].tree_order) || 0,
+                            orderPath: path.slice(1).map(n => Number(n.tree_order) || 0),
+                            name: row.name, date: dateText(row.reply_date) };
+                    }).sort((a, b) => {
+                        if (a.root !== b.root) return a.rootOrder - b.rootOrder || a.root.localeCompare(b.root, 'zh-CN');
+                        for (let i = 0; i < Math.min(a.orderPath.length, b.orderPath.length); i++) {
+                            if (a.orderPath[i] !== b.orderPath[i]) return a.orderPath[i] - b.orderPath[i];
+                        }
+                        return a.orderPath.length - b.orderPath.length;
+                    });
+            };
             const columnsByProject = new Map(projects.map(p => [p.id, leafColumns(trees.get(p.id))]));
             const approvedKeys = new Set([].concat(...Array.from(columnsByProject.values())).filter(column => column.date).map(column => column.key));
             // Merge same leaf names under the same root, regardless of intermediate paths.

+ 10 - 6
app/service/quality_inspection_att.js

@@ -55,8 +55,9 @@ module.exports = app => {
          * @param {Array} payload 载荷
          * @return {Promise<void>} 数据库插入执行实例
          */
-        async saveFileMsgToDb(payload) {
-            return await this.db.insert(this.tableName, payload);
+        async saveFileMsgToDb(payload, transaction = false) {
+            const conn = transaction || this.db;
+            return await conn.insert(this.tableName, payload);
         }
 
         /**
@@ -64,8 +65,9 @@ module.exports = app => {
          * @param {Number} id 文件id
          * @return {Promise<void>} 数据库查询实例
          */
-        async getMaterialFileById(id) {
-            return await this.getDataByCondition({ id });
+        async getMaterialFileById(id, transaction = false) {
+            const conn = transaction || this.db;
+            return await conn.get(this.tableName, { id });
         }
 
         /**
@@ -73,8 +75,10 @@ module.exports = app => {
          * @param {Number} id - 附件id
          * @return {void}
          */
-        async delete(id) {
-            return await this.deleteById(id);
+        async delete(id, transaction = false) {
+            const conn = transaction || this.db;
+            const result = await conn.delete(this.tableName, { id });
+            return result.affectedRows > 0;
         }
     }
     return QualityInspectionAtt;

+ 10 - 6
app/service/safe_inspection_att.js

@@ -55,8 +55,9 @@ module.exports = app => {
          * @param {Array} payload 载荷
          * @return {Promise<void>} 数据库插入执行实例
          */
-        async saveFileMsgToDb(payload) {
-            return await this.db.insert(this.tableName, payload);
+        async saveFileMsgToDb(payload, transaction = false) {
+            const conn = transaction || this.db;
+            return await conn.insert(this.tableName, payload);
         }
 
         /**
@@ -64,8 +65,9 @@ module.exports = app => {
          * @param {Number} id 文件id
          * @return {Promise<void>} 数据库查询实例
          */
-        async getMaterialFileById(id) {
-            return await this.getDataByCondition({ id });
+        async getMaterialFileById(id, transaction = false) {
+            const conn = transaction || this.db;
+            return await conn.get(this.tableName, { id });
         }
 
         /**
@@ -73,8 +75,10 @@ module.exports = app => {
          * @param {Number} id - 附件id
          * @return {void}
          */
-        async delete(id) {
-            return await this.deleteById(id);
+        async delete(id, transaction = false) {
+            const conn = transaction || this.db;
+            const result = await conn.delete(this.tableName, { id });
+            return result.affectedRows > 0;
         }
     }
     return SafeInspectionAtt;

+ 8 - 10
app/view/project_screen/guangyuan_finished.ejs

@@ -1,14 +1,12 @@
 <script id="gy-finished-data" type="application/json"><%- JSON.stringify(finishedProjects).replace(/</g, '\\u003c') %></script>
-<div id="gy-finished-page" class="gy-finished-page" hidden>
-    <section class="gy-card"><div class="gy-title"><h2>项目完工投资</h2><span>单位:万元</span></div><div id="gy-finished-investment-chart" class="gy-chart"></div></section>
-    <div class="gy-finished-layout">
-        <section class="gy-card gy-project-list"><div class="gy-title"><h2>项目列表</h2></div><div class="gy-list-heading">项目名称</div><div id="gy-finished-tree" class="gy-scroll"></div></section>
-        <div class="gy-finished-detail">
-            <div class="gy-detail-top">
-                <section class="gy-card"><div class="gy-title"><h2>项目进度</h2></div><div id="gy-finished-progress" class="gy-detail-text"></div></section>
-                <section class="gy-card"><div class="gy-title"><h2>项目概况</h2></div><div id="gy-finished-intro" class="gy-detail-text gy-scroll"></div></section>
-            </div>
-            <section class="gy-card"><div class="gy-title"><h2>全过程造价趋势</h2><span>单位:万元</span></div><div id="gy-finished-cost-chart" class="gy-chart"></div></section>
+<div id="gy-finished-page" class="gy-planned-page" hidden>
+    <section class="gy-card gy-project-list"><div class="gy-title"><h2>项目列表</h2></div><div class="gy-list-heading">项目名称</div><div id="gy-finished-tree" class="gy-scroll"></div></section>
+    <div class="gy-planned-detail">
+        <div class="gy-detail-top">
+            <section class="gy-card"><div class="gy-title"><h2>项目进度</h2></div><div id="gy-finished-progress" class="gy-detail-text"></div></section>
+            <section class="gy-card"><div class="gy-title"><h2>项目概况</h2></div><div id="gy-finished-intro" class="gy-detail-text gy-scroll"></div></section>
         </div>
+        <section class="gy-card"><div class="gy-title"><h2>全过程造价趋势</h2><span>单位:万元</span></div><div id="gy-finished-cost-chart" class="gy-chart"></div></section>
+        <section class="gy-card"><div class="gy-title"><h2>项目完工投资</h2><span>单位:万元</span></div><div id="gy-finished-investment-chart" class="gy-chart"></div></section>
     </div>
 </div>

+ 24 - 13
app/view/quality/inspection_information.ejs

@@ -198,19 +198,30 @@
                     </div>
                     <% } %>
                     <% } %>
-                    <table class="table table-bordered mt-3">
-                        <thead>
-                        <tr>
-                            <th></th>
-                            <th class="text-center">附件</th>
-                            <th class="text-center">上传者</th>
-                            <th class="text-center" width="200">上传时间</th>
-                            <th class="text-center">操作</th>
-                        </tr>
-                        </thead>
-                        <tbody id="file-content">
-                        </tbody>
-                    </table>
+                    <div class="inspection-detail-attachments mt-3 mb-3">
+                        <table class="table table-bordered inspection-detail-attachments-table">
+                            <colgroup>
+                                <col class="attachment-col-number">
+                                <col class="attachment-col-name">
+                                <col class="attachment-col-stage">
+                                <col class="attachment-col-user">
+                                <col class="attachment-col-time">
+                                <col class="attachment-col-actions">
+                            </colgroup>
+                            <thead>
+                            <tr>
+                                <th>序号</th>
+                                <th class="text-center">附件</th>
+                                <th>阶段</th>
+                                <th>上传者</th>
+                                <th>上传时间</th>
+                                <th>操作</th>
+                            </tr>
+                            </thead>
+                            <tbody id="file-content">
+                            </tbody>
+                        </table>
+                    </div>
                     <% if (inspection.flow_version === 1) { %>
                         <% include ./inspection_flow.ejs %>
                     <% } else { %>

+ 1 - 1
app/view/quality/inspection_modal.ejs

@@ -34,7 +34,7 @@
                 <div class="modal-height-500" style="overflow: auto;">
                     <table class="table table-sm table-bordered">
                         <thead class="text-center">
-                        <tr><th width="45%">文件名</th><th width="15%">上传人</th><th width="25%">上传时间</th><th width="15%">操作</th></tr>
+                        <tr><th>阶段</th><th>文件名</th><th>上传人</th><th>上传时间</th><th>操作</th></tr>
                         </thead>
                         <tbody id="file-content"></tbody>
                     </table>

+ 24 - 13
app/view/safe/inspection_information.ejs

@@ -198,19 +198,30 @@
                     </div>
                     <% } %>
                     <% } %>
-                    <table class="table table-bordered mt-3">
-                        <thead>
-                        <tr>
-                            <th></th>
-                            <th class="text-center">附件</th>
-                            <th class="text-center">上传者</th>
-                            <th class="text-center" width="200">上传时间</th>
-                            <th class="text-center">操作</th>
-                        </tr>
-                        </thead>
-                        <tbody id="file-content">
-                        </tbody>
-                    </table>
+                    <div class="inspection-detail-attachments mt-3 mb-3">
+                        <table class="table table-bordered inspection-detail-attachments-table">
+                            <colgroup>
+                                <col class="attachment-col-number">
+                                <col class="attachment-col-name">
+                                <col class="attachment-col-stage">
+                                <col class="attachment-col-user">
+                                <col class="attachment-col-time">
+                                <col class="attachment-col-actions">
+                            </colgroup>
+                            <thead>
+                            <tr>
+                                <th>序号</th>
+                                <th class="text-center">附件</th>
+                                <th>阶段</th>
+                                <th>上传者</th>
+                                <th>上传时间</th>
+                                <th>操作</th>
+                            </tr>
+                            </thead>
+                            <tbody id="file-content">
+                            </tbody>
+                        </table>
+                    </div>
                     <% if (inspection.flow_version === 1) { %>
                         <% include ./inspection_flow.ejs %>
                     <% } else { %>

+ 1 - 1
app/view/safe/inspection_modal.ejs

@@ -34,7 +34,7 @@
                 <div class="modal-height-500" style="overflow: auto;">
                     <table class="table table-sm table-bordered">
                         <thead class="text-center">
-                        <tr><th width="45%">文件名</th><th width="15%">上传人</th><th width="25%">上传时间</th><th width="15%">操作</th></tr>
+                        <tr><th>阶段</th><th>文件名</th><th>上传人</th><th>上传时间</th><th>操作</th></tr>
                         </thead>
                         <tbody id="file-content"></tbody>
                     </table>

+ 2 - 1
app/view/setting/module.ejs

@@ -62,7 +62,7 @@
                                                 <span id="project-screen-selected-name">大屏预览</span>
                                                 <div class="ml-auto">
                                                     <button type="button" class="btn btn-outline-primary btn-sm" id="project-screen-default">设为默认</button>
-                                                    <button type="button" class="btn btn-outline-danger btn-sm ml-1" id="project-screen-delete">删除</button>
+                                                    <button type="button" class="btn btn-outline-danger btn-sm ml-1" id="project-screen-delete">移除</button>
                                                 </div>
                                             </div>
                                             <div class="project-screen-preview-stage d-flex align-items-center justify-content-center overflow-auto">
@@ -100,6 +100,7 @@
                                                     <th>用户</th>
                                                     <th>公司</th>
                                                     <th>职位</th>
+                                                    <th width="140" class="text-nowrap">手机号</th>
                                                     <th width="90">查看 <input type="checkbox" class="ml-2 align-middle" id="project-screen-permission-all"></th>
                                                 </tr>
                                                 </thead>

+ 3 - 3
app/view/setting/module_modal.ejs

@@ -20,15 +20,15 @@
     <div class="modal-dialog" role="document">
         <div class="modal-content">
             <div class="modal-header">
-                <h5 class="modal-title">删除大屏</h5>
+                <h5 class="modal-title">移除大屏</h5>
             </div>
             <div class="modal-body">
-                <p class="mb-1">确定删除“<strong id="project-screen-delete-name"></strong>”吗?</p>
+                <p class="mb-1">确定移除“<strong id="project-screen-delete-name"></strong>”吗?</p>
                 <p class="text-muted mb-0" id="project-screen-delete-hint"></p>
             </div>
             <div class="modal-footer">
                 <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
-                <button type="button" class="btn btn-danger btn-sm" id="project-screen-delete-confirm">确认删除</button>
+                <button type="button" class="btn btn-danger btn-sm" id="project-screen-delete-confirm">确认移除</button>
             </div>
         </div>
     </div>

+ 2 - 2
app/view/wap/shenpi_inspection.ejs

@@ -117,13 +117,13 @@
                 <div class="mt-3" id="file-content">
                     <div style="display: flex;">
                         <b>附件</b>
-                        <% if (inspection.filePermission) { %><span style="margin-left: auto;"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></span><% } %>
+                        <% if (typeof inspection.canUploadAttachments === 'boolean' ? inspection.canUploadAttachments : (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission)) { %><span style="margin-left: auto;"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></span><% } %>
                     </div>
                     <div>
                         <% for (const file of fileList) { %>
                             <a href="<% if (file.viewpath) { %><%- file.viewpath %><% } else { %>/wap/message/download/file/<%- file.id %><% } %>" target="_blank">
                                 <div class="card my-1" style="cursor: pointer">
-                                    <div class="card-body"><i class="fa fa-file"></i> <%- file.filename %><small class="pull-right text-secondary"><%- file.filesize %></small></div>
+                                    <div class="card-body"><span class="badge badge-light mr-1"><%= file.stageName || '上报' %></span><i class="fa fa-file"></i> <%- file.filename %><small class="pull-right text-secondary"><%- file.filesize %></small></div>
                                 </div>
                             </a>
                         <% } %>

+ 11 - 0
sql/update.sql

@@ -196,3 +196,14 @@ ALTER TABLE `zh_quality_inspection_audit`
     (`qiid`, `times`, `flow_stage`, `stage_round`, `is_old`, `status`),
   ADD INDEX `idx_quality_audit_todo`
     (`aid`, `status`, `qiid`, `times`);
+
+-- 安全、质量巡检附件阶段:flow_stage 字段已添加,本段不重复新增字段。
+-- 历史未标记附件默认归入上报,保留已标记的整改、批复附件;本段可重复执行。
+
+ALTER TABLE `zh_safe_inspection_attachment`
+  MODIFY COLUMN `flow_stage` tinyint unsigned NOT NULL DEFAULT 1
+    COMMENT '附件上传阶段:1上报(含历史附件),2整改,3批复';
+
+ALTER TABLE `zh_quality_inspection_attachment`
+  MODIFY COLUMN `flow_stage` tinyint unsigned NOT NULL DEFAULT 1
+    COMMENT '附件上传阶段:1上报(含历史附件),2整改,3批复';