Forráskód Böngészése

安全巡检,web语音助手接入阿里云提交

ellisran 2 hete
szülő
commit
f5dcfa9fd5
41 módosított fájl, 4954 hozzáadás és 142 törlés
  1. 8 0
      app/base/base_controller.js
  2. 2 1
      app/const/audit.js
  3. 3 0
      app/const/code_rule.js
  4. 2 0
      app/const/shenpi.js
  5. 3 0
      app/const/sp_page_show.js
  6. 3 0
      app/controller/change_controller.js
  7. 6 5
      app/controller/quality_controller.js
  8. 2 1
      app/controller/report_archive_controller.js
  9. 504 0
      app/controller/safe_controller.js
  10. 4 3
      app/controller/sub_proj_setting_controller.js
  11. 37 0
      app/controller/wap_controller.js
  12. 84 1
      app/lib/nls_token.js
  13. 1 1
      app/middleware/inspection_check.js
  14. 126 0
      app/middleware/safe_inspection_check.js
  15. 349 0
      app/public/js/safe_inspection.js
  16. 631 0
      app/public/js/safe_inspection_information.js
  17. 58 0
      app/public/js/safe_tender.js
  18. 50 60
      app/public/js/setting_manage.js
  19. 10 0
      app/public/js/wap/global.js
  20. 19 6
      app/router.js
  21. 315 0
      app/service/safe_inspection.js
  22. 82 0
      app/service/safe_inspection_att.js
  23. 1185 0
      app/service/safe_inspection_audit.js
  24. 6 0
      app/service/tender_permission.js
  25. 1 1
      app/view/quality/inspection_modal.ejs
  26. 111 0
      app/view/safe/inspection.ejs
  27. 468 0
      app/view/safe/inspection_information.ejs
  28. 248 0
      app/view/safe/inspection_information_modal.ejs
  29. 166 0
      app/view/safe/inspection_modal.ejs
  30. 5 0
      app/view/safe/sub_memu_list.ejs
  31. 14 0
      app/view/safe/sub_menu.ejs
  32. 16 0
      app/view/safe/sub_mini_menu.ejs
  33. 25 0
      app/view/safe/tender.ejs
  34. 1 0
      app/view/safe/tender_modal.ejs
  35. 5 5
      app/view/shares/tender_permission_modal.ejs
  36. 29 4
      app/view/sp_setting/manage.ejs
  37. 245 53
      app/view/wap/inspection.ejs
  38. 23 0
      config/menu.js
  39. 52 0
      config/web.js
  40. 1 0
      package.json
  41. 54 1
      sql/update.sql

+ 8 - 0
app/base/base_controller.js

@@ -55,6 +55,14 @@ class BaseController extends Controller {
                                 child.url = `/sp/${ctx.subProject.id}/contract/tender`;
                             }
                         }
+                    } else if (index === 'safe') {
+                        for (const child of im.children) {
+                            if (child.msg === 'payment') {
+                                // child.url = `/sp/${ctx.subProject.id}/contract/panel`;
+                            } else if (child.msg === 'inspection') {
+                                child.url = `/sp/${ctx.subProject.id}/safe/inspection`;
+                            }
+                        }
                     } else if (index === 'financial') {
                         im.url = `/sp/${ctx.subProject.id}/${im.controller}/${ctx.subProject.financialToUrl}`;
                     } else if (index === 'inspection') {

+ 2 - 1
app/const/audit.js

@@ -1379,7 +1379,7 @@ const financial = (function() {
     return { status, statusString, statusClass, auditString, auditStringClass, auditProgress, auditProgressClass, filter, statusButton, statusButtonClass };
 })();
 
-// 质量巡检
+// 质量巡检 & 安全巡检
 const inspection = (function() {
     const status = {
         uncheck: 1, // 待审批
@@ -1530,6 +1530,7 @@ const pushType = {
     financial: 11,
     phasePay: 12,
     inspection: 13,
+    safeInspection: 14,
 };
 
 module.exports = {

+ 3 - 0
app/const/code_rule.js

@@ -16,6 +16,7 @@ const ruleType = {
     apply: 5,
     plan: 6,
     inspection: 7,
+    safe_inspection: 8,
 };
 const ruleField = [];
 ruleField[ruleType.measure] = 'm_rule';
@@ -25,6 +26,7 @@ ruleField[ruleType.will] = 'will';
 ruleField[ruleType.apply] = 'apply';
 ruleField[ruleType.plan] = 'plan';
 ruleField[ruleType.inspection] = 'inspection';
+ruleField[ruleType.safe_inspection] = 'safe_inspection';
 const ruleString = [];
 ruleString[ruleType.measure] = 'measure';
 ruleString[ruleType.change] = 'change';
@@ -33,6 +35,7 @@ ruleString[ruleType.will] = 'will';
 ruleString[ruleType.apply] = 'apply';
 ruleString[ruleType.plan] = 'plan';
 ruleString[ruleType.inspection] = 'inspection';
+ruleString[ruleType.safe_inspection] = 'safe_inspection';
 
 
 // 中间计量编号规则

+ 2 - 0
app/const/shenpi.js

@@ -19,6 +19,7 @@ const sp_type = {
     // financial: 8, // 资金支付审批流程设置不出现在这里,但请别用8这个类型控制审批流程,因为数据库我用了8来表示资金支付固定审批流
     phasePay: 9,
     inspection: 10,
+    safe_inspection: 11,
 };
 const sp_other_type = {
     financial: 8,
@@ -42,6 +43,7 @@ const sp_lc = [
     // { code: 'financial', type: sp_type.financial, name: '资金支付审批' },
     { code: 'phasePay', type: sp_type.phasePay, name: '合同支付审批' },
     { code: 'inspection', type: sp_type.inspection, name: '质量巡检审批' },
+    { code: 'safe_inspection', type: sp_type.inspection, name: '安全巡检审批' },
 ];
 
 const sp_status = {

+ 3 - 0
app/const/sp_page_show.js

@@ -34,6 +34,8 @@ const tenderPageControl = [
     { title: '施工日志', name: 'openConstruction', value: pageStatus.show, type: 'checkbox' },
     { title: '合同管理', name: 'openTenderContract', value: pageStatus.show, type: 'checkbox' },
     { title: '质量管理', name: 'quality', value: pageStatus.show, type: 'checkbox' },
+    { title: '质量巡检', name: 'qualityInspection', value: pageStatus.show, type: 'checkbox' },
+    { title: '安全巡检', name: 'safeInspection', value: pageStatus.show, type: 'checkbox' },
 ];
 // 报表相关开关
 const reportPageControl = [
@@ -111,6 +113,7 @@ const defaultSetting = {
     correctCalcContractTp: 0,
     quality: 1,
     qualityInspection: 1,
+    safeInspection: 1,
 };
 
 module.exports = {

+ 3 - 0
app/controller/change_controller.js

@@ -253,6 +253,9 @@ module.exports = app => {
                         case codeRuleConst.ruleField[codeRuleConst.ruleType.inspection]:
                             changeCount = await ctx.service.qualityInspection.count({ tid: tenderId });
                             break;
+                        case codeRuleConst.ruleField[codeRuleConst.ruleType.safe_inspection]:
+                            changeCount = await ctx.service.safeInspection.count({ tid: tenderId });
+                            break;
                         default:
                             break;
                     }

+ 6 - 5
app/controller/quality_controller.js

@@ -148,9 +148,10 @@ module.exports = app => {
                 }
                 let uids;
                 let auditList = [];
+                const tenderPermissionKeys = ['quality', 'inspection', 'safe_inspection'];
                 switch (data.type) {
                     case 'add-audit':
-                        if (!data.key || !['quality', 'inspection'].includes(data.key)) throw '参数有误';
+                        if (!data.key || !tenderPermissionKeys.includes(data.key)) throw '参数有误';
                         // // 判断用户是单个还是数组
                         uids = data.id instanceof Array ? data.id : [data.id];
                         // // 判断该用户的组是否已加入到表中,已加入则提示无需添加
@@ -170,7 +171,7 @@ module.exports = app => {
                         responseData.data = await ctx.service.tenderPermission.getPartsPermission(tenderInfo.id, [data.key]);
                         break;
                     case 'del-audit':
-                        if (!data.key || !['quality', 'inspection'].includes(data.key)) throw '参数有误';
+                        if (!data.key || !tenderPermissionKeys.includes(data.key)) throw '参数有误';
                         uids = data.id instanceof Array ? data.id : [data.id];
                         if (uids.length === 0) throw '没有选择要移除的用户';
                         auditList = await ctx.service.tenderPermission.getPartsPermission(tenderInfo.id, [data.key]);
@@ -189,12 +190,12 @@ module.exports = app => {
                         responseData.data = await ctx.service.tenderPermission.getPartsPermission(tenderInfo.id, [data.key]);
                         break;
                     case 'save-permission':
-                        if (!data.key || !['quality', 'inspection'].includes(data.key)) throw '参数有误';
+                        if (!data.key || !tenderPermissionKeys.includes(data.key)) throw '参数有误';
                         uids = data.uid instanceof Array ? data.uid : [data.uid];
                         await ctx.service.tenderPermission.saveOnePermission(tenderInfo.id, uids, data.members, [data.key]);
                         break;
                     case 'list':
-                        if (!data.key || !['quality', 'inspection'].includes(data.key)) throw '参数有误';
+                        if (!data.key || !tenderPermissionKeys.includes(data.key)) throw '参数有误';
                         responseData.data = await ctx.service.tenderPermission.getPartsPermission(tenderInfo.id, [data.key]);
                         break;
                     default: throw '参数有误';
@@ -614,7 +615,7 @@ module.exports = app => {
                 const renderData = {
                     moment,
                     tender,
-                    permission: ctx.permission.quality,
+                    permission: ctx.permission.inspection,
                     rule_type,
                     codeRule,
                     dealCode: ctx.tender.info.deal_info.dealCode,

+ 2 - 1
app/controller/report_archive_controller.js

@@ -918,6 +918,7 @@ module.exports = app => {
                 if (!(oss_result && oss_result.url && oss_result.res.status === 200)) {
                     throw '上传文件失败';
                 }
+                if (stream) await sendToWormhole(stream);
                 const versionId = oss_result.res.headers['x-oss-version-id'];
                 // 记录签名和保存
                 await ctx.service.netcasignLog.add(uuid, role, ctx.session.sessionUser.accountId, versionId);
@@ -925,7 +926,7 @@ module.exports = app => {
                 ctx.body = { err: 0, msg: '', data: signLogList };
             } catch (err) {
                 // 必须将上传的文件流消费掉,要不然浏览器响应会卡死
-                await sendToWormhole(stream);
+                if (stream) await sendToWormhole(stream);
                 this.log(err);
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }

+ 504 - 0
app/controller/safe_controller.js

@@ -0,0 +1,504 @@
+'use strict';
+
+/**
+ * 标段管理控制器
+ *
+ * @author Mai
+ * @date 2025/7/17
+ * @version
+ */
+
+const auditConst = require('../const/audit');
+const auditType = require('../const/audit').auditType;
+const shenpiConst = require('../const/shenpi');
+const codeRuleConst = require('../const/code_rule');
+const contractConst = require('../const/contract');
+const moment = require('moment');
+const sendToWormhole = require('stream-wormhole');
+const fs = require('fs');
+const path = require('path');
+const PermissionCheck = require('../const/account_permission').PermissionCheck;
+
+module.exports = app => {
+    class SafeController extends app.BaseController {
+        constructor(ctx) {
+            super(ctx);
+            ctx.showProject = true;
+            // ctx.showTitle = true;
+        }
+
+        loadMenu(ctx) {
+            super.loadMenu(ctx);
+            // 虚拟menu,以保证标题显示正确
+            ctx.menu = {
+                name: '安全管理',
+                display: false,
+                caption: '安全管理',
+                controller: 'safe',
+            };
+        }
+
+        async inspectionTender(ctx) {
+            try {
+                if (!ctx.subProject.page_show.safeInspection) throw '该功能已关闭';
+
+                const renderData = {
+                    is_inspection: ctx.url.includes('inspection') ? 1 : 0,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.safe.tender),
+                };
+
+                const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
+                renderData.accountList = accountList;
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                const accountGroupList = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                }).filter(x => { return x.groupList.length > 0; });
+                // const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                // renderData.accountGroup = unitList.map(item => {
+                //     const groupList = accountList.filter(item1 => item1.company === item.name);
+                //     return { groupName: item.name, groupList };
+                // });
+                renderData.accountGroup = accountGroupList;
+                renderData.accountInfo = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                renderData.tenderList = await ctx.service.tender.getSpecList(ctx.service.tenderPermission, 'safe_inspection', ctx.session.sessionUser.is_admin ? 'all' : '');
+                renderData.categoryData = await this.ctx.service.category.getAllCategory(this.ctx.subProject);
+                renderData.selfCategoryLevel = this.ctx.subProject.permission.self_category_level;
+                renderData.permissionConst = ctx.service.tenderPermission.partPermissionConst('safe_inspection');
+                renderData.permissionBlock = ctx.service.tenderPermission.partPermissionBlock('safe_inspection');
+                await this.layout('safe/tender.ejs', renderData, 'safe/tender_modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '无法查看安全巡检数据');
+                ctx.redirect('/dashboard');
+            }
+        }
+
+        /**
+         * 变更管理 页面 (Get)
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async inspection(ctx) {
+            try {
+                if (!ctx.subProject.page_show.safeInspection) throw '该功能已关闭';
+                const status = parseInt(ctx.query.status) || 0;
+                await this._filterInspection(ctx, status);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '无法查看质量管理数据');
+                ctx.redirect(`/sp/${ctx.subProject.id}/safe/inspection`);
+            }
+        }
+
+        // 质量巡检单功能
+        async _filterInspection(ctx, status = 0) {
+            try {
+                ctx.session.sessionUser.tenderId = ctx.tender.id;
+                const sorts = ctx.query.sort ? ctx.query.sort : 0;
+                const orders = ctx.query.order ? ctx.query.order : 0;
+                const filter = JSON.parse(JSON.stringify(auditConst.inspection.filter));
+                filter.count = [];
+                filter.count[filter.status.pending] = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, filter.status.pending);
+                filter.count[filter.status.uncheck] = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, filter.status.uncheck);
+                filter.count[filter.status.checking] = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, filter.status.checking);
+                filter.count[filter.status.rectification] = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, filter.status.rectification);
+                filter.count[filter.status.checked] = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, filter.status.checked);
+                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')) : [];
+                for (const item of inspectionList) {
+                    item.attList = ctx.helper._.filter(allAttList, { qiid: item.id });
+                }
+                // 分页相关
+                const page = ctx.page;
+                const pageSize = ctx.pageSize;
+                const pageInfo = {
+                    page,
+                    pageSizeSelect: 1,
+                    pageSize,
+                    total_num: total,
+                    total: Math.ceil(total / pageSize),
+                    queryData: JSON.stringify(ctx.urlInfo.query),
+                };
+                let codeRule = [];
+                let c_connector = '1';
+                let c_rule_first = 1;
+                const rule_type = 'safe_inspection';
+                const tender = await this.service.tender.getDataById(ctx.tender.id);
+                if (tender.c_code_rules) {
+                    const c_code_rules = JSON.parse(tender.c_code_rules);
+                    codeRule = c_code_rules[rule_type + '_rule'] !== undefined ? c_code_rules[rule_type + '_rule'] : [];
+                    c_connector = c_code_rules[rule_type + '_connector'] !== undefined ? c_code_rules[rule_type + '_connector'] : '1';
+                    c_rule_first = c_code_rules[rule_type + '_rule_first'] !== undefined ? c_code_rules[rule_type + '_rule_first'] : 1;
+                }
+                for (const rule of codeRule) {
+                    switch (rule.rule_type) {
+                        case codeRuleConst.measure.ruleType.dealCode:
+                            rule.preview = ctx.tender.info.deal_info.dealCode;
+                            break;
+                        case codeRuleConst.measure.ruleType.tenderName:
+                            rule.preview = tender.name;
+                            break;
+                        case codeRuleConst.measure.ruleType.inDate:
+                            rule.preview = moment().format('YYYY');
+                            break;
+                        case codeRuleConst.measure.ruleType.text:
+                            rule.preview = rule.text;
+                            break;
+                        case codeRuleConst.measure.ruleType.addNo:
+                            const s = '0000000000';
+                            rule.preview = s.substr(s.length - rule.format);
+                            break;
+                        default: break;
+                    }
+                }
+                const renderData = {
+                    moment,
+                    tender,
+                    permission: ctx.permission.safe_inspection,
+                    rule_type,
+                    codeRule,
+                    dealCode: ctx.tender.info.deal_info.dealCode,
+                    c_connector,
+                    c_rule_first,
+                    ruleType: codeRuleConst.ruleType[rule_type],
+                    ruleConst: codeRuleConst.measure,
+                    filter,
+                    inspectionList,
+                    auditType,
+                    auditConst: auditConst.inspection,
+                    status,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.safe.inspection),
+                    pageInfo,
+                };
+                await this.layout('safe/inspection.ejs', renderData, 'safe/inspection_modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '无法查看安全巡检数据');
+                ctx.redirect(`/sp/${ctx.subProject.id}/safe/inspection`);
+            }
+        }
+
+        /**
+         * 新增变更申请 (Post)
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async inspectionSave(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const reponseData = {
+                    err: 0, msg: '', data: {},
+                };
+                switch (data.type) {
+                    case 'add':
+                        if (!data.code || data.code === '') {
+                            throw '编号不能为空';
+                        }
+                        if (!data.check_item || !data.check_date) {
+                            throw '请填写检查项和日期';
+                        }
+                        reponseData.data = await ctx.service.safeInspection.add(ctx.tender.id, ctx.session.sessionUser.accountId, data.code, data.check_item, data.check_date);
+                        break;
+                    default:throw '参数有误';
+                }
+                ctx.body = reponseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString() };
+            }
+        }
+
+        /**
+         * 获取审批界面所需的 原报、审批人数据等
+         * @param ctx
+         * @return {Promise<void>}
+         * @private
+         */
+        async _getInspectionAuditViewData(ctx) {
+            await ctx.service.safeInspection.loadAuditViewData(ctx.inspection);
+        }
+
+        async inspectionInformation(ctx) {
+            try {
+                const whiteList = this.ctx.app.config.multipart.whitelist;
+                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 renderData = {
+                    moment,
+                    tender,
+                    inspection: ctx.inspection,
+                    auditConst: auditConst.inspection,
+                    fileList,
+                    whiteList,
+                    auditType,
+                    shenpiConst,
+                    deleteFilePermission: PermissionCheck.delFile(this.ctx.session.sessionUser.permission),
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.safe.inspection_information),
+                    preUrl: `/sp/${ctx.subProject.id}/safe/tender/${ctx.tender.id}/inspection/${ctx.inspection.id}/information`,
+                };
+                // data.accountGroup = accountGroup;
+                // 获取所有项目参与者
+                const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
+                renderData.accountList = accountList;
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                renderData.accountGroup = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                }).filter(x => { return x.groupList.length > 0; });
+                await this.layout('safe/inspection_information.ejs', renderData, 'safe/inspection_information_modal.ejs');
+            } catch (err) {
+                this.log(err);
+                ctx.redirect(`/sp/${ctx.subProject.id}/safe/tender/${ctx.tender.id}/inspection`);
+            }
+        }
+
+        async inspectionInformationSave(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const reponseData = {
+                    err: 0, msg: '', data: {},
+                };
+                switch (data.type) {
+                    case 'update-field':
+                        if (!(!ctx.inspection.readOnly || ctx.inspection.rectificationPower)) {
+                            throw '当前状态不可修改';
+                        }
+                        if (data.update.check_item !== undefined && data.update.check_item === '') {
+                            throw '检查项不能为空';
+                        }
+                        if (data.update.check_date !== undefined && data.update.check_date === '') {
+                            throw '请填写检查日期';
+                        }
+                        if (data.update.rectification_item !== undefined && data.update.rectification_item === '') {
+                            throw '整改情况不能为空';
+                        }
+                        if (data.update.rectification_date !== undefined && data.update.rectification_date === '') {
+                            throw '请填写整改日期';
+                        }
+                        const fields = ['id', 'check_item', 'check_situation', 'action', 'check_date', 'inspector', 'rectification_item', 'rectification_date'];
+                        if (!this.checkFieldExists(data.update, fields)) {
+                            throw '参数有误';
+                        }
+                        reponseData.data = await ctx.service.safeInspection.defaultUpdate(data.update);
+                        break;
+                    case 'add-audit':
+                        const id = this.app._.toInteger(data.auditorId);
+                        if (isNaN(id) || id <= 0) {
+                            throw '参数错误';
+                        }
+                        // 检查权限等
+                        if (ctx.inspection.uid !== ctx.session.sessionUser.accountId) {
+                            throw '您无权添加审核人';
+                        }
+                        if (ctx.inspection.status !== auditConst.inspection.status.uncheck && ctx.inspection.status !== auditConst.inspection.status.checkNo) {
+                            throw '当前不允许添加审核人';
+                        }
+
+                        ctx.inspection.auditorList = await ctx.service.safeInspectionAudit.getAuditors(ctx.inspection.id, ctx.inspection.times);
+                        // 检查审核人是否已存在
+                        const exist = this.app._.find(ctx.inspection.auditorList, { aid: id });
+                        if (exist) {
+                            throw '该审核人已存在,请勿重复添加';
+                        }
+                        const result = await ctx.service.safeInspectionAudit.addAuditor(ctx.inspection.id, id, ctx.inspection.times);
+                        if (!result) {
+                            throw '添加审核人失败';
+                        }
+                        reponseData.data = await ctx.service.safeInspectionAudit.getUserGroup(ctx.inspection.id, ctx.inspection.times);
+                        break;
+                    case 'del-audit':
+                        const id2 = data.auditorId instanceof Number ? data.auditorId : this.app._.toNumber(data.auditorId);
+                        if (isNaN(id2) || id2 <= 0) {
+                            throw '参数错误';
+                        }
+                        const result2 = await ctx.service.safeInspectionAudit.deleteAuditor(ctx.inspection.id, id2, ctx.inspection.times);
+                        if (!result2) {
+                            throw '移除审核人失败';
+                        }
+                        reponseData.data = await ctx.service.safeInspectionAudit.getAuditors(ctx.inspection.id, ctx.inspection.times);
+                        break;
+                    case 'start-inspection':
+                        if (ctx.inspection.readOnly) {
+                            throw '当前状态不可提交';
+                        }
+                        await ctx.service.safeInspectionAudit.start(ctx.inspection.id, ctx.inspection.times);
+                        break;
+                    case 'del-inspection':
+                        if (ctx.inspection.readOnly) {
+                            throw '当前状态不可删除';
+                        }
+                        await ctx.service.safeInspection.delInspection(ctx.inspection.id);
+                        break;
+                    case 'check':
+                        if (!ctx.inspection || !(ctx.inspection.status === auditConst.inspection.status.checking || ctx.inspection.status === auditConst.inspection.status.checkNoPre)) {
+                            throw '当前质量巡检数据有误';
+                        }
+                        if (ctx.inspection.curAuditorIds.length === 0 || ctx.inspection.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) === -1) {
+                            throw '您无权进行该操作';
+                        }
+                        await ctx.service.safeInspectionAudit.check(ctx.inspection, data);
+                        break;
+                    case 'rectification':
+                        if (!ctx.inspection || ctx.inspection.status !== auditConst.inspection.status.rectification) {
+                            throw '当前质量巡检数据有误';
+                        }
+                        if (ctx.inspection.curAuditorIds.length === 0 || ctx.inspection.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) === -1) {
+                            throw '您无权进行该操作';
+                        }
+                        await ctx.service.safeInspectionAudit.rectification(ctx.inspection, data);
+                        break;
+                    default:throw '参数有误';
+                }
+                ctx.body = reponseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString() };
+            }
+        }
+
+        checkFieldExists(update, fields) {
+            for (const field of Object.keys(update)) {
+                if (!fields.includes(field)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * 上传附件
+         * @param {*} ctx 上下文
+         */
+        async uploadInspectionFile(ctx) {
+            let stream;
+            try {
+                // this._checkAdvanceFileCanModify(ctx);
+                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;
+                    }
+                    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}`;
+                    // 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();
+                const payload = files.map(file => {
+                    let idx;
+                    if (Array.isArray(parts.field.name)) {
+                        idx = parts.field.name.findIndex(name => name === file.name);
+                    } else {
+                        idx = 'isString';
+                    }
+                    const newFile = {
+                        tid: ctx.tender.id,
+                        qiid: ctx.inspection.id,
+                        uid: ctx.session.sessionUser.accountId,
+                        filename: file.name,
+                        fileext: file.ext,
+                        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);
+                // 将最新的当前标段的所有文件信息返回
+                const data = await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                ctx.body = { err: 0, msg: '', data };
+            } catch (err) {
+                stream && (await sendToWormhole(stream));
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        /**
+         * 删除附件
+         * @param {Ojbect} ctx 上下文
+         */
+        async deleteInspectionFile(ctx) {
+            try {
+                const { id } = JSON.parse(ctx.request.body.data);
+                const fileInfo = await ctx.service.safeInspectionAtt.getDataById(id);
+                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);
+                ctx.body = { err: 0, msg: '请求成功', data };
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        /**
+         * 下载附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async downloadInspectionFile(ctx) {
+            const id = ctx.params.fid;
+            if (id) {
+                try {
+                    const fileInfo = await ctx.service.safeInspectionAtt.getDataById(id);
+                    if (fileInfo !== undefined && fileInfo !== '') {
+                        // const fileName = path.join(__dirname, '../', 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);
+                        } else if (userAgent.indexOf('firefox') >= 0) {
+                            disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(fileInfo.filename) + '"';
+                        } else {
+                            /* safari等其他非主流浏览器只能自求多福了 */
+                            disposition = 'attachment; filename=' + new Buffer(fileInfo.filename).toString('binary');
+                        }
+                        ctx.response.set({
+                            'Content-Type': 'application/octet-stream',
+                            'Content-Disposition': disposition,
+                            'Content-Length': fileInfo.filesize,
+                        });
+                        // ctx.body = await fs.createReadStream(fileName);
+                        ctx.body = await ctx.helper.ossFileGet(fileInfo.filepath);
+                    } else {
+                        throw '不存在该文件';
+                    }
+                } catch (err) {
+                    this.log(err);
+                    this.setMessage(err.toString(), this.messageType.ERROR);
+                }
+            }
+        }
+    }
+
+    return SafeController;
+};

+ 4 - 3
app/controller/sub_proj_setting_controller.js

@@ -592,8 +592,8 @@ module.exports = app => {
                     subProjects,
                 };
                 renderData.selfCategoryLevel = await this.ctx.service.projectAccount.getSelfCategoryLevel(this.ctx.session.sessionUser.accountId);
-                renderData.permissionConst = ctx.service.tenderPermission.partPermissionConst(['quality', 'inspection']);
-                renderData.permissionBlock = ctx.service.tenderPermission.partPermissionBlock(['quality', 'inspection']);
+                renderData.permissionConst = ctx.service.tenderPermission.partPermissionConst(['quality', 'inspection', 'safe_inspection']);
+                renderData.permissionBlock = ctx.service.tenderPermission.partPermissionBlock(['quality', 'inspection', 'safe_inspection']);
                 await this.layout('sp_setting/manage.ejs', renderData, 'sp_setting/manage_modal.ejs');
             } catch (error) {
                 ctx.log(error);
@@ -648,6 +648,7 @@ module.exports = app => {
                         responseData.data.constructionAuditList = await ctx.service.constructionAudit.getList(tender.id);
                         responseData.data.qualityAuditList = await ctx.service.tenderPermission.getPartsPermission(tender.id, ['quality']);
                         responseData.data.inspectionAuditList = await ctx.service.tenderPermission.getPartsPermission(tender.id, ['inspection']);
+                        responseData.data.safe_inspectionAuditList = await ctx.service.tenderPermission.getPartsPermission(tender.id, ['safe_inspection']);
                         break;
                     case 'copy2otu':
                         if (data.userType === 'tourist') {
@@ -658,7 +659,7 @@ module.exports = app => {
                             await ctx.service.contractAudit.setOtherTender(data.tidList, data.auditList);
                         } else if (data.userType === 'construction') {
                             await ctx.service.constructionAudit.setOtherTender(data.tidList, data.auditList);
-                        } else if (data.userType === 'quality' || data.userType === 'inspection') {
+                        } else if (data.userType === 'quality' || data.userType === 'inspection' || data.userType === 'safe_inspection') {
                             await ctx.service.tenderPermission.setOtherTender(data.tidList, data.auditList, [data.userType]);
                         } else {
                             throw '参数有误';

+ 37 - 0
app/controller/wap_controller.js

@@ -22,6 +22,7 @@ const auditType = require('../const/audit').auditType;
 const AiInspect = require('../lib/ai_inspect');
 const uuid = require('node-uuid');
 const nlsToken = require('../lib/nls_token');
+const streamToArray = require('stream-to-array');
 
 module.exports = app => {
 
@@ -1235,6 +1236,42 @@ module.exports = app => {
             }
             ctx.body = responseData;
         }
+
+        async voiceOneShot(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: '',
+            };
+            const stream = await ctx.getFileStream();
+            try {
+                // 读取字节流
+                const parts = await streamToArray(stream);
+                // 转化为buffer
+                const buffer = Buffer.concat(parts);
+
+                if (!buffer || buffer.length === 0) {
+                    throw '音频数据为空';
+                }
+
+                // 调用 service 执行一句话识别
+                const nls_token = new nlsToken(this.ctx);
+                const token = await nls_token.getToken();
+                const text = await nls_token.oneShotRecognition(token, buffer);
+                console.log(text);
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                responseData.data = text;
+            } catch (error) {
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                responseData.err = 1;
+                responseData.msg = error.message ? error.message : error;
+            }
+            ctx.body = responseData;
+        }
     }
 
     return WapController;

+ 84 - 1
app/lib/nls_token.js

@@ -10,7 +10,13 @@
 
 const Core = require('@alicloud/pop-core');
 const smsAli = require('../const/sms_alitemplate.js');
+const Nls = require('alibabacloud-nls');
 class NlsToken {
+    constructor(ctx) {
+        this.ctx = ctx;
+        this.appkey = 'nE0d2Ue08rtPhb9e';
+        this.shotUrl = 'wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1';
+    }
 
     async getToken() {
         const client = new Core({
@@ -30,12 +36,89 @@ class NlsToken {
 
         try {
             const result = await client.request('CreateToken', params, requestOption);
-            return result.Token;
+            return result.Token.Id;
         } catch (err) {
             this.ctx.logger.error('获取语音识别Token失败', err);
             throw err;
         }
     }
+
+    /**
+     * audioBuffer: Buffer of wav (16k)
+     */
+    async oneShotRecognition(token, audioBuffer, url = this.shotUrl, appkey = this.appkey) {
+        return new Promise(async (resolve, reject) => {
+            let finalText = '';
+
+            const sr = new Nls.SpeechRecognition({
+                url,
+                appkey,
+                token,
+                enable_punctuation_prediction: false,
+                enable_inverse_text_normalization: true,
+            });
+
+            sr.on('started', (msg) => {
+                try {
+                    const data = JSON.parse(msg);
+                    // console.log('Client recv started:', data);
+                } catch (err) {
+                    console.log('started parse error:', err);
+                }
+            });
+
+            sr.on('changed', (msg) => {
+                try {
+                    const data = JSON.parse(msg);
+                    if (data.payload && data.payload.result) {
+                        finalText = data.payload.result;
+                    }
+                    console.log('changed finalText:', finalText);
+                } catch (err) {
+                    console.log('changed parse error:', err);
+                }
+            });
+
+            sr.on('completed', (msg) => {
+                try {
+                    const data = JSON.parse(msg);
+                    if (data.payload && data.payload.result) {
+                        finalText = data.payload.result;
+                    }
+                    // console.log('completed finalText:', finalText);
+                } catch (err) {
+                    console.log('completed parse error:', err);
+                }
+            });
+
+            sr.on('closed', () => {
+                // console.log('closed finalText:', finalText);
+                resolve(finalText.trim());
+            });
+
+            sr.on('failed', (msg) => {
+                try {
+                    const data = JSON.parse(msg);
+                    reject(new Error(JSON.stringify(data)));
+                } catch (err) {
+                    reject(new Error(msg));
+                }
+            });
+
+            try {
+                // 启动识别
+                await sr.start(sr.defaultStartParams(), true, 6000);
+
+                if (!sr.sendAudio(audioBuffer)) {
+                    throw new Error('send audio failed');
+                }
+                await sr.close();
+            } catch (err) {
+                console.log('error on start:', err);
+                return reject(err);
+            }
+        });
+    }
 }
 
 module.exports = NlsToken;

+ 1 - 1
app/middleware/inspection_check.js

@@ -21,7 +21,7 @@ module.exports = options => {
      * @param {function} next - 中间件继续执行的方法
      * @return {void}
      */
-    return function* InspectionCheck(next) {
+    return function* QualityInspectionCheck(next) {
         try {
             // 获取revise
             if (!this.subProject.page_show.qualityInspection) {

+ 126 - 0
app/middleware/safe_inspection_check.js

@@ -0,0 +1,126 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Ellisran
+ * @date 2020/10/15
+ * @version
+ */
+
+const status = require('../const/audit').inspection.status;
+const shenpiConst = require('../const/shenpi');
+const _ = require('lodash');
+
+module.exports = options => {
+    /**
+     * 标段校验 中间件
+     * 1. 读取标段数据(包括属性)
+     * 2. 检验用户是否可见标段(不校验具体权限)
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* SafeInspectionCheck(next) {
+        try {
+            // 获取revise
+            if (!this.subProject.page_show.safeInspection) {
+                throw '该功能已关闭';
+            }
+            const qiid = this.params.qiid || this.request.body.qiid;
+            if (!qiid) {
+                throw '您访问的质量巡检不存在';
+            }
+            const inspection = yield this.service.safeInspection.getDataById(qiid);
+            if (!inspection) throw '质量巡检数据有误';
+            // 读取原报、审核人数据
+            yield this.service.safeInspection.loadUser(inspection);
+            // 权限相关
+            // todo 校验权限 (标段参与人、分享)
+            const accountId = this.session.sessionUser.accountId,
+                auditorIds = _.map(inspection.auditors, 'aid'),
+                shareIds = [];
+            const permission = this.session.sessionUser.permission;
+            if (accountId === inspection.uid) { // 原报
+                inspection.filePermission = true;
+            } else if (auditorIds.indexOf(accountId) !== -1) { // 审批人
+                if (inspection.status === status.uncheck) {
+                    throw '您无权查看该数据';
+                }
+                inspection.filePermission = true;
+            } else if (inspection.status === status.checkNo && inspection.uid !== accountId) {
+                const preAuditors = yield this.service.safeInspectionAudit.getAuditors(inspection.id, inspection.times - 1);
+                const preAuditorIds = _.map(preAuditors, 'aid');
+                if (preAuditorIds.indexOf(accountId) === -1) {
+                    throw '您无权查看该数据';
+                }
+                inspection.filePermission = true;
+            } else if (this.tender.isTourist || this.session.sessionUser.is_admin) {
+                inspection.filePermission = this.tender.touristPermission.file || auditorIds.indexOf(accountId) !== -1;
+            } else if (shareIds.indexOf(accountId) !== -1 || (permission !== null && permission.tender !== undefined && permission.tender.indexOf('2') !== -1)) { // 分享人
+                if (inspection.status === status.uncheck) {
+                    throw '您无权查看该数据';
+                }
+                inspection.filePermission = false;
+            } else { // 其他不可见
+                throw '您无权查看该数据';
+            }
+            // 调差的readOnly 指表格和页面只能看不能改,和审批无关
+            inspection.readOnly = !((inspection.status === status.uncheck || inspection.status === status.checkNo) && accountId === inspection.uid);
+            inspection.rectificationPower = inspection.status === status.rectification && inspection.curAuditorIds.indexOf(accountId) !== -1;
+            inspection.shenpiPower = (inspection.status === status.checking || inspection.status === status.checkNoPre) && inspection.curAuditorIds.indexOf(accountId) !== -1;
+            this.inspection = inspection;
+            // 根据状态判断是否需要更新审批人列表
+            if ((inspection.status === status.uncheck || inspection.status === status.checkNo) && this.tender.info.shenpi.inspection !== shenpiConst.sp_status.sqspr) {
+                const shenpi_status = this.tender.info.shenpi.inspection;
+                // 进一步比较审批流是否与审批流程设置的相同,不同则替换为固定审批流或固定的终审
+                const auditList = yield this.service.safeInspectionAudit.getAllDataByCondition({ where: { qiid: inspection.id, times: inspection.times, is_rectification: 0 }, orders: [['order', 'asc']] });
+                if (shenpi_status === shenpiConst.sp_status.gdspl) {
+                    const shenpiList = yield this.service.shenpiAudit.getAllDataByCondition({ where: { tid: inspection.tid, sp_type: shenpiConst.sp_type.inspection, sp_status: shenpi_status } });
+                    // 判断2个id数组是否相同,不同则删除原审批流,切换成固定的审批流
+                    let sameAudit = auditList.length === shenpiList.length;
+                    if (sameAudit) {
+                        for (const audit of auditList) {
+                            const shenpi = shenpiList.find(x => { return x.audit_id === audit.aid; });
+                            if (!shenpi || shenpi.audit_order !== audit.audit_order || shenpi.audit_type !== audit.audit_type) {
+                                sameAudit = false;
+                                break;
+                            }
+                        }
+                    }
+                    if (!sameAudit) {
+                        yield this.service.safeInspectionAudit.updateNewAuditList(inspection, shenpiList);
+                        yield this.service.safeInspection.loadUser(inspection);
+                    }
+                } else if (shenpi_status === shenpiConst.sp_status.gdzs) {
+                    const shenpiInfo = yield this.service.shenpiAudit.getDataByCondition({ tid: inspection.tid, sp_type: shenpiConst.sp_type.inspection, sp_status: shenpi_status });
+                    // 判断最后一个id是否与固定终审id相同,不同则删除原审批流中如果存在的id和添加终审
+                    const lastAuditors = auditList.filter(x => { x.order === auditList[auditList.length - 1].order; });
+                    if (shenpiInfo && (lastAuditors.length === 0 || (lastAuditors.length > 1 || shenpiInfo.audit_id !== lastAuditors[0].aid))) {
+                        yield this.service.safeInspectionAudit.updateLastAudit(inspection, auditList, shenpiInfo.audit_id);
+                        yield this.service.safeInspection.loadUser(inspection);
+                    } else if (!shenpiInfo) {
+                        // 不存在终审人的状态下这里恢复为授权审批人
+                        this.tender.info.shenpi.inspection = shenpiConst.sp_status.sqspr;
+                    }
+                }
+            }
+            yield next;
+        } catch (err) {
+            console.log(err);
+            // 输出错误到日志
+            if (err.stack) {
+                this.logger.error(err);
+            } else {
+                this.getLogger('fail').info(JSON.stringify({
+                    error: err,
+                    project: this.session.sessionProject,
+                    user: this.session.sessionUser,
+                    body: this.session.body,
+                }));
+            }
+            // 重定向值标段管理
+            this.redirect(this.request.headers.referer);
+        }
+    };
+};

+ 349 - 0
app/public/js/safe_inspection.js

@@ -0,0 +1,349 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2018/8/21
+ * @version
+ */
+// 向后端请求中间计量号
+function getNewCode() {
+    postData('/tender/'+ tenderId +'/change/newCode', { type: rulesType }, function (code) {
+        if (code !== '') {
+            $('#bj-code').val(code);
+        }
+    });
+}
+
+class codeRuleSet {
+    constructor (obj) {
+        this.body = obj;
+        // 切换规则组件类型
+        $('.rule-change', obj).change(function () {
+            const codeType = this.selectedIndex-1;
+            if (codeType === ruleConst.ruleType.addNo) {
+                $('#format', obj).show();
+                $('#text', obj).show();
+                $('#text>label', obj).text('起始编号');
+                $('#text>input', obj).val('001');
+                const s = '0000000000' + 1;
+                $('#text>input', obj).val(s.substr(s.length - $('#format>input', obj).val()));
+            } else if (codeType === ruleConst.ruleType.text) {
+                $('#format', obj).hide();
+                $('#text', obj).show();
+                $('#text>label', obj).text('文本');
+                $('#text>input', obj).val('').attr('placeholder', '请在这里输入需要的文本');
+            } else {
+                $('#format', obj).hide();
+                $('#text', obj).hide();
+            }
+        });
+        // 修改编号位数
+        $('#format>input', obj).change(function () {
+            const s = '0000000000' + parseInt($('#text>input', obj).val());
+            $('#text>input', obj).val(s.substr(s.length - $(this).val()));
+        });
+
+        // 修改连接符
+        $('.connector-change', obj).change(function () {
+            const connectorType = this.options[this.selectedIndex].text;
+            const rules = $('span>span', obj), ruleText = [];
+            for (const r of rules) {
+                ruleText.push($.trim(r.innerText));
+            }
+            if (connectorType === '无') {
+                $('#preview', obj).text(ruleText.join(''));
+            } else {
+                $('#preview', obj).text(ruleText.join(connectorType));
+            }
+            connectorRule = this.options[this.selectedIndex].value;
+        });
+
+        // 新增规则组件
+        $('#addRule', obj).click(function () {
+            const codeType = $('select', obj)[1].selectedIndex-1;
+            const rule = {rule_type: codeType}, html = [];
+            let preview;
+            switch (codeType) {
+                case ruleConst.ruleType.dealCode: {
+                    if (dealCode === '') {
+                        toastr.error('当前标段合同编号为空,请选择其他组件。');
+                        return false;
+                    }
+                    preview = dealCode;
+                    break;
+                }
+                case ruleConst.ruleType.tenderName: {
+                    preview = tenderName;
+                    break;
+                }
+                case ruleConst.ruleType.text: {
+                    rule.text = $('#text>input', obj).val();
+                    if (rule.text === '') {
+                        toastr.error('文本内容不允许为空。');
+                        return false;
+                    }
+                    preview = rule.text;
+                    break;
+                }
+                case ruleConst.ruleType.inDate: {
+                    preview = moment().format('YYYY');
+                    break;
+                }
+                case ruleConst.ruleType.addNo: {
+                    rule.format = parseInt($('#format>input', obj).val());
+                    rule.start = parseInt($('#text>input', obj).val());
+                    if ($('#text>input', obj).val().length !== rule.format) {
+                        toastr.error('起始编号位数和自动编号位数不一致。');
+                        return false;
+                    }
+                    const s = '0000000000';
+                    preview = s.substr(s.length - rule.format);
+                    break;
+                }
+                default: {
+                    toastr.error('请选择组件再添加');
+                    return false;
+                }
+            }
+            // 更新规则
+            codeRule.push(rule);
+            // 更新规则显示
+            html.push('<span class="badge badge-light" title="' + ruleConst.ruleString[codeType] + '" rule="' + JSON.stringify(rule) + '">');
+            html.push('<span>' + preview + '</span>');
+            html.push('<a href="javascript: void(0);" class="text-danger" title="移除"><i class="fa fa-remove"></i></a>');
+            html.push('</span>');
+            const part = $('#ruleParts', obj).append(html.join(''));
+            // 更新规则预览
+            const connectorType = connectorRule !== '' && parseInt(connectorRule) !== ruleConst.connectorType.nothing ? ruleConst.connectorString[connectorRule] : '';
+            const previewtext = $.trim($('#preview', obj).text()) === '' ? preview : $.trim($('#preview', obj).text()) + connectorType + preview;
+            $('#preview', obj).text(previewtext);
+        });
+        // 删除规则组件
+        $($('#ruleParts', obj)).on('click', 'a', function () {
+            const index = $('a', obj).index(this);
+            codeRule.splice(index-1, 1);
+            $(this).parent().remove();
+            const rules = $('span>span', obj), ruleText = [];
+            for (const r of rules) {
+                ruleText.push($.trim(r.innerText));
+            }
+            const connectorType = connectorRule !== '' && parseInt(connectorRule) !== ruleConst.connectorType.nothing ? ruleConst.connectorString[connectorRule] : '';
+            $('#preview', obj).text(ruleText.join(connectorType));
+        });
+    }
+}
+$(document).ready(() => {
+    // 首次进入设置
+    let showNoNeed = false;
+    if (cRuleFirst) {
+        codeRule = [];
+        showNoNeed = true;
+        $('#setting').modal('show');
+    }
+    // 设置
+    const ruleSet = new codeRuleSet($('div.modal-body', '#setting'));
+    $('#setRule', '#setting').bind('click', function () {
+        const data = {
+            rule: ruleType,
+            type: rulesType,
+            connector: connectorRule,
+            data: JSON.stringify(codeRule),
+        };
+        if (codeRule.length !== 0) {
+            $('#autoCodeShow').show();
+        }
+        postData('/tender/rule', data, function () {
+            if (cRuleFirst && showNoNeed) {
+                $('#changeFirst').click();
+                $('.ml-auto a[href="#add-bj"]').click();
+            } else {
+                $('#setting').modal('hide');
+            }
+        });
+    })
+    $('.ml-auto').on('click', 'a', function () {
+        const content = $(this).attr('href');
+        if (content === '#add-bj') {
+            $('#add-bj-modal').modal('show')
+            getNewCode();
+            if ($('#changeList').children.length === 0) {
+                $('#addCancel').hide();
+            } else {
+                $('#addCancel').show();
+            }
+            $('#bj-code').removeClass('is-invalid');
+        }
+    });
+    // 获取最新可用变更令号
+    $('#autoCode').click(getNewCode);
+    // 新增变更令 确认
+    $('#addOk').click(function () {
+        $(this).attr('disabled', true);
+        if ($('#check_item').val().length === 0) {
+            $('#check_item').addClass('is-invalid');
+            $('#name_error_msg').show();
+            $('#name_error_msg').text('检查项不能为空。');
+            $(this).attr('disabled', false);
+            setTimeout(function () {
+                $('#check_item').removeClass('is-invalid');
+                $('#name_error_msg').hide();
+            }, 2000);
+            return;
+        }
+        if ($('#check_item').val().length > 255) {
+            $('#chek_item').addClass('is-invalid');
+            $('#name_error_msg').show();
+            $('#name_error_msg').text('检查项超过255个字,请缩减。');
+            $(this).attr('disabled', false);
+            setTimeout(function () {
+                $('#check_item').removeClass('is-invalid');
+                $('#name_error_msg').hide();
+            }, 2000);
+            return;
+        }
+        if ($('#check_date').val() === '') {
+            $('#check_date').addClass('is-invalid');
+            $('#check_date').siblings('.invalid-feedback').show();
+            $('#check_date').siblings('.invalid-feedback').text('检查日期不能为空。');
+            $(this).attr('disabled', false);
+            setTimeout(function () {
+                $('#check_date').removeClass('is-invalid');
+                $('#check_date').siblings('.invalid-feedback').hide();
+            }, 2000);
+            return;
+        } else {
+            // 判断日期格式
+            const reg = /^\d{4}-\d{2}-\d{2}$/;
+            if (!reg.test($('#check_date').val())) {
+                $('#check_date').addClass('is-invalid');
+                $('#check_date').siblings('.invalid-feedback').show();
+                $('#check_date').siblings('.invalid-feedback').text('检查日期格式错误,应为YYYY-MM-DD。');
+                $(this).attr('disabled', false);
+                setTimeout(function () {
+                    $('#check_date').removeClass('is-invalid');
+                    $('#check_date').siblings('.invalid-feedback').hide();
+                }, 2000);
+                return;
+            }
+        }
+        const data = {
+            type: 'add',
+            code: $('#bj-code').val(),
+            check_item: $('#check_item').val(),
+            check_date: $('#check_date').val(),
+        };
+        if (data.code || data.code !== '') {
+            postData(`/sp/${spid}/safe/tender/${tenderId}/inspection/save`, data, function (rst) {
+                $('#bj-code').removeClass('is-invalid');
+                $('#add-bj-modal').modal('hide');
+                $(this).attr('disabled', false);
+                window.location.href = `/sp/${spid}/safe/tender/${tenderId}/inspection/${rst.id}/information`;
+            }, function () {
+                $('#bj-code').addClass('is-invalid');
+                $('#bjHint').show();
+                $(this).attr('disabled', false);
+            });
+        }
+    });
+
+    //状态切换
+    $('#status_select a').on('click', function () {
+        const status = $(this).data('val');
+        let url = `/sp/${spid}/safe/tender/${tenderId}/inspection`;
+        const filterString = setChangeFilterData('safe-inspection-'+ tenderId +'-list-order', status !== 0 ? '?status='+ status : '');
+        if (filterString) url = url + filterString;
+        window.location.href = url;
+    });
+    // 不再显示首次使用
+    $('#changeFirst').click(function () {
+        showNoNeed = false;
+        $('#changeFirst').remove();
+        $('#hide_modal').show();
+        $('#setting').modal('hide');
+        postData('/tender/'+ tenderId +'/rule/first', { type: rulesType }, function () {
+        });
+    });
+
+    // 排序初始化
+    let orderSetting = getLocalCache('safe-inspection-'+ tenderId +'-list-order');
+    if (!orderSetting) orderSetting = 'time|desc';
+    const orders = orderSetting.split('|');
+    $("#sort-radio input[value='"+ orders[0] +"']").prop('checked', true);
+    $("#order-radio input[value='"+ orders[1] +"']").prop('checked', true);
+    if (orders[0] === 'time') {
+        $('#bpaixu').text('排序:创建时间');
+    } else {
+        $('#bpaixu').text('排序:编号');
+    }
+    $('#sort-radio input[name="paizhi"]').click(function () {
+        const orderStr = $(this).val() + '|' + $('#order-radio input[name="paixu"]:checked').val();
+        setLocalCache('safe-inspection-'+ tenderId +'-list-order', orderStr);
+        let link = window.location.origin + window.location.pathname;
+        const filterData = [];
+        if ($('#zhankai').data('status') !== '0') {
+            filterData.push('status=' + $('#zhankai').data('status'));
+        }
+        filterData.push('sort='+ $(this).val());
+        filterData.push('order=' + $('#order-radio input[name="paixu"]:checked').val());
+        if (getLocalCache('account-pageSize')) {
+            filterData.push('pageSize=' + getLocalCache('account-pageSize'));
+        }
+        if (filterData.length > 0) {
+            link += '?' + filterData.join('&');
+        }
+        window.location.href = link;
+    });
+    $('#order-radio input[name="paixu"]').click(function () {
+        const orderStr = $('#sort-radio input[name="paizhi"]:checked').val() + '|' + $(this).val();
+        setLocalCache('safe-inspection-'+ tenderId +'-list-order', orderStr);
+        let link = window.location.origin + window.location.pathname;
+        const filterData = [];
+        if ($('#zhankai').data('status') !== '0') {
+            filterData.push('status=' + $('#zhankai').data('status'));
+        }
+        filterData.push('sort='+ $('#sort-radio input[name="paizhi"]:checked').val());
+        filterData.push('order=' + $(this).val());
+        if (getLocalCache('account-pageSize')) {
+            filterData.push('pageSize=' + getLocalCache('account-pageSize'));
+        }
+        if (filterData.length > 0) {
+            link += '?' + filterData.join('&');
+        }
+        window.location.href = link;
+    });
+
+    $('.show-files').on('click', function () {
+        const id = parseInt($(this).data('id'));
+        const info = _.find(inspectionList, { id: id });
+        console.log(info);
+        handleFileList(info.attList || []);
+    });
+
+    function handleFileList(files = []) {
+        $('#file-content').empty();
+        let html = '';
+        files.forEach((file, idx) => {
+            html += `<tr><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="/sp/${spid}/safe/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a></td></tr>`
+        })
+        $('#file-content').append(html);
+    }
+
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+        }
+    });
+});

+ 631 - 0
app/public/js/safe_inspection_information.js

@@ -0,0 +1,631 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author lanjianrong
+ * @date 2020/8/7
+ * @version
+ */
+
+$(document).ready(function () {
+    autoFlashHeight();
+
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+        }
+    });
+
+    // 展开历史审核记录
+    $('td #fold-btn').click(function () {
+        const type = $(this).data('target')
+        const auditCard = $(this).parent().parent()
+        if (type === 'show') {
+            $(this).data('target', 'hide')
+            auditCard.find('.fold-card').slideDown('swing', () => {
+                auditCard.find('#fold-btn').text('收起历史审核记录')
+            })
+        } else {
+            $(this).data('target', 'show')
+            auditCard.find('.fold-card').slideUp('swing', () => {
+                auditCard.find('#fold-btn').text('展开历史审核记录')
+            })
+        }
+    });
+
+    // 添加审批流程按钮逻辑
+    $('.book-list').on('click', 'dt', function () {
+        const idx = $(this).find('.acc-btn').attr('data-groupid')
+        const type = $(this).find('.acc-btn').attr('data-type')
+        if (type === 'hide') {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).show(() => {
+                $(this).children().find('i').removeClass('fa-plus-square').addClass('fa-minus-square-o')
+                $(this).find('.acc-btn').attr('data-type', 'show')
+
+            })
+        } else {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).hide(() => {
+                $(this).children().find('i').removeClass('fa-minus-square-o').addClass('fa-plus-square')
+                $(this).find('.acc-btn').attr('data-type', 'hide')
+            })
+        }
+        return false
+    })
+
+    let timer = null
+    let oldSearchVal = null
+    $('.gr-search').bind('input propertychange', function (e) {
+        oldSearchVal = e.target.value
+        timer && clearTimeout(timer)
+        timer = setTimeout(() => {
+            const newVal = $('#gr-search').val()
+            let html = ''
+            if (newVal && newVal === oldSearchVal) {
+                accountList.filter(item => item && inspection.uid !== item.id && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1))).forEach(item => {
+                    html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                class="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span>
+                    </dd>`
+                })
+                $('.book-list').empty()
+                $('.book-list').append(html)
+            } else {
+                if (!$('.acc-btn').length) {
+                    accountGroup.forEach((group, idx) => {
+                        if (!group) return
+                        html += `<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i>
+                        </a> ${group.groupName}</dt>
+                        <div class="dd-content" data-toggleid="${idx}">`
+                        group.groupList.forEach(item => {
+                            if (item.id !== inspection.uid) {
+                                html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                                    <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                            class="ml-auto">${item.mobile || ''}</span></p>
+                                    <span class="text-muted">${item.role || ''}</span>
+                                </dd>`
+                            }
+                        });
+                        html += '</div>'
+                    })
+                    $('.book-list').empty()
+                    $('.book-list').append(html)
+                }
+            }
+        }, 400);
+    })
+    if (!inspection.readOnly) {
+        const checkDate = $('#check_date').datepicker({
+            autoClose: true,
+            onSelect: function (formattedDate, date, inst) {
+                if (!date && inspection.check_date) {
+                    toastr.error('检查日期不能为空');
+                    checkDate.selectDate(inspection.check_date ? new Date(inspection.check_date) : new Date());
+                    return;
+                }
+                // 判断日期格式
+                const check_date = moment(date).format('YYYY-MM-DD');
+                const reg = /^\d{4}-\d{2}-\d{2}$/;
+                if (!reg.test(check_date)) {
+                    toastr.error('检查日期格式错误,应为YYYY-MM-DD。');
+                    return;
+                }
+                if (check_date !== moment(inspection.check_date).format('YYYY-MM-DD')) {
+                    updateInspection('check_date', check_date);
+                }
+
+            }
+        }).data('datepicker');
+        checkDate.selectDate(inspection.check_date ? new Date(inspection.check_date) : new Date());
+
+        $('#check_table textarea').on('change', function (e) {
+            const value = $(this).val().trim();
+            const key = $(this).data('key');
+            if (value !== inspection[key]) {
+                updateInspection(key, value);
+            }
+        });
+
+        $("#check_table input").on('change', function (e) {
+            const value = $(this).val().trim();
+            const key = $(this).data('key');
+            if (key === 'check_date') {
+                if (!value && inspection.check_date) {
+                    toastr.error('检查日期不能为空');
+                    checkDate.selectDate(inspection.check_date ? new Date(inspection.check_date) : new Date());
+                    return;
+                }
+                // 判断日期格式
+                const reg = /^\d{4}-\d{2}-\d{2}$/;
+                if (!reg.test(value)) {
+                    toastr.error('检查日期格式错误,应为YYYY-MM-DD。');
+                    checkDate.selectDate(inspection.check_date ? new Date(inspection.check_date) : new Date());
+                    return;
+                }
+            }
+            if (value !== inspection[key]) {
+                updateInspection(key, value);
+            }
+        });
+
+        $('#check_table dl').on('click', 'dd', function () {
+            const id = parseInt($(this).data('id'))
+            if (id !== 0) {
+                const user = _.find(accountList, { id });
+                $('#inspector-set').html(`<span class="badge">
+                          ${user.name}
+                                <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">
+                              <a class="dropdown-item" href="javascript:void(0);">确认移除检查人?</a>
+                              <div class="dropdown-divider"></div>
+                              <div class="px-2 py-1 text-center">
+                                <button class="btn btn-sm btn-danger remove-btn">移除</button>
+                                <button class="btn btn-sm btn-secondary">取消</button>
+                              </div>
+                            </div>
+                          </span>
+                          </span>`);
+                $('#inspector-set').siblings('.dropdown').attr('style', 'display:none!important;');
+                updateInspection('inspector', user.name);
+            }
+        });
+
+        $('body').on('click', '#check_table .remove-btn', function () {
+            updateInspection('inspector', '');
+            $('#inspector-set').html('');
+            $('#inspector-set').siblings('.dropdown').show();
+        });
+
+        function updateInspection(field, value) {
+            const data = {
+                id: inspection.id,
+            };
+            data[field] = value;
+            postData(`${preUrl}/save`, {type: 'update-field', update: data}, function (result) {
+                inspection[field] = value;
+                if (field === 'check_date') {
+                    checkDate.selectDate(inspection.check_date ? new Date(inspection.check_date) : new Date());
+                }
+            }, function () {
+                if (field === 'check_date') {
+                    checkDate.selectDate(inspection.check_date ? new Date(inspection.check_date) : new Date());
+                } else {
+                    $(`#check_table textarea[data-key=${field}]`).val(inspection[field] || '');
+                }
+            });
+        }
+
+        // 添加到审批流程中
+        $('#shenpi_select dl').on('click', 'dd', function () {
+            const id = parseInt($(this).data('id'))
+            if (id !== 0) {
+                postData(preUrl + '/save', {type: 'add-audit', auditorId: id}, (datas) => {
+                    // <p class="m-0 ml-2"><small class="text-muted">中交第一公路工程局有限公司国道311线满别公路施工一分部</small></p>
+                    const html = [];
+                    // 如果是重新上报,添加到重新上报列表中
+                    const auditorshtml = [];
+                    for (const [index, data] of datas.entries()) {
+                        if (index !== 0) {
+                            html.push('<li class="list-group-item d-flex" auditorId="' + data[0].aid + '">');
+                            html.push(`<div class="col-auto">${index}</div>`);
+                            html.push('<div class="col">');
+                            for (const auditor of data) {
+                                html.push(`<div class="d-inline-block mx-1"><i class="fa fa-user text-muted"></i> ${auditor.name} <small class="text-muted">${auditor.role}</small></div>`);
+                            }
+                            html.push('</div>');
+                            html.push('<div class="col-auto">');
+                            if (data[0].audit_type !== auditType.key.common) {
+                                html.push(`<span class="badge badge-pill badge-${auditType.info[data[0].audit_type].class} badge-bg-small"><small>${auditType.info[data[0].audit_type].long}</small></span>`);
+                            }
+                            if (shenpi_status === shenpiConst.sp_status.sqspr || (shenpi_status === shenpiConst.sp_status.gdzs && index + 1 !== datas.length)) {
+                                html.push('<a href="javascript: void(0)" class="text-danger pull-right ml-1">移除</a>');
+                            }
+                            html.push('</div>');
+                            html.push('</li>');
+                        }
+                        // 添加新审批人流程修改
+                        auditorshtml.push('<li class="list-group-item d-flex justify-content-between align-items-center" data-auditorid="' + data[0].aid + '">');
+                        auditorshtml.push('<span class="mr-1"><i class="fa ' + (index === 0 ? 'fa-play-circle fa-rotate-90' : index + 1 === datas.length ? 'fa-stop-circle' : 'fa-chevron-circle-down') + '"></i></span>');
+                        auditorshtml.push('<span class="text-muted">');
+                        for (const auditor of data) {
+                            auditorshtml.push(`<small class="d-inline-block text-dark mx-1" title="${auditor.role}" data-auditorId="${auditor.uid}">${auditor.name}</small>`);
+                        }
+                        auditorshtml.push('</span>');
+                        auditorshtml.push('<div class="d-flex ml-auto">');
+                        if (data[0].audit_type !== auditType.key.common) {
+                            auditorshtml.push(`<span class="badge badge-pill badge-${auditType.info[data[0].audit_type].class} p-1"><small>${auditType.info[data[0].audit_type].short}</small></span>`);
+                        }
+                        if (index === 0) {
+                            auditorshtml.push('<span class="badge badge-light badge-pill ml-auto"><small>原报</small></span>');
+                        } else if (index + 1 === datas.length) {
+                            auditorshtml.push('<span class="badge badge-light badge-pill"><small>终审</small></span>');
+                        } else {
+                            auditorshtml.push('<span class="badge badge-light badge-pill"><small>' + transFormToChinese(index) + '审</small></span>');
+                        }
+                    }
+                    $('#auditors').html(html.join(''));
+                    $('#auditors2').html(auditorshtml.join(''));
+                });
+            }
+        });
+
+        // 删除审批人
+        $('body').on('click', '#auditors li a', function () {
+            const li = $(this).parents('li');
+            const data = {
+                type: 'del-audit',
+                auditorId: parseInt(li.attr('auditorId')),
+            };
+            postData(preUrl + '/save', data, (result) => {
+                li.remove();
+                for (const rst of result) {
+                    const aLi = $('li[auditorid=' + rst.aid + ']');
+                    $('div:first', aLi).text(rst.order);
+                }
+                // 删除左边审核人
+                $(`#auditors2 li[data-auditorid='${data.auditorId}']`).remove();
+                if ($('#auditors2 li').length !== 0 && !$('#auditors-list li i').hasClass('fa-stop-circle')) {
+                    console.log($('#auditors2 li').length - 1, $('#auditors2 li').eq($('#auditors2 li').length - 1).find('i'));
+                    $('#auditors2 li').eq($('#auditors2 li').length - 1).find('i')
+                        .removeClass('fa-chevron-circle-down').addClass('fa-stop-circle');
+                }
+                for (let i = 0; i < $('#auditors2 li').length; i++) {
+                    $('#auditors2 li').eq(i).find('.badge-pill').children('small').text(i === 0 ? '原报' : (i + 1 === $('#auditors2 li').length ? '终' : transFormToChinese(i)) + '审');
+                }
+            })
+        });
+
+        $('#del-inspection-btn').click(function() {
+            const text = $('#del-inspection-text').val().trim();
+            if (text.length === 0 || text !== '确认删除本次巡检') {
+                toastr.error('请正确输入“确认删除本次巡检”');
+                return;
+            }
+            postData(preUrl + '/save', {type: 'del-inspection' }, function (result) {
+                let link = `/sp/${spid}/safe/tender/${tender_id}/inspection`;
+                let orderSetting = getLocalCache('safe-inspection-'+ tender_id +'-list-order');
+                if (!orderSetting) orderSetting = 'time|desc';
+                const orders = orderSetting.split('|');
+                const filterData = [];
+                filterData.push('sort='+ orders[0]);
+                filterData.push('order=' + orders[1]);
+                if (getLocalCache('account-pageSize')) {
+                    filterData.push('pageSize=' + getLocalCache('account-pageSize'));
+                }
+                if (filterData.length > 0) {
+                    link += '?' + filterData.join('&');
+                }
+                window.location.href = link;
+            });
+        });
+
+        $('#judge-start-btn').click(function () {
+            const flag = !(inspection.code && inspection.check_item && inspection.check_date);
+            if (flag) {
+                toastr.warning('请完善巡检信息再提交');
+                return;
+            }
+            if ($('#auditors li').length === 0) {
+                if(shenpi_status === shenpiConst.sp_status.gdspl) {
+                    toastr.error('请联系管理员添加审批人');
+                } else {
+                    toastr.error('请先选择审批人,再上报数据');
+                }
+                return false;
+            }
+            $('#sp-done').modal('show');
+        });
+
+        $('#start-btn').click(function () {
+            $('#start-btn').prop('disabled', true);
+            postData(preUrl + '/save', { type: 'start-inspection' }, function (result) {
+                window.location.reload();
+            });
+        });
+    } else if (inspection.shenpiPower) {
+        // 添加到审批流程中
+        $('dl').on('click', 'dd', function () {
+            const id = parseInt($(this).data('id'))
+            if (id !== 0) {
+                const user = _.find(accountList, { id });
+                $('#rectification-user-set').html(`<span class="badge">
+                              ${user.name}
+                                    <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">
+                                  <a class="dropdown-item" href="javascript:void(0);">确认移除整改人?</a>
+                                  <div class="dropdown-divider"></div>
+                                  <div class="px-2 py-1 text-center">
+                                    <button class="btn btn-sm btn-danger remove-btn">移除</button>
+                                    <button class="btn btn-sm btn-secondary">取消</button>
+                                  </div>
+                                </div>
+                              </span>
+                              </span>`);
+                $('#rectification-uid').val(user.id);
+                $('#rectification-user-set').siblings('.dropdown').attr('style', 'display:none!important;');
+            }
+        });
+
+        // 删除审批人
+        $('body').on('click', '#rectification-user-set .remove-btn', function () {
+            $('#rectification-user-set').html('');
+            $('#rectification-uid').val('');
+            $('#rectification-user-set').siblings('.dropdown').show();
+        });
+
+        $('#approval-success-btn').click(function () {
+            if (inspection.finalAuditorIds.indexOf(cur_uid) !== -1 && $('#rectification-uid').val() === '') {
+                toastr.warning('请选择整改人');
+                return;
+            }
+            const opinion = $('#sp-done').find('textarea[name="opinion"]').eq(0).val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
+            if (opinion.length === 0) {
+                toastr.warning('请填写审核意见');
+                return;
+            }
+            const data = {
+                type: 'check',
+                checkType: auditConst.status.checked,
+                opinion,
+                rectification_uid: $('#rectification-uid').val(),
+            }
+            postData(preUrl + '/save', data, function (result) {
+                window.location.reload();
+            });
+        });
+
+        $('#approval-back-btn').click(function () {
+            console.log($('#sp-back').find('textarea[name="opinion"]').eq(0).val());
+            const opinion = $('#sp-back').find('textarea[name="opinion"]').eq(0).val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
+            if (opinion.length === 0) {
+                toastr.warning('请填写审核意见');
+                return;
+            }
+            const inlineRadio1 = $('#inlineRadio1:checked').val();
+            const inlineRadio2 = $('#inlineRadio2:checked').val();
+            if (!inlineRadio1 && !inlineRadio2) {
+                if (!$('#warning-text').length) {
+                    $('#reject-process').prepend('<p id="warning-text" style="color: red; margin: 0;">请选择退回流程</p>');
+                }
+                return;
+            }
+            if ($('#warning-text').length) $('#warning-text').remove()
+            const data = {
+                type: 'check',
+                checkType: parseInt(inlineRadio1 ? inlineRadio1 : inlineRadio2),
+                opinion,
+            }
+            postData(preUrl + '/save', data, function (result) {
+                window.location.reload();
+            });
+        });
+
+        $('#approval-stop-btn').click(function () {
+            const opinion = $('#sp-close').find('textarea[name="opinion"]').eq(0).val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
+            if (opinion.length === 0) {
+                toastr.warning('请填写关闭原因');
+                return;
+            }
+            const data = {
+                type: 'check',
+                checkType: auditConst.status.checkStop,
+                opinion,
+            }
+            postData(preUrl + '/save', data, function (result) {
+                window.location.reload();
+            });
+        });
+    } else if (inspection.rectificationPower) {
+        $('#judge-success-btn').click(function () {
+            const flag = !(inspection.rectification_item && inspection.rectification_date);
+            if (flag) {
+                toastr.warning('请完善整改单再提交');
+                return;
+            }
+            $('#sp-done').modal('show');
+        });
+        // 整改完成
+        $('#rectification-success-btn').click(function () {
+            const opinion = $('#sp-done').find('textarea[name="opinion"]').eq(0).val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
+            if (opinion.length === 0) {
+                toastr.warning('请填写审核意见');
+                return;
+            }
+            const data = {
+                type: 'rectification',
+                checkType: auditConst.status.checked,
+                opinion,
+            }
+            postData(preUrl + '/save', data, function (result) {
+                window.location.reload();
+            });
+        });
+
+        $('#rectification-back-btn').click(function () {
+            const opinion = $('#sp-back').find('textarea[name="opinion"]').eq(0).val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
+            if (opinion.length === 0) {
+                toastr.warning('请填写审核意见');
+                return;
+            }
+            const data = {
+                type: 'rectification',
+                checkType: auditConst.status.checkNoPre,
+                opinion,
+            }
+            postData(preUrl + '/save', data, function (result) {
+                window.location.reload();
+            });
+        });
+
+        const rectificationDate = $('#rectification_date').datepicker({
+            autoClose: true,
+            onSelect: function (formattedDate, date, inst) {
+                if (!date && inspection.rectification_date) {
+                    toastr.error('检查日期不能为空');
+                    rectificationDate.selectDate(inspection.rectification_date ? new Date(inspection.rectification_date) : '');
+                    return;
+                }
+                // 判断日期格式
+                const rectification_date = moment(date).format('YYYY-MM-DD');
+                const reg = /^\d{4}-\d{2}-\d{2}$/;
+                if (!reg.test(rectification_date)) {
+                    toastr.error('整改日期格式错误,应为YYYY-MM-DD。');
+                    return;
+                }
+                if (rectification_date !== moment(inspection.rectification_date).format('YYYY-MM-DD')) {
+                    updateInspection('rectification_date', rectification_date);
+                }
+
+            }
+        }).data('datepicker');
+        rectificationDate.selectDate(inspection.rectification_date ? new Date(inspection.rectification_date) : '');
+
+        $('#rectification_table textarea').on('change', function (e) {
+            const value = $(this).val().trim();
+            const key = $(this).data('key');
+            if (value !== inspection[key]) {
+                updateInspection(key, value);
+            }
+        });
+
+        $("#rectification_table input").on('change', function (e) {
+            const value = $(this).val().trim();
+            const key = $(this).data('key');
+            if (key === 'check_date') {
+                if (!value && inspection.rectification_date) {
+                    toastr.error('检查日期不能为空');
+                    rectificationDate.selectDate(inspection.rectification_date ? new Date(inspection.rectification_date) : '');
+                    return;
+                }
+                // 判断日期格式
+                const reg = /^\d{4}-\d{2}-\d{2}$/;
+                if (!reg.test(value)) {
+                    toastr.error('检查日期格式错误,应为YYYY-MM-DD。');
+                    rectificationDate.selectDate(inspection.rectification_date ? new Date(inspection.rectification_date) : '');
+                    return;
+                }
+            }
+            if (value !== inspection[key]) {
+                updateInspection(key, value);
+            }
+        });
+
+        function updateInspection(field, value) {
+            const data = {
+                id: inspection.id,
+            };
+            data[field] = value;
+            console.log(data);
+            postData(`${preUrl}/save`, {type: 'update-field', update: data}, function (result) {
+                inspection[field] = value;
+                if (field === 'rectification_date') {
+                    rectificationDate.selectDate(inspection.rectification_date ? new Date(inspection.rectification_date) : '');
+                }
+            }, function () {
+                if (field === 'rectification_date') {
+                    rectificationDate.selectDate(inspection.rectification_date ? new Date(inspection.rectification_date) : '');
+                } else {
+                    $(`#rectification_table textarea[data-key=${field}]`).val(inspection[field] || '');
+                }
+            });
+        }
+    }
+
+    handleFileList(fileList);
+
+    $('#file-ok').click(function () {
+        const files = Array.from($('#file-modal')[0].files)
+        const valiData = files.map(v => {
+            const ext = v.name.substring(v.name.lastIndexOf('.') + 1)
+            return {
+                size: v.size,
+                ext
+            }
+        });
+        if (validateFiles(valiData)) {
+            if (files.length) {
+                const formData = new FormData();
+                files.forEach(file => {
+                    formData.append('name', file.name);
+                    formData.append('size', file.size);
+                    formData.append('file', file);
+                })
+                postDataWithFile(`${preUrl}/file/upload`, formData, function (result) {
+                    handleFileList(result);
+                    $('#file-modal').val('');
+                    $('#file-cancel').click();
+                });
+            }
+        }
+    })
+    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
+                }
+            }
+            return {...file, showDel}
+        })
+        let html = 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>` : '';
+        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>`
+            }
+        })
+        $('#file-content').append(html);
+    }
+
+    $('#file-content').on('click', 'a', function () {
+        if ($(this).hasClass('file-del')) {
+            const id = $(this).data('id');
+            postData(`${preUrl}/file/delete`, {id}, (result) => {
+                handleFileList(result);
+            })
+        }
+    });
+})
+
+
+/**
+ * 校验文件大小、格式
+ * @param {Array} files 文件数组
+ */
+function validateFiles(files) {
+    if (files.length > 10) {
+        toastr.error('至多同时上传10个文件');
+        return false
+    }
+    return files.every(file => {
+        if (file.size > 1024 * 1024 * 50) {
+            toastr.error('文件大小限制为50MB');
+            return false
+        }
+        if (whiteList.indexOf('.' + file.ext) === -1) {
+            toastr.error('请上传正确的格式文件');
+            return false
+        }
+        return true
+    })
+}
+

+ 58 - 0
app/public/js/safe_tender.js

@@ -0,0 +1,58 @@
+'use strict';
+
+const tenderListSpec = (function(){
+    function getTenderNodeHtml(node, arr, pid) {
+        const html = [];
+        html.push('<tr pid="' + pid + '">');
+        // 名称
+        html.push('<td style="min-width: 200px" class="in-' + node.level + '">');
+        if (node.cid) {
+            html.push('<span onselectstart="return false" style="{-moz-user-select:none}" class="fold-switch mr-1" title="收起" cid="'+ node.sort_id +'"><i class="fa fa-minus-square-o"></i></span> <i class="fa fa-folder-o"></i> ', node.name);
+        } else {
+            html.push('<span class="text-muted mr-2">');
+            html.push(arr.indexOf(node) === arr.length - 1 ? '└' : '├');
+            html.push('</span>');
+            //html.push('<a href="/tender/' + node.id + '">', node[c.field], '</a>');
+            html.push(`<a href="/sp/${spid}/safe/tender/${node.id}/${isInspection ? 'inspection' : 'info'}" name="name" style="min-width: 200px;word-break:break-all;" id="${node.id}">${node.name}</a>`);
+        }
+        html.push('</td>');
+
+        // 创建时间
+        html.push('<td style="width: 8%" class="text-center">');
+        html.push(node.create_time ? moment(node.create_time).format('YYYY-MM-DD HH:mm:ss') : '');
+        html.push('</td>');
+        // 设置
+        if (is_admin) {
+            html.push('<td style="width: 10%" class="text-center">');
+            if (!node.cid) {
+                html.push(`<a href="javascript:void(0);" data-toggle="modal" data-tid="${node.id}" class="btn btn-sm btn-outline-primary member-manage"> 成员管理 </a>`);
+            }
+            html.push('</td>');
+        }
+        html.push('</tr>');
+        return html.join('');
+    }
+    function getTenderTreeHeaderHtml() {
+        const html = [];
+        const left = $('#sub-menu').css('display') === 'none' ? 56 : 176;
+        html.push('<table class="table table-hover table-bordered">');
+        html.push('<thead style="position: sticky;left:'+ left +'px;top: 0;" class="text-center">', '<tr>');
+        html.push('<th style="min-width: 50%">',  '标段名称',  tenderListOrder.getOrderButton('name'), '</th>');
+        html.push('<th style="width: 15%">', '创建时间',  tenderListOrder.getOrderButton('create_time'), '</th>');
+        if (is_admin) {
+            html.push('<th style="width: 15%">', '操作', '</th>');
+        }
+        html.push('</tr>');
+        html.push('</thead>');
+        return html.join('');
+    }
+    return { getTenderNodeHtml, getTenderTreeHeaderHtml }
+})();
+
+$(document).ready(() => {
+    const memberPermission = MemberPermission();
+    $('.member-manage').click(function(){
+        const tid = this.getAttribute('data-tid');
+        memberPermission.show({ data: { tid }, loadUrl: `/sp/${spid}/quality/member`, saveUrl: `/sp/${spid}/quality/memberSave`});
+    });
+});

+ 50 - 60
app/public/js/setting_manage.js

@@ -177,8 +177,10 @@ $(document).ready(() => {
         construction: '施工日志',
         quality: '质量管理',
         inspection: '质量巡检',
+        safe_inspection: '安全巡检',
     };
-    const tabTypeKeys = ['tourist', 'schedule', 'contract', 'construction', 'quality', 'inspection'];
+    const tabTypeKeys = ['tourist', 'schedule', 'contract', 'construction', 'quality', 'inspection', 'safe_inspection'];
+    const tenderPermissionKeys = ['quality', 'inspection', 'safe_inspection'];
 
     const $filterTenderDone = $('body #filter-tender-done')
     if (window.location.search && window.location.search.split('done=')[1]) {
@@ -215,8 +217,9 @@ $(document).ready(() => {
             setScheduleHtml(result.scheduleAuditList);
             setContractHtml(result.contractAuditList);
             setConstructionHtml(result.constructionAuditList);
-            setTenderPermissionHtml(result.qualityAuditList, 'quality');
-            setTenderPermissionHtml(result.inspectionAuditList, 'inspection');
+            for (const tpkey of tenderPermissionKeys) {
+                setTenderPermissionHtml(result[tpkey + 'AuditList'], tpkey);
+            }
             resetAddUserHtml();
         });
     });
@@ -241,10 +244,8 @@ $(document).ready(() => {
                 $('#contract-tip').show();
             } else if ($(this).attr('href') === '#sgrz') {
                 $('#add_user_dropdownMenuButton').attr('data-type', 'construction');
-            } else if ($(this).attr('href') === '#zlgl') {
-                $('#add_user_dropdownMenuButton').attr('data-type', 'quality');
-            } else if ($(this).attr('href') === '#zlxj') {
-                $('#add_user_dropdownMenuButton').attr('data-type', 'inspection');
+            } else if (_.includes(tenderPermissionKeys, $(this).attr('href').substring(1))) {
+                $('#add_user_dropdownMenuButton').attr('data-type', $(this).attr('href').substring(1));
             }
         }
     });
@@ -325,52 +326,29 @@ $(document).ready(() => {
         postData('/sp/' + spid + '/construction/' + cur_tenderid + '/audit/save', prop, function (data) {
         });
     });
-
-    // 权限更改
-    $('body').on('click', '#quality-users input[type="checkbox"]', function () {
-        const uid = parseInt($(this).parents('tr').data('uid'));
-        const member = {
-            uid,
-            quality: [1],
-        };
-        $(this).parents('tr').find('input[type="checkbox"]').each(function () {
-            if ($(this).is(':checked')) {
-                member.quality.push($(this).data('value'));
-            }
-        });
-        const prop = {
-            type: 'save-permission',
-            uid,
-            members: [member],
-            key: 'quality',
-        };
-        const _self = $(this);
-        postData('/sp/' + spid + '/quality/' + cur_tenderid + '/audit/save', prop, function (data) {
-        });
-    });
-
-    // 权限更改
-    $('body').on('click', '#inspection-users input[type="checkbox"]', function () {
-        const uid = parseInt($(this).parents('tr').data('uid'));
-        const member = {
-            uid,
-            inspection: [1],
-        };
-        $(this).parents('tr').find('input[type="checkbox"]').each(function () {
-            if ($(this).is(':checked')) {
-                member.inspection.push($(this).data('value'));
-            }
-        });
-        const prop = {
-            type: 'save-permission',
-            uid,
-            members: [member],
-            key: 'inspection',
-        };
-        const _self = $(this);
-        postData('/sp/' + spid + '/quality/' + cur_tenderid + '/audit/save', prop, function (data) {
+    for (const tpKey of tenderPermissionKeys) {
+        $('body').on('click', `#${tpKey}-users input[type="checkbox"]`, function () {
+            const uid = parseInt($(this).parents('tr').data('uid'));
+            const member = {
+                uid,
+            };
+            member[tpKey] = [1];
+            $(this).parents('tr').find('input[type="checkbox"]').each(function () {
+                if ($(this).is(':checked')) {
+                    member[tpKey].push($(this).data('value'));
+                }
+            });
+            const prop = {
+                type: 'save-permission',
+                uid,
+                members: [member],
+                key: tpKey,
+            };
+            const _self = $(this);
+            postData('/sp/' + spid + '/quality/' + cur_tenderid + '/audit/save', prop, function (data) {
+            });
         });
-    });
+    }
 
     for (const key of tabTypeKeys) {
         $('body').on('click', `#${key}-users .remove-${key}-user`, function () {
@@ -397,7 +375,7 @@ $(document).ready(() => {
                 $('#'+ type + '-users').find('tr[data-id="'+ id +'"]').remove();
                 $('#remove-user').modal('hide');
             });
-        } else if (type === 'quality' || type === 'inspection') {
+        } else if (_.includes(tenderPermissionKeys, type)) {
             postData('/sp/' + spid + '/quality/' + cur_tenderid + '/audit/save', { type: 'del-audit', id, key: type }, function (data) {
                 $('#'+ type + '-users').find('tr[data-uid="'+ id +'"]').remove();
                 $('#remove-user').modal('hide');
@@ -606,7 +584,7 @@ $(document).ready(() => {
                 postData('/sp/' + spid + '/construction/' + cur_tenderid + '/audit/save', prop, function (datas) {
                     setConstructionHtml(datas);
                 });
-            } else if (type === 'quality' || type === 'inspection') {
+            } else if (_.includes(tenderPermissionKeys, type)) {
                 const user = _.find(accountList, function (item) {
                     return item.id === id;
                 });
@@ -775,14 +753,26 @@ $(document).ready(() => {
                 }
             } else if (userType === 'construction') {
                 userData.is_report = $('#construction-users tr').eq(i).find('input[type="checkbox"]').eq(0).is(':checked') ? 1 : 0;
-            } else if (userType === 'quality') {
+            } else if (_.includes(tenderPermissionKeys, userType)) {
                 userData.member = [1];
-                if ($('#quality-users tr').eq(i).find('input[data-block="upload"]').eq(0).is(':checked')) userData.member.push(2);
-                if ($('#quality-users tr').eq(i).find('input[data-block="add"]').eq(0).is(':checked')) userData.member.push(3);
-            } else if (userType === 'inspection') {
-                userData.member = [1];
-                if ($('#inspection-users tr').eq(i).find('input[data-block="add"]').eq(0).is(':checked')) userData.member.push(2);
+                $('#'+ userType +'-users tr').eq(i).find('input[type="checkbox"]').each(function () {
+                    if ($(this).is(':checked')) {
+                        userData.member.push(parseInt($(this).attr('data-block')));
+                    }
+                });
             }
+            // else if (userType === 'quality') {
+            //     userData.member = [1];
+            //     if ($('#quality-users tr').eq(i).find('input[data-block="upload"]').eq(0).is(':checked')) userData.member.push(2);
+            //     if ($('#quality-users tr').eq(i).find('input[data-block="add"]').eq(0).is(':checked')) userData.member.push(3);
+            // } else if (userType === 'inspection') {
+            //     userData.member = [1];
+            //     if ($('#inspection-users tr').eq(i).find('input[data-block="add"]').eq(0).is(':checked')) userData.member.push(2);
+            // } else if (userType === 'safe_inspection') {
+            //     userData.member = [1];
+            //     if ($('#safe_inspection-users tr').eq(i).find('input[data-block="add"]').eq(0).is(':checked')) userData.member.push(2);
+            //     if ($('#safe_inspection-users tr').eq(i).find('input[data-block="view_all"]').eq(0).is(':checked')) userData.member.push(3);
+            // }
             saIdList.push(userData);
         }
         data.auditList = saIdList;

+ 10 - 0
app/public/js/wap/global.js

@@ -112,6 +112,16 @@ const postDataWithFile = function (url, formData, successCallback, errorCallBack
     });
 };
 
+const postDataWithFileAsync = function (url, formData, showWaiting = true) {
+    return new Promise(function (resolve, reject) {
+        postDataWithFile(url, formData, result => {
+            resolve(result);
+        }, err => {
+            reject(err);
+        }, showWaiting);
+    });
+};
+
 /**
  * 提示框
  *

+ 19 - 6
app/router.js

@@ -60,7 +60,9 @@ module.exports = app => {
     const financialPayCheck = app.middlewares.financialPayCheck();
     const financialPayAuditCheck = app.middlewares.financialPayAuditCheck();
     // 质量巡检中间件
-    const inspectionCheck = app.middlewares.inspectionCheck();
+    const qualityInspectionCheck = app.middlewares.qualityInspectionCheck();
+    // 安全巡检中间件
+    const safeInspectionCheck = app.middlewares.safeInspectionCheck();
     // 登入登出相关
     app.get('/login', 'loginController.index');
     app.get('/login/:code', 'loginController.index');
@@ -488,11 +490,21 @@ module.exports = app => {
     app.post('/sp/:id/quality/tender/:tid/rule/save', sessionAuth, subProjectCheck, tenderCheck, projectManagerCheck, 'qualityController.ruleSave');
     app.get('/sp/:id/quality/tender/:tid/inspection', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'qualityController.inspection');
     app.post('/sp/:id/quality/tender/:tid/inspection/save', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'qualityController.inspectionSave');
-    app.get('/sp/:id/quality/tender/:tid/inspection/:qiid/information', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, inspectionCheck, 'qualityController.inspectionInformation');
-    app.post('/sp/:id/quality/tender/:tid/inspection/:qiid/information/save', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, inspectionCheck, 'qualityController.inspectionInformationSave');
-    app.post('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/upload', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, inspectionCheck, 'qualityController.uploadInspectionFile');
-    app.post('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, inspectionCheck, 'qualityController.deleteInspectionFile');
-    app.get('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/:fid/download', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, inspectionCheck, 'qualityController.downloadInspectionFile');
+    app.get('/sp/:id/quality/tender/:tid/inspection/:qiid/information', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.inspectionInformation');
+    app.post('/sp/:id/quality/tender/:tid/inspection/:qiid/information/save', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.inspectionInformationSave');
+    app.post('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/upload', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.uploadInspectionFile');
+    app.post('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.deleteInspectionFile');
+    app.get('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/:fid/download', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.downloadInspectionFile');
+
+    // 安全管理
+    app.get('/sp/:id/safe/inspection', sessionAuth, subProjectCheck, 'safeController.inspectionTender');
+    app.get('/sp/:id/safe/tender/:tid/inspection', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'safeController.inspection');
+    app.post('/sp/:id/safe/tender/:tid/inspection/save', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'safeController.inspectionSave');
+    app.get('/sp/:id/safe/tender/:tid/inspection/:qiid/information', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, safeInspectionCheck, 'safeController.inspectionInformation');
+    app.post('/sp/:id/safe/tender/:tid/inspection/:qiid/information/save', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, safeInspectionCheck, 'safeController.inspectionInformationSave');
+    app.post('/sp/:id/safey/tender/:tid/inspection/:qiid/information/file/upload', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, safeInspectionCheck, 'safeController.uploadInspectionFile');
+    app.post('/sp/:id/safe/tender/:tid/inspection/:qiid/information/file/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, safeInspectionCheck, 'safeController.deleteInspectionFile');
+    app.get('/sp/:id/safe/tender/:tid/inspection/:qiid/information/file/:fid/download', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, safeInspectionCheck, 'safeController.downloadInspectionFile');
 
     // ------------------------- 项目内部相关 -----------------------------
 
@@ -1079,6 +1091,7 @@ module.exports = app => {
     app.get('/wap/inspection', 'wapController.inspection');
     app.post('/wap/inspection/ask', 'wapController.inspectionAiAsk');
     app.post('/wap/voice/token', 'wapController.voiceToken');
+    app.post('/wap/voice/oneshot', 'wapController.voiceOneShot');
 
     // 微信
     app.get('/wx', 'wechatController.index');

+ 315 - 0
app/service/safe_inspection.js

@@ -0,0 +1,315 @@
+'use strict';
+
+/**
+ * 质量管理 - 巡检单
+ *
+ * @author Mai
+ * @date 2024/7/22
+ * @version
+ */
+const auditConst = require('../const/audit').inspection;
+const auditType = require('../const/audit').auditType;
+module.exports = app => {
+
+    class SafeInspection extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'safe_inspection';
+        }
+
+        async loadUser(inspection) {
+            const status = auditConst.status;
+            const accountId = this.ctx.session.sessionUser.accountId;
+
+            inspection.user = await this.ctx.service.projectAccount.getAccountInfoById(inspection.uid);
+            if (inspection.rectification_uid) {
+                inspection.rectification_user = await this.ctx.service.projectAccount.getAccountInfoById(inspection.rectification_uid);
+            }
+            inspection.auditors = await this.ctx.service.safeInspectionAudit.getAuditors(inspection.id, inspection.times); // 全部参与的审批人
+            inspection.auditorIds = this._.map(inspection.auditors, 'aid');
+            inspection.curAuditors = inspection.auditors.filter(x => { return x.status === status.checking || x.status === status.rectification; }); // 当前流程中审批中的审批人
+            inspection.curAuditorIds = this._.map(inspection.curAuditors, 'aid');
+            inspection.flowAuditors = inspection.curAuditors.length > 0 ? inspection.auditors.filter(x => { return x.order === inspection.curAuditors[0].order; }) : []; // 当前流程中参与的审批人(包含会签时,审批通过的人)
+            inspection.flowAuditorIds = this._.map(inspection.flowAuditors, 'aid');
+            inspection.nextAuditors = inspection.curAuditors.length > 0 ? inspection.auditors.filter(x => { return x.order === inspection.curAuditors[0].order + 1; }) : [];
+            inspection.nextAuditorIds = this._.map(inspection.nextAuditors, 'aid');
+            const newAuditors = inspection.auditors.filter(x => { return x.is_old === 0 && x.is_rectification === 0; });
+            inspection.auditorGroups = this.ctx.helper.groupAuditors(newAuditors);
+            inspection.userGroups = this.ctx.helper.groupAuditorsUniq(inspection.auditorGroups);
+            inspection.userGroups.unshift([{
+                aid: inspection.user.id, order: 0, times: inspection.times, audit_order: 0, audit_type: auditType.key.common,
+                name: inspection.user.name, role: inspection.user.role, company: inspection.user.company,
+            }]);
+            inspection.finalAuditorIds = inspection.userGroups[inspection.userGroups.length - 1].map(x => { return x.aid; });
+        }
+
+        async loadAuditViewData(inspection) {
+            const times = inspection.status === auditConst.status.checkNo ? inspection.times - 1 : inspection.times;
+
+            if (!inspection.user) inspection.user = await this.ctx.service.projectAccount.getAccountInfoById(inspection.uid);
+            inspection.auditHistory = await this.ctx.service.safeInspectionAudit.getAuditorHistory(inspection.id, times);
+            // 获取审批流程中左边列表
+            if (inspection.status === auditConst.status.checkNo && inspection.uid !== this.ctx.session.sessionUser.accountId) {
+                const auditors = await this.ctx.service.safeInspectionAudit.getAuditors(inspection.id, times); // 全部参与的审批人
+                const newAuditors = auditors.filter(x => { return x.is_old === 0 && x.is_rectification === 0; });
+                const auditorGroups = this.ctx.helper.groupAuditors(newAuditors);
+                inspection.auditors2 = this.ctx.helper.groupAuditorsUniq(auditorGroups);
+                inspection.auditors2.unshift([{
+                    aid: inspection.user.id, order: 0, times: inspection.times - 1, audit_order: 0, audit_type: auditType.key.common,
+                    name: inspection.user.name, role: inspection.user.role, company: inspection.user.company,
+                }]);
+            } else {
+                inspection.auditors2 = inspection.userGroups;
+            }
+            if (inspection.status === auditConst.status.uncheck || inspection.status === auditConst.status.checkNo) {
+                inspection.auditorList = await this.ctx.service.safeInspectionAudit.getAuditors(inspection.id, inspection.times);
+            }
+        }
+
+        async add(tenderId, userId, code, check_item, check_date) {
+            const sql = 'SELECT COUNT(*) as count FROM ?? WHERE `tid` = ? AND `code` = ?';
+            const sqlParam = [this.tableName, tenderId, code];
+            const codeCount = await this.db.queryOne(sql, sqlParam);
+            const count = codeCount.count;
+            if (count > 0) {
+                throw '编号重复';
+            }
+
+            // 初始化事务
+            const transaction = await this.db.beginTransaction();
+            let result = false;
+            try {
+                const inspection = {
+                    tid: tenderId,
+                    uid: userId,
+                    status: auditConst.status.uncheck,
+                    times: 1,
+                    code,
+                    check_item,
+                    check_date,
+                    inspector: this.ctx.session.sessionUser.name,
+                    create_time: new Date(),
+                };
+                const operate = await transaction.insert(this.tableName, inspection);
+
+                if (operate.affectedRows <= 0) {
+                    throw '新建质量巡检数据失败';
+                }
+                inspection.id = operate.insertId;
+                // 先找出标段最近存在的变更令审批人的变更令info
+                const preChangeInfo = await this.getHaveAuditLastInfo(tenderId);
+                if (preChangeInfo) {
+                    // 并把之前存在的变更令审批人添加到zh_change_audit
+                    const auditResult = await this.ctx.service.safeInspectionAudit.copyPreAuditors(transaction, preChangeInfo, inspection);
+                    if (!auditResult) {
+                        throw '复制上一次审批流程失败';
+                    }
+                }
+                result = inspection;
+                await transaction.commit();
+            } catch (error) {
+                console.log(error);
+                // 回滚
+                await transaction.rollback();
+            }
+            return result;
+        }
+
+        async delInspection(id) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 删除预付款记录
+                await transaction.delete(this.tableName, { id });
+                // 删除附件
+                const fileInfo = await this.db.select(this.ctx.service.safeInspectionAtt.tableName, { where: { qiid: id } });
+                await transaction.delete(this.ctx.service.safeInspectionAtt.tableName, { qiid: id });
+                await this.ctx.helper.delFiles(fileInfo);
+                // 先删除文件
+                // for (let i = 0; i < fileInfo.length; i++) {
+                //     const file = fileInfo[i];
+                //     if (fs.existsSync(path.resolve(this.app.baseDir, './app', file.filepath))) {
+                //         fs.unlinkSync(path.resolve(this.app.baseDir, './app', file.filepath));
+                //         // fs.unlinkSync(path.resolve(this.app.baseDir, zipPath));
+                //     }
+                //     await this.ctx.app.fujianOss.delete(file.filepath);
+                // }
+                // 删除审批记录
+                await transaction.delete(this.ctx.service.safeInspectionAudit.tableName, { qiid: id });
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async getHaveAuditLastInfo(tenderId) {
+            const sql = 'SELECT a.* FROM ?? as a LEFT JOIN ?? as b ON a.`id` = b.`qiid` WHERE a.`tid` = ? ORDER BY a.`create_time` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.safeInspectionAudit.tableName, tenderId];
+            return await this.db.queryOne(sql, sqlParam);
+        }
+
+        async getListByStatus(tid, status, hadlimit = 0, sortBy = '', orderBy = '') {
+            let sql = '';
+            let sqlParam = '';
+            if ((this.ctx.tender.isTourist || this.ctx.session.sessionUser.is_admin) && status === 0) {
+                sql = 'SELECT a.* FROM ?? As a WHERE a.tid = ?';
+                sqlParam = [this.tableName, tid];
+            } else {
+                switch (status) {
+                    case 0: // 所有
+                        sql =
+                            'SELECT a.* FROM ?? AS a WHERE a.tid = ? AND' +
+                            ' (a.uid = ? OR (a.status != ? AND a.id IN (SELECT b.qiid FROM ?? AS b WHERE b.aid = ? GROUP BY b.qiid))' +
+                            ' OR a.status = ?)';
+                        sqlParam = [
+                            this.tableName,
+                            tid,
+                            this.ctx.session.sessionUser.accountId,
+                            auditConst.status.uncheck,
+                            this.ctx.service.safeInspectionAudit.tableName,
+                            this.ctx.session.sessionUser.accountId,
+                            auditConst.status.checked,
+                        ];
+                        break;
+                    case auditConst.filter.status.pending: // 待处理(你的)
+                        sql = 'SELECT a.* FROM ?? as a WHERE a.tid = ? AND (a.id in(SELECT b.qiid FROM ?? as b WHERE b.tid = ? AND b.aid = ? AND (b.status = ? OR b.status = ?)) OR (a.uid = ? AND (a.status = ? OR a.status = ?)))';
+                        sqlParam = [this.tableName, tid, this.ctx.service.safeInspectionAudit.tableName, tid, this.ctx.session.sessionUser.accountId, auditConst.status.checking, auditConst.status.rectification, this.ctx.session.sessionUser.accountId, auditConst.status.uncheck, auditConst.status.checkNo];
+                        break;
+                    case auditConst.filter.status.uncheck: // 待上报(所有的)PS:取未上报,退回,修订的变更令
+                        sql =
+                            'SELECT a.* FROM ?? AS a WHERE ' +
+                            'a.uid = ? AND a.tid = ? AND (a.status = ? OR a.status = ?)';
+                        sqlParam = [
+                            this.tableName,
+                            this.ctx.session.sessionUser.accountId,
+                            tid,
+                            auditConst.status.uncheck,
+                            auditConst.status.checkNo,
+                        ];
+                        break;
+                    case auditConst.filter.status.checking: // 进行中(所有的)
+                        sql =
+                            'SELECT a.* FROM ?? AS a WHERE ' +
+                            '(a.status = ? OR a.status = ?) AND a.tid = ?' +
+                            (this.ctx.session.sessionUser.is_admin ? '' : ' AND a.id IN (SELECT b.qiid FROM ?? AS b WHERE b.aid = ? GROUP BY b.qiid)');
+                        sqlParam = [this.tableName, status, auditConst.status.checkNoPre, tid, this.ctx.service.safeInspectionAudit.tableName, this.ctx.session.sessionUser.accountId];
+                        break;
+                    case auditConst.filter.status.rectification: // 整改中(所有的)
+                    case auditConst.filter.status.checkStop: // 终止(所有的)
+                        sql =
+                            'SELECT a.* FROM ?? AS a WHERE ' +
+                            'a.status = ? AND a.tid = ?' +
+                            (this.ctx.session.sessionUser.is_admin ? '' : ' AND a.id IN (SELECT b.qiid FROM ?? AS b WHERE b.aid = ? GROUP BY b.qiid)');
+                        sqlParam = [this.tableName, status, tid, this.ctx.service.safeInspectionAudit.tableName, this.ctx.session.sessionUser.accountId];
+                        break;
+                    case auditConst.filter.status.checked: // 已完成(所有的)
+                        sql = 'SELECT a.* FROM ?? as a WHERE a.status = ? AND a.tid = ?';
+                        sqlParam = [this.tableName, status, tid];
+                        break;
+                    default:
+                        break;
+                }
+            }
+            if (sortBy && orderBy) {
+                if (sortBy === 'code') {
+                    sql += ' ORDER BY CHAR_LENGTH(a.code) ' + orderBy + ',convert(a.code using gbk) ' + orderBy;
+                } else {
+                    sql += ' ORDER BY a.create_time ' + orderBy;
+                }
+            } else {
+                sql += ' ORDER BY a.create_time DESC';
+            }
+            if (hadlimit) {
+                const limit = this.ctx.pageSize ? this.ctx.pageSize : this.app.config.pageSize;
+                const offset = limit * (this.ctx.page - 1);
+                const limitString = offset >= 0 ? offset + ',' + limit : limit;
+                sql += ' LIMIT ' + limitString;
+            }
+            const list = await this.db.query(sql, sqlParam);
+            return list;
+        }
+
+        /**
+         * 获取支付个数
+         * @param {int} spid - 项目id
+         * @param {int} status - 状态
+         * @return {void}
+         */
+        async getCountByStatus(tid, status = 0) {
+            if ((this.ctx.tender.isTourist || this.ctx.session.sessionUser.is_admin) && status === 0) {
+                const sql5 = 'SELECT count(*) AS count FROM ?? AS a WHERE a.tid = ?';
+                const sqlParam5 = [this.tableName, tid];
+                const result5 = await this.db.query(sql5, sqlParam5);
+                return result5[0].count;
+            }
+            switch (status) {
+                case 0: // 所有
+                    const sql =
+                        'SELECT count(*) AS count FROM ?? AS a WHERE a.tid = ? AND ' +
+                        '(a.uid = ? OR (a.status != ? AND a.id IN (SELECT b.qiid FROM ?? AS b WHERE b.aid = ? GROUP BY b.qiid)) OR a.status = ?)';
+                    const sqlParam = [
+                        this.tableName,
+                        tid,
+                        this.ctx.session.sessionUser.accountId,
+                        auditConst.status.uncheck,
+                        this.ctx.service.safeInspectionAudit.tableName,
+                        this.ctx.session.sessionUser.accountId,
+                        auditConst.status.checked,
+                    ];
+                    const result = await this.db.query(sql, sqlParam);
+                    return result[0].count;
+                case auditConst.filter.status.pending: // 待处理(你的)
+                    const sql6 = 'SELECT count(*) AS count FROM ?? as a WHERE a.tid = ? AND (a.id in(SELECT b.qiid FROM ?? as b WHERE b.tid = ? AND b.aid = ? AND (b.status = ? OR b.status = ?)) OR (a.uid = ? AND (a.status = ? OR a.status = ?)))';
+                    const sqlParam6 = [this.tableName, tid, this.ctx.service.safeInspectionAudit.tableName, tid, this.ctx.session.sessionUser.accountId, auditConst.status.checking, auditConst.status.rectification, this.ctx.session.sessionUser.accountId, auditConst.status.uncheck, auditConst.status.checkNo];
+                    const result6 = await this.db.query(sql6, sqlParam6);
+                    return result6[0].count;
+                case auditConst.filter.status.uncheck: // 待上报(所有的)PS:取未上报,退回的变更立项
+                    const sql2 =
+                        'SELECT count(*) AS count FROM ?? AS a WHERE ' +
+                        'a.uid = ? AND a.tid = ? AND (a.status = ? OR a.status = ?)';
+                    const sqlParam2 = [
+                        this.tableName,
+                        this.ctx.session.sessionUser.accountId,
+                        tid,
+                        auditConst.status.uncheck,
+                        auditConst.status.checkNo,
+                    ];
+                    const result2 = await this.db.query(sql2, sqlParam2);
+                    return result2[0].count;
+                case auditConst.filter.status.checking: // 进行中(所有的)
+                    const sql7 =
+                        'SELECT count(*) AS count FROM ?? as a WHERE ' +
+                        '(a.status = ? OR a.status = ?) AND a.tid = ?' +
+                        (this.ctx.session.sessionUser.is_admin ? '' : ' AND a.id IN (SELECT b.qiid FROM ?? AS b WHERE b.aid = ? GROUP BY b.qiid)');
+                    const sqlParam7 = [this.tableName, status, auditConst.status.checkNoPre, tid, this.ctx.service.safeInspectionAudit.tableName, this.ctx.session.sessionUser.accountId];
+                    const result7 = await this.db.query(sql7, sqlParam7);
+                    return result7[0].count;
+                case auditConst.filter.status.rectification: // 整改中(所有的)
+                case auditConst.filter.status.checkStop: // 终止(所有的)
+                    const sql3 =
+                        'SELECT count(*) AS count FROM ?? as a WHERE ' +
+                        'a.status = ? AND a.tid = ?' +
+                        (this.ctx.session.sessionUser.is_admin ? '' : ' AND a.id IN (SELECT b.qiid FROM ?? AS b WHERE b.aid = ? GROUP BY b.qiid)');
+                    const sqlParam3 = [this.tableName, status, tid, this.ctx.service.safeInspectionAudit.tableName, this.ctx.session.sessionUser.accountId];
+                    const result3 = await this.db.query(sql3, sqlParam3);
+                    return result3[0].count;
+                case auditConst.filter.status.checked: // 已完成(所有的)
+                    const sql4 = 'SELECT count(*) AS count FROM ?? as a WHERE a.status = ? AND a.tid = ?';
+                    const sqlParam4 = [this.tableName, status, tid];
+                    const result4 = await this.db.query(sql4, sqlParam4);
+                    return result4[0].count;
+                default:
+                    break;
+            }
+        }
+    }
+
+    return SafeInspection;
+};

+ 82 - 0
app/service/safe_inspection_att.js

@@ -0,0 +1,82 @@
+'use strict';
+const archiver = require('archiver');
+const path = require('path');
+const fs = require('fs');
+/**
+ * 附件表 数据模型
+ * @author LanJianRong
+ * @date 2020/6/30
+ * @version
+ */
+
+module.exports = app => {
+    class SafeInspectionAtt extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'safe_inspection_attachment';
+        }
+
+        /**
+         * 获取当前标段(期)所有上传的附件
+         * @param {Number} tid 标段id
+         * @param {Number?} mid 期id
+         * @return {Promise<void>} 数据库查询实例
+         */
+        async getAllAtt(tid, qiid) {
+            const { ctx } = this;
+            // qiid 如果qiid只有一个就转成数组
+            if (!tid || !qiid) {
+                return [];
+            }
+            qiid = qiid instanceof Array ? qiid : [qiid];
+            const sql = 'SELECT a.*,b.name as username FROM ?? as a LEFT JOIN ?? as b ON a.uid = b.id WHERE a.tid = ? AND a.qiid in (' + ctx.helper.getInArrStrSqlFilter(qiid) + ') ORDER BY upload_time DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, tid];
+            const result = await this.db.query(sql, sqlParam);
+            return result.map(item => {
+                item.orginpath = this.ctx.app.config.fujianOssPath + item.filepath;
+                if (!ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `/tender/${ctx.tender.id}/change/plan/${item.cpid}/information/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                }
+                return item;
+            });
+        }
+
+
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 获取单个文件信息
+         * @param {Number} id 文件id
+         * @return {Promise<void>} 数据库查询实例
+         */
+        async getMaterialFileById(id) {
+            return await this.getDataByCondition({ id });
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+    }
+    return SafeInspectionAtt;
+};
+

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1185 - 0
app/service/safe_inspection_audit.js


+ 6 - 0
app/service/tender_permission.js

@@ -31,11 +31,17 @@ module.exports = app => {
                 inspection: {
                     view: { title: '查看', value: 1, isDefault: 1 },
                     add: { title: '新增巡检', value: 2 },
+                },
+                safe_inspection: {
+                    view: { title: '查看', value: 1, isDefault: 1 },
+                    add: { title: '新增巡检', value: 2 },
+                    view_all: { title: '查看所有巡检', value: 3 },
                 }
             };
             this.PermissionBlock = [
                 { key: 'quality', name: '质量管理', field: 'quality' },
                 { key: 'inspection', name: '质量巡检', field: 'inspection' },
+                { key: 'safe_inspection', name: '安全巡检', field: 'safe_inspection' },
             ];
             for (const p of this.PermissionBlock) {
                 if (p.children) {

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

@@ -73,7 +73,7 @@
                     </div>
                     <div class="form-group">
                         <label>日期<b class="text-danger">*</b></label>
-                        <input id="check_date" class="datepicker-here form-control form-control-sm" placeholder="请选择检查日期" data-date-format="yyyy-MM-dd" data-language="zh" type="text">
+                        <input id="check_date" name="quality-check-date" class="datepicker-here form-control form-control-sm" placeholder="请选择检查日期" data-date-format="yyyy-MM-dd" data-language="zh" type="text">
                         <div class="invalid-feedback" style="display: none">请输入日期</div>
                     </div>
                 </div>

+ 111 - 0
app/view/safe/inspection.ejs

@@ -0,0 +1,111 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="btn-group" id="sort-dropdown">
+                        <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="bpaixu">排序:创建时间</button>
+                        <div class="dropdown-menu" aria-labelledby="bpaixu">
+                            <ul class="list-unstyled px-3 mb-0" id="sort-radio">
+                                <li class="mb-2">
+                                    <div class="custom-control custom-radio">
+                                        <input type="radio" class="custom-control-input" id="pai1" name="paizhi" value="time" checked="">
+                                        <label class="custom-control-label" for="pai1">创建时间</label>
+                                    </div>
+                                </li>
+                                <li class="mb-2">
+                                    <div class="custom-control custom-radio">
+                                        <input type="radio" class="custom-control-input" id="pai3" name="paizhi" value="code">
+                                        <label class="custom-control-label" for="pai3">编号</label>
+                                    </div>
+                                </li>
+                            </ul>
+                            <ul class="list-unstyled px-3 pt-2 mb-0 border-top" id="order-radio">
+                                <li class="mb-2">
+                                    <div class="custom-control custom-radio">
+                                        <input type="radio" class="custom-control-input" id="pdown" name="paixu" value="desc" checked="">
+                                        <label class="custom-control-label" for="pdown">降序</label>
+                                    </div>
+                                </li>
+                                <li class="mb-2">
+                                    <div class="custom-control custom-radio">
+                                        <input type="radio" class="custom-control-input" id="pup" name="paixu" value="asc">
+                                        <label class="custom-control-label" for="pup">升序</label>
+                                    </div>
+                                </li>
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-inline-block">
+                    <div class="btn-group">
+                        <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" data-status="<%- status %>" id="zhankai"><% if (status !== 0) { %><%- filter.statusString[status] %>(<%- filter.count[status] %>)<% } else { %>全部<% } %></button>
+                        <div class="dropdown-menu" aria-labelledby="zhankai" id="status_select">
+                            <% if (status !== 0) { %><a class="dropdown-item" data-val="0" href="javascript:void(0);">全部</a><% } %>
+                            <% for (const fs in filter.status) { %>
+                                <% const f = filter.status[fs]; %>
+                                <% if (f !== status) { %><a class="dropdown-item" data-val="<%- f %>" href="javascript:void(0);"><%- filter.statusString[f] %>(<%- filter.count[f] %>)</a><% } %>
+                            <% } %>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="ml-auto">
+                <% if (permission.add) { %>
+                <a href="#add-bj" data-toggle="modal" data-target="#add-bj" class="btn btn-sm btn-primary pull-right ml-1">新建巡检</a>
+                <a href="#setting" data-toggle="modal" data-target="#setting" class="btn btn-sm btn-outline-primary pull-right ml-1"><i class="fa fa-cog"></i></a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <table class="table table-bordered">
+                    <thead class="text-center">
+                    <tr>
+                        <th width="13%" id="sort_change">编号</th><th width="5%">部位</th>
+                        <th width="10%">检查项目</th><th width="25%">检查情况</th><th width="13%">处理要求及措施</th>
+                        <th width="10%">检查日期</th><th width="7%">附件</th><th width="13%">状态</th>
+                    </tr>
+                    </thead>
+                    <tbody id="changeList">
+                    <% for (const c of inspectionList) { %>
+                        <tr><td><a href="/sp/<%- ctx.subProject.id %>/quality/tender/<%- ctx.tender.id %>/inspection/<%- c.id %>/information"><%- c.code %></a></td><td></td>
+                            <td><%- c.check_item %></td>
+                            <td><%- c.check_situation %></td>
+                            <td><%- c.action %></td>
+                            <td><%- c.check_date ? moment(c.check_date).format('YYYY-MM-DD') : '' %></td>
+                            <td class="text-center">
+                                <a class="show-files" href="#file" data-toggle="modal" data-target="#file" data-id="<%- c.id %>"><i class="fa fa-paperclip"></i> <%- c.attList.length > 0 ? c.attList.length : '' %></a>
+                            </td>
+                            <td class="text-center">
+                                <span class="<%- auditConst.auditStringClass[c.status] %>"><% if (c.status !== auditConst.status.uncheck) { %><i class="fa fa-circle <%- auditConst.auditStringClass[c.status] %>"></i> <% } %><%- auditConst.auditString[c.status] %></span>
+                            </td>
+                        </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+                <!--翻页-->
+                <% include ../layout/page.ejs %>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    autoFlashHeight();
+    const tenderId = parseInt('<%- ctx.tender.id %>');
+    const tenderName = JSON.parse(unescape('<%- escape(JSON.stringify(tender.name)) %>'));
+    const dealCode = JSON.parse(unescape('<%- escape(JSON.stringify(dealCode)) %>'));
+    const ruleConst = JSON.parse(unescape('<%- escape(JSON.stringify(ruleConst)) %>'));
+    let codeRule = JSON.parse(unescape('<%- escape(JSON.stringify(codeRule)) %>'));
+    let connectorRule = '<%- c_connector %>';
+    const cRuleFirst = parseInt('<%- c_rule_first %>');
+    const ruleType = parseInt('<%- ruleType %>');
+    const rulesType = '<%- rule_type %>';
+    const auditType = JSON.parse(unescape('<%- escape(JSON.stringify(auditType)) %>'));
+    const auditConst = JSON.parse(unescape('<%- escape(JSON.stringify(auditConst)) %>'));
+    const inspectionList = JSON.parse(unescape('<%- escape(JSON.stringify(inspectionList)) %>'));
+</script>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 468 - 0
app/view/safe/inspection_information.ejs


+ 248 - 0
app/view/safe/inspection_information_modal.ejs

@@ -0,0 +1,248 @@
+<!--添加附件-->
+<div class="modal fade" id="addfujian">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="myModalLabel">上传附件</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="file-modal">单个文件大小限制:50MB,支持office等文档格式、图片格式、压缩包格式</label>
+                    <!-- <p><a href="javascript: void(0);" class="btn btn-primary" id="file-modal-target">选择文件</a></p> -->
+                    <input type="file" id="file-modal" multiple="multiple">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button id="file-cancel" type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">取消</button>
+                <button id="file-ok" type="button" class="btn btn-primary btn-sm">添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% if (!inspection.readOnly) { %>
+<!--删除巡检-->
+<div class="modal fade" id="del" 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">
+                <p class="mb-2">删除后,数据无法恢复,请谨慎操作。</p>
+                <p class="mb-2">请在下方文本框输入文本「<span class="text-danger">确认删除本次巡检</span>」,以此确认删除操作。</p>
+                <p class="mb-2"><input type="text" class="form-control form-control-sm" id="del-inspection-text" placeholder="输入文本,确认删除"></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="del-inspection-btn">确定删除</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--提交审批-->
+<div class="modal fade" id="sp-done" 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>
+            <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-success" id="start-btn">确认提交</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if (inspection.shenpiPower) { %>
+    <div class="modal fade" id="sp-back" 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>审批意见</label>
+                        <textarea class="form-control form-control-sm" name="opinion" rows="5">不同意</textarea>
+                    </div>
+                    <!--退回至上一审批人-->
+                    <% if (inspection.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0) { %>
+                        <div id="reject-process" class="alert alert-warning"
+                             style="margin-top: 15px;">
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" name="checkType"
+                                       id="inlineRadio1" value="<%- auditConst.status.checkNo %>">
+                                <label class="form-check-label" for="inlineRadio1">退回原报
+                                    <%- inspection.user.name %></label>
+                            </div>
+                            <% if (inspection.curAuditors[0].audit_order > 1) { %>
+                                <div class="form-check form-check-inline">
+                                    <input class="form-check-input" type="radio" name="checkType" id="inlineRadio2" value="<%- auditConst.status.checkNoPre %>">
+                                    <label class="form-check-label" for="inlineRadio2">退回上一审批人
+                                        <% const pre = inspection.auditHistory[inspection.auditHistory.length - 1].find(x => { return x.audit_order === inspection.curAuditors[0].audit_order - 1}); %>
+                                        <%- (pre.audit_type === auditType.key.common ? pre.auditors[0].name : `${pre.audit_order}审`)%></label>
+                                    </label>
+                                </div>
+                            <% } %>
+                        </div>
+                    <% } %>
+                </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-warning" id="approval-back-btn">确认退回</button>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="modal fade" id="sp-done" 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>审批意见</label>
+                        <textarea class="form-control form-control-sm" name="opinion" rows="5">同意</textarea>
+                    </div>
+                    <% if (inspection.finalAuditorIds.indexOf(ctx.session.sessionUser.accountId) !== -1) { %>
+                    <div class="alert alert-success">审批通过并指派人员整改:
+                        <input type="hidden" id="rectification-uid" value="<%- inspection.rectification_uid ? inspection.rectification_uid : '' %>">
+                        <span class="d-inline-block" id="rectification-user-set">
+                            <% if (inspection.rectification_uid && inspection.rectification_user) { %>
+                            <span class="badge">
+                              <%- inspection.rectification_user.name %>
+                                    <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">
+                                  <a class="dropdown-item" href="javascript:void(0);">确认移除整改人?</a>
+                                  <div class="dropdown-divider"></div>
+                                  <div class="px-2 py-1 text-center">
+                                    <button class="btn btn-sm btn-danger remove-btn">移除</button>
+                                    <button class="btn btn-sm btn-secondary">取消</button>
+                                  </div>
+                                </div>
+                              </span>
+                              </span>
+                            <% } %>
+                        </span>
+                        <div class="d-inline-block dropdown" <% if (inspection.rectification_user) { %>style="display: none!important;" <% } %>>
+                            <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button"
+                                    id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true"
+                                    aria-expanded="false">
+                                选择整改人
+                            </button>
+                            <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton"
+                                 style="width:220px">
+                                <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"
+                                                             placeholder="姓名/手机 检索" autocomplete="off"></div>
+                                <dl class="list-unstyled book-list">
+                                    <% accountGroup.forEach((group, idx) => { %>
+                                        <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>" data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                        <div class="dd-content" data-toggleid="<%- idx %>">
+                                            <% group.groupList.forEach(item => { %>
+                                                <% if (item.id !== inspection.uid) { %>
+                                                <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>" >
+                                                    <p class="mb-0 d-flex">
+                                                        <span class="text-primary"><%- item.name %></span>
+                                                        <span class="ml-auto"><%- item.mobile %></span></p>
+                                                    <span class="text-muted"><%- item.role %></span>
+                                                </dd>
+                                                <% } %>
+                                            <% });%>
+                                        </div>
+                                    <% }) %>
+                                </dl>
+                            </div>
+                        </div>
+                    </div>
+                    <% } else if (inspection.nextAuditors.length > 0) { %>
+                    <div class="alert alert-success">下一个审批人:
+                        <% const next = inspection.auditHistory[inspection.auditHistory.length - 1].find(x => { return x.audit_order === inspection.curAuditors[0].audit_order + 1}); %>
+                        <%- (next.audit_type === auditType.key.common ? next.auditors[0].name : `${next.audit_order}审`)%></div>
+                    <% } %>
+                </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-success" id="approval-success-btn">确认通过</button>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="modal fade" id="sp-close" 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>关闭原因</label>
+                        <textarea class="form-control form-control-sm" name="opinion" rows="5"></textarea>
+                    </div>
+                    <div class="alert alert-danger">审批关闭,将直接停止该巡检流程。</div>
+                </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="approval-stop-btn">确认关闭</button>
+                </div>
+            </div>
+        </div>
+    </div>
+<% } %>
+<% if (inspection.rectificationPower) { %>
+    <div class="modal fade" id="sp-back" 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>审批意见</label>
+                        <textarea class="form-control form-control-sm" name="opinion" rows="5">不同意</textarea>
+                    </div>
+                    <!--退回至上一审批人-->
+                    <% if (inspection.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0) { %>
+                        <div id="reject-process" class="alert alert-warning"
+                             style="margin-top: 15px;">
+                                退回上一审批人
+                                <% const pre = inspection.auditHistory[inspection.auditHistory.length - 1].find(x => { return x.audit_order === inspection.curAuditors[0].audit_order - 1}); %>
+                                <%- (pre.audit_type === auditType.key.common ? pre.auditors[0].name : `${pre.audit_order}审`)%>
+                        </div>
+                    <% } %>
+                </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-warning" id="rectification-back-btn">确认退回</button>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="modal fade" id="sp-done" 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>整改意见</label>
+                        <textarea class="form-control form-control-sm" name="opinion" rows="5">已整改</textarea>
+                    </div>
+                </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-success" id="rectification-success-btn">确认通过</button>
+                </div>
+            </div>
+        </div>
+    </div>
+<% } %>

+ 166 - 0
app/view/safe/inspection_modal.ejs

@@ -0,0 +1,166 @@
+<!--审批流程/结果-->
+<div class="modal fade" id="sp-list" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">审批流程</h5>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-4 modal-height-500" style="overflow: auto">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush" id="auditor-list">
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto" id="audit-list">
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--附件-->
+<div class="modal fade" id="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="modal-height-500" style="overflow: auto;">
+                    <table class="table table-sm table-bordered">
+                        <thead>
+                        <tr><th>文件名</th><th>上传人</th><th>上传时间</th><th>操作</th></tr>
+                        </thead>
+                        <tbody id="file-content"></tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <!-- <button type="button" class="btn btn-primary">确定</button> -->
+            </div>
+        </div>
+    </div>
+</div>
+<% if (permission.add) { %>
+    <!--弹出添加变更令-->
+    <div class="modal fade" id="add-bj-modal" 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>编号<b class="text-danger">*</b></label>
+                        <div class="input-group">
+                            <input type="text" class="form-control form-control-sm is-invalid" placeholder="请输入编号" value="变更申请编号" id="bj-code">
+                            <div class="input-group-append" id="autoCodeShow" <% if (codeRule.length === 0) { %>style="display: none"<% } %>>
+                                <button class="btn btn-sm btn-outline-secondary" type="button" title="自动编号" id="autoCode"><i class="fa fa-repeat"></i></button>
+                            </div>
+                            <div class="invalid-feedback" style="display: none" id="bjHint">您输入的编号已存在。</div>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label>检查项<b class="text-danger">*</b></label>
+                        <input class="form-control form-control-sm" value="" type="text" id="check_item">
+                        <div class="invalid-feedback" style="display: none" id="name_error_msg">超过255个字,请缩减名称。</div>
+                    </div>
+                    <div class="form-group">
+                        <label>日期<b class="text-danger">*</b></label>
+                        <input id="check_date" name="safe-check-date" class="datepicker-here form-control form-control-sm" placeholder="请选择检查日期" data-date-format="yyyy-MM-dd" data-language="zh" type="text">
+                        <div class="invalid-feedback" style="display: none">请输入日期</div>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal" id="addCancel">关闭</button>
+                    <a href="javascript: void(0)" class="btn btn-primary btn-sm" id="addOk">确认新建</a>
+                </div>
+            </div>
+        </div>
+    </div>
+    <!--设置-->
+    <div class="modal fade" id="setting" 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">
+                    <ul class="nav nav-tabs mb-3" role="tablist">
+                        <li class="nav-item">
+                            <a class="nav-link active" data-toggle="tab" href="#bianhao" role="tab" aria-controls="home" aria-selected="true">编号规则</a>
+                        </li>
+                    </ul>
+                    <div class="tab-content">
+                        <div class="tab-pane active" id="bianhao">
+                            <h5>
+                                当前规则:
+                                <span id="preview">
+                                    <% if (codeRule && codeRule instanceof Array) { %>
+                                        <% const preview = []; %>
+                                        <% for (const rule of codeRule) { %>
+                                            <% preview.push(rule.preview); %>
+                                        <% } %>
+                                        <%- preview.join(c_connector !== null && c_connector !== '3' ? ruleConst.connectorString[c_connector] : ''); %>
+                                    <% } %>
+                                </span>
+                            </h5>
+                            <h5 id="ruleParts">
+                                <% if (codeRule && codeRule instanceof Array) { %>
+                                    <% for (const rule of codeRule) { %>
+                                        <span class="badge badge-light" title="<%- ruleConst.ruleString[rule.rule_type] %>">
+                                    <span>
+                                        <%- rule.preview %>
+                                    </span>
+                                    <a href="javascript: void(0);" class="text-danger" title="移除"><i class="fa fa-remove"></i></a>
+                                </span>
+                                    <% } %>
+                                <% } %>
+                            </h5>
+                            <h5 class="my-3">连接符</h5>
+                            <div class="form-group">
+                                <select class="form-control form-control-sm connector-change">
+                                    <option disabled selected>请选择</option>
+                                    <% for (const index in ruleConst.connectorString) { %>
+                                        <option value="<%- index %>" <% if (c_connector !== null && parseInt(c_connector) === parseInt(index)) { %>selected<% } %>><%- ruleConst.connectorString[index] %></option>
+                                    <% } %>
+                                </select>
+                            </div>
+                            <h5 class="my-3">添加新规则组件</h5>
+                            <div class="form-group">
+                                <select class="form-control form-control-sm rule-change">
+                                    <option disabled selected>请选择组件</option>
+                                    <% for (const index in ruleConst.ruleString) { %>
+                                        <option value="<%- index %>"><%- ruleConst.ruleString[index] %></option>
+                                    <% } %>
+                                </select>
+                            </div>
+                            <div class="form-group" id="format" style="display: none">
+                                <label>自动编号位数</label>
+                                <input min="3" class="form-control form-control-sm" step="1" max="6" value="3" type="number">
+                            </div>
+                            <div class="form-group" id="text" style="display: none">
+                                <label>起始编号</label>
+                                <input class="form-control form-control-sm" value="001" type="text">
+                            </div>
+                            <button class="btn btn-sm btn-outline-primary" id="addRule">添加组件</button>
+                        </div>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <% if (c_rule_first) { %><button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal" id="changeFirst">暂时不需要</button><% } %>
+                    <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal" id="hide_modal" <% if (c_rule_first) { %>style="display: none"<% } %>>关闭</button>
+                    <button type="button" class="btn btn-primary btn-sm" id="setRule">确定添加</button>
+                </div>
+            </div>
+        </div>
+    </div>
+<% } %>
+
+

+ 5 - 0
app/view/safe/sub_memu_list.ejs

@@ -0,0 +1,5 @@
+<nav-menu title="返回" url="/sp/<%- ctx.subProject.id %>/safe<%- ctx.url.includes('/inspection') ? '/inspection' : '' %>" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<% if (!ctx.url.includes('/inspection')) { %>
+<% } else { %>
+<nav-menu title="安全巡检" url="/sp/<%- ctx.subProject.id %>/safe/tender/<%= ctx.tender.id %>/inspection %>" ml="3" active="<%= ctx.url.indexOf('/inspection') %>"></nav-menu>
+<% } %>

+ 14 - 0
app/view/safe/sub_menu.ejs

@@ -0,0 +1,14 @@
+<div class="panel-sidebar" id="sub-menu">
+    <div class="sidebar-title" data-toggle="tooltip" data-placement="right" data-original-title="<%- ctx.tender.data.name %>">
+        <%- (ctx.tender.data.name.length > 15 ? ctx.tender.data.name.substring(0,15) + '...' : ctx.tender.data.name) %>
+    </div>
+    <div class="scrollbar-auto">
+        <% include ./sub_memu_list.ejs %>
+        <div class="side-fold"><a href="javascript: void(0)" data-toggle="tooltip" data-placement="top" data-original-title="折叠侧栏" id="to-mini-menu"><i class="fa fa-upload fa-rotate-270"></i></a></div>
+    </div>
+    <script>
+        new Vue({
+            el: '.scrollbar-auto',
+        });
+    </script>
+</div>

+ 16 - 0
app/view/safe/sub_mini_menu.ejs

@@ -0,0 +1,16 @@
+<!--折起的菜单-->
+<div class="min-side" id="sub-mini-menu" style="display: none;">
+    <div id="sub-mini-hint" class="side-switch" data-container="body" data-toggle="popover" data-placement="bottom" data-content="这里打开收起的菜单栏"></div>
+    <div class="side-switch">
+        <i class="fa fa-bars"></i>
+    </div>
+    <div class="side-menu" id="mini-menu-list" style="display: none">
+        <% include ./sub_memu_list.ejs %>
+        <div class="side-fold"><a href="javascript: void(0);" data-toggle="tooltip" data-placement="top" data-original-title="展开侧栏" id="to-menu"><i class="fa fa-upload fa-rotate-90"></i></a></div>
+    </div>
+</div>
+<script>
+    new Vue({
+        el: '.side-menu',
+    });
+</script>

+ 25 - 0
app/view/safe/tender.ejs

@@ -0,0 +1,25 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex">
+            <div class="d-inline-block" id="show-level"></div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const tenders = JSON.parse(unescape('<%- escape(JSON.stringify(tenderList)) %>'));
+    const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
+    const selfCategoryLevel = '<%- (selfCategoryLevel || '') %>';
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+
+    const pid = '<%- ctx.session.sessionProject.id %>';
+    const uphlname = 'user_<%- ctx.session.sessionUser.accountId %>_pro_<% ctx.session.sessionProject.id %>_category_hide_list';
+    const isInspection = parseInt('<%- is_inspection %>');
+</script>

+ 1 - 0
app/view/safe/tender_modal.ejs

@@ -0,0 +1 @@
+<% include ../shares/tender_permission_modal.ejs %>

+ 5 - 5
app/view/shares/tender_permission_modal.ejs

@@ -61,8 +61,8 @@
     </div>
 </div>
 <script>
-    const accountList = JSON.parse('<%- JSON.stringify(accountList) %>');
-    const accountGroup = JSON.parse('<%- JSON.stringify(accountGroup) %>');
-    const permissionConst = JSON.parse('<%- JSON.stringify(permissionConst) %>');
-    const permissionBlock = JSON.parse('<%- JSON.stringify(permissionBlock) %>');
-</script>
+    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+    const permissionConst = JSON.parse(unescape('<%- escape(JSON.stringify(permissionConst)) %>'));
+    const permissionBlock = JSON.parse(unescape('<%- escape(JSON.stringify(permissionBlock)) %>'));
+</script>

+ 29 - 4
app/view/sp_setting/manage.ejs

@@ -28,8 +28,9 @@
                     <a class="nav-item nav-link" data-toggle="tab" href="#tzpro" role="tab">投资进度</a>
                     <a class="nav-item nav-link" data-toggle="tab" href="#htgl" role="tab">合同管理</a>
                     <a class="nav-item nav-link" data-toggle="tab" href="#sgrz" role="tab">施工日志</a>
-                    <a class="nav-item nav-link" data-toggle="tab" href="#zlgl" role="tab">质量管理</a>
-                    <a class="nav-item nav-link" data-toggle="tab" href="#zlxj" role="tab">质量巡检</a>
+                    <a class="nav-item nav-link" data-toggle="tab" href="#quality" role="tab">质量管理</a>
+                    <a class="nav-item nav-link" data-toggle="tab" href="#inspection" role="tab">质量巡检</a>
+                    <a class="nav-item nav-link" data-toggle="tab" href="#safe_inspection" role="tab">安全巡检</a>
 <!--                    <a class="nav-item nav-link" data-toggle="tab" href="#subadmin" role="tab">标段管理员</a>-->
                     <div class="ml-auto" id="user-set" style="display: none">
                         <div class="row">
@@ -228,7 +229,7 @@
                         </div>
                     </div>
                     <!--质量管理 -->
-                    <div id="zlgl" class="tab-pane">
+                    <div id="quality" class="tab-pane">
                         <div class="col-8" style="max-width: 800px">
                             <table class="table table-hover table-bordered table-sm">
                                 <thead class="text-center">
@@ -252,7 +253,7 @@
                         </div>
                     </div>
                     <!--质量巡检 -->
-                    <div id="zlxj" class="tab-pane">
+                    <div id="inspection" class="tab-pane">
                         <div class="col-8" style="max-width: 800px">
                             <table class="table table-hover table-bordered table-sm">
                                 <thead class="text-center">
@@ -275,6 +276,30 @@
                             </table>
                         </div>
                     </div>
+                    <!--安全巡检 -->
+                    <div id="safe_inspection" class="tab-pane">
+                        <div class="col-8" style="max-width: 800px">
+                            <table class="table table-hover table-bordered table-sm">
+                                <thead class="text-center">
+                                <tr>
+                                    <th class="align-middle" rowspan="2">成员名称</th>
+                                    <th class="align-middle" rowspan="2">角色/职位</th>
+                                    <% const safeInspectionPb = permissionBlock.find(item => item.key === 'safe_inspection'); %>
+                                    <th colspan="<%- safeInspectionPb.permission.filter(x => { return !x.isDefault; }).length %>"><%- safeInspectionPb.name %></th>
+                                    <th class="align-middle" rowspan="2">操作</th>
+                                </tr>
+                                <tr>
+                                    <% for (const p of safeInspectionPb.permission) { %>
+                                        <% if (p.isDefault) continue; %>
+                                        <th><%- p.title %></th>
+                                    <% } %>
+                                </tr>
+                                </thead>
+                                <tbody id="safe_inspection-users">
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
                     <!--标段管理员 -->
                     <div id="subadmin" class="tab-pane">
                         <div class="col-6">

+ 245 - 53
app/view/wap/inspection.ejs

@@ -66,13 +66,17 @@
             right: 0;
             width: 100%;
             background: #fff;
-            padding: 22px env(safe-area-inset-right) calc(10px + env(safe-area-inset-bottom)) env(safe-area-inset-left);
+            padding: 10px env(safe-area-inset-right) calc(10px + env(safe-area-inset-bottom)) env(safe-area-inset-left);
             box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.1);
             z-index: 999;
         }
 
-        .chat-footer .input-group {
-            bottom: 22px;
+        /*.chat-footer.wx {*/
+        /*    padding-top: 22px; !* 微信才有的上间距 *!*/
+        /*}*/
+
+        .chat-footer.wx .input-group {
+            margin-bottom: 22px;
         }
 
         .chat-footer input.form-control {
@@ -156,17 +160,29 @@
 
         .input-icon {
             position: absolute;
-            right: 75px; /* 根据按钮宽度调整 */
+            left: 10px;    /* ---- 改成左边 ---- */
             top: 50%;
             transform: translateY(-50%);
             cursor: pointer;
             color: #888;
             z-index: 10;
+            font-size: 18px;
         }
 
+        /* 激活时高亮 */
         #voice-icon.active {
             color: #007bff;
         }
+
+        /* 输入框左边留位置给语音按钮 */
+        #user-input {
+            margin-left: 40px !important;
+            padding-left: 0px;
+        }
+        .chat-footer.wx #user-input {
+            margin-left: 12px !important; /* 或 0px */
+            padding-left: 0px;
+        }
         .xj-title {
             position: sticky;
             top: 0;
@@ -246,14 +262,28 @@
         </div>
     </div>
     <div class="chat-footer">
+<!--        <div class="input-group">-->
+<!--            <input type="text" id="user-input" class="form-control" placeholder="请输入...">-->
+<!--            <div class="input-icon">-->
+<!--                <i id="voice-icon" class="fa fa-microphone"></i>-->
+<!--            </div>-->
+<!--            <div class="input-group-append">-->
+<!--                <button class="btn btn-primary btn-send" id="send-btn">发送</button>-->
+<!--            </div>-->
+<!--        </div>-->
         <div class="input-group">
-            <input type="text" id="user-input" class="form-control" placeholder="请输入...">
-            <div class="input-icon">
+
+            <!-- 左侧语音按钮 -->
+            <div class="input-icon" id="voice-container">
                 <i id="voice-icon" class="fa fa-microphone"></i>
             </div>
+
+            <input type="text" id="user-input" class="form-control" placeholder="请输入...">
+
             <div class="input-group-append">
                 <button class="btn btn-primary btn-send" id="send-btn">发送</button>
             </div>
+
         </div>
     </div>
 </div>
@@ -357,62 +387,224 @@
             $(window).on('resize', checkKeyboardOpen);
         });
 
-        const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
-        recognition.continuous = false;
-        recognition.lang = 'zh-CN';
-        recognition.interimResults = false;
+        // const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
+        // recognition.continuous = false;
+        // recognition.lang = 'zh-CN';
+        // recognition.interimResults = false;
+        //
+        // let isRecording = false;
+        // let pressTimer = null;
+        //
+        // $('#voice-icon').on('click', function () {
+        //
+        //     if (!isRecording) {
+        //         // 👉 开始录音
+        //         try {
+        //             recognition.start();
+        //             isRecording = true;
+        //             $(this).addClass('active');
+        //             $('#recording-toast').fadeIn(); // 显示录音提示
+        //         } catch (e) {
+        //             console.error('开始录音失败:', e);
+        //         }
+        //     } else {
+        //         // 👉 停止录音
+        //         recognition.stop();
+        //         isRecording = false;
+        //         $(this).removeClass('active');
+        //         $('#recording-toast').fadeOut(); // 隐藏录音提示
+        //     }
+        // });
+        //
+        // // 👉 识别结果
+        // recognition.onresult = function (event) {
+        //     const result = event.results[0][0].transcript.trim();
+        //     $('#user-input').val(result);
+        // };
+        //
+        // // 👉 错误处理
+        // recognition.onerror = function (err) {
+        //     console.error('语音识别出错:', err);
+        //     $('#voice-icon').removeClass('active');
+        //     $('#recording-toast').fadeOut();
+        //     isRecording = false;
+        // };
+        //
+        // // 👉 识别自然结束(可能用户不说话)
+        // recognition.onend = function () {
+        //     $('#voice-icon').removeClass('active');
+        //     $('#recording-toast').fadeOut();
+        //     isRecording = false;
+        // };
+
+        if (isWeChat()) {
+            $('#voice-icon').hide(); // 在微信内隐藏语音按钮
+            // 输入框恢复正常 padding
+            // $('#user-input').css('padding-left', '0px !important');
+            $('.chat-footer').addClass('wx');
+  //           const style = document.createElement('style');
+  //           style.innerHTML = `
+  //   .chat-footer { padding-top: 22px !important; }
+  //   .chat-footer .input-group { bottom: 22px !important; }
+  // `;
+  //           document.head.appendChild(style);
+        }
 
         let isRecording = false;
-        let pressTimer = null;
-
-        $('#voice-icon').on('touchstart', function (e) {
-            e.preventDefault();
-            $(this).addClass('active');
-            // if (!isRecording) {
-            //     recognition.start();
-            //     $(this).addClass('active');
-            //     $('#recording-toast').fadeIn(); // 👉 显示录音提示
-            //     isRecording = true;
-            // }
-            pressTimer = setTimeout(() => {
-                recognition.start();
-                isRecording = true;
-                $('#recording-toast').fadeIn(); // 👉 显示录音提示
-            }, 300); // 按住 300ms 开始录音,可根据体验调整
-        });
+        let mediaStream = null;
+        let audioCtx = null;
+        let processor = null;
+        let buffers = [];
+        const TARGET_SAMPLE_RATE = 16000;
 
-        $('#voice-icon').on('touchend touchcancel', function (e) {
-            e.preventDefault();
-            clearTimeout(pressTimer);
-            if (isRecording) {
-                recognition.stop();
-                isRecording = false;
-                $('#recording-toast').fadeOut(); // 👉 隐藏录音提示
+// start / collect audio
+        async function startRecording() {
+            mediaStream = await navigator.mediaDevices.getUserMedia({
+                audio: { echoCancellation: true, noiseSuppression: true }
+            });
+            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+            const source = audioCtx.createMediaStreamSource(mediaStream);
+
+            processor = audioCtx.createScriptProcessor(4096, 1, 1);
+            buffers = [];
+
+            processor.onaudioprocess = (e) => {
+                const ch = e.inputBuffer.getChannelData(0);
+                buffers.push(new Float32Array(ch));
+            };
+
+            source.connect(processor);
+            processor.connect(audioCtx.destination);
+        }
+
+        function mergeBuffers(buffers) {
+            let total = buffers.reduce((s, b) => s + b.length, 0);
+            const out = new Float32Array(total);
+            let offset = 0;
+            for (const b of buffers) {
+                out.set(b, offset);
+                offset += b.length;
             }
-            $(this).removeClass('active');
-        });
+            return out;
+        }
 
-        recognition.onresult = function (event) {
-            const result = event.results[0][0].transcript.trim();
-            $('#user-input').val(result);
-        };
+        function downsampleBuffer(buffer, inSampleRate, outSampleRate) {
+            if (outSampleRate === inSampleRate) return buffer;
+            const ratio = inSampleRate / outSampleRate;
+            const newLength = Math.round(buffer.length / ratio);
+            const result = new Float32Array(newLength);
+            let offsetResult = 0;
+            let offsetBuffer = 0;
+            while (offsetResult < newLength) {
+                const nextOffsetBuffer = Math.round((offsetResult + 1) * ratio);
+                let accum = 0, count = 0;
+                for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
+                    accum += buffer[i];
+                    count++;
+                }
+                result[offsetResult] = accum / Math.max(1, count);
+                offsetResult++;
+                offsetBuffer = nextOffsetBuffer;
+            }
+            return result;
+        }
 
-        recognition.onerror = function (err) {
-            console.error('语音识别出错:', err);
-            $('#voice-icon').removeClass('active');
-            $('#recording-toast').fadeOut(); // 👉 出错时也隐藏
-            isRecording = false;
-        };
+        function floatTo16BitPCM(float32Array) {
+            const l = float32Array.length;
+            const buf = new Int16Array(l);
+            for (let i = 0; i < l; i++) {
+                let s = Math.max(-1, Math.min(1, float32Array[i]));
+                buf[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
+            }
+            return buf;
+        }
 
-        recognition.onend = function () {
-            $('#voice-icon').removeClass('active');
-            $('#recording-toast').fadeOut(); // 👉 识别结束时隐藏
-            isRecording = false;
-        };
+        function makeWav(int16Arr, sampleRate) {
+            const buffer = new ArrayBuffer(44 + int16Arr.length * 2);
+            const view = new DataView(buffer);
+            function writeString(view, offset, str) {
+                for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
+            }
+            writeString(view, 0, 'RIFF');
+            view.setUint32(4, 36 + int16Arr.length * 2, true);
+            writeString(view, 8, 'WAVE');
+            writeString(view, 12, 'fmt ');
+            view.setUint32(16, 16, true);
+            view.setUint16(20, 1, true);
+            view.setUint16(22, 1, true);
+            view.setUint32(24, sampleRate, true);
+            view.setUint32(28, sampleRate * 2, true);
+            view.setUint16(32, 2, true);
+            view.setUint16(34, 16, true);
+            writeString(view, 36, 'data');
+            view.setUint32(40, int16Arr.length * 2, true);
+            let p = 44;
+            for (let i = 0; i < int16Arr.length; i++, p += 2) {
+                view.setInt16(p, int16Arr[i], true);
+            }
+            return new Blob([view], { type: 'audio/wav' });
+        }
 
-        if (isWeChat()) {
-            $('#voice-icon').hide(); // 在微信内隐藏语音按钮
+        async function stopRecordingAndBuildWav() {
+            try {
+                processor.disconnect();
+                audioCtx.close();
+                mediaStream.getTracks().forEach(t => t.stop());
+            } catch (e) {}
+            const merged = mergeBuffers(buffers);
+            const inSampleRate = (audioCtx && audioCtx.sampleRate) || 48000;
+            const down = downsampleBuffer(merged, inSampleRate, TARGET_SAMPLE_RATE);
+            const int16 = floatTo16BitPCM(down);
+            const wavBlob = makeWav(int16, TARGET_SAMPLE_RATE);
+            return wavBlob;
         }
+
+        $('#voice-icon').on('click', async function () {
+            if (!isRecording) {
+                try {
+                    await startRecording();
+                    isRecording = true;
+                    $(this).addClass('active');
+                    $('#recording-toast').text('正在录音...').fadeIn();
+                } catch (e) {
+                    console.error('start fail', e);
+                    toastr.error('无法获取麦克风权限');
+                    // toastr.error(e);
+                }
+            } else {
+                isRecording = false;
+                $(this).removeClass('active');
+                $('#recording-toast').text('上传中...').fadeIn();
+
+                const wav = await stopRecordingAndBuildWav();
+
+                try {
+                    // 构造 FormData(file 字段名为 audio)
+                    const fd = new FormData();
+                    fd.append('audio', wav, 'rec.wav');
+
+                    const resp = await postDataWithFileAsync('/wap/voice/oneshot', fd);
+                    // 你的封装应返回解析后的 JSON
+                    if (resp) {
+                        // 当前 input 的原有内容
+                        const oldText = $('#user-input').val() || '';
+                        // 去掉中文标点
+                        const newText = resp ? resp.replace(/[,。!?;:]/g, '') : '';
+                        $('#user-input').val(oldText + (newText || ''));
+                    } else {
+                        toastr.warning('抱歉,未能识别成功');
+                        // toastr.error('未能识别成功:' + (resp && resp.msg ? resp.msg : '未知'));
+                    }
+                } catch (err) {
+                    // alert(err);
+                    // console.error(err);
+                    toastr.error('上传或识别出错');
+                } finally {
+                    $('#recording-toast').fadeOut();
+                    $('#recording-toast').text('正在录音...');
+                }
+            }
+        });
     });
 
     function isWeChat() {

+ 23 - 0
config/menu.js

@@ -141,6 +141,29 @@ const menu = {
         controllers: ['quality'],
         includedUrl: { quality: ['/inspection'] },
     },
+    safe: {
+        name: '安全管理',
+        icon: 'fa-shield',
+        display: true,
+        // url: '/contract/detail',
+        children: [
+            {
+                msg: 'payment',
+                name: '安全计量',
+                caption: '安全计量',
+                controller: 'safe',
+                notIncludedUrl: ['/inspection'],
+            },
+            {
+                msg: 'inspection',
+                name: '安全巡检',
+                caption: '安全巡检',
+                controllers: ['safe'],
+                includedUrl: { safe: ['/inspection'] },
+            },
+        ],
+        caption: '安全管理',
+    },
     file: {
         name: '资料管理',
         icon: 'fa-file-zip-o',

+ 52 - 0
config/web.js

@@ -2315,6 +2315,58 @@ const JsFiles = {
                 mergeFile: 'quality_rule',
             },
         },
+        safe: {
+            tender: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/PinYinOrder.bundle.js',
+                    '/public/js/shares/tender_list_order.js',
+                    '/public/js/shares/show_level.js',
+                    '/public/js/shares/tender_permission.js',
+                    '/public/js/tender_showhide.js',
+                    '/public/js/tender_list_base.js',
+                    '/public/js/safe_tender.js',
+                ],
+                mergeFile: 'safe_tender',
+            },
+            inspection: {
+                files: [
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/safe_inspection.js',
+                ],
+                mergeFile: 'safe_inspection',
+            },
+            inspection_information: {
+                files: [
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/safe_inspection_information.js',
+                ],
+                mergeFile: 'safe_inspection_information',
+            },
+        },
     },
 };
 

+ 1 - 0
package.json

@@ -7,6 +7,7 @@
         "@alicloud/pop-core": "^1.7.9",
         "@wecom/crypto": "^1.0.1",
         "ali-rds": "^3.3.0",
+        "alibabacloud-nls": "^1.0.4",
         "archiver": "^5.0.2",
         "atob": "^2.1.2",
         "axios": "^1.3.4",

+ 54 - 1
sql/update.sql

@@ -58,12 +58,65 @@ CREATE TABLE `zh_quality_inspection_attachment`  (
   INDEX `idx_cid`(`qiid`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '质量巡检附件表' ROW_FORMAT = Dynamic;
 
+CREATE TABLE `zh_safe_inspection`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `tid` int(11) NOT NULL COMMENT '标段id',
+  `code` varchar(255) NOT NULL COMMENT '编号',
+  `status` tinyint(2) NOT NULL COMMENT '审批状态',
+  `times` tinyint(2) NOT NULL DEFAULT 1 COMMENT '审批次数',
+  `uid` int(11) NOT NULL COMMENT '创建人id',
+  `check_item` varchar(1000) NULL DEFAULT '' COMMENT '检查项',
+  `check_situation` varchar(1000) NULL DEFAULT '' COMMENT '检查情况',
+  `action` varchar(1000) NULL DEFAULT '' COMMENT '处理要求及措施',
+  `check_date` datetime NULL DEFAULT NULL COMMENT '检查日期',
+  `inspector` varchar(255) NULL COMMENT '检查人',
+  `rectification_item` varchar(1000) NULL DEFAULT '' COMMENT '整改内容',
+  `rectification_date` datetime NULL DEFAULT NULL COMMENT '整改日期',
+  `rectification_uid` int(11) NULL DEFAULT NULL COMMENT '整改人id',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT = '安全巡检单表';
+
+CREATE TABLE `zh_safe_inspection_audit`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `tid` int(11) NOT NULL COMMENT '标段id',
+  `qiid` int(11) NOT NULL COMMENT '安全巡检id',
+  `aid` int(11) NOT NULL COMMENT '审批人id',
+  `order` int(11) NOT NULL COMMENT '审批顺序',
+  `times` tinyint(2) NOT NULL COMMENT '审批次数',
+  `status` tinyint(2) NOT NULL COMMENT '审批状态',
+  `begin_time` datetime NULL DEFAULT NULL COMMENT '开始审批时间',
+  `end_time` datetime NULL DEFAULT NULL COMMENT '结束审批时间',
+  `opinion` varchar(1000) NULL COMMENT '审批意见',
+  `is_rectification` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否是整改人',
+  `is_old` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '是否为旧流程(用于管理员修改流程时旧数据保留但不影响新流程)',
+  `audit_type` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '审批类型(1个人,2会签,3或签)',
+  `audit_order` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '审批顺序',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT = '安全巡检审批表';
+
+CREATE TABLE `zh_safe_inspection_attachment`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `tid` int(11) NOT NULL COMMENT '标段id',
+  `qiid` int(11) NOT NULL COMMENT '巡检id',
+  `uid` int(11) NOT NULL COMMENT '上传者id',
+  `filename` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件名称',
+  `fileext` varchar(5) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件后缀',
+  `filesize` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件大小',
+  `filepath` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件存储路径',
+  `upload_time` datetime NOT NULL COMMENT '上传时间',
+  `extra_upload` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否为审核通过后再次上传的文件,0为否',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_cid`(`qiid`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '安全巡检附件表' ROW_FORMAT = Dynamic;
+
 ALTER TABLE `zh_s2b_spec_pull`
 ADD COLUMN `extra_option` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '{}' COMMENT '额外配置' AFTER `pull_class`,
 ADD COLUMN `check_api` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'api验证方法' AFTER `extra_option`;
 
 ALTER TABLE `zh_tender_permission`
-ADD COLUMN `inspection` varchar(255) NOT NULL DEFAULT '' COMMENT '质量巡检权限(,分隔,具体见代码定义)' AFTER `quality`;
+ADD COLUMN `inspection` varchar(255) NOT NULL DEFAULT '' COMMENT '质量巡检权限(,分隔,具体见代码定义)' AFTER `quality`,
+ADD COLUMN `safe_inspection` varchar(255) NOT NULL DEFAULT '' COMMENT '安全巡检权限(,分隔,具体见代码定义)' AFTER `inspection`;
 
 ALTER TABLE `zh_budget`
 ADD COLUMN `final_type` varchar(50) NOT NULL DEFAULT 'code_name' COMMENT '决算汇总规则' AFTER `final_id`;